diff --git a/README.md b/README.md index 43171fa..8e9c36c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ La [carte native beta.005](docs/atlas-beta005.md) prend le relais après l’étude de Xaero et des alternatives libres. +## beta.115 — Fourneau : dépôts et chauffe + +Correctif du décalage par trois cases, combustible stable et progression +par cuisson. [Diagnostic et essais](docs/fourneau-transfers-beta115.md). +Préparation locale pour la prochaine livraison commune avec l’atelier. + ## beta.114 — Animaux portés et fuite en vol Un animal volant porté entraîne son porteur après un coup. La trajectoire diff --git a/docs/fourneau-transfers-beta115.md b/docs/fourneau-transfers-beta115.md new file mode 100644 index 0000000..f88a880 --- /dev/null +++ b/docs/fourneau-transfers-beta115.md @@ -0,0 +1,74 @@ +# beta.115 — Dépôts et chauffe du Fourneau + +Ticket sur `codex/fourneau-transfers-beta115`, après le signalement de piles +qui se réalignent pendant les dépôts rapides avec Maj, dans Ingrédients et +Combustibles, et d'une chauffe difficile à comprendre. + +## Diagnostic + +La méthode `mayPlace` de la case serveur utilisait `index`, masqué par le +champ hérité `Slot.index` (position affichée). Elle validait donc successivement +une entrée, un combustible et une sortie, au lieu des 27 entrées ou des +27 combustibles. Le client acceptait le dépôt puis le serveur le refusait. +Le test reproduit le refus d’un déplacement de la case 1 vers la case 23. +Le correctif utilise explicitement `physicalIndex` pour la validation. + +## Comportement + +Les transferts Maj dans les vues complètes du Fourneau sont anticipés sur le +client, avec les mêmes destinations et règles de combustible que le serveur. +Les cases se remplissent dans l'ordre, après fusion des piles compatibles. +Un dépôt manuel conserve la case choisie ; aucun tri périodique des ingrédients +n'est ajouté. Les recherches masquent une partie du stock : leurs transferts +entrants continuent d'attendre la réponse du serveur, qui connaît tout le stock. + +La chauffe emprunte du combustible à une autre case uniquement pendant le tick +natif. La pile non brûlée retourne immédiatement à sa case d'origine, avec tous +ses composants. Les restes natifs restent associés à leur cuisson : un seau de +lave devient un seau, puis un seau d'eau avec une éponge mouillée. + +Les 27 entrées ont chacune une barre et une infobulle : cuisson en cours, +manque de combustible avec progression conservée, résultat bloqué, absence de +recette. Le panneau indique le nombre de cuissons et la réserve allumée en +secondes de travail cumulées. Ce n'est pas une durée réelle restante : les +cuissons parallèles se partagent la réserve. « Fonctionnement » explique les +onglets, les gestes et le rendement normal du combustible en FR/EN. + +Les recettes, résultats, expérience et règles de trémies restent natifs. +Aucun changement de format, de monde ou de génération ; les 27 fours physiques +conservent l'unique copie des objets et de leurs temps de chauffe. + +## Vérifications + +Le test natif `Furnace115ClientChecks` passe avec un serveur intégré, sur un +nouveau monde plat de graine 115 : vrai glisser avec Maj sur neuf piles nommées, +remplissage consécutif, déplacement manuel vers la case 23 et répartition native +sur six cases de colonnes différentes. Les 27 cases de chaque onglet valident +leur rôle physique. Le combustible nommé conserve sa position et ses composants. + +La cuisson parallèle, ses états synchronisés, la progression conservée à court +de combustible, les 27 cuissons ravitaillées, le seau de lave et l'éponge, +l'extraction des résultats et la sauvegarde native passent. Captures FR/EN +relues dans `build/furnace115-evidence/`. Marqueur `FURNACE115_PASS` dans +`build/furnace115-client.log`. + +`check build assemblePack assembleTestPack -x :sanctuary:runGameTest` réussit +en 2 min 29 s, 125 tâches. Le panneau d'aide reçoit ensuite une dernière +vérification client et une reconstruction ; la livraison commune beta.116 +relancera les contrôles complets sur l'ensemble des changements. + +Le GameTest dédié reste exclu conformément au refus antérieur de son EULA. +Pas d'essai Windows ou de réseau distant avec latence simulée. Aucun monde +personnel ouvert. Cette étape est préparée localement pour la livraison commune +avec les matériaux de sculpture ; le canal publié reste beta.114. + +Archives locales vérifiées (même JAR Sanctuary, sources concordantes, aucune +classe de test ni changement de production extérieur au ticket) : + +- `Sanctuary-beta.115.mrpack` : 10184430 octets, SHA-256 + `8fcac63b1bf5a01f680fb46666924705db3a2675e102037278e9d650d04cbf22`. +- `Sanctuary-Test-beta.115.mrpack` : 10203354 octets, SHA-256 + `5b98fdb8842690158a20b422d6aa5496d74e572b3429498ffb25476d24e87f8a`. +- JAR Sanctuary : `2066d3191d7ee27904927fab269d8160136f048742c3223713e8ed4fe90dd8f5`. + +Reçu : `build/furnace115-artifact.json`. diff --git a/gradle.properties b/gradle.properties index 7803055..5970bf5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,8 +9,8 @@ loom_version=1.17.20 fabric_api_version=0.160.5+26.3 # Release counter: beta.001, .002, .003, ... (see docs/versioning.md). -mod_version=beta.114 -pack_version=beta.114 +mod_version=beta.115 +pack_version=beta.115 resource_pack_version=beta.090 maven_group=fr.koka.sanctuary jei_version=30.32.0-sanctuary.3 diff --git a/mods/sanctuary/build.gradle b/mods/sanctuary/build.gradle index ec7b230..a1d9a5e 100644 --- a/mods/sanctuary/build.gradle +++ b/mods/sanctuary/build.gradle @@ -93,6 +93,9 @@ tasks.named('runGameTest') { } tasks.named('processGametestResources') { + def furnace115Client = providers.gradleProperty('sanctuaryFurnace115ClientTests').map { it.toBoolean() }.getOrElse(false) + inputs.property('sanctuaryFurnace115ClientTests', furnace115Client) + if (furnace115Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.Furnace115ClientChecks') } def carryPanic114Client = providers.gradleProperty('sanctuaryCarryPanic114ClientTests').map { it.toBoolean() }.getOrElse(false) inputs.property('sanctuaryCarryPanic114ClientTests', carryPanic114Client) if (carryPanic114Client) filter { line -> line.replace('fr.koka.sanctuary.gametest.SanctuaryClientRenderTests', 'fr.koka.sanctuary.client.CarryPanic114ClientChecks') } diff --git a/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Furnace115ClientChecks.java b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Furnace115ClientChecks.java new file mode 100644 index 0000000..f13b4b0 --- /dev/null +++ b/mods/sanctuary/src/clientGametest/java/fr/koka/sanctuary/client/Furnace115ClientChecks.java @@ -0,0 +1,103 @@ +package fr.koka.sanctuary.client; + +import com.mojang.blaze3d.platform.InputConstants; +import fr.koka.sanctuary.gametest.Starter039TestSupport; +import fr.koka.sanctuary.multiblock.*; +import fr.koka.sanctuary.mixin.MultiblockFurnaceAccess; +import fr.koka.sanctuary.mixin.client.AbstractContainerScreenAccessor; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.gametest.v1.*; +import net.fabricmc.fabric.api.client.gametest.v1.context.ClientGameTestContext; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; +import net.minecraft.client.input.*; +import net.minecraft.core.*; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.item.*; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.*; + +/** Native quick-transfer, arbitrary slot placement, shared cooking and synchronized feedback. */ +public final class Furnace115ClientChecks implements FabricClientGameTest { + private static final BlockPos ORIGIN=new BlockPos(2,1,2); + private static MultiblockData.Machine oven; + public void runTest(ClientGameTestContext c){ + var intro=new AtomicBoolean(true); + c.runOnClient(m->{m.options.renderDistance().set(6);m.options.simulationDistance().set(5);m.options.pauseOnLostFocus=false;IntroClient.setQuickTestIntro(true);}); + ClientTickEvents.END_CLIENT_TICK.register(m->{if(intro.get()&&m.gui.screen() instanceof HelloWorldScreen screen){ + Starter039TestSupport.selectFirst(screen); + screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getClass().getSimpleName().equals("ColorButton")).findFirst().orElseThrow().onPress(enter()); + screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getContents() instanceof net.minecraft.network.chat.contents.TranslatableContents t&&t.getKey().equals("sanctuary.progression.enter")).findFirst().orElseThrow().onPress(enter());intro.set(false); + }}); + try(var world=c.worldBuilder().adjustSettings(s->{s.setName("Furnace115 "+UUID.randomUUID());s.setSeed("115");s.setAllowCommands(true);s.setGameMode(WorldCreationUiState.SelectedGameMode.SURVIVAL);s.setWorldType(s.getNormalPresetList().stream().filter(e->e.preset()!=null&&e.preset().getRegisteredName().equals("sanctuary_test:flat")).findFirst().orElseThrow());}).create()){ + c.waitFor(m->ProgressionClient.active()&&m.player!=null&&m.gui.screen()==null&&!IntroSpawnFlash.active(),1200);var server=world.getServer(); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();p.getAbilities().invulnerable=true;p.onUpdateAbilities();p.getInventory().clearContent();p.teleportTo(1.5,1,1.5); + for(int x=-2;x<8;x++)for(int z=-2;z<8;z++)p.level().setBlockAndUpdate(new BlockPos(x,0,z),Blocks.STONE.defaultBlockState()); + oven=new MultiblockData.Machine(UUID.randomUUID(),ORIGIN,true,Direction.NORTH);for(var pos:oven.members())p.level().setBlockAndUpdate(pos,Blocks.FURNACE.defaultBlockState());check(Multiblocks.assemble(p,oven),"Assemble"); + for(int i=0;i<9;i++){var stack=new ItemStack(Items.RAW_IRON,8);stack.set(DataComponents.CUSTOM_NAME,Component.literal("Batch "+i));p.getInventory().setItem(i,stack);}Multiblocks.open(p,oven,ORIGIN,0,"",0); + }); + c.waitFor(m->m.gui.screen() instanceof MultiblockScreen&&m.player.containerMenu.getSlot(54).hasItem(),200); + // One native Shift-drag crosses nine hotbar stacks, with no mouse release between them. + c.getInput().holdKey(o->o.keyShift);move(c,54);c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),1)); + for(int i=55;i<63;i++)move(c,i); + c.runOnClient(m->m.mouseHandler.onButton(m.getWindow().handle(),new MouseButtonInfo(InputConstants.MOUSE_BUTTON_LEFT,InputConstants.MOD_SHIFT),0));c.getInput().releaseKey(o->o.keyShift); + c.waitTicks(8);c.takeScreenshot("furnace115-inputs"); + c.waitFor(m->m.player.containerMenu.getSlot(8).hasItem()&&!m.player.containerMenu.getSlot(62).hasItem(),150); + c.runOnClient(m->{for(int i=0;i<9;i++)check(m.player.containerMenu.getSlot(i).getItem().getHoverName().getString().equals("Batch "+i),"Shift-drag fills consecutive UI slots, no three-column gaps");}); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);for(int i=0;i<9;i++)check(stock.getItem(i*3).getCount()==8,"Physical inputs correspond to visible slots");}); + // Move an ingredient to a distant, arbitrary slot using real inventory packets. + click(c,1,0,ContainerInput.PICKUP);click(c,23,0,ContainerInput.PICKUP);c.waitTicks(10); + c.runOnClient(m->{check(m.player.containerMenu.getSlot(1).getItem().isEmpty()&&m.player.containerMenu.getSlot(23).getItem().getCount()==8,"Manual placement remains in chosen cell: "+m.player.containerMenu.slots.stream().filter(slot->slot.hasItem()).map(slot->slot.index+":"+slot.getItem()).toList()+", cursor="+m.player.containerMenu.getCarried());}); + click(c,23,0,ContainerInput.PICKUP); + click(c,-999,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(0,0),ContainerInput.QUICK_CRAFT); + for(int i:new int[]{1,9,10,13,14,22})click(c,i,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(1,0),ContainerInput.QUICK_CRAFT); + click(c,-999,net.minecraft.world.inventory.AbstractContainerMenu.getQuickcraftMask(2,0),ContainerInput.QUICK_CRAFT);click(c,23,0,ContainerInput.PICKUP);c.waitTicks(6); + c.runOnClient(m->{for(int i:new int[]{1,9,10,13,14,22})check(m.player.containerMenu.getSlot(i).getItem().getCount()==1,"Native drag distribution keeps every chosen column");check(m.player.containerMenu.getSlot(23).getItem().getCount()==2&&m.player.containerMenu.getCarried().isEmpty(),"Distributed count conserved");}); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);check(stock.getItem(69).getCount()==2&&stock.getItem(3).getCount()==1,"Manual distribution matches server");for(int i=0;i<27;i++)check(p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.RAW_IRON)),"Every visible input validates its physical input slot, not Slot.index");reset(stock);stock.setItem(0,new ItemStack(Items.RAW_IRON,4));stock.setItem(18,new ItemStack(Items.RAW_GOLD,4)); + var coal=new ItemStack(Items.COAL,5);coal.set(DataComponents.CUSTOM_NAME,Component.literal("Stationary fuel"));stock.setItem(79,coal); + for(int tick=0;tick<205;tick++)MultiblockCooking.tick(p.level(),oven); + check(stock.getItem(79).getCount()==4&&stock.getItem(79).getHoverName().getString().equals("Stationary fuel"),"Named unburnt fuel stays in original cell"); + for(int i=1;i<79;i+=3)check(stock.getItem(i).isEmpty(),"No displaced coal in other slots"); + check(stock.getItem(2).is(Items.IRON_INGOT)&&stock.getItem(20).is(Items.GOLD_INGOT),"Shared heat cooks both arbitrary lanes"); + Multiblocks.open(p,oven,ORIGIN,0,"",0); + }); + c.waitTicks(6);c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.cooking==2&&menu.heat>0&&menu.laneState(6)==MultiblockMenu.COOKING,"Real per-lane feedback synchronizes");});c.takeScreenshot("furnace115-cooking"); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);reset(stock); + stock.setItem(0,new ItemStack(Items.RAW_IRON));stock.setItem(3,new ItemStack(Items.DIAMOND));stock.setItem(6,new ItemStack(Items.RAW_GOLD));stock.setItem(8,new ItemStack(Items.GOLD_INGOT,64)); + var a=(MultiblockFurnaceAccess)stock.cell(0);a.sanctuary$progress(80);a.sanctuary$total(200);p.containerMenu.broadcastChanges(); + });c.waitTicks(6); + c.runOnClient(m->{var menu=(MultiblockMenu)m.player.containerMenu;check(menu.laneState(0)==MultiblockMenu.NO_FUEL&&menu.laneProgress(0)==40,"Paused progress is visible and preserved");check(menu.laneState(1)==MultiblockMenu.NO_RECIPE,"Invalid recipe explained");check(menu.laneState(2)==MultiblockMenu.BLOCKED,"Blocked output explained");check(menu.cooking==0&&menu.heat==0,"Cold furnace clearly shown");});c.takeScreenshot("furnace115-paused"); + server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();cookingRegressions(p);var stock=new MultiblockContainer(p.level(),oven);reset(stock); + for(int i=0;i<9;i++){var stack=new ItemStack(Items.COAL,5);stack.set(DataComponents.CUSTOM_NAME,Component.literal("Fuel "+i));p.getInventory().setItem(i,stack);}Multiblocks.open(p,oven,ORIGIN,0,"",1); + });c.waitFor(m->m.player.containerMenu instanceof MultiblockMenu menu&&menu.view.section()==1&&menu.getSlot(54).hasItem(),100); + // No cooking in this fixture: compare the predicted UI in the same call, before any server reply. + c.runOnClient(m->{var menu=m.player.containerMenu;for(int i=0;i<9;i++){ + m.gameMode.handleContainerInput(menu.containerId,54+i,0,ContainerInput.QUICK_MOVE,m.player); + check(menu.getSlot(54+i).getItem().isEmpty()&&menu.getSlot(i).getItem().getCount()==5,"Immediate Shift prediction uses consecutive fuel cells");} + check(!menu.getSlot(9).mayPlace(new ItemStack(Items.DIAMOND)),"Client rejects invalid fuel like server");}); + c.waitTicks(8);server.runOnServer(s->{var p=s.getPlayerList().getPlayers().getFirst();var stock=new MultiblockContainer(p.level(),oven);for(int i=0;i<9;i++)check(stock.getItem(i*3+1).getCount()==5,"Fuel server mapping matches prediction");for(int i=0;i<27;i++){check(p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.COAL)),"All 27 fuel cells accept fuel");check(!p.containerMenu.getSlot(i).mayPlace(new ItemStack(Items.DIAMOND)),"All 27 fuel cells refuse other items");}});c.takeScreenshot("furnace115-fuel"); + c.runOnClient(m->{var screen=(MultiblockScreen)m.gui.screen();screen.children().stream().filter(Button.class::isInstance).map(Button.class::cast).filter(b->b.getMessage().getString().equals(Multiblocks.text("how_to_cook").getString())).findFirst().orElseThrow().onPress(enter());check(screen.helpVisible(),"Help button opens its explanation");});c.waitTicks(5);c.takeScreenshot("furnace115-help"); + var reload=c.computeOnClient(m->{m.getLanguageManager().setSelected("fr_fr");return m.reloadResourcePacks();});c.waitFor(m->reload.isDone()&&m.gui.overlay()==null,1200);reload.join();c.waitTicks(3);c.takeScreenshot("furnace115-help-fr"); + System.out.println("FURNACE115_PASS native Shift-drag, consecutive input/fuel mapping, immediate prediction, arbitrary placement, stationary named fuel, heat/output/pause/recipe feedback, fuel budget, refuelling, buckets, save and result extraction"); + }finally{intro.set(false);} + } + private static void cookingRegressions(ServerPlayer p){ + var stock=new MultiblockContainer(p.level(),oven);reset(stock);stock.setItem(0,new ItemStack(Items.WET_SPONGE));stock.setItem(79,new ItemStack(Items.LAVA_BUCKET));for(int t=0;t<205;t++)MultiblockCooking.tick(p.level(),oven); + check(stock.getItem(2).is(Items.SPONGE)&&stock.getItem(1).is(Items.WATER_BUCKET)&&stock.getItem(79).isEmpty(),"Native lava and wet sponge remainder"); + reset(stock);for(int i=0;i<81;i+=3)stock.setItem(i,new ItemStack(Items.RAW_IRON));stock.setItem(79,new ItemStack(Items.COAL));for(int t=0;t<90;t++)MultiblockCooking.tick(p.level(),oven); + int partial=((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress();check(partial>0&&partial<200,"One coal cannot cook all 27 items");for(int t=0;t<30;t++)MultiblockCooking.tick(p.level(),oven);check(((MultiblockFurnaceAccess)stock.cell(0)).sanctuary$progress()==partial,"No lost paid progress when out of fuel"); + stock.setItem(79,new ItemStack(Items.COAL,4));for(int t=0;t<205;t++)MultiblockCooking.tick(p.level(),oven);int produced=0;for(int i=2;i<81;i+=3)produced+=stock.getItem(i).getCount();check(produced==27,"Refuelling completes all lanes exactly once"); + var f=(AbstractFurnaceBlockEntity)stock.cell(0);var restored=(AbstractFurnaceBlockEntity)BlockEntity.loadStatic(f.getBlockPos(),f.getBlockState(),f.saveWithFullMetadata(p.level().registryAccess()),p.level().registryAccess());check(((MultiblockFurnaceAccess)restored).sanctuary$heat()==((MultiblockFurnaceAccess)f).sanctuary$heat(),"Native heat survives save"); + Multiblocks.open(p,oven,ORIGIN,0,"",2);var menu=(MultiblockMenu)p.containerMenu;check(!menu.getSlot(0).mayPlace(new ItemStack(Items.STONE)),"Results refuse input");check(!menu.quickMoveStack(p,0).isEmpty()&&stock.getItem(2).isEmpty(),"Native result slot can extract"); + } + private static void reset(MultiblockContainer stock){stock.clearContent();for(int i=0;i<81;i+=3){var a=(MultiblockFurnaceAccess)stock.cell(i);a.sanctuary$heat(0);a.sanctuary$progress(0);}} + private static void click(ClientGameTestContext c,int slot,int button,ContainerInput type){c.runOnClient(m->m.gameMode.handleContainerInput(m.player.containerMenu.containerId,slot,button,type,m.player));} + private static void move(ClientGameTestContext c,int index){c.runOnClient(m->{var a=(AbstractContainerScreenAccessor)m.gui.screen();var slot=m.player.containerMenu.getSlot(index);var w=m.getWindow();double x=a.sanctuary$getLeftPos()+slot.x+8,y=a.sanctuary$getTopPos()+slot.y+8;double sx=(double)w.getScreenWidth()/w.getGuiScaledWidth(),sy=(double)w.getScreenHeight()/w.getGuiScaledHeight();m.mouseHandler.onMove(w.handle(),x*sx,y*sy,(x-m.mouseHandler.getScaledXPos(w))*sx,(y-m.mouseHandler.getScaledYPos(w))*sy);m.mouseHandler.handleAccumulatedMovement();});c.waitTick();} + private static KeyEvent enter(){return new KeyEvent(InputConstants.KEY_RETURN,0,0);} + private static void check(boolean ok,String message){if(!ok)throw new AssertionError(message);} +} diff --git a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/MultiblockScreen.java b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/MultiblockScreen.java index f34b9d1..83d3e3b 100644 --- a/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/MultiblockScreen.java +++ b/mods/sanctuary/src/main/java/fr/koka/sanctuary/client/MultiblockScreen.java @@ -11,6 +11,7 @@ public final class MultiblockScreen extends ContainerScreen { private EditBox search; private Button previous,next,apply; private double wheel; + private boolean showHelp; private final java.util.List