From 060aec33ac5341ed298581d6515fd305af904e22 Mon Sep 17 00:00:00 2001 From: koka Date: Wed, 26 Aug 2026 02:01:17 +0200 Subject: [PATCH 1/5] fix(itsalive): fiabiliser les postes culinaires --- itsalive/FOOD_WIKI.md | 19 +++-- itsalive/build.gradle | 65 +++++++++++++++++- .../culinary/block/CookingCauldronBlock.java | 13 ++++ .../block/FermentationBarrelBlock.java | 12 ++++ .../culinary/block/FryingPanBlock.java | 13 ++++ .../culinary/block/KitchenOvenBlock.java | 12 ++++ .../AbstractCulinaryContainerBlockEntity.java | 1 + .../AbstractTimedCulinaryBlockEntity.java | 27 +++++++- .../entity/CookingCauldronBlockEntity.java | 61 +++++++++++++++- .../entity/FermentationBarrelBlockEntity.java | 14 ++++ .../itsalive/machine/FoodPressBlock.java | 21 +++--- .../machine/FoodPressBlockEntity.java | 31 +++------ .../itsalive/machine/RedstoneMillBlock.java | 5 +- .../machine/RedstoneMillBlockEntity.java | 15 ++++ .../assets/itsalive/textures/item/wine.png | Bin 357 -> 561 bytes .../itsalive/recipe/press_olive_block.json | 2 +- .../itsalive/recipe/press_olive_paste.json | 7 -- 17 files changed, 264 insertions(+), 54 deletions(-) delete mode 100644 itsalive/src/main/resources/data/itsalive/recipe/press_olive_paste.json diff --git a/itsalive/FOOD_WIKI.md b/itsalive/FOOD_WIKI.md index 6ef6b44..6798c3a 100644 --- a/itsalive/FOOD_WIKI.md +++ b/itsalive/FOOD_WIKI.md @@ -1,6 +1,6 @@ -# Wiki cuisine — Sanctuary 26.2 alpha.194 +# Wiki cuisine — Sanctuary 26.2 alpha.196 -Ce guide décrit le système de nourriture réellement disponible dans **Sanctuary 26.2.0-alpha.194**, fourni par **It's Alive 0.0.0-alpha.27**. Il couvre les cultures, la découverte climatique, les machines, les ingrédients transformés et les 64 pages du livre de recettes. +Ce guide décrit le système de nourriture réellement disponible dans **Sanctuary 26.2.0-alpha.196**, fourni par **It's Alive 0.0.0-alpha.28**. Il couvre les cultures, la découverte climatique, les machines, les ingrédients transformés et les 64 pages du livre de recettes. ## Démarrage rapide @@ -134,13 +134,14 @@ La presse est un réservoir en verre cerclé de métal. |---|---| | Bloc de tomates | 9 pulpes de tomate | | Bloc de raisins | 9 moûts de raisin | -| Bloc d'olives | 9 pâtes d'olive | -| Pâte d'olive insérée à la main | Huile d'olive | +| Bloc d'olives | 9 huiles d'olive | -La pulpe de tomate et la pâte d'olive se récupèrent avec un **bol**. Le moût de raisin et l'huile d'olive se récupèrent avec une **fiole en verre**. +La pulpe de tomate se récupère avec un **bol**. Le moût de raisin et l'huile d'olive se récupèrent avec une **fiole en verre**. La pâte d'olive historique reste enregistrée uniquement pour relire les anciennes sauvegardes ; la presse n'en produit plus et ne demande plus de second pressage manuel. Automatisation : un hopper au-dessus insère uniquement le récipient adapté au contenu visible ; un inventaire ou hopper sous la presse récupère les portions emballées. Sans inventaire dessous, les bols et fioles ne sont pas avalés en stockage caché. +Casser une presse détruit désormais ses ingrédients, son liquide et ses récipients en attente : seul le bloc de presse tombe. Ce comportement empêche de dupliquer des bols ou des fioles en interrompant une extraction. + ### Moulin redstone Recette : @@ -190,6 +191,8 @@ La marmite n'a pas de recette séparée : utilise un **chaudron vanilla rempli d L'eau initiale compte comme une bouteille d'eau pour les recettes qui en demandent. Une bouteille d'eau inutilisée en trop est tolérée. Quand la marmite est entièrement vidée, elle redevient un chaudron vanilla vide. Le café colore la mixture en brun. +Quand une recette consomme un contenant, par exemple un seau de lait, le contenant vide est projeté au-dessus de la marmite. Pendant cette courte fontaine, la marmite refuse tout nouvel ingrédient afin de ne pas avaler aussitôt l'objet rendu. + ### Four de cuisine Recette : un four vanilla au centre, entouré de huit blocs de briques. @@ -216,6 +219,10 @@ Planche Dalle bois Planche - Un clic droit récupère le produit fini ; sinon il retire le dernier ingrédient. - Hopper au-dessus ou sur les côtés : ingrédients ; hopper dessous : résultat. +### Redstone et comparateurs + +Les six postes — presse, moulin, poêle, marmite, four et tonneau — fournissent un signal de comparateur sémantique. Le signal vaut 0 quand le poste est vide, varie de 1 à 14 selon son remplissage ou son avancement, puis atteint 15 lorsqu'un résultat est prêt à extraire. Les changements d'inventaire et de progression actualisent automatiquement les comparateurs voisins. + ## Ingrédients de base connus Ces transformations sont disponibles sans page secrète. @@ -403,4 +410,4 @@ Les recettes sont dans `data/itsalive/recipe/`. Les familles utilisent les préf - `itsalive:kitchen_oven` - `itsalive:fermentation_barrel` -Ce wiki reflète les données et comportements présents dans le workspace alpha.194 ; les recettes JSON restent la source de vérité si un datapack les surcharge. +Ce wiki reflète les données et comportements présents dans le workspace alpha.196 ; les recettes JSON restent la source de vérité si un datapack les surcharge. diff --git a/itsalive/build.gradle b/itsalive/build.gradle index 4fefd04..43521ae 100644 --- a/itsalive/build.gradle +++ b/itsalive/build.gradle @@ -396,7 +396,7 @@ tasks.register("verifyItsAlive") { def climateSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text def pineappleSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/PineappleItem.java").text def worldgenSource = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ItsAliveAgricultureWorldGeneration.java").text - if (project.version.toString() != "0.0.0-alpha.27" + if (project.version.toString() != "0.0.0-alpha.28" || climateMigration.target?.pack_version != "26.2.0-alpha.157" || climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24" || !climateSource.contains("PlayerBlockBreakEvents.AFTER.register") @@ -1069,6 +1069,69 @@ tasks.named("sourcesJar") { dependsOn tasks.named("generateAgricultureResources") } +tasks.register("verifyCulinaryReliability") { + group = "verification" + description = "Checks the alpha.28 press, olive oil, comparator, cauldron and wine contracts." + inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + inputs.file(file("src/main/resources/data/itsalive/recipe/press_olive_block.json")) + inputs.file(file("src/main/resources/assets/itsalive/textures/item/wine.png")) + inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary")) + inputs.files(fileTree("src/main/java/fr/koka99cab/sanctuary26/itsalive/machine")) + doLast { + def migration = new JsonSlurper().parse(rootProject.file( + "pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + def oliveRecipe = new JsonSlurper().parse( + file("src/main/resources/data/itsalive/recipe/press_olive_block.json")) + def press = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java").text + def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text + def container = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java").text + def comparatorBlocks = [ + "machine/FoodPressBlock.java", + "machine/RedstoneMillBlock.java", + "culinary/block/FryingPanBlock.java", + "culinary/block/CookingCauldronBlock.java", + "culinary/block/KitchenOvenBlock.java", + "culinary/block/FermentationBarrelBlock.java" + ] + def incompleteComparators = comparatorBlocks.findAll { relativePath -> + def source = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/${relativePath}").text + !source.contains("hasAnalogOutputSignal") || !source.contains("getAnalogOutputSignal") + } + def wine = ImageIO.read(file("src/main/resources/assets/itsalive/textures/item/wine.png")) + int bordeauxPixels = 0 + for (int y = 0; y < wine.height; y++) { + for (int x = 0; x < wine.width; x++) { + int argb = wine.getRGB(x, y) + int alpha = (argb >>> 24) & 0xff + int red = (argb >>> 16) & 0xff + int green = (argb >>> 8) & 0xff + int blue = argb & 0xff + if (alpha > 0 && red >= 90 && red > green * 1.8D && red > blue * 1.15D) { + bordeauxPixels++ + } + } + } + if (project.version.toString() != "0.0.0-alpha.28" + || migration.target?.modules?.itsalive != "0.0.0-alpha.28" + || migration.culinary_fixes?.food_press_contents_destroyed_on_break != true + || migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6 + || oliveRecipe.ingredient != "itsalive:olive_block" + || oliveRecipe.result?.id != "itsalive:olive_oil" + || oliveRecipe.result?.count != 9 + || file("src/main/resources/data/itsalive/recipe/press_olive_paste.json").exists() + || press.contains("Containers.dropContents(level, pos, press)") + || !incompleteComparators.isEmpty() + || !container.contains("updateNeighbourForOutputSignal") + || !cauldron.contains("REMAINDER_EJECTION_LOCK_TICKS = 20") + || !cauldron.contains("level.addFreshEntity(ejected)") + || !cauldron.contains("inputLockTicks <= 0") + || wine.width != 16 || wine.height != 16 || bordeauxPixels < 24) { + throw new GradleException("It's Alive alpha.28 culinary reliability contract is incomplete: comparators=${incompleteComparators}, bordeauxPixels=${bordeauxPixels}") + } + } +} + tasks.named("check") { dependsOn tasks.named("verifyItsAlive") + dependsOn tasks.named("verifyCulinaryReliability") } diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/CookingCauldronBlock.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/CookingCauldronBlock.java index 93971f1..a05ad78 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/CookingCauldronBlock.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/CookingCauldronBlock.java @@ -7,6 +7,7 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryExperience; import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing; import java.util.function.Supplier; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Containers; import net.minecraft.world.InteractionHand; @@ -206,6 +207,18 @@ public final class CookingCauldronBlock extends BaseEntityBlock { Containers.updateNeighboursAfterDestroy(state, level, pos); } + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof CookingCauldronBlockEntity cauldron + ? cauldron.analogOutputSignal() + : 0; + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java index 8ca16ad..1ed2ddf 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java @@ -139,6 +139,18 @@ public final class FermentationBarrelBlock extends BaseEntityBlock { Containers.updateNeighboursAfterDestroy(state, level, pos); } + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof FermentationBarrelBlockEntity barrel + ? barrel.analogOutputSignal() + : 0; + } + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { return defaultBlockState().setValue(FACING, context.getNearestLookingDirection().getOpposite()); diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FryingPanBlock.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FryingPanBlock.java index 0d7bac7..b56f8c9 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FryingPanBlock.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FryingPanBlock.java @@ -5,6 +5,7 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryExperience; import fr.koka99cab.sanctuary26.itsalive.culinary.block.entity.FryingPanBlockEntity; import java.util.function.Supplier; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Containers; import net.minecraft.world.InteractionHand; @@ -148,6 +149,18 @@ public final class FryingPanBlock extends BaseEntityBlock { Containers.updateNeighboursAfterDestroy(state, level, pos); } + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof FryingPanBlockEntity pan + ? pan.analogOutputSignal() + : 0; + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/KitchenOvenBlock.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/KitchenOvenBlock.java index ad1d340..013ef77 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/KitchenOvenBlock.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/KitchenOvenBlock.java @@ -112,6 +112,18 @@ public final class KitchenOvenBlock extends BaseEntityBlock { Containers.updateNeighboursAfterDestroy(state, level, pos); } + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof KitchenOvenBlockEntity oven + ? oven.analogOutputSignal() + : 0; + } + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java index 7b9b9e9..51fea95 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java @@ -219,6 +219,7 @@ public abstract class AbstractCulinaryContainerBlockEntity extends BlockEntity setChanged(); if (level != null) { level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), Block.UPDATE_CLIENTS); + level.updateNeighbourForOutputSignal(worldPosition, getBlockState().getBlock()); } } diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java index bf71adb..bcb85fa 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java @@ -77,7 +77,7 @@ public abstract class AbstractTimedCulinaryBlockEntity extends AbstractCulinaryC resetProgress(); return false; } - consumeSlots(slots, 0); + consumeRecipeInputs(level, slots); putResult(outputSlot, result); progress = 0; activeDuration = 0; @@ -86,6 +86,31 @@ public abstract class AbstractTimedCulinaryBlockEntity extends AbstractCulinaryC return true; } + /** Hook for open appliances that need to eject crafting remainders. */ + protected void consumeRecipeInputs(ServerLevel level, int[] slots) { + consumeSlots(slots, 0); + } + + /** Semantic comparator contract: 0 empty, 1-14 loaded/working, 15 result ready. */ + public int analogOutputSignal() { + if (!getItem(outputSlot).isEmpty()) { + return 15; + } + int occupied = 0; + for (int slot = 0; slot < inputSlots; slot++) { + if (!getItem(slot).isEmpty()) { + occupied++; + } + } + if (occupied == 0) { + return 0; + } + double loadedFraction = (double) occupied / inputSlots; + double workingFraction = progressFraction(); + return Math.min(14, Math.max(1, + (int) Math.ceil(Math.max(loadedFraction, workingFraction) * 14.0D))); + } + protected boolean hasProcessableRecipe(ServerLevel level) { return CulinaryRecipeTypes.find(level, new CulinaryRecipeInput(appliance, inputView(0, inputSlots))) .map(holder -> canFitResult(outputSlot, holder.value().result())) diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java index 9d53fbe..12a814d 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java @@ -4,6 +4,8 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryAppliance; import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryHeat; import fr.koka99cab.sanctuary26.itsalive.culinary.block.CookingCauldronBlock; import fr.koka99cab.sanctuary26.itsalive.registry.ItsAliveFoodProcessing; +import java.util.ArrayList; +import java.util.List; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.particles.ParticleTypes; @@ -12,6 +14,7 @@ import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.ItemStackTemplate; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; @@ -26,11 +29,13 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock public static final int INPUT_SLOTS = 6; public static final int OUTPUT_SLOT = 6; public static final int SIZE = 7; + private static final int REMAINDER_EJECTION_LOCK_TICKS = 20; private static final int[] INPUTS = {0, 1, 2, 3, 4, 5}; private static final int[] OUTPUT = {OUTPUT_SLOT}; private long assemblyStartedAtGameTime = -1L; private long lastIngredientChangedAtGameTime = -1L; private boolean assemblyComplete; + private int inputLockTicks; public CookingCauldronBlockEntity(BlockPos pos, BlockState state) { this(((CookingCauldronBlock) state.getBlock()).blockEntityType(), pos, state); @@ -43,6 +48,9 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock public static void serverTick( ServerLevel level, BlockPos pos, BlockState state, CookingCauldronBlockEntity cauldron ) { + if (cauldron.inputLockTicks > 0) { + cauldron.inputLockTicks--; + } cauldron.absorbDroppedItems(level, pos); boolean heated = CulinaryHeat.hasHeatBelow(level, pos); cauldron.tickAssembly(level, heated); @@ -65,12 +73,58 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock } public int insertIngredient(ItemStack source, int amount) { - if (hasIngredients() && assemblyWindowElapsed()) { + if (!canAcceptIngredients() || hasIngredients() && assemblyWindowElapsed()) { return 0; } return insert(source, amount, 0, INPUT_SLOTS, stack -> true); } + public boolean canAcceptIngredients() { + return inputLockTicks <= 0 && !hasFinishedProduct(); + } + + @Override + protected void consumeRecipeInputs(ServerLevel level, int[] slots) { + List remainders = new ArrayList<>(); + for (int relativeSlot : slots) { + ItemStack ingredient = items.get(relativeSlot); + if (ingredient.isEmpty()) { + continue; + } + ItemStackTemplate remainder = ingredient.getItem().getCraftingRemainder(); + ingredient.shrink(1); + if (ingredient.isEmpty()) { + items.set(relativeSlot, ItemStack.EMPTY); + } + if (remainder != null) { + remainders.add(remainder.create()); + } + } + if (remainders.isEmpty()) { + return; + } + inputLockTicks = REMAINDER_EJECTION_LOCK_TICKS; + for (int index = 0; index < remainders.size(); index++) { + double angle = Math.PI * 2.0D * index / remainders.size() + + level.getRandom().nextDouble() * 0.35D; + double horizontalSpeed = 0.07D + level.getRandom().nextDouble() * 0.04D; + ItemEntity ejected = new ItemEntity( + level, + worldPosition.getX() + 0.5D, + worldPosition.getY() + 1.05D, + worldPosition.getZ() + 0.5D, + remainders.get(index) + ); + ejected.setDeltaMovement( + Math.cos(angle) * horizontalSpeed, + 0.34D + level.getRandom().nextDouble() * 0.08D, + Math.sin(angle) * horizontalSpeed + ); + ejected.setPickUpDelay(REMAINDER_EJECTION_LOCK_TICKS); + level.addFreshEntity(ejected); + } + } + private void tickAssembly(ServerLevel level, boolean heated) { if (hasFinishedProduct() || !hasIngredients()) { resetAssembly(); @@ -204,6 +258,9 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock } private void absorbDroppedItems(ServerLevel level, BlockPos pos) { + if (!canAcceptIngredients()) { + return; + } AABB bowl = new AABB(pos).inflate(0.05).expandTowards(0.0, 0.9, 0.0); for (ItemEntity entity : level.getEntitiesOfClass(ItemEntity.class, bowl, entity -> !entity.getItem().isEmpty())) { @@ -275,7 +332,7 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock @Override public boolean canPlaceItem(int slot, ItemStack stack) { - return slot >= 0 && slot < INPUT_SLOTS; + return canAcceptIngredients() && slot >= 0 && slot < INPUT_SLOTS; } @Override diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java index 3f3451a..3b2e7d0 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java @@ -140,6 +140,20 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain return !getItem(OUTPUT_SLOT).isEmpty(); } + public int analogOutputSignal() { + if (hasFinishedProduct()) { + return 15; + } + int occupied = 0; + for (int slot = 0; slot < INPUT_SLOTS; slot++) { + if (!getItem(slot).isEmpty()) { + occupied++; + } + } + return occupied == 0 ? 0 : Math.min(14, Math.max(1, + (int) Math.ceil((double) occupied / INPUT_SLOTS * 14.0D))); + } + public boolean hasIngredients() { for (int slot = 0; slot < INPUT_SLOTS; slot++) { if (!getItem(slot).isEmpty()) { diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java index a074415..576996c 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java @@ -9,7 +9,6 @@ import net.minecraft.world.Containers; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; @@ -96,14 +95,7 @@ public final class FoodPressBlock extends BaseEntityBlock { if (press.collectOne(stack, player)) { return InteractionResult.SUCCESS_SERVER; } - int inserted = press.insertSecondaryInput(stack); - if (inserted <= 0) { - return InteractionResult.PASS; - } - if (!player.hasInfiniteMaterials()) { - stack.shrink(inserted); - } - return InteractionResult.SUCCESS_SERVER; + return InteractionResult.PASS; } @Override @@ -129,9 +121,10 @@ public final class FoodPressBlock extends BaseEntityBlock { BlockPos pos, boolean movedByPiston ) { - if (!movedByPiston && level.getBlockEntity(pos) instanceof FoodPressBlockEntity press) { - Containers.dropContents(level, pos, press); - } + // Loose portions and packaging slots are implementation details, not real + // item stacks that may exist without their bowl or bottle. Destroy them with + // the reservoir so breaking a partly emptied press can never mint unpackaged + // food. The block's loot table still returns the press itself. Containers.updateNeighboursAfterDestroy(state, level, pos); } @@ -142,7 +135,9 @@ public final class FoodPressBlock extends BaseEntityBlock { @Override protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { - return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(level.getBlockEntity(pos)); + return level.getBlockEntity(pos) instanceof FoodPressBlockEntity press + ? press.analogOutputSignal() + : 0; } @Override diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java index 5aeb637..3e18329 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java @@ -73,27 +73,7 @@ public final class FoodPressBlockEntity extends BlockEntity implements WorldlyCo public boolean canInteractWith(ItemStack held) { ItemStack output = items.get(OUTPUT_SLOT); - return !output.isEmpty() && held.is(requiredContainer(output)) - || output.isEmpty() && held.is(ItsAliveFoodProcessing.OLIVE_PASTE); - } - - /** Olive paste is the only loose ingredient; whole produce must be compressed physically. */ - public int insertSecondaryInput(ItemStack offered) { - if (!offered.is(ItsAliveFoodProcessing.OLIVE_PASTE) - || !(level instanceof ServerLevel serverLevel) - || !items.get(OUTPUT_SLOT).isEmpty()) { - return 0; - } - RecipeHolder holder = recipeCheck - .getRecipeFor(new SingleRecipeInput(offered), serverLevel) - .orElse(null); - if (holder == null) { - return 0; - } - items.set(OUTPUT_SLOT, holder.value().assemble(new SingleRecipeInput(offered))); - setChangedAndNotify(); - syncVisualContents(); - return 1; + return !output.isEmpty() && held.is(requiredContainer(output)); } public boolean collectOne(ItemStack container, Player player) { @@ -302,6 +282,15 @@ public final class FoodPressBlockEntity extends BlockEntity implements WorldlyCo } } + /** Comparator level follows the nine visible reservoir portions. */ + public int analogOutputSignal() { + if (!items.get(PACKAGED_OUTPUT_SLOT).isEmpty()) { + return 15; + } + int portions = Math.min(9, items.get(OUTPUT_SLOT).getCount()); + return portions <= 0 ? 0 : Math.min(15, (int) Math.ceil(portions * 15.0D / 9.0D)); + } + @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlock.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlock.java index d0d98ce..49a5ee6 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlock.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlock.java @@ -12,7 +12,6 @@ import net.minecraft.world.Containers; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.context.BlockPlaceContext; @@ -148,7 +147,9 @@ public final class RedstoneMillBlock extends BaseEntityBlock { @Override protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { - return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(level.getBlockEntity(pos)); + return level.getBlockEntity(pos) instanceof RedstoneMillBlockEntity mill + ? mill.analogOutputSignal() + : 0; } @Override diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java index 77e8605..e2bd1c7 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java @@ -279,6 +279,21 @@ public final class RedstoneMillBlockEntity extends BlockEntity implements Worldl } } + public int analogOutputSignal() { + if (!items.get(OUTPUT_SLOT).isEmpty()) { + return 15; + } + boolean hasInput = !items.get(INPUT_SLOT).isEmpty(); + boolean hasFuel = !items.get(FUEL_SLOT).isEmpty() || burnTime > 0; + if (!hasInput && !hasFuel) { + return 0; + } + double loadedFraction = (hasInput ? 0.5D : 0.0D) + (hasFuel ? 0.5D : 0.0D); + double workingFraction = totalProcessTime <= 0 ? 0.0D : (double) progress / totalProcessTime; + return Math.min(14, Math.max(1, + (int) Math.ceil(Math.max(loadedFraction, workingFraction) * 14.0D))); + } + @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); diff --git a/itsalive/src/main/resources/assets/itsalive/textures/item/wine.png b/itsalive/src/main/resources/assets/itsalive/textures/item/wine.png index 2d957332cd56685bd9816d0d7cd5b02d5b44a744..3d429c3d571f0776245587242053c1efaf888816 100644 GIT binary patch literal 561 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`3dtTpz6=aiY77hwEes65fI|H(?D8gRi>Fdh=ibGOdQ?v4W z@gty+YKdz^NlIc#s#S7PDv)9@GB7gJHL%b%G7T{@wlXoaGB(mSFt9Q(ILvm+7)3*F zeoAIqC2kFyqKo>08Z_WGlw{_n7MCRE7GUZzF@#uhFWKP=0|TR=r;B5V#p&b(iHrjs z{-3>(3`8M|qb_hwGIsgP8 zzWlXrE?cGaK-rH&uOk6M*1?*hCPO zQ1S7r@!{@6D>d{EN^%=0oH=<`L05ytykUX(yZygTfem&3*T(|_LU;Cn>Hh9$iF2vwvI~6FJ#&(m-(^w~*lEp?q6O*QU-i=}LM6H%*3V%(z~d->SgcosnIYUa_t{;s SnA^YrW$<+Mb6Mw<&;$TjP|OPe delta 330 zcmV-Q0k!_I1myydB!2;OQb$4nuFf3k0003WNklKj6PL68C9uElPm<#sjODO=YbXU^~7X-kyf<=YQ-1OLJ3^`Y|1#)fwQG z_W*cvKbR@k1s_91E}=Uq0@fV5BnStg1GJ5@0*Nyxypg zJzSyr(!lk4q$_7iNMx!%%@=DX0Mls$0N3mBUR>sQdp`7Ox9}w)1V$i_@%07*qoM6N<$f(4eA)&Kwi diff --git a/itsalive/src/main/resources/data/itsalive/recipe/press_olive_block.json b/itsalive/src/main/resources/data/itsalive/recipe/press_olive_block.json index e53447b..9acf559 100644 --- a/itsalive/src/main/resources/data/itsalive/recipe/press_olive_block.json +++ b/itsalive/src/main/resources/data/itsalive/recipe/press_olive_block.json @@ -2,7 +2,7 @@ "type": "itsalive:food_pressing", "ingredient": "itsalive:olive_block", "result": { - "id": "itsalive:olive_paste", + "id": "itsalive:olive_oil", "count": 9 } } diff --git a/itsalive/src/main/resources/data/itsalive/recipe/press_olive_paste.json b/itsalive/src/main/resources/data/itsalive/recipe/press_olive_paste.json deleted file mode 100644 index dd1940d..0000000 --- a/itsalive/src/main/resources/data/itsalive/recipe/press_olive_paste.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "itsalive:food_pressing", - "ingredient": "itsalive:olive_paste", - "result": { - "id": "itsalive:olive_oil" - } -} -- 2.54.0 From 1c4b65f6434fab48d725cc33ddfc7b143cacdd06 Mon Sep 17 00:00:00 2001 From: koka Date: Wed, 26 Aug 2026 02:01:26 +0200 Subject: [PATCH 2/5] fix(sanctuary): orienter les boites aux lettres --- sanctuary/build.gradle | 36 +++++++++++++++++-- .../sanctuary/shop/DeliveryBoxBlock.java | 26 +++++++++++++- .../sanctuary/blockstates/delivery_box.json | 5 ++- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index 2141404..1f97dc8 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -2878,7 +2878,7 @@ tasks.register("verifyProgressivePauseMenu") { "pack/migrations/26.2.0-alpha.190-progressive-pause-menu-to-alpha.191.json")) def pauseMenu = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/mixin/client/PauseScreenMixin.java").text def costs = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/progression/SanctuaryProgressionCosts.java").text - if (project.version.toString() != "0.0.0-alpha.111" + if (project.version.toString() != "0.0.0-alpha.112" || migration.target?.pack_version != "26.2.0-alpha.191" || migration.target?.modules?.sanctuary != "0.0.0-alpha.109" || migration.pause_menu?.progression_full_width != true @@ -2916,7 +2916,7 @@ tasks.register("verifyShopPricingLayout") { "pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json")) def screen = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/client/SanctuaryShopScreen.java").text def progression = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopProgression.java").text - if (project.version.toString() != "0.0.0-alpha.111" + if (project.version.toString() != "0.0.0-alpha.112" || migration.target?.modules?.sanctuary != "0.0.0-alpha.110" || migration.shop_layout?.locked_offer_buttons != 6 || migration.shop_layout?.button_slots != [4, 5, 6, 7, 8, 9] @@ -2947,7 +2947,7 @@ tasks.register("verifyShopCsvBossGates") { "pack/migrations/26.2.0-alpha.194-shop-csv-boss-gates-to-alpha.195.json")) def provider = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopCatalogProvider.java").text def catalog = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/ShopOfferCatalog.java").text - if (project.version.toString() != "0.0.0-alpha.111" + if (project.version.toString() != "0.0.0-alpha.112" || migration.target?.pack_version != "26.2.0-alpha.195" || migration.target?.modules?.sanctuary != "0.0.0-alpha.111" || migration.compatibility?.sanctuary_data_version != 11 @@ -2965,7 +2965,37 @@ tasks.named("verifySanctuary") { dependsOn tasks.named("verifyShopCsvBossGates") } +tasks.register("verifyDeliveryBoxOrientation") { + group = "verification" + description = "Checks the alpha.112 four-way horizontal mailbox placement contract." + inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java")) + inputs.file(file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json")) + doLast { + def migration = new JsonSlurper().parse(rootProject.file( + "pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + def source = file("src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java").text + def blockstate = new JsonSlurper().parse( + file("src/main/resources/assets/sanctuary/blockstates/delivery_box.json")) + def expectedVariants = ["facing=north", "facing=east", "facing=south", "facing=west"].toSet() + if (project.version.toString() != "0.0.0-alpha.112" + || migration.target?.modules?.sanctuary != "0.0.0-alpha.112" + || migration.mailbox?.id != "sanctuary:delivery_box" + || migration.mailbox?.horizontal_facing != true + || !source.contains("BlockStateProperties.HORIZONTAL_FACING") + || !source.contains("getStateForPlacement(BlockPlaceContext context)") + || !source.contains("builder.add(FACING)") + || blockstate.variants.keySet() != expectedVariants + || blockstate.variants["facing=east"].y != 90 + || blockstate.variants["facing=south"].y != 180 + || blockstate.variants["facing=west"].y != 270) { + throw new GradleException("Sanctuary alpha.112 delivery-box orientation contract is incomplete") + } + } +} + tasks.named("check") { dependsOn tasks.named("verifySanctuary") + dependsOn tasks.named("verifyDeliveryBoxOrientation") dependsOn tasks.named("celestialGraphSmoke") } diff --git a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java index 8f21275..6321ea0 100644 --- a/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java +++ b/sanctuary/src/main/java/fr/koka99cab/sanctuary26/sanctuary/shop/DeliveryBoxBlock.java @@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.sanctuary.shop; import com.mojang.serialization.MapCodec; import java.util.UUID; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Containers; @@ -11,19 +12,42 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Mirror; import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.Rotation; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.phys.BlockHitResult; public final class DeliveryBoxBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(DeliveryBoxBlock::new); - public DeliveryBoxBlock(Properties properties) { super(properties); } + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + public DeliveryBoxBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } @Override protected MapCodec codec() { return CODEC; } @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new DeliveryBoxBlockEntity(pos, state); } @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } + @Override public BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + @Override protected BlockState rotate(BlockState state, Rotation rotation) { + return state.setValue(FACING, rotation.rotate(state.getValue(FACING))); + } + @Override protected BlockState mirror(BlockState state, Mirror mirror) { + return rotate(state, mirror.getRotation(state.getValue(FACING))); + } + @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } @Override public void setPlacedBy(Level level, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { super.setPlacedBy(level, pos, state, placer, stack); diff --git a/sanctuary/src/main/resources/assets/sanctuary/blockstates/delivery_box.json b/sanctuary/src/main/resources/assets/sanctuary/blockstates/delivery_box.json index 3e0c750..d91aae6 100644 --- a/sanctuary/src/main/resources/assets/sanctuary/blockstates/delivery_box.json +++ b/sanctuary/src/main/resources/assets/sanctuary/blockstates/delivery_box.json @@ -1,5 +1,8 @@ { "variants": { - "": { "model": "sanctuary:block/delivery_box" } + "facing=north": { "model": "sanctuary:block/delivery_box" }, + "facing=east": { "model": "sanctuary:block/delivery_box", "y": 90 }, + "facing=south": { "model": "sanctuary:block/delivery_box", "y": 180 }, + "facing=west": { "model": "sanctuary:block/delivery_box", "y": 270 } } } -- 2.54.0 From e2e207c4c7aeedaf76bff03b92182b3017222c2d Mon Sep 17 00:00:00 2001 From: koka Date: Wed, 26 Aug 2026 02:01:26 +0200 Subject: [PATCH 3/5] fix(iliketomoveit): predire le balancement du grappin --- iliketomoveit/build.gradle | 61 +++++++- .../client/ILikeToMoveItClient.java | 10 +- .../grapple/ClientGrapplePrediction.java | 147 ++++++++++++++++++ .../grapple/GrappleController.java | 102 ++++++------ .../grapple/GrappleSwingPhysics.java | 61 ++++++++ .../network/GrappleStatePayload.java | 107 +++++++++++++ .../network/ILikeToMoveItNetworking.java | 5 +- .../src/main/resources/fabric.mod.json | 2 +- .../grapple/GrappleSwingPhysicsSmoke.java | 38 +++++ 9 files changed, 473 insertions(+), 60 deletions(-) create mode 100644 iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java create mode 100644 iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysics.java create mode 100644 iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java create mode 100644 iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysicsSmoke.java diff --git a/iliketomoveit/build.gradle b/iliketomoveit/build.gradle index 74c2c94..feed3cb 100644 --- a/iliketomoveit/build.gradle +++ b/iliketomoveit/build.gradle @@ -1,5 +1,18 @@ import groovy.json.JsonSlurper +tasks.register("grapplePhysicsSmoke", JavaExec) { + group = "verification" + description = "Checks deterministic shared grappling physics and client rope projection." + classpath = sourceSets.test.runtimeClasspath + mainClass = "fr.koka99cab.sanctuary26.iliketomoveit.grapple.GrappleSwingPhysicsSmoke" + dependsOn tasks.named("testClasses") +} + +tasks.named("test") { + // The physics contract is an executable smoke task, not a JUnit engine test. + failOnNoDiscoveredTests = false +} + tasks.register("verifyWaystones") { group = "verification" description = "Checks the dimension-local Waystone migration, direct elevator controls, persistence, networking and Atlas." @@ -60,10 +73,10 @@ tasks.register("verifyWaystones") { def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) if (manifest.custom?.sanctuary26?.data_version != 3 - || manifest.custom?.sanctuary26?.network_protocol != 4 + || manifest.custom?.sanctuary26?.network_protocol != 5 || manifest.depends?.anotherworld != ">=0.0.0-alpha.46" || manifest.depends?.sanctuary != ">=0.0.0-alpha.21") { - throw new GradleException("I Like To Move It must expose data v3, protocol 4 and its Another World alpha.46/Sanctuary alpha.21 dependencies") + throw new GradleException("I Like To Move It must expose data v3, protocol 5 and its Another World alpha.46/Sanctuary alpha.21 dependencies") } ["fr_fr", "en_us", "ru_ru"].each { language -> def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json")) @@ -97,6 +110,9 @@ tasks.register("verifyGrappling") { 'class GrapplingRodItem', 'class GrapplingHookEntity', 'class GrappleController', + 'class GrappleSwingPhysics', + 'class ClientGrapplePrediction', + 'id("s2c/grapple_state")', 'MAX_CHARGED_FLIGHT_DISTANCE = 120.0D', 'MAX_SESSION_DISTANCE = 152.0D', 'MAX_SWING_SPEED = 6.5D', @@ -158,8 +174,8 @@ tasks.register("verifyGrappling") { def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) if (!manifest.mixins?.any { it.config == "iliketomoveit.client.mixins.json" && it.environment == "client" } || manifest.custom?.sanctuary26?.data_version != 3 - || manifest.custom?.sanctuary26?.network_protocol != 4) { - throw new GradleException("Grappling must keep I Like To Move It aggregate data v3/protocol 4 and register its client mixin") + || manifest.custom?.sanctuary26?.network_protocol != 5) { + throw new GradleException("Grappling must keep I Like To Move It aggregate data v3/protocol 5 and register its client mixin") } ["fr_fr", "en_us", "ru_ru"].each { language -> def translations = new JsonSlurper().parse(file("src/main/resources/assets/iliketomoveit/lang/${language}.json")) @@ -267,7 +283,7 @@ tasks.register("verifyPortals") { } def atlasCavernsMigration = new JsonSlurper().parse(rootProject.file( "pack/migrations/26.2.0-alpha.149-atlas-caverns-to-alpha.150.json")) - if (project.version.toString() != "0.0.0-alpha.12" + if (project.version.toString() != "0.0.0-alpha.13" || atlasCavernsMigration.target?.pack_version != "26.2.0-alpha.150" || atlasCavernsMigration.target?.modules?.iliketomoveit != "0.0.0-alpha.9" || atlasCavernsMigration.portals?.active_routes != [ @@ -303,7 +319,7 @@ tasks.register("verifySilverAircraft") { def manifest = new JsonSlurper().parse(file("src/main/resources/fabric.mod.json")) def migration = 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.12" + if (project.version.toString() != "0.0.0-alpha.13" || recipe.pattern != ["PGP", "GCG", "PBP"] || recipe.key != [B: "anotherworld:silver_block", C: "anotherworld:aircraft_cockpit", G: "anotherworld:silver_gear", P: "anotherworld:silver_plate"] @@ -318,9 +334,42 @@ tasks.register("verifySilverAircraft") { } } +tasks.register("verifyGrapplePrediction") { + group = "verification" + description = "Checks the alpha.13 bounded server-authoritative grapple prediction protocol." + inputs.file(rootProject.file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysics.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java")) + doLast { + def migration = new JsonSlurper().parse(rootProject.file( + "pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + def payload = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java").text + def controller = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java").text + def prediction = file("src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java").text + if (project.version.toString() != "0.0.0-alpha.13" + || migration.target?.modules?.iliketomoveit != "0.0.0-alpha.13" + || migration.grappling?.owner_prediction_payload != "iliketomoveit:s2c/grapple_state" + || migration.compatibility?.iliketomoveit_source_network_protocol != 4 + || migration.compatibility?.iliketomoveit_target_network_protocol != 5 + || !payload.contains('id("s2c/grapple_state")') + || !payload.contains("MAX_COORDINATE = 30_000_000.0D") + || !payload.contains("MAX_VELOCITY = 12.0D") + || !controller.contains("GrappleSwingPhysics.step") + || !controller.contains("syncPrediction(player, session)") + || !prediction.contains("HARD_DIVERGENCE_SQ") + || !prediction.contains("GrappleSwingPhysics.step")) { + throw new GradleException("I Like To Move It alpha.13 grapple prediction contract is incomplete") + } + } +} + tasks.named("check") { + dependsOn tasks.named("grapplePhysicsSmoke") dependsOn tasks.named("verifyWaystones") dependsOn tasks.named("verifyGrappling") + dependsOn tasks.named("verifyGrapplePrediction") dependsOn tasks.named("verifyPortals") dependsOn tasks.named("verifySilverAircraft") } diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/ILikeToMoveItClient.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/ILikeToMoveItClient.java index b4b1514..521d72c 100644 --- a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/ILikeToMoveItClient.java +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/ILikeToMoveItClient.java @@ -3,6 +3,7 @@ package fr.koka99cab.sanctuary26.iliketomoveit.client; import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod; import fr.koka99cab.sanctuary26.iliketomoveit.client.gui.AircraftEngineScreen; import fr.koka99cab.sanctuary26.iliketomoveit.client.gui.WaystoneScreen; +import fr.koka99cab.sanctuary26.iliketomoveit.client.grapple.ClientGrapplePrediction; import fr.koka99cab.sanctuary26.iliketomoveit.client.item.GrapplingRodCastProperty; import fr.koka99cab.sanctuary26.iliketomoveit.client.model.AircraftModel; import fr.koka99cab.sanctuary26.iliketomoveit.client.model.MagicCloudModel; @@ -14,6 +15,7 @@ import fr.koka99cab.sanctuary26.iliketomoveit.client.sound.MagicCloudSoundManage import fr.koka99cab.sanctuary26.iliketomoveit.client.waystone.ClientWaystones; import fr.koka99cab.sanctuary26.iliketomoveit.entity.AircraftEntity; import fr.koka99cab.sanctuary26.iliketomoveit.network.AircraftControlPayload; +import fr.koka99cab.sanctuary26.iliketomoveit.network.GrappleStatePayload; import fr.koka99cab.sanctuary26.iliketomoveit.network.WaystoneOpenPayload; import fr.koka99cab.sanctuary26.iliketomoveit.network.WaystoneSyncPayload; import fr.koka99cab.sanctuary26.iliketomoveit.registry.ILikeToMoveItEntityTypes; @@ -57,10 +59,16 @@ public final class ILikeToMoveItClient implements ClientModInitializer { payload.elevatorFloorCount(), payload.destinations())))); ClientPlayNetworking.registerGlobalReceiver(WaystoneSyncPayload.ID, (payload, context) -> context.client().execute(() -> ClientWaystones.update(payload))); - ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ClientWaystones.clear()); + ClientPlayNetworking.registerGlobalReceiver(GrappleStatePayload.ID, (payload, context) -> + context.client().execute(() -> ClientGrapplePrediction.update(context.client(), payload))); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + ClientWaystones.clear(); + ClientGrapplePrediction.clear(); + }); SanctuaryAtlasMarkers.register(ILikeToMoveItMod.id("waystones"), ClientWaystones::atlasMarkers); ClientTickEvents.END_CLIENT_TICK.register(client -> { if (client.player == null) return; + ClientGrapplePrediction.tick(client); Entity vehicle = client.player.getVehicle(); if (vehicle instanceof AircraftEntity aircraft) { boolean throttle = client.options.keyJump.isDown(); diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java new file mode 100644 index 0000000..2cbceeb --- /dev/null +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/client/grapple/ClientGrapplePrediction.java @@ -0,0 +1,147 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.client.grapple; + +import fr.koka99cab.sanctuary26.iliketomoveit.grapple.GrappleSwingPhysics; +import fr.koka99cab.sanctuary26.iliketomoveit.network.GrappleStatePayload; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.entity.player.Input; +import net.minecraft.world.phys.Vec3; + +/** Local rope constraint that prevents vanilla movement prediction fighting the server pendulum. */ +public final class ClientGrapplePrediction { + private static final double PLAYER_CENTER_FACTOR = 0.5D; + private static final double REEL_IN_SPEED = 0.18D; + private static final double LOWER_ROPE_SPEED = 0.22D; + private static final double MIN_ROPE_LENGTH = 1.0D; + private static final int PACKET_TIMEOUT_TICKS = 10; + private static final double HARD_DIVERGENCE_SQ = 36.0D; + + private static int sessionId = -1; + private static int sequence = -1; + private static boolean anchored; + private static boolean ropeTaut; + private static Vec3 anchor = Vec3.ZERO; + private static Vec3 authoritativeCenter = Vec3.ZERO; + private static Vec3 swingVelocity = Vec3.ZERO; + private static double ropeLength; + private static double maxRopeLength; + private static int ticksWithoutPacket; + + private ClientGrapplePrediction() { + } + + public static void update(Minecraft client, GrappleStatePayload payload) { + if (!payload.active()) { + // A stop for an older hook must not cancel a newer session if the client + // drains both packets during the same frame. + if (payload.sessionId() == sessionId && payload.sequence() > sequence) { + clear(); + } + return; + } + if (payload.sessionId() == sessionId && payload.sequence() <= sequence) { + return; + } + boolean newSession = payload.sessionId() != sessionId; + sessionId = payload.sessionId(); + sequence = payload.sequence(); + anchored = payload.anchored(); + ropeTaut = payload.ropeTaut(); + anchor = payload.anchor(); + authoritativeCenter = payload.center(); + ropeLength = newSession ? payload.ropeLength() : lerp(ropeLength, payload.ropeLength(), 0.35D); + maxRopeLength = payload.maxRopeLength(); + swingVelocity = newSession + ? payload.velocity() + : swingVelocity.lerp(payload.velocity(), 0.2D); + ticksWithoutPacket = 0; + if (client.player != null && anchored && ropeTaut) { + Vec3 center = playerCenter(client.player); + if (center.distanceToSqr(authoritativeCenter) > HARD_DIVERGENCE_SQ) { + client.player.setPos( + authoritativeCenter.x, + authoritativeCenter.y - client.player.getBbHeight() * PLAYER_CENTER_FACTOR, + authoritativeCenter.z + ); + client.player.setDeltaMovement(payload.velocity()); + swingVelocity = payload.velocity(); + } + } + } + + public static void tick(Minecraft client) { + if (sessionId < 0 || client.player == null || client.level == null) { + return; + } + if (++ticksWithoutPacket > PACKET_TIMEOUT_TICKS) { + clear(); + return; + } + if (!anchored || !ropeTaut) { + return; + } + + boolean reel = client.options.keyJump.isDown(); + boolean lower = client.options.keyShift.isDown(); + if (reel && !lower) { + ropeLength = Math.max(MIN_ROPE_LENGTH, ropeLength - REEL_IN_SPEED); + } else if (lower && !reel) { + ropeLength = Math.min(maxRopeLength, ropeLength + LOWER_ROPE_SPEED); + } + + LocalPlayer player = client.player; + Vec3 center = playerCenter(player); + Vec3 constrainedCenter = GrappleSwingPhysics.constrainCenter(anchor, ropeLength, center); + Vec3 radialCorrection = constrainedCenter.subtract(center); + if (radialCorrection.lengthSqr() > 1.0E-6D) { + player.move(MoverType.SELF, radialCorrection); + constrainedCenter = playerCenter(player); + } + GrappleSwingPhysics.Step predicted = GrappleSwingPhysics.step( + anchor, ropeLength, constrainedCenter, swingVelocity, moveIntent(player)); + swingVelocity = predicted.velocity(); + player.setDeltaMovement(swingVelocity); + player.resetFallDistance(); + } + + public static void clear() { + sessionId = -1; + sequence = -1; + anchored = false; + ropeTaut = false; + anchor = Vec3.ZERO; + authoritativeCenter = Vec3.ZERO; + swingVelocity = Vec3.ZERO; + ropeLength = 0.0D; + maxRopeLength = 0.0D; + ticksWithoutPacket = 0; + } + + private static Vec3 playerCenter(LocalPlayer player) { + return player.position().add(0.0D, player.getBbHeight() * PLAYER_CENTER_FACTOR, 0.0D); + } + + /** Mirrors ServerPlayer#getLastClientMoveIntent from the local key state. */ + private static Vec3 moveIntent(LocalPlayer player) { + Input input = player.input.keyPresses; + double strafe = input.left() == input.right() ? 0.0D : input.left() ? 1.0D : -1.0D; + double forward = input.forward() == input.backward() ? 0.0D : input.forward() ? 1.0D : -1.0D; + Vec3 local = new Vec3(strafe, 0.0D, forward); + if (local.lengthSqr() < 1.0E-7D) { + return Vec3.ZERO; + } + if (local.lengthSqr() > 1.0D) { + local = local.normalize(); + } + double yaw = player.getYRot() * Math.PI / 180.0D; + double sine = Math.sin(yaw); + double cosine = Math.cos(yaw); + return new Vec3(local.x * cosine - local.z * sine, 0.0D, + local.z * cosine + local.x * sine); + } + + private static double lerp(double start, double end, double factor) { + return start + (end - start) * factor; + } +} diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java index 713dfca..99bf5e2 100644 --- a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleController.java @@ -1,12 +1,14 @@ package fr.koka99cab.sanctuary26.iliketomoveit.grapple; import fr.koka99cab.sanctuary26.iliketomoveit.entity.GrapplingHookEntity; +import fr.koka99cab.sanctuary26.iliketomoveit.network.GrappleStatePayload; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.core.BlockPos; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; @@ -47,7 +49,6 @@ public final class GrappleController { private static final double COLLISION_MISMATCH_TOLERANCE_SQ = 1.0E-4D; private static final double WALL_RELEASE_HORIZONTAL_DAMPING = 0.25D; private static final double WALL_RELEASE_DROP_SPEED = 0.18D; - private static final double SWING_PUMP = 0.055D; private static final double REEL_IN_SPEED = 0.18D; private static final double REEL_SPEED_FROM_SWING_SPEED = 0.035D; private static final double MAX_DYNAMIC_REEL_IN_SPEED = 0.46D; @@ -55,13 +56,10 @@ public final class GrappleController { private static final double STUCK_REEL_IN_SPEED = 0.24D; private static final double STUCK_SPEED_SQ = 0.018D; private static final int STUCK_REEL_DELAY = 8; - private static final double PENDULUM_GRAVITY = 0.08D; - private static final double SWING_DAMPING = 0.998D; private static final double FALL_INERTIA_TO_SWING = 1.22D; private static final double CATCH_LIFT_FROM_SPEED = 0.18D; private static final double CATCH_LIFT_FROM_FALL = 0.22D; private static final double MAX_CATCH_LIFT = 0.72D; - private static final double MAX_SWING_SPEED = 6.5D; private static final double PLAYER_CENTER_FACTOR = 0.5D; private static final double RELEASE_BOOST_SPEED = 0.85D; private static final double RELEASE_BOOST_FROM_SWING_SPEED = 0.12D; @@ -113,7 +111,10 @@ public final class GrappleController { GrapplingHookEntity hook = new GrapplingHookEntity(level, player, startPosition, velocity, maxFlightDistance, maxFlightTicks); level.addFreshEntity(hook); - ACTIVE.put(player.getUUID(), new GrappleSession(level.dimension(), startPosition, BlockPos.containing(startPosition), hook.getId())); + GrappleSession session = new GrappleSession( + level.dimension(), startPosition, BlockPos.containing(startPosition), hook.getId()); + ACTIVE.put(player.getUUID(), session); + syncPrediction(player, session); stack.hurtAndBreak(1, player, hand); level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.FISHING_BOBBER_THROW, SoundSource.PLAYERS, 0.8F, 1.15F); @@ -145,6 +146,7 @@ public final class GrappleController { player.resetFallDistance(); levelSound(player, SoundEvents.LEAD_TIED, 0.8F, 1.1F); + syncPrediction(player, session); } public static void expire(ServerPlayer player, GrapplingHookEntity hook) { @@ -169,6 +171,7 @@ public final class GrappleController { applyReleaseBoost(player, session); } + stopPrediction(player, session); discardVisualHook((ServerLevel) player.level(), session); return true; } @@ -192,6 +195,7 @@ public final class GrappleController { if (!session.anchored) { tickFlyingHook(player, session, hook); + syncPrediction(player, session); return; } @@ -209,6 +213,7 @@ public final class GrappleController { session.ropeTaut = false; session.swingVelocity = player.getDeltaMovement(); session.jumpHeldLastTick = session.manualReel; + syncPrediction(player, session); return; } @@ -220,6 +225,7 @@ public final class GrappleController { session.ropeTaut = true; session.jumpHeldLastTick = session.manualReel; + syncPrediction(player, session); } private static void tickFlyingHook(ServerPlayer player, GrappleSession session, GrapplingHookEntity hook) { @@ -271,24 +277,16 @@ public final class GrappleController { private static boolean simulatePendulum(ServerPlayer player, GrappleSession session, Vec3 center, boolean firstCatch) { Vec3 radial = center.subtract(session.anchorPosition).normalize(); - Vec3 currentCenter = session.anchorPosition.add(radial.scale(session.ropeLength)); - Vec3 swingVelocity = firstCatch ? preservedCatchVelocity(player, radial) : projectOntoSwingPlane(session.swingVelocity, radial); - - Vec3 gravity = new Vec3(0.0D, -PENDULUM_GRAVITY, 0.0D); - swingVelocity = swingVelocity.add(projectOntoSwingPlane(gravity, radial)); - swingVelocity = swingVelocity.add(swingInput(player, radial)).scale(SWING_DAMPING); - swingVelocity = limitSpeed(swingVelocity); - - Vec3 nextCenter = currentCenter.add(swingVelocity); - Vec3 nextRadial = nextCenter.subtract(session.anchorPosition); - if (nextRadial.lengthSqr() < 0.001D) { - nextRadial = radial; - } else { - nextRadial = nextRadial.normalize(); - } - - Vec3 constrainedCenter = session.anchorPosition.add(nextRadial.scale(session.ropeLength)); - Vec3 actualVelocity = limitSpeed(constrainedCenter.subtract(currentCenter)); + Vec3 carriedVelocity = firstCatch ? preservedCatchVelocity(player, radial) : session.swingVelocity; + GrappleSwingPhysics.Step step = GrappleSwingPhysics.step( + session.anchorPosition, + session.ropeLength, + center, + carriedVelocity, + player.getLastClientMoveIntent() + ); + Vec3 constrainedCenter = step.center(); + Vec3 actualVelocity = step.velocity(); session.swingVelocity = actualVelocity; if (!movePlayerAlongRope(player, constrainedCenter)) { @@ -298,7 +296,6 @@ public final class GrappleController { player.setDeltaMovement(actualVelocity); player.resetFallDistance(); - player.hurtMarked = true; return true; } @@ -351,7 +348,8 @@ public final class GrappleController { private static Vec3 preservedCatchVelocity(ServerPlayer player, Vec3 radial) { Vec3 incomingVelocity = player.getDeltaMovement(); - Vec3 tangentVelocity = projectOntoSwingPlane(incomingVelocity, radial).add(catchLift(radial, incomingVelocity)); + Vec3 tangentVelocity = GrappleSwingPhysics.projectOntoSwingPlane(incomingVelocity, radial) + .add(catchLift(radial, incomingVelocity)); double fallSpeed = Math.max(0.0D, -incomingVelocity.y); double outwardSpeed = Math.max(0.0D, incomingVelocity.dot(radial)); double targetSwingSpeed = Math.max(fallSpeed, outwardSpeed) * FALL_INERTIA_TO_SWING; @@ -370,7 +368,7 @@ public final class GrappleController { return Vec3.ZERO; } - Vec3 upwardTangent = projectOntoSwingPlane(new Vec3(0.0D, 1.0D, 0.0D), radial); + Vec3 upwardTangent = GrappleSwingPhysics.projectOntoSwingPlane(new Vec3(0.0D, 1.0D, 0.0D), radial); if (upwardTangent.lengthSqr() < 0.001D) { return Vec3.ZERO; } @@ -380,28 +378,19 @@ public final class GrappleController { return upwardTangent.normalize().scale(lift); } - private static Vec3 swingInput(ServerPlayer player, Vec3 radial) { - Vec3 tangentIntent = projectOntoSwingPlane(player.getLastClientMoveIntent(), radial); - if (tangentIntent.lengthSqr() < 0.001D) { - return Vec3.ZERO; - } - - return tangentIntent.normalize().scale(SWING_PUMP); - } - private static Vec3 catchSwingDirection(ServerPlayer player, Vec3 radial, Vec3 tangentVelocity) { if (tangentVelocity.lengthSqr() > 0.001D) { return tangentVelocity.normalize(); } Vec3 moveIntent = player.getLastClientMoveIntent(); - Vec3 tangentIntent = projectOntoSwingPlane(moveIntent, radial); + Vec3 tangentIntent = GrappleSwingPhysics.projectOntoSwingPlane(moveIntent, radial); if (tangentIntent.lengthSqr() > 0.001D) { return tangentIntent.normalize(); } Vec3 look = player.calculateViewVector(player.getXRot(), player.getYRot()); - Vec3 tangentLook = projectOntoSwingPlane(look, radial); + Vec3 tangentLook = GrappleSwingPhysics.projectOntoSwingPlane(look, radial); if (tangentLook.lengthSqr() > 0.001D) { return tangentLook.normalize(); } @@ -414,19 +403,6 @@ public final class GrappleController { return new Vec3(1.0D, 0.0D, 0.0D); } - private static Vec3 projectOntoSwingPlane(Vec3 direction, Vec3 radial) { - return direction.subtract(radial.scale(direction.dot(radial))); - } - - private static Vec3 limitSpeed(Vec3 velocity) { - double speed = velocity.length(); - if (speed <= MAX_SWING_SPEED) { - return velocity; - } - - return velocity.scale(MAX_SWING_SPEED / speed); - } - private static void applyReleaseBoost(ServerPlayer player, GrappleSession session) { if (!player.getLastClientInput().sprint() || !canBoostOnRelease(session)) { return; @@ -488,6 +464,31 @@ public final class GrappleController { } } + private static void syncPrediction(ServerPlayer player, GrappleSession session) { + if (!ServerPlayNetworking.canSend(player, GrappleStatePayload.ID)) { + return; + } + ServerPlayNetworking.send(player, new GrappleStatePayload( + session.visualHookId, + ++session.sequence, + true, + session.anchored, + session.ropeTaut, + session.anchorPosition, + session.ropeLength, + session.maxRopeLength, + playerCenter(player), + session.swingVelocity + )); + } + + private static void stopPrediction(ServerPlayer player, GrappleSession session) { + if (ServerPlayNetworking.canSend(player, GrappleStatePayload.ID)) { + ServerPlayNetworking.send(player, + GrappleStatePayload.stopped(session.visualHookId, ++session.sequence)); + } + } + private static void playRetrieveSound(ServerLevel level, ServerPlayer player) { level.playSound(null, player.getX(), player.getY(), player.getZ(), SoundEvents.LEAD_UNTIED, SoundSource.PLAYERS, 0.9F, 1.0F); } @@ -515,6 +516,7 @@ public final class GrappleController { private boolean manualLower; private boolean jumpHeldLastTick; private int stillTicks; + private int sequence; private GrappleSession(ResourceKey dimension, Vec3 anchorPosition, BlockPos anchorBlockPos, int visualHookId) { this.dimension = dimension; diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysics.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysics.java new file mode 100644 index 0000000..97ab21c --- /dev/null +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysics.java @@ -0,0 +1,61 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.grapple; + +import net.minecraft.world.phys.Vec3; + +/** Deterministic pendulum step shared by the authoritative server and local prediction. */ +public final class GrappleSwingPhysics { + public static final double MAX_SWING_SPEED = 6.5D; + private static final double PENDULUM_GRAVITY = 0.08D; + private static final double SWING_DAMPING = 0.998D; + private static final double SWING_PUMP = 0.055D; + + private GrappleSwingPhysics() { + } + + public static Step step( + Vec3 anchor, + double ropeLength, + Vec3 center, + Vec3 carriedVelocity, + Vec3 inputIntent + ) { + Vec3 radial = center.subtract(anchor); + if (radial.lengthSqr() < 0.001D || ropeLength <= 0.0D) { + return new Step(center, Vec3.ZERO); + } + radial = radial.normalize(); + Vec3 currentCenter = anchor.add(radial.scale(ropeLength)); + Vec3 swingVelocity = projectOntoSwingPlane(carriedVelocity, radial); + Vec3 gravity = new Vec3(0.0D, -PENDULUM_GRAVITY, 0.0D); + swingVelocity = swingVelocity.add(projectOntoSwingPlane(gravity, radial)); + Vec3 tangentIntent = projectOntoSwingPlane(inputIntent, radial); + if (tangentIntent.lengthSqr() >= 0.001D) { + swingVelocity = swingVelocity.add(tangentIntent.normalize().scale(SWING_PUMP)); + } + swingVelocity = limitSpeed(swingVelocity.scale(SWING_DAMPING)); + + Vec3 nextRadial = currentCenter.add(swingVelocity).subtract(anchor); + nextRadial = nextRadial.lengthSqr() < 0.001D ? radial : nextRadial.normalize(); + Vec3 constrainedCenter = anchor.add(nextRadial.scale(ropeLength)); + return new Step(constrainedCenter, limitSpeed(constrainedCenter.subtract(currentCenter))); + } + + public static Vec3 constrainCenter(Vec3 anchor, double ropeLength, Vec3 center) { + Vec3 radial = center.subtract(anchor); + return radial.lengthSqr() < 0.001D || ropeLength <= 0.0D + ? center + : anchor.add(radial.normalize().scale(ropeLength)); + } + + public static Vec3 projectOntoSwingPlane(Vec3 direction, Vec3 radial) { + return direction.subtract(radial.scale(direction.dot(radial))); + } + + private static Vec3 limitSpeed(Vec3 velocity) { + double speed = velocity.length(); + return speed <= MAX_SWING_SPEED ? velocity : velocity.scale(MAX_SWING_SPEED / speed); + } + + public record Step(Vec3 center, Vec3 velocity) { + } +} diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java new file mode 100644 index 0000000..00da068 --- /dev/null +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/GrappleStatePayload.java @@ -0,0 +1,107 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.network; + +import fr.koka99cab.sanctuary26.iliketomoveit.ILikeToMoveItMod; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.phys.Vec3; + +/** Fixed-size authoritative state used only to predict the owning player's swing. */ +public record GrappleStatePayload( + int sessionId, + int sequence, + boolean active, + boolean anchored, + boolean ropeTaut, + Vec3 anchor, + double ropeLength, + double maxRopeLength, + Vec3 center, + Vec3 velocity +) implements CustomPacketPayload { + private static final double MAX_COORDINATE = 30_000_000.0D; + private static final double MAX_ROPE_LENGTH = 216.0D; + private static final double MAX_VELOCITY = 12.0D; + public static final Type ID = + new Type<>(ILikeToMoveItMod.id("s2c/grapple_state")); + public static final StreamCodec CODEC = + StreamCodec.ofMember(GrappleStatePayload::write, GrappleStatePayload::read); + + public GrappleStatePayload { + sessionId = Math.max(-1, sessionId); + sequence = Math.max(0, sequence); + anchor = bounded(anchor, MAX_COORDINATE); + center = bounded(center, MAX_COORDINATE); + velocity = bounded(velocity, MAX_VELOCITY); + ropeLength = boundedScalar(ropeLength, 0.0D, MAX_ROPE_LENGTH); + maxRopeLength = boundedScalar(maxRopeLength, ropeLength, MAX_ROPE_LENGTH); + maxRopeLength = Math.max(ropeLength, maxRopeLength); + if (!active) { + anchored = false; + ropeTaut = false; + } + } + + public static GrappleStatePayload stopped(int sessionId, int sequence) { + return new GrappleStatePayload(sessionId, sequence, false, false, false, + Vec3.ZERO, 0.0D, 0.0D, Vec3.ZERO, Vec3.ZERO); + } + + private static GrappleStatePayload read(RegistryFriendlyByteBuf buffer) { + return new GrappleStatePayload( + buffer.readVarInt(), + buffer.readVarInt(), + buffer.readBoolean(), + buffer.readBoolean(), + buffer.readBoolean(), + readVec(buffer), + buffer.readDouble(), + buffer.readDouble(), + readVec(buffer), + readVec(buffer) + ); + } + + private void write(RegistryFriendlyByteBuf buffer) { + buffer.writeVarInt(sessionId); + buffer.writeVarInt(sequence); + buffer.writeBoolean(active); + buffer.writeBoolean(anchored); + buffer.writeBoolean(ropeTaut); + writeVec(buffer, anchor); + buffer.writeDouble(ropeLength); + buffer.writeDouble(maxRopeLength); + writeVec(buffer, center); + writeVec(buffer, velocity); + } + + private static Vec3 readVec(RegistryFriendlyByteBuf buffer) { + return new Vec3(buffer.readDouble(), buffer.readDouble(), buffer.readDouble()); + } + + private static void writeVec(RegistryFriendlyByteBuf buffer, Vec3 value) { + buffer.writeDouble(value.x); + buffer.writeDouble(value.y); + buffer.writeDouble(value.z); + } + + private static Vec3 bounded(Vec3 value, double maximum) { + if (value == null) { + return Vec3.ZERO; + } + return new Vec3( + boundedScalar(value.x, -maximum, maximum), + boundedScalar(value.y, -maximum, maximum), + boundedScalar(value.z, -maximum, maximum) + ); + } + + private static double boundedScalar(double value, double minimum, double maximum) { + return Double.isFinite(value) ? Math.clamp(value, minimum, maximum) : 0.0D; + } + + @Override + public Type type() { + return ID; + } +} diff --git a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/ILikeToMoveItNetworking.java b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/ILikeToMoveItNetworking.java index d5c67c4..f4ff98a 100644 --- a/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/ILikeToMoveItNetworking.java +++ b/iliketomoveit/src/main/java/fr/koka99cab/sanctuary26/iliketomoveit/network/ILikeToMoveItNetworking.java @@ -10,7 +10,7 @@ import net.minecraft.world.entity.Entity; * Networking contract for I Like To Move It. Payload IDs must use the module namespace. */ public final class ILikeToMoveItNetworking { - public static final int PROTOCOL_VERSION = 4; + public static final int PROTOCOL_VERSION = 5; private static boolean initialized; private ILikeToMoveItNetworking() { @@ -26,6 +26,7 @@ public final class ILikeToMoveItNetworking { PayloadTypeRegistry.serverboundPlay().register(WaystoneRenamePayload.ID, WaystoneRenamePayload.CODEC); PayloadTypeRegistry.clientboundPlay().register(WaystoneOpenPayload.ID, WaystoneOpenPayload.CODEC); PayloadTypeRegistry.clientboundPlay().register(WaystoneSyncPayload.ID, WaystoneSyncPayload.CODEC); + PayloadTypeRegistry.clientboundPlay().register(GrappleStatePayload.ID, GrappleStatePayload.CODEC); ServerPlayNetworking.registerGlobalReceiver(AircraftControlPayload.ID, (payload, context) -> { Entity vehicle = context.player().getVehicle(); if (vehicle instanceof AircraftEntity aircraft && aircraft.hasPassenger(context.player())) { @@ -37,7 +38,7 @@ public final class ILikeToMoveItNetworking { ServerPlayNetworking.registerGlobalReceiver(WaystoneRenamePayload.ID, (payload, context) -> WaystoneTeleportation.rename(context.player(), payload.pos(), payload.name())); WaystoneSyncService.initialize(); - ILikeToMoveItMod.LOGGER.debug("[{}] Networking contract v{} ready; aircraft and waystone payloads registered.", + ILikeToMoveItMod.LOGGER.debug("[{}] Networking contract v{} ready; aircraft, grapple and waystone payloads registered.", ILikeToMoveItMod.DISPLAY_NAME, PROTOCOL_VERSION); } } diff --git a/iliketomoveit/src/main/resources/fabric.mod.json b/iliketomoveit/src/main/resources/fabric.mod.json index 3223b76..41a7cff 100644 --- a/iliketomoveit/src/main/resources/fabric.mod.json +++ b/iliketomoveit/src/main/resources/fabric.mod.json @@ -37,7 +37,7 @@ "system_type": "gameplay-system", "namespace": "iliketomoveit", "data_version": 3, - "network_protocol": 4, + "network_protocol": 5, "lifecycle": "${lifecycle}", "pack_version": "${pack_version}", "descriptions": { diff --git a/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysicsSmoke.java b/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysicsSmoke.java new file mode 100644 index 0000000..7a0509a --- /dev/null +++ b/iliketomoveit/src/test/java/fr/koka99cab/sanctuary26/iliketomoveit/grapple/GrappleSwingPhysicsSmoke.java @@ -0,0 +1,38 @@ +package fr.koka99cab.sanctuary26.iliketomoveit.grapple; + +import net.minecraft.world.phys.Vec3; + +/** Determinism and rope-constraint smoke for the shared client/server pendulum. */ +public final class GrappleSwingPhysicsSmoke { + private GrappleSwingPhysicsSmoke() { + } + + public static void main(String[] args) { + Vec3 anchor = new Vec3(4.0D, 30.0D, -2.0D); + Vec3 center = new Vec3(4.0D, 20.0D, -2.0D); + Vec3 velocity = new Vec3(1.2D, -0.4D, 0.7D); + Vec3 input = new Vec3(0.4D, 0.0D, 1.0D); + + GrappleSwingPhysics.Step first = GrappleSwingPhysics.step(anchor, 10.0D, center, velocity, input); + GrappleSwingPhysics.Step repeated = GrappleSwingPhysics.step(anchor, 10.0D, center, velocity, input); + assertNear(first.center().distanceTo(anchor), 10.0D, 1.0E-9D, "rope length drifted"); + assertNear(first.velocity().length(), repeated.velocity().length(), 1.0E-12D, + "identical steps were not deterministic"); + if (!first.center().equals(repeated.center()) || !first.velocity().equals(repeated.velocity())) { + throw new IllegalStateException("Shared pendulum state is not bit-for-bit deterministic"); + } + if (first.velocity().length() > GrappleSwingPhysics.MAX_SWING_SPEED + 1.0E-9D) { + throw new IllegalStateException("Pendulum exceeded its bounded speed"); + } + + Vec3 constrained = GrappleSwingPhysics.constrainCenter(anchor, 7.5D, + new Vec3(19.0D, 3.0D, 8.0D)); + assertNear(constrained.distanceTo(anchor), 7.5D, 1.0E-9D, "client rope projection drifted"); + } + + private static void assertNear(double actual, double expected, double tolerance, String message) { + if (!Double.isFinite(actual) || Math.abs(actual - expected) > tolerance) { + throw new IllegalStateException(message + ": expected " + expected + ", got " + actual); + } + } +} -- 2.54.0 From d93c1c71ad4f58830e5cd1c67a6fcccad91f9c26 Mon Sep 17 00:00:00 2001 From: koka Date: Wed, 26 Aug 2026 02:01:31 +0200 Subject: [PATCH 4/5] chore(release): preparer Sanctuary 26.2.0-alpha.196 --- CHANGELOG.md | 14 ++++ README.md | 10 ++- build.gradle | 60 ++++++++++++--- gradle.properties | 8 +- ...ry-mailbox-grapple-fixes-to-alpha.196.json | 74 +++++++++++++++++++ pack/packwiz/index.toml | 28 +++---- pack/packwiz/mods/jei.pw.toml | 8 +- pack/packwiz/pack.toml | 4 +- pack/prism/modrinth.index.json | 2 +- pack/release.json | 4 +- 10 files changed, 172 insertions(+), 40 deletions(-) create mode 100644 pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json diff --git a/CHANGELOG.md b/CHANGELOG.md index d64bad2..c82bb25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve - Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46. - Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées. +## `26.2.0-alpha.196` + +Modules modifiés : **It's Alive `0.0.0-alpha.28`**, **I Like To Move It `0.0.0-alpha.13`** et **Sanctuary `0.0.0-alpha.112`**. Les sept autres modules restent inchangés. + +- donne au vin une texture bordeaux distincte sans changer son identifiant ; +- détruit le contenu interne d'une presse alimentaire cassée, tout en laissant tomber le bloc de presse, afin de supprimer la duplication des bols et fioles pendant une extraction ; +- transforme directement un bloc de neuf olives en neuf portions d'huile récupérables avec des fioles et retire le second pressage manuel de la pâte d'olive ; +- expose l'état de la presse, du moulin, de la poêle, de la marmite, du four et du tonneau aux comparateurs, de 0 pour vide à 15 pour un résultat prêt ; +- éjecte en fontaine les contenants rendus par les recettes de marmite et verrouille brièvement les nouvelles insertions pendant leur restitution ; +- rend la boîte aux lettres orientable dans les quatre directions horizontales ; +- ajoute au grappin une prédiction locale fondée sur la même physique que le serveur et une réconciliation bornée, sans retirer l'autorité serveur ; +- porte le protocole I Like To Move It de 4 à 5 pour l'état borné du grappin, sans migration de sauvegarde ni changement d'identifiant persistant ; +- publie l'index Packwiz alpha.196 et actualise JEI de `30.25.0.177` vers `30.26.0.182`. + ## `26.2.0-alpha.195` Modules modifiés : **Only Fun `0.0.0-alpha.19`** et **Sanctuary `0.0.0-alpha.111`**. Les huit autres modules restent inchangés. diff --git a/README.md b/README.md index e354f6b..505394c 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,15 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`) Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module. -Version courante du pack local : **`26.2.0-alpha.195`**. Only Fun passe en **`0.0.0-alpha.19`** et Sanctuary en **`0.0.0-alpha.111`** ; les huit autres modules restent inchangés. +Version courante du pack local : **`26.2.0-alpha.196`**. It's Alive passe en **`0.0.0-alpha.28`**, I Like To Move It en **`0.0.0-alpha.13`** et Sanctuary en **`0.0.0-alpha.112`** ; les sept autres modules restent inchangés. + +L’alpha.196 fiabilise les postes culinaires : la presse détruit son contenu si elle est cassée, l’huile d’olive se récupère directement en fioles et les six machines exposent leur état au comparateur. La marmite éjecte les contenants consommés avant d’accepter un nouvel ingrédient, et le vin possède une teinte bordeaux distincte. La boîte aux lettres devient orientable horizontalement. Enfin, le grappin conserve son autorité serveur tout en ajoutant une prédiction locale et une réconciliation bornée pour supprimer les à-coups multijoueur. L’alpha.195 charge le catalogue Shop depuis le CSV rééquilibré et conserve les contrôles serveur sur chaque offre. Le Magic Cloud et le Magic Carpet restent catalogués avec leur prix futur, mais sont invisibles et impossibles à acheter tant que les boss correspondants et leur état persistant ne sont pas implémentés. L’alpha.194 ouvre la cuisine avec huit cultures supplémentaires — riz, soja, haricot, pois chiche, piment, basilic, menthe et coriandre — ainsi que le café, la vanille, l’olivier et le citronnier. Le riz pousse réellement immergé sur de la boue, le haricot se récolte jeune ou sec, le café se torréfie et la vanille se sèche au smoker. Le climat détermine seulement la découverte : les nouvelles plantes sauvages apparaissent dans les nouveaux chunks, tandis qu’une chance rare de semence sur la végétation permet de continuer un monde existant. Aucune réinitialisation de sauvegarde n’est requise. -Le [wiki cuisine de l’alpha.194](itsalive/FOOD_WIKI.md) détaille les cultures, les six postes, l’automatisation, les ingrédients transformés, le livre et ses 64 recettes. +Le [wiki cuisine de l’alpha.196](itsalive/FOOD_WIKI.md) détaille les cultures, les six postes, l’automatisation, les ingrédients transformés, le livre et ses 64 recettes. L’alpha.193 donne à chaque item du Shop une petite variation de prix stable et reproductible, tout en réservant des tarifs explicites aux vrais objets exceptionnels. L’œuf doré, porte d’entrée extrêmement rare vers les poules à gemmes, coûte désormais 811 rubis ou 59 saphirs dans le catalogue. Les six emplacements verrouillés occupent directement les six cartes restantes de la grille : ils se débloquent toujours dans l’ordre pour 2, 4, 8, 12, 18 puis 25 niveaux. L’aide de boîte aux lettres remonte légèrement et la bourse rubis/saphirs s’ancre en bas à droite. @@ -188,7 +190,7 @@ Le détail de chaque publication est conservé dans le [journal des changements] - `Mod.id(path)` est l'unique fabrique locale d'identifiants. - Les anciennes sauvegardes gardent leurs IDs historiques pendant la migration. Tout changement futur exige une table de migration explicite ; il ne faut pas réenregistrer silencieusement un contenu sous un nouvel ID. - Chaque module possède un seul hub `registry/*Registries`. Une feature ajoute son registre à ce hub, sans effet de bord dispersé dans des initialisations statiques. -- Les payloads utilisent un namespace stable. Chaque module expose `PROTOCOL_VERSION`; Red-Stoner utilise le protocole 3 depuis l'intégration de Canon, I Like To Move It le protocole 4 et Only Fun conserve le protocole 2. +- Les payloads utilisent un namespace stable. Chaque module expose `PROTOCOL_VERSION`; Red-Stoner utilise le protocole 3 depuis l'intégration de Canon, I Like To Move It le protocole 5 et Only Fun conserve le protocole 2. - Les données monde appartiennent au module conceptuellement responsable. Elles sont chargées paresseusement, utilisent une clé stable et sérialisent toujours `data_version`. - Une migration de données est monotone et marque la donnée comme modifiée. Une donnée créée par une version plus récente est signalée sans être réécrite à l'aveugle. - Chaque log porte le logger du module et un préfixe public identifiable. Le niveau debug de MasterKey est persistant mais ne modifie pas globalement la configuration SLF4J. @@ -317,7 +319,7 @@ La donnée globale v1 `iliketomoveit:portal_links` affecte un UUID à chaque moi ## Grappin et poulies -I Like To Move It possède désormais la migration complète de Spider-Man sous les IDs `iliketomoveit:grappling_hand`, `iliketomoveit:grappling_rod`, `iliketomoveit:grappling_hook` et `iliketomoveit:pulley_head`. Le grappin conserve sa charge, sa portée, son balancement autoritaire côté serveur, ses commandes de rappel/descente et la restitution de l'élan au lâcher. La main se fabrique, tandis qu'une seconde peut aussi apparaître dans les coffres de la cité perdue de Portals avec une probabilité de 12 %. +I Like To Move It possède désormais la migration complète de Spider-Man sous les IDs `iliketomoveit:grappling_hand`, `iliketomoveit:grappling_rod`, `iliketomoveit:grappling_hook` et `iliketomoveit:pulley_head`. Le grappin conserve sa charge, sa portée, son balancement autoritaire côté serveur, ses commandes de rappel/descente et la restitution de l'élan au lâcher. En multijoueur, le client prédit le même pendule puis se réconcilie avec des états serveur bornés ; le serveur reste la source de vérité. La main se fabrique, tandis qu'une seconde peut aussi apparaître dans les coffres de la cité perdue de Portals avec une probabilité de 12 %. Deux têtes de poulie peuvent être liées jusqu'à 128 blocs afin de créer une tyrolienne. La traversée vérifie les collisions, transporte le joueur à vitesse constante et émet un signal redstone de 15 pendant le passage : Red-Stoner pourra donc automatiser la mécanique plus tard sans en devenir propriétaire. Another World reste propriétaire des seuls matériaux éventuellement employés dans les recettes. diff --git a/build.gradle b/build.gradle index 41f895a..e0a089d 100644 --- a/build.gradle +++ b/build.gradle @@ -3427,8 +3427,8 @@ tasks.register("verifyProgressivePauseMenuRelease") { [(locale): new JsonSlurper().parse(file( "sanctuary/src/main/resources/assets/sanctuary/lang/${locale}.json"))] } - if (rootProject.pack_version != "26.2.0-alpha.195" - || rootProject.sanctuary_version != "0.0.0-alpha.111" + if (rootProject.pack_version != "26.2.0-alpha.196" + || rootProject.sanctuary_version != "0.0.0-alpha.112" || rootProject.onlyfun_version != "0.0.0-alpha.19" || 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"] @@ -3477,7 +3477,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") { def redStonerRegistry = file( "redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/registry/RedStonerRegistries.java").text def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json") - if (rootProject.pack_version != "26.2.0-alpha.195" + if (rootProject.pack_version != "26.2.0-alpha.196" || rootProject.anotherworld_version != "0.0.0-alpha.49" || rootProject.redstoner_version != "0.0.0-alpha.10" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version @@ -4121,9 +4121,9 @@ tasks.register("verifyShopPricingLayoutRelease") { "pack/migrations/26.2.0-alpha.192-shop-pricing-layout-to-alpha.193.json")) 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.195" + if (rootProject.pack_version != "26.2.0-alpha.196" || rootProject.onlyfun_version != "0.0.0-alpha.19" - || rootProject.sanctuary_version != "0.0.0-alpha.111" + || rootProject.sanctuary_version != "0.0.0-alpha.112" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.192" || migration.target?.pack_version != "26.2.0-alpha.193" @@ -4166,8 +4166,8 @@ tasks.register("verifyCuisineCropsRelease") { def climate = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/ClimateCropForaging.java").text def rice = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/RiceCropBlock.java").text def bean = file("itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/agriculture/BeanCropBlock.java").text - if (rootProject.pack_version != "26.2.0-alpha.195" - || rootProject.itsalive_version != "0.0.0-alpha.27" + if (rootProject.pack_version != "26.2.0-alpha.196" + || rootProject.itsalive_version != "0.0.0-alpha.28" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.193" || migration.source?.modules != [itsalive: "0.0.0-alpha.26"] @@ -4215,9 +4215,9 @@ tasks.register("verifyShopCsvBossGatesRelease") { def bossGate = file("onlyfun/src/main/java/fr/koka99cab/sanctuary26/onlyfun/shop/OnlyFunBossShopGate.java").text 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.195" + if (rootProject.pack_version != "26.2.0-alpha.196" || rootProject.onlyfun_version != "0.0.0-alpha.19" - || rootProject.sanctuary_version != "0.0.0-alpha.111" + || rootProject.sanctuary_version != "0.0.0-alpha.112" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version || migration.source?.pack_version != "26.2.0-alpha.194" || migration.source?.modules != [onlyfun: "0.0.0-alpha.18", sanctuary: "0.0.0-alpha.110"] @@ -4248,6 +4248,48 @@ tasks.named("verifyWorkspace") { dependsOn tasks.named("verifyShopCsvBossGatesRelease") } +tasks.register("verifyCulinaryMailboxGrappleFixesRelease") { + group = "verification" + description = "Checks the complete Sanctuary 26.2.0-alpha.196 reliability release contract." + inputs.file(file("pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + inputs.file(file("pack/release.json")) + inputs.file(file("pack/prism/modrinth.index.json")) + inputs.file(file("pack/packwiz/pack.toml")) + doLast { + def migration = new JsonSlurper().parse(file( + "pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json")) + def release = new JsonSlurper().parse(file("pack/release.json")) + def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) + def packwiz = file("pack/packwiz/pack.toml").text + if (rootProject.pack_version != "26.2.0-alpha.196" + || migration.source?.pack_version != "26.2.0-alpha.195" + || migration.target?.pack_version != "26.2.0-alpha.196" + || migration.target?.modules != [ + itsalive: "0.0.0-alpha.28", + iliketomoveit: "0.0.0-alpha.13", + sanctuary: "0.0.0-alpha.112"] + || migration.culinary_fixes?.olive_block_output?.count != 9 + || migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6 + || migration.mailbox?.horizontal_facing != true + || migration.grappling?.payload_fixed_size_and_bounded != true + || migration.grappling?.server_authoritative_collisions != true + || migration.distribution?.packwiz_index_refreshed != true + || migration.distribution?.jei?.target != "30.26.0.182" + || migration.compatibility?.save_migration_required != false + || migration.compatibility?.registry_ids_changed != false + || release.pack_version != "26.2.0-alpha.196" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.196.mrpack" + || prism.versionId != "26.2.0-alpha.196" + || !packwiz.contains('version = "26.2.0-alpha.196"')) { + throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete") + } + } +} + +tasks.named("verifyWorkspace") { + dependsOn tasks.named("verifyCulinaryMailboxGrappleFixesRelease") +} + tasks.named("check") { dependsOn tasks.named("verifyWorkspace") dependsOn tasks.named("verifyTransport") diff --git a/gradle.properties b/gradle.properties index 65e3451..9bc63ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,17 +18,17 @@ redstoner_version=0.0.0-alpha.10 redstoner_lifecycle=active anotherworld_version=0.0.0-alpha.49 anotherworld_lifecycle=active -itsalive_version=0.0.0-alpha.27 +itsalive_version=0.0.0-alpha.28 itsalive_lifecycle=active ouch_version=0.0.0-alpha.8 ouch_lifecycle=active masterkey_version=0.0.0-alpha.8 masterkey_lifecycle=active -iliketomoveit_version=0.0.0-alpha.12 +iliketomoveit_version=0.0.0-alpha.13 iliketomoveit_lifecycle=active onlyfun_version=0.0.0-alpha.19 onlyfun_lifecycle=active -sanctuary_version=0.0.0-alpha.111 +sanctuary_version=0.0.0-alpha.112 sanctuary_lifecycle=active -pack_version=26.2.0-alpha.195 +pack_version=26.2.0-alpha.196 maven_group=fr.koka99cab.sanctuary26 diff --git a/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json b/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json new file mode 100644 index 0000000..2b2fc39 --- /dev/null +++ b/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json @@ -0,0 +1,74 @@ +{ + "schema_version": 1, + "source": { + "pack_version": "26.2.0-alpha.195", + "modules": { + "itsalive": "0.0.0-alpha.27", + "iliketomoveit": "0.0.0-alpha.12", + "sanctuary": "0.0.0-alpha.111" + } + }, + "target": { + "pack_version": "26.2.0-alpha.196", + "modules": { + "itsalive": "0.0.0-alpha.28", + "iliketomoveit": "0.0.0-alpha.13", + "sanctuary": "0.0.0-alpha.112" + } + }, + "culinary_fixes": { + "wine_palette": "bordeaux_garnet", + "food_press_contents_destroyed_on_break": true, + "olive_block_output": { + "item": "itsalive:olive_oil", + "count": 9, + "container": "minecraft:glass_bottle" + }, + "legacy_olive_paste_id_preserved": true, + "comparator_contract": { + "blocks": [ + "itsalive:food_press", + "itsalive:redstone_mill", + "itsalive:frying_pan", + "itsalive:cooking_cauldron", + "itsalive:kitchen_oven", + "itsalive:fermentation_barrel" + ], + "empty": 0, + "loaded_or_working": "1-14", + "result_ready": 15 + }, + "cauldron_remainders_ejected": true, + "cauldron_input_lock_ticks": 20 + }, + "mailbox": { + "id": "sanctuary:delivery_box", + "horizontal_facing": true, + "owner_inventory_and_queue_unchanged": true + }, + "grappling": { + "owner_prediction_payload": "iliketomoveit:s2c/grapple_state", + "payload_fixed_size_and_bounded": true, + "shared_deterministic_pendulum": true, + "server_authoritative_collisions": true, + "per_tick_forced_velocity_packet_removed": true + }, + "distribution": { + "packwiz_index_refreshed": true, + "jei": { + "source": "30.25.0.177", + "target": "30.26.0.182" + } + }, + "compatibility": { + "itsalive_data_version": 1, + "itsalive_network_protocol": 2, + "iliketomoveit_data_version": 3, + "iliketomoveit_source_network_protocol": 4, + "iliketomoveit_target_network_protocol": 5, + "sanctuary_data_version": 11, + "sanctuary_network_protocol": 18, + "save_migration_required": false, + "registry_ids_changed": false + } +} diff --git a/pack/packwiz/index.toml b/pack/packwiz/index.toml index fe68d36..8ce3eb4 100644 --- a/pack/packwiz/index.toml +++ b/pack/packwiz/index.toml @@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0" [[files]] file = "mods/ambiance-0.0.0-alpha.11.jar" -hash = "bf780025630e2ee078078e1a6b0a9833503a02cffc9e3dddc274c44cd7d9d287" +hash = "a9bf37428a07f3dfbb506c0b99fc3ae40fb8654c1540603de96cd4326c231df9" [[files]] file = "mods/anotherworld-0.0.0-alpha.49.jar" -hash = "936db6fd7dbd6ce7d9e701a339e0695e2edd7fa03dd7bc3c789551d4418bf474" +hash = "6d151de5c07eef1f5eeb1af9c66ae9d8dc627432d9d84468b34272305fe02438" [[files]] file = "mods/events-0.0.0-alpha.0.jar" -hash = "e7d86ec2aab71105596a03148b91ccdabee42bf3e2ca8b2ab78ad4a2adf03442" +hash = "f0a2f9ac21865710183550f91e0d9ef44056269eefa4215b9c4e604569b7a6ab" [[files]] file = "mods/fabric-api.pw.toml" @@ -30,8 +30,8 @@ hash = "df032a6b051d0262cf24945c43e0e6b915e8cc1d62af62fe933932e7f35960c4" metafile = true [[files]] -file = "mods/iliketomoveit-0.0.0-alpha.12.jar" -hash = "b20fb18dd55569f1e8250a2ff4dd15731e965d6fe7f20329087b0cd596ac3507" +file = "mods/iliketomoveit-0.0.0-alpha.13.jar" +hash = "90335af8d390dc874d7930b6d566f984c96d87f357ab0ba7b73d501b841d1871" [[files]] file = "mods/iris.pw.toml" @@ -39,33 +39,33 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da" metafile = true [[files]] -file = "mods/itsalive-0.0.0-alpha.27.jar" -hash = "24f61ff6d7300ce17faae7f8e3f9dab63cedd1b005371209b88859b9e389a2d2" +file = "mods/itsalive-0.0.0-alpha.28.jar" +hash = "de81dd16379277b372a442b15c847d2925bc89d45703cde91cd66f8c92af3854" [[files]] file = "mods/jei.pw.toml" -hash = "cb6b81977ed8f80dae5fddf3bb08255162d4ff918fcccadab697707d1228f1a9" +hash = "9b6ff8adcf51525a33f3180dad84327cb77bb818f3ca31547635689cea523cbf" metafile = true [[files]] file = "mods/masterkey-0.0.0-alpha.8.jar" -hash = "62bf000ee8b7a1cad6fc4c95c3b9b8d51e005a111b300a6855dee8bbd9377bc7" +hash = "07bc6d9dc2df1f2b7be44408d05d36faef5a8c1c591e2ae9e4bdfa20c004a9d4" [[files]] file = "mods/onlyfun-0.0.0-alpha.19.jar" -hash = "f61fb53e6959eb356436b24c8347147e709b71834f5f0a517f4e48bfd6e27953" +hash = "7249ff62b9ef7e114c45d8083ede5b94cf20e2cf0eec216b12ab08c021a1caf1" [[files]] file = "mods/ouch-0.0.0-alpha.8.jar" -hash = "d58e50d3b48fc6bcc630a26c24732d94a09d326f4b1ff0de7bade187b2b3913e" +hash = "b3a641c015ae24ca958e03303b493c594ccb0b91969282f774da2d6849fd7936" [[files]] file = "mods/redstoner-0.0.0-alpha.10.jar" -hash = "a8207a5e00c646a33cf178ad598084dd85a6e538ea57096b5df082c8ac52d755" +hash = "db3a714ff043ac04a1ca8a1c752f614a7c1aecd557ea2ef2ca72af203cd25039" [[files]] -file = "mods/sanctuary-0.0.0-alpha.111.jar" -hash = "cea043f3c0852d5e17cf19a6fd6a2caeb01faa8f2e7c44a8a8c0a617bd2452d5" +file = "mods/sanctuary-0.0.0-alpha.112.jar" +hash = "63976528da2a467bd1dbe1432e1444f3c161d24dcbfed3c9e4755669a84538d9" [[files]] file = "mods/simple-voice-chat.pw.toml" diff --git a/pack/packwiz/mods/jei.pw.toml b/pack/packwiz/mods/jei.pw.toml index c19ce3b..09f9a94 100644 --- a/pack/packwiz/mods/jei.pw.toml +++ b/pack/packwiz/mods/jei.pw.toml @@ -1,14 +1,14 @@ name = "Just Enough Items (JEI)" -filename = "jei-26.2-fabric-30.25.0.177.jar" +filename = "jei-26.2-fabric-30.26.0.182.jar" side = "both" pin = true [download] -url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/AFgObZjc/jei-26.2-fabric-30.25.0.177.jar" +url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/x6nG9OT2/jei-26.2-fabric-30.26.0.182.jar" hash-format = "sha512" -hash = "749454d81f0b8e9860995e4fea6703573453cc16d5fec7b24c97b8c58d319988bb4d8f49e20b5e0e18781ee1c5e90a6bc7c2ef0046bf7a3ffac5d885a15d9740" +hash = "a79a3aaa4d3c17659bdd4823b525890aea20c271a7168a0ece9aa926b0770b0571c0fba20cda7ed54f57abf80d55a7bfdcfc37a1c7c92ee6f7c3bcdb84404054" [update] [update.modrinth] mod-id = "u6dRKJwZ" -version = "AFgObZjc" +version = "x6nG9OT2" diff --git a/pack/packwiz/pack.toml b/pack/packwiz/pack.toml index 5bf9f64..62267cb 100644 --- a/pack/packwiz/pack.toml +++ b/pack/packwiz/pack.toml @@ -1,12 +1,12 @@ name = "Sanctuary 26.2" author = "KOKA99CAB" -version = "26.2.0-alpha.195" +version = "26.2.0-alpha.196" pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "b975524b49b564ed765a28636d6797bab332032c988f651dcdea0851e70bf7b0" +hash = "2491c55df3788a5384385ccb67d33c806bcedc24281f94bc1b38e6e37a578263" [versions] fabric = "0.19.3" diff --git a/pack/prism/modrinth.index.json b/pack/prism/modrinth.index.json index 77b6f7a..bb039f2 100644 --- a/pack/prism/modrinth.index.json +++ b/pack/prism/modrinth.index.json @@ -3,7 +3,7 @@ "game": "minecraft", "name": "Sanctuary 26.2", "summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.", - "versionId": "26.2.0-alpha.195", + "versionId": "26.2.0-alpha.196", "files": [ { "path": "mods/fabric-api-0.155.0+26.2.jar", diff --git a/pack/release.json b/pack/release.json index dd5d1af..85e8754 100644 --- a/pack/release.json +++ b/pack/release.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "pack_version": "26.2.0-alpha.195", + "pack_version": "26.2.0-alpha.196", "release_channel": "alpha", "minecraft_version": "26.2", "fabric_loader_version": "0.19.3", @@ -9,7 +9,7 @@ "project_id": "FgOqyAXd", "project_slug": "sanctuary-koka99", "project_url": "https://modrinth.com/modpack/sanctuary-koka99", - "primary_file": "sanctuary-26.2.0-alpha.195.mrpack", + "primary_file": "sanctuary-26.2.0-alpha.196.mrpack", "shared_client_server_pack": true, "update_owner": "packwiz-installer", "check_on_launch": true, -- 2.54.0 From 7f971acce7c98fb790278b098f6112066be3d4d0 Mon Sep 17 00:00:00 2001 From: koka Date: Wed, 26 Aug 2026 12:24:34 +0200 Subject: [PATCH 5/5] fix(itsalive): notifier les observateurs comme vanilla --- CHANGELOG.md | 1 + build.gradle | 2 ++ itsalive/FOOD_WIKI.md | 2 ++ itsalive/build.gradle | 22 ++++++++++++++++++- .../entity/CookingCauldronBlockEntity.java | 9 ++++---- .../block/entity/FryingPanBlockEntity.java | 2 +- .../block/entity/KitchenOvenBlockEntity.java | 2 +- .../machine/FoodPressBlockEntity.java | 2 +- .../machine/RedstoneMillBlockEntity.java | 2 +- ...ry-mailbox-grapple-fixes-to-alpha.196.json | 5 +++++ 10 files changed, 40 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c82bb25..3e7e81e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Modules modifiés : **It's Alive `0.0.0-alpha.28`**, **I Like To Move It `0.0.0 - détruit le contenu interne d'une presse alimentaire cassée, tout en laissant tomber le bloc de presse, afin de supprimer la duplication des bols et fioles pendant une extraction ; - transforme directement un bloc de neuf olives en neuf portions d'huile récupérables avec des fioles et retire le second pressage manuel de la pâte d'olive ; - expose l'état de la presse, du moulin, de la poêle, de la marmite, du four et du tonneau aux comparateurs, de 0 pour vide à 15 pour un résultat prêt ; +- notifie les observateurs lors des changements visibles de niveau, de mélange ou d'allumage, comme les blocs vanilla, sans impulsion à chaque tick interne ; - éjecte en fontaine les contenants rendus par les recettes de marmite et verrouille brièvement les nouvelles insertions pendant leur restitution ; - rend la boîte aux lettres orientable dans les quatre directions horizontales ; - ajoute au grappin une prédiction locale fondée sur la même physique que le serveur et une réconciliation bornée, sans retirer l'autorité serveur ; diff --git a/build.gradle b/build.gradle index e0a089d..1ae4543 100644 --- a/build.gradle +++ b/build.gradle @@ -4270,6 +4270,8 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") { sanctuary: "0.0.0-alpha.112"] || migration.culinary_fixes?.olive_block_output?.count != 9 || migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6 + || migration.culinary_fixes?.observer_contract?.behavior != "vanilla_block_state_changes" + || migration.culinary_fixes?.observer_contract?.inventory_or_progress_ticks != false || migration.mailbox?.horizontal_facing != true || migration.grappling?.payload_fixed_size_and_bounded != true || migration.grappling?.server_authoritative_collisions != true diff --git a/itsalive/FOOD_WIKI.md b/itsalive/FOOD_WIKI.md index 6798c3a..01e264c 100644 --- a/itsalive/FOOD_WIKI.md +++ b/itsalive/FOOD_WIKI.md @@ -223,6 +223,8 @@ Planche Dalle bois Planche Les six postes — presse, moulin, poêle, marmite, four et tonneau — fournissent un signal de comparateur sémantique. Le signal vaut 0 quand le poste est vide, varie de 1 à 14 selon son remplissage ou son avancement, puis atteint 15 lorsqu'un résultat est prêt à extraire. Les changements d'inventaire et de progression actualisent automatiquement les comparateurs voisins. +Les observateurs suivent le comportement vanilla : ils émettent une impulsion quand le bloc change réellement d'état visible, par exemple lorsque la presse change de niveau, que la marmite change de mélange ou qu'un appareil s'allume ou s'éteint. Ils ne réagissent pas à chaque tick de progression ni aux changements d'inventaire invisibles ; pour ces informations, il faut utiliser un comparateur. + ## Ingrédients de base connus Ces transformations sont disponibles sans page secrète. diff --git a/itsalive/build.gradle b/itsalive/build.gradle index 43521ae..7e33351 100644 --- a/itsalive/build.gradle +++ b/itsalive/build.gradle @@ -1085,6 +1085,23 @@ tasks.register("verifyCulinaryReliability") { def press = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlock.java").text def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text def container = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractCulinaryContainerBlockEntity.java").text + def observableStateUpdates = [ + "machine/FoodPressBlockEntity.java": [ + ".setValue(FoodPressBlock.LEVEL, visibleLevel), Block.UPDATE_ALL)"], + "machine/RedstoneMillBlockEntity.java": [ + "state.setValue(RedstoneMillBlock.LIT, lit), Block.UPDATE_ALL"], + "culinary/block/entity/FryingPanBlockEntity.java": [ + "state.setValue(FryingPanBlock.LIT, heated), Block.UPDATE_ALL"], + "culinary/block/entity/CookingCauldronBlockEntity.java": [ + "currentState.setValue(CookingCauldronBlock.LIT, heated), Block.UPDATE_ALL", + "state.setValue(CookingCauldronBlock.MIXTURE, mixture),\n\t\t\t\tBlock.UPDATE_ALL);"], + "culinary/block/entity/KitchenOvenBlockEntity.java": [ + "state.setValue(KitchenOvenBlock.LIT, lit), Block.UPDATE_ALL"] + ] + def incompleteObserverUpdates = observableStateUpdates.findAll { relativePath, snippets -> + def source = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/${relativePath}").text + snippets.any { !source.contains(it) } + } def comparatorBlocks = [ "machine/FoodPressBlock.java", "machine/RedstoneMillBlock.java", @@ -1115,18 +1132,21 @@ tasks.register("verifyCulinaryReliability") { || migration.target?.modules?.itsalive != "0.0.0-alpha.28" || migration.culinary_fixes?.food_press_contents_destroyed_on_break != true || migration.culinary_fixes?.comparator_contract?.blocks?.size() != 6 + || migration.culinary_fixes?.observer_contract?.behavior != "vanilla_block_state_changes" + || migration.culinary_fixes?.observer_contract?.inventory_or_progress_ticks != false || oliveRecipe.ingredient != "itsalive:olive_block" || oliveRecipe.result?.id != "itsalive:olive_oil" || oliveRecipe.result?.count != 9 || file("src/main/resources/data/itsalive/recipe/press_olive_paste.json").exists() || press.contains("Containers.dropContents(level, pos, press)") || !incompleteComparators.isEmpty() + || !incompleteObserverUpdates.isEmpty() || !container.contains("updateNeighbourForOutputSignal") || !cauldron.contains("REMAINDER_EJECTION_LOCK_TICKS = 20") || !cauldron.contains("level.addFreshEntity(ejected)") || !cauldron.contains("inputLockTicks <= 0") || wine.width != 16 || wine.height != 16 || bordeauxPixels < 24) { - throw new GradleException("It's Alive alpha.28 culinary reliability contract is incomplete: comparators=${incompleteComparators}, bordeauxPixels=${bordeauxPixels}") + throw new GradleException("It's Alive alpha.28 culinary reliability contract is incomplete: comparators=${incompleteComparators}, observers=${incompleteObserverUpdates}, bordeauxPixels=${bordeauxPixels}") } } } diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java index 12a814d..ed554df 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java @@ -66,9 +66,10 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock 3, 0.22, 0.02, 0.22, 0.015 ); } - if (state.hasProperty(CookingCauldronBlock.LIT) - && state.getValue(CookingCauldronBlock.LIT) != heated) { - level.setBlock(pos, state.setValue(CookingCauldronBlock.LIT, heated), Block.UPDATE_CLIENTS); + BlockState currentState = cauldron.getBlockState(); + if (currentState.hasProperty(CookingCauldronBlock.LIT) + && currentState.getValue(CookingCauldronBlock.LIT) != heated) { + level.setBlock(pos, currentState.setValue(CookingCauldronBlock.LIT, heated), Block.UPDATE_ALL); } } @@ -300,7 +301,7 @@ public final class CookingCauldronBlockEntity extends AbstractTimedCulinaryBlock if (state.hasProperty(CookingCauldronBlock.MIXTURE) && state.getValue(CookingCauldronBlock.MIXTURE) != mixture) { level.setBlock(worldPosition, state.setValue(CookingCauldronBlock.MIXTURE, mixture), - Block.UPDATE_CLIENTS); + Block.UPDATE_ALL); } } diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FryingPanBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FryingPanBlockEntity.java index 7aef750..0e548bc 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FryingPanBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FryingPanBlockEntity.java @@ -46,7 +46,7 @@ public final class FryingPanBlockEntity extends AbstractTimedCulinaryBlockEntity pan.resetAssembly(); } if (state.hasProperty(FryingPanBlock.LIT) && state.getValue(FryingPanBlock.LIT) != heated) { - level.setBlock(pos, state.setValue(FryingPanBlock.LIT, heated), Block.UPDATE_CLIENTS); + level.setBlock(pos, state.setValue(FryingPanBlock.LIT, heated), Block.UPDATE_ALL); } } diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/KitchenOvenBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/KitchenOvenBlockEntity.java index 8b4ec89..cfea489 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/KitchenOvenBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/KitchenOvenBlockEntity.java @@ -76,7 +76,7 @@ public final class KitchenOvenBlockEntity extends AbstractTimedCulinaryBlockEnti boolean lit = oven.burnTime > 0; if (state.hasProperty(KitchenOvenBlock.LIT) && state.getValue(KitchenOvenBlock.LIT) != lit) { - level.setBlock(pos, state.setValue(KitchenOvenBlock.LIT, lit), Block.UPDATE_CLIENTS); + level.setBlock(pos, state.setValue(KitchenOvenBlock.LIT, lit), Block.UPDATE_ALL); } if (lit && oven.burnTime % 20 == 0) { oven.sync(); diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java index 3e18329..a9398c7 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/FoodPressBlockEntity.java @@ -252,7 +252,7 @@ public final class FoodPressBlockEntity extends BlockEntity implements WorldlyCo || state.getValue(FoodPressBlock.LEVEL) != visibleLevel)) { level.setBlock(worldPosition, state .setValue(FoodPressBlock.CONTENTS, content) - .setValue(FoodPressBlock.LEVEL, visibleLevel), Block.UPDATE_CLIENTS); + .setValue(FoodPressBlock.LEVEL, visibleLevel), Block.UPDATE_ALL); } } diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java index e2bd1c7..276926b 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/machine/RedstoneMillBlockEntity.java @@ -249,7 +249,7 @@ public final class RedstoneMillBlockEntity extends BlockEntity implements Worldl BlockState state = getBlockState(); boolean lit = burnTime > 0; if (state.hasProperty(RedstoneMillBlock.LIT) && state.getValue(RedstoneMillBlock.LIT) != lit) { - level.setBlock(worldPosition, state.setValue(RedstoneMillBlock.LIT, lit), Block.UPDATE_CLIENTS); + level.setBlock(worldPosition, state.setValue(RedstoneMillBlock.LIT, lit), Block.UPDATE_ALL); } } diff --git a/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json b/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json index 2b2fc39..eb047dc 100644 --- a/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json +++ b/pack/migrations/26.2.0-alpha.195-culinary-mailbox-grapple-fixes-to-alpha.196.json @@ -38,6 +38,11 @@ "loaded_or_working": "1-14", "result_ready": 15 }, + "observer_contract": { + "behavior": "vanilla_block_state_changes", + "observable_states": ["contents", "level", "mixture", "lit"], + "inventory_or_progress_ticks": false + }, "cauldron_remainders_ejected": true, "cauldron_input_lock_ticks": 20 }, -- 2.54.0