diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb0caf..300ced2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`**, **Ambiance `0.0.0-alpha.15`**, **Only Fun `0.0.0-alpha.23`** et **Ouch `0.0.0-alpha.10`**. 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. @@ -23,6 +23,9 @@ Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.11 - 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` diff --git a/build.gradle b/build.gradle index ee6139b..c3790c1 100644 --- a/build.gradle +++ b/build.gradle @@ -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.23" + || 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"] @@ -3645,9 +3645,17 @@ project(":onlyfun") { 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.22, including its public Abjection projectile contract." + 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")) @@ -3741,10 +3749,10 @@ project(":onlyfun") { 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.23" + || project.version.toString() != "0.0.0-alpha.24" || manifest.depends?.sanctuary != "*" || manifest.provides != ["abjection"] - || manifest.custom.sanctuary26.network_protocol != 2 + || 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")') @@ -3758,6 +3766,13 @@ project(":onlyfun") { || !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" @@ -3772,6 +3787,12 @@ project(":onlyfun") { 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") @@ -3919,6 +3940,7 @@ project(":onlyfun") { tasks.named("check") { dependsOn tasks.named("abjectionActionContractSmoke") + dependsOn tasks.named("shitActionRequestSmoke") dependsOn tasks.named("onlyFunGeneratorSmoke") dependsOn tasks.named("verifyOnlyFun") } @@ -4185,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.23" + || 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" @@ -4279,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.23" + || 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" @@ -5548,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.23" + || 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" @@ -5656,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 != "0.0.0-alpha.23" + || 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" @@ -5695,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 diff --git a/gradle.properties b/gradle.properties index 6809971..71091b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ 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.23 +onlyfun_version=0.0.0-alpha.24 onlyfun_lifecycle=active sanctuary_version=0.0.0-alpha.119 sanctuary_lifecycle=active diff --git a/onlyfun/ABJECTION.md b/onlyfun/ABJECTION.md index bd72f50..f83e5b9 100644 --- a/onlyfun/ABJECTION.md +++ b/onlyfun/ABJECTION.md @@ -28,3 +28,12 @@ explicitement d’Only Fun sans cycle inverse, enregistre le type d’entité so `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. diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitActionRequestHandler.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitActionRequestHandler.java new file mode 100644 index 0000000..e2dbbc8 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitActionRequestHandler.java @@ -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 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); + } +} diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/OnlyFunClient.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/OnlyFunClient.java index 823b791..a995507 100644 --- a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/OnlyFunClient.java +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/client/OnlyFunClient.java @@ -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; diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/OnlyFunNetworking.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/OnlyFunNetworking.java index cca8c50..a1e0507 100644 --- a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/OnlyFunNetworking.java +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/OnlyFunNetworking.java @@ -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); } diff --git a/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/ShitActionRequestPayload.java b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/ShitActionRequestPayload.java new file mode 100644 index 0000000..4a4fd70 --- /dev/null +++ b/onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/network/ShitActionRequestPayload.java @@ -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 ID = + new Type<>(OnlyFunMod.id("c2s/shit_action")); + public static final StreamCodec CODEC = + CustomPacketPayload.codec((payload, buffer) -> {}, buffer -> new ShitActionRequestPayload()); + + @Override + public Type type() { + return ID; + } +} diff --git a/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json b/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json index 641177d..c507c3c 100644 --- a/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json +++ b/onlyfun/src/main/resources/assets/onlyfun/lang/en_us.json @@ -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" } diff --git a/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json b/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json index 4b3bc27..e06e14b 100644 --- a/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json +++ b/onlyfun/src/main/resources/assets/onlyfun/lang/fr_fr.json @@ -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" } diff --git a/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json b/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json index f8589f3..812f033 100644 --- a/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json +++ b/onlyfun/src/main/resources/assets/onlyfun/lang/ru_ru.json @@ -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": "Испражниться" } diff --git a/onlyfun/src/main/resources/fabric.mod.json b/onlyfun/src/main/resources/fabric.mod.json index 4701a7a..f3e3531 100644 --- a/onlyfun/src/main/resources/fabric.mod.json +++ b/onlyfun/src/main/resources/fabric.mod.json @@ -34,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": { diff --git a/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitActionRequestSmoke.java b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitActionRequestSmoke.java new file mode 100644 index 0000000..d61239e --- /dev/null +++ b/onlyfun/src/test/java/fr/koka99cab/sanctuary26/onlyfun/abjection/ShitActionRequestSmoke.java @@ -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); + } +}