Compare commits

...
Author SHA1 Message Date
koka 14980c993f Merge branch 'main' into feature/abjection-keybind
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	CHANGELOG.md
#	build.gradle
2026-08-29 18:49:57 +02:00
koka e17433f28f Merge pull request 'feat(ouch): restaurer le projectile Abjection' (#77) from feature/abjection-projectile into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #77
2026-08-29 16:41:13 +00:00
koka 0833d4e44f Merge branch 'main' into feature/abjection-projectile
Verification 26.2 / check (pull_request) Canceled after 0s
# Conflicts:
#	CHANGELOG.md
#	build.gradle
2026-08-29 18:40:16 +02:00
koka 07b2eba5c5 Merge pull request 'feat(onlyfun): intégrer les actions Abjection' (#76) from feature/abjection-actions into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: #76
2026-08-29 16:36:59 +00:00
koka 91b4def490 feat(onlyfun): ajouter la touche Abjection 2026-08-29 15:21:38 +02:00
koka 122ff939aa feat(ouch): restaurer le projectile Abjection 2026-08-29 15:17:37 +02:00
24 changed files with 472 additions and 22 deletions
+7 -1
View File
@@ -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`** et **Only Fun `0.0.0-alpha.22`**. 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 laffichage 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.
@@ -20,6 +20,12 @@ Modules modifiés : **It's Alive `0.0.0-alpha.36`**, **Sanctuary `0.0.0-alpha.11
- importe le cœur GPL historique dAbjection 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 dOnly Fun.
- rend `abjection:shit_ball` lançable à la main et par dispenser grâce à une fabrique publique installée par Ouch, sans réenregistrer lobjet possédé par Only Fun ;
- restaure le projectile historique de 0,25 bloc, sa consommation unique hors créatif, son rendu dobjet, 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 dOuch et dOnly 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 dun même tick et revalide côté serveur la connexion, la vie et le mode spectateur avant de réutiliser exactement laction `/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`
+36 -10
View File
@@ -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.22"
|| 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")
}
}
@@ -3644,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.21, including the historical Abjection server actions."
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"))
@@ -3740,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.22"
|| 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")')
@@ -3754,6 +3763,16 @@ project(":onlyfun") {
|| !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"
@@ -3768,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")
@@ -3915,6 +3940,7 @@ project(":onlyfun") {
tasks.named("check") {
dependsOn tasks.named("abjectionActionContractSmoke")
dependsOn tasks.named("shitActionRequestSmoke")
dependsOn tasks.named("onlyFunGeneratorSmoke")
dependsOn tasks.named("verifyOnlyFun")
}
@@ -4181,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.22"
|| 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"
@@ -4275,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.22"
|| 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"
@@ -4523,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"
@@ -5544,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.22"
|| 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"
@@ -5652,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.22"
|| 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"
@@ -5691,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
View File
@@ -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.22
onlyfun_version=0.0.0-alpha.24
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.119
sanctuary_lifecycle=active
+17
View File
@@ -20,3 +20,20 @@ ou à la déconnexion.
Le bloc `abjection:shit_cube` reprend les propriétés de la boue, lapparence 16×16, la recette
neuf boules, le butin et loutil 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 lobjet et le point dinstallation `AbjectionProjectileApi`. Ouch, qui dépend
explicitement dOnly Fun sans cycle inverse, enregistre le type dentité 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 à limpact et
ajoute sept secondes à la nausée déjà présente sans dupliquer dobjet.
## 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.
@@ -18,7 +18,7 @@ public final class AbjectionRegistries {
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 Item(properties.stacksTo(16).useItemDescriptionPrefix()));
properties -> new ShitBallItem(properties.stacksTo(16).useItemDescriptionPrefix()));
public static final Item SHIT_CUBE_ITEM = registerBlockItem("shit_cube", SHIT_CUBE);
private AbjectionRegistries() {
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
@@ -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);
}
@@ -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;
}
}
@@ -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": "Испражниться"
}
+1 -1
View File
@@ -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": {
@@ -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);
}
}
+45 -3
View File
@@ -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);
}
}
@@ -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);
}
+1
View File
@@ -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);
}
}