Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14980c993f | ||
|
|
e17433f28f | ||
|
|
0833d4e44f | ||
|
|
07b2eba5c5 | ||
|
|
0f95a5cfc7 | ||
|
|
028d63b64a | ||
|
|
91b4def490 | ||
|
|
122ff939aa | ||
|
|
3fef2d5437 |
+10
-1
@@ -8,7 +8,7 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
|
||||
|
||||
## Non distribué
|
||||
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`** et **Ambiance `0.0.0-alpha.15`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
|
||||
Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.119`**, **Ambiance `0.0.0-alpha.15`**, **Only Fun `0.0.0-alpha.24`** et **Ouch `0.0.0-alpha.10`**. Le pack distribué reste en `26.2.0-alpha.210` jusqu’à la prochaine release.
|
||||
|
||||
- remplace les pages noires du Livre interdit par l’affichage clair du livre vanilla ;
|
||||
- conserve les seize entrées, les découvertes personnelles, le partage sur pupitre, les IDs, les données v3 et le protocole réseau 4.
|
||||
@@ -17,6 +17,15 @@ Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.11
|
||||
- ajoute le disque blanc, à fabriquer puis renommer avec une URL YouTube HTTPS avant de l’insérer dans un jukebox ;
|
||||
- valide strictement l’hôte et l’identifiant vidéo côté serveur, ne transmet qu’un identifiant borné de onze caractères et limite chaque client à quatre lectures de trente minutes maximum ;
|
||||
- diffuse le son en mono positionnel dans le canal des disques, sans nouvelle donnée persistante ; Ambiance conserve ses données v2 et passe son protocole réseau de 5 à 6.
|
||||
- importe le cœur GPL historique d’Abjection dans Only Fun, sans onzième JAR : `/piss`, `/vomit`, `/shit` et `/smoke` retrouvent leurs durées, jets, impacts et messages ;
|
||||
- restaure `abjection:shit_ball` ainsi que le bloc boueux `abjection:shit_cube`, sa recette, son butin et ses ressources originales en FR/EN/RU ;
|
||||
- expose une API serveur unique pour les commandes et le futur raccourci, nettoie les actions temporaires à la mort, en spectateur ou à la déconnexion, et conserve les données v2 ainsi que le protocole réseau 2 d’Only Fun.
|
||||
- rend `abjection:shit_ball` lançable à la main et par dispenser grâce à une fabrique publique installée par Ouch, sans réenregistrer l’objet possédé par Only Fun ;
|
||||
- restaure le projectile historique de 0,25 bloc, sa consommation unique hors créatif, son rendu d’objet, son impact boueux et ses sept secondes de nausée ajoutées à la durée existante ;
|
||||
- déclare la dépendance sans cycle `ouch -> onlyfun` ; les données et protocoles réseau d’Ouch et d’Only Fun restent inchangés.
|
||||
- ajoute la touche configurable « Chier » dans les contrôles Only Fun, assignée à `J` par défaut et sans répétition lorsque la touche reste maintenue ;
|
||||
- transmet une demande C2S vide, déduplique le spam d’un même tick et revalide côté serveur la connexion, la vie et le mode spectateur avant de réutiliser exactement l’action `/shit` ;
|
||||
- conserve les données Only Fun v2 et porte son protocole réseau de 2 à 3, sans nouveau cooldown, besoin ni donnée persistante.
|
||||
|
||||
## `26.2.0-alpha.210`
|
||||
|
||||
|
||||
+80
-9
@@ -3439,7 +3439,7 @@ tasks.register("verifyProgressivePauseMenuRelease") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.modules != [sanctuary: "0.0.0-alpha.108", onlyfun: "0.0.0-alpha.17"]
|
||||
|| migration.target?.modules != [sanctuary: "0.0.0-alpha.109", onlyfun: "0.0.0-alpha.17"]
|
||||
@@ -3606,6 +3606,7 @@ project(":ouch") {
|
||||
dependencies {
|
||||
implementation project(":anotherworld")
|
||||
implementation project(":ambiance")
|
||||
implementation project(":onlyfun")
|
||||
implementation project(":sanctuary")
|
||||
}
|
||||
}
|
||||
@@ -3636,9 +3637,25 @@ project(":onlyfun") {
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.loot.OnlyFunGeneratorSmoke"
|
||||
}
|
||||
|
||||
tasks.register("abjectionActionContractSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks the bounded historical Abjection action contract."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActionContractSmoke"
|
||||
}
|
||||
|
||||
tasks.register("shitActionRequestSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks one-shot and same-tick deduplication for the Abjection key request."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyOnlyFun") {
|
||||
group = "verification"
|
||||
description = "Checks Only Fun alpha.19 lootboxes, CSV Shop catalog and anchored Cobble Dungeons."
|
||||
description = "Checks Only Fun alpha.24, including the bounded configurable Abjection key."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.file(file("art/shop/shop_catalog_editable.csv"))
|
||||
@@ -3721,9 +3738,61 @@ project(":onlyfun") {
|
||||
def shopCsv = file("art/shop/shop_catalog_editable.csv")
|
||||
def shopCsvText = shopCsv.getText("UTF-8")
|
||||
def shopCsvLines = shopCsv.readLines("UTF-8")
|
||||
def abjectionLanguages = ["fr_fr", "en_us", "ru_ru"].collectEntries { locale ->
|
||||
[(locale): new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/abjection/lang/${locale}.json"))]
|
||||
}
|
||||
def shitCubeTexture = javax.imageio.ImageIO.read(file(
|
||||
"src/main/resources/assets/abjection/textures/block/shit_cube.png"))
|
||||
def shitBallTexture = javax.imageio.ImageIO.read(file(
|
||||
"src/main/resources/assets/abjection/textures/item/shit_ball.png"))
|
||||
def shitCubeRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/abjection/recipe/shit_cube.json"))
|
||||
if (manifest.custom.sanctuary26.data_version != 2
|
||||
|| project.version.toString() != "0.0.0-alpha.21"
|
||||
|| project.version.toString() != "0.0.0-alpha.24"
|
||||
|| manifest.depends?.sanctuary != "*"
|
||||
|| manifest.provides != ["abjection"]
|
||||
|| manifest.custom.sanctuary26.network_protocol != 3
|
||||
|| !javaText.contains('NAMESPACE = "abjection"')
|
||||
|| !javaText.contains('public static StartResult start(ServerPlayer player, Action action)')
|
||||
|| !javaText.contains('PISS(15 * 20, "piss")')
|
||||
|| !javaText.contains('VOMIT(8 * 20, "vomit")')
|
||||
|| !javaText.contains('SHIT(6 * 20, "shit")')
|
||||
|| !javaText.contains('SMOKE(10 * 20, "smoke")')
|
||||
|| !javaText.contains('ServerPlayConnectionEvents.DISCONNECT')
|
||||
|| !javaText.contains('action == Action.SHIT && !restarted')
|
||||
|| !javaText.contains('new ItemStack(AbjectionRegistries.SHIT_BALL)')
|
||||
|| !javaText.contains('BlockBehaviour.Properties.ofFullCopy(Blocks.MUD)')
|
||||
|| !javaText.contains('class ShitBallItem extends Item implements ProjectileItem')
|
||||
|| !javaText.contains('public static void registerFactory(Factory implementation)')
|
||||
|| !javaText.contains('return AbjectionProjectileApi.create(level, position, stack, direction)')
|
||||
|| !javaText.contains('id("c2s/shit_action")')
|
||||
|| !javaText.contains('CustomPacketPayload.codec((payload, buffer) -> {},')
|
||||
|| !javaText.contains('GLFW.GLFW_KEY_J')
|
||||
|| !javaText.contains('while (shitKey.consumeClick())')
|
||||
|| !javaText.contains('server.getPlayerList().getPlayer(player.getUUID()) != player')
|
||||
|| !javaText.contains('acceptTick(player.getUUID(), server.getTickCount())')
|
||||
|| !javaText.contains('AbjectionActions.start(player, AbjectionActions.Action.SHIT)')
|
||||
|| shitCubeTexture == null || shitCubeTexture.width != 16 || shitCubeTexture.height != 16
|
||||
|| shitBallTexture == null || shitBallTexture.width != 16 || shitBallTexture.height != 16
|
||||
|| shitCubeRecipe.key?.get("#") != "abjection:shit_ball"
|
||||
|| shitCubeRecipe.result?.id != "abjection:shit_cube"
|
||||
|| !file("src/main/resources/data/abjection/loot_table/blocks/shit_cube.json").isFile()
|
||||
|| !file("src/main/resources/data/minecraft/tags/block/mineable/shovel.json").text
|
||||
.contains('"abjection:shit_cube"')
|
||||
|| abjectionLanguages.any { locale, translations ->
|
||||
!["item.abjection.shit_ball", "block.abjection.shit_cube",
|
||||
"command.abjection.piss.started", "command.abjection.vomit.started",
|
||||
"command.abjection.shit.started", "command.abjection.smoke.started"].every {
|
||||
translations.containsKey(it)
|
||||
}
|
||||
}
|
||||
|| ["fr_fr", "en_us", "ru_ru"].any { locale ->
|
||||
def translations = new JsonSlurper().parse(file(
|
||||
"src/main/resources/assets/onlyfun/lang/${locale}.json"))
|
||||
!translations.containsKey("key.categories.onlyfun.onlyfun")
|
||||
|| !translations.containsKey("key.onlyfun.shit")
|
||||
}
|
||||
|| !javaText.contains("ANOMALY_DENOMINATOR = 256")
|
||||
|| !javaText.contains("PITY_START = 24")
|
||||
|| !javaText.contains("class LootboxItem")
|
||||
@@ -3870,6 +3939,8 @@ project(":onlyfun") {
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("abjectionActionContractSmoke")
|
||||
dependsOn tasks.named("shitActionRequestSmoke")
|
||||
dependsOn tasks.named("onlyFunGeneratorSmoke")
|
||||
dependsOn tasks.named("verifyOnlyFun")
|
||||
}
|
||||
@@ -4136,7 +4207,7 @@ tasks.register("verifyShopPricingLayoutRelease") {
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def index = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.192"
|
||||
@@ -4230,7 +4301,7 @@ tasks.register("verifyShopCsvBossGatesRelease") {
|
||||
def catalog = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunShopCatalog.java").text
|
||||
def offerCatalog = file("sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.194"
|
||||
@@ -4478,7 +4549,7 @@ tasks.register("verifyWeatherTntAmbianceRelease") {
|
||||
def rootBuild = file("build.gradle").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|
||||
|| rootProject.ouch_version != "0.0.0-alpha.9"
|
||||
|| rootProject.ouch_version != "0.0.0-alpha.10"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.199"
|
||||
|| migration.source?.modules != [ambiance: "0.0.0-alpha.11", ouch: "0.0.0-alpha.8"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.200"
|
||||
@@ -5499,7 +5570,7 @@ tasks.register("verifyCanapliaAlpha209Release") {
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.ambiance_version != "0.0.0-alpha.15"
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.21"
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.207"
|
||||
|| migration.source?.modules != expectedSource
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.208"
|
||||
@@ -5607,7 +5678,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.210"
|
||||
|| rootProject.sanctuary_version != "0.0.0-alpha.119"
|
||||
|| rootProject.onlyfun_version != expectedTarget.onlyfun
|
||||
|| rootProject.onlyfun_version != "0.0.0-alpha.24"
|
||||
|| rootProject.iliketomoveit_version != expectedTarget.iliketomoveit
|
||||
|| rootProject.itsalive_version != "0.0.0-alpha.36"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.209"
|
||||
@@ -5646,7 +5717,7 @@ tasks.register("verifyAlpha210Release") {
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.data_version != 11
|
||||
|| manifests.sanctuary.custom?.sanctuary26?.network_protocol != 22
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.data_version != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 2
|
||||
|| manifests.onlyfun.custom?.sanctuary26?.network_protocol != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.data_version != 3
|
||||
|| manifests.iliketomoveit.custom?.sanctuary26?.network_protocol != 5
|
||||
|| manifests.itsalive.custom?.sanctuary26?.data_version != 3
|
||||
|
||||
+2
-2
@@ -20,13 +20,13 @@ anotherworld_version=0.0.0-alpha.55
|
||||
anotherworld_lifecycle=active
|
||||
itsalive_version=0.0.0-alpha.36
|
||||
itsalive_lifecycle=active
|
||||
ouch_version=0.0.0-alpha.9
|
||||
ouch_version=0.0.0-alpha.10
|
||||
ouch_lifecycle=active
|
||||
masterkey_version=0.0.0-alpha.8
|
||||
masterkey_lifecycle=active
|
||||
iliketomoveit_version=0.0.0-alpha.15
|
||||
iliketomoveit_lifecycle=active
|
||||
onlyfun_version=0.0.0-alpha.21
|
||||
onlyfun_version=0.0.0-alpha.24
|
||||
onlyfun_lifecycle=active
|
||||
sanctuary_version=0.0.0-alpha.119
|
||||
sanctuary_lifecycle=active
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Abjection dans Only Fun
|
||||
|
||||
Le runtime historique GPL-3.0-or-later de `Fun/abjection` est intégré au JAR **Only Fun**. Le
|
||||
namespace `abjection` et les IDs `abjection:shit_ball` et `abjection:shit_cube` sont conservés ; le
|
||||
manifeste Only Fun fournit l’alias Fabric `abjection`, sans créer un onzième module.
|
||||
|
||||
## ABJ-01 — actions serveur
|
||||
|
||||
- `/piss` émet son jet pendant 15 secondes et peut activer un bouton touché ;
|
||||
- `/vomit` émet son jet pendant 8 secondes ;
|
||||
- `/shit` émet son jet pendant 6 secondes et crée régulièrement la même
|
||||
`abjection:shit_ball` que les lots suivants ;
|
||||
- `/smoke` émet de la fumée pendant 10 secondes.
|
||||
|
||||
`AbjectionActions.start` est l’unique API publique de déclenchement. Chaque nouvelle demande
|
||||
redémarre seulement l’action correspondante, sans dupliquer la boule initiale de `/shit`. L’état
|
||||
reste en mémoire serveur, n’est jamais sauvegardé et est supprimé à la mort, au passage spectateur
|
||||
ou à la déconnexion.
|
||||
|
||||
Le bloc `abjection:shit_cube` reprend les propriétés de la boue, l’apparence 16×16, la recette
|
||||
neuf boules, le butin et l’outil pelle historiques. Les textes sont disponibles en français,
|
||||
anglais et russe. Only Fun reste en données v2 et protocole réseau 2 pour ABJ-01.
|
||||
|
||||
## ABJ-02 — projectile Ouch
|
||||
|
||||
Only Fun expose l’objet et le point d’installation `AbjectionProjectileApi`. Ouch, qui dépend
|
||||
explicitement d’Only Fun sans cycle inverse, enregistre le type d’entité sous le même ID historique
|
||||
`abjection:shit_ball` et installe la fabrique commune au lancer manuel et au dispenser. Le
|
||||
projectile mesure 0,25 bloc, consomme une unité hors créatif, joue le son de boue à l’impact et
|
||||
ajoute sept secondes à la nausée déjà présente sans dupliquer d’objet.
|
||||
|
||||
## ABJ-03 — raccourci configurable
|
||||
|
||||
La catégorie de contrôles Only Fun expose « Chier », assigné à `J` par défaut. Le client utilise
|
||||
`consumeClick`, puis envoie un payload vide `onlyfun:c2s/shit_action`. Le serveur accepte au plus
|
||||
une demande par joueur et par tick, revalide que le joueur est toujours connecté, vivant et non
|
||||
spectateur, puis appelle le même `AbjectionActions.start(SHIT)` que `/shit`. Maintenir la touche
|
||||
ne produit donc pas de répétition par tick et la commande reste disponible. Only Fun conserve ses
|
||||
données v2 et passe son protocole réseau de 2 à 3.
|
||||
@@ -1,6 +1,9 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickencarry.ChickenCarryController;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.command.AbjectionCommands;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.OnlyFunNetworking;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunRegistries;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.registry.OnlyFunCreativeTab;
|
||||
@@ -31,6 +34,7 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
AbjectionRegistries.initialize();
|
||||
OnlyFunRegistries.initialize();
|
||||
OnlyFunCreativeTab.initialize();
|
||||
OnlyFunGenerator.initialize();
|
||||
@@ -42,6 +46,8 @@ public final class OnlyFunMod implements ModInitializer {
|
||||
OnlyFunNetworking.initialize();
|
||||
ChickenCarryController.register();
|
||||
OnlyFunCommands.register();
|
||||
AbjectionActions.initialize();
|
||||
AbjectionCommands.register();
|
||||
LOGGER.info("[{}] Loot generator, natural Lucky Blocks, mob lootboxes and chicken rules ready (data v{}, network v{}).",
|
||||
DISPLAY_NAME, CURRENT_DATA_VERSION, OnlyFunNetworking.PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import java.util.function.Function;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
|
||||
/** Historical Abjection registry IDs, owned and distributed by Only Fun. */
|
||||
public final class AbjectionRegistries {
|
||||
public static final String NAMESPACE = "abjection";
|
||||
public static final Block SHIT_CUBE = registerBlock("shit_cube", Block::new,
|
||||
BlockBehaviour.Properties.ofFullCopy(Blocks.MUD));
|
||||
public static final Item SHIT_BALL = registerItem("shit_ball",
|
||||
properties -> new ShitBallItem(properties.stacksTo(16).useItemDescriptionPrefix()));
|
||||
public static final Item SHIT_CUBE_ITEM = registerBlockItem("shit_cube", SHIT_CUBE);
|
||||
|
||||
private AbjectionRegistries() {
|
||||
}
|
||||
|
||||
public static Identifier id(String path) {
|
||||
return Identifier.fromNamespaceAndPath(NAMESPACE, path);
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
// Class initialization owns registration; this method makes the startup order explicit.
|
||||
}
|
||||
|
||||
private static Item registerItem(String path, Function<Item.Properties, Item> factory) {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id(path));
|
||||
return Registry.register(BuiltInRegistries.ITEM, key,
|
||||
factory.apply(new Item.Properties().setId(key)));
|
||||
}
|
||||
|
||||
private static Block registerBlock(String path, Function<BlockBehaviour.Properties, Block> factory,
|
||||
BlockBehaviour.Properties properties) {
|
||||
ResourceKey<Block> key = ResourceKey.create(Registries.BLOCK, id(path));
|
||||
return Registry.register(BuiltInRegistries.BLOCK, key, factory.apply(properties.setId(key)));
|
||||
}
|
||||
|
||||
private static Item registerBlockItem(String path, Block block) {
|
||||
ResourceKey<Item> key = ResourceKey.create(Registries.ITEM, id(path));
|
||||
return Registry.register(BuiltInRegistries.ITEM, key,
|
||||
new BlockItem(block, new Item.Properties().setId(key).useBlockDescriptionPrefix()));
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Validates connection/player state and deduplicates malicious same-tick key requests. */
|
||||
public final class ShitActionRequestHandler {
|
||||
private static final Map<UUID, Integer> LAST_REQUEST_TICK = new HashMap<>();
|
||||
|
||||
private ShitActionRequestHandler() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
ServerPlayNetworking.registerGlobalReceiver(ShitActionRequestPayload.ID,
|
||||
(payload, context) -> handle(context.player()));
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) ->
|
||||
clear(handler.player.getUUID()));
|
||||
}
|
||||
|
||||
private static void handle(ServerPlayer player) {
|
||||
MinecraftServer server = player.level().getServer();
|
||||
if (server == null || server.getPlayerList().getPlayer(player.getUUID()) != player
|
||||
|| !player.isAlive() || player.isSpectator()
|
||||
|| !acceptTick(player.getUUID(), server.getTickCount())) return;
|
||||
AbjectionActions.start(player, AbjectionActions.Action.SHIT);
|
||||
}
|
||||
|
||||
static boolean acceptTick(UUID playerId, int tick) {
|
||||
Integer previous = LAST_REQUEST_TICK.put(playerId, tick);
|
||||
return previous == null || previous != tick;
|
||||
}
|
||||
|
||||
static void clear(UUID playerId) {
|
||||
LAST_REQUEST_TICK.remove(playerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionProjectileApi;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.ProjectileItem;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Historical item identity with a projectile contract implemented by the owning Ouch module. */
|
||||
public final class ShitBallItem extends Item implements ProjectileItem {
|
||||
public ShitBallItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Projectile asProjectile(Level level, Position position, ItemStack stack, Direction direction) {
|
||||
return AbjectionProjectileApi.create(level, position, stack, direction);
|
||||
}
|
||||
}
|
||||
+383
@@ -0,0 +1,383 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.ColorParticleOption;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.core.particles.ItemParticleOption;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.entity.HumanoidArm;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.ClipContext;
|
||||
import net.minecraft.world.level.block.ButtonBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Public server API shared by Abjection commands and bounded client requests. */
|
||||
public final class AbjectionActions {
|
||||
public enum Action {
|
||||
PISS(15 * 20, "piss"),
|
||||
VOMIT(8 * 20, "vomit"),
|
||||
SHIT(6 * 20, "shit"),
|
||||
SMOKE(10 * 20, "smoke");
|
||||
|
||||
private final int durationTicks;
|
||||
private final String id;
|
||||
|
||||
Action(int durationTicks, String id) {
|
||||
this.durationTicks = durationTicks;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int durationTicks() {
|
||||
return durationTicks;
|
||||
}
|
||||
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
public enum StartResult {
|
||||
STARTED,
|
||||
RESTARTED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
private static final Map<UUID, EnumMap<Action, Integer>> ACTIVE = new HashMap<>();
|
||||
private static final int STREAM_SEGMENTS = 64;
|
||||
private static final double STREAM_STEP_LENGTH = 0.05D;
|
||||
private static final double STREAM_STEP_SCALE = STREAM_STEP_LENGTH / 0.28D;
|
||||
private static final DustParticleOptions PISS_BRIGHT = new DustParticleOptions(0xEDD62E, 0.24F);
|
||||
private static final DustParticleOptions PISS_GOLD = new DustParticleOptions(0xD8B825, 0.2F);
|
||||
private static final DustParticleOptions PISS_AMBER = new DustParticleOptions(0xC6941A, 0.19F);
|
||||
private static final DustParticleOptions PISS_DEEP_GOLD = new DustParticleOptions(0xB88714, 0.17F);
|
||||
private static final DustParticleOptions VOMIT_GREEN = new DustParticleOptions(0x88A436, 0.24F);
|
||||
private static final DustParticleOptions VOMIT_YELLOW = new DustParticleOptions(0xB6B342, 0.2F);
|
||||
private static final DustParticleOptions VOMIT_ORANGE = new DustParticleOptions(0xA96A32, 0.19F);
|
||||
private static final DustParticleOptions VOMIT_BROWN = new DustParticleOptions(0x6E5828, 0.17F);
|
||||
private static final DustParticleOptions SHIT_SPLAT = new DustParticleOptions(0x4A2B14, 1.4F);
|
||||
private static final DustParticleOptions SHIT_MUD = new DustParticleOptions(0x6A4521, 0.22F);
|
||||
private static final ColorParticleOption PISS_BRIGHT_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xF0E35A);
|
||||
private static final ColorParticleOption PISS_GOLD_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xD8C43A);
|
||||
private static final ColorParticleOption PISS_AMBER_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xCDA128);
|
||||
private static final ColorParticleOption VOMIT_GREEN_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0x95B33F);
|
||||
private static final ColorParticleOption VOMIT_YELLOW_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xC4C35B);
|
||||
private static final ColorParticleOption VOMIT_ORANGE_MIST =
|
||||
ColorParticleOption.create(ParticleTypes.ENTITY_EFFECT, 0xB97A3D);
|
||||
private static final ItemParticleOption VOMIT_FLESH =
|
||||
new ItemParticleOption(ParticleTypes.ITEM, Items.ROTTEN_FLESH);
|
||||
private static final ItemParticleOption VOMIT_EYE =
|
||||
new ItemParticleOption(ParticleTypes.ITEM, Items.SPIDER_EYE);
|
||||
private static final ItemParticleOption VOMIT_POTATO =
|
||||
new ItemParticleOption(ParticleTypes.ITEM, Items.POISONOUS_POTATO);
|
||||
private static boolean initialized;
|
||||
|
||||
private AbjectionActions() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerTickEvents.END_SERVER_TICK.register(AbjectionActions::tick);
|
||||
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> clear(handler.player.getUUID()));
|
||||
}
|
||||
|
||||
public static StartResult start(ServerPlayer player, Action action) {
|
||||
if (player == null || action == null || !player.isAlive() || player.isSpectator()
|
||||
|| player.level().getServer() == null) return StartResult.REJECTED;
|
||||
boolean restarted = restart(player.getUUID(), action);
|
||||
if (action == Action.SHIT && !restarted) spawnShitDrop(player.level(), player);
|
||||
return restarted ? StartResult.RESTARTED : StartResult.STARTED;
|
||||
}
|
||||
|
||||
static boolean restart(UUID playerId, Action action) {
|
||||
EnumMap<Action, Integer> actions = ACTIVE.computeIfAbsent(playerId, ignored ->
|
||||
new EnumMap<>(Action.class));
|
||||
return actions.put(action, action.durationTicks()) != null;
|
||||
}
|
||||
|
||||
public static void clear(UUID playerId) {
|
||||
ACTIVE.remove(playerId);
|
||||
}
|
||||
|
||||
static int remaining(UUID playerId, Action action) {
|
||||
return ACTIVE.getOrDefault(playerId, new EnumMap<>(Action.class)).getOrDefault(action, 0);
|
||||
}
|
||||
|
||||
private static void tick(MinecraftServer server) {
|
||||
Iterator<Map.Entry<UUID, EnumMap<Action, Integer>>> players = ACTIVE.entrySet().iterator();
|
||||
while (players.hasNext()) {
|
||||
Map.Entry<UUID, EnumMap<Action, Integer>> entry = players.next();
|
||||
ServerPlayer player = server.getPlayerList().getPlayer(entry.getKey());
|
||||
if (player == null || !player.isAlive() || player.isSpectator()) {
|
||||
players.remove();
|
||||
continue;
|
||||
}
|
||||
Iterator<Map.Entry<Action, Integer>> actions = entry.getValue().entrySet().iterator();
|
||||
while (actions.hasNext()) {
|
||||
Map.Entry<Action, Integer> action = actions.next();
|
||||
emit(player, action.getKey(), action.getValue());
|
||||
int remaining = action.getValue() - 1;
|
||||
if (remaining <= 0) actions.remove();
|
||||
else action.setValue(remaining);
|
||||
}
|
||||
if (entry.getValue().isEmpty()) players.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private static void emit(ServerPlayer player, Action action, int remaining) {
|
||||
switch (action) {
|
||||
case PISS -> spawnPissStream(player);
|
||||
case VOMIT -> spawnVomitStream(player);
|
||||
case SHIT -> spawnShitStream(player, remaining);
|
||||
case SMOKE -> spawnSmoke(player);
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnPissStream(ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 position = pissOrigin(player);
|
||||
Vec3 velocity = pissVelocity(player);
|
||||
for (int step = 0; step < STREAM_SEGMENTS; step++) {
|
||||
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
|
||||
Vec3 midpoint = position.lerp(next, 0.5D);
|
||||
level.sendParticles(pissCore(step), midpoint.x, midpoint.y, midpoint.z, 2,
|
||||
0.012D, 0.012D, 0.012D, 0.0D);
|
||||
if ((step & 1) == 0) level.sendParticles(step % 4 == 0 ? PISS_DEEP_GOLD : PISS_BRIGHT,
|
||||
midpoint.x, midpoint.y, midpoint.z, 2, 0.01D, 0.01D, 0.01D, 0.0D);
|
||||
if (step % 3 == 0) level.sendParticles(pissMist(step), midpoint.x, midpoint.y, midpoint.z,
|
||||
2, 0.012D, 0.012D, 0.012D, 0.0D);
|
||||
HitResult hit = clip(level, player, position, next);
|
||||
if (hit.getType() != HitResult.Type.MISS) {
|
||||
activateButton(level, player, hit);
|
||||
pissImpact(level, hit.getLocation());
|
||||
return;
|
||||
}
|
||||
position = next;
|
||||
velocity = velocity.add(0.0D, -0.03D * STREAM_STEP_SCALE, 0.0D)
|
||||
.scale(Math.pow(0.985D, STREAM_STEP_SCALE));
|
||||
if (position.y < level.getMinY()) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnVomitStream(ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 position = vomitOrigin(player);
|
||||
Vec3 velocity = vomitVelocity(player);
|
||||
level.sendParticles(ParticleTypes.WHITE_SMOKE, position.x, position.y, position.z,
|
||||
2, 0.012D, 0.01D, 0.012D, 0.002D);
|
||||
for (int step = 0; step < STREAM_SEGMENTS; step++) {
|
||||
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
|
||||
Vec3 midpoint = position.lerp(next, 0.5D);
|
||||
level.sendParticles(vomitCore(step), midpoint.x, midpoint.y, midpoint.z, 2,
|
||||
0.012D, 0.012D, 0.012D, 0.0D);
|
||||
if ((step & 1) == 0) level.sendParticles(step % 4 == 0 ? VOMIT_BROWN : VOMIT_GREEN,
|
||||
midpoint.x, midpoint.y, midpoint.z, 2, 0.01D, 0.01D, 0.01D, 0.0D);
|
||||
HitResult hit = clip(level, player, position, next);
|
||||
if (hit.getType() != HitResult.Type.MISS) {
|
||||
vomitImpact(level, hit.getLocation());
|
||||
return;
|
||||
}
|
||||
position = next;
|
||||
velocity = velocity.add(0.0D, -0.038D * STREAM_STEP_SCALE, 0.0D)
|
||||
.scale(Math.pow(0.982D, STREAM_STEP_SCALE));
|
||||
if (position.y < level.getMinY()) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnShitStream(ServerPlayer player, int remaining) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 position = shitOrigin(player);
|
||||
Vec3 velocity = shitVelocity(player);
|
||||
if (remaining < Action.SHIT.durationTicks() && remaining % 20 == 0) spawnShitDrop(level, player);
|
||||
for (int step = 0; step < STREAM_SEGMENTS; step++) {
|
||||
Vec3 next = position.add(velocity.scale(STREAM_STEP_LENGTH));
|
||||
Vec3 midpoint = position.lerp(next, 0.5D);
|
||||
float progress = step / (float) (STREAM_SEGMENTS - 1);
|
||||
float spread = 0.002F + progress * 0.018F;
|
||||
level.sendParticles(new DustParticleOptions(0x5B3417,
|
||||
Mth.lerp(progress * progress, 0.05F, 1.55F)), midpoint.x, midpoint.y, midpoint.z,
|
||||
1, spread, spread, spread, 0.0D);
|
||||
if ((step & 1) == 0) level.sendParticles(new DustParticleOptions(0x75461F,
|
||||
Mth.lerp(progress * progress, 0.04F, 0.85F)), midpoint.x, midpoint.y, midpoint.z,
|
||||
1, spread, spread, spread, 0.0D);
|
||||
HitResult hit = clip(level, player, position, next);
|
||||
if (hit.getType() != HitResult.Type.MISS) {
|
||||
level.sendParticles(SHIT_SPLAT, hit.getLocation().x, hit.getLocation().y,
|
||||
hit.getLocation().z, 2, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
level.sendParticles(SHIT_MUD, hit.getLocation().x, hit.getLocation().y,
|
||||
hit.getLocation().z, 4, 0.04D, 0.025D, 0.04D, 0.0D);
|
||||
return;
|
||||
}
|
||||
position = next;
|
||||
velocity = velocity.add(0.0D, -0.032D * STREAM_STEP_SCALE, 0.0D)
|
||||
.scale(Math.pow(0.986D, STREAM_STEP_SCALE));
|
||||
if (position.y < level.getMinY()) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void spawnSmoke(ServerPlayer player) {
|
||||
ServerLevel level = player.level();
|
||||
Vec3 mouth = mouthOrigin(player);
|
||||
level.sendParticles(ParticleTypes.SMOKE, mouth.x, mouth.y, mouth.z,
|
||||
2, 0.015D, 0.01D, 0.015D, 0.003D);
|
||||
if (level.getRandom().nextInt(3) == 0) level.sendParticles(ParticleTypes.WHITE_SMOKE,
|
||||
mouth.x, mouth.y, mouth.z, 1, 0.01D, 0.008D, 0.01D, 0.002D);
|
||||
}
|
||||
|
||||
private static HitResult clip(ServerLevel level, ServerPlayer player, Vec3 from, Vec3 to) {
|
||||
return level.clip(new ClipContext(from, to, ClipContext.Block.COLLIDER, ClipContext.Fluid.ANY, player));
|
||||
}
|
||||
|
||||
private static Vec3 pissOrigin(ServerPlayer player) {
|
||||
Vec3 forward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize();
|
||||
Vec3 right = new Vec3(-forward.z, 0.0D, forward.x);
|
||||
double side = player.getMainArm() == HumanoidArm.RIGHT ? -0.12D : 0.12D;
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.55D, 0.0D)
|
||||
.add(forward.scale(0.24D)).add(right.scale(side));
|
||||
}
|
||||
|
||||
private static Vec3 pissVelocity(ServerPlayer player) {
|
||||
float pitch = Mth.clamp(player.getXRot() + 22.0F, 6.0F, 55.0F);
|
||||
return Vec3.directionFromRotation(pitch, player.getYRot()).normalize().scale(1.05D)
|
||||
.add(player.getDeltaMovement().multiply(0.18D, 0.0D, 0.18D));
|
||||
}
|
||||
|
||||
private static Vec3 vomitOrigin(ServerPlayer player) {
|
||||
Vec3 forward = Vec3.directionFromRotation(player.getXRot(), player.getYRot()).normalize();
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.82D, 0.0D)
|
||||
.add(forward.scale(0.18D));
|
||||
}
|
||||
|
||||
private static Vec3 vomitVelocity(ServerPlayer player) {
|
||||
float pitch = Mth.clamp(player.getXRot() + 8.0F, -18.0F, 42.0F);
|
||||
return Vec3.directionFromRotation(pitch, player.getYRot()).normalize().scale(0.8D)
|
||||
.add(player.getDeltaMovement().multiply(0.15D, 0.0D, 0.15D));
|
||||
}
|
||||
|
||||
private static Vec3 shitOrigin(ServerPlayer player) {
|
||||
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.44D, 0.0D)
|
||||
.add(backward.scale(0.16D));
|
||||
}
|
||||
|
||||
private static Vec3 shitVelocity(ServerPlayer player) {
|
||||
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
|
||||
return backward.scale(0.68D).add(0.0D, 0.08D, 0.0D)
|
||||
.add(player.getDeltaMovement().multiply(0.1D, 0.0D, 0.1D));
|
||||
}
|
||||
|
||||
private static Vec3 mouthOrigin(ServerPlayer player) {
|
||||
Vec3 forward = Vec3.directionFromRotation(player.getXRot(), player.getYRot()).normalize();
|
||||
return player.position().add(0.0D, player.getBbHeight() * 0.82D, 0.0D)
|
||||
.add(forward.scale(0.12D));
|
||||
}
|
||||
|
||||
private static void spawnShitDrop(ServerLevel level, ServerPlayer player) {
|
||||
Vec3 backward = Vec3.directionFromRotation(0.0F, player.getYRot()).normalize().scale(-1.0D);
|
||||
Vec3 position = shitOrigin(player).add(backward.scale(0.08D)).add(0.0D, -0.08D, 0.0D);
|
||||
Vec3 velocity = backward.scale(0.05D).add(0.0D, 0.02D, 0.0D);
|
||||
ItemEntity item = new ItemEntity(level, position.x, position.y, position.z,
|
||||
new ItemStack(AbjectionRegistries.SHIT_BALL), velocity.x, velocity.y, velocity.z);
|
||||
item.setPickUpDelay(10);
|
||||
item.setThrower(player);
|
||||
level.addFreshEntity(item);
|
||||
}
|
||||
|
||||
private static DustParticleOptions pissCore(int step) {
|
||||
return switch (step % 3) {
|
||||
case 0 -> PISS_BRIGHT;
|
||||
case 1 -> PISS_GOLD;
|
||||
default -> PISS_AMBER;
|
||||
};
|
||||
}
|
||||
|
||||
private static ColorParticleOption pissMist(int step) {
|
||||
return switch (step % 3) {
|
||||
case 0 -> PISS_BRIGHT_MIST;
|
||||
case 1 -> PISS_GOLD_MIST;
|
||||
default -> PISS_AMBER_MIST;
|
||||
};
|
||||
}
|
||||
|
||||
private static DustParticleOptions vomitCore(int step) {
|
||||
return switch (step % 3) {
|
||||
case 0 -> VOMIT_GREEN;
|
||||
case 1 -> VOMIT_YELLOW;
|
||||
default -> VOMIT_ORANGE;
|
||||
};
|
||||
}
|
||||
|
||||
private static ItemParticleOption vomitChunk(ServerLevel level) {
|
||||
return switch (level.getRandom().nextInt(3)) {
|
||||
case 0 -> VOMIT_FLESH;
|
||||
case 1 -> VOMIT_EYE;
|
||||
default -> VOMIT_POTATO;
|
||||
};
|
||||
}
|
||||
|
||||
private static void activateButton(ServerLevel level, ServerPlayer player, HitResult hit) {
|
||||
if (!(hit instanceof BlockHitResult blockHit)) return;
|
||||
BlockPos pos = blockHit.getBlockPos();
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (state.getBlock() instanceof ButtonBlock button && !state.getValue(ButtonBlock.POWERED)) {
|
||||
button.press(state, level, pos, player);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pissImpact(ServerLevel level, Vec3 position) {
|
||||
for (DustParticleOptions particle : new DustParticleOptions[] {PISS_BRIGHT, PISS_GOLD, PISS_AMBER}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 3,
|
||||
0.02D, 0.02D, 0.02D, 0.0D);
|
||||
}
|
||||
for (ColorParticleOption particle : new ColorParticleOption[] {
|
||||
PISS_BRIGHT_MIST, PISS_GOLD_MIST, PISS_AMBER_MIST}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 2,
|
||||
0.03D, 0.022D, 0.03D, 0.0D);
|
||||
}
|
||||
level.sendParticles(ParticleTypes.BUBBLE_POP, position.x, position.y, position.z,
|
||||
8, 0.05D, 0.03D, 0.05D, 0.01D);
|
||||
}
|
||||
|
||||
private static void vomitImpact(ServerLevel level, Vec3 position) {
|
||||
for (DustParticleOptions particle : new DustParticleOptions[] {
|
||||
VOMIT_GREEN, VOMIT_YELLOW, VOMIT_ORANGE, VOMIT_BROWN}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 3,
|
||||
0.02D, 0.02D, 0.02D, 0.0D);
|
||||
}
|
||||
for (ColorParticleOption particle : new ColorParticleOption[] {
|
||||
VOMIT_GREEN_MIST, VOMIT_YELLOW_MIST, VOMIT_ORANGE_MIST}) {
|
||||
level.sendParticles(particle, position.x, position.y, position.z, 2,
|
||||
0.03D, 0.022D, 0.03D, 0.0D);
|
||||
}
|
||||
level.sendParticles(ParticleTypes.SPLASH, position.x, position.y, position.z,
|
||||
4, 0.035D, 0.015D, 0.035D, 0.01D);
|
||||
level.sendParticles(ParticleTypes.SNEEZE, position.x, position.y, position.z,
|
||||
2, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
level.sendParticles(ParticleTypes.ITEM_SLIME, position.x, position.y, position.z,
|
||||
3, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
for (int index = 0; index < 2; index++) level.sendParticles(vomitChunk(level),
|
||||
position.x, position.y, position.z, 1, 0.03D, 0.02D, 0.03D, 0.0D);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Minimal public object contract consumed by Ouch's Abjection projectile implementation. */
|
||||
public final class AbjectionItems {
|
||||
private AbjectionItems() {
|
||||
}
|
||||
|
||||
public static Item shitBall() {
|
||||
return AbjectionRegistries.SHIT_BALL;
|
||||
}
|
||||
|
||||
public static Identifier shitBallId() {
|
||||
return Identifier.fromNamespaceAndPath("abjection", "shit_ball");
|
||||
}
|
||||
|
||||
public static boolean isShitBall(ItemStack stack) {
|
||||
return stack.is(AbjectionRegistries.SHIT_BALL);
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Owner-neutral projectile factory contract: Only Fun owns the item, while Ouch installs its
|
||||
* combat implementation without creating a reverse dependency.
|
||||
*/
|
||||
public final class AbjectionProjectileApi {
|
||||
@FunctionalInterface
|
||||
public interface Factory {
|
||||
Projectile create(Level level, Position position, ItemStack stack, Direction direction);
|
||||
}
|
||||
|
||||
private static Factory factory;
|
||||
|
||||
private AbjectionProjectileApi() {
|
||||
}
|
||||
|
||||
public static void registerFactory(Factory implementation) {
|
||||
Objects.requireNonNull(implementation, "implementation");
|
||||
if (factory != null) throw new IllegalStateException("Abjection projectile factory already registered");
|
||||
factory = implementation;
|
||||
}
|
||||
|
||||
public static Projectile create(Level level, Position position, ItemStack stack, Direction direction) {
|
||||
if (factory == null) throw new IllegalStateException("Ouch has not registered the Abjection projectile factory");
|
||||
return factory.create(level, position, stack, direction);
|
||||
}
|
||||
|
||||
public static boolean isReady() {
|
||||
return factory != null;
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,34 @@ package fr.koka99cab.sanctuary26.onlyfun.client;
|
||||
import fr.koka99cab.sanctuary26.iliketomoveit.physics.VehicleFlightPhysics;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatControlPayload;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.chickenboat.ChickenBoatUtil;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.network.ShitActionRequestPayload;
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.keymapping.v1.KeyMappingHelper;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public final class OnlyFunClient implements ClientModInitializer {
|
||||
private static final KeyMapping.Category ONLY_FUN_CATEGORY =
|
||||
KeyMapping.Category.register(OnlyFunMod.id("onlyfun"));
|
||||
private static KeyMapping shitKey;
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
shitKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.onlyfun.shit", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, ONLY_FUN_CATEGORY));
|
||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||
if (client.player == null || client.level == null) return;
|
||||
while (shitKey.consumeClick()) {
|
||||
if (ClientPlayNetworking.canSend(ShitActionRequestPayload.ID)) {
|
||||
ClientPlayNetworking.send(new ShitActionRequestPayload());
|
||||
}
|
||||
}
|
||||
Entity vehicle = client.player.getVehicle();
|
||||
if (!(vehicle instanceof AbstractBoat boat) || !ChickenBoatUtil.hasChickenPassenger(boat)) return;
|
||||
if (!ClientPlayNetworking.canSend(ChickenBoatControlPayload.ID)) return;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.command;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionActions;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Historical root commands, routed through the shared server action API. */
|
||||
public final class AbjectionCommands {
|
||||
private AbjectionCommands() {
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
for (AbjectionActions.Action action : AbjectionActions.Action.values()) {
|
||||
dispatcher.register(Commands.literal(action.id())
|
||||
.executes(context -> start(context.getSource(), action)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static int start(CommandSourceStack source, AbjectionActions.Action action)
|
||||
throws CommandSyntaxException {
|
||||
ServerPlayer player = source.getPlayerOrException();
|
||||
AbjectionActions.StartResult result = AbjectionActions.start(player, action);
|
||||
if (result == AbjectionActions.StartResult.REJECTED) return 0;
|
||||
String suffix = result == AbjectionActions.StartResult.RESTARTED ? "restarted" : "started";
|
||||
source.sendSuccess(() -> Component.translatable(
|
||||
"command.abjection." + action.id() + "." + suffix), false);
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -9,7 +9,7 @@ import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
|
||||
* Networking contract for Only Fun. Payload IDs must use the module namespace.
|
||||
*/
|
||||
public final class OnlyFunNetworking {
|
||||
public static final int PROTOCOL_VERSION = 2;
|
||||
public static final int PROTOCOL_VERSION = 3;
|
||||
private static boolean initialized;
|
||||
|
||||
private OnlyFunNetworking() {
|
||||
@@ -21,7 +21,9 @@ public final class OnlyFunNetworking {
|
||||
}
|
||||
initialized = true;
|
||||
PayloadTypeRegistry.serverboundPlay().register(ChickenBoatControlPayload.ID, ChickenBoatControlPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(ShitActionRequestPayload.ID, ShitActionRequestPayload.CODEC);
|
||||
ChickenBoatServerController.register();
|
||||
fr.koka99cab.sanctuary26.onlyfun.abjection.ShitActionRequestHandler.register();
|
||||
OnlyFunMod.LOGGER.debug("[{}] Networking contract v{} ready; chicken boat controls registered.",
|
||||
OnlyFunMod.DISPLAY_NAME, PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
|
||||
/** Empty and therefore strictly bounded request to start the existing server-owned action. */
|
||||
public record ShitActionRequestPayload() implements CustomPacketPayload {
|
||||
public static final Type<ShitActionRequestPayload> ID =
|
||||
new Type<>(OnlyFunMod.id("c2s/shit_action"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, ShitActionRequestPayload> CODEC =
|
||||
CustomPacketPayload.codec((payload, buffer) -> {}, buffer -> new ShitActionRequestPayload());
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.OnlyFunMod;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.abjection.AbjectionRegistries;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
@@ -27,6 +28,8 @@ public final class OnlyFunCreativeTab {
|
||||
output.accept(OnlyFunRegistries.CAPE_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.SPAWN_EGG_LOOTBOX);
|
||||
output.accept(OnlyFunRegistries.ANOMALY_LOOTBOX);
|
||||
output.accept(AbjectionRegistries.SHIT_BALL);
|
||||
output.accept(AbjectionRegistries.SHIT_CUBE_ITEM);
|
||||
})
|
||||
.build()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"variants": {
|
||||
"": {
|
||||
"model": "abjection:block/shit_cube"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "abjection:item/shit_ball"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"model": {
|
||||
"type": "minecraft:model",
|
||||
"model": "abjection:item/shit_cube"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"item.abjection.shit_ball": "Shit Ball",
|
||||
"block.abjection.shit_cube": "Shit Cube",
|
||||
"command.abjection.piss.started": "Here we go for 15 seconds.",
|
||||
"command.abjection.piss.restarted": "The timer restarts for 15 seconds.",
|
||||
"command.abjection.vomit.started": "Yuck. You vomit for 8 seconds.",
|
||||
"command.abjection.vomit.restarted": "You vomit again for 8 seconds.",
|
||||
"command.abjection.shit.started": "You shit in a stream for 6 seconds.",
|
||||
"command.abjection.shit.restarted": "The shit stream restarts for 6 seconds.",
|
||||
"command.abjection.smoke.started": "You blow smoke for 10 seconds.",
|
||||
"command.abjection.smoke.restarted": "You keep smoking for 10 seconds."
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"item.abjection.shit_ball": "Boule de merde",
|
||||
"block.abjection.shit_cube": "Cube de merde",
|
||||
"command.abjection.piss.started": "C'est parti pour 15 secondes.",
|
||||
"command.abjection.piss.restarted": "Le timer repart pour 15 secondes.",
|
||||
"command.abjection.vomit.started": "Beurk. Tu vomis pendant 8 secondes.",
|
||||
"command.abjection.vomit.restarted": "Tu revomis pour 8 secondes.",
|
||||
"command.abjection.shit.started": "Tu chies en jet pendant 6 secondes.",
|
||||
"command.abjection.shit.restarted": "Le jet de merde repart pour 6 secondes.",
|
||||
"command.abjection.smoke.started": "Tu souffles de la fumée pendant 10 secondes.",
|
||||
"command.abjection.smoke.restarted": "Tu continues de fumer pendant 10 secondes."
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"item.abjection.shit_ball": "Шарик дерьма",
|
||||
"block.abjection.shit_cube": "Куб дерьма",
|
||||
"command.abjection.piss.started": "Струя запущена на 15 секунд.",
|
||||
"command.abjection.piss.restarted": "Таймер струи перезапущен на 15 секунд.",
|
||||
"command.abjection.vomit.started": "Фу. Тебя рвёт 8 секунд.",
|
||||
"command.abjection.vomit.restarted": "Тебя снова рвёт 8 секунд.",
|
||||
"command.abjection.shit.started": "Ты испражняешься струёй 6 секунд.",
|
||||
"command.abjection.shit.restarted": "Струя дерьма перезапущена на 6 секунд.",
|
||||
"command.abjection.smoke.started": "Ты выпускаешь дым 10 секунд.",
|
||||
"command.abjection.smoke.restarted": "Ты продолжаешь курить ещё 10 секунд."
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:block/cube_all",
|
||||
"textures": {
|
||||
"all": "abjection:block/shit_cube"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"parent": "minecraft:item/generated",
|
||||
"textures": {
|
||||
"layer0": "abjection:item/shit_ball"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"parent": "abjection:block/shit_cube"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 852 B |
Binary file not shown.
|
After Width: | Height: | Size: 261 B |
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Rolls: %s | dry streak: %s | anomalies: %s | last: %s",
|
||||
"command.onlyfun.give": "Granted Only Fun source: %s",
|
||||
"command.onlyfun.roll": "Outcome: %s (%s)",
|
||||
"command.onlyfun.simulate": "Simulation %s × %s: %s"
|
||||
"command.onlyfun.simulate": "Simulation %s × %s: %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Shit"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Tirages : %s | série sèche : %s | anomalies : %s | dernier : %s",
|
||||
"command.onlyfun.give": "Source Only Fun donnée : %s",
|
||||
"command.onlyfun.roll": "Résultat : %s (%s)",
|
||||
"command.onlyfun.simulate": "Simulation %s × %s : %s"
|
||||
"command.onlyfun.simulate": "Simulation %s × %s : %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Chier"
|
||||
}
|
||||
|
||||
@@ -41,5 +41,7 @@
|
||||
"command.onlyfun.history": "Розыгрыши: %s | серия: %s | аномалии: %s | последнее: %s",
|
||||
"command.onlyfun.give": "Выдан источник Only Fun: %s",
|
||||
"command.onlyfun.roll": "Результат: %s (%s)",
|
||||
"command.onlyfun.simulate": "Симуляция %s × %s: %s"
|
||||
"command.onlyfun.simulate": "Симуляция %s × %s: %s",
|
||||
"key.categories.onlyfun.onlyfun": "Only Fun",
|
||||
"key.onlyfun.shit": "Испражниться"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "minecraft:block",
|
||||
"pools": [
|
||||
{
|
||||
"bonus_rolls": 0.0,
|
||||
"conditions": [
|
||||
{
|
||||
"condition": "minecraft:survives_explosion"
|
||||
}
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
"type": "minecraft:item",
|
||||
"name": "abjection:shit_cube"
|
||||
}
|
||||
],
|
||||
"rolls": 1.0
|
||||
}
|
||||
],
|
||||
"random_sequence": "abjection:blocks/shit_cube"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "minecraft:crafting_shaped",
|
||||
"category": "building",
|
||||
"key": {
|
||||
"#": "abjection:shit_ball"
|
||||
},
|
||||
"pattern": [
|
||||
"###",
|
||||
"###",
|
||||
"###"
|
||||
],
|
||||
"result": {
|
||||
"count": 1,
|
||||
"id": "abjection:shit_cube"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"abjection:shit_cube"
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
"KOKA99CAB"
|
||||
],
|
||||
"license": "GPL-3.0-or-later",
|
||||
"provides": [
|
||||
"abjection"
|
||||
],
|
||||
"icon": "assets/onlyfun/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
@@ -31,7 +34,7 @@
|
||||
"system_type": "gameplay-system",
|
||||
"namespace": "onlyfun",
|
||||
"data_version": 2,
|
||||
"network_protocol": 2,
|
||||
"network_protocol": 3,
|
||||
"lifecycle": "${lifecycle}",
|
||||
"pack_version": "${pack_version}",
|
||||
"descriptions": {
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.abjection;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Executable check for one physical press and same-tick spam deduplication. */
|
||||
public final class ShitActionRequestSmoke {
|
||||
private ShitActionRequestSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000059");
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 100), "first press was rejected");
|
||||
require(!ShitActionRequestHandler.acceptTick(playerId, 100), "same-tick spam was accepted");
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 101), "next physical press was rejected");
|
||||
ShitActionRequestHandler.clear(playerId);
|
||||
require(ShitActionRequestHandler.acceptTick(playerId, 101), "disconnect cleanup retained input state");
|
||||
ShitActionRequestHandler.clear(playerId);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package fr.koka99cab.sanctuary26.onlyfun.api.abjection;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
|
||||
/** Executable regression check for the bounded historical action contract. */
|
||||
public final class AbjectionActionContractSmoke {
|
||||
private AbjectionActionContractSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
require(AbjectionActions.Action.PISS.durationTicks() == 300, "piss duration changed");
|
||||
require(AbjectionActions.Action.VOMIT.durationTicks() == 160, "vomit duration changed");
|
||||
require(AbjectionActions.Action.SHIT.durationTicks() == 120, "shit duration changed");
|
||||
require(AbjectionActions.Action.SMOKE.durationTicks() == 200, "smoke duration changed");
|
||||
require(Set.of("piss", "vomit", "shit", "smoke").equals(Set.of(
|
||||
AbjectionActions.Action.PISS.id(), AbjectionActions.Action.VOMIT.id(),
|
||||
AbjectionActions.Action.SHIT.id(), AbjectionActions.Action.SMOKE.id())),
|
||||
"historical command surface changed");
|
||||
|
||||
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000057");
|
||||
require(!AbjectionActions.restart(playerId, AbjectionActions.Action.SHIT),
|
||||
"first action incorrectly marked as a restart");
|
||||
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 120,
|
||||
"initial duration was not installed");
|
||||
require(AbjectionActions.restart(playerId, AbjectionActions.Action.SHIT),
|
||||
"second action was not marked as a restart");
|
||||
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 120,
|
||||
"restart did not reset the full duration");
|
||||
AbjectionActions.clear(playerId);
|
||||
require(AbjectionActions.remaining(playerId, AbjectionActions.Action.SHIT) == 0,
|
||||
"cleanup left a temporary action behind");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
+45
-3
@@ -12,9 +12,10 @@ def miningRifleTextureHash = "4e4295c5c8a8f71d03307809448ff64363eec2d2aefdc538c4
|
||||
|
||||
tasks.register("verifyOuch") {
|
||||
group = "verification"
|
||||
description = "Checks Ouch alpha.9, including the Ambiance-backed Weather TNT and migrated Gunz arsenal."
|
||||
description = "Checks Ouch alpha.10, including the shared Abjection projectile implementation."
|
||||
inputs.files(fileTree("src/main/java"))
|
||||
inputs.files(fileTree("src/main/resources"))
|
||||
inputs.files(rootProject.fileTree("onlyfun/src/main/java"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.1.2-gunz-to-26.2.0-alpha.41.json"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.99-canon-weather-ownership-to-alpha.100.json"))
|
||||
inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
@@ -25,6 +26,34 @@ tasks.register("verifyOuch") {
|
||||
def weatherRegistry = file("src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchWeatherTnt.java").text
|
||||
def weatherEntity = file("src/main/java/fr/koka99cab/sanctuary26/ouch/entity/WeatherTntEntity.java").text
|
||||
def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json"))
|
||||
def abjectionRegistry = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/ouch/registry/OuchAbjectionProjectiles.java").text
|
||||
def abjectionEntity = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/ouch/entity/ShitBallEntity.java").text
|
||||
def abjectionClient = file(
|
||||
"src/main/java/fr/koka99cab/sanctuary26/ouch/client/OuchClient.java").text
|
||||
def onlyFunProjectileApi = rootProject.file(
|
||||
"onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/api/abjection/AbjectionProjectileApi.java").text
|
||||
if (manifest.depends?.onlyfun != ">=0.0.0-alpha.23"
|
||||
|| manifest.custom?.sanctuary26?.data_version != 1
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 1
|
||||
|| !rootProject.file("build.gradle").text.contains('implementation project(":onlyfun")')
|
||||
|| !onlyFunProjectileApi.contains("public interface Factory")
|
||||
|| !abjectionRegistry.contains('AbjectionProjectileApi.registerFactory(')
|
||||
|| !abjectionRegistry.contains('AbjectionItems.shitBallId()')
|
||||
|| abjectionRegistry.contains('AbjectionRegistries')
|
||||
|| !abjectionRegistry.contains('DispenserBlock.registerProjectileBehavior(AbjectionItems.shitBall())')
|
||||
|| !abjectionRegistry.contains('UseItemCallback.EVENT.register')
|
||||
|| !abjectionRegistry.contains('stack.consume(1, player)')
|
||||
|| !abjectionRegistry.contains('Projectile.spawnProjectileFromRotation(')
|
||||
|| !abjectionRegistry.contains('.sized(0.25F, 0.25F)')
|
||||
|| !abjectionEntity.contains('NAUSEA_DURATION_TICKS = 7 * 20')
|
||||
|| !abjectionEntity.contains('current == null ? 0 : current.getDuration()')
|
||||
|| !abjectionEntity.contains('SoundEvents.MUD_PLACE')
|
||||
|| !abjectionEntity.contains('discard()')
|
||||
|| !abjectionClient.contains('new ThrownItemRenderer<>(context)')) {
|
||||
throw new GradleException("Ouch alpha.10 Abjection projectile contract is incomplete")
|
||||
}
|
||||
if (!itemRegistry.contains("MIGRATED_WEAPONS = List.of(CREEPERLOCK, MITRAILLETTE, EXPULSEUR, ASPIRATEUR)")
|
||||
|| !itemRegistry.contains("WEAPONS = List.of(CREEPERLOCK, MITRAILLETTE, EXPULSEUR, ASPIRATEUR, MINING_RIFLE, BETA_BOW)")
|
||||
|| !creativeTab.contains('OuchMod.id("arsenal")')
|
||||
@@ -89,7 +118,7 @@ tasks.register("verifyOuch") {
|
||||
def creeperlockRecipe = new JsonSlurper().parse(file("src/main/resources/data/ouch/recipe/creeperlock.json"))
|
||||
def silverEngineeringMigration = new JsonSlurper().parse(rootProject.file(
|
||||
"pack/migrations/26.2.0-alpha.184-silver-engineering-to-alpha.185.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.9"
|
||||
if (project.version.toString() != "0.0.0-alpha.10"
|
||||
|| mitrailletteRecipe.pattern != ["RRP", "IGI", " B "]
|
||||
|| mitrailletteRecipe.key != [B: "minecraft:stone_button", G: "minecraft:redstone",
|
||||
I: "anotherworld:silver_ingot", P: "anotherworld:silver_plate", R: "anotherworld:silver_rod"]
|
||||
@@ -317,10 +346,23 @@ tasks.register("verifyOuch") {
|
||||
throw new GradleException("Runtime Ouch resources must not re-register the legacy gunz namespace: ${forbiddenNamespaceReferences}")
|
||||
}
|
||||
|
||||
logger.lifecycle("Ouch verified: 6 enchantable items, 4 custom enchantments, debuffed bases, shared headshots, 6 recipes and FR/EN/RU translations.")
|
||||
logger.lifecycle("Ouch verified: arsenal, Weather TNT and shared Abjection projectile contract.")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("test") {
|
||||
failOnNoDiscoveredTests = false
|
||||
}
|
||||
|
||||
tasks.register("shitBallContractSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks additive and overflow-safe Shit Ball nausea."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.ouch.entity.ShitBallContractSmoke"
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn tasks.named("shitBallContractSmoke")
|
||||
dependsOn tasks.named("verifyOuch")
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@ package fr.koka99cab.sanctuary26.ouch.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.ouch.OuchMod;
|
||||
import fr.koka99cab.sanctuary26.ouch.entity.WeatherTntEntity;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchWeatherTnt;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry;
|
||||
import net.minecraft.client.renderer.entity.EntityRenderer;
|
||||
import net.minecraft.client.renderer.entity.TntRenderer;
|
||||
import net.minecraft.client.renderer.entity.ThrownItemRenderer;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
|
||||
public final class OuchClient implements ClientModInitializer {
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
EntityRendererRegistry.register(OuchAbjectionProjectiles.ENTITY,
|
||||
context -> new ThrownItemRenderer<>(context));
|
||||
registerWeatherRenderer(OuchWeatherTnt.ENTITY);
|
||||
registerWeatherRenderer(OuchWeatherTnt.REDSTONER_COMPAT_ENTITY);
|
||||
registerWeatherRenderer(OuchWeatherTnt.LEGACY_ENTITY);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.entity;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionItems;
|
||||
import fr.koka99cab.sanctuary26.ouch.registry.OuchAbjectionProjectiles;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrowableItemProjectile;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.EntityHitResult;
|
||||
import net.minecraft.world.phys.HitResult;
|
||||
|
||||
/** Server-authoritative historical Abjection projectile, owned by Ouch. */
|
||||
public final class ShitBallEntity extends ThrowableItemProjectile {
|
||||
public static final int NAUSEA_DURATION_TICKS = 7 * 20;
|
||||
|
||||
public ShitBallEntity(EntityType<? extends ShitBallEntity> type, Level level) {
|
||||
super(type, level);
|
||||
}
|
||||
|
||||
public ShitBallEntity(Level level, LivingEntity owner, ItemStack stack) {
|
||||
super(OuchAbjectionProjectiles.ENTITY, owner, level, stack.copyWithCount(1));
|
||||
}
|
||||
|
||||
public ShitBallEntity(Level level, double x, double y, double z, ItemStack stack) {
|
||||
super(OuchAbjectionProjectiles.ENTITY, x, y, z, level, stack.copyWithCount(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Item getDefaultItem() {
|
||||
return AbjectionItems.shitBall();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHitEntity(EntityHitResult hit) {
|
||||
super.onHitEntity(hit);
|
||||
if (!(level() instanceof ServerLevel) || !(hit.getEntity() instanceof LivingEntity target)) return;
|
||||
MobEffectInstance current = target.getEffect(MobEffects.NAUSEA);
|
||||
int duration = extendedNauseaDuration(current == null ? 0 : current.getDuration());
|
||||
target.addEffect(new MobEffectInstance(MobEffects.NAUSEA, duration), getOwner());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHit(HitResult hit) {
|
||||
super.onHit(hit);
|
||||
if (level() instanceof ServerLevel level) {
|
||||
level.playSound(null, getX(), getY(), getZ(), SoundEvents.MUD_PLACE,
|
||||
SoundSource.PLAYERS, 1.0F, 0.75F);
|
||||
discard();
|
||||
}
|
||||
}
|
||||
|
||||
static int extendedNauseaDuration(int currentDuration) {
|
||||
return (int) Math.min(Integer.MAX_VALUE, Math.max(0L, currentDuration) + NAUSEA_DURATION_TICKS);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.registry;
|
||||
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionItems;
|
||||
import fr.koka99cab.sanctuary26.onlyfun.api.abjection.AbjectionProjectileApi;
|
||||
import fr.koka99cab.sanctuary26.ouch.entity.ShitBallEntity;
|
||||
import net.fabricmc.fabric.api.event.player.UseItemCallback;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.Position;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.stats.Stats;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.entity.projectile.Projectile;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.DispenserBlock;
|
||||
|
||||
/** Shared manual/dispenser creation path for {@code abjection:shit_ball}. */
|
||||
public final class OuchAbjectionProjectiles {
|
||||
private static final float SHOOT_POWER = 1.5F;
|
||||
private static final ResourceKey<EntityType<?>> KEY = ResourceKey.create(
|
||||
Registries.ENTITY_TYPE, AbjectionItems.shitBallId());
|
||||
public static final EntityType<ShitBallEntity> ENTITY = Registry.register(
|
||||
BuiltInRegistries.ENTITY_TYPE, KEY,
|
||||
EntityType.Builder.<ShitBallEntity>of(ShitBallEntity::new, MobCategory.MISC)
|
||||
.sized(0.25F, 0.25F).clientTrackingRange(4).updateInterval(10).build(KEY));
|
||||
private static boolean initialized;
|
||||
|
||||
private OuchAbjectionProjectiles() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
AbjectionProjectileApi.registerFactory(OuchAbjectionProjectiles::createFromDispenser);
|
||||
DispenserBlock.registerProjectileBehavior(AbjectionItems.shitBall());
|
||||
UseItemCallback.EVENT.register((player, level, hand) -> {
|
||||
ItemStack stack = player.getItemInHand(hand);
|
||||
if (!AbjectionItems.isShitBall(stack) || player.isSpectator()) return InteractionResult.PASS;
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
if (!(level instanceof ServerLevel serverLevel)) return InteractionResult.FAIL;
|
||||
serverLevel.playSound(null, player.getX(), player.getY(), player.getZ(),
|
||||
SoundEvents.SNOWBALL_THROW, SoundSource.NEUTRAL, 0.5F,
|
||||
0.4F / (serverLevel.getRandom().nextFloat() * 0.4F + 0.8F));
|
||||
Projectile.spawnProjectileFromRotation(OuchAbjectionProjectiles::createFromPlayer,
|
||||
serverLevel, stack, player, 0.0F, SHOOT_POWER, 1.0F);
|
||||
player.awardStat(Stats.ITEM_USED.get(AbjectionItems.shitBall()));
|
||||
stack.consume(1, player);
|
||||
return InteractionResult.SUCCESS_SERVER;
|
||||
});
|
||||
}
|
||||
|
||||
public static ShitBallEntity createFromPlayer(ServerLevel level, LivingEntity owner, ItemStack stack) {
|
||||
return new ShitBallEntity(level, owner, stack);
|
||||
}
|
||||
|
||||
public static Projectile createFromDispenser(Level level, Position position, ItemStack stack,
|
||||
Direction direction) {
|
||||
return new ShitBallEntity(level, position.x(), position.y(), position.z(), stack);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public final class OuchRegistries {
|
||||
initialized = true;
|
||||
OuchItems.initialize();
|
||||
OuchWeatherTnt.initialize();
|
||||
OuchAbjectionProjectiles.initialize();
|
||||
OuchCreativeTab.initialize();
|
||||
OuchMod.LOGGER.debug("[{}] Weapon registry hub initialized.", OuchMod.DISPLAY_NAME);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"fabric-api": ">=0.154.2+26.2",
|
||||
"anotherworld": ">=0.0.0-alpha.46",
|
||||
"ambiance": ">=0.0.0-alpha.12",
|
||||
"onlyfun": ">=0.0.0-alpha.23",
|
||||
"sanctuary": ">=0.0.0-alpha.110"
|
||||
},
|
||||
"custom": {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package fr.koka99cab.sanctuary26.ouch.entity;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
|
||||
/** Executable regression test for additive and overflow-safe nausea duration. */
|
||||
public final class ShitBallContractSmoke {
|
||||
private ShitBallContractSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SharedConstants.tryDetectVersion();
|
||||
Bootstrap.bootStrap();
|
||||
require(ShitBallEntity.NAUSEA_DURATION_TICKS == 140, "historical duration changed");
|
||||
require(ShitBallEntity.extendedNauseaDuration(0) == 140, "first hit duration is wrong");
|
||||
require(ShitBallEntity.extendedNauseaDuration(60) == 200,
|
||||
"a new impact did not add seven seconds");
|
||||
require(ShitBallEntity.extendedNauseaDuration(-10) == 140,
|
||||
"negative input was not normalized");
|
||||
require(ShitBallEntity.extendedNauseaDuration(Integer.MAX_VALUE) == Integer.MAX_VALUE,
|
||||
"duration overflow was not saturated");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user