diff --git a/CHANGELOG.md b/CHANGELOG.md index 89bcffe..98ab14b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ 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.198` + +Modules modifiés : **It's Alive `0.0.0-alpha.30`** et **Sanctuary `0.0.0-alpha.114`**. Les huit autres modules restent inchangés. + +- limite chaque emplacement d'entrée du tonneau de fermentation à une seule unité ; +- refuse les doublons et les ingrédients qui ne peuvent plus compléter aucune recette de fermentation chargée ; +- bloque ainsi les hoppers dès qu'une recette complète est assemblée, au lieu d'absorber progressivement tout leur inventaire ; +- expose dans le `BlockState` du tonneau les transitions vide, un à quatre ingrédients et résultat prêt afin que les observateurs vanilla émettent une impulsion fiable ; +- conserve le comparateur pour lire le remplissage et l'état prêt sans générer d'impulsion à chaque tick de fermentation ; +- aligne le shift-clic de l’inventaire étendu sur vanilla : la hotbar alimente toutes les rangées de stockage débloquées, rangées 5 et 6 comprises, tandis que le stockage et l’overflow n’alimentent que la hotbar ; +- conserve les piles déjà présentes dans les anciennes sauvegardes, tous les IDs, les données It's Alive v1 et le protocole 3, sans migration destructive. + ## `26.2.0-alpha.197` Modules modifiés : **It's Alive `0.0.0-alpha.29`** et **Sanctuary `0.0.0-alpha.113`**. Les huit autres modules restent inchangés. diff --git a/README.md b/README.md index 5d974c1..64f9202 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ 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.197`**. Sanctuary passe en **`0.0.0-alpha.114`** ; It's Alive reste à **`0.0.0-alpha.29`** et les huit autres modules restent inchangés. +Version courante du pack local : **`26.2.0-alpha.198`**. It's Alive passe en **`0.0.0-alpha.30`** et Sanctuary en **`0.0.0-alpha.114`** ; les huit autres modules restent inchangés. -Le shift-clic de l’inventaire suit le comportement vanilla sur toutes les rangées débloquées : la hotbar alimente le stockage, rangées 5 et 6 comprises, et une rangée de stockage ou d’overflow n’alimente que la hotbar. +L’alpha.198 borne l’automatisation du tonneau de fermentation : chaque emplacement n’accepte qu’une unité utile à une recette encore réalisable, les doublons invalides sont refusés et les observateurs suivent les changements visibles du contenu sans impulsion de progression parasite. Elle intègre aussi la correction de Chris : le shift-clic de l’inventaire suit le comportement vanilla sur toutes les rangées débloquées, y compris les rangées 5 et 6, tandis qu’une rangée de stockage ou d’overflow n’alimente que la hotbar. L’alpha.197 ajoute un panneau communautaire de demandes de quêtes dans Sanctuary. Le demandeur renseigne son pseudonyme, un objectif, un type KILL/BRING/PAY, une quantité et une cible validée par le serveur ; un autre joueur prend la demande au clic droit et peut l’abandonner accroupi. Le panneau reste récupérable uniquement par son propriétaire ou un opérateur. La marmite It’s Alive regroupe désormais jusqu’à 64 objets strictement identiques dans une même pile logique et cuit un lot borné par l’ingrédient le plus rare et la capacité de sortie, afin de produire directement une pile de sel, de crème ou d’autres préparations. diff --git a/build.gradle b/build.gradle index 544e77a..05a3621 100644 --- a/build.gradle +++ b/build.gradle @@ -3427,7 +3427,7 @@ 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.197" + if (rootProject.pack_version != "26.2.0-alpha.198" || rootProject.sanctuary_version != "0.0.0-alpha.114" || rootProject.onlyfun_version != "0.0.0-alpha.19" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version @@ -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.197" + if (rootProject.pack_version != "26.2.0-alpha.198" || 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,7 +4121,7 @@ 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.197" + if (rootProject.pack_version != "26.2.0-alpha.198" || rootProject.onlyfun_version != "0.0.0-alpha.19" || rootProject.sanctuary_version != "0.0.0-alpha.114" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version @@ -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.197" - || rootProject.itsalive_version != "0.0.0-alpha.29" + if (rootProject.pack_version != "26.2.0-alpha.198" + || rootProject.itsalive_version != "0.0.0-alpha.30" || 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,7 +4215,7 @@ 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.197" + if (rootProject.pack_version != "26.2.0-alpha.198" || rootProject.onlyfun_version != "0.0.0-alpha.19" || rootProject.sanctuary_version != "0.0.0-alpha.114" || release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version @@ -4261,7 +4261,7 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") { 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.197" + if (rootProject.pack_version != "26.2.0-alpha.198" || migration.source?.pack_version != "26.2.0-alpha.195" || migration.target?.pack_version != "26.2.0-alpha.196" || migration.target?.modules != [ @@ -4279,10 +4279,10 @@ tasks.register("verifyCulinaryMailboxGrappleFixesRelease") { || 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.197" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.197.mrpack" - || prism.versionId != "26.2.0-alpha.197" - || !packwiz.contains('version = "26.2.0-alpha.197"')) { + || release.pack_version != "26.2.0-alpha.198" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.198.mrpack" + || prism.versionId != "26.2.0-alpha.198" + || !packwiz.contains('version = "26.2.0-alpha.198"')) { throw new GradleException("The alpha.196 culinary, mailbox and grapple release contract is incomplete") } } @@ -4307,9 +4307,9 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") { def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json")) def packwiz = file("pack/packwiz/pack.toml").text def sanctuaryManifest = new JsonSlurper().parse(file("sanctuary/src/main/resources/fabric.mod.json")) - if (rootProject.pack_version != "26.2.0-alpha.197" - || rootProject.itsalive_version != "0.0.0-alpha.29" - || rootProject.sanctuary_version != "0.0.0-alpha.113" + if (rootProject.pack_version != "26.2.0-alpha.198" + || rootProject.itsalive_version != "0.0.0-alpha.30" + || rootProject.sanctuary_version != "0.0.0-alpha.114" || migration.source?.pack_version != "26.2.0-alpha.196" || migration.target?.pack_version != "26.2.0-alpha.197" || migration.target?.modules != [itsalive: "0.0.0-alpha.29", sanctuary: "0.0.0-alpha.113"] @@ -4327,10 +4327,10 @@ tasks.register("verifyQuestRequestsCauldronBatchesRelease") { || migration.distribution?.jei?.target != "30.26.0.186" || migration.compatibility?.sanctuary_target_network_protocol != 19 || sanctuaryManifest.custom?.sanctuary26?.network_protocol != 19 - || release.pack_version != "26.2.0-alpha.197" - || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.197.mrpack" - || prism.versionId != "26.2.0-alpha.197" - || !packwiz.contains('version = "26.2.0-alpha.197"')) { + || release.pack_version != "26.2.0-alpha.198" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.198.mrpack" + || prism.versionId != "26.2.0-alpha.198" + || !packwiz.contains('version = "26.2.0-alpha.198"')) { throw new GradleException("The alpha.197 quest-request and cauldron-batch release contract is incomplete") } } @@ -4340,6 +4340,56 @@ tasks.named("verifyWorkspace") { dependsOn tasks.named("verifyQuestRequestsCauldronBatchesRelease") } +tasks.register("verifyFermentationBarrelAutomationRelease") { + group = "verification" + description = "Checks the complete Sanctuary 26.2.0-alpha.198 fermentation barrel release." + inputs.file(file("pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.json")) + inputs.file(file("pack/release.json")) + inputs.file(file("pack/prism/modrinth.index.json")) + inputs.file(file("pack/packwiz/pack.toml")) + inputs.file(file("pack/packwiz/index.toml")) + inputs.file(file("itsalive/src/main/resources/fabric.mod.json")) + doLast { + def migration = new JsonSlurper().parse(file( + "pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.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 + def packwizIndex = file("pack/packwiz/index.toml").text + def itsAliveManifest = new JsonSlurper().parse(file("itsalive/src/main/resources/fabric.mod.json")) + if (rootProject.pack_version != "26.2.0-alpha.198" + || rootProject.itsalive_version != "0.0.0-alpha.30" + || migration.source?.pack_version != "26.2.0-alpha.197" + || migration.target?.pack_version != "26.2.0-alpha.198" + || migration.target?.modules != [itsalive: "0.0.0-alpha.30", sanctuary: "0.0.0-alpha.114"] + || migration.fermentation_barrel?.maximum_units_per_input_slot != 1 + || migration.fermentation_barrel?.loaded_recipe_prefix_filter != true + || migration.fermentation_barrel?.observer_state?.vanilla_neighbor_updates != true + || migration.inventory_shift_click?.owner != "sanctuary" + || migration.inventory_shift_click?.hotbar_to_all_unlocked_storage_rows != true + || migration.inventory_shift_click?.storage_and_overflow_to_hotbar_only != true + || migration.inventory_shift_click?.shift_double_click_respects_inventory_groups != true + || migration.compatibility?.itsalive_data_version != 1 + || migration.compatibility?.itsalive_network_protocol != 3 + || migration.compatibility?.save_migration_required != false + || migration.compatibility?.registry_ids_changed != false + || migration.distribution?.packwiz_index_refreshed != true + || itsAliveManifest.custom?.sanctuary26?.data_version != 1 + || itsAliveManifest.custom?.sanctuary26?.network_protocol != 3 + || release.pack_version != "26.2.0-alpha.198" + || release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.198.mrpack" + || prism.versionId != "26.2.0-alpha.198" + || !packwiz.contains('version = "26.2.0-alpha.198"') + || !packwizIndex.contains('file = "mods/itsalive-0.0.0-alpha.30.jar"')) { + throw new GradleException("The alpha.198 fermentation barrel release contract is incomplete") + } + } +} + +tasks.named("verifyWorkspace") { + dependsOn tasks.named("verifyFermentationBarrelAutomationRelease") +} + tasks.named("check") { dependsOn tasks.named("verifyWorkspace") dependsOn tasks.named("verifyTransport") diff --git a/gradle.properties b/gradle.properties index 49a2a18..569d5d9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ 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.29 +itsalive_version=0.0.0-alpha.30 itsalive_lifecycle=active ouch_version=0.0.0-alpha.8 ouch_lifecycle=active @@ -30,5 +30,5 @@ onlyfun_version=0.0.0-alpha.19 onlyfun_lifecycle=active sanctuary_version=0.0.0-alpha.114 sanctuary_lifecycle=active -pack_version=26.2.0-alpha.197 +pack_version=26.2.0-alpha.198 maven_group=fr.koka99cab.sanctuary26 diff --git a/itsalive/FOOD_WIKI.md b/itsalive/FOOD_WIKI.md index 738e243..b6aed0b 100644 --- a/itsalive/FOOD_WIKI.md +++ b/itsalive/FOOD_WIKI.md @@ -220,6 +220,7 @@ Planche Dalle bois Planche - L'ingrédient ou le résultat est visible sur le couvercle et l'overlay indique le temps restant. - 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. +- Chaque emplacement d'entrée accepte une seule unité. L'automatisation refuse les doublons et tout ajout qui ne peut plus compléter une recette connue, puis se verrouille naturellement dès que la recette est complète. ### Redstone et comparateurs @@ -227,6 +228,8 @@ Les six postes — presse, moulin, poêle, marmite, four et tonneau — fourniss 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. +Le tonneau de fermentation expose également ses transitions de contenu : vide, un à quatre ingrédients acceptés, puis résultat prêt. Un observateur placé contre lui réagit à chacune de ces transitions, mais pas à chaque tick de macération. + ## Ingrédients de base connus Ces transformations sont disponibles sans page secrète. diff --git a/itsalive/build.gradle b/itsalive/build.gradle index 4b36f2c..702cf48 100644 --- a/itsalive/build.gradle +++ b/itsalive/build.gradle @@ -11,6 +11,14 @@ tasks.register("culinaryBatchingSmoke", JavaExec) { mainClass = "fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryBatchingSmoke" } +tasks.register("culinaryPartialMatchingSmoke", JavaExec) { + group = "verification" + description = "Checks shapeless recipe prefixes used by hopper-fed fermentation barrels." + dependsOn tasks.named("testClasses") + classpath = sourceSets.test.runtimeClasspath + mainClass = "fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryPartialMatchingSmoke" +} + tasks.named("test") { // Quantity contracts are executable smoke tasks, not JUnit engine tests. failOnNoDiscoveredTests = false @@ -409,7 +417,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.29" + if (project.version.toString() != "0.0.0-alpha.30" || climateMigration.target?.pack_version != "26.2.0-alpha.157" || climateMigration.target?.modules?.itsalive != "0.0.0-alpha.24" || !climateSource.contains("PlayerBlockBreakEvents.AFTER.register") @@ -1141,7 +1149,7 @@ tasks.register("verifyCulinaryReliability") { } } } - if (project.version.toString() != "0.0.0-alpha.29" + if (project.version.toString() != "0.0.0-alpha.30" || 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 @@ -1179,7 +1187,7 @@ tasks.register("verifyCauldronBatching") { def recipe = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java").text def cauldron = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/CookingCauldronBlockEntity.java").text def timed = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/AbstractTimedCulinaryBlockEntity.java").text - if (project.version.toString() != "0.0.0-alpha.29" + if (project.version.toString() != "0.0.0-alpha.30" || migration.target?.modules?.itsalive != "0.0.0-alpha.29" || migration.cauldron_batches?.logical_stack_limit != 64 || migration.cauldron_batches?.identity != "same_item_same_components" @@ -1198,9 +1206,53 @@ tasks.register("verifyCauldronBatching") { } } +tasks.register("verifyFermentationBarrelAutomation") { + group = "verification" + description = "Checks alpha.30 bounded hopper input and vanilla observer states for fermentation barrels." + inputs.file(rootProject.file( + "pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.json")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java")) + inputs.file(file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java")) + inputs.file(file("src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json")) + doLast { + def migration = new JsonSlurper().parse(rootProject.file( + "pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.json")) + def block = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/FermentationBarrelBlock.java").text + def barrel = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/block/entity/FermentationBarrelBlockEntity.java").text + def recipe = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java").text + def partialMatching = file("src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java").text + def blockstate = new JsonSlurper().parse( + file("src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json")) + if (project.version.toString() != "0.0.0-alpha.30" + || migration.source?.modules?.itsalive != "0.0.0-alpha.29" + || migration.target?.modules?.itsalive != "0.0.0-alpha.30" + || migration.fermentation_barrel?.maximum_units_per_input_slot != 1 + || migration.fermentation_barrel?.loaded_recipe_prefix_filter != true + || migration.fermentation_barrel?.further_input_rejected_when_recipe_complete != true + || migration.fermentation_barrel?.observer_state?.property != "contents" + || migration.fermentation_barrel?.observer_state?.progress_tick_updates != false + || migration.compatibility?.existing_input_stacks_preserved != true + || !block.contains('IntegerProperty.create("contents", 0, 5)') + || !block.contains("builder.add(FACING, CONTENTS)") + || !barrel.contains("getMaxStackSize(ItemStack stack)") + || !barrel.contains("!getItem(slot).isEmpty()") + || !barrel.contains("CulinaryRecipeTypes.acceptsPartial") + || !barrel.contains("state.setValue(FermentationBarrelBlock.CONTENTS, contents), Block.UPDATE_ALL") + || !recipe.contains("acceptsPartial(List stacks)") + || !partialMatching.contains("canAssignDistinctIngredients") + || blockstate.multipart?.size() != 6) { + throw new GradleException("It's Alive alpha.30 fermentation barrel automation contract is incomplete") + } + } +} + tasks.named("check") { dependsOn tasks.named("verifyItsAlive") dependsOn tasks.named("verifyCulinaryReliability") dependsOn tasks.named("verifyCauldronBatching") dependsOn tasks.named("culinaryBatchingSmoke") + dependsOn tasks.named("verifyFermentationBarrelAutomation") + dependsOn tasks.named("culinaryPartialMatchingSmoke") } 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 1ed2ddf..609fa7d 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 @@ -26,11 +26,14 @@ 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.level.block.state.properties.IntegerProperty; import net.minecraft.world.phys.BlockHitResult; /** World-time fermentation inventory with top/side input and bottom hopper output. */ public final class FermentationBarrelBlock extends BaseEntityBlock { public static final EnumProperty FACING = BlockStateProperties.FACING; + /** 0 empty, 1-4 accepted ingredients, 5 finished product. */ + public static final IntegerProperty CONTENTS = IntegerProperty.create("contents", 0, 5); private final Supplier> blockEntityType; private final MapCodec instanceCodec; @@ -41,7 +44,9 @@ public final class FermentationBarrelBlock extends BaseEntityBlock { super(properties); this.blockEntityType = blockEntityType; this.instanceCodec = MapCodec.unit(this); - registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + registerDefaultState(stateDefinition.any() + .setValue(FACING, Direction.NORTH) + .setValue(CONTENTS, 0)); } @Override @@ -173,6 +178,6 @@ public final class FermentationBarrelBlock extends BaseEntityBlock { @Override protected void createBlockStateDefinition(StateDefinition.Builder builder) { - builder.add(FACING); + builder.add(FACING, CONTENTS); } } 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 3b2e7d0..639ab6a 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 @@ -5,6 +5,8 @@ import fr.koka99cab.sanctuary26.itsalive.culinary.block.FermentationBarrelBlock; import fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryProcessRecipe; import fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryRecipeInput; import fr.koka99cab.sanctuary26.itsalive.culinary.recipe.CulinaryRecipeTypes; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -12,6 +14,7 @@ import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; @@ -41,6 +44,7 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain ServerLevel level, BlockPos pos, BlockState state, FermentationBarrelBlockEntity barrel ) { barrel.process(level); + barrel.syncObservableState(); } private void process(ServerLevel level) { @@ -84,7 +88,14 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain } public int insertIngredient(ItemStack source, int amount) { - return insert(source, amount, 0, INPUT_SLOTS, this::isFermentationIngredient); + if (source.isEmpty() || amount <= 0) return 0; + for (int slot = 0; slot < INPUT_SLOTS; slot++) { + if (!canPlaceItem(slot, source)) continue; + items.set(slot, source.copyWithCount(1)); + contentsChanged(slot); + return 1; + } + return 0; } private boolean isFermentationIngredient(ItemStack stack) { @@ -177,7 +188,20 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain @Override public boolean canPlaceItem(int slot, ItemStack stack) { - return slot >= 0 && slot < INPUT_SLOTS && isFermentationIngredient(stack); + if (slot < 0 || slot >= INPUT_SLOTS || !getItem(slot).isEmpty() + || hasFinishedProduct() || !isFermentationIngredient(stack) + || !(level instanceof ServerLevel serverLevel)) { + return false; + } + List candidate = new ArrayList<>(inputView(0, INPUT_SLOTS)); + candidate.set(slot, stack.copyWithCount(1)); + return CulinaryRecipeTypes.acceptsPartial( + serverLevel, CulinaryAppliance.FERMENTATION_BARREL, candidate); + } + + @Override + public int getMaxStackSize(ItemStack stack) { + return 1; } @Override @@ -204,6 +228,12 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain } } + @Override + protected void sync() { + super.sync(); + syncObservableState(); + } + private void resetFermentation() { if (startedAtGameTime >= 0 || activeRecipe != null || activeDuration != 0) { startedAtGameTime = -1L; @@ -213,6 +243,25 @@ public final class FermentationBarrelBlockEntity extends AbstractCulinaryContain } } + private void syncObservableState() { + if (level == null) return; + int contents = hasFinishedProduct() ? 5 : occupiedInputSlots(); + BlockState state = getBlockState(); + if (state.hasProperty(FermentationBarrelBlock.CONTENTS) + && state.getValue(FermentationBarrelBlock.CONTENTS) != contents) { + level.setBlock(worldPosition, + state.setValue(FermentationBarrelBlock.CONTENTS, contents), Block.UPDATE_ALL); + } + } + + private int occupiedInputSlots() { + int occupied = 0; + for (int slot = 0; slot < INPUT_SLOTS; slot++) { + if (!getItem(slot).isEmpty()) occupied++; + } + return occupied; + } + @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java new file mode 100644 index 0000000..f8d5576 --- /dev/null +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatching.java @@ -0,0 +1,30 @@ +package fr.koka99cab.sanctuary26.itsalive.culinary.recipe; + +/** Pure bipartite matching used to validate shapeless recipe prefixes. */ +public final class CulinaryPartialMatching { + private CulinaryPartialMatching() { + } + + public static boolean canAssignDistinctIngredients(boolean[][] compatibility) { + if (compatibility.length == 0) return true; + int ingredientCount = compatibility[0].length; + if (compatibility.length > ingredientCount) return false; + for (boolean[] row : compatibility) { + if (row.length != ingredientCount) { + throw new IllegalArgumentException("Compatibility matrix must be rectangular"); + } + } + return assign(compatibility, 0, new boolean[ingredientCount]); + } + + private static boolean assign(boolean[][] compatibility, int stackIndex, boolean[] usedIngredients) { + if (stackIndex >= compatibility.length) return true; + for (int ingredientIndex = 0; ingredientIndex < usedIngredients.length; ingredientIndex++) { + if (usedIngredients[ingredientIndex] || !compatibility[stackIndex][ingredientIndex]) continue; + usedIngredients[ingredientIndex] = true; + if (assign(compatibility, stackIndex + 1, usedIngredients)) return true; + usedIngredients[ingredientIndex] = false; + } + return false; + } +} diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java index f5d89fd..e322e67 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryProcessRecipe.java @@ -4,6 +4,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryAppliance; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -79,6 +80,23 @@ public final class CulinaryProcessRecipe implements Recipe return assignIngredient(0, stacks, new int[stacks.size()], assignment) ? assignment : null; } + /** True when every occupied slot can still become part of this recipe. */ + public boolean acceptsPartial(List stacks) { + List occupied = new ArrayList<>(); + for (ItemStack stack : stacks) { + if (!stack.isEmpty()) occupied.add(stack); + } + if (occupied.size() > ingredients.size()) return false; + boolean[][] compatibility = new boolean[occupied.size()][ingredients.size()]; + for (int stackIndex = 0; stackIndex < occupied.size(); stackIndex++) { + for (int ingredientIndex = 0; ingredientIndex < ingredients.size(); ingredientIndex++) { + compatibility[stackIndex][ingredientIndex] = + ingredients.get(ingredientIndex).test(occupied.get(stackIndex)); + } + } + return CulinaryPartialMatching.canAssignDistinctIngredients(compatibility); + } + private int[] matchingDistinctSlots(List stacks) { int[] assignment = new int[ingredients.size()]; Arrays.fill(assignment, -1); diff --git a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryRecipeTypes.java b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryRecipeTypes.java index f5deeb9..01f545c 100644 --- a/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryRecipeTypes.java +++ b/itsalive/src/main/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryRecipeTypes.java @@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.itsalive.culinary.recipe; import fr.koka99cab.sanctuary26.itsalive.ItsAliveMod; import fr.koka99cab.sanctuary26.itsalive.culinary.CulinaryAppliance; +import java.util.List; import java.util.Optional; import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; @@ -79,4 +80,18 @@ public final class CulinaryRecipeTypes { .anyMatch(recipe -> recipe.appliance() == appliance && recipe.ingredients().stream().anyMatch(ingredient -> ingredient.test(stack))); } + + /** True when all supplied stacks form a valid, shapeless prefix of one loaded recipe. */ + public static boolean acceptsPartial( + ServerLevel level, + CulinaryAppliance appliance, + List stacks + ) { + if (type == null) return false; + return level.getServer().getRecipeManager().getRecipes().stream() + .map(RecipeHolder::value) + .filter(CulinaryProcessRecipe.class::isInstance) + .map(CulinaryProcessRecipe.class::cast) + .anyMatch(recipe -> recipe.appliance() == appliance && recipe.acceptsPartial(stacks)); + } } diff --git a/itsalive/src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json b/itsalive/src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json index 43b03aa..31b5a39 100644 --- a/itsalive/src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json +++ b/itsalive/src/main/resources/assets/itsalive/blockstates/fermentation_barrel.json @@ -1,10 +1,10 @@ { - "variants": { - "facing=down": { "model": "itsalive:block/fermentation_barrel", "x": 180 }, - "facing=east": { "model": "itsalive:block/fermentation_barrel", "x": 90, "y": 90 }, - "facing=north": { "model": "itsalive:block/fermentation_barrel", "x": 90 }, - "facing=south": { "model": "itsalive:block/fermentation_barrel", "x": 90, "y": 180 }, - "facing=up": { "model": "itsalive:block/fermentation_barrel" }, - "facing=west": { "model": "itsalive:block/fermentation_barrel", "x": 90, "y": 270 } - } + "multipart": [ + { "when": { "facing": "down" }, "apply": { "model": "itsalive:block/fermentation_barrel", "x": 180 } }, + { "when": { "facing": "east" }, "apply": { "model": "itsalive:block/fermentation_barrel", "x": 90, "y": 90 } }, + { "when": { "facing": "north" }, "apply": { "model": "itsalive:block/fermentation_barrel", "x": 90 } }, + { "when": { "facing": "south" }, "apply": { "model": "itsalive:block/fermentation_barrel", "x": 90, "y": 180 } }, + { "when": { "facing": "up" }, "apply": { "model": "itsalive:block/fermentation_barrel" } }, + { "when": { "facing": "west" }, "apply": { "model": "itsalive:block/fermentation_barrel", "x": 90, "y": 270 } } + ] } diff --git a/itsalive/src/test/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatchingSmoke.java b/itsalive/src/test/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatchingSmoke.java new file mode 100644 index 0000000..3fc723e --- /dev/null +++ b/itsalive/src/test/java/fr/koka99cab/sanctuary26/itsalive/culinary/recipe/CulinaryPartialMatchingSmoke.java @@ -0,0 +1,23 @@ +package fr.koka99cab.sanctuary26.itsalive.culinary.recipe; + +/** Pure recipe-prefix smoke for hopper-fed culinary appliances. */ +public final class CulinaryPartialMatchingSmoke { + private CulinaryPartialMatchingSmoke() { + } + + public static void main(String[] args) { + expect(true, new boolean[0][0], "empty prefix"); + expect(true, new boolean[][] {{true, false, false}}, "single valid ingredient"); + expect(true, new boolean[][] {{false, true}, {true, false}}, "shapeless assignment"); + expect(false, new boolean[][] {{true, false}, {true, false}}, "duplicate ingredient"); + expect(false, new boolean[][] {{false, false}}, "unrelated ingredient"); + expect(false, new boolean[][] {{true}, {true}}, "too many ingredients"); + } + + private static void expect(boolean expected, boolean[][] compatibility, String label) { + boolean actual = CulinaryPartialMatching.canAssignDistinctIngredients(compatibility); + if (actual != expected) { + throw new AssertionError(label + ": expected " + expected + ", got " + actual); + } + } +} diff --git a/pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.json b/pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.json new file mode 100644 index 0000000..addedac --- /dev/null +++ b/pack/migrations/26.2.0-alpha.197-fermentation-barrel-automation-to-alpha.198.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "source": { + "pack_version": "26.2.0-alpha.197", + "modules": { + "itsalive": "0.0.0-alpha.29", + "sanctuary": "0.0.0-alpha.113" + } + }, + "target": { + "pack_version": "26.2.0-alpha.198", + "modules": { + "itsalive": "0.0.0-alpha.30", + "sanctuary": "0.0.0-alpha.114" + } + }, + "fermentation_barrel": { + "id": "itsalive:fermentation_barrel", + "input_slots": 4, + "maximum_units_per_input_slot": 1, + "loaded_recipe_prefix_filter": true, + "duplicate_ingredients_rejected_unless_recipe_requires_them": true, + "further_input_rejected_when_recipe_complete": true, + "observer_state": { + "property": "contents", + "empty": 0, + "accepted_ingredients": "1-4", + "result_ready": 5, + "vanilla_neighbor_updates": true, + "progress_tick_updates": false + } + }, + "inventory_shift_click": { + "owner": "sanctuary", + "hotbar_to_all_unlocked_storage_rows": true, + "storage_and_overflow_to_hotbar_only": true, + "shift_double_click_respects_inventory_groups": true + }, + "distribution": { + "packwiz_index_refreshed": true + }, + "compatibility": { + "itsalive_data_version": 1, + "itsalive_network_protocol": 3, + "existing_input_stacks_preserved": true, + "save_migration_required": false, + "registry_ids_changed": false + } +} diff --git a/pack/packwiz/index.toml b/pack/packwiz/index.toml index 27a8dad..a3e7b9f 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 = "d8a2f1936e9c43ad36e3ffab5f791315970a66f374bf3f6fc387d56107203898" +hash = "70c23a4e4059eaf52f99e4a31585169c6599b4e33ad7554cddbbec536096cc64" [[files]] file = "mods/anotherworld-0.0.0-alpha.49.jar" -hash = "992c86f16136a9c013888ced20d863528d4f6c48244bb1d342c189b3ac4ad739" +hash = "69f6c36628503f5abf04ec3f9e8351d7b3e5468829573733590641a9078760d0" [[files]] file = "mods/events-0.0.0-alpha.0.jar" -hash = "a637271a50be68666a9eb86bd4a817f19d0592f81127b96746377cb2558394e0" +hash = "2cd283f8b85055a405947a41325f9780b98046b659a24a95c3279d96c97205d5" [[files]] file = "mods/fabric-api.pw.toml" @@ -31,7 +31,7 @@ metafile = true [[files]] file = "mods/iliketomoveit-0.0.0-alpha.13.jar" -hash = "d2b1c2c0769a3da4a7374ae8f39311531b48683dea277f8c3f084d77c50b795b" +hash = "b1b33c5fe7089b8d1baac4ce5fbd0ee386def1456b71af6f761d6ae800e61690" [[files]] file = "mods/iris.pw.toml" @@ -39,8 +39,8 @@ hash = "84a3cef87679c0d21dceb4641167dc0368b9a3593698f6a3632485e9b3a8d5da" metafile = true [[files]] -file = "mods/itsalive-0.0.0-alpha.29.jar" -hash = "2ccfc7b6aead6d00f5dc84a4627acba9ab9da70229c066799089dd98de826b90" +file = "mods/itsalive-0.0.0-alpha.30.jar" +hash = "57a7a91b47c5648710fa165a894c785c5958afe07f7d7f4e3ef4d0cc38cacd8b" [[files]] file = "mods/jei.pw.toml" @@ -49,23 +49,23 @@ metafile = true [[files]] file = "mods/masterkey-0.0.0-alpha.8.jar" -hash = "11f885417f45cc6fa752f561ecb475ad43a99071f88fd6a0f0521a292a93642e" +hash = "90803d3a67065ee02cca4fc32c8f18301e93e1ff2c4c8002feb5f28c7ec014da" [[files]] file = "mods/onlyfun-0.0.0-alpha.19.jar" -hash = "d9b5e7bfd96aac0bc2cfc6394249c86c74ad58d6685a3af37bb090682377a23a" +hash = "86c7f88ec348f678cb873d2386df38ca300928f4b0e7c350cae722ba42389986" [[files]] file = "mods/ouch-0.0.0-alpha.8.jar" -hash = "4152c0ecbdde7450c49c7e28367efa02ae666bb29dbb247ac90da3393e6485a0" +hash = "bd57342e13832fe67e7abeb0f2768170552fd868668a6f6940cd73f57dd7b37c" [[files]] file = "mods/redstoner-0.0.0-alpha.10.jar" -hash = "73262d1f88f820935ce3e59e1884bce66c6f1883bf5becbc9d2136a64d862011" +hash = "2259c2a982c14d29f27c7a0908aeee11b7156b2823202a200aa89af24703eb23" [[files]] -file = "mods/sanctuary-0.0.0-alpha.113.jar" -hash = "31155aad4c2030a5ede42b1b1d16cba191eaa0ade65051ff4d3973bf2db94c42" +file = "mods/sanctuary-0.0.0-alpha.114.jar" +hash = "7e45eb1a6756ed9a95d79d794b7c2e7a6918388fd9e5286160cd890d1dfaf0f6" [[files]] file = "mods/simple-voice-chat.pw.toml" diff --git a/pack/packwiz/pack.toml b/pack/packwiz/pack.toml index d08237a..66b4c89 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.197" +version = "26.2.0-alpha.198" pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "c191cd8a9c30d94babee9696a0d67268368f943c44fa26da45e1ba9996db03c7" +hash = "55cce7f3d86dbb8546626ea5f3ed5596efa656ab41f528d8b7ed963470b81219" [versions] fabric = "0.19.3" diff --git a/pack/prism/modrinth.index.json b/pack/prism/modrinth.index.json index 6b37d28..eca64f8 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.197", + "versionId": "26.2.0-alpha.198", "files": [ { "path": "mods/fabric-api-0.158.0+26.2.jar", diff --git a/pack/release.json b/pack/release.json index 5f27326..cd303e6 100644 --- a/pack/release.json +++ b/pack/release.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "pack_version": "26.2.0-alpha.197", + "pack_version": "26.2.0-alpha.198", "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.197.mrpack", + "primary_file": "sanctuary-26.2.0-alpha.198.mrpack", "shared_client_server_pack": true, "update_owner": "packwiz-installer", "check_on_launch": true, diff --git a/sanctuary/build.gradle b/sanctuary/build.gradle index c9684d3..94c88ba 100644 --- a/sanctuary/build.gradle +++ b/sanctuary/build.gradle @@ -3097,4 +3097,3 @@ tasks.named("check") { dependsOn tasks.named("celestialGraphSmoke") dependsOn tasks.named("questRequestModelSmoke") } -