From 49b2fc37371a4d4ed330cdffc79233e408dbf851 Mon Sep 17 00:00:00 2001
From: JohnBraham
Date: Fri, 23 Oct 2026 16:26:24 +0100
Subject: [PATCH 1/2] Port Power Advantage to Minecraft 1.13.2
---
.github/workflows/ci.yml | 70 +-
.github/workflows/codeql-analysis.yml | 50 +-
.github/workflows/validate-gradle-build.yml | 4 +-
.gitignore | 38 +-
CHANGELOG.txt | 13 +
README.md | 61 +-
build.gradle | 514 +-
docs/1.13/CHUNK-AND-DRILLS.md | 339 +
docs/1.13/CONNECTED-MIGRATION.md | 223 +
docs/1.13/CURRENT-QUALIFICATION.md | 216 +
docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md | 243 +
docs/1.13/DRILL-PROGRESS-REPAIR.md | 156 +
docs/1.13/ECLIPSE-METADATA.md | 40 +
docs/1.13/ENDURANCE.md | 229 +
docs/1.13/MANUFACTURING-PROGRESSION.md | 188 +
docs/1.13/MIGRATION-MATRIX.md | 196 +
docs/1.13/MIGRATION.md | 56 +
docs/1.13/OIL-TRANSFER.md | 65 +
.../PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md | 322 +
docs/1.13/PLAYER-AND-ENTITY-MIGRATION.md | 398 +
docs/1.13/RECIPE-DISCOVERY.md | 232 +
docs/1.13/TRANSFORMER-ORDERING.md | 243 +
gradle.properties | 39 +-
libs/RF-API.jar | Bin 7251 -> 0 bytes
libs/RF-API.zip | Bin 7160 -> 0 bytes
.../poweradvantage/PowerAdvantage.java | 743 --
.../compat/BaseMetalsCompat.java | 129 -
.../mcmoddev/poweradvantage/init/Blocks.java | 188 -
.../poweradvantage/init/Entities.java | 89 -
.../mcmoddev/poweradvantage/init/Fluids.java | 146 -
.../mcmoddev/poweradvantage/init/Fuels.java | 44 -
.../mcmoddev/poweradvantage/init/Items.java | 82 -
.../poweradvantage/init/ModSupport.java | 124 -
.../mcmoddev/poweradvantage/init/Recipes.java | 85 -
.../machines/conveyors/BlockConveyor.java | 127 -
.../conveyors/BlockConveyorFilter.java | 124 -
.../conveyors/TileEntityOreFilter.java | 24 -
.../conveyors/TileEntitySmeltableFilter.java | 14 -
.../FluidDischargeTileEntity.java | 273 -
.../fluidmachines/FluidDrainTileEntity.java | 275 -
.../fluidmachines/StorageTankBlock.java | 148 -
.../TerminalFluidPipeTileEntity.java | 177 -
.../machines/fluidmachines/package-info.java | 12 -
.../NotRecipeModeConditionFactory.java | 19 -
.../OreDictEmptyConditionFactory.java | 17 -
.../condition/RecipeModeConditionFactory.java | 19 -
.../util/FluidHandlerHelper.java | 26 -
.../poweradvantage/util/FluidIdHelper.java | 29 -
.../poweradvantage/api/ConduitBlock.java | 24 +-
.../cyano/poweradvantage/api/ConduitType.java | 2 +-
.../api/FluidCategoryRegistry.java | 87 +
.../cyano/poweradvantage/api/GUIBlock.java | 57 +-
.../api/PowerConnectorContext.java | 8 +-
.../poweradvantage/api/PoweredEntity.java | 26 +-
.../api/fluid/ColoredFluid.java | 44 +-
.../api/fluid/FluidConduitBlock.java | 10 +-
.../api/fluid/FluidNetworkApi.java | 86 +
.../api/fluid/FluidPoweredEntity.java | 52 +-
.../poweradvantage/api/fluid/FluidStack.java | 56 +
.../poweradvantage/api/fluid/FluidTank.java | 84 +
.../api/fluid/FluidTankInfo.java | 14 +
.../api/fluid/IAdvantageFluidHandler.java | 19 +
.../api/fluid/InteractiveFluidBlock.java | 124 +-
.../api/migration/LegacyConfigMigration.java | 178 +
.../api/migration/LegacyMigrationApi.java | 167 +
.../migration/LegacyOreSpawnConfigBridge.java | 273 +
.../modsupport/LightWeightPowerRegistry.java | 10 +-
.../api/modsupport/TileEntityConverter.java | 17 +-
.../api/modsupport/Wrappers.java | 224 +-
.../api/modsupport/rf/BlockRFConverter.java | 93 +-
.../modsupport/rf/TileEntityRFConverter.java | 233 +-
.../rf/TileEntityRFElectricityConverter.java | 6 +-
.../rf/TileEntityRFQuantumConverter.java | 6 +-
.../rf/TileEntityRFSteamConverter.java | 4 +-
.../techreborn/BlockTRConverter.java | 114 +-
.../techreborn/TileEntityTRConverter.java | 257 +-
.../TileEntityTRElectricityConverter.java | 6 +-
.../api/simple/BlockSimpleFluidConduit.java | 406 +-
.../api/simple/BlockSimpleFluidMachine.java | 4 +-
.../api/simple/BlockSimplePowerConduit.java | 467 +-
.../api/simple/BlockSimplePowerMachine.java | 106 +-
.../api/simple/SimpleMachineGUI.java | 28 +-
.../simple/TileEntitySimpleFluidMachine.java | 86 +-
.../simple/TileEntitySimplePowerMachine.java | 52 +-
.../mc/poweradvantage/PowerAdvantage.java | 147 +
.../moddev/mc}/poweradvantage/RecipeMode.java | 2 +-
.../mc}/poweradvantage/blocks/BlockFrame.java | 18 +-
.../blocks/BlockPowerSwitch.java | 60 +-
.../poweradvantage/blocks}/package-info.java | 4 +-
.../client/ClientOilRenderer.java | 64 +
.../compat/BaseMetalsCompat.java | 40 +
.../conduitnetwork/ConduitNetworkManager.java | 33 +-
.../conduitnetwork/ConduitRegistry.java | 56 +-
.../conduitnetwork/package-info.java | 2 +-
.../config/PowerAdvantageConfig.java | 96 +
.../poweradvantage/graphics/package-info.java | 2 +-
.../gui/FilteredFluidTankGUI.java | 12 +-
.../mc}/poweradvantage/gui/FluidTankGUI.java | 75 +-
.../gui/FunctionalCreativeTab.java | 18 +-
.../mc}/poweradvantage/gui/StillGUI.java | 8 +-
.../mc/poweradvantage/gui}/package-info.java | 4 +-
.../moddev/mc/poweradvantage/init/Blocks.java | 154 +
.../mc/poweradvantage/init/Entities.java | 88 +
.../moddev/mc/poweradvantage/init/Fluids.java | 158 +
.../moddev/mc/poweradvantage/init/Fuels.java | 32 +
.../moddev/mc}/poweradvantage/init/GUI.java | 10 +-
.../mc}/poweradvantage/init/ItemGroups.java | 17 +-
.../moddev/mc/poweradvantage/init/Items.java | 62 +
.../mc}/poweradvantage/init/Materials.java | 2 +-
.../mc/poweradvantage/init/ModSupport.java | 64 +
.../mc/poweradvantage/init/Recipes.java | 57 +
.../poweradvantage/init/TreasureChests.java | 10 +-
.../mc}/poweradvantage/init/Villages.java | 10 +-
.../mc}/poweradvantage/init/WorldGen.java | 2 +-
.../mc}/poweradvantage/init/package-info.java | 2 +-
.../poweradvantage/items/RotationTool.java | 25 +-
.../poweradvantage/items}/package-info.java | 4 +-
.../machines/conveyors/BlockConveyor.java | 52 +
.../conveyors/BlockConveyorFilter.java | 68 +
.../conveyors/TileEntityBlockFilter.java | 6 +-
.../conveyors/TileEntityConveyor.java | 69 +-
.../conveyors/TileEntityConveyorFilter.java | 28 +-
.../conveyors/TileEntityFoodFilter.java | 6 +-
.../conveyors/TileEntityFuelFilter.java | 6 +-
.../conveyors/TileEntityInventoryFilter.java | 7 +-
.../conveyors/TileEntityOreFilter.java | 19 +
.../conveyors/TileEntityOverflowFilter.java | 6 +-
.../conveyors/TileEntityPlantFilter.java | 4 +-
.../conveyors/TileEntitySmeltableFilter.java | 20 +
.../machines/conveyors/package-info.java | 4 +-
.../creative/InfiniteEnergyBlock.java | 7 +-
.../creative/InfiniteEnergyTileEntity.java | 17 +-
.../fluidmachines/FluidDischargeBlock.java | 49 +-
.../FluidDischargeTileEntity.java | 105 +
.../fluidmachines/FluidDrainBlock.java | 36 +-
.../fluidmachines/FluidDrainTileEntity.java | 110 +
.../fluidmachines/FluidPipeBlock.java | 8 +-
.../fluidmachines/MetalTankBlock.java | 5 +-
.../fluidmachines/MetalTankTileEntity.java | 23 +-
.../machines/fluidmachines/StillBlock.java | 40 +-
.../fluidmachines/StillTileEntity.java | 68 +-
.../fluidmachines/StorageTankBlock.java | 92 +
.../fluidmachines/StorageTankTileEntity.java | 14 +-
.../modsupport/TerminalFluidPipeBlock.java | 17 +-
.../TerminalFluidPipeTileEntity.java | 112 +
.../machines/fluidmachines/package-info.java | 12 +
.../mc}/poweradvantage/math/BlockPos4D.java | 8 +-
.../mc}/poweradvantage/math/Integer2D.java | 2 +-
.../mc/poweradvantage/math}/package-info.java | 2 +-
.../migration/AdvantageLegacyMigrations.java | 92 +
.../migration/LegacyWorldDataHook.java | 538 ++
.../mc}/poweradvantage/package-info.java | 2 +-
.../poweradvantage/recipe/MaterialTags.java | 42 +
.../mc/poweradvantage/recipe/PartsLoot.java | 65 +
.../recipe/RecipeConditions.java | 36 +
.../recipe/RecipeDiscovery.java | 35 +
.../poweradvantage/registry/FuelRegistry.java | 17 +-
.../registry/ITileEntityGUI.java | 8 +-
.../registry/MachineGUIRegistry.java | 67 +-
.../poweradvantage/registry/package-info.java | 2 +-
.../still/recipe/DistillationRecipe.java | 14 +-
.../recipe/DistillationRecipeRegistry.java | 45 +-
.../still/recipe/ExplicitFluidReference.java | 14 +-
.../registry/still/recipe/FluidReference.java | 6 +-
.../recipe/RegistryNameFluidReference.java | 13 +-
.../util/FluidContainerHelper.java | 56 +
.../util/FluidHandlerHelper.java | 18 +
.../mc/poweradvantage/util/FluidIdHelper.java | 27 +
.../poweradvantage/util/HashCodeHelper.java | 2 +-
.../poweradvantage/util/InventoryWrapper.java | 18 +-
.../mc}/poweradvantage/util/PowerHelper.java | 27 +-
.../util/ReversibleHashMap.java | 4 +-
.../poweradvantage/util/ReversibleMap.java | 2 +-
.../mc/poweradvantage/util}/package-info.java | 2 +-
src/main/resources/META-INF/coremods.json | 6 +
src/main/resources/META-INF/mods.toml | 40 +
.../blockstates/converter_rf_electricity.json | 27 +-
.../blockstates/converter_rf_quantum.json | 27 +-
.../blockstates/converter_rf_steam.json | 27 +-
.../blockstates/converter_tr_electricity.json | 27 +-
.../poweradvantage/blockstates/crude_oil.json | 8 +-
.../blockstates/fluid_discharge.json | 27 +-
.../blockstates/fluid_drain.json | 27 +-
.../blockstates/fluid_metal_tank.json | 31 +-
.../blockstates/fluid_pipe.json | 455 +-
.../blockstates/fluid_pipe_terminal.json | 455 +-
.../blockstates/fluid_storage_tank.json | 27 +-
.../blockstates/fluid_switch.json | 16 +-
.../blockstates/infinite_electricity.json | 27 +-
.../blockstates/infinite_quantum.json | 27 +-
.../blockstates/infinite_steam.json | 27 +-
.../blockstates/item_conveyor.json | 33 +-
.../blockstates/item_filter_block.json | 23 +-
.../blockstates/item_filter_food.json | 23 +-
.../blockstates/item_filter_fuel.json | 23 +-
.../blockstates/item_filter_inventory.json | 23 +-
.../blockstates/item_filter_ore.json | 23 +-
.../blockstates/item_filter_overflow.json | 23 +-
.../blockstates/item_filter_plant.json | 23 +-
.../blockstates/item_filter_smelt.json | 23 +-
.../blockstates/refined_oil.json | 6 +-
.../blockstates/steel_frame.json | 12 +-
.../poweradvantage/blockstates/still.json | 46 +-
.../assets/poweradvantage/lang/de_de.json | 77 +
.../assets/poweradvantage/lang/de_de.lang | 55 -
.../assets/poweradvantage/lang/en_au.json | 77 +
.../assets/poweradvantage/lang/en_au.lang | 54 -
.../assets/poweradvantage/lang/en_ca.json | 77 +
.../assets/poweradvantage/lang/en_ca.lang | 54 -
.../assets/poweradvantage/lang/en_en.json | 77 +
.../assets/poweradvantage/lang/en_en.lang | 54 -
.../assets/poweradvantage/lang/en_gb.json | 77 +
.../assets/poweradvantage/lang/en_gb.lang | 53 -
.../assets/poweradvantage/lang/en_pt.json | 77 +
.../assets/poweradvantage/lang/en_pt.lang | 53 -
.../assets/poweradvantage/lang/en_us.json | 75 +
.../assets/poweradvantage/lang/en_us.lang | 52 -
.../assets/poweradvantage/lang/es_es.json | 75 +
.../assets/poweradvantage/lang/es_es.lang | 56 -
.../assets/poweradvantage/lang/es_mx.json | 75 +
.../assets/poweradvantage/lang/es_mx.lang | 56 -
.../assets/poweradvantage/lang/it_it.json | 75 +
.../assets/poweradvantage/lang/it_it.lang | 55 -
.../assets/poweradvantage/lang/ja_jp.json | 75 +
.../assets/poweradvantage/lang/ja_jp.lang | 56 -
.../assets/poweradvantage/lang/ko_kr.json | 75 +
.../assets/poweradvantage/lang/ko_kr.lang | 56 -
.../assets/poweradvantage/lang/pt_br.json | 75 +
.../assets/poweradvantage/lang/pt_br.lang | 54 -
.../assets/poweradvantage/lang/zh_cn.json | 75 +
.../assets/poweradvantage/lang/zh_cn.lang | 57 -
.../models/block/converter_rf_quantum.json | 2 +-
.../models/block/crude_oil.json | 6 +
.../models/block/refined_oil.json | 6 +
.../models/block/still_off.json | 6 +-
.../poweradvantage/models/block/still_on.json | 6 +-
.../poweradvantage/models/item/crude_oil.json | 3 +
.../models/item/crude_oil_bucket.json | 6 +
.../models/item/refined_oil.json | 3 +
.../models/item/refined_oil_bucket.json | 6 +
.../poweradvantage/recipes/_factories.json | 7 -
.../recipes/fluid_discharge.json | 30 -
.../poweradvantage/recipes/fluid_drain.json | 30 -
.../recipes/fluid_metal_tank_iron_normal.json | 28 -
.../recipes/fluid_metal_tank_steel.json | 22 -
.../fluid_pipe_copper_apocalyptic.json | 25 -
.../recipes/fluid_pipe_copper_normal.json | 25 -
.../recipes/fluid_pipe_iron_apocalyptic.json | 25 -
.../recipes/fluid_pipe_iron_normal.json | 25 -
.../recipes/fluid_pipe_iron_tech.json | 25 -
.../recipes/fluid_pipe_lead_apocalyptic.json | 25 -
.../recipes/fluid_storage_tank.json | 22 -
.../poweradvantage/recipes/fluid_switch.json | 24 -
.../poweradvantage/recipes/item_conveyor.json | 26 -
.../recipes/item_filter_block.json | 24 -
.../recipes/item_filter_food.json | 23 -
.../recipes/item_filter_fuel.json | 24 -
.../recipes/item_filter_inventory.json | 23 -
.../recipes/item_filter_ore.json | 24 -
.../recipes/item_filter_overflow.json | 19 -
.../recipes/item_filter_plant.json | 24 -
.../recipes/item_filter_smelt.json | 23 -
.../poweradvantage/recipes/rotator_tool.json | 22 -
.../sprocket_bronze_nugget_tech_no_rod.json | 33 -
.../recipes/sprocket_rod_normal.json | 29 -
.../recipes/sprocket_rod_tech.json | 29 -
.../recipes/sprocket_stickwood_normal.json | 29 -
.../poweradvantage/recipes/steel_frame.json | 18 -
.../assets/poweradvantage/recipes/still.json | 23 -
.../coremods/poweradvantage_113_migration.js | 96 +
.../coremods/poweradvantage_material_tags.js | 28 +
.../coremods/poweradvantage_oil_renderer.js | 55 +
.../coremods/poweradvantage_parts_loot.js | 25 +
.../resources/data/forge/tags/items/bars.json | 7 +
.../data/forge/tags/items/bars/steel.json | 7 +
.../data/forge/tags/items/dusts/charcoal.json | 4 +
.../data/forge/tags/items/dusts/coal.json | 4 +
.../data/forge/tags/items/dusts/lead.json | 4 +
.../data/forge/tags/items/dusts/redstone.json | 6 +
.../data/forge/tags/items/dusts/silver.json | 4 +
.../data/forge/tags/items/dusts/tin.json | 4 +
.../data/forge/tags/items/frames/steel.json | 6 +
.../data/forge/tags/items/gears.json | 6 +
.../data/forge/tags/items/gears/steel.json | 6 +
.../data/forge/tags/items/gems/diamond.json | 6 +
.../data/forge/tags/items/gems/emerald.json | 6 +
.../data/forge/tags/items/governors.json | 4 +
.../data/forge/tags/items/ingots/brass.json | 4 +
.../data/forge/tags/items/ingots/copper.json | 4 +
.../data/forge/tags/items/ingots/gold.json | 6 +
.../data/forge/tags/items/ingots/iron.json | 6 +
.../data/forge/tags/items/ingots/lead.json | 4 +
.../data/forge/tags/items/ingots/nickel.json | 4 +
.../data/forge/tags/items/ingots/plastic.json | 6 +
.../forge/tags/items/ingots/refined_iron.json | 4 +
.../data/forge/tags/items/ingots/rubber.json | 4 +
.../data/forge/tags/items/ingots/steel.json | 4 +
.../data/forge/tags/items/ingots/zinc.json | 4 +
.../data/forge/tags/items/nuggets/bronze.json | 4 +
.../data/forge/tags/items/nuggets/copper.json | 4 +
.../data/forge/tags/items/nuggets/gold.json | 6 +
.../data/forge/tags/items/nuggets/iron.json | 6 +
.../data/forge/tags/items/nuggets/lead.json | 4 +
.../data/forge/tags/items/nuggets/tin.json | 4 +
.../data/forge/tags/items/pipes.json | 6 +
.../data/forge/tags/items/plastic.json | 6 +
.../data/forge/tags/items/plates/copper.json | 4 +
.../data/forge/tags/items/plates/iron.json | 4 +
.../data/forge/tags/items/plates/steel.json | 4 +
.../forge/tags/items/power_supply_units.json | 4 +
.../resources/data/forge/tags/items/rods.json | 4 +
.../data/forge/tags/items/rods/copper.json | 4 +
.../data/forge/tags/items/rods/wooden.json | 6 +
.../data/forge/tags/items/rubber.json | 4 +
.../data/forge/tags/items/sprockets.json | 6 +
.../forge/tags/items/sprockets/steel.json | 6 +
.../data/forge/tags/items/stone.json | 6 +
.../tags/items/storage_blocks/diamond.json | 6 +
.../tags/items/storage_blocks/steel.json | 4 +
.../recipes/bioplastic_from_starch.json | 32 +
.../recipes/converter_rf_electricity.json | 54 +
.../recipes/converter_rf_quantum.json | 54 +
.../recipes/converter_rf_steam.json | 54 +
.../converter_tr_electricity_iron.json | 43 +
...converter_tr_electricity_refined_iron.json | 43 +
.../advancements/recipes/fluid_discharge.json | 65 +
.../advancements/recipes/fluid_drain.json | 65 +
.../recipes/fluid_metal_tank_iron_normal.json | 43 +
.../recipes/fluid_metal_tank_steel.json | 43 +
.../fluid_pipe_copper_apocalyptic.json | 32 +
.../recipes/fluid_pipe_copper_normal.json | 32 +
.../recipes/fluid_pipe_iron_apocalyptic.json | 32 +
.../recipes/fluid_pipe_iron_normal.json | 32 +
.../recipes/fluid_pipe_iron_tech.json | 32 +
.../recipes/fluid_pipe_lead_apocalyptic.json | 32 +
.../recipes/fluid_storage_tank.json | 43 +
.../advancements/recipes/fluid_switch.json | 54 +
.../advancements/recipes/item_conveyor.json | 54 +
.../recipes/item_filter_block.json | 54 +
.../recipes/item_filter_food.json | 54 +
.../recipes/item_filter_fuel.json | 54 +
.../recipes/item_filter_inventory.json | 54 +
.../advancements/recipes/item_filter_ore.json | 54 +
.../recipes/item_filter_overflow.json | 43 +
.../recipes/item_filter_plant.json | 54 +
.../recipes/item_filter_smelt.json | 54 +
.../advancements/recipes/rotator_tool.json | 43 +
.../sprocket_bronze_nugget_tech_no_rod.json | 43 +
.../recipes/sprocket_rod_normal.json | 43 +
.../recipes/sprocket_rod_tech.json | 43 +
.../recipes/sprocket_stickwood_normal.json | 32 +
.../recipes/starch_from_potato.json | 43 +
.../advancements/recipes/steel_frame.json | 32 +
.../advancements/recipes/still.json | 54 +
.../loot_tables/chests/advantage_parts.json | 72 +
.../poweradvantage/orespawn/provider.json | 57 +
.../recipes/bioplastic_from_starch.json | 11 +
.../recipes/converter_rf_electricity.json | 31 +
.../recipes/converter_rf_quantum.json | 27 +
.../recipes/converter_rf_steam.json | 31 +
.../converter_tr_electricity_iron.json | 34 +
...converter_tr_electricity_refined_iron.json | 30 +
.../recipes/crushing_fluid_discharge.json | 20 +
.../recipes/crushing_fluid_drain.json | 20 +
.../recipes/crushing_fluid_storage_tank.json | 20 +
.../recipes/crushing_item_conveyor.json | 24 +
.../recipes/crushing_item_filter_block.json | 24 +
.../recipes/crushing_item_filter_food.json | 24 +
.../recipes/crushing_item_filter_fuel.json | 24 +
.../crushing_item_filter_inventory.json | 24 +
.../recipes/crushing_item_filter_ore.json | 24 +
.../crushing_item_filter_overflow.json | 24 +
.../recipes/crushing_item_filter_plant.json | 24 +
.../recipes/crushing_item_filter_smelt.json | 24 +
.../recipes/crushing_potato_starch.json | 21 +
.../recipes/fluid_discharge.json | 44 +
.../poweradvantage/recipes/fluid_drain.json | 44 +
.../recipes/fluid_metal_tank_iron_normal.json | 34 +
.../recipes/fluid_metal_tank_steel.json | 30 +
.../fluid_pipe_copper_apocalyptic.json | 28 +
.../recipes/fluid_pipe_copper_normal.json | 28 +
.../recipes/fluid_pipe_iron_apocalyptic.json | 28 +
.../recipes/fluid_pipe_iron_normal.json | 28 +
.../recipes/fluid_pipe_iron_tech.json | 28 +
.../recipes/fluid_pipe_lead_apocalyptic.json | 28 +
.../recipes/fluid_storage_tank.json | 30 +
.../poweradvantage/recipes/fluid_switch.json | 32 +
.../poweradvantage/recipes/item_conveyor.json | 34 +
.../recipes/item_filter_block.json | 24 +
.../recipes/item_filter_food.json | 23 +
.../recipes/item_filter_fuel.json | 24 +
.../recipes/item_filter_inventory.json | 23 +
.../recipes/item_filter_ore.json | 29 +
.../recipes/item_filter_overflow.json | 19 +
.../recipes/item_filter_plant.json | 23 +
.../recipes/item_filter_smelt.json | 23 +
.../poweradvantage/recipes/rotator_tool.json | 30 +
.../sprocket_bronze_nugget_tech_no_rod.json | 39 +
.../recipes/sprocket_rod_normal.json | 35 +
.../recipes/sprocket_rod_tech.json | 35 +
.../recipes/sprocket_stickwood_normal.json | 35 +
.../recipes/starch_from_potato.json | 11 +
.../poweradvantage/recipes/steel_frame.json | 23 +
.../data/poweradvantage/recipes/still.json | 28 +
src/main/resources/pack.mcmeta | 2 +-
.../api/fluid/FluidNetworkApiTest.java | 93 +
.../migration/LegacyConfigMigrationTest.java | 58 +
.../LegacyOreSpawnConfigBridgeTest.java | 65 +
.../ConduitNetworkManagerTest.java | 67 +
.../AdvantageLegacyMigrationsTest.java | 42 +
.../migration/LegacyEnchantmentsTest.java | 62 +
.../migration/LegacyEntitiesTest.java | 122 +
.../resources/ClientResourceContractTest.java | 120 +
.../resources/MaterialTagTransformerTest.java | 60 +
.../resources/OilRendererTransformerTest.java | 113 +
.../resources/PartsLootTransformerTest.java | 45 +
.../resources/RecipeUnlockContractTest.java | 131 +
.../resources/StarchRecipeTest.java | 46 +
.../orespawn-legacy/poweradvantage.json | 18 +
test-rig/README.md | 161 +
test-rig/build.gradle | 179 +
test-rig/client-resources/README.md | 44 +
.../client/ClientResourceProbe.java | 129 +
.../advantageworks/client/OilRenderProbe.java | 109 +
.../client/RecipeBookGroupingProbe.java | 169 +
.../client/TankVisualProbe.java | 124 +
.../resources/META-INF/mods.toml | 16 +
.../client-resources/resources/pack.mcmeta | 6 +
test-rig/client-resources/run.ps1 | 79 +
test-rig/eclipse/EclipseModelProbe.java | 36 +
test-rig/gradle.properties | 8 +
test-rig/migration-probe/build.gradle | 44 +
test-rig/migration-probe/settings.gradle | 4 +
.../migration/LegacyActors.java | 210 +
.../migration/LegacyConnected.java | 155 +
.../migration/LegacyDimensionEntities.java | 476 ++
.../advantageworks/migration/LegacyProbe.java | 241 +
test-rig/migration/README.md | 253 +
test-rig/migration/actors.py | 253 +
test-rig/migration/actors_native.py | 66 +
test-rig/migration/audit_actors.py | 93 +
test-rig/migration/audit_actors_native.py | 50 +
test-rig/migration/audit_connected.py | 157 +
.../migration/audit_connected_progress.py | 66 +
test-rig/migration/audit_dimensions.py | 104 +
test-rig/migration/audit_drill_restart.py | 85 +
test-rig/migration/audit_endurance.py | 65 +
.../migration/audit_extended_lifecycle.py | 91 +
test-rig/migration/audit_lifecycle.py | 82 +
test-rig/migration/audit_pipe_only.py | 84 +
test-rig/migration/audit_player_layout.py | 46 +
test-rig/migration/audit_respawn.py | 71 +
test-rig/migration/audit_results.py | 53 +
test-rig/migration/audit_transformer_order.py | 107 +
test-rig/migration/connected.py | 362 +
test-rig/migration/dimensions.py | 348 +
test-rig/migration/drill_restart.py | 119 +
test-rig/migration/endurance.py | 342 +
test-rig/migration/extended_lifecycle.py | 427 ++
test-rig/migration/lifecycle.py | 434 ++
test-rig/migration/matrix.py | 412 ++
test-rig/migration/oil_transfer.py | 154 +
test-rig/migration/partial_network_trace.py | 72 +
test-rig/migration/pin_dimension_profiles.py | 55 +
test-rig/migration/pin_drill_profiles.py | 100 +
test-rig/migration/pipe_only_lifecycle.py | 110 +
test-rig/migration/player_layout.py | 80 +
test-rig/migration/progression.py | 145 +
test-rig/migration/recipe_unlocks.py | 127 +
test-rig/migration/requirements.txt | 1 +
test-rig/migration/respawn.py | 153 +
test-rig/migration/test_actors.py | 75 +
test-rig/migration/test_audit_connected.py | 48 +
test-rig/migration/test_bridge_profiles.py | 24 +
test-rig/migration/test_connected.py | 115 +
test-rig/migration/test_connected_progress.py | 40 +
test-rig/migration/test_dimensions.py | 117 +
test-rig/migration/test_drill_restart.py | 47 +
test-rig/migration/test_endurance.py | 151 +
test-rig/migration/test_extended_lifecycle.py | 106 +
test-rig/migration/test_lifecycle.py | 86 +
test-rig/migration/test_matrix.py | 47 +
test-rig/migration/test_oil_transfer.py | 54 +
.../migration/test_pin_dimension_profiles.py | 62 +
.../migration/test_pipe_only_lifecycle.py | 54 +
test-rig/migration/test_progression.py | 32 +
test-rig/migration/test_recipe_unlocks.py | 32 +
test-rig/migration/test_respawn.py | 113 +
test-rig/migration/test_transformer_order.py | 181 +
.../migration/trace_connected_progress.py | 60 +
.../TransformerOrderBridge.java | 232 +
test-rig/migration/transformer_order.py | 463 ++
test-rig/profiles/dev-core-apocalyptic.json | 37 +
.../profiles/dev-core-tech-progression.json | 37 +
test-rig/profiles/dev-core.json | 36 +
test-rig/profiles/packaged-current.json | 65 +
test-rig/profiles/packaged-decoupled.json | 57 +
test-rig/profiles/packaged-mineralogy.json | 68 +
.../profiles/packaged-optional-power.json | 57 +
test-rig/profiles/worldgen-advantage.json | 150 +
test-rig/profiles/worldgen-legacy-import.json | 217 +
test-rig/profiles/worldgen-mineralogy.json | 160 +
test-rig/settings.gradle | 5 +
.../connected/ConnectedSession.java | 120 +
.../moddev/mc/advantageworks/ActorProbe.java | 128 +
.../advantageworks/AdvantageWorksCommand.java | 78 +
.../mc/advantageworks/AdvantageWorksMod.java | 27 +
.../mc/advantageworks/ConnectedProbe.java | 113 +
.../advantageworks/DimensionEntityProbe.java | 370 +
.../mc/advantageworks/JsonNbtConverter.java | 52 +
.../mc/advantageworks/LifecycleProbe.java | 418 ++
.../mc/advantageworks/MigrationProbe.java | 93 +
.../mc/advantageworks/ProcessProgression.java | 246 +
.../mc/advantageworks/ProgressionProbe.java | 260 +
.../mc/advantageworks/RecipeBookProbe.java | 92 +
.../mc/advantageworks/RecipeUnlockProbe.java | 242 +
.../mc/advantageworks/WorksController.java | 1004 +++
.../mc/advantageworks/WorksDefinition.java | 83 +
.../advantageworks/WorksDefinitionLoader.java | 256 +
.../mc/advantageworks/WorksResultWriter.java | 75 +
.../src/main/resources/META-INF/mods.toml | 31 +
.../works/works-1.13.2.json | 6547 +++++++++++++++++
.../mc/advantageworks/runner/WorksRunner.java | 990 +++
.../advantageworks/ConnectedSessionTest.java | 33 +
.../mc/advantageworks/LifecycleProbeTest.java | 63 +
.../advantageworks/ProfileManifestTest.java | 102 +
.../advantageworks/WorksDefinitionTest.java | 211 +
tools/generate-recipe-unlocks.js | 56 +
528 files changed, 37354 insertions(+), 7148 deletions(-)
create mode 100644 docs/1.13/CHUNK-AND-DRILLS.md
create mode 100644 docs/1.13/CONNECTED-MIGRATION.md
create mode 100644 docs/1.13/CURRENT-QUALIFICATION.md
create mode 100644 docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md
create mode 100644 docs/1.13/DRILL-PROGRESS-REPAIR.md
create mode 100644 docs/1.13/ECLIPSE-METADATA.md
create mode 100644 docs/1.13/ENDURANCE.md
create mode 100644 docs/1.13/MANUFACTURING-PROGRESSION.md
create mode 100644 docs/1.13/MIGRATION-MATRIX.md
create mode 100644 docs/1.13/MIGRATION.md
create mode 100644 docs/1.13/OIL-TRANSFER.md
create mode 100644 docs/1.13/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md
create mode 100644 docs/1.13/PLAYER-AND-ENTITY-MIGRATION.md
create mode 100644 docs/1.13/RECIPE-DISCOVERY.md
create mode 100644 docs/1.13/TRANSFORMER-ORDERING.md
delete mode 100644 libs/RF-API.jar
delete mode 100644 libs/RF-API.zip
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/PowerAdvantage.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/compat/BaseMetalsCompat.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/Blocks.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/Entities.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/Fluids.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/Fuels.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/Items.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/ModSupport.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/init/Recipes.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/BlockConveyor.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/BlockConveyorFilter.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityOreFilter.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StorageTankBlock.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/package-info.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/recipe/condition/NotRecipeModeConditionFactory.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/recipe/condition/OreDictEmptyConditionFactory.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/recipe/condition/RecipeModeConditionFactory.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/util/FluidHandlerHelper.java
delete mode 100644 src/main/java/com/mcmoddev/poweradvantage/util/FluidIdHelper.java
create mode 100644 src/main/java/cyano/poweradvantage/api/FluidCategoryRegistry.java
create mode 100644 src/main/java/cyano/poweradvantage/api/fluid/FluidNetworkApi.java
create mode 100644 src/main/java/cyano/poweradvantage/api/fluid/FluidStack.java
create mode 100644 src/main/java/cyano/poweradvantage/api/fluid/FluidTank.java
create mode 100644 src/main/java/cyano/poweradvantage/api/fluid/FluidTankInfo.java
create mode 100644 src/main/java/cyano/poweradvantage/api/fluid/IAdvantageFluidHandler.java
create mode 100644 src/main/java/cyano/poweradvantage/api/migration/LegacyConfigMigration.java
create mode 100644 src/main/java/cyano/poweradvantage/api/migration/LegacyMigrationApi.java
create mode 100644 src/main/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridge.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/RecipeMode.java (96%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/blocks/BlockFrame.java (60%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/blocks/BlockPowerSwitch.java (67%)
rename src/main/java/{com/mcmoddev/poweradvantage/items => zone/moddev/mc/poweradvantage/blocks}/package-info.java (76%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/compat/BaseMetalsCompat.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/conduitnetwork/ConduitNetworkManager.java (85%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/conduitnetwork/ConduitRegistry.java (87%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/conduitnetwork/package-info.java (87%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/graphics/package-info.java (70%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/gui/FilteredFluidTankGUI.java (84%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/gui/FluidTankGUI.java (51%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/gui/FunctionalCreativeTab.java (81%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/gui/StillGUI.java (87%)
rename src/main/java/{com/mcmoddev/poweradvantage/blocks => zone/moddev/mc/poweradvantage/gui}/package-info.java (77%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/GUI.java (93%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/ItemGroups.java (71%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/Items.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/Materials.java (77%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/TreasureChests.java (94%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/Villages.java (84%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/WorldGen.java (93%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/init/package-info.java (77%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/items/RotationTool.java (65%)
rename src/main/java/{com/mcmoddev/poweradvantage/gui => zone/moddev/mc/poweradvantage/items}/package-info.java (76%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityBlockFilter.java (61%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityConveyor.java (87%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java (87%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityFoodFilter.java (61%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityFuelFilter.java (62%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java (74%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java (86%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/TileEntityPlantFilter.java (77%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/conveyors/package-info.java (75%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/creative/InfiniteEnergyBlock.java (89%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java (79%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java (57%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/FluidDrainBlock.java (61%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/FluidPipeBlock.java (86%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/MetalTankBlock.java (89%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java (83%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/StillBlock.java (70%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/StillTileEntity.java (87%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java (88%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java (79%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/math/BlockPos4D.java (97%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/math/Integer2D.java (95%)
rename src/main/java/{com/mcmoddev/poweradvantage/util => zone/moddev/mc/poweradvantage/math}/package-info.java (55%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/package-info.java (81%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/FuelRegistry.java (90%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/ITileEntityGUI.java (87%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/MachineGUIRegistry.java (59%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/package-info.java (67%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/still/recipe/DistillationRecipe.java (92%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java (79%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/still/recipe/ExplicitFluidReference.java (75%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/still/recipe/FluidReference.java (93%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java (76%)
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java
create mode 100644 src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/util/HashCodeHelper.java (96%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/util/InventoryWrapper.java (94%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/util/PowerHelper.java (59%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/util/ReversibleHashMap.java (97%)
rename src/main/java/{com/mcmoddev => zone/moddev/mc}/poweradvantage/util/ReversibleMap.java (81%)
rename src/main/java/{com/mcmoddev/poweradvantage/math => zone/moddev/mc/poweradvantage/util}/package-info.java (55%)
create mode 100644 src/main/resources/META-INF/coremods.json
create mode 100644 src/main/resources/META-INF/mods.toml
create mode 100644 src/main/resources/assets/poweradvantage/lang/de_de.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/de_de.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/en_au.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/en_au.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/en_ca.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/en_ca.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/en_en.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/en_en.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/en_gb.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/en_gb.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/en_pt.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/en_pt.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/en_us.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/en_us.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/es_es.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/es_es.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/es_mx.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/es_mx.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/it_it.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/it_it.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/ja_jp.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/ja_jp.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/ko_kr.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/ko_kr.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/pt_br.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/pt_br.lang
create mode 100644 src/main/resources/assets/poweradvantage/lang/zh_cn.json
delete mode 100644 src/main/resources/assets/poweradvantage/lang/zh_cn.lang
create mode 100644 src/main/resources/assets/poweradvantage/models/block/crude_oil.json
create mode 100644 src/main/resources/assets/poweradvantage/models/block/refined_oil.json
create mode 100644 src/main/resources/assets/poweradvantage/models/item/crude_oil.json
create mode 100644 src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json
create mode 100644 src/main/resources/assets/poweradvantage/models/item/refined_oil.json
create mode 100644 src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/_factories.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_discharge.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_drain.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_iron_normal.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_steel.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_normal.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_normal.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_tech.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_storage_tank.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/fluid_switch.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_conveyor.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_block.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_food.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_fuel.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_inventory.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_ore.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_overflow.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_plant.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/item_filter_smelt.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/rotator_tool.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/sprocket_rod_normal.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/sprocket_rod_tech.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/sprocket_stickwood_normal.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/steel_frame.json
delete mode 100644 src/main/resources/assets/poweradvantage/recipes/still.json
create mode 100644 src/main/resources/coremods/poweradvantage_113_migration.js
create mode 100644 src/main/resources/coremods/poweradvantage_material_tags.js
create mode 100644 src/main/resources/coremods/poweradvantage_oil_renderer.js
create mode 100644 src/main/resources/coremods/poweradvantage_parts_loot.js
create mode 100644 src/main/resources/data/forge/tags/items/bars.json
create mode 100644 src/main/resources/data/forge/tags/items/bars/steel.json
create mode 100644 src/main/resources/data/forge/tags/items/dusts/charcoal.json
create mode 100644 src/main/resources/data/forge/tags/items/dusts/coal.json
create mode 100644 src/main/resources/data/forge/tags/items/dusts/lead.json
create mode 100644 src/main/resources/data/forge/tags/items/dusts/redstone.json
create mode 100644 src/main/resources/data/forge/tags/items/dusts/silver.json
create mode 100644 src/main/resources/data/forge/tags/items/dusts/tin.json
create mode 100644 src/main/resources/data/forge/tags/items/frames/steel.json
create mode 100644 src/main/resources/data/forge/tags/items/gears.json
create mode 100644 src/main/resources/data/forge/tags/items/gears/steel.json
create mode 100644 src/main/resources/data/forge/tags/items/gems/diamond.json
create mode 100644 src/main/resources/data/forge/tags/items/gems/emerald.json
create mode 100644 src/main/resources/data/forge/tags/items/governors.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/brass.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/copper.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/gold.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/iron.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/lead.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/nickel.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/plastic.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/refined_iron.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/rubber.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/steel.json
create mode 100644 src/main/resources/data/forge/tags/items/ingots/zinc.json
create mode 100644 src/main/resources/data/forge/tags/items/nuggets/bronze.json
create mode 100644 src/main/resources/data/forge/tags/items/nuggets/copper.json
create mode 100644 src/main/resources/data/forge/tags/items/nuggets/gold.json
create mode 100644 src/main/resources/data/forge/tags/items/nuggets/iron.json
create mode 100644 src/main/resources/data/forge/tags/items/nuggets/lead.json
create mode 100644 src/main/resources/data/forge/tags/items/nuggets/tin.json
create mode 100644 src/main/resources/data/forge/tags/items/pipes.json
create mode 100644 src/main/resources/data/forge/tags/items/plastic.json
create mode 100644 src/main/resources/data/forge/tags/items/plates/copper.json
create mode 100644 src/main/resources/data/forge/tags/items/plates/iron.json
create mode 100644 src/main/resources/data/forge/tags/items/plates/steel.json
create mode 100644 src/main/resources/data/forge/tags/items/power_supply_units.json
create mode 100644 src/main/resources/data/forge/tags/items/rods.json
create mode 100644 src/main/resources/data/forge/tags/items/rods/copper.json
create mode 100644 src/main/resources/data/forge/tags/items/rods/wooden.json
create mode 100644 src/main/resources/data/forge/tags/items/rubber.json
create mode 100644 src/main/resources/data/forge/tags/items/sprockets.json
create mode 100644 src/main/resources/data/forge/tags/items/sprockets/steel.json
create mode 100644 src/main/resources/data/forge/tags/items/stone.json
create mode 100644 src/main/resources/data/forge/tags/items/storage_blocks/diamond.json
create mode 100644 src/main/resources/data/forge/tags/items/storage_blocks/steel.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json
create mode 100644 src/main/resources/data/poweradvantage/advancements/recipes/still.json
create mode 100644 src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json
create mode 100644 src/main/resources/data/poweradvantage/orespawn/provider.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_discharge.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_drain.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/fluid_switch.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_conveyor.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_block.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_food.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_ore.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_plant.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/rotator_tool.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/starch_from_potato.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/steel_frame.json
create mode 100644 src/main/resources/data/poweradvantage/recipes/still.json
create mode 100644 src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java
create mode 100644 src/test/java/cyano/poweradvantage/api/migration/LegacyConfigMigrationTest.java
create mode 100644 src/test/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridgeTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/resources/ClientResourceContractTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java
create mode 100644 src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java
create mode 100644 src/test/resources/orespawn-legacy/poweradvantage.json
create mode 100644 test-rig/README.md
create mode 100644 test-rig/build.gradle
create mode 100644 test-rig/client-resources/README.md
create mode 100644 test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java
create mode 100644 test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java
create mode 100644 test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java
create mode 100644 test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java
create mode 100644 test-rig/client-resources/resources/META-INF/mods.toml
create mode 100644 test-rig/client-resources/resources/pack.mcmeta
create mode 100644 test-rig/client-resources/run.ps1
create mode 100644 test-rig/eclipse/EclipseModelProbe.java
create mode 100644 test-rig/gradle.properties
create mode 100644 test-rig/migration-probe/build.gradle
create mode 100644 test-rig/migration-probe/settings.gradle
create mode 100644 test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyActors.java
create mode 100644 test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyConnected.java
create mode 100644 test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.java
create mode 100644 test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyProbe.java
create mode 100644 test-rig/migration/README.md
create mode 100644 test-rig/migration/actors.py
create mode 100644 test-rig/migration/actors_native.py
create mode 100644 test-rig/migration/audit_actors.py
create mode 100644 test-rig/migration/audit_actors_native.py
create mode 100644 test-rig/migration/audit_connected.py
create mode 100644 test-rig/migration/audit_connected_progress.py
create mode 100644 test-rig/migration/audit_dimensions.py
create mode 100644 test-rig/migration/audit_drill_restart.py
create mode 100644 test-rig/migration/audit_endurance.py
create mode 100644 test-rig/migration/audit_extended_lifecycle.py
create mode 100644 test-rig/migration/audit_lifecycle.py
create mode 100644 test-rig/migration/audit_pipe_only.py
create mode 100644 test-rig/migration/audit_player_layout.py
create mode 100644 test-rig/migration/audit_respawn.py
create mode 100644 test-rig/migration/audit_results.py
create mode 100644 test-rig/migration/audit_transformer_order.py
create mode 100644 test-rig/migration/connected.py
create mode 100644 test-rig/migration/dimensions.py
create mode 100644 test-rig/migration/drill_restart.py
create mode 100644 test-rig/migration/endurance.py
create mode 100644 test-rig/migration/extended_lifecycle.py
create mode 100644 test-rig/migration/lifecycle.py
create mode 100644 test-rig/migration/matrix.py
create mode 100644 test-rig/migration/oil_transfer.py
create mode 100644 test-rig/migration/partial_network_trace.py
create mode 100644 test-rig/migration/pin_dimension_profiles.py
create mode 100644 test-rig/migration/pin_drill_profiles.py
create mode 100644 test-rig/migration/pipe_only_lifecycle.py
create mode 100644 test-rig/migration/player_layout.py
create mode 100644 test-rig/migration/progression.py
create mode 100644 test-rig/migration/recipe_unlocks.py
create mode 100644 test-rig/migration/requirements.txt
create mode 100644 test-rig/migration/respawn.py
create mode 100644 test-rig/migration/test_actors.py
create mode 100644 test-rig/migration/test_audit_connected.py
create mode 100644 test-rig/migration/test_bridge_profiles.py
create mode 100644 test-rig/migration/test_connected.py
create mode 100644 test-rig/migration/test_connected_progress.py
create mode 100644 test-rig/migration/test_dimensions.py
create mode 100644 test-rig/migration/test_drill_restart.py
create mode 100644 test-rig/migration/test_endurance.py
create mode 100644 test-rig/migration/test_extended_lifecycle.py
create mode 100644 test-rig/migration/test_lifecycle.py
create mode 100644 test-rig/migration/test_matrix.py
create mode 100644 test-rig/migration/test_oil_transfer.py
create mode 100644 test-rig/migration/test_pin_dimension_profiles.py
create mode 100644 test-rig/migration/test_pipe_only_lifecycle.py
create mode 100644 test-rig/migration/test_progression.py
create mode 100644 test-rig/migration/test_recipe_unlocks.py
create mode 100644 test-rig/migration/test_respawn.py
create mode 100644 test-rig/migration/test_transformer_order.py
create mode 100644 test-rig/migration/trace_connected_progress.py
create mode 100644 test-rig/migration/transformer-order-probe/TransformerOrderBridge.java
create mode 100644 test-rig/migration/transformer_order.py
create mode 100644 test-rig/profiles/dev-core-apocalyptic.json
create mode 100644 test-rig/profiles/dev-core-tech-progression.json
create mode 100644 test-rig/profiles/dev-core.json
create mode 100644 test-rig/profiles/packaged-current.json
create mode 100644 test-rig/profiles/packaged-decoupled.json
create mode 100644 test-rig/profiles/packaged-mineralogy.json
create mode 100644 test-rig/profiles/packaged-optional-power.json
create mode 100644 test-rig/profiles/worldgen-advantage.json
create mode 100644 test-rig/profiles/worldgen-legacy-import.json
create mode 100644 test-rig/profiles/worldgen-mineralogy.json
create mode 100644 test-rig/settings.gradle
create mode 100644 test-rig/shared-probe/zone/moddev/mc/advantageworks/connected/ConnectedSession.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/ActorProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksCommand.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/AdvantageWorksMod.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/ConnectedProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/DimensionEntityProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/JsonNbtConverter.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/LifecycleProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/MigrationProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/ProcessProgression.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/ProgressionProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeBookProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/RecipeUnlockProbe.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksController.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinition.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksDefinitionLoader.java
create mode 100644 test-rig/src/main/java/zone/moddev/mc/advantageworks/WorksResultWriter.java
create mode 100644 test-rig/src/main/resources/META-INF/mods.toml
create mode 100644 test-rig/src/main/resources/assets/advantageworkstest/works/works-1.13.2.json
create mode 100644 test-rig/src/runner/java/zone/moddev/mc/advantageworks/runner/WorksRunner.java
create mode 100644 test-rig/src/test/java/zone/moddev/mc/advantageworks/ConnectedSessionTest.java
create mode 100644 test-rig/src/test/java/zone/moddev/mc/advantageworks/LifecycleProbeTest.java
create mode 100644 test-rig/src/test/java/zone/moddev/mc/advantageworks/ProfileManifestTest.java
create mode 100644 test-rig/src/test/java/zone/moddev/mc/advantageworks/WorksDefinitionTest.java
create mode 100644 tools/generate-recipe-unlocks.js
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a8f78e58..3dec651c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,81 +1,89 @@
-name: Power Advantage 1.12 CI
+name: Power Advantage 1.13 CI
on:
push:
- branches:
- - master-1.12.2
- - 'feature/**'
+ branches: [master-1.13.2, 'feature/**']
pull_request:
- branches:
- - master-1.12.2
+ branches: [master-1.13.2]
permissions:
contents: read
concurrency:
- group: poweradvantage-1.12-${{ github.workflow }}-${{ github.ref }}
+ group: poweradvantage-1.13-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build, test, and audit
runs-on: ubuntu-latest
- timeout-minutes: 60
-
+ timeout-minutes: 90
steps:
- - name: Check out source
+ - name: Check out PowerAdvantage
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
-
+ - name: Check out pinned OreSpawn
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ repository: MinecraftModDevelopmentMods/OreSpawn
+ ref: 6a7c2bb07b9a7771128812fcc07d185f7d9df58b
+ path: .ci-dependencies/OreSpawn
+ - name: Check out pinned Base Metals
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ repository: MinecraftModDevelopmentMods/BaseMetals
+ ref: 1376a20d7d5ba22d3abc77dc34b49a2c88943697
+ path: .ci-dependencies/BaseMetals
- name: Install Java 8 toolchain
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: temurin
java-version: '8.0.502+7'
-
- name: Install Java 17 for Gradle
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: microsoft
java-version: '17'
-
- name: Set up Gradle
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6
-
- - name: Make the wrapper executable
- run: chmod +x ./gradlew
-
- - name: Stage exact RebornCore compile API
- id: reborncore
+ - name: Build pinned development dependencies
+ id: dependencies
+ shell: bash
run: |
- mirror="$RUNNER_TEMP/reborncore-mirror"
- bash gradle/stage-reborncore-release.sh "$mirror"
- echo "repository=$mirror" >> "$GITHUB_OUTPUT"
-
+ set -euo pipefail
+ orespawn="$GITHUB_WORKSPACE/.ci-dependencies/OreSpawn"
+ basemetals="$GITHUB_WORKSPACE/.ci-dependencies/BaseMetals"
+ chmod +x "$orespawn/gradlew" "$basemetals/gradlew"
+ (cd "$orespawn" && ./gradlew jar --no-daemon --stacktrace)
+ (cd "$basemetals" && ./gradlew jar --no-daemon --stacktrace)
+ osjar="$orespawn/build/libs-dev/OreSpawn-4.1.0.113021-deobf.jar"
+ bmjar="$basemetals/build/libs-dev/BaseMetals-3.0.1.113021-deobf.jar"
+ test -f "$osjar" && test -f "$bmjar"
+ echo "orespawn=$osjar" >> "$GITHUB_OUTPUT"
+ echo "basemetals=$bmjar" >> "$GITHUB_OUTPUT"
- name: Build and audit release artifacts
run: >-
./gradlew clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts
writeReleaseChecksums verifyEclipseProductionClasspath
- -PrebornCoreVerificationRepository=${{ steps.reborncore.outputs.repository }}
+ -PbaseMetalsDeobfJar=${{ steps.dependencies.outputs.basemetals }}
+ -PoreSpawnDeobfJar=${{ steps.dependencies.outputs.orespawn }}
--no-daemon --stacktrace
-
- name: Upload audited release candidate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
- name: PowerAdvantage-1.12.2-${{ github.sha }}
+ name: PowerAdvantage-1.13.2-${{ github.sha }}
if-no-files-found: error
retention-days: 30
path: |
- build/libs/PowerAdvantage-2.4.1.112021.jar
- build/libs/PowerAdvantage-2.4.1.112021-sources.jar
- build/libs/PowerAdvantage-2.4.1.112021-javadoc.jar
+ build/libs/PowerAdvantage-2.4.4.113021.jar
+ build/libs/PowerAdvantage-2.4.4.113021-sources.jar
+ build/libs/PowerAdvantage-2.4.4.113021-javadoc.jar
build/release/SHA256SUMS
CHANGELOG.txt
-
- name: Upload diagnostics on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
- name: PowerAdvantage-1.12.2-diagnostics-${{ github.sha }}
+ name: PowerAdvantage-1.13.2-diagnostics-${{ github.sha }}
if-no-files-found: ignore
retention-days: 14
path: |
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 77d6fe58..756d9db5 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -2,12 +2,9 @@ name: CodeQL
on:
push:
- branches:
- - master-1.12.2
- - 'feature/**'
+ branches: [master-1.13.2, 'feature/**']
pull_request:
- branches:
- - master-1.12.2
+ branches: [master-1.13.2]
schedule:
- cron: '47 7 * * 4'
@@ -20,44 +17,55 @@ jobs:
analyze:
name: Analyze Java
runs-on: ubuntu-latest
- timeout-minutes: 45
-
+ timeout-minutes: 75
steps:
- - name: Check out source
+ - name: Check out PowerAdvantage
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
-
+ - name: Check out pinned OreSpawn
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ repository: MinecraftModDevelopmentMods/OreSpawn
+ ref: 6a7c2bb07b9a7771128812fcc07d185f7d9df58b
+ path: .ci-dependencies/OreSpawn
+ - name: Check out pinned Base Metals
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ repository: MinecraftModDevelopmentMods/BaseMetals
+ ref: 1376a20d7d5ba22d3abc77dc34b49a2c88943697
+ path: .ci-dependencies/BaseMetals
- name: Install Java 8 toolchain
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: temurin
java-version: '8.0.502+7'
-
- name: Install Java 17 for Gradle
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: microsoft
java-version: '17'
-
- name: Set up Gradle
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6
-
- - name: Stage exact RebornCore compile API
- id: reborncore
+ - name: Build pinned development dependencies
+ id: dependencies
+ shell: bash
run: |
- mirror="$RUNNER_TEMP/reborncore-mirror"
- bash gradle/stage-reborncore-release.sh "$mirror"
- echo "repository=$mirror" >> "$GITHUB_OUTPUT"
-
+ set -euo pipefail
+ orespawn="$GITHUB_WORKSPACE/.ci-dependencies/OreSpawn"
+ basemetals="$GITHUB_WORKSPACE/.ci-dependencies/BaseMetals"
+ chmod +x "$orespawn/gradlew" "$basemetals/gradlew"
+ (cd "$orespawn" && ./gradlew jar --no-daemon --stacktrace)
+ (cd "$basemetals" && ./gradlew jar --no-daemon --stacktrace)
+ echo "orespawn=$orespawn/build/libs-dev/OreSpawn-4.1.0.113021-deobf.jar" >> "$GITHUB_OUTPUT"
+ echo "basemetals=$basemetals/build/libs-dev/BaseMetals-3.0.1.113021-deobf.jar" >> "$GITHUB_OUTPUT"
- name: Initialize CodeQL
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
with:
languages: java-kotlin
-
- name: Compile production code
run: |
chmod +x ./gradlew
./gradlew clean classes --rerun-tasks --no-build-cache --no-daemon --stacktrace \
- -PrebornCoreVerificationRepository=${{ steps.reborncore.outputs.repository }}
-
+ -PbaseMetalsDeobfJar=${{ steps.dependencies.outputs.basemetals }} \
+ -PoreSpawnDeobfJar=${{ steps.dependencies.outputs.orespawn }}
- name: Analyze
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
diff --git a/.github/workflows/validate-gradle-build.yml b/.github/workflows/validate-gradle-build.yml
index 8959c1a8..d4d55ba3 100644
--- a/.github/workflows/validate-gradle-build.yml
+++ b/.github/workflows/validate-gradle-build.yml
@@ -3,11 +3,11 @@ name: Validate Gradle Wrapper
on:
push:
branches:
- - master-1.12.2
+ - master-1.13.2
- 'feature/**'
pull_request:
branches:
- - master-1.12.2
+ - master-1.13.2
permissions:
contents: read
diff --git a/.gitignore b/.gitignore
index 1104f3ff..f5eaedc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,9 @@
/run/
/run-data/
/run.*
+/$run/
+/eula.txt
+/server.properties
/*.log
/classes/
/logs/
@@ -52,10 +55,39 @@
/hs_err_pid*.log
/replay_pid*.log
-# Local dependency experiments; the RF API is an intentional tracked input.
+# Nested Advantage Works outputs
+/test-rig/.gradle/
+/test-rig/.cache/
+/test-rig/build/
+/test-rig/migration-probe/.gradle/
+/test-rig/migration-probe/build/
+/test-rig/migration/.venv/
+/test-rig/migration/__pycache__/
+
+# Local dependency artifacts; 1.13 converters use Forge Energy instead.
/libs/*
-!/libs/RF-API.jar
-!/libs/RF-API.zip
+*.jar
+*.zip
+!/gradle/wrapper/gradle-wrapper.jar
/secret.json
+# Nested local tooling and generated output
+AGENTS.md
+AGENT.md
+CLAUDE.md
+GEMINI.md
+ARCHITECT.md
+agent-notes/
+.codex/
+.agent/
+.agents/
+.claude/
+.gemini/
+.cursor/
+agent-*.md
+codex-*.md
+*.log
+*.class
+__pycache__/
+*.pyc
/.DS_Store
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 4f3babda..2d22e857 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,3 +1,16 @@
+Power Advantage 2.4.4.113021
+
+- Port Power Advantage to Minecraft 1.13.2 with ForgeGradle 7 and Java 8 support.
+- Keep existing block, item, fluid and saved-state names, the legacy public API and Iron Age Furniture compatibility facades.
+- Add direct 1.10 and 1.12 world migration, including inventories, fluids, entities, pets, enchantments and old configuration files. Back up worlds before upgrading.
+- Restore conduit connections after reloads without forcing unloaded chunks to load.
+- Fix models, translations, oil buckets, fluid gauges and transparent floor edges around containers.
+- Add ingredient-based recipe discoveries, separate unrelated recipe-book entries, and allow potatoes to be crafted into starch.
+- Restore processing, recycling and parts-loot paths while retaining recipe modes and configured material aliases.
+- Use Forge tags, Base Metals crushing and OreSpawn 4 deposits; retain old generation settings and user overrides.
+- Keep the RF and Tech Reborn converter names but use Forge Energy instead of obsolete external APIs.
+- Add separate Power-only and combined Eclipse launches, reproducible builds and repeatable migration and operating tests. See the qualification notes for remaining coverage gaps.
+
Power Advantage 2.4.1.112021
- Restore conduit networks and machine GUI state after a world reload.
diff --git a/README.md b/README.md
index eef70e91..0522cd4e 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,68 @@
# PowerAdvantage
-Power Advantage provides shared power, fluid, and item transport systems for its Minecraft 1.12.2 expansion mods.
+Power Advantage provides shared power, fluid, item transport, and compatibility APIs for its Minecraft 1.13.2 expansion mods.
-## Building Minecraft 1.12.2
+This port keeps existing registry names and includes migration from 1.10 and 1.12.
+Always back up a world before upgrading, and check that its other mods also support the target version.
+The [qualification notes](docs/1.13/CURRENT-QUALIFICATION.md) separate tested scenarios from remaining gaps;
+they are not a guarantee that an entire modpack can be upgraded without losses.
-The `master-1.12.2` build uses ForgeGradle 7.0.34 and Gradle 9.6.1. Run Gradle with Java 17; Gradle resolves the Java 8 toolchain used for compilation.
+Recipe-book discovery now follows relevant ingredients. See the [all-Advantage material checklist and automated discovery tests](docs/1.13/RECIPE-DISCOVERY.md). To regenerate the checked-in unlock JSON after changing recipes, run `node tools/generate-recipe-unlocks.js`; `check` rejects missing or stale ingredient criteria.
+
+The [manufacturing progression audit](docs/1.13/MANUFACTURING-PROGRESSION.md) traces survival parts from raw resources, including configured material aliases, processing, loot-only parts and automated restart checks.
+
+## Building Minecraft 1.13.2
+
+The `master-1.13.2` build uses ForgeGradle 7.0.34 and Gradle 9.6.1. Run Gradle with Java 17; Gradle resolves the Java 8 production toolchain.
+
+Build the pinned Base Metals `3.0.1.113021` and OreSpawn `4.1.0.113021` deobf jars in their sibling checkouts, then run:
```text
-gradlew.bat clean check build verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums
+gradlew.bat clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath
```
-Release jars are written to `build/libs`. The deobfuscated development jar for sibling mod compilation is written to `build/libs-dev` by `deobfJar` or `build`.
+The default dependency paths follow the local MMD checkout layout. Other layouts can pass `-PbaseMetalsDeobfJar=` and `-PoreSpawnDeobfJar=`. Dependencies are present on development classpaths but are never embedded in PowerAdvantage artifacts.
+
+Release jars are written to `build/libs`; `deobfJar` writes the sibling-development jar to `build/libs-dev`.
-For Eclipse, import the repository as an existing Gradle project and run:
+For Eclipse, import this repository as an existing Gradle project and run:
```text
gradlew.bat cleanEclipse verifyEclipseProductionClasspath
```
-Base Metals and OreSpawn remain required distribution dependencies but are deliberately absent from the compile classpath. Optional RF and RebornCore integrations compile against SHA-256-pinned API inputs and are not bundled or added to normal development launches. CI stages the exact historical RebornCore Maven release because the later CurseForge archive with the same version has different contents.
+Refresh the project in Eclipse, allow its Java build to finish, and launch the generated `runClient.launch`. Eclipse launches discover the mod from `bin/main`; command-line Gradle launches use `build/forge-run-mod-classes/main`. The Eclipse audit checks that discovery points at the same production output Eclipse compiles into, preventing a client that starts without PowerAdvantage.
+
+Eclipse copies production resources from `build/resources/main`, not directly from `src/main/resources`. This prevents its builder from overwriting the expanded `mods.toml` with raw `${version}` placeholders. After updating this setup, use **Gradle > Refresh Gradle Project**, then **Project > Clean** before relaunching. After editing resources or version properties, run `processResources` or refresh the Gradle project before the Eclipse build. See the [metadata regression notes](docs/1.13/ECLIPSE-METADATA.md).
+
+To test all three Advantage mods together, run `gradlew.bat prepareCombinedEclipseLaunch`, refresh Eclipse, and launch `runClientAllAdvantage.launch`. This additional profile loads the pinned Steam/Electric deobf jars, Base Metals and OreSpawn alongside Power's Eclipse output. It uses the same `run` directory and test saves; it does not copy, open or modify a save automatically. The ordinary `runClient.launch` remains Power-only. Alternative checkout layouts can pass `-PsteamAdvantageDeobfJar=` and `-PelectricAdvantageDeobfJar=`; supplied jars must match the `test-rig/profiles/dev-core.json` pins. Regenerate the combined launch after refreshing its dependencies or the base launch.
+
+The combined [client resource probe](test-rig/client-resources/README.md), with `-RequireExtensions`, checks item names, baked models, textures and every valid block state across all three Advantage mods. The legacy Steam/Electric model-path and selector failures are repaired; [the qualification notes](docs/1.13/OIL-TRANSFER.md#combined-resource-repair-21-october-2026) retain the failing evidence and subsequent full-scope result. Native resource checks remain separate from manual graphical/gameplay acceptance.
+
+The [client resource probe](test-rig/client-resources/README.md) checks native baked models, textures, creative entries, and translated item names without opening a world. Static resource and locale regressions also run under `check`.
+
+## Compatibility
+
+The implementation namespace is `zone.moddev.mc.poweradvantage`. The historical `cyano.poweradvantage.api` surface, including `FluidNetworkApi`, remains available to dependent mods. Registry names, fluids, tile IDs, NBT keys, OreSpawn provider IDs, and configuration meaning remain stable.
+
+PowerAdvantage owns the shared pre-flattening migration hook used by all three Advantage mods. See [the 1.13 migration guide](docs/1.13/MIGRATION.md) before opening an irreplaceable 1.10 or 1.12 world. The [Advantage Works rig](test-rig/README.md) provides repeatable machine, restart, world-generation, and migration checks.
+
+The [endurance protocol](docs/1.13/ENDURANCE.md) exercises repeat-batch operation in fresh and migrated disposable worlds, followed by a complete server-process restart. Its coverage limits are separate from frozen-state migration and manual client acceptance.
+
+The [chunk and moving-drill protocol](docs/1.13/CHUNK-AND-DRILLS.md) adds genuine chunk unload/reload witnesses, finite-energy track moves in all six facings, rear-output conservation, and mid-block progress checks.
+
+The [partial-network and extended-drill protocol](docs/1.13/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md) separates endpoint unloading/recovery from whole-network reloads, and covers repeated moves, vertical track travel, junctions, blocked output and redstone interruption.
+
+The [player and entity migration report](docs/1.13/PLAYER-AND-ENTITY-MIGRATION.md) covers player files, inventories, nested items, ownership, saved entities and full restarts. The eight tested routes pass with the repaired 1.12 bridge. Use Electric Advantage `2.2.3.112021` for the current turbine and drill fixes; the original `2.2.1.112021` jar remains a failing historical control.
+
+The [additional-dimension protocol](docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md)
+covers Nether/End player and entity fixtures, native passenger/leash relationships
+and separate death/respawn controls. The [transformer-order report](docs/1.13/TRANSFORMER-ORDERING.md)
+qualifies both BaseMetals/Advantage application orders and records the
+order-dependent provenance of loose-file backups.
+
+Networks use loaded chunks only. Missing conduit sections interrupt distribution;
+normal chunk loading reconnects them without replacing pipes. The same protocol
+records acquisition-stack diagnostics and pipe-only bridge regressions.
+
+The historical 1.10 handbook remains on the [1.10.2 branch](https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/blob/master-1.10.2/docs/1.10/README.md).
diff --git a/build.gradle b/build.gradle
index d23c0569..fa73db01 100644
--- a/build.gradle
+++ b/build.gradle
@@ -76,7 +76,7 @@ tasks.withType(AbstractArchiveTask).configureEach {
def archiveTextSuffixes = [
'.cfg', '.css', '.html', '.info', '.java', '.js', '.json', '.lang',
- '.mcmeta', '.md', '.properties', '.txt', '.xml'
+ '.mcmeta', '.md', '.properties', '.toml', '.txt', '.xml'
]
def archiveTextPatterns = archiveTextSuffixes.collect { "**/*${it}".toString() }
archiveTextPatterns.addAll(['**/element-list', '**/package-list'])
@@ -90,42 +90,53 @@ minecraft {
mappings channel: project.mapping_channel, version: project.mapping_version
String configuredRunDirectory = providers.gradleProperty('powerAdvantageRunDirectory')
.getOrElse('run')
+ def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main')
+ def forgeMainOutputPath = forgeRunModClassesDirectory.get().asFile.absolutePath
+ def mainModClasses = "${mod_id}%%${forgeMainOutputPath}${File.pathSeparator}${mod_id}%%${forgeMainOutputPath}"
+ def forge25RunTargets = [client: 'fmluserdevclient', server: 'fmluserdevserver', data: 'fmluserdevdata']
runs {
configureEach {
+ mainClass = 'net.minecraftforge.userdev.LaunchTesting'
workingDir.convention layout.projectDirectory.dir(configuredRunDirectory)
- systemProperty 'forge.logging.console.level', 'debug'
+ systemProperty 'forge.logging.markers', 'REGISTRIES'
+ systemProperty 'forge.logging.console.level', 'info'
+ mods { poweradvantage { source sourceSets.main } }
+ environment 'target', forge25RunTargets[name]
+ environment 'MCP_MAPPINGS', "${mapping_channel}_${mapping_version}"
+ environment 'MCP_VERSION', mcp_version
+ environment 'FORGE_VERSION', forge_version
+ environment 'FORGE_GROUP', 'net.minecraftforge'
+ environment 'MC_VERSION', minecraft_version
+ environment 'MOD_CLASSES', mainModClasses
}
register('client')
register('server') {
args '--nogui'
}
+ register('data') {
+ workingDir.convention layout.projectDirectory.dir('run-data')
+ args '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/'),
+ '--existing', file('src/main/resources/')
+ }
}
}
-// Forge 1.12 expects each development mod to expose classes and resources from
-// one classpath root. ForgeGradle 7 splits those outputs, so merge only the
-// launch output without contaminating the canonical source-set directories.
-def forge12DevelopmentOutput = layout.buildDirectory.dir('forge12-development/main')
-def syncForge12DevelopmentResources = tasks.register('syncForge12DevelopmentResources', Sync) {
+sourceSets.main.resources {
+ srcDir 'src/generated/resources'
+ exclude '.cache/**'
+}
+
+def forgeRunModClassesDirectory = layout.buildDirectory.dir('forge-run-mod-classes/main')
+def prepareForgeRunModClasses = tasks.register('prepareForgeRunModClasses', Sync) {
group = 'ide'
- description = 'Merges classes and processed resources for Forge 1.12 Gradle launches.'
+ description = 'Merges classes and processed resources for Forge 1.13 launches.'
dependsOn tasks.named('classes')
- from tasks.named('compileJava', JavaCompile).flatMap { it.destinationDirectory }
- from sourceSets.main.output.resourcesDir
- into forge12DevelopmentOutput
-}
-tasks.configureEach {
- if (name == 'runClient' || name == 'runServer') {
- dependsOn syncForge12DevelopmentResources
- doFirst {
- Set splitMainOutputs = [
- tasks.named('compileJava', JavaCompile).get().destinationDirectory.get().asFile,
- sourceSets.main.output.resourcesDir
- ] as Set
- classpath = files(forge12DevelopmentOutput, classpath.filter {
- !splitMainOutputs.contains(it)
- })
- }
+ from sourceSets.main.output
+ into forgeRunModClassesDirectory
+}
+tasks.withType(JavaExec).configureEach {
+ if (name in ['runClient', 'runServer', 'runData']) {
+ dependsOn prepareForgeRunModClasses
}
}
@@ -134,14 +145,6 @@ repositories {
maven fg.forgeMaven
maven fg.minecraftLibsMaven
- if (providers.gradleProperty('rebornCoreVerificationRepository').isPresent()) {
- maven {
- name = 'RebornCoreReleaseVerificationMirror'
- url = uri(providers.gradleProperty('rebornCoreVerificationRepository').get())
- metadataSources { artifact() }
- content { includeGroup('curse.maven') }
- }
- }
exclusiveContent {
forRepository {
maven {
@@ -156,39 +159,39 @@ repositories {
name = 'MinecraftLibraries'
url = 'https://libraries.minecraft.net/'
}
- if (!providers.gradleProperty('rebornCoreVerificationRepository').isPresent()) {
- maven {
- name = 'PinnedRebornCoreRelease'
- url = 'https://maven.modmuss50.me/'
- content { includeGroup('RebornCore') }
- }
- }
}
-def rfApiJar = file('libs/RF-API.jar')
-def usingStagedRebornCore = providers.gradleProperty('rebornCoreVerificationRepository').isPresent()
-def rebornCoreCoordinate = usingStagedRebornCore
- ? "curse.maven:reborncore-${project.reborncore_curse_project_id}:${project.reborncore_curse_file_id}"
- : "RebornCore:RebornCore-1.12.2:${project.reborncore_version}:universal"
+// The default paths mirror the local MMD checkout layout without tying the
+// build to a drive letter. CI and other layouts can override either path.
+def baseMetalsDeobfJar = file(providers.gradleProperty('baseMetalsDeobfJar')
+ .getOrElse('../../../BaseMetals/BaseMetals 113/BaseMetals/build/libs-dev/'
+ + 'BaseMetals-3.0.1.113021-deobf.jar'))
+def oreSpawnDeobfJar = file(providers.gradleProperty('oreSpawnDeobfJar')
+ .getOrElse('../../../MinecraftMineralogy/OS113/OreSpawn/build/libs-dev/'
+ + 'OreSpawn-4.1.0.113021-deobf.jar'))
dependencies {
implementation minecraft.dependency(
"net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}")
- // Mavenizer's legacy metadata omits this compile-time Minecraft library.
- compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209'
- // These integrations are optional at runtime. Compile against their APIs,
- // but do not package or launch PowerAdvantage with the provider mods.
- compileOnly files(rfApiJar)
- compileOnly rebornCoreCoordinate
+ compileOnly files(baseMetalsDeobfJar, oreSpawnDeobfJar)
+ runtimeOnly files(baseMetalsDeobfJar, oreSpawnDeobfJar)
+ testImplementation 'junit:junit:4.13.2'
}
tasks.named('processResources', ProcessResources) {
filteringCharset = 'UTF-8'
- inputs.property('version', project.version)
- inputs.property('mcversion', project.minecraft_version)
- filesMatching('mcmod.info') {
- expand version: project.version, mcversion: project.minecraft_version
+ inputs.properties([
+ version: project.version,
+ minecraft_version_range: project.minecraft_version_range,
+ forge_version_range: project.forge_version_range,
+ loader_version_range: project.loader_version_range
+ ])
+ filesMatching('META-INF/mods.toml') {
+ expand version: project.version,
+ minecraft_version_range: project.minecraft_version_range,
+ forge_version_range: project.forge_version_range,
+ loader_version_range: project.loader_version_range
}
filesMatching(archiveTextPatterns, normalizeArchiveLineEndings)
}
@@ -310,13 +313,13 @@ tasks.register('verifyReleaseConfiguration') {
group = 'verification'
description = 'Validates release identity, target metadata, and optional integration policy.'
doLast {
- if (project.mod_version != '2.4.1.112021') {
+ if (project.mod_version != '2.4.4.113021') {
throw new GradleException("Unexpected PowerAdvantage release version: ${project.mod_version}")
}
- if (project.minecraft_version != '1.12.2'
- || project.forge_version != '14.23.5.2859'
- || project.mapping_channel != 'stable'
- || project.mapping_version != '39-1.12') {
+ if (project.minecraft_version != '1.13.2'
+ || project.forge_version != '25.0.223'
+ || project.mapping_channel != 'snapshot'
+ || project.mapping_version != '20180921-1.13') {
throw new GradleException('Unexpected Minecraft, Forge, or mappings target')
}
if (project.loader_name != 'forge' || project.loader_code != '1'
@@ -326,36 +329,34 @@ tasks.register('verifyReleaseConfiguration') {
throw new GradleException('Unexpected release dispatcher target metadata')
}
if (project.mod_id != 'poweradvantage'
- || project.group.toString() != 'com.mcmoddev.poweradvantage'
+ || project.group.toString() != 'zone.moddev.mc.poweradvantage'
|| base.archivesName.get() != 'PowerAdvantage'
|| project.curseforge_project_id != '240982') {
throw new GradleException('Unexpected Forge, Maven, archive, or CurseForge identity')
}
if (project.cf_requirements != 'base-metals,mmd-orespawn'
|| project.cf_optionals != 'additional-loot-tables'
- || project.cf_versions != '1.12.2') {
+ || project.cf_versions != '1.13.2') {
throw new GradleException('Unexpected CurseForge dependency or game-version metadata')
}
- if (project.reborncore_curse_project_id != '500866'
- || project.reborncore_curse_file_id != '3386724'
- || project.reborncore_version != '3.19.4.529'
- || project.reborncore_download_url != 'https://maven.modmuss50.me/RebornCore/'
- + 'RebornCore-1.12.2/3.19.4.529/RebornCore-1.12.2-3.19.4.529-universal.jar'
- || project.reborncore_sha256 != 'B6D11200C5EC66B0031EF4E635AC0203A23CBA6B21F046D402D41E3375798667') {
- throw new GradleException('Unexpected RebornCore compile API identity')
+ if (project.basemetals_version != '3.0.1.113021'
+ || project.orespawn_version != '4.1.0.113021'
+ || project.basemetals_source_commit != '1376a20d7d5ba22d3abc77dc34b49a2c88943697'
+ || project.orespawn_source_commit != '6a7c2bb07b9a7771128812fcc07d185f7d9df58b') {
+ throw new GradleException('Unexpected Base Metals or OreSpawn dependency identity')
}
String mainSource = file(
- 'src/main/java/com/mcmoddev/poweradvantage/PowerAdvantage.java').getText('UTF-8')
+ 'src/main/java/zone/moddev/mc/poweradvantage/PowerAdvantage.java').getText('UTF-8')
if (!mainSource.contains('MODID = "poweradvantage"')
- || !mainSource.contains('VERSION = "2.4.1.112021"')
- || !mainSource.contains('dependencies = "after:basemetals"')) {
- throw new GradleException('PowerAdvantage source identity or optional BaseMetals policy changed')
+ || !mainSource.contains('VERSION = "2.4.4.113021"')
+ || !mainSource.contains('@Mod(PowerAdvantage.MODID)')) {
+ throw new GradleException('PowerAdvantage source identity changed')
}
if (!file('src/main/java/cyano/poweradvantage/api/PoweredEntity.java').isFile()) {
throw new GradleException('Legacy cyano.poweradvantage.api compatibility surface is missing')
}
String entitySource = file(
- 'src/main/java/com/mcmoddev/poweradvantage/init/Entities.java').getText('UTF-8')
+ 'src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java').getText('UTF-8')
[
'tileentity.pipe_terminal', 'tileentity.fluid_drain',
'tileentity.fluid_discharge', 'tileentity.fluid_storage_tank',
@@ -368,10 +369,49 @@ tasks.register('verifyReleaseConfiguration') {
throw new GradleException("Persistent tile entity id changed or disappeared: ${id}")
}
}
- def metadata = new JsonSlurper().parse(file('src/main/resources/mcmod.info'))
- def mod = metadata instanceof List ? metadata.first() : metadata
- if (mod.modid != 'poweradvantage') {
- throw new GradleException('Source mcmod.info does not use the stable poweradvantage mod id')
+ String modsToml = file('src/main/resources/META-INF/mods.toml').getText('UTF-8')
+ ['modId="poweradvantage"', 'modId="basemetals"', 'modId="orespawn"',
+ 'mandatory=true'].each { required ->
+ if (!modsToml.contains(required)) {
+ throw new GradleException("Source mods.toml is missing ${required}")
+ }
+ }
+ if (!modsToml.contains('modId="basemetals"\nmandatory=false')) {
+ throw new GradleException('Base Metals must remain Forge-optional; CurseForge installs it for players')
+ }
+ String steelBarsTag = file(
+ 'src/main/resources/data/forge/tags/items/bars/steel.json').getText('UTF-8')
+ if (!steelBarsTag.contains('"optional"') || !steelBarsTag.contains('"basemetals:steel_bars"')) {
+ throw new GradleException('Steel bars must use Forge optional tag entries when Base Metals is absent')
+ }
+ [
+ 'dusts/coal', 'dusts/lead', 'dusts/redstone', 'dusts/silver', 'dusts/tin',
+ 'gems/diamond', 'gems/emerald',
+ 'ingots/brass', 'ingots/copper', 'ingots/gold', 'ingots/iron',
+ 'ingots/lead', 'ingots/nickel', 'ingots/steel', 'ingots/zinc',
+ 'nuggets/bronze', 'nuggets/copper', 'nuggets/gold', 'nuggets/iron',
+ 'nuggets/lead', 'nuggets/tin',
+ 'plates/copper', 'plates/iron', 'plates/steel',
+ 'rods/copper', 'rods/wooden', 'stone',
+ 'storage_blocks/diamond', 'storage_blocks/steel'
+ ].each { tagPath ->
+ if (!file("src/main/resources/data/forge/tags/items/${tagPath}.json").isFile()) {
+ throw new GradleException("Missing decoupled Forge item tag declaration: ${tagPath}")
+ }
+ }
+ Map provider = new JsonSlurper().parse(file(
+ 'src/main/resources/data/poweradvantage/orespawn/provider.json')) as Map
+ Map desertOil = provider.fluid_deposits[
+ 'poweradvantage:fluid_deposit/desert_crude_oil'].dimensions['minecraft:overworld'] as Map
+ if (desertOil.biome_dictionary != ['SANDY']
+ || (desertOil.excluded_biome_dictionary as Set) != (['BEACH', 'MESA'] as Set)) {
+ throw new GradleException(
+ 'Desert crude oil must use Forge SANDY biomes while excluding BEACH and MESA')
+ }
+ if ((desertOil.host_families as Set) != ([
+ 'sedimentary', 'metamorphic', 'igneous_intrusive', 'igneous_volcanic'] as Set)) {
+ throw new GradleException(
+ 'Desert crude oil must remain compatible with OreSpawn geology-family hosts')
}
}
}
@@ -381,69 +421,34 @@ tasks.register('verifyReleaseDependencies') {
description = 'Resolves and audits Forge plus the exact optional compile APIs.'
dependsOn tasks.named('verifyReleaseConfiguration')
doLast {
- if (!rfApiJar.isFile()) {
- throw new GradleException("Missing tracked RF API jar: ${rfApiJar}")
- }
- String actualRfHash = MessageDigest.getInstance('SHA-256')
- .digest(rfApiJar.bytes).encodeHex().toString().toUpperCase()
- if (actualRfHash != project.rf_api_sha256) {
- throw new GradleException("RF API SHA-256 is ${actualRfHash}, expected ${project.rf_api_sha256}")
- }
-
- def compileArtifacts = configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts
- String expectedRebornCoreGroup = usingStagedRebornCore ? 'curse.maven' : 'RebornCore'
- String expectedRebornCoreName = usingStagedRebornCore
- ? "reborncore-${project.reborncore_curse_project_id}"
- : 'RebornCore-1.12.2'
- def rebornCoreArtifacts = compileArtifacts.findAll { artifact ->
- def id = artifact.moduleVersion.id
- id.group == expectedRebornCoreGroup && id.name == expectedRebornCoreName
- }
- String expectedRebornCoreVersion = usingStagedRebornCore
- ? project.reborncore_curse_file_id
- : project.reborncore_version
- String expectedRebornCore = "${expectedRebornCoreGroup}:${expectedRebornCoreName}:" \
- + expectedRebornCoreVersion
- if (rebornCoreArtifacts.size() != 1
- || "${rebornCoreArtifacts.first().moduleVersion.id.group}:"
- + "${rebornCoreArtifacts.first().moduleVersion.id.name}:"
- + "${rebornCoreArtifacts.first().moduleVersion.id.version}" != expectedRebornCore) {
- throw new GradleException("Expected exactly ${expectedRebornCore}, found "
- + rebornCoreArtifacts.collect { it.moduleVersion.id })
- }
- File rebornCoreJar = rebornCoreArtifacts.first().file
- String actualRebornCoreHash = MessageDigest.getInstance('SHA-256')
- .digest(rebornCoreJar.bytes).encodeHex().toString().toUpperCase()
- if (actualRebornCoreHash != project.reborncore_sha256) {
- throw new GradleException("RebornCore SHA-256 is ${actualRebornCoreHash}, expected "
- + project.reborncore_sha256)
- }
- new ZipFile(rebornCoreJar).withCloseable { zip ->
- ['reborncore/api/power/IEnergyInterfaceTile.class',
- 'reborncore/api/power/EnumPowerTier.class'].each { required ->
- if (zip.getEntry(required) == null) {
- throw new GradleException("RebornCore compile API is missing ${required}")
- }
+ [(baseMetalsDeobfJar): project.basemetals_deobf_sha256,
+ (oreSpawnDeobfJar): project.orespawn_deobf_sha256].each { File dependencyJar, String expectedHash ->
+ if (!dependencyJar.isFile()) {
+ throw new GradleException("Missing pinned dependency jar: ${dependencyJar}")
+ }
+ String actualHash = MessageDigest.getInstance('SHA-256')
+ .digest(dependencyJar.bytes).encodeHex().toString().toUpperCase()
+ if (actualHash != expectedHash) {
+ throw new GradleException("${dependencyJar.name} SHA-256 is ${actualHash}, expected ${expectedHash}")
}
}
-
Set compileFiles = configurations.compileClasspath.files.collect { it.canonicalFile } as Set
- if (!compileFiles.contains(rfApiJar.canonicalFile)
- || !compileFiles.contains(rebornCoreJar.canonicalFile)) {
- throw new GradleException('Optional RF or RebornCore API is missing from compileClasspath')
+ if (!compileFiles.contains(baseMetalsDeobfJar.canonicalFile)
+ || !compileFiles.contains(oreSpawnDeobfJar.canonicalFile)) {
+ throw new GradleException('Pinned Base Metals or OreSpawn API is missing from compileClasspath')
}
Set runtimeFiles = configurations.runtimeClasspath.files.collect { it.canonicalFile } as Set
- if (runtimeFiles.contains(rfApiJar.canonicalFile)
- || runtimeFiles.contains(rebornCoreJar.canonicalFile)) {
- throw new GradleException('Optional RF or RebornCore API leaked onto runtimeClasspath')
+ if (!runtimeFiles.contains(baseMetalsDeobfJar.canonicalFile)
+ || !runtimeFiles.contains(oreSpawnDeobfJar.canonicalFile)) {
+ throw new GradleException('Pinned Base Metals or OreSpawn is missing from development runtime')
}
- List forbiddenRuntimeMods = runtimeFiles.collect { it.name.toLowerCase(Locale.ROOT) }
+ List obsoleteRuntimeMods = runtimeFiles.collect { it.name.toLowerCase(Locale.ROOT) }
.findAll { name ->
- name.contains('basemetals') || name.contains('orespawn')
+ name.contains('rf-api') || name.contains('redstoneflux')
|| name.contains('tesla') || name.contains('reborncore')
}
- if (!forbiddenRuntimeMods.isEmpty()) {
- throw new GradleException("Optional mods leaked onto runtimeClasspath: ${forbiddenRuntimeMods}")
+ if (!obsoleteRuntimeMods.isEmpty()) {
+ throw new GradleException("Obsolete energy APIs leaked onto runtimeClasspath: ${obsoleteRuntimeMods}")
}
}
}
@@ -486,27 +491,33 @@ tasks.register('verifyReleaseArtifacts') {
File mainJar = new File(libs, expectedReleaseFiles.get()[0])
new ZipFile(mainJar).withCloseable { zip ->
List names = zip.entries().collect { it.name }
- ['mcmod.info', 'pack.mcmeta',
- 'com/mcmoddev/poweradvantage/PowerAdvantage.class',
+ ['META-INF/mods.toml', 'pack.mcmeta',
+ 'zone/moddev/mc/poweradvantage/PowerAdvantage.class',
'cyano/poweradvantage/api/PoweredEntity.class',
- 'assets/poweradvantage/lang/en_us.lang'].each { required ->
+ 'cyano/poweradvantage/api/fluid/FluidNetworkApi.class',
+ 'assets/poweradvantage/lang/en_us.json',
+ 'data/poweradvantage/recipes/fluid_drain.json',
+ 'data/poweradvantage/advancements/recipes/sprocket_rod_normal.json'].each { required ->
if (!names.contains(required)) {
throw new GradleException("Release jar is missing ${required}")
}
}
['agent-notes/', '.codex/', 'src/test/', 'fernflower_abstract_parameter_names.txt',
'cofh/api/', 'reborncore/api/', 'cyano/basemetals/', 'com/mcmoddev/lib/',
+ 'zone/moddev/mc/basemetals/', 'zone/moddev/mc/orespawn/',
+ 'zone/moddev/mc/advantageworks/', 'com/mojang/serialization/',
'net/minecraft/', 'net/minecraftforge/'].each { forbidden ->
if (names.any { it.contains(forbidden) }) {
throw new GradleException("Release jar contains forbidden entry matching ${forbidden}")
}
}
- def metadata = new JsonSlurper().parseText(zip.getInputStream(zip.getEntry('mcmod.info'))
- .getText(StandardCharsets.UTF_8.name()))
- def mod = metadata instanceof List ? metadata.first() : metadata
- if (mod.modid != project.mod_id || mod.version != project.mod_version
- || mod.mcversion != project.minecraft_version) {
- throw new GradleException('Packaged mcmod.info identity, version, or target is incorrect')
+ String modsToml = zip.getInputStream(zip.getEntry('META-INF/mods.toml'))
+ .getText(StandardCharsets.UTF_8.name())
+ if (!modsToml.contains('modId="poweradvantage"')
+ || !modsToml.contains("version=\"${project.mod_version}\"")
+ || !modsToml.contains('modId="basemetals"')
+ || !modsToml.contains('modId="orespawn"')) {
+ throw new GradleException('Packaged mods.toml identity, version, or dependencies are incorrect')
}
def manifest = new Manifest(zip.getInputStream(zip.getEntry('META-INF/MANIFEST.MF'))).mainAttributes
if (manifest.getValue('Implementation-Version') != project.mod_version
@@ -530,7 +541,7 @@ tasks.register('verifyReleaseArtifacts') {
}
new ZipFile(new File(libs, expectedReleaseFiles.get()[1])).withCloseable { sourceZip ->
- if (sourceZip.getEntry('com/mcmoddev/poweradvantage/PowerAdvantage.java') == null
+ if (sourceZip.getEntry('zone/moddev/mc/poweradvantage/PowerAdvantage.java') == null
|| sourceZip.getEntry('cyano/poweradvantage/api/PoweredEntity.java') == null) {
throw new GradleException('Sources jar is missing implementation or legacy API sources')
}
@@ -548,6 +559,7 @@ tasks.register('writeReleaseChecksums') {
description = 'Writes SHA-256 checksums for the audited release jars.'
dependsOn tasks.named('verifyReleaseArtifacts')
def outputFile = layout.buildDirectory.file('release/SHA256SUMS')
+ inputs.files(provider { expectedReleaseFiles.get().collect { layout.buildDirectory.file("libs/${it}").get().asFile } })
outputs.file(outputFile)
doLast {
File output = outputFile.get().asFile
@@ -698,10 +710,24 @@ eclipse {
classpath {
downloadSources = true
downloadJavadoc = true
+ file.whenMerged { model ->
+ // Eclipse must not overwrite expanded metadata with src/main/resources templates.
+ def rawResources = sourceSets.main.resources.srcDirs.collect { relativePath(it).replace('\\', '/') }
+ model.entries.removeAll { entry ->
+ entry instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder
+ && (rawResources.contains(entry.path) || entry.path == 'build/resources/main')
+ }
+ def processed = new org.gradle.plugins.ide.eclipse.model.SourceFolder('build/resources/main', 'bin/main')
+ processed.entryAttributes['gradle_scope'] = 'main'
+ processed.entryAttributes['gradle_used_by_scope'] = 'main,test'
+ model.entries.add(processed)
+ }
}
synchronizationTasks 'isolateEclipseProductionRuns'
}
+tasks.named('eclipseClasspath') { dependsOn tasks.named('processResources') }
+
tasks.register('configureEclipseBuildship') {
group = 'ide'
description = 'Creates Buildship preferences for ForgeGradle 7 Eclipse imports.'
@@ -751,6 +777,21 @@ tasks.register('isolateEclipseProductionRuns') {
contents = contents.replace(
'key="MC_VERSION" value="${MC_VERSION}"',
"key=\"MC_VERSION\" value=\"${project.minecraft_version}\"")
+ if (launch.name in ['runClient.launch', 'runServer.launch', 'runData.launch']) {
+ // Eclipse compiles into bin/main, not Gradle's merged run output.
+ def launchXml = new groovy.xml.XmlSlurper().parseText(contents)
+ def modClasses = launchXml.mapAttribute.find {
+ it.@key.text() == 'org.eclipse.debug.core.environmentVariables'
+ }.mapEntry.findAll { it.@key.text() == 'MOD_CLASSES' }
+ if (modClasses.size() != 1) {
+ throw new GradleException("${launch.name} must define one MOD_CLASSES entry")
+ }
+ String mainOutput = file('bin/main').absolutePath
+ String eclipseModClasses = "${project.mod_id}%%${mainOutput}" +
+ "${File.pathSeparator}${project.mod_id}%%${mainOutput}"
+ modClasses[0].@value = eclipseModClasses
+ contents = groovy.xml.XmlUtil.serialize(launchXml)
+ }
launch.setText(contents.replace('\r\n', '\n'), 'UTF-8')
}
['runClient.launch', 'runServer.launch'].each { name ->
@@ -840,21 +881,52 @@ tasks.register('verifyEclipseProductionClasspath') {
|| !projectMetadata.contains('org.eclipse.buildship.core.gradleprojectbuilder')) {
throw new GradleException('Eclipse project metadata is missing its Buildship nature or builder')
}
- File eclipseModMetadata = file('bin/main/mcmod.info')
- def eclipseMetadata = new JsonSlurper().parseText(eclipseModMetadata.getText('UTF-8'))
- def eclipseMod = eclipseMetadata instanceof List ? eclipseMetadata.first() : eclipseMetadata
- if (eclipseMod.modid != project.mod_id || eclipseMod.version != project.mod_version
- || eclipseMod.mcversion != project.minecraft_version) {
+ File eclipseModMetadata = file('bin/main/META-INF/mods.toml')
+ String eclipseMetadata = eclipseModMetadata.getText('UTF-8')
+ if (!eclipseMetadata.contains("modId=\"${project.mod_id}\"")
+ || !eclipseMetadata.contains("version=\"${project.mod_version}\"")) {
throw new GradleException('Eclipse output contains incorrect mod metadata')
}
+ def eclipseClasspath = new groovy.xml.XmlSlurper().parse(file('.classpath'))
+ def processedResources = eclipseClasspath.classpathentry.findAll {
+ it.@kind.text() == 'src' && it.@path.text() == 'build/resources/main'
+ }
+ def rawResourcePaths = sourceSets.main.resources.srcDirs.collect { relativePath(it).replace('\\', '/') }
+ if (processedResources.size() != 1 || processedResources[0].@output.text() != 'bin/main'
+ || eclipseClasspath.classpathentry.any {
+ it.@kind.text() == 'src' && rawResourcePaths.contains(it.@path.text())
+ }) {
+ throw new GradleException('Eclipse must copy processed resources, never raw mod metadata templates')
+ }
+ String mainOutput = eclipseClasspath.classpathentry.find {
+ it.@kind.text() == 'src' && it.@path.text() == 'src/main/java'
+ }.@output.text()
+ if (file(mainOutput).canonicalFile != file('bin/main').canonicalFile) {
+ throw new GradleException('Eclipse production classes must use bin/main')
+ }
+ String expectedModClasses = "${project.mod_id}%%${file(mainOutput).absolutePath}" +
+ "${File.pathSeparator}${project.mod_id}%%${file(mainOutput).absolutePath}"
+ ['runClient.launch', 'runServer.launch', 'runData.launch'].each { name ->
+ def launch = new groovy.xml.XmlSlurper().parse(file(name))
+ String modClasses = launch.mapAttribute.find {
+ it.@key.text() == 'org.eclipse.debug.core.environmentVariables'
+ }.mapEntry.find { it.@key.text() == 'MOD_CLASSES' }.@value.text()
+ if (modClasses != expectedModClasses) {
+ throw new GradleException("${name} does not discover the Eclipse production mod in bin/main")
+ }
+ }
['runClient.launch', 'runServer.launch'].each { name ->
+ def launch = new groovy.xml.XmlSlurper().parse(file(name))
String contents = file(name).getText('UTF-8').replace('\\', '/')
- if (!contents.contains('org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE')
- || !contents.contains('value="true"')
+ if (launch.booleanAttribute.find {
+ it.@key.text() == 'org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE'
+ }.@value.text() != 'true'
|| contents.contains('src/test') || contents.contains('bin/test')) {
throw new GradleException("${name} does not isolate production code")
}
- if (!contents.contains('PROJECT_ATTR" value="PowerAdvantage"')) {
+ if (launch.stringAttribute.find {
+ it.@key.text() == 'org.eclipse.jdt.launching.PROJECT_ATTR'
+ }.@value.text() != 'PowerAdvantage') {
throw new GradleException("${name} targets the wrong Eclipse project")
}
if (contents.contains('org.eclipse.buildship.core.gradleclasspathcontainer')) {
@@ -882,9 +954,9 @@ tasks.register('verifyEclipseProductionClasspath') {
throw new GradleException(
"${name} does not preserve the Forge bootstrap classpath order")
}
- if (contents.contains('${MC_VERSION}')
- || !contents.contains(
- "key=\"MC_VERSION\" value=\"${project.minecraft_version}\"")) {
+ if (contents.contains('${MC_VERSION}') || launch.mapAttribute.find {
+ it.@key.text() == 'org.eclipse.debug.core.environmentVariables'
+ }.mapEntry.find { it.@key.text() == 'MC_VERSION' }.@value.text() != project.minecraft_version) {
throw new GradleException("${name} does not define a literal Minecraft version")
}
}
@@ -901,11 +973,27 @@ tasks.register('verifyModelAssets') {
inputs.dir(blockstateRoot)
doLast {
+ Set vanillaAssets = [] as Set
+ configurations.runtimeClasspath.files.findAll {
+ it.name.startsWith('client-extra-') && it.name.endsWith('.jar')
+ }.each { File archive ->
+ new ZipFile(archive).withCloseable { zip ->
+ zip.entries().each { entry -> vanillaAssets.add(entry.name) }
+ }
+ }
def verifyReference = { String reference, String assetType, String sourcePath ->
if (reference.startsWith('#')) return
int separator = reference.indexOf(':')
String namespace = separator >= 0 ? reference.substring(0, separator) : 'minecraft'
String path = separator >= 0 ? reference.substring(separator + 1) : reference
+ if (namespace == 'minecraft') {
+ if (assetType == 'models' && path.startsWith('builtin/')) return
+ String suffix = assetType == 'textures' ? '.png' : '.json'
+ if (!vanillaAssets.contains("assets/minecraft/${assetType}/${path}${suffix}".toString())) {
+ throw new GradleException("${sourcePath} references missing vanilla ${assetType} asset ${reference}")
+ }
+ return
+ }
if (namespace != project.mod_id) return
File target = new File(resourceRoot,
@@ -936,7 +1024,7 @@ tasks.register('verifyModelAssets') {
if (node instanceof Map) {
node.each { key, value ->
if (key == 'model' && value instanceof String) {
- verifyReference(value, 'models/block', sourcePath)
+ verifyReference(value, 'models', sourcePath)
} else {
verifyBlockstateModels(value, sourcePath)
}
@@ -946,7 +1034,57 @@ tasks.register('verifyModelAssets') {
}
}
fileTree(blockstateRoot) { include '**/*.json' }.files.sort().each { File blockstateFile ->
- verifyBlockstateModels(new JsonSlurper().parse(blockstateFile), blockstateFile.path)
+ def blockstate = new JsonSlurper().parse(blockstateFile)
+ if (blockstate.containsKey('forge_marker')
+ || blockstate.variants?.containsKey('normal') || blockstate.variants?.containsKey('fluid')) {
+ throw new GradleException("${blockstateFile.path} uses a removed legacy blockstate format")
+ }
+ verifyBlockstateModels(blockstate, blockstateFile.path)
+ }
+ }
+}
+
+tasks.register('verifyRecipeTagGuards') {
+ group = 'verification'
+ description = 'Rejects recipes that can collapse when a Forge material tag is empty.'
+ File recipeRoot = file("src/main/resources/data/${project.mod_id}/recipes")
+ inputs.dir(recipeRoot)
+ doLast {
+ Closure collectTags
+ collectTags = { Object node, Set tags ->
+ if (node instanceof Map) {
+ node.each { key, value ->
+ if (key == 'conditions' || key == 'result') return
+ if (key == 'tag' && value instanceof String && value.startsWith('forge:')) {
+ tags.add(value)
+ } else {
+ collectTags(value, tags)
+ }
+ }
+ } else if (node instanceof Collection) {
+ node.each { collectTags(it, tags) }
+ }
+ }
+ fileTree(recipeRoot) { include '**/*.json' }.files.sort().each { File recipeFile ->
+ Map recipe = new JsonSlurper().parse(recipeFile) as Map
+ Set tags = [] as Set
+ collectTags(recipe, tags)
+ Collection conditions = (recipe.conditions ?: []) as Collection
+ if (conditions.any { !(it instanceof Map) }) {
+ throw new GradleException("${recipeFile.path} contains a non-object recipe condition")
+ }
+ Set guards = conditions
+ .findAll { it instanceof Map && it.type == 'poweradvantage:tag_not_empty' }
+ .collect { it.tag as String } as Set
+ Set emptyGuards = conditions.findAll { it.type == 'poweradvantage:tag_empty' }
+ .collect { it.tag as String } as Set
+ if (!(emptyGuards.intersect(guards)).isEmpty()) {
+ throw new GradleException("${recipeFile.path} requires the same tag to be empty and nonempty")
+ }
+ Set missing = tags - guards
+ if (!missing.isEmpty()) {
+ throw new GradleException("${recipeFile.path} lacks tag_not_empty guards for ${missing.sort()}")
+ }
}
}
}
@@ -954,4 +1092,72 @@ tasks.register('verifyModelAssets') {
tasks.named('check') {
dependsOn tasks.named('verifyReleaseConfiguration')
dependsOn tasks.named('verifyModelAssets')
+ dependsOn tasks.named('verifyRecipeTagGuards')
+}
+
+def compileClientResourceProbe = tasks.register('compileClientResourceProbe', JavaCompile) {
+ group = 'verification'
+ description = 'Compiles the build-only client model and translation probe.'
+ dependsOn tasks.named('classes')
+ source fileTree('test-rig/client-resources/java')
+ classpath = files(sourceSets.main.output, sourceSets.main.compileClasspath)
+ destinationDirectory = layout.buildDirectory.dir('client-resource-probe/classes')
+}
+
+tasks.register('clientResourceProbeJar', Jar) {
+ group = 'verification'
+ description = 'Builds a test-only client probe; never part of a release or normal launch.'
+ dependsOn compileClientResourceProbe
+ archiveFileName = 'advantageclientresources-deobf.jar'
+ destinationDirectory = layout.buildDirectory.dir('client-resource-probe')
+ from compileClientResourceProbe.flatMap { it.destinationDirectory }
+ from 'test-rig/client-resources/resources'
+}
+
+// Optional coordinated play-test launch. The ordinary launch stays Power-only;
+// these deobf jars are runtime inputs, never release contents or compile dependencies.
+def advantageExtensionJars = [
+ steamadvantage: file(providers.gradleProperty('steamAdvantageDeobfJar').getOrElse(
+ '../SteamAdvantage/SteamAdvantage/build/libs-dev/SteamAdvantage-2.2.5.113021-deobf.jar')),
+ electricadvantage: file(providers.gradleProperty('electricAdvantageDeobfJar').getOrElse(
+ '../ElectricAdvantage/ElectricAdvantage/build/libs-dev/ElectricAdvantage-2.2.5.113021-deobf.jar'))
+]
+
+tasks.register('prepareCombinedEclipseLaunch') {
+ group = 'ide'
+ description = 'Creates an additional production-only Eclipse client launch with all three Advantage mods.'
+ dependsOn tasks.named('verifyEclipseProductionClasspath')
+ doLast {
+ def profile = new JsonSlurper().parse(file('test-rig/profiles/dev-core.json'))
+ advantageExtensionJars.each { String id, File jar ->
+ def pin = profile.mods.find { it.id == id }
+ if (!jar.isFile()) throw new GradleException("Build ${id} first, or override its *AdvantageDeobfJar property: ${jar}")
+ String hash = MessageDigest.getInstance('SHA-256').digest(jar.bytes).encodeHex().toString()
+ if (pin == null || !hash.equalsIgnoreCase(pin.sha256 as String)) {
+ throw new GradleException("${id} does not match the audited dev-core dependency pin: ${jar}")
+ }
+ }
+ def launch = new groovy.xml.XmlParser().parse(file('runClient.launch'))
+ def classpath = launch.listAttribute.find { it.@key == 'org.eclipse.jdt.launching.CLASSPATH' }
+ if (classpath == null) throw new GradleException('Base client launch has no explicit production classpath')
+ advantageExtensionJars.values().each { File jar ->
+ String entry = groovy.xml.XmlUtil.serialize(new Node(null, 'runtimeClasspathEntry',
+ [externalArchive: jar.canonicalPath, type: '2', path: '5']))
+ classpath.appendNode('listEntry', [value: entry])
+ }
+ File combined = file('runClientAllAdvantage.launch')
+ combined.setText(groovy.xml.XmlUtil.serialize(launch), 'UTF-8')
+ def parsed = new groovy.xml.XmlSlurper().parse(combined)
+ def archivePaths = parsed.listAttribute.find {
+ it.@key.text() == 'org.eclipse.jdt.launching.CLASSPATH'
+ }.listEntry.collect { new groovy.xml.XmlSlurper().parseText(it.@value.text()).@externalArchive.text() }
+ advantageExtensionJars.values().each { File jar ->
+ if (archivePaths.count(jar.canonicalPath) != 1) throw new GradleException("Missing or repeated extension: ${jar}")
+ }
+ if (combined.text.contains('advantageworkstest') || combined.text.contains('client-resource-probe')
+ || combined.text.contains('bin/test') || combined.text.contains('buildship.core.gradleclasspathcontainer')) {
+ throw new GradleException('Combined launch leaked test code or an unfiltered classpath')
+ }
+ logger.lifecycle("Prepared ${combined.name}; uses the same run directory and saves as runClient.launch.")
+ }
}
diff --git a/docs/1.13/CHUNK-AND-DRILLS.md b/docs/1.13/CHUNK-AND-DRILLS.md
new file mode 100644
index 00000000..b4a9f219
--- /dev/null
+++ b/docs/1.13/CHUNK-AND-DRILLS.md
@@ -0,0 +1,339 @@
+# Chunk recovery and moving-drill qualification
+
+These tests complement the [frozen migration matrix](MIGRATION-MATRIX.md) and
+[repeat-load endurance](ENDURANCE.md). They operate packaged 1.13.2 Advantage
+mods in disposable worlds, not Eclipse classes or an irreplaceable player save.
+
+Latest qualification: [the current network-repair candidate passes](#current-network-repair-regression).
+The earlier repair results and original failure evidence remain below for comparison.
+
+## Protocol
+
+`test-rig/migration/lifecycle.py` creates a fresh world and a copy of the original
+Cyano 1.10 -> maintained 1.12 -> 1.13 conversion fixture. It builds isolated lanes
+above superflat terrain, at least 2,048 blocks from spawn. The converted copy's
+lanes are newly commissioned equipment; this is not operation coverage for every
+original machine in an old world.
+
+Twelve drills cover both mods in all six facings. Each advances one block along
+horizontal track across a chunk boundary, against an immediate bedrock stop.
+Each carries seven dirt with identifying custom item NBT and thirteen apples.
+Public energy setters commission a finite reserve within the actual capacity:
+40/50 steam or 400/500 electricity. There is no generation during the move.
+
+A test-only world-tick observer records the drill immediately before and after
+its native movement. It checks destination, serialized position, facing, exact
+inventory including tags, replacement of the old drill with conduit, and cost:
+10 steam plus its ordinary 1/16-per-tick decay, or 250 electricity. After recording
+the move, the observer adds an external redstone block to hold the new drill.
+It never rewrites that drill's NBT, inventory, progress or energy.
+
+Independent lanes place a steam furnace, electric furnace and fluid receiver on
+the opposite side of a chunk seam from their supply/conduit. Native inventory
+setters feed four iron ores per furnace; solid fuel is retained in the steam
+furnace. Public fluid-network offers supply five measured 1,000 mB water batches
+to a 10,000 mB metal tank. Sources and pipes are not replaced across reloads.
+
+Three cycles release the fixture chunk tickets and queue normal provider unloads.
+The non-loading observer requires actual Forge unload events, absence from the
+loaded-chunk map, and continued absence after another two seconds. Reload requires
+new load events and different tile instances, stable facing/inventory/tank contents,
+and resumed ore processing/fluid acceptance. A snapshot must not load the chunk
+whose absence it claims to measure.
+
+The server then saves and exits completely. A new process checks retained moved
+drills, inventories and 4,000 mB water before new inputs. Derived active/redstone
+flags settle through normal ticks; they are not treated as immutable save state.
+The original pipe at each moved drill is now supplied, rear chests are installed,
+and bounded mining checks one cobblestone -> one gravel for Steam and nine
+cobblestone -> nine cobblestone drops for Electric. All twenty carried items and
+their tags must reach the rear chest with the mined output. Another real chunk
+cycle checks those results remain intact.
+
+Finally, long-lived obsidian targets establish nonzero mining progress. Chunk
+reload and a second full process restart must retain progress toward the same
+target. These observations distinguish retained completed items from lost work
+on an unfinished block. No redstone intervention is made during the mid-block
+checks, because redstone intentionally cancels progress.
+
+The repair regression also performs two separate saved-NBT read checks after
+the real process restarts. `legacy-read` round-trips each drill's actual saved
+record with only the optional target-state snapshot removed. It never invents
+inventory, energy or progress values. `changed-target-read` replaces the saved
+target's centre with bedrock before reading that record; progress and target
+coordinates must clear. These focused read-path checks supplement, rather than
+replace, the genuine chunk and process lifecycle tests.
+
+## Repeating the test
+
+From the PowerAdvantage checkout, with built/pinned release and harness jars:
+
+```text
+gradlew.bat -p test-rig check assemble
+test-rig/migration/.venv/Scripts/python.exe -m unittest discover -s test-rig/migration -p test_*.py -v
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/lifecycle.py --java8 --migrated --output run-lifecycle/new-run --port 25586
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/audit_lifecycle.py --run run-lifecycle/new-run --output run-lifecycle/new-run/qualification-audit.json
+```
+
+Use the normal Python executable on other platforms. Output directories must be
+new. All source fixture files are fingerprinted before/after; staged jars are
+SHA-256 checked. The runner stops its own servers on both success and failure.
+Minecraft worlds, logs and reports remain Git-ignored. A completed run may return
+`FAIL` for a reproducible gameplay defect; a passing evidence audit validates the
+record, not the defective gameplay.
+
+## Coverage limits
+
+This gate covers one horizontal track step for each of the six drill facings,
+not every track direction, long multi-step travel, track junctions or blocked/full
+output behavior. Whole fixture networks unload together; deliberately partial
+network unload and asymmetric loading are separate scenarios. It does not claim
+client animation/GUI acceptance, elevators, weapons, or a complete Sylvester
+modpack upgrade. Known Forge/Base Metals log errors remain visible in the reports.
+
+The subsequent [partial-network and extended-drill campaign](PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md)
+addresses selected limits without changing what this earlier gate proved.
+
+## Before repair: 6 October 2026
+
+The authoritative bounded-reserve run is `run-lifecycle/20261006-native-final`.
+It completed against both a fresh 1.13 world and a disposable copy of the
+original 1.10 -> maintained 1.12 -> 1.13 fixture. Earlier fixture trials remain
+diagnostic evidence only. No production gameplay code changed in this pass.
+
+| Acceptance case | Fresh 1.13 | Converted fixture |
+| --- | --- | --- |
+| Five genuine chunk unload/reload cycles | PASS | PASS |
+| Network processing resumes without replacing pipes/sources | PASS | PASS |
+| Water conservation: five offers, 5,000 mB retained | PASS | PASS |
+| Moved drill positions, six facings, inventories and custom item NBT | PASS | PASS |
+| Steam helper-bit orientation after reload | PASS | PASS |
+| Steam movement energy cost | PASS | PASS |
+| Electric movement energy cost | FAIL | FAIL |
+| Bounded mining yield retained across reload | PASS | PASS |
+| Output reaches an empty vanilla rear chest | FAIL | FAIL |
+| Unfinished mining progress survives chunk/full restart | FAIL | FAIL |
+| Independent evidence audit | PASS | PASS |
+| Overall machine qualification | FAIL | FAIL |
+
+Each world exercised 60 unique chunks over five cycles: 300 independently
+confirmed unload events per world, 600 total. Each also performed twelve moves
+and two full process restarts. All six Steam and six Electric facings were
+covered in both worlds. Each drill retained its twenty carried items, their
+custom tags, and the exact mined yield; the failed output check means those
+items remained inside the drill, not that they disappeared.
+
+The locally retained `lifecycle-results.json` contains every case, input jar
+SHA-256, source-fixture protection result and log diagnostic.
+`qualification-audit.json` independently reconciles those verdicts against raw
+snapshots, real chunk events and staged jar hashes. The source fixture's before
+and after fingerprints match. All six server processes shut down normally;
+five emitted Forge's temporary-resource-directory cleanup error on exit. There
+were no ticking-block-entity crashes, failed native inventory commands or
+"Can't keep up!" warnings. These cleanup diagnostics remain in the report.
+
+The harness used was `AdvantageWorksTestHarness-1.1.0.113021.jar`, SHA-256
+`559D141BF7798C5BD2748170BDEBC235BB3FE2D732CB5DE75CFA2E5E9405E421`.
+All production jar pins remained unchanged. The harness checks passed (nine
+Java tests), the migration runner tests passed (33 Python tests), and
+PowerAdvantage's `check verifyReleaseArtifacts` passed, including harness
+isolation. None of those passes overrides a failed gameplay assertion.
+
+## Confirmed defect register
+
+### EA-113-001: Electric track moves do not pay their energy cost
+
+- Expected: the moved Electric Drill retains its inventory/facing and debits the
+ defined `ENERGY_COST_MOVE = 250` from the transferred reserve.
+- Observed in both worlds: all six moves leave 400 electricity as 400,
+ rather than 150. The finite reserve is within the drill's 500-unit capacity.
+- Cause: `ElectricDrillTileEntity.followTrack()` copies energy into the new tile,
+ then calls `this.subtractEnergy(...)` on the old, removed tile instead
+ (before-repair source: `machines/ElectricDrillTileEntity.java:164`).
+- Impact: free track travel, not lost inventory. Steam's 10-unit debit works.
+- Older versions: the same implementation is present in maintained 1.10 and
+ 1.12 source. Those versions were not runtime-qualified for this defect here.
+
+### AW-113-001: Drills cannot insert into a new empty vanilla rear chest
+
+- Expected: both drills deliver stored and freshly mined items into an available
+ rear inventory, consistent with their documented automation purpose.
+- Observed in both worlds: every rear chest remains empty after 60 seconds.
+ The drill retains the exact twenty carried items plus the correct mined yield:
+ one gravel for Steam or nine cobblestone for Electric. Custom item NBT survives.
+- Cause: both `inventoryTransfer()` implementations treat only `null` as an empty
+ destination. `InventoryWrapper.getStackInSlot()` returns vanilla's non-null
+ `ItemStack.EMPTY`, which matches neither insertion branch. Before-repair locations:
+ Steam `machines/SteamDrillTileEntity.java:419`, Electric
+ `machines/ElectricDrillTileEntity.java:426`, Power
+ `util/InventoryWrapper.java:236`.
+- Impact: rear output stalls; this observation is not item disappearance.
+- Older versions: maintained 1.12 has the same null-only check. A legacy 1.10
+ empty-slot contract is different; do not claim the same runtime failure there.
+
+### AW-113-002: Reload resets work on an unfinished drill target
+
+- Expected: saved mining progress continues against an unchanged target, while
+ inventories, energy and facing are restored.
+- Observed in both worlds: progress of 92-98 ticks becomes 0-6 ticks after
+ chunk reload; progress of 100-107 becomes 1-6 after full restart. The target
+ remains the same obsidian block in all twelve lanes per world. No redstone
+ signal or target completion accounts for the reset.
+- Cause: both drills read saved `progress`, then reconstruct the target through
+ `targetBlock()`, which unconditionally sets `progress = 0`. Deferred target
+ reconstruction repeats this on the first loaded tick. Before-repair reset locations:
+ Steam `machines/SteamDrillTileEntity.java:324` and Electric
+ `machines/ElectricDrillTileEntity.java:313`.
+- Impact: unfinished work is lost; repeated unloads can prevent a slow block
+ from ever completing. Already collected items remain intact.
+- Older versions: the same reconstruction/reset sequence exists in maintained
+ 1.10 and 1.12 source. Runtime reproduction here is specifically the 1.13 candidate.
+
+These are failing acceptance cases, not `EXPECTED_FAIL` baselines or rewritten
+gameplay requirements. The observations above describe the original failing
+jars; the repair and its independently audited rerun are recorded below. No
+defect is silently accepted by this test report.
+
+## Repair implementation
+
+The 1.13.2 candidate repairs are confined to the two drill implementations:
+
+- Electric track movement charges the new tile before marking it dirty and
+ removing the original. The defined 250-unit price is unchanged.
+- Both rear-output paths recognize `null` and `ItemStack.EMPTY`, preserve stack
+ tags and the one-item transfer rate, and mark source/destination inventories
+ dirty after successful insertion.
+- Target reconstruction retains saved progress, bounded by zero and the
+ recalculated goal. New target acquisition, redstone cancellation and target
+ invalidation still reset work. Steam initializes its redstone reading when
+ restoring the target, avoiding the constructor's default shutdown state.
+- Existing `progress`, `targetX`, `targetY` and `targetZ` keys remain unchanged.
+ Optional `TargetState` (Steam) and `TargetStates` (Electric's nine blocks)
+ snapshots permit changed-target detection on restoration. Legacy records
+ without a snapshot remain readable, but cannot prove a target's old identity;
+ they reconstruct the current target at their saved coordinates.
+
+Mod versions remain the unpublished `2.2.5.113021` port candidates. No registry,
+tile/entity ID, recipe, dependency or PowerAdvantage gameplay changes accompany
+these repairs. The 1.10 and 1.12 implementations are not modified here.
+
+## Repair retest
+
+The bounded-reserve run `run-lifecycle/20261006-drill-fixes` completed on
+6 October 2026. Both profiles and the independent evidence audit passed.
+
+| Repair acceptance case | Fresh 1.13 | Converted fixture |
+| --- | --- | --- |
+| All six Steam/Electric movements, inventory/facing and exact energy debit | PASS | PASS |
+| Five genuine chunk unload/reload cycles and resumed network processing | PASS | PASS |
+| 5,000 mB water conserved | PASS | PASS |
+| All carried items, custom tags and mined drops reach the rear chest | PASS | PASS |
+| Output and helper-bit orientation retained after chunk reload | PASS | PASS |
+| Unfinished progress retained after chunk reload and full process restart | PASS | PASS |
+| Legacy record without target snapshot retains progress | PASS | PASS |
+| Changed saved target clears progress and target coordinates | PASS | PASS |
+| Independent evidence audit and overall machine qualification | PASS | PASS |
+
+The rerun covered 24 moves, 600 actual fixture-chunk unloads, four full process
+restarts and 48 extra saved-record read checks across the two worlds. Electric
+movement now leaves 150 from a 400-unit reserve. Steam still pays 10 plus normal
+decay. The exact twenty carried items plus one gravel (Steam) or nine cobblestone
+(Electric) reached each rear chest and survived reload with their custom tags.
+
+Unfinished progress of 92-97 ticks continued to 100-105 after chunk reload;
+201-206 ticks continued to 208-213 after full restart. The target coordinates
+remained unchanged. The legacy read kept progressing; the changed-target read
+cleared work rather than applying it to bedrock.
+
+The original failed run remains intact. Reauditing it with the strengthened
+oracle still reports gameplay `FAIL`, while the repaired run reports `PASS`.
+The auditor recomputes individual verdicts and overall qualification from raw
+snapshots instead of trusting the runner's status labels.
+
+Fixed candidate release pins:
+
+| Jar | SHA-256 |
+| --- | --- |
+| `SteamAdvantage-2.2.5.113021.jar` | `D7D8A2E728E779EF0B624EF8AF3437F3354D618F12E279BE1C134F4AD5E690E3` |
+| `ElectricAdvantage-2.2.5.113021.jar` | `72027C2DC273FD28F23AF01BB3EB495C75076E19B27A38CBDC5213B35A6198EB` |
+| `AdvantageWorksTestHarness-1.1.0.113021.jar` | `B5043325294299E97B5C2E20559FEF35E9DBF0799B12C43D36E7EC1DB5D39083` |
+
+PowerAdvantage, Base Metals and OreSpawn pins did not change. Development and
+packaged profiles now reference the repaired Steam/Electric hashes. The full
+FG7 release suites and Eclipse audits passed for both changed mods, including
+six Steam tests and nine Electric tests. The harness's nine Java tests and
+34 Python runner tests passed; PowerAdvantage's artifact/isolation audit passed.
+Separate forced-rebuild Gradle invocations produced identical release, deobf,
+sources and Javadoc hashes for both changed mods. All twenty Steam/Electric
+development and packaged profile pins match the rebuilt files.
+
+The converted source fixture's fingerprints match, and all test servers stopped.
+Four processes logged the known Forge temporary-directory cleanup diagnostic;
+two fresh-world boots logged its config-file watcher diagnostic. Neither is a
+machine crash or a failed assertion. No ticking-block-entity crash, native
+inventory command failure or "Can't keep up!" warning was found. The raw logs
+and their diagnostics remain in `lifecycle-results.json`.
+
+This is focused chunk/drill qualification, not a new complete migration-matrix,
+30-minute endurance, clean-cache or manual client campaign. The coverage limits
+above still apply. No original player save, 1.10/1.12 code, commit or published
+artifact was modified.
+
+## Current network-repair regression
+
+On 6 October 2026, `run-lifecycle/20261006-current-power-reload-154618` repeated
+the unchanged whole-network and mid-block protocol after the PowerAdvantage
+[partial-network repair](PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md). Both the runner
+and independent `qualification-audit.json` report **PASS** for fresh 1.13 and
+the original Cyano 1.10 -> maintained 1.12 -> 1.13 disposable copy.
+
+| Acceptance case | Fresh 1.13 | Converted fixture |
+| --- | --- | --- |
+| All six Steam/Electric drill facings, movement cost, carried items and tags | PASS | PASS |
+| Five real whole-network chunk unload/reload cycles | PASS | PASS |
+| Furnace processing and fluid delivery resume without replacing equipment | PASS | PASS |
+| Exactly 5,000 mB water retained | PASS | PASS |
+| Bounded mining yield reaches rear chests and survives reload | PASS | PASS |
+| Steam helper-bit orientation restores correctly | PASS | PASS |
+| Same-target unfinished progress survives chunk reload and full restart | PASS | PASS |
+| Legacy saved record retains progress; changed target clears it | PASS | PASS |
+| Independent evidence audit and machine qualification | PASS | PASS |
+
+The run records 24 native drill moves, 600 real chunk-unload events, four full
+process restarts and 48 saved-record read checks across both profiles. Each
+profile retains all twenty carried items per drill, including their custom tags,
+plus the expected one gravel (Steam) or nine cobblestone (Electric) mining yield.
+Fresh-world progress advances from 97-99 to 105-107 ticks after chunk reload and
+206-208 to 213-215 after restart. Converted-world progress advances from 92-98 to
+100-106 and 201-207 to 208-214 respectively; target coordinates remain unchanged.
+
+The new artifact pins are:
+
+| Artifact | SHA-256 |
+| --- | --- |
+| Power release `2.4.4.113021` | `0CCA0AF27497431C3A64091809F8DD328C84D82500CF05456366770C65485E74` |
+| Harness release `1.1.0.113021` | `80402A725EC02B9D169B0E11CE3E99EF0892F6E6468891A618A34A4E5EC7C615` |
+
+Steam/Electric release hashes remain those in the earlier repair table. All six
+staged jars match their manifest pins in both worlds. All 53 Python regression
+and negative-control tests pass. The converted source fixture's before/after
+fingerprints match, all six server processes stop normally, and port 25586 has
+no remaining listener. There is no ticking-machine crash, failed inventory
+command or keep-up warning.
+
+Three Forge temporary-resource-pack cleanup FATALs and two Forge configuration
+watcher FATALs remain recorded in `lifecycle-results.json` and the console logs.
+They do not abort server startup, assertions or ordinary shutdown; they are not
+silently removed from the evidence or described as a clean-log pass.
+
+No production code, artifacts, source-version worlds or prior results changed
+during this rerun. The converted-world lanes are newly commissioned equipment,
+not proof that every original player machine has been qualified. The earlier
+30-minute endurance results still apply to their older jar hashes; refreshed
+endurance, broader player/entity migration and manual client checks remain
+separate gates.
+
+The subsequent [current-candidate endurance rerun](ENDURANCE.md#current-candidate-qualification)
+now passes thirty real minutes, complete restarts and resumed processing on
+these exact release hashes in both worlds. Broader migration and manual client
+coverage remain separate gates; the original endurance evidence is unchanged.
diff --git a/docs/1.13/CONNECTED-MIGRATION.md b/docs/1.13/CONNECTED-MIGRATION.md
new file mode 100644
index 00000000..33dbefb1
--- /dev/null
+++ b/docs/1.13/CONNECTED-MIGRATION.md
@@ -0,0 +1,223 @@
+# Connected Legacy Works: Migration and Continued Operation
+
+## Verdict
+
+**Follow-up:** the subsequent [legacy drill repair](DRILL-PROGRESS-REPAIR.md)
+fixes both drill types in maintained 1.10/1.12 code. Native restart tests and all
+eight migration routes pass with the repaired bridge. The verdict below records
+the original pre-repair campaign and its failures; those records are retained.
+
+The 23 October 2026 campaign builds and operates five connected assemblies under
+five historical source profiles, then migrates copies of the **already working
+equipment**. Nothing is rebuilt, refilled, replaced or manually reconnected after
+import. The production jars are unchanged from the
+[current-candidate qualification](CURRENT-QUALIFICATION.md#scope).
+
+All eight routes preserve their frozen saved state and operate successfully in
+1.13, including a complete 1.13 process restart. All five direct routes pass.
+The three chained routes remain **FAIL** overall because the intermediate 1.12
+Electric Drill loses partial mining progress when ticking resumes after reload.
+That failure is not waived because the subsequent 1.13 stage succeeds.
+
+| Source profile | Direct to 1.13 | Via 1.12, including intermediate restart | 1.13 operation and restart |
+| --- | --- | --- | --- |
+| Original Cyano 1.10 | PASS | FAIL: `Q-CONNECTED-001` | PASS on both routes |
+| Early local 1.10 | PASS | FAIL: `Q-CONNECTED-001` | PASS on both routes |
+| Maintained 1.10 | PASS | FAIL: `Q-CONNECTED-001` | PASS on both routes |
+| Early local 1.12 | PASS | Not applicable | PASS |
+| Maintained 1.12 | PASS | Not applicable | PASS |
+
+These are the archived profiles described in the
+[source provenance ledger](MIGRATION-MATRIX.md), not newly rebuilt historical
+mods. There is no original published Cyano 1.12 fixture. The chained runtime uses
+the separately pinned repaired Electric 1.12 bridge, without replacing the
+historical source profile.
+
+## What Was Built
+
+Each source world contains 456 declared block positions in five isolated
+assemblies. Targets and helper-bit positions are observed alongside equipment.
+The rigs begin with exactly 90,000 mB water, finite coal and item inputs, zero
+machine energy, and an empty lithium battery. There are no infinite power sources.
+All construction and inventory seeding occur only in the initial legacy process.
+
+| Assembly | Physical connection and workload |
+| --- | --- |
+| `C-CONVEYOR` | Chest containing 1,728 dirt, two east-facing conveyors, empty destination chest |
+| `C-STEAM` | Three coal boilers, each supplied through a fluid pipe from a water container; steam main, steam tank, three-lane furnace and rock crusher |
+| `C-ELECTRIC` | Two supplied coal boilers, steam main, steam-to-electric generator, cables, initially empty lithium battery array and electric furnace |
+| `C-SDRILL` | Two supplied boilers, steam pipes, north-facing steam drill, rear output chest and 63 cobblestone targets |
+| `C-EDRILL` | Two supplied boilers, generator and cables, north-facing electric drill, rear chest and 32 nine-block target planes |
+
+The steam drill crushes cobblestone into gravel. The electric drill harvests
+ordinary cobblestone; it is not a crusher. Both require exactly one retained item
+per removed target in this fixture. Coal/water are not replenished between stages.
+
+After 803 native source ticks, **each historical profile** had produced:
+
+| Witness | Measured output |
+| --- | --- |
+| Conveyor destination | 114 dirt |
+| Steam furnace | 12 iron ingots |
+| Steam crusher | 5 gravel |
+| Electric furnace | 3 iron ingots |
+| Steam drill | 24 gravel |
+| Electric drill | 9 cobblestone |
+| Lithium battery | 8,000 charge, from the connected generator |
+
+The remaining inputs, current progress, stored steam/electricity and water are
+saved by the real source-version serializers. Each source save is fingerprinted
+after shutdown and is never started again; conversion uses copies.
+
+## Observation Protocol
+
+1. Build the assembly once in a new legacy superflat world. Validate cold input
+ quantities and zero power; run 803 ticks and require real output.
+2. Save, stop the process, fingerprint the source world and copy it.
+3. Load the copy with machinery held out of the native ticking list. Compare
+ actual block states and complete saved tile NBT, including nested inventories,
+ fluids, energy bits, progress and target coordinates, before any machine work.
+4. Permit exactly 512 normal world ticks and check further output, finite water,
+ inventory accounting and retained tile identity. Save and stop completely.
+5. Start another process, compare the saved checkpoint while frozen, then permit
+ another 512 ticks and check continued operation.
+6. On chained routes perform the same load/work/restart sequence under 1.12
+ before copying that worked save into 1.13.
+
+The adapter holds and restores the real tickable tile list without reconstructing
+tiles. During measured intervals Minecraft performs normal machine and network
+updates. Bounded chunk tickets and an unlogged-in fake-player sentinel keep the
+fixture active; this is not a networked-player test. No pressure, inventory,
+progress or conduit-cache priming is performed after construction.
+
+Each original machine carries an NBT witness marker. During a process interval
+its object identity must remain unchanged as well. The 1.13 command adapter has
+no construction method; it supports only snapshots and tick-bounded observation.
+Command logs must contain only those operations, `save-all flush` and `stop`.
+Source-only build markers are not copied to later runtimes.
+
+All 22 conversion/restart state comparisons have zero differences. All sixteen
+1.13 operating intervals pass material accounting and work checks. Each route
+also requires actual additional items across its two 1.13 intervals; animation
+or an increasing progress bar alone cannot satisfy that gate. Steam drills in
+the longer chained route finish all 63 targets during the first 1.13 interval;
+their subsequent restart checks completed-job retention, not further excavation.
+
+The shared item totals are conserved: dirt `1,728`, steam-furnace ore plus ingots
+`192`, electric-furnace ore plus ingots `64`, and crusher cobblestone plus gravel
+`64`. Drill drops must equal removed targets with the correct product identity.
+Water cannot increase, and stored energy must be finite and nonnegative. Full NBT
+equality applies to frozen boundaries; normal fuel consumption and generation
+are allowed during native operation. This does not establish exact energy
+efficiency or cross-version throughput equality for every machine.
+
+The offline audit records 450 passing checks and five failures: the three
+intermediate restart checks plus the two enclosing split-campaign verdicts.
+These are one reproduced legacy defect, not five separate bugs. All source-world
+fingerprints, archived/staged jars and forbidden-mutation checks pass. The
+separate first-tick audit reports `REPRODUCED_LEGACY_FAILURE_TARGET_PASS`.
+
+## Findings Retained
+
+### Q-CONNECTED-001: Intermediate 1.12 Electric Drill Progress Loss
+
+The electric drill reaches saved progress `512` of a 576-tick nine-cobblestone
+cycle. Frozen reload preserves `512`, but the first native 1.12 tick resets it to
+`1`. Repeated 512-tick sessions therefore do not finish that saved target. The
+failure is reproduced on all three chained routes.
+
+An isolated comparison copies that same worked 1.12 world into both runtimes:
+
+| Runtime | Frozen loaded progress | After one native tick | Continued operation |
+| --- | --- | --- | --- |
+| Qualified 1.12 bridge | 512 | 1: FAIL | Completes a new cycle when allowed 700 further ticks |
+| Current 1.13 | 512 | 513: PASS | Continues mining without losing saved work |
+
+In the 1.12 implementation, deferred target reconstruction calls `targetBlock`,
+which assigns `progress = 0`. The existing 1.13 `restoreTargetBlock` rebuilds the
+target and drops while retaining valid saved progress. Neither the source save
+nor the migrator is repairing the 1.12 runtime in this test.
+
+Source locations: legacy Electric
+`machines/ElectricDrillTileEntity.java`, methods `tickUpdate`, `powerUpdate`,
+`targetBlock` and `loadFrom`; current
+[ElectricDrillTileEntity.java](../../../ElectricAdvantage/ElectricAdvantage/src/main/java/zone/moddev/mc/electricadvantage/machines/ElectricDrillTileEntity.java),
+methods `restoreTargetBlock` and `read`.
+
+No mined items disappear and the machine is not permanently stalled; it must
+repeat unfinished work. Repairing the maintained 1.12 implementation is separate
+work. The three chained campaign failures and the offline audit's nonzero exit
+remain visible. No production fix is included in this pass.
+
+### Dependency Recipe Difference: Obsidian
+
+An earlier pilot successfully preserved the original saved blocks/NBT and
+continued drilling, but did not preserve *newly generated recipe output*: the
+old Base Metals pack dropped one obsidian, whereas pinned Base Metals 1.13
+defines `basemetals:obsidian_crushing` as four obsidian powders. Previously saved
+obsidian items remained unchanged. This is a dependency recipe difference, not
+item duplication during migration, but it prevents claiming identical behaviour
+for every old crusher input. The failed pilot remains under
+`run-connected-migration/20261023-original-pilot2/`.
+
+The final fixture uses cobblestone because both source and target packs agree on
+its crushing output. Initial setup/oracle mistakes involving `Tank.Empty` and
+electric-versus-steam drill products also remain in their original diagnostic
+runs. Cold-state and negative oracle tests now guard against those mistakes.
+
+## Evidence and Repetition
+
+Local ignored evidence roots:
+
+```text
+run-connected-migration/20261023-working-legacy110/
+run-connected-migration/20261023-working-maintained110/
+run-connected-migration/20261023-working-112/
+run-connected-migration/20261023-connected-audit.json
+run-connected-migration/20261023-progress-trace-verified/
+```
+
+Each contains raw snapshots, compressed NBT, source fingerprints, staged jar
+identities, hash-addressed input archives, commands, console logs and verdicts.
+Production/dependency pins match the previous qualification; only test probes
+changed. Existing Base Metals/Forge/OreSpawn log diagnostics remain recorded,
+including the Base Metals leaves transformer and legacy bucket registration
+messages. State/operation success does not mean error-free modpack logs.
+
+From the PowerAdvantage checkout, after building both legacy probes and the
+current harness as described in the [runner instructions](../../test-rig/migration/README.md):
+
+```text
+python test-rig/migration/connected.py --java8 --profile-manifest --output
+python test-rig/migration/audit_connected.py --output
+```
+
+`--sources` and `--port` permit separate campaigns. Supply all completed roots to
+the offline audit; it requires all five sources/eight routes, the qualified
+803/512-tick protocol, unchanged input jars/source worlds, valid commands and raw
+state/operation evidence. It exits nonzero while the intermediate failures remain.
+The default profile path is a local archived qualification, not a portable public
+download. Supply equivalent hash-pinned inputs on another machine.
+
+`trace_connected_progress.py` accepts `--fixture` pointing to a completed chained
+`*-112` runtime and `--profile-manifest`, `--java8`, `--output`; it uses a separate
+port and disposable copies. `audit_connected_progress.py --run
+--fixture --output ` independently checks the
+one-tick failure and successful target continuation.
+
+The harness compiles/checks and remains absent from the Power release jar.
+All 158 Python tests and 20 harness JUnit tests pass. Regression/oracle tests
+include rejection of lost/duplicated items,
+replaced tiles, invalid power, unmeasured ticks, progress reset and incomplete
+campaign evidence. All production hashes remain unchanged. Nothing is committed,
+pushed or published.
+
+## Remaining Scope
+
+This closes the specific connected-source-equipment gap for these five
+assemblies, not for every card in the works. It does not cover connected imported
+elevators, growth chambers, every filter, pumps, oil processing, turrets, muskets,
+all drill facings/tracks or third-party converters. Those retain their
+[remaining-behaviour checklist](CURRENT-QUALIFICATION.md#remaining-behavioural-checklist).
+The pre-existing `Q-RIG-001` moving-drill oracle finding is unchanged.
+Real-client interaction and whole-Sylvester-modpack acceptance remain separate.
diff --git a/docs/1.13/CURRENT-QUALIFICATION.md b/docs/1.13/CURRENT-QUALIFICATION.md
new file mode 100644
index 00000000..db655b3b
--- /dev/null
+++ b/docs/1.13/CURRENT-QUALIFICATION.md
@@ -0,0 +1,216 @@
+# Current Candidate Qualification and Remaining Tests
+
+## Scope
+
+This is the 23 October 2026 rerun against the current, unpublished 1.13.2 jars.
+It supplements, rather than replaces, the historical failed and repaired reports.
+The main campaign and independent audits completed: all 29 steps passed, all ten
+protected fixture fingerprints remained unchanged and all 45 input/archive checks
+passed. The separate all-station sweep retains two drill-location oracle failures
+per phase. This is **core qualification with an open rig finding and coverage
+gaps**, not an all-setups-pass or release-approval claim.
+
+The subsequent [connected legacy works campaign](CONNECTED-MIGRATION.md) closes
+the source-operating-fixture gap for five bounded assemblies. All eight 1.13
+stages preserve state and resume work after restart. The three chained routes
+retain a **1.12 intermediate** electric-drill progress-reset failure; this is not
+an all-routes-pass claim. The results below remain the earlier campaign's record.
+
+The later [legacy drill repair and rerun](DRILL-PROGRESS-REPAIR.md) resolves that
+intermediate failure in both drill types on maintained 1.10/1.12. All eight
+connected routes and the two-restart native checks now pass with the repaired
+artifacts. The original failed campaign is not overwritten; other coverage gaps
+and `Q-RIG-001` remain open.
+
+| Artifact | SHA-256 |
+| --- | --- |
+| `PowerAdvantage-2.4.4.113021.jar` | `E5B62769812AE02393A305CF33D2E168BFB2B6D370AB04D43409B94F5740D193` |
+| `SteamAdvantage-2.2.5.113021.jar` | `A87F141913D511DB025FEE6D6FBA4000098598D8692B4BA53E7E80C67B14ACCA` |
+| `ElectricAdvantage-2.2.5.113021.jar` | `C785155C05133D1A408613D2C9738A32C9818E31C2599ECBD9F2FE76965DB5BA` |
+| Works harness `1.1.0.113021` | `0D0F71F25EB0D99FC871ACBD9120E14EA6F14B3349E4DB2F1A0283D2A2A84A1B` |
+
+The harness hash above belongs to that archived campaign. The connected
+extension uses harness SHA-256
+`2A260DF4B66454B57ECB6B021BCF26B3A812CA5FC0A8B9C4C9E904879AA67A1C`;
+the three production hashes have not changed.
+
+The server uses Forge `25.0.223`, Base Metals `3.0.1.113021` and OreSpawn
+`4.1.0.113021`. Production and probe jars are archived by content hash before
+launch. The original source fixtures, source jars and protected player saves are
+never opened as running worlds. Every conversion uses a disposable copy.
+
+The source profiles retain original Cyano 1.10, early local 1.10, maintained 1.10,
+early local 1.12 and maintained 1.12. There is no original published Cyano 1.12
+fixture. Chained routes use the separately pinned repaired Electric 1.12
+`2.2.2.112021` bridge; the historical `2.2.1.112021` source remains unchanged.
+See [source provenance](MIGRATION-MATRIX.md) and the
+[bridge repair](PLAYER-AND-ENTITY-MIGRATION.md).
+
+## Rerun Results
+
+| Gate | Current result | Retained evidence under the run root |
+| --- | --- | --- |
+| Block/tile state migration | PASS: eight direct/chained routes, conversion/restart/stability, 355-359 block and 332-336 tile witnesses per route | `matrix/matrix-results.json`, `matrix-audit.json` |
+| Commissioned operation inside converted copies | Automated assertions accepted in all eight routes, eleven stations before/after restart; manual checks remain open | `operation-/operation*-results.json` |
+| Strict player/entity migration | PASS: eight routes, frozen checkpoints, player adapter, native entity ticks and restart | `actors/actor-results.json`, `actors-audit.json` |
+| Normal-ticking actor controls and inventory layouts | PASS: eight normal-ticking routes and four inventory-layout routes | `actors-native/native-results.json`, `player-layout/layout-results.json`, corresponding audits |
+| Nether/End migration | PASS: all eight routes in both dimensions, native ticks and complete restarts | `dimensions/dimension-results.json`, `dimensions-audit.json` |
+| Transformer ordering | Semantic PASS in six runtimes/twelve launches; backup-origin order dependence remains documented, not waived | `transformer-order/summary.json`, `transformer-order-audit.json` |
+| Six-facing drills and chunk lifecycle | PASS in fresh and original-release chained worlds: 24 moves, 600 observed chunk unloads, progress/output/energy checks and full restart | `lifecycle/lifecycle-results.json`, `lifecycle-audit.json` |
+| Partial networks and extended drills | PASS in both worlds: 144 native moves, 24 partial-unload cases, twelve reload-order cases, scarcity/blockage/redstone and full restart | `extended-lifecycle/extended-results.json`, `extended-lifecycle-audit.json` |
+| Pipe-only unloaded bridge | PASS: three fresh-world cycles, absent-bridge isolation, retained endpoints and resumed processing/fluid conservation | `pipe-only/pipe-only-results.json`, `pipe-only-audit.json` |
+| Thirty-minute repeated production | PASS in both worlds: thirty timed batches, full restart and resumed processing | `endurance/endurance-results.json`, `endurance-audit.json` |
+| Oil bucket/drain/pipe/container lanes | PASS: four crude/refined lanes, exact identities/volumes and two process restarts | `oil-transfer/oil-transfer-results.json` |
+| Runner/oracle unit tests | PASS: 140 tests | `runner-unit-tests.log` |
+| All 21 walkable bays activated | FAIL: two stale drill-location assertions in each phase; other 19 bays remain manual-qualified only | `all-stations-*.json`; see `Q-RIG-001` below |
+
+Each endurance world ran for 1,801.563 seconds: 36,233 game ticks in the fresh
+world and 36,041 in the converted copy. Each produced 120 items in each of five
+processing lanes, routed 480 items through each of the plain and three filter
+lanes, and converted 60,000 mB crude oil to 30,000 mB refined oil. These are thirty
+distinct timed batches, not repeated readings of unchanged output. The resumed
+post-restart batch is checked separately and is not counted as timed production.
+
+No measured machine-state, item or fluid loss occurred in the accepted cases.
+Every test server stopped; no viewer instance was left running. The existing
+unrelated server was not stopped or changed. The log diagnostics and the
+transformer [backup-provenance limitation](TRANSFORMER-ORDERING.md#backup-provenance-finding)
+remain recorded; semantic migration success does not make partial loose-file
+backups a substitute for a complete original-world backup.
+
+## Evidence and Interpretation
+
+Local evidence is under the ignored directory
+`run-qualification/20261023-current/`. It contains archived input profiles,
+commands, console logs, raw NBT snapshots, per-stage comparisons, source
+fingerprints and independent audit reports. Generated worlds and jars are not
+documentation assets and must not be committed.
+
+Three different claims must stay separate:
+
+1. **State preservation:** the old serializer's actual saved state survives
+ conversion and restart. Frozen witnesses do not prove processing throughput.
+2. **Operating behaviour:** bounded machinery performs the measured work, with
+ the specified item/fluid/energy accounting and lifecycle checks.
+3. **Presentation and player interaction:** a real client can operate controls,
+ see correct GUI values, and interact with other players and entities.
+
+Commissioning new machines inside a converted world is not proof that every
+original player-built network runs correctly. Registry/metadata coverage is not
+the Cartesian product of every orientation, fuel, network topology, automation
+face, shortage, unload timing and upgrade route.
+
+Station `MANUAL_REQUIRED` is not promoted to `PASS` by a successful runner exit.
+An existence assertion proves only that a block/tile is present. A report audit
+can pass while faithfully retaining gameplay failures. Known Forge/Base Metals
+diagnostic messages remain in logs; successful state assertions do not imply
+error-free logs or approval of the whole modpack.
+
+## Walkable Drill Check Finding
+
+The additional fresh-world all-station sweep starts all 21 bays, saves, fully
+stops Minecraft and checks again after restart. Both phases currently report
+94 assertion passes, two failures and three Mineralogy-dependent skips. The
+other 19 stations retain `MANUAL_REQUIRED`, not complete gameplay acceptance.
+
+`Q-RIG-001`: the `S-05` down-facing and `E-06` up-facing assertions require a
+drill at its original coordinate even after the station has powered it and
+provided track. The snapshots instead contain a down-facing Steam drill one
+block above its start and an up-facing Electric drill one block below its start.
+Both displaced witnesses remain in the restarted snapshot. The other five
+facing assertions in each bay pass.
+
+This is a stale fixed-position oracle after activation, not evidence by itself
+that a drill vanished or rotated. It also is not sufficient evidence of correct
+movement, energy consumption or output. The separate movement-aware campaigns
+must decide those claims. The all-station result remains **FAIL** until its
+moving-witness assertions are repaired and independently rerun; this pass does
+not weaken them or label the failures expected passes. Raw records are
+`all-stations-commissioned.json`, `all-stations-full-restart.json` and
+`all-stations-results.json` under the local evidence root.
+
+## Remaining Behavioural Checklist
+
+This is the work queue, not a list of newly discovered defects. A behaviour can
+have partial coverage without its complete acceptance card being satisfied.
+The definitions in
+[`works-1.13.2.json`](../../test-rig/src/main/resources/assets/advantageworkstest/works/works-1.13.2.json)
+retain the detailed manual cards.
+
+| Station | Behaviour still needing dedicated evidence | Suitable next method |
+| --- | --- | --- |
+| `W-01` fluid transfer | Matching/nonmatching tank filters, redstone fluid isolation, all terminal/third-party handler faces, portable-tank break/place retention versus intentional metal-tank loss | Automated bounded volume/identity fixtures; client check for controls |
+| `W-02` drain/discharge | Source versus flowing water, stepped 32-step search boundary, full-destination backpressure and exact discharge-to-world conservation | Automated terrain and blocked-output fixtures |
+| `W-03` distillation | Mid-cycle input/progress/fuel restart, full-output and interruption cases; Mineralogy oil equivalence on these exact artifacts | Automated finite-input fixtures with Mineralogy profile |
+| `W-04` sorting | Complete zero/one/many configurable-filter routing matrix, every category filter, vertical/entity pickup, all overflow branch blockages, comparator levels and loss/duplication accounting | Automated item-count fixtures, not tile-existence checks |
+| `S-01` boilers | All four boilers' valid/invalid fuels, starvation and output saturation, exact electric-to-steam relationship, oil aliases/config overrides and sustained measured rated load | Automated finite-fuel/water/energy fixtures |
+| `S-02` distribution | Direct machine-to-machine conduction and short/long-network comparison under measured scarcity; matching GUI/server readings in a connected client | Automated topology fixture plus real client |
+| `S-03` storage | Tank backup priority, scarcity/recovery and redstone branch isolation with whole-circuit accounting | Automated finite supply/demand fixture |
+| `S-04` processing | All furnace lanes, all automation faces, invalid inputs, full outputs, mid-cycle restart, steam-still throughput and full comparator scale | Automated bounded processor fixtures |
+| `S-05` Steam Drill | Continuous naturally supplied multi-step travel; every new/imported orientation combined with every mid-block/unload/track condition; visual helper-bit restoration | Extended automatic cross-product plus real client |
+| `S-06` Steam Pump | Lift/depth geometry, pipe-section costs, full output, empty pool and repeated shaft cleanup; pumping throughput rather than isolated tank persistence | Automated finite-pool and energy accounting |
+| `S-07` elevator | Actual up/down travel, redstone pulses, ceiling stops, range/steam costs, riders and mid-travel unload/restart recovery without duplicate platforms | Automated movement fixtures; real player riding/control test |
+| `S-08` musket | Real loading/firing, cartridge/durability consumption, damage/range, block interactions and every enchantment's effects | Automated firing/target fixtures plus real client; enclosed disposable range |
+| `E-01` generation | Photovoltaic time/weather/light matrix, exact steam-conversion rate/cap, turbine damage and player interaction | Automated environmental/energy fixtures plus real client |
+| `E-02` batteries | All four batteries charging/discharging at capacity boundaries, mixed-array transfer, backup priority, switching and exact whole-circuit energy accounting | Automated finite generator/consumer fixture; tooltip/GUI checks |
+| `E-03` processors | Fabricator and other processors' valid/invalid/full-output/scarce-power cases, automation faces and mid-cycle restart across machine-specific inventories | Automated recipe/output fixtures; not just crafting reachability |
+| `E-04` fluid plant | Steam/electric pump geometry and work costs, live pump persistence/resume, powered still/refinery blockage and interrupted fluid/energy paths | Automated isolated finite-fluid fixtures; client controls |
+| `E-05` growth | Actual seed/crop/sapling harvests, shared chamber starvation, soil/water/electricity consumption and mid-growth restart | Automated crop-output and resource accounting |
+| `E-06` Electric Drill | Same remaining cross-product and continuously powered travel as `S-05`; client laser/render behaviour | Extended automatic fixture plus real client |
+| `E-07` light/turret | LED emitted light and priority, turret owner/team permissions, targeting/range, real hits, shot cost and reacquisition across reload | Automated target/energy fixtures plus authenticated players |
+| `E-08` assembler | Actual reference-recipe crafting, recursion limits, reversible recipes, output blockage and timeout protection | Isolated time-bounded automatic fixture; no valued world |
+| `P-API-01` fluid API | Real IronAgeFurniture caller and third-party interaction beyond the native pipe/tank contract | Integration profile/client with IAF; API contract stays separate |
+
+### Recommended Order
+
+1. The [connected-source pilot](CONNECTED-MIGRATION.md) is implemented for
+ conveyor transfer, supplied steam processing/storage, electric generation and
+ processing, and both stationary drills. `Q-CONNECTED-001` in the maintained
+ legacy implementations is now repaired and independently rerun; retain its
+ original failure evidence. Extend the same no-rebuild protocol to other
+ assemblies as their dedicated behavioural fixtures are added. The isolated
+ state matrix and newly commissioned converted Works remain complementary
+ evidence.
+2. Repair `Q-RIG-001`: distinguish initial facing/placement from expected track
+ movement, then assert moved identity, facing, inventory and energy. Do not
+ clear a failure by merely removing the assertion or resetting before checking.
+3. Elevator movement and growth-chamber output: large functional gaps with
+ practical, finite inputs and measurable outputs.
+4. Conveyor/filter and tank priority matrices: automate the advertised variants
+ that the walkable bays currently represent only structurally.
+5. Pump geometry/costs, boiler fuel/starvation and processor blocked-output cases.
+6. Generator environmental states, batteries and optional Forge Energy converters.
+7. Musket/turret gameplay, real-player permissions and GUI/server agreement.
+
+An individual row closes only when its required outputs and retained state are
+recorded. The same fixture should be exercised natively and after relevant
+direct/chained conversions, not merely copied into an already converted world.
+
+## Cross-Cutting Limits
+
+- The latest recipe manufacturing/discovery and native resource/render results
+ remain in [Manufacturing Progression](MANUFACTURING-PROGRESSION.md) and
+ [Recipe Discovery](RECIPE-DISCOVERY.md). Those are not machine/network tests.
+- Not every original saved machine is connected and operated by the migration
+ matrix. State lost by an old serializer before conversion cannot be recovered
+ by declaring the new migrator successful.
+- Current optional integration, Mineralogy, OreSpawn world-generation/profile
+ migration and configuration-import combinations need their own exact-artifact
+ reruns; superflat machine tests cannot prove terrain-generation behaviour.
+- Full network login/handshake, integrated-server host loading, returning to the
+ menu and replacing worlds within the same client process, GUI controls,
+ pressure/power bars, sounds, targeting visuals, multiple players and all modpack
+ interactions remain distinct from dedicated-server adapter tests.
+- Chunk/drill campaigns do not exhaust every unload timing, portal travel,
+ mounted dimension transfer, arbitrary dimensions or natural animal behaviour.
+- Advancement/statistic and third-party capability migration is not implied by
+ item-NBT, player-adapter or owned-entity persistence coverage.
+- The [Forge respawn finding](DIMENSIONS-AND-ENTITY-LIFECYCLE.md#forge-25-spawn-lookup-finding)
+ remains open: a real-client bed/death test is required. The earlier eight
+ valid-bed/forced-spawn failures are not changed to passes by this campaign.
+- A complete Sylvester modpack conversion has not been accepted. Unsupported
+ unrelated mods are not acceptable silent data loss. Retain a full original
+ world backup; loose-file backups cannot roll back flattened region files.
+
+No gameplay fix, version bump, commit, push, PR, tag or release is part of this
+qualification pass.
diff --git a/docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md b/docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md
new file mode 100644
index 00000000..d39df3e1
--- /dev/null
+++ b/docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md
@@ -0,0 +1,243 @@
+# Additional Dimensions and Entity Lifecycle
+
+## Scope
+
+This extends the [Overworld player/entity matrix](PLAYER-AND-ENTITY-MIGRATION.md)
+with source-serialized Nether (`-1`) and End (`1`) fixtures. It is packaged,
+dedicated-server qualification, not client login, a death-screen test or a
+whole-modpack upgrade. No production code or mod jar is changed by these tests.
+
+Five source profiles are exercised: original Cyano 1.10, early maintained 1.10,
+current maintained 1.10, early 1.12 and maintained 1.12. Every profile upgrades
+directly; the three 1.10 profiles also upgrade through the repaired Electric
+`2.2.2.112021` bridge. The original 1.12 source jar remains unchanged. That gives
+eight routes per dimension, sixteen altogether.
+
+The source worlds are copies of the fingerprinted matrix fixtures. Their actual
+legacy serializers build each dimension fixture. The originals and completed
+generated source fixtures are hashed before/after use and never started as targets.
+The original Sylvester save is not opened or modified.
+
+## Witnesses and Assertions
+
+Each selected dimension has a bounded, covered bedrock enclosure containing:
+
+- A saved player with charged batteries, a damaged enchanted musket, oil items,
+ Ender inventory, experience, health, UUID and an Overworld spawn coordinate.
+- A boat carrying an owned, sitting wolf. Both individual entity state and the
+ actual vehicle/passenger relationship are checked.
+- A tamed horse with saddle, iron armour and a fence-knot leash. Ownership,
+ health, attributes, equipment and the live leash endpoint must survive.
+- An Electric hydroelectric generator and its saved turbine entity. The turbine
+ UUID and parent coordinate must remain intact without the machine manufacturing
+ a replacement. Machine updates remain frozen.
+
+Four persistent entity witnesses must exist exactly once. A native leash knot is
+transient in 1.13: its UUID is not a persistent identity guarantee. The comparison
+requires its type and fence coordinates, not an unchanged knot UUID. Other live
+holder UUIDs, passenger UUIDs and persistent entity UUIDs remain exact.
+
+Legacy horse `Type=0` becomes `minecraft:horse`; old wolf collar colours are
+normalized to the same colour meaning. New Forge attributes are allowed, but
+existing attributes may not change. The generator's internal `initDone` cache
+flag is excluded. Item quantities, energy tags, damage, enchantments, ownership,
+dimensions, parent coordinates and other compared state are not relaxed.
+
+After native ticks, movement coordinates and rotation are not equality tests.
+Actual passenger and leash relationships remain mandatory. There is no reseeding,
+replacement-entity creation or inventory refill between migration checkpoints.
+
+## Timing and Restart Protocol
+
+The native campaign deliberately permits native entity initialization **before
+the first converted save**, then measures at least sixty entity ticks. Chained
+routes do the same in 1.12 and completely restart that intermediate process.
+Each target has conversion, real player-file adapter, full restart, further live
+ticks and another full restart checkpoints. Backups must match predecessor bytes
+and remain unchanged on later loads.
+
+Chunk tickets/force-loading and an unlogged-in sentinel keep the fixture active.
+The sentinel has a packet-discarding connection so vanilla entity tracking and
+End boss bars can operate without a socket. This is not a player login or Forge
+handshake. Native dragon code is not disabled or replaced.
+
+### Zero-Tick Leash Limit
+
+An earlier zero-tick diagnostic did **not** preserve the horse's leash. Vanilla
+1.13 `EntityLiving` reads the `Leash` tag into pending data, but its writer emits
+that tag only from a resolved live holder. Saving before its first initialization
+can therefore erase the pending endpoint. The later native-initialization test
+is a separate control, not a rewritten zero-tick PASS.
+
+The cached Forge 25 injected source shows the reader retaining pending NBT, the
+writer requiring a live holder and native initialization finding/recreating the
+fence knot. Those findings are also recorded in the raw source/converted/runtime
+snapshots. The existing strict Overworld matrix does not contain leashes and
+must not be cited as zero-tick leash coverage.
+
+## Death and Respawn Supplement
+
+Independent cases restore a copied source-serialized player file, then exercise
+native death and respawn with a dummy connection. This explicit case reset is
+not part of the migration persistence proof. The six cases are `keepInventory`
+on/off crossed with forced spawn, valid bed and missing bed, in each dimension.
+
+Assertions require the same UUID, expected health/dimension, exact retained or
+dropped inventories, unchanged Ender inventory, battery charge, musket damage
+and enchantments, experience semantics, spawn position and persisted player file.
+Dropped stacks are reconciled by identity and quantity, ignoring only bag slot.
+Each source has fourteen counted carried items. No missing or duplicated item is
+accepted as an expected failure.
+
+The final results and native spawn diagnostics are recorded separately below.
+An evidence replay PASS means records and assertions agree; it does not promote
+a native respawn FAIL to gameplay PASS.
+
+### Forge 25 Spawn Lookup Finding
+
+The ordinary `NetHandlerPlayServer.PERFORM_RESPAWN` path reproduces the same
+failure as the earlier direct `PlayerList` pilot: a valid bed or forced Overworld
+spawn is ignored, the player returns to world spawn and saved spawn keys disappear.
+The source player has the correct `SpawnX/Y/Z=160/100/24` before and after death;
+the live Overworld `getBedLocation` nevertheless returns null before recreation.
+Missing-bed cases returning to world spawn are expected and are not failures.
+
+Inspection of the installed Forge 25 server bytecode and matching injected source
+points to a native field mismatch: `setSpawnPoint` and NBT loading/writing use
+`EntityPlayer.spawnPos`, whereas Forge's Overworld `getBedLocation(dim)` returns
+the distinct `bedLocation` sleeping-position field. The respawn handler resolves
+the destination dimension before asking for that location. Passing Overworld
+instead of the original dimension would not repair this lookup.
+
+The cached `net.minecraftforge:forge:1.13.2-25.0.223` injected `EntityPlayer.java`
+has the Overworld getter at lines 1580-1582 and setter at 1624-1629. In the
+installed SRG class, the getter reads `field_71081_bT` at bytecode offset 8;
+the setter writes `field_71077_c` at offset 84. These are distinct fields, not
+an inferred failure caused by an incorrectly requested destination dimension.
+
+This is a candidate **platform-path defect**, not observed Advantage item loss.
+The probe does not seed the sleeping-position field, alter vanilla methods or
+rewrite expectations to obtain a PASS. The same handler is reached by an actual
+respawn request, but an authenticated client sleeping/death test remains necessary
+before calling its full gameplay impact confirmed. No production workaround is
+included here.
+
+## Repeating the Tests
+
+Run from the PowerAdvantage checkout with a hash-pinned manifest containing the
+dimension probes. `pin_dimension_profiles.py` replaces only test probes in an
+existing qualification manifest and checks that production/dependency bytes
+remain unchanged. Preserve or archive those probe jars before rebuilding them.
+All output paths must be new, ignored directories.
+
+```text
+python test-rig/migration/pin_dimension_profiles.py --base --output --archive
+python test-rig/migration/dimensions.py --profile-manifest --java8 --output --native-load
+python test-rig/migration/audit_dimensions.py --run --output
+python test-rig/migration/respawn.py --profile-manifest --java8 --fixtures --output --port 25623
+python test-rig/migration/audit_respawn.py --run --output
+python -m unittest discover -s test-rig/migration -p test_*.py
+```
+
+The Python environment requires `nbtlib`; the local configured interpreter is
+`test-rig/migration/.venv/Scripts/python.exe`. Servers bind localhost only and
+shut down between phases. Existing listeners are never stopped. Reports contain
+staged-jar pins, raw binary NBT, semantic comparisons, tick measurements, backups,
+source fingerprints and console logs. Worlds, jars and generated reports remain
+untracked.
+
+## Qualified Results, 7 October 2026
+
+The final dimension runs use identical archived test pins and unchanged
+production/dependency jars. Both commands exit zero; both independent replays
+exit zero and require all eight requested route IDs, all four persistent
+witnesses' tick evidence and unchanged source fixtures.
+
+| Campaign | Result |
+| --- | --- |
+| Nether, all eight direct/chained routes | PASS |
+| End, all eight direct/chained routes | PASS |
+| Intermediate 1.12 save and full restart on all six chains | PASS |
+| Player file/adapter, exact normalized item NBT and charged/damaged/enchantment-bearing items | PASS |
+| Persistent UUIDs, ownership, equipment, live passenger/leash endpoints and turbine parent | PASS |
+| Target conversion, native ticks and two full process restarts | PASS |
+| Predecessor-byte backups and later backup stability | PASS |
+| Both independent dimension replays | PASS, zero integrity findings |
+| Python regression/oracle suite | 128 tests, zero failures/errors |
+| Java probe builds, 110/112/113 | `compileJava check assemble` PASS; legacy tests NO-SOURCE, modern 12 tests PASS |
+
+The final death/respawn supplement uses those completed dimension fixtures,
+not the interrupted pilot worlds. Each dimension contributes eighteen cases:
+original 1.10, maintained 1.12 and original-1.10-chained 1.13, each crossed with
+the six keep/drop and spawn modes. Every case uses the native `PERFORM_RESPAWN`
+handler, archives its saved player and `level.dat`, and checks missing-bed
+respawns against the actual saved world-spawn coordinate and `spawnRadius`.
+
+| Respawn group | Exact result |
+| --- | --- |
+| Original 1.10, both dimensions | 12 PASS |
+| Maintained 1.12, both dimensions | 12 PASS |
+| 1.13 missing-bed cases, both keep/drop modes and dimensions | 4 PASS |
+| 1.13 valid-bed/forced-spawn cases, both modes and dimensions | 8 FAIL: spawn lookup/retained spawn keys only |
+| Carried/drop quantities, item NBT, charge, damage, enchantments, Ender contents, UUID and health/dimension checks | PASS in all 36 cases |
+| Saved player files match the native post-respawn observations | PASS in all 36 cases |
+| Both independent respawn replays | PASS: recorded failures reproduce, no integrity findings |
+
+The respawn runners intentionally exit **1**, because eight behavioral assertions
+fail. Their audits exit zero because those results agree with the raw records;
+this is not an all-tests-pass or release-approval claim.
+
+### Evidence and Pins
+
+Large saves, staged libraries, binary snapshots and logs are retained under
+`C:\Users\johnb\AppData\Local\AdvantageWorks\qualification\`:
+
+- `20261007-dimensions-nether-final`: eight routes, scope, raw NBT, tick evidence,
+ source fingerprints, backups and `integrity-audit.json`.
+- `20261007-dimensions-end-final`: the matching eight End routes and replay.
+- `20261007-respawn-nether-qualified`: eighteen native cases and their replay.
+- `20261007-respawn-end-qualified`: eighteen native cases and their replay.
+- `20261007-order-full-r2`: the separately qualified six transformer-order cases.
+
+Small manifests/reports are retained under ignored `run-dimension-migration/`.
+The pin manifest is `20261007-inputs/profiles-qualified.json`; its provenance
+requires every production/dependency SHA-256 to match the preceding accepted
+actor campaign. The archived test artifacts are:
+
+```text
+AdvantageMigrationProbe-110-1.0.0.jar and AdvantageMigrationProbe-112-1.0.0.jar
+38565B6671E249B6E12D5B238355FD7D48218B261789E7E92316F9D9DC29F94A
+AdvantageWorksTestHarness-1.1.0.113021.jar
+8B39BB591A376A626FC25C7A8167F05F0937338B9BE21D45DCE9D1D60B8C092E
+```
+
+Reproduction of these split runs uses `dimensions.py --dimensions -1` or `1`
+with separate outputs/ports; use `respawn.py --districts nether` or `end` and
+its matching final dimension root. Archive copies protect the probe pins from
+later builds. Nothing is installed into an active client or server.
+
+Earlier full-r1/r2, smoke and respawn pilots are retained diagnostics, not the
+accepted campaigns. They exposed duplicate freeze-list restoration, pending
+vanilla leash serialization, null sentinel connections during tracking/End boss
+packets and legacy chunk-ticket double release during shutdown. The final probes
+correct the test-only connection/freeze/ticket handling; they do not suppress
+native dragon behavior or restore lost production data. Replay now rejects
+tracking, crash and shutdown exceptions even if the process exits zero.
+
+Item-stack dictionaries compare exactly after declared serialization normalization.
+Unknown turbine-like names are not aliases; duplicate helper witnesses are
+rejected. New native entity defaults outside the selected semantic surface remain
+allowed. Existing BaseMetals leaves-fixer/config/resource-pack diagnostics are
+retained separately: these results are not a blanket error-free-log claim.
+
+## Remaining Limits
+
+Client login/handshake, death-screen interaction, rendering, GUI and ownership
+synchronization remain manual gates. This does not test portal travel, mounted
+player travel, combat, riding elevators, advancements/statistics or arbitrary
+third-party capabilities/entities. Fixed, invulnerable fixture actors establish
+persistence and native relationships, not complete natural animal gameplay.
+
+See [transformer ordering](TRANSFORMER-ORDERING.md) for mixed BaseMetals/Advantage
+hook execution and the separate backup-provenance caveat. A complete pre-upgrade
+world copy is still required; loose-file backups do not restore converted regions.
diff --git a/docs/1.13/DRILL-PROGRESS-REPAIR.md b/docs/1.13/DRILL-PROGRESS-REPAIR.md
new file mode 100644
index 00000000..012d0f72
--- /dev/null
+++ b/docs/1.13/DRILL-PROGRESS-REPAIR.md
@@ -0,0 +1,156 @@
+# Legacy Drill Progress Repair
+
+## Result
+
+`Q-CONNECTED-001` is repaired in the maintained 1.10.2 and 1.12.2 Steam and
+Electric implementations. The 1.13.2 implementations already had the repair and
+remain unchanged. The red native run reproduced the defect in all four affected
+mod/version combinations; the repaired native run and all eight connected
+migration routes pass. The original failed records remain intact.
+
+This closes the intermediate drill-progress failure in
+[Connected Legacy Works](CONNECTED-MIGRATION.md), not the other entries in the
+[remaining behavioural checklist](CURRENT-QUALIFICATION.md#remaining-behavioural-checklist).
+No commit, push, PR or publication is included.
+
+## Implementation and Candidates
+
+Both legacy drills saved `progress` and `targetX/targetY/targetZ`, but rebuilt
+transient drops/hardness after loading through `targetBlock`, which reset that
+saved progress. Restoration now remembers the saved value, reconstructs the
+target and clamps the value to its current mining goal. Both the deferred
+first-tick path and the already-attached-world NBT read path use this helper.
+
+Steam's cached redstone flag also starts as `true`. Restoring it from the actual
+world before the resumed tick prevents that uninitialized flag from discarding
+the saved work. Actual redstone disabling, power costs, mining time factors,
+output recipes and track behaviour are not changed.
+
+Reading an untargeted tile clears old progress, target pointers and deferred
+state instead of retaining work from an earlier NBT read. No registry, tile,
+package, inventory, fluid, recipe, dependency or NBT key is renamed. This is a
+runtime repair; existing worlds need no new datafixer for it.
+
+| Version | Mod | Candidate | Local branch |
+| --- | --- | --- | --- |
+| 1.10.2 | Steam | `2.2.5.110021` | `feature/steamadvantage-1.10.2-drill-progress` |
+| 1.10.2 | Electric | `2.2.5.110021` | `feature/electricadvantage-1.10.2-drill-progress` |
+| 1.12.2 | Steam | `2.2.2.112021` | `feature/steamadvantage-1.12.2-drill-progress` |
+| 1.12.2 | Electric | `2.2.3.112021` | Existing `feature/electricadvantage-1.12.2-turbine-persistence` |
+
+The pending Electric 1.12 turbine repair remains present; this candidate builds
+on it rather than replacing it. PowerAdvantage production code and all 1.13
+release versions/hashes remain unchanged. The Power 1.10 branch
+`feature/poweradvantage-1.10.2-drill-regression-profiles` changes only mutable
+rig profiles to reference the new Steam/Electric filenames, versions and hashes.
+
+| Release jar | SHA-256 |
+| --- | --- |
+| `SteamAdvantage-2.2.5.110021.jar` | `1C9DE2F9EE29439B727D2087E18BAD5277115BCCF7162F0F9342F322F44F9E9E` |
+| `ElectricAdvantage-2.2.5.110021.jar` | `1854A854332FC02151C289BAEAF9DC3C6677ED6CDC1ED04EA8EDD0CB54A085F5` |
+| `SteamAdvantage-2.2.2.112021.jar` | `A249E7A90729519BD19040F3BF6042E871E61EFF7A67E4CEF464458B1E9F0A15` |
+| `ElectricAdvantage-2.2.3.112021.jar` | `B5AB6769E05FE87CED655EB4B729C3EE016BD9D069DDC7111432F061BA7D08E2` |
+
+Each jar has exactly the previous entry set. Only its drill class, version-bearing
+main class, manifest and `mcmod.info` differ from the preserved pre-fix jar.
+Every other resource/class is byte-identical. `javap -protected` confirms the
+drill's public/protected API is unchanged. Release audits reject embedded
+dependency/test/agent content and require Java 8 bytecode.
+
+## Native Red-to-Green Test
+
+`drill_restart.py` starts with copies of the existing working, connected
+maintained 1.10 and 1.12 fixtures. The unchanged 1.13 control imports the latter.
+It never builds or refills the equipment and writes no inventory, energy or
+progress NBT. A maximum of 100 seven-tick intervals is allowed to reach a
+natural mid-cycle checkpoint with both drills powered. That checkpoint is saved
+and the process stopped.
+
+Each runtime then undergoes two complete restarts. At each restart the test:
+
+1. Compares the entire saved block/tile state before any machine ticks.
+2. Permits exactly one native tick, requiring both saved targets and original
+ tile witnesses to remain and each drill's progress to increase by exactly one.
+3. Permits 620 more native ticks and checks actual item output and conservation,
+ finite water/energy and retained tile instances.
+4. Saves and shuts down. Before the second restart, bounded natural ticking
+ again selects a mid-cycle checkpoint; no progress is supplied by the harness.
+
+At the first restart in both legacy runtimes:
+
+| Drill | Saved progress | Unpatched first tick | Repaired first tick |
+| --- | --- | --- | --- |
+| Steam | 17 | 0: FAIL | 18: PASS |
+| Electric | 209 | 1: FAIL | 210: PASS |
+
+Both repaired versions and the unchanged 1.13 control also pass their second
+restart and subsequent production interval. This gives twelve successful
+first-tick observations across six mod/version cases. The native evidence audit
+reports `PASS`; the original run remains `FAIL`, with a separate audit reporting
+`REPRODUCED_FOUR_LEGACY_FAILURES` rather than rewriting its verdict.
+
+## Migration Rerun
+
+All five historical source profiles remain pinned to their original jars,
+including the affected maintained source jars. The only bridge changes are the
+explicit new Steam/Electric 1.12 candidates. The 1.13 target jars and every
+Base Metals/OreSpawn/Power dependency remain unchanged.
+
+The eight direct/chained routes rerun the connected source construction,
+803-tick source qualification, 512-tick intermediate/target intervals and full
+process restarts described in the original protocol. All eight routes now pass,
+including all three formerly failing 1.12 intermediate restarts. All 22 frozen
+state comparisons have zero differences. The independent audit passes 491
+checks, including all source fingerprints and archived/staged jar checks.
+
+Historical sources can still contain the old runtime defect: they provide
+unchanged saved fixtures, not repaired historical binaries. The native candidate
+test separately proves the fixed 1.10 and 1.12 builds operate and restart
+correctly. State/operation success is not a clean-log or whole-modpack claim;
+known Forge/Base Metals/OreSpawn diagnostics remain in the raw console records.
+
+## Build and Test Evidence
+
+All four affected mods pass:
+
+```text
+clean check build javadoc verifyReleaseDependencies verifyReleaseArtifacts writeReleaseChecksums verifyEclipseProductionClasspath
+```
+
+The four legacy builds pass 18 JUnit tests in total, including ten new drill
+persistence tests and the retained turbine/OreSpawn tests. The Python runner and
+oracle suite passes 162 tests. Sources, Javadoc, deobf and release jars are built;
+Eclipse launch metadata is regenerated with production/test isolation.
+
+The removed local OreSpawn 4.0.8 deobf dependency was rebuilt from pinned commit
+`5a50df1158e948c8db55814e819090b34c12d765` in a disposable checkout. Its SHA-256
+is exactly the existing pin
+`33BFB8C9D626D071147A0952A74B633F0BCC6A27E0408B4524370F4833B003C2`.
+The build uses `-PoreSpawnDeobfJar=`; it does not upgrade OreSpawn or
+alter its active checkout. A future clean build still needs that exact artifact
+or its documented pinned-source bootstrap.
+
+Local ignored evidence is under `run-drill-progress/`:
+
+```text
+20261023-before-artifacts/ Pre-fix jars and failed unit-test records
+20261023-red/ Unpatched native reproduction and audit
+20261023-candidate-inputs/ Immutable candidate inputs and replacement provenance
+20261023-green/ Repaired native run and audit
+20261023-matrix-original/ Original/early 1.10 direct and chained routes
+20261023-matrix-maintained/ Maintained 1.10 direct and chained routes
+20261023-matrix-112/ Early/maintained 1.12 direct routes
+20261023-matrix-audit.json Combined eight-route audit
+20261023-artifact-comparison.json Exact entry/resource/API comparison
+20261023-workspace-profiles/ Mutable 1.10 profile update record
+```
+
+The runners take explicit hash-pinned manifests. Use `pin_drill_profiles.py`
+to archive pre-fix outputs and prepare `native110`/`bridge112` overrides without
+changing historical source profiles; `drill_restart.py` performs the native
+checks and `audit_drill_restart.py` replays the raw evidence. See each script's
+`--help` and the [runner instructions](../../test-rig/migration/README.md).
+
+Original worlds, earlier failed evidence and immutable historical jars remain
+untouched. Real-client GUIs, arbitrary unload timing and other outstanding
+machine behaviours are not newly accepted by this focused repair.
diff --git a/docs/1.13/ECLIPSE-METADATA.md b/docs/1.13/ECLIPSE-METADATA.md
new file mode 100644
index 00000000..752333a8
--- /dev/null
+++ b/docs/1.13/ECLIPSE-METADATA.md
@@ -0,0 +1,40 @@
+# Eclipse Mod Metadata Regression
+
+## Failure: 22 October 2026
+
+The combined Eclipse launch rejected PowerAdvantage as incompatible with Steam and Electric, despite the built jar having the correct version. The user's `run/logs/latest.log` loaded Power from `bin/main`; that output contained the literal `${version}`, `${loader_version_range}` and other placeholders from the source `META-INF/mods.toml` template.
+
+Gradle had expanded the template correctly under `build/resources/main`. However, the Eclipse Java build path still listed `src/main/resources`, so an Eclipse build could copy the raw file over the processed copy previously staged in `bin/main`. The earlier disposable client probe merged Gradle outputs and therefore did not exercise this failure. No dependency version range needs weakening.
+
+## Repair
+
+All three projects now supply exactly one production resource source root to Eclipse: `build/resources/main`, with output `bin/main`. Raw resource roots are removed from both generated `.classpath` files and the Tooling API Eclipse model imported by Buildship. `eclipseClasspath` depends on `processResources`; existing Gradle synchronization tasks stage processed resources before launch preparation. The Eclipse audits reject raw resource roots and check the expanded mod ID/version in `bin/main`.
+
+In Eclipse, use **Gradle > Refresh Gradle Project**, then **Project > Clean**, allow the build to finish, and relaunch `runClientAllAdvantage`. Resource/version edits require `processResources` or a Gradle refresh before the Eclipse build. Java classes remain compiled by Eclipse into `bin/main`; normal launches still exclude test code. No gameplay classes, world data, registry identities or dependency ranges were changed.
+
+## Evidence
+
+- `build/client-resource-runs/20261022-050744-ad86296d/` retains the failing Eclipse-output snapshot. The new preflight detects its unresolved template without opening a world or starting Minecraft.
+- `build/client-resource-runs/20261022-051012-75105698/client-input.json` records `output_source: Eclipse`, the combined launch and the snapshotted metadata hash. No Gradle resources were overlaid on that snapshot.
+- The corresponding `client-resources.json` reports actual Forge versions Power `2.4.4.113021`, Steam `2.2.5.113021` and Electric `2.2.5.113021`, plus PASS for all 94 items and 905 block states. Existing oil and tank rendering/synchronization checks also pass.
+- `build/eclipse-tooling-models.log` records successful Gradle Tooling API Eclipse-model checks for all three projects: `build/resources/main -> bin/main`, with no raw production resource roots.
+- All three generated Eclipse/classpath audits pass, and each `bin/main/META-INF/mods.toml` matches its processed counterpart byte-for-byte. This validates the metadata/resource configuration and actual Eclipse output, not an automated run of the Eclipse GUI itself.
+
+## Repeat
+
+Run Gradle with Java 17, then use Java 8 for the disposable client:
+
+```powershell
+./gradlew.bat check clientResourceProbeJar prepareCombinedEclipseLaunch
+./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -OutputSource Eclipse
+```
+
+After a genuine Eclipse rebuild, repeat the second command without rerunning Gradle staging first; this checks what Eclipse actually left in `bin/main`. The probe copies that output and never opens the user's save.
+
+The independent Buildship-model check uses the Gradle installation's Tooling API jars and Java 17 source-file mode:
+
+```text
+java --class-path "/lib/*" test-rig/eclipse/EclipseModelProbe.java "" "" "" "" ""
+```
+
+Both tools are test-only and are excluded from mod artifacts and ordinary play-test launches. Reports and generated IDE files remain local and ignored.
diff --git a/docs/1.13/ENDURANCE.md b/docs/1.13/ENDURANCE.md
new file mode 100644
index 00000000..6d4a0334
--- /dev/null
+++ b/docs/1.13/ENDURANCE.md
@@ -0,0 +1,229 @@
+# Advantage Works 1.13.2 endurance qualification
+
+This is a repeated-load test, not an accelerated clock or an idle server left
+running. Read the [migration matrix](MIGRATION-MATRIX.md) separately: preserving
+old save data and operating rebuilt Works equipment are different gates.
+
+Latest qualification: [the current candidates pass](#current-candidate-qualification).
+Earlier runs remain recorded below with their original artifact identities.
+
+## Protocol
+
+`test-rig/migration/endurance.py` creates two disposable, operator-marked worlds:
+
+- A fresh packaged 1.13.2 Works world.
+- A copy of the original Cyano 1.10 -> maintained 1.12 -> 1.13 conversion result.
+
+Both use the packaged Advantage jars with Base Metals and OreSpawn. Artifact
+hashes and the source fixture's fingerprint are checked. No source world is
+opened in Minecraft. The Works bays are rebuilt in the converted **copy**; this
+is not an endurance proof for every original machine in that save.
+
+Servers run concurrently on loopback ports 25584 and 25585. Thirty real one-minute
+intervals receive bounded new inputs after the preceding batch passes. Machine
+blocks, progress, temperature and conduit networks are not reset between batches.
+Only measured feed/output inventories and the oil circuit are replenished,
+using native inventory commands and fluid handlers, never tile NBT writes.
+NBT inventory writes would trigger Power's loaded-network refresh and could
+conceal a stale-network defect; they are not allowed in the qualified run.
+At least 18 game ticks per wall-clock second must elapse over the measured run.
+
+Each interval requires:
+
+- Steam and electric furnaces: four iron ores become exactly four ingots each.
+- Steam and electric crushers: four cobblestone become exactly four gravel each.
+- Electric oven: four raw beef become exactly four cooked beef.
+- Plain conveyor: all 16 dirt reach the destination with no extra or held items.
+- Zero-filter lane: eight dirt and eight apples pass forward, none go below.
+- One-filter and multi-filter lanes: eight matching dirt go below and eight
+ nonmatching apples go forward; no source or held input remains.
+- Distillation: 2,000 mB crude oil become exactly 1,000 mB refined oil, with no
+ crude remaining. All three tanks and the still's input tank are counted because
+ the shared bidirectional pipe network can return output to the supply tank.
+- Disconnected pump, storage and public-API water witnesses retain respectively
+ 1,000, 2,000 and 4,000 mB of water.
+- Geothermal heat stays above zero; energy stays finite and nonnegative; item
+ counts remain valid; battery charge stays within the four battery capacities.
+- Stored partial-charge battery items retain their entire inventory NBT, and the
+ hydroelectric generator's battery has gained charge.
+
+After the load run, the runner checks station assertions, leaves power sources
+and pipes intact, saves, shuts Minecraft down completely, starts a new process and repeats
+the content assertions **before** reseeding. It then resumes operation for another
+minute and requires another correct batch. No `build`, `reset`, station restart,
+or power-block replacement is performed on the processing lanes after loading.
+The public fluid API's saved full tank must retain its contents throughout.
+
+Minute-by-minute JSON/SNBT checkpoints, throughput ledgers, staged artifact hashes,
+console logs and a final `endurance-results.json` remain in the ignored run root.
+Station `MANUAL_REQUIRED` results remain manual; the throughput oracle's PASS does
+not imply a human GUI acceptance result. Error/fatal log lines are retained in the
+report rather than suppressed.
+
+## Run and repeat
+
+From the PowerAdvantage checkout, after building the harness:
+
+```text
+gradlew.bat -p test-rig check assemble
+test-rig/migration/.venv/Scripts/python.exe -m unittest discover -s test-rig/migration -p test_*.py -v
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/endurance.py --java8 --migrated --output run-endurance/new-run
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/audit_endurance.py --run run-endurance/new-run --output run-endurance/new-run/qualification-audit.json
+```
+
+Use the normal Python executable on other platforms. The converted runtime must
+contain `AdvantageWorksMigration/level.dat`. Output directories must be new.
+`--profile-manifest` accepts the migration runner's pinned `target113` profile
+format for another installation; `--port` changes the pair of loopback ports.
+`--seconds 180` is a preflight, reported as `SMOKE_PASS_NOT_ENDURANCE`, never as a
+30-minute qualification. Keep allow markers and harness jars out of real worlds.
+
+The oracle's negative controls prove that missing, duplicated, unexpected and
+unprocessed items, wrong routing, fluid loss/identity errors, closed-tank loss,
+saved battery loss, negative energy and overcharge fail the test.
+
+Preflights also repaired three test-fixture problems, not gameplay code:
+initial in-flight conveyor items were not cleared before measuring a new batch;
+the oil checker watched only the still rather than the whole circuit; and an
+uncontained network-test drill bored into the adjacent scarcity bay. The network
+drills now face immediate bedrock stops, protected by a manifest regression test.
+The oil station's start action refills its source through the actual fluid handler
+rather than merging a fluid compound into an existing empty-tank NBT marker.
+Oil is supplied only at start, not during setup, so the initial batch has no
+preloaded oil hidden in the still. Native inventory commands use the 1.13
+`container.N` slot names, not the older `slot.container.N` spelling.
+
+## Coverage limits
+
+Active bays are `W-01,W-03,W-04,S-01,S-02,S-03,S-04,E-01,E-02,E-03,E-05,P-API-01`.
+Additional inert checks cover `S-06,S-05,E-04,E-06`. `E-04` is not activated beside
+the exclusive `E-01` environmental test. Extra equipment in those bays may tick,
+but only the quantities listed above receive repeated-load qualification.
+
+This run does not qualify moving drills/track recovery, elevators, weapons,
+turrets, loose player data, chunk unload, growth throughput, every fluid machine,
+all boiler fuel/environment combinations, every recipe mode, or a full Sylvester
+modpack upgrade. GUI rendering and client/server presentation remain manual gates.
+There is no system-wide fuel/energy conservation assertion: infinite test supplies
+are intentional, and fuel slots are replenished. Material processing ratios and
+closed fluid storage do have exact quantity assertions.
+
+## Results
+
+This section records the earlier jar set, before the drill and network repairs.
+The [current-candidate rerun](#current-candidate-qualification) is recorded separately.
+
+Qualified locally on 2026-10-06 using Power `2.4.4.113021`, Steam/Electric
+`2.2.5.113021`, Base Metals `3.0.1.113021`, OreSpawn `4.1.0.113021`, and the
+harness SHA-256 `20E0C1D4F0F12682FDD5CB307B31F50534DBB3F79094FAA75847A24DA93E8246`,
+recorded in that run's `input-jars.json`. The current `packaged-current.json`
+may pin a newer test-only harness for subsequent qualification; it must not be
+used to infer the historical run's harness hash. No Advantage gameplay code or
+release jar was changed during this testing pass.
+
+| Environment | Timed load | Verified batches | Full restart | Resumed work | Audit |
+| --- | --- | --- | --- | --- | --- |
+| Fresh 1.13.2 Works | 1,801.56 seconds | 30 | PASS | PASS | PASS |
+| Original 1.10 -> maintained 1.12 -> 1.13 copy | 1,801.56 seconds | 30 | PASS | PASS | PASS |
+
+Per environment, the timed window produced 120 items in each of the five
+processing lanes (600 total), routed 480 plain-conveyor items plus 480 in each
+of the three filter lanes (1,920 total), and converted 60,000 mB crude oil to
+exactly 30,000 mB refined oil. Before/after-restart checks are repeated observations
+of the last batch, not extra production. The resumed batch is verified separately.
+All closed-water and stored-battery witnesses passed; no measured loss, duplication
+or incorrect yield was found. The source fixture's fingerprint remained unchanged.
+
+The independent audit verified all 13,179 inventory-command acknowledgements
+during the timed window and 439 after restart, per world. It also checked all six
+staged jar hashes, thirty distinct batch records, and that no processing station
+was rebuilt/reset/restarted to make the saved networks work. Both servers exited
+normally; no test server remains listening on the four temporary test ports.
+
+Twenty-four Python oracle/audit/migration tests, five Java harness tests, harness
+isolation/build checks, and the normal Power release artifact checks passed.
+All station-level human checks still report `MANUAL_REQUIRED`.
+
+Evidence is retained locally under the Git-ignored
+`run-endurance/20261006-native-qualified/`: `endurance-results.json`,
+`qualification-audit.json`, each world's `endurance-ledger.json`, minute snapshots,
+copied saves, and startup/restart console logs. The earlier
+`run-endurance/20261006-qualified/` completed its quantity checks but used NBT-based
+replenishment: it is **diagnostic only**, not the network-endurance qualification.
+All shorter preflights and failed fixture trials are retained separately.
+
+The logs are not error-free. The migrated world retains the already documented
+Base Metals leaves-transformer error. Forge 25 also reports its config-watcher
+and temporary mod-pack cleanup issues. These remain in `log_diagnostics`; there
+were no `Can't keep up!` warnings in the qualified logs. No Base Metals, OreSpawn,
+or Forge source was modified to suppress these messages.
+
+## Current candidate qualification
+
+On 6 October 2026, `run-endurance/20261006-current-candidates-160410` repeated
+the unchanged native-feed protocol on the current drill- and network-repair
+candidates. Both fresh 1.13 and the original Cyano 1.10 -> maintained 1.12 ->
+1.13 disposable copy report **PASS**, including complete process restart,
+retained-content checks before reseeding, and one new operating batch.
+
+| Environment | Timed load | Verified batches | Game ticks | Restart/resume | Audits |
+| --- | --- | --- | --- | --- | --- |
+| Fresh 1.13.2 Works | 1,801.56 seconds | 30 | 36,207 | PASS | PASS |
+| Original 1.10 -> maintained 1.12 -> 1.13 copy | 1,801.56 seconds | 30 | 36,037 | PASS | PASS |
+
+Per world, the timed window produced 120 items in each of the five processing
+lanes (600 total), routed 480 plain-conveyor items and 480 through each of the
+three filter lanes (1,920 total), and converted 60,000 mB crude oil to exactly
+30,000 mB refined oil. There were no residual inputs, unexpected products or
+measured item/fluid losses. The resumed batch is a separate successful batch;
+before/after-restart observations of the last timed batch are not extra output.
+
+The isolated pump, storage tank and public-API tank retained respectively
+1,000, 2,000 and 4,000 mB water. Stored partial-charge battery item NBT remained
+unchanged, hydroelectric lithium batteries reached their 8,000-unit capacity
+without overcharge, geothermal temperatures stayed above zero, and measured
+energy remained finite and nonnegative through all checkpoints and both restarts.
+
+Exact release pins:
+
+| Artifact | SHA-256 |
+| --- | --- |
+| Power `2.4.4.113021` | `0CCA0AF27497431C3A64091809F8DD328C84D82500CF05456366770C65485E74` |
+| Steam `2.2.5.113021` | `D7D8A2E728E779EF0B624EF8AF3437F3354D618F12E279BE1C134F4AD5E690E3` |
+| Electric `2.2.5.113021` | `72027C2DC273FD28F23AF01BB3EB495C75076E19B27A38CBDC5213B35A6198EB` |
+| Harness `1.1.0.113021` | `80402A725EC02B9D169B0E11CE3E99EF0892F6E6468891A618A34A4E5EC7C615` |
+
+All six staged artifacts, including Base Metals and OreSpawn, match their
+manifest hashes in both worlds. All 53 Python regression and negative-control
+tests pass. The independent command/hash audit verifies 13,179 acknowledged
+inventory writes during the timed phase and 439 after restart per world, thirty
+distinct timed batches, and no processing-station rebuild/reset or replacement
+of power sources and pipes after restart.
+
+A separate offline replay uses the station definitions from each staged
+harness jar and reparses all 33 checkpoints per world: thirty timed batches,
+before restart, after restart and resumed work. All 1,056 station checkpoint
+records agree with the quantity/state oracle and ledgers, including output
+totals, closed-water witnesses, stored battery NBT and world-clock progression.
+The replay sends no server commands and never rewrites original evidence.
+Its result is retained as `raw-snapshot-audit.json` alongside
+`qualification-audit.json`, `endurance-results.json`, ledgers, snapshots and logs.
+
+The runner used loopback ports 25604/25605 after its initial bind check found
+an unrelated server on 25584. That server remains untouched. All four test
+server processes stopped normally, neither test port remains listening, and
+the converted source fixture's before/after fingerprints match.
+
+The logs retain two known Base Metals leaves-transformer errors in the converted
+world (initial startup and restart), and one Forge temporary-resource-pack
+cleanup FATAL on fresh-world shutdown. There are no ticking-machine crashes,
+failed inventory commands, config-watcher FATALs or keep-up warnings in this run.
+The diagnostic lines remain visible; this is not an error-free-log claim.
+
+No production code, release artifact, immutable fixture or earlier result was
+changed, and no commit, push or publication was performed. The converted Works
+bays are newly commissioned equipment, not an endurance qualification of every
+original player machine. The coverage limits above remain: moving drills and
+chunk lifecycle have their separate reports, while broader player/entity
+migration, every machine/fuel/recipe combination and manual client acceptance
+are not qualified by this endurance run.
diff --git a/docs/1.13/MANUFACTURING-PROGRESSION.md b/docs/1.13/MANUFACTURING-PROGRESSION.md
new file mode 100644
index 00000000..5c781bcd
--- /dev/null
+++ b/docs/1.13/MANUFACTURING-PROGRESSION.md
@@ -0,0 +1,188 @@
+# Manufacturing Progression: Minecraft 1.13.2
+
+## Works Notice
+
+An entry in the recipe book is not proof of a usable workshop. This audit starts
+with natural resources and follows the actual loaded recipes to each registered
+Advantage component. Normal and Tech Progression are manufacturing modes;
+Apocalyptic deliberately requires salvaged parts. Creative sources and
+machine-generated helpers are not survival crafting targets.
+
+This concerns the local Power 2.4.4.113021 and Steam/Electric 2.2.5.113021
+candidates, using pinned Base Metals 3.0.1.113021 and OreSpawn 4.1.0.113021.
+It does not rename existing items, blocks, fluids, tile IDs, NBT or recipes.
+
+## Material Ledger
+
+| Material | Supply And Use |
+| --- | --- |
+| Wire | The electric conduit is a member of `forge:wires`. This supplies the electric boiler, electric switch, LED, photovoltaic generator and Normal PSU recipes. |
+| Carbon powder | `forge:dusts/carbon` includes Base Metals coal and charcoal powders through required `forge:dusts/coal` and `forge:dusts/charcoal` references. Coal/charcoal can be crushed with a crackhammer or a powered crusher. |
+| Rubber | Bioplastic joins `forge:rubber` and `forge:ingots/rubber` only when Power's `options.plasticEqualsRubber` is true. Turning it off retains the plastic recipe alternatives and preserves any rubber supplied by another data pack or mod. Petrolplastic is plastic, not an unconditional rubber substitute. |
+| Plastic | Both `poweradvantage:bioplastic_ingot` and `electricadvantage:petrolplastic_ingot` belong to `forge:plastic` and `forge:ingots/plastic`. |
+| Solder | Normal mode additionally accepts lead, tin or silver ingots through `forge:solder`, as the legacy mod did. Tech/Apocalyptic require actual solder unless a user data pack adds alternatives. |
+| Circuit board | `forge:circuit_boards` means the finished `electricadvantage:control_circuit`, not the blank board. A blank board cannot bypass the microchip/control-circuit stage. |
+
+Forge 25's early optional nested-tag resolution can cache an unresolved tag.
+The carbon references are therefore required, with empty declarations so reduced
+mod sets still load safely. Empty tags remain empty: there is no free substitute
+for a missing material provider.
+
+Configured aliases are applied at the end of item-tag reload, before recipe and
+advancement parsing. Server tag serialization therefore includes the same policy
+used by recipes. The initial vanilla-only bootstrap is ignored until FML's mod
+list and Advantage items exist. Resource reload starts from fresh data-pack tags,
+so aliases do not accumulate when a mode or option changes between runs.
+
+## The First Workshop
+
+1. Mine and process metals through Base Metals. Iron and copper equipment lead
+ to crackhammers, steel, brass, bars, rods and plates.
+2. Craft one ordinary or poisonous potato into one starch in the inventory grid
+ or a crafting table. Crackhammers and both powered crushers remain alternative
+ routes at the same ratio. Smelt starch for one bioplastic ingot (200 ticks,
+ 0.1 XP). This removes the plastic/refinery bootstrap loop.
+3. Crush coal or charcoal for carbon powder. Silicon blend smelts to silicon;
+ solder blend smelts to solder (200 ticks and 0.5 XP each).
+4. Smelt lithium ore or powder for one lithium ingot (200 ticks, 0.5 XP). Crushing
+ lithium ore yields two powders; crushing an ingot yields one. Nine lithium
+ nuggets combine into one ingot, and one ingot makes nine nuggets.
+5. Sulfur ore can be mined for its existing powder drops or crushed for four
+ powders. Use sulfur, lithium, solder and circuits for the electrical parts.
+6. Use frames, pipes and conveyors to build the steam/electric machinery. Finding
+ a relevant ingredient earns the matching recipe discovery; no `/recipe give`
+ command is needed. See the [inventory checklist](RECIPE-DISCOVERY.md).
+
+Bioplastic is the initial plastic supply. Petrolplastic is a later alternative:
+collect crude oil in a bucket, distil two buckets into one refined-oil bucket,
+then process refined oil in a powered plastic refinery. The refinery consumes
+100 mB refined oil and 1,200 electricity per ingot, unchanged from the machine's
+existing code. Oil is acquired from a world source, not crafted out of an empty
+bucket. World-generation qualification is a separate test suite.
+
+## Converters
+
+All four preserved converters now have acquisition recipes:
+
+| Converter | Crafting Inputs |
+| --- | --- |
+| RF-labelled steam | Governor, steel frame and redstone block in a row |
+| RF-labelled electricity | PSU, steel frame and redstone block in a row |
+| RF-labelled quantum | Ender pearl, steel frame and redstone block in a row |
+| Tech Reborn-labelled electricity | PSU surrounded by eight refined-iron ingots; ordinary iron is the fallback only when no refined-iron tag provider exists |
+
+The labels and saved identities remain historical; the 1.13 implementations use
+Forge Energy. Steam or Electric Advantage supplies the relevant governor or PSU.
+The recipes remain guarded when that expansion or another ingredient is absent.
+
+## Apocalyptic Salvage
+
+Apocalyptic intentionally disables new manufacture of sprockets, governors and
+PSUs, and forbids new photovoltaic generators. This is not an empty-tag defect.
+Mineshaft, dungeon, blacksmith and stronghold chest pools supply the original
+Advantage parts; Steam/Electric also contribute to end-city chests. Native loot
+tables now provide a fallback when Additional Loot Tables is absent. Existing
+legacy pools, weights, quantities and musket-enchantment functions are retained.
+Data packs can replace the namespaced `chests/advantage_parts` tables.
+
+The native fallback is disabled when Additional Loot Tables is installed, to
+avoid injecting both copies. Setting Power's treasure-chest loot factor to zero
+also disables it. Positive factor scaling is not newly implemented: the legacy
+static pools did not scale their weights with that value. Optional ALT behavior
+is not qualified by the default no-ALT pack.
+
+The original recycling paths recover steel plates, fluid pipes, governors, PSUs,
+control circuits and microchips from salvaged equipment. Photovoltaic equipment
+remains intentionally unobtainable in a fresh Apocalyptic world; use hydro or
+steam generation. Preserved existing photovoltaic blocks are not deleted.
+
+## Inspector's Procedure
+
+```text
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/progression.py --java8 --output run-progression/new-current
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/progression.py --java8 --output run-progression/new-baseline --baseline
+```
+
+The runner creates a marked disposable superflat world, stages hash-checked
+packaged jars and exposes an operator-only `/advprogression
Because these classes are not part of the API, they are not well documented. They were only meant
for the eyes of the mod author(s).
*/
-package com.mcmoddev.poweradvantage.items;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.blocks;
\ No newline at end of file
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java b/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java
new file mode 100644
index 00000000..b5831497
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/client/ClientOilRenderer.java
@@ -0,0 +1,64 @@
+package zone.moddev.mc.poweradvantage.client;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.common.MinecraftForge;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.init.Fluids.PowerFluid;
+
+/** Forge 25's native fluid renderer otherwise uses water sprites for custom fluids. */
+@OnlyIn(Dist.CLIENT)
+public final class ClientOilRenderer {
+ private static volatile TextureAtlasSprite[] crudeSprites;
+ private static volatile TextureAtlasSprite[] refinedSprites;
+
+ private ClientOilRenderer() {}
+
+ public static void register() {
+ MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::onTexturePre);
+ MinecraftForge.EVENT_BUS.addListener(ClientOilRenderer::onTexturePost);
+ }
+
+ private static ResourceLocation texture(String name) {
+ return new ResourceLocation(PowerAdvantage.MODID, "blocks/" + name);
+ }
+
+ private static void onTexturePre(TextureStitchEvent.Pre event) {
+ for (String name : new String[]{"crude_oil_still", "crude_oil_flow", "refined_oil_still", "refined_oil_flow"}) {
+ event.getMap().registerSprite(Minecraft.getInstance().getResourceManager(), texture(name));
+ }
+ }
+
+ private static void onTexturePost(TextureStitchEvent.Post event) {
+ crudeSprites = new TextureAtlasSprite[]{event.getMap().getSprite(texture("crude_oil_still")),
+ event.getMap().getSprite(texture("crude_oil_flow"))};
+ refinedSprites = new TextureAtlasSprite[]{event.getMap().getSprite(texture("refined_oil_still")),
+ event.getMap().getSprite(texture("refined_oil_flow"))};
+ }
+
+ /** Avoid the water-overlay branch without putting oil in the lava tag. */
+ public static boolean useOilPath(IFluidState state, boolean vanillaValue) {
+ return isOil(state) || vanillaValue;
+ }
+
+ public static TextureAtlasSprite[] overrideSprites(IFluidState state, TextureAtlasSprite[] vanillaSprites) {
+ if (!isOil(state)) return vanillaSprites;
+ TextureAtlasSprite[] sprites = ((PowerFluid) state.getFluid()).getKind()
+ == zone.moddev.mc.poweradvantage.init.Fluids.Kind.CRUDE ? crudeSprites : refinedSprites;
+ return sprites == null ? vanillaSprites : sprites;
+ }
+
+ /** The existing oil textures already contain their colour; do not apply biome water tint. */
+ public static int overrideColor(IFluidState state, int vanillaColor) {
+ return isOil(state) ? 0xFFFFFF : vanillaColor;
+ }
+
+ private static boolean isOil(IFluidState state) {
+ return state != null && state.getFluid() instanceof PowerFluid;
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/compat/BaseMetalsCompat.java b/src/main/java/zone/moddev/mc/poweradvantage/compat/BaseMetalsCompat.java
new file mode 100644
index 00000000..2f900a7d
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/compat/BaseMetalsCompat.java
@@ -0,0 +1,40 @@
+package zone.moddev.mc.poweradvantage.compat;
+
+import net.minecraft.block.Block;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.ModList;
+import net.minecraftforge.registries.ForgeRegistries;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+
+/**
+ * Compatibility entry points retained for old internal callers. Crushing recipes
+ * are data-driven in Base Metals 1.13 and live under data/<modid>/recipes.
+ */
+public final class BaseMetalsCompat {
+ private BaseMetalsCompat() {}
+
+ public static boolean isLoaded() { return ModList.get().isLoaded("basemetals"); }
+
+ public static void setStrongHammers(boolean enabled) {
+ PowerAdvantage.LOGGER.debug("Ignoring legacy strong-hammers setting on Base Metals 1.13");
+ }
+
+ public static void addCrusherRecipe(String inputTag, ItemStack output) {
+ PowerAdvantage.LOGGER.debug("Crushing recipe for {} is supplied as data in 1.13", inputTag);
+ }
+
+ public static void addCrusherRecipe(Block input, ItemStack output) {
+ PowerAdvantage.LOGGER.debug("Crushing recipe for {} is supplied as data in 1.13", input.getRegistryName());
+ }
+
+ public static ItemStack getSteelPlate() {
+ Item item = ForgeRegistries.ITEMS.getValue(new ResourceLocation("basemetals", "steel_plate"));
+ return item == null ? null : new ItemStack(item);
+ }
+
+ public static void registerFuelForOre(String tagName, short burnTime) {
+ PowerAdvantage.LOGGER.debug("Fuel tag {} is data-driven in 1.13", tagName);
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/ConduitNetworkManager.java b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java
similarity index 85%
rename from src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/ConduitNetworkManager.java
rename to src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java
index 68642edd..a8cf03f6 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/ConduitNetworkManager.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManager.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.conduitnetwork;
+package zone.moddev.mc.poweradvantage.conduitnetwork;
import java.util.ArrayList;
import java.util.Collections;
@@ -9,10 +9,11 @@
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PowerConnectorContext;
-import com.mcmoddev.poweradvantage.math.BlockPos4D;
-import com.mcmoddev.poweradvantage.util.PowerHelper;
+import zone.moddev.mc.poweradvantage.math.BlockPos4D;
+import zone.moddev.mc.poweradvantage.util.PowerHelper;
import net.minecraft.block.state.IBlockState;
import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
/**
@@ -123,6 +124,28 @@ public void invalidateAll() {
}
}
+ /**
+ * Invalidates networks crossing a changed chunk or touching its boundary.
+ * Boundary networks must also be rescanned when a pipe-only chunk returns.
+ *
+ * @param dimension dimension containing the changed chunk
+ * @param chunk changed chunk position
+ */
+ public void invalidateChunk(int dimension, ChunkPos chunk) {
+ lock.writeLock().lock();
+ try {
+ int minX = (chunk.x << 4) - 1;
+ int minZ = (chunk.z << 4) - 1;
+ for (BlockPos4D coord : new ArrayList<>(networkCache.keySet())) {
+ if (coord.dimension == dimension && coord.pos.getX() >= minX
+ && coord.pos.getX() <= minX + 17 && coord.pos.getZ() >= minZ
+ && coord.pos.getZ() <= minZ + 17) invalidate(coord);
+ }
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
/**
* Invalidates the cached network at a block location and then rescans the world to make a new
* cached network
@@ -136,6 +159,8 @@ public void revalidate(BlockPos4D coord, World w, ConduitType type) {
lock.writeLock().lock();
try {
invalidate(coord);
+ if (!w.isBlockLoaded(coord.pos)) return;
+ createNewNetwork(coord);
recursiveScan(w, coord, w.getBlockState(coord.pos), type);
} finally {
lock.writeLock().unlock();
@@ -172,7 +197,7 @@ protected void recursiveScan(World w, BlockPos4D coord, IBlockState src, Conduit
for (int i = 0; i < EnumFacing.values().length; i++) {
EnumFacing face = EnumFacing.values()[i];
BlockPos4D n = coord.offset(face);
- if (areInSameNetwork(coord, n)) {
+ if (!w.isBlockLoaded(n.pos) || areInSameNetwork(coord, n)) {
continue;
}
IBlockState blockstate = w.getBlockState(n.pos);
diff --git a/src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/ConduitRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/ConduitRegistry.java
rename to src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java
index 0c5afcc9..1c32a0be 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/ConduitRegistry.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitRegistry.java
@@ -1,6 +1,6 @@
-package com.mcmoddev.poweradvantage.conduitnetwork;
+package zone.moddev.mc.poweradvantage.conduitnetwork;
-import com.mcmoddev.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.IPowerMachine;
import cyano.poweradvantage.api.ITypedConduit;
@@ -8,13 +8,17 @@
import cyano.poweradvantage.api.modsupport.ExternalPowerRequest;
import cyano.poweradvantage.api.modsupport.LightWeightPowerRegistry;
import cyano.poweradvantage.api.modsupport.Wrappers;
-import com.mcmoddev.poweradvantage.math.BlockPos4D;
+import zone.moddev.mc.poweradvantage.math.BlockPos4D;
import net.minecraft.block.Block;
import net.minecraft.block.ITileEntityProvider;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
+import net.minecraftforge.common.util.LazyOptional;
+import net.minecraftforge.energy.CapabilityEnergy;
+import net.minecraftforge.energy.IEnergyStorage;
+import net.minecraftforge.event.world.ChunkEvent;
import java.util.*;
import java.util.concurrent.locks.Lock;
@@ -87,7 +91,8 @@ public List getRequestsForPower(World w, BlockPos coord, ConduitTy
* priority.
*/
public List getRequestsForPower(World w, BlockPos coord, ConduitType conduitType, ConduitType energyType) {
- int dimension = w.provider.getDimension();
+ if (!w.isBlockLoaded(coord)) return Collections.emptyList();
+ int dimension = w.dimension.getType().getId();
clearIfWorldChanged(w, dimension);
ConduitNetworkManager manager = getConduitNetworkManager(conduitType);
BlockPos4D bp = new BlockPos4D(dimension, coord);
@@ -107,6 +112,7 @@ public List getRequestsForPower(World w, BlockPos coord, ConduitTy
private List collectRequests(World w, List net, ConduitType conduitType, ConduitType energyType) {
List requests = new ArrayList<>();
for (BlockPos4D pos : net) {
+ if (!w.isBlockLoaded(pos.pos)) continue;
Block b = w.getBlockState(pos.pos).getBlock();
if (b instanceof ITileEntityProvider) {
TileEntity e = w.getTileEntity(pos.pos);
@@ -122,20 +128,13 @@ private List collectRequests(World w, List net, Condui
requests.add(req);
}
}
- } else {
- if (PowerAdvantage.detectedRF && PowerAdvantage.rfConversionTable.containsKey(conduitType)) {
- if (e instanceof cofh.api.energy.IEnergyReceiver) {
- requests.add(Wrappers.wrapRFPowerRequest(
- (cofh.api.energy.IEnergyReceiver) e, energyType, PowerAdvantage.rfConversionTable.get(conduitType)
- ));
- }
- }
- if (PowerAdvantage.detectedTechReborn && PowerAdvantage.trConversionTable.containsKey(conduitType)) {
- if (e instanceof reborncore.api.power.IEnergyInterfaceTile) {
- requests.add(Wrappers.wrapTRPowerRequest(
- (reborncore.api.power.IEnergyInterfaceTile) e, energyType, PowerAdvantage.trConversionTable.get(conduitType)
- ));
- }
+ } else if (e != null && PowerAdvantage.forgeEnergyConversionTable.containsKey(conduitType)) {
+ for (EnumFacing side : EnumFacing.values()) {
+ LazyOptional capability = e.getCapability(CapabilityEnergy.ENERGY, side);
+ if (!capability.isPresent()) continue;
+ capability.ifPresent(storage -> requests.add(Wrappers.wrapForgeEnergyRequest(storage,
+ energyType, PowerAdvantage.forgeEnergyConversionTable.get(conduitType))));
+ break;
}
}
}
@@ -262,6 +261,27 @@ public void conduitBlockLoadedEvent(World w, int dimension, ConduitType... types
}
}
+ /** Invalidates only networks affected by a server chunk load or unload. */
+ public void onChunkLoad(ChunkEvent.Load event) {
+ invalidateChunk(event);
+ }
+
+ /** Invalidates only networks affected by a server chunk load or unload. */
+ public void onChunkUnload(ChunkEvent.Unload event) {
+ invalidateChunk(event);
+ }
+
+ private void invalidateChunk(ChunkEvent event) {
+ if (!(event.getWorld() instanceof World)) return;
+ World world = (World) event.getWorld();
+ if (world.isRemote) return;
+ int dimension = world.dimension.getType().getId();
+ clearIfWorldChanged(world, dimension);
+ for (ConduitNetworkManager manager : networkManagers.values()) {
+ manager.invalidateChunk(dimension, event.getChunk().getPos());
+ }
+ }
+
/**
* Invoke this method anytime a conduit block is removed from the world
*
diff --git a/src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/package-info.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/package-info.java
index 5a96a5da..0dae2dea 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/conduitnetwork/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/conduitnetwork/package-info.java
@@ -4,4 +4,4 @@
the same power type and caching the resulting list of sources and sinks. Every time a conduit block
is placed or broken, any adjacent networks are revalidated.
*/
-package com.mcmoddev.poweradvantage.conduitnetwork;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.conduitnetwork;
\ No newline at end of file
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java b/src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java
new file mode 100644
index 00000000..36a0c796
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/config/PowerAdvantageConfig.java
@@ -0,0 +1,96 @@
+package zone.moddev.mc.poweradvantage.config;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import net.minecraftforge.common.ForgeConfigSpec;
+import net.minecraftforge.fml.config.ModConfig;
+import cyano.poweradvantage.api.FluidCategoryRegistry;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.RecipeMode;
+
+/** Common configuration with the same gameplay defaults as the legacy cfg file. */
+public final class PowerAdvantageConfig {
+ private static final String DEFAULT_DISTILLATION = "2*crude_oil->1*refined_oil";
+
+ public static final ForgeConfigSpec SPEC;
+ public static final ForgeConfigSpec.ConfigValue RECIPE_MODE;
+ public static final ForgeConfigSpec.DoubleValue CHEST_LOOT_FACTOR;
+ public static final ForgeConfigSpec.BooleanValue PLASTIC_IS_RUBBER;
+ public static final ForgeConfigSpec.BooleanValue USE_OTHER_FLUIDS;
+ public static final ForgeConfigSpec.ConfigValue DISTILLER_RECIPES;
+ public static final ForgeConfigSpec.ConfigValue ADDITIONAL_CRUDE_OIL_ALIASES;
+ public static final ForgeConfigSpec.ConfigValue RF_CONVERSIONS;
+ public static final ForgeConfigSpec.ConfigValue TECH_REBORN_CONVERSIONS;
+
+ private static boolean loaded;
+
+ static {
+ ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
+ builder.push("options");
+ RECIPE_MODE = builder.comment("NORMAL, TECH_PROGRESSION, or APOCALYPTIC. Restart after changing.")
+ .worldRestart().define("recipeMode", "NORMAL");
+ CHEST_LOOT_FACTOR = builder.defineInRange("treasureChestLootFactor", 0.5D, 0.0D, 1000.0D);
+ PLASTIC_IS_RUBBER = builder.define("plasticEqualsRubber", true);
+ USE_OTHER_FLUIDS = builder.define("useOtherFluids", false);
+ builder.pop();
+ builder.push("recipes");
+ DISTILLER_RECIPES = builder.define("distillerRecipes", DEFAULT_DISTILLATION);
+ builder.pop();
+ builder.push("compatibility");
+ ADDITIONAL_CRUDE_OIL_ALIASES = builder.comment(
+ "Semi-colon delimited fluid registry names to treat as crude oil. Built-in aliases are always retained.")
+ .define("additionalCrudeOilFluidAliases", "");
+ builder.pop();
+ builder.push("energy");
+ RF_CONVERSIONS = builder.comment(
+ "Forge Energy units per Advantage energy unit for the historical RF-labelled converters.")
+ .define("rfConversions", "steam=8;electricity=0.25;quantum=8");
+ TECH_REBORN_CONVERSIONS = builder.comment(
+ "Forge Energy units per Advantage energy unit for the historical Tech Reborn-labelled converter.")
+ .define("techRebornConversions", "electricity=0.25");
+ builder.pop();
+ SPEC = builder.build();
+ }
+
+ private PowerAdvantageConfig() {}
+
+ public static void onConfigLoading(ModConfig.Loading event) {
+ if (event.getConfig().getSpec() != SPEC) return;
+ loaded = true;
+ apply();
+ }
+
+ public static void apply() {
+ if (!loaded) return;
+ String configuredMode = RECIPE_MODE.get().trim().toUpperCase(Locale.ROOT);
+ try {
+ PowerAdvantage.recipeMode = RecipeMode.valueOf(configuredMode);
+ } catch (IllegalArgumentException ex) {
+ PowerAdvantage.LOGGER.warn("Unknown recipe mode '{}'; using NORMAL", configuredMode);
+ PowerAdvantage.recipeMode = RecipeMode.NORMAL;
+ }
+ PowerAdvantage.chestLootFactor = CHEST_LOOT_FACTOR.get().floatValue();
+ PowerAdvantage.plasticIsAlsoRubber = PLASTIC_IS_RUBBER.get();
+ PowerAdvantage.useOtherFluids = USE_OTHER_FLUIDS.get();
+ for (String alias : ADDITIONAL_CRUDE_OIL_ALIASES.get().split(";")) {
+ if (!alias.trim().isEmpty()) {
+ FluidCategoryRegistry.registerAlias(FluidCategoryRegistry.CRUDE_OIL, alias);
+ }
+ }
+ }
+
+ public static String[] distillationRecipes() {
+ if (!loaded) return new String[] { DEFAULT_DISTILLATION };
+ return Arrays.stream(DISTILLER_RECIPES.get().split(";"))
+ .map(String::trim).filter(value -> !value.isEmpty()).toArray(String[]::new);
+ }
+
+ public static String rfConversions() {
+ return loaded ? RF_CONVERSIONS.get() : "steam=8;electricity=0.25;quantum=8";
+ }
+
+ public static String techRebornConversions() {
+ return loaded ? TECH_REBORN_CONVERSIONS.get() : "electricity=0.25";
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/graphics/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/graphics/package-info.java
similarity index 70%
rename from src/main/java/com/mcmoddev/poweradvantage/graphics/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/graphics/package-info.java
index 0a2cbe9c..d75d8375 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/graphics/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/graphics/package-info.java
@@ -2,4 +2,4 @@
This package contains classes related to on-demand rendering, such as for
entities and animated blocks.
*/
-package com.mcmoddev.poweradvantage.graphics;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.graphics;
\ No newline at end of file
diff --git a/src/main/java/com/mcmoddev/poweradvantage/gui/FilteredFluidTankGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java
similarity index 84%
rename from src/main/java/com/mcmoddev/poweradvantage/gui/FilteredFluidTankGUI.java
rename to src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java
index ab29c34a..2f13f20d 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/gui/FilteredFluidTankGUI.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/FilteredFluidTankGUI.java
@@ -1,11 +1,11 @@
-package com.mcmoddev.poweradvantage.gui;
+package zone.moddev.mc.poweradvantage.gui;
import cyano.poweradvantage.api.fluid.FluidPoweredEntity;
import cyano.poweradvantage.api.simple.SimpleMachineGUI;
-import com.mcmoddev.poweradvantage.math.Integer2D;
+import zone.moddev.mc.poweradvantage.math.Integer2D;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fluids.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidStack;
public class FilteredFluidTankGUI extends SimpleMachineGUI {
@@ -27,8 +27,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int
guiDisplayImage, guiContainer, x, y, z);
}
}
- GlStateManager.color(1f, 1f, 1f, 1f); // reset tint
- guiContainer.mc.renderEngine.bindTexture(guiDisplayImage);
+ GlStateManager.color4f(1f, 1f, 1f, 1f); // reset tint
+ guiContainer.mc.getTextureManager().bindTexture(guiDisplayImage);
guiContainer.drawTexturedModalRect(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height)
}
@@ -45,7 +45,7 @@ public static void drawFluidBar(
FluidTankGUI.drawFluidFilledRectangle(guiContainer, fs, x + xPos, y + yPos + barSlotHeight - h, w, h, zLevel);
//guiContainer.drawModalRectWithCustomSizedTexture(x+xPos, y+yPos+barSlotHeight-h, 0, 0, w, h, 16, h);//h * texPerPixel); // x, y, u, v, width, height, textureWidth, textureHeight
- guiContainer.mc.renderEngine.bindTexture(displayImage);
+ guiContainer.mc.getTextureManager().bindTexture(displayImage);
guiContainer.drawTexturedModalRect(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height)
}
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/gui/FluidTankGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java
similarity index 51%
rename from src/main/java/com/mcmoddev/poweradvantage/gui/FluidTankGUI.java
rename to src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java
index d4cf5ae8..e21c64ce 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/gui/FluidTankGUI.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/FluidTankGUI.java
@@ -1,22 +1,22 @@
-package com.mcmoddev.poweradvantage.gui;
+package zone.moddev.mc.poweradvantage.gui;
import cyano.poweradvantage.api.fluid.FluidPoweredEntity;
import cyano.poweradvantage.api.simple.SimpleMachineGUI;
-import com.mcmoddev.poweradvantage.math.Integer2D;
+import zone.moddev.mc.poweradvantage.math.Integer2D;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fluids.FluidStack;
+import net.minecraft.world.biome.BiomeColors;
+import cyano.poweradvantage.api.fluid.FluidStack;
-import java.util.HashMap;
-import java.util.Map;
public class FluidTankGUI extends SimpleMachineGUI {
- private static final Map realTextureLocationCache = new HashMap<>();
public FluidTankGUI(ResourceLocation image) {
super(image,
@@ -35,8 +35,8 @@ public void drawGUIDecorations(Object srcEntity, GUIContainer guiContainer, int
guiDisplayImage, guiContainer, x, y, z);
}
}
- GlStateManager.color(1f, 1f, 1f, 1f); // reset tint
- guiContainer.mc.renderEngine.bindTexture(guiDisplayImage);
+ GlStateManager.color4f(1f, 1f, 1f, 1f); // reset tint
+ guiContainer.mc.getTextureManager().bindTexture(guiDisplayImage);
guiContainer.drawTexturedModalRect(x + 77, y + 6, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height)
}
@@ -52,7 +52,7 @@ public static void drawFluidBar(
FluidTankGUI.drawFluidFilledRectangle(guiContainer, fs, x + xPos, y + yPos + barSlotHeight - h, w, h, zLevel);
//guiContainer.drawModalRectWithCustomSizedTexture(x+xPos, y+yPos+barSlotHeight-h, 0, 0, w, h, 16, h);//h * texPerPixel); // x, y, u, v, width, height, textureWidth, textureHeight
- guiContainer.mc.renderEngine.bindTexture(displayImage);
+ guiContainer.mc.getTextureManager().bindTexture(displayImage);
guiContainer.drawTexturedModalRect(x + xPos - 3, y + yPos - 3, 176, 0, 22, 66); // x, y, textureOffsetX, textureOffsetY, width, height)
}
@@ -62,40 +62,35 @@ public static void drawFluidFilledRectangle(
int x, int y, int width, int height,
float zLevel
) {
- if (fs == null || fs.getFluid() == null) return;
- ResourceLocation fluidTexture = realTextureLocationCache.computeIfAbsent(fs.getFluid().getStill(fs),
- (ResourceLocation r) -> new ResourceLocation(r.getNamespace(), "textures/".concat(r.getPath()).concat(".png"))
- );
- guiContainer.mc.renderEngine.bindTexture(fluidTexture);
-
- int colorARGB = fs.getFluid().getColor(fs);
- float oneOver256 = 0.00390625F;
- //float alpha = oneOver256 * ((colorARGB >>> 24) & 0xFF);
- float alpha = 1f;
- float red = oneOver256 * ((colorARGB >>> 16) & 0xFF);
- float green = oneOver256 * ((colorARGB >>> 8) & 0xFF);
- float blue = oneOver256 * ((colorARGB) & 0xFF);
-
- float f1 = 0.00390625F * 16;
+ if (fs == null || fs.getFluid() == null || width <= 0 || height <= 0) return;
+ // The atlas supplies the current animation frame; water's sprite is grayscale.
+ TextureAtlasSprite sprite = guiContainer.mc.getModelManager().getBlockModelShapes()
+ .getModel(fs.getFluid().getDefaultState().getBlockState()).getParticleTexture();
+ guiContainer.mc.getTextureManager().bindTexture(TextureMap.LOCATION_BLOCKS_TEXTURE);
+ int color = 0xFFFFFF;
+ if (fs.getFluid().isEquivalentTo(net.minecraft.init.Fluids.WATER)) {
+ color = guiContainer.mc.world != null && guiContainer.mc.player != null
+ ? BiomeColors.getWaterColor(guiContainer.mc.world, guiContainer.mc.player.getPosition()) : 0x3F76E4;
+ }
+ float red = ((color >>> 16) & 255) / 255F;
+ float green = ((color >>> 8) & 255) / 255F;
+ float blue = (color & 255) / 255F;
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder vertexbuffer = tessellator.getBuffer();
vertexbuffer.begin(7, DefaultVertexFormats.POSITION_TEX_COLOR);
- vertexbuffer
- .pos((double) (x + 0), (double) (y + height), (double) zLevel)
- .tex(0, f1)
- .color(red, green, blue, alpha).endVertex();
- vertexbuffer
- .pos((double) (x + width), (double) (y + height), (double) zLevel)
- .tex(f1, f1)
- .color(red, green, blue, alpha).endVertex();
- vertexbuffer
- .pos((double) (x + width), (double) (y + 0), (double) zLevel)
- .tex(f1, 0)
- .color(red, green, blue, alpha).endVertex();
- vertexbuffer
- .pos((double) (x + 0), (double) (y + 0), (double) zLevel)
- .tex(0, 0)
- .color(red, green, blue, alpha).endVertex();
+ for (int dy = 0; dy < height; dy += 16) {
+ int tileHeight = Math.min(16, height - dy);
+ for (int dx = 0; dx < width; dx += 16) {
+ int tileWidth = Math.min(16, width - dx);
+ double u0 = sprite.getMinU(), u1 = sprite.getInterpolatedU(tileWidth);
+ double v0 = sprite.getMinV(), v1 = sprite.getInterpolatedV(tileHeight);
+ vertexbuffer.pos(x + dx, y + dy + tileHeight, zLevel).tex(u0, v1).color(red, green, blue, 1F).endVertex();
+ vertexbuffer.pos(x + dx + tileWidth, y + dy + tileHeight, zLevel).tex(u1, v1).color(red, green, blue, 1F).endVertex();
+ vertexbuffer.pos(x + dx + tileWidth, y + dy, zLevel).tex(u1, v0).color(red, green, blue, 1F).endVertex();
+ vertexbuffer.pos(x + dx, y + dy, zLevel).tex(u0, v0).color(red, green, blue, 1F).endVertex();
+ }
+ }
tessellator.draw();
}
+
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/gui/FunctionalCreativeTab.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/FunctionalCreativeTab.java
similarity index 81%
rename from src/main/java/com/mcmoddev/poweradvantage/gui/FunctionalCreativeTab.java
rename to src/main/java/zone/moddev/mc/poweradvantage/gui/FunctionalCreativeTab.java
index 0b16830d..6d7414a7 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/gui/FunctionalCreativeTab.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/FunctionalCreativeTab.java
@@ -1,17 +1,17 @@
-package com.mcmoddev.poweradvantage.gui;
+package zone.moddev.mc.poweradvantage.gui;
-import net.minecraft.creativetab.CreativeTabs;
+import net.minecraft.item.ItemGroup;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.util.NonNullList;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Uses java 8 function API to lazily pass an item icon
* @author DrCyano
*
*/
-public class FunctionalCreativeTab extends CreativeTabs {
+public class FunctionalCreativeTab extends ItemGroup {
private final java.util.function.Supplier itemSupplier;
private final java.util.Comparator itemSortingAlgorithm;
@@ -61,9 +61,9 @@ public boolean hasSearchBar() {
* @param itemList All the items in the tab so you can sort them
*/
@Override
- @SideOnly(Side.CLIENT)
- public void displayAllRelevantItems(NonNullList itemList) {
- super.displayAllRelevantItems(itemList);
+ @OnlyIn(Dist.CLIENT)
+ public void fill(NonNullList itemList) {
+ super.fill(itemList);
itemList.sort(itemSortingAlgorithm);
}
@@ -71,7 +71,7 @@ public void displayAllRelevantItems(NonNullList itemList) {
/**
* Gets the item used in the tab icon
*/
- @SideOnly(Side.CLIENT)
+ @OnlyIn(Dist.CLIENT)
@Override
public ItemStack createIcon() {
return new ItemStack(itemSupplier.get());
diff --git a/src/main/java/com/mcmoddev/poweradvantage/gui/StillGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/gui/StillGUI.java
rename to src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java
index 2f4bac29..e091e2ec 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/gui/StillGUI.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/StillGUI.java
@@ -1,10 +1,10 @@
-package com.mcmoddev.poweradvantage.gui;
+package zone.moddev.mc.poweradvantage.gui;
import cyano.poweradvantage.api.simple.SimpleMachineGUI;
-import com.mcmoddev.poweradvantage.machines.fluidmachines.StillTileEntity;
-import com.mcmoddev.poweradvantage.math.Integer2D;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity;
+import zone.moddev.mc.poweradvantage.math.Integer2D;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fluids.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidStack;
public class StillGUI extends SimpleMachineGUI {
diff --git a/src/main/java/com/mcmoddev/poweradvantage/blocks/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/gui/package-info.java
similarity index 77%
rename from src/main/java/com/mcmoddev/poweradvantage/blocks/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/gui/package-info.java
index 335c0081..a8bcce10 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/blocks/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/gui/package-info.java
@@ -3,10 +3,10 @@
This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API.
If you are writing an add-on mod, use the code in this package for reference, but you should not
depend on it's implementation details as future updates might change them. For a better way to get
-PowerAdvantage content, look in the com.mcmoddev.poweradvantage.init package.
+PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package.
Because these classes are not part of the API, they are not well documented. They were only meant
for the eyes of the mod author(s).
*/
-package com.mcmoddev.poweradvantage.blocks;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.gui;
\ No newline at end of file
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java
new file mode 100644
index 00000000..f79778ef
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Blocks.java
@@ -0,0 +1,154 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.GUIBlock;
+import cyano.poweradvantage.api.fluid.InteractiveFluidBlock;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockFlowingFluid;
+import net.minecraft.block.material.Material;
+import net.minecraft.init.MobEffects;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemBlock;
+import net.minecraft.potion.PotionEffect;
+import net.minecraftforge.event.RegistryEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.blocks.BlockFrame;
+import zone.moddev.mc.poweradvantage.blocks.BlockPowerSwitch;
+import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor;
+import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyorFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityBlockFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFoodFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFuelFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityInventoryFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOreFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOverflowFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityPlantFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntitySmeltableFilter;
+import zone.moddev.mc.poweradvantage.machines.creative.InfiniteEnergyBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDischargeBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDrainBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidPipeBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.StorageTankBlock;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport.TerminalFluidPipeBlock;
+
+/** Stable block and block-item registry for the 1.13 port. */
+@Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
+public final class Blocks {
+ private static final float STEEL_BLAST_RESISTANCE = 37.5f;
+ private static final Map ALL_BLOCKS = new LinkedHashMap<>();
+
+ public static GUIBlock fluid_drain;
+ public static GUIBlock fluid_discharge;
+ public static GUIBlock storage_tank;
+ public static GUIBlock metal_storage_tank;
+ public static GUIBlock still;
+ public static Block fluid_pipe;
+ public static Block fluid_pipe_terminal;
+ public static Block fluid_switch;
+ public static GUIBlock item_conveyor;
+ public static GUIBlock item_filter_block;
+ public static GUIBlock item_filter_food;
+ public static GUIBlock item_filter_fuel;
+ public static GUIBlock item_filter_inventory;
+ public static GUIBlock item_filter_ore;
+ public static GUIBlock item_filter_plant;
+ public static GUIBlock item_filter_smelt;
+ public static GUIBlock item_filter_overflow;
+ public static Block steel_frame;
+ public static GUIBlock infinite_steam;
+ public static GUIBlock infinite_electricity;
+ public static GUIBlock infinite_quantum;
+ public static BlockFlowingFluid crude_oil_block;
+ public static BlockFlowingFluid refined_oil_block;
+
+ private static boolean initialized;
+
+ private Blocks() {}
+
+ public static synchronized void init() {
+ if (initialized) return;
+ ItemGroups.init();
+ Fluids.init();
+
+ fluid_drain = (GUIBlock) add(new FluidDrainBlock(), "fluid_drain");
+ fluid_discharge = (GUIBlock) add(new FluidDischargeBlock(), "fluid_discharge");
+ storage_tank = (GUIBlock) add(new StorageTankBlock(), "fluid_storage_tank");
+ metal_storage_tank = (GUIBlock) add(new MetalTankBlock(), "fluid_metal_tank");
+ still = (GUIBlock) add(new StillBlock(), "still");
+ fluid_pipe = add(new FluidPipeBlock(), "fluid_pipe");
+ fluid_pipe_terminal = add(new TerminalFluidPipeBlock(), "fluid_pipe_terminal");
+ fluid_switch = add(new BlockPowerSwitch(Fluids.fluidConduit_general), "fluid_switch");
+ steel_frame = add(new BlockFrame(Material.PISTON, 0.75F, STEEL_BLAST_RESISTANCE), "steel_frame");
+
+ final float hardness = 0.75F;
+ item_conveyor = (GUIBlock) add(new BlockConveyor(Material.PISTON, hardness), "item_conveyor");
+ item_filter_block = filter("item_filter_block", TileEntityBlockFilter.class, hardness);
+ item_filter_food = filter("item_filter_food", TileEntityFoodFilter.class, hardness);
+ item_filter_fuel = filter("item_filter_fuel", TileEntityFuelFilter.class, hardness);
+ item_filter_inventory = filter("item_filter_inventory", TileEntityInventoryFilter.class, hardness);
+ item_filter_ore = filter("item_filter_ore", TileEntityOreFilter.class, hardness);
+ item_filter_plant = filter("item_filter_plant", TileEntityPlantFilter.class, hardness);
+ item_filter_smelt = filter("item_filter_smelt", TileEntitySmeltableFilter.class, hardness);
+ item_filter_overflow = filter("item_filter_overflow", TileEntityOverflowFilter.class, hardness);
+
+ infinite_steam = (GUIBlock) add(new InfiniteEnergyBlock(new ConduitType("steam")), "infinite_steam");
+ infinite_electricity = (GUIBlock) add(new InfiniteEnergyBlock(new ConduitType("electricity")), "infinite_electricity");
+ infinite_quantum = (GUIBlock) add(new InfiniteEnergyBlock(new ConduitType("quantum")), "infinite_quantum");
+
+ Block.Properties oilProperties = Block.Properties.create(Material.WATER).doesNotBlockMovement()
+ .hardnessAndResistance(100.0F).variableOpacity();
+ refined_oil_block = (BlockFlowingFluid) add(new InteractiveFluidBlock(Fluids.refined_oil,
+ oilProperties, true, (world, entity) -> entity.addPotionEffect(new PotionEffect(MobEffects.NAUSEA, 200))),
+ "refined_oil");
+ crude_oil_block = (BlockFlowingFluid) add(new InteractiveFluidBlock(Fluids.crude_oil,
+ oilProperties, true, (world, entity) -> entity.addPotionEffect(new PotionEffect(MobEffects.SLOWNESS, 200, 2))),
+ "crude_oil");
+
+ ModSupport.initBlocks();
+ initialized = true;
+ }
+
+ private static GUIBlock filter(String id, Class extends zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor> type,
+ float hardness) {
+ return (GUIBlock) add(new BlockConveyorFilter(Material.PISTON, hardness, type), id);
+ }
+
+ public static Block add(Block block, String id) {
+ block.setRegistryName(PowerAdvantage.MODID, id);
+ ALL_BLOCKS.put(id, block);
+ return block;
+ }
+
+ @SubscribeEvent
+ public static void registerBlocks(RegistryEvent.Register event) {
+ init();
+ event.getRegistry().registerAll(ALL_BLOCKS.values().toArray(new Block[0]));
+ }
+
+ @SubscribeEvent
+ public static void registerBlockItems(RegistryEvent.Register event) {
+ init();
+ for (Map.Entry entry : ALL_BLOCKS.entrySet()) {
+ Item.Properties properties = new Item.Properties();
+ if (!"fluid_pipe_terminal".equals(entry.getKey())
+ && !(entry.getValue() instanceof BlockFlowingFluid)) {
+ properties.group(ItemGroups.tab_powerAdvantage);
+ }
+ ItemBlock item = new ItemBlock(entry.getValue(), properties);
+ item.setRegistryName(PowerAdvantage.MODID, entry.getKey());
+ event.getRegistry().register(item);
+ }
+ }
+
+ public static Map getModBlockRegistry() {
+ return Collections.unmodifiableMap(ALL_BLOCKS);
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java
new file mode 100644
index 00000000..5817e733
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Entities.java
@@ -0,0 +1,88 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.tileentity.TileEntityType;
+import net.minecraftforge.event.RegistryEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityBlockFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityConveyor;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFoodFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityFuelFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityInventoryFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOreFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityOverflowFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntityPlantFilter;
+import zone.moddev.mc.poweradvantage.machines.conveyors.TileEntitySmeltableFilter;
+import zone.moddev.mc.poweradvantage.machines.creative.InfiniteEnergyTileEntity;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDischargeTileEntity;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidDrainTileEntity;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankTileEntity;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillTileEntity;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.StorageTankTileEntity;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport.TerminalFluidPipeTileEntity;
+import cyano.poweradvantage.api.modsupport.rf.TileEntityRFElectricityConverter;
+import cyano.poweradvantage.api.modsupport.rf.TileEntityRFQuantumConverter;
+import cyano.poweradvantage.api.modsupport.rf.TileEntityRFSteamConverter;
+import cyano.poweradvantage.api.modsupport.techreborn.TileEntityTRElectricityConverter;
+
+/** Stable 1.13 tile entity type registrations. */
+@Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
+public final class Entities {
+ public static TileEntityType PIPE_TERMINAL;
+ public static TileEntityType FLUID_DRAIN;
+ public static TileEntityType FLUID_DISCHARGE;
+ public static TileEntityType STORAGE_TANK;
+ public static TileEntityType METAL_TANK;
+ public static TileEntityType STILL;
+ public static TileEntityType CONVEYOR;
+ public static TileEntityType FILTER_BLOCK;
+ public static TileEntityType FILTER_FOOD;
+ public static TileEntityType FILTER_FUEL;
+ public static TileEntityType FILTER_INVENTORY;
+ public static TileEntityType FILTER_ORE;
+ public static TileEntityType FILTER_PLANT;
+ public static TileEntityType FILTER_SMELT;
+ public static TileEntityType FILTER_OVERFLOW;
+ public static TileEntityType INFINITE_ENERGY;
+ public static TileEntityType RF_STEAM_CONVERTER;
+ public static TileEntityType RF_ELECTRICITY_CONVERTER;
+ public static TileEntityType RF_QUANTUM_CONVERTER;
+ public static TileEntityType TR_ELECTRICITY_CONVERTER;
+
+ private Entities() {}
+
+ @SubscribeEvent
+ public static void register(RegistryEvent.Register> event) {
+ PIPE_TERMINAL = register(event, "tileentity.pipe_terminal", TerminalFluidPipeTileEntity::new);
+ FLUID_DRAIN = register(event, "tileentity.fluid_drain", FluidDrainTileEntity::new);
+ FLUID_DISCHARGE = register(event, "tileentity.fluid_discharge", FluidDischargeTileEntity::new);
+ STORAGE_TANK = register(event, "tileentity.fluid_storage_tank", StorageTankTileEntity::new);
+ METAL_TANK = register(event, "tileentity.fluid_metal_tank", MetalTankTileEntity::new);
+ STILL = register(event, "tileentity.still", StillTileEntity::new);
+ CONVEYOR = register(event, "item_conveyor", TileEntityConveyor::new);
+ FILTER_BLOCK = register(event, "item_filter_block", TileEntityBlockFilter::new);
+ FILTER_FOOD = register(event, "item_filter_food", TileEntityFoodFilter::new);
+ FILTER_FUEL = register(event, "item_filter_fuel", TileEntityFuelFilter::new);
+ FILTER_INVENTORY = register(event, "item_filter_inventory", TileEntityInventoryFilter::new);
+ FILTER_ORE = register(event, "item_filter_ore", TileEntityOreFilter::new);
+ FILTER_PLANT = register(event, "item_filter_plant", TileEntityPlantFilter::new);
+ FILTER_SMELT = register(event, "item_filter_smelt", TileEntitySmeltableFilter::new);
+ FILTER_OVERFLOW = register(event, "item_filter_overflow", TileEntityOverflowFilter::new);
+ INFINITE_ENERGY = register(event, "infinite_energy_source", InfiniteEnergyTileEntity::new);
+ RF_STEAM_CONVERTER = register(event, "rf_steam_converter_tileentity", TileEntityRFSteamConverter::new);
+ RF_ELECTRICITY_CONVERTER = register(event, "rf_electricity_converter_tileentity", TileEntityRFElectricityConverter::new);
+ RF_QUANTUM_CONVERTER = register(event, "rf_quantum_converter_tileentity", TileEntityRFQuantumConverter::new);
+ TR_ELECTRICITY_CONVERTER = register(event, "tr_electricity_converter_tileentity", TileEntityTRElectricityConverter::new);
+ }
+
+ private static TileEntityType register(
+ RegistryEvent.Register> event, String id,
+ java.util.function.Supplier extends T> factory) {
+ TileEntityType type = TileEntityType.Builder.create(factory).build(null);
+ type.setRegistryName(PowerAdvantage.MODID, id);
+ event.getRegistry().register(type);
+ return type;
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java
new file mode 100644
index 00000000..c149fe13
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Fluids.java
@@ -0,0 +1,158 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import cyano.poweradvantage.api.ConduitType;
+import net.minecraft.block.BlockFlowingFluid;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.fluid.FlowingFluid;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.item.Item;
+import net.minecraft.state.StateContainer;
+import net.minecraft.util.BlockRenderLayer;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.registry.IRegistry;
+import net.minecraft.world.IWorld;
+import net.minecraft.world.IWorldReaderBase;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+
+/** Native 1.13 fluids and their PowerAdvantage conduit identities. */
+public final class Fluids {
+ public static final ConduitType fluidConduit_general = new ConduitType("fluid");
+ private static final Map FLUID_TYPES = new IdentityHashMap<>();
+
+ public static ConduitType fluidConduit_water;
+ public static ConduitType fluidConduit_lava;
+ public static PowerFluid.Source crude_oil;
+ public static PowerFluid.Flowing flowing_crude_oil;
+ public static PowerFluid.Source refined_oil;
+ public static PowerFluid.Flowing flowing_refined_oil;
+
+ private static boolean initialized;
+
+ private Fluids() {}
+
+ public static synchronized void init() {
+ if (initialized) return;
+ fluidConduit_water = registerNewFluid(net.minecraft.init.Fluids.WATER);
+ fluidConduit_lava = registerNewFluid(net.minecraft.init.Fluids.LAVA);
+
+ flowing_crude_oil = new PowerFluid.Flowing(Kind.CRUDE);
+ crude_oil = new PowerFluid.Source(Kind.CRUDE);
+ flowing_refined_oil = new PowerFluid.Flowing(Kind.REFINED);
+ refined_oil = new PowerFluid.Source(Kind.REFINED);
+
+ register("flowing_crude_oil", flowing_crude_oil);
+ register("crude_oil", crude_oil);
+ register("flowing_refined_oil", flowing_refined_oil);
+ register("refined_oil", refined_oil);
+ registerNewFluid(crude_oil);
+ registerNewFluid(flowing_crude_oil);
+ registerNewFluid(refined_oil);
+ registerNewFluid(flowing_refined_oil);
+ initialized = true;
+ }
+
+ private static void register(String name, Fluid fluid) {
+ IRegistry.field_212619_h.put(new ResourceLocation(PowerAdvantage.MODID, name), fluid);
+ for (IFluidState state : fluid.getStateContainer().getValidStates()) {
+ Fluid.STATE_REGISTRY.add(state);
+ }
+ }
+
+ public static ConduitType registerNewFluid(Fluid fluid) {
+ ResourceLocation id = IRegistry.field_212619_h.getKey(fluid);
+ ConduitType type = new ConduitType(id == null ? "unknown_fluid" : id.toString());
+ FLUID_TYPES.put(fluid, type);
+ return type;
+ }
+
+ public static boolean isFluidType(ConduitType type) {
+ return type.equals(fluidConduit_general) || FLUID_TYPES.containsValue(type);
+ }
+
+ public static ConduitType fluidToConduitType(Fluid fluid) {
+ ConduitType type = FLUID_TYPES.get(fluid);
+ return type == null ? registerNewFluid(fluid) : type;
+ }
+
+ public static Fluid conduitTypeToFluid(ConduitType type) {
+ for (Map.Entry entry : FLUID_TYPES.entrySet()) {
+ if (ConduitType.areSameType(entry.getValue(), type)) return entry.getKey();
+ }
+ return null;
+ }
+
+ public static Fluid byName(String name) {
+ if (name == null || name.trim().isEmpty()) return null;
+ ResourceLocation id = name.indexOf(':') >= 0 ? new ResourceLocation(name)
+ : new ResourceLocation(PowerAdvantage.MODID, name);
+ Fluid fluid = IRegistry.field_212619_h.func_212608_b(id);
+ if (fluid == null && name.indexOf(':') < 0) {
+ fluid = IRegistry.field_212619_h.func_212608_b(new ResourceLocation("minecraft", name));
+ }
+ return fluid;
+ }
+
+ public enum Kind { CRUDE, REFINED }
+
+ public abstract static class PowerFluid extends FlowingFluid {
+ private final Kind kind;
+
+ protected PowerFluid(Kind kind) { this.kind = kind; }
+ public Kind getKind() { return kind; }
+
+ @Override public Fluid getFlowingFluid() {
+ return kind == Kind.CRUDE ? flowing_crude_oil : flowing_refined_oil;
+ }
+ @Override public Fluid getStillFluid() {
+ return kind == Kind.CRUDE ? crude_oil : refined_oil;
+ }
+ @Override public Item getFilledBucket() {
+ return kind == Kind.CRUDE ? Items.crude_oil_bucket : Items.refined_oil_bucket;
+ }
+ @Override @OnlyIn(Dist.CLIENT) public BlockRenderLayer getRenderLayer() {
+ return BlockRenderLayer.TRANSLUCENT;
+ }
+ @Override protected boolean canSourcesMultiply() { return false; }
+ @Override protected void beforeReplacingBlock(IWorld world, BlockPos pos, IBlockState state) {
+ state.dropBlockAsItem(world.getWorld(), pos, 0);
+ }
+ @Override public int getSlopeFindDistance(IWorldReaderBase world) { return 4; }
+ @Override public int getLevelDecreasePerBlock(IWorldReaderBase world) { return 1; }
+ @Override public int getTickRate(IWorldReaderBase world) { return kind == Kind.CRUDE ? 15 : 10; }
+ @Override protected float getExplosionResistance() { return 100.0F; }
+ @Override public boolean isEquivalentTo(Fluid fluid) {
+ return fluid == getStillFluid() || fluid == getFlowingFluid();
+ }
+ @Override protected boolean canOtherFlowInto(IFluidState state, Fluid fluid, EnumFacing direction) {
+ return direction == EnumFacing.DOWN && !fluid.isEquivalentTo(this);
+ }
+ @Override public IBlockState getBlockState(IFluidState state) {
+ BlockFlowingFluid block = kind == Kind.CRUDE ? Blocks.crude_oil_block : Blocks.refined_oil_block;
+ return block.getDefaultState().with(BlockFlowingFluid.LEVEL, Integer.valueOf(getLevelFromState(state)));
+ }
+
+ public static final class Flowing extends PowerFluid {
+ public Flowing(Kind kind) { super(kind); }
+ @Override protected void fillStateContainer(StateContainer.Builder builder) {
+ super.fillStateContainer(builder);
+ builder.add(LEVEL_1_TO_8);
+ }
+ @Override public int getLevel(IFluidState state) { return state.get(LEVEL_1_TO_8); }
+ @Override public boolean isSource(IFluidState state) { return false; }
+ }
+
+ public static final class Source extends PowerFluid {
+ public Source(Kind kind) { super(kind); }
+ @Override public int getLevel(IFluidState state) { return 8; }
+ @Override public boolean isSource(IFluidState state) { return true; }
+ }
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java
new file mode 100644
index 00000000..3d343180
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Fuels.java
@@ -0,0 +1,32 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import zone.moddev.mc.poweradvantage.registry.FuelRegistry;
+import net.minecraft.item.ItemStack;
+
+
+
+public abstract class Fuels {
+
+ public static final int CRUDE_OIL_FUEL_PER_FLUID_UNIT = 5;
+ public static final int REFINED_OIL_FUEL_PER_FLUID_UNIT = 25;
+
+ private static boolean initDone = false;
+
+ public static void init() {
+ if (initDone) return;
+
+ Items.init();
+ FuelRegistry.getInstance().registerFuel(Items.crude_oil_bucket,
+ (short) (1000 * CRUDE_OIL_FUEL_PER_FLUID_UNIT));
+ FuelRegistry.getInstance().registerFuel(Items.refined_oil_bucket,
+ (short) Math.min(Short.MAX_VALUE, 1000 * REFINED_OIL_FUEL_PER_FLUID_UNIT));
+ FuelRegistry.getInstance().registerPostBurnItem(Items.crude_oil_bucket,
+ (ItemStack stack) -> new ItemStack(net.minecraft.init.Items.BUCKET));
+ FuelRegistry.getInstance().registerPostBurnItem(Items.refined_oil_bucket,
+ (ItemStack stack) -> new ItemStack(net.minecraft.init.Items.BUCKET));
+
+
+ initDone = true;
+ }
+
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/GUI.java b/src/main/java/zone/moddev/mc/poweradvantage/init/GUI.java
similarity index 93%
rename from src/main/java/com/mcmoddev/poweradvantage/init/GUI.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/GUI.java
index 5e769763..3d845d7a 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/GUI.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/GUI.java
@@ -1,11 +1,11 @@
-package com.mcmoddev.poweradvantage.init;
+package zone.moddev.mc.poweradvantage.init;
import net.minecraft.util.ResourceLocation;
-import com.mcmoddev.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
import cyano.poweradvantage.api.simple.SimpleMachineGUI;
-import com.mcmoddev.poweradvantage.gui.*;
-import com.mcmoddev.poweradvantage.math.Integer2D;
-import com.mcmoddev.poweradvantage.registry.MachineGUIRegistry;
+import zone.moddev.mc.poweradvantage.gui.*;
+import zone.moddev.mc.poweradvantage.math.Integer2D;
+import zone.moddev.mc.poweradvantage.registry.MachineGUIRegistry;
public abstract class GUI {
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/ItemGroups.java b/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java
similarity index 71%
rename from src/main/java/com/mcmoddev/poweradvantage/init/ItemGroups.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java
index 47a91f80..c55a03c1 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/ItemGroups.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/ItemGroups.java
@@ -1,17 +1,16 @@
-package com.mcmoddev.poweradvantage.init;
+package zone.moddev.mc.poweradvantage.init;
-import com.mcmoddev.poweradvantage.PowerAdvantage;
-import com.mcmoddev.poweradvantage.gui.FunctionalCreativeTab;
-import net.minecraft.block.ITileEntityProvider;
-import net.minecraft.creativetab.CreativeTabs;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.gui.FunctionalCreativeTab;
+import net.minecraft.item.ItemGroup;
import net.minecraft.item.Item;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemStack;
-import net.minecraftforge.fml.common.registry.ForgeRegistries;
+import net.minecraftforge.registries.ForgeRegistries;
public class ItemGroups {
- public static CreativeTabs tab_powerAdvantage;
+ public static ItemGroup tab_powerAdvantage;
private static boolean initDone = false;
@@ -43,7 +42,7 @@ private static String catagorize(ItemStack i) {
Item item = i.getItem();
sb.append(ForgeRegistries.ITEMS.getKey(item).getNamespace());
if (item instanceof ItemBlock) {
- if (((ItemBlock) item).getBlock() instanceof ITileEntityProvider) {
+ if (((ItemBlock) item).getBlock().hasTileEntity(((ItemBlock) item).getBlock().getDefaultState())) {
sb.append("A");
} else {
sb.append("B");
@@ -52,8 +51,6 @@ private static String catagorize(ItemStack i) {
sb.append("I");
}
sb.append(item.getTranslationKey());
- sb.append(i.getMetadata());
-
return sb.toString();
}
}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java
new file mode 100644
index 00000000..339d1e46
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Items.java
@@ -0,0 +1,62 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemBucket;
+import net.minecraftforge.event.RegistryEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.items.RotationTool;
+
+/** Stable item registry for PowerAdvantage 1.13. */
+@Mod.EventBusSubscriber(modid = PowerAdvantage.MODID, bus = Mod.EventBusSubscriber.Bus.MOD)
+public final class Items {
+ public static Item starch;
+ public static Item bioplastic_ingot;
+ public static Item sprocket;
+ public static Item rotator_tool;
+ public static Item crude_oil_bucket;
+ public static Item refined_oil_bucket;
+
+ public static final Map allItems = new LinkedHashMap<>();
+ private static boolean initialized;
+
+ private Items() {}
+
+ public static synchronized void init() {
+ if (initialized) return;
+ Blocks.init();
+ starch = add("starch", new Item(properties()));
+ bioplastic_ingot = add("bioplastic_ingot", new Item(properties()));
+ sprocket = add("sprocket", new Item(properties()));
+ rotator_tool = add("rotator_tool", new RotationTool(properties().maxStackSize(1)));
+ crude_oil_bucket = add("crude_oil_bucket", new ItemBucket(Fluids.crude_oil,
+ new Item.Properties().group(ItemGroups.tab_powerAdvantage).maxStackSize(1)));
+ refined_oil_bucket = add("refined_oil_bucket", new ItemBucket(Fluids.refined_oil,
+ new Item.Properties().group(ItemGroups.tab_powerAdvantage).maxStackSize(1)));
+ initialized = true;
+ }
+
+ private static Item.Properties properties() {
+ return new Item.Properties().group(ItemGroups.tab_powerAdvantage);
+ }
+
+ private static Item add(String id, Item item) {
+ item.setRegistryName(PowerAdvantage.MODID, id);
+ allItems.put(id, item);
+ return item;
+ }
+
+ @SubscribeEvent
+ public static void register(RegistryEvent.Register event) {
+ init();
+ event.getRegistry().registerAll(allItems.values().toArray(new Item[0]));
+ }
+
+ public static Item getItemByName(String id) { return allItems.get(id); }
+ public static Set> getAllRegisteredItems() { return allItems.entrySet(); }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/Materials.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Materials.java
similarity index 77%
rename from src/main/java/com/mcmoddev/poweradvantage/init/Materials.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/Materials.java
index f0467502..4373aac3 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/Materials.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Materials.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.init;
+package zone.moddev.mc.poweradvantage.init;
public class Materials {
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java b/src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java
new file mode 100644
index 00000000..9120d9b6
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/ModSupport.java
@@ -0,0 +1,64 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import java.util.Locale;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.modsupport.rf.BlockRFConverter;
+import cyano.poweradvantage.api.modsupport.rf.TileEntityRFElectricityConverter;
+import cyano.poweradvantage.api.modsupport.rf.TileEntityRFQuantumConverter;
+import cyano.poweradvantage.api.modsupport.rf.TileEntityRFSteamConverter;
+import cyano.poweradvantage.api.modsupport.techreborn.BlockTRConverter;
+import cyano.poweradvantage.api.modsupport.techreborn.TileEntityTRElectricityConverter;
+import net.minecraft.block.Block;
+import net.minecraft.block.material.Material;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.config.PowerAdvantageConfig;
+
+/** Registers the converter blocks whose historical IDs are retained in 1.13. */
+public final class ModSupport {
+ public static Block converter_rf_steam;
+ public static Block converter_rf_electricity;
+ public static Block converter_rf_quantum;
+ public static Block converter_tr_electricity;
+
+ private static boolean initialized;
+
+ private ModSupport() {}
+
+ public static synchronized void initBlocks() {
+ if (initialized) return;
+ float hardness = 0.75F;
+ Material material = Material.PISTON;
+ converter_rf_steam = Blocks.add(new BlockRFConverter(material, hardness, new ConduitType("steam"),
+ TileEntityRFSteamConverter.class), "converter_rf_steam");
+ converter_rf_electricity = Blocks.add(new BlockRFConverter(material, hardness, new ConduitType("electricity"),
+ TileEntityRFElectricityConverter.class), "converter_rf_electricity");
+ converter_rf_quantum = Blocks.add(new BlockRFConverter(material, hardness, new ConduitType("quantum"),
+ TileEntityRFQuantumConverter.class), "converter_rf_quantum");
+ converter_tr_electricity = Blocks.add(new BlockTRConverter(material, hardness, new ConduitType("electricity"),
+ TileEntityTRElectricityConverter.class), "converter_tr_electricity");
+ initialized = true;
+ }
+
+ public static void configureForgeEnergyConversions() {
+ PowerAdvantage.forgeEnergyConversionTable.clear();
+ PowerAdvantage.techRebornForgeEnergyConversionTable.clear();
+ parseConversions(PowerAdvantageConfig.rfConversions(), PowerAdvantage.forgeEnergyConversionTable);
+ parseConversions(PowerAdvantageConfig.techRebornConversions(),
+ PowerAdvantage.techRebornForgeEnergyConversionTable);
+ }
+
+ private static void parseConversions(String descriptor, java.util.Map destination) {
+ for (String entry : descriptor.split(";")) {
+ int separator = entry.indexOf('=');
+ if (separator <= 0) continue;
+ String name = entry.substring(0, separator).trim().toLowerCase(Locale.ROOT);
+ try {
+ float factor = Float.parseFloat(entry.substring(separator + 1).trim());
+ if (factor > 0) destination.put(new ConduitType(name), factor);
+ } catch (NumberFormatException exception) {
+ PowerAdvantage.LOGGER.warn("Ignoring invalid Forge Energy conversion '{}'", entry);
+ }
+ }
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java
new file mode 100644
index 00000000..eea8f0f3
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Recipes.java
@@ -0,0 +1,57 @@
+package zone.moddev.mc.poweradvantage.init;
+
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import cyano.poweradvantage.api.FluidCategoryRegistry;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.registry.IRegistry;
+import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipeRegistry;
+
+
+public abstract class Recipes {
+
+ /*
+ * Normal - can craft all necessary machine parts
+ * Apocalyptic - must find key parts as treasure in chests
+ * Tech-progression - making the first key part is very complicated, but once made, key parts can be duplicated fairly easily
+ */
+
+
+ public static void initDistillationRecipes(String[] distillRecipes) {
+ for (String recipe : distillRecipes) {
+ String r = recipe.trim();
+ if (r.isEmpty()) continue;
+ try {
+ int numIn, numOut;
+ String fluidIn, fluidOut;
+ String inputStr = r.substring(0, r.indexOf("->")).trim();
+ numIn = Integer.parseInt(inputStr.substring(0, inputStr.indexOf('*')).trim());
+ fluidIn = inputStr.substring(inputStr.indexOf('*') + 1).trim();
+ String outputStr = r.substring(inputStr.length() + "->".length()).trim();
+ numOut = Integer.parseInt(outputStr.substring(0, outputStr.indexOf('*')).trim());
+ fluidOut = outputStr.substring(outputStr.indexOf('*') + 1).trim();
+ DistillationRecipeRegistry.addDistillationRecipe(fluidIn, numIn, fluidOut, numOut);
+ } catch (Exception ex) {
+ PowerAdvantage.LOGGER.error("Failed to add fluid distillation recipe '{}'", r, ex);
+ }
+ }
+ initCrudeOilAliasDistillationRecipes();
+ }
+
+ private static void initCrudeOilAliasDistillationRecipes() {
+ Fluid refinedOil = Fluids.refined_oil;
+ if (refinedOil == null) return;
+ ResourceLocation outputId = IRegistry.field_212619_h.getKey(refinedOil);
+ if (outputId == null) return;
+
+ for (Fluid input : FluidCategoryRegistry.getRegisteredFluids(FluidCategoryRegistry.CRUDE_OIL)) {
+ if (DistillationRecipeRegistry.getInstance().getDistillationRecipeForFluid(input) != null) continue;
+ ResourceLocation inputId = IRegistry.field_212619_h.getKey(input);
+ if (inputId != null) {
+ DistillationRecipeRegistry.addDistillationRecipe(inputId.toString(), 2, outputId.toString(), 1);
+ }
+ }
+ DistillationRecipeRegistry.clearRecipeCache();
+ }
+
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/TreasureChests.java b/src/main/java/zone/moddev/mc/poweradvantage/init/TreasureChests.java
similarity index 94%
rename from src/main/java/com/mcmoddev/poweradvantage/init/TreasureChests.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/TreasureChests.java
index 2cd9e46e..4ea3adfd 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/TreasureChests.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/TreasureChests.java
@@ -1,7 +1,7 @@
-package com.mcmoddev.poweradvantage.init;
+package zone.moddev.mc.poweradvantage.init;
-import com.mcmoddev.poweradvantage.PowerAdvantage;
-import net.minecraftforge.fml.common.FMLLog;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
import org.apache.logging.log4j.Level;
import java.io.IOException;
@@ -33,7 +33,7 @@ private static void writeLootFile(Path file, String content) {
Files.createDirectories(file.getParent());
Files.write(file, Arrays.asList(content), Charset.forName("UTF-8"));
} catch (IOException e) {
- FMLLog.log(Level.ERROR, e, "Error writing additional-loot-table files");
+ PowerAdvantage.LOGGER.error("Error writing additional-loot-table files", e);
}
}
@@ -108,4 +108,4 @@ private static void writeLootFile(Path file, String content) {
" }\n" +
" ]\n" +
"}\n";
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/Villages.java b/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java
similarity index 84%
rename from src/main/java/com/mcmoddev/poweradvantage/init/Villages.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java
index a652ea84..25ab569f 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/Villages.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/Villages.java
@@ -1,11 +1,10 @@
-package com.mcmoddev.poweradvantage.init;
+package zone.moddev.mc.poweradvantage.init;
import net.minecraft.entity.passive.EntityVillager;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.common.FMLLog;
-import net.minecraftforge.fml.common.registry.ForgeRegistries;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.fml.common.registry.VillagerRegistry;
-import org.apache.logging.log4j.Level;
public abstract class Villages {
// TODO: add machinist villager
@@ -22,12 +21,11 @@ public abstract class Villages {
public static void init() {
if (initDone) return;
- Entities.init();
try {
insertTrades(3, 3, 1, new EntityVillager.ListItemForEmeralds(Items.sprocket, new EntityVillager.PriceInfo(-4, -1)));
insertTrades(0, 1, 1, new EntityVillager.ListItemForEmeralds(Items.bioplastic_ingot, new EntityVillager.PriceInfo(-8, -4)));
} catch (RuntimeException e) {
- FMLLog.log(Level.ERROR, e, "Failed to add trades to villagers");
+ PowerAdvantage.LOGGER.error("Failed to add trades to villagers", e);
}
initDone = true;
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/WorldGen.java b/src/main/java/zone/moddev/mc/poweradvantage/init/WorldGen.java
similarity index 93%
rename from src/main/java/com/mcmoddev/poweradvantage/init/WorldGen.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/WorldGen.java
index c8b73943..77cc2cba 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/WorldGen.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/WorldGen.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.init;
+package zone.moddev.mc.poweradvantage.init;
public abstract class WorldGen {
diff --git a/src/main/java/com/mcmoddev/poweradvantage/init/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/init/package-info.java
similarity index 77%
rename from src/main/java/com/mcmoddev/poweradvantage/init/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/init/package-info.java
index 9b9febb0..2711107c 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/init/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/init/package-info.java
@@ -2,4 +2,4 @@
This package contains registries for all of the new content added by PowerAdvantage. Use these
classes to interact with the items and blocks.
*/
-package com.mcmoddev.poweradvantage.init;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.init;
\ No newline at end of file
diff --git a/src/main/java/com/mcmoddev/poweradvantage/items/RotationTool.java b/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java
similarity index 65%
rename from src/main/java/com/mcmoddev/poweradvantage/items/RotationTool.java
rename to src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java
index fe1cd62a..3011304b 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/items/RotationTool.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/items/RotationTool.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.items;
+package zone.moddev.mc.poweradvantage.items;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
@@ -8,20 +8,21 @@
import net.minecraft.util.Rotation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemUseContext;
public class RotationTool extends net.minecraft.item.Item {
- public RotationTool() {
- super();
- super.setMaxStackSize(1);
+ public RotationTool(Item.Properties properties) {
+ super(properties);
}
@Override
- public EnumActionResult onItemUse(
- final EntityPlayer player, final World w, final BlockPos coord,
- final EnumHand hand, final EnumFacing facing,
- final float partialX, final float partialY, final float partialZ
- ) {
+ public EnumActionResult onItemUse(ItemUseContext context) {
+ final EntityPlayer player = context.getPlayer();
+ final World w = context.getWorld();
+ final BlockPos coord = context.getPos();
+ final EnumFacing facing = context.getFace();
IBlockState bs = w.getBlockState(coord);
/* // comment this line to turn the wrench into a hacker tool
try { // hacker info
@@ -37,9 +38,9 @@ public EnumActionResult onItemUse(
}
//*/
- if (bs.getBlock().rotateBlock(w, coord, facing)) return EnumActionResult.SUCCESS;
- if (bs.withRotation(Rotation.COUNTERCLOCKWISE_90) != bs) {
- w.setBlockState(coord, bs.withRotation(Rotation.COUNTERCLOCKWISE_90));
+ IBlockState rotated = bs.rotate(w, coord, Rotation.COUNTERCLOCKWISE_90);
+ if (rotated != bs) {
+ w.setBlockState(coord, rotated);
return EnumActionResult.SUCCESS;
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/gui/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/items/package-info.java
similarity index 76%
rename from src/main/java/com/mcmoddev/poweradvantage/gui/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/items/package-info.java
index c23b41d6..84786047 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/gui/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/items/package-info.java
@@ -3,10 +3,10 @@
This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API.
If you are writing an add-on mod, use the code in this package for reference, but you should not
depend on it's implementation details as future updates might change them. For a better way to get
-PowerAdvantage content, look in the com.mcmoddev.poweradvantage.init package.
+PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package.
Because these classes are not part of the API, they are not well documented. They were only meant
for the eyes of the mod author(s).
*/
-package com.mcmoddev.poweradvantage.gui;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.items;
\ No newline at end of file
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java
new file mode 100644
index 00000000..a9393128
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyor.java
@@ -0,0 +1,52 @@
+package zone.moddev.mc.poweradvantage.machines.conveyors;
+
+import cyano.poweradvantage.api.GUIBlock;
+import net.minecraft.block.Block;
+import net.minecraft.block.SoundType;
+import net.minecraft.block.material.Material;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.state.DirectionProperty;
+import net.minecraft.state.StateContainer;
+import net.minecraft.state.properties.BlockStateProperties;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Mirror;
+import net.minecraft.util.Rotation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IBlockReader;
+import net.minecraft.world.World;
+
+/** Six-facing conveyor block; legacy metadata remains the facing index. */
+public class BlockConveyor extends GUIBlock {
+ public static final DirectionProperty FACING = BlockStateProperties.FACING;
+
+ public BlockConveyor(Material material, float hardness) {
+ super(Block.Properties.create(material).hardnessAndResistance(hardness)
+ .sound(SoundType.METAL).variableOpacity());
+ setDefaultState(getStateContainer().getBaseState().with(FACING, EnumFacing.NORTH));
+ }
+
+ @Override public TileEntity createNewTileEntity(IBlockReader world) { return new TileEntityConveyor(); }
+ @Override public IBlockState rotate(IBlockState state, Rotation rotation) { return state.with(FACING, rotation.rotate(state.get(FACING))); }
+ @Override public IBlockState mirror(IBlockState state, Mirror mirror) { return state.rotate(mirror.toRotation(state.get(FACING))); }
+ @Override public IBlockState getStateForPlacement(BlockItemUseContext context) {
+ return getDefaultState().with(FACING, context.getFace().getOpposite());
+ }
+ @Override protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); }
+ @Override public boolean isOpaqueCube(IBlockState state) { return false; }
+ @Override public boolean isFullCube(IBlockState state) { return false; }
+ @Override public boolean hasComparatorInputOverride(IBlockState state) { return true; }
+
+ @Override
+ public int getComparatorInputOverride(IBlockState state, World world, BlockPos pos) {
+ TileEntity tile = world.getTileEntity(pos);
+ return tile instanceof TileEntityConveyor && !((TileEntityConveyor) tile).getStackInSlot(0).isEmpty() ? 7 : 0;
+ }
+
+ public IBlockState getStateFromMeta(int meta) {
+ EnumFacing facing = EnumFacing.byIndex(meta);
+ return getDefaultState().with(FACING, facing == null ? EnumFacing.NORTH : facing);
+ }
+ public int getMetaFromState(IBlockState state) { return state.get(FACING).getIndex(); }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java
new file mode 100644
index 00000000..6f3c8a37
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/BlockConveyorFilter.java
@@ -0,0 +1,68 @@
+package zone.moddev.mc.poweradvantage.machines.conveyors;
+
+import cyano.poweradvantage.api.GUIBlock;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockHorizontal;
+import net.minecraft.block.SoundType;
+import net.minecraft.block.material.Material;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.item.BlockItemUseContext;
+import net.minecraft.state.DirectionProperty;
+import net.minecraft.state.StateContainer;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.Mirror;
+import net.minecraft.util.Rotation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IBlockReader;
+import net.minecraft.world.World;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+
+/** Horizontal conveyor filter block retaining its legacy metadata codec. */
+public class BlockConveyorFilter extends GUIBlock {
+ public static final DirectionProperty FACING = BlockHorizontal.HORIZONTAL_FACING;
+ private final Class extends TileEntityConveyor> tileEntityClass;
+
+ public BlockConveyorFilter(Material material, float hardness,
+ Class extends TileEntityConveyor> tileEntityClass) {
+ super(Block.Properties.create(material).hardnessAndResistance(hardness)
+ .sound(SoundType.METAL).variableOpacity());
+ this.tileEntityClass = tileEntityClass;
+ setDefaultState(getStateContainer().getBaseState().with(FACING, EnumFacing.NORTH));
+ }
+
+ @Override
+ public TileEntity createNewTileEntity(IBlockReader world) {
+ try {
+ return tileEntityClass.newInstance();
+ } catch (ReflectiveOperationException exception) {
+ PowerAdvantage.LOGGER.error("Could not create conveyor filter {}", tileEntityClass.getName(), exception);
+ return null;
+ }
+ }
+
+ @Override public IBlockState rotate(IBlockState state, Rotation rotation) { return state.with(FACING, rotation.rotate(state.get(FACING))); }
+ @Override public IBlockState mirror(IBlockState state, Mirror mirror) { return state.rotate(mirror.toRotation(state.get(FACING))); }
+ @Override public IBlockState getStateForPlacement(BlockItemUseContext context) {
+ return getDefaultState().with(FACING, context.getPlacementHorizontalFacing());
+ }
+ @Override protected void fillStateContainer(StateContainer.Builder builder) { builder.add(FACING); }
+ @Override public boolean isOpaqueCube(IBlockState state) { return false; }
+ @Override public boolean isFullCube(IBlockState state) { return false; }
+ @Override public boolean hasComparatorInputOverride(IBlockState state) { return true; }
+
+ @Override
+ public int getComparatorInputOverride(IBlockState state, World world, BlockPos pos) {
+ TileEntity tile = world.getTileEntity(pos);
+ if (!(tile instanceof TileEntityConveyorFilter)) return 0;
+ TileEntityConveyorFilter filter = (TileEntityConveyorFilter) tile;
+ if (filter.getStackInSlot(0).isEmpty()) return 0;
+ return filter.matchesFilter(filter.getStackInSlot(0)) ? 15 : 7;
+ }
+
+ public IBlockState getStateFromMeta(int meta) {
+ EnumFacing facing = EnumFacing.byIndex(meta);
+ return getDefaultState().with(FACING, facing.getAxis().isHorizontal() ? facing : EnumFacing.NORTH);
+ }
+ public int getMetaFromState(IBlockState state) { return state.get(FACING).getIndex(); }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityBlockFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java
similarity index 61%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityBlockFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java
index b25d8740..6df07954 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityBlockFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityBlockFilter.java
@@ -1,9 +1,11 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemStack;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityBlockFilter extends TileEntityConveyorFilter {
+ public TileEntityBlockFilter() { super(Entities.FILTER_BLOCK, 1); }
@Override
public boolean matchesFilter(ItemStack item) {
@@ -11,4 +13,4 @@ public boolean matchesFilter(ItemStack item) {
return item.getItem() instanceof ItemBlock;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityConveyor.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityConveyor.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java
index 6d45415d..acee3c27 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityConveyor.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyor.java
@@ -1,6 +1,6 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
-import com.mcmoddev.poweradvantage.util.InventoryWrapper;
+import zone.moddev.mc.poweradvantage.util.InventoryWrapper;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.entity.player.EntityPlayer;
@@ -11,6 +11,7 @@
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityHopper;
+import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ITickable;
import net.minecraft.util.math.AxisAlignedBB;
@@ -18,13 +19,15 @@
import net.minecraft.util.text.TextComponentBase;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextComponentTranslation;
+import net.minecraft.util.text.ITextComponent;
import net.minecraft.world.ILockableContainer;
import net.minecraft.world.World;
-import net.minecraftforge.fml.common.FMLLog;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityConveyor extends TileEntity implements ITickable, ISidedInventory {
@@ -39,17 +42,17 @@ public class TileEntityConveyor extends TileEntity implements ITickable, ISidedI
public TileEntityConveyor(){
- this(1);
+ this(Entities.CONVEYOR, 1);
}
- public TileEntityConveyor(int inventorySize){
- super();
+ protected TileEntityConveyor(TileEntityType> type, int inventorySize){
+ super(type);
inventory = new ItemStack[inventorySize];
}
@Override
- public void update() {
+ public void tick() {
World w = getWorld();
if(w.isRemote) return;
if(transferCooldown > 0) transferCooldown--;
@@ -96,7 +99,7 @@ public void update() {
getInventory()[0].grow(1);
}
if(entityStack.isEmpty()){
- e.setDead();
+ e.remove();
} else {
e.setItem(entityStack);
}
@@ -190,10 +193,10 @@ protected static boolean transferItem(ISidedInventory src, EnumFacing srcFace, I
*/
public EnumFacing getFacing(){
IBlockState state = getWorld().getBlockState(getPos());
- if(state.getProperties().containsKey(BlockConveyor.FACING)){
- return (EnumFacing) state.getProperties().get(BlockConveyor.FACING);
+ if(state.has(BlockConveyor.FACING)){
+ return state.get(BlockConveyor.FACING);
}else{
- FMLLog.warning(this.getClass().getName()+".getFacing(): blockstate for block at this position ("+getPos()+") does not have property \"BlockConveyor.FACING\"!");
+ PowerAdvantage.LOGGER.warn("{}.getFacing(): blockstate at {} does not have BlockConveyor.FACING", this.getClass().getName(), getPos());
return EnumFacing.NORTH;
}
}
@@ -204,41 +207,41 @@ protected ItemStack[] getInventory(){
/**
- * You must override this method and call super.readFromNBT(...).
+ * You must override this method and call super.read(...).
* This method reads data from an NBT data tag (either from a data packet
* or loaded from the Chunk).
* @param tagRoot The root of the NBT
*/
@Override
- public void readFromNBT(final NBTTagCompound tagRoot) {
- super.readFromNBT(tagRoot);
+ public void read(final NBTTagCompound tagRoot) {
+ super.read(tagRoot);
ItemStack[] inventory = this.getInventory();
if(inventory != null ){
Arrays.fill(inventory, null);
- final NBTTagList nbttaglist = tagRoot.getTagList("Items", 10);
- for (int i = 0; i < nbttaglist.tagCount() && i < inventory.length; ++i) {
- final NBTTagCompound nbttagcompound1 = nbttaglist.getCompoundTagAt(i);
+ final NBTTagList nbttaglist = tagRoot.getList("Items", 10);
+ for (int i = 0; i < nbttaglist.size() && i < inventory.length; ++i) {
+ final NBTTagCompound nbttagcompound1 = nbttaglist .getCompound(i);
final byte n = nbttagcompound1.getByte("Slot");
if (n >= 0 && n < inventory.length) {
- ItemStack stack = new ItemStack(nbttagcompound1);
+ ItemStack stack = ItemStack.read(nbttagcompound1);
inventory[n] = stack.isEmpty() ? null : stack;
}
}
}
- if (tagRoot.hasKey("CustomName", 8)) {
+ if (tagRoot .contains("CustomName", 8)) {
this.customName = tagRoot.getString("CustomName");
}
}
/**
- * You must override this method and call super.writeToNBT(...).
+ * You must override this method and call super .write(...).
* This method writes data to an NBT data tag (either to a data packet
* or saved to the Chunk).
* @param tagRoot The root of the NBT
*/
@Override
- public NBTTagCompound writeToNBT(final NBTTagCompound tagRoot) {
- super.writeToNBT(tagRoot);
+ public NBTTagCompound write(final NBTTagCompound tagRoot) {
+ super .write(tagRoot);
ItemStack[] inventory = this.getInventory();
if(inventory != null ){
final NBTTagList nbttaglist = new NBTTagList();
@@ -246,8 +249,8 @@ public NBTTagCompound writeToNBT(final NBTTagCompound tagRoot) {
if (inventory[i] != null && !inventory[i].isEmpty()) {
final NBTTagCompound nbttagcompound1 = new NBTTagCompound();
nbttagcompound1.setByte("Slot", (byte)i);
- inventory[i].writeToNBT(nbttagcompound1);
- nbttaglist.appendTag(nbttagcompound1);
+ inventory[i] .write(nbttagcompound1);
+ nbttaglist .add(nbttagcompound1);
}
}
tagRoot.setTag("Items", nbttaglist);
@@ -266,7 +269,6 @@ public NBTTagCompound writeToNBT(final NBTTagCompound tagRoot) {
* @param newSate State after change
* @return true if this TileEntity should be invalidated (deleted and recreated), false otherwise
*/
- @Override
public boolean shouldRefresh(World world, BlockPos pos, IBlockState oldState, IBlockState newSate) {
return oldState.getBlock() != newSate.getBlock();
}
@@ -300,7 +302,7 @@ public ItemStack decrStackSize(int slot, int decrement) {
this.getInventory()[slot] = null;
return itemstack;
}
- final ItemStack itemstack = this.getInventory()[slot].splitStack(decrement);
+ final ItemStack itemstack = this.getInventory()[slot] .split(decrement);
if (this.getInventory()[slot].isEmpty()) {
this.getInventory()[slot] = null;
}
@@ -382,10 +384,7 @@ public void setInventorySlotContents(int slot, ItemStack item) {
@Override
public TextComponentBase getDisplayName() {
- if (this.hasCustomName()) {
- return new TextComponentString(this.getName());
- }
- return new TextComponentTranslation(this.getName(), new Object[0]);
+ return (TextComponentBase) getName();
}
private String customName = null;
@@ -395,8 +394,14 @@ public TextComponentBase getDisplayName() {
* given by the player (i.e. with an anvil)
*/
@Override
- public String getName() {
- return this.hasCustomName() ? this.customName : this.getUnlocalizedName();
+ public ITextComponent getName() {
+ return this.hasCustomName() ? new TextComponentString(this.customName)
+ : new TextComponentTranslation(this.getUnlocalizedName());
+ }
+
+ @Override
+ public ITextComponent getCustomName() {
+ return this.hasCustomName() ? new TextComponentString(this.customName) : null;
}
/**
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java
index c8afc01d..fe0065a2 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityConveyorFilter.java
@@ -1,6 +1,6 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
-import com.mcmoddev.poweradvantage.util.InventoryWrapper;
+import zone.moddev.mc.poweradvantage.util.InventoryWrapper;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.item.EntityItem;
import net.minecraft.inventory.IInventory;
@@ -8,21 +8,27 @@
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityHopper;
+import net.minecraft.tileentity.TileEntityType;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
-import net.minecraftforge.fml.common.FMLLog;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.init.Entities;
public abstract class TileEntityConveyorFilter extends TileEntityConveyor {
public TileEntityConveyorFilter() {
- this(1);
+ this(Entities.FILTER_BLOCK, 1);
+ }
+
+ protected TileEntityConveyorFilter(TileEntityType> type, int slots) {
+ super(type, slots);
}
public abstract boolean matchesFilter(ItemStack item);
@Override
- public void update() {
+ public void tick() {
World w = getWorld();
if (w.isRemote) return;
if (transferCooldown > 0) transferCooldown--;
@@ -110,12 +116,6 @@ private void dropItem(TileEntity target) {
}
}
-
- public TileEntityConveyorFilter(int inventorySize) {
- super(inventorySize);
- }
-
-
/**
* Gets the direction that the blockstate is facing
*
@@ -124,10 +124,10 @@ public TileEntityConveyorFilter(int inventorySize) {
@Override
public EnumFacing getFacing() {
IBlockState state = getWorld().getBlockState(getPos());
- if (state.getProperties().containsKey(BlockConveyorFilter.FACING)) {
- return (EnumFacing) state.getProperties().get(BlockConveyorFilter.FACING);
+ if (state.has(BlockConveyorFilter.FACING)) {
+ return state.get(BlockConveyorFilter.FACING);
} else {
- FMLLog.warning(this.getClass().getName() + ".getFacing(): blockstate for block at this position (" + getPos() + ") does not have property \"BlockConveyorFilter.FACING\"!");
+ PowerAdvantage.LOGGER.warn("{}.getFacing(): blockstate at {} does not have BlockConveyorFilter.FACING", this.getClass().getName(), getPos());
return EnumFacing.NORTH;
}
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityFoodFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java
similarity index 61%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityFoodFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java
index c9b66468..9c084af2 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityFoodFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFoodFilter.java
@@ -1,9 +1,11 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
import net.minecraft.item.ItemFood;
import net.minecraft.item.ItemStack;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityFoodFilter extends TileEntityConveyorFilter {
+ public TileEntityFoodFilter() { super(Entities.FILTER_FOOD, 1); }
@Override
public boolean matchesFilter(ItemStack item) {
@@ -11,4 +13,4 @@ public boolean matchesFilter(ItemStack item) {
return item.getItem() instanceof ItemFood;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityFuelFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java
similarity index 62%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityFuelFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java
index 631cd732..44732b76 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityFuelFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityFuelFilter.java
@@ -1,10 +1,12 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntityFurnace;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityFuelFilter extends TileEntityConveyorFilter {
+ public TileEntityFuelFilter() { super(Entities.FILTER_FUEL, 1); }
@Override
public boolean matchesFilter(ItemStack item) {
@@ -12,4 +14,4 @@ public boolean matchesFilter(ItemStack item) {
return TileEntityFurnace.isItemFuel(item);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java
similarity index 74%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java
index 358d016c..dd622709 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityInventoryFilter.java
@@ -1,6 +1,7 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
import net.minecraft.item.ItemStack;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityInventoryFilter extends TileEntityConveyorFilter {
@@ -8,7 +9,7 @@ public class TileEntityInventoryFilter extends TileEntityConveyorFilter {
public static final int NUM_FILTER_SLOTS = 9;
public TileEntityInventoryFilter() {
- super(NUM_FILTER_SLOTS + 1);
+ super(Entities.FILTER_INVENTORY, NUM_FILTER_SLOTS + 1);
}
@Override
@@ -22,4 +23,4 @@ public boolean matchesFilter(ItemStack item) {
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java
new file mode 100644
index 00000000..4a2bd89c
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOreFilter.java
@@ -0,0 +1,19 @@
+package zone.moddev.mc.poweradvantage.machines.conveyors;
+
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.common.Tags;
+import zone.moddev.mc.poweradvantage.init.Entities;
+
+public class TileEntityOreFilter extends TileEntityConveyorFilter {
+ public TileEntityOreFilter() { super(Entities.FILTER_ORE, 1); }
+
+ @Override
+ public boolean matchesFilter(ItemStack item) {
+ if (item == null || item.isEmpty()) return false;
+ return Tags.Items.INGOTS.contains(item.getItem())
+ || Tags.Items.ORES.contains(item.getItem())
+ || Tags.Items.DUSTS.contains(item.getItem())
+ || Tags.Items.NUGGETS.contains(item.getItem());
+ }
+
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java
similarity index 86%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java
index 4c99e1a1..6e5757ca 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityOverflowFilter.java
@@ -1,13 +1,15 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
-import com.mcmoddev.poweradvantage.util.InventoryWrapper;
+import zone.moddev.mc.poweradvantage.util.InventoryWrapper;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.ISidedInventory;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityOverflowFilter extends TileEntityConveyorFilter {
+ public TileEntityOverflowFilter() { super(Entities.FILTER_OVERFLOW, 1); }
@Override
public boolean matchesFilter(ItemStack item) {
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityPlantFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java
similarity index 77%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityPlantFilter.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java
index dace8978..1c9e3f60 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/TileEntityPlantFilter.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntityPlantFilter.java
@@ -1,12 +1,14 @@
-package com.mcmoddev.poweradvantage.machines.conveyors;
+package zone.moddev.mc.poweradvantage.machines.conveyors;
import net.minecraft.block.Block;
import net.minecraft.block.IGrowable;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemStack;
import net.minecraftforge.common.IPlantable;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class TileEntityPlantFilter extends TileEntityConveyorFilter {
+ public TileEntityPlantFilter() { super(Entities.FILTER_PLANT, 1); }
@Override
public boolean matchesFilter(ItemStack item) {
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java
new file mode 100644
index 00000000..01464de4
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/TileEntitySmeltableFilter.java
@@ -0,0 +1,20 @@
+package zone.moddev.mc.poweradvantage.machines.conveyors;
+
+import net.minecraft.inventory.InventoryBasic;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.text.TextComponentString;
+import net.minecraftforge.common.crafting.VanillaRecipeTypes;
+import zone.moddev.mc.poweradvantage.init.Entities;
+
+public class TileEntitySmeltableFilter extends TileEntityConveyorFilter {
+ public TileEntitySmeltableFilter() { super(Entities.FILTER_SMELT, 1); }
+
+ @Override
+ public boolean matchesFilter(ItemStack item) {
+ if (item == null || item.isEmpty() || world == null) return false;
+ InventoryBasic probe = new InventoryBasic(new TextComponentString("smelting probe"), 1);
+ probe.setInventorySlotContents(0, item.copy());
+ return world.getRecipeManager().getRecipe(probe, world, VanillaRecipeTypes.SMELTING) != null;
+ }
+
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/package-info.java
similarity index 75%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/package-info.java
index 276bf587..23e6fb8a 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/conveyors/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/conveyors/package-info.java
@@ -3,10 +3,10 @@
This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API.
If you are writing an add-on mod, use the code in this package for reference, but you should not
depend on it's implementation details as future updates might change them. For a better way to get
-PowerAdvantage content, look in the com.mcmoddev.poweradvantage.init package.
+PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package.
Because these classes are not part of the API, they are not well documented. They were only meant
for the eyes of the mod author(s).
*/
-package com.mcmoddev.poweradvantage.machines.conveyors;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.machines.conveyors;
\ No newline at end of file
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/creative/InfiniteEnergyBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java
similarity index 89%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/creative/InfiniteEnergyBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java
index cb22ece6..feea40dc 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/creative/InfiniteEnergyBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyBlock.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.machines.creative;
+package zone.moddev.mc.poweradvantage.machines.creative;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PoweredEntity;
@@ -7,6 +7,7 @@
import net.minecraft.block.state.IBlockState;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
+import net.minecraft.world.IBlockReader;
public class InfiniteEnergyBlock extends BlockSimplePowerMachine {
@@ -15,7 +16,7 @@ public InfiniteEnergyBlock(ConduitType energyType) {
}
@Override
- public PoweredEntity createNewTileEntity(World world, int metaDataValue) {
+ public PoweredEntity createNewTileEntity(IBlockReader world) {
return new InfiniteEnergyTileEntity(this.getTypes()[0]);
}
@@ -51,4 +52,4 @@ public boolean isPowerSink(ConduitType powerType) {
public boolean isPowerSource(ConduitType powerType) {
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java
similarity index 79%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java
index 11f7c0e0..89b48cab 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/creative/InfiniteEnergyTileEntity.java
@@ -1,9 +1,10 @@
-package com.mcmoddev.poweradvantage.machines.creative;
+package zone.moddev.mc.poweradvantage.machines.creative;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
+import zone.moddev.mc.poweradvantage.init.Entities;
public class InfiniteEnergyTileEntity extends TileEntitySimplePowerMachine {
@@ -12,12 +13,12 @@ public class InfiniteEnergyTileEntity extends TileEntitySimplePowerMachine {
private final ConduitType[] type = new ConduitType[1];
public InfiniteEnergyTileEntity(ConduitType energyType) {
- super(energyType, 1000f, InfiniteEnergyTileEntity.class.getName());
+ super(Entities.INFINITE_ENERGY, energyType, 1000f, "tile.poweradvantage.infinite_energy_source.name");
this.type[0] = energyType;
}
public InfiniteEnergyTileEntity() {
- super(new ConduitType("power"), 1000f, InfiniteEnergyTileEntity.class.getName());
+ super(Entities.INFINITE_ENERGY, new ConduitType("power"), 1000f, "tile.poweradvantage.infinite_energy_source.name");
this.type[0] = new ConduitType("power");
}
@@ -88,16 +89,16 @@ public void powerUpdate() {
}
@Override
- public NBTTagCompound writeToNBT(final NBTTagCompound tagRoot) {
- super.writeToNBT(tagRoot);
+ public NBTTagCompound write(final NBTTagCompound tagRoot) {
+ super .write(tagRoot);
tagRoot.setString("InfType", this.type[0].toString());
return tagRoot;
}
@Override
- public void readFromNBT(final NBTTagCompound tagRoot) {
- super.readFromNBT(tagRoot);
- if (tagRoot.hasKey("InfType")) {
+ public void read(final NBTTagCompound tagRoot) {
+ super.read(tagRoot);
+ if (tagRoot.contains("InfType", 8)) {
this.type[0] = new ConduitType(tagRoot.getString("InfType"));
}
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java
similarity index 57%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java
index ec062c55..c7b2dd7a 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeBlock.java
@@ -1,68 +1,43 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PoweredEntity;
import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine;
import cyano.poweradvantage.api.simple.TileEntitySimplePowerMachine;
-import com.mcmoddev.poweradvantage.init.ItemGroups;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.InventoryHelper;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
+import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-import java.util.Random;
public class FluidDischargeBlock extends BlockSimpleFluidMachine {
public FluidDischargeBlock() {
super(Material.PISTON, 3f);
- super.setCreativeTab(ItemGroups.tab_powerAdvantage);
}
@Override
- public PoweredEntity createNewTileEntity(World w, int m) {
+ public PoweredEntity createNewTileEntity(IBlockReader w) {
return new FluidDischargeTileEntity();
}
-
- /**
- * Override of default block behavior
- */
- @Override
- public Item getItemDropped(final IBlockState state, final Random prng, final int i3) {
- return Item.getItemFromBlock(this);
- }
-
/**
* Destroys the TileEntity associated with this block when this block
* breaks.
*/
@Override
- public void breakBlock(final World world, final BlockPos coord, final IBlockState bs) {
- final TileEntity tileEntity = world.getTileEntity(coord);
- if (tileEntity instanceof TileEntitySimplePowerMachine) {
- InventoryHelper.dropInventoryItems(world, coord, (IInventory) tileEntity);
- world.updateComparatorOutputLevel(coord, this);
+ public void onReplaced(IBlockState state, World world, BlockPos coord, IBlockState newState, boolean isMoving) {
+ if (state.getBlock() != newState.getBlock()) {
+ final TileEntity tileEntity = world.getTileEntity(coord);
+ if (tileEntity instanceof TileEntitySimplePowerMachine) {
+ InventoryHelper.dropInventoryItems(world, coord, (IInventory) tileEntity);
+ world.updateComparatorOutputLevel(coord, this);
+ }
}
- super.breakBlock(world, coord, bs);
- }
-
- /**
- * (Client-only) Override of default block behavior
- */
- @SideOnly(Side.CLIENT)
- @Override
- public ItemStack getPickBlock(IBlockState state, RayTraceResult target, World world, BlockPos pos, EntityPlayer player) {
- return new ItemStack(this);
+ super.onReplaced(state, world, coord, newState, isMoving);
}
@Override
@@ -101,4 +76,4 @@ public boolean isPowerSink(ConduitType powerType) {
public boolean isPowerSource(ConduitType powerType) {
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java
new file mode 100644
index 00000000..6184efd2
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDischargeTileEntity.java
@@ -0,0 +1,105 @@
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler;
+import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.network.NetworkManager;
+import net.minecraft.network.play.server.SPacketUpdateTileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper;
+
+/** Places stored fluid into the world or into a tank below. */
+public class FluidDischargeTileEntity extends TileEntitySimpleFluidMachine {
+ private int oldLevel = -1;
+
+ public FluidDischargeTileEntity() {
+ super(Entities.FLUID_DISCHARGE, FluidTank.BUCKET_VOLUME, "tile.poweradvantage.fluid_discharge.name");
+ }
+
+ @Override
+ public void powerUpdate() {
+ BlockPos below = getPos().down();
+ IAdvantageFluidHandler other = FluidHandlerHelper.getHandler(getWorld().getTileEntity(below), EnumFacing.UP);
+ if (getTank().getFluidAmount() > 0 && other != null) {
+ int accepted = other.fill(EnumFacing.UP, getTank().getFluid(), true);
+ if (accepted > 0) getTank().drain(accepted, true);
+ } else if (getTank().getFluidAmount() >= FluidTank.BUCKET_VOLUME) {
+ Fluid fluid = getTank().getFluid().getFluid();
+ BlockPos target = findPlacement(getWorld(), below, fluid, 32);
+ if (target != null) {
+ getWorld().setBlockState(target, fluid.getDefaultState().getBlockState());
+ getTank().drain(FluidTank.BUCKET_VOLUME, true);
+ }
+ }
+ if (oldLevel != getTank().getFluidAmount()) {
+ oldLevel = getTank().getFluidAmount();
+ sync();
+ }
+ super.powerUpdate();
+ }
+
+ private static BlockPos findPlacement(World world, BlockPos initial, Fluid fluid, int range) {
+ if (world.isAirBlock(initial)) return initial;
+ BlockPos position = initial;
+ EnumFacing[] directions = { EnumFacing.DOWN, EnumFacing.NORTH, EnumFacing.WEST, EnumFacing.SOUTH, EnumFacing.EAST };
+ while (range-- > 0) {
+ IFluidState state = world.getFluidState(position);
+ if (state.isEmpty()) return position;
+ java.util.List candidates = new java.util.ArrayList<>();
+ for (EnumFacing direction : directions) {
+ BlockPos candidate = position.offset(direction);
+ IFluidState candidateState = world.getFluidState(candidate);
+ if (world.isAirBlock(candidate) || (!candidateState.isEmpty()
+ && candidateState.getFluid().isEquivalentTo(fluid) && !candidateState.isSource())) {
+ candidates.add(candidate);
+ }
+ }
+ if (candidates.isEmpty()) return null;
+ position = candidates.get(candidates.size() == 1 ? 0 : world.rand.nextInt(candidates.size()));
+ }
+ return null;
+ }
+
+ @Override public void tickUpdate(boolean server) {}
+ public FluidStack getFluid() { return getTank().getFluid(); }
+ public int getFluidCapacity() { return getTank().getCapacity(); }
+ @Override public boolean canAccept(Fluid fluid) { return true; }
+ @Override protected ItemStack[] getInventory() { return new ItemStack[0]; }
+ public int getRedstoneOutput() { return getTank().getFluidAmount() * 15 / getTank().getCapacity(); }
+ @Override public boolean isPowerSink(ConduitType type) { return true; }
+ @Override public boolean isPowerSource(ConduitType type) { return false; }
+ @Override public boolean isFluidSource() { return false; }
+ @Override public boolean isFluidSink() { return true; }
+
+ @Override public NBTTagCompound getUpdateTag() {
+ NBTTagCompound tag = new NBTTagCompound();
+ getTank().writeToNBT(tag);
+ return tag;
+ }
+ @Override public void handleUpdateTag(NBTTagCompound tag) { getTank().readFromNBT(tag); }
+ @Override public SPacketUpdateTileEntity getUpdatePacket() {
+ return new SPacketUpdateTileEntity(getPos(), 0, getUpdateTag());
+ }
+ @Override public void onDataPacket(NetworkManager net, SPacketUpdateTileEntity packet) {
+ handleUpdateTag(packet.getNbtCompound());
+ }
+
+ public static BlockPos scanFluidSpaceForNonsourceBlock(World world, BlockPos initial, Fluid fluid, int range) {
+ return findPlacement(world, initial, fluid, range);
+ }
+ public static boolean isFluidBlock(net.minecraft.block.Block block, Fluid fluid) {
+ return FluidDrainTileEntity.isFluidBlock(block, fluid);
+ }
+ public static boolean isFluidSourceBlock(net.minecraft.block.Block block, Fluid fluid, World world, BlockPos pos) {
+ return FluidDrainTileEntity.isFluidSourceBlock(block, fluid, world, pos);
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDrainBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java
similarity index 61%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDrainBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java
index e10f63a1..18b1c818 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidDrainBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainBlock.java
@@ -1,55 +1,27 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PoweredEntity;
import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine;
-import com.mcmoddev.poweradvantage.init.ItemGroups;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.RayTraceResult;
+import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-import java.util.Random;
public class FluidDrainBlock extends BlockSimpleFluidMachine {
public FluidDrainBlock() {
super(Material.PISTON, 3f);
- super.setCreativeTab(ItemGroups.tab_powerAdvantage);
}
@Override
- public PoweredEntity createNewTileEntity(World w, int m) {
+ public PoweredEntity createNewTileEntity(IBlockReader w) {
return new FluidDrainTileEntity();
}
-
- /**
- * Override of default block behavior
- */
- @Override
- public Item getItemDropped(final IBlockState state, final Random prng, final int i3) {
- return Item.getItemFromBlock(this);
- }
-
-
- /**
- * (Client-only) Override of default block behavior
- */
- @SideOnly(Side.CLIENT)
- @Override
- public ItemStack getPickBlock(IBlockState state, RayTraceResult target, World world, BlockPos pos, EntityPlayer player) {
- return new ItemStack(this);
- }
-
@Override
public boolean hasComparatorInputOverride(IBlockState state) {
return true;
@@ -86,4 +58,4 @@ public boolean isPowerSink(ConduitType powerType) {
public boolean isPowerSource(ConduitType powerType) {
return true;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java
new file mode 100644
index 00000000..304a7e3a
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidDrainTileEntity.java
@@ -0,0 +1,110 @@
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import cyano.poweradvantage.api.fluid.FluidTankInfo;
+import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler;
+import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper;
+
+/** Pulls source blocks or adjacent Advantage tanks into the pipe network. */
+public class FluidDrainTileEntity extends TileEntitySimpleFluidMachine {
+ public FluidDrainTileEntity() {
+ super(Entities.FLUID_DRAIN, FluidTank.BUCKET_VOLUME, "tile.poweradvantage.fluid_drain.name");
+ }
+
+ @Override
+ public void powerUpdate() {
+ if (getTank().getFluidAmount() > 0) tryPushFluid(getPos().down(), EnumFacing.UP);
+
+ for (EnumFacing side : EnumFacing.values()) {
+ BlockPos adjacent = getPos().offset(side);
+ IAdvantageFluidHandler other = FluidHandlerHelper.getHandler(
+ getWorld().getTileEntity(adjacent), side.getOpposite());
+ if (other == null) continue;
+ for (FluidTankInfo info : other.getTankInfo(side.getOpposite())) {
+ FluidStack available = info.fluid;
+ if (available == null || (getTank().getFluid() != null
+ && !getTank().getFluid().getFluid().isEquivalentTo(available.getFluid()))) continue;
+ FluidStack drained = other.drain(side.getOpposite(), getTank().getCapacity() - getTank().getFluidAmount(), false);
+ if (drained == null) continue;
+ int accepted = getTank().fill(drained, false);
+ if (accepted > 0) {
+ FluidStack moved = other.drain(side.getOpposite(), accepted, true);
+ if (moved != null) getTank().fill(moved, true);
+ super.powerUpdate();
+ return;
+ }
+ }
+ }
+
+ if (getTank().getFluidAmount() == 0) {
+ BlockPos source = scanFluidSpaceForSourceBlock(getWorld(), getPos().up(), 32);
+ if (source != null) {
+ Fluid fluid = getWorld().getFluidState(source).getFluid();
+ getTank().fill(new FluidStack(fluid, FluidTank.BUCKET_VOLUME), true);
+ getWorld().setBlockState(source, Blocks.AIR.getDefaultState());
+ }
+ }
+ super.powerUpdate();
+ }
+
+ private void tryPushFluid(BlockPos position, EnumFacing face) {
+ IAdvantageFluidHandler handler = FluidHandlerHelper.getHandler(getWorld().getTileEntity(position), face);
+ if (handler == null || getTank().getFluid() == null) return;
+ int accepted = handler.fill(face, getTank().getFluid(), true);
+ if (accepted > 0) {
+ getTank().drain(accepted, true);
+ sync();
+ }
+ }
+
+ public FluidStack getFluid() { return getTank().getFluid(); }
+ public int getFluidCapacity() { return getTank().getCapacity(); }
+ public int getRedstoneOutput() { return getTank().getFluidAmount() * 15 / getTank().getCapacity(); }
+ @Override public boolean canAccept(Fluid fluid) { return true; }
+ @Override protected ItemStack[] getInventory() { return new ItemStack[0]; }
+ @Override public void tickUpdate(boolean server) {}
+ @Override public boolean isPowerSink(ConduitType type) { return false; }
+ @Override public boolean isPowerSource(ConduitType type) { return true; }
+ @Override public boolean isFluidSource() { return true; }
+ @Override public boolean isFluidSink() { return false; }
+
+ public static BlockPos scanFluidSpaceForSourceBlock(World world, BlockPos initial, int range) {
+ BlockPos position = initial;
+ EnumFacing[] directions = { EnumFacing.UP, EnumFacing.NORTH, EnumFacing.WEST, EnumFacing.SOUTH, EnumFacing.EAST };
+ while (range-- > 0) {
+ IFluidState state = world.getFluidState(position);
+ if (!state.isEmpty() && state.isSource()) return position;
+ java.util.List candidates = new java.util.ArrayList<>();
+ for (EnumFacing direction : directions) {
+ BlockPos candidate = position.offset(direction);
+ IFluidState candidateState = world.getFluidState(candidate);
+ if (!candidateState.isEmpty() && (state.isEmpty()
+ || candidateState.getFluid().isEquivalentTo(state.getFluid()))) candidates.add(candidate);
+ }
+ if (candidates.isEmpty()) return null;
+ position = candidates.get(candidates.size() == 1 ? 0 : world.rand.nextInt(candidates.size()));
+ }
+ return null;
+ }
+
+ public static boolean isFluidBlock(net.minecraft.block.Block block, Fluid fluid) {
+ return block.getDefaultState().getFluidState().getFluid().isEquivalentTo(fluid);
+ }
+
+ public static boolean isFluidSourceBlock(net.minecraft.block.Block block, Fluid fluid, World world, BlockPos pos) {
+ IFluidState state = world.getFluidState(pos);
+ return !state.isEmpty() && state.isSource() && state.getFluid().isEquivalentTo(fluid);
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidPipeBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java
similarity index 86%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidPipeBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java
index 4cb90c79..8401b595 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/FluidPipeBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/FluidPipeBlock.java
@@ -1,12 +1,11 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.simple.BlockSimpleFluidConduit;
-import com.mcmoddev.poweradvantage.init.Blocks;
-import com.mcmoddev.poweradvantage.util.FluidHandlerHelper;
+import zone.moddev.mc.poweradvantage.init.Blocks;
+import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
-import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
@@ -19,7 +18,6 @@ public class FluidPipeBlock extends BlockSimpleFluidConduit {
public FluidPipeBlock() {
super(Material.PISTON, 0.75f, 4f / 16f);
- super.setCreativeTab(CreativeTabs.DECORATIONS);
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/MetalTankBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java
similarity index 89%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/MetalTankBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java
index b69dfdf7..44a93d74 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/MetalTankBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankBlock.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PoweredEntity;
@@ -8,6 +8,7 @@
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
+import net.minecraft.world.IBlockReader;
public class MetalTankBlock extends BlockSimpleFluidMachine {
@@ -16,7 +17,7 @@ public MetalTankBlock() {
}
@Override
- public PoweredEntity createNewTileEntity(World world, int metaDataValue) {
+ public PoweredEntity createNewTileEntity(IBlockReader world) {
return new MetalTankTileEntity();
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java
similarity index 83%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java
index 7ca83b37..006ea61b 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/MetalTankTileEntity.java
@@ -1,18 +1,21 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PowerRequest;
import cyano.poweradvantage.api.fluid.FluidRequest;
import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine;
import net.minecraft.item.ItemStack;
-import net.minecraftforge.common.ForgeModContainer;
-import net.minecraftforge.fluids.*;
+import net.minecraft.fluid.Fluid;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.poweradvantage.util.FluidContainerHelper;
@SuppressWarnings("deprecation")
public class MetalTankTileEntity extends TileEntitySimpleFluidMachine {
public MetalTankTileEntity() {
- super(Fluid.BUCKET_VOLUME * 10, MetalTankTileEntity.class.getName());
+ super(Entities.METAL_TANK, FluidTank.BUCKET_VOLUME * 10, "tile.poweradvantage.fluid_metal_tank.name");
}
@@ -27,7 +30,7 @@ public MetalTankTileEntity() {
public FluidRequest getFluidRequest(Fluid offer) {
FluidStack filterFluid = getFilter();
if (filterFluid != null
- && !(FluidRegistry.getFluidName(offer).equalsIgnoreCase(FluidRegistry.getFluidName(filterFluid)))) {
+ && !offer.isEquivalentTo(filterFluid.getFluid())) {
// offer blocked by filter
return FluidRequest.REQUEST_NOTHING;
}
@@ -48,8 +51,9 @@ public FluidRequest getFluidRequest(Fluid offer) {
private FluidStack getFilter() {
ItemStack item = filterInventory[0];
- if (item == null) return null;
- return FluidUtil.getFluidContained(item);
+ if (item == null || item.isEmpty()) return null;
+ Fluid fluid = FluidContainerHelper.fluidForBucket(item.getItem());
+ return fluid == null ? null : new FluidStack(fluid, FluidTank.BUCKET_VOLUME);
}
/**
@@ -89,8 +93,7 @@ protected ItemStack[] getInventory() {
public boolean isItemValidForSlot(final int slot, final ItemStack item) {
if (this.getInventory() == null) return false;
if (slot >= this.getInventory().length) return false;
- if (item.getItem() == ForgeModContainer.getInstance().universalBucket) return true;
- return FluidUtil.getFluidContained(item) != null;
+ return item != null && !item.isEmpty() && FluidContainerHelper.fluidForBucket(item.getItem()) != null;
}
private Fluid lastFluid = null;
@@ -98,7 +101,7 @@ public boolean isItemValidForSlot(final int slot, final ItemStack item) {
@Override
public void tickUpdate(boolean isServerWorld) {
- if (isServerWorld && this.getWorld().getTotalWorldTime() % 11 == 0) {
+ if (isServerWorld && this.getWorld().getGameTime() % 11 == 0) {
// send update
FluidStack current = getTank().getFluid();
Fluid currentFluid = (current != null ? current.getFluid() : null);
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StillBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java
similarity index 70%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StillBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java
index 354e0127..9a8a8e87 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StillBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillBlock.java
@@ -1,17 +1,17 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.ConduitType;
import cyano.poweradvantage.api.PoweredEntity;
import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine;
import net.minecraft.block.material.Material;
-import net.minecraft.block.properties.IProperty;
-import net.minecraft.block.properties.PropertyBool;
-import net.minecraft.block.state.BlockStateContainer;
+import net.minecraft.state.BooleanProperty;
+import net.minecraft.state.StateContainer;
import net.minecraft.block.state.IBlockState;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.IBlockReader;
+import net.minecraft.world.IWorldReader;
import net.minecraft.world.World;
public class StillBlock extends BlockSimpleFluidMachine {
@@ -20,19 +20,20 @@ public class StillBlock extends BlockSimpleFluidMachine {
/**
* Blockstate property
*/
- public static final PropertyBool ACTIVE = PropertyBool.create("active");
+ public static final BooleanProperty ACTIVE = BooleanProperty.create("active");
public StillBlock() {
super(Material.PISTON, 3f);
- this.setDefaultState(getDefaultState().withProperty(ACTIVE, false));
+ this.setDefaultState(getDefaultState().with(ACTIVE, false));
}
/**
* Creates a blockstate
*/
@Override
- protected BlockStateContainer createBlockState() {
- return new BlockStateContainer(this, new IProperty[]{ACTIVE, FACING});
+ protected void fillStateContainer(StateContainer.Builder builder) {
+ super.fillStateContainer(builder);
+ builder.add(ACTIVE);
}
/**
@@ -44,8 +45,8 @@ public IBlockState getStateFromMeta(final int metaValue) {
if (enumFacing.getAxis() == EnumFacing.Axis.Y) {
enumFacing = EnumFacing.NORTH;
}
- return this.getDefaultState().withProperty(FACING, enumFacing)
- .withProperty(ACTIVE, (metaValue & 0x4) != 0);
+ return this.getDefaultState().with(FACING, enumFacing)
+ .with(ACTIVE, (metaValue & 0x4) != 0);
}
/**
@@ -54,12 +55,12 @@ public IBlockState getStateFromMeta(final int metaValue) {
@Override
public int getMetaFromState(final IBlockState bs) {
int extraBit;
- if ((Boolean) (bs.getValue(ACTIVE))) {
+ if ((Boolean) (bs .get(ACTIVE))) {
extraBit = 0x4;
} else {
extraBit = 0;
}
- return facingToMeta((EnumFacing) bs.getValue(FACING)) | extraBit;
+ return facingToMeta((EnumFacing) bs .get(FACING)) | extraBit;
}
private int facingToMeta(EnumFacing f) {
@@ -94,19 +95,12 @@ private EnumFacing metaToFacing(int i) {
}
@Override
- public int getLightValue(IBlockState bs, IBlockAccess world, BlockPos coord) {
- if (bs instanceof StillBlock) {
- if ((Boolean) bs.getValue(ACTIVE)) {
- return 12;
- } else {
- return 0;
- }
- }
- return 0;
+ public int getLightValue(IBlockState bs, IWorldReader world, BlockPos coord) {
+ return bs.get(ACTIVE) ? 12 : 0;
}
@Override
- public PoweredEntity createNewTileEntity(World world, int metaDataValue) {
+ public PoweredEntity createNewTileEntity(IBlockReader world) {
return new StillTileEntity();
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StillTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillTileEntity.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StillTileEntity.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillTileEntity.java
index 3833a1e8..1a0d6ea4 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StillTileEntity.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StillTileEntity.java
@@ -1,25 +1,29 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.fluid.FluidRequest;
import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine;
-import com.mcmoddev.poweradvantage.registry.FuelRegistry;
-import com.mcmoddev.poweradvantage.registry.still.recipe.DistillationRecipe;
-import com.mcmoddev.poweradvantage.registry.still.recipe.DistillationRecipeRegistry;
-import com.mcmoddev.poweradvantage.util.FluidIdHelper;
+import zone.moddev.mc.poweradvantage.registry.FuelRegistry;
+import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipe;
+import zone.moddev.mc.poweradvantage.registry.still.recipe.DistillationRecipeRegistry;
+import zone.moddev.mc.poweradvantage.util.FluidIdHelper;
import net.minecraft.block.state.IBlockState;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World;
-import net.minecraftforge.fluids.*;
+import net.minecraft.fluid.Fluid;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import cyano.poweradvantage.api.fluid.FluidTankInfo;
+import zone.moddev.mc.poweradvantage.init.Entities;
@SuppressWarnings("deprecation")
public class StillTileEntity extends TileEntitySimpleFluidMachine {
private final ItemStack[] inventory = new ItemStack[1];
- private final FluidTank inputTank = new FluidTank(Fluid.BUCKET_VOLUME);
+ private final FluidTank inputTank = new FluidTank(FluidTank.BUCKET_VOLUME);
private int[] dataFields = new int[6];
private static final int DATAFIELD_FLUID_ID1 = 0; // index in the dataFields array
private static final int DATAFIELD_FLUID_VOLUME1 = 1; // index in the dataFields array
@@ -37,7 +41,7 @@ public class StillTileEntity extends TileEntitySimpleFluidMachine {
public StillTileEntity() {
- super(Fluid.BUCKET_VOLUME, StillTileEntity.class.getName());
+ super(Entities.STILL, FluidTank.BUCKET_VOLUME, "tile.poweradvantage.still.name");
}
@Override
@@ -55,7 +59,7 @@ public void tickUpdate(boolean isServerWorld) {
distill();
}
if (burnTime <= 0) {
- getWorld().setBlockState(getPos(), getWorld().getBlockState(getPos()).withProperty(StillBlock.ACTIVE, false));
+ getWorld().setBlockState(getPos(), getWorld().getBlockState(getPos()).with(StillBlock.ACTIVE, false));
}
} else {
short fuel = getFuelBurnTime(inventory[0]);
@@ -63,7 +67,7 @@ public void tickUpdate(boolean isServerWorld) {
burnTime = fuel;
totalBurnTime = fuel;
decrementFuel();
- getWorld().setBlockState(getPos(), getWorld().getBlockState(getPos()).withProperty(StillBlock.ACTIVE, true));
+ getWorld().setBlockState(getPos(), getWorld().getBlockState(getPos()).with(StillBlock.ACTIVE, true));
}
}
}
@@ -243,8 +247,8 @@ public FluidTankInfo[] getTankInfo(EnumFacing face) {
* @param tagRoot An NBT tag
*/
@Override
- public NBTTagCompound writeToNBT(final NBTTagCompound tagRoot) {
- super.writeToNBT(tagRoot);
+ public NBTTagCompound write(final NBTTagCompound tagRoot) {
+ super .write(tagRoot);
NBTTagCompound tankTag2 = new NBTTagCompound();
inputTank.writeToNBT(tankTag2);
tagRoot.setTag("InputTank", tankTag2);
@@ -259,20 +263,20 @@ public NBTTagCompound writeToNBT(final NBTTagCompound tagRoot) {
* @param tagRoot An NBT tag
*/
@Override
- public void readFromNBT(final NBTTagCompound tagRoot) {
- super.readFromNBT(tagRoot);
- if (tagRoot.hasKey("InputTank")) {
- NBTTagCompound tankTag2 = tagRoot.getCompoundTag("InputTank");
+ public void read(final NBTTagCompound tagRoot) {
+ super.read(tagRoot);
+ if (tagRoot.contains("InputTank", 10)) {
+ NBTTagCompound tankTag2 = tagRoot .getCompound("InputTank");
inputTank.readFromNBT(tankTag2);
- if (tankTag2.hasKey("Empty")) {
+ if (tankTag2.contains("Empty", 1)) {
// empty the tank if NBT says its empty (not default behavior of Tank.readFromNBT(...) )
inputTank.setFluid(null);
}
}
- if (tagRoot.hasKey("burnTime")) {
+ if (tagRoot.contains("burnTime", 99)) {
this.burnTime = tagRoot.getShort("burnTime");
}
- if (tagRoot.hasKey("totalBurnTime")) {
+ if (tagRoot.contains("totalBurnTime", 99)) {
this.totalBurnTime = tagRoot.getShort("totalBurnTime");
}
}
@@ -330,7 +334,7 @@ public void onDataFieldUpdate() {
int fluidID2 = dataFields[DATAFIELD_FLUID_ID2];
int fluidVolume2 = dataFields[DATAFIELD_FLUID_VOLUME2];
if (fluidVolume1 <= 0) {
- inputTank.setFluid(new FluidStack(FluidRegistry.WATER, 0));
+ inputTank.setFluid(new FluidStack(net.minecraft.init.Fluids.WATER, 0));
} else {
Fluid fluid = FluidIdHelper.getFluid(fluidID1);
if (fluid != null) {
@@ -339,7 +343,7 @@ public void onDataFieldUpdate() {
}
}
if (fluidVolume2 <= 0) {
- getTank().setFluid(new FluidStack(FluidRegistry.WATER, 0));
+ getTank().setFluid(new FluidStack(net.minecraft.init.Fluids.WATER, 0));
} else {
Fluid fluid = FluidIdHelper.getFluid(fluidID2);
if (fluid != null) {
@@ -372,14 +376,14 @@ public void onDataFieldUpdate() {
*/
public void prepareDataFieldsForSync() {
if (inputTank.getFluid() == null || inputTank.getFluidAmount() <= 0) {
- dataFields[DATAFIELD_FLUID_ID1] = FluidIdHelper.getFluidId(FluidRegistry.WATER);
+ dataFields[DATAFIELD_FLUID_ID1] = FluidIdHelper.getFluidId(net.minecraft.init.Fluids.WATER);
dataFields[DATAFIELD_FLUID_VOLUME1] = 0;
} else {
dataFields[DATAFIELD_FLUID_ID1] = FluidIdHelper.getFluidId(inputTank.getFluid().getFluid());
dataFields[DATAFIELD_FLUID_VOLUME1] = inputTank.getFluidAmount();
}
if (getTank().getFluid() == null || getTank().getFluidAmount() <= 0) {
- dataFields[DATAFIELD_FLUID_ID2] = FluidIdHelper.getFluidId(FluidRegistry.WATER);
+ dataFields[DATAFIELD_FLUID_ID2] = FluidIdHelper.getFluidId(net.minecraft.init.Fluids.WATER);
dataFields[DATAFIELD_FLUID_VOLUME2] = 0;
} else {
dataFields[DATAFIELD_FLUID_ID2] = FluidIdHelper.getFluidId(getTank().getFluid().getFluid());
@@ -408,29 +412,29 @@ public NBTTagCompound createDataFieldUpdateTag() {
@Override
public void readDataFieldUpdateTag(NBTTagCompound tag) {
boolean usedFluidNbt = false;
- if (tag.hasKey("InputTank", 10)) {
- NBTTagCompound inputTankTag = tag.getCompoundTag("InputTank");
+ if (tag .contains("InputTank", 10)) {
+ NBTTagCompound inputTankTag = tag .getCompound("InputTank");
inputTank.readFromNBT(inputTankTag);
- if (inputTankTag.hasKey("Empty")) {
+ if (inputTankTag.contains("Empty", 1)) {
inputTank.setFluid(null);
}
usedFluidNbt = true;
}
- if (tag.hasKey("Tank", 10)) {
- NBTTagCompound outputTankTag = tag.getCompoundTag("Tank");
+ if (tag .contains("Tank", 10)) {
+ NBTTagCompound outputTankTag = tag .getCompound("Tank");
getTank().readFromNBT(outputTankTag);
- if (outputTankTag.hasKey("Empty")) {
+ if (outputTankTag.contains("Empty", 1)) {
getTank().setFluid(null);
}
usedFluidNbt = true;
}
- if (tag.hasKey("burnTime")) {
+ if (tag.contains("burnTime", 99)) {
this.burnTime = tag.getShort("burnTime");
}
- if (tag.hasKey("totalBurnTime")) {
+ if (tag.contains("totalBurnTime", 99)) {
this.totalBurnTime = tag.getShort("totalBurnTime");
}
- if (tag.hasKey("[]", 11)) {
+ if (tag .contains("[]", 11)) {
int[] newData = tag.getIntArray("[]");
System.arraycopy(newData, 0, this.getDataFieldArray(), 0, Math.min(newData.length, this.getDataFieldArray().length));
if (!usedFluidNbt) {
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java
new file mode 100644
index 00000000..4be50893
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankBlock.java
@@ -0,0 +1,92 @@
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.PoweredEntity;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import cyano.poweradvantage.api.simple.BlockSimpleFluidMachine;
+import net.minecraft.block.material.Material;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.client.util.ITooltipFlag;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.registry.IRegistry;
+import net.minecraft.util.text.ITextComponent;
+import net.minecraft.util.text.TextComponentString;
+import net.minecraft.util.text.TextComponentTranslation;
+import net.minecraft.world.IBlockReader;
+import net.minecraft.world.World;
+import zone.moddev.mc.poweradvantage.init.Fluids;
+
+/** Portable tank block. FluidID and Volume remain unchanged for old item stacks. */
+public class StorageTankBlock extends BlockSimpleFluidMachine {
+ public StorageTankBlock() { super(Material.PISTON, 3F); }
+
+ @Override public PoweredEntity createNewTileEntity(IBlockReader world) { return new StorageTankTileEntity(); }
+ @Override public boolean hasComparatorInputOverride(IBlockState state) { return true; }
+ @Override public int getComparatorInputOverride(IBlockState state, World world, BlockPos pos) {
+ TileEntity tile = world.getTileEntity(pos);
+ return tile instanceof StorageTankTileEntity ? ((StorageTankTileEntity) tile).getRedstoneOutput() : 0;
+ }
+ @Override public boolean isPowerSink(ConduitType type) { return true; }
+ @Override public boolean isPowerSource(ConduitType type) { return true; }
+
+ @Override
+ public void onBlockPlacedBy(World world, BlockPos pos, IBlockState state,
+ @Nullable EntityLivingBase placer, ItemStack stack) {
+ super.onBlockPlacedBy(world, pos, state, placer, stack);
+ NBTTagCompound tag = stack.getTag();
+ TileEntity tile = world.getTileEntity(pos);
+ if (tag != null && tag.contains("FluidID", 8) && tag.contains("Volume", 99)
+ && tile instanceof StorageTankTileEntity) {
+ net.minecraft.fluid.Fluid fluid = Fluids.byName(tag.getString("FluidID"));
+ if (fluid != null) ((StorageTankTileEntity) tile).getTank()
+ .setFluid(new FluidStack(fluid, tag.getInt("Volume")));
+ }
+ }
+
+ @Override
+ public void harvestBlock(World world, EntityPlayer player, BlockPos pos, IBlockState state,
+ @Nullable TileEntity tile, ItemStack tool) {
+ if (!world.isRemote && !world.restoringBlockSnapshots) {
+ spawnAsEntity(world, pos, tile instanceof StorageTankTileEntity
+ ? createItem((StorageTankTileEntity) tile) : new ItemStack(this));
+ }
+ }
+
+ public ItemStack createItem(StorageTankTileEntity tile) {
+ ItemStack stack = new ItemStack(this);
+ if (tile.getTank().getFluidAmount() > 0) {
+ NBTTagCompound tag = new NBTTagCompound();
+ net.minecraft.util.ResourceLocation id = IRegistry.field_212619_h
+ .getKey(tile.getTank().getFluid().getFluid());
+ if (id != null) tag.setString("FluidID", id.toString());
+ tag.setInt("Volume", tile.getTank().getFluidAmount());
+ stack.setTag(tag);
+ }
+ return stack;
+ }
+
+ @Override
+ public void addInformation(ItemStack stack, @Nullable IBlockReader world,
+ List tooltip, ITooltipFlag flag) {
+ NBTTagCompound tag = stack.getTag();
+ if (tag == null || !tag.contains("FluidID", 8) || !tag.contains("Volume", 99)) {
+ tooltip.add(new TextComponentTranslation("tooltip.poweradvantage.empty"));
+ return;
+ }
+ float buckets = tag.getInt("Volume") / (float) FluidTank.BUCKET_VOLUME;
+ tooltip.add(new TextComponentString(buckets + " buckets of " + tag.getString("FluidID")));
+ }
+
+ /** Retained for callers from the 1.12 implementation. */
+ public void displayInformation(ItemStack stack) {}
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java
similarity index 88%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java
index 986c1906..467e5cbc 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/StorageTankTileEntity.java
@@ -1,18 +1,20 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
import cyano.poweradvantage.api.PowerRequest;
import cyano.poweradvantage.api.fluid.FluidRequest;
import cyano.poweradvantage.api.simple.TileEntitySimpleFluidMachine;
import net.minecraft.item.ItemStack;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.FluidStack;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.fluid.Fluid;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import zone.moddev.mc.poweradvantage.init.Entities;
@SuppressWarnings("deprecation")
public class StorageTankTileEntity extends TileEntitySimpleFluidMachine {
public StorageTankTileEntity() {
- super(Fluid.BUCKET_VOLUME * 4, StorageTankTileEntity.class.getName());
+ super(Entities.STORAGE_TANK, FluidTank.BUCKET_VOLUME * 4, "tile.poweradvantage.fluid_storage_tank.name");
}
/**
@@ -78,7 +80,7 @@ protected ItemStack[] getInventory() {
@Override
public void tickUpdate(boolean isServerWorld) {
- if (isServerWorld && this.getWorld().getTotalWorldTime() % 11 == 0) {
+ if (isServerWorld && this.getWorld().getGameTime() % 11 == 0) {
// send update
FluidStack current = getTank().getFluid();
Fluid currentFluid = (current != null ? current.getFluid() : null);
diff --git a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java
similarity index 79%
rename from src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java
rename to src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java
index 88de8bb1..ae7ef633 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeBlock.java
@@ -1,9 +1,9 @@
-package com.mcmoddev.poweradvantage.machines.fluidmachines.modsupport;
+package zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport;
import cyano.poweradvantage.api.ConduitType;
-import com.mcmoddev.poweradvantage.init.Blocks;
-import com.mcmoddev.poweradvantage.machines.fluidmachines.FluidPipeBlock;
-import com.mcmoddev.poweradvantage.util.FluidHandlerHelper;
+import zone.moddev.mc.poweradvantage.init.Blocks;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.FluidPipeBlock;
+import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper;
import net.minecraft.block.Block;
import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.state.IBlockState;
@@ -12,14 +12,14 @@
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.IBlockReader;
import net.minecraft.world.World;
@SuppressWarnings("deprecation")
public class TerminalFluidPipeBlock extends FluidPipeBlock implements ITileEntityProvider {
@Override
- public TileEntity createNewTileEntity(World worldIn, int meta) {
+ public TileEntity createNewTileEntity(IBlockReader worldIn) {
return new TerminalFluidPipeTileEntity();
}
@@ -73,8 +73,9 @@ private void pipeCheck(World w, BlockPos pos) {
* method.
*/
@Override
- protected boolean canConnectTo(IBlockAccess w, BlockPos thisBlock, IBlockState bs, EnumFacing face, BlockPos otherBlock) {
- return super.canConnectTo(w, thisBlock, bs, face, otherBlock)
+ protected boolean canConnectTo(IBlockReader w, BlockPos thisBlock, IBlockState bs, EnumFacing face) {
+ BlockPos otherBlock = thisBlock.offset(face);
+ return super.canConnectTo(w, thisBlock, bs, face)
|| FluidHandlerHelper.hasHandler(w.getTileEntity(otherBlock), face.getOpposite());
}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java
new file mode 100644
index 00000000..80411777
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/modsupport/TerminalFluidPipeTileEntity.java
@@ -0,0 +1,112 @@
+package zone.moddev.mc.poweradvantage.machines.fluidmachines.modsupport;
+
+import java.util.List;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.PowerConnectorContext;
+import cyano.poweradvantage.api.PowerRequest;
+import cyano.poweradvantage.api.PoweredEntity;
+import cyano.poweradvantage.api.fluid.FluidRequest;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import zone.moddev.mc.poweradvantage.conduitnetwork.ConduitRegistry;
+import zone.moddev.mc.poweradvantage.init.Entities;
+import zone.moddev.mc.poweradvantage.init.Fluids;
+import zone.moddev.mc.poweradvantage.util.FluidHandlerHelper;
+
+/** Bridges adjacent Advantage fluid handlers onto the conduit request network. */
+public class TerminalFluidPipeTileEntity extends PoweredEntity {
+ private final EnumFacing[] faces = EnumFacing.values();
+ private final IAdvantageFluidHandler[] neighbors = new IAdvantageFluidHandler[faces.length];
+
+ public TerminalFluidPipeTileEntity() { super(Entities.PIPE_TERMINAL); }
+
+ @Override public void tickUpdate(boolean server) {
+ if (!server) return;
+ for (int index = 0; index < faces.length; index++) {
+ TileEntity tile = getWorld().getTileEntity(getPos().offset(faces[index]));
+ neighbors[index] = FluidHandlerHelper.getHandler(tile, faces[index].getOpposite());
+ }
+ }
+
+ @Override public boolean isPowerSink(ConduitType type) { return true; }
+ @Override public boolean isPowerSource(ConduitType type) { return true; }
+ @Override public boolean canAcceptConnection(PowerConnectorContext connection) {
+ return ConduitType.areSameType(connection.powerType, Fluids.fluidConduit_general)
+ || Fluids.conduitTypeToFluid(connection.powerType) != null;
+ }
+ @Override public ConduitType[] getTypes() { return new ConduitType[] { Fluids.fluidConduit_general }; }
+ @Override public float getEnergyCapacity(ConduitType type) { return 0; }
+
+ @Override public float getEnergy(ConduitType type) {
+ int total = 0;
+ for (int index = 0; index < neighbors.length; index++) {
+ IAdvantageFluidHandler neighbor = neighbors[index];
+ if (neighbor == null) continue;
+ FluidStack available = neighbor.drain(faces[index].getOpposite(), Integer.MAX_VALUE, false);
+ if (available != null) total += available.amount;
+ }
+ return total;
+ }
+
+ @Override public void powerUpdate() {
+ for (int index = 0; index < neighbors.length; index++) {
+ IAdvantageFluidHandler neighbor = neighbors[index];
+ if (neighbor == null) continue;
+ EnumFacing face = faces[index].getOpposite();
+ FluidStack available = neighbor.drain(face, Integer.MAX_VALUE, false);
+ if (available == null || available.amount <= 0) continue;
+ int moved = transmitFluidToConsumers(available, PowerRequest.LOW_PRIORITY);
+ if (moved > 0) neighbor.drain(face, moved, true);
+ }
+ }
+
+ protected int transmitFluidToConsumers(FluidStack available, byte minimumPriority) {
+ ConduitType type = Fluids.fluidToConduitType(available.getFluid());
+ List requests = ConduitRegistry.getInstance()
+ .getRequestsForPower(getWorld(), getPos(), Fluids.fluidConduit_general, type);
+ float remaining = available.amount;
+ for (PowerRequest request : requests) {
+ if (request.entity == this || request.amount <= 0) continue;
+ if (request.priority < minimumPriority) break;
+ float offered = Math.min(remaining, request.amount);
+ float accepted = Math.max(0, Math.min(offered, request.entity.addEnergy(offered, type)));
+ remaining -= accepted;
+ if (remaining <= 0) break;
+ }
+ return available.amount - (int) remaining;
+ }
+
+ @Override public void setEnergy(float energy, ConduitType type) {}
+
+ @Override public float addEnergy(float energy, ConduitType type) {
+ Fluid fluid = Fluids.conduitTypeToFluid(type);
+ if (fluid == null || energy <= 0) return 0;
+ int accepted = 0;
+ int offered = (int) energy;
+ for (int index = 0; index < neighbors.length && accepted < offered; index++) {
+ IAdvantageFluidHandler neighbor = neighbors[index];
+ if (neighbor == null) continue;
+ accepted += neighbor.fill(faces[index].getOpposite(), new FluidStack(fluid, offered - accepted), true);
+ }
+ return accepted;
+ }
+
+ @Override public float subtractEnergy(float energy, ConduitType type) { return 0; }
+
+ @Override public PowerRequest getPowerRequest(ConduitType type) {
+ Fluid fluid = Fluids.conduitTypeToFluid(type);
+ if (fluid == null) return PowerRequest.REQUEST_NOTHING;
+ int demand = 0;
+ FluidStack offer = new FluidStack(fluid, Integer.MAX_VALUE);
+ for (int index = 0; index < neighbors.length; index++) {
+ IAdvantageFluidHandler neighbor = neighbors[index];
+ if (neighbor != null) demand += neighbor.fill(faces[index].getOpposite(), offer, false);
+ }
+ return demand <= 0 ? PowerRequest.REQUEST_NOTHING
+ : new FluidRequest((byte) (PowerRequest.LOW_PRIORITY - 1), demand, this);
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java
new file mode 100644
index 00000000..2df28e68
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/machines/fluidmachines/package-info.java
@@ -0,0 +1,12 @@
+/**
+
+This package contains content that is added by PowerAdvantage implementing the PowerAdvantage API.
+If you are writing an add-on mod, use the code in this package for reference, but you should not
+depend on its implementation details as future updates might change them. For a better way to get
+PowerAdvantage content, look in the zone.moddev.mc.poweradvantage.init package.
+
+Because these classes are not part of the API, they are not well documented. They were only meant
+for the eyes of the mod author(s).
+
+*/
+package zone.moddev.mc.poweradvantage.machines.fluidmachines;
diff --git a/src/main/java/com/mcmoddev/poweradvantage/math/BlockPos4D.java b/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java
similarity index 97%
rename from src/main/java/com/mcmoddev/poweradvantage/math/BlockPos4D.java
rename to src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java
index 299726cc..9e63f0c9 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/math/BlockPos4D.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/math/BlockPos4D.java
@@ -1,6 +1,6 @@
-package com.mcmoddev.poweradvantage.math;
+package zone.moddev.mc.poweradvantage.math;
-import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.INBTBase;
import net.minecraft.nbt.NBTTagIntArray;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.math.BlockPos;
@@ -271,7 +271,7 @@ public BlockPos4D[] getNeighbors() {
return n;
}
- public NBTBase toNBT() {
+ public INBTBase toNBT() {
int[] data = new int[4];
data[0] = dimension;
data[1] = pos.getX();
@@ -280,7 +280,7 @@ public NBTBase toNBT() {
return new NBTTagIntArray(data);
}
- public static BlockPos4D fromNBT(NBTBase tag) {
+ public static BlockPos4D fromNBT(INBTBase tag) {
int[] data = ((NBTTagIntArray) tag).getIntArray();
return new BlockPos4D(data[0], data[1], data[2], data[3]);
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/math/Integer2D.java b/src/main/java/zone/moddev/mc/poweradvantage/math/Integer2D.java
similarity index 95%
rename from src/main/java/com/mcmoddev/poweradvantage/math/Integer2D.java
rename to src/main/java/zone/moddev/mc/poweradvantage/math/Integer2D.java
index 3fa2db7d..987a8d9f 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/math/Integer2D.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/math/Integer2D.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.math;
+package zone.moddev.mc.poweradvantage.math;
/**
* This class is used to store 2D integer coordinates
diff --git a/src/main/java/com/mcmoddev/poweradvantage/util/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/math/package-info.java
similarity index 55%
rename from src/main/java/com/mcmoddev/poweradvantage/util/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/math/package-info.java
index bb17f239..72c1ee2a 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/util/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/math/package-info.java
@@ -3,4 +3,4 @@
This package contains helper classes.
*/
-package com.mcmoddev.poweradvantage.util;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.math;
\ No newline at end of file
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java b/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java
new file mode 100644
index 00000000..9d14b677
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrations.java
@@ -0,0 +1,92 @@
+package zone.moddev.mc.poweradvantage.migration;
+
+import cyano.poweradvantage.api.migration.LegacyMigrationApi;
+import cyano.poweradvantage.api.simple.BlockSimplePowerMachine;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockFlowingFluid;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.util.ResourceLocation;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.blocks.BlockPowerSwitch;
+import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyor;
+import zone.moddev.mc.poweradvantage.machines.conveyors.BlockConveyorFilter;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.StillBlock;
+
+/** Registers PowerAdvantage's 1.10 and 1.12 identities with the shared hook. */
+public final class AdvantageLegacyMigrations {
+ private static boolean registered;
+
+ private AdvantageLegacyMigrations() {}
+
+ public static synchronized void registerPowerAdvantage() {
+ if (registered) return;
+ LegacyMigrationApi.registerNamespace(PowerAdvantage.MODID);
+
+ for (String path : new String[] {
+ "fluid_drain", "fluid_discharge", "fluid_storage_tank", "fluid_metal_tank", "still",
+ "fluid_pipe", "fluid_pipe_terminal", "steel_frame",
+ "item_conveyor", "item_filter_block", "item_filter_food", "item_filter_fuel",
+ "item_filter_inventory", "item_filter_ore", "item_filter_plant", "item_filter_smelt",
+ "item_filter_overflow", "fluid_switch", "infinite_steam", "infinite_electricity",
+ "infinite_quantum", "crude_oil", "refined_oil",
+ "converter_rf_steam", "converter_rf_electricity", "converter_rf_quantum",
+ "converter_tr_electricity" }) {
+ LegacyMigrationApi.registerBlockStateCodec(id(path), AdvantageLegacyMigrations::decodeState);
+ }
+
+ registerTile("tileentity.pipe_terminal");
+ registerTile("tileentity.fluid_drain");
+ registerTile("tileentity.fluid_discharge");
+ registerTile("tileentity.fluid_storage_tank");
+ registerTile("tileentity.fluid_metal_tank");
+ registerTile("tileentity.still");
+ registerTile("item_conveyor");
+ registerTile("item_filter_block");
+ registerTile("item_filter_food");
+ registerTile("item_filter_fuel");
+ registerTile("item_filter_inventory");
+ registerTile("item_filter_ore");
+ registerTile("item_filter_plant");
+ registerTile("item_filter_smelt");
+ registerTile("item_filter_overflow");
+ registerTile("infinite_energy_source");
+ registerTile("rf_steam_converter_tileentity");
+ registerTile("rf_electricity_converter_tileentity");
+ registerTile("rf_quantum_converter_tileentity");
+ registerTile("tr_electricity_converter_tileentity");
+
+ LegacyMigrationApi.registerFluidAlias("crude_oil", "poweradvantage:crude_oil");
+ LegacyMigrationApi.registerFluidAlias("refined_oil", "poweradvantage:refined_oil");
+ LegacyMigrationApi.registerFluidAlias("mineralogy_crude_oil", "mineralogy:crude_oil");
+ LegacyMigrationApi.registerLegacyFluidBucket("crude_oil", id("crude_oil_bucket"));
+ LegacyMigrationApi.registerLegacyFluidBucket("poweradvantage:crude_oil", id("crude_oil_bucket"));
+ LegacyMigrationApi.registerLegacyFluidBucket("refined_oil", id("refined_oil_bucket"));
+ LegacyMigrationApi.registerLegacyFluidBucket("poweradvantage:refined_oil", id("refined_oil_bucket"));
+ registered = true;
+ }
+
+ private static void registerTile(String path) {
+ String current = id(path).toString();
+ LegacyMigrationApi.registerTileEntityAlias(current, current);
+ LegacyMigrationApi.registerTileEntityAlias(PowerAdvantage.MODID + "." + path, current);
+ LegacyMigrationApi.registerTileEntityAlias("minecraft:" + PowerAdvantage.MODID + "." + path, current);
+ }
+
+ private static IBlockState decodeState(Block block, int metadata) {
+ if (block instanceof StillBlock) return ((StillBlock) block).getStateFromMeta(metadata);
+ if (block instanceof BlockConveyor) return ((BlockConveyor) block).getStateFromMeta(metadata);
+ if (block instanceof BlockConveyorFilter) return ((BlockConveyorFilter) block).getStateFromMeta(metadata);
+ if (block instanceof BlockPowerSwitch) return ((BlockPowerSwitch) block).getStateFromMeta(metadata);
+ if (block instanceof BlockSimplePowerMachine) {
+ return ((BlockSimplePowerMachine) block).getStateFromMeta(metadata);
+ }
+ if (block instanceof BlockFlowingFluid) {
+ return block.getDefaultState().with(BlockFlowingFluid.LEVEL, Integer.valueOf(metadata & 15));
+ }
+ return block.getDefaultState();
+ }
+
+ private static ResourceLocation id(String path) {
+ return new ResourceLocation(PowerAdvantage.MODID, path);
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java b/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java
new file mode 100644
index 00000000..a8fc5041
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook.java
@@ -0,0 +1,538 @@
+package zone.moddev.mc.poweradvantage.migration;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.mojang.datafixers.Dynamic;
+import com.mojang.datafixers.DataFixer;
+import com.mojang.datafixers.DataFixUtils;
+
+import cpw.mods.modlauncher.api.INameMappingService;
+import cyano.poweradvantage.api.migration.LegacyMigrationApi;
+import net.minecraft.block.Block;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.nbt.CompressedStreamTools;
+import net.minecraft.nbt.INBTBase;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTUtil;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.datafix.fixes.BlockStateFlatteningMap;
+import net.minecraft.util.datafix.DataFixesManager;
+import net.minecraft.util.datafix.TypeReferences;
+import net.minecraft.world.storage.SaveHandler;
+import net.minecraft.world.storage.WorldInfo;
+import net.minecraftforge.fml.WorldPersistenceHooks;
+import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
+import net.minecraftforge.registries.ForgeRegistries;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+
+/**
+ * Shared Advantage pre-flattening migration hook for Minecraft 1.10 and 1.12
+ * worlds. Add-ons contribute their identities through {@link LegacyMigrationApi}.
+ */
+public final class LegacyWorldDataHook implements WorldPersistenceHooks.WorldPersistenceHook {
+ private static final LegacyWorldDataHook INSTANCE = new LegacyWorldDataHook();
+ private static final ResourceLocation BLOCK_REGISTRY = new ResourceLocation("minecraft", "blocks");
+ private static final Map LEGACY_WORLD_DATA = new ConcurrentHashMap<>();
+ private static final BitSet LEGACY_BLOCK_IDS = new BitSet();
+ private static final Map LEGACY_ENCHANTMENTS = new LinkedHashMap<>();
+ private static final Set LEGACY_CHUNKS = Collections.newSetFromMap(new ConcurrentHashMap<>());
+ private static final Map LEGACY_BLOCK_COUNTS = new ConcurrentHashMap<>();
+ private static final Map VANILLA_BLOCK_ENTITY_IDS = vanillaBlockEntityIds();
+ private static final String PRESERVE_CHUNK_MARKER = "AdvantageLegacyPreserveChunk";
+ private static final int LAST_PRE_FLATTENING_VERSION = 1343;
+ private static boolean legacyWorldActive;
+ private static boolean registered;
+
+ private LegacyWorldDataHook() {}
+
+ public static synchronized void register() {
+ if (!registered) {
+ WorldPersistenceHooks.addHook(INSTANCE);
+ registered = true;
+ }
+ }
+
+ /** Called by the coremod before Forge reads level.dat. */
+ public static synchronized void prepareLegacyWorld(File levelDat) {
+ AdvantageLegacyMigrations.registerPowerAdvantage();
+ legacyWorldActive = false;
+ LEGACY_ENCHANTMENTS.clear();
+ LEGACY_CHUNKS.clear();
+ LEGACY_BLOCK_COUNTS.clear();
+ File source = legacyRegistrySource(levelDat);
+ if (source == null) return;
+ try (FileInputStream input = new FileInputStream(source)) {
+ NBTTagCompound root = CompressedStreamTools.readCompressed(input);
+ if (!root.contains("FML", 10)) return;
+ prepareLegacyData(levelDat.getParentFile(), root.getCompound("FML"));
+ migrateLoosePlayerData(levelDat, root);
+ } catch (IOException exception) {
+ PowerAdvantage.LOGGER.warn("Could not inspect '{}' for legacy Advantage registry data", source, exception);
+ }
+ }
+
+ /** Called by the coremod immediately after a legacy chunk NBT is read. */
+ public static synchronized void prepareLegacyChunk(NBTTagCompound root) {
+ if (root == null) return;
+ migrateLegacyNbt(root);
+ migrateLegacyEntities(root);
+ if (!legacyWorldActive || !root.contains("Level", 10)) return;
+ NBTTagCompound level = root.getCompound("Level");
+ int legacyBlocks = countLegacyBlocks(level);
+ if (legacyBlocks > 0) {
+ LEGACY_BLOCK_COUNTS.put(Long.valueOf(chunkKey(level.getInt("xPos"), level.getInt("zPos"))),
+ Integer.valueOf(legacyBlocks));
+ }
+ level.setBoolean("TerrainPopulated", true);
+ level.setBoolean("LightPopulated", true);
+ level.setBoolean(PRESERVE_CHUNK_MARKER, true);
+ }
+
+ private static void migrateLegacyEntities(NBTTagCompound root) {
+ int version = root.contains("DataVersion", 99) ? root.getInt("DataVersion") : -1;
+ if (version >= LAST_PRE_FLATTENING_VERSION || !root.contains("Level", 10)) return;
+ DataFixer fixer = DataFixesManager.getDataFixer();
+ Set> vanillaIds = fixer.getSchema(DataFixUtils.makeKey(version))
+ .findChoiceType(TypeReferences.ENTITY).types().keySet();
+ NBTTagList entities = root.getCompound("Level").getList("Entities", 10);
+ for (int index = 0; index < entities.size(); index++) {
+ entities.set(index, migrateVanillaEntity(entities.getCompound(index), version, fixer, vanillaIds));
+ }
+ }
+
+ private static NBTTagCompound migrateVanillaEntity(NBTTagCompound entity, int version,
+ DataFixer fixer, Set> vanillaIds) {
+ boolean hasPassengers = entity.contains("Passengers", 9);
+ NBTTagList passengers = entity.getList("Passengers", 10);
+ // An unknown mod choice can stop a mixed chunk's early entity-ID fixes.
+ // Fix each recognised vanilla entity independently, without its passengers.
+ if (vanillaIds.contains(entity.getString("id"))) {
+ NBTTagCompound isolated = entity.copy();
+ isolated.removeTag("Passengers");
+ entity = NBTUtil.update(fixer, TypeReferences.ENTITY_TYPE, isolated,
+ version, LAST_PRE_FLATTENING_VERSION);
+ }
+ for (int index = 0; index < passengers.size(); index++) {
+ passengers.set(index, migrateVanillaEntity(passengers.getCompound(index), version, fixer, vanillaIds));
+ }
+ if (hasPassengers) entity.setTag("Passengers", passengers);
+ return entity;
+ }
+
+ /** Called after vanilla datafixing, before a legacy chunk is returned. */
+ public static NBTTagCompound finalizeLegacyChunk(NBTTagCompound root) {
+ if (root != null && root.contains("Level", 10)) {
+ NBTTagCompound level = root.getCompound("Level");
+ if (level.getBoolean(PRESERVE_CHUNK_MARKER)) {
+ level.setString("Status", "postprocessed");
+ level.removeTag(PRESERVE_CHUNK_MARKER);
+ }
+ }
+ return root;
+ }
+
+ public static boolean shouldBlockWorldgenWrite(net.minecraft.util.math.BlockPos position) {
+ return legacyWorldActive && position != null
+ && LEGACY_CHUNKS.contains(Long.valueOf(chunkKey(position.getX() >> 4, position.getZ() >> 4)));
+ }
+
+ @Override public String getModId() { return "FML"; }
+
+ @Override
+ public NBTTagCompound getDataForWriting(SaveHandler handler, WorldInfo info) {
+ NBTTagCompound legacy = LEGACY_WORLD_DATA.get(worldKey(handler.getWorldDirectory()));
+ return legacy == null ? new NBTTagCompound() : legacy.copy();
+ }
+
+ @Override
+ public void readData(SaveHandler handler, WorldInfo info, NBTTagCompound tag) {
+ prepareLegacyData(handler.getWorldDirectory(), tag);
+ }
+
+ private static synchronized void prepareLegacyData(File worldDirectory, NBTTagCompound tag) {
+ if (!tag.contains("Registries", 10)) return;
+ NBTTagCompound registries = tag.getCompound("Registries");
+ if (!registries.contains(BLOCK_REGISTRY.toString(), 10)) return;
+ installLegacyEnchantments(registries);
+ String key = worldKey(worldDirectory);
+ boolean first = !LEGACY_WORLD_DATA.containsKey(key);
+ int states = installLegacyBlockStates(registries.getCompound(BLOCK_REGISTRY.toString()));
+ if (first) LEGACY_WORLD_DATA.put(key, tag.copy());
+ legacyWorldActive = states > 0;
+ int chunks = indexLegacyChunks(worldDirectory);
+ if (first && states > 0) {
+ PowerAdvantage.LOGGER.info("Prepared {} legacy Advantage states and protected {} existing chunks in '{}'",
+ Integer.valueOf(states), Integer.valueOf(chunks), worldDirectory);
+ writeMigrationReport(worldDirectory, states, chunks);
+ }
+ }
+
+ private static int installLegacyBlockStates(NBTTagCompound snapshot) {
+ LEGACY_BLOCK_IDS.clear();
+ Map ids = new LinkedHashMap<>();
+ NBTTagList savedIds = snapshot.getList("ids", 10);
+ int highestState = 0;
+ for (int index = 0; index < savedIds.size(); index++) {
+ NBTTagCompound entry = savedIds.getCompound(index);
+ ResourceLocation id = safeId(normalizeLegacyRegistryName(entry.getString("K")));
+ if (id == null || !LegacyMigrationApi.handlesNamespace(id.getNamespace())) continue;
+ int numeric = entry.getInt("V");
+ ids.put(id, Integer.valueOf(numeric));
+ LEGACY_BLOCK_IDS.set(numeric);
+ highestState = Math.max(highestState, (numeric << 4) | 15);
+ }
+ if (ids.isEmpty()) return 0;
+
+ Dynamic>[] table = expandFlatteningTable(highestState + 1);
+ int mapped = 0;
+ for (Map.Entry entry : ids.entrySet()) {
+ ResourceLocation target = LegacyMigrationApi.blockTarget(entry.getKey());
+ Block block = ForgeRegistries.BLOCKS.getValue(target);
+ if (block == null) {
+ throw new IllegalStateException("No Advantage replacement for legacy block " + entry.getKey()
+ + " (expected " + target + ")");
+ }
+ for (int metadata = 0; metadata < 16; metadata++) {
+ IBlockState state = LegacyMigrationApi.decodeBlockState(entry.getKey(), block, metadata);
+ table[(entry.getValue().intValue() << 4) | metadata] =
+ BlockStateFlatteningMap.makeDynamic(NBTUtil.writeBlockState(state).toString());
+ mapped++;
+ }
+ }
+ return mapped;
+ }
+
+ private static void installLegacyEnchantments(NBTTagCompound registries) {
+ LEGACY_ENCHANTMENTS.clear();
+ NBTTagList entries = registries.getCompound("minecraft:enchantments").getList("ids", 10);
+ for (int index = 0; index < entries.size(); index++) {
+ NBTTagCompound entry = entries.getCompound(index);
+ ResourceLocation name = safeId(entry.getString("K"));
+ if (name != null) LEGACY_ENCHANTMENTS.put(Integer.valueOf(entry.getInt("V")), name.toString());
+ }
+ }
+
+ /** Resolve saved IDs, not today's registry order or vanilla's conflicting numeric IDs. */
+ static boolean migrateLegacyEnchantments(NBTTagCompound compound, Map savedIds) {
+ boolean changed = migrateEnchantmentList(compound, "StoredEnchantments", savedIds);
+ changed |= migrateEnchantmentList(compound, "Enchantments", savedIds);
+ if (compound.contains("ench", 9)) {
+ NBTTagList enchantments = compound.getList("ench", 10);
+ if (!enchantments.isEmpty() && !savedIds.isEmpty()) {
+ // A partial conversion would leave an unreadable mixed old/new list.
+ for (int index = 0; index < enchantments.size(); index++) {
+ NBTTagCompound enchantment = enchantments.getCompound(index);
+ if (enchantment.contains("id", 99)
+ && !savedIds.containsKey(Integer.valueOf(enchantment.getInt("id")))) {
+ throw new IllegalStateException("Missing saved enchantment mapping for " + enchantment.getInt("id"));
+ }
+ }
+ migrateEnchantmentList(compound, "ench", savedIds);
+ compound.setTag("Enchantments", enchantments);
+ compound.removeTag("ench");
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ private static boolean migrateEnchantmentList(NBTTagCompound compound, String key, Map savedIds) {
+ boolean changed = false;
+ NBTTagList list = compound.getList(key, 10);
+ for (int index = 0; index < list.size(); index++) {
+ NBTTagCompound enchantment = list.getCompound(index);
+ if (!enchantment.contains("id", 99)) continue;
+ String name = savedIds.get(Integer.valueOf(enchantment.getInt("id")));
+ if (name != null) {
+ enchantment.setString("id", name);
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ private static void migrateLoosePlayerData(File levelDat, NBTTagCompound root) throws IOException {
+ boolean changed = migrateLegacyNbt(root) > 0;
+ if (changed && levelDat.isFile()) writeWithBackup(levelDat, root);
+ File playerData = new File(levelDat.getParentFile(), "playerdata");
+ File[] files = playerData.listFiles((directory, name) -> name.endsWith(".dat"));
+ if (files == null) return;
+ Arrays.sort(files);
+ for (File file : files) {
+ try (FileInputStream input = new FileInputStream(file)) {
+ NBTTagCompound player = CompressedStreamTools.readCompressed(input);
+ if (migrateLegacyNbt(player) > 0) writeWithBackup(file, player);
+ }
+ }
+ }
+
+ private static int migrateLegacyNbt(INBTBase value) {
+ int changed = 0;
+ if (value instanceof NBTTagCompound) {
+ NBTTagCompound compound = (NBTTagCompound) value;
+ String id = compound.getString("id");
+ String vanillaBlockEntity = VANILLA_BLOCK_ENTITY_IDS.get(id);
+ if (vanillaBlockEntity != null) {
+ compound.setString("id", vanillaBlockEntity);
+ id = vanillaBlockEntity;
+ changed++;
+ }
+
+ String tileTarget = LegacyMigrationApi.tileTarget(id);
+ String entityTarget = LegacyMigrationApi.entityTarget(tileTarget);
+ if (!entityTarget.equals(normalizeIdentity(id))) {
+ compound.setString("id", entityTarget);
+ id = entityTarget;
+ changed++;
+ }
+
+ if ("forge:bucketfilled".equalsIgnoreCase(id) && compound.contains("tag", 10)) {
+ NBTTagCompound tag = compound.getCompound("tag");
+ ResourceLocation bucket = LegacyMigrationApi.bucketTarget(tag.getString("FluidName"));
+ if (bucket != null) {
+ compound.setString("id", bucket.toString());
+ tag.removeTag("FluidName");
+ tag.removeTag("Amount");
+ if (tag.isEmpty()) compound.removeTag("tag");
+ changed++;
+ }
+ } else if (compound.contains("Count", 99)) {
+ ResourceLocation oldId = safeId(id);
+ if (oldId != null && LegacyMigrationApi.handlesNamespace(oldId.getNamespace())) {
+ ResourceLocation target = LegacyMigrationApi.itemTarget(oldId);
+ if (!target.equals(oldId)) {
+ compound.setString("id", target.toString());
+ changed++;
+ }
+ if (LegacyMigrationApi.isDurableItem(oldId)
+ && compound.contains("Damage", 99) && compound.getInt("Damage") > 0) {
+ NBTTagCompound tag = compound.contains("tag", 10)
+ ? compound.getCompound("tag") : new NBTTagCompound();
+ if (!tag.contains("Damage", 99)) {
+ tag.setInt("Damage", compound.getInt("Damage"));
+ compound.setTag("tag", tag);
+ changed++;
+ }
+ }
+ }
+ }
+
+ changed += migrateFluidName(compound, "FluidName");
+ changed += migrateFluidName(compound, "FluidID");
+ if (migrateLegacyEnchantments(compound, LEGACY_ENCHANTMENTS)) changed++;
+ if (LegacyMigrationApi.migrateRegisteredNbt(compound)) changed++;
+ for (String key : new ArrayList<>(compound.keySet())) {
+ INBTBase child = compound.getTag(key);
+ if (child != null) changed += migrateLegacyNbt(child);
+ }
+ } else if (value instanceof NBTTagList) {
+ NBTTagList list = (NBTTagList) value;
+ for (int index = 0; index < list.size(); index++) changed += migrateLegacyNbt(list.get(index));
+ }
+ return changed;
+ }
+
+ private static int migrateFluidName(NBTTagCompound compound, String key) {
+ if (!compound.contains(key, 8)) return 0;
+ String oldName = compound.getString(key);
+ String target = LegacyMigrationApi.fluidTarget(oldName);
+ if (target.equals(normalizeIdentity(oldName))) return 0;
+ compound.setString(key, target);
+ return 1;
+ }
+
+ private static File legacyRegistrySource(File levelDat) {
+ for (File candidate : new File[] { levelDat, new File(levelDat.getParentFile(), "level.dat_old") }) {
+ if (!candidate.isFile()) continue;
+ try (FileInputStream input = new FileInputStream(candidate)) {
+ NBTTagCompound root = CompressedStreamTools.readCompressed(input);
+ if (root.contains("FML", 10) && containsLegacyRegistryEntry(root)) return candidate;
+ } catch (IOException ignored) {}
+ }
+ return null;
+ }
+
+ private static boolean containsLegacyRegistryEntry(NBTTagCompound root) {
+ NBTTagList ids = root.getCompound("FML").getCompound("Registries")
+ .getCompound(BLOCK_REGISTRY.toString()).getList("ids", 10);
+ for (int index = 0; index < ids.size(); index++) {
+ ResourceLocation id = safeId(normalizeLegacyRegistryName(ids.getCompound(index).getString("K")));
+ if (id != null && LegacyMigrationApi.handlesNamespace(id.getNamespace())) return true;
+ }
+ return false;
+ }
+
+ private static int countLegacyBlocks(NBTTagCompound level) {
+ int found = 0;
+ NBTTagList sections = level.getList("Sections", 10);
+ for (int sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) {
+ NBTTagCompound section = sections.getCompound(sectionIndex);
+ byte[] blocks = section.getByteArray("Blocks");
+ if (blocks.length != 4096) continue;
+ byte[] add = section.getByteArray("Add");
+ for (int index = 0; index < blocks.length; index++) {
+ int high = add.length == 2048 ? (add[index >> 1] >> ((index & 1) * 4)) & 15 : 0;
+ if (LEGACY_BLOCK_IDS.get((blocks[index] & 255) | (high << 8))) found++;
+ }
+ }
+ return found;
+ }
+
+ /** Snapshot used by migration probes after spawn chunks have loaded. */
+ public static Map legacyPreparedBlockCounts() {
+ return new LinkedHashMap<>(LEGACY_BLOCK_COUNTS);
+ }
+
+ private static int indexLegacyChunks(File worldDirectory) {
+ LEGACY_CHUNKS.clear();
+ File regionDirectory = new File(worldDirectory, "region");
+ File[] regions = regionDirectory.listFiles((directory, name) -> name.matches("r\\.-?\\d+\\.-?\\d+\\.mc[ar]"));
+ if (regions == null) return 0;
+ byte[] locations = new byte[4096];
+ for (File region : regions) {
+ String[] parts = region.getName().split("\\.");
+ if (parts.length != 4) continue;
+ try (InputStream input = Files.newInputStream(region.toPath())) {
+ int read = 0;
+ while (read < locations.length) {
+ int count = input.read(locations, read, locations.length - read);
+ if (count < 0) break;
+ read += count;
+ }
+ int regionX = Integer.parseInt(parts[1]);
+ int regionZ = Integer.parseInt(parts[2]);
+ for (int index = 0; index < read / 4; index++) {
+ int offset = index * 4;
+ if ((locations[offset] | locations[offset + 1] | locations[offset + 2] | locations[offset + 3]) != 0) {
+ LEGACY_CHUNKS.add(Long.valueOf(chunkKey(
+ regionX * 32 + (index & 31), regionZ * 32 + (index >> 5))));
+ }
+ }
+ } catch (IOException | NumberFormatException exception) {
+ PowerAdvantage.LOGGER.warn("Could not inspect legacy chunk locations in '{}'", region, exception);
+ }
+ }
+ return LEGACY_CHUNKS.size();
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Dynamic>[] expandFlatteningTable(int requiredLength) {
+ try {
+ String fieldName = ObfuscationReflectionHelper.remapName(INameMappingService.Domain.FIELD, "field_199200_b");
+ Field valuesField = BlockStateFlatteningMap.class.getDeclaredField(fieldName);
+ valuesField.setAccessible(true);
+ Field modifiersField = Field.class.getDeclaredField("modifiers");
+ modifiersField.setAccessible(true);
+ modifiersField.setInt(valuesField, valuesField.getModifiers() & ~Modifier.FINAL);
+ Dynamic>[] current = (Dynamic>[]) valuesField.get(null);
+ if (current.length >= requiredLength) return current;
+ Dynamic>[] expanded = Arrays.copyOf(current, requiredLength);
+ valuesField.set(null, expanded);
+ return expanded;
+ } catch (ReflectiveOperationException exception) {
+ throw new IllegalStateException("Could not expand Minecraft's legacy block-state flattening table", exception);
+ }
+ }
+
+ private static void writeWithBackup(File file, NBTTagCompound data) throws IOException {
+ File backup = new File(file.getParentFile(), file.getName() + ".poweradvantage-legacy-backup");
+ if (!backup.exists()) Files.copy(file.toPath(), backup.toPath(), StandardCopyOption.COPY_ATTRIBUTES);
+ File temporary = new File(file.getParentFile(), file.getName() + ".poweradvantage.tmp");
+ try (FileOutputStream output = new FileOutputStream(temporary)) {
+ CompressedStreamTools.writeCompressed(data, output);
+ }
+ try {
+ Files.move(temporary.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING,
+ StandardCopyOption.ATOMIC_MOVE);
+ } catch (java.nio.file.AtomicMoveNotSupportedException ignored) {
+ Files.move(temporary.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ private static void writeMigrationReport(File worldDirectory, int states, int chunks) {
+ File report = new File(worldDirectory, "poweradvantage-migration-report.txt");
+ if (report.exists()) return;
+ List lines = Arrays.asList(
+ "Advantage 1.13 legacy migration",
+ "Prepared block states: " + states,
+ "Protected existing chunks: " + chunks,
+ "Namespaces: " + String.join(", ", LegacyMigrationApi.namespaces()),
+ "Source files were backed up before any loose NBT mutation.");
+ try {
+ Files.write(report.toPath(), lines, StandardCharsets.UTF_8);
+ } catch (IOException exception) {
+ PowerAdvantage.LOGGER.warn("Could not write migration report '{}'", report, exception);
+ }
+ }
+
+ private static String normalizeLegacyRegistryName(String name) {
+ String normalized = normalizeIdentity(name);
+ return normalized.indexOf(':') < 0 ? PowerAdvantage.MODID + ":" + normalized : normalized;
+ }
+
+ private static String normalizeIdentity(String value) {
+ return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
+ }
+
+ private static ResourceLocation safeId(String value) {
+ try { return value == null || value.isEmpty() ? null : new ResourceLocation(value.toLowerCase(Locale.ROOT)); }
+ catch (RuntimeException ignored) { return null; }
+ }
+
+ private static long chunkKey(int x, int z) {
+ return ((long) x & 0xffffffffL) << 32 | ((long) z & 0xffffffffL);
+ }
+
+ private static String worldKey(File directory) {
+ try { return directory.getCanonicalPath(); }
+ catch (IOException exception) { return directory.getAbsolutePath(); }
+ }
+
+ private static Map vanillaBlockEntityIds() {
+ Map ids = new LinkedHashMap<>();
+ ids.put("Airportal", "minecraft:end_portal");
+ ids.put("Banner", "minecraft:banner");
+ ids.put("Beacon", "minecraft:beacon");
+ ids.put("Cauldron", "minecraft:brewing_stand");
+ ids.put("Chest", "minecraft:chest");
+ ids.put("Comparator", "minecraft:comparator");
+ ids.put("Control", "minecraft:command_block");
+ ids.put("DLDetector", "minecraft:daylight_detector");
+ ids.put("Dropper", "minecraft:dropper");
+ ids.put("EnchantTable", "minecraft:enchanting_table");
+ ids.put("EndGateway", "minecraft:end_gateway");
+ ids.put("EnderChest", "minecraft:ender_chest");
+ ids.put("FlowerPot", "minecraft:flower_pot");
+ ids.put("Furnace", "minecraft:furnace");
+ ids.put("Hopper", "minecraft:hopper");
+ ids.put("MobSpawner", "minecraft:mob_spawner");
+ ids.put("Music", "minecraft:noteblock");
+ ids.put("Piston", "minecraft:piston");
+ ids.put("RecordPlayer", "minecraft:jukebox");
+ ids.put("Sign", "minecraft:sign");
+ ids.put("Skull", "minecraft:skull");
+ ids.put("Structure", "minecraft:structure_block");
+ ids.put("Trap", "minecraft:dispenser");
+ return Collections.unmodifiableMap(ids);
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/package-info.java
similarity index 81%
rename from src/main/java/com/mcmoddev/poweradvantage/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/package-info.java
index d8c0ec97..b594f496 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/package-info.java
@@ -2,4 +2,4 @@
This package contains the main mod class. If you are making an add-on mod, you probably want to use
classes from the cyano.poweradvantage.api.simple package
*/
-package com.mcmoddev.poweradvantage;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage;
\ No newline at end of file
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java
new file mode 100644
index 00000000..07d33c43
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/MaterialTags.java
@@ -0,0 +1,42 @@
+package zone.moddev.mc.poweradvantage.recipe;
+
+import net.minecraft.item.Item;
+import net.minecraft.tags.ItemTags;
+import net.minecraft.tags.Tag;
+import net.minecraft.tags.TagCollection;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.ModList;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.RecipeMode;
+import zone.moddev.mc.poweradvantage.init.Items;
+
+/** Config-dependent legacy aliases, applied before recipes and advancements read tags. */
+public final class MaterialTags {
+ private MaterialTags() {}
+
+ public static void finishReload() {
+ // DedicatedServer constructs an initial vanilla resource manager before FML loads mods.
+ if (ModList.get() == null || Items.bioplastic_ingot == null) return;
+ TagCollection tags = ItemTags.getCollection();
+ if (PowerAdvantage.plasticIsAlsoRubber) {
+ append(tags, "forge:rubber", java.util.Collections.singleton(Items.bioplastic_ingot));
+ append(tags, "forge:ingots/rubber", java.util.Collections.singleton(Items.bioplastic_ingot));
+ }
+ if (PowerAdvantage.recipeMode == RecipeMode.NORMAL && ModList.get().isLoaded("electricadvantage")) {
+ for (String metal : new String[] {"lead", "tin", "silver"}) {
+ append(tags, "forge:solder", tags.getOrCreate(new ResourceLocation("forge", "ingots/" + metal)).getAllElements());
+ }
+ }
+ // Refresh wrappers after replacing immutable tags. The same collection is
+ // also serialized by NetworkTagManager, so clients receive the server policy.
+ ItemTags.setCollection(tags);
+ }
+
+ private static void append(TagCollection tags, String id, java.util.Collection additions) {
+ ResourceLocation name = new ResourceLocation(id);
+ Tag.Builder builder = Tag.Builder.create();
+ for (Item item : tags.getOrCreate(name).getAllElements()) builder.add(item);
+ for (Item item : additions) if (item != null) builder.add(item);
+ tags.getTagMap().put(name, builder.build(name));
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java
new file mode 100644
index 00000000..1172c1a8
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/PartsLoot.java
@@ -0,0 +1,65 @@
+package zone.moddev.mc.poweradvantage.recipe;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.world.storage.loot.LootTable;
+import net.minecraft.world.storage.loot.LootTableManager;
+import net.minecraftforge.common.ForgeHooks;
+import net.minecraftforge.fml.ModList;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+
+/** Native legacy parts, confined to original vanilla chest data without ALT. */
+public final class PartsLoot {
+ private static final Set CHESTS = new HashSet<>(Arrays.asList(
+ "chests/abandoned_mineshaft", "chests/simple_dungeon", "chests/village_blacksmith",
+ "chests/stronghold_corridor", "chests/stronghold_crossing", "chests/end_city_treasure"));
+
+ private PartsLoot() {}
+
+ /** Forge 25 treats the Default pack as custom and skips LootTableLoadEvent. */
+ public static LootTable loadLootTable(Gson gson, ResourceLocation name, String data,
+ boolean defaultPack, LootTableManager manager) {
+ String path = name.getPath();
+ if (path.startsWith("loot_tables/") && path.endsWith(".json"))
+ path = path.substring("loot_tables/".length(), path.length() - ".json".length());
+ if (defaultPack && name.getNamespace().equals("minecraft") && CHESTS.contains(path)
+ && ModList.get() != null && ModList.get().isLoaded("poweradvantage")
+ && PowerAdvantage.chestLootFactor > 0 && !hasAdditionalLootTables()) {
+ JsonObject table = new JsonParser().parse(data).getAsJsonObject();
+ JsonArray pools = table.getAsJsonArray("pools");
+ if (pools == null) { pools = new JsonArray(); table.add("pools", pools); }
+ for (String mod : new String[] {"poweradvantage", "steamadvantage", "electricadvantage"}) {
+ if (!ModList.get().isLoaded(mod) || (mod.equals("poweradvantage") && path.equals("chests/end_city_treasure"))) continue;
+ String poolName = mod + ":legacy_parts";
+ boolean exists = false;
+ for (com.google.gson.JsonElement pool : pools)
+ if (pool.getAsJsonObject().has("name") && poolName.equals(pool.getAsJsonObject().get("name").getAsString())) exists = true;
+ if (exists) continue;
+ JsonObject pool = new JsonObject();
+ pool.addProperty("name", poolName);
+ pool.addProperty("rolls", 1);
+ JsonObject entry = new JsonObject();
+ entry.addProperty("type", "loot_table");
+ entry.addProperty("name", mod + ":chests/advantage_parts");
+ entry.addProperty("weight", 1);
+ JsonArray entries = new JsonArray();
+ entries.add(entry);
+ pool.add("entries", entries);
+ pools.add(pool);
+ }
+ data = table.toString();
+ }
+ return ForgeHooks.loadLootTable(gson, name, data, defaultPack, manager);
+ }
+
+ private static boolean hasAdditionalLootTables() {
+ return ModList.get().getMods().stream().anyMatch(mod -> mod.getModId().replace("_", "").replace("-", "")
+ .equals("additionalloottables"));
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java
new file mode 100644
index 00000000..65109ce5
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeConditions.java
@@ -0,0 +1,36 @@
+package zone.moddev.mc.poweradvantage.recipe;
+
+import net.minecraft.tags.ItemTags;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.crafting.CraftingHelper;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.RecipeMode;
+
+/** Registers the data-pack conditions used by PowerAdvantage recipe modes. */
+public final class RecipeConditions {
+ private static boolean registered;
+
+ private RecipeConditions() {}
+
+ public static synchronized void register() {
+ if (registered) return;
+ CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "recipe_mode"), json -> {
+ RecipeMode expected = RecipeMode.valueOf(JsonUtils.getString(json, "mode").toUpperCase());
+ return () -> PowerAdvantage.recipeMode == expected;
+ });
+ CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "not_recipe_mode"), json -> {
+ RecipeMode excluded = RecipeMode.valueOf(JsonUtils.getString(json, "mode").toUpperCase());
+ return () -> PowerAdvantage.recipeMode != excluded;
+ });
+ CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "tag_empty"), json -> {
+ ResourceLocation tagId = new ResourceLocation(JsonUtils.getString(json, "tag"));
+ return () -> ItemTags.getCollection().getOrCreate(tagId).getAllElements().isEmpty();
+ });
+ CraftingHelper.register(new ResourceLocation(PowerAdvantage.MODID, "tag_not_empty"), json -> {
+ ResourceLocation tagId = new ResourceLocation(JsonUtils.getString(json, "tag"));
+ return () -> !ItemTags.getCollection().getOrCreate(tagId).getAllElements().isEmpty();
+ });
+ registered = true;
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java
new file mode 100644
index 00000000..8be8b53a
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/recipe/RecipeDiscovery.java
@@ -0,0 +1,35 @@
+package zone.moddev.mc.poweradvantage.recipe;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import net.minecraft.advancements.Advancement;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.item.crafting.IRecipe;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.fml.common.gameevent.PlayerEvent;
+
+/** Retains earned discoveries when recipe modes or material providers change. */
+public final class RecipeDiscovery {
+ private static final Set NAMESPACES = new HashSet<>(Arrays.asList(
+ "poweradvantage", "steamadvantage", "electricadvantage"));
+
+ private RecipeDiscovery() {}
+
+ public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
+ if (!(event.getPlayer() instanceof EntityPlayerMP)) return;
+ EntityPlayerMP player = (EntityPlayerMP) event.getPlayer();
+ // Disabled recipes have no reward to grant. Replay only already-earned
+ // discoveries when that exact recipe becomes available on a later login.
+ for (IRecipe recipe : player.getServer().getRecipeManager().getRecipes()) {
+ if (!NAMESPACES.contains(recipe.getId().getNamespace()) || recipe.isDynamic()
+ || player.getRecipeBook().isUnlocked(recipe)) continue;
+ Advancement advancement = player.getServer().getAdvancementManager().getAdvancement(
+ new ResourceLocation(recipe.getId().getNamespace(), "recipes/" + recipe.getId().getPath()));
+ if (advancement != null && player.getAdvancements().getProgress(advancement).isDone()) {
+ player.unlockRecipes(Collections.singletonList(recipe));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/FuelRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java
similarity index 90%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/FuelRegistry.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java
index ec98b411..2a82b95e 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/FuelRegistry.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/FuelRegistry.java
@@ -1,10 +1,10 @@
-package com.mcmoddev.poweradvantage.registry;
+package zone.moddev.mc.poweradvantage.registry;
import net.minecraft.block.Block;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntityFurnace;
-import net.minecraftforge.fml.common.IFuelHandler;
+import net.minecraftforge.event.ForgeEventFactory;
import java.util.*;
import java.util.concurrent.locks.Lock;
@@ -13,11 +13,11 @@
/**
* The fuel registry keeps track of the cooking time for new burnables added by PowerAdvantage. The
- * actual registration of these burnables occurs in com.mcmoddev.poweradvantage.init.Fuels
+ * actual registration of these burnables occurs in zone.moddev.mc.poweradvantage.init.Fuels
* @author DrCyano
*
*/
-public class FuelRegistry implements IFuelHandler{
+public class FuelRegistry {
private final Map>> burnMap = new HashMap<>();
@@ -124,7 +124,6 @@ public List> getPostBurnAlgorithmsForItem(Item ite
* @param fuel The fuel item
* @return The number of ticks that the item should burn, or 0 if it isn't flammable
*/
- @Override
public int getBurnTime(ItemStack fuel) {
if(fuel == null) return 0;
for(Function algorithm : getFuelAlgorithmsForItem(fuel.getItem())){
@@ -140,8 +139,12 @@ public int getBurnTime(ItemStack fuel) {
* @return The number of ticks that the item should burn, or 0 if it isn't flammable
*/
public static short getActualBurntimeForItem(ItemStack fuel){
- if(fuel == null) return 0;
- return (short)TileEntityFurnace.getItemBurnTime(fuel);
+ if(fuel == null || fuel.isEmpty()) return 0;
+ int custom = getInstance().getBurnTime(fuel);
+ if (custom > 0) return (short) Math.min(Short.MAX_VALUE, custom);
+ Integer vanilla = TileEntityFurnace.getBurnTimes().get(fuel.getItem());
+ int burnTime = ForgeEventFactory.getItemBurnTime(fuel, vanilla == null ? 0 : vanilla.intValue());
+ return (short) Math.min(Short.MAX_VALUE, Math.max(0, burnTime));
}
/**
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/ITileEntityGUI.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java
similarity index 87%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/ITileEntityGUI.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java
index 86d52d11..7cc88006 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/ITileEntityGUI.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/ITileEntityGUI.java
@@ -1,9 +1,9 @@
-package com.mcmoddev.poweradvantage.registry;
+package zone.moddev.mc.poweradvantage.registry;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.tileentity.TileEntity;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
/**
* This interface is used to combine the two different classes of a block GUI
@@ -30,6 +30,6 @@ public interface ITileEntityGUI {
* @param player The player that is going to use the GUI
* @return An instance of net.minecraft.client.gui.inventory.GuiContainer
*/
- @SideOnly(Side.CLIENT)
+ @OnlyIn(Dist.CLIENT)
public abstract net.minecraft.client.gui.inventory.GuiContainer getContainerGUI(TileEntity e, EntityPlayer player);
}
\ No newline at end of file
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/MachineGUIRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java
similarity index 59%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/MachineGUIRegistry.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java
index 7901a2d5..0fe16d22 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/MachineGUIRegistry.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/MachineGUIRegistry.java
@@ -1,12 +1,23 @@
-package com.mcmoddev.poweradvantage.registry;
+package zone.moddev.mc.poweradvantage.registry;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.entity.player.InventoryPlayer;
+import net.minecraft.inventory.Container;
+import net.minecraft.network.PacketBuffer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.text.ITextComponent;
+import net.minecraft.util.text.TextComponentString;
+import net.minecraft.world.IInteractionObject;
import net.minecraft.world.World;
-import net.minecraftforge.fml.common.network.IGuiHandler;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import net.minecraftforge.fml.network.FMLPlayMessages;
+import net.minecraftforge.fml.network.NetworkHooks;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.api.distmarker.OnlyIn;
import java.util.HashMap;
import java.util.Map;
@@ -26,7 +37,7 @@
* @author DrCyano
*
*/
-public class MachineGUIRegistry implements IGuiHandler {
+public class MachineGUIRegistry {
private static final AtomicInteger guiIDCounter = new AtomicInteger(1);
private static final Map guiTable = new HashMap();
@@ -86,8 +97,7 @@ public void removeGUI(int guiID) {
/**
* Implementation of net.minecraftforge.fml.common.network.IGuiHandler
*/
- @Override
- @SideOnly(Side.CLIENT)
+ @OnlyIn(Dist.CLIENT)
public Object getClientGuiElement(
int id, EntityPlayer player, World world,
int x, int y, int z
@@ -102,7 +112,6 @@ public Object getClientGuiElement(
/**
* Implementation of net.minecraftforge.fml.common.network.IGuiHandler
*/
- @Override
public Object getServerGuiElement(
int id, EntityPlayer player, World world,
int x, int y, int z
@@ -113,4 +122,44 @@ public Object getServerGuiElement(
}
return null;
}
-}
\ No newline at end of file
+
+ public static void open(EntityPlayerMP player, World world, BlockPos pos, int guiId) {
+ ITileEntityGUI definition = guiTable.get(guiId);
+ if (definition == null) return;
+ TileEntity tile = world.getTileEntity(pos);
+ if (tile == null) return;
+ NetworkHooks.openGui(player, new IInteractionObject() {
+ @Override
+ public Container createContainer(InventoryPlayer inventory, EntityPlayer entityPlayer) {
+ return definition.getContainer(tile, entityPlayer);
+ }
+
+ @Override
+ public String getGuiID() {
+ return "poweradvantage:machine";
+ }
+
+ @Override public ITextComponent getName() { return new TextComponentString("Power Advantage"); }
+ @Override public boolean hasCustomName() { return false; }
+ @Override public ITextComponent getCustomName() { return null; }
+ }, buffer -> {
+ buffer.writeBlockPos(pos);
+ buffer.writeVarInt(guiId);
+ });
+ }
+
+ @OnlyIn(Dist.CLIENT)
+ public static GuiScreen createClientGui(FMLPlayMessages.OpenContainer message) {
+ PacketBuffer data = message.getAdditionalData();
+ BlockPos pos = data.readBlockPos();
+ int guiId = data.readVarInt();
+ Minecraft minecraft = Minecraft.getInstance();
+ if (minecraft.world == null || minecraft.player == null) return null;
+ TileEntity tile = minecraft.world.getTileEntity(pos);
+ ITileEntityGUI definition = guiTable.get(guiId);
+ if (tile == null || definition == null) return null;
+ GuiContainer screen = definition.getContainerGUI(tile, minecraft.player);
+ screen.inventorySlots.windowId = message.getWindowId();
+ return screen;
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/package-info.java
similarity index 67%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/package-info.java
index b5c7066c..d71055ea 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/package-info.java
@@ -2,4 +2,4 @@
This package contains registry classes for registering content related to
PowerAdvantage.
*/
-package com.mcmoddev.poweradvantage.registry;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.registry;
\ No newline at end of file
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/DistillationRecipe.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java
similarity index 92%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/DistillationRecipe.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java
index 7d9ddb53..6ee510aa 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/DistillationRecipe.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipe.java
@@ -1,12 +1,12 @@
-package com.mcmoddev.poweradvantage.registry.still.recipe;
+package zone.moddev.mc.poweradvantage.registry.still.recipe;
import java.util.Collection;
import java.util.LinkedList;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.FluidRegistry;
-import net.minecraftforge.fluids.FluidStack;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.util.registry.IRegistry;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
/**
* Superclass for all fluid distillation recipes.
@@ -119,8 +119,8 @@ public boolean isValidOutput(Fluid output) {
*/
public Collection getValidInputs() {
LinkedList l = new LinkedList<>();
- for (Fluid f : FluidRegistry.getRegisteredFluids().values()) {
- FluidStack stack = new FluidStack(f, Fluid.BUCKET_VOLUME);
+ for (Fluid f : IRegistry.field_212619_h) {
+ FluidStack stack = new FluidStack(f, FluidTank.BUCKET_VOLUME);
if (this.isValidInput(stack)) {
l.add(stack);
}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java
similarity index 79%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java
index 8a58ae35..6975d664 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/DistillationRecipeRegistry.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.registry.still.recipe;
+package zone.moddev.mc.poweradvantage.registry.still.recipe;
import java.util.ArrayList;
import java.util.HashMap;
@@ -7,11 +7,12 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
-import com.mcmoddev.poweradvantage.PowerAdvantage;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.FluidRegistry;
-import net.minecraftforge.fluids.FluidStack;
-import net.minecraftforge.fml.common.FMLLog;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
+import net.minecraft.fluid.Fluid;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.registry.IRegistry;
+import zone.moddev.mc.poweradvantage.init.Fluids;
/**
* This class handles all of the recipes for machines that "smelt" fluids.
@@ -82,24 +83,16 @@ public static void clearRecipeCache(){
*/
public static void addDistillationRecipe(String inputFluidName, int inputFluidVolume,
String outputFluidName, int outputFluidVolume){
- if(!FluidRegistry.isFluidRegistered(inputFluidName)){
- FMLLog.warning("%s: Fluid named %s was used in distillation recipe %sx%s -> %sx%s, but there is no fluid registered under the name %s",
- PowerAdvantage.MODID,inputFluidName,
- inputFluidVolume,inputFluidName,outputFluidVolume,outputFluidName,
- inputFluidName);
- }
- if(!FluidRegistry.isFluidRegistered(outputFluidName)){
- FMLLog.warning("%s: Fluid named %s was used in distillation recipe %sx%s -> %sx%s, but there is no fluid registered under the name %s",
- PowerAdvantage.MODID,outputFluidName,
- inputFluidVolume,inputFluidName,outputFluidVolume,outputFluidName,
- outputFluidName);
+ if (Fluids.byName(inputFluidName) == null || Fluids.byName(outputFluidName) == null) {
+ PowerAdvantage.LOGGER.warn("Skipping distillation recipe {}x{} -> {}x{} because a fluid is unavailable",
+ inputFluidVolume, inputFluidName, outputFluidVolume, outputFluidName);
+ return;
}
DistillationRecipe recipe = new DistillationRecipe(
new RegistryNameFluidReference(inputFluidName),inputFluidVolume,
new RegistryNameFluidReference(outputFluidName),outputFluidVolume);
getInstance().addRecipe(recipe);
- FMLLog.info("%s: Added distillation recipe %sx\"%s\" -> %sx\"%s\"",
- PowerAdvantage.MODID,
+ PowerAdvantage.LOGGER.info("Added distillation recipe {}x{} -> {}x{}",
inputFluidVolume,inputFluidName,outputFluidVolume,outputFluidName);
}
@@ -119,9 +112,8 @@ public static void addExplicitDistillationRecipe(Fluid inputFluidInstance, int i
new ExplicitFluidReference(inputFluidInstance),inputFluidVolume,
new ExplicitFluidReference(outputFluidInstance),outputFluidVolume);
getInstance().addRecipe(recipe);
- FMLLog.info("%s: Added distillation recipe %sx%s -> %sx%s",
- PowerAdvantage.MODID,
- inputFluidVolume,inputFluidInstance.getUnlocalizedName(),outputFluidVolume,outputFluidInstance.getUnlocalizedName());
+ PowerAdvantage.LOGGER.info("Added explicit distillation recipe {}x{} -> {}x{}",
+ inputFluidVolume, id(inputFluidInstance), outputFluidVolume, id(outputFluidInstance));
}
/**
@@ -134,8 +126,8 @@ public static void addExplicitDistillationRecipe(Fluid inputFluidInstance, int i
public static void addDistillationRecipe(FluidStack input, FluidStack output){
String inputFluidName; int inputFluidVolume;
String outputFluidName; int outputFluidVolume;
- inputFluidName = input.getFluid().getName();
- outputFluidName = output.getFluid().getName();
+ inputFluidName = id(input.getFluid());
+ outputFluidName = id(output.getFluid());
// factorize to simplify recipe to smallest ratio
int gcf = factor(input.amount, output.amount);
inputFluidVolume = input.amount / gcf;
@@ -143,6 +135,11 @@ public static void addDistillationRecipe(FluidStack input, FluidStack output){
addDistillationRecipe(inputFluidName, inputFluidVolume,
outputFluidName, outputFluidVolume);
}
+
+ private static String id(Fluid fluid) {
+ ResourceLocation id = IRegistry.field_212619_h.getKey(fluid);
+ return id == null ? "unknown" : id.toString();
+ }
private static int factor(int a, int b){
for(int f = Math.min(a, b); f > 1; f--){
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/ExplicitFluidReference.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java
similarity index 75%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/ExplicitFluidReference.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java
index f912bfa9..5fe583a4 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/ExplicitFluidReference.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/ExplicitFluidReference.java
@@ -1,6 +1,8 @@
-package com.mcmoddev.poweradvantage.registry.still.recipe;
+package zone.moddev.mc.poweradvantage.registry.still.recipe;
-import net.minecraftforge.fluids.Fluid;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.registry.IRegistry;
/**
* Implementation of FluidReference that compares specific fluids (fluids of same name but different
@@ -42,7 +44,8 @@ public boolean matches(Fluid fluid) {
*/
@Override
public boolean matches(String fluidName) {
- return reference.getName() == fluidName; // yes, I do want to check instance rather than text content
+ ResourceLocation id = IRegistry.field_212619_h.getKey(reference);
+ return id != null && (id.toString().equals(fluidName) || id.getPath().equals(fluidName));
}
/**
@@ -62,6 +65,7 @@ public Fluid getFluid() {
*/
@Override
public int hashCode() {
- return reference.getName().hashCode();
+ ResourceLocation id = IRegistry.field_212619_h.getKey(reference);
+ return id == null ? System.identityHashCode(reference) : id.hashCode();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/FluidReference.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/FluidReference.java
similarity index 93%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/FluidReference.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/FluidReference.java
index 05b098df..ac62fc97 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/FluidReference.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/FluidReference.java
@@ -1,7 +1,7 @@
-package com.mcmoddev.poweradvantage.registry.still.recipe;
+package zone.moddev.mc.poweradvantage.registry.still.recipe;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.FluidStack;
+import net.minecraft.fluid.Fluid;
+import cyano.poweradvantage.api.fluid.FluidStack;
/**
* This is a simple class used to identify fluids by either registry name or explicit object
* reference.
diff --git a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java
similarity index 76%
rename from src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java
rename to src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java
index 72aabbfa..159e6fb5 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/registry/still/recipe/RegistryNameFluidReference.java
@@ -1,7 +1,9 @@
-package com.mcmoddev.poweradvantage.registry.still.recipe;
+package zone.moddev.mc.poweradvantage.registry.still.recipe;
-import net.minecraftforge.fluids.Fluid;
-import net.minecraftforge.fluids.FluidRegistry;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.registry.IRegistry;
+import zone.moddev.mc.poweradvantage.init.Fluids;
public class RegistryNameFluidReference extends FluidReference {
@@ -26,7 +28,8 @@ public RegistryNameFluidReference(String registryName) {
*/
@Override
public boolean matches(Fluid fluid) {
- return reference.equals(fluid.getName());
+ ResourceLocation id = IRegistry.field_212619_h.getKey(fluid);
+ return id != null && (reference.equals(id.toString()) || reference.equals(id.getPath()));
}
/**
@@ -47,7 +50,7 @@ public boolean matches(String fluidName) {
*/
@Override
public Fluid getFluid() {
- return FluidRegistry.getFluid(reference);
+ return Fluids.byName(reference);
}
/**
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java
new file mode 100644
index 00000000..73f98307
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidContainerHelper.java
@@ -0,0 +1,56 @@
+package zone.moddev.mc.poweradvantage.util;
+
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.fluid.FluidTank;
+import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.init.Items;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.EnumHand;
+import net.minecraft.util.registry.IRegistry;
+
+/** Native bucket interaction for Forge 25's transitional fluid implementation. */
+public final class FluidContainerHelper {
+ private FluidContainerHelper() {}
+
+ public static boolean interact(EntityPlayer player, EnumHand hand,
+ IAdvantageFluidHandler handler, EnumFacing face) {
+ ItemStack held = player.getHeldItem(hand);
+ if (held.isEmpty()) return false;
+
+ if (held.getItem() == Items.BUCKET) {
+ FluidStack available = handler.drain(face, FluidTank.BUCKET_VOLUME, false);
+ if (available == null || available.amount < FluidTank.BUCKET_VOLUME) return false;
+ Item filledBucket = available.getFluid().getFilledBucket();
+ if (filledBucket == null || filledBucket == Items.AIR) return false;
+ handler.drain(face, FluidTank.BUCKET_VOLUME, true);
+ replaceOne(player, hand, held, new ItemStack(filledBucket));
+ return true;
+ }
+
+ Fluid contained = fluidForBucket(held.getItem());
+ if (contained == null) return false;
+ FluidStack bucket = new FluidStack(contained, FluidTank.BUCKET_VOLUME);
+ if (handler.fill(face, bucket, false) < FluidTank.BUCKET_VOLUME) return false;
+ handler.fill(face, bucket, true);
+ replaceOne(player, hand, held, new ItemStack(Items.BUCKET));
+ return true;
+ }
+
+ public static Fluid fluidForBucket(Item bucketItem) {
+ for (Fluid fluid : IRegistry.field_212619_h) {
+ if (fluid.getFilledBucket() == bucketItem && bucketItem != Items.BUCKET) return fluid;
+ }
+ return null;
+ }
+
+ private static void replaceOne(EntityPlayer player, EnumHand hand, ItemStack held, ItemStack replacement) {
+ if (player.abilities.isCreativeMode) return;
+ held.shrink(1);
+ if (held.isEmpty()) player.setHeldItem(hand, replacement);
+ else if (!player.inventory.addItemStackToInventory(replacement)) player.dropItem(replacement, false);
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java
new file mode 100644
index 00000000..9208e701
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidHandlerHelper.java
@@ -0,0 +1,18 @@
+package zone.moddev.mc.poweradvantage.util;
+
+import cyano.poweradvantage.api.fluid.IAdvantageFluidHandler;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+
+/** Looks up the stable Advantage fluid handler exposed by local machines. */
+public final class FluidHandlerHelper {
+ private FluidHandlerHelper() {}
+
+ public static IAdvantageFluidHandler getHandler(TileEntity tileEntity, EnumFacing face) {
+ return tileEntity instanceof IAdvantageFluidHandler ? (IAdvantageFluidHandler) tileEntity : null;
+ }
+
+ public static boolean hasHandler(TileEntity tileEntity, EnumFacing face) {
+ return getHandler(tileEntity, face) != null;
+ }
+}
diff --git a/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java
new file mode 100644
index 00000000..87450318
--- /dev/null
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/FluidIdHelper.java
@@ -0,0 +1,27 @@
+package zone.moddev.mc.poweradvantage.util;
+
+import net.minecraft.fluid.Fluid;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.registry.IRegistry;
+
+public final class FluidIdHelper {
+
+ private FluidIdHelper() {
+ throw new IllegalAccessError("Not an instantiable class");
+ }
+
+ public static int getFluidId(Fluid fluid) {
+ if (fluid == null) return 0;
+ ResourceLocation name = IRegistry.field_212619_h.getKey(fluid);
+ return name == null ? 0 : name.toString().hashCode();
+ }
+
+ public static Fluid getFluid(int id) {
+ if (id == 0) return null;
+ for (Fluid fluid : IRegistry.field_212619_h) {
+ ResourceLocation name = IRegistry.field_212619_h.getKey(fluid);
+ if (name != null && name.toString().hashCode() == id) return fluid;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/util/HashCodeHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/HashCodeHelper.java
similarity index 96%
rename from src/main/java/com/mcmoddev/poweradvantage/util/HashCodeHelper.java
rename to src/main/java/zone/moddev/mc/poweradvantage/util/HashCodeHelper.java
index 34e6b878..efed6360 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/util/HashCodeHelper.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/HashCodeHelper.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.util;
+package zone.moddev.mc.poweradvantage.util;
public abstract class HashCodeHelper {
diff --git a/src/main/java/com/mcmoddev/poweradvantage/util/InventoryWrapper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java
similarity index 94%
rename from src/main/java/com/mcmoddev/poweradvantage/util/InventoryWrapper.java
rename to src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java
index a5f51fed..abe19dad 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/util/InventoryWrapper.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/InventoryWrapper.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.util;
+package zone.moddev.mc.poweradvantage.util;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.IInventory;
@@ -124,8 +124,8 @@ public void clear() {
}
@Override
- public String getName() {
- return "Inventory";
+ public ITextComponent getName() {
+ return new TextComponentString("Inventory");
}
@Override
@@ -133,6 +133,11 @@ public boolean hasCustomName() {
return false;
}
+ @Override
+ public ITextComponent getCustomName() {
+ return null;
+ }
+
@Override
public TextComponentBase getDisplayName() {
return new TextComponentString("Inventory");
@@ -173,10 +178,15 @@ public ITextComponent getDisplayName() {
}
@Override
- public String getName() {
+ public ITextComponent getName() {
return inventory.getName();
}
+ @Override
+ public ITextComponent getCustomName() {
+ return inventory.getCustomName();
+ }
+
@Override
public boolean hasCustomName() {
return inventory.hasCustomName();
diff --git a/src/main/java/com/mcmoddev/poweradvantage/util/PowerHelper.java b/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java
similarity index 59%
rename from src/main/java/com/mcmoddev/poweradvantage/util/PowerHelper.java
rename to src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java
index 0119d953..ca9dc8cb 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/util/PowerHelper.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/PowerHelper.java
@@ -1,11 +1,12 @@
-package com.mcmoddev.poweradvantage.util;
+package zone.moddev.mc.poweradvantage.util;
-import cofh.api.energy.IEnergyReceiver;
-import com.mcmoddev.poweradvantage.PowerAdvantage;
+import zone.moddev.mc.poweradvantage.PowerAdvantage;
import cyano.poweradvantage.api.ITypedConduit;
import cyano.poweradvantage.api.PowerConnectorContext;
import net.minecraft.block.ITileEntityProvider;
-import reborncore.api.power.IEnergyInterfaceTile;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraftforge.energy.CapabilityEnergy;
/**
* Collection of utility methods
@@ -26,19 +27,15 @@ public static boolean areConnectable(PowerConnectorContext connection) {
// both blocks are Power Advantage conductors
return ((ITypedConduit) connection.thisBlock.getBlock()).canAcceptConnection(connection)
&& ((ITypedConduit) connection.otherBlock.getBlock()).canAcceptConnection(connection.reverse());
- } else if (connection.otherBlock.getBlock() instanceof ITileEntityProvider) {
- if (PowerAdvantage.detectedRF
- && PowerAdvantage.rfConversionTable.containsKey(connection.powerType)) {
- // RF cross-mod compatibility
- return connection.world.getTileEntity(connection.otherBlockPosition) instanceof IEnergyReceiver;
- }
- if (PowerAdvantage.detectedTechReborn
- && PowerAdvantage.trConversionTable.containsKey(connection.powerType)) {
- // TR cross-mod compatibility
- return connection.world.getTileEntity(connection.otherBlockPosition) instanceof IEnergyInterfaceTile;
+ } else if (connection.otherBlock.getBlock() instanceof ITileEntityProvider
+ && PowerAdvantage.forgeEnergyConversionTable.containsKey(connection.powerType)) {
+ TileEntity tile = connection.world.getTileEntity(connection.otherBlockPosition);
+ if (tile == null) return false;
+ for (EnumFacing side : EnumFacing.values()) {
+ if (tile.getCapability(CapabilityEnergy.ENERGY, side).isPresent()) return true;
}
}
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/util/ReversibleHashMap.java b/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleHashMap.java
similarity index 97%
rename from src/main/java/com/mcmoddev/poweradvantage/util/ReversibleHashMap.java
rename to src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleHashMap.java
index 15d38442..f9df4fe0 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/util/ReversibleHashMap.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleHashMap.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.util;
+package zone.moddev.mc.poweradvantage.util;
import java.util.Collection;
import java.util.Map;
@@ -111,4 +111,4 @@ public Set valueSet() {
return reverse.keySet();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/mcmoddev/poweradvantage/util/ReversibleMap.java b/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleMap.java
similarity index 81%
rename from src/main/java/com/mcmoddev/poweradvantage/util/ReversibleMap.java
rename to src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleMap.java
index dd1503c5..d8b39b04 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/util/ReversibleMap.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/ReversibleMap.java
@@ -1,4 +1,4 @@
-package com.mcmoddev.poweradvantage.util;
+package zone.moddev.mc.poweradvantage.util;
public interface ReversibleMap extends java.util.Map {
diff --git a/src/main/java/com/mcmoddev/poweradvantage/math/package-info.java b/src/main/java/zone/moddev/mc/poweradvantage/util/package-info.java
similarity index 55%
rename from src/main/java/com/mcmoddev/poweradvantage/math/package-info.java
rename to src/main/java/zone/moddev/mc/poweradvantage/util/package-info.java
index ba2ed2d8..8346da0c 100644
--- a/src/main/java/com/mcmoddev/poweradvantage/math/package-info.java
+++ b/src/main/java/zone/moddev/mc/poweradvantage/util/package-info.java
@@ -3,4 +3,4 @@
This package contains helper classes.
*/
-package com.mcmoddev.poweradvantage.math;
\ No newline at end of file
+package zone.moddev.mc.poweradvantage.util;
\ No newline at end of file
diff --git a/src/main/resources/META-INF/coremods.json b/src/main/resources/META-INF/coremods.json
new file mode 100644
index 00000000..e788d50e
--- /dev/null
+++ b/src/main/resources/META-INF/coremods.json
@@ -0,0 +1,6 @@
+{
+ "poweradvantage_113_migration": "coremods/poweradvantage_113_migration.js",
+ "poweradvantage_oil_renderer": "coremods/poweradvantage_oil_renderer.js",
+ "poweradvantage_material_tags": "coremods/poweradvantage_material_tags.js",
+ "poweradvantage_parts_loot": "coremods/poweradvantage_parts_loot.js"
+}
diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml
new file mode 100644
index 00000000..f2b5cabe
--- /dev/null
+++ b/src/main/resources/META-INF/mods.toml
@@ -0,0 +1,40 @@
+modLoader="javafml"
+loaderVersion="${loader_version_range}"
+license="LGPL-3.0-only"
+issueTrackerURL="https://github.com/MinecraftModDevelopmentMods/PowerAdvantage/issues"
+
+[[mods]]
+modId="poweradvantage"
+version="${version}"
+displayName="Power Advantage"
+displayURL="https://www.curseforge.com/minecraft/mc-mods/power-advantage"
+authors="DrCyano (aka Cyanobacterium), the MMD Team"
+description='''API and shared machines for pressure, electricity, quantum power, fluids, and item transport.'''
+
+[[dependencies.poweradvantage]]
+modId="basemetals"
+mandatory=false
+versionRange="[3.0.1.113021,4.0.0)"
+ordering="AFTER"
+side="BOTH"
+
+[[dependencies.poweradvantage]]
+modId="orespawn"
+mandatory=true
+versionRange="[4.1.0.113021,5.0.0)"
+ordering="AFTER"
+side="BOTH"
+
+[[dependencies.poweradvantage]]
+modId="forge"
+mandatory=true
+versionRange="${forge_version_range}"
+ordering="NONE"
+side="BOTH"
+
+[[dependencies.poweradvantage]]
+modId="minecraft"
+mandatory=true
+versionRange="${minecraft_version_range}"
+ordering="NONE"
+side="BOTH"
diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json
index 97d881ea..35df5016 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_electricity.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:converter_rf_electricity", "y": 90 },
- "facing=north": { "model": "poweradvantage:converter_rf_electricity" },
- "facing=south": { "model": "poweradvantage:converter_rf_electricity", "y": 180 },
- "facing=west": { "model": "poweradvantage:converter_rf_electricity", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/converter_rf_electricity",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/converter_rf_electricity"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/converter_rf_electricity",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/converter_rf_electricity",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json
index 5e285905..257c03cf 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_quantum.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:converter_rf_quantum", "y": 90 },
- "facing=north": { "model": "poweradvantage:converter_rf_quantum" },
- "facing=south": { "model": "poweradvantage:converter_rf_quantum", "y": 180 },
- "facing=west": { "model": "poweradvantage:converter_rf_quantum", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/converter_rf_quantum",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/converter_rf_quantum"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/converter_rf_quantum",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/converter_rf_quantum",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json
index 110dd846..d868d075 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/converter_rf_steam.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:converter_rf_steam", "y": 90 },
- "facing=north": { "model": "poweradvantage:converter_rf_steam" },
- "facing=south": { "model": "poweradvantage:converter_rf_steam", "y": 180 },
- "facing=west": { "model": "poweradvantage:converter_rf_steam", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/converter_rf_steam",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/converter_rf_steam"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/converter_rf_steam",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/converter_rf_steam",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json b/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json
index f3c69427..d7730168 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/converter_tr_electricity.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:converter_tr_electricity", "y": 90 },
- "facing=north": { "model": "poweradvantage:converter_tr_electricity" },
- "facing=south": { "model": "poweradvantage:converter_tr_electricity", "y": 180 },
- "facing=west": { "model": "poweradvantage:converter_tr_electricity", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/converter_tr_electricity",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/converter_tr_electricity"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/converter_tr_electricity",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/converter_tr_electricity",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json b/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json
index 2f679747..9ff671aa 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/crude_oil.json
@@ -1,9 +1,5 @@
{
- "forge_marker": 1,
"variants": {
- "fluid": {
- "model": "forge:fluid",
- "custom": { "fluid": "crude_oil" }
- }
+ "": { "model": "poweradvantage:block/crude_oil" }
}
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json
index 33f98428..1266604e 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_discharge.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:fluid_discharge", "y": 90 },
- "facing=north": { "model": "poweradvantage:fluid_discharge" },
- "facing=south": { "model": "poweradvantage:fluid_discharge", "y": 180 },
- "facing=west": { "model": "poweradvantage:fluid_discharge", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/fluid_discharge",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/fluid_discharge"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/fluid_discharge",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/fluid_discharge",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json
index 3997feec..70980092 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_drain.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:fluid_drain", "y": 90 },
- "facing=north": { "model": "poweradvantage:fluid_drain" },
- "facing=south": { "model": "poweradvantage:fluid_drain", "y": 180 },
- "facing=west": { "model": "poweradvantage:fluid_drain", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/fluid_drain",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/fluid_drain"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/fluid_drain",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/fluid_drain",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json
index 167fb814..5f1ee90e 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_metal_tank.json
@@ -1,8 +1,23 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:fluid_metal_tank", "y": 90, "uvlock": true },
- "facing=north": { "model": "poweradvantage:fluid_metal_tank", "uvlock": true },
- "facing=south": { "model": "poweradvantage:fluid_metal_tank", "y": 180, "uvlock": true },
- "facing=west": { "model": "poweradvantage:fluid_metal_tank", "y": 270, "uvlock": true }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/fluid_metal_tank",
+ "y": 90,
+ "uvlock": true
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/fluid_metal_tank",
+ "uvlock": true
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/fluid_metal_tank",
+ "y": 180,
+ "uvlock": true
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/fluid_metal_tank",
+ "y": 270,
+ "uvlock": true
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json
index 3d945543..74b70ea6 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe.json
@@ -1,70 +1,389 @@
{
- "__comment": "blockstate parameters MUST be in alphabetical order!",
- "variants": {
- "down=false,east=false,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_ensw", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 90, "y": 0, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=false,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 270, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 270, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 180, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 270, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_ns", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 0, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 180, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_ns", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_ns", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 270, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=false,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 180, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 90, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_dnu", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 180, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 270, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_dnu", "x": 270, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 270, "y": 0, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 270, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 180, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 90, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 180, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 270, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_ensw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_ensw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 270, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_ensw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 270, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 270, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 180, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_densuw", "x": 0, "y": 0, "uvlock": false }
-
+ "__comment": "blockstate parameters MUST be in alphabetical order!",
+ "variants": {
+ "down=false,east=false,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ns",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_ns",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ns",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 270,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_densuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json
index a6e77200..e5b42d6d 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_pipe_terminal.json
@@ -1,70 +1,389 @@
{
- "__comment": "blockstate parameters MUST be in alphabetical order!",
- "variants": {
- "down=false,east=false,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_0", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 90, "y": 0, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=false,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_n", "x": 270, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_n", "x": 0, "y": 270, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 180, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 270, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_ns", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 90, "y": 0, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 180, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_ns", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_ns", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 270, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=false,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nw", "x": 270, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 180, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 90, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_dnu", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 180, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 90, "y": 270, "uvlock": false },
- "down=true,east=false,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_dnu", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=true,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_dnu", "x": 270, "y": 90, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_dnu", "x": 270, "y": 0, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=false,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nuw", "x": 0, "y": 270, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=false,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 180, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 90, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 180, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 270, "uvlock": false },
- "down=true,east=true,north=false,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_ensw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_ensw", "x": 90, "y": 90, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 180, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=false,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 90, "y": 270, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 180, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_ensw", "x": 0, "y": 0, "uvlock": false },
- "down=false,east=true,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 270, "uvlock": false },
- "down=false,east=false,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_nsuw", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=true,west=false": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 270, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=false,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 270, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=false,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 180, "uvlock": false },
- "down=true,east=true,north=false,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 0, "uvlock": false },
- "down=true,east=false,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 0, "y": 90, "uvlock": false },
- "down=false,east=true,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_desuw", "x": 90, "y": 0, "uvlock": false },
- "down=true,east=true,north=true,south=true,up=true,west=true": { "model": "poweradvantage:fluid_pipe_densuw", "x": 0, "y": 0, "uvlock": false }
-
+ "__comment": "blockstate parameters MUST be in alphabetical order!",
+ "variants": {
+ "down=false,east=false,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_0",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_n",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ns",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_ns",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ns",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nw",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 90,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 270,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_dnu",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=false,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nuw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=false,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 90,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 180,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 90,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_ensw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=false,east=false,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_nsuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=true,west=false": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 270,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=false,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 270,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=false,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 180,
+ "uvlock": false
+ },
+ "down=true,east=true,north=false,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=false,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 0,
+ "y": 90,
+ "uvlock": false
+ },
+ "down=false,east=true,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_desuw",
+ "x": 90,
+ "y": 0,
+ "uvlock": false
+ },
+ "down=true,east=true,north=true,south=true,up=true,west=true": {
+ "model": "poweradvantage:block/fluid_pipe_densuw",
+ "x": 0,
+ "y": 0,
+ "uvlock": false
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json
index 1becb1c2..c835f911 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_storage_tank.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:fluid_storage_tank", "y": 90 },
- "facing=north": { "model": "poweradvantage:fluid_storage_tank" },
- "facing=south": { "model": "poweradvantage:fluid_storage_tank", "y": 180 },
- "facing=west": { "model": "poweradvantage:fluid_storage_tank", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/fluid_storage_tank",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/fluid_storage_tank"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/fluid_storage_tank",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/fluid_storage_tank",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json b/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json
index a3e5b659..5817285b 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/fluid_switch.json
@@ -1,6 +1,10 @@
-{
- "variants": {
- "active=false": { "model": "poweradvantage:fluid_switch_off"},
- "active=true": { "model": "poweradvantage:fluid_switch_on"}
- }
-}
+{
+ "variants": {
+ "active=false": {
+ "model": "poweradvantage:block/fluid_switch_off"
+ },
+ "active=true": {
+ "model": "poweradvantage:block/fluid_switch_on"
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json b/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json
index 8cc55cc4..e06b9490 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/infinite_electricity.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:infinite_electricity", "y": 90 },
- "facing=north": { "model": "poweradvantage:infinite_electricity" },
- "facing=south": { "model": "poweradvantage:infinite_electricity", "y": 180 },
- "facing=west": { "model": "poweradvantage:infinite_electricity", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/infinite_electricity",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/infinite_electricity"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/infinite_electricity",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/infinite_electricity",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json b/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json
index 7ba2ae4c..1e7ad502 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/infinite_quantum.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:infinite_quantum", "y": 90 },
- "facing=north": { "model": "poweradvantage:infinite_quantum" },
- "facing=south": { "model": "poweradvantage:infinite_quantum", "y": 180 },
- "facing=west": { "model": "poweradvantage:infinite_quantum", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/infinite_quantum",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/infinite_quantum"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/infinite_quantum",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/infinite_quantum",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json b/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json
index ea59a65c..3ad324b6 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/infinite_steam.json
@@ -1,8 +1,19 @@
-{
- "variants": {
- "facing=east": { "model": "poweradvantage:infinite_steam", "y": 90 },
- "facing=north": { "model": "poweradvantage:infinite_steam" },
- "facing=south": { "model": "poweradvantage:infinite_steam", "y": 180 },
- "facing=west": { "model": "poweradvantage:infinite_steam", "y": 270 }
- }
-}
+{
+ "variants": {
+ "facing=east": {
+ "model": "poweradvantage:block/infinite_steam",
+ "y": 90
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/infinite_steam"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/infinite_steam",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/infinite_steam",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json b/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json
index f0008637..93c7ac9e 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_conveyor.json
@@ -1,10 +1,27 @@
{
- "variants": {
- "facing=down": { "model": "poweradvantage:item_conveyor", "x": 90 },
- "facing=up": { "model": "poweradvantage:item_conveyor", "x": 270 },
- "facing=north": { "model": "poweradvantage:item_conveyor" },
- "facing=south": { "model": "poweradvantage:item_conveyor", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_conveyor", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_conveyor", "y": 90 }
+ "variants": {
+ "facing=down": {
+ "model": "poweradvantage:block/item_conveyor",
+ "x": 90
+ },
+ "facing=up": {
+ "model": "poweradvantage:block/item_conveyor",
+ "x": 270
+ },
+ "facing=north": {
+ "model": "poweradvantage:block/item_conveyor"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_conveyor",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_conveyor",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_conveyor",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json
index 9ac27147..39140fb1 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_block.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_block" },
- "facing=south": { "model": "poweradvantage:item_filter_block", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_block", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_block", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_block"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_block",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_block",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_block",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json
index f3d0201d..6cc97515 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_food.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_food" },
- "facing=south": { "model": "poweradvantage:item_filter_food", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_food", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_food", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_food"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_food",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_food",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_food",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json
index e3ddd504..9bbcc350 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_fuel.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_fuel" },
- "facing=south": { "model": "poweradvantage:item_filter_fuel", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_fuel", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_fuel", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_fuel"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_fuel",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_fuel",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_fuel",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json
index cb50460c..ce93778d 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_inventory.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_inventory" },
- "facing=south": { "model": "poweradvantage:item_filter_inventory", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_inventory", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_inventory", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_inventory"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_inventory",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_inventory",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_inventory",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json
index 9983ebcc..402f28c9 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_ore.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_ore" },
- "facing=south": { "model": "poweradvantage:item_filter_ore", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_ore", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_ore", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_ore"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_ore",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_ore",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_ore",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json
index cae43edb..83a02f1c 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_overflow.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_overflow" },
- "facing=south": { "model": "poweradvantage:item_filter_overflow", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_overflow", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_overflow", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_overflow"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_overflow",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_overflow",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_overflow",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json
index ac947ea7..0527268e 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_plant.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_plant" },
- "facing=south": { "model": "poweradvantage:item_filter_plant", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_plant", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_plant", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_plant"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_plant",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_plant",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_plant",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json b/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json
index 072ae292..eb7fb555 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/item_filter_smelt.json
@@ -1,8 +1,19 @@
{
- "variants": {
- "facing=north": { "model": "poweradvantage:item_filter_smelt" },
- "facing=south": { "model": "poweradvantage:item_filter_smelt", "y": 180 },
- "facing=west": { "model": "poweradvantage:item_filter_smelt", "y": 270 },
- "facing=east": { "model": "poweradvantage:item_filter_smelt", "y": 90 }
+ "variants": {
+ "facing=north": {
+ "model": "poweradvantage:block/item_filter_smelt"
+ },
+ "facing=south": {
+ "model": "poweradvantage:block/item_filter_smelt",
+ "y": 180
+ },
+ "facing=west": {
+ "model": "poweradvantage:block/item_filter_smelt",
+ "y": 270
+ },
+ "facing=east": {
+ "model": "poweradvantage:block/item_filter_smelt",
+ "y": 90
}
-}
\ No newline at end of file
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json b/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json
index 9b98029e..80e8102b 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/refined_oil.json
@@ -1,9 +1,5 @@
{
- "forge_marker": 1,
"variants": {
- "fluid": {
- "model": "forge:fluid",
- "custom": { "fluid": "refined_oil" }
- }
+ "": { "model": "poweradvantage:block/refined_oil" }
}
}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json b/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json
index c39fa3b5..6adc29c6 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/steel_frame.json
@@ -1,5 +1,7 @@
-{
- "variants": {
- "normal": { "model": "poweradvantage:steel_frame" }
- }
-}
+{
+ "variants": {
+ "": {
+ "model": "poweradvantage:block/steel_frame"
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/blockstates/still.json b/src/main/resources/assets/poweradvantage/blockstates/still.json
index bcd36cb3..6fe4921b 100644
--- a/src/main/resources/assets/poweradvantage/blockstates/still.json
+++ b/src/main/resources/assets/poweradvantage/blockstates/still.json
@@ -1,12 +1,34 @@
-{
- "variants": {
- "active=false,facing=east": { "model": "poweradvantage:still_off", "y": 90 },
- "active=false,facing=north": { "model": "poweradvantage:still_off"},
- "active=false,facing=south": { "model": "poweradvantage:still_off", "y": 180},
- "active=false,facing=west": { "model": "poweradvantage:still_off", "y": 270},
- "active=true,facing=east": { "model": "poweradvantage:still_on", "y": 90 },
- "active=true,facing=north": { "model": "poweradvantage:still_on"},
- "active=true,facing=south": { "model": "poweradvantage:still_on", "y": 180},
- "active=true,facing=west": { "model": "poweradvantage:still_on", "y": 270}
- }
-}
+{
+ "variants": {
+ "active=false,facing=east": {
+ "model": "poweradvantage:block/still_off",
+ "y": 90
+ },
+ "active=false,facing=north": {
+ "model": "poweradvantage:block/still_off"
+ },
+ "active=false,facing=south": {
+ "model": "poweradvantage:block/still_off",
+ "y": 180
+ },
+ "active=false,facing=west": {
+ "model": "poweradvantage:block/still_off",
+ "y": 270
+ },
+ "active=true,facing=east": {
+ "model": "poweradvantage:block/still_on",
+ "y": 90
+ },
+ "active=true,facing=north": {
+ "model": "poweradvantage:block/still_on"
+ },
+ "active=true,facing=south": {
+ "model": "poweradvantage:block/still_on",
+ "y": 180
+ },
+ "active=true,facing=west": {
+ "model": "poweradvantage:block/still_on",
+ "y": 270
+ }
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/de_de.json b/src/main/resources/assets/poweradvantage/lang/de_de.json
new file mode 100644
index 00000000..bdf24685
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/de_de.json
@@ -0,0 +1,77 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Große Ofen (formlos)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Große Ofen (geformt)",
+ "tooltip.poweradvantage.empty": "Leer",
+ "tooltip.poweradvantage.buckets_of": "%x Eimer %y",
+ "itemGroup.poweradvantage": "Leistungsvorteil",
+ "tile.poweradvantage.fluid_drain.name": "Ablauf",
+ "tile.poweradvantage.fluid_discharge.name": "Austragsrost",
+ "tile.poweradvantage.fluid_storage_tank.name": "Lagertank",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Speicher",
+ "tile.poweradvantage.fluid_pipe.name": "Rohr",
+ "tile.poweradvantage.item_conveyor.name": "Förderer",
+ "tile.poweradvantage.item_filter_block.name": "Block Auswahl Förder",
+ "tile.poweradvantage.item_filter_food.name": "Lebensmittel Auswahl Förder",
+ "tile.poweradvantage.item_filter_fuel.name": "Brennstoff Auswahl Förder",
+ "tile.poweradvantage.item_filter_inventory.name": "Anpassbare Auswahl Förder",
+ "tile.poweradvantage.item_filter_ore.name": "Erz Auswahl Förder",
+ "tile.poweradvantage.item_filter_plant.name": "Pflanze Auswahl Förder",
+ "tile.poweradvantage.item_filter_smelt.name": "Kochbare Auswahl Förder",
+ "tile.poweradvantage.item_filter_overflow.name": "Überlauf Auswahl Förder",
+ "tile.poweradvantage.steel_frame.name": "Stahlrahmen",
+ "item.poweradvantage.starch.name": "Kartoffelstärke",
+ "item.poweradvantage.bioplastic_ingot.name": "Biokunststoff-Barren",
+ "item.poweradvantage.sprocket.name": "Kettenrad",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metall-Lagertank",
+ "item.poweradvantage.rotator_tool.name": "Drehwerkzeug",
+ "tile.poweradvantage.infinite_steam.name": "Unendlichen Dampfkraft",
+ "tile.poweradvantage.infinite_electricity.name": "Unendliche Elektrische Leistung",
+ "tile.poweradvantage.infinite_quantum.name": "Unendliche Quanten Leistung",
+ "tile.poweradvantage.crude_oil.name": "Rohöl",
+ "fluid.poweradvantage.crude_oil": "Rohöl",
+ "item.bucket.crude_oil": "Eimer Rohöl",
+ "tile.poweradvantage.still.name": "Brennerei",
+ "tile.poweradvantage.refined_oil.name": "Raffiniertes Öl",
+ "fluid.poweradvantage.refined_oil": "Raffiniertes Öl",
+ "item.bucket.refined_oil": "Eimer für Raffiniertes Öl",
+ "tile.poweradvantage.fluid_switch.name": "Ventil",
+ "tile.poweradvantage.converter_rf_steam.name": "Dampf-RF Leistungstransformator",
+ "tile.poweradvantage.converter_rf_electricity.name": "Elektrizität-RF Leistungstransformator",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quant-RF Leistungstransformator",
+ "tile.poweradvantage.converter_tr_electricity.name": "Elektrizität-EU Leistungstransformator",
+ "block.poweradvantage.fluid_drain": "Ablauf",
+ "block.poweradvantage.fluid_discharge": "Austragsrost",
+ "block.poweradvantage.fluid_storage_tank": "Lagertank",
+ "item.poweradvantage.fluid_storage_tank_item": "Speicher",
+ "block.poweradvantage.fluid_pipe": "Rohr",
+ "block.poweradvantage.item_conveyor": "Förderer",
+ "block.poweradvantage.item_filter_block": "Block Auswahl Förder",
+ "block.poweradvantage.item_filter_food": "Lebensmittel Auswahl Förder",
+ "block.poweradvantage.item_filter_fuel": "Brennstoff Auswahl Förder",
+ "block.poweradvantage.item_filter_inventory": "Anpassbare Auswahl Förder",
+ "block.poweradvantage.item_filter_ore": "Erz Auswahl Förder",
+ "block.poweradvantage.item_filter_plant": "Pflanze Auswahl Förder",
+ "block.poweradvantage.item_filter_smelt": "Kochbare Auswahl Förder",
+ "block.poweradvantage.item_filter_overflow": "Überlauf Auswahl Förder",
+ "block.poweradvantage.steel_frame": "Stahlrahmen",
+ "item.poweradvantage.starch": "Kartoffelstärke",
+ "item.poweradvantage.bioplastic_ingot": "Biokunststoff-Barren",
+ "item.poweradvantage.sprocket": "Kettenrad",
+ "block.poweradvantage.fluid_metal_tank": "Metall-Lagertank",
+ "item.poweradvantage.rotator_tool": "Drehwerkzeug",
+ "block.poweradvantage.infinite_steam": "Unendlichen Dampfkraft",
+ "block.poweradvantage.infinite_electricity": "Unendliche Elektrische Leistung",
+ "block.poweradvantage.infinite_quantum": "Unendliche Quanten Leistung",
+ "block.poweradvantage.crude_oil": "Rohöl",
+ "block.poweradvantage.still": "Brennerei",
+ "block.poweradvantage.refined_oil": "Raffiniertes Öl",
+ "block.poweradvantage.fluid_switch": "Ventil",
+ "block.poweradvantage.converter_rf_steam": "Dampf-RF Leistungstransformator",
+ "block.poweradvantage.converter_rf_electricity": "Elektrizität-RF Leistungstransformator",
+ "block.poweradvantage.converter_rf_quantum": "Quant-RF Leistungstransformator",
+ "block.poweradvantage.converter_tr_electricity": "Elektrizität-EU Leistungstransformator",
+ "item.poweradvantage.crude_oil_bucket": "Eimer Rohöl",
+ "item.poweradvantage.refined_oil_bucket": "Eimer für Raffiniertes Öl",
+ "block.poweradvantage.fluid_pipe_terminal": "Rohr"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/de_de.lang b/src/main/resources/assets/poweradvantage/lang/de_de.lang
deleted file mode 100644
index 6d09be01..00000000
--- a/src/main/resources/assets/poweradvantage/lang/de_de.lang
+++ /dev/null
@@ -1,55 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Große Ofen (formlos)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Große Ofen (geformt)
-
-tooltip.poweradvantage.empty=Leer
-tooltip.poweradvantage.buckets_of=%x Eimer %y
-
-itemGroup.poweradvantage=Leistungsvorteil
-
-tile.poweradvantage.fluid_drain.name=Ablauf
-tile.poweradvantage.fluid_discharge.name=Austragsrost
-tile.poweradvantage.fluid_storage_tank.name=Lagertank
-item.poweradvantage.fluid_storage_tank_item.name=Speicher
-tile.poweradvantage.fluid_pipe.name=Rohr
-
-tile.poweradvantage.item_conveyor.name=Förderer
-tile.poweradvantage.item_filter_block.name=Block Auswahl Förder
-tile.poweradvantage.item_filter_food.name=Lebensmittel Auswahl Förder
-tile.poweradvantage.item_filter_fuel.name=Brennstoff Auswahl Förder
-tile.poweradvantage.item_filter_inventory.name=Anpassbare Auswahl Förder
-tile.poweradvantage.item_filter_ore.name=Erz Auswahl Förder
-tile.poweradvantage.item_filter_plant.name=Pflanze Auswahl Förder
-tile.poweradvantage.item_filter_smelt.name=Kochbare Auswahl Förder
-tile.poweradvantage.item_filter_overflow.name=Überlauf Auswahl Förder
-tile.poweradvantage.steel_frame.name=Stahlrahmen
-
-
-item.poweradvantage.starch.name=Kartoffelstärke
-item.poweradvantage.bioplastic_ingot.name=Biokunststoff-Barren
-item.poweradvantage.sprocket.name=Kettenrad
-
-
-tile.poweradvantage.fluid_metal_tank.name=Metall-Lagertank
-item.poweradvantage.rotator_tool.name=Drehwerkzeug
-
-tile.poweradvantage.infinite_steam.name=Unendlichen Dampfkraft
-tile.poweradvantage.infinite_electricity.name=Unendliche Elektrische Leistung
-tile.poweradvantage.infinite_quantum.name=Unendliche Quanten Leistung
-
-tile.poweradvantage.crude_oil.name=Rohöl
-fluid.poweradvantage.crude_oil=Rohöl
-item.bucket.crude_oil=Eimer Rohöl
-
-
-tile.poweradvantage.still.name=Brennerei
-
-tile.poweradvantage.refined_oil.name=Raffiniertes Öl
-fluid.poweradvantage.refined_oil=Raffiniertes Öl
-item.bucket.refined_oil=Eimer für Raffiniertes Öl
-tile.poweradvantage.fluid_switch.name=Ventil
-
-tile.poweradvantage.converter_rf_steam.name=Dampf-RF Leistungstransformator
-tile.poweradvantage.converter_rf_electricity.name=Elektrizität-RF Leistungstransformator
-tile.poweradvantage.converter_rf_quantum.name=Quant-RF Leistungstransformator
-tile.poweradvantage.converter_tr_electricity.name=Elektrizität-EU Leistungstransformator
diff --git a/src/main/resources/assets/poweradvantage/lang/en_au.json b/src/main/resources/assets/poweradvantage/lang/en_au.json
new file mode 100644
index 00000000..7b1619fe
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/en_au.json
@@ -0,0 +1,77 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)",
+ "tooltip.poweradvantage.empty": "Empty",
+ "tooltip.poweradvantage.buckets_of": "%x buckets of %y",
+ "itemGroup.poweradvantage": "Power Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Drain",
+ "tile.poweradvantage.fluid_discharge.name": "Discharge Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank",
+ "tile.poweradvantage.fluid_pipe.name": "Pipe",
+ "tile.poweradvantage.item_conveyor.name": "Conveyor",
+ "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor",
+ "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Starch",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool.name": "Rotation Wrench",
+ "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power",
+ "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power",
+ "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power",
+ "tile.poweradvantage.crude_oil.name": "Crude Oil",
+ "fluid.poweradvantage.crude_oil": "Crude Oil",
+ "item.bucket.crude_oil": "Bucket of Crude Oil",
+ "tile.poweradvantage.still.name": "Distillation Furnace",
+ "tile.poweradvantage.refined_oil.name": "Refined Oil",
+ "fluid.poweradvantage.refined_oil": "Refined Oil",
+ "item.bucket.refined_oil": "Bucket of Refined Oil",
+ "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve",
+ "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer",
+ "block.poweradvantage.fluid_drain": "Drain",
+ "block.poweradvantage.fluid_discharge": "Discharge Grate",
+ "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item": "Storage Tank",
+ "block.poweradvantage.fluid_pipe": "Pipe",
+ "block.poweradvantage.item_conveyor": "Conveyor",
+ "block.poweradvantage.item_filter_block": "Block Filter Conveyor",
+ "block.poweradvantage.item_filter_food": "Food Filter Conveyor",
+ "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filter Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Starch",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool": "Rotation Wrench",
+ "block.poweradvantage.infinite_steam": "Unlimited Steam Power",
+ "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power",
+ "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power",
+ "block.poweradvantage.crude_oil": "Crude Oil",
+ "block.poweradvantage.still": "Distillation Furnace",
+ "block.poweradvantage.refined_oil": "Refined Oil",
+ "block.poweradvantage.fluid_switch": "Fluid Switch Valve",
+ "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer",
+ "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer",
+ "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer",
+ "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer",
+ "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil",
+ "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil",
+ "block.poweradvantage.fluid_pipe_terminal": "Pipe"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/en_au.lang b/src/main/resources/assets/poweradvantage/lang/en_au.lang
deleted file mode 100644
index 43ffeb7d..00000000
--- a/src/main/resources/assets/poweradvantage/lang/en_au.lang
+++ /dev/null
@@ -1,54 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Large Furnace (Shapeless)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Large Furnace (Shaped)
-
-tooltip.poweradvantage.empty=Empty
-tooltip.poweradvantage.buckets_of=%x buckets of %y
-
-itemGroup.poweradvantage=Power Advantage
-
-tile.poweradvantage.fluid_drain.name=Drain
-tile.poweradvantage.fluid_discharge.name=Discharge Grate
-tile.poweradvantage.fluid_storage_tank.name=Portable Fluid Tank
-item.poweradvantage.fluid_storage_tank_item.name=Storage Tank
-tile.poweradvantage.fluid_pipe.name=Pipe
-
-tile.poweradvantage.item_conveyor.name=Conveyor
-tile.poweradvantage.item_filter_block.name=Block Filter Conveyor
-tile.poweradvantage.item_filter_food.name=Food Filter Conveyor
-tile.poweradvantage.item_filter_fuel.name=Fuel Filter Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filter Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filter Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filter Conveyor
-tile.poweradvantage.item_filter_smelt.name=Furnace Filter Conveyor
-tile.poweradvantage.item_filter_overflow.name=Overflow Filter Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Starch
-item.poweradvantage.bioplastic_ingot.name=Bioplastic Ingot
-item.poweradvantage.sprocket.name=Sprocket
-
-tile.poweradvantage.fluid_metal_tank.name=Metal Fluid Tank
-item.poweradvantage.rotator_tool.name=Rotation Wrench
-
-tile.poweradvantage.infinite_steam.name=Unlimited Steam Power
-tile.poweradvantage.infinite_electricity.name=Unlimited Electrical Power
-tile.poweradvantage.infinite_quantum.name=Unlimited Quantum Power
-
-tile.poweradvantage.crude_oil.name=Crude Oil
-fluid.poweradvantage.crude_oil=Crude Oil
-item.bucket.crude_oil=Bucket of Crude Oil
-
-
-tile.poweradvantage.still.name=Distillation Furnace
-
-tile.poweradvantage.refined_oil.name=Refined Oil
-fluid.poweradvantage.refined_oil=Refined Oil
-item.bucket.refined_oil=Bucket of Refined Oil
-tile.poweradvantage.fluid_switch.name=Fluid Switch Valve
-
-tile.poweradvantage.converter_rf_steam.name=Steam-RF Transformer
-tile.poweradvantage.converter_rf_electricity.name=Electricity-RF Transformer
-tile.poweradvantage.converter_rf_quantum.name=Quantum-RF Transformer
-tile.poweradvantage.converter_tr_electricity.name=Electricity-EU Transformer
diff --git a/src/main/resources/assets/poweradvantage/lang/en_ca.json b/src/main/resources/assets/poweradvantage/lang/en_ca.json
new file mode 100644
index 00000000..7b1619fe
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/en_ca.json
@@ -0,0 +1,77 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)",
+ "tooltip.poweradvantage.empty": "Empty",
+ "tooltip.poweradvantage.buckets_of": "%x buckets of %y",
+ "itemGroup.poweradvantage": "Power Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Drain",
+ "tile.poweradvantage.fluid_discharge.name": "Discharge Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank",
+ "tile.poweradvantage.fluid_pipe.name": "Pipe",
+ "tile.poweradvantage.item_conveyor.name": "Conveyor",
+ "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor",
+ "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Starch",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool.name": "Rotation Wrench",
+ "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power",
+ "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power",
+ "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power",
+ "tile.poweradvantage.crude_oil.name": "Crude Oil",
+ "fluid.poweradvantage.crude_oil": "Crude Oil",
+ "item.bucket.crude_oil": "Bucket of Crude Oil",
+ "tile.poweradvantage.still.name": "Distillation Furnace",
+ "tile.poweradvantage.refined_oil.name": "Refined Oil",
+ "fluid.poweradvantage.refined_oil": "Refined Oil",
+ "item.bucket.refined_oil": "Bucket of Refined Oil",
+ "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve",
+ "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer",
+ "block.poweradvantage.fluid_drain": "Drain",
+ "block.poweradvantage.fluid_discharge": "Discharge Grate",
+ "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item": "Storage Tank",
+ "block.poweradvantage.fluid_pipe": "Pipe",
+ "block.poweradvantage.item_conveyor": "Conveyor",
+ "block.poweradvantage.item_filter_block": "Block Filter Conveyor",
+ "block.poweradvantage.item_filter_food": "Food Filter Conveyor",
+ "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filter Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Starch",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool": "Rotation Wrench",
+ "block.poweradvantage.infinite_steam": "Unlimited Steam Power",
+ "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power",
+ "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power",
+ "block.poweradvantage.crude_oil": "Crude Oil",
+ "block.poweradvantage.still": "Distillation Furnace",
+ "block.poweradvantage.refined_oil": "Refined Oil",
+ "block.poweradvantage.fluid_switch": "Fluid Switch Valve",
+ "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer",
+ "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer",
+ "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer",
+ "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer",
+ "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil",
+ "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil",
+ "block.poweradvantage.fluid_pipe_terminal": "Pipe"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/en_ca.lang b/src/main/resources/assets/poweradvantage/lang/en_ca.lang
deleted file mode 100644
index 43ffeb7d..00000000
--- a/src/main/resources/assets/poweradvantage/lang/en_ca.lang
+++ /dev/null
@@ -1,54 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Large Furnace (Shapeless)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Large Furnace (Shaped)
-
-tooltip.poweradvantage.empty=Empty
-tooltip.poweradvantage.buckets_of=%x buckets of %y
-
-itemGroup.poweradvantage=Power Advantage
-
-tile.poweradvantage.fluid_drain.name=Drain
-tile.poweradvantage.fluid_discharge.name=Discharge Grate
-tile.poweradvantage.fluid_storage_tank.name=Portable Fluid Tank
-item.poweradvantage.fluid_storage_tank_item.name=Storage Tank
-tile.poweradvantage.fluid_pipe.name=Pipe
-
-tile.poweradvantage.item_conveyor.name=Conveyor
-tile.poweradvantage.item_filter_block.name=Block Filter Conveyor
-tile.poweradvantage.item_filter_food.name=Food Filter Conveyor
-tile.poweradvantage.item_filter_fuel.name=Fuel Filter Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filter Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filter Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filter Conveyor
-tile.poweradvantage.item_filter_smelt.name=Furnace Filter Conveyor
-tile.poweradvantage.item_filter_overflow.name=Overflow Filter Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Starch
-item.poweradvantage.bioplastic_ingot.name=Bioplastic Ingot
-item.poweradvantage.sprocket.name=Sprocket
-
-tile.poweradvantage.fluid_metal_tank.name=Metal Fluid Tank
-item.poweradvantage.rotator_tool.name=Rotation Wrench
-
-tile.poweradvantage.infinite_steam.name=Unlimited Steam Power
-tile.poweradvantage.infinite_electricity.name=Unlimited Electrical Power
-tile.poweradvantage.infinite_quantum.name=Unlimited Quantum Power
-
-tile.poweradvantage.crude_oil.name=Crude Oil
-fluid.poweradvantage.crude_oil=Crude Oil
-item.bucket.crude_oil=Bucket of Crude Oil
-
-
-tile.poweradvantage.still.name=Distillation Furnace
-
-tile.poweradvantage.refined_oil.name=Refined Oil
-fluid.poweradvantage.refined_oil=Refined Oil
-item.bucket.refined_oil=Bucket of Refined Oil
-tile.poweradvantage.fluid_switch.name=Fluid Switch Valve
-
-tile.poweradvantage.converter_rf_steam.name=Steam-RF Transformer
-tile.poweradvantage.converter_rf_electricity.name=Electricity-RF Transformer
-tile.poweradvantage.converter_rf_quantum.name=Quantum-RF Transformer
-tile.poweradvantage.converter_tr_electricity.name=Electricity-EU Transformer
diff --git a/src/main/resources/assets/poweradvantage/lang/en_en.json b/src/main/resources/assets/poweradvantage/lang/en_en.json
new file mode 100644
index 00000000..7b1619fe
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/en_en.json
@@ -0,0 +1,77 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)",
+ "tooltip.poweradvantage.empty": "Empty",
+ "tooltip.poweradvantage.buckets_of": "%x buckets of %y",
+ "itemGroup.poweradvantage": "Power Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Drain",
+ "tile.poweradvantage.fluid_discharge.name": "Discharge Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank",
+ "tile.poweradvantage.fluid_pipe.name": "Pipe",
+ "tile.poweradvantage.item_conveyor.name": "Conveyor",
+ "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor",
+ "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Starch",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool.name": "Rotation Wrench",
+ "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power",
+ "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power",
+ "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power",
+ "tile.poweradvantage.crude_oil.name": "Crude Oil",
+ "fluid.poweradvantage.crude_oil": "Crude Oil",
+ "item.bucket.crude_oil": "Bucket of Crude Oil",
+ "tile.poweradvantage.still.name": "Distillation Furnace",
+ "tile.poweradvantage.refined_oil.name": "Refined Oil",
+ "fluid.poweradvantage.refined_oil": "Refined Oil",
+ "item.bucket.refined_oil": "Bucket of Refined Oil",
+ "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve",
+ "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer",
+ "block.poweradvantage.fluid_drain": "Drain",
+ "block.poweradvantage.fluid_discharge": "Discharge Grate",
+ "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item": "Storage Tank",
+ "block.poweradvantage.fluid_pipe": "Pipe",
+ "block.poweradvantage.item_conveyor": "Conveyor",
+ "block.poweradvantage.item_filter_block": "Block Filter Conveyor",
+ "block.poweradvantage.item_filter_food": "Food Filter Conveyor",
+ "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filter Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Starch",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool": "Rotation Wrench",
+ "block.poweradvantage.infinite_steam": "Unlimited Steam Power",
+ "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power",
+ "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power",
+ "block.poweradvantage.crude_oil": "Crude Oil",
+ "block.poweradvantage.still": "Distillation Furnace",
+ "block.poweradvantage.refined_oil": "Refined Oil",
+ "block.poweradvantage.fluid_switch": "Fluid Switch Valve",
+ "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer",
+ "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer",
+ "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer",
+ "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer",
+ "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil",
+ "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil",
+ "block.poweradvantage.fluid_pipe_terminal": "Pipe"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/en_en.lang b/src/main/resources/assets/poweradvantage/lang/en_en.lang
deleted file mode 100644
index 43ffeb7d..00000000
--- a/src/main/resources/assets/poweradvantage/lang/en_en.lang
+++ /dev/null
@@ -1,54 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Large Furnace (Shapeless)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Large Furnace (Shaped)
-
-tooltip.poweradvantage.empty=Empty
-tooltip.poweradvantage.buckets_of=%x buckets of %y
-
-itemGroup.poweradvantage=Power Advantage
-
-tile.poweradvantage.fluid_drain.name=Drain
-tile.poweradvantage.fluid_discharge.name=Discharge Grate
-tile.poweradvantage.fluid_storage_tank.name=Portable Fluid Tank
-item.poweradvantage.fluid_storage_tank_item.name=Storage Tank
-tile.poweradvantage.fluid_pipe.name=Pipe
-
-tile.poweradvantage.item_conveyor.name=Conveyor
-tile.poweradvantage.item_filter_block.name=Block Filter Conveyor
-tile.poweradvantage.item_filter_food.name=Food Filter Conveyor
-tile.poweradvantage.item_filter_fuel.name=Fuel Filter Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filter Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filter Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filter Conveyor
-tile.poweradvantage.item_filter_smelt.name=Furnace Filter Conveyor
-tile.poweradvantage.item_filter_overflow.name=Overflow Filter Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Starch
-item.poweradvantage.bioplastic_ingot.name=Bioplastic Ingot
-item.poweradvantage.sprocket.name=Sprocket
-
-tile.poweradvantage.fluid_metal_tank.name=Metal Fluid Tank
-item.poweradvantage.rotator_tool.name=Rotation Wrench
-
-tile.poweradvantage.infinite_steam.name=Unlimited Steam Power
-tile.poweradvantage.infinite_electricity.name=Unlimited Electrical Power
-tile.poweradvantage.infinite_quantum.name=Unlimited Quantum Power
-
-tile.poweradvantage.crude_oil.name=Crude Oil
-fluid.poweradvantage.crude_oil=Crude Oil
-item.bucket.crude_oil=Bucket of Crude Oil
-
-
-tile.poweradvantage.still.name=Distillation Furnace
-
-tile.poweradvantage.refined_oil.name=Refined Oil
-fluid.poweradvantage.refined_oil=Refined Oil
-item.bucket.refined_oil=Bucket of Refined Oil
-tile.poweradvantage.fluid_switch.name=Fluid Switch Valve
-
-tile.poweradvantage.converter_rf_steam.name=Steam-RF Transformer
-tile.poweradvantage.converter_rf_electricity.name=Electricity-RF Transformer
-tile.poweradvantage.converter_rf_quantum.name=Quantum-RF Transformer
-tile.poweradvantage.converter_tr_electricity.name=Electricity-EU Transformer
diff --git a/src/main/resources/assets/poweradvantage/lang/en_gb.json b/src/main/resources/assets/poweradvantage/lang/en_gb.json
new file mode 100644
index 00000000..7b1619fe
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/en_gb.json
@@ -0,0 +1,77 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)",
+ "tooltip.poweradvantage.empty": "Empty",
+ "tooltip.poweradvantage.buckets_of": "%x buckets of %y",
+ "itemGroup.poweradvantage": "Power Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Drain",
+ "tile.poweradvantage.fluid_discharge.name": "Discharge Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank",
+ "tile.poweradvantage.fluid_pipe.name": "Pipe",
+ "tile.poweradvantage.item_conveyor.name": "Conveyor",
+ "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor",
+ "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Starch",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool.name": "Rotation Wrench",
+ "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power",
+ "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power",
+ "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power",
+ "tile.poweradvantage.crude_oil.name": "Crude Oil",
+ "fluid.poweradvantage.crude_oil": "Crude Oil",
+ "item.bucket.crude_oil": "Bucket of Crude Oil",
+ "tile.poweradvantage.still.name": "Distillation Furnace",
+ "tile.poweradvantage.refined_oil.name": "Refined Oil",
+ "fluid.poweradvantage.refined_oil": "Refined Oil",
+ "item.bucket.refined_oil": "Bucket of Refined Oil",
+ "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve",
+ "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer",
+ "block.poweradvantage.fluid_drain": "Drain",
+ "block.poweradvantage.fluid_discharge": "Discharge Grate",
+ "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item": "Storage Tank",
+ "block.poweradvantage.fluid_pipe": "Pipe",
+ "block.poweradvantage.item_conveyor": "Conveyor",
+ "block.poweradvantage.item_filter_block": "Block Filter Conveyor",
+ "block.poweradvantage.item_filter_food": "Food Filter Conveyor",
+ "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filter Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Starch",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool": "Rotation Wrench",
+ "block.poweradvantage.infinite_steam": "Unlimited Steam Power",
+ "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power",
+ "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power",
+ "block.poweradvantage.crude_oil": "Crude Oil",
+ "block.poweradvantage.still": "Distillation Furnace",
+ "block.poweradvantage.refined_oil": "Refined Oil",
+ "block.poweradvantage.fluid_switch": "Fluid Switch Valve",
+ "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer",
+ "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer",
+ "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer",
+ "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer",
+ "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil",
+ "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil",
+ "block.poweradvantage.fluid_pipe_terminal": "Pipe"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/en_gb.lang b/src/main/resources/assets/poweradvantage/lang/en_gb.lang
deleted file mode 100644
index c534462f..00000000
--- a/src/main/resources/assets/poweradvantage/lang/en_gb.lang
+++ /dev/null
@@ -1,53 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Large Furnace (Shapeless)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Large Furnace (Shaped)
-
-tooltip.poweradvantage.empty=Empty
-tooltip.poweradvantage.buckets_of=%x buckets of %y
-
-itemGroup.poweradvantage=Power Advantage
-
-tile.poweradvantage.fluid_drain.name=Drain
-tile.poweradvantage.fluid_discharge.name=Discharge Grate
-tile.poweradvantage.fluid_storage_tank.name=Portable Fluid Tank
-item.poweradvantage.fluid_storage_tank_item.name=Storage Tank
-tile.poweradvantage.fluid_pipe.name=Pipe
-
-tile.poweradvantage.item_conveyor.name=Conveyor
-tile.poweradvantage.item_filter_block.name=Block Filter Conveyor
-tile.poweradvantage.item_filter_food.name=Food Filter Conveyor
-tile.poweradvantage.item_filter_fuel.name=Fuel Filter Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filter Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filter Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filter Conveyor
-tile.poweradvantage.item_filter_smelt.name=Furnace Filter Conveyor
-tile.poweradvantage.item_filter_overflow.name=Overflow Filter Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Starch
-item.poweradvantage.bioplastic_ingot.name=Bioplastic Ingot
-item.poweradvantage.sprocket.name=Sprocket
-
-tile.poweradvantage.fluid_metal_tank.name=Metal Fluid Tank
-item.poweradvantage.rotator_tool.name=Rotation Wrench
-
-tile.poweradvantage.infinite_steam.name=Unlimited Steam Power
-tile.poweradvantage.infinite_electricity.name=Unlimited Electrical Power
-tile.poweradvantage.infinite_quantum.name=Unlimited Quantum Power
-
-tile.poweradvantage.crude_oil.name=Crude Oil
-fluid.poweradvantage.crude_oil=Crude Oil
-item.bucket.crude_oil=Bucket of Crude Oil
-
-tile.poweradvantage.still.name=Distillation Furnace
-
-tile.poweradvantage.refined_oil.name=Refined Oil
-fluid.poweradvantage.refined_oil=Refined Oil
-item.bucket.refined_oil=Bucket of Refined Oil
-tile.poweradvantage.fluid_switch.name=Fluid Switch Valve
-
-tile.poweradvantage.converter_rf_steam.name=Steam-RF Transformer
-tile.poweradvantage.converter_rf_electricity.name=Electricity-RF Transformer
-tile.poweradvantage.converter_rf_quantum.name=Quantum-RF Transformer
-tile.poweradvantage.converter_tr_electricity.name=Electricity-EU Transformer
diff --git a/src/main/resources/assets/poweradvantage/lang/en_pt.json b/src/main/resources/assets/poweradvantage/lang/en_pt.json
new file mode 100644
index 00000000..0c78c2f9
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/en_pt.json
@@ -0,0 +1,77 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)",
+ "tooltip.poweradvantage.empty": "Empty",
+ "tooltip.poweradvantage.buckets_of": "%x buckets of %y",
+ "itemGroup.poweradvantage": "Power Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Drain",
+ "tile.poweradvantage.fluid_discharge.name": "Discharge Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Storage Tank",
+ "tile.poweradvantage.fluid_pipe.name": "Pipe",
+ "tile.poweradvantage.item_conveyor.name": "Conveyor",
+ "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor",
+ "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Starch",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool.name": "Rotation Wrench",
+ "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power",
+ "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power",
+ "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power",
+ "tile.poweradvantage.crude_oil.name": "Black Gold",
+ "fluid.poweradvantage.crude_oil": "Black Gold",
+ "item.bucket.crude_oil": "Bucket o' Black Gold",
+ "tile.poweradvantage.still.name": "'Stillery",
+ "tile.poweradvantage.refined_oil.name": "Lamp Oil",
+ "fluid.poweradvantage.refined_oil": "Lamp Oil",
+ "item.bucket.refined_oil": "Bucket o' Lamp Oil",
+ "tile.poweradvantage.fluid_switch.name": "Big Red Valve",
+ "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer",
+ "block.poweradvantage.fluid_drain": "Drain",
+ "block.poweradvantage.fluid_discharge": "Discharge Grate",
+ "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank",
+ "item.poweradvantage.fluid_storage_tank_item": "Storage Tank",
+ "block.poweradvantage.fluid_pipe": "Pipe",
+ "block.poweradvantage.item_conveyor": "Conveyor",
+ "block.poweradvantage.item_filter_block": "Block Filter Conveyor",
+ "block.poweradvantage.item_filter_food": "Food Filter Conveyor",
+ "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filter Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Starch",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool": "Rotation Wrench",
+ "block.poweradvantage.infinite_steam": "Unlimited Steam Power",
+ "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power",
+ "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power",
+ "block.poweradvantage.crude_oil": "Black Gold",
+ "block.poweradvantage.still": "'Stillery",
+ "block.poweradvantage.refined_oil": "Lamp Oil",
+ "block.poweradvantage.fluid_switch": "Big Red Valve",
+ "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer",
+ "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer",
+ "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer",
+ "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer",
+ "item.poweradvantage.crude_oil_bucket": "Bucket o' Black Gold",
+ "item.poweradvantage.refined_oil_bucket": "Bucket o' Lamp Oil",
+ "block.poweradvantage.fluid_pipe_terminal": "Pipe"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/en_pt.lang b/src/main/resources/assets/poweradvantage/lang/en_pt.lang
deleted file mode 100644
index 2e39647c..00000000
--- a/src/main/resources/assets/poweradvantage/lang/en_pt.lang
+++ /dev/null
@@ -1,53 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Large Furnace (Shapeless)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Large Furnace (Shaped)
-
-tooltip.poweradvantage.empty=Empty
-tooltip.poweradvantage.buckets_of=%x buckets of %y
-
-itemGroup.poweradvantage=Power Advantage
-
-tile.poweradvantage.fluid_drain.name=Drain
-tile.poweradvantage.fluid_discharge.name=Discharge Grate
-tile.poweradvantage.fluid_storage_tank.name=Portable Fluid Tank
-item.poweradvantage.fluid_storage_tank_item.name=Storage Tank
-tile.poweradvantage.fluid_pipe.name=Pipe
-
-tile.poweradvantage.item_conveyor.name=Conveyor
-tile.poweradvantage.item_filter_block.name=Block Filter Conveyor
-tile.poweradvantage.item_filter_food.name=Food Filter Conveyor
-tile.poweradvantage.item_filter_fuel.name=Fuel Filter Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filter Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filter Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filter Conveyor
-tile.poweradvantage.item_filter_smelt.name=Furnace Filter Conveyor
-tile.poweradvantage.item_filter_overflow.name=Overflow Filter Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Starch
-item.poweradvantage.bioplastic_ingot.name=Bioplastic Ingot
-item.poweradvantage.sprocket.name=Sprocket
-
-tile.poweradvantage.fluid_metal_tank.name=Metal Fluid Tank
-item.poweradvantage.rotator_tool.name=Rotation Wrench
-
-tile.poweradvantage.infinite_steam.name=Unlimited Steam Power
-tile.poweradvantage.infinite_electricity.name=Unlimited Electrical Power
-tile.poweradvantage.infinite_quantum.name=Unlimited Quantum Power
-
-tile.poweradvantage.crude_oil.name=Black Gold
-fluid.poweradvantage.crude_oil=Black Gold
-item.bucket.crude_oil=Bucket o' Black Gold
-
-tile.poweradvantage.still.name='Stillery
-
-tile.poweradvantage.refined_oil.name=Lamp Oil
-fluid.poweradvantage.refined_oil=Lamp Oil
-item.bucket.refined_oil=Bucket o' Lamp Oil
-tile.poweradvantage.fluid_switch.name=Big Red Valve
-
-tile.poweradvantage.converter_rf_steam.name=Steam-RF Transformer
-tile.poweradvantage.converter_rf_electricity.name=Electricity-RF Transformer
-tile.poweradvantage.converter_rf_quantum.name=Quantum-RF Transformer
-tile.poweradvantage.converter_tr_electricity.name=Electricity-EU Transformer
diff --git a/src/main/resources/assets/poweradvantage/lang/en_us.json b/src/main/resources/assets/poweradvantage/lang/en_us.json
new file mode 100644
index 00000000..76a243b9
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/en_us.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Large Furnace (Shapeless)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Large Furnace (Shaped)",
+ "tooltip.poweradvantage.empty": "Empty",
+ "tooltip.poweradvantage.buckets_of": "%x buckets of %y",
+ "itemGroup.poweradvantage": "Power Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Drain",
+ "tile.poweradvantage.fluid_discharge.name": "Discharge Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Portable Fluid Tank",
+ "tile.poweradvantage.fluid_pipe.name": "Pipe",
+ "tile.poweradvantage.item_conveyor.name": "Conveyor",
+ "tile.poweradvantage.item_filter_block.name": "Block Filter Conveyor",
+ "tile.poweradvantage.item_filter_food.name": "Food Filter Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "Fuel Filter Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filter Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filter Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filter Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Furnace Filter Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Overflow Filter Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Starch",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool.name": "Rotation Wrench",
+ "tile.poweradvantage.infinite_steam.name": "Unlimited Steam Power",
+ "tile.poweradvantage.infinite_electricity.name": "Unlimited Electrical Power",
+ "tile.poweradvantage.infinite_quantum.name": "Unlimited Quantum Power",
+ "tile.poweradvantage.crude_oil.name": "Crude Oil",
+ "fluid.poweradvantage.crude_oil": "Crude Oil",
+ "item.bucket.crude_oil": "Bucket of Crude Oil",
+ "tile.poweradvantage.still.name": "Distillation Furnace",
+ "tile.poweradvantage.refined_oil.name": "Refined Oil",
+ "fluid.poweradvantage.refined_oil": "Refined Oil",
+ "item.bucket.refined_oil": "Bucket of Refined Oil",
+ "tile.poweradvantage.fluid_switch.name": "Fluid Switch Valve",
+ "tile.poweradvantage.converter_rf_steam.name": "Steam-RF Transformer",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricity-RF Transformer",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantum-RF Transformer",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricity-EU Transformer",
+ "block.poweradvantage.fluid_drain": "Drain",
+ "block.poweradvantage.fluid_discharge": "Discharge Grate",
+ "block.poweradvantage.fluid_storage_tank": "Portable Fluid Tank",
+ "block.poweradvantage.fluid_pipe": "Pipe",
+ "block.poweradvantage.item_conveyor": "Conveyor",
+ "block.poweradvantage.item_filter_block": "Block Filter Conveyor",
+ "block.poweradvantage.item_filter_food": "Food Filter Conveyor",
+ "block.poweradvantage.item_filter_fuel": "Fuel Filter Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filter Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filter Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filter Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Furnace Filter Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Overflow Filter Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Starch",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic Ingot",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Metal Fluid Tank",
+ "item.poweradvantage.rotator_tool": "Rotation Wrench",
+ "block.poweradvantage.infinite_steam": "Unlimited Steam Power",
+ "block.poweradvantage.infinite_electricity": "Unlimited Electrical Power",
+ "block.poweradvantage.infinite_quantum": "Unlimited Quantum Power",
+ "block.poweradvantage.crude_oil": "Crude Oil",
+ "block.poweradvantage.still": "Distillation Furnace",
+ "block.poweradvantage.refined_oil": "Refined Oil",
+ "block.poweradvantage.fluid_switch": "Fluid Switch Valve",
+ "block.poweradvantage.converter_rf_steam": "Steam-RF Transformer",
+ "block.poweradvantage.converter_rf_electricity": "Electricity-RF Transformer",
+ "block.poweradvantage.converter_rf_quantum": "Quantum-RF Transformer",
+ "block.poweradvantage.converter_tr_electricity": "Electricity-EU Transformer",
+ "item.poweradvantage.crude_oil_bucket": "Bucket of Crude Oil",
+ "item.poweradvantage.refined_oil_bucket": "Bucket of Refined Oil",
+ "block.poweradvantage.fluid_pipe_terminal": "Pipe"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/en_us.lang b/src/main/resources/assets/poweradvantage/lang/en_us.lang
deleted file mode 100644
index 9f6054a1..00000000
--- a/src/main/resources/assets/poweradvantage/lang/en_us.lang
+++ /dev/null
@@ -1,52 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Large Furnace (Shapeless)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Large Furnace (Shaped)
-
-tooltip.poweradvantage.empty=Empty
-tooltip.poweradvantage.buckets_of=%x buckets of %y
-
-itemGroup.poweradvantage=Power Advantage
-
-tile.poweradvantage.fluid_drain.name=Drain
-tile.poweradvantage.fluid_discharge.name=Discharge Grate
-tile.poweradvantage.fluid_storage_tank.name=Portable Fluid Tank
-tile.poweradvantage.fluid_pipe.name=Pipe
-
-tile.poweradvantage.item_conveyor.name=Conveyor
-tile.poweradvantage.item_filter_block.name=Block Filter Conveyor
-tile.poweradvantage.item_filter_food.name=Food Filter Conveyor
-tile.poweradvantage.item_filter_fuel.name=Fuel Filter Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filter Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filter Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filter Conveyor
-tile.poweradvantage.item_filter_smelt.name=Furnace Filter Conveyor
-tile.poweradvantage.item_filter_overflow.name=Overflow Filter Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Starch
-item.poweradvantage.bioplastic_ingot.name=Bioplastic Ingot
-item.poweradvantage.sprocket.name=Sprocket
-
-
-tile.poweradvantage.fluid_metal_tank.name=Metal Fluid Tank
-item.poweradvantage.rotator_tool.name=Rotation Wrench
-
-tile.poweradvantage.infinite_steam.name=Unlimited Steam Power
-tile.poweradvantage.infinite_electricity.name=Unlimited Electrical Power
-tile.poweradvantage.infinite_quantum.name=Unlimited Quantum Power
-
-tile.poweradvantage.crude_oil.name=Crude Oil
-fluid.poweradvantage.crude_oil=Crude Oil
-item.bucket.crude_oil=Bucket of Crude Oil
-
-tile.poweradvantage.still.name=Distillation Furnace
-tile.poweradvantage.refined_oil.name=Refined Oil
-fluid.poweradvantage.refined_oil=Refined Oil
-item.bucket.refined_oil=Bucket of Refined Oil
-tile.poweradvantage.fluid_switch.name=Fluid Switch Valve
-
-tile.poweradvantage.converter_rf_steam.name=Steam-RF Transformer
-tile.poweradvantage.converter_rf_electricity.name=Electricity-RF Transformer
-tile.poweradvantage.converter_rf_quantum.name=Quantum-RF Transformer
-tile.poweradvantage.converter_tr_electricity.name=Electricity-EU Transformer
diff --git a/src/main/resources/assets/poweradvantage/lang/es_es.json b/src/main/resources/assets/poweradvantage/lang/es_es.json
new file mode 100644
index 00000000..68471f7c
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/es_es.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Gran Horno (no forma)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Gran Horno (en forma)",
+ "tooltip.poweradvantage.empty": "Vacío",
+ "tooltip.poweradvantage.buckets_of ": "%x cubos de %y",
+ "itemGroup.poweradvantage": "Ventaja de Energía",
+ "tile.poweradvantage.fluid_drain.name": "Drenaje",
+ "tile.poweradvantage.fluid_discharge.name": "Descarga Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Tanque de almacenamiento",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Tanque de almacenamiento",
+ "tile.poweradvantage.fluid_pipe.name": "Tubo",
+ "tile.poweradvantage.item_conveyor.name": "Transportadores",
+ "tile.poweradvantage.item_filter_block.name": "Bloquear Filtro Transportadores",
+ "tile.poweradvantage.item_filter_food.name": "Filtro Alimentos Transportadores",
+ "tile.poweradvantage.item_filter_fuel.name": "Filtro de combustible Transportadores",
+ "tile.poweradvantage.item_filter_inventory.name": "Filtro Transportadores",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Transportadores Filtro",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Transportadores Filtro",
+ "tile.poweradvantage.item_filter_smelt.name": "Horno Filtro Transportadores",
+ "tile.poweradvantage.item_filter_overflow.name": "Desbordamiento Filtro Transportadores",
+ "tile.poweradvantage.steel_frame.name": "Marco de Acero",
+ "item.poweradvantage.starch.name": "Almidón",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplástico Lingote",
+ "item.poweradvantage.sprocket.name": "Piñón",
+ "tile.poweradvantage.fluid_metal_tank.name": "Tanque de Almacenamiento de Acero",
+ "item.poweradvantage.rotator_tool.name": "Herramienta de Rotación",
+ "tile.poweradvantage.infinite_steam.name": "La Energía de Vapor Infinito",
+ "tile.poweradvantage.infinite_electricity.name": "Energía Eléctrica Infinito",
+ "tile.poweradvantage.infinite_quantum.name": "Cuántica Infinito",
+ "tile.poweradvantage.crude_oil.name": "Petróleo Crudo",
+ "fluid.poweradvantage.crude_oil": "Petróleo Crudo",
+ "item.bucket.crude_oil.name": "Cubo de Crudo",
+ "tile.poweradvantage.still.name": "Destilería",
+ "tile.poweradvantage.refined_oil.name": "Refinado de Petróleo",
+ "fluid.poweradvantage.refined_oil": "Refinado de Petróleo",
+ "item.bucket.refined_oil.name": "Cubo de Refinado de Petróleo",
+ "tile.poweradvantage.fluid_switch.name": "Válvula",
+ "tile.poweradvantage.converter_rf_steam.name": "Vapor-RF Transformador",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricidad-RF Transformador",
+ "tile.poweradvantage.converter_rf_quantum.name": "Cuántico-RF Transformador",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricidad-EU Transformador",
+ "block.poweradvantage.fluid_drain": "Drenaje",
+ "block.poweradvantage.fluid_discharge": "Descarga Grate",
+ "block.poweradvantage.fluid_storage_tank": "Tanque de almacenamiento",
+ "item.poweradvantage.fluid_storage_tank_item": "Tanque de almacenamiento",
+ "block.poweradvantage.fluid_pipe": "Tubo",
+ "block.poweradvantage.item_conveyor": "Transportadores",
+ "block.poweradvantage.item_filter_block": "Bloquear Filtro Transportadores",
+ "block.poweradvantage.item_filter_food": "Filtro Alimentos Transportadores",
+ "block.poweradvantage.item_filter_fuel": "Filtro de combustible Transportadores",
+ "block.poweradvantage.item_filter_inventory": "Filtro Transportadores",
+ "block.poweradvantage.item_filter_ore": "Ore Transportadores Filtro",
+ "block.poweradvantage.item_filter_plant": "Plantable Transportadores Filtro",
+ "block.poweradvantage.item_filter_smelt": "Horno Filtro Transportadores",
+ "block.poweradvantage.item_filter_overflow": "Desbordamiento Filtro Transportadores",
+ "block.poweradvantage.steel_frame": "Marco de Acero",
+ "item.poweradvantage.starch": "Almidón",
+ "item.poweradvantage.bioplastic_ingot": "Bioplástico Lingote",
+ "item.poweradvantage.sprocket": "Piñón",
+ "block.poweradvantage.fluid_metal_tank": "Tanque de Almacenamiento de Acero",
+ "item.poweradvantage.rotator_tool": "Herramienta de Rotación",
+ "block.poweradvantage.infinite_steam": "La Energía de Vapor Infinito",
+ "block.poweradvantage.infinite_electricity": "Energía Eléctrica Infinito",
+ "block.poweradvantage.infinite_quantum": "Cuántica Infinito",
+ "block.poweradvantage.crude_oil": "Petróleo Crudo",
+ "block.poweradvantage.still": "Destilería",
+ "block.poweradvantage.refined_oil": "Refinado de Petróleo",
+ "block.poweradvantage.fluid_switch": "Válvula",
+ "block.poweradvantage.converter_rf_steam": "Vapor-RF Transformador",
+ "block.poweradvantage.converter_rf_electricity": "Electricidad-RF Transformador",
+ "block.poweradvantage.converter_rf_quantum": "Cuántico-RF Transformador",
+ "block.poweradvantage.converter_tr_electricity": "Electricidad-EU Transformador",
+ "block.poweradvantage.fluid_pipe_terminal": "Tubo"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/es_es.lang b/src/main/resources/assets/poweradvantage/lang/es_es.lang
deleted file mode 100644
index 3a1bb88b..00000000
--- a/src/main/resources/assets/poweradvantage/lang/es_es.lang
+++ /dev/null
@@ -1,56 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Gran Horno (no forma)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Gran Horno (en forma)
-
-tooltip.poweradvantage.empty=Vacío
-tooltip.poweradvantage.buckets_of =%x cubos de %y
-
-itemGroup.poweradvantage=Ventaja de Energía
-
-tile.poweradvantage.fluid_drain.name=Drenaje
-tile.poweradvantage.fluid_discharge.name=Descarga Grate
-tile.poweradvantage.fluid_storage_tank.name=Tanque de almacenamiento
-item.poweradvantage.fluid_storage_tank_item.name=Tanque de almacenamiento
-tile.poweradvantage.fluid_pipe.name=Tubo
-
-tile.poweradvantage.item_conveyor.name=Transportadores
-tile.poweradvantage.item_filter_block.name=Bloquear Filtro Transportadores
-tile.poweradvantage.item_filter_food.name=Filtro Alimentos Transportadores
-tile.poweradvantage.item_filter_fuel.name=Filtro de combustible Transportadores
-tile.poweradvantage.item_filter_inventory.name=Filtro Transportadores
-tile.poweradvantage.item_filter_ore.name=Ore Transportadores Filtro
-tile.poweradvantage.item_filter_plant.name=Plantable Transportadores Filtro
-tile.poweradvantage.item_filter_smelt.name=Horno Filtro Transportadores
-tile.poweradvantage.item_filter_overflow.name=Desbordamiento Filtro Transportadores
-tile.poweradvantage.steel_frame.name=Marco de Acero
-
-
-item.poweradvantage.starch.name=Almidón
-item.poweradvantage.bioplastic_ingot.name=Bioplástico Lingote
-item.poweradvantage.sprocket.name=Piñón
-
-
-tile.poweradvantage.fluid_metal_tank.name=Tanque de Almacenamiento de Acero
-item.poweradvantage.rotator_tool.name=Herramienta de Rotación
-
-tile.poweradvantage.infinite_steam.name=La Energía de Vapor Infinito
-tile.poweradvantage.infinite_electricity.name=Energía Eléctrica Infinito
-tile.poweradvantage.infinite_quantum.name=Cuántica Infinito
-
-tile.poweradvantage.crude_oil.name=Petróleo Crudo
-fluid.poweradvantage.crude_oil=Petróleo Crudo
-item.bucket.crude_oil.name=Cubo de Crudo
-
-
-tile.poweradvantage.still.name=Destilería
-
-tile.poweradvantage.refined_oil.name=Refinado de Petróleo
-fluid.poweradvantage.refined_oil=Refinado de Petróleo
-item.bucket.refined_oil.name=Cubo de Refinado de Petróleo
-tile.poweradvantage.fluid_switch.name=Válvula
-
-
-tile.poweradvantage.converter_rf_steam.name=Vapor-RF Transformador
-tile.poweradvantage.converter_rf_electricity.name=Electricidad-RF Transformador
-tile.poweradvantage.converter_rf_quantum.name=Cuántico-RF Transformador
-tile.poweradvantage.converter_tr_electricity.name=Electricidad-EU Transformador
diff --git a/src/main/resources/assets/poweradvantage/lang/es_mx.json b/src/main/resources/assets/poweradvantage/lang/es_mx.json
new file mode 100644
index 00000000..c0c5cb2e
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/es_mx.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Gran Horno (no forma)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Gran Horno (en forma)",
+ "tooltip.poweradvantage.empty": "Vacío",
+ "tooltip.poweradvantage.buckets_of": "%x cubos de %y",
+ "itemGroup.poweradvantage": "Ventaja de Energía",
+ "tile.poweradvantage.fluid_drain.name": "Drenaje",
+ "tile.poweradvantage.fluid_discharge.name": "Descarga Grate",
+ "tile.poweradvantage.fluid_storage_tank.name": "Tanque de almacenamiento",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Tanque de almacenamiento",
+ "tile.poweradvantage.fluid_pipe.name": "Tubo",
+ "tile.poweradvantage.item_conveyor.name": "Transportadores",
+ "tile.poweradvantage.item_filter_block.name": "Bloquear Filtro Transportadores",
+ "tile.poweradvantage.item_filter_food.name": "Filtro Alimentos Transportadores",
+ "tile.poweradvantage.item_filter_fuel.name": "Filtro de combustible Transportadores",
+ "tile.poweradvantage.item_filter_inventory.name": "Filtro Transportadores",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Transportadores Filtro",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Transportadores Filtro",
+ "tile.poweradvantage.item_filter_smelt.name": "Horno Filtro Transportadores",
+ "tile.poweradvantage.item_filter_overflow.name": "Desbordamiento Filtro Transportadores",
+ "tile.poweradvantage.steel_frame.name": "Marco de Acero",
+ "item.poweradvantage.starch.name": "Almidón",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplástico Lingote",
+ "item.poweradvantage.sprocket.name": "Piñón",
+ "tile.poweradvantage.fluid_metal_tank.name": "Tanque de Almacenamiento de Acero",
+ "item.poweradvantage.rotator_tool.name": "Herramienta de Rotación",
+ "tile.poweradvantage.infinite_steam.name": "La Energía de Vapor Infinito",
+ "tile.poweradvantage.infinite_electricity.name": "Energía Eléctrica Infinito",
+ "tile.poweradvantage.infinite_quantum.name": "Cuántica Infinito",
+ "tile.poweradvantage.crude_oil.name": "Petróleo Crudo",
+ "fluid.poweradvantage.crude_oil": "Petróleo Crudo",
+ "item.bucket.crude_oil.name": "Cubo de Crudo",
+ "tile.poweradvantage.still.name": "Destilería",
+ "tile.poweradvantage.refined_oil.name": "Refinado de Petróleo",
+ "fluid.poweradvantage.refined_oil": "Refinado de Petróleo",
+ "item.bucket.refined_oil.name": "Cubo de Refinado de Petróleo",
+ "tile.poweradvantage.fluid_switch.name": "Válvula",
+ "tile.poweradvantage.converter_rf_steam.name": "Vapor-RF Transformador",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricidad-RF Transformador",
+ "tile.poweradvantage.converter_rf_quantum.name": "Cuántico-RF Transformador",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricidad-EU Transformador",
+ "block.poweradvantage.fluid_drain": "Drenaje",
+ "block.poweradvantage.fluid_discharge": "Descarga Grate",
+ "block.poweradvantage.fluid_storage_tank": "Tanque de almacenamiento",
+ "item.poweradvantage.fluid_storage_tank_item": "Tanque de almacenamiento",
+ "block.poweradvantage.fluid_pipe": "Tubo",
+ "block.poweradvantage.item_conveyor": "Transportadores",
+ "block.poweradvantage.item_filter_block": "Bloquear Filtro Transportadores",
+ "block.poweradvantage.item_filter_food": "Filtro Alimentos Transportadores",
+ "block.poweradvantage.item_filter_fuel": "Filtro de combustible Transportadores",
+ "block.poweradvantage.item_filter_inventory": "Filtro Transportadores",
+ "block.poweradvantage.item_filter_ore": "Ore Transportadores Filtro",
+ "block.poweradvantage.item_filter_plant": "Plantable Transportadores Filtro",
+ "block.poweradvantage.item_filter_smelt": "Horno Filtro Transportadores",
+ "block.poweradvantage.item_filter_overflow": "Desbordamiento Filtro Transportadores",
+ "block.poweradvantage.steel_frame": "Marco de Acero",
+ "item.poweradvantage.starch": "Almidón",
+ "item.poweradvantage.bioplastic_ingot": "Bioplástico Lingote",
+ "item.poweradvantage.sprocket": "Piñón",
+ "block.poweradvantage.fluid_metal_tank": "Tanque de Almacenamiento de Acero",
+ "item.poweradvantage.rotator_tool": "Herramienta de Rotación",
+ "block.poweradvantage.infinite_steam": "La Energía de Vapor Infinito",
+ "block.poweradvantage.infinite_electricity": "Energía Eléctrica Infinito",
+ "block.poweradvantage.infinite_quantum": "Cuántica Infinito",
+ "block.poweradvantage.crude_oil": "Petróleo Crudo",
+ "block.poweradvantage.still": "Destilería",
+ "block.poweradvantage.refined_oil": "Refinado de Petróleo",
+ "block.poweradvantage.fluid_switch": "Válvula",
+ "block.poweradvantage.converter_rf_steam": "Vapor-RF Transformador",
+ "block.poweradvantage.converter_rf_electricity": "Electricidad-RF Transformador",
+ "block.poweradvantage.converter_rf_quantum": "Cuántico-RF Transformador",
+ "block.poweradvantage.converter_tr_electricity": "Electricidad-EU Transformador",
+ "block.poweradvantage.fluid_pipe_terminal": "Tubo"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/es_mx.lang b/src/main/resources/assets/poweradvantage/lang/es_mx.lang
deleted file mode 100644
index 749917d1..00000000
--- a/src/main/resources/assets/poweradvantage/lang/es_mx.lang
+++ /dev/null
@@ -1,56 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Gran Horno (no forma)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Gran Horno (en forma)
-
-tooltip.poweradvantage.empty=Vacío
-tooltip.poweradvantage.buckets_of=%x cubos de %y
-
-itemGroup.poweradvantage=Ventaja de Energía
-
-tile.poweradvantage.fluid_drain.name=Drenaje
-tile.poweradvantage.fluid_discharge.name=Descarga Grate
-tile.poweradvantage.fluid_storage_tank.name=Tanque de almacenamiento
-item.poweradvantage.fluid_storage_tank_item.name=Tanque de almacenamiento
-tile.poweradvantage.fluid_pipe.name=Tubo
-
-tile.poweradvantage.item_conveyor.name=Transportadores
-tile.poweradvantage.item_filter_block.name=Bloquear Filtro Transportadores
-tile.poweradvantage.item_filter_food.name=Filtro Alimentos Transportadores
-tile.poweradvantage.item_filter_fuel.name=Filtro de combustible Transportadores
-tile.poweradvantage.item_filter_inventory.name=Filtro Transportadores
-tile.poweradvantage.item_filter_ore.name=Ore Transportadores Filtro
-tile.poweradvantage.item_filter_plant.name=Plantable Transportadores Filtro
-tile.poweradvantage.item_filter_smelt.name=Horno Filtro Transportadores
-tile.poweradvantage.item_filter_overflow.name=Desbordamiento Filtro Transportadores
-tile.poweradvantage.steel_frame.name=Marco de Acero
-
-
-item.poweradvantage.starch.name=Almidón
-item.poweradvantage.bioplastic_ingot.name=Bioplástico Lingote
-item.poweradvantage.sprocket.name=Piñón
-
-
-tile.poweradvantage.fluid_metal_tank.name=Tanque de Almacenamiento de Acero
-item.poweradvantage.rotator_tool.name=Herramienta de Rotación
-
-tile.poweradvantage.infinite_steam.name=La Energía de Vapor Infinito
-tile.poweradvantage.infinite_electricity.name=Energía Eléctrica Infinito
-tile.poweradvantage.infinite_quantum.name=Cuántica Infinito
-
-tile.poweradvantage.crude_oil.name=Petróleo Crudo
-fluid.poweradvantage.crude_oil=Petróleo Crudo
-item.bucket.crude_oil.name=Cubo de Crudo
-
-
-tile.poweradvantage.still.name=Destilería
-
-tile.poweradvantage.refined_oil.name=Refinado de Petróleo
-fluid.poweradvantage.refined_oil=Refinado de Petróleo
-item.bucket.refined_oil.name=Cubo de Refinado de Petróleo
-tile.poweradvantage.fluid_switch.name=Válvula
-
-
-tile.poweradvantage.converter_rf_steam.name=Vapor-RF Transformador
-tile.poweradvantage.converter_rf_electricity.name=Electricidad-RF Transformador
-tile.poweradvantage.converter_rf_quantum.name=Cuántico-RF Transformador
-tile.poweradvantage.converter_tr_electricity.name=Electricidad-EU Transformador
diff --git a/src/main/resources/assets/poweradvantage/lang/it_it.json b/src/main/resources/assets/poweradvantage/lang/it_it.json
new file mode 100644
index 00000000..b69cfcc1
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/it_it.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Grande Fornace (informe)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Grande Fornace (forma)",
+ "tooltip.poweradvantage.empty": "vuoto",
+ "tooltip.poweradvantage.buckets_of": "%x secchi d'%y",
+ "itemGroup.poweradvantage": "Potenza Advantage",
+ "tile.poweradvantage.fluid_drain.name": "Scarico",
+ "tile.poweradvantage.fluid_discharge.name": "Griglia di scarico",
+ "tile.poweradvantage.fluid_storage_tank.name": "Serbatoio",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Serbatoio",
+ "tile.poweradvantage.fluid_pipe.name": "Tubo",
+ "tile.poweradvantage.item_conveyor.name": "Trasportatore",
+ "tile.poweradvantage.item_filter_block.name": "Filtro di blocco trasportatore",
+ "tile.poweradvantage.item_filter_food.name": "Filtro alimentare Conveyor",
+ "tile.poweradvantage.item_filter_fuel.name": "carburante Filtro Conveyor",
+ "tile.poweradvantage.item_filter_inventory.name": "Filtro Conveyor",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Filtro Conveyor",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Filtro Conveyor",
+ "tile.poweradvantage.item_filter_smelt.name": "Forno Filtro Conveyor",
+ "tile.poweradvantage.item_filter_overflow.name": "Straripamento Filtro Conveyor",
+ "tile.poweradvantage.steel_frame.name": "Steel Frame",
+ "item.poweradvantage.starch.name": "Amido",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastica Lingotto",
+ "item.poweradvantage.sprocket.name": "Sprocket",
+ "tile.poweradvantage.fluid_metal_tank.name": "Serbatoio in Metallo",
+ "item.poweradvantage.rotator_tool.name": "Strumento di Rotazione",
+ "tile.poweradvantage.infinite_steam.name": "Forza Vapore Infinite",
+ "tile.poweradvantage.infinite_electricity.name": "Elettricità Infinite",
+ "tile.poweradvantage.infinite_quantum.name": "Atomica Infinite",
+ "tile.poweradvantage.crude_oil.name": "Olio a Crudo",
+ "fluid.poweradvantage.crude_oil": "Olio a Crudo",
+ "item.bucket.crude_oil.name": "Secchio di Greggio",
+ "tile.poweradvantage.still.name": "Distilleria",
+ "tile.poweradvantage.refined_oil.name": "Olio Raffinato",
+ "fluid.poweradvantage.refined_oil": "Olio Raffinato",
+ "item.bucket.refined_oil.name": "Secchio di Olio Raffinato",
+ "tile.poweradvantage.fluid_switch.name": "Valvola",
+ "tile.poweradvantage.converter_rf_steam.name": "Vapore-RF Trasformatore",
+ "tile.poweradvantage.converter_rf_electricity.name": "Elettricità-RF Trasformatore",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantistico-RF Trasformatore",
+ "tile.poweradvantage.converter_tr_electricity.name": "Elettricità-EU Trasformatore",
+ "block.poweradvantage.fluid_drain": "Scarico",
+ "block.poweradvantage.fluid_discharge": "Griglia di scarico",
+ "block.poweradvantage.fluid_storage_tank": "Serbatoio",
+ "item.poweradvantage.fluid_storage_tank_item": "Serbatoio",
+ "block.poweradvantage.fluid_pipe": "Tubo",
+ "block.poweradvantage.item_conveyor": "Trasportatore",
+ "block.poweradvantage.item_filter_block": "Filtro di blocco trasportatore",
+ "block.poweradvantage.item_filter_food": "Filtro alimentare Conveyor",
+ "block.poweradvantage.item_filter_fuel": "carburante Filtro Conveyor",
+ "block.poweradvantage.item_filter_inventory": "Filtro Conveyor",
+ "block.poweradvantage.item_filter_ore": "Ore Filtro Conveyor",
+ "block.poweradvantage.item_filter_plant": "Plantable Filtro Conveyor",
+ "block.poweradvantage.item_filter_smelt": "Forno Filtro Conveyor",
+ "block.poweradvantage.item_filter_overflow": "Straripamento Filtro Conveyor",
+ "block.poweradvantage.steel_frame": "Steel Frame",
+ "item.poweradvantage.starch": "Amido",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastica Lingotto",
+ "item.poweradvantage.sprocket": "Sprocket",
+ "block.poweradvantage.fluid_metal_tank": "Serbatoio in Metallo",
+ "item.poweradvantage.rotator_tool": "Strumento di Rotazione",
+ "block.poweradvantage.infinite_steam": "Forza Vapore Infinite",
+ "block.poweradvantage.infinite_electricity": "Elettricità Infinite",
+ "block.poweradvantage.infinite_quantum": "Atomica Infinite",
+ "block.poweradvantage.crude_oil": "Olio a Crudo",
+ "block.poweradvantage.still": "Distilleria",
+ "block.poweradvantage.refined_oil": "Olio Raffinato",
+ "block.poweradvantage.fluid_switch": "Valvola",
+ "block.poweradvantage.converter_rf_steam": "Vapore-RF Trasformatore",
+ "block.poweradvantage.converter_rf_electricity": "Elettricità-RF Trasformatore",
+ "block.poweradvantage.converter_rf_quantum": "Quantistico-RF Trasformatore",
+ "block.poweradvantage.converter_tr_electricity": "Elettricità-EU Trasformatore",
+ "block.poweradvantage.fluid_pipe_terminal": "Tubo"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/it_it.lang b/src/main/resources/assets/poweradvantage/lang/it_it.lang
deleted file mode 100644
index 8dc5fbb4..00000000
--- a/src/main/resources/assets/poweradvantage/lang/it_it.lang
+++ /dev/null
@@ -1,55 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Grande Fornace (informe)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Grande Fornace (forma)
-
-tooltip.poweradvantage.empty=vuoto
-tooltip.poweradvantage.buckets_of=%x secchi d'%y
-
-itemGroup.poweradvantage=Potenza Advantage
-
-tile.poweradvantage.fluid_drain.name=Scarico
-tile.poweradvantage.fluid_discharge.name=Griglia di scarico
-tile.poweradvantage.fluid_storage_tank.name=Serbatoio
-item.poweradvantage.fluid_storage_tank_item.name=Serbatoio
-tile.poweradvantage.fluid_pipe.name=Tubo
-
-tile.poweradvantage.item_conveyor.name=Trasportatore
-tile.poweradvantage.item_filter_block.name=Filtro di blocco trasportatore
-tile.poweradvantage.item_filter_food.name=Filtro alimentare Conveyor
-tile.poweradvantage.item_filter_fuel.name=carburante Filtro Conveyor
-tile.poweradvantage.item_filter_inventory.name=Filtro Conveyor
-tile.poweradvantage.item_filter_ore.name=Ore Filtro Conveyor
-tile.poweradvantage.item_filter_plant.name=Plantable Filtro Conveyor
-tile.poweradvantage.item_filter_smelt.name=Forno Filtro Conveyor
-tile.poweradvantage.item_filter_overflow.name=Straripamento Filtro Conveyor
-tile.poweradvantage.steel_frame.name=Steel Frame
-
-
-item.poweradvantage.starch.name=Amido
-item.poweradvantage.bioplastic_ingot.name=Bioplastica Lingotto
-item.poweradvantage.sprocket.name=Sprocket
-
-
-tile.poweradvantage.fluid_metal_tank.name=Serbatoio in Metallo
-item.poweradvantage.rotator_tool.name=Strumento di Rotazione
-
-tile.poweradvantage.infinite_steam.name=Forza Vapore Infinite
-tile.poweradvantage.infinite_electricity.name=Elettricità Infinite
-tile.poweradvantage.infinite_quantum.name=Atomica Infinite
-
-tile.poweradvantage.crude_oil.name=Olio a Crudo
-fluid.poweradvantage.crude_oil=Olio a Crudo
-item.bucket.crude_oil.name=Secchio di Greggio
-
-
-tile.poweradvantage.still.name=Distilleria
-tile.poweradvantage.refined_oil.name=Olio Raffinato
-fluid.poweradvantage.refined_oil=Olio Raffinato
-item.bucket.refined_oil.name=Secchio di Olio Raffinato
-tile.poweradvantage.fluid_switch.name=Valvola
-
-
-tile.poweradvantage.converter_rf_steam.name=Vapore-RF Trasformatore
-tile.poweradvantage.converter_rf_electricity.name=Elettricità-RF Trasformatore
-tile.poweradvantage.converter_rf_quantum.name=Quantistico-RF Trasformatore
-tile.poweradvantage.converter_tr_electricity.name=Elettricità-EU Trasformatore
diff --git a/src/main/resources/assets/poweradvantage/lang/ja_jp.json b/src/main/resources/assets/poweradvantage/lang/ja_jp.json
new file mode 100644
index 00000000..b65f8ea5
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/ja_jp.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "大型炉(型崩れ)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "大型炉(形)",
+ "tooltip.poweradvantage.empty": "空",
+ "tooltip.poweradvantage.buckets_of": "%yの%xバケット",
+ "itemGroup.poweradvantage": "パワー·アドバンテージ",
+ "tile.poweradvantage.fluid_drain.name": "ドレン",
+ "tile.poweradvantage.fluid_discharge.name": "放電グレート",
+ "tile.poweradvantage.fluid_storage_tank.name": "貯蔵タンク",
+ "item.poweradvantage.fluid_storage_tank_item.name": "貯蔵タンク",
+ "tile.poweradvantage.fluid_pipe.name": "パイプ",
+ "tile.poweradvantage.item_conveyor.name": "コンベア",
+ "tile.poweradvantage.item_filter_block.name": "ブロックフィルタコンベア",
+ "tile.poweradvantage.item_filter_food.name": "フードフィルターコンベア",
+ "tile.poweradvantage.item_filter_fuel.name": "燃料フィルターコンベア",
+ "tile.poweradvantage.item_filter_inventory.name": "フィルタコンベヤー",
+ "tile.poweradvantage.item_filter_ore.name": "鉱石フィルターコンベア",
+ "tile.poweradvantage.item_filter_plant.name": "プラントフィルタコンベヤー",
+ "tile.poweradvantage.item_filter_smelt.name": "炉フィルターコンベア",
+ "tile.poweradvantage.item_filter_overflow.name": "オーバーフローフィルタコンベヤー",
+ "tile.poweradvantage.steel_frame.name": "鉄骨",
+ "item.poweradvantage.starch.name": "デンプン",
+ "item.poweradvantage.bioplastic_ingot.name": "は、バイオプラスチックインゴットを",
+ "item.poweradvantage.sprocket.name": "スプロケット",
+ "tile.poweradvantage.infinite_steam.name": "無限の蒸気動力",
+ "tile.poweradvantage.infinite_electricity.name": "無限の電力",
+ "tile.poweradvantage.infinite_quantum.name": "無限の量子パワー",
+ "tile.poweradvantage.fluid_metal_tank.name": "金属製貯蔵タンク",
+ "item.poweradvantage.rotator_tool.name": "回転ツール",
+ "tile.poweradvantage.crude_oil.name": "原油",
+ "fluid.poweradvantage.crude_oil": "原油",
+ "item.bucket.crude_oil.name": "原油のバケット",
+ "tile.poweradvantage.still.name": "蒸留酒製造所",
+ "tile.poweradvantage.refined_oil.name": "精製油",
+ "fluid.poweradvantage.refined_oil": "精製油",
+ "item.bucket.refined_oil.name": "精製油のバケツ",
+ "tile.poweradvantage.fluid_switch.name": "バルブ",
+ "tile.poweradvantage.converter_rf_steam.name": "蒸気-RFトランス",
+ "tile.poweradvantage.converter_rf_electricity.name": "電気-RFトランス",
+ "tile.poweradvantage.converter_rf_quantum.name": "量子-RFトランス",
+ "tile.poweradvantage.converter_tr_electricity.name": "電気-EUトランス",
+ "block.poweradvantage.fluid_drain": "ドレン",
+ "block.poweradvantage.fluid_discharge": "放電グレート",
+ "block.poweradvantage.fluid_storage_tank": "貯蔵タンク",
+ "item.poweradvantage.fluid_storage_tank_item": "貯蔵タンク",
+ "block.poweradvantage.fluid_pipe": "パイプ",
+ "block.poweradvantage.item_conveyor": "コンベア",
+ "block.poweradvantage.item_filter_block": "ブロックフィルタコンベア",
+ "block.poweradvantage.item_filter_food": "フードフィルターコンベア",
+ "block.poweradvantage.item_filter_fuel": "燃料フィルターコンベア",
+ "block.poweradvantage.item_filter_inventory": "フィルタコンベヤー",
+ "block.poweradvantage.item_filter_ore": "鉱石フィルターコンベア",
+ "block.poweradvantage.item_filter_plant": "プラントフィルタコンベヤー",
+ "block.poweradvantage.item_filter_smelt": "炉フィルターコンベア",
+ "block.poweradvantage.item_filter_overflow": "オーバーフローフィルタコンベヤー",
+ "block.poweradvantage.steel_frame": "鉄骨",
+ "item.poweradvantage.starch": "デンプン",
+ "item.poweradvantage.bioplastic_ingot": "は、バイオプラスチックインゴットを",
+ "item.poweradvantage.sprocket": "スプロケット",
+ "block.poweradvantage.infinite_steam": "無限の蒸気動力",
+ "block.poweradvantage.infinite_electricity": "無限の電力",
+ "block.poweradvantage.infinite_quantum": "無限の量子パワー",
+ "block.poweradvantage.fluid_metal_tank": "金属製貯蔵タンク",
+ "item.poweradvantage.rotator_tool": "回転ツール",
+ "block.poweradvantage.crude_oil": "原油",
+ "block.poweradvantage.still": "蒸留酒製造所",
+ "block.poweradvantage.refined_oil": "精製油",
+ "block.poweradvantage.fluid_switch": "バルブ",
+ "block.poweradvantage.converter_rf_steam": "蒸気-RFトランス",
+ "block.poweradvantage.converter_rf_electricity": "電気-RFトランス",
+ "block.poweradvantage.converter_rf_quantum": "量子-RFトランス",
+ "block.poweradvantage.converter_tr_electricity": "電気-EUトランス",
+ "block.poweradvantage.fluid_pipe_terminal": "パイプ"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/ja_jp.lang b/src/main/resources/assets/poweradvantage/lang/ja_jp.lang
deleted file mode 100644
index 199869da..00000000
--- a/src/main/resources/assets/poweradvantage/lang/ja_jp.lang
+++ /dev/null
@@ -1,56 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-Translate
-nei.poweradvantage.recipehandler.shapelessfurnace.name=大型炉(型崩れ)
-nei.poweradvantage.recipehandler.shapedfurnace.name=大型炉(形)
-
-tooltip.poweradvantage.empty=空
-tooltip.poweradvantage.buckets_of=%yの%xバケット
-
-itemGroup.poweradvantage=パワー·アドバンテージ
-
-tile.poweradvantage.fluid_drain.name=ドレン
-tile.poweradvantage.fluid_discharge.name=放電グレート
-tile.poweradvantage.fluid_storage_tank.name=貯蔵タンク
-item.poweradvantage.fluid_storage_tank_item.name=貯蔵タンク
-tile.poweradvantage.fluid_pipe.name=パイプ
-
-tile.poweradvantage.item_conveyor.name=コンベア
-tile.poweradvantage.item_filter_block.name=ブロックフィルタコンベア
-tile.poweradvantage.item_filter_food.name=フードフィルターコンベア
-tile.poweradvantage.item_filter_fuel.name=燃料フィルターコンベア
-tile.poweradvantage.item_filter_inventory.name=フィルタコンベヤー
-tile.poweradvantage.item_filter_ore.name=鉱石フィルターコンベア
-tile.poweradvantage.item_filter_plant.name=プラントフィルタコンベヤー
-tile.poweradvantage.item_filter_smelt.name=炉フィルターコンベア
-tile.poweradvantage.item_filter_overflow.name=オーバーフローフィルタコンベヤー
-tile.poweradvantage.steel_frame.name=鉄骨
-
-
-item.poweradvantage.starch.name=デンプン
-item.poweradvantage.bioplastic_ingot.name=は、バイオプラスチックインゴットを
-item.poweradvantage.sprocket.name=スプロケット
-
-tile.poweradvantage.infinite_steam.name=無限の蒸気動力
-tile.poweradvantage.infinite_electricity.name=無限の電力
-tile.poweradvantage.infinite_quantum.name=無限の量子パワー
-
-
-tile.poweradvantage.fluid_metal_tank.name=金属製貯蔵タンク
-item.poweradvantage.rotator_tool.name=回転ツール
-
-tile.poweradvantage.crude_oil.name=原油
-fluid.poweradvantage.crude_oil=原油
-item.bucket.crude_oil.name=原油のバケット
-
-
-tile.poweradvantage.still.name=蒸留酒製造所
-tile.poweradvantage.refined_oil.name=精製油
-fluid.poweradvantage.refined_oil=精製油
-item.bucket.refined_oil.name=精製油のバケツ
-tile.poweradvantage.fluid_switch.name=バルブ
-
-
-tile.poweradvantage.converter_rf_steam.name=蒸気-RFトランス
-tile.poweradvantage.converter_rf_electricity.name=電気-RFトランス
-tile.poweradvantage.converter_rf_quantum.name=量子-RFトランス
-tile.poweradvantage.converter_tr_electricity.name=電気-EUトランス
diff --git a/src/main/resources/assets/poweradvantage/lang/ko_kr.json b/src/main/resources/assets/poweradvantage/lang/ko_kr.json
new file mode 100644
index 00000000..4680143e
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/ko_kr.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "대형 전기로 (못생긴)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "대형 전기로 (모양)",
+ "tooltip.poweradvantage.empty": "빈",
+ "tooltip.tooltip.poweradvantage.buckets_of": "%는 X 버킷",
+ "itemGroup.poweradvantage": "전원의 장점을 ",
+ "tile.poweradvantage.fluid_drain.name": "드레인",
+ "tile.poweradvantage.fluid_discharge.name": "방전 화격자",
+ "tile.poweradvantage.fluid_storage_tank.name": "저장 탱크",
+ "item.poweradvantage.fluid_storage_tank_item.name": "는 저장 탱크를 ",
+ "tile.poweradvantage.fluid_pipe.name": "파이프",
+ "tile.poweradvantage.item_conveyor.name": "컨베이어",
+ "tile.poweradvantage.item_filter_block.name": "블록 필터 컨베이어",
+ "tile.poweradvantage.item_filter_food.name": "식품 필터 컨베이어",
+ "tile.poweradvantage.item_filter_fuel.name": "연료 필터 컨베이어",
+ "tile.poweradvantage.item_filter_inventory.name": "필터 컨베이어",
+ "tile.poweradvantage.item_filter_ore.name": "광석 필터 컨베이어",
+ "tile.poweradvantage.item_filter_plant.name": "물만 주면 식물이 필터 컨베이어",
+ "tile.poweradvantage.item_filter_smelt.name": "노 필터 컨베이어",
+ "tile.poweradvantage.item_filter_overflow.name": "오버 플로우필터 컨베이어",
+ "tile.poweradvantage.steel_frame.name": "스틸 프레임",
+ "item.poweradvantage.starch.name": "전분",
+ "item.poweradvantage.bioplastic_ingot.name": "는 바이오 플라스틱 잉곳을 ",
+ "item.poweradvantage.sprocket.name": "스프로킷",
+ "tile.poweradvantage.fluid_metal_tank.name": "금속 저장조",
+ "item.poweradvantage.rotator_tool.name": "회전 도구",
+ "tile.poweradvantage.infinite_steam.name": "무한 증기 력",
+ "tile.poweradvantage.infinite_electricity.name": "무한 전력",
+ "tile.poweradvantage.infinite_quantum.name": "무한 양자 전력",
+ "tile.poweradvantage.crude_oil.name": "원유",
+ "fluid.poweradvantage.crude_oil": "원유",
+ "item.bucket.crude_oil.name": "원유의 버킷",
+ "tile.poweradvantage.still.name": "증류소",
+ "tile.poweradvantage.refined_oil.name": "정유",
+ "fluid.poweradvantage.refined_oil": "정유",
+ "item.bucket.refined_oil.name": "정유의 통",
+ "tile.poweradvantage.fluid_switch.name": "판막",
+ "tile.poweradvantage.converter_rf_steam.name": "증기-RF 전원 트랜스",
+ "tile.poweradvantage.converter_rf_electricity.name": "전기-RF 전원 트랜스",
+ "tile.poweradvantage.converter_rf_quantum.name": "양자-RF 전원 트랜스",
+ "tile.poweradvantage.converter_tr_electricity.name": "전기-EU 전원 트랜스",
+ "block.poweradvantage.fluid_drain": "드레인",
+ "block.poweradvantage.fluid_discharge": "방전 화격자",
+ "block.poweradvantage.fluid_storage_tank": "저장 탱크",
+ "item.poweradvantage.fluid_storage_tank_item": "는 저장 탱크를 ",
+ "block.poweradvantage.fluid_pipe": "파이프",
+ "block.poweradvantage.item_conveyor": "컨베이어",
+ "block.poweradvantage.item_filter_block": "블록 필터 컨베이어",
+ "block.poweradvantage.item_filter_food": "식품 필터 컨베이어",
+ "block.poweradvantage.item_filter_fuel": "연료 필터 컨베이어",
+ "block.poweradvantage.item_filter_inventory": "필터 컨베이어",
+ "block.poweradvantage.item_filter_ore": "광석 필터 컨베이어",
+ "block.poweradvantage.item_filter_plant": "물만 주면 식물이 필터 컨베이어",
+ "block.poweradvantage.item_filter_smelt": "노 필터 컨베이어",
+ "block.poweradvantage.item_filter_overflow": "오버 플로우필터 컨베이어",
+ "block.poweradvantage.steel_frame": "스틸 프레임",
+ "item.poweradvantage.starch": "전분",
+ "item.poweradvantage.bioplastic_ingot": "는 바이오 플라스틱 잉곳을 ",
+ "item.poweradvantage.sprocket": "스프로킷",
+ "block.poweradvantage.fluid_metal_tank": "금속 저장조",
+ "item.poweradvantage.rotator_tool": "회전 도구",
+ "block.poweradvantage.infinite_steam": "무한 증기 력",
+ "block.poweradvantage.infinite_electricity": "무한 전력",
+ "block.poweradvantage.infinite_quantum": "무한 양자 전력",
+ "block.poweradvantage.crude_oil": "원유",
+ "block.poweradvantage.still": "증류소",
+ "block.poweradvantage.refined_oil": "정유",
+ "block.poweradvantage.fluid_switch": "판막",
+ "block.poweradvantage.converter_rf_steam": "증기-RF 전원 트랜스",
+ "block.poweradvantage.converter_rf_electricity": "전기-RF 전원 트랜스",
+ "block.poweradvantage.converter_rf_quantum": "양자-RF 전원 트랜스",
+ "block.poweradvantage.converter_tr_electricity": "전기-EU 전원 트랜스",
+ "block.poweradvantage.fluid_pipe_terminal": "파이프"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/ko_kr.lang b/src/main/resources/assets/poweradvantage/lang/ko_kr.lang
deleted file mode 100644
index de5d6bed..00000000
--- a/src/main/resources/assets/poweradvantage/lang/ko_kr.lang
+++ /dev/null
@@ -1,56 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=대형 전기로 (못생긴)
-nei.poweradvantage.recipehandler.shapedfurnace.name=대형 전기로 (모양)
-
-tooltip.poweradvantage.empty=빈
-tooltip.tooltip.poweradvantage.buckets_of=%는 X 버킷
-
-itemGroup.poweradvantage=전원의 장점을
-
-tile.poweradvantage.fluid_drain.name=드레인
-tile.poweradvantage.fluid_discharge.name=방전 화격자
-tile.poweradvantage.fluid_storage_tank.name=저장 탱크
-item.poweradvantage.fluid_storage_tank_item.name=는 저장 탱크를
-tile.poweradvantage.fluid_pipe.name=파이프
-
-tile.poweradvantage.item_conveyor.name=컨베이어
-tile.poweradvantage.item_filter_block.name=블록 필터 컨베이어
-tile.poweradvantage.item_filter_food.name=식품 필터 컨베이어
-tile.poweradvantage.item_filter_fuel.name=연료 필터 컨베이어
-tile.poweradvantage.item_filter_inventory.name=필터 컨베이어
-tile.poweradvantage.item_filter_ore.name=광석 필터 컨베이어
-tile.poweradvantage.item_filter_plant.name=물만 주면 식물이 필터 컨베이어
-tile.poweradvantage.item_filter_smelt.name=노 필터 컨베이어
-tile.poweradvantage.item_filter_overflow.name=오버 플로우필터 컨베이어
-tile.poweradvantage.steel_frame.name=스틸 프레임
-
-
-item.poweradvantage.starch.name=전분
-item.poweradvantage.bioplastic_ingot.name=는 바이오 플라스틱 잉곳을
-item.poweradvantage.sprocket.name=스프로킷
-
-
-tile.poweradvantage.fluid_metal_tank.name=금속 저장조
-item.poweradvantage.rotator_tool.name=회전 도구
-
-
-tile.poweradvantage.infinite_steam.name=무한 증기 력
-tile.poweradvantage.infinite_electricity.name=무한 전력
-tile.poweradvantage.infinite_quantum.name=무한 양자 전력
-
-tile.poweradvantage.crude_oil.name=원유
-fluid.poweradvantage.crude_oil=원유
-item.bucket.crude_oil.name=원유의 버킷
-
-
-tile.poweradvantage.still.name=증류소
-tile.poweradvantage.refined_oil.name=정유
-fluid.poweradvantage.refined_oil=정유
-item.bucket.refined_oil.name=정유의 통
-tile.poweradvantage.fluid_switch.name=판막
-
-
-tile.poweradvantage.converter_rf_steam.name=증기-RF 전원 트랜스
-tile.poweradvantage.converter_rf_electricity.name=전기-RF 전원 트랜스
-tile.poweradvantage.converter_rf_quantum.name=양자-RF 전원 트랜스
-tile.poweradvantage.converter_tr_electricity.name=전기-EU 전원 트랜스
diff --git a/src/main/resources/assets/poweradvantage/lang/pt_br.json b/src/main/resources/assets/poweradvantage/lang/pt_br.json
new file mode 100644
index 00000000..f837618a
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/pt_br.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "Forno Grande (no forma)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "Forno Grande (em forma)",
+ "tooltip.poweradvantage.empty": "vazio",
+ "tooltip.poweradvantage.buckets_of": "%x baldes de %y",
+ "itemGroup.poweradvantage": "Vantagem de Poder",
+ "tile.poweradvantage.fluid_drain.name": "Dreno",
+ "tile.poweradvantage.fluid_discharge.name": "Grate Discharge",
+ "tile.poweradvantage.fluid_storage_tank.name": "Tanque de armazenamento",
+ "item.poweradvantage.fluid_storage_tank_item.name": "Tanque de armazenamento",
+ "tile.poweradvantage.fluid_pipe.name": "Tubulação",
+ "tile.poweradvantage.item_conveyor.name": "Transportador",
+ "tile.poweradvantage.item_filter_block.name": "Bloquear filtro transportador",
+ "tile.poweradvantage.item_filter_food.name": "Transportador Filtro de Alimentos",
+ "tile.poweradvantage.item_filter_fuel.name": "Filtro de combustível Transportador",
+ "tile.poweradvantage.item_filter_inventory.name": "Transportador Filtro",
+ "tile.poweradvantage.item_filter_ore.name": "Ore Transportadora Filtro",
+ "tile.poweradvantage.item_filter_plant.name": "Plantable Transportadora Filtro",
+ "tile.poweradvantage.item_filter_smelt.name": "Fornalha Filtro Transportador",
+ "tile.poweradvantage.item_filter_overflow.name": "Transbordamento Transportador Filtro",
+ "tile.poweradvantage.steel_frame.name": "Armação de Aço",
+ "item.poweradvantage.starch.name": "Amido",
+ "item.poweradvantage.bioplastic_ingot.name": "Bioplastic lingote",
+ "item.poweradvantage.sprocket.name": "Roda Dentada",
+ "tile.poweradvantage.fluid_metal_tank.name": "Tanque de Armazenamento de Aço",
+ "item.poweradvantage.rotator_tool.name": "Ferramenta de Rotação",
+ "tile.poweradvantage.infinite_steam.name": "Energia a Vapor Infinito",
+ "tile.poweradvantage.infinite_electricity.name": "Energia Elétrica Infinito",
+ "tile.poweradvantage.infinite_quantum.name": "Energia Quantum Infinito",
+ "tile.poweradvantage.crude_oil.name": "Óleo Cru",
+ "fluid.poweradvantage.crude_oil": "Óleo Cru",
+ "item.bucket.crude_oil.name": "Balde de Óleo Cru",
+ "tile.poweradvantage.still.name": "Destilaria",
+ "tile.poweradvantage.refined_oil.name": "Óleo Refinado",
+ "fluid.poweradvantage.refined_oil": "Óleo Refinado",
+ "item.bucket.refined_oil.name": "Balde de Óleo Refinado",
+ "tile.poweradvantage.fluid_switch.name": "Válvula",
+ "tile.poweradvantage.converter_rf_steam.name": "Vapor-RF Transformador",
+ "tile.poweradvantage.converter_rf_electricity.name": "Electricidade-RF Transformador",
+ "tile.poweradvantage.converter_rf_quantum.name": "Quantidade-RF Transformador",
+ "tile.poweradvantage.converter_tr_electricity.name": "Electricidade-EU Transformador",
+ "block.poweradvantage.fluid_drain": "Dreno",
+ "block.poweradvantage.fluid_discharge": "Grate Discharge",
+ "block.poweradvantage.fluid_storage_tank": "Tanque de armazenamento",
+ "item.poweradvantage.fluid_storage_tank_item": "Tanque de armazenamento",
+ "block.poweradvantage.fluid_pipe": "Tubulação",
+ "block.poweradvantage.item_conveyor": "Transportador",
+ "block.poweradvantage.item_filter_block": "Bloquear filtro transportador",
+ "block.poweradvantage.item_filter_food": "Transportador Filtro de Alimentos",
+ "block.poweradvantage.item_filter_fuel": "Filtro de combustível Transportador",
+ "block.poweradvantage.item_filter_inventory": "Transportador Filtro",
+ "block.poweradvantage.item_filter_ore": "Ore Transportadora Filtro",
+ "block.poweradvantage.item_filter_plant": "Plantable Transportadora Filtro",
+ "block.poweradvantage.item_filter_smelt": "Fornalha Filtro Transportador",
+ "block.poweradvantage.item_filter_overflow": "Transbordamento Transportador Filtro",
+ "block.poweradvantage.steel_frame": "Armação de Aço",
+ "item.poweradvantage.starch": "Amido",
+ "item.poweradvantage.bioplastic_ingot": "Bioplastic lingote",
+ "item.poweradvantage.sprocket": "Roda Dentada",
+ "block.poweradvantage.fluid_metal_tank": "Tanque de Armazenamento de Aço",
+ "item.poweradvantage.rotator_tool": "Ferramenta de Rotação",
+ "block.poweradvantage.infinite_steam": "Energia a Vapor Infinito",
+ "block.poweradvantage.infinite_electricity": "Energia Elétrica Infinito",
+ "block.poweradvantage.infinite_quantum": "Energia Quantum Infinito",
+ "block.poweradvantage.crude_oil": "Óleo Cru",
+ "block.poweradvantage.still": "Destilaria",
+ "block.poweradvantage.refined_oil": "Óleo Refinado",
+ "block.poweradvantage.fluid_switch": "Válvula",
+ "block.poweradvantage.converter_rf_steam": "Vapor-RF Transformador",
+ "block.poweradvantage.converter_rf_electricity": "Electricidade-RF Transformador",
+ "block.poweradvantage.converter_rf_quantum": "Quantidade-RF Transformador",
+ "block.poweradvantage.converter_tr_electricity": "Electricidade-EU Transformador",
+ "block.poweradvantage.fluid_pipe_terminal": "Tubulação"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/pt_br.lang b/src/main/resources/assets/poweradvantage/lang/pt_br.lang
deleted file mode 100644
index 3141fcbe..00000000
--- a/src/main/resources/assets/poweradvantage/lang/pt_br.lang
+++ /dev/null
@@ -1,54 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-nei.poweradvantage.recipehandler.shapelessfurnace.name=Forno Grande (no forma)
-nei.poweradvantage.recipehandler.shapedfurnace.name=Forno Grande (em forma)
-
-tooltip.poweradvantage.empty=vazio
-tooltip.poweradvantage.buckets_of=%x baldes de %y
-
-itemGroup.poweradvantage=Vantagem de Poder
-
-tile.poweradvantage.fluid_drain.name=Dreno
-tile.poweradvantage.fluid_discharge.name=Grate Discharge
-tile.poweradvantage.fluid_storage_tank.name=Tanque de armazenamento
-item.poweradvantage.fluid_storage_tank_item.name=Tanque de armazenamento
-tile.poweradvantage.fluid_pipe.name=Tubulação
-
-tile.poweradvantage.item_conveyor.name=Transportador
-tile.poweradvantage.item_filter_block.name=Bloquear filtro transportador
-tile.poweradvantage.item_filter_food.name=Transportador Filtro de Alimentos
-tile.poweradvantage.item_filter_fuel.name=Filtro de combustível Transportador
-tile.poweradvantage.item_filter_inventory.name=Transportador Filtro
-tile.poweradvantage.item_filter_ore.name=Ore Transportadora Filtro
-tile.poweradvantage.item_filter_plant.name=Plantable Transportadora Filtro
-tile.poweradvantage.item_filter_smelt.name=Fornalha Filtro Transportador
-tile.poweradvantage.item_filter_overflow.name=Transbordamento Transportador Filtro
-tile.poweradvantage.steel_frame.name=Armação de Aço
-
-
-item.poweradvantage.starch.name=Amido
-item.poweradvantage.bioplastic_ingot.name=Bioplastic lingote
-item.poweradvantage.sprocket.name=Roda Dentada
-
-
-tile.poweradvantage.fluid_metal_tank.name=Tanque de Armazenamento de Aço
-item.poweradvantage.rotator_tool.name=Ferramenta de Rotação
-
-tile.poweradvantage.infinite_steam.name=Energia a Vapor Infinito
-tile.poweradvantage.infinite_electricity.name=Energia Elétrica Infinito
-tile.poweradvantage.infinite_quantum.name=Energia Quantum Infinito
-
-tile.poweradvantage.crude_oil.name=Óleo Cru
-fluid.poweradvantage.crude_oil=Óleo Cru
-item.bucket.crude_oil.name=Balde de Óleo Cru
-
-
-tile.poweradvantage.still.name=Destilaria
-tile.poweradvantage.refined_oil.name=Óleo Refinado
-fluid.poweradvantage.refined_oil=Óleo Refinado
-item.bucket.refined_oil.name=Balde de Óleo Refinado
-tile.poweradvantage.fluid_switch.name=Válvula
-
-tile.poweradvantage.converter_rf_steam.name=Vapor-RF Transformador
-tile.poweradvantage.converter_rf_electricity.name=Electricidade-RF Transformador
-tile.poweradvantage.converter_rf_quantum.name=Quantidade-RF Transformador
-tile.poweradvantage.converter_tr_electricity.name=Electricidade-EU Transformador
diff --git a/src/main/resources/assets/poweradvantage/lang/zh_cn.json b/src/main/resources/assets/poweradvantage/lang/zh_cn.json
new file mode 100644
index 00000000..b8c066bb
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/lang/zh_cn.json
@@ -0,0 +1,75 @@
+{
+ "language_support_goals": "english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)",
+ "nei.poweradvantage.recipehandler.shapelessfurnace.name": "大熔炉(无形的)",
+ "nei.poweradvantage.recipehandler.shapedfurnace.name": "大熔炉(有形的)",
+ "tooltip.poweradvantage.empty": "空",
+ "tooltip.poweradvantage.buckets_of": "%x水桶的%y",
+ "itemGroup.poweradvantage": "能量优势",
+ "tile.poweradvantage.fluid_drain.name": "抽水机",
+ "tile.poweradvantage.fluid_discharge.name": "排液器",
+ "tile.poweradvantage.fluid_storage_tank.name": "便携式液体储罐",
+ "item.poweradvantage.fluid_storage_tank_item.name": "便携式液体储罐",
+ "tile.poweradvantage.fluid_pipe.name": "液体管",
+ "tile.poweradvantage.item_conveyor.name": "输送机",
+ "tile.poweradvantage.item_filter_block.name": "方块过滤输送机",
+ "tile.poweradvantage.item_filter_food.name": "食品过滤输送机",
+ "tile.poweradvantage.item_filter_fuel.name": "燃料过滤输送机",
+ "tile.poweradvantage.item_filter_inventory.name": "选择过滤输送机",
+ "tile.poweradvantage.item_filter_ore.name": "矿物过滤输送机",
+ "tile.poweradvantage.item_filter_plant.name": "种子过滤输送机",
+ "tile.poweradvantage.item_filter_smelt.name": "熔炉过滤输送机",
+ "tile.poweradvantage.item_filter_overflow.name": "溢出过滤输送机",
+ "tile.poweradvantage.steel_frame.name": "钢框架",
+ "item.poweradvantage.starch.name": "淀粉",
+ "item.poweradvantage.bioplastic_ingot.name": "生物塑料锭",
+ "item.poweradvantage.sprocket.name": "链轮",
+ "tile.poweradvantage.fluid_metal_tank.name": "金属液罐",
+ "item.poweradvantage.rotator_tool.name": "旋转扳手",
+ "tile.poweradvantage.infinite_steam.name": "无限蒸汽动力",
+ "tile.poweradvantage.infinite_electricity.name": "无限电力",
+ "tile.poweradvantage.infinite_quantum.name": "无限量子动力",
+ "tile.poweradvantage.crude_oil.name": "原油",
+ "fluid.poweradvantage.crude_oil": "原油",
+ "item.bucket.crude_oil.name": "原油桶",
+ "tile.poweradvantage.still.name": "酒廠",
+ "tile.poweradvantage.refined_oil.name": "成品油",
+ "fluid.poweradvantage.refined_oil": "成品油",
+ "item.bucket.refined_oil.name": "成品油的桶",
+ "tile.poweradvantage.fluid_switch.name": "閥",
+ "tile.poweradvantage.converter_rf_steam.name": "蒸汽-RF 變壓器",
+ "tile.poweradvantage.converter_rf_electricity.name": "電力-RF 變壓器",
+ "tile.poweradvantage.converter_rf_quantum.name": "量子-RF 變壓器",
+ "tile.poweradvantage.converter_tr_electricity.name": "電力-EU 變壓器",
+ "block.poweradvantage.fluid_drain": "抽水机",
+ "block.poweradvantage.fluid_discharge": "排液器",
+ "block.poweradvantage.fluid_storage_tank": "便携式液体储罐",
+ "item.poweradvantage.fluid_storage_tank_item": "便携式液体储罐",
+ "block.poweradvantage.fluid_pipe": "液体管",
+ "block.poweradvantage.item_conveyor": "输送机",
+ "block.poweradvantage.item_filter_block": "方块过滤输送机",
+ "block.poweradvantage.item_filter_food": "食品过滤输送机",
+ "block.poweradvantage.item_filter_fuel": "燃料过滤输送机",
+ "block.poweradvantage.item_filter_inventory": "选择过滤输送机",
+ "block.poweradvantage.item_filter_ore": "矿物过滤输送机",
+ "block.poweradvantage.item_filter_plant": "种子过滤输送机",
+ "block.poweradvantage.item_filter_smelt": "熔炉过滤输送机",
+ "block.poweradvantage.item_filter_overflow": "溢出过滤输送机",
+ "block.poweradvantage.steel_frame": "钢框架",
+ "item.poweradvantage.starch": "淀粉",
+ "item.poweradvantage.bioplastic_ingot": "生物塑料锭",
+ "item.poweradvantage.sprocket": "链轮",
+ "block.poweradvantage.fluid_metal_tank": "金属液罐",
+ "item.poweradvantage.rotator_tool": "旋转扳手",
+ "block.poweradvantage.infinite_steam": "无限蒸汽动力",
+ "block.poweradvantage.infinite_electricity": "无限电力",
+ "block.poweradvantage.infinite_quantum": "无限量子动力",
+ "block.poweradvantage.crude_oil": "原油",
+ "block.poweradvantage.still": "酒廠",
+ "block.poweradvantage.refined_oil": "成品油",
+ "block.poweradvantage.fluid_switch": "閥",
+ "block.poweradvantage.converter_rf_steam": "蒸汽-RF 變壓器",
+ "block.poweradvantage.converter_rf_electricity": "電力-RF 變壓器",
+ "block.poweradvantage.converter_rf_quantum": "量子-RF 變壓器",
+ "block.poweradvantage.converter_tr_electricity": "電力-EU 變壓器",
+ "block.poweradvantage.fluid_pipe_terminal": "液体管"
+}
diff --git a/src/main/resources/assets/poweradvantage/lang/zh_cn.lang b/src/main/resources/assets/poweradvantage/lang/zh_cn.lang
deleted file mode 100644
index 99b269e3..00000000
--- a/src/main/resources/assets/poweradvantage/lang/zh_cn.lang
+++ /dev/null
@@ -1,57 +0,0 @@
-language_support_goals=english (en_US,en_EN,en_CA,en_GB, en_AU, en_PT), spanish (es_MX,es_ES), chinese (zh_CN), japanese (ja_JA), portugese (pt_BR), german (de_DE), italian (it_IT), korean (ko_KR)
-
-nei.poweradvantage.recipehandler.shapelessfurnace.name=大熔炉(无形的)
-nei.poweradvantage.recipehandler.shapedfurnace.name=大熔炉(有形的)
-
-tooltip.poweradvantage.empty=空
-tooltip.poweradvantage.buckets_of=%x水桶的%y
-
-itemGroup.poweradvantage=能量优势
-
-tile.poweradvantage.fluid_drain.name=抽水机
-tile.poweradvantage.fluid_discharge.name=排液器
-tile.poweradvantage.fluid_storage_tank.name=便携式液体储罐
-item.poweradvantage.fluid_storage_tank_item.name=便携式液体储罐
-tile.poweradvantage.fluid_pipe.name=液体管
-
-tile.poweradvantage.item_conveyor.name=输送机
-tile.poweradvantage.item_filter_block.name=方块过滤输送机
-tile.poweradvantage.item_filter_food.name=食品过滤输送机
-tile.poweradvantage.item_filter_fuel.name=燃料过滤输送机
-tile.poweradvantage.item_filter_inventory.name=选择过滤输送机
-tile.poweradvantage.item_filter_ore.name=矿物过滤输送机
-tile.poweradvantage.item_filter_plant.name=种子过滤输送机
-tile.poweradvantage.item_filter_smelt.name=熔炉过滤输送机
-tile.poweradvantage.item_filter_overflow.name=溢出过滤输送机
-tile.poweradvantage.steel_frame.name=钢框架
-
-
-item.poweradvantage.starch.name=淀粉
-item.poweradvantage.bioplastic_ingot.name=生物塑料锭
-item.poweradvantage.sprocket.name=链轮
-
-
-tile.poweradvantage.fluid_metal_tank.name=金属液罐
-item.poweradvantage.rotator_tool.name=旋转扳手
-
-tile.poweradvantage.infinite_steam.name=无限蒸汽动力
-tile.poweradvantage.infinite_electricity.name=无限电力
-tile.poweradvantage.infinite_quantum.name=无限量子动力
-
-tile.poweradvantage.crude_oil.name=原油
-fluid.poweradvantage.crude_oil=原油
-item.bucket.crude_oil.name=原油桶
-
-
-tile.poweradvantage.still.name=酒廠
-
-tile.poweradvantage.refined_oil.name=成品油
-fluid.poweradvantage.refined_oil=成品油
-item.bucket.refined_oil.name=成品油的桶
-tile.poweradvantage.fluid_switch.name=閥
-
-
-tile.poweradvantage.converter_rf_steam.name=蒸汽-RF 變壓器
-tile.poweradvantage.converter_rf_electricity.name=電力-RF 變壓器
-tile.poweradvantage.converter_rf_quantum.name=量子-RF 變壓器
-tile.poweradvantage.converter_tr_electricity.name=電力-EU 變壓器
diff --git a/src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json b/src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json
index 82f513c9..5733e1cb 100644
--- a/src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json
+++ b/src/main/resources/assets/poweradvantage/models/block/converter_rf_quantum.json
@@ -2,7 +2,7 @@
"parent": "block/block",
"textures": {
"face": "poweradvantage:blocks/steel_frame",
- "core": "minecraft:blocks/ice_packed",
+ "core": "minecraft:block/packed_ice",
"plug": "poweradvantage:blocks/filter_bottom",
"particle": "poweradvantage:materials/steel"
},
diff --git a/src/main/resources/assets/poweradvantage/models/block/crude_oil.json b/src/main/resources/assets/poweradvantage/models/block/crude_oil.json
new file mode 100644
index 00000000..37af9662
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/models/block/crude_oil.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:block/block",
+ "textures": {
+ "particle": "poweradvantage:blocks/crude_oil_still"
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/models/block/refined_oil.json b/src/main/resources/assets/poweradvantage/models/block/refined_oil.json
new file mode 100644
index 00000000..81449e23
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/models/block/refined_oil.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:block/block",
+ "textures": {
+ "particle": "poweradvantage:blocks/refined_oil_still"
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/models/block/still_off.json b/src/main/resources/assets/poweradvantage/models/block/still_off.json
index 132b4027..69edeb89 100644
--- a/src/main/resources/assets/poweradvantage/models/block/still_off.json
+++ b/src/main/resources/assets/poweradvantage/models/block/still_off.json
@@ -1,9 +1,9 @@
{
"parent": "block/block",
"textures": {
- "top": "blocks/furnace_top",
- "front": "blocks/furnace_front_off",
- "side": "blocks/furnace_side",
+ "top": "minecraft:block/furnace_top",
+ "front": "minecraft:block/furnace_front",
+ "side": "minecraft:block/furnace_side",
"tank": "poweradvantage:materials/steel",
"particle": "poweradvantage:materials/steel"
},
diff --git a/src/main/resources/assets/poweradvantage/models/block/still_on.json b/src/main/resources/assets/poweradvantage/models/block/still_on.json
index 399a8e43..ac5eb132 100644
--- a/src/main/resources/assets/poweradvantage/models/block/still_on.json
+++ b/src/main/resources/assets/poweradvantage/models/block/still_on.json
@@ -1,9 +1,9 @@
{
"parent": "block/block",
"textures": {
- "top": "blocks/furnace_top",
- "front": "blocks/furnace_front_on",
- "side": "blocks/furnace_side",
+ "top": "minecraft:block/furnace_top",
+ "front": "minecraft:block/furnace_front_on",
+ "side": "minecraft:block/furnace_side",
"tank": "poweradvantage:materials/steel",
"particle": "poweradvantage:materials/steel"
},
diff --git a/src/main/resources/assets/poweradvantage/models/item/crude_oil.json b/src/main/resources/assets/poweradvantage/models/item/crude_oil.json
new file mode 100644
index 00000000..79fdb9e9
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/models/item/crude_oil.json
@@ -0,0 +1,3 @@
+{
+ "parent": "poweradvantage:item/crude_oil_bucket"
+}
diff --git a/src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json b/src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json
new file mode 100644
index 00000000..9e70d56f
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/models/item/crude_oil_bucket.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "poweradvantage:items/bucket_crude_oil"
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/models/item/refined_oil.json b/src/main/resources/assets/poweradvantage/models/item/refined_oil.json
new file mode 100644
index 00000000..0a841a81
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/models/item/refined_oil.json
@@ -0,0 +1,3 @@
+{
+ "parent": "poweradvantage:item/refined_oil_bucket"
+}
diff --git a/src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json b/src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json
new file mode 100644
index 00000000..48c1456a
--- /dev/null
+++ b/src/main/resources/assets/poweradvantage/models/item/refined_oil_bucket.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "poweradvantage:items/bucket_refined_oil"
+ }
+}
diff --git a/src/main/resources/assets/poweradvantage/recipes/_factories.json b/src/main/resources/assets/poweradvantage/recipes/_factories.json
deleted file mode 100644
index b2b96bce..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/_factories.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "conditions": {
- "recipe_mode": "com.mcmoddev.poweradvantage.recipe.condition.RecipeModeConditionFactory",
- "not_recipe_mode": "com.mcmoddev.poweradvantage.recipe.condition.NotRecipeModeConditionFactory",
- "ore_dict_empty": "com.mcmoddev.poweradvantage.recipe.condition.OreDictEmptyConditionFactory"
- }
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_discharge.json b/src/main/resources/assets/poweradvantage/recipes/fluid_discharge.json
deleted file mode 100644
index dd039fdb..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_discharge.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "ppp",
- "w#w",
- " x "
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "bars"
- },
- "w": {
- "type": "forge:ore_dict",
- "ore": "plateSteel"
- },
- "#": {
- "type": "forge:ore_dict",
- "ore": "frameSteel"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_discharge"
- },
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_drain.json b/src/main/resources/assets/poweradvantage/recipes/fluid_drain.json
deleted file mode 100644
index 066a9166..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_drain.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- " x ",
- "w#w",
- "ppp"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "bars"
- },
- "w": {
- "type": "forge:ore_dict",
- "ore": "plateSteel"
- },
- "#": {
- "type": "forge:ore_dict",
- "ore": "frameSteel"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_drain"
- },
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_iron_normal.json
deleted file mode 100644
index f7a0ffce..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_iron_normal.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- "xpx",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotIron"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_metal_tank"
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "NORMAL"
- }
- ],
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_steel.json b/src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_steel.json
deleted file mode 100644
index 268706e2..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_metal_tank_steel.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- "xpx",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotSteel"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_metal_tank"
- },
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json
deleted file mode 100644
index 84184b8f..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- " ",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotCopper"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_pipe",
- "count": 3
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "APOCALYPTIC"
- }
- ],
- "group": "pipes"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_normal.json b/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_normal.json
deleted file mode 100644
index 72d06a5f..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_copper_normal.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- " ",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotCopper"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_pipe",
- "count": 6
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "NORMAL"
- }
- ],
- "group": "pipes"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json
deleted file mode 100644
index 510eec30..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- " ",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotIron"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_pipe",
- "count": 3
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "APOCALYPTIC"
- }
- ],
- "group": "pipes"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_normal.json b/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_normal.json
deleted file mode 100644
index 20141152..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_normal.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- " ",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotIron"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_pipe",
- "count": 6
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "NORMAL"
- }
- ],
- "group": "pipes"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_tech.json b/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_tech.json
deleted file mode 100644
index 31b2694b..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_iron_tech.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- " ",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotIron"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_pipe",
- "count": 6
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "TECH_PROGRESSION"
- }
- ],
- "group": "pipes"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json
deleted file mode 100644
index 07569626..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- " ",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotLead"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_pipe",
- "count": 3
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "APOCALYPTIC"
- }
- ],
- "group": "pipes"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_storage_tank.json b/src/main/resources/assets/poweradvantage/recipes/fluid_storage_tank.json
deleted file mode 100644
index 9eb41503..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_storage_tank.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- "xpx",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotPlastic"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_storage_tank"
- },
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/fluid_switch.json b/src/main/resources/assets/poweradvantage/recipes/fluid_switch.json
deleted file mode 100644
index 083cf0ad..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/fluid_switch.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- " l ",
- "pfp"
- ],
- "key": {
- "l": {
- "item": "minecraft:lever"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- },
- "f": {
- "type": "forge:ore_dict",
- "ore": "frameSteel"
- }
- },
- "result": {
- "item": "poweradvantage:fluid_switch"
- },
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_conveyor.json b/src/main/resources/assets/poweradvantage/recipes/item_conveyor.json
deleted file mode 100644
index 7012ade1..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_conveyor.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- "ghg",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "plateSteel"
- },
- "g": {
- "type": "forge:ore_dict",
- "ore": "sprocket"
- },
- "h": {
- "item": "minecraft:hopper"
- }
- },
- "result": {
- "item": "poweradvantage:item_conveyor",
- "count": 5
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_block.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_block.json
deleted file mode 100644
index cfa04a40..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_block.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "item": "minecraft:stone",
- "data": 0
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_block"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_food.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_food.json
deleted file mode 100644
index f4543d09..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_food.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "item": "minecraft:bread"
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_food"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_fuel.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_fuel.json
deleted file mode 100644
index 2ef78397..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_fuel.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "item": "minecraft:coal",
- "data": 32767
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_fuel"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_inventory.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_inventory.json
deleted file mode 100644
index b9a3574f..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_inventory.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "item": "minecraft:chest"
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_inventory"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_ore.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_ore.json
deleted file mode 100644
index 9d8c1fa9..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_ore.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "type": "forge:ore_dict",
- "ore": "ingotGold"
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_ore"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_overflow.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_overflow.json
deleted file mode 100644
index de525d34..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_overflow.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_overflow"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_plant.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_plant.json
deleted file mode 100644
index d767e537..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_plant.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "type": "forge:ore_dict",
- "ore": "treeSapling"
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_plant"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/item_filter_smelt.json b/src/main/resources/assets/poweradvantage/recipes/item_filter_smelt.json
deleted file mode 100644
index ed854d35..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/item_filter_smelt.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "x",
- "y",
- "z"
- ],
- "key": {
- "x": {
- "item": "poweradvantage:item_conveyor"
- },
- "y": {
- "item": "minecraft:furnace"
- },
- "z": {
- "item": "minecraft:wooden_pressure_plate"
- }
- },
- "result": {
- "item": "poweradvantage:item_filter_smelt"
- },
- "group": "conveyors"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/rotator_tool.json b/src/main/resources/assets/poweradvantage/recipes/rotator_tool.json
deleted file mode 100644
index e45df535..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/rotator_tool.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xx",
- "x*",
- " x"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotIron"
- },
- "*": {
- "type": "forge:ore_dict",
- "ore": "sprocket"
- }
- },
- "result": {
- "item": "poweradvantage:rotator_tool"
- },
- "group": "tools"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/assets/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json
deleted file mode 100644
index 19638164..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- " x ",
- "x/x",
- " x "
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotSteel"
- },
- "/": {
- "type": "forge:ore_dict",
- "ore": "nuggetBronze"
- }
- },
- "result": {
- "item": "poweradvantage:sprocket",
- "count": 4
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "TECH_PROGRESSION"
- },
- {
- "type": "poweradvantage:ore_dict_empty",
- "ore": "rod"
- }
- ],
- "group": "parts"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/sprocket_rod_normal.json b/src/main/resources/assets/poweradvantage/recipes/sprocket_rod_normal.json
deleted file mode 100644
index cd893dc0..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/sprocket_rod_normal.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- " x ",
- "x/x",
- " x "
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotSteel"
- },
- "/": {
- "type": "forge:ore_dict",
- "ore": "rod"
- }
- },
- "result": {
- "item": "poweradvantage:sprocket",
- "count": 4
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "NORMAL"
- }
- ],
- "group": "parts"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/sprocket_rod_tech.json b/src/main/resources/assets/poweradvantage/recipes/sprocket_rod_tech.json
deleted file mode 100644
index 85152156..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/sprocket_rod_tech.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- " x ",
- "x/x",
- " x "
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotSteel"
- },
- "/": {
- "type": "forge:ore_dict",
- "ore": "rod"
- }
- },
- "result": {
- "item": "poweradvantage:sprocket",
- "count": 4
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "TECH_PROGRESSION"
- }
- ],
- "group": "parts"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/sprocket_stickwood_normal.json b/src/main/resources/assets/poweradvantage/recipes/sprocket_stickwood_normal.json
deleted file mode 100644
index a000cb6b..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/sprocket_stickwood_normal.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- " x ",
- "x/x",
- " x "
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "ingotSteel"
- },
- "/": {
- "type": "forge:ore_dict",
- "ore": "stickWood"
- }
- },
- "result": {
- "item": "poweradvantage:sprocket",
- "count": 4
- },
- "conditions": [
- {
- "type": "poweradvantage:recipe_mode",
- "mode": "NORMAL"
- }
- ],
- "group": "parts"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/steel_frame.json b/src/main/resources/assets/poweradvantage/recipes/steel_frame.json
deleted file mode 100644
index cf4b2091..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/steel_frame.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "xxx",
- "x x",
- "xxx"
- ],
- "key": {
- "x": {
- "type": "forge:ore_dict",
- "ore": "barsSteel"
- }
- },
- "result": {
- "item": "poweradvantage:steel_frame"
- },
- "group": "parts"
-}
diff --git a/src/main/resources/assets/poweradvantage/recipes/still.json b/src/main/resources/assets/poweradvantage/recipes/still.json
deleted file mode 100644
index 7717c482..00000000
--- a/src/main/resources/assets/poweradvantage/recipes/still.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "type": "forge:ore_shaped",
- "pattern": [
- "bpb",
- " f "
- ],
- "key": {
- "b": {
- "item": "minecraft:bucket"
- },
- "p": {
- "type": "forge:ore_dict",
- "ore": "pipe"
- },
- "f": {
- "item": "minecraft:furnace"
- }
- },
- "result": {
- "item": "poweradvantage:still"
- },
- "group": "fluid_machines"
-}
diff --git a/src/main/resources/coremods/poweradvantage_113_migration.js b/src/main/resources/coremods/poweradvantage_113_migration.js
new file mode 100644
index 00000000..0d430ad1
--- /dev/null
+++ b/src/main/resources/coremods/poweradvantage_113_migration.js
@@ -0,0 +1,96 @@
+var Opcodes = Java.type('org.objectweb.asm.Opcodes');
+var InsnNode = Java.type('org.objectweb.asm.tree.InsnNode');
+var InsnList = Java.type('org.objectweb.asm.tree.InsnList');
+var JumpInsnNode = Java.type('org.objectweb.asm.tree.JumpInsnNode');
+var LabelNode = Java.type('org.objectweb.asm.tree.LabelNode');
+var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode');
+var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode');
+
+var hook = 'zone/moddev/mc/poweradvantage/migration/LegacyWorldDataHook';
+
+function initializeCoreMod() {
+ return {
+ 'poweradvantage_legacy_world_info': {
+ 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.SaveFormatOld' },
+ 'transformer': function(classNode) {
+ var patched = false;
+ for (var i = 0; i < classNode.methods.size(); ++i) {
+ var method = classNode.methods.get(i);
+ if (method.desc !== '(Ljava/io/File;Lcom/mojang/datafixers/DataFixer;' +
+ 'Lnet/minecraft/world/storage/SaveHandler;)Lnet/minecraft/world/storage/WorldInfo;') continue;
+ var prefix = new InsnList();
+ prefix.add(new VarInsnNode(Opcodes.ALOAD, 0));
+ prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook,
+ 'prepareLegacyWorld', '(Ljava/io/File;)V', false));
+ method.instructions.insert(prefix);
+ patched = true;
+ }
+ if (!patched) throw new Error('Power Advantage could not patch the Forge 25 legacy world reader');
+ return classNode;
+ }
+ },
+ 'poweradvantage_legacy_chunk_status': {
+ 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.chunk.storage.AnvilChunkLoader' },
+ 'transformer': function(classNode) {
+ var descriptor = '(Lnet/minecraft/world/dimension/DimensionType;' +
+ 'Lnet/minecraft/world/storage/WorldSavedDataStorage;II)' +
+ 'Lnet/minecraft/nbt/NBTTagCompound;';
+ var patchedRead = false;
+ var patchedReturn = false;
+ for (var i = 0; i < classNode.methods.size(); ++i) {
+ var method = classNode.methods.get(i);
+ if (method.desc !== descriptor) continue;
+ for (var instruction = method.instructions.getFirst(); instruction !== null;
+ instruction = instruction.getNext()) {
+ var previous = instruction.getPrevious();
+ if (instruction.getOpcode() === Opcodes.ASTORE && previous !== null
+ && previous.getOpcode() === Opcodes.INVOKESTATIC
+ && previous.owner === 'net/minecraft/nbt/CompressedStreamTools'
+ && previous.desc.endsWith(')Lnet/minecraft/nbt/NBTTagCompound;')) {
+ var prepare = new InsnList();
+ prepare.add(new VarInsnNode(Opcodes.ALOAD, instruction.var));
+ prepare.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook,
+ 'prepareLegacyChunk', '(Lnet/minecraft/nbt/NBTTagCompound;)V', false));
+ method.instructions.insert(instruction, prepare);
+ patchedRead = true;
+ } else if (instruction.getOpcode() === Opcodes.ARETURN) {
+ method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC,
+ hook, 'finalizeLegacyChunk',
+ '(Lnet/minecraft/nbt/NBTTagCompound;)Lnet/minecraft/nbt/NBTTagCompound;', false));
+ patchedReturn = true;
+ }
+ }
+ }
+ if (!patchedRead || !patchedReturn) {
+ throw new Error('Power Advantage could not patch the Forge 25 legacy chunk loader');
+ }
+ return classNode;
+ }
+ },
+ 'poweradvantage_legacy_worldgen_guard': {
+ 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.gen.WorldGenRegion' },
+ 'transformer': function(classNode) {
+ var descriptor = '(Lnet/minecraft/util/math/BlockPos;' +
+ 'Lnet/minecraft/block/state/IBlockState;I)Z';
+ var patched = false;
+ for (var i = 0; i < classNode.methods.size(); ++i) {
+ var method = classNode.methods.get(i);
+ if (method.desc !== descriptor) continue;
+ var allowed = new LabelNode();
+ var prefix = new InsnList();
+ prefix.add(new VarInsnNode(Opcodes.ALOAD, 1));
+ prefix.add(new MethodInsnNode(Opcodes.INVOKESTATIC, hook,
+ 'shouldBlockWorldgenWrite', '(Lnet/minecraft/util/math/BlockPos;)Z', false));
+ prefix.add(new JumpInsnNode(Opcodes.IFEQ, allowed));
+ prefix.add(new InsnNode(Opcodes.ICONST_0));
+ prefix.add(new InsnNode(Opcodes.IRETURN));
+ prefix.add(allowed);
+ method.instructions.insert(prefix);
+ patched = true;
+ }
+ if (!patched) throw new Error('Power Advantage could not patch the Forge 25 legacy-world write guard');
+ return classNode;
+ }
+ }
+ };
+}
diff --git a/src/main/resources/coremods/poweradvantage_material_tags.js b/src/main/resources/coremods/poweradvantage_material_tags.js
new file mode 100644
index 00000000..6923c547
--- /dev/null
+++ b/src/main/resources/coremods/poweradvantage_material_tags.js
@@ -0,0 +1,28 @@
+var Opcodes = Java.type('org.objectweb.asm.Opcodes');
+var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode');
+
+function initializeCoreMod() {
+ return {
+ 'poweradvantage_material_tags': {
+ 'target': { 'type': 'CLASS', 'name': 'net.minecraft.tags.NetworkTagManager' },
+ 'transformer': function(classNode) {
+ var patched = 0;
+ for (var i = 0; i < classNode.methods.size(); ++i) {
+ var method = classNode.methods.get(i);
+ if (method.desc !== '(Lnet/minecraft/resources/IResourceManager;)V') continue;
+ for (var instruction = method.instructions.getFirst(); instruction !== null;
+ instruction = instruction.getNext()) {
+ if (instruction.getOpcode() !== Opcodes.RETURN) continue;
+ // Forge 25 has no tags-loaded event. This runs after tag resolution,
+ // but before the following recipe and advancement reload listeners.
+ method.instructions.insertBefore(instruction, new MethodInsnNode(Opcodes.INVOKESTATIC,
+ 'zone/moddev/mc/poweradvantage/recipe/MaterialTags', 'finishReload', '()V', false));
+ patched++;
+ }
+ }
+ if (patched !== 1) throw new Error('Power Advantage could not locate the Forge 25 tag reload boundary');
+ return classNode;
+ }
+ }
+ };
+}
diff --git a/src/main/resources/coremods/poweradvantage_oil_renderer.js b/src/main/resources/coremods/poweradvantage_oil_renderer.js
new file mode 100644
index 00000000..8b7ad02e
--- /dev/null
+++ b/src/main/resources/coremods/poweradvantage_oil_renderer.js
@@ -0,0 +1,55 @@
+var Opcodes = Java.type('org.objectweb.asm.Opcodes');
+var InsnList = Java.type('org.objectweb.asm.tree.InsnList');
+var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode');
+var VarInsnNode = Java.type('org.objectweb.asm.tree.VarInsnNode');
+
+function initializeCoreMod() {
+ return {
+ 'poweradvantage_oil_renderer': {
+ 'target': { 'type': 'CLASS', 'name': 'net.minecraft.client.renderer.BlockFluidRenderer' },
+ 'transformer': function(classNode) {
+ var descriptor = '(Lnet/minecraft/world/IWorldReader;Lnet/minecraft/util/math/BlockPos;' +
+ 'Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/fluid/IFluidState;)Z';
+ var patched = false;
+ for (var i = 0; i < classNode.methods.size(); ++i) {
+ var method = classNode.methods.get(i);
+ // The descriptor also matches the production SRG-named method.
+ if (method.desc !== descriptor) continue;
+ var flag = false, sprites = false, color = false;
+ for (var instruction = method.instructions.getFirst(); instruction !== null;
+ instruction = instruction.getNext()) {
+ var hookName = null, hookDescriptor = null;
+ if (!flag && instruction.getOpcode() === Opcodes.ISTORE && instruction.var === 5) {
+ hookName = 'useOilPath';
+ hookDescriptor = '(Lnet/minecraft/fluid/IFluidState;Z)Z';
+ flag = true;
+ } else if (!sprites && instruction.getOpcode() === Opcodes.ASTORE && instruction.var === 6) {
+ hookName = 'overrideSprites';
+ hookDescriptor = '(Lnet/minecraft/fluid/IFluidState;[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;)' +
+ '[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;';
+ sprites = true;
+ } else if (!color && instruction.getOpcode() === Opcodes.ISTORE && instruction.var === 7) {
+ hookName = 'overrideColor';
+ hookDescriptor = '(Lnet/minecraft/fluid/IFluidState;I)I';
+ color = true;
+ }
+ if (hookName === null) continue;
+ var reference = instruction.var === 6;
+ var hook = new InsnList();
+ hook.add(new VarInsnNode(Opcodes.ALOAD, 4));
+ hook.add(new VarInsnNode(reference ? Opcodes.ALOAD : Opcodes.ILOAD, instruction.var));
+ hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
+ 'zone/moddev/mc/poweradvantage/client/ClientOilRenderer',
+ hookName, hookDescriptor, false));
+ hook.add(new VarInsnNode(reference ? Opcodes.ASTORE : Opcodes.ISTORE, instruction.var));
+ method.instructions.insert(instruction, hook);
+ }
+ if (!flag || !sprites || !color) throw new Error('Power Advantage could not patch the Forge 25 fluid renderer');
+ patched = true;
+ }
+ if (!patched) throw new Error('Power Advantage could not find the Forge 25 fluid renderer');
+ return classNode;
+ }
+ }
+ };
+}
diff --git a/src/main/resources/coremods/poweradvantage_parts_loot.js b/src/main/resources/coremods/poweradvantage_parts_loot.js
new file mode 100644
index 00000000..3698954f
--- /dev/null
+++ b/src/main/resources/coremods/poweradvantage_parts_loot.js
@@ -0,0 +1,25 @@
+var MethodInsnNode = Java.type('org.objectweb.asm.tree.MethodInsnNode');
+
+function initializeCoreMod() {
+ return {
+ 'poweradvantage_parts_loot': {
+ 'target': { 'type': 'CLASS', 'name': 'net.minecraft.world.storage.loot.LootTableManager' },
+ 'transformer': function(classNode) {
+ var patched = 0;
+ for (var i = 0; i < classNode.methods.size(); ++i) {
+ var method = classNode.methods.get(i);
+ for (var instruction = method.instructions.getFirst(); instruction !== null;
+ instruction = instruction.getNext()) {
+ if (instruction instanceof MethodInsnNode && instruction.owner === 'net/minecraftforge/common/ForgeHooks'
+ && instruction.name === 'loadLootTable') {
+ instruction.owner = 'zone/moddev/mc/poweradvantage/recipe/PartsLoot';
+ patched++;
+ }
+ }
+ }
+ if (patched !== 1) throw new Error('Power Advantage could not locate the Forge 25 loot loading bridge');
+ return classNode;
+ }
+ }
+ };
+}
diff --git a/src/main/resources/data/forge/tags/items/bars.json b/src/main/resources/data/forge/tags/items/bars.json
new file mode 100644
index 00000000..e58980d9
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/bars.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:iron_bars",
+ "#forge:bars/steel"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/bars/steel.json b/src/main/resources/data/forge/tags/items/bars/steel.json
new file mode 100644
index 00000000..742303f7
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/bars/steel.json
@@ -0,0 +1,7 @@
+{
+ "replace": false,
+ "values": [],
+ "optional": [
+ "basemetals:steel_bars"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/dusts/charcoal.json b/src/main/resources/data/forge/tags/items/dusts/charcoal.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/dusts/charcoal.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/dusts/coal.json b/src/main/resources/data/forge/tags/items/dusts/coal.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/dusts/coal.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/dusts/lead.json b/src/main/resources/data/forge/tags/items/dusts/lead.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/dusts/lead.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/dusts/redstone.json b/src/main/resources/data/forge/tags/items/dusts/redstone.json
new file mode 100644
index 00000000..ffe4ba88
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/dusts/redstone.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:redstone"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/dusts/silver.json b/src/main/resources/data/forge/tags/items/dusts/silver.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/dusts/silver.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/dusts/tin.json b/src/main/resources/data/forge/tags/items/dusts/tin.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/dusts/tin.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/frames/steel.json b/src/main/resources/data/forge/tags/items/frames/steel.json
new file mode 100644
index 00000000..c0f5e8e6
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/frames/steel.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:steel_frame"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/gears.json b/src/main/resources/data/forge/tags/items/gears.json
new file mode 100644
index 00000000..b3b11578
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/gears.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:sprocket"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/gears/steel.json b/src/main/resources/data/forge/tags/items/gears/steel.json
new file mode 100644
index 00000000..b3b11578
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/gears/steel.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:sprocket"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/gems/diamond.json b/src/main/resources/data/forge/tags/items/gems/diamond.json
new file mode 100644
index 00000000..0b7f5224
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/gems/diamond.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:diamond"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/gems/emerald.json b/src/main/resources/data/forge/tags/items/gems/emerald.json
new file mode 100644
index 00000000..0f3bdf92
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/gems/emerald.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:emerald"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/governors.json b/src/main/resources/data/forge/tags/items/governors.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/governors.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/brass.json b/src/main/resources/data/forge/tags/items/ingots/brass.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/brass.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/copper.json b/src/main/resources/data/forge/tags/items/ingots/copper.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/copper.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/gold.json b/src/main/resources/data/forge/tags/items/ingots/gold.json
new file mode 100644
index 00000000..74a63e9c
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/gold.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:gold_ingot"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/iron.json b/src/main/resources/data/forge/tags/items/ingots/iron.json
new file mode 100644
index 00000000..5e89bbc7
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/iron.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:iron_ingot"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/lead.json b/src/main/resources/data/forge/tags/items/ingots/lead.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/lead.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/nickel.json b/src/main/resources/data/forge/tags/items/ingots/nickel.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/nickel.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/plastic.json b/src/main/resources/data/forge/tags/items/ingots/plastic.json
new file mode 100644
index 00000000..6f60c386
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/plastic.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:bioplastic_ingot"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/refined_iron.json b/src/main/resources/data/forge/tags/items/ingots/refined_iron.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/refined_iron.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/rubber.json b/src/main/resources/data/forge/tags/items/ingots/rubber.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/rubber.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/steel.json b/src/main/resources/data/forge/tags/items/ingots/steel.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/steel.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/ingots/zinc.json b/src/main/resources/data/forge/tags/items/ingots/zinc.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/ingots/zinc.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/nuggets/bronze.json b/src/main/resources/data/forge/tags/items/nuggets/bronze.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/nuggets/bronze.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/nuggets/copper.json b/src/main/resources/data/forge/tags/items/nuggets/copper.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/nuggets/copper.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/nuggets/gold.json b/src/main/resources/data/forge/tags/items/nuggets/gold.json
new file mode 100644
index 00000000..3c978eae
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/nuggets/gold.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:gold_nugget"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/nuggets/iron.json b/src/main/resources/data/forge/tags/items/nuggets/iron.json
new file mode 100644
index 00000000..2e6bd5ff
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/nuggets/iron.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:iron_nugget"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/nuggets/lead.json b/src/main/resources/data/forge/tags/items/nuggets/lead.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/nuggets/lead.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/nuggets/tin.json b/src/main/resources/data/forge/tags/items/nuggets/tin.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/nuggets/tin.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/pipes.json b/src/main/resources/data/forge/tags/items/pipes.json
new file mode 100644
index 00000000..c62f9bb8
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/pipes.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:fluid_pipe"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/plastic.json b/src/main/resources/data/forge/tags/items/plastic.json
new file mode 100644
index 00000000..6f60c386
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/plastic.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:bioplastic_ingot"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/plates/copper.json b/src/main/resources/data/forge/tags/items/plates/copper.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/plates/copper.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/plates/iron.json b/src/main/resources/data/forge/tags/items/plates/iron.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/plates/iron.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/plates/steel.json b/src/main/resources/data/forge/tags/items/plates/steel.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/plates/steel.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/power_supply_units.json b/src/main/resources/data/forge/tags/items/power_supply_units.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/power_supply_units.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/rods.json b/src/main/resources/data/forge/tags/items/rods.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/rods.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/rods/copper.json b/src/main/resources/data/forge/tags/items/rods/copper.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/rods/copper.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/rods/wooden.json b/src/main/resources/data/forge/tags/items/rods/wooden.json
new file mode 100644
index 00000000..90f5f079
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/rods/wooden.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:stick"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/rubber.json b/src/main/resources/data/forge/tags/items/rubber.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/rubber.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/forge/tags/items/sprockets.json b/src/main/resources/data/forge/tags/items/sprockets.json
new file mode 100644
index 00000000..b3b11578
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/sprockets.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:sprocket"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/sprockets/steel.json b/src/main/resources/data/forge/tags/items/sprockets/steel.json
new file mode 100644
index 00000000..b3b11578
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/sprockets/steel.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "poweradvantage:sprocket"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/stone.json b/src/main/resources/data/forge/tags/items/stone.json
new file mode 100644
index 00000000..0e81af11
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/stone.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:stone"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/storage_blocks/diamond.json b/src/main/resources/data/forge/tags/items/storage_blocks/diamond.json
new file mode 100644
index 00000000..34d42073
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/storage_blocks/diamond.json
@@ -0,0 +1,6 @@
+{
+ "replace": false,
+ "values": [
+ "minecraft:diamond_block"
+ ]
+}
diff --git a/src/main/resources/data/forge/tags/items/storage_blocks/steel.json b/src/main/resources/data/forge/tags/items/storage_blocks/steel.json
new file mode 100644
index 00000000..5d341ba2
--- /dev/null
+++ b/src/main/resources/data/forge/tags/items/storage_blocks/steel.json
@@ -0,0 +1,4 @@
+{
+ "replace": false,
+ "values": []
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json b/src/main/resources/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json
new file mode 100644
index 00000000..b12696d4
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/bioplastic_from_starch.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:starch"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:bioplastic_from_starch"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:bioplastic_from_starch"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json b/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json
new file mode 100644
index 00000000..fdb15ac4
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_electricity.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:power_supply_units"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:frames/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:redstone_block"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:converter_rf_electricity"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:converter_rf_electricity"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json b/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json
new file mode 100644
index 00000000..3e740797
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_quantum.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:ender_pearl"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:frames/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:redstone_block"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:converter_rf_quantum"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:converter_rf_quantum"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json b/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json
new file mode 100644
index 00000000..990200c0
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/converter_rf_steam.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:governors"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:frames/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:redstone_block"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:converter_rf_steam"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:converter_rf_steam"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json
new file mode 100644
index 00000000..bd7f8c97
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_iron.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/iron"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:power_supply_units"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:converter_tr_electricity_iron"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:converter_tr_electricity_iron"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json
new file mode 100644
index 00000000..e8ad9a09
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/converter_tr_electricity_refined_iron.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/refined_iron"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:power_supply_units"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:converter_tr_electricity_refined_iron"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:converter_tr_electricity_refined_iron"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json
new file mode 100644
index 00000000..92d9a799
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_discharge.json
@@ -0,0 +1,65 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:bars"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:plates/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:frames/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_4": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_discharge"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_ingredient_4",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_discharge"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json
new file mode 100644
index 00000000..e3105a37
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_drain.json
@@ -0,0 +1,65 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:bars"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:plates/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:frames/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_4": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_drain"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_ingredient_4",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_drain"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json
new file mode 100644
index 00000000..c7d0feb8
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_iron_normal.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/iron"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_metal_tank_iron_normal"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_metal_tank_iron_normal"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json
new file mode 100644
index 00000000..6d74fa18
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_metal_tank_steel.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_metal_tank_steel"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_metal_tank_steel"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json
new file mode 100644
index 00000000..27009313
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_apocalyptic.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/copper"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_pipe_copper_apocalyptic"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_pipe_copper_apocalyptic"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json
new file mode 100644
index 00000000..729c73ec
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_copper_normal.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/copper"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_pipe_copper_normal"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_pipe_copper_normal"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json
new file mode 100644
index 00000000..23f83225
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_apocalyptic.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/iron"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_pipe_iron_apocalyptic"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_pipe_iron_apocalyptic"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json
new file mode 100644
index 00000000..3e073674
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_normal.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/iron"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_pipe_iron_normal"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_pipe_iron_normal"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json
new file mode 100644
index 00000000..0b9c5b1f
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_iron_tech.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/iron"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_pipe_iron_tech"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_pipe_iron_tech"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json
new file mode 100644
index 00000000..32c9450b
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_pipe_lead_apocalyptic.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/lead"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_pipe_lead_apocalyptic"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_pipe_lead_apocalyptic"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json
new file mode 100644
index 00000000..4ed10ce5
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_storage_tank.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/plastic"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_storage_tank"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_storage_tank"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json
new file mode 100644
index 00000000..08e18f07
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/fluid_switch.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:lever"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:frames/steel"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:fluid_switch"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:fluid_switch"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json
new file mode 100644
index 00000000..604ef11f
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_conveyor.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:plates/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:sprockets"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:hopper"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_conveyor"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_conveyor"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json
new file mode 100644
index 00000000..75e1064f
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_block.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:stone"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_block"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_block"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json
new file mode 100644
index 00000000..a781e2d6
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_food.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:bread"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_food"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_food"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json
new file mode 100644
index 00000000..4bef4dd2
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_fuel.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:coal"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_fuel"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_fuel"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json
new file mode 100644
index 00000000..cbf44f37
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_inventory.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:chest"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_inventory"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_inventory"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json
new file mode 100644
index 00000000..8b572bdf
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_ore.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/gold"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_ore"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_ore"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json
new file mode 100644
index 00000000..46807bcb
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_overflow.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_overflow"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_overflow"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json
new file mode 100644
index 00000000..8fcd57bc
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_plant.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "minecraft:saplings"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_plant"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_plant"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json
new file mode 100644
index 00000000..b0f57b85
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/item_filter_smelt.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "poweradvantage:item_conveyor"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:furnace"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:item_filter_smelt"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:item_filter_smelt"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json b/src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json
new file mode 100644
index 00000000..b8367265
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/rotator_tool.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/iron"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:sprockets"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:rotator_tool"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:rotator_tool"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json
new file mode 100644
index 00000000..088df53f
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_bronze_nugget_tech_no_rod.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:nuggets/bronze"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:sprocket_bronze_nugget_tech_no_rod"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:sprocket_bronze_nugget_tech_no_rod"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json
new file mode 100644
index 00000000..a322a855
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_normal.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:rods"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:sprocket_rod_normal"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:sprocket_rod_normal"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json
new file mode 100644
index 00000000..5f79ed5b
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_rod_tech.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/steel"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:rods"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:sprocket_rod_tech"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:sprocket_rod_tech"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json
new file mode 100644
index 00000000..2ed87710
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/sprocket_stickwood_normal.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:ingots/steel"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:sprocket_stickwood_normal"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:sprocket_stickwood_normal"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json b/src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json
new file mode 100644
index 00000000..1b43de31
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/starch_from_potato.json
@@ -0,0 +1,43 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:potato"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:poisonous_potato"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:starch_from_potato"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:starch_from_potato"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json b/src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json
new file mode 100644
index 00000000..48cafc5e
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/steel_frame.json
@@ -0,0 +1,32 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:bars/steel"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:steel_frame"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:steel_frame"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/advancements/recipes/still.json b/src/main/resources/data/poweradvantage/advancements/recipes/still.json
new file mode 100644
index 00000000..c3199575
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/advancements/recipes/still.json
@@ -0,0 +1,54 @@
+{
+ "parent": "minecraft:recipes/root",
+ "criteria": {
+ "has_ingredient_1": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:bucket"
+ }
+ ]
+ }
+ },
+ "has_ingredient_2": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "tag": "forge:pipes"
+ }
+ ]
+ }
+ },
+ "has_ingredient_3": {
+ "trigger": "minecraft:inventory_changed",
+ "conditions": {
+ "items": [
+ {
+ "item": "minecraft:furnace"
+ }
+ ]
+ }
+ },
+ "has_recipe": {
+ "trigger": "minecraft:recipe_unlocked",
+ "conditions": {
+ "recipe": "poweradvantage:still"
+ }
+ }
+ },
+ "requirements": [
+ [
+ "has_ingredient_1",
+ "has_ingredient_2",
+ "has_ingredient_3",
+ "has_recipe"
+ ]
+ ],
+ "rewards": {
+ "recipes": [
+ "poweradvantage:still"
+ ]
+ }
+}
diff --git a/src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json b/src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json
new file mode 100644
index 00000000..c4998bb3
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/loot_tables/chests/advantage_parts.json
@@ -0,0 +1,72 @@
+{
+ "pools": [
+ {
+ "__comment": "25% chance of a Power Advantage item",
+ "rolls": 1,
+ "entries": [
+ {
+ "type": "empty",
+ "weight": 60
+ },
+ {
+ "type": "item",
+ "name": "poweradvantage:sprocket",
+ "weight": 5,
+ "functions": [
+ {
+ "function": "set_count",
+ "count": {
+ "min": 2,
+ "max": 6
+ }
+ }
+ ]
+ },
+ {
+ "type": "item",
+ "name": "poweradvantage:fluid_pipe",
+ "weight": 5,
+ "functions": [
+ {
+ "function": "set_count",
+ "count": {
+ "min": 1,
+ "max": 4
+ }
+ }
+ ]
+ },
+ {
+ "type": "item",
+ "name": "poweradvantage:fluid_storage_tank",
+ "weight": 3
+ },
+ {
+ "type": "item",
+ "name": "poweradvantage:fluid_drain",
+ "weight": 2
+ },
+ {
+ "type": "item",
+ "name": "poweradvantage:fluid_discharge",
+ "weight": 2
+ },
+ {
+ "type": "item",
+ "name": "poweradvantage:item_conveyor",
+ "weight": 3,
+ "functions": [
+ {
+ "function": "set_count",
+ "count": {
+ "min": 1,
+ "max": 5
+ }
+ }
+ ]
+ }
+ ],
+ "name": "poweradvantage_legacy_parts_0"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/orespawn/provider.json b/src/main/resources/data/poweradvantage/orespawn/provider.json
new file mode 100644
index 00000000..926e1005
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/orespawn/provider.json
@@ -0,0 +1,57 @@
+{
+ "schema_version": 4,
+ "provider_modid": "poweradvantage",
+ "provider_revision": 1,
+ "profile_defaults": {
+ "place_fluid_deposits": true
+ },
+ "fluid_deposits": {
+ "poweradvantage:fluid_deposit/desert_crude_oil": {
+ "enabled": true,
+ "block": "poweradvantage:crude_oil",
+ "dimensions": {
+ "minecraft:overworld": {
+ "enabled": true,
+ "min_y": 0,
+ "max_y": 48,
+ "frequency": 0.08,
+ "min_radius": 5,
+ "max_radius": 12,
+ "min_vertical_radius": 2,
+ "max_vertical_radius": 5,
+ "max_lobes": 4,
+ "min_solid_cover": 2,
+ "min_solid_shell": 1,
+ "host_families": [
+ "sedimentary",
+ "metamorphic",
+ "igneous_intrusive",
+ "igneous_volcanic"
+ ],
+ "host_blocks": [
+ "minecraft:stone"
+ ],
+ "host_tags": [
+ "forge:stone"
+ ],
+ "biome_ids": [],
+ "excluded_biome_ids": [],
+ "biome_dictionary": [
+ "SANDY"
+ ],
+ "excluded_biome_dictionary": [
+ "BEACH",
+ "MESA"
+ ],
+ "geomes": {}
+ }
+ }
+ }
+ },
+ "rocks": {},
+ "ores": {},
+ "geomes": {},
+ "biome_rules": {},
+ "terrain_dimensions": {},
+ "templates": {}
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json b/src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json
new file mode 100644
index 00000000..2eea1c7b
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/bioplastic_from_starch.json
@@ -0,0 +1,11 @@
+{
+ "type": "minecraft:smelting",
+ "ingredient": {
+ "item": "poweradvantage:starch"
+ },
+ "result": "poweradvantage:bioplastic_ingot",
+ "experience": 0.1,
+ "cookingtime": 200,
+ "conditions": [],
+ "group": "poweradvantage:bioplastic_ingot"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json b/src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json
new file mode 100644
index 00000000..14ce2a66
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/converter_rf_electricity.json
@@ -0,0 +1,31 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xyz"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:power_supply_units"
+ },
+ "y": {
+ "tag": "forge:frames/steel"
+ },
+ "z": {
+ "item": "minecraft:redstone_block"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:converter_rf_electricity"
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:power_supply_units"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:frames/steel"
+ }
+ ],
+ "group": "poweradvantage:converter_rf_electricity"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json b/src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json
new file mode 100644
index 00000000..847a7766
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/converter_rf_quantum.json
@@ -0,0 +1,27 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xyz"
+ ],
+ "key": {
+ "x": {
+ "item": "minecraft:ender_pearl"
+ },
+ "y": {
+ "tag": "forge:frames/steel"
+ },
+ "z": {
+ "item": "minecraft:redstone_block"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:converter_rf_quantum"
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:frames/steel"
+ }
+ ],
+ "group": "poweradvantage:converter_rf_quantum"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json b/src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json
new file mode 100644
index 00000000..8063d676
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/converter_rf_steam.json
@@ -0,0 +1,31 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xyz"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:governors"
+ },
+ "y": {
+ "tag": "forge:frames/steel"
+ },
+ "z": {
+ "item": "minecraft:redstone_block"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:converter_rf_steam"
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:governors"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:frames/steel"
+ }
+ ],
+ "group": "poweradvantage:converter_rf_steam"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json b/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json
new file mode 100644
index 00000000..7e651368
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_iron.json
@@ -0,0 +1,34 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "XXX",
+ "XZX",
+ "XXX"
+ ],
+ "key": {
+ "X": {
+ "tag": "forge:ingots/iron"
+ },
+ "Z": {
+ "tag": "forge:power_supply_units"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:converter_tr_electricity"
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_empty",
+ "tag": "forge:ingots/refined_iron"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/iron"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:power_supply_units"
+ }
+ ],
+ "group": "poweradvantage:converter_tr_electricity"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json b/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json
new file mode 100644
index 00000000..b54899e9
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/converter_tr_electricity_refined_iron.json
@@ -0,0 +1,30 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "XXX",
+ "XZX",
+ "XXX"
+ ],
+ "key": {
+ "X": {
+ "tag": "forge:ingots/refined_iron"
+ },
+ "Z": {
+ "tag": "forge:power_supply_units"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:converter_tr_electricity"
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/refined_iron"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:power_supply_units"
+ }
+ ],
+ "group": "poweradvantage:converter_tr_electricity"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json b/src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json
new file mode 100644
index 00000000..be1433fc
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_fluid_discharge.json
@@ -0,0 +1,20 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:fluid_discharge"
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 2
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json b/src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json
new file mode 100644
index 00000000..9f29a41c
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_fluid_drain.json
@@ -0,0 +1,20 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:fluid_drain"
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 2
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json b/src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json
new file mode 100644
index 00000000..ef926eb3
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_fluid_storage_tank.json
@@ -0,0 +1,20 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:fluid_storage_tank"
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json
new file mode 100644
index 00000000..938aa2cd
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_conveyor.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json
new file mode 100644
index 00000000..dfbcd2d6
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_block.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_block"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json
new file mode 100644
index 00000000..21c06c04
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_food.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_food"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json
new file mode 100644
index 00000000..15f45a8f
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_fuel.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_fuel"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json
new file mode 100644
index 00000000..c3121aaf
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_inventory.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_inventory"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json
new file mode 100644
index 00000000..c452fc85
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_ore.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_ore"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json
new file mode 100644
index 00000000..87033def
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_overflow.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_overflow"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json
new file mode 100644
index 00000000..07236306
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_plant.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_plant"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json
new file mode 100644
index 00000000..bdf912e2
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_item_filter_smelt.json
@@ -0,0 +1,24 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": {
+ "item": "poweradvantage:item_filter_smelt"
+ },
+ "result": {
+ "item": "basemetals:steel_plate",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ },
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json b/src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json
new file mode 100644
index 00000000..9282b29e
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/crushing_potato_starch.json
@@ -0,0 +1,21 @@
+{
+ "type": "basemetals:crushing",
+ "ingredient": [
+ {
+ "item": "minecraft:potato"
+ },
+ {
+ "item": "minecraft:poisonous_potato"
+ }
+ ],
+ "result": {
+ "item": "poweradvantage:starch",
+ "count": 1
+ },
+ "conditions": [
+ {
+ "type": "forge:mod_loaded",
+ "modid": "basemetals"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_discharge.json b/src/main/resources/data/poweradvantage/recipes/fluid_discharge.json
new file mode 100644
index 00000000..ecf2d303
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_discharge.json
@@ -0,0 +1,44 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "ppp",
+ "w#w",
+ " x "
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:bars"
+ },
+ "w": {
+ "tag": "forge:plates/steel"
+ },
+ "#": {
+ "tag": "forge:frames/steel"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_discharge"
+ },
+ "group": "poweradvantage:fluid_discharge",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:bars"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:frames/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_drain.json b/src/main/resources/data/poweradvantage/recipes/fluid_drain.json
new file mode 100644
index 00000000..94a9e5b5
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_drain.json
@@ -0,0 +1,44 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ " x ",
+ "w#w",
+ "ppp"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:bars"
+ },
+ "w": {
+ "tag": "forge:plates/steel"
+ },
+ "#": {
+ "tag": "forge:frames/steel"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_drain"
+ },
+ "group": "poweradvantage:fluid_drain",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:bars"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:frames/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json b/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json
new file mode 100644
index 00000000..f97e9c64
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_iron_normal.json
@@ -0,0 +1,34 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ "xpx",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/iron"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_metal_tank"
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "NORMAL"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/iron"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ }
+ ],
+ "group": "poweradvantage:fluid_metal_tank"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json b/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json
new file mode 100644
index 00000000..2045c3b2
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_metal_tank_steel.json
@@ -0,0 +1,30 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ "xpx",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/steel"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_metal_tank"
+ },
+ "group": "poweradvantage:fluid_metal_tank",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json
new file mode 100644
index 00000000..fb955d41
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_apocalyptic.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ " ",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/copper"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 3
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/copper"
+ }
+ ],
+ "group": "poweradvantage:fluid_pipe"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json
new file mode 100644
index 00000000..28b0f330
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_copper_normal.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ " ",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/copper"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 6
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "NORMAL"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/copper"
+ }
+ ],
+ "group": "poweradvantage:fluid_pipe"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json
new file mode 100644
index 00000000..304275bb
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_apocalyptic.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ " ",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/iron"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 3
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/iron"
+ }
+ ],
+ "group": "poweradvantage:fluid_pipe"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json
new file mode 100644
index 00000000..138d1a20
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_normal.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ " ",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/iron"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 6
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "NORMAL"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/iron"
+ }
+ ],
+ "group": "poweradvantage:fluid_pipe"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json
new file mode 100644
index 00000000..ecfae1ea
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_iron_tech.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ " ",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/iron"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 6
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "TECH_PROGRESSION"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/iron"
+ }
+ ],
+ "group": "poweradvantage:fluid_pipe"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json
new file mode 100644
index 00000000..d5d4c736
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_pipe_lead_apocalyptic.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ " ",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/lead"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_pipe",
+ "count": 3
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "APOCALYPTIC"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/lead"
+ }
+ ],
+ "group": "poweradvantage:fluid_pipe"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json b/src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json
new file mode 100644
index 00000000..65034d0e
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_storage_tank.json
@@ -0,0 +1,30 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ "xpx",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/plastic"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_storage_tank"
+ },
+ "group": "poweradvantage:fluid_storage_tank",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/plastic"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/fluid_switch.json b/src/main/resources/data/poweradvantage/recipes/fluid_switch.json
new file mode 100644
index 00000000..c5614c49
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/fluid_switch.json
@@ -0,0 +1,32 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ " l ",
+ "pfp"
+ ],
+ "key": {
+ "l": {
+ "item": "minecraft:lever"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ },
+ "f": {
+ "tag": "forge:frames/steel"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:fluid_switch"
+ },
+ "group": "poweradvantage:fluid_switch",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:frames/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_conveyor.json b/src/main/resources/data/poweradvantage/recipes/item_conveyor.json
new file mode 100644
index 00000000..ecbcede0
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_conveyor.json
@@ -0,0 +1,34 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ "ghg",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:plates/steel"
+ },
+ "g": {
+ "tag": "forge:sprockets"
+ },
+ "h": {
+ "item": "minecraft:hopper"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_conveyor",
+ "count": 5
+ },
+ "group": "poweradvantage:item_conveyor",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:plates/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:sprockets"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_block.json b/src/main/resources/data/poweradvantage/recipes/item_filter_block.json
new file mode 100644
index 00000000..1e341238
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_block.json
@@ -0,0 +1,24 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "item": "minecraft:stone",
+ "data": 0
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_block"
+ },
+ "group": "poweradvantage:item_filter_block"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_food.json b/src/main/resources/data/poweradvantage/recipes/item_filter_food.json
new file mode 100644
index 00000000..624079b6
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_food.json
@@ -0,0 +1,23 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "item": "minecraft:bread"
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_food"
+ },
+ "group": "poweradvantage:item_filter_food"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json b/src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json
new file mode 100644
index 00000000..d6a5c935
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_fuel.json
@@ -0,0 +1,24 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "item": "minecraft:coal",
+ "data": 32767
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_fuel"
+ },
+ "group": "poweradvantage:item_filter_fuel"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json b/src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json
new file mode 100644
index 00000000..597acd77
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_inventory.json
@@ -0,0 +1,23 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "item": "minecraft:chest"
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_inventory"
+ },
+ "group": "poweradvantage:item_filter_inventory"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_ore.json b/src/main/resources/data/poweradvantage/recipes/item_filter_ore.json
new file mode 100644
index 00000000..acc6ad2b
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_ore.json
@@ -0,0 +1,29 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "tag": "forge:ingots/gold"
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_ore"
+ },
+ "group": "poweradvantage:item_filter_ore",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/gold"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json b/src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json
new file mode 100644
index 00000000..ce858ccc
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_overflow.json
@@ -0,0 +1,19 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_overflow"
+ },
+ "group": "poweradvantage:item_filter_overflow"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_plant.json b/src/main/resources/data/poweradvantage/recipes/item_filter_plant.json
new file mode 100644
index 00000000..5d4f1c23
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_plant.json
@@ -0,0 +1,23 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "tag": "minecraft:saplings"
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_plant"
+ },
+ "group": "poweradvantage:item_filter_plant"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json b/src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json
new file mode 100644
index 00000000..04e84649
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/item_filter_smelt.json
@@ -0,0 +1,23 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "x",
+ "y",
+ "z"
+ ],
+ "key": {
+ "x": {
+ "item": "poweradvantage:item_conveyor"
+ },
+ "y": {
+ "item": "minecraft:furnace"
+ },
+ "z": {
+ "item": "minecraft:oak_pressure_plate"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:item_filter_smelt"
+ },
+ "group": "poweradvantage:item_filter_smelt"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/rotator_tool.json b/src/main/resources/data/poweradvantage/recipes/rotator_tool.json
new file mode 100644
index 00000000..d90ea08a
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/rotator_tool.json
@@ -0,0 +1,30 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xx",
+ "x*",
+ " x"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/iron"
+ },
+ "*": {
+ "tag": "forge:sprockets"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:rotator_tool"
+ },
+ "group": "poweradvantage:rotator_tool",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/iron"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:sprockets"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json b/src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json
new file mode 100644
index 00000000..b699bc2c
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/sprocket_bronze_nugget_tech_no_rod.json
@@ -0,0 +1,39 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ " x ",
+ "x/x",
+ " x "
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/steel"
+ },
+ "/": {
+ "tag": "forge:nuggets/bronze"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:sprocket",
+ "count": 4
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "TECH_PROGRESSION"
+ },
+ {
+ "type": "poweradvantage:tag_empty",
+ "tag": "forge:rods"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:nuggets/bronze"
+ }
+ ],
+ "group": "poweradvantage:sprocket"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json b/src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json
new file mode 100644
index 00000000..5ce0069a
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/sprocket_rod_normal.json
@@ -0,0 +1,35 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ " x ",
+ "x/x",
+ " x "
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/steel"
+ },
+ "/": {
+ "tag": "forge:rods"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:sprocket",
+ "count": 4
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "NORMAL"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:rods"
+ }
+ ],
+ "group": "poweradvantage:sprocket"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json b/src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json
new file mode 100644
index 00000000..4a080402
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/sprocket_rod_tech.json
@@ -0,0 +1,35 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ " x ",
+ "x/x",
+ " x "
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/steel"
+ },
+ "/": {
+ "tag": "forge:rods"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:sprocket",
+ "count": 4
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "TECH_PROGRESSION"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:rods"
+ }
+ ],
+ "group": "poweradvantage:sprocket"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json b/src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json
new file mode 100644
index 00000000..90d6d9fc
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/sprocket_stickwood_normal.json
@@ -0,0 +1,35 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ " x ",
+ "x/x",
+ " x "
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:ingots/steel"
+ },
+ "/": {
+ "tag": "forge:rods/wooden"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:sprocket",
+ "count": 4
+ },
+ "conditions": [
+ {
+ "type": "poweradvantage:recipe_mode",
+ "mode": "NORMAL"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:ingots/steel"
+ },
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:rods/wooden"
+ }
+ ],
+ "group": "poweradvantage:sprocket"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/starch_from_potato.json b/src/main/resources/data/poweradvantage/recipes/starch_from_potato.json
new file mode 100644
index 00000000..1333f013
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/starch_from_potato.json
@@ -0,0 +1,11 @@
+{
+ "type": "minecraft:crafting_shapeless",
+ "ingredients": [
+ [
+ { "item": "minecraft:potato" },
+ { "item": "minecraft:poisonous_potato" }
+ ]
+ ],
+ "result": { "item": "poweradvantage:starch", "count": 1 },
+ "group": "poweradvantage:starch"
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/steel_frame.json b/src/main/resources/data/poweradvantage/recipes/steel_frame.json
new file mode 100644
index 00000000..b1452d5a
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/steel_frame.json
@@ -0,0 +1,23 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "xxx",
+ "x x",
+ "xxx"
+ ],
+ "key": {
+ "x": {
+ "tag": "forge:bars/steel"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:steel_frame"
+ },
+ "group": "poweradvantage:steel_frame",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:bars/steel"
+ }
+ ]
+}
diff --git a/src/main/resources/data/poweradvantage/recipes/still.json b/src/main/resources/data/poweradvantage/recipes/still.json
new file mode 100644
index 00000000..ddb37426
--- /dev/null
+++ b/src/main/resources/data/poweradvantage/recipes/still.json
@@ -0,0 +1,28 @@
+{
+ "type": "minecraft:crafting_shaped",
+ "pattern": [
+ "bpb",
+ " f "
+ ],
+ "key": {
+ "b": {
+ "item": "minecraft:bucket"
+ },
+ "p": {
+ "tag": "forge:pipes"
+ },
+ "f": {
+ "item": "minecraft:furnace"
+ }
+ },
+ "result": {
+ "item": "poweradvantage:still"
+ },
+ "group": "poweradvantage:still",
+ "conditions": [
+ {
+ "type": "poweradvantage:tag_not_empty",
+ "tag": "forge:pipes"
+ }
+ ]
+}
diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta
index 53f90d8b..50efb50e 100644
--- a/src/main/resources/pack.mcmeta
+++ b/src/main/resources/pack.mcmeta
@@ -1,6 +1,6 @@
{
"pack": {
"description": "Power Advantage resources",
- "pack_format": 3
+ "pack_format": 4
}
}
diff --git a/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java b/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java
new file mode 100644
index 00000000..ba9626ef
--- /dev/null
+++ b/src/test/java/cyano/poweradvantage/api/fluid/FluidNetworkApiTest.java
@@ -0,0 +1,93 @@
+package cyano.poweradvantage.api.fluid;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import cyano.poweradvantage.api.ConduitType;
+import cyano.poweradvantage.api.IPowerMachine;
+import cyano.poweradvantage.api.PowerConnectorContext;
+import cyano.poweradvantage.api.PowerRequest;
+import net.minecraft.fluid.EmptyFluid;
+import net.minecraft.fluid.Fluid;
+
+public class FluidNetworkApiTest {
+ private static final ConduitType WATER = new ConduitType("test-water");
+ private static final Fluid TEST_FLUID = new EmptyFluid();
+
+ @Test
+ public void distributesOnlyWhatConsumersAccept() {
+ Sink partial = new Sink(25);
+ Sink full = new Sink(100);
+ int accepted = FluidNetworkApi.distributeFluid(Arrays.asList(
+ new PowerRequest(PowerRequest.HIGH_PRIORITY, 80, partial),
+ new PowerRequest(PowerRequest.MEDIUM_PRIORITY, 100, full)),
+ WATER, 100, PowerRequest.LAST_PRIORITY, null);
+ assertEquals(100, accepted);
+ assertEquals(25, partial.stored, 0);
+ assertEquals(75, full.stored, 0);
+ }
+
+ @Test
+ public void respectsPriorityAndSourceExclusion() {
+ Sink source = new Sink(100);
+ Sink lowPriority = new Sink(100);
+ int accepted = FluidNetworkApi.distributeFluid(Arrays.asList(
+ new PowerRequest(PowerRequest.HIGH_PRIORITY, 100, source),
+ new PowerRequest(PowerRequest.BACKUP_PRIORITY, 100, lowPriority)),
+ WATER, 100, PowerRequest.LOW_PRIORITY, source);
+ assertEquals(0, accepted);
+ assertEquals(0, source.stored, 0);
+ assertEquals(0, lowPriority.stored, 0);
+ }
+
+ @Test
+ public void respectsTransferLimitWithoutMutatingOffer() {
+ Sink sink = new Sink(1000);
+ FluidStack offered = new FluidStack(TEST_FLUID, 750);
+ int accepted = FluidNetworkApi.distributeFluid(Arrays.asList(
+ new PowerRequest(PowerRequest.MEDIUM_PRIORITY, 1000, sink)),
+ WATER, offered, 300, PowerRequest.LAST_PRIORITY, null);
+ assertEquals(300, accepted);
+ assertEquals(750, offered.amount);
+ }
+
+ @Test
+ public void clampsInvalidConsumerResponses() {
+ Sink overAccepting = new Sink(1000) {
+ @Override public float addEnergy(float energy, ConduitType type) { return energy + 100; }
+ };
+ Sink rejecting = new Sink(1000) {
+ @Override public float addEnergy(float energy, ConduitType type) { return -10; }
+ };
+ assertEquals(40, FluidNetworkApi.distributeFluid(Arrays.asList(
+ new PowerRequest(PowerRequest.MEDIUM_PRIORITY, 100, overAccepting)),
+ WATER, 40, PowerRequest.LAST_PRIORITY, null));
+ assertEquals(0, FluidNetworkApi.distributeFluid(Arrays.asList(
+ new PowerRequest(PowerRequest.MEDIUM_PRIORITY, 100, rejecting)),
+ WATER, 40, PowerRequest.LAST_PRIORITY, null));
+ }
+
+ private static class Sink implements IPowerMachine {
+ private final float acceptanceLimit;
+ private float stored;
+
+ private Sink(float acceptanceLimit) { this.acceptanceLimit = acceptanceLimit; }
+ @Override public float getEnergyCapacity(ConduitType type) { return acceptanceLimit; }
+ @Override public float getEnergy(ConduitType type) { return stored; }
+ @Override public void setEnergy(float energy, ConduitType type) { stored = energy; }
+ @Override public float addEnergy(float energy, ConduitType type) {
+ float accepted = Math.min(energy, acceptanceLimit - stored);
+ stored += accepted;
+ return accepted;
+ }
+ @Override public float subtractEnergy(float energy, ConduitType type) { return 0; }
+ @Override public PowerRequest getPowerRequest(ConduitType type) { return PowerRequest.REQUEST_NOTHING; }
+ @Override public boolean isPowerSink(ConduitType type) { return true; }
+ @Override public boolean isPowerSource(ConduitType type) { return false; }
+ @Override public boolean canAcceptConnection(PowerConnectorContext connection) { return true; }
+ @Override public ConduitType[] getTypes() { return new ConduitType[] { WATER }; }
+ }
+}
diff --git a/src/test/java/cyano/poweradvantage/api/migration/LegacyConfigMigrationTest.java b/src/test/java/cyano/poweradvantage/api/migration/LegacyConfigMigrationTest.java
new file mode 100644
index 00000000..ad0589d9
--- /dev/null
+++ b/src/test/java/cyano/poweradvantage/api/migration/LegacyConfigMigrationTest.java
@@ -0,0 +1,58 @@
+package cyano.poweradvantage.api.migration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import cyano.poweradvantage.api.migration.LegacyConfigMigration.Mapping;
+import cyano.poweradvantage.api.migration.LegacyConfigMigration.ValueType;
+
+public class LegacyConfigMigrationTest {
+ @Rule public final TemporaryFolder temporary = new TemporaryFolder();
+
+ @Test
+ public void importsOnceAndRetainsLegacyFile() throws Exception {
+ Path config = temporary.newFolder("config").toPath();
+ Path legacy = config.resolve("example.cfg");
+ Files.write(legacy, Arrays.asList(
+ "options {",
+ " B:enabled=false",
+ " I:count=17",
+ " S:name=old value",
+ "}",
+ "\"other values\" {",
+ " S:ratio=0.25",
+ "}"), StandardCharsets.UTF_8);
+
+ LegacyConfigMigration.Result first = LegacyConfigMigration.migrate(config, "example",
+ Mapping.of("options", "enabled", "options", "enabled", ValueType.BOOLEAN),
+ Mapping.of("options", "count", "options", "count", ValueType.INTEGER),
+ Mapping.of("options", "name", "options", "name", ValueType.STRING),
+ Mapping.of("other values", "ratio", "energy", "ratio", ValueType.DOUBLE));
+
+ assertTrue(first.wasMigrated());
+ assertEquals(4, first.getImportedValues());
+ assertTrue(Files.exists(legacy));
+ assertTrue(Files.exists(first.getReportFile()));
+ String toml = new String(Files.readAllBytes(first.getTargetFile()), StandardCharsets.UTF_8);
+ assertTrue(toml.contains("enabled = false"));
+ assertTrue(toml.contains("count = 17"));
+ assertTrue(toml.contains("name = \"old value\""));
+ assertTrue(toml.contains("ratio = 0.25"));
+
+ byte[] originalTarget = Files.readAllBytes(first.getTargetFile());
+ LegacyConfigMigration.Result second = LegacyConfigMigration.migrate(config, "example",
+ Mapping.of("options", "enabled", "options", "enabled", ValueType.BOOLEAN));
+ assertFalse(second.wasMigrated());
+ assertTrue(Arrays.equals(originalTarget, Files.readAllBytes(first.getTargetFile())));
+ }
+}
diff --git a/src/test/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridgeTest.java b/src/test/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridgeTest.java
new file mode 100644
index 00000000..f38f35b8
--- /dev/null
+++ b/src/test/java/cyano/poweradvantage/api/migration/LegacyOreSpawnConfigBridgeTest.java
@@ -0,0 +1,65 @@
+package cyano.poweradvantage.api.migration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class LegacyOreSpawnConfigBridgeTest {
+ @Rule
+ public final TemporaryFolder temporary = new TemporaryFolder();
+
+ @Test
+ public void convertsOs1RulesWithoutChangingTheirSource() throws Exception {
+ Path config = temporary.newFolder("config").toPath();
+ Path legacyDirectory = Files.createDirectories(config.resolve("orespawn"));
+ Path source = legacyDirectory.resolve("poweradvantage.json");
+ Files.copy(getClass().getResourceAsStream("/orespawn-legacy/poweradvantage.json"), source);
+ byte[] original = Files.readAllBytes(source);
+
+ LegacyOreSpawnConfigBridge.Result result =
+ LegacyOreSpawnConfigBridge.migrate(config, "poweradvantage");
+
+ assertTrue(result.wasMigrated());
+ assertEquals(1, result.getRuleCount());
+ assertTrue(Files.exists(config.resolve("poweradvantage-orespawn.json")));
+ assertEquals(new String(original, StandardCharsets.UTF_8),
+ new String(Files.readAllBytes(source), StandardCharsets.UTF_8));
+ try (Reader reader = Files.newBufferedReader(result.getTarget(), StandardCharsets.UTF_8)) {
+ JsonObject provider = new JsonParser().parse(reader).getAsJsonObject();
+ JsonObject ores = provider.getAsJsonObject("ores");
+ assertTrue(ores.has("poweradvantage:legacy/poweradvantage_crude_oil_0"));
+ assertFalse(provider.getAsJsonObject("fluid_deposits")
+ .has("poweradvantage:fluid_deposit/desert_crude_oil"));
+ }
+ String report = new String(Files.readAllBytes(
+ config.resolve("orespawn-os3-migration-report.json")), StandardCharsets.UTF_8);
+ assertTrue(report.contains("os1_config_source"));
+ assertTrue(report.contains("poweradvantage:legacy/poweradvantage_crude_oil_0"));
+ }
+
+ @Test
+ public void existingModernProfileRemainsAuthoritative() throws Exception {
+ Path config = temporary.newFolder("authoritative").toPath();
+ Path legacyDirectory = Files.createDirectories(config.resolve("orespawn"));
+ Files.copy(getClass().getResourceAsStream("/orespawn-legacy/poweradvantage.json"),
+ legacyDirectory.resolve("poweradvantage.json"));
+ Files.write(config.resolve("orespawn-worldgen.json"), "{}".getBytes(StandardCharsets.UTF_8));
+
+ LegacyOreSpawnConfigBridge.Result result =
+ LegacyOreSpawnConfigBridge.migrate(config, "poweradvantage");
+
+ assertFalse(result.wasMigrated());
+ assertFalse(Files.exists(config.resolve("poweradvantage-orespawn.json")));
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java
new file mode 100644
index 00000000..6fa96761
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/conduitnetwork/ConduitNetworkManagerTest.java
@@ -0,0 +1,67 @@
+package zone.moddev.mc.poweradvantage.conduitnetwork;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import cyano.poweradvantage.api.ConduitType;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.ChunkPos;
+import org.junit.Test;
+import zone.moddev.mc.poweradvantage.math.BlockPos4D;
+
+public class ConduitNetworkManagerTest {
+ private final ConduitNetworkManager manager = new ConduitNetworkManager(new ConduitType("test"));
+
+ private static BlockPos4D pos(int dimension, int x, int z) {
+ return new BlockPos4D(dimension, new BlockPos(x, 80, z));
+ }
+
+ @Test
+ public void unloadingAnIntermediateChunkInvalidatesTheWholeNetwork() {
+ BlockPos4D source = pos(0, 13, 8), bridge = pos(0, 20, 8), sink = pos(0, 40, 8);
+ manager.addBlockToNetwork(source, bridge);
+ manager.addBlockToNetwork(source, sink);
+ manager.invalidateChunk(0, new ChunkPos(1, 0));
+ assertFalse(manager.isValidatedNetwork(source));
+ assertFalse(manager.isValidatedNetwork(bridge));
+ assertFalse(manager.isValidatedNetwork(sink));
+ }
+
+ @Test
+ public void loadingAPipeOnlyChunkInvalidatesBothBoundaryNetworks() {
+ BlockPos4D left = pos(0, 15, 8), right = pos(0, 32, 8);
+ manager.createNewNetwork(left);
+ manager.createNewNetwork(right);
+ manager.invalidateChunk(0, new ChunkPos(1, 0));
+ assertFalse(manager.isValidatedNetwork(left));
+ assertFalse(manager.isValidatedNetwork(right));
+ }
+
+ @Test
+ public void allFourChunkBoundariesAreInvalidated() {
+ BlockPos4D[] edges = {pos(0, 15, 20), pos(0, 32, 20), pos(0, 20, 15), pos(0, 20, 32)};
+ for (BlockPos4D edge : edges) manager.createNewNetwork(edge);
+ manager.invalidateChunk(0, new ChunkPos(1, 1));
+ for (BlockPos4D edge : edges) assertFalse(manager.isValidatedNetwork(edge));
+ }
+
+ @Test
+ public void negativeChunkCoordinatesUseTheSameBoundaryRules() {
+ BlockPos4D edge = pos(0, -17, -8);
+ manager.createNewNetwork(edge);
+ manager.invalidateChunk(0, new ChunkPos(-1, -1));
+ assertFalse(manager.isValidatedNetwork(edge));
+ }
+
+ @Test
+ public void unrelatedNetworksAndDimensionsRemainCached() {
+ BlockPos4D nearby = pos(0, 14, 8), distant = pos(0, 80, 8), otherDimension = pos(-1, 20, 8);
+ manager.createNewNetwork(nearby);
+ manager.createNewNetwork(distant);
+ manager.createNewNetwork(otherDimension);
+ manager.invalidateChunk(0, new ChunkPos(1, 0));
+ assertTrue(manager.isValidatedNetwork(nearby));
+ assertTrue(manager.isValidatedNetwork(distant));
+ assertTrue(manager.isValidatedNetwork(otherDimension));
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java
new file mode 100644
index 00000000..81b77250
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/AdvantageLegacyMigrationsTest.java
@@ -0,0 +1,42 @@
+package zone.moddev.mc.poweradvantage.migration;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import cyano.poweradvantage.api.migration.LegacyMigrationApi;
+import java.util.Arrays;
+import net.minecraft.util.ResourceLocation;
+import org.junit.Test;
+
+public class AdvantageLegacyMigrationsTest {
+ @Test public void registersEveryLegacyPowerBlockCodec() {
+ AdvantageLegacyMigrations.registerPowerAdvantage();
+ for (String path : Arrays.asList(
+ "fluid_drain", "fluid_discharge", "fluid_storage_tank", "fluid_metal_tank",
+ "still", "fluid_pipe", "fluid_pipe_terminal", "steel_frame", "item_conveyor",
+ "item_filter_block", "item_filter_food", "item_filter_fuel",
+ "item_filter_inventory", "item_filter_ore", "item_filter_plant",
+ "item_filter_smelt", "item_filter_overflow", "fluid_switch", "infinite_steam",
+ "infinite_electricity", "infinite_quantum", "crude_oil", "refined_oil",
+ "converter_rf_steam", "converter_rf_electricity", "converter_rf_quantum",
+ "converter_tr_electricity")) {
+ assertNotNull(path, LegacyMigrationApi.blockStateCodecs().get(
+ new ResourceLocation("poweradvantage", path)));
+ }
+ }
+
+ @Test public void retainsLegacyTileFluidAndBucketIdentities() {
+ AdvantageLegacyMigrations.registerPowerAdvantage();
+ assertTrue(LegacyMigrationApi.handlesNamespace("poweradvantage"));
+ assertEquals("poweradvantage:tileentity.fluid_storage_tank",
+ LegacyMigrationApi.tileTarget("poweradvantage.tileentity.fluid_storage_tank"));
+ assertEquals("poweradvantage:tileentity.fluid_storage_tank",
+ LegacyMigrationApi.tileTarget("minecraft:poweradvantage.tileentity.fluid_storage_tank"));
+ assertEquals("poweradvantage:crude_oil", LegacyMigrationApi.fluidTarget("crude_oil"));
+ assertEquals(new ResourceLocation("poweradvantage", "crude_oil_bucket"),
+ LegacyMigrationApi.bucketTarget("crude_oil"));
+ assertEquals(new ResourceLocation("poweradvantage", "refined_oil_bucket"),
+ LegacyMigrationApi.bucketTarget("poweradvantage:refined_oil"));
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java
new file mode 100644
index 00000000..abc9d523
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEnchantmentsTest.java
@@ -0,0 +1,62 @@
+package zone.moddev.mc.poweradvantage.migration;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class LegacyEnchantmentsTest {
+ @Test public void resolvesOriginalMusketIdsBeforeVanillaCanAssignBindingCurse() {
+ Map saved = new LinkedHashMap<>();
+ saved.put(10, "steamadvantage:high_explosive");
+ saved.put(11, "steamadvantage:powderless");
+ saved.put(12, "steamadvantage:rapid_reload");
+ saved.put(13, "steamadvantage:recoil");
+ NBTTagCompound tag = new NBTTagCompound();
+ NBTTagList list = new NBTTagList();
+ for (Integer id : saved.keySet()) {
+ NBTTagCompound entry = new NBTTagCompound();
+ entry.setShort("id", id.shortValue());
+ entry.setShort("lvl", (short) 3);
+ list.add(entry);
+ }
+ tag.setTag("ench", list);
+ assertTrue(LegacyWorldDataHook.migrateLegacyEnchantments(tag, saved));
+ assertFalse(tag.contains("ench", 9));
+ for (int i = 0; i < 4; i++) {
+ assertEquals(saved.get(10 + i), tag.getList("Enchantments", 10).getCompound(i).getString("id"));
+ assertEquals(3, tag.getList("Enchantments", 10).getCompound(i).getShort("lvl"));
+ }
+ assertFalse(LegacyWorldDataHook.migrateLegacyEnchantments(tag, saved));
+ }
+
+ @Test public void honorsWorldSpecificIdsAndPreservesStoredEnchantments() {
+ Map saved = new LinkedHashMap<>();
+ saved.put(173, "steamadvantage:rapid_reload");
+ NBTTagCompound book = new NBTTagCompound();
+ NBTTagList list = new NBTTagList();
+ NBTTagCompound entry = new NBTTagCompound();
+ entry.setShort("id", (short) 173);
+ entry.setShort("lvl", (short) 2);
+ list.add(entry);
+ book.setTag("StoredEnchantments", list);
+ assertTrue(LegacyWorldDataHook.migrateLegacyEnchantments(book, saved));
+ assertEquals("steamadvantage:rapid_reload", book.getList("StoredEnchantments", 10).getCompound(0).getString("id"));
+ assertEquals(2, book.getList("StoredEnchantments", 10).getCompound(0).getShort("lvl"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void refusesIncompleteOldListInsteadOfSilentlyDroppingAnEnchantment() {
+ Map saved = new LinkedHashMap<>();
+ saved.put(10, "steamadvantage:high_explosive");
+ NBTTagCompound tag = new NBTTagCompound();
+ NBTTagList list = new NBTTagList();
+ NBTTagCompound entry = new NBTTagCompound();
+ entry.setShort("id", (short) 99);
+ list.add(entry);
+ tag.setTag("ench", list);
+ LegacyWorldDataHook.migrateLegacyEnchantments(tag, saved);
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java
new file mode 100644
index 00000000..5d1262d7
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/migration/LegacyEntitiesTest.java
@@ -0,0 +1,122 @@
+package zone.moddev.mc.poweradvantage.migration;
+
+import com.mojang.datafixers.DataFixTypes;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTUtil;
+import net.minecraft.util.datafix.DataFixesManager;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class LegacyEntitiesTest {
+ @Test public void retainsWolfInDirect110Chunk() {
+ assertWolf(false);
+ }
+
+ @Test public void retainsWolfBesideCustomEntityInDirect110Chunk() {
+ assertWolf(true);
+ }
+
+ @Test public void fixesVanillaPassengersInsideUnknownModEntities() {
+ NBTTagCompound vehicle = entity("example:vehicle");
+ vehicle.setString("CustomState", "keep-verbatim");
+ NBTTagCompound wolf = entity("Wolf");
+ wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f");
+ NBTTagCompound nested = entity("example:seat");
+ NBTTagList nestedPassengers = new NBTTagList();
+ nestedPassengers.add(entity("Cow"));
+ nested.setTag("Passengers", nestedPassengers);
+ NBTTagList passengers = new NBTTagList();
+ passengers.add(wolf);
+ passengers.add(nested);
+ vehicle.setTag("Passengers", passengers);
+ NBTTagCompound root = chunk(vehicle, 512);
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ NBTTagCompound actual = root.getCompound("Level").getList("Entities", 10).getCompound(0);
+ assertEquals("example:vehicle", actual.getString("id"));
+ assertEquals("keep-verbatim", actual.getString("CustomState"));
+ NBTTagList fixed = actual.getList("Passengers", 10);
+ assertEquals(2, fixed.size());
+ assertEquals("minecraft:wolf", fixed.getCompound(0).getString("id"));
+ assertEquals(wolf.getString("OwnerUUID"), fixed.getCompound(0).getString("OwnerUUID"));
+ assertEquals("example:seat", fixed.getCompound(1).getString("id"));
+ assertEquals("minecraft:cow", fixed.getCompound(1).getList("Passengers", 10).getCompound(0).getString("id"));
+ }
+
+ @Test public void usesVanillaSubtypeFixersRatherThanLowercasingOldIds() {
+ NBTTagCompound skeleton = entity("Skeleton");
+ skeleton.setByte("SkeletonType", (byte) 1);
+ NBTTagCompound root = chunk(skeleton, 512);
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ assertEquals("minecraft:wither_skeleton", root.getCompound("Level").getList("Entities", 10).getCompound(0).getString("id"));
+ }
+
+ @Test public void leavesModernEntityAndUnrelatedCustomNbtUnchangedOnRepeatedPreparation() {
+ NBTTagCompound wolf = entity("minecraft:wolf");
+ wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f");
+ wolf.setTag("CustomData", entity("Wolf"));
+ NBTTagCompound root = chunk(wolf, 1343);
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ assertEquals(wolf, root.getCompound("Level").getList("Entities", 10).getCompound(0));
+ }
+
+ @Test public void repeatPreparationDoesNotLoseConvertedVanillaState() {
+ NBTTagCompound wolf = entity("Wolf");
+ wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f");
+ NBTTagCompound root = chunk(wolf, 512);
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ NBTTagCompound once = root.copy();
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ assertEquals(once, root);
+ }
+
+ private static NBTTagCompound entity(String id) {
+ NBTTagCompound entity = new NBTTagCompound();
+ entity.setString("id", id);
+ return entity;
+ }
+
+ private static NBTTagCompound chunk(NBTTagCompound entity, int version) {
+ NBTTagCompound root = new NBTTagCompound();
+ root.setInt("DataVersion", version);
+ NBTTagCompound level = new NBTTagCompound();
+ NBTTagList entities = new NBTTagList();
+ entities.add(entity);
+ level.setTag("Entities", entities);
+ root.setTag("Level", level);
+ return root;
+ }
+
+ private static void assertWolf(boolean customEntity) {
+ NBTTagCompound root = new NBTTagCompound();
+ root.setInt("DataVersion", 512);
+ NBTTagCompound level = new NBTTagCompound();
+ NBTTagList entities = new NBTTagList();
+ NBTTagCompound wolf = new NBTTagCompound();
+ wolf.setString("id", "Wolf");
+ wolf.setString("OwnerUUID", "a614e658-6270-33c3-9297-7ce80908964f");
+ wolf.setBoolean("Sitting", true);
+ wolf.setLong("UUIDMost", 123L);
+ wolf.setLong("UUIDLeast", 456L);
+ entities.add(wolf);
+ if (customEntity) {
+ NBTTagCompound turbine = new NBTTagCompound();
+ turbine.setString("id", "electricadvantage.electricadvantage.Hydroturbine");
+ turbine.setIntArray("parentPos", new int[] {164, 81, 8});
+ entities.add(turbine);
+ }
+ level.setTag("Entities", entities);
+ root.setTag("Level", level);
+ LegacyWorldDataHook.prepareLegacyChunk(root);
+ NBTTagCompound fixed = NBTUtil.update(DataFixesManager.getDataFixer(), DataFixTypes.CHUNK, root, 512, 1343);
+ NBTTagCompound actual = fixed.getCompound("Level").getList("Entities", 10).getCompound(0);
+ assertEquals("minecraft:wolf", actual.getString("id"));
+ assertEquals(wolf.getString("OwnerUUID"), actual.getString("OwnerUUID"));
+ assertTrue(actual.getBoolean("Sitting"));
+ assertEquals(123L, actual.getLong("UUIDMost"));
+ assertEquals(456L, actual.getLong("UUIDLeast"));
+ assertEquals(customEntity ? 2 : 1, fixed.getCompound("Level").getList("Entities", 10).size());
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/ClientResourceContractTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/ClientResourceContractTest.java
new file mode 100644
index 00000000..64247224
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/ClientResourceContractTest.java
@@ -0,0 +1,120 @@
+package zone.moddev.mc.poweradvantage.resources;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.*;
+
+public class ClientResourceContractTest {
+ private static final Path ROOT = Paths.get("src/main/resources");
+ private static final Path ASSETS = ROOT.resolve("assets/poweradvantage");
+
+ @Test public void blockstatesResolveLiteralModelPaths() throws IOException {
+ for (Path file : jsonFiles(ASSETS.resolve("blockstates"))) {
+ verifyModelReferences(read(file), file);
+ }
+ }
+
+ @Test public void blockstatesDoNotUseRemovedLegacySelectors() throws IOException {
+ for (Path file : jsonFiles(ASSETS.resolve("blockstates"))) {
+ JsonObject state = read(file);
+ assertFalse(file + " uses the removed Forge blockstate format", state.has("forge_marker"));
+ if (state.has("variants")) {
+ assertFalse(file + " uses the removed normal selector", state.getAsJsonObject("variants").has("normal"));
+ assertFalse(file + " uses the removed fluid selector", state.getAsJsonObject("variants").has("fluid"));
+ }
+ }
+ }
+
+ @Test public void modelParentsAndTexturesExistInActualResourcePacks() throws IOException {
+ for (Path file : jsonFiles(ASSETS.resolve("models"))) {
+ JsonObject model = read(file);
+ if (model.has("parent")) verifyReference(model.get("parent").getAsString(), "models", ".json", file);
+ if (model.has("textures")) {
+ for (Map.Entry texture : model.getAsJsonObject("textures").entrySet()) {
+ verifyReference(texture.getValue().getAsString(), "textures", ".png", file);
+ }
+ }
+ }
+ }
+
+ @Test public void oilBucketsAndHiddenFluidItemsHaveModels() {
+ for (String id : new String[] {"crude_oil", "refined_oil", "crude_oil_bucket", "refined_oil_bucket"}) {
+ assertTrue("Missing item model for " + id, Files.isRegularFile(ASSETS.resolve("models/item/" + id + ".json")));
+ }
+ }
+
+ @Test public void everyLocaleRetainsLegacyNamesAndProvidesModernAliases() throws IOException {
+ for (Path file : jsonFiles(ASSETS.resolve("lang"))) {
+ JsonObject locale = read(file);
+ for (Map.Entry entry : locale.entrySet()) {
+ String key = entry.getKey();
+ if ((key.startsWith("tile.poweradvantage.") || key.startsWith("item.poweradvantage."))
+ && key.endsWith(".name")) {
+ String modern = key.substring(0, key.length() - ".name".length());
+ if (modern.startsWith("tile.")) modern = "block." + modern.substring("tile.".length());
+ assertEquals(file + " missing modern alias " + modern, entry.getValue(), locale.get(modern));
+ }
+ }
+ }
+ }
+
+ @Test public void englishCoversEveryBlockAndOilBucket() throws IOException {
+ JsonObject locale = read(ASSETS.resolve("lang/en_us.json"));
+ for (Path file : jsonFiles(ASSETS.resolve("blockstates"))) {
+ String id = file.getFileName().toString().replace(".json", "");
+ assertTrue("Missing block name: " + id, locale.has("block.poweradvantage." + id));
+ }
+ for (String id : new String[] {"crude_oil", "refined_oil"}) {
+ assertEquals(locale.get("item.bucket." + id), locale.get("item.poweradvantage." + id + "_bucket"));
+ }
+ }
+
+ private static void verifyModelReferences(JsonElement node, Path file) {
+ if (node.isJsonObject()) {
+ for (Map.Entry entry : node.getAsJsonObject().entrySet()) {
+ if (entry.getKey().equals("model")) verifyReference(entry.getValue().getAsString(), "models", ".json", file);
+ else verifyModelReferences(entry.getValue(), file);
+ }
+ } else if (node.isJsonArray()) {
+ for (JsonElement child : node.getAsJsonArray()) verifyModelReferences(child, file);
+ }
+ }
+
+ private static void verifyReference(String reference, String directory, String suffix, Path file) {
+ if (reference.startsWith("#")) return;
+ String[] parts = reference.split(":", 2);
+ String namespace = parts.length == 2 ? parts[0] : "minecraft";
+ String path = parts.length == 2 ? parts[1] : parts[0];
+ if (directory.equals("models") && namespace.equals("minecraft") && path.startsWith("builtin/")) return;
+ String resource = "assets/" + namespace + "/" + directory + "/" + path + suffix;
+ boolean found = namespace.equals("poweradvantage") ? Files.isRegularFile(ROOT.resolve(resource))
+ : ClientResourceContractTest.class.getClassLoader().getResource(resource) != null;
+ assertTrue(file + " references missing resource " + resource, found);
+ }
+
+ private static JsonObject read(Path file) throws IOException {
+ try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
+ return new JsonParser().parse(reader).getAsJsonObject();
+ }
+ }
+
+ private static List jsonFiles(Path directory) throws IOException {
+ try (Stream files = Files.walk(directory)) {
+ return files.filter(path -> path.toString().endsWith(".json")).sorted().collect(Collectors.toList());
+ }
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java
new file mode 100644
index 00000000..e816f940
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/MaterialTagTransformerTest.java
@@ -0,0 +1,60 @@
+package zone.moddev.mc.poweradvantage.resources;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import static org.junit.Assert.*;
+
+public class MaterialTagTransformerTest {
+ @Test public void hooksThePinnedReloadAfterAllTagCollectionsAreInstalled() throws Exception {
+ check(false);
+ }
+
+ @Test public void hooksProductionNamesWithoutDependingOnMappings() throws Exception {
+ check(true);
+ }
+
+ private static void check(boolean productionName) throws Exception {
+ ClassNode node = new ClassNode();
+ try (InputStream input = MaterialTagTransformerTest.class.getResourceAsStream(
+ "/net/minecraft/tags/NetworkTagManager.class")) {
+ assertNotNull(input);
+ new ClassReader(input).accept(node, 0);
+ }
+ MethodNode reload = null;
+ for (MethodNode method : node.methods) {
+ if (method.desc.equals("(Lnet/minecraft/resources/IResourceManager;)V")) reload = method;
+ }
+ assertNotNull(reload);
+ if (productionName) reload.name = "func_195410_a";
+ ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
+ try (Reader script = Files.newBufferedReader(Paths.get("src/main/resources/coremods/poweradvantage_material_tags.js"),
+ StandardCharsets.UTF_8)) { engine.eval(script); }
+ engine.put("manager", node);
+ engine.eval("initializeCoreMod().poweradvantage_material_tags.transformer(manager)");
+ int installed = 0, hooks = 0;
+ for (AbstractInsnNode instruction : reload.instructions.toArray()) {
+ if (!(instruction instanceof MethodInsnNode)) continue;
+ MethodInsnNode call = (MethodInsnNode) instruction;
+ if (call.owner.startsWith("net/minecraft/tags/") && call.desc.endsWith("TagCollection;)V")) installed++;
+ if (call.owner.equals("zone/moddev/mc/poweradvantage/recipe/MaterialTags")) {
+ assertEquals(3, installed);
+ assertEquals("finishReload", call.name);
+ assertEquals(Opcodes.RETURN, call.getNext().getOpcode());
+ hooks++;
+ }
+ }
+ assertEquals(1, hooks);
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java
new file mode 100644
index 00000000..9734df96
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/OilRendererTransformerTest.java
@@ -0,0 +1,113 @@
+package zone.moddev.mc.poweradvantage.resources;
+
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.InsnList;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import org.objectweb.asm.tree.VarInsnNode;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.junit.Assert.*;
+
+/** Exercises the shipped JavaScript against the pinned native renderer bytecode. */
+public class OilRendererTransformerTest {
+ private static final String RENDER = "(Lnet/minecraft/world/IWorldReader;Lnet/minecraft/util/math/BlockPos;"
+ + "Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/fluid/IFluidState;)Z";
+ private static final String OWNER = "zone/moddev/mc/poweradvantage/client/ClientOilRenderer";
+ private static final String PEER = "test/SiblingFluidRenderer";
+
+ @Test public void patchesMappedRenderer() throws Exception {
+ ClassNode node = renderer();
+ transform(node);
+ assertHooks(node, OWNER);
+ }
+
+ @Test public void patchesProductionNamedRenderer() throws Exception {
+ ClassNode node = renderer();
+ render(node).name = "func_209557_a";
+ transform(node);
+ assertHooks(node, OWNER);
+ }
+
+ @Test public void preservesEarlierSiblingHooks() throws Exception {
+ ClassNode node = renderer();
+ addSiblingHooks(node);
+ transform(node);
+ assertHooks(node, OWNER);
+ assertHooks(node, PEER);
+ }
+
+ @Test public void preservesLaterSiblingHooks() throws Exception {
+ ClassNode node = renderer();
+ transform(node);
+ addSiblingHooks(node);
+ assertHooks(node, OWNER);
+ assertHooks(node, PEER);
+ }
+
+ private static ClassNode renderer() throws Exception {
+ ClassNode node = new ClassNode();
+ try (InputStream input = OilRendererTransformerTest.class.getResourceAsStream(
+ "/net/minecraft/client/renderer/BlockFluidRenderer.class")) {
+ assertNotNull("Pinned Forge renderer is absent", input);
+ new ClassReader(input).accept(node, 0);
+ }
+ return node;
+ }
+
+ private static MethodNode render(ClassNode node) {
+ for (MethodNode method : node.methods) if (RENDER.equals(method.desc)) return method;
+ throw new AssertionError("Pinned renderer signature changed");
+ }
+
+ private static void transform(ClassNode node) throws Exception {
+ ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
+ assertNotNull("Run transformer tests with the Java 8 toolchain", engine);
+ try (Reader script = Files.newBufferedReader(Paths.get("src/main/resources/coremods/poweradvantage_oil_renderer.js"),
+ StandardCharsets.UTF_8)) {
+ engine.eval(script);
+ }
+ engine.put("renderer", node);
+ assertSame(node, engine.eval("initializeCoreMod().poweradvantage_oil_renderer.transformer(renderer)"));
+ }
+
+ private static void assertHooks(ClassNode node, String owner) {
+ int count = 0;
+ for (AbstractInsnNode instruction : render(node).instructions.toArray()) {
+ if (instruction instanceof MethodInsnNode && owner.equals(((MethodInsnNode) instruction).owner)) count++;
+ }
+ assertEquals("Missing or duplicated hooks for " + owner, 3, count);
+ }
+
+ private static void addSiblingHooks(ClassNode node) {
+ // Base Metals and Mineralogy hook these same first local stores. Keep both chains intact.
+ boolean[] patched = new boolean[3];
+ for (AbstractInsnNode instruction : render(node).instructions.toArray()) {
+ if (!(instruction instanceof VarInsnNode)) continue;
+ int slot = ((VarInsnNode) instruction).var;
+ if (slot < 5 || slot > 7 || patched[slot - 5]) continue;
+ boolean reference = slot == 6;
+ if (instruction.getOpcode() != (reference ? Opcodes.ASTORE : Opcodes.ISTORE)) continue;
+ String desc = slot == 5 ? "Z" : reference ? "[Lnet/minecraft/client/renderer/texture/TextureAtlasSprite;" : "I";
+ InsnList hook = new InsnList();
+ hook.add(new VarInsnNode(Opcodes.ALOAD, 4));
+ hook.add(new VarInsnNode(reference ? Opcodes.ALOAD : Opcodes.ILOAD, slot));
+ hook.add(new MethodInsnNode(Opcodes.INVOKESTATIC, PEER, "override",
+ "(Lnet/minecraft/fluid/IFluidState;" + desc + ")" + desc, false));
+ hook.add(new VarInsnNode(reference ? Opcodes.ASTORE : Opcodes.ISTORE, slot));
+ render(node).instructions.insert(instruction, hook);
+ patched[slot - 5] = true;
+ }
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java
new file mode 100644
index 00000000..30fbade1
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/PartsLootTransformerTest.java
@@ -0,0 +1,45 @@
+package zone.moddev.mc.poweradvantage.resources;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import org.junit.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+import static org.junit.Assert.*;
+
+public class PartsLootTransformerTest {
+ @Test public void redirectsOnlyThePinnedForgeLoaderWithAnIdenticalStackContract() throws Exception {
+ ClassNode node = new ClassNode();
+ try (InputStream input = getClass().getResourceAsStream("/net/minecraft/world/storage/loot/LootTableManager.class")) {
+ assertNotNull(input);
+ new ClassReader(input).accept(node, 0);
+ }
+ ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
+ try (Reader script = Files.newBufferedReader(Paths.get("src/main/resources/coremods/poweradvantage_parts_loot.js"),
+ StandardCharsets.UTF_8)) { engine.eval(script); }
+ engine.put("manager", node);
+ engine.eval("initializeCoreMod().poweradvantage_parts_loot.transformer(manager)");
+ int calls = 0;
+ for (MethodNode method : node.methods) for (AbstractInsnNode instruction : method.instructions.toArray()) {
+ if (!(instruction instanceof MethodInsnNode)) continue;
+ MethodInsnNode call = (MethodInsnNode) instruction;
+ if (call.owner.equals("zone/moddev/mc/poweradvantage/recipe/PartsLoot")) {
+ calls++;
+ assertEquals(Opcodes.INVOKESTATIC, call.getOpcode());
+ assertEquals("loadLootTable", call.name);
+ assertEquals("(Lcom/google/gson/Gson;Lnet/minecraft/util/ResourceLocation;Ljava/lang/String;ZLnet/minecraft/world/storage/loot/LootTableManager;)Lnet/minecraft/world/storage/loot/LootTable;", call.desc);
+ }
+ assertFalse(call.owner.equals("net/minecraftforge/common/ForgeHooks") && call.name.equals("loadLootTable"));
+ }
+ assertEquals(1, calls);
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java
new file mode 100644
index 00000000..6c026f38
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/RecipeUnlockContractTest.java
@@ -0,0 +1,131 @@
+package zone.moddev.mc.poweradvantage.resources;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class RecipeUnlockContractTest {
+ private static final String MOD = "poweradvantage";
+ private static final Path DATA = Paths.get("src/main/resources/data", MOD);
+ private static final Set IGNORED = new LinkedHashSet<>(java.util.Arrays.asList(
+ "item:minecraft:stick", "item:minecraft:string", "item:minecraft:feather", "tag:forge:rods/wooden"));
+
+ @Test public void everyCraftingRecipeHasExactlyOneUnlockAndProcessingHasNone() throws IOException {
+ Set expected = new LinkedHashSet<>();
+ for (Path file : files(DATA.resolve("recipes"))) {
+ if (crafting(read(file))) expected.add(file.getFileName().toString());
+ }
+ Set actual = files(DATA.resolve("advancements/recipes")).stream()
+ .map(file -> file.getFileName().toString()).collect(Collectors.toSet());
+ assertFalse("No crafting recipes found", expected.isEmpty());
+ assertEquals("Missing or orphaned recipe unlocks", expected, actual);
+ }
+
+ @Test public void discoveriesAreSingleIngredientAlternativesNotAnEntireCraftingGrid() throws IOException {
+ for (Path file : files(DATA.resolve("recipes"))) {
+ JsonObject recipe = read(file);
+ if (!crafting(recipe)) continue;
+ JsonObject unlock = read(DATA.resolve("advancements/recipes").resolve(file.getFileName()));
+ assertEquals("minecraft:recipes/root", unlock.get("parent").getAsString());
+ assertFalse("Recipe unlocks should not create visible advancement tabs", unlock.has("display"));
+ JsonObject criteria = unlock.getAsJsonObject("criteria");
+ Set actual = new LinkedHashSet<>();
+ for (Map.Entry entry : criteria.entrySet()) {
+ if (entry.getKey().equals("has_recipe")) continue;
+ JsonObject criterion = entry.getValue().getAsJsonObject();
+ assertEquals(file.toString(), "minecraft:inventory_changed", criterion.get("trigger").getAsString());
+ JsonArray items = criterion.getAsJsonObject("conditions").getAsJsonArray("items");
+ assertEquals("Finding ONE ingredient must be enough: " + file, 1, items.size());
+ JsonObject predicate = items.get(0).getAsJsonObject();
+ assertEquals("Only an item or tag predicate is expected", 1, predicate.size());
+ actual.add(predicateKey(predicate));
+ }
+ List choices = new ArrayList<>();
+ if (recipe.has("key")) {
+ for (Map.Entry entry : recipe.getAsJsonObject("key").entrySet())
+ collect(entry.getValue(), choices);
+ } else collect(recipe.has("ingredients") ? recipe.get("ingredients") : recipe.get("ingredient"), choices);
+ Set expected = choices.stream().map(RecipeUnlockContractTest::predicateKey)
+ .filter(key -> !IGNORED.contains(key)).collect(Collectors.toCollection(LinkedHashSet::new));
+ if (expected.isEmpty()) for (JsonObject choice : choices) expected.add(predicateKey(choice));
+ assertEquals("Trigger must correspond to a relevant recipe ingredient: " + file, expected, actual);
+ JsonArray requirements = unlock.getAsJsonArray("requirements");
+ assertEquals("All criteria must form one OR group", 1, requirements.size());
+ Set alternatives = new LinkedHashSet<>();
+ for (JsonElement key : requirements.get(0).getAsJsonArray()) alternatives.add(key.getAsString());
+ assertEquals(criteria.entrySet().stream().map(Map.Entry::getKey).collect(Collectors.toSet()), alternatives);
+ }
+ }
+
+ @Test public void rewardAndRecipeCommandCriterionReferToTheExactVariant() throws IOException {
+ for (Path file : files(DATA.resolve("advancements/recipes"))) {
+ String id = MOD + ":" + file.getFileName().toString().replace(".json", "");
+ JsonObject unlock = read(file);
+ JsonArray recipes = unlock.getAsJsonObject("rewards").getAsJsonArray("recipes");
+ assertEquals(1, recipes.size());
+ assertEquals(id, recipes.get(0).getAsString());
+ JsonObject criterion = unlock.getAsJsonObject("criteria").getAsJsonObject("has_recipe");
+ assertEquals("minecraft:recipe_unlocked", criterion.get("trigger").getAsString());
+ assertEquals(id, criterion.getAsJsonObject("conditions").get("recipe").getAsString());
+ assertFalse("Forge recipe conditions are not valid advancement conditions", unlock.has("conditions"));
+ }
+ }
+
+ @Test public void recipeBookGroupsIdentifyTheOutputNotABroadMachineCategory() throws IOException {
+ for (Path file : files(DATA.resolve("recipes"))) {
+ JsonObject recipe = read(file);
+ if (!crafting(recipe)) continue;
+ JsonElement result = recipe.get("result");
+ String output = result.isJsonPrimitive() ? result.getAsString()
+ : result.getAsJsonObject().get("item").getAsString();
+ assertTrue("Missing output-specific group: " + file, recipe.has("group"));
+ assertEquals("Different items must not share a recipe-book cell: " + file,
+ output, recipe.get("group").getAsString());
+ }
+ }
+
+ private static boolean crafting(JsonObject recipe) {
+ String type = recipe.get("type").getAsString();
+ return type.equals("minecraft:crafting_shaped") || type.equals("minecraft:crafting_shapeless")
+ || type.equals("minecraft:smelting");
+ }
+
+ private static String predicateKey(JsonObject predicate) {
+ String type = predicate.has("tag") ? "tag" : "item";
+ return type + ":" + predicate.get(type).getAsString();
+ }
+
+ private static void collect(JsonElement element, List result) {
+ if (element.isJsonArray()) for (JsonElement child : element.getAsJsonArray()) collect(child, result);
+ else result.add(element.getAsJsonObject());
+ }
+
+ private static JsonObject read(Path file) throws IOException {
+ try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
+ return new JsonParser().parse(reader).getAsJsonObject();
+ }
+ }
+
+ private static List files(Path directory) throws IOException {
+ if (!Files.isDirectory(directory)) return java.util.Collections.emptyList();
+ try (Stream files = Files.walk(directory)) {
+ return files.filter(path -> path.toString().endsWith(".json")).sorted().collect(Collectors.toList());
+ }
+ }
+}
diff --git a/src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java b/src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java
new file mode 100644
index 00000000..dab10709
--- /dev/null
+++ b/src/test/java/zone/moddev/mc/poweradvantage/resources/StarchRecipeTest.java
@@ -0,0 +1,46 @@
+package zone.moddev.mc.poweradvantage.resources;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class StarchRecipeTest {
+ @Test public void craftingAcceptsOneOrdinaryOrPoisonousPotatoWithoutAnotherMod() throws Exception {
+ JsonObject recipe = read("starch_from_potato");
+ assertEquals("minecraft:crafting_shapeless", recipe.get("type").getAsString());
+ assertFalse(recipe.has("conditions"));
+ JsonArray inputs = recipe.getAsJsonArray("ingredients");
+ assertEquals(1, inputs.size());
+ assertEquals(potatoes(), items(inputs.get(0).getAsJsonArray()));
+ assertEquals("poweradvantage:starch", recipe.getAsJsonObject("result").get("item").getAsString());
+ assertEquals(1, recipe.getAsJsonObject("result").get("count").getAsInt());
+ }
+
+ @Test public void existingCrushingKeepsBothPotatoesAndItsOneToOneRatio() throws Exception {
+ JsonObject recipe = read("crushing_potato_starch");
+ assertEquals("basemetals:crushing", recipe.get("type").getAsString());
+ assertEquals(potatoes(), items(recipe.getAsJsonArray("ingredient")));
+ assertEquals("poweradvantage:starch", recipe.getAsJsonObject("result").get("item").getAsString());
+ assertEquals(1, recipe.getAsJsonObject("result").get("count").getAsInt());
+ }
+
+ private static JsonObject read(String name) throws Exception {
+ return new JsonParser().parse(new String(Files.readAllBytes(Paths.get(
+ "src/main/resources/data/poweradvantage/recipes/" + name + ".json")), StandardCharsets.UTF_8)).getAsJsonObject();
+ }
+ private static Set potatoes() {
+ return new HashSet<>(java.util.Arrays.asList("minecraft:potato", "minecraft:poisonous_potato"));
+ }
+ private static Set items(JsonArray values) {
+ Set items = new HashSet<>();
+ values.forEach(value -> items.add(value.getAsJsonObject().get("item").getAsString()));
+ return items;
+ }
+}
diff --git a/src/test/resources/orespawn-legacy/poweradvantage.json b/src/test/resources/orespawn-legacy/poweradvantage.json
new file mode 100644
index 00000000..861b8d11
--- /dev/null
+++ b/src/test/resources/orespawn-legacy/poweradvantage.json
@@ -0,0 +1,18 @@
+{
+ "dimensions": [
+ {
+ "dimension": 0,
+ "ores": [
+ {
+ "blockID": "poweradvantage:crude_oil",
+ "size": 64,
+ "variation": 50,
+ "frequency": 0.25,
+ "minHeight": 16,
+ "maxHeight": 64,
+ "biomes": []
+ }
+ ]
+ }
+ ]
+}
diff --git a/test-rig/README.md b/test-rig/README.md
new file mode 100644
index 00000000..0ea64d1e
--- /dev/null
+++ b/test-rig/README.md
@@ -0,0 +1,161 @@
+# Advantage Works 1.13.2 Test Rig
+
+> **Commissioning notice:** This harness builds disposable works. It is not a gameplay mod, registers no content, and must never be shipped inside a PowerAdvantage jar.
+
+The rig turns the 20 cards from the Advantage Works 1.10 handbook plus a public-API fixture into one walkable superflat server world. Power Works lies west of the central concourse, Steam Works north, Electric Works east, and the drill, lift, turbine, pumping, musket, and turret proving grounds south. See the [1.13 migration guide](../docs/1.13/MIGRATION.md) for this port's qualification status.
+
+Use the [current qualification and remaining-tests checklist](../docs/1.13/CURRENT-QUALIFICATION.md)
+to distinguish structural bay checks from measured machine operation and manual
+acceptance. Building every bay does not prove every behaviour on its card.
+
+The [connected legacy migration runner](../docs/1.13/CONNECTED-MIGRATION.md)
+builds and runs real assemblies under old jars, then resumes copied equipment
+without replacing machines or refilling supplies. Its intermediate 1.12 failure
+is retained separately from successful 1.13 operation.
+
+For non-interactive recipe checks, see [Recipe Discovery](../docs/1.13/RECIPE-DISCOVERY.md) and [Manufacturing Progression](../docs/1.13/MANUFACTURING-PROGRESSION.md). The progression runner starts from raw resources, records every item/recipe path, runs live processing witnesses and checks all modes, resource reloads and full restarts. It uses a separate marked disposable world, not the walkable viewer save.
+
+## Safety interlocks
+
+The `/advworks` command refuses to build or clear unless both conditions hold:
+
+- the level name begins with `AdvantageWorks`;
+- the server working directory contains `advantage-works.allow`.
+
+Each station has declared bounds. Reset clears blocks only inside those bounds and removes only entities tagged by that station. Generated saves, staged jars, logs, reports, Forge server files, caches, and local settings are ignored by Git.
+
+## Operator controls
+
+Every bay has clickable start, stop, reset, and check signs plus a wool status lamp. Console operators can use:
+
+```text
+/advworks build [all|power|steam|electric]
+/advworks reset
+/advworks start
+/advworks stop
+/advworks check
+/advworks status [station]
+/advworks checkpoint
+/advworks sample-worldgen
+```
+
+Before a player uses the clickable controls, grant that tester operator permission with `op ` at the server console. The harness intentionally keeps `/advworks` at permission level 2 as a second interlock beyond the marked disposable world.
+
+The definition at `src/main/resources/assets/advantageworkstest/works/works-1.13.2.json` owns station origins, bounds, placements, setup/start/stop actions, assertions, required mods, and manual checks. Station IDs are the handbook card IDs `W-01` through `E-08`, plus `P-API-01` for the stable fluid-network API.
+
+## Result meanings
+
+| Result | Meaning |
+| --- | --- |
+| `PASS` | Every automated assertion passed and no manual observation remains. |
+| `FAIL` | An ordinary assertion failed. Automated runners stop with failure. |
+| `EXPECTED_FAIL` | A declared handbook bug reproduced; the assertion names its bug ID. |
+| `UNEXPECTED_PASS` | A declared bug no longer reproduced and needs review before expectations change. |
+| `SKIP` | A required mod or integration is absent. |
+| `MANUAL_REQUIRED` | Structural assertions passed, but gameplay observations remain on the station card. |
+
+Animation and GUI appearance are never sufficient proof. Use checkpoints and reconcile bounded items, fluids, and energy.
+
+The focused [oil-transfer protocol](../docs/1.13/OIL-TRANSFER.md) adds four isolated remote lanes: crude/refined oil through a drain and descending/horizontal pipes into each container type. It uses real bucket placement, exact fluid identity/volume assertions, two complete process restarts, and a second fill without replacing machinery. This closes a gap in `W-01`/`W-02`, whose drain assertions alone did not establish bounded oil conservation.
+
+## Build and development run
+
+Run Gradle with Java 17. The harness and all Minecraft code compile to Java 8 bytecode.
+
+```text
+gradlew.bat -p test-rig clean test assemble
+gradlew.bat -p test-rig verifyWorksDev
+gradlew.bat -p test-rig runWorksDev
+```
+
+On Linux or macOS use `./gradlew` with the same arguments. `verifyWorksDev` builds all stations, runs each bounded start/stop sequence, saves, stops the server process, starts it again, checkpoints the retained state, and reruns the structural assertions. `runWorksDev` leaves the disposable server open at `localhost:25565` for a human inspector.
+
+Long `worksRunSeconds` holds do not replenish finite inputs and may mostly observe
+idle equipment. For thirty minutes of measured repeat batches, use the separate
+[endurance runner](migration/README.md#repeated-load-endurance) and read its
+[qualification limits](../docs/1.13/ENDURANCE.md).
+For genuine chunk unload/reload and moving-drill tests, use the separate
+[chunk and drill protocol](../docs/1.13/CHUNK-AND-DRILLS.md). Its results distinguish
+inventory and network recovery from the separate movement-cost, rear-output
+and unfinished-progress acceptance checks, with before/after repair evidence.
+The [extended lifecycle protocol](../docs/1.13/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md)
+adds separate supply/consumer unloading, reload orders, repeated moves, vertical
+tracks, junctions, blocked chests, low energy and redstone controls.
+The ordinary lifecycle runner gives `W-03` at least sixty seconds to finish its
+2,000 mB oil batch; its exact circuit-wide yield assertion cannot finish in the
+five-second quick-check default used for other stations.
+
+Limit an automated run to selected lifecycle bays, or turn one into a 30-minute endurance run, without changing its profile:
+
+```text
+gradlew.bat -p test-rig verifyWorksDev -PworksStations=W-04
+gradlew.bat -p test-rig verifyWorksPackagedCurrent -PworksStations=W-04,S-02 -PworksRunSeconds=1800
+gradlew.bat -p test-rig verifyWorksPackagedCurrent -PworksStations=E-01 -PworksServerPort=25566
+```
+
+Every machinery profile still builds and checks the complete works; the selection controls which stations are started, stopped, and checkpointed around the full process restart. Normal-world generation profiles deliberately skip station construction. The configured run duration is also used as a post-restart settle interval before assertions, allowing an eight-tick distribution cycle to complete. Use `worksServerPort` when another local server occupies the profile's default port. The effective station list, duration, and port are recorded in `run-manifest.json`.
+
+The development profile uses current PowerAdvantage source plus the sibling SteamAdvantage and ElectricAdvantage deobf jars. The normal development runtime also includes the pinned Base Metals and OreSpawn deobf jars required by the 1.13 data-driven crushing and provider APIs. Use the packaged decoupled profile to prove behavior without Base Metals.
+
+## Run matrix
+
+The separate [original-release/chained migration matrix](../docs/1.13/MIGRATION-MATRIX.md)
+freezes source-version machines, compares their serialized state through conversion,
+and checks a full server restart. Its [runner instructions](migration/README.md)
+describe the source fixtures, safety interlocks, and pinned input profiles.
+
+| Profile/task | Purpose |
+| --- | --- |
+| `verifyWorksDev` | Current Power source and sibling deobf extensions with pinned Base Metals and OreSpawn 4 |
+| `verifyWorksPackagedCurrent` | Reobfuscated Advantage jars with BaseMetals `3.0.1.113021` and pinned OreSpawn `4.1.0.113021` |
+| `verifyWorksPackagedDecoupled` | Reobfuscated Advantage jars with OreSpawn 4 but without BaseMetals/Mineralogy |
+| `verifyWorksPackagedMineralogy` | Reobfuscated Advantage and Mineralogy jars; exercises both crude oils through distillation, transfer, persistence, and boilers |
+| `verifyWorksPackagedOptionalPower` | Preserved RF- and Tech Reborn-labelled converter identities through Forge Energy |
+| `verifyWorksTechProgression` | Registration/build audit under `TECH_PROGRESSION` |
+| `verifyWorksApocalyptic` | Registration/build audit under `APOCALYPTIC` |
+| `verifyWorksWorldgenAdvantage` | Normal world: Electric fallback sulfur/lithium, Power desert oil, user-disable persistence |
+| `verifyWorksWorldgenMineralogy` | Normal world: Mineralogy sulfur/ocean oil, Electric lithium fallback, user-enable persistence |
+| `verifyWorksWorldgenLegacyImport` | Normal world: OS1 files migrate once, remain authoritative, and do not duplicate modern rules |
+
+Profiles pin filenames, mod IDs, versions, and SHA-256 values. Most workspace-built harness jars are identified and version-checked without a source-dependent hash; `packaged-current` also pins the harness used for the endurance qualification. Refresh that hash deliberately after rebuilding changed harness sources. The packaged runner downloads only the pinned official Forge installer, verifies it, installs a fresh server into the disposable runtime, and launches Minecraft with Java 8.
+
+Set these only for profiles that need them:
+
+```text
+ADVANTAGE_WORKS_CURRENT_MODS=
+ADVANTAGE_WORKS_JAVA8_HOME=
+ADVANTAGE_WORKS_GRADLE_JAVA_HOME=
+```
+
+## Station register
+
+| District | Cards |
+| --- | --- |
+| Power Works | `W-01` fluid persistence; `W-02` drain/discharge reach; `W-03` distillation; `W-04` conveyors and all filters |
+| Power API Works | `P-API-01` public fluid offer, transfer limit, full consumer, invalid target, and input immutability |
+| Steam Works | `S-01` boilers; `S-02` topology; `S-03` storage/scarcity; `S-04` furnace/crusher/still |
+| South proving grounds | `S-05` drill; `S-06` pump; `S-07` elevator; `S-08` musket; `E-01` generators/turbine; `E-06` drill; `E-07` light/turret |
+| Electric Works | `E-02` batteries/distribution; `E-03` processors; `E-04` fluids; `E-05` growth; `E-08` isolated assembler diagnostic |
+
+Ore generation is intentionally excluded from the superflat works. The three `verifyWorksWorldgen*` tasks create separately named disposable normal worlds, locate nearby desert and ocean biome samples, generate bounded chunk squares, count the target blocks, check oil biome placement, and repeat the same scan after a full process restart.
+
+## OreSpawn profile checks
+
+The normal-world profiles inspect `/serverconfig/orespawn-worldgen.json` after first shutdown and after restart. They verify default ownership, edit a persisted `enabled` flag to simulate a user choice, and require OreSpawn to preserve that choice. The legacy profile stages the retained OS1 Power/Electric files under `config/orespawn/`, requires their migration report and provider files, and rejects simultaneous modern duplicate IDs.
+
+`worldgen-mineralogy` covers Mineralogy-over-Electric sulfur ownership and both oil biomes. BaseMinerals does not yet have a 1.13 provider release, so its ownership combinations remain statically covered by ElectricAdvantage's provider tests and are an explicit pending runtime matrix item.
+
+The sampler records block counts, sampled biome centers, any deposit-producing chunks outside their declared biome selector, and informational per-block spill at biome boundaries. OreSpawn selects deposits by chunk-centre biome. Existing chunks are sampled again after restart; disabled rules affect only terrain generated later and therefore must not erase or alter the recorded blocks. Solid ore counts must remain exact. The legacy profile excludes only `poweradvantage:crude_oil` from exact count comparison because OS1 represented it as an exposed ore vein and the fluid can continue flowing between observations; its migrated rule, enabled state, and positive presence are still asserted on both runs.
+
+## Confirmed-bug regression witnesses
+
+| Station | Automated witness | Manual remainder |
+| --- | --- | --- |
+| `S-01` | Seeds both `Tempoerature` and `Temperature`, saves canonically, and requires retained heat after a full process restart (`SA-110-002`). | Compare the geothermal GUI gauge with the saved value. |
+| `S-04` | An isolated unpowered crusher with one cobblestone must report comparator level `1` before and after restart (`SA-110-003`). | Check intermediate and full stack levels when commissioning redstone automation. |
+| `S-06` | An isolated pump must retain exactly 1,000 mB of water after restart (`SA-110-001`). | Confirm the pump GUI agrees with the server NBT. |
+| `E-01` | Requires an active hydroelectric generator and charge in its directly connected battery after restart (`EA-110-003`). | Open the hydroelectric GUI and confirm a normal-width active bar. |
+
+## Release isolation
+
+`verifyHarnessIsolation` opens the PowerAdvantage release jar and rejects any `zone/moddev/mc/advantageworks` class or `assets/advantageworkstest` resource. The normal PowerAdvantage build never includes the nested `test-rig` source set.
diff --git a/test-rig/build.gradle b/test-rig/build.gradle
new file mode 100644
index 00000000..a83dd969
--- /dev/null
+++ b/test-rig/build.gradle
@@ -0,0 +1,179 @@
+import org.apache.tools.ant.filters.FixCrLfFilter
+
+plugins {
+ id 'java'
+ id 'net.minecraftforge.renamer' version '1.1.5'
+ id 'net.minecraftforge.gradle' version '7.0.34'
+}
+
+group = 'zone.moddev.mc.advantageworks'
+version = project.harness_version
+base.archivesName = 'AdvantageWorksTestHarness'
+
+java {
+ toolchain.languageVersion = JavaLanguageVersion.of(8)
+}
+
+tasks.withType(JavaCompile).configureEach {
+ javaCompiler = javaToolchains.compilerFor {
+ languageVersion = JavaLanguageVersion.of(8)
+ }
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ options.encoding = 'UTF-8'
+}
+
+tasks.withType(AbstractArchiveTask).configureEach {
+ preserveFileTimestamps = false
+ reproducibleFileOrder = true
+}
+
+sourceSets {
+ main.java.srcDir 'shared-probe'
+ runner {
+ java.srcDir 'src/runner/java'
+ resources.srcDir 'src/runner/resources'
+ }
+}
+
+minecraft {
+ mappings channel: project.mapping_channel, version: project.mapping_version
+}
+
+repositories {
+ minecraft.mavenizer(it)
+ maven fg.forgeMaven
+ maven fg.minecraftLibsMaven
+ mavenCentral()
+ maven {
+ name = 'MinecraftLibraries'
+ url = 'https://libraries.minecraft.net/'
+ }
+}
+
+dependencies {
+ implementation minecraft.dependency(
+ "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}")
+ compileOnly 'org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209'
+ runnerImplementation 'com.google.code.gson:gson:2.8.0'
+ testImplementation 'junit:junit:4.13.2'
+ compileOnly files(project.findProperty('powerAdvantageDeobfJar')
+ ?: '../build/libs-dev/PowerAdvantage-2.4.4.113021-deobf.jar')
+}
+
+tasks.named('processResources', ProcessResources) {
+ filteringCharset = 'UTF-8'
+ inputs.property('version', project.version)
+ inputs.property('mcversion', project.minecraft_version)
+ filesMatching('META-INF/mods.toml') {
+ expand version: project.version
+ }
+ filesMatching(['**/*.json', '**/*.info', '**/*.properties', '**/*.md']) {
+ filter(FixCrLfFilter,
+ eol: FixCrLfFilter.CrLf.newInstance('lf'),
+ eof: FixCrLfFilter.AddAsisRemove.newInstance('asis'))
+ }
+}
+
+tasks.named('jar', Jar) {
+ archiveClassifier = 'deobf'
+ destinationDirectory = layout.buildDirectory.dir('libs-dev')
+ manifest {
+ attributes([
+ 'Implementation-Title': base.archivesName.get(),
+ 'Implementation-Version': project.version,
+ 'Built-On': "${project.minecraft_version}-${project.forge_version}"
+ ])
+ }
+}
+
+def releaseJar = renamer.classes(tasks.named('jar', Jar)) {
+ map.from minecraft.dependency.toSrgFile
+ archiveClassifier = null
+}
+
+tasks.register('runnerJar', Jar) {
+ dependsOn tasks.named('runnerClasses')
+ archiveClassifier = 'runner'
+ from sourceSets.runner.output
+ manifest.attributes('Main-Class': 'zone.moddev.mc.advantageworks.runner.WorksRunner')
+}
+
+tasks.named('assemble') {
+ dependsOn releaseJar
+ dependsOn tasks.named('runnerJar')
+}
+
+def registerWorksRunner = { String taskName, String profileName, boolean automated, String taskDescription ->
+ tasks.register(taskName, JavaExec) {
+ group = 'verification'
+ description = taskDescription
+ dependsOn tasks.named('jar')
+ dependsOn releaseJar
+ dependsOn tasks.named('runnerClasses')
+ classpath = sourceSets.runner.runtimeClasspath
+ mainClass = 'zone.moddev.mc.advantageworks.runner.WorksRunner'
+ if (!automated) standardInput = System.in
+ args '--profile', file("profiles/${profileName}.json").absolutePath
+ if (automated) args '--automated'
+ if (project.hasProperty('worksStations')) {
+ args '--stations', project.property('worksStations').toString()
+ }
+ if (project.hasProperty('worksRunSeconds')) {
+ args '--run-seconds', project.property('worksRunSeconds').toString()
+ }
+ if (project.hasProperty('worksServerPort')) {
+ args '--server-port', project.property('worksServerPort').toString()
+ }
+ }
+}
+
+registerWorksRunner('runWorksDev', 'dev-core', false,
+ 'Builds and launches a disposable interactive 1.13.2 Advantage Works development server.')
+registerWorksRunner('verifyWorksDev', 'dev-core', true,
+ 'Generates, checks, restarts, and rechecks the disposable development works.')
+registerWorksRunner('verifyWorksTechProgression', 'dev-core-tech-progression', true,
+ 'Checks registration and construction with TECH_PROGRESSION recipes.')
+registerWorksRunner('verifyWorksApocalyptic', 'dev-core-apocalyptic', true,
+ 'Checks registration and construction with APOCALYPTIC recipes.')
+registerWorksRunner('verifyWorksPackagedCurrent', 'packaged-current', true,
+ 'Runs packaged Advantage mods with current BaseMetals and pinned OreSpawn 4.')
+registerWorksRunner('verifyWorksPackagedDecoupled', 'packaged-decoupled', true,
+ 'Runs packaged Advantage mods without BaseMetals or Mineralogy, retaining required OreSpawn 4.')
+registerWorksRunner('verifyWorksPackagedMineralogy', 'packaged-mineralogy', true,
+ 'Runs Mineralogy crude oil through packaged Power distillation and Steam boilers.')
+registerWorksRunner('verifyWorksPackagedOptionalPower', 'packaged-optional-power', true,
+ 'Exercises the preserved RF- and Tech Reborn-labelled converter IDs through Forge Energy.')
+registerWorksRunner('verifyWorksWorldgenAdvantage', 'worldgen-advantage', true,
+ 'Samples Advantage fallback ores and desert oil, then verifies profile overrides through restart.')
+registerWorksRunner('verifyWorksWorldgenMineralogy', 'worldgen-mineralogy', true,
+ 'Samples Mineralogy-owned sulfur/ocean oil and Advantage fallbacks through restart.')
+registerWorksRunner('verifyWorksWorldgenLegacyImport', 'worldgen-legacy-import', true,
+ 'Imports legacy Power/Electric OreSpawn files and rejects duplicate active modern rules.')
+registerWorksRunner('runWorksPackagedCurrent', 'packaged-current', false,
+ 'Launches the complete packaged works on localhost for manual inspection.')
+
+tasks.register('verifyHarnessIsolation') {
+ group = 'verification'
+ description = 'Confirms the PowerAdvantage release contains no test-harness content.'
+ doLast {
+ File powerJar = file('../build/libs/PowerAdvantage-2.4.4.113021.jar')
+ if (!powerJar.isFile()) {
+ throw new GradleException("Build PowerAdvantage first: ${powerJar}")
+ }
+ def forbidden = ['zone/moddev/mc/advantageworks/', 'assets/advantageworkstest/']
+ def zip = new java.util.zip.ZipFile(powerJar)
+ try {
+ def leaked = zip.entries().findAll { entry -> forbidden.any { entry.name.startsWith(it) } }
+ if (leaked) {
+ throw new GradleException("Harness content leaked into PowerAdvantage: ${leaked*.name}")
+ }
+ } finally {
+ zip.close()
+ }
+ }
+}
+
+tasks.named('check') {
+ dependsOn tasks.named('verifyHarnessIsolation')
+}
diff --git a/test-rig/client-resources/README.md b/test-rig/client-resources/README.md
new file mode 100644
index 00000000..b76729b7
--- /dev/null
+++ b/test-rig/client-resources/README.md
@@ -0,0 +1,44 @@
+# Native Client Resource Check
+
+This build-only Forge 1.13.2 probe checks PowerAdvantage, or all three Advantage mods in the combined profile, after Minecraft bakes its models and stitches its textures. It registers no gameplay content, opens no world, and shuts down its own disposable client when finished. It is not included in release jars or ordinary Eclipse launches.
+
+For native recipe-book grouping, first complete the packaged `recipe_unlocks.py`
+run, then pass its output directory:
+
+```powershell
+./test-rig/client-resources/run.ps1 -JavaHome '' -LaunchProfile runClientAllAdvantage.launch -RequireExtensions -RecipeBookFixtures 'run-recipe-unlocks/new-current'
+```
+
+The server exports its actual recipe packets and matching metadata. The client
+imports the generated world's Forge registry snapshot before decoding those
+packets, reproducing the item-ID synchronization normally supplied by Forge's
+connection handshake. It then rebuilds native `RecipeBookClient`/`RecipeList`
+cells for Normal, Tech and Apocalyptic modes: each cell must contain exactly one
+output identity, all loaded variants must remain accessible, and same-output
+alternatives must not split into extra cells. A framebuffer image,
+`recipe-book-cells.png`, renders the actual Minecraft recipe buttons for the
+four electronics and nine conveyor/filter outputs reported hidden by users.
+The probe reads only the disposable fixture's saved registry data; it never
+opens that world or a user's world in Minecraft. This is a native cell-model and
+button-rendering check, not manual navigation or notification acceptance.
+
+Run Gradle with Java 17, then supply a Java 8 runtime to the launcher:
+
+```powershell
+./gradlew.bat check clientResourceProbeJar verifyEclipseProductionClasspath
+./test-rig/client-resources/run.ps1 -JavaHome ''
+```
+
+Each run gets a new directory under `build/client-resource-runs/`. The runner snapshots production classes/resources, uses the audited launch dependencies, and loads only the probe as an additional mod. It retains logs and `client-resources.json`; missing reports, failed assertions, and model/texture errors fail the run. Existing saves, configuration, and prior reports are untouched.
+
+The default `-OutputSource Gradle` merges Gradle's classes and processed resources. To test the actual Eclipse output instead, pass `-OutputSource Eclipse`; this copies `bin/main` verbatim and does not overlay Gradle resources. `client-input.json` records the selected output, launch and metadata hash. Missing/unexpanded metadata fails the preflight before Minecraft opens, and the native result records the versions Forge actually discovers. The [Eclipse metadata regression](../../docs/1.13/ECLIPSE-METADATA.md) explains why this is a separate acceptance gate.
+
+For coordinated startup, first run `gradlew.bat prepareCombinedEclipseLaunch`, then add `-LaunchProfile runClientAllAdvantage.launch -RequireExtensions` to the probe command. The report's `advantage_mods` must list all three mods. With this flag, resource and item-name assertions cover Power, Steam and Electric, and model/blockstate errors from any of their namespaces fail the run. Without it, the original Power-only scope is unchanged. Pin every rebuilt expansion jar in the development and packaged rig profiles before regenerating the combined launch.
+
+Assertions cover every registered item and valid block state in the selected Advantage namespaces, nonempty item/machine geometry, missing-model and missing-texture sprites, English item names, the shared creative tab and its icon, and agreement between Power's modern block and legacy GUI translation keys. Native fluid block models are intentionally particle-only. This test does not certify machine operation, animated entity rendering, or third-party resource packs.
+
+The six `ClientResourceContractTest` checks first failed against the pre-repair resources. They cover literal model paths, removed legacy blockstate selectors, model parents/textures (including vanilla assets), oil item models, locale aliases, and English block/bucket coverage.
+
+Tank visual regressions also use the native client. They test Minecraft's actual neighbour-face culling beneath both tanks, synchronize a full 10,000 mB metal tank and then an empty tank, and render the filtered tank's gauge and frame into an OpenGL framebuffer. The water pixel must be blue; `tank-water-gauge.png` retains the rendered indicator. These checks reproduced hidden floor faces and an untinted `ADADAD` water pixel before the rendering repairs. No test-world loading or fluid-network changes are required for this focused check.
+
+Oil regressions verify both native buckets' fluid identities, then run Minecraft's actual `BlockFluidRenderer` against a read-only synthetic neighbourhood. Still and flowing crude/refined oil must emit vertices using their own atlas sprites, without a biome water tint. Vanilla water and lava are control cases. The probe installs vanilla water/lava tag memberships explicitly because no server has synchronized tags at the main menu. It changes no production tags or saves. Before the repair all four oil cases used water sprites and the blue `3F76E4` tint; `fluid_rendering` in the JSON report retains the results. Separate Java 8 transformer tests cover mapped/SRG-style method names and preservation of sibling hook chains in either order; these structural checks do not certify every third-party renderer or resource pack.
diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java
new file mode 100644
index 00000000..09195a85
--- /dev/null
+++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/ClientResourceProbe.java
@@ -0,0 +1,129 @@
+package zone.moddev.mc.advantageworks.client;
+
+import com.google.gson.JsonObject;
+import net.minecraft.block.Block;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.model.BakedQuad;
+import net.minecraft.client.renderer.model.IBakedModel;
+import net.minecraft.client.resources.I18n;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.NonNullList;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.registries.ForgeRegistries;
+import org.apache.logging.log4j.LogManager;
+import zone.moddev.mc.poweradvantage.init.ItemGroups;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+/** Runs after native resource baking, without opening or generating a world. */
+@Mod(ClientResourceProbe.MODID)
+@Mod.EventBusSubscriber(modid = ClientResourceProbe.MODID, value = Dist.CLIENT)
+public final class ClientResourceProbe {
+ static final String MODID = "advantageclientresources";
+ private static boolean finished;
+ private static int ticks;
+
+ @SubscribeEvent public static void tick(TickEvent.ClientTickEvent event) {
+ if (!Boolean.getBoolean("advantageworks.clientResources") || finished || event.phase != TickEvent.Phase.END) return;
+ Minecraft minecraft = Minecraft.getInstance();
+ if (++ticks < 5 || (minecraft.getModelManager().getMissingModel() == null && ticks < 1200)) return;
+ finished = true;
+ JsonObject result = new JsonObject();
+ try {
+ com.google.gson.JsonArray mods = new com.google.gson.JsonArray();
+ JsonObject versions = new JsonObject();
+ for (String id : new String[]{"poweradvantage", "steamadvantage", "electricadvantage"}) {
+ boolean loaded = net.minecraftforge.fml.ModList.get().isLoaded(id);
+ if (loaded) {
+ mods.add(id);
+ versions.addProperty(id, net.minecraftforge.fml.ModList.get().getModContainerById(id).get()
+ .getModInfo().getVersion().toString());
+ }
+ if (Boolean.getBoolean("advantageworks.requireExtensions")) require(loaded, "Combined launch omitted " + id);
+ }
+ result.add("advantage_mods", mods);
+ result.add("mod_versions", versions);
+ require(versions.has("poweradvantage") && zone.moddev.mc.poweradvantage.PowerAdvantage.VERSION
+ .equals(versions.get("poweradvantage").getAsString()), "Power metadata does not match the compiled version");
+ require(minecraft.getModelManager().getMissingModel() != null, "Timed out before model baking");
+ int items = 0;
+ int states = 0;
+ IBakedModel missing = minecraft.getModelManager().getMissingModel();
+ for (Item item : ForgeRegistries.ITEMS) {
+ ResourceLocation id = item.getRegistryName();
+ if (id == null || !included(id.getNamespace())) continue;
+ ItemStack stack = new ItemStack(item);
+ String key = item.getTranslationKey(stack);
+ require(!I18n.format(key).equals(key), "Untranslated item: " + id + " -> " + key);
+ IBakedModel model = minecraft.getItemRenderer().getItemModelWithOverrides(stack, null, null);
+ verifyModel(model, missing, null, id.toString(), true);
+ items++;
+ }
+ for (Block block : ForgeRegistries.BLOCKS) {
+ ResourceLocation id = block.getRegistryName();
+ if (id == null || !included(id.getNamespace())) continue;
+ for (IBlockState state : block.getStateContainer().getValidStates()) {
+ IBakedModel model = minecraft.getModelManager().getBlockModelShapes().getModel(state);
+ verifyModel(model, missing, state, state.toString(), !(block instanceof net.minecraft.block.BlockFlowingFluid));
+ states++;
+ }
+ }
+ NonNullList contents = NonNullList.create();
+ ItemGroups.tab_powerAdvantage.fill(contents);
+ require(items > 0 && states > 0, "Power Advantage content is not registered");
+ require(!contents.isEmpty(), "Power Advantage creative tab is empty");
+ require(!ItemGroups.tab_powerAdvantage.getIcon().isEmpty(), "Power Advantage creative tab has no icon");
+ require(I18n.format("block.poweradvantage.still").equals(I18n.format("tile.poweradvantage.still.name")),
+ "Legacy GUI translation differs from modern block name");
+ TankVisualProbe.verify(minecraft, result);
+ OilRenderProbe.verify(minecraft, result);
+ RecipeBookGroupingProbe.verify(minecraft, result);
+ result.addProperty("status", "PASS");
+ result.addProperty("items", items);
+ result.addProperty("block_states", states);
+ result.addProperty("creative_entries", contents.size());
+ result.addProperty("screen", minecraft.currentScreen == null ? "none" : minecraft.currentScreen.getClass().getName());
+ LogManager.getLogger(MODID).info("ADVANTAGE_CLIENT_RESOURCES PASS items={} block_states={} creative_entries={}", items, states, contents.size());
+ } catch (RuntimeException | AssertionError failure) {
+ result.addProperty("status", "FAIL");
+ result.addProperty("error", failure.toString());
+ LogManager.getLogger(MODID).error("ADVANTAGE_CLIENT_RESOURCES FAIL", failure);
+ } finally {
+ try { Files.write(Paths.get("client-resources.json"), result.toString().getBytes(StandardCharsets.UTF_8)); }
+ catch (IOException failure) { throw new IllegalStateException("Could not write client resource result", failure); }
+ finally { minecraft.shutdown(); }
+ }
+ }
+
+ private static boolean included(String namespace) {
+ return "poweradvantage".equals(namespace) || (Boolean.getBoolean("advantageworks.requireExtensions")
+ && ("steamadvantage".equals(namespace) || "electricadvantage".equals(namespace)));
+ }
+
+ private static void verifyModel(IBakedModel model, IBakedModel missing, IBlockState state, String id, boolean requireQuads) {
+ require(model != missing, "Missing baked model: " + id);
+ require(!"missingno".equals(model.getParticleTexture().getName().getPath()), "Missing particle texture: " + id);
+ Random random = new Random(42L);
+ List quads = new ArrayList<>(model.getQuads(state, null, random));
+ for (EnumFacing side : EnumFacing.values()) quads.addAll(model.getQuads(state, side, random));
+ require(!requireQuads || !quads.isEmpty(), "Empty baked model: " + id);
+ for (BakedQuad quad : quads) require(!"missingno".equals(quad.getSprite().getName().getPath()), "Missing face texture: " + id);
+ }
+
+ private static void require(boolean condition, String message) {
+ if (!condition) throw new IllegalStateException(message);
+ }
+}
diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java
new file mode 100644
index 00000000..b980ba59
--- /dev/null
+++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/OilRenderProbe.java
@@ -0,0 +1,109 @@
+package zone.moddev.mc.advantageworks.client;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BlockFluidRenderer;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.fluid.FlowingFluid;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.item.ItemBucket;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IWorldReader;
+import zone.moddev.mc.poweradvantage.init.Fluids;
+import zone.moddev.mc.poweradvantage.init.Items;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Proxy;
+import java.nio.ByteBuffer;
+
+/** Exercises the native fluid renderer, not just oil's particle-only block model. */
+final class OilRenderProbe {
+ private OilRenderProbe() {}
+
+ static void verify(Minecraft minecraft, JsonObject result) {
+ // Before a world/server is opened, vanilla's fluid tags have not been synchronized.
+ net.minecraft.tags.TagCollection tags = new net.minecraft.tags.TagCollection<>(id -> true,
+ net.minecraft.util.registry.IRegistry.field_212619_h::func_212608_b, "tags/fluids", false, "fluid");
+ tags.register(net.minecraft.tags.Tag.Builder.create().add(net.minecraft.init.Fluids.WATER,
+ net.minecraft.init.Fluids.FLOWING_WATER).build(new ResourceLocation("minecraft", "water")));
+ tags.register(net.minecraft.tags.Tag.Builder.create().add(net.minecraft.init.Fluids.LAVA,
+ net.minecraft.init.Fluids.FLOWING_LAVA).build(new ResourceLocation("minecraft", "lava")));
+ net.minecraft.tags.FluidTags.setCollection(tags);
+ requireBucket(Items.crude_oil_bucket, Fluids.crude_oil);
+ requireBucket(Items.refined_oil_bucket, Fluids.refined_oil);
+ result.addProperty("oil_bucket_identities", true);
+
+ JsonArray cases = new JsonArray();
+ result.add("fluid_rendering", cases);
+ boolean valid = true;
+ valid &= check(minecraft, Fluids.crude_oil.getDefaultState(), "poweradvantage:blocks/crude_oil_still", false, cases);
+ valid &= check(minecraft, Fluids.flowing_crude_oil.getFlowingFluidState(5, false), "poweradvantage:blocks/crude_oil_flow", true, cases);
+ valid &= check(minecraft, Fluids.refined_oil.getDefaultState(), "poweradvantage:blocks/refined_oil_still", false, cases);
+ valid &= check(minecraft, Fluids.flowing_refined_oil.getFlowingFluidState(5, false), "poweradvantage:blocks/refined_oil_flow", true, cases);
+ valid &= check(minecraft, net.minecraft.init.Fluids.WATER.getDefaultState(), "minecraft:block/water_still", false, cases);
+ valid &= check(minecraft, net.minecraft.init.Fluids.LAVA.getDefaultState(), "minecraft:block/lava_still", false, cases);
+ if (!valid) throw new IllegalStateException("Native fluid renderer used the wrong sprite or tint: " + cases);
+ }
+
+ private static void requireBucket(net.minecraft.item.Item bucket, Fluid expected) {
+ try {
+ for (Field field : ItemBucket.class.getDeclaredFields()) {
+ if (field.getType() != Fluid.class) continue;
+ field.setAccessible(true);
+ if (field.get(bucket) == expected && expected.getFilledBucket() == bucket) return;
+ }
+ } catch (ReflectiveOperationException failure) {
+ throw new IllegalStateException("Cannot inspect native bucket fluid", failure);
+ }
+ throw new IllegalStateException("Wrong bucket fluid: " + bucket.getRegistryName());
+ }
+
+ private static boolean check(Minecraft minecraft, IFluidState state, String texture, boolean flowing, JsonArray cases) {
+ // A tiny read-only neighbourhood supplies a slope for flowing sprites; no save is opened.
+ IWorldReader world = (IWorldReader) Proxy.newProxyInstance(OilRenderProbe.class.getClassLoader(),
+ new Class>[]{IWorldReader.class}, (proxy, method, args) -> {
+ BlockPos pos = args != null && args.length > 0 && args[0] instanceof BlockPos ? (BlockPos) args[0] : null;
+ if (method.getName().equals("getBlockState")) return blockAt(state, flowing, pos);
+ if (method.getName().equals("getFluidState")) return blockAt(state, flowing, pos).getFluidState();
+ if (method.getName().equals("getCombinedLight")) return 0x00F000F0;
+ if (method.getName().equals("getBiome")) return net.minecraft.init.Biomes.PLAINS;
+ throw new UnsupportedOperationException(method.toString());
+ });
+ BufferBuilder buffer = new BufferBuilder(4096);
+ buffer.begin(org.lwjgl.opengl.GL11.GL_QUADS, DefaultVertexFormats.BLOCK);
+ boolean rendered = new BlockFluidRenderer().render(world, BlockPos.ORIGIN, buffer, state);
+ buffer.finishDrawing();
+ TextureAtlasSprite sprite = minecraft.getTextureMap().getSprite(new ResourceLocation(texture));
+ ByteBuffer data = buffer.getByteBuffer();
+ float u = data.getFloat(DefaultVertexFormats.BLOCK.getUvOffsetById(0));
+ float v = data.getFloat(DefaultVertexFormats.BLOCK.getUvOffsetById(0) + 4);
+ int colorOffset = DefaultVertexFormats.BLOCK.getColorOffset();
+ int rgb = (data.get(colorOffset) & 255) << 16 | (data.get(colorOffset + 1) & 255) << 8 | data.get(colorOffset + 2) & 255;
+ boolean correctSprite = !"missingno".equals(sprite.getName().getPath())
+ && u >= sprite.getMinU() && u <= sprite.getMaxU() && v >= sprite.getMinV() && v <= sprite.getMaxV();
+ boolean correctTint = !(state.getFluid() instanceof Fluids.PowerFluid) || rgb == 0xFFFFFF;
+ JsonObject entry = new JsonObject();
+ entry.addProperty("fluid", net.minecraft.util.registry.IRegistry.field_212619_h.getKey(state.getFluid()).toString());
+ entry.addProperty("block", state.getBlockState().getBlock().getRegistryName().toString());
+ entry.addProperty("expected_sprite", texture);
+ entry.addProperty("correct_sprite", correctSprite);
+ entry.addProperty("vertex_rgb", String.format("%06X", rgb));
+ entry.addProperty("vertices", buffer.getVertexCount());
+ cases.add(entry);
+ return rendered && buffer.getVertexCount() > 0 && correctSprite && correctTint;
+ }
+
+ private static IBlockState blockAt(IFluidState state, boolean flowing, BlockPos pos) {
+ if (BlockPos.ORIGIN.equals(pos)) return state.getBlockState();
+ if (flowing && BlockPos.ORIGIN.east().equals(pos)) {
+ return ((FlowingFluid) state.getFluid()).getFlowingFluidState(3, false).getBlockState();
+ }
+ return (pos.getY() < 0 ? net.minecraft.init.Blocks.STONE : net.minecraft.init.Blocks.AIR).getDefaultState();
+ }
+}
diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java
new file mode 100644
index 00000000..47512b32
--- /dev/null
+++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/RecipeBookGroupingProbe.java
@@ -0,0 +1,169 @@
+package zone.moddev.mc.advantageworks.client;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import io.netty.buffer.Unpooled;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.imageio.ImageIO;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.recipebook.GuiButtonRecipe;
+import net.minecraft.client.gui.recipebook.RecipeList;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.shader.Framebuffer;
+import net.minecraft.client.util.RecipeBookClient;
+import net.minecraft.item.crafting.IRecipe;
+import net.minecraft.item.crafting.RecipeBook;
+import net.minecraft.item.crafting.RecipeItemHelper;
+import net.minecraft.item.crafting.RecipeManager;
+import net.minecraft.network.PacketBuffer;
+import net.minecraft.network.play.server.SPacketUpdateRecipes;
+import org.lwjgl.BufferUtils;
+import org.lwjgl.opengl.GL11;
+
+/** Decodes actual server recipe packets and exercises Minecraft's real cell grouping. */
+final class RecipeBookGroupingProbe {
+ static void verify(Minecraft minecraft, JsonObject report) {
+ Path root = Paths.get("recipe-book-fixtures");
+ if (!Files.isDirectory(root)) return;
+ JsonArray checks = new JsonArray();
+ try (Stream files = Files.list(root)) {
+ // A separate main-menu client has not performed Forge's registry handshake.
+ // Import only the generated server fixture's ID snapshot, never a user world.
+ Map registry =
+ net.minecraftforge.registries.RegistryManager.ACTIVE.takeSnapshot(false);
+ net.minecraft.nbt.NBTTagCompound saved;
+ try (java.io.InputStream input = Files.newInputStream(root.resolve("server-registries.dat"))) {
+ saved = net.minecraft.nbt.CompressedStreamTools.readCompressed(input).getCompound("fml").getCompound("Registries");
+ }
+ require(saved.contains("minecraft:items", 10) && saved.contains("minecraft:blocks", 10), "Missing server registry snapshot");
+ for (String key : saved.keySet()) registry.put(new net.minecraft.util.ResourceLocation(key),
+ net.minecraftforge.registries.ForgeRegistry.Snapshot.read(saved.getCompound(key)));
+ require(net.minecraftforge.registries.GameData.injectSnapshot(registry, false, false).isEmpty(),
+ "Client lacks a server registry entry");
+ List packets = files.filter(file -> file.toString().endsWith(".bin")).sorted().collect(Collectors.toList());
+ require(!packets.isEmpty(), "No recipe-book packets supplied");
+ for (Path file : packets) {
+ JsonObject manifest = new JsonParser().parse(new String(Files.readAllBytes(Paths.get(
+ file.toString().replace(".bin", ".json"))), StandardCharsets.UTF_8)).getAsJsonObject();
+ Map expected = new HashMap<>();
+ manifest.getAsJsonArray("bookRecipes").forEach(row -> expected.put(row.getAsJsonObject().get("id").getAsString(), row.getAsJsonObject()));
+ SPacketUpdateRecipes packet = new SPacketUpdateRecipes();
+ PacketBuffer buffer = new PacketBuffer(Unpooled.wrappedBuffer(Files.readAllBytes(file)));
+ try { packet.readPacketData(buffer); require(!buffer.isReadable(), "Trailing recipe packet bytes"); }
+ finally { buffer.release(); }
+ RecipeManager manager = new RecipeManager();
+ Set cellKeys = new HashSet<>();
+ for (IRecipe recipe : packet.getRecipes()) {
+ JsonObject row = expected.get(recipe.getId().toString());
+ require(row != null, "Unexpected network recipe " + recipe.getId());
+ String output = recipe.getRecipeOutput().getItem().getRegistryName().toString();
+ require(output.equals(row.get("output").getAsString()) && recipe.getRecipeOutput().getCount() == row.get("count").getAsInt(),
+ "Client/server item registry mismatch for " + recipe.getId());
+ require(recipe.getGroup().equals(row.get("group").getAsString()) && recipe.getGroup().equals(output), "Group changed in recipe packet");
+ manager.addRecipe(recipe);
+ cellKeys.add(recipe.getType().getId() + "|" + output);
+ }
+ require(manager.getRecipes().size() == expected.size(), "Missing network recipes");
+ RecipeBookClient book = new RecipeBookClient(manager);
+ manager.getRecipes().forEach(book::unlock);
+ book.rebuildTable();
+ int counted = 0;
+ for (RecipeList cell : book.getRecipes()) {
+ Set outputs = cell.getRecipes().stream().map(recipe -> recipe.getRecipeOutput().getItem().getRegistryName().toString())
+ .collect(Collectors.toSet());
+ require(outputs.size() == 1 && cell.hasSingleResultItem(), "Mixed items share a native recipe-book cell: " + outputs);
+ counted += cell.getRecipes().size();
+ cell.updateKnownRecipes(book);
+ cell.canCraft(new RecipeItemHelper(), 3, 3, book);
+ require(cell.getRecipes(false).size() == cell.getRecipes().size(), "Unlocked variants hidden in native cell");
+ }
+ require(counted == expected.size() && book.getRecipes().size() == cellKeys.size(), "Missing or split native cells");
+ JsonObject check = new JsonObject();
+ check.addProperty("packet", file.getFileName().toString());
+ check.addProperty("recipes", counted);
+ check.addProperty("cells", book.getRecipes().size());
+ check.addProperty("status", "PASS");
+ checks.add(check);
+ if (file.getFileName().toString().equals("recipes-normal-sweep.bin")) render(minecraft, book);
+ }
+ } catch (Exception failure) { throw new IllegalStateException("Native recipe-book validation failed", failure); }
+ report.add("recipe_book_grouping", checks);
+ }
+
+ private static void render(Minecraft minecraft, RecipeBookClient book) throws Exception {
+ List shown = new ArrayList<>();
+ Set selected = new HashSet<>(java.util.Arrays.asList("electricadvantage:blank_circuit_board",
+ "electricadvantage:integrated_circuit", "electricadvantage:control_circuit", "electricadvantage:psu", "poweradvantage:item_conveyor"));
+ for (RecipeList cell : book.getRecipes()) {
+ IRecipe recipe = cell.getRecipes().get(0);
+ String id = recipe.getRecipeOutput().getItem().getRegistryName().toString();
+ if (selected.contains(id) || id.startsWith("poweradvantage:item_filter_")) shown.add(cell);
+ }
+ shown.sort(java.util.Comparator.comparing(cell -> cell.getRecipes().get(0).getRecipeOutput().getItem().getRegistryName().toString()));
+ require(shown.size() == 13, "The thirteen reported circuit/conveyor cells must be separate");
+ int width = 1000, height = 180;
+ Framebuffer target = new Framebuffer(width, height, true);
+ target.bindFramebuffer(true);
+ GlStateManager.matrixMode(GL11.GL_PROJECTION);
+ GlStateManager.pushMatrix(); GlStateManager.loadIdentity();
+ GlStateManager.ortho(0, width, height, 0, -1000, 1000);
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+ GlStateManager.pushMatrix(); GlStateManager.loadIdentity();
+ GlStateManager.disableDepthTest();
+ try {
+ GL11.glClearColor(0.12F, 0.12F, 0.12F, 1F);
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
+ for (int i = 0; i < shown.size(); i++) {
+ int x = 10 + (i % 5) * 198, y = 12 + (i / 5) * 55;
+ GuiButtonRecipe button = new GuiButtonRecipe();
+ for (Field field : GuiButtonRecipe.class.getDeclaredFields()) {
+ if (field.getType() == RecipeList.class || field.getType() == RecipeBook.class) {
+ field.setAccessible(true);
+ field.set(button, field.getType() == RecipeList.class ? shown.get(i) : book);
+ }
+ }
+ button.setPosition(x, y);
+ button.render(-1, -1, 1F);
+ GlStateManager.disableLighting();
+ minecraft.fontRenderer.drawString(shown.get(i).getRecipes().get(0).getRecipeOutput().getDisplayName().getFormattedText(), x + 30, y + 8, 0xFFFFFF);
+ }
+ ByteBuffer pixels = BufferUtils.createByteBuffer(width * height * 4);
+ GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ int colored = 0;
+ for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) {
+ int index = ((height - y - 1) * width + x) * 4;
+ int rgb = (pixels.get(index) & 255) << 16 | (pixels.get(index + 1) & 255) << 8 | pixels.get(index + 2) & 255;
+ image.setRGB(x, y, rgb);
+ if ((rgb & 255) > 80 || ((rgb >>> 16) & 255) > 80) colored++;
+ }
+ require(colored > 1000, "Native recipe buttons rendered blank");
+ ImageIO.write(image, "png", new File("recipe-book-cells.png"));
+ } finally {
+ GlStateManager.popMatrix(); GlStateManager.matrixMode(GL11.GL_PROJECTION);
+ GlStateManager.popMatrix(); GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+ target.deleteFramebuffer(); minecraft.getFramebuffer().bindFramebuffer(true);
+ GlStateManager.enableDepthTest();
+ }
+ }
+
+ private static void require(boolean condition, String message) {
+ if (!condition) throw new IllegalStateException(message);
+ }
+}
diff --git a/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java
new file mode 100644
index 00000000..a01821a5
--- /dev/null
+++ b/test-rig/client-resources/java/zone/moddev/mc/advantageworks/client/TankVisualProbe.java
@@ -0,0 +1,124 @@
+package zone.moddev.mc.advantageworks.client;
+
+import com.google.gson.JsonObject;
+import cyano.poweradvantage.api.fluid.FluidStack;
+import cyano.poweradvantage.api.simple.SimpleMachineGUI;
+import net.minecraft.block.Block;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.shader.Framebuffer;
+import net.minecraft.entity.player.InventoryPlayer;
+import net.minecraft.fluid.IFluidState;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IBlockReader;
+import org.lwjgl.BufferUtils;
+import org.lwjgl.opengl.GL11;
+import zone.moddev.mc.poweradvantage.gui.FilteredFluidTankGUI;
+import zone.moddev.mc.poweradvantage.gui.FluidTankGUI;
+import zone.moddev.mc.poweradvantage.init.Blocks;
+import zone.moddev.mc.poweradvantage.machines.fluidmachines.MetalTankTileEntity;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/** Checks native neighbour culling and the actual fluid-bar drawing function. */
+final class TankVisualProbe {
+ private TankVisualProbe() {}
+
+ static void verify(Minecraft minecraft, JsonObject result) {
+ boolean metalFloor = floorVisible(Blocks.metal_storage_tank.getDefaultState());
+ boolean portableFloor = floorVisible(Blocks.storage_tank.getDefaultState());
+ result.addProperty("metal_tank_floor_visible", metalFloor);
+ result.addProperty("portable_tank_floor_visible", portableFloor);
+
+ MetalTankTileEntity serverTank = new MetalTankTileEntity();
+ serverTank.getTank().fill(new FluidStack(net.minecraft.init.Fluids.WATER, 10000), true);
+ MetalTankTileEntity clientTank = new MetalTankTileEntity();
+ clientTank.onDataPacket(null, serverTank.getUpdatePacket());
+ result.addProperty("synced_tank_amount", clientTank.getTank().getFluidAmount());
+ require(clientTank.getTank().getFluidAmount() == 10000, "Filled tank packet lost water");
+ serverTank.getTank().drain(10000, true);
+ clientTank.onDataPacket(null, serverTank.getUpdatePacket());
+ require(clientTank.getTank().getFluidAmount() == 0, "Empty tank packet retained old water");
+
+ int rgb = renderWater(minecraft, clientTank);
+ result.addProperty("water_bar_rgb", String.format("%06X", rgb));
+ int red = (rgb >>> 16) & 255;
+ int green = (rgb >>> 8) & 255;
+ int blue = rgb & 255;
+ require(metalFloor && portableFloor, "Inset tank culled the floor beneath its exposed corners");
+ require(blue > red + 20 && blue > green + 10, "Water gauge is not blue: " + String.format("%06X", rgb));
+ }
+
+ private static boolean floorVisible(final IBlockState tank) {
+ IBlockReader world = new IBlockReader() {
+ @Override public TileEntity getTileEntity(BlockPos pos) { return null; }
+ @Override public IBlockState getBlockState(BlockPos pos) {
+ return pos.equals(BlockPos.ORIGIN.up()) ? tank : net.minecraft.init.Blocks.GRASS_BLOCK.getDefaultState();
+ }
+ @Override public IFluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); }
+ };
+ return Block.shouldSideBeRendered(net.minecraft.init.Blocks.GRASS_BLOCK.getDefaultState(), world, BlockPos.ORIGIN, EnumFacing.UP);
+ }
+
+ private static int renderWater(Minecraft minecraft, MetalTankTileEntity tile) {
+ FilteredFluidTankGUI definition = new FilteredFluidTankGUI(new ResourceLocation("poweradvantage", "textures/gui/container/fluid_metal_tank_gui.png"));
+ SimpleMachineGUI.GUIContainer screen = definition.new GUIContainer(new InventoryPlayer(null), tile);
+ screen.mc = minecraft;
+ tile.getTank().setFluid(new FluidStack(net.minecraft.init.Fluids.WATER, 10000));
+ final int width = 128, height = 96;
+ Framebuffer framebuffer = new Framebuffer(width, height, true);
+ framebuffer.bindFramebuffer(true);
+ GlStateManager.matrixMode(GL11.GL_PROJECTION);
+ GlStateManager.pushMatrix();
+ GlStateManager.loadIdentity();
+ GlStateManager.ortho(0.0D, width, height, 0.0D, -1.0D, 1.0D);
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+ GlStateManager.pushMatrix();
+ GlStateManager.loadIdentity();
+ GlStateManager.disableLighting();
+ GlStateManager.disableDepthTest();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ GlStateManager.enableTexture2D();
+ GlStateManager.color4f(1F, 1F, 1F, 1F);
+ try {
+ GL11.glClearColor(0F, 0F, 0F, 1F);
+ GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
+ definition.drawGUIDecorations(tile, screen, 0, 0, 0F);
+ ByteBuffer pixels = BufferUtils.createByteBuffer(width * height * 4);
+ GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels);
+ BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ int index = ((height - y - 1) * width + x) * 4;
+ image.setRGB(x, y, (pixels.get(index) & 255) << 16
+ | (pixels.get(index + 1) & 255) << 8 | pixels.get(index + 2) & 255);
+ }
+ }
+ ImageIO.write(image, "png", new File("tank-water-gauge.png"));
+ return image.getRGB(88, 39) & 0xFFFFFF;
+ } catch (IOException failure) {
+ throw new IllegalStateException("Could not retain the native tank gauge image", failure);
+ } finally {
+ GlStateManager.popMatrix();
+ GlStateManager.matrixMode(GL11.GL_PROJECTION);
+ GlStateManager.popMatrix();
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+ framebuffer.deleteFramebuffer();
+ minecraft.getFramebuffer().bindFramebuffer(true);
+ GlStateManager.enableDepthTest();
+ }
+ }
+
+ private static void require(boolean condition, String message) {
+ if (!condition) throw new IllegalStateException(message);
+ }
+}
diff --git a/test-rig/client-resources/resources/META-INF/mods.toml b/test-rig/client-resources/resources/META-INF/mods.toml
new file mode 100644
index 00000000..87823e5f
--- /dev/null
+++ b/test-rig/client-resources/resources/META-INF/mods.toml
@@ -0,0 +1,16 @@
+modLoader="javafml"
+loaderVersion="[25,)"
+license="GPL-3.0-only"
+
+[[mods]]
+modId="advantageclientresources"
+version="1"
+displayName="Advantage Client Resource Probe"
+description='''Build-only model, texture and translation checks. Never distribute with the mod.'''
+
+[[dependencies.advantageclientresources]]
+modId="poweradvantage"
+mandatory=true
+versionRange="[2.4.4.113021,)"
+ordering="AFTER"
+side="CLIENT"
diff --git a/test-rig/client-resources/resources/pack.mcmeta b/test-rig/client-resources/resources/pack.mcmeta
new file mode 100644
index 00000000..ba29765c
--- /dev/null
+++ b/test-rig/client-resources/resources/pack.mcmeta
@@ -0,0 +1,6 @@
+{
+ "pack": {
+ "description": "Build-only Advantage client resource checks",
+ "pack_format": 4
+ }
+}
diff --git a/test-rig/client-resources/run.ps1 b/test-rig/client-resources/run.ps1
new file mode 100644
index 00000000..13291664
--- /dev/null
+++ b/test-rig/client-resources/run.ps1
@@ -0,0 +1,79 @@
+param([Parameter(Mandatory = $true)][string]$JavaHome,
+ [ValidateSet('runClient.launch', 'runClientAllAdvantage.launch')][string]$LaunchProfile = 'runClient.launch',
+ [ValidateSet('Gradle', 'Eclipse')][string]$OutputSource = 'Gradle',
+ [switch]$RequireExtensions,
+ [string]$RecipeBookFixtures)
+$ErrorActionPreference = 'Stop'
+$project = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
+$java = Join-Path $JavaHome 'bin/java.exe'
+if (-not (Test-Path -LiteralPath $java)) { throw "Java 8 runtime not found: $java" }
+$probe = Join-Path $project 'build/client-resource-probe/advantageclientresources-deobf.jar'
+if (-not (Test-Path -LiteralPath $probe)) { throw 'Run gradlew.bat clientResourceProbeJar verifyEclipseProductionClasspath first.' }
+[xml]$launch = Get-Content -LiteralPath (Join-Path $project $LaunchProfile) -Raw
+$attributes = @{}
+foreach ($attribute in $launch.launchConfiguration.stringAttribute) { $attributes[$attribute.key] = $attribute.value }
+$runtime = Join-Path $project ('build/client-resource-runs/' + [DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 8))
+$modOutput = Join-Path $runtime 'poweradvantage'
+New-Item -ItemType Directory -Path $modOutput,(Join-Path $runtime 'mods') -Force | Out-Null
+if ($OutputSource -eq 'Eclipse') {
+ Copy-Item -Path (Join-Path $project 'bin/main/*') -Destination $modOutput -Recurse
+} else {
+ Copy-Item -Path (Join-Path $project 'build/classes/java/main/*') -Destination $modOutput -Recurse
+ Copy-Item -Path (Join-Path $project 'build/resources/main/*') -Destination $modOutput -Recurse -Force
+}
+$metadataPath = Join-Path $modOutput 'META-INF/mods.toml'
+$inputReport = [ordered]@{ output_source = $OutputSource; launch_profile = $LaunchProfile; require_extensions = [bool]$RequireExtensions }
+if (Test-Path -LiteralPath $metadataPath) { $inputReport.metadata_sha256 = (Get-FileHash -LiteralPath $metadataPath).Hash }
+$inputReport | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $runtime 'client-input.json') -Encoding utf8
+if (-not (Test-Path -LiteralPath $metadataPath) -or (Get-Content -LiteralPath $metadataPath -Raw) -match '\$\{[^}]+\}') {
+ $failure = "The $OutputSource output contains missing or unexpanded mod metadata; inspect $runtime. Refresh Gradle/Eclipse before launching."
+ [ordered]@{ status = 'FAIL'; phase = 'launch-preflight'; error = $failure } | ConvertTo-Json |
+ Set-Content -LiteralPath (Join-Path $runtime 'client-resources.json') -Encoding utf8
+ throw $failure
+}
+Copy-Item -LiteralPath $probe -Destination (Join-Path $runtime 'mods/advantageclientresources.jar')
+if ($RecipeBookFixtures) {
+ $fixtures = (Resolve-Path -LiteralPath $RecipeBookFixtures).Path
+ $destination = Join-Path $runtime 'recipe-book-fixtures'
+ New-Item -ItemType Directory -Path $destination | Out-Null
+ $packets = @(Get-ChildItem -LiteralPath $fixtures -Filter 'recipes-*-sweep.bin')
+ if ($packets.Count -ne 3) { throw 'Recipe grouping requires Normal, Tech and Apocalyptic sweep packets.' }
+ foreach ($packet in $packets) {
+ Copy-Item -LiteralPath $packet.FullName -Destination $destination
+ Copy-Item -LiteralPath ([IO.Path]::ChangeExtension($packet.FullName, '.json')) -Destination $destination
+ }
+ $registrySource = Join-Path $fixtures 'AdvantageWorksMigration/level.dat'
+ Copy-Item -LiteralPath $registrySource -Destination (Join-Path $destination 'server-registries.dat')
+}
+$archives = foreach ($entry in $launch.launchConfiguration.listAttribute.listEntry) {
+ [xml]$classpathEntry = $entry.value
+ if ($classpathEntry.runtimeClasspathEntry.externalArchive) { $classpathEntry.runtimeClasspathEntry.externalArchive }
+}
+$arguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.PROGRAM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') })
+$vmArguments = @([regex]::Matches($attributes['org.eclipse.jdt.launching.VM_ARGUMENTS'], '"[^"]*"|\S+') | ForEach-Object { $_.Value.Trim('"') })
+if ($RequireExtensions) { $vmArguments += '-Dadvantageworks.requireExtensions=true' }
+$previous = @{}
+try {
+ foreach ($entry in $launch.launchConfiguration.mapAttribute.mapEntry) {
+ $previous[$entry.key] = [Environment]::GetEnvironmentVariable($entry.key, 'Process')
+ [Environment]::SetEnvironmentVariable($entry.key, $entry.value, 'Process')
+ }
+ $env:MOD_CLASSES = "poweradvantage%%$modOutput;poweradvantage%%$modOutput"
+ Push-Location -LiteralPath $runtime
+ try {
+ & $java @vmArguments '-Dadvantageworks.clientResources=true' '-Xmx2G' '-cp' ((@($modOutput) + $archives) -join [IO.Path]::PathSeparator) $attributes['org.eclipse.jdt.launching.MAIN_TYPE'] @arguments
+ if ($LASTEXITCODE -ne 0) { throw "Client exited with code $LASTEXITCODE; inspect $runtime/logs" }
+ } finally { Pop-Location }
+} finally {
+ foreach ($key in $previous.Keys) { [Environment]::SetEnvironmentVariable($key, $previous[$key], 'Process') }
+}
+$resultPath = Join-Path $runtime 'client-resources.json'
+if (-not (Test-Path -LiteralPath $resultPath)) { throw "Client did not produce a result; inspect $runtime/logs" }
+$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
+if ($result.status -ne 'PASS') { throw "Client resource check failed: $($result.error); inspect $runtime" }
+$log = Get-Content -LiteralPath (Join-Path $runtime 'logs/latest.log') -Raw
+$namespaces = if ($RequireExtensions) { '(?:poweradvantage|steamadvantage|electricadvantage)' } else { 'poweradvantage' }
+if ($log -match "Unable to load model:.*${namespaces}:|Exception loading blockstate definition:.*${namespaces}:|\[FML\.TEXTURE_ERRORS/\]") {
+ throw "Client logged a model or texture error; inspect $runtime/logs/latest.log"
+}
+Write-Output "PASS: $($result.items) items, $($result.block_states) block states, $($result.creative_entries) creative entries. Evidence: $runtime"
diff --git a/test-rig/eclipse/EclipseModelProbe.java b/test-rig/eclipse/EclipseModelProbe.java
new file mode 100644
index 00000000..3dd02008
--- /dev/null
+++ b/test-rig/eclipse/EclipseModelProbe.java
@@ -0,0 +1,36 @@
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import org.gradle.tooling.GradleConnector;
+import org.gradle.tooling.ProjectConnection;
+import org.gradle.tooling.model.eclipse.EclipseProject;
+import org.gradle.tooling.model.eclipse.EclipseSourceDirectory;
+
+/** Java 17 source-file tool: verify the Eclipse model Buildship actually imports. */
+public final class EclipseModelProbe {
+ public static void main(String[] args) {
+ if (args.length < 3) throw new IllegalArgumentException("Gradle home, Gradle user home, then project directories required");
+ for (int i = 2; i < args.length; i++) {
+ try (ProjectConnection connection = GradleConnector.newConnector()
+ .useInstallation(new File(args[0])).useGradleUserHomeDir(new File(args[1]))
+ .forProjectDirectory(new File(args[i])).connect()) {
+ EclipseProject project = connection.model(EclipseProject.class).withArguments("--offline")
+ .setJavaHome(new File(System.getProperty("java.home"))).get();
+ int processed = 0;
+ List directories = new ArrayList<>();
+ for (EclipseSourceDirectory source : project.getSourceDirectories()) {
+ String path = source.getPath().replace('\\', '/');
+ directories.add(path + " -> " + source.getOutput());
+ if ("src/main/resources".equals(path) || "src/generated/resources".equals(path))
+ throw new AssertionError("Buildship would copy raw templates: " + args[i] + " " + path);
+ if ("build/resources/main".equals(path)) {
+ if (!"bin/main".equals(source.getOutput())) throw new AssertionError("Wrong resource output");
+ processed++;
+ }
+ }
+ if (processed != 1) throw new AssertionError("Expected one processed resource root: " + directories);
+ System.out.println("PASS Eclipse model " + args[i] + ": " + directories);
+ }
+ }
+ }
+}
diff --git a/test-rig/gradle.properties b/test-rig/gradle.properties
new file mode 100644
index 00000000..f28952b1
--- /dev/null
+++ b/test-rig/gradle.properties
@@ -0,0 +1,8 @@
+org.gradle.jvmargs=-Xmx2G
+org.gradle.daemon=false
+
+minecraft_version=1.13.2
+forge_version=25.0.223
+mapping_channel=snapshot
+mapping_version=20180921-1.13
+harness_version=1.1.0.113021
diff --git a/test-rig/migration-probe/build.gradle b/test-rig/migration-probe/build.gradle
new file mode 100644
index 00000000..83181665
--- /dev/null
+++ b/test-rig/migration-probe/build.gradle
@@ -0,0 +1,44 @@
+plugins {
+ id 'java'
+ id 'net.minecraftforge.renamer' version '1.1.5'
+ id 'net.minecraftforge.gradle' version '7.0.34'
+}
+
+def target = providers.gradleProperty('probeTarget').getOrElse('110')
+if (!(target in ['110', '112'])) throw new GradleException('probeTarget must be 110 or 112')
+version = '1.0.0'
+group = 'zone.moddev.mc.advantageworks'
+base.archivesName = "AdvantageMigrationProbe-${target}"
+layout.buildDirectory = file("build/${target}")
+java.toolchain.languageVersion = JavaLanguageVersion.of(8)
+sourceSets.main.java.srcDir '../shared-probe'
+tasks.withType(JavaCompile).configureEach {
+ sourceCompatibility = '1.8'
+ targetCompatibility = '1.8'
+ options.encoding = 'UTF-8'
+}
+tasks.withType(AbstractArchiveTask).configureEach {
+ preserveFileTimestamps = false
+ reproducibleFileOrder = true
+}
+minecraft {
+ mappings channel: 'stable', version: target == '110' ? '29-1.10.2' : '39-1.12'
+}
+repositories {
+ minecraft.mavenizer(it)
+ maven fg.forgeMaven
+ maven fg.minecraftLibsMaven
+ mavenCentral()
+}
+dependencies {
+ implementation minecraft.dependency('net.minecraftforge:forge:' +
+ (target == '110' ? '1.10.2-12.18.3.2511' : '1.12.2-14.23.5.2859'))
+}
+tasks.named('jar', Jar) {
+ archiveClassifier = 'deobf'
+}
+def release = renamer.classes(tasks.named('jar', Jar)) {
+ map.from minecraft.dependency.toSrgFile
+ archiveClassifier = null
+}
+tasks.named('assemble') { dependsOn release }
diff --git a/test-rig/migration-probe/settings.gradle b/test-rig/migration-probe/settings.gradle
new file mode 100644
index 00000000..3f3cb84a
--- /dev/null
+++ b/test-rig/migration-probe/settings.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
+}
+rootProject.name = 'AdvantageMigrationProbe'
diff --git a/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyActors.java b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyActors.java
new file mode 100644
index 00000000..d499782e
--- /dev/null
+++ b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyActors.java
@@ -0,0 +1,210 @@
+package zone.moddev.mc.advantageworks.migration;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.mojang.authlib.GameProfile;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.UUID;
+import net.minecraft.block.Block;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.item.EntityItem;
+import net.minecraft.entity.item.EntityMinecartChest;
+import net.minecraft.entity.passive.EntityWolf;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.CompressedStreamTools;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.management.PlayerInteractionManager;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.WorldServer;
+
+/** Source-version serializers, without linking to any Advantage implementation. */
+final class LegacyActors {
+ private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create();
+ private static final String NAME = "AdvWorksPlayer";
+ private static final UUID PLAYER = UUID.nameUUIDFromBytes(("OfflinePlayer:" + NAME).getBytes(StandardCharsets.UTF_8));
+ private static final String WITNESS = "AdvWorksActor";
+
+ static boolean witnessed(Entity entity) { return entity.getEntityData().hasKey(WITNESS); }
+
+ static void build(MinecraftServer server, WorldServer world) throws Exception {
+ if (!world.loadedEntityList.stream().noneMatch(LegacyActors::witnessed)) {
+ throw new IllegalStateException("Actors already exist; use a new disposable copy");
+ }
+ for (int x = 128; x <= 191; x += 16) world.getChunkProvider().provideChunk(x >> 4, 0);
+ for (int x = 140; x < 172; x++) {
+ for (int z = 4; z < 13; z++) world.setBlockState(new BlockPos(x, 79, z), net.minecraft.init.Blocks.BEDROCK.getDefaultState(), 2);
+ }
+ EntityPlayerMP player = player(server, world);
+ NBTTagCompound data = player.writeToNBT(new NBTTagCompound());
+ data.setTag("Inventory", inventory());
+ NBTTagList ender = inventory();
+ while (ender.tagCount() > 27) ender.removeTag(ender.tagCount() - 1);
+ data.setTag("EnderItems", ender);
+ data.setFloat("Health", 17f);
+ data.setInteger("XpLevel", 23);
+ data.setInteger("XpTotal", 751);
+ data.setFloat("XpP", 0.375f);
+ data.setInteger("SelectedItemSlot", 7);
+ data.setInteger("foodLevel", 16);
+ data.setFloat("foodSaturationLevel", 3.5f);
+ player.readFromNBT(data);
+ player.setPositionAndRotation(144.5, 80, 8.5, 37, -12);
+ world.getSaveHandler().getPlayerNBTManager().writePlayerData(player);
+ NBTTagCompound actual = player.writeToNBT(new NBTTagCompound());
+ world.getSaveHandler().saveWorldInfoWithPlayer(world.getWorldInfo(), actual);
+ JsonObject definition = new JsonObject();
+ definition.addProperty("playerName", NAME);
+ definition.addProperty("playerUuid", PLAYER.toString());
+ definition.addProperty("universalBucket", Item.REGISTRY.containsKey(new ResourceLocation("forge:bucketfilled")));
+ definition.addProperty("minX", 128); definition.addProperty("maxX", 191);
+ Files.write(new File("actor-fixture.json").toPath(), JSON.toJson(definition).getBytes(StandardCharsets.UTF_8));
+
+ EntityMinecartChest cart = new EntityMinecartChest(world, 148.5, 80, 8.5);
+ cart.setInventorySlotContents(0, stack("electricadvantage:lithium_battery", 3));
+ cart.setInventorySlotContents(1, stack("steamadvantage:musket", 1));
+ add(world, cart, "cart");
+ EntityItem dropped = new EntityItem(world, 152.5, 80, 8.5, stack("steamadvantage:musket", 1));
+ dropped.setPickupDelay(32767);
+ add(world, dropped, "dropped-musket");
+ EntityItem bucket = new EntityItem(world, 156.5, 80, 8.5, oilBucket());
+ bucket.setPickupDelay(32767);
+ add(world, bucket, "dropped-oil");
+ EntityWolf wolf = new EntityWolf(world);
+ wolf.setPositionAndRotation(160.5, 80, 8.5, 42, 0);
+ wolf.setTamed(true); wolf.setOwnerId(PLAYER); wolf.setSitting(true);
+ add(world, wolf, "owned-wolf");
+ BlockPos pos = new BlockPos(164, 81, 8);
+ world.setBlockState(pos, Block.REGISTRY.getObject(new ResourceLocation("electricadvantage:hydroelectric_generator")).getDefaultState(), 2);
+ TileEntity parent = world.getTileEntity(pos);
+ Entity turbine = null;
+ for (String prefix : new String[] {"zone.moddev.mc", "com.mcmoddev", "cyano"}) {
+ try {
+ Class> type = Class.forName(prefix + ".electricadvantage.entities.HydroturbineEntity");
+ turbine = (Entity) type.getConstructor(net.minecraft.world.World.class, TileEntity.class).newInstance(world, parent);
+ break;
+ } catch (ClassNotFoundException ignored) { }
+ }
+ if (turbine == null) throw new IllegalStateException("No source hydroturbine implementation");
+ add(world, turbine, "hydroturbine");
+ }
+
+ private static EntityPlayerMP player(MinecraftServer server, WorldServer world) {
+ return new EntityPlayerMP(server, world, new GameProfile(PLAYER, NAME), new PlayerInteractionManager(world));
+ }
+
+ private static NBTTagList inventory() {
+ NBTTagList result = new NBTTagList();
+ String[] names = {"steamadvantage:musket", "electricadvantage:lithium_battery", "electricadvantage:lead_acid_battery",
+ "electricadvantage:alkaline_battery", "electricadvantage:nickel_hydride_battery", "poweradvantage:sprocket",
+ "minecraft:dirt", "minecraft:apple"};
+ int slot = 0;
+ for (String name : names) {
+ ItemStack stack = stack(name, name.endsWith("musket") ? 1 : 3);
+ NBTTagCompound nbt = stack.writeToNBT(new NBTTagCompound());
+ nbt.setByte("Slot", (byte) slot++); result.appendTag(nbt);
+ }
+ NBTTagCompound oil = oilBucket().writeToNBT(new NBTTagCompound());
+ oil.setByte("Slot", (byte) slot); result.appendTag(oil);
+ for (int armor = 0; armor < 4; armor++) {
+ String[] pieces = {"boots", "leggings", "chestplate", "helmet"};
+ ItemStack stack = stack("minecraft:iron_" + pieces[armor], 1);
+ stack.setItemDamage(9 + armor);
+ NBTTagCompound nbt = stack.writeToNBT(new NBTTagCompound());
+ nbt.setByte("Slot", (byte) (100 + armor)); result.appendTag(nbt);
+ }
+ NBTTagCompound offhand = stack("minecraft:shield", 1).writeToNBT(new NBTTagCompound());
+ offhand.setByte("Slot", (byte) -106); result.appendTag(offhand);
+ return result;
+ }
+
+ private static ItemStack stack(String name, int count) {
+ Item item = Item.REGISTRY.getObject(new ResourceLocation(name));
+ if (item == null || !Item.REGISTRY.containsKey(new ResourceLocation(name))) throw new IllegalStateException("Missing witness item " + name);
+ ItemStack result = new ItemStack(item, count);
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setString("MigrationWitness", name);
+ tag.setTag("Nested", new NBTTagCompound());
+ tag.getCompoundTag("Nested").setString("OwnerNote", "keep-this-verbatim");
+ if (name.contains("battery")) tag.setFloat("energy", 123f);
+ result.setTagCompound(tag);
+ if (item.isDamageable()) result.setItemDamage(5);
+ if (name.endsWith(":musket")) {
+ for (ResourceLocation enchantment : net.minecraft.enchantment.Enchantment.REGISTRY.getKeys()) {
+ if (enchantment.toString().startsWith("steamadvantage:")) {
+ result.addEnchantment(net.minecraft.enchantment.Enchantment.REGISTRY.getObject(enchantment), 1);
+ }
+ }
+ }
+ return result;
+ }
+
+ private static ItemStack oilBucket() {
+ if (!Item.REGISTRY.containsKey(new ResourceLocation("forge:bucketfilled"))) {
+ return stack("poweradvantage:crude_oil", 1);
+ }
+ ItemStack stack = stack("forge:bucketfilled", 1);
+ stack.getTagCompound().setString("FluidName", "crude_oil");
+ stack.getTagCompound().setInteger("Amount", 1000);
+ return stack;
+ }
+
+ private static void add(WorldServer world, Entity entity, String label) {
+ entity.setUniqueId(UUID.nameUUIDFromBytes(("AdvWorksEntity:" + label).getBytes(StandardCharsets.UTF_8)));
+ entity.getEntityData().setString(WITNESS, label);
+ entity.motionX = 0; entity.motionY = 0; entity.motionZ = 0;
+ if (!world.spawnEntity(entity)) throw new IllegalStateException("Failed to spawn " + label);
+ }
+
+ static void snapshot(MinecraftServer server, WorldServer world, String label, boolean readPlayer) throws Exception {
+ net.minecraftforge.common.util.FakePlayer sentinel = net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world);
+ sentinel.setPosition(128.5, 80, -16.5);
+ if (!world.playerEntities.contains(sentinel)) world.playerEntities.add(sentinel);
+ for (int x = 6; x <= 13; x++) {
+ for (int z = -2; z <= 2; z++) world.getChunkProvider().provideChunk(x, z);
+ }
+ EntityPlayerMP player = player(server, world);
+ if (readPlayer) {
+ if (server.getPlayerList().readPlayerDataFromFile(player) == null) throw new IllegalStateException("No player file loaded");
+ world.getSaveHandler().getPlayerNBTManager().writePlayerData(player);
+ }
+ JsonObject result = new JsonObject();
+ result.addProperty("playerAdapter", readPlayer);
+ result.addProperty("frozen", LegacyProbe.actorsFrozen(world));
+ if (readPlayer) result.add("player", encode(player.writeToNBT(new NBTTagCompound())));
+ JsonArray entities = new JsonArray();
+ for (Entity entity : new ArrayList<>(world.loadedEntityList)) {
+ if (!witnessed(entity)) continue;
+ NBTTagCompound nbt = new NBTTagCompound();
+ if (!entity.writeToNBTAtomically(nbt)) throw new IllegalStateException("Could not serialize entity");
+ JsonObject entry = encode(nbt);
+ entry.addProperty("witness", entity.getEntityData().getString(WITNESS));
+ entry.addProperty("ticksExisted", entity.ticksExisted);
+ entities.add(entry);
+ }
+ result.add("entities", entities);
+ result.addProperty("gameTime", world.getTotalWorldTime());
+ Files.write(new File("actors-" + label + ".json").toPath(), JSON.toJson(result).getBytes(StandardCharsets.UTF_8));
+ System.out.println("ADVACTORS COMPLETE " + label);
+ }
+
+ private static JsonObject encode(NBTTagCompound nbt) throws Exception {
+ JsonObject result = new JsonObject();
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ CompressedStreamTools.writeCompressed(nbt, bytes);
+ result.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray()));
+ return result;
+ }
+}
diff --git a/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyConnected.java b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyConnected.java
new file mode 100644
index 00000000..97e738a3
--- /dev/null
+++ b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyConnected.java
@@ -0,0 +1,155 @@
+package zone.moddev.mc.advantageworks.migration;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import net.minecraft.block.Block;
+import net.minecraft.block.properties.IProperty;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.ICommandSender;
+import net.minecraft.nbt.JsonToNBT;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.WorldServer;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import zone.moddev.mc.advantageworks.connected.ConnectedSession;
+
+/** Builds only new legacy fixtures; imported equipment is never constructed here. */
+final class LegacyConnected implements ConnectedSession.Port {
+ private final WorldServer world;
+ private final List held = new ArrayList<>();
+ private ConnectedSession session;
+ private static LegacyConnected instance;
+ private static Object owner;
+
+ private LegacyConnected(WorldServer world) { this.world = world; }
+
+ static void register(Object mod) { owner = mod; MinecraftForge.EVENT_BUS.register(new Events()); }
+
+ private static LegacyConnected get(WorldServer world) throws Exception {
+ if (!world.getWorldInfo().getWorldName().equals("AdvantageWorksMigration")) throw new IllegalStateException("Unexpected connected world");
+ if (instance == null || instance.world != world) {
+ instance = new LegacyConnected(world);
+ instance.session = new ConnectedSession(instance);
+ }
+ return instance;
+ }
+
+ private static boolean enabled() { return new File("advantage-connected.allow").isFile(); }
+
+ static CommandBase command() {
+ return new CommandBase() {
+ @Override public String getName() { return "advconnected"; }
+ @Override public String getUsage(ICommandSender sender) { return "advconnected build|snapshot | step "; }
+ @Override public int getRequiredPermissionLevel() { return 4; }
+ @Override public void execute(MinecraftServer server, ICommandSender sender, String[] args) {
+ try {
+ if (!enabled() || args.length < 2) throw new IllegalStateException("Unmarked connected fixture");
+ String operation = args[0];
+ int ticks = operation.equals("step") ? Integer.parseInt(args[1]) : 0;
+ String label = args[operation.equals("step") ? 2 : 1];
+ get(net.minecraftforge.common.DimensionManager.getWorld(0)).session.execute(operation, label, ticks, true);
+ } catch (Exception ex) { throw new IllegalStateException("Connected probe failed", ex); }
+ }
+ };
+ }
+
+ public static final class Events {
+ @SubscribeEvent public void tick(TickEvent.WorldTickEvent event) {
+ if (!enabled() || event.world.isRemote || event.world.provider.getDimension() != 0) return;
+ try {
+ ConnectedSession current = get((WorldServer) event.world).session;
+ if (event.phase == TickEvent.Phase.START) current.start(); else current.end();
+ } catch (Exception ex) { throw new IllegalStateException("Connected probe failed", ex); }
+ }
+ }
+
+ @Override public void prepare(JsonObject definition) {
+ JsonArray b = definition.getAsJsonArray("bounds");
+ net.minecraftforge.common.ForgeChunkManager.Ticket ticket = null;
+ int used = 0;
+ for (int x = b.get(0).getAsInt() >> 4; x <= b.get(3).getAsInt() >> 4; x++) {
+ for (int z = b.get(2).getAsInt() >> 4; z <= b.get(5).getAsInt() >> 4; z++) {
+ if (ticket == null || used >= ticket.getChunkListDepth()) {
+ ticket = net.minecraftforge.common.ForgeChunkManager.requestTicket(owner, world, net.minecraftforge.common.ForgeChunkManager.Type.NORMAL);
+ if (ticket == null) throw new IllegalStateException("No bounded fixture chunk ticket");
+ used = 0;
+ }
+ net.minecraftforge.common.ForgeChunkManager.forceChunk(ticket, new net.minecraft.util.math.ChunkPos(x, z));
+ used++;
+ world.getChunkProvider().provideChunk(x, z);
+ }
+ }
+ net.minecraftforge.common.util.FakePlayer sentinel = net.minecraftforge.common.util.FakePlayerFactory.getMinecraft(world);
+ sentinel.setPosition(0.5, 80, 0.5);
+ if (!world.playerEntities.contains(sentinel)) world.playerEntities.add(sentinel);
+ }
+
+ @Override public void hold() { held.addAll(world.tickableTileEntities); world.tickableTileEntities.clear(); }
+ @Override public void restore() { for (TileEntity tile : held) if (!tile.isInvalid() && !world.tickableTileEntities.contains(tile)) world.tickableTileEntities.add(tile); held.clear(); }
+ @Override public long time() { return world.getTotalWorldTime(); }
+
+ @Override public void build(JsonObject definition) throws Exception {
+ for (JsonElement value : definition.getAsJsonArray("blocks")) {
+ JsonObject e = value.getAsJsonObject();
+ BlockPos pos = position(e);
+ if (!world.isAirBlock(pos)) throw new IllegalStateException("Source fixture position already occupied: " + pos);
+ ResourceLocation name = new ResourceLocation(e.get("name").getAsString());
+ if (!Block.REGISTRY.containsKey(name)) throw new IllegalStateException("Missing source block: " + name);
+ Block block = Block.REGISTRY.getObject(name);
+ world.setBlockState(pos, block.getStateFromMeta(e.has("metadata") ? e.get("metadata").getAsInt() : 0), 2);
+ TileEntity tile = world.getTileEntity(pos);
+ if (tile != null) {
+ if (e.has("seed")) {
+ NBTTagCompound tag = tile.writeToNBT(new NBTTagCompound());
+ NBTTagCompound seed = JsonToNBT.getTagFromJson(e.get("seed").getAsString());
+ for (String key : seed.getKeySet()) {
+ if (!key.equals("Tank") && !key.equals("Items")) throw new IllegalArgumentException("Only initial fluid/items may be supplied");
+ tag.setTag(key, seed.getTag(key));
+ }
+ tile.readFromNBT(tag);
+ }
+ tile.getTileData().setString("AdvConnectedWitness", e.get("id").getAsString());
+ tile.markDirty();
+ }
+ }
+ }
+
+ @Override public JsonArray snapshot(JsonObject definition) throws Exception {
+ JsonArray result = new JsonArray();
+ for (JsonElement value : definition.getAsJsonArray("blocks")) {
+ JsonObject e = value.getAsJsonObject();
+ JsonObject entry = new com.google.gson.Gson().fromJson(e.toString(), JsonObject.class);
+ entry.remove("seed");
+ BlockPos pos = position(e);
+ IBlockState state = world.getBlockState(pos);
+ entry.addProperty("actualName", state.getBlock().getRegistryName().toString());
+ JsonObject properties = new JsonObject();
+ for (java.util.Map.Entry, Comparable>> p : state.getProperties().entrySet()) properties.addProperty(p.getKey().getName(), p.getValue().toString().toLowerCase(java.util.Locale.ROOT));
+ entry.add("properties", properties);
+ TileEntity tile = world.getTileEntity(pos);
+ if (tile != null) {
+ NBTTagCompound tag = tile.writeToNBT(new NBTTagCompound());
+ java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream();
+ net.minecraft.nbt.CompressedStreamTools.writeCompressed(tag, bytes);
+ entry.addProperty("nbt", tag.toString());
+ entry.addProperty("nbtBase64", java.util.Base64.getEncoder().encodeToString(bytes.toByteArray()));
+ entry.addProperty("objectIdentity", System.identityHashCode(tile));
+ entry.addProperty("witness", tile.getTileData().getString("AdvConnectedWitness"));
+ }
+ result.add(entry);
+ }
+ return result;
+ }
+
+ private static BlockPos position(JsonObject e) { return new BlockPos(e.get("x").getAsInt(), e.get("y").getAsInt(), e.get("z").getAsInt()); }
+}
diff --git a/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.java b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.java
new file mode 100644
index 00000000..8e6cafc9
--- /dev/null
+++ b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.java
@@ -0,0 +1,476 @@
+package zone.moddev.mc.advantageworks.migration;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonObject;
+import com.mojang.authlib.GameProfile;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockBed;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityLiving;
+import net.minecraft.entity.EntityHanging;
+import net.minecraft.entity.item.EntityBoat;
+import net.minecraft.entity.item.EntityItem;
+import net.minecraft.entity.EntityLeashKnot;
+import net.minecraft.entity.passive.EntityWolf;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.init.Blocks;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.CompressedStreamTools;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import net.minecraft.network.EnumPacketDirection;
+import net.minecraft.network.NetHandlerPlayServer;
+import net.minecraft.network.NetworkManager;
+import net.minecraft.network.Packet;
+import net.minecraft.network.play.client.CPacketClientStatus;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.management.PlayerInteractionManager;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.DamageSource;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.ChunkPos;
+import net.minecraft.world.World;
+import net.minecraft.world.WorldServer;
+import net.minecraftforge.common.DimensionManager;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.ForgeChunkManager;
+import net.minecraftforge.common.util.FakePlayer;
+import net.minecraftforge.common.util.FakePlayerFactory;
+import net.minecraftforge.event.world.WorldEvent;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+
+/** Additional-dimension source fixtures; no compile dependency on an Advantage mod. */
+final class LegacyDimensionEntities {
+ private static final Gson JSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
+ private static final File MARKER = new File("advantage-migration.allow");
+ private static final File DEFINITION = new File("dimension-fixture.json");
+ private static final String WITNESS = "AdvWorksDimensionEntity";
+ private static final BlockPos SPAWN = new BlockPos(160, 100, 24);
+ private static final Set LIVE = Collections.newSetFromMap(new IdentityHashMap());
+ private static final Map> TICKETS = new IdentityHashMap<>();
+ private static Object mod;
+ private final Map> held = new IdentityHashMap<>();
+
+ static void register(Object owner) {
+ mod = owner;
+ ForgeChunkManager.setForcedChunkLoadingCallback(owner, (tickets, world) -> {
+ // Each marked process requests its own bounded tickets before loading the fixture.
+ for (ForgeChunkManager.Ticket ticket : tickets) ForgeChunkManager.releaseTicket(ticket);
+ });
+ MinecraftForge.EVENT_BUS.register(new LegacyDimensionEntities());
+ }
+
+ private static boolean marked(World world) {
+ return MARKER.isFile() && !world.isRemote
+ && "AdvantageWorksMigration".equals(world.getWorldInfo().getWorldName());
+ }
+
+ private static boolean nativeOnLoad(JsonObject definition) {
+ return definition.has("nativeOnLoad") && definition.get("nativeOnLoad").getAsBoolean();
+ }
+
+ private static boolean frozen(World world, JsonObject definition) {
+ return marked(world) && DEFINITION.isFile() && !nativeOnLoad(definition) && !LIVE.contains(world);
+ }
+
+ private static boolean witnessed(Entity entity) { return entity.getEntityData().hasKey(WITNESS, 8); }
+
+ @SubscribeEvent public void load(WorldEvent.Load event) {
+ if (!marked(event.getWorld()) || !DEFINITION.isFile()) return;
+ try {
+ JsonObject definition = definition();
+ if (event.getWorld().provider.getDimension() == dimension(definition)) prepare((WorldServer) event.getWorld());
+ } catch (Exception ex) { throw new IllegalStateException("Dimension load preparation failed", ex); }
+ }
+
+ @SubscribeEvent public void tick(TickEvent.WorldTickEvent event) {
+ if (!marked(event.world) || !DEFINITION.isFile()) return;
+ if (event.phase == TickEvent.Phase.START) {
+ try {
+ JsonObject definition = definition();
+ int selected = dimension(definition);
+ if (event.world.provider.getDimension() == 0 && nativeOnLoad(definition)) {
+ if (DimensionManager.getWorld(selected) == null) DimensionManager.initDimension(selected);
+ WorldServer selectedWorld = DimensionManager.getWorld(selected);
+ if (selectedWorld == null || !marked(selectedWorld)) throw new IllegalStateException("Dimension unavailable");
+ prepare(selectedWorld);
+ }
+ if (event.world.provider.getDimension() != selected) return;
+ // Pin/load before either frozen observation or normal native initialization.
+ prepare((WorldServer) event.world);
+ if (!frozen(event.world, definition)) return;
+ List entities = witnesses((WorldServer) event.world);
+ entities.retainAll(event.world.loadedEntityList);
+ event.world.loadedEntityList.removeAll(entities);
+ held.put(event.world, entities);
+ } catch (Exception ex) { throw new IllegalStateException("Dimension tick preparation failed", ex); }
+ } else {
+ List entities = held.remove(event.world);
+ if (entities != null) for (Entity entity : entities) {
+ if (!event.world.loadedEntityList.contains(entity)) event.world.loadedEntityList.add(entity);
+ }
+ }
+ }
+
+ @SubscribeEvent public void unload(WorldEvent.Unload event) {
+ held.remove(event.getWorld()); LIVE.remove(event.getWorld());
+ // Forge's HIGHEST unload handler already owns ticket cleanup.
+ TICKETS.remove(event.getWorld());
+ }
+
+ static void execute(MinecraftServer server, String operation, String label) throws Exception {
+ if (!label.matches("[a-zA-Z0-9_-]+") || !MARKER.isFile()) throw new IllegalArgumentException("Unmarked runtime or unsafe label");
+ if (!operation.matches("build|read|snapshot|operate|respawn")) throw new IllegalArgumentException("Invalid dimension operation");
+ JsonObject definition = definition();
+ WorldServer overworld = DimensionManager.getWorld(0);
+ if (overworld == null || !marked(overworld)) throw new IllegalStateException("Unexpected overworld");
+ int dimension = dimension(definition);
+ if (DimensionManager.getWorld(dimension) == null) DimensionManager.initDimension(dimension);
+ WorldServer world = DimensionManager.getWorld(dimension);
+ if (world == null || !marked(world) || world.provider.getDimension() != dimension) throw new IllegalStateException("Dimension unavailable");
+ prepare(world);
+ if (operation.equals("build")) build(server, world, definition);
+ if (operation.equals("operate")) LIVE.add(world);
+ JsonObject result = snapshot(server, world, definition, operation);
+ Files.write(new File("dimensions-" + label + ".json").toPath(), JSON.toJson(result).getBytes(StandardCharsets.UTF_8));
+ System.out.println("ADVDIMENSIONS COMPLETE " + label);
+ }
+
+ private static JsonObject definition() throws Exception {
+ JsonObject result = JSON.fromJson(new String(Files.readAllBytes(DEFINITION.toPath()), StandardCharsets.UTF_8), JsonObject.class);
+ dimension(result);
+ String name = result.has("playerName") ? result.get("playerName").getAsString() : "AdvWorksDimensionPlayer";
+ UUID uuid = result.has("playerUuid") ? UUID.fromString(result.get("playerUuid").getAsString())
+ : UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8));
+ result.addProperty("playerName", name); result.addProperty("playerUuid", uuid.toString());
+ return result;
+ }
+
+ private static int dimension(JsonObject definition) {
+ int result = definition.get("dimension").getAsInt();
+ if (result != -1 && result != 1) throw new IllegalArgumentException("Expected Nether (-1) or End (1)");
+ return result;
+ }
+
+ private static void prepare(WorldServer world) {
+ if (!TICKETS.containsKey(world)) {
+ List tickets = new ArrayList<>();
+ ForgeChunkManager.Ticket ticket = null;
+ int used = 0;
+ for (int x = 6; x <= 13; x++) for (int z = -2; z <= 2; z++) {
+ if (ticket == null || (ticket.getChunkListDepth() > 0 && used >= ticket.getChunkListDepth())) {
+ ticket = ForgeChunkManager.requestTicket(mod, world, ForgeChunkManager.Type.NORMAL);
+ if (ticket == null) throw new IllegalStateException("Could not pin fixture chunks");
+ tickets.add(ticket); used = 0;
+ }
+ ForgeChunkManager.forceChunk(ticket, new ChunkPos(x, z)); used++;
+ }
+ TICKETS.put(world, tickets);
+ }
+ FakePlayer sentinel = FakePlayerFactory.getMinecraft(world);
+ if (sentinel.connection == null) new OfflineConnection(world.getMinecraftServer(), sentinel);
+ sentinel.setPosition(144.5, 100, 8.5);
+ if (!world.playerEntities.contains(sentinel)) world.playerEntities.add(sentinel);
+ for (int x = 6; x <= 13; x++) for (int z = -2; z <= 2; z++) world.getChunkProvider().provideChunk(x, z);
+ }
+
+ private static void platform(WorldServer world, int minX, int maxX, int minZ, int maxZ) {
+ for (int x = minX; x <= maxX; x++) for (int z = minZ; z <= maxZ; z++) {
+ world.setBlockState(new BlockPos(x, 99, z), Blocks.BEDROCK.getDefaultState(), 2);
+ world.setBlockState(new BlockPos(x, 106, z), Blocks.BEDROCK.getDefaultState(), 2);
+ for (int y = 100; y < 106; y++) world.setBlockState(new BlockPos(x, y, z),
+ (x == minX || x == maxX || z == minZ || z == maxZ ? Blocks.BEDROCK : Blocks.AIR).getDefaultState(), 2);
+ }
+ }
+
+ private static void build(MinecraftServer server, WorldServer world, JsonObject definition) throws Exception {
+ if (!witnesses(world).isEmpty() || definition.has("entities")) throw new IllegalStateException("Use a fresh disposable fixture");
+ platform(world, 140, 180, 2, 14);
+ EntityPlayerMP player = player(server, world, definition);
+ NBTTagCompound data = player.writeToNBT(new NBTTagCompound());
+ data.setTag("Inventory", inventory()); data.setTag("EnderItems", inventory());
+ data.setInteger("Dimension", dimension(definition)); data.setFloat("Health", 17f);
+ data.setInteger("XpLevel", 23); data.setInteger("XpTotal", 751); data.setFloat("XpP", 0.375f);
+ data.setInteger("SelectedItemSlot", 3); data.setInteger("foodLevel", 16); data.setFloat("foodSaturationLevel", 3.5f);
+ player.readFromNBT(data);
+ player.setPositionAndRotation(144.5, 100, 8.5, 37, -12);
+ player.setSpawnChunk(SPAWN, true, 0);
+ world.getSaveHandler().getPlayerNBTManager().writePlayerData(player);
+
+ EntityBoat boat = new EntityBoat(world, 152.5, 100, 8.5);
+ boat.setNoGravity(true); add(world, boat, "boat");
+ EntityWolf wolf = new EntityWolf(world);
+ wolf.setPositionAndRotation(152.5, 100, 8.5, 42, 0);
+ wolf.setTamed(true); wolf.setOwnerId(player.getUniqueID()); wolf.setSitting(true);
+ wolf.setNoAI(true); wolf.enablePersistence(); add(world, wolf, "passenger-wolf");
+ if (!wolf.startRiding(boat, true)) throw new IllegalStateException("Passenger could not mount boat");
+ EntityLiving horse = (EntityLiving) Class.forName("net.minecraft.entity.passive.EntityHorse")
+ .getConstructor(World.class).newInstance(world);
+ NBTTagCompound horseData = horse.writeToNBT(new NBTTagCompound());
+ horseData.setInteger("Type", 0); horseData.setInteger("Variant", 0); horseData.setBoolean("Tame", true);
+ horseData.setString("OwnerUUID", player.getUniqueID().toString());
+ horseData.setTag("SaddleItem", stack("minecraft:saddle", 1).writeToNBT(new NBTTagCompound()));
+ horseData.setTag("ArmorItem", stack("minecraft:iron_horse_armor", 1).writeToNBT(new NBTTagCompound()));
+ horse.readFromNBT(horseData);
+ horse.setPositionAndRotation(164.5, 100, 8.5, 0, 0); horse.setNoAI(true); horse.enablePersistence();
+ add(world, horse, "horse");
+ BlockPos fence = new BlockPos(168, 100, 8);
+ world.setBlockState(fence, Blocks.OAK_FENCE.getDefaultState(), 2);
+ EntityLeashKnot knot = new EntityLeashKnot(world, fence); add(world, knot, "leash-knot");
+ leashApi(horse, new String[] {"setLeashHolder", "setLeashedToEntity", "func_110162_b"},
+ new Class>[] {Entity.class, boolean.class}, knot, false);
+ BlockPos generator = new BlockPos(176, 101, 8);
+ ResourceLocation generatorId = new ResourceLocation("electricadvantage:hydroelectric_generator");
+ if (!Block.REGISTRY.containsKey(generatorId)) throw new IllegalStateException("Missing source generator");
+ world.setBlockState(generator, Block.REGISTRY.getObject(generatorId).getDefaultState(), 2);
+ TileEntity tile = world.getTileEntity(generator);
+ if (tile == null) throw new IllegalStateException("Missing generator tile");
+ Entity turbine = null;
+ for (String prefix : new String[] {"zone.moddev.mc", "com.mcmoddev", "cyano"}) {
+ try { turbine = (Entity) Class.forName(prefix + ".electricadvantage.entities.HydroturbineEntity")
+ .getConstructor(World.class, TileEntity.class).newInstance(world, tile); break; }
+ catch (ClassNotFoundException ignored) { }
+ }
+ if (turbine == null) throw new IllegalStateException("No source hydroturbine implementation");
+ add(world, turbine, "hydroturbine");
+ JsonArray entities = new JsonArray();
+ for (Entity entity : witnesses(world)) {
+ JsonObject entry = new JsonObject(); entry.addProperty("witness", entity.getEntityData().getString(WITNESS));
+ entry.addProperty("uuid", entity.getUniqueID().toString()); entities.add(entry);
+ }
+ definition.add("entities", entities);
+ definition.addProperty("universalBucket", Item.REGISTRY.containsKey(new ResourceLocation("forge:bucketfilled")));
+ JsonObject geometry = new JsonObject(); geometry.addProperty("minX", 144); geometry.addProperty("maxX", 176);
+ geometry.addProperty("y", 100); geometry.addProperty("z", 8); definition.add("geometry", geometry);
+ JsonObject parent = new JsonObject(); parent.addProperty("x", 176); parent.addProperty("y", 101); parent.addProperty("z", 8);
+ parent.addProperty("name", generatorId.toString()); definition.add("generator", parent);
+ Files.write(DEFINITION.toPath(), JSON.toJson(definition).getBytes(StandardCharsets.UTF_8));
+ }
+
+ private static EntityPlayerMP player(MinecraftServer server, WorldServer world, JsonObject definition) {
+ return new EntityPlayerMP(server, world, new GameProfile(UUID.fromString(definition.get("playerUuid").getAsString()),
+ definition.get("playerName").getAsString()), new PlayerInteractionManager(world));
+ }
+
+ private static EntityPlayerMP readPlayer(MinecraftServer server, WorldServer world, JsonObject definition) {
+ EntityPlayerMP player = player(server, world, definition);
+ if (server.getPlayerList().readPlayerDataFromFile(player) == null) throw new IllegalStateException("No player file loaded");
+ if (player.dimension != dimension(definition) || player.getServerWorld() != world) throw new IllegalStateException("Player loaded in wrong dimension");
+ return player;
+ }
+
+ private static NBTTagList inventory() {
+ NBTTagList result = new NBTTagList();
+ String[] names = {"steamadvantage:musket", "electricadvantage:lithium_battery", "electricadvantage:lead_acid_battery",
+ "electricadvantage:alkaline_battery", "electricadvantage:nickel_hydride_battery"};
+ for (int slot = 0; slot < names.length; slot++) {
+ NBTTagCompound nbt = stack(names[slot], slot == 0 ? 1 : 3).writeToNBT(new NBTTagCompound());
+ nbt.setByte("Slot", (byte) slot); result.appendTag(nbt);
+ }
+ ItemStack oil;
+ if (Item.REGISTRY.containsKey(new ResourceLocation("forge:bucketfilled"))) {
+ oil = stack("forge:bucketfilled", 1); oil.getTagCompound().setString("FluidName", "crude_oil");
+ oil.getTagCompound().setInteger("Amount", 1000);
+ } else oil = stack("poweradvantage:crude_oil", 1);
+ NBTTagCompound nbt = oil.writeToNBT(new NBTTagCompound()); nbt.setByte("Slot", (byte) 5); result.appendTag(nbt);
+ return result;
+ }
+
+ private static ItemStack stack(String name, int count) {
+ ResourceLocation id = new ResourceLocation(name);
+ if (!Item.REGISTRY.containsKey(id)) throw new IllegalStateException("Missing source item " + name);
+ Item item = Item.REGISTRY.getObject(id); ItemStack result = new ItemStack(item, count);
+ NBTTagCompound tag = new NBTTagCompound(); tag.setString("MigrationWitness", name);
+ NBTTagCompound nested = new NBTTagCompound(); nested.setString("OwnerNote", "keep-this-verbatim"); tag.setTag("Nested", nested);
+ if (name.contains("battery")) tag.setFloat("energy", 123f);
+ result.setTagCompound(tag); if (item.isDamageable()) result.setItemDamage(5);
+ if (name.endsWith(":musket")) for (ResourceLocation enchantment : net.minecraft.enchantment.Enchantment.REGISTRY.getKeys()) {
+ if (enchantment.toString().startsWith("steamadvantage:")) result.addEnchantment(net.minecraft.enchantment.Enchantment.REGISTRY.getObject(enchantment), 1);
+ }
+ return result;
+ }
+
+ private static void add(WorldServer world, Entity entity, String witness) {
+ entity.setUniqueId(UUID.nameUUIDFromBytes(("AdvWorksDimensionEntity:" + world.provider.getDimension() + ":" + witness).getBytes(StandardCharsets.UTF_8)));
+ entity.getEntityData().setString(WITNESS, witness); entity.setEntityInvulnerable(true);
+ entity.motionX = 0; entity.motionY = 0; entity.motionZ = 0;
+ if (!world.spawnEntity(entity)) throw new IllegalStateException("Failed to spawn " + witness);
+ }
+
+ private static List witnesses(WorldServer world) {
+ Set seen = Collections.newSetFromMap(new IdentityHashMap());
+ List result = new ArrayList<>();
+ for (Entity entity : new ArrayList<>(world.loadedEntityList)) collect(entity, seen, result);
+ result.sort((a, b) -> a.getEntityData().getString(WITNESS).compareTo(b.getEntityData().getString(WITNESS)));
+ return result;
+ }
+
+ private static void collect(Entity entity, Set seen, List result) {
+ if (!seen.add(entity)) return;
+ if (witnessed(entity)) result.add(entity);
+ for (Entity passenger : entity.getPassengers()) collect(passenger, seen, result);
+ }
+
+ private static JsonObject snapshot(MinecraftServer server, WorldServer world, JsonObject definition, String operation) throws Exception {
+ JsonObject result = new JsonObject(); result.add("definition", definition);
+ result.addProperty("operation", operation); result.addProperty("dimension", world.provider.getDimension());
+ result.addProperty("gameTime", world.getTotalWorldTime()); result.addProperty("frozen", frozen(world, definition));
+ result.addProperty("nativeOnLoad", nativeOnLoad(definition));
+ boolean read = operation.equals("read") || operation.equals("build") || operation.equals("respawn");
+ result.addProperty("playerAdapter", read); result.add("playerWorldDimension", JsonNull.INSTANCE);
+ if (read) {
+ EntityPlayerMP player = readPlayer(server, world, definition);
+ result.addProperty("playerWorldDimension", player.getServerWorld().provider.getDimension());
+ result.add("player", encode(player.writeToNBT(new NBTTagCompound())));
+ if (operation.equals("respawn")) result.add("respawn", respawn(server, world, definition, player));
+ else world.getSaveHandler().getPlayerNBTManager().writePlayerData(player);
+ }
+ JsonArray entities = new JsonArray();
+ for (Entity entity : witnesses(world)) {
+ // The optional chunk-root writer refuses passengers; the atomic writer does not.
+ NBTTagCompound nbt = new NBTTagCompound();
+ if (!entity.writeToNBTAtomically(nbt)) throw new IllegalStateException("Could not serialize dimension witness");
+ JsonObject entry = encode(nbt); entry.addProperty("witness", entity.getEntityData().getString(WITNESS));
+ entry.addProperty("uuid", entity.getUniqueID().toString()); entry.addProperty("ticksExisted", entity.ticksExisted);
+ entry.addProperty("entityId", entity.getEntityId());
+ entry.addProperty("instanceId", System.identityHashCode(entity));
+ entry.add("vehicleUuid", entity.getRidingEntity() == null ? JsonNull.INSTANCE : JSON.toJsonTree(entity.getRidingEntity().getUniqueID().toString()));
+ JsonArray passengers = new JsonArray(); for (Entity passenger : entity.getPassengers()) passengers.add(JSON.toJsonTree(passenger.getUniqueID().toString()));
+ entry.add("passengerUuids", passengers);
+ Entity leash = entity instanceof EntityLiving ? (Entity) leashApi((EntityLiving) entity,
+ new String[] {"getLeashHolder", "getLeashedToEntity", "func_110166_bE"}, new Class>[0]) : null;
+ entry.add("leashHolderUuid", leash == null ? JsonNull.INSTANCE : JSON.toJsonTree(leash.getUniqueID().toString()));
+ entry.add("leashHolderType", leash == null ? JsonNull.INSTANCE : JSON.toJsonTree(leash instanceof EntityLeashKnot
+ ? "minecraft:leash_knot" : leash.getClass().getName()));
+ BlockPos leashPos = leash instanceof EntityHanging ? ((EntityHanging) leash).getHangingPosition() : null;
+ entry.add("leashHolderPosition", leashPos == null ? JsonNull.INSTANCE
+ : JSON.toJsonTree(new int[] {leashPos.getX(), leashPos.getY(), leashPos.getZ()}));
+ entities.add(entry);
+ }
+ result.add("entities", entities);
+ BlockPos pos = new BlockPos(176, 101, 8); JsonObject generator = new JsonObject();
+ generator.addProperty("actualName", world.getBlockState(pos).getBlock().getRegistryName().toString());
+ TileEntity tile = world.getTileEntity(pos); if (tile != null) generator.add("tile", encode(tile.writeToNBT(new NBTTagCompound())));
+ result.add("generator", generator);
+ return result;
+ }
+
+ private static JsonObject respawn(MinecraftServer server, WorldServer world, JsonObject definition, EntityPlayerMP player) throws Exception {
+ JsonObject options = definition.has("respawn") ? definition.getAsJsonObject("respawn") : new JsonObject();
+ boolean keep = !options.has("keepInventory") || options.get("keepInventory").getAsBoolean();
+ String bedMode = options.has("bedMode") ? options.get("bedMode").getAsString() : "forced";
+ if (!bedMode.matches("forced|bed|missing")) throw new IllegalArgumentException("Invalid bed mode");
+ WorldServer destination = DimensionManager.getWorld(0);
+ platform(destination, 156, 166, 20, 29);
+ if (bedMode.equals("bed")) {
+ destination.setBlockState(SPAWN, Blocks.BED.getDefaultState().withProperty(BlockBed.FACING, EnumFacing.EAST)
+ .withProperty(BlockBed.PART, BlockBed.EnumPartType.FOOT), 2);
+ destination.setBlockState(SPAWN.east(), Blocks.BED.getDefaultState().withProperty(BlockBed.FACING, EnumFacing.EAST)
+ .withProperty(BlockBed.PART, BlockBed.EnumPartType.HEAD), 2);
+ }
+ player.setSpawnChunk(SPAWN, bedMode.equals("forced"), 0);
+ JsonObject result = new JsonObject(); result.addProperty("scope", "server-only-dummy-network");
+ result.addProperty("handler", "NetHandlerPlayServer.PERFORM_RESPAWN");
+ result.addProperty("keepInventory", keep); result.addProperty("bedMode", bedMode);
+ NBTTagCompound before = player.writeToNBT(new NBTTagCompound());
+ result.addProperty("fromDimension", player.dimension); result.add("before", encode(before));
+ JsonObject diagnostics = new JsonObject(); result.add("spawnDiagnostics", diagnostics);
+ diagnostics.add("before", spawnDiagnostics(player, before));
+ Set existing = Collections.newSetFromMap(new IdentityHashMap()); existing.addAll(world.loadedEntityList);
+ boolean oldSource = world.getGameRules().getBoolean("keepInventory"), oldTarget = destination.getGameRules().getBoolean("keepInventory");
+ EntityPlayerMP replacement = null;
+ try {
+ world.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(keep));
+ destination.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(keep));
+ OfflineConnection connection = new OfflineConnection(server, player);
+ player.setHealth(0); player.onDeath(new DamageSource("advworksDimensionDeath"));
+ NBTTagCompound afterDeath = player.writeToNBT(new NBTTagCompound());
+ result.add("afterDeath", encode(afterDeath));
+ diagnostics.add("afterDeath", spawnDiagnostics(player, afterDeath));
+ connection.processClientStatus(new CPacketClientStatus(CPacketClientStatus.State.PERFORM_RESPAWN));
+ replacement = server.getPlayerList().getPlayerByUUID(player.getUniqueID());
+ if (replacement == null || replacement == player) throw new IllegalStateException("Native respawn handler did not replace player");
+ new OfflineConnection(server, replacement);
+ NBTTagCompound after = replacement.writeToNBT(new NBTTagCompound());
+ result.add("after", encode(after));
+ diagnostics.add("after", spawnDiagnostics(replacement, after));
+ result.addProperty("toDimension", replacement.getServerWorld().provider.getDimension());
+ BlockPos bed = replacement.getBedLocation(replacement.dimension);
+ result.add("bedLocation", bed == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {bed.getX(), bed.getY(), bed.getZ()}));
+ JsonArray drops = new JsonArray();
+ for (Entity entity : world.loadedEntityList) if (entity instanceof EntityItem && !existing.contains(entity)) {
+ NBTTagCompound nbt = entity.writeToNBT(new NBTTagCompound()); nbt.setString("id", "minecraft:item"); drops.add(encode(nbt));
+ }
+ result.add("drops", drops);
+ return result;
+ } finally {
+ try {
+ if (replacement != null) server.getPlayerList().playerLoggedOut(replacement);
+ } finally {
+ world.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(oldSource));
+ destination.getGameRules().setOrCreateGameRule("keepInventory", Boolean.toString(oldTarget));
+ }
+ }
+ }
+
+ private static JsonObject spawnDiagnostics(EntityPlayerMP player, NBTTagCompound nbt) throws Exception {
+ JsonObject result = new JsonObject(); result.addProperty("queryDimension", 0);
+ result.addProperty("playerWorldDimension", player.getServerWorld().provider.getDimension());
+ try {
+ result.add("spawnDimension", JSON.toJsonTree(player.getClass().getMethod("getSpawnDimension").invoke(player)));
+ } catch (NoSuchMethodException ignored) {
+ // Forge 1.10 has dimension-aware bed getters but no player spawn-dimension getter.
+ result.add("spawnDimension", JsonNull.INSTANCE);
+ }
+ BlockPos bed = player.getBedLocation(0);
+ result.add("bedLocation", bed == null ? JsonNull.INSTANCE : JSON.toJsonTree(new int[] {bed.getX(), bed.getY(), bed.getZ()}));
+ result.addProperty("spawnForced", player.isSpawnForced(0));
+ JsonObject spawn = new JsonObject();
+ for (String key : new String[] {"SpawnX", "SpawnY", "SpawnZ"}) {
+ spawn.add(key, nbt.hasKey(key, 99) ? JSON.toJsonTree(nbt.getInteger(key)) : JsonNull.INSTANCE);
+ }
+ spawn.add("SpawnForced", nbt.hasKey("SpawnForced", 99) ? JSON.toJsonTree(nbt.getBoolean("SpawnForced")) : JsonNull.INSTANCE);
+ spawn.add("SpawnDimension", nbt.hasKey("SpawnDimension", 8) ? JSON.toJsonTree(nbt.getString("SpawnDimension"))
+ : nbt.hasKey("SpawnDimension", 99) ? JSON.toJsonTree(nbt.getInteger("SpawnDimension")) : JsonNull.INSTANCE);
+ result.add("serializedSpawn", spawn);
+ return result;
+ }
+
+ private static final class OfflineConnection extends NetHandlerPlayServer {
+ OfflineConnection(MinecraftServer server, EntityPlayerMP player) { super(server, new NetworkManager(EnumPacketDirection.SERVERBOUND), player); }
+ @Override public void sendPacket(Packet> packet) { /* Deliberately no socket or client login. */ }
+ }
+
+ private static Object leashApi(EntityLiving entity, String[] names, Class>[] types, Object... args) throws Exception {
+ // MCP renamed these native leash methods between the two source mappings.
+ for (String name : names) {
+ try { return EntityLiving.class.getMethod(name, types).invoke(entity, args); }
+ catch (NoSuchMethodException ignored) { }
+ }
+ throw new IllegalStateException("No native leash API");
+ }
+
+ private static JsonObject encode(NBTTagCompound nbt) throws Exception {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(); CompressedStreamTools.writeCompressed(nbt, bytes);
+ JsonObject result = new JsonObject(); result.addProperty("nbtBase64", Base64.getEncoder().encodeToString(bytes.toByteArray())); return result;
+ }
+}
diff --git a/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyProbe.java b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyProbe.java
new file mode 100644
index 00000000..a30d2810
--- /dev/null
+++ b/test-rig/migration-probe/src/main/java/zone/moddev/mc/advantageworks/migration/LegacyProbe.java
@@ -0,0 +1,241 @@
+package zone.moddev.mc.advantageworks.migration;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+import net.minecraft.block.Block;
+import net.minecraft.block.properties.IProperty;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.command.CommandBase;
+import net.minecraft.command.ICommandSender;
+import net.minecraft.init.Blocks;
+import net.minecraft.nbt.JsonToNBT;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.event.world.WorldEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
+import net.minecraftforge.fml.common.event.FMLServerStartingEvent;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+
+/** Disposable legacy fixture writer; deliberately has no Advantage compile dependency. */
+@Mod(modid = "advantagemigrationprobe", name = "Advantage Migration Probe", version = "1.0.0",
+ acceptableRemoteVersions = "*", serverSideOnly = true)
+public final class LegacyProbe {
+ private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create();
+ private static final File MARKER = new File("advantage-migration.allow");
+ private final java.util.Map> heldActors = new java.util.IdentityHashMap<>();
+
+ @Mod.EventHandler public void initialize(FMLPreInitializationEvent event) {
+ MinecraftForge.EVENT_BUS.register(this);
+ LegacyDimensionEntities.register(this);
+ LegacyConnected.register(this);
+ }
+
+ @SubscribeEvent public void load(WorldEvent.Load event) { freeze(event.getWorld()); }
+ @SubscribeEvent public void tick(TickEvent.WorldTickEvent event) {
+ if (event.phase == TickEvent.Phase.START) {
+ freeze(event.world);
+ if (actorsFrozen(event.world)) {
+ List held = new ArrayList<>();
+ for (net.minecraft.entity.Entity entity : event.world.loadedEntityList) {
+ if (LegacyActors.witnessed(entity)) held.add(entity);
+ }
+ event.world.loadedEntityList.removeAll(held);
+ heldActors.put(event.world, held);
+ }
+ } else {
+ List held = heldActors.remove(event.world);
+ if (held != null) event.world.loadedEntityList.addAll(held);
+ }
+ }
+
+ static boolean actorsFrozen(World world) {
+ return MARKER.isFile() && !world.isRemote && new File("actor-fixture.json").isFile()
+ && !new File("actor-operate.allow").isFile();
+ }
+
+ private void freeze(World world) {
+ if (MARKER.isFile() && !world.isRemote && !new File("migration-operate.allow").isFile()) {
+ world.tickableTileEntities.clear();
+ }
+ }
+
+ @Mod.EventHandler public void starting(FMLServerStartingEvent event) {
+ event.registerServerCommand(LegacyConnected.command());
+ event.registerServerCommand(new CommandBase() {
+ @Override public String getName() { return "advprobe"; }
+ @Override public String getUsage(ICommandSender sender) { return "advprobe build|snapshot "; }
+ @Override public int getRequiredPermissionLevel() { return 4; }
+ @Override public void execute(MinecraftServer server, ICommandSender sender, String[] args) {
+ try {
+ if (!MARKER.isFile()) throw new IllegalStateException("Unmarked migration runtime");
+ World world = net.minecraftforge.common.DimensionManager.getWorld(0);
+ if (!world.getWorldInfo().getWorldName().equals("AdvantageWorksMigration")) {
+ throw new IllegalStateException("Refusing an unexpected world");
+ }
+ if (args.length == 2 && args[0].startsWith("dimensions-")) {
+ LegacyDimensionEntities.execute(server, args[0].substring("dimensions-".length()), args[1]);
+ return;
+ }
+ if (args.length == 2 && args[0].startsWith("actors-")) {
+ if (!args[1].matches("[a-zA-Z0-9_-]+")) throw new IllegalArgumentException("Invalid label");
+ if (args[0].equals("actors-build")) LegacyActors.build(server, (net.minecraft.world.WorldServer) world);
+ else if (!args[0].equals("actors-read") && !args[0].equals("actors-snapshot")) throw new IllegalArgumentException("Invalid actor command");
+ LegacyActors.snapshot(server, (net.minecraft.world.WorldServer) world, args[1], !args[0].equals("actors-snapshot"));
+ return;
+ }
+ if (args.length == 0 || !(args[0].equals("build") || args[0].equals("snapshot"))) {
+ throw new IllegalArgumentException("Expected build or snapshot");
+ }
+ File definitionFile = new File("migration-fixture.json");
+ JsonArray definition;
+ if (args[0].equals("build")) {
+ definition = build(world);
+ Files.write(definitionFile.toPath(), JSON.toJson(definition).getBytes(StandardCharsets.UTF_8));
+ } else {
+ definition = JSON.fromJson(new String(Files.readAllBytes(definitionFile.toPath()),
+ StandardCharsets.UTF_8), JsonArray.class);
+ }
+ snapshot(world, definition, args.length > 1 ? args[1] : "source");
+ System.out.println("ADVPROBE COMPLETE " + args[0]);
+ } catch (Exception ex) { throw new IllegalStateException("Migration probe failed", ex); }
+ }
+ });
+ }
+
+ private JsonArray build(World world) throws Exception {
+ JsonArray definition = new JsonArray();
+ List names = new ArrayList<>();
+ for (ResourceLocation name : Block.REGISTRY.getKeys()) {
+ if (name.toString().matches("(poweradvantage|steamadvantage|electricadvantage):.*")) names.add(name);
+ }
+ names.sort((left, right) -> left.toString().compareTo(right.toString()));
+ int index = 0;
+ for (ResourceLocation name : names) {
+ Block block = Block.REGISTRY.getObject(name);
+ java.util.Set seen = new java.util.HashSet<>();
+ for (int meta = 0; meta < 16; meta++) {
+ IBlockState state;
+ try { state = block.getStateFromMeta(meta); } catch (RuntimeException ignored) { continue; }
+ if (!seen.add(state.toString())) continue;
+ // Flowing liquids are tested as sealed sources; scheduled fluid ticks are not frozen.
+ if ((name.toString().endsWith(":crude_oil") || name.toString().endsWith(":refined_oil")) && meta != 0) continue;
+ BlockPos pos = new BlockPos(-48 + (index % 24) * 4, 80, -48 + (index / 24) * 4);
+ index++;
+ world.setBlockState(pos.down(), Blocks.BEDROCK.getDefaultState(), 2);
+ if (name.toString().endsWith("oil")) {
+ for (net.minecraft.util.EnumFacing facing : net.minecraft.util.EnumFacing.HORIZONTALS) {
+ world.setBlockState(pos.offset(facing), Blocks.GLASS.getDefaultState(), 2);
+ }
+ }
+ world.setBlockState(pos, state, 2);
+ TileEntity tile = world.getTileEntity(pos);
+ if (tile != null) {
+ NBTTagCompound nbt = tile.writeToNBT(new NBTTagCompound());
+ // Both canonical and historical typo keys seed the original serializers.
+ nbt.merge(JsonToNBT.getTagFromJson("{Temperature:37.5f,Tempoerature:37.5f,progress:7s,Progress:7s,"
+ + "Tank:{FluidName:\"water\",Amount:750},TankOut:{FluidName:\"water\",Amount:750},"
+ + "InputTank:{FluidName:\"crude_oil\",Amount:750},TankIn:{FluidName:\"crude_oil\",Amount:750},"
+ + "Items:[{Slot:0b,id:\"minecraft:dirt\",Count:13b,Damage:0s}]}"));
+ int[] energy = nbt.getIntArray("Energy");
+ for (int i = 0; i < energy.length; i++) energy[i] = Float.floatToIntBits(100f);
+ nbt.setIntArray("Energy", energy);
+ tile.readFromNBT(nbt);
+ tile.markDirty();
+ }
+ JsonObject entry = new JsonObject();
+ entry.addProperty("x", pos.getX()); entry.addProperty("y", pos.getY()); entry.addProperty("z", pos.getZ());
+ entry.addProperty("name", name.toString()); entry.addProperty("metadata", block.getMetaFromState(state));
+ definition.add(entry);
+ }
+ }
+ List itemNames = new ArrayList<>();
+ for (ResourceLocation name : net.minecraft.item.Item.REGISTRY.getKeys()) {
+ if (name.toString().matches("(poweradvantage|steamadvantage|electricadvantage):.*")) itemNames.add(name);
+ }
+ itemNames.sort((left, right) -> left.toString().compareTo(right.toString()));
+ for (int start = 0; start < itemNames.size(); start += 25) {
+ BlockPos pos = new BlockPos(-56, 80, -48 + start / 25 * 4);
+ world.setBlockState(pos.down(), Blocks.BEDROCK.getDefaultState(), 2);
+ world.setBlockState(pos, Blocks.CHEST.getDefaultState(), 2);
+ net.minecraft.tileentity.TileEntityChest chest = (net.minecraft.tileentity.TileEntityChest) world.getTileEntity(pos);
+ for (int slot = 0; slot < 25 && start + slot < itemNames.size(); slot++) {
+ net.minecraft.item.Item item = net.minecraft.item.Item.REGISTRY.getObject(itemNames.get(start + slot));
+ net.minecraft.item.ItemStack stack = new net.minecraft.item.ItemStack(item, item.isDamageable() ? 1 : 3);
+ if (item.isDamageable()) stack.setItemDamage(5);
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setString("MigrationWitness", itemNames.get(start + slot).toString());
+ if (itemNames.get(start + slot).toString().contains("battery")) tag.setFloat("energy", 123f);
+ stack.setTagCompound(tag);
+ if (itemNames.get(start + slot).toString().endsWith(":musket")) {
+ for (ResourceLocation enchantment : net.minecraft.enchantment.Enchantment.REGISTRY.getKeys()) {
+ if (enchantment.toString().startsWith("steamadvantage:")) {
+ stack.addEnchantment(net.minecraft.enchantment.Enchantment.REGISTRY.getObject(enchantment), 1);
+ }
+ }
+ }
+ chest.setInventorySlotContents(slot, stack);
+ }
+ if (start == 0) {
+ net.minecraft.item.Item bucket = net.minecraft.item.Item.REGISTRY.getObject(new ResourceLocation("forge:bucketfilled"));
+ if (bucket != null) {
+ for (int slot = 25; slot <= 26; slot++) {
+ net.minecraft.item.ItemStack stack = new net.minecraft.item.ItemStack(bucket);
+ NBTTagCompound tag = new NBTTagCompound();
+ tag.setString("FluidName", slot == 25 ? "crude_oil" : "refined_oil");
+ tag.setInteger("Amount", 1000);
+ stack.setTagCompound(tag);
+ chest.setInventorySlotContents(slot, stack);
+ }
+ }
+ }
+ chest.markDirty();
+ JsonObject entry = new JsonObject();
+ entry.addProperty("x", pos.getX()); entry.addProperty("y", pos.getY()); entry.addProperty("z", pos.getZ());
+ entry.addProperty("name", "minecraft:chest"); entry.addProperty("metadata", 2);
+ definition.add(entry);
+ }
+ return definition;
+ }
+
+ private void snapshot(World world, JsonArray definition, String label) throws Exception {
+ if (!label.matches("[a-zA-Z0-9_-]+")) throw new IllegalArgumentException("Invalid snapshot label");
+ JsonArray result = new JsonArray();
+ for (JsonElement element : definition) {
+ JsonObject entry = JSON.fromJson(JSON.toJson(element), JsonObject.class);
+ BlockPos pos = new BlockPos(entry.get("x").getAsInt(), entry.get("y").getAsInt(), entry.get("z").getAsInt());
+ IBlockState state = world.getBlockState(pos);
+ entry.addProperty("actualName", state.getBlock().getRegistryName().toString());
+ JsonObject properties = new JsonObject();
+ for (java.util.Map.Entry, Comparable>> property : state.getProperties().entrySet()) {
+ properties.addProperty(property.getKey().getName(), property.getValue().toString().toLowerCase(java.util.Locale.ROOT));
+ }
+ entry.add("properties", properties);
+ TileEntity tile = world.getTileEntity(pos);
+ if (tile != null) {
+ tile.markDirty();
+ NBTTagCompound nbt = tile.writeToNBT(new NBTTagCompound());
+ entry.addProperty("nbt", nbt.toString());
+ java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream();
+ net.minecraft.nbt.CompressedStreamTools.writeCompressed(nbt, bytes);
+ entry.addProperty("nbtBase64", java.util.Base64.getEncoder().encodeToString(bytes.toByteArray()));
+ }
+ result.add(entry);
+ }
+ Files.write(new File("migration-" + label + ".json").toPath(), JSON.toJson(result).getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/test-rig/migration/README.md b/test-rig/migration/README.md
new file mode 100644
index 00000000..1705ab24
--- /dev/null
+++ b/test-rig/migration/README.md
@@ -0,0 +1,253 @@
+# Packaged migration matrix
+
+For current-version inventory-trigger and recipe-book persistence tests, see
+[Recipe Discovery](../../docs/1.13/RECIPE-DISCOVERY.md). The dedicated runner tests
+all three recipe modes, independent expansions, missing material providers and
+full-process restarts in fresh disposable worlds.
+
+For raw-material-to-machine reachability and live furnace, hammer, oil and
+plastic processing, see [Manufacturing Progression](../../docs/1.13/MANUFACTURING-PROGRESSION.md).
+Run `progression.py --java8 --output run-progression/new-current`;
+`--mode NORMAL`, `--mode TECH_PROGRESSION` or `--mode APOCALYPTIC` narrows a
+diagnostic run. The default qualifies all modes with both rubber settings.
+
+For the standalone current-version bucket/drain/pipe/container regression, see [Oil Transfer](../../docs/1.13/OIL-TRANSFER.md). It reuses this runner's packaged server and lifecycle snapshots but does not migrate or open an existing world.
+
+For working **source-version connected assemblies**, use `connected.py`, not
+`matrix.py --operate`: the latter commissions new machines after conversion.
+Read the [connected protocol, results and retained failures](../../docs/1.13/CONNECTED-MIGRATION.md).
+The connected runner builds once in a fresh legacy process, supplies finite
+items/water, runs 803 ticks, then permits only snapshots, 512-tick intervals,
+saving and shutdown in intermediate/target copies. Imported worlds carry no
+construction authority. Both adapters require `advantage-connected.allow` and
+`migration-operate.allow`; only the initial source receives `connected-build.allow`.
+
+```text
+python test-rig/migration/connected.py --java8 --profile-manifest --output
+python test-rig/migration/audit_connected.py --output
+```
+
+Run from the Power checkout. For split campaigns, supply all roots to the audit;
+it requires five sources and eight routes and does not accept partial evidence.
+`--sources`, `--routes` and `--port` narrow diagnostic runs, not complete acceptance.
+Current production pins remain unchanged; rebuilt test probes are archived and
+repinned explicitly. The default profile is an existing local qualification,
+so another machine must supply its own equivalent hash-checked manifest.
+The current full campaign/audit intentionally exits nonzero for the 1.12
+intermediate drill-progress loss rather than silently treating it as expected.
+
+That is the **original** campaign. The subsequent
+[legacy drill repair](../../docs/1.13/DRILL-PROGRESS-REPAIR.md) passes all eight
+routes with explicit candidate overrides while keeping historical source jars
+unchanged. `drill_restart.py` adds native 1.10/1.12/1.13 two-restart checks for
+both naturally powered mid-cycle drills, and `audit_drill_restart.py` verifies
+first-tick progress, frozen state, further production and untouched source saves.
+Use `--red` only to audit a retained two-version unpatched reproduction; its
+campaign result remains FAIL. `pin_drill_profiles.py` archives old outputs,
+prepares separate candidate profiles and refreshes mutable 1.10 rig references.
+None of these scripts modifies archived historical manifests or production jars.
+
+This runner never opens a source world in Minecraft. It creates disposable,
+explicitly marked superflat fixtures through source-version serializers, hashes
+the saved worlds, and converts only copies. Read the
+[qualification report](../../docs/1.13/MIGRATION-MATRIX.md) before interpreting PASS.
+
+## Prerequisites
+
+- Java 17 for Gradle; Java 8 for each legacy Minecraft server.
+- Python 3 with `requirements.txt` installed in an ignored local virtual environment.
+- Built Power/Steam/Electric 1.13 jars and the Works harness.
+- Built legacy probes for both source runtimes:
+
+```text
+gradlew.bat --no-daemon -p test-rig/migration-probe -PprobeTarget=110 assemble
+gradlew.bat --no-daemon -p test-rig/migration-probe -PprobeTarget=112 assemble
+gradlew.bat -p test-rig check assemble
+```
+
+The legacy probe compiles against Minecraft/Forge only, not any Advantage version.
+It registers no blocks/items. Its operator-only build/snapshot commands require
+both `advantage-migration.allow` and the exact `AdvantageWorksMigration` world name.
+The 1.13 snapshot adapter lives in the separate Works harness, never in release jars.
+
+## Inputs and repeatability
+
+The local default profiles discover the existing qualified server libraries and
+jar locations recorded in `matrix.py`. Those paths are workspace conveniences,
+not public distribution dependencies. On another machine, supply an explicit
+`--profile-manifest` with entries in this format:
+
+```json
+{
+ "original110": {
+ "seed": "../servers/forge110",
+ "launcher": "forge-1.10.2-12.18.3.2511-universal.jar",
+ "jars": [{"path": "../jars/Original.jar", "sha256": "FULL_UPPERCASE_SHA256"}]
+ }
+}
+```
+
+Include all required mod jars and the appropriate probe/harness in each profile.
+A full manifest has `original110`, `early110`, `maintained110`, `legacy112`,
+`maintained112`, and `target113`; chained routes use `maintained112` as the bridge.
+Relative paths resolve against the manifest file. A previous run's `profiles.json`
+can serve as a manifest while its pinned input files still match.
+The `seed` supplies installed Forge launchers and libraries, never a world or mods.
+The runner rejects hash mismatches and unavailable requested profiles.
+
+The default original-release profile requires the two original Sylvester jars
+and the official Electric jar staged under
+`run-migration-matrix/original-release-jars/`. Its filenames, hashes, and Cyano
+class provenance are checked. No jar is downloaded automatically by this runner.
+
+## Commands
+
+From the PowerAdvantage checkout (replace the Java path with the local Java 8):
+
+```text
+test-rig/migration/.venv/Scripts/python.exe -m unittest discover -s test-rig/migration -p test_matrix.py -v
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/matrix.py --java8 --output run-migration-matrix/fresh-run
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/matrix.py --java8 --sources original110 --fixtures run-migration-matrix/fresh-run --output run-migration-matrix/repeat-run
+test-rig/migration/.venv/Scripts/python.exe test-rig/migration/matrix.py --java8 --operate run-migration-matrix/repeat-run/original110-chained-113 --output run-migration-matrix/operation-run
+```
+
+Use the environment's normal Python executable on other platforms. Output paths
+must be new directories. The local server binds only `127.0.0.1:25584`, disables
+online authentication for these automated fixtures, and is shut down between phases.
+No viewer server is left running.
+
+Each route saves and fully stops Minecraft after conversion, launches a new
+process, snapshots again, and checks source preservation and restart stability.
+The operation pass enables ticking in another copy, commissions nine Works
+stations, checks two inert drill bays, and repeats checks after full restart.
+Manual checks retain `MANUAL_REQUIRED`; this is not a human GUI acceptance test.
+
+Never copy the allow markers, probe jars, or harness into a real gameplay instance.
+Do not commit worlds, dependency jars, caches, logs, or generated reports.
+
+`audit_results.py` combines completed run roots, verifies the immutable source
+fixtures and actual staged-jar hashes, and retains log errors alongside the state
+result. It never opens Minecraft. Supply `--results`, `--fixtures`, `--operation`,
+and a Git-ignored `--output` JSON path; see `--help` for the argument shapes.
+
+## Repeated-load endurance
+
+`endurance.py` runs fresh and original-release chained converted copies for thirty
+real minutes, checks exact repeat-batch throughput and storage each minute, then
+performs a complete process restart and another operating batch. See the
+[endurance protocol and qualification limits](../../docs/1.13/ENDURANCE.md).
+Use `--help` for paths, alternate pinned server profiles and shorter preflights;
+a short smoke run is never labelled an endurance pass.
+
+## Chunk lifecycle and moving drills
+
+`lifecycle.py` observes actual chunk unload/reload events and six-facing drill
+movement, saved inventories/energy, rear output, and mid-block progress. It tests
+fresh and converted disposable worlds and performs complete process restarts.
+Read the [protocol and coverage limits](../../docs/1.13/CHUNK-AND-DRILLS.md) and
+use `audit_lifecycle.py` to independently reconcile the raw evidence. A gameplay
+FAIL must not be relabelled PASS simply because the evidence audit succeeds.
+
+`extended_lifecycle.py` adds partial supply/consumer unloading, both reload orders,
+repeated native moves, vertical tracks, junctions, low reserves, full rear chests
+and redstone interruptions. See the [extended protocol](../../docs/1.13/PARTIAL-NETWORKS-AND-EXTENDED-DRILLS.md);
+`audit_extended_lifecycle.py` reconciles the raw records independently.
+
+`partial_network_trace.py` captures opt-in real chunk-acquisition stacks.
+`pipe_only_lifecycle.py` checks unloading/reconnection through a bridge chunk
+with no machine endpoints, including zero fluid acceptance while disconnected.
+Use `audit_pipe_only.py` for its independent state/conservation audit. These
+diagnostics use fresh, marked disposable worlds and never operate on player saves.
+
+## Players and saved entities
+
+`actors.py` extends the qualified source fixtures with a synthetic, deterministic
+offline player and five tagged entities. Legacy game serializers produce the
+inventory, Ender Chest, armour, offhand, charged batteries, damaged/enchantment-bearing
+musket, custom tags, and oil item/bucket witnesses. A source without the registered
+Forge universal bucket uses its crude-oil item instead; `actor-fixture.json`
+records that bucket coverage is unavailable for that source.
+
+The player adapter is the real `PlayerList.readPlayerDataFromFile` and
+`IPlayerFileData.writePlayerData` operating on `EntityPlayerMP`, not `FakePlayer`.
+It does not establish a network connection or exercise the Forge login handshake.
+A separate unlogged-in Forge fake-player sentinel prevents the witness world
+from going idle; it never loads or saves the actual witness player's file.
+Embedded single-player NBT is injected from the source serializer into `level.dat`
+and observed before the dedicated server removes host-only data. This is a
+pre-datafix embedded-data test, not integrated-server login acceptance.
+
+```text
+python actors.py --java8 --output
+python audit_actors.py --run --output
+python actors_native.py --java8 --fixtures --output
+python audit_actors_native.py --run --fixtures --output
+python player_layout.py --java8 --fixtures --output
+python audit_player_layout.py --run --output
+```
+
+Run these commands from this directory using the configured Python environment,
+or supply the script paths from the PowerAdvantage checkout. The default ports
+are isolated localhost ports 25616, 25617 and 25618. Existing listeners are not stopped.
+The runner requires the local hash-checked source fixtures listed in
+`qualified_baselines()` and stages the dependency profiles recorded by `matrix.py`.
+
+The actor, native and player-layout runners also accept `--profile-manifest`
+with the same hash-pinned format as `matrix.py`. An optional `bridge112` entry
+selects only the intermediate runtime; `maintained112` stays the original source
+profile. This allows a repaired 1.12 Electric jar to be tested without silently
+replacing historical fixtures or artifacts. The original `2.2.1.112021` Electric
+jar may be preserved under ignored `run-migration-matrix/legacy-release-jars/`;
+the default source profile verifies its qualified SHA-256. Never overwrite it
+with a rebuilt candidate. Profile manifests and jars remain local and untracked.
+
+Use `actors.py --routes original110-chained early110-chained maintained110-chained`
+alongside the normal arguments for a focused chained regression. The strict
+runner now saves and completely restarts the 1.12 intermediate before converting
+it to 1.13. Both checkpoints require the legacy probe's explicit `frozen` flag
+and zero measured entity ticks. `audit_actors.py --routes ...` can independently
+audit a specified completed subset without rewriting the original reports.
+
+The strict campaign suppresses entity ticking before the first converted save,
+checks migrated player files, runs the player adapter across complete restarts,
+then releases entities for at least sixty measured entity ticks and restarts again.
+Freeze checkpoints require every present tagged entity's counter to remain zero;
+a Forge update flag alone is not accepted as proof. Tagged witnesses are removed
+from the entity-update list at tick start and restored at tick end.
+Per-entity `ticksExisted` deltas are mandatory: elapsed world time alone cannot pass.
+The native control separately allows entity ticks before the first save, including
+the intermediate 1.12 stage on chained routes. It does not restore lost entity
+fields, reseed inventories, or replace missing entities to obtain a pass.
+The layout supplement covers main-bag boundaries, the last Ender Chest slot,
+armour/offhand, and recursively stored enchanted, charged and oil item stacks.
+Tile ticking remains suppressed so machines cannot manufacture replacement
+turbines and hide loss of the saved entity. Generated saves, logs, snapshots,
+backups and reports remain Git-ignored. See the
+[actor qualification report](../../docs/1.13/PLAYER-AND-ENTITY-MIGRATION.md)
+for verdicts, timing-sensitive failures, and untested client/ownership cases.
+
+After a completed native-feed run, `audit_endurance.py --run --output
+` independently verifies command acknowledgements, untouched
+restart networks, distinct batch records and the actual staged jar hashes. It
+rejects older NBT-fed diagnostic passes, which could refresh caches while feeding.
+
+## Additional Dimensions and Transformer Ordering
+
+`dimensions.py --native-load` extends all eight source routes to both the Nether
+and End, with real player-file adapters, a mounted owned wolf, leashed equipped
+horse and saved turbine. It requires native initialization before the first save
+and measured entity ticks on intermediate/target restarts. `audit_dimensions.py`
+replays the evidence. The separate `respawn.py` supplement exercises keep/drop
+inventory and forced/valid/missing-bed cases; `audit_respawn.py` reconciles saved
+files and item drops. See the [dimension protocol and limits](../../docs/1.13/DIMENSIONS-AND-ENTITY-LIFECYCLE.md).
+Use `pin_dimension_profiles.py` to pin rebuilt test probes without changing
+production/dependency pins; rebuilding an input afterwards makes the runner refuse it.
+
+`transformer_order.py` swaps only the actual BaseMetals/PowerAdvantage transformer
+pairs inside a disposable Forge coremods library, while retaining unchanged mod
+jars. It observes real application and emitted hook order, then compares original
+1.10 and maintained 1.12 migrations across natural and both forced orders.
+`audit_transformer_order.py` replays the bytecode, semantic and backup evidence.
+See the [ordering report](../../docs/1.13/TRANSFORMER-ORDERING.md), especially the
+first-writer/second-writer backup distinction. Never install the patched library
+in a gameplay instance.
diff --git a/test-rig/migration/actors.py b/test-rig/migration/actors.py
new file mode 100644
index 00000000..170d384e
--- /dev/null
+++ b/test-rig/migration/actors.py
@@ -0,0 +1,253 @@
+"""Player-file adapters and saved entity migration on disposable, pinned runtimes."""
+
+import argparse
+import json
+from pathlib import Path
+import shutil
+import socket
+import time
+
+import nbtlib
+from matrix import (POWER, WORLD, Server, copy_fixture, differences, digest, fingerprint,
+ intermediate_profile, normalized, parse_nbt, plain, selected_profiles, stage, write_json)
+
+
+PLAYER_KEYS = ("Inventory", "EnderItems", "SelectedItemSlot", "Pos", "Rotation", "Dimension",
+ "UUIDMost", "UUIDLeast", "Health", "XpLevel", "XpTotal", "XpP", "foodLevel", "foodSaturationLevel")
+ENTITY_KEYS = ("id", "UUIDMost", "UUIDLeast", "Pos", "Rotation", "Motion", "Item", "Items",
+ "OwnerUUID", "Sitting", "parentPos", "ForgeData")
+VANILLA_ENTITIES = {"Item": "minecraft:item", "MinecartChest": "minecraft:chest_minecart", "Wolf": "minecraft:wolf"}
+
+
+def enchantments(runtime):
+ level = nbtlib.load(runtime / WORLD / "level.dat")
+ entries = level.get("FML", {}).get("Registries", {}).get("minecraft:enchantments", {}).get("ids", [])
+ return {int(e["V"]): str(e["K"]) for e in entries}
+
+
+def semantic(value, keys, registry):
+ result = normalized(plain(value), registry)
+ if result.get("id") in VANILLA_ENTITIES: result["id"] = VANILLA_ENTITIES[result["id"]]
+ if "hydroturbine" in str(result.get("id", "")).lower(): result["id"] = "electricadvantage:hydroturbine"
+ for key in ("Inventory", "EnderItems", "Items"):
+ if key in result: result[key] = sorted(result[key], key=lambda entry: int(entry["Slot"]))
+ return {key: result[key] for key in keys if key in result}
+
+
+def capture(runtime, label, evidence=None):
+ definition = json.loads((runtime / "actor-fixture.json").read_text())
+ registry = enchantments(runtime)
+ record = json.loads((runtime / f"actors-{label}.json").read_text())
+ player_file = evidence / "player.dat" if evidence else runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat")
+ result = {"file": semantic(nbtlib.load(player_file).unpack(), PLAYER_KEYS, registry),
+ "entities": {}}
+ if "embedded" in record: result["embedded"] = semantic(parse_nbt(record["embedded"]), PLAYER_KEYS, registry)
+ if record["playerAdapter"]: result["adapter"] = semantic(parse_nbt(record["player"]), PLAYER_KEYS, registry)
+ for entry in record["entities"]:
+ witness = entry["witness"]
+ if witness in result["entities"]: raise AssertionError(f"Duplicate entity witness {witness}")
+ result["entities"][witness] = semantic(parse_nbt(entry), ENTITY_KEYS, registry)
+ return result
+
+
+def compare(expected, actual):
+ issues = differences(expected, actual)
+ if set(expected["entities"]) != set(actual["entities"]):
+ issues.append({"path": "entities.keys", "expected": sorted(expected["entities"]), "actual": sorted(actual["entities"])})
+ for container in ("file", "embedded", "adapter"):
+ if container not in expected or container not in actual: continue
+ for inventory in ("Inventory", "EnderItems"):
+ before, after = expected[container].get(inventory, []), actual[container].get(inventory, [])
+ if len(before) != len(after): continue # differences already reports length changes
+ old_slots = [entry["Slot"] for entry in before]
+ new_slots = [entry["Slot"] for entry in after]
+ if len(set(new_slots)) != len(new_slots) or old_slots != new_slots:
+ issues.append({"path": f"{container}.{inventory}.slots", "expected": old_slots, "actual": new_slots})
+ return {"status": "FAIL" if issues else "PASS", "differences": issues,
+ "player_status": "FAIL" if any(not d["path"].lstrip(".").startswith("entities") for d in issues) else "PASS",
+ "entity_status": "FAIL" if any(d["path"].lstrip(".").startswith("entities") for d in issues) else "PASS"}
+
+
+def tick_result(released, final):
+ before = {e["witness"]: e["ticksExisted"] for e in released["entities"]}
+ ticks = {e["witness"]: e["ticksExisted"] - before[e["witness"]] for e in final["entities"] if e["witness"] in before}
+ world_ticks = final["gameTime"] - released["gameTime"]
+ ok = world_ticks >= 60 and bool(ticks) and min(ticks.values()) >= 60
+ return {"status": "PASS" if ok else "FAIL", "world_ticks": world_ticks, "entity_ticks": ticks}
+
+
+def freeze_result(record):
+ counters = {e["witness"]: e["ticksExisted"] for e in record["entities"]}
+ return {"status": "PASS" if record.get("frozen") is True and bool(counters) and all(t == 0 for t in counters.values()) else "FAIL",
+ "entity_ticks": counters}
+
+
+def qualified_baselines():
+ root = POWER / "run-migration-matrix"
+ return {"original110": root / "original-cyano-20261005-r1/original110",
+ "early110": root / "full-20261005-r1/original110",
+ "maintained110": root / "full-20261005-r1/maintained110",
+ "legacy112": root / "full-20261005-r1/legacy112",
+ "maintained112": root / "full-20261005-r1/maintained112"}
+
+
+def boot(runtime, profile, java, port):
+ launcher = stage(runtime, profile)
+ properties = runtime / "server.properties"
+ properties.write_text(properties.read_text().replace("server-port=25584", f"server-port={port}"))
+ return launcher
+
+
+def actor_command(server, modern, operation, label):
+ command = f"advactors {operation} {label}" if modern else f"advprobe actors-{operation} {label}"
+ server.send(command)
+ server.wait("ADVACTORS COMPLETE " + label, 120)
+
+
+def phase(runtime, java, launcher, modern, label, operation="read"):
+ server = Server(runtime, java, launcher, label)
+ try:
+ if operation == "operate":
+ actor_command(server, modern, "operate" if modern else "snapshot", label + "-release")
+ time.sleep(4)
+ actor_command(server, modern, "snapshot" if modern else "read", label)
+ else:
+ actor_command(server, modern, operation, label)
+ server.send("save-all flush")
+ server.wait("Saved the", 30)
+ finally:
+ server.close()
+ if not modern:
+ # A dedicated server has no host player. Seed an integrated-style section
+ # from the actual source serializer; do not claim integrated-server login.
+ definition = json.loads((runtime / "actor-fixture.json").read_text())
+ player_file = runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat")
+ level = nbtlib.load(runtime / WORLD / "level.dat")
+ level["Data"]["Player"] = nbtlib.Compound(dict(nbtlib.load(player_file)))
+ level.save()
+ state = capture(runtime, label)
+ if not modern: state["embedded"] = state["file"]
+ evidence = runtime / "actor-evidence" / label
+ evidence.mkdir(parents=True, exist_ok=False)
+ definition = json.loads((runtime / "actor-fixture.json").read_text())
+ shutil.copy2(runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat"), evidence / "player.dat")
+ shutil.copy2(runtime / WORLD / "level.dat", evidence / "level.dat")
+ write_json(runtime / f"semantic-{label}.json", state)
+ return state
+
+
+def clone(source, target):
+ copy_fixture(source, target)
+ shutil.copy2(source / "actor-fixture.json", target / "actor-fixture.json")
+
+
+def backup_state(runtime):
+ return {str(path.relative_to(runtime / WORLD)): digest(path)
+ for path in (runtime / WORLD).rglob("*.poweradvantage-legacy-backup")}
+
+
+def run(args):
+ with socket.socket() as check:
+ check.bind(("127.0.0.1", args.port))
+ available = selected_profiles(args)
+ valid_routes = {name + "-" + route for name in args.sources
+ for route in (["direct", "chained"] if name.endswith("110") else ["direct"])}
+ if args.routes and not set(args.routes).issubset(valid_routes):
+ raise ValueError(f"Unavailable routes: {set(args.routes) - valid_routes}")
+ for name in args.sources:
+ fixture = qualified_baselines()[name]
+ if fingerprint(fixture / WORLD) != json.loads((fixture / "fixture-hashes.json").read_text()):
+ raise AssertionError(f"Changed immutable fixture {fixture}")
+ root = args.output
+ root.mkdir(parents=True, exist_ok=False)
+ write_json(root / "profiles.json", {name: {"seed": str(p[0]), "launcher": p[1],
+ "jars": [{"path": str(j), "sha256": digest(j)} for j in p[2]]} for name, p in available.items()})
+ results = {}
+ for name in args.sources:
+ routes = [route for route in (["direct", "chained"] if name.endswith("110") else ["direct"])
+ if not args.routes or name + "-" + route in args.routes]
+ if not routes: continue
+ source = qualified_baselines()[name]
+ before = fingerprint(source / WORLD)
+ expected_hashes = json.loads((source / "fixture-hashes.json").read_text())
+ if before != expected_hashes: raise AssertionError(f"Changed immutable fixture {source}")
+ fixture = root / name
+ launcher = boot(fixture, available[name], args.java8, args.port)
+ copy_fixture(source, fixture)
+ print(f"BUILD actors using source serializers: {name}", flush=True)
+ baseline = phase(fixture, args.java8, launcher, False, "source", "build")
+ if len(baseline["entities"]) != 5: raise AssertionError("Source did not retain five entities")
+ if len(baseline["file"]["Inventory"]) < 14: raise AssertionError("Missing source inventory/armor/offhand")
+ immutable = fingerprint(fixture / WORLD)
+ write_json(fixture / "fixture-hashes.json", immutable)
+ for route in routes:
+ label = name + "-" + route
+ print("MIGRATE " + label, flush=True)
+ input_fixture = fixture
+ verdict = {}
+ if route == "chained":
+ middle = root / (label + "-112")
+ middle_launcher = boot(middle, intermediate_profile(available), args.java8, args.port)
+ clone(fixture, middle)
+ state = phase(middle, args.java8, middle_launcher, False, "intermediate")
+ verdict["frozen_intermediate"] = freeze_result(json.loads((middle / "actors-intermediate.json").read_text()))
+ verdict["intermediate"] = compare(baseline, state)
+ state = phase(middle, args.java8, middle_launcher, False, "intermediate-restart")
+ verdict["frozen_intermediate_restart"] = freeze_result(json.loads((middle / "actors-intermediate-restart.json").read_text()))
+ verdict["intermediate_restart"] = compare(baseline, state)
+ input_fixture = middle
+ target = root / (label + "-113")
+ launcher = boot(target, available["target113"], args.java8, args.port)
+ clone(input_fixture, target)
+ copied_player = next((target / WORLD / "playerdata").glob("*.dat"))
+ original_player_hash = digest(copied_player)
+ # First inspect migrated files/entities without invoking the player loader.
+ converted = phase(target, args.java8, launcher, True, "converted", "snapshot")
+ verdict["frozen_conversion"] = freeze_result(json.loads((target / "actors-converted.json").read_text()))
+ expected = {key: value for key, value in baseline.items() if key != "adapter"}
+ verdict["conversion"] = compare(expected, converted)
+ backups = backup_state(target)
+ backup_file = copied_player.with_name(copied_player.name + ".poweradvantage-legacy-backup")
+ verdict["backup"] = {"status": "PASS" if backup_file.is_file() and digest(backup_file) == original_player_hash else "FAIL",
+ "files": backups}
+ loaded = phase(target, args.java8, launcher, True, "adapter")
+ verdict["frozen_adapter"] = freeze_result(json.loads((target / "actors-adapter.json").read_text()))
+ adapter_expected = {key: value for key, value in baseline.items() if key != "embedded"}
+ verdict["player_adapter"] = compare(adapter_expected, loaded)
+ restarted = phase(target, args.java8, launcher, True, "restarted")
+ verdict["frozen_restart"] = freeze_result(json.loads((target / "actors-restarted.json").read_text()))
+ verdict["restart"] = compare(adapter_expected, restarted)
+ verdict["idempotence"] = compare(loaded, restarted)
+ verdict["backup_stability"] = {"status": "PASS" if backup_state(target) == backups else "FAIL"}
+ runtime_state = phase(target, args.java8, launcher, True, "runtime", "operate")
+ survival = {"file": baseline["file"], "entities": {key: {k: v for k, v in value.items() if k not in ("Pos", "Motion", "Rotation")}
+ for key, value in baseline["entities"].items()}}
+ verdict["runtime_survival"] = compare(survival, runtime_state)
+ release = json.loads((target / "actors-runtime-release.json").read_text())
+ ticked = json.loads((target / "actors-runtime.json").read_text())
+ verdict["runtime_ticks"] = tick_result(release, ticked)
+ runtime_restart = phase(target, args.java8, launcher, True, "runtime-restart")
+ verdict["runtime_restart"] = compare(survival, runtime_restart)
+ results[label] = verdict
+ write_json(target / "actor-comparison.json", verdict)
+ write_json(root / "actor-results.json", results)
+ print(label + ": " + ", ".join(f"{key}={value['status']}" for key, value in verdict.items()), flush=True)
+ if fingerprint(fixture / WORLD) != immutable: raise AssertionError("Generated actor fixture changed")
+ if fingerprint(source / WORLD) != before: raise AssertionError("Historical source fixture changed")
+ write_json(root / "scope.json", {"real_client_login": "NOT_TESTED", "runtime_behaviour": "Entity survival, ownership and contents after >=60 native ticks and full restart",
+ "embedded_player": "Source-serialized NBT injected into level.dat; observed before dedicated-server host data removal",
+ "player_path": "PlayerList.readPlayerDataFromFile -> real EntityPlayerMP -> IPlayerFileData.writePlayerData",
+ "routes": len(results), "immutable_sources": "PASS"})
+ return 1 if any(stage["status"] != "PASS" for route in results.values() for stage in route.values()) else 0
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--workspace", type=Path, default=POWER.parents[1])
+ parser.add_argument("--profile-manifest", type=Path, help="Hash-pinned profiles; optional bridge112 overrides only the intermediate runtime")
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25616)
+ parser.add_argument("--sources", nargs="+", choices=list(qualified_baselines()), default=list(qualified_baselines()))
+ parser.add_argument("--routes", nargs="+", help="Optional route subset for focused regression checks")
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/actors_native.py b/test-rig/migration/actors_native.py
new file mode 100644
index 00000000..02ff7147
--- /dev/null
+++ b/test-rig/migration/actors_native.py
@@ -0,0 +1,66 @@
+"""Control: migrate actors and let them tick before their first target save."""
+import argparse
+import json
+from pathlib import Path
+import socket
+
+from actors import WORLD, boot, clone, compare, phase, tick_result
+from matrix import POWER, fingerprint, intermediate_profile, selected_profiles, write_json
+
+
+def run(args):
+ with socket.socket() as check: check.bind(("127.0.0.1", args.port))
+ root = args.output
+ root.mkdir(parents=True, exist_ok=False)
+ available = selected_profiles(args)
+ results = {}
+ source_results = json.loads((args.fixtures / "actor-results.json").read_text())
+ for label in source_results:
+ if args.routes and label not in args.routes: continue;
+ source_name = label.rsplit("-", 1)[0]
+ source = args.fixtures / source_name
+ bridge = source
+ untouched = fingerprint(bridge / WORLD)
+ if label.endswith("-chained"):
+ bridge = root / (label + "-112")
+ launcher112 = boot(bridge, intermediate_profile(available), args.java8, args.port)
+ clone(source, bridge)
+ (bridge / "actor-operate.allow").touch()
+ phase(bridge, args.java8, launcher112, False, "native112", "operate")
+ target = root / label
+ launcher = boot(target, available["target113"], args.java8, args.port)
+ clone(bridge, target)
+ bridge_before = fingerprint(bridge / WORLD)
+ # Unlike the frozen campaign, both stages allow parent links to resolve
+ # through native ticks before the first save.
+ (target / "actor-operate.allow").touch()
+ baseline = json.loads((source / "semantic-source.json").read_text())
+ expected = {"file": baseline["file"], "entities": {key: {k: v for k, v in value.items() if k not in ("Pos", "Motion", "Rotation")}
+ for key, value in baseline["entities"].items()}}
+ verdict = {}
+ for checkpoint in ("native", "native-restart"):
+ state = phase(target, args.java8, launcher, True, checkpoint, "operate")
+ verdict[checkpoint] = compare(expected, state)
+ released = json.loads((target / f"actors-{checkpoint}-release.json").read_text())
+ final = json.loads((target / f"actors-{checkpoint}.json").read_text())
+ verdict[checkpoint]["timing"] = tick_result(released, final)
+ if verdict[checkpoint]["timing"]["status"] != "PASS":
+ verdict[checkpoint]["status"] = "FAIL"
+ if fingerprint(source / WORLD) != untouched: raise AssertionError("Source fixture changed")
+ if fingerprint(bridge / WORLD) != bridge_before: raise AssertionError("Intermediate fixture changed")
+ results[label] = verdict
+ write_json(root / "native-results.json", results)
+ print(label + ": " + ", ".join(key + "=" + value["status"] for key, value in verdict.items()), flush=True)
+ return int(any(stage["status"] != "PASS" for route in results.values() for stage in route.values()))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--workspace", type=Path, default=POWER.parents[1])
+ parser.add_argument("--profile-manifest", type=Path, help="Hash-pinned profiles; optional bridge112 overrides only the intermediate runtime")
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--fixtures", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25617)
+ parser.add_argument("--routes", nargs="+", help="Optional route subset for a short preflight")
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/audit_actors.py b/test-rig/migration/audit_actors.py
new file mode 100644
index 00000000..cd886195
--- /dev/null
+++ b/test-rig/migration/audit_actors.py
@@ -0,0 +1,93 @@
+"""Replay raw actor snapshots and saved player files without running Minecraft."""
+import argparse
+import json
+from pathlib import Path
+
+from actors import WORLD, backup_state, capture, compare, tick_result, freeze_result
+from matrix import digest, fingerprint, write_json
+
+
+def audit(root, selected=None):
+ published = json.loads((root / "actor-results.json").read_text())
+ if selected and not set(selected).issubset(published):
+ raise ValueError(f"Unavailable audit routes: {set(selected) - set(published)}")
+ findings = []
+ routes = {}
+ for label, verdict in published.items():
+ if selected and label not in selected: continue
+ name = label.rsplit("-", 1)[0]
+ source = root / name
+ target = root / (label + "-113")
+ baseline = capture(source, "source", source / "actor-evidence/source")
+ baseline["embedded"] = baseline["file"]
+ if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()):
+ findings.append("Generated fixture changed: " + name)
+ for runtime in [source, target] + ([root / (label + "-112")] if label.endswith("-chained") else []):
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]:
+ findings.append("Staged artifact changed: " + jar["file"])
+ replayed = {}
+ states = {phase: capture(target, phase, target / "actor-evidence" / phase)
+ for phase in ("converted", "adapter", "restarted", "runtime", "runtime-restart")}
+ for stage, phase in (("frozen_conversion", "converted"), ("frozen_adapter", "adapter"), ("frozen_restart", "restarted")):
+ frozen = freeze_result(json.loads((target / f"actors-{phase}.json").read_text()))
+ if frozen != verdict[stage] or frozen["status"] != "PASS": findings.append("Pre-tick freeze not proven: " + label + "/" + phase)
+ replayed["conversion"] = compare({k: v for k, v in baseline.items() if k != "adapter"}, states["converted"])
+ for stage, phase in (("player_adapter", "adapter"), ("restart", "restarted")):
+ replayed[stage] = compare({k: v for k, v in baseline.items() if k != "embedded"}, states[phase])
+ replayed["idempotence"] = compare(states["adapter"], states["restarted"])
+ survival = {"file": baseline["file"], "entities": {key: {k: v for k, v in value.items() if k not in ("Pos", "Motion", "Rotation")}
+ for key, value in baseline["entities"].items()}}
+ replayed["runtime_survival"] = compare(survival, states["runtime"])
+ replayed["runtime_restart"] = compare(survival, states["runtime-restart"])
+ if "intermediate" in verdict:
+ middle = root / (label + "-112")
+ state = capture(middle, "intermediate", middle / "actor-evidence/intermediate")
+ if "frozen_intermediate" in verdict:
+ frozen = freeze_result(json.loads((middle / "actors-intermediate.json").read_text()))
+ if frozen != verdict["frozen_intermediate"] or frozen["status"] != "PASS":
+ findings.append("Intermediate pre-tick freeze not proven: " + label)
+ state["embedded"] = state["file"]
+ replayed["intermediate"] = compare(baseline, state)
+ if "intermediate_restart" in verdict:
+ state = capture(middle, "intermediate-restart", middle / "actor-evidence/intermediate-restart")
+ state["embedded"] = state["file"]
+ replayed["intermediate_restart"] = compare(baseline, state)
+ frozen = freeze_result(json.loads((middle / "actors-intermediate-restart.json").read_text()))
+ if frozen != verdict["frozen_intermediate_restart"] or frozen["status"] != "PASS":
+ findings.append("Intermediate restart pre-tick freeze not proven: " + label)
+ for stage, value in replayed.items():
+ if value != verdict[stage]: findings.append("Oracle replay disagrees: " + label + "/" + stage)
+ release = json.loads((target / "actors-runtime-release.json").read_text())
+ ticked = json.loads((target / "actors-runtime.json").read_text())
+ timing = tick_result(release, ticked)
+ if timing != verdict["runtime_ticks"] or timing["status"] != "PASS": findings.append("Insufficient native entity ticks: " + label)
+ if backup_state(target) != verdict["backup"]["files"]: findings.append("Migration backups changed: " + label)
+ predecessor = root / (label + "-112") if label.endswith("-chained") else source
+ definition = json.loads((source / "actor-fixture.json").read_text())
+ player_relative = Path("playerdata") / (definition["playerUuid"] + ".dat")
+ for relative in (Path("level.dat"), player_relative):
+ backup = target / WORLD / relative.with_name(relative.name + ".poweradvantage-legacy-backup")
+ if not backup.is_file() or digest(backup) != digest(predecessor / WORLD / relative):
+ findings.append("Backup does not match original predecessor bytes: " + label + "/" + str(relative))
+ errors = []
+ for log in target.glob("console-*.log"):
+ text = log.read_text(errors="replace")
+ if "ADVACTORS COMPLETE" not in text or "Stopping server" not in text: findings.append("Incomplete phase: " + str(log))
+ errors.extend({"log": log.name, "line": line} for line in text.splitlines() if "/ERROR]" in line or "/FATAL]" in line
+ or "Exception loading entity" in line)
+ routes[label] = {"replayed_stages": replayed, "timing": timing, "diagnostics": errors}
+ return {"status": "FAIL" if findings else "PASS", "meaning": "Evidence integrity, not gameplay acceptance",
+ "findings": findings, "routes": routes}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--routes", nargs="+", help="Audit only the explicitly listed completed routes")
+ args = parser.parse_args()
+ result = audit(args.run, args.routes)
+ write_json(args.output, result)
+ print(result["status"] + ": " + str(len(result["routes"])) + " routes; " + str(result["findings"]))
+ raise SystemExit(result["status"] != "PASS")
diff --git a/test-rig/migration/audit_actors_native.py b/test-rig/migration/audit_actors_native.py
new file mode 100644
index 00000000..19c7468b
--- /dev/null
+++ b/test-rig/migration/audit_actors_native.py
@@ -0,0 +1,50 @@
+"""Independently replay native actor controls from NBT and tick counters."""
+import argparse
+import json
+from pathlib import Path
+
+from actors import WORLD, capture, compare, tick_result
+from matrix import digest, fingerprint, write_json
+
+
+def audit(root, fixtures):
+ published = json.loads((root / "native-results.json").read_text())
+ issues, routes = [], {}
+ for label, verdict in published.items():
+ source = fixtures / label.rsplit("-", 1)[0]
+ target = root / label
+ if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()):
+ issues.append("Actor source fixture changed: " + label)
+ baseline = capture(source, "source", source / "actor-evidence/source")
+ expected = {"file": baseline["file"], "entities": {key: {k: v for k, v in value.items() if k not in ("Pos", "Motion", "Rotation")}
+ for key, value in baseline["entities"].items()}}
+ for runtime in [target] + ([root / (label + "-112")] if label.endswith("-chained") else []):
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]:
+ issues.append("Staged artifact changed: " + str(runtime / jar["file"]))
+ replayed = {}
+ for phase in ("native", "native-restart"):
+ state = capture(target, phase, target / "actor-evidence" / phase)
+ replayed[phase] = compare(expected, state)
+ release = json.loads((target / f"actors-{phase}-release.json").read_text())
+ final = json.loads((target / f"actors-{phase}.json").read_text())
+ replayed[phase]["timing"] = tick_result(release, final)
+ if replayed[phase]["timing"]["status"] != "PASS": replayed[phase]["status"] = "FAIL"
+ if replayed[phase] != verdict[phase]: issues.append("Raw native replay disagrees: " + label + "/" + phase)
+ log = (target / f"console-{phase}.log").read_text(errors="replace")
+ if "ADVACTORS COMPLETE" not in log or "Stopping server" not in log: issues.append("Incomplete native phase: " + label)
+ routes[label] = replayed
+ return {"status": "FAIL" if issues else "PASS", "meaning": "Evidence integrity, not gameplay acceptance",
+ "findings": issues, "routes": routes}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--fixtures", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run, args.fixtures)
+ write_json(args.output, result)
+ print(result["status"] + ": " + str(len(result["routes"])) + " routes; " + str(result["findings"]))
+ raise SystemExit(result["status"] != "PASS")
diff --git a/test-rig/migration/audit_connected.py b/test-rig/migration/audit_connected.py
new file mode 100644
index 00000000..f609db09
--- /dev/null
+++ b/test-rig/migration/audit_connected.py
@@ -0,0 +1,157 @@
+"""Independently recheck connected-world evidence without starting Minecraft."""
+import argparse
+import json
+from pathlib import Path
+
+from connected import SOURCES, cold_state, entries, fixture, operation, read, state
+from matrix import WORLD, compare, digest, fingerprint, write_json
+
+
+def expected_routes(sources):
+ return {name + "-" + route for name in sources
+ for route in (("direct", "chained") if name.endswith("110") else ("direct",))}
+
+
+def audit(roots):
+ checks, diagnostics, sources, routes = [], [], set(), set()
+
+ def check(name, action):
+ try:
+ result = action()
+ if result is False or isinstance(result, dict) and result.get("status") != "PASS":
+ raise AssertionError(str(result))
+ checks.append(dict(name=name, status="PASS"))
+ except Exception as error:
+ checks.append(dict(name=name, status="FAIL", reason=str(error)))
+
+ def equal(actual, expected):
+ if actual != expected:
+ raise AssertionError(f"Expected {expected}, got {actual}")
+
+ def snapshot(runtime, label, definition, builds):
+ document = read(runtime, label)
+ equal(document["buildCallsThisProcess"], builds)
+ equal(document["held"], True)
+ equal(document["snapshot"], json.loads((runtime / ("migration-" + label + ".json")).read_text()))
+ rows = entries(document)
+ equal(len(rows), len(definition["blocks"]))
+ equal(set(rows), {b["id"] for b in definition["blocks"]})
+ for expected in definition["blocks"]:
+ for key, value in expected.items():
+ if key != "seed":
+ equal(rows[expected["id"]][key], value)
+ return document
+
+ def commands(runtime, label, expected):
+ equal(json.loads((runtime / ("console-" + label + ".commands.json")).read_text()), expected)
+ log = (runtime / ("console-" + label + ".log")).read_text(encoding="utf-8")
+ if "Done (" not in log or "Stopping server" not in log:
+ raise AssertionError("Missing startup or graceful shutdown marker")
+ if any(marker in log for marker in ("Connected probe failed", "Encountered an unexpected exception", "A fatal error has been detected")):
+ raise AssertionError("Fatal probe/server diagnostic")
+ errors = [line for line in log.splitlines() if "/ERROR]" in line or "/FATAL]" in line]
+ if errors:
+ diagnostics.append(dict(runtime=str(runtime), phase=label, lines=errors[:100], total=len(errors)))
+
+ def phase(runtime, label, definition):
+ loaded = snapshot(runtime, label + "-loaded", definition, 0)
+ worked = snapshot(runtime, label + "-worked", definition, 0)
+ equal(loaded["requestedTicks"], 0)
+ equal(loaded["advancedTicks"], 0)
+ equal(worked["requestedTicks"], 512)
+ commands(runtime, label, ["advconnected snapshot " + label + "-loaded",
+ "advconnected step 512 " + label + "-worked", "save-all flush", "stop"])
+ if (runtime / "connected-build.allow").exists() or (runtime / "connected-built.json").exists():
+ raise AssertionError("Imported runtime has source construction authority/evidence")
+ return operation(loaded, worked, 512)
+
+ for root in roots:
+ report = json.loads((root / "connected-results.json").read_text())
+ label = root.name
+ check(label + ": completed campaign", lambda: equal(report["status"], "PASS"))
+ check(label + ": route completeness", lambda: equal(set(report["routes"]), expected_routes(report["sources"])))
+ check(label + ": source uniqueness", lambda: not (sources & set(report["sources"])))
+ check(label + ": route uniqueness", lambda: not (routes & set(report["routes"])))
+ sources.update(report["sources"])
+ routes.update(report["routes"])
+ profiles = json.loads((root / "profiles.json").read_text())
+ provenance = json.loads((root / "input-provenance.json").read_text())
+ base = Path(provenance["base"])
+ check(label + ": archived base profile", lambda: equal(digest(base), provenance["sha256"]))
+ previous = json.loads(base.read_text())
+ for name, profile in profiles.items():
+ check(label + ": " + name + " jar identities", lambda n=name, p=profile:
+ equal([Path(j["path"]).name for j in p["jars"]], [Path(j["path"]).name for j in previous[n]["jars"]]))
+ for old, jar in zip(previous[name]["jars"], profile["jars"]):
+ check(label + ": archive " + name + "/" + Path(jar["path"]).name,
+ lambda j=jar: equal(digest(Path(j["path"])), j["sha256"]))
+ if not Path(jar["path"]).name.startswith(("AdvantageMigrationProbe-", "AdvantageWorksTestHarness-")):
+ check(label + ": unchanged production/dependency " + name + "/" + Path(jar["path"]).name,
+ lambda a=old, b=jar: equal(a["sha256"], b["sha256"]))
+ for runtime in root.iterdir():
+ if not runtime.is_dir() or not (runtime / "input-jars.json").exists():
+ continue
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ check(runtime.name + ": staged " + jar["file"],
+ lambda r=runtime, j=jar: equal(digest(r / "mods" / j["file"]), j["sha256"]))
+ for name in report["sources"]:
+ source = root / name
+ definition = json.loads((source / "connected-fixture.json").read_text())
+ check(name + ": exact finite construction plan", lambda d=definition, n=name: equal(d, fixture(n)))
+ check(name + ": immutable saved source", lambda s=source:
+ equal(fingerprint(s / WORLD), json.loads((s / "fixture-hashes.json").read_text())))
+
+ def source_operation():
+ cold = snapshot(source, "cold", definition, 1)
+ worked = snapshot(source, "source", definition, 1)
+ if cold_state(cold)["status"] != "PASS":
+ return cold_state(cold)
+ equal(cold["requestedTicks"], 0)
+ commands(source, "source", ["advconnected build cold", "advconnected step 803 source", "save-all flush", "stop"])
+ if state(worked)["cases"]["battery"]["charge"] <= 0:
+ raise AssertionError("No real source battery charging")
+ return operation(cold, worked, 803)
+
+ check(name + ": real source operation from cold finite supplies", source_operation)
+ for route in ("direct", "chained") if name.endswith("110") else ("direct",):
+ route_name = name + "-" + route
+ previous_runtime, checkpoint = source, "source"
+ if route == "chained":
+ middle = root / (route_name + "-112")
+ check(route_name + ": intermediate operation", lambda r=middle, d=definition: phase(r, "intermediate", d))
+ check(route_name + ": intermediate preservation", lambda s=source, r=middle:
+ compare(s / "migration-source.json", r / "migration-intermediate-loaded.json"))
+ check(route_name + ": intermediate restart operation", lambda r=middle, d=definition: phase(r, "intermediate-restart", d))
+ check(route_name + ": intermediate restart preservation", lambda r=middle:
+ compare(r / "migration-intermediate-worked.json", r / "migration-intermediate-restart-loaded.json"))
+ previous_runtime, checkpoint = middle, "intermediate-restart-worked"
+ target = root / (route_name + "-113")
+ check(route_name + ": target operation", lambda r=target, d=definition: phase(r, "converted", d))
+ check(route_name + ": target preservation", lambda p=previous_runtime, c=checkpoint, r=target:
+ compare(p / ("migration-" + c + ".json"), r / "migration-converted-loaded.json"))
+ check(route_name + ": restart operation", lambda r=target, d=definition: phase(r, "restarted", d))
+ check(route_name + ": restart preservation", lambda r=target:
+ compare(r / "migration-converted-worked.json", r / "migration-restarted-loaded.json"))
+
+ def output(runtime=target):
+ first = state(read(runtime, "converted-loaded"))
+ last = state(read(runtime, "restarted-worked"))
+ missing = [n for n, case in first["cases"].items() if n != "battery" and case["remaining"] > 0
+ and last["cases"][n]["produced"] <= case["produced"]]
+ equal(missing, [])
+ check(route_name + ": real additional items across both target intervals", output)
+ check("All five historical sources tested", lambda: equal(sources, set(SOURCES)))
+ check("All eight direct/chained routes tested", lambda: equal(routes, expected_routes(SOURCES)))
+ return dict(status="FAIL" if any(c["status"] != "PASS" for c in checks) else "PASS",
+ sources=sorted(sources), routes=sorted(routes), checks=checks, diagnostics=diagnostics)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("roots", nargs="+", type=Path)
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ result = audit(args.roots)
+ write_json(args.output, result)
+ print(result["status"], len(result["checks"]), "checks;", len(result["routes"]), "routes")
+ raise SystemExit(result["status"] != "PASS")
diff --git a/test-rig/migration/audit_connected_progress.py b/test-rig/migration/audit_connected_progress.py
new file mode 100644
index 00000000..31234567
--- /dev/null
+++ b/test-rig/migration/audit_connected_progress.py
@@ -0,0 +1,66 @@
+"""Reconcile the one-tick legacy/new-runtime comparison from raw snapshots."""
+import argparse
+import json
+from pathlib import Path
+
+from connected import data, entries, operation, read
+from matrix import WORLD, compare, digest, fingerprint, write_json
+
+
+def first_tick(loaded, first):
+ old = entries(loaded)["C-EDRILL-drill"]
+ new = entries(first)["C-EDRILL-drill"]
+ before, after = data(old), data(new)
+ if loaded["requestedTicks"] != 0 or not loaded["held"]:
+ raise AssertionError("Initial snapshot was not frozen")
+ if first["requestedTicks"] != 1 or first["advancedTicks"] != 1 or not first["held"]:
+ raise AssertionError("Missing one-tick observation")
+ if old["objectIdentity"] != new["objectIdentity"] or old["witness"] != new["witness"]:
+ raise AssertionError("Drill was replaced")
+ if any(before.get(key) != after.get(key) for key in ("targetX", "targetY", "targetZ")):
+ raise AssertionError("This control requires an unchanged mining target")
+ return dict(status="PASS" if after["progress"] == before["progress"] + 1 else "FAIL",
+ loaded_progress=before["progress"], first_tick_progress=after["progress"])
+
+
+def audit(root, fixture):
+ report = dict(controls={})
+ definition = json.loads((fixture / "connected-fixture.json").read_text())
+ for name in ("legacy112", "target113"):
+ runtime = root / name
+ loaded, first, continued = (read(runtime, label) for label in ("loaded", "first-tick", "continued"))
+ commands = json.loads((runtime / "console-trace.commands.json").read_text())
+ if commands != ["advconnected snapshot loaded", "advconnected step 1 first-tick", "advconnected step 700 continued", "save-all flush", "stop"]:
+ raise AssertionError("Unexpected mutation command")
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]:
+ raise AssertionError("Changed staged input")
+ if (runtime / "connected-build.allow").exists() or (runtime / "connected-built.json").exists():
+ raise AssertionError("Trace was allowed to rebuild")
+ if json.loads((runtime / "connected-fixture.json").read_text()) != definition:
+ raise AssertionError("Trace definition changed")
+ if any(read(runtime, label)["buildCallsThisProcess"] != 0 for label in ("loaded", "first-tick", "continued")):
+ raise AssertionError("Trace rebuilt equipment")
+ report["controls"][name] = dict(first_tick=first_tick(loaded, first),
+ preservation=compare(fixture / "migration-intermediate-restart-worked.json", runtime / "migration-loaded.json"),
+ continued_operation=operation(first, continued, 700))
+ if fingerprint(fixture / WORLD) != json.loads((root / "progress-trace.json").read_text())["source_hashes"]:
+ raise AssertionError("Trace source fingerprint changed")
+ legacy, target = report["controls"]["legacy112"], report["controls"]["target113"]
+ reproduced = legacy["first_tick"]["first_tick_progress"] < legacy["first_tick"]["loaded_progress"]
+ report["status"] = "REPRODUCED_LEGACY_FAILURE_TARGET_PASS" if reproduced and target["first_tick"]["status"] == "PASS" else "FAIL"
+ if any(c[gate]["status"] != "PASS" for c in report["controls"].values() for gate in ("preservation", "continued_operation")):
+ report["status"] = "FAIL"
+ return report
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", required=True, type=Path)
+ parser.add_argument("--fixture", required=True, type=Path)
+ parser.add_argument("--output", required=True, type=Path)
+ args = parser.parse_args()
+ result = audit(args.run, args.fixture)
+ write_json(args.output, result)
+ print(result["status"])
+ raise SystemExit(result["status"] == "FAIL")
diff --git a/test-rig/migration/audit_dimensions.py b/test-rig/migration/audit_dimensions.py
new file mode 100644
index 00000000..055959ee
--- /dev/null
+++ b/test-rig/migration/audit_dimensions.py
@@ -0,0 +1,104 @@
+"""Independently replay raw dimension snapshots, item NBT and lifecycle evidence."""
+
+import argparse
+import json
+from pathlib import Path
+
+from actors import backup_state, freeze_result
+from dimensions import WORLD, capture, runtime_errors, strict_expected, survival_expected, tick_result, verdict
+from matrix import digest, fingerprint, write_json
+
+
+def audit(root):
+ published = json.loads((root / "dimension-results.json").read_text())
+ scope = json.loads((root / "scope.json").read_text())
+ native = scope.get("native_load", False)
+ findings = []
+ replayed = {}
+ for route, results in published.items():
+ district, label = route.split("/")
+ name = label.rsplit("-", 1)[0]
+ source = root / district / name
+ target = root / district / (label + "-113")
+ baseline = capture(source, "source", source / "dimension-evidence/source")
+ if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()):
+ findings.append("Generated source fixture changed: " + route)
+ runtimes = [source, target]
+ stages = {}
+ if label.endswith("-chained"):
+ middle = root / district / (label + "-112")
+ runtimes.append(middle)
+ for phase in ("intermediate", "intermediate-restart"):
+ state = capture(middle, phase, middle / "dimension-evidence" / phase)
+ stages[phase] = verdict(survival_expected(baseline) if native else strict_expected(baseline), state)
+ if native:
+ stages[phase + "-ticks"] = tick_result(json.loads((middle / f"dimensions-{phase}-release.json").read_text()),
+ json.loads((middle / f"dimensions-{phase}.json").read_text()))
+ else:
+ stages[phase + "-frozen"] = freeze_result(json.loads((middle / f"dimensions-{phase}.json").read_text()))
+ states = {phase: capture(target, phase, target / "dimension-evidence" / phase)
+ for phase in ("converted", "adapter", "restarted", "runtime", "runtime-restart")}
+ expectation = survival_expected(baseline) if native else strict_expected(baseline, adapter=False)
+ expectation.pop("adapter", None)
+ expectation.pop("playerWorldDimension", None)
+ stages["conversion"] = verdict(expectation, states["converted"])
+ stages["adapter"] = verdict(survival_expected(baseline) if native else strict_expected(baseline), states["adapter"])
+ stages["restart"] = verdict(survival_expected(baseline) if native else strict_expected(baseline), states["restarted"])
+ stages["idempotence"] = verdict(survival_expected(states["adapter"]) if native else strict_expected(states["adapter"]), states["restarted"])
+ stages["runtime"] = verdict(survival_expected(baseline), states["runtime"])
+ stages["runtime-restart"] = verdict(survival_expected(baseline), states["runtime-restart"])
+ if native:
+ stages["conversion-ticks"] = tick_result(json.loads((target / "dimensions-conversion-release.json").read_text()),
+ json.loads((target / "dimensions-converted.json").read_text()))
+ else:
+ for key, phase in (("conversion-frozen", "converted"), ("restart-frozen", "restarted")):
+ stages[key] = freeze_result(json.loads((target / f"dimensions-{phase}.json").read_text()))
+ for key, start, end in (("runtime-ticks", "released", "runtime"),
+ ("restart-ticks", "restart-released", "runtime-restart")):
+ stages[key] = tick_result(json.loads((target / f"dimensions-{start}.json").read_text()),
+ json.loads((target / f"dimensions-{end}.json").read_text()))
+ for key, value in stages.items():
+ if value != results[key]:
+ findings.append("Oracle replay disagrees: " + route + "/" + key)
+ predecessor = runtimes[-1] if label.endswith("-chained") else source
+ player_uuid = json.loads((source / "dimension-fixture.json").read_text())["playerUuid"]
+ affected_files = [Path("playerdata") / (player_uuid + ".dat")]
+ if "level.dat.poweradvantage-legacy-backup" in results["backup"]["files"]:
+ affected_files.append(Path("level.dat"))
+ for relative in affected_files:
+ backup = target / WORLD / relative.with_name(relative.name + ".poweradvantage-legacy-backup")
+ if not backup.is_file() or digest(backup) != digest(predecessor / WORLD / relative):
+ findings.append("Backup does not match predecessor bytes: " + route + "/" + str(relative))
+ if backup_state(target) != results["backup"]["files"]:
+ findings.append("Backups changed: " + route)
+ diagnostics = []
+ for runtime in runtimes:
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]:
+ findings.append("Staged artifact changed: " + route + "/" + jar["file"])
+ for log in runtime.glob("console-*.log"):
+ content = log.read_text(errors="replace")
+ if "ADVDIMENSIONS COMPLETE" not in content or "Stopping server" not in content:
+ findings.append("Incomplete server phase: " + str(log))
+ findings.extend("Native runtime failure: " + str(log) + ": " + line for line in runtime_errors(content))
+ diagnostics.extend({"log": str(log.relative_to(root)), "line": line}
+ for line in content.splitlines() if "/ERROR]" in line or "/FATAL]" in line
+ or "Exception loading entity" in line)
+ replayed[route] = {"stages": stages, "diagnostics": diagnostics}
+ if scope["routes"] != len(published) or scope["immutable_sources"] != "PASS":
+ findings.append("Missing route/source preservation evidence")
+ if "expected_route_ids" in scope and set(scope["expected_route_ids"]) != set(published):
+ findings.append("Route coverage differs from requested matrix")
+ return {"status": "FAIL" if findings else "PASS", "meaning": "Evidence integrity, not client acceptance",
+ "findings": findings, "routes": replayed}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(result["status"] + ": " + str(len(result["routes"])) + " routes; " + str(result["findings"]))
+ raise SystemExit(result["status"] != "PASS")
diff --git a/test-rig/migration/audit_drill_restart.py b/test-rig/migration/audit_drill_restart.py
new file mode 100644
index 00000000..e7b8276c
--- /dev/null
+++ b/test-rig/migration/audit_drill_restart.py
@@ -0,0 +1,85 @@
+"""Reconcile native drill restart evidence and keep the original failures visible."""
+import argparse
+import json
+from pathlib import Path
+import re
+
+from connected import entries, operation, read, state
+from drill_restart import first_tick
+from matrix import WORLD, compare, digest, fingerprint, write_json
+
+
+def audit(root, red=False):
+ raw = json.loads((root / "drill-restart-results.json").read_text())
+ expected = {"110", "112"} if red else {"110", "112", "113"}
+ if set(raw["targets"]) != expected or raw["status"] != ("FAIL" if red else "PASS"):
+ raise AssertionError("Incomplete campaign or incorrect overall verdict")
+ profiles = json.loads((root / "profiles.json").read_text())
+ for profile in profiles.values():
+ for jar in profile["jars"]:
+ if digest(Path(jar["path"])) != jar["sha256"]:
+ raise AssertionError("Archived jar changed")
+ result = dict(targets={})
+ for name, target in raw["targets"].items():
+ runtime = root / name
+ source = Path(target["source"])
+ if fingerprint(source / WORLD) != target["source_hashes"]:
+ raise AssertionError("Source fixture changed")
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]:
+ raise AssertionError("Staged jar changed")
+ if (runtime / "connected-build.allow").exists() or (runtime / "connected-built.json").exists():
+ raise AssertionError("Native test had construction authority")
+ for path in runtime.glob("connected-*.json"):
+ if path.name == "connected-fixture.json":
+ continue
+ snapshot = json.loads(path.read_text())
+ if snapshot["buildCallsThisProcess"] != 0 or state(snapshot)["status"] != "PASS":
+ raise AssertionError("Native snapshot lost items or rebuilt equipment")
+ counterpart = runtime / path.name.replace("connected-", "migration-", 1)
+ if snapshot["snapshot"] != json.loads(counterpart.read_text()):
+ raise AssertionError("Raw NBT and semantic records disagree")
+ for path in runtime.glob("console-*.commands.json"):
+ commands = json.loads(path.read_text())
+ for command in commands:
+ if command not in ("save-all flush", "stop") and not re.fullmatch(
+ r"advconnected (snapshot (loaded-source|ready-[01]|restart-[12]-loaded)|step (7 ready-[01]-align-\d+|1 restart-[12]-first|620 restart-[12]-worked))", command):
+ raise AssertionError("Unexpected mutation command: " + command)
+ log = path.with_name(path.name.replace(".commands.json", ".log")).read_text(encoding="utf-8")
+ if "Done (" not in log or "Stopping server" not in log or "Connected probe failed" in log:
+ raise AssertionError("Native process did not start/shut down cleanly")
+ initial = compare(source / "migration-source.json", runtime / "migration-loaded-source.json")
+ if initial["status"] != "PASS":
+ raise AssertionError("Initial imported state differs")
+ phases = {}
+ for number in (1, 2):
+ label = "restart-" + str(number)
+ loaded, first, worked = (read(runtime, label + suffix) for suffix in ("-loaded", "-first", "-worked"))
+ preserved = compare(runtime / f"migration-ready-{number - 1}.json", runtime / ("migration-" + label + "-loaded.json"))
+ if preserved["status"] != "PASS":
+ raise AssertionError("Saved checkpoint changed before ticking")
+ live = operation(first, worked, 620)
+ if live["status"] != "PASS":
+ raise AssertionError("Continued native operation failed: " + str(live))
+ tick = first_tick(loaded, first)
+ if red:
+ if tick["status"] != "FAIL" or len(tick["failures"]) != 2 or any(
+ tick["progress"][station]["after"] >= tick["progress"][station]["before"] for station in ("C-SDRILL", "C-EDRILL")):
+ raise AssertionError("Expected both original resets to be reproduced")
+ elif tick["status"] != "PASS":
+ raise AssertionError("Repaired first tick failed: " + str(tick))
+ phases[label] = dict(preservation=preserved, first_tick=tick, operation=live)
+ result["targets"][name] = dict(phases=phases)
+ result["status"] = "REPRODUCED_FOUR_LEGACY_FAILURES" if red else "PASS"
+ return result
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--red", action="store_true")
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run, args.red)
+ write_json(args.output, result)
+ print(result["status"])
diff --git a/test-rig/migration/audit_endurance.py b/test-rig/migration/audit_endurance.py
new file mode 100644
index 00000000..870b68fd
--- /dev/null
+++ b/test-rig/migration/audit_endurance.py
@@ -0,0 +1,65 @@
+"""Independently check native-feed endurance reports, commands and staged jars."""
+
+import argparse
+import json
+from pathlib import Path
+
+from matrix import digest, write_json
+
+
+def expected_inventory_acknowledgements(batches):
+ # Five machine inventories, four neighbouring chests, seven destinations,
+ # four source chests; initial conveyor clearing also preserves filter slots.
+ ordinary = 7 + 6 + 10 + 9 + 2 + 4 * 27 + 7 * 27 + 4 * 27
+ return batches * ordinary + 9, ordinary
+
+
+def audit(root):
+ report = json.loads((root / "endurance-results.json").read_text())
+ assert report["status"] == "PASS", "Not a completed endurance pass"
+ assert report["duration_seconds"] >= 1800 and report["interval_seconds"] == 60
+ assert report.get("feeding_protocol", "").startswith("IInventory replaceitem"), "NBT-fed runs are diagnostics only"
+ assert report["source_fixture_unchanged"] is True
+ result = {"status": "PASS", "source_report": str(root / "endurance-results.json"), "profiles": {}}
+ errors = ("Unknown slot", "Could not replace", "Unknown or incomplete command", "Incorrect argument",
+ "Unknown item", "There is no container")
+ for name, profile in report["profiles"].items():
+ assert profile["status"] == "PASS" and profile["restart"] == "PASS" and profile["resumed_processing"] == "PASS"
+ assert profile["elapsed_seconds"] >= report["duration_seconds"]
+ assert profile["game_ticks"] >= report["duration_seconds"] * 18
+ runtime = root / name
+ initial, restarted = expected_inventory_acknowledgements(profile["endurance_batches"])
+ counts = {}
+ for filename, expected in (("console-endurance.log", initial), ("console-endurance-restart.log", restarted)):
+ content = (runtime / filename).read_text(encoding="utf-8")
+ assert not any(error in content for error in errors), f"Failed commands in {name}/{filename}"
+ assert content.count("Replaced a slot at") == expected, f"Missing inventory writes in {name}/{filename}"
+ if "restart" in filename:
+ assert not any(f"Started {station} " in content for station in ("S-04", "E-03", "E-02", "S-02", "E-01")), \
+ "Power stations were restarted instead of testing loaded networks"
+ assert "Advantage Works built" not in content and "Reset 1 station" not in content
+ counts[filename] = expected
+ pins = {artifact["file"]: artifact["sha256"] for artifact in report["artifacts"]}
+ jars = sorted((runtime / "mods").glob("*.jar"))
+ assert {jar.name for jar in jars} == set(pins), f"Staged jar identities changed in {name}"
+ assert all(digest(jar) == pins[jar.name] for jar in jars), f"Staged jar hashes changed in {name}"
+ ledger = json.loads((runtime / "endurance-ledger.json").read_text())
+ batches = [entry for entry in ledger if entry["label"].startswith("cycle-")]
+ expected_batches = report["duration_seconds"] // report["interval_seconds"]
+ assert len(batches) == profile["endurance_batches"] == expected_batches
+ assert {entry["label"] for entry in batches} == {f"cycle-{index:03}" for index in range(1, expected_batches + 1)}
+ assert [entry["label"] for entry in ledger[-3:]] == ["before-restart", "after-restart", "restart-resumed"]
+ result["profiles"][name] = {"status": "PASS", "verified_inventory_commands": counts,
+ "staged_jars": len(jars), "distinct_endurance_batches": len(batches)}
+ assert set(result["profiles"]) == {"fresh113", "original110-chained113"}
+ return result
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(json.dumps(result, indent=2))
diff --git a/test-rig/migration/audit_extended_lifecycle.py b/test-rig/migration/audit_extended_lifecycle.py
new file mode 100644
index 00000000..e7f00a73
--- /dev/null
+++ b/test-rig/migration/audit_extended_lifecycle.py
@@ -0,0 +1,91 @@
+"""Recompute partial-network and extended-drill verdicts from raw server evidence."""
+import argparse
+import json
+from pathlib import Path
+
+from endurance import require
+from extended_lifecycle import (fixtures, movement_results, order_results, output_results,
+ partial_result, qualification, redstone_results, reserve_results, restart_results)
+from lifecycle import at, counts, parse
+from matrix import digest, write_json
+
+
+def audit(root):
+ report = json.loads((root / "extended-results.json").read_text())
+ require(report.get("source_fixture_unchanged") is True, "Source protection missing")
+ require(json.loads((root / "source-fingerprint-before.json").read_text())
+ == json.loads((root / "source-fingerprint-after.json").read_text()), "Fixture fingerprints changed")
+ require(set(report["profiles"]) == {"fresh113", "original110-chained113"}, "Incomplete profile coverage")
+ result = {"status": "PASS", "qualification": "PASS", "profiles": {}}
+ definition = json.loads(json.dumps(fixtures()))
+ for name, summary in report["profiles"].items():
+ runtime = root / name
+ require(json.loads((runtime / "lifecycle-fixture.json").read_text()) == definition, "Fixture geometry differs")
+ def snapshot(label):
+ return json.loads((runtime / "advantage-works-results" / f"lifecycle-{label}.json").read_text())
+ inputs = json.loads((runtime / "input-jars.json").read_text())
+ require(len(inputs) == 6, "Unexpected jar count")
+ for jar in inputs: require(digest(runtime / "mods" / jar["file"]) == jar["sha256"], "Staged jar changed")
+ require({p["file"]: p["sha256"] for p in inputs} == {p["file"]: p["sha256"] for p in report["artifacts"]}, "Report pins differ")
+ computed = {"insufficient_energy": reserve_results(definition, snapshot("low-before"), snapshot("low-stalled")),
+ "moves": movement_results(definition, snapshot("step-4-complete")),
+ "blocked_output": output_results(definition, snapshot("blocked-output"), True),
+ "restart": restart_results(definition, snapshot("blocked-output"), snapshot("blocked-retained")),
+ "blocked_after_restart": output_results(definition, snapshot("blocked-retained"), True),
+ "resumed_output": output_results(definition, snapshot("freed-output"), False),
+ "redstone": redstone_results(definition, snapshot("redstone-before"), snapshot("redstone-held"), snapshot("redstone-resumed")),
+ "network_processing": "PASS", "partial_networks": [], "reload_orders": []}
+ for cycle, group in enumerate(("machine", "supply", "supply", "machine"), 1):
+ label = f"partial-{cycle}-{group}"
+ samples = [snapshot(f"{label}-sample-{n}") for n in range(8)]
+ confirmed = snapshot(label + "-confirmed"); confirmed["confirmed"] = True; samples.append(confirmed)
+ computed["partial_networks"].append({"label": label, "group": group,
+ "cases": partial_result(definition, snapshot(label+"-before"), samples,
+ snapshot(label+"-loaded"), group)})
+ for order in (("machine", "supply"), ("supply", "machine")):
+ label = "order-" + "-".join(order)
+ computed["reload_orders"].append({"label": label, "order": list(order),
+ "cases": order_results(definition, snapshot(label+"-before"), snapshot(label+"-absent"),
+ snapshot(label+"-first"), snapshot(label+"-settled"),
+ snapshot(label+"-loaded"), order)})
+ for key, value in computed.items(): require(value == summary[key], "Verdict differs from raw evidence: " + key)
+ for label, fluid in [("network-commissioned", 1000)] + [(f"partial-resumed-{n}", (n+1)*1000) for n in range(1,5)] + [
+ ("order-machine-supply-resumed", 5000), ("order-supply-machine-resumed", 5000)]:
+ document = snapshot(label)
+ for f in definition:
+ if f["id"] in ("C-STEAM", "C-ELECTRIC"):
+ machine = at(document, f["id"], f["witnesses"][4])
+ items = counts([machine])
+ require(items["minecraft:iron_ingot"] == 4 and items["minecraft:iron_ore"] == 0
+ and sum(machine["energy"].values()) > 0, "Processing did not resume: " + label)
+ elif f["id"] == "C-FLUID":
+ tank = parse(at(document, f["id"], f["witnesses"][4])).get("Tank", {})
+ require(tank.get("FluidName") == "minecraft:water" and tank.get("Amount") == fluid, "Fluid conservation failure")
+ checked = qualification(computed)
+ require(checked == summary["status"], "Profile status disagrees with raw verdicts")
+ logs = [p.read_text(encoding="utf-8") for p in runtime.glob("console-*.log")]
+ require(len(logs) == 2, "Full process restart missing")
+ require(all("Stopping server" in log for log in logs), "Server shutdown evidence missing")
+ require(not any("Unknown item slot" in log or "Could not replace slot" in log or "Ticking block entity" in log
+ or "Encountered an unexpected exception" in log for log in logs), "Command failure or server crash")
+ end = snapshot("redstone-resumed")
+ events = end.get("chunkEvents", [])
+ result["profiles"][name] = {"evidence_audit": "PASS", "machine_qualification": checked,
+ "native_moves": len(snapshot("step-4-complete")["movements"]),
+ "real_unloads_before_restart": sum(e["operation"] == "unload" for e in snapshot("blocked-output")["chunkEvents"]),
+ "partial_cases": sum(len(e["cases"]) for e in computed["partial_networks"]),
+ "reload_order_cases": sum(len(e["cases"]) for e in computed["reload_orders"]),
+ "staged_jars": len(inputs)}
+ if checked != "PASS": result["qualification"] = "FAIL"
+ require(report["status"] == result["qualification"], "Overall status disagrees")
+ return result
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(json.dumps(result, indent=2))
diff --git a/test-rig/migration/audit_lifecycle.py b/test-rig/migration/audit_lifecycle.py
new file mode 100644
index 00000000..7618a96a
--- /dev/null
+++ b/test-rig/migration/audit_lifecycle.py
@@ -0,0 +1,82 @@
+"""Independently reconcile lifecycle results with staged jars and raw snapshots."""
+import argparse
+import json
+from pathlib import Path
+from lifecycle import FACINGS, LifecycleRun, at, changed_target_results, fixtures, move_result, offset, parse, progress_results, require_unloaded
+from endurance import require
+from matrix import digest, write_json
+
+
+def audit(root):
+ report = json.loads((root / "lifecycle-results.json").read_text())
+ require(report.get("source_fixture_unchanged") is True, "Source protection not verified")
+ require(set(report["profiles"]) == {"fresh113", "original110-chained113"}, "Incomplete profile coverage")
+ result = {"status": "PASS", "qualification": "PASS", "profiles": {}}
+ for name, summary in report["profiles"].items():
+ require(summary.get("chunk_cycles") == 5 and summary.get("full_restart") == "PASS", "Incomplete lifecycle run")
+ runtime = root / name
+ definition = json.loads((runtime / "lifecycle-fixture.json").read_text())
+ require(definition == json.loads(json.dumps(fixtures())), "Unexpected fixture geometry")
+ def snapshot(label):
+ return json.loads((runtime / "advantage-works-results" / f"lifecycle-{label}.json").read_text())
+ inputs = json.loads((runtime / "input-jars.json").read_text())
+ require(len(inputs) == 6, "Unexpected mod count")
+ for jar in inputs:
+ require(digest(runtime / "mods" / jar["file"]) == jar["sha256"], "Staged jar changed")
+ require({e["file"]: e["sha256"] for e in inputs} == {e["file"]: e["sha256"] for e in report["artifacts"]}, "Report pins differ")
+ movement_records = {m["id"]: m for m in snapshot("commissioned")["movements"]}
+ recomputed = {f["id"]: move_result(f, movement_records[f["id"]]) for f in definition if "from" in f}
+ require(recomputed == summary["move_cases"], "Movement verdict differs from raw evidence")
+ verdicts = [recomputed]
+ chunk_events = 0
+ for label in [f"chunk-cycle-{n}" for n in range(1, 4)] + ["mining-chunk-reload", "midblock-chunk"]:
+ before, absent, loaded = snapshot(label + "-before"), snapshot(label + "-absent-confirmed"), snapshot(label + "-loaded")
+ require_unloaded(before, absent)
+ chunk_events += len(absent["chunks"])
+ for a, b in zip(before["chunks"], loaded["chunks"]):
+ require(a["pos"] == b["pos"] and b["loaded"] and b["loads"] > a["loads"], "No reload event")
+ runner = LifecycleRun(runtime, None, None, definition)
+ for label, key in (("restart-resumed", "mining"), ("mining-retained", "mining_after_chunk_reload")):
+ mining = runner.mining_results(snapshot(label))
+ require(mining == summary[key], "Mining result mismatch")
+ verdicts.append(mining)
+ for label in ("restart-resumed", "mining-retained"):
+ document = snapshot(label)
+ for fixture in definition:
+ if fixture.get("energyType") != "steam": continue
+ helper = at(document, fixture["id"], offset(fixture["to"], fixture["direction"]))
+ require(helper["block"] == "steamadvantage:drillbit" and parse(helper).get("dir") == list(FACINGS).index(fixture["facing"]),
+ "Steam drill-bit orientation was not restored")
+ for route, first, second in (("chunk_reload", "midblock-before-chunk", "midblock-after-chunk"),
+ ("full_restart", "midblock-before-restart", "midblock-after-restart")):
+ progress = progress_results(definition, snapshot(first), snapshot(second))
+ require(progress == summary["mid_block_progress"][route],
+ "Progress verdict differs from raw evidence")
+ verdicts.append(progress)
+ if "save_read_guards" in summary:
+ legacy = progress_results(definition, snapshot("save-read-before"), snapshot("save-read-legacy"))
+ changed = changed_target_results(definition, snapshot("save-read-legacy"), snapshot("save-read-changed"))
+ require(legacy == summary["save_read_guards"]["legacy_read"], "Legacy-read verdict differs from raw evidence")
+ require(changed == summary["save_read_guards"]["changed_target_read"], "Changed-target verdict differs from raw evidence")
+ verdicts.extend((legacy, changed))
+ qualification = "PASS" if all(case["status"] == "PASS" for group in verdicts for case in group.values()) else "FAIL"
+ require(summary["status"] == qualification, "Profile qualification label differs from raw case verdicts")
+ logs = [p.read_text(encoding="utf-8") for p in runtime.glob("console-*.log")]
+ require(not any("Unknown item slot" in log or "Could not replace slot" in log or "Ticking block entity" in log for log in logs),
+ "Command error or machine crash")
+ result["profiles"][name] = {"evidence_audit": "PASS", "machine_qualification": qualification,
+ "drill_moves": len(recomputed), "real_chunk_unloads": chunk_events, "staged_jars": len(inputs),
+ "steam_helper_orientations": 6}
+ if qualification != "PASS": result["qualification"] = "FAIL"
+ require(report["status"] == result["qualification"], "Overall qualification label is inconsistent")
+ return result
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(json.dumps(result, indent=2))
diff --git a/test-rig/migration/audit_pipe_only.py b/test-rig/migration/audit_pipe_only.py
new file mode 100644
index 00000000..6a0586ee
--- /dev/null
+++ b/test-rig/migration/audit_pipe_only.py
@@ -0,0 +1,84 @@
+"""Reconstruct pipe-only bridge verdicts from raw snapshots and packaged inputs."""
+import argparse
+import json
+from pathlib import Path
+
+from lifecycle import at, counts, parse
+from matrix import digest, write_json
+from pipe_only_lifecycle import fixtures
+
+
+def bridge_result(definition, before, absent, confirmed, loaded, resumed, cycle):
+ errors = []
+ if confirmed["worldTime"] - absent["worldTime"] < 40:
+ errors.append("absence was shorter than forty game ticks")
+ for f in definition:
+ bridge = f["groups"]["bridge"][0]
+ old = next(c for c in before["chunks"] if c["pos"] == bridge)
+ for snapshot in (absent, confirmed):
+ state = next(c for c in snapshot["chunks"] if c["pos"] == bridge)
+ if state["loaded"] or state["unloads"] <= old["unloads"]:
+ errors.append(f["id"] + ": bridge was not genuinely absent")
+ state = next(c for c in loaded["chunks"] if c["pos"] == bridge)
+ if not state["loaded"] or state["loads"] <= old["loads"]:
+ errors.append(f["id"] + ": missing deliberate reload event")
+ for pos in f["witnesses"][:2]:
+ original = at(before, f["id"], pos)
+ for snapshot in (absent, confirmed, resumed):
+ endpoint = at(snapshot, f["id"], pos)
+ if endpoint.get("tileIdentity") != original.get("tileIdentity"):
+ errors.append(f["id"] + ": retained endpoint replaced")
+ if endpoint.get("block") != original.get("block") or endpoint.get("state") != original.get("state"):
+ errors.append(f["id"] + ": retained endpoint changed")
+ for pos in f["witnesses"]:
+ original, restored = at(before, f["id"], pos), at(resumed, f["id"], pos)
+ if restored.get("block") != original.get("block") or restored.get("state") != original.get("state"):
+ errors.append(f["id"] + ": bridge block/state changed")
+ endpoint = at(resumed, f["id"], f["witnesses"][1])
+ if f["id"] == "C-FLUID":
+ if parse(endpoint).get("Tank", {}).get("Amount") != (cycle + 1) * 1000:
+ errors.append("fluid loss or duplication")
+ for snapshot in (absent, confirmed):
+ if parse(at(snapshot, f["id"], f["witnesses"][1])).get("Tank") != parse(at(before, f["id"], f["witnesses"][1])).get("Tank"):
+ errors.append("fluid changed while disconnected")
+ elif counts([endpoint])["minecraft:iron_ingot"] != 4 or counts([endpoint])["minecraft:iron_ore"] != 0:
+ errors.append(f["id"] + ": processing did not resume")
+ return {"status": "FAIL" if errors else "PASS", "failures": sorted(set(errors))}
+
+
+def audit(root):
+ definition = fixtures()
+ result = {"status": "PASS", "cycles": [], "failures": []}
+ if json.loads((root / "lifecycle-fixture.json").read_text()) != definition:
+ result["failures"].append("fixture geometry differs")
+ for cycle in range(3):
+ def read(label):
+ return json.loads((root / "advantage-works-results" / f"lifecycle-bridge-{cycle}-{label}.json").read_text())
+ result["cycles"].append(bridge_result(definition, *(read(label) for label in
+ ("before", "isolated", "confirmed", "loaded", "resumed")), cycle))
+ jars = json.loads((root / "input-jars.json").read_text())
+ if len(jars) != 6 or any(digest(root / "mods" / jar["file"]) != jar["sha256"] for jar in jars):
+ result["failures"].append("staged artifact hashes differ")
+ log = (root / "console-pipe-only.log").read_text(encoding="utf-8")
+ for cycle in range(3):
+ for operation, label in (("isolated", "isolated"), ("offer-blocked", "blocked-offer"), ("offer", "reconnected-offer")):
+ if f"ADVLIFECYCLE COMPLETE {operation} bridge-{cycle}-{label}" not in log:
+ result["failures"].append("missing successful real network assertion")
+ if "Stopping server" not in log or "Saving chunks for level" not in log:
+ result["failures"].append("missing graceful shutdown")
+ result["log_errors"] = [line for line in log.splitlines() if "/ERROR]" in line or "/FATAL]" in line]
+ if "/ERROR]" in log or "Can't keep up!" in log:
+ result["failures"].append("server errors or timing warnings")
+ if result["failures"] or any(c["status"] != "PASS" for c in result["cycles"]): result["status"] = "FAIL"
+ return result
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(json.dumps(result, indent=2))
+ if result["status"] != "PASS": raise SystemExit(1)
diff --git a/test-rig/migration/audit_player_layout.py b/test-rig/migration/audit_player_layout.py
new file mode 100644
index 00000000..8992ed49
--- /dev/null
+++ b/test-rig/migration/audit_player_layout.py
@@ -0,0 +1,46 @@
+"""Replay bag-slot and nested-item player fixtures from retained binary NBT."""
+import argparse
+import json
+from pathlib import Path
+
+from actors import WORLD, capture
+from matrix import digest, fingerprint, write_json
+from player_layout import player_verdict
+
+
+def audit(root):
+ published = json.loads((root / "layout-results.json").read_text())
+ issues, routes = [], {}
+ for label, verdict in published.items():
+ source = root / label.rsplit("-", 1)[0]
+ target = root / (label + "-113")
+ baseline = capture(source, "source", source / "actor-evidence/source")
+ if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()): issues.append("Source fixture changed: " + label)
+ slots = [entry["Slot"] for entry in baseline["file"]["Inventory"]]
+ ender_slots = [entry["Slot"] for entry in baseline["file"]["EnderItems"]]
+ if not all(slot in slots for slot in (9, 18, 27, 35, -106, 100, 101, 102, 103)): issues.append("Missing inventory boundary: " + label)
+ if 26 not in ender_slots: issues.append("Missing last Ender Chest slot: " + label)
+ musket = next(s for s in baseline["file"]["Inventory"] if s["id"] == "steamadvantage:musket")
+ if len(musket["tag"]["Nested"]["Items"]) != 3: issues.append("Missing recursive item witnesses: " + label)
+ replayed = {phase: player_verdict(baseline, capture(target, phase, target / "actor-evidence" / phase))
+ for phase in ("converted", "restarted")}
+ if "intermediate" in verdict:
+ middle = root / (label + "-112")
+ replayed["intermediate"] = player_verdict(baseline, capture(middle, "intermediate", middle / "actor-evidence/intermediate"))
+ if replayed != verdict: issues.append("Raw layout replay disagrees: " + label)
+ for runtime in [source, target] + ([root / (label + "-112")] if "intermediate" in verdict else []):
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]: issues.append("Staged artifact changed: " + label)
+ routes[label] = replayed
+ return {"status": "FAIL" if issues else "PASS", "findings": issues, "routes": routes}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(result["status"] + ": " + str(len(result["routes"])) + " routes; " + str(result["findings"]))
+ raise SystemExit(result["status"] != "PASS")
diff --git a/test-rig/migration/audit_respawn.py b/test-rig/migration/audit_respawn.py
new file mode 100644
index 00000000..cf642ee0
--- /dev/null
+++ b/test-rig/migration/audit_respawn.py
@@ -0,0 +1,71 @@
+"""Replay native death/respawn NBT and the actual persisted player files."""
+
+import argparse
+import json
+from pathlib import Path
+
+import nbtlib
+
+from actors import enchantments, semantic
+from dimensions import PLAYER_STATE, exact_item_differences, runtime_errors
+from matrix import differences, digest, fingerprint, parse_nbt, write_json
+from respawn import respawn_verdict, spawn_reference
+
+
+def audit(root):
+ published = json.loads((root / "respawn-results.json").read_text())
+ findings = []
+ results = {}
+ audited = set()
+ for label, expected in published.items():
+ district, profile, case = label.split("/")
+ runtime = root / district / profile
+ record = json.loads((runtime / f"dimensions-{case}.json").read_text())
+ registry = enchantments(runtime)
+ actual = respawn_verdict(record, registry, spawn_reference(runtime / "respawn-evidence" / case / "level.dat"))
+ saved = semantic(nbtlib.load(runtime / "respawn-evidence" / case / "player.dat").unpack(), PLAYER_STATE, registry)
+ after = semantic(parse_nbt(record["respawn"]["after"]), PLAYER_STATE, registry)
+ saved_issues = differences(after, saved, "saved-player")
+ saved_issues.extend(exact_item_differences(after, saved, "saved-player"))
+ actual["differences"].extend(saved_issues)
+ if saved_issues:
+ actual["status"] = "FAIL"
+ if actual != expected:
+ findings.append("Oracle replay disagrees: " + label)
+ results[label] = actual
+ if runtime not in audited:
+ audited.add(runtime)
+ for jar in json.loads((runtime / "input-jars.json").read_text()):
+ if digest(runtime / "mods" / jar["file"]) != jar["sha256"]:
+ findings.append("Staged artifact changed: " + str(runtime) + "/" + jar["file"])
+ log = (runtime / "console-respawn.log").read_text(errors="replace")
+ if "Stopping server" not in log:
+ findings.append("Incomplete shutdown: " + str(runtime))
+ findings.extend("Native runtime failure: " + str(runtime) + ": " + line for line in runtime_errors(log))
+ for mode in ("keep", "drop"):
+ for bed in ("forced", "bed", "missing"):
+ if "ADVDIMENSIONS COMPLETE " + mode + "-" + bed not in log:
+ findings.append("Missing case completion: " + str(runtime) + "/" + mode + "-" + bed)
+ scope = json.loads((root / "scope.json").read_text())
+ if scope["cases"] != len(published) or scope["source_fixtures"] != "UNCHANGED":
+ findings.append("Missing case/source preservation evidence")
+ if "expected_case_ids" in scope and set(scope["expected_case_ids"]) != set(published):
+ findings.append("Case coverage differs from requested matrix")
+ sources = root / "source-fingerprints.json"
+ if sources.is_file():
+ for label, source in json.loads(sources.read_text()).items():
+ if fingerprint(Path(source["path"])) != source["hashes"]:
+ findings.append("Source fixture changed: " + label)
+ return {"status": "FAIL" if findings else "PASS", "meaning": "Evidence integrity, not client death-screen acceptance",
+ "findings": findings, "cases": results}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run)
+ write_json(args.output, result)
+ print(result["status"] + ": " + str(len(result["cases"])) + " cases; " + str(result["findings"]))
+ raise SystemExit(result["status"] != "PASS")
diff --git a/test-rig/migration/audit_results.py b/test-rig/migration/audit_results.py
new file mode 100644
index 00000000..d4d1e05b
--- /dev/null
+++ b/test-rig/migration/audit_results.py
@@ -0,0 +1,53 @@
+"""Collect existing matrix evidence without launching or changing any world."""
+
+import argparse
+import json
+from pathlib import Path
+from matrix import WORLD, digest, fingerprint, write_json
+
+
+def audit(args):
+ report = {"routes": {}, "immutable_fixtures": [], "staged_jar_manifests": 0,
+ "operation": {}, "runtime_log_findings": []}
+ for root in args.results:
+ results = json.loads((root / "matrix-results.json").read_text())
+ for label, stages in results.items():
+ for stage, result in stages.items():
+ if result["status"] != "PASS": raise AssertionError(f"Failed {root.name}/{label}/{stage}")
+ # The first local run predated identification of the actual Cyano artifacts.
+ corrected = label.replace("original110", "early110") if root.name == "full-20261005-r3" else label
+ if corrected in report["routes"]: raise AssertionError(f"Duplicate route: {corrected}")
+ report["routes"][corrected] = {"source_report": str(root / "matrix-results.json"), "stages": stages}
+ for manifest in root.glob("*/input-jars.json"):
+ for entry in json.loads(manifest.read_text()):
+ staged = manifest.parent / "mods" / entry["file"]
+ if digest(staged) != entry["sha256"]: raise AssertionError(f"Staged jar changed: {staged}")
+ report["staged_jar_manifests"] += 1
+ for log in root.glob("*-113/console-*.log"):
+ errors = [line for line in log.read_text(encoding="utf-8").splitlines() if "/ERROR]" in line or "/FATAL]" in line]
+ if errors: report["runtime_log_findings"].append({"log": str(log), "errors": errors})
+ for root in args.fixtures:
+ for hashes in root.glob("*/fixture-hashes.json"):
+ expected = json.loads(hashes.read_text())
+ if fingerprint(hashes.parent / WORLD) != expected:
+ raise AssertionError(f"Immutable fixture changed: {hashes.parent}")
+ report["immutable_fixtures"].append({"path": str(hashes.parent), "files": len(expected), "status": "PASS"})
+ for phase in ("operation", "operation-restart"):
+ results = json.loads((args.operation / (phase + "-results.json")).read_text())
+ if not results or any(status not in ("PASS", "MANUAL_REQUIRED") for status in results.values()):
+ raise AssertionError(f"Operation assertion failed: {phase}")
+ report["operation"][phase] = results
+ report["status"] = "STATE_MATRIX_PASS_WITH_REMAINING_RELEASE_GATES"
+ write_json(args.output, report)
+ print(f"PASS: {len(report['routes'])} routes, {len(report['immutable_fixtures'])} unchanged source fixtures, "
+ f"{report['staged_jar_manifests']} verified staged-jar manifests")
+ print(f"Report: {args.output}")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--results", nargs="+", type=Path, required=True)
+ parser.add_argument("--fixtures", nargs="+", type=Path, required=True)
+ parser.add_argument("--operation", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ audit(parser.parse_args())
diff --git a/test-rig/migration/audit_transformer_order.py b/test-rig/migration/audit_transformer_order.py
new file mode 100644
index 00000000..9c7f8ecd
--- /dev/null
+++ b/test-rig/migration/audit_transformer_order.py
@@ -0,0 +1,107 @@
+"""Read-only replay of raw packaged transformer-order qualification evidence."""
+
+import argparse
+import json
+from pathlib import Path
+
+import nbtlib
+
+from actors import (PLAYER_KEYS, capture, compare as compare_actors, enchantments, freeze_result,
+ qualified_baselines, semantic)
+from matrix import WORLD, compare as compare_blocks, digest, fingerprint, write_json
+from transformer_order import (COREMODS, METAL_BLOCKS, assert_staged, block_semantics,
+ backup_provenance, migration_backups, order_evidence, strict_equal,
+ target_profile, validate_patch)
+
+
+def audit(root):
+ scope = json.loads((root / "scope.json").read_text())
+ profile = target_profile(Path(scope["profile_manifest"]))
+ if digest(Path(scope["profile_manifest"])) != scope["profile_manifest_sha256"]:
+ raise AssertionError("Archived input manifest changed")
+ patched = root / "coremods-order-test.jar"
+ patch = validate_patch(profile[0] / COREMODS, patched)
+ reported_build = json.loads((root / "probe-build.json").read_text())
+ if any(reported_build[key] != value for key, value in patch.items()):
+ raise AssertionError("Patched-library evidence changed")
+ sources = json.loads((root / "immutable-source-hashes.json").read_text())
+ for source in sources.values():
+ if fingerprint(Path(source["path"]) / WORLD) != source["hashes"]:
+ raise AssertionError("Immutable source world changed")
+ published = json.loads((root / "results.json").read_text())
+ results, states = {}, {}
+ for route, verdict in published.items():
+ name, mode = route.split("/")
+ fixture, runtime = root / (name + "-mixed-input"), root / (name + "-" + mode)
+ if fingerprint(fixture / WORLD) != json.loads((fixture / "fixture-hashes.json").read_text()):
+ raise AssertionError("Mixed input world changed")
+ assert_staged(runtime)
+ if digest(runtime / COREMODS) != digest(patched):
+ raise AssertionError("Staged test library changed")
+ # Reconstruct the oracle from source actor NBT and the actual mixed input file.
+ expected = capture(Path(sources[name]["path"]), "source")
+ player_uuid = json.loads((fixture / "actor-fixture.json").read_text())["playerUuid"]
+ expected["file"] = semantic(nbtlib.load(fixture / WORLD / "playerdata" / (player_uuid + ".dat")).unpack(),
+ PLAYER_KEYS, enchantments(fixture))
+ expected["embedded"] = expected["file"]
+ expected["adapter"] = expected["file"]
+ if expected != json.loads((fixture / "semantic-source.json").read_text()):
+ raise AssertionError("Saved semantic oracle disagrees with raw input")
+ converted = capture(runtime, "converted", runtime / "ordering-evidence/converted")
+ adapter = capture(runtime, "converted-adapter", runtime / "ordering-evidence/converted-adapter")
+ adapter.pop("embedded", None)
+ restarted = capture(runtime, "restarted", runtime / "ordering-evidence/restarted")
+ restarted.pop("embedded", None)
+ replayed = {"conversion": compare_actors({k: v for k, v in expected.items() if k != "adapter"}, converted),
+ "adapter": compare_actors({k: v for k, v in expected.items() if k != "embedded"}, adapter),
+ "restart": compare_actors({k: v for k, v in expected.items() if k != "embedded"}, restarted),
+ "idempotence": strict_equal(adapter, restarted),
+ "backup_provenance": backup_provenance(fixture, runtime)}
+ for label, control in (("converted", "converted_controls"), ("restarted", "restart_controls")):
+ proof = order_evidence(runtime, mode, label)
+ if proof != verdict[control]["order"]:
+ raise AssertionError("Replayed actual order differs from published order")
+ freeze = freeze_result(json.loads((runtime / f"actors-{label}.json").read_text()))
+ if freeze["status"] != "PASS" or freeze != verdict[control]["freeze"]:
+ raise AssertionError("Unproven zero-tick checkpoint")
+ replayed["blocks_" + label] = compare_blocks(qualified_baselines()[name] / "migration-source.json",
+ runtime / f"migration-stock-{label}.json")
+ full = json.loads((runtime / f"migration-{label}.json").read_text())
+ stock = json.loads((runtime / f"migration-stock-{label}.json").read_text())
+ if full[:-len(METAL_BLOCKS)] != stock:
+ raise AssertionError("Stock witness subset is not the original raw snapshot")
+ replayed["metals_" + label] = strict_equal(list(METAL_BLOCKS), [e["actualName"] for e in full[-len(METAL_BLOCKS):]])
+ adapter_freeze = freeze_result(json.loads((runtime / "actors-converted-adapter.json").read_text()))
+ if adapter_freeze["status"] != "PASS" or adapter_freeze != verdict["converted_controls"]["adapter_freeze"]:
+ raise AssertionError("Unproven adapter zero-tick checkpoint")
+ for key, value in replayed.items():
+ if value != verdict[key]:
+ raise AssertionError(f"Replay disagrees: {route}/{key}")
+ backups = json.loads((runtime / "ordering-backups.json").read_text())
+ if strict_equal(backups, migration_backups(runtime)) != verdict["backup_stability"]:
+ raise AssertionError("Backup-stability replay disagrees")
+ results[route] = replayed
+ states.setdefault(name, {})[mode] = {"actors": adapter, "actors_restart": restarted,
+ "blocks": block_semantics(runtime, "converted"),
+ "blocks_restart": block_semantics(runtime, "restarted")}
+ cross = {name: {mode: strict_equal(next(iter(modes.values())), state) for mode, state in modes.items()}
+ for name, modes in states.items()}
+ if cross != json.loads((root / "cross-order.json").read_text()):
+ raise AssertionError("Replayed cross-order semantics disagree")
+ semantic_pass = all(v["status"] == "PASS" for route in results.values() for v in route.values())
+ cross_pass = all(v["status"] == "PASS" for modes in cross.values() for v in modes.values())
+ return {"status": "PASS", "meaning": "Evidence integrity; semantic qualification is reported separately",
+ "semantic_status": "PASS" if semantic_pass and cross_pass else "FAIL",
+ "runtimes": len(results), "replayed": results, "cross_order": cross, "patch": patch,
+ "immutable_source_count": len(sources)}
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--run", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ result = audit(args.run.resolve())
+ write_json(args.output, result)
+ print(f"{result['status']}: {result['runtimes']} runtimes; semantic={result['semantic_status']}")
+ raise SystemExit(result["semantic_status"] != "PASS")
diff --git a/test-rig/migration/connected.py b/test-rig/migration/connected.py
new file mode 100644
index 00000000..ba439065
--- /dev/null
+++ b/test-rig/migration/connected.py
@@ -0,0 +1,362 @@
+"""Run connected legacy machinery, migrate untouched copies, and resume it."""
+import argparse
+from collections import Counter
+import json
+from pathlib import Path
+import shutil
+import socket
+import struct
+
+from matrix import (POWER, RIG, WORLD, Server, compare, copy_fixture, digest, fingerprint,
+ intermediate_profile, parse_nbt, plain, stage, write_json)
+
+SOURCES = ("original110", "early110", "maintained110", "legacy112", "maintained112")
+
+
+def fixture(source):
+ blocks = []
+ def put(station, role, x, z, name, seed=None, meta=0, y=80):
+ entry = dict(id=f"{station}-{role}", station=station, kind=role, x=x, y=y, z=z,
+ name=name, metadata=meta)
+ if seed is not None:
+ entry["seed"] = seed
+ blocks.append(entry)
+ def items(entries):
+ return "{Items:[" + ",".join('{Slot:%db,id:"%s",Count:%db,Damage:0s}' % e for e in entries) + "]}"
+ def supply(station, x, n):
+ for i in range(n):
+ bx = x + 1 + i * 4
+ put(station, f"water-{i}", bx, 50, "poweradvantage:fluid_metal_tank",
+ '{Tank:{FluidName:"water",Amount:10000}}')
+ put(station, f"water-pipe-{i}", bx, 49, "poweradvantage:fluid_pipe")
+ put(station, f"boiler-{i}", bx, 48, "steamadvantage:steam_boiler_coal",
+ items([(0, "minecraft:coal", 32)]))
+ for dx in range(1, 13):
+ put(station, f"steam-pipe-{dx}", x + dx, 47, "steamadvantage:steam_pipe")
+ station = "C-CONVEYOR"
+ put(station, "input", 1, 40, "minecraft:chest", items([(i, "minecraft:dirt", 64) for i in range(27)]))
+ put(station, "belt-1", 2, 40, "poweradvantage:item_conveyor", meta=5)
+ put(station, "belt-2", 3, 40, "poweradvantage:item_conveyor", meta=5)
+ put(station, "output", 4, 40, "minecraft:chest")
+ station = "C-STEAM"
+ supply(station, 64, 3)
+ put(station, "storage", 76, 46, "steamadvantage:steam_tank")
+ put(station, "furnace", 68, 46, "steamadvantage:steam_furnace",
+ items([(0, "minecraft:coal", 32)] + [(i, "minecraft:iron_ore", 64) for i in (1, 2, 3)]))
+ put(station, "crusher", 74, 46, "steamadvantage:steam_crusher", items([(0, "minecraft:cobblestone", 64)]))
+ station = "C-ELECTRIC"
+ supply(station, 128, 2)
+ put(station, "generator", 141, 47, "electricadvantage:steam_powered_generator")
+ for x in (142, 144):
+ put(station, f"wire-{x}", x, 47, "electricadvantage:electric_conduit")
+ put(station, "battery", 143, 47, "electricadvantage:electric_battery_array",
+ '{Items:[{Slot:0b,id:"electricadvantage:lithium_battery",Count:1b,Damage:0s,tag:{energy:0.0f}}]}')
+ put(station, "furnace", 145, 47, "electricadvantage:electric_furnace", items([(0, "minecraft:iron_ore", 64)]))
+ for station, origin, electric in (("C-SDRILL", 192, False), ("C-EDRILL", 256, True)):
+ supply(station, origin, 2)
+ if electric:
+ put(station, "generator", origin + 13, 47, "electricadvantage:steam_powered_generator")
+ pipe = "electricadvantage:electric_conduit"
+ else:
+ put(station, "steam-link", origin + 13, 47, "steamadvantage:steam_pipe")
+ pipe = "steamadvantage:steam_pipe"
+ for dx in (14, 15):
+ put(station, f"power-link-{dx}", origin + dx, 47, pipe)
+ for z in range(43, 47):
+ put(station, f"side-link-{z}", origin + 15, z, pipe)
+ put(station, "drill", origin + 14, 43, "electricadvantage:electric_drill" if electric else "steamadvantage:steam_drill", meta=2)
+ put(station, "rear", origin + 14, 44, "minecraft:chest")
+ for layer in range(32 if electric else 63):
+ for dx in (-1, 0, 1) if electric else (0,):
+ for dy in (-1, 0, 1) if electric else (0,):
+ put(station, f"target-{layer}-{dx + 1}-{dy + 1}", origin + 14 + dx, 42 - layer,
+ "minecraft:cobblestone", y=80 + dy)
+ return dict(format=1, sourceProfile=source, bounds=[0, 78, -32, 280, 84, 64], blocks=blocks)
+
+
+def read(runtime, label):
+ return json.loads((runtime / f"connected-{label}.json").read_text())
+
+
+def entries(document):
+ return {e["id"]: e for e in document["snapshot"]}
+
+
+def data(entry):
+ return plain(parse_nbt(entry)) if "nbtBase64" in entry else {}
+
+
+def stocks(document, station, kinds=None):
+ counts = Counter()
+ for e in document["snapshot"]:
+ if e["station"] == station and (kinds is None or e["kind"] in kinds):
+ for stack in data(e).get("Items", []):
+ counts[stack["id"]] += int(stack["Count"])
+ return counts
+
+
+def state(document):
+ failures, cases, water = [], {}, Counter()
+ for station, raw, product, total in (("C-CONVEYOR", "minecraft:dirt", "minecraft:dirt", 1728),
+ ("C-STEAM", "minecraft:iron_ore", "minecraft:iron_ingot", 192),
+ ("C-ELECTRIC", "minecraft:iron_ore", "minecraft:iron_ingot", 64)):
+ count = stocks(document, station)
+ accounted = count[raw] if raw == product else count[raw] + count[product]
+ if accounted != total:
+ failures.append(f"{station}: expected {total} {raw}/{product}, got {accounted}")
+ produced = stocks(document, station, {"output"})[product] if raw == product else count[product]
+ cases[station] = dict(produced=produced, remaining=total - produced, accounted=accounted)
+ count = stocks(document, "C-STEAM")
+ if count["minecraft:cobblestone"] + count["minecraft:gravel"] != 64:
+ failures.append("C-STEAM: crusher input/output lost or duplicated")
+ cases["C-CRUSHER"] = dict(produced=count["minecraft:gravel"], remaining=count["minecraft:cobblestone"])
+ for station in ("C-SDRILL", "C-EDRILL"):
+ targets = [e for e in document["snapshot"] if e["station"] == station and e["kind"].startswith("target-")]
+ remaining = sum(e["actualName"] == e["name"] for e in targets)
+ mined = len(targets) - remaining
+ drops = stocks(document, station, {"drill", "rear"})
+ output = sum(drops.values())
+ if output != mined:
+ failures.append(f"{station}: mined {mined} blocks but retained {output} drops")
+ product = "minecraft:gravel" if station == "C-SDRILL" else "minecraft:cobblestone"
+ if set(drops) - {product}:
+ failures.append(station + ": unexpected drill product identity")
+ cases[station] = dict(produced=output, remaining=remaining, targets=len(targets))
+ for e in document["snapshot"]:
+ if "nbtBase64" not in e or e["kind"].startswith("target-"):
+ continue
+ if e.get("witness") != e["id"]:
+ failures.append(e["id"] + ": source tile witness lost/replaced")
+ for value in data(e).get("Energy", []):
+ number = struct.unpack("!f", struct.pack("!I", int(value) & 0xffffffff))[0]
+ if not (0 <= number < 1000000):
+ failures.append(e["id"] + ": invalid energy")
+ tank = data(e).get("Tank", {})
+ if int(tank.get("Amount", 0)) > 0:
+ if tank.get("FluidName") not in ("water", "minecraft:water"):
+ failures.append(e["id"] + ": unexpected fluid")
+ water[e["station"]] += int(tank["Amount"])
+ charge = [s.get("tag", {}).get("energy", 0) for s in data(entries(document)["C-ELECTRIC-battery"]).get("Items", [])]
+ cases["battery"] = dict(charge=sum(charge))
+ return dict(status="FAIL" if failures else "PASS", failures=failures, cases=cases, water=dict(water))
+
+
+def cold_state(document):
+ verdict = state(document)
+ if sum(verdict["water"].values()) != 90000:
+ verdict["failures"].append("Source must start with exactly 90,000 mB water")
+ for e in document["snapshot"]:
+ if e["name"].startswith(("steamadvantage:", "electricadvantage:")):
+ if any(data(e).get("Energy", [])):
+ verdict["failures"].append(e["id"] + ": nonzero initial power reserve")
+ if verdict["cases"]["battery"]["charge"] != 0:
+ verdict["failures"].append("Initial battery is not empty")
+ verdict["status"] = "FAIL" if verdict["failures"] else "PASS"
+ return verdict
+
+
+def operation(before, after, ticks):
+ verdict = state(after)
+ previous = state(before)
+ for station, amount in verdict["water"].items():
+ if amount > previous["water"].get(station, 0):
+ verdict["failures"].append(station + ": finite water was created")
+ if not after.get("held") or after["requestedTicks"] != ticks or after["advancedTicks"] != ticks:
+ verdict["failures"].append("Missing exact native tick interval/freeze proof")
+ for name, case in previous["cases"].items():
+ if name == "battery":
+ continue
+ current = verdict["cases"][name]
+ if case["remaining"] > 0 and current["produced"] <= case["produced"]:
+ # A nine-block electric drilling cycle can span an observation window.
+ old = data(entries(before).get(name + "-drill", {})).get("progress", 0)
+ new = data(entries(after).get(name + "-drill", {})).get("progress", 0)
+ if name not in ("C-SDRILL", "C-EDRILL") or new <= old:
+ verdict["failures"].append(name + ": no further work from retained inputs")
+ old_entries = entries(before)
+ for key, entry in entries(after).items():
+ if "objectIdentity" in old_entries[key] and not entry["kind"].startswith("target-"):
+ if entry.get("objectIdentity") != old_entries[key]["objectIdentity"]:
+ verdict["failures"].append(key + ": tile replaced during the observation interval")
+ verdict["status"] = "FAIL" if verdict["failures"] else "PASS"
+ return verdict
+
+
+def pins(base, output):
+ profiles = json.loads(base.read_text())
+ changes = []
+ for name, profile in profiles.items():
+ for index, jar in enumerate(profile["jars"]):
+ original = Path(jar["path"])
+ if digest(original) != jar["sha256"]:
+ raise AssertionError("Changed archived input: " + str(original))
+ path = original
+ if original.name.startswith("AdvantageMigrationProbe-"):
+ target = original.name.split("-")[1]
+ path = RIG / f"migration-probe/build/{target}/libs" / original.name
+ elif original.name.startswith("AdvantageWorksTestHarness-"):
+ path = RIG / "build/libs" / original.name
+ actual = digest(path)
+ archived = output / "inputs" / actual / path.name
+ archived.parent.mkdir(parents=True, exist_ok=True)
+ if not archived.exists():
+ shutil.copy2(path, archived)
+ if digest(archived) != actual:
+ raise AssertionError("Archive changed")
+ if path != original:
+ changes.append(dict(profile=name, previous=jar, replacement=str(path), sha256=actual))
+ profile["jars"][index] = dict(path=str(archived.resolve()), sha256=actual)
+ write_json(output / "profiles.json", profiles)
+ write_json(output / "input-provenance.json", dict(base=str(base.resolve()), sha256=digest(base),
+ replacements=changes, production_and_dependency_pins="UNCHANGED"))
+ return {name: (Path(p["seed"]), p["launcher"], [Path(j["path"]) for j in p["jars"]]) for name, p in profiles.items()}
+
+
+def prepare(runtime, profile, definition, port, source=None):
+ launcher = stage(runtime, profile)
+ if source is not None:
+ copy_fixture(source, runtime)
+ definition = json.loads((source / "connected-fixture.json").read_text())
+ else:
+ write_json(runtime / "migration-fixture.json", definition["blocks"])
+ write_json(runtime / "connected-fixture.json", definition)
+ for marker in ("advantage-connected.allow", "migration-operate.allow"):
+ (runtime / marker).touch()
+ props = runtime / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={port}")
+ + "spawn-monsters=false\nspawn-animals=false\n")
+ return launcher
+
+
+class ConnectedServer(Server):
+ def __init__(self, *args):
+ self.commands = []
+ super().__init__(*args)
+
+ def send(self, command):
+ if command != "stop" and not command.startswith(("advconnected ", "save-all")):
+ raise AssertionError("Forbidden connected-world mutation: " + command)
+ self.commands.append(command)
+ super().send(command)
+
+ def observe(self, operation_name, label, ticks=0):
+ self.send(f"advconnected step {ticks} {label}" if ticks else f"advconnected {operation_name} {label}")
+ self.wait("ADVCONNECTED COMPLETE " + label, max(120, ticks / 20 + 90))
+
+ def close(self):
+ try:
+ super().close()
+ finally:
+ path = Path(self.log.name)
+ write_json(path.with_suffix(".commands.json"), self.commands)
+
+
+def boot(runtime, args, launcher, label):
+ return ConnectedServer(runtime, args.java8, launcher, label)
+
+
+def resume(runtime, args, launcher, label):
+ server = boot(runtime, args, launcher, label)
+ try:
+ server.observe("snapshot", label + "-loaded")
+ loaded = read(runtime, label + "-loaded")
+ server.observe("step", label + "-worked", args.ticks)
+ worked = read(runtime, label + "-worked")
+ verdict = operation(loaded, worked, args.ticks)
+ if worked["buildCallsThisProcess"] != 0:
+ verdict["status"] = "FAIL"
+ verdict["failures"].append("Equipment rebuilt")
+ server.send("save-all flush")
+ finally:
+ server.close()
+ return verdict
+
+
+def run(args):
+ with socket.socket() as listener:
+ listener.bind(("127.0.0.1", args.port))
+ args.output.mkdir(parents=True, exist_ok=False)
+ available = pins(args.profile_manifest, args.output)
+ report = dict(status="RUNNING", sources={}, routes={},
+ protocol="Source-only construction; finite fuel/water; target snapshot/step only; no block/inventory/energy/network resets")
+ try:
+ for name in args.sources:
+ print("BUILD AND RUN " + name, flush=True)
+ definition = fixture(name)
+ source = args.output / name
+ launcher = prepare(source, available[name], definition, args.port)
+ (source / "connected-build.allow").touch()
+ server = boot(source, args, launcher, "source")
+ try:
+ server.observe("build", "cold")
+ cold = read(source, "cold")
+ if cold_state(cold)["status"] != "PASS":
+ raise AssertionError("Invalid cold source fixture: " + json.dumps(cold_state(cold)))
+ server.observe("step", "source", args.warmup)
+ actual = read(source, "source")
+ qualified = operation(cold, actual, args.warmup)
+ if qualified["cases"]["battery"]["charge"] <= 0:
+ qualified["failures"].append("Battery did not charge from real generation")
+ qualified["status"] = "FAIL"
+ report["sources"][name] = qualified
+ server.send("save-all flush")
+ finally:
+ server.close()
+ write_json(args.output / "connected-results.json", report)
+ if qualified["status"] != "PASS":
+ raise AssertionError("Legacy source did not operate: " + json.dumps(qualified))
+ source_hashes = fingerprint(source / WORLD)
+ write_json(source / "fixture-hashes.json", source_hashes)
+ for route in ("direct", "chained") if name.endswith("110") else ("direct",):
+ label = name + "-" + route
+ if args.routes and label not in args.routes:
+ continue
+ print("IMPORT AND RESUME " + label, flush=True)
+ previous, checkpoint = source, "source"
+ result = {}
+ if route == "chained":
+ middle = args.output / (label + "-112")
+ launcher112 = prepare(middle, intermediate_profile(available), definition, args.port, source)
+ result["intermediate_operation"] = resume(middle, args, launcher112, "intermediate")
+ result["intermediate_preservation"] = compare(source / "migration-source.json", middle / "migration-intermediate-loaded.json")
+ result["intermediate_restart"] = resume(middle, args, launcher112, "intermediate-restart")
+ result["intermediate_restart_preservation"] = compare(middle / "migration-intermediate-worked.json", middle / "migration-intermediate-restart-loaded.json")
+ previous, checkpoint = middle, "intermediate-restart-worked"
+ target = args.output / (label + "-113")
+ launcher113 = prepare(target, available["target113"], definition, args.port, previous)
+ result["operation"] = resume(target, args, launcher113, "converted")
+ result["preservation"] = compare(previous / ("migration-" + checkpoint + ".json"), target / "migration-converted-loaded.json")
+ result["restart_operation"] = resume(target, args, launcher113, "restarted")
+ result["restart_preservation"] = compare(target / "migration-converted-worked.json", target / "migration-restarted-loaded.json")
+ first = state(read(target, "converted-loaded"))
+ last = state(read(target, "restarted-worked"))
+ missing = [key for key, case in first["cases"].items() if key != "battery" and case["remaining"] > 0
+ and last["cases"][key]["produced"] <= case["produced"]]
+ result["real_output_across_restarts"] = dict(status="FAIL" if missing else "PASS", failures=missing)
+ result["status"] = "PASS" if all(v["status"] == "PASS" for v in result.values()) else "FAIL"
+ report["routes"][label] = result
+ write_json(args.output / "connected-results.json", report)
+ print(label + ": " + result["status"], flush=True)
+ if fingerprint(source / WORLD) != source_hashes:
+ raise AssertionError("Immutable connected source fixture changed")
+ report["status"] = "PASS" if report["routes"] and all(r["status"] == "PASS" for r in report["routes"].values()) else "FAIL"
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ write_json(args.output / "connected-results.json", report)
+ print(report["status"] + ": " + str(args.output), flush=True)
+ return int(report["status"] != "PASS")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--profile-manifest", type=Path, default=POWER / "run-qualification/20261023-current/profiles-archived.json")
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--sources", nargs="+", choices=SOURCES, default=list(SOURCES))
+ parser.add_argument("--routes", nargs="+")
+ parser.add_argument("--port", type=int, default=25655)
+ parser.add_argument("--warmup", type=int, default=803)
+ parser.add_argument("--ticks", type=int, default=512)
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/dimensions.py b/test-rig/migration/dimensions.py
new file mode 100644
index 00000000..9d870ec4
--- /dev/null
+++ b/test-rig/migration/dimensions.py
@@ -0,0 +1,348 @@
+"""Nether/End players and live entity relationships on disposable pinned worlds."""
+
+import argparse
+import copy
+import json
+from pathlib import Path
+import shutil
+import socket
+import time
+
+import nbtlib
+
+from actors import (PLAYER_KEYS, backup_state, boot, enchantments, freeze_result,
+ qualified_baselines, semantic, tick_result as actor_tick_result)
+from matrix import (POWER, WORLD, Server, copy_fixture, differences, digest,
+ fingerprint, intermediate_profile, normalized, parse_nbt,
+ plain, selected_profiles, write_json)
+
+
+PLAYER_STATE = PLAYER_KEYS + ("SpawnX", "SpawnY", "SpawnZ", "SpawnForced", "SpawnDimension")
+ENTITY_STATE = ("id", "UUIDMost", "UUIDLeast", "Pos", "Rotation", "Motion", "Dimension",
+ "OwnerUUID", "Sitting", "parentPos", "ForgeData", "Leashed", "Leash",
+ "Health", "Variant", "Type", "Items", "SaddleItem", "ArmorItem", "Passengers",
+ "CollarColor", "Attributes")
+ENTITY_IDS = {"Boat": "minecraft:boat", "EntityHorse": "minecraft:horse",
+ "Horse": "minecraft:horse", "LeashKnot": "minecraft:leash_knot",
+ "Wolf": "minecraft:wolf",
+ "electricadvantage.electricadvantage.Hydroturbine": "electricadvantage:hydroturbine",
+ "electricadvantage:electricadvantage.Hydroturbine": "electricadvantage:hydroturbine"}
+WITNESSES = {"boat", "passenger-wolf", "horse", "hydroturbine"}
+
+
+def runtime_errors(content):
+ markers = ("Encountered an unexpected exception", "Exception stopping the server",
+ '\"Silently\" catching entity tracking error.', "Dimension probe failed",
+ "Dimension tick preparation failed", "Dimension load preparation failed")
+ return [line for line in content.splitlines() if any(marker in line for marker in markers)]
+
+
+def tick_result(before, after):
+ result = actor_tick_result(before, after)
+ for record in (before, after):
+ names = [entry["witness"] for entry in record["entities"]]
+ if not WITNESSES.issubset(names) or len(names) != len(set(names)):
+ result["status"] = "FAIL"
+ return result
+
+
+def exact_item_differences(expected, actual, path=""):
+ issues = []
+ if isinstance(expected, dict) and isinstance(actual, dict):
+ if "Count" in expected and "id" in expected:
+ if expected != actual:
+ issues.append({"path": path + ".exact-item", "expected": expected, "actual": actual})
+ else:
+ for key in expected.keys() & actual.keys():
+ issues.extend(exact_item_differences(expected[key], actual[key], path + "." + key))
+ elif isinstance(expected, list) and isinstance(actual, list):
+ for index, (old, new) in enumerate(zip(expected, actual)):
+ issues.extend(exact_item_differences(old, new, path + f"[{index}]"))
+ return issues
+
+
+def canonical_entities(value, legacy=False):
+ if isinstance(value, list):
+ return [canonical_entities(entry, legacy) for entry in value]
+ if not isinstance(value, dict):
+ return value
+ result = {key: canonical_entities(entry, legacy) for key, entry in value.items()}
+ if result.get("id") in ENTITY_IDS:
+ result["id"] = ENTITY_IDS[result["id"]]
+ if result.get("id") == "minecraft:horse" and result.get("Type") == 0:
+ # Vanilla's horse split encodes legacy Type=0 in the new entity identity.
+ result.pop("Type")
+ if result.get("id") == "minecraft:wolf" and legacy and "CollarColor" in result:
+ result["CollarColor"] = 15 - result["CollarColor"]
+ return result
+
+
+def entity_state(nbt):
+ result = {key: nbt[key] for key in ENTITY_STATE if key in nbt}
+ if "Passengers" in result:
+ result["Passengers"] = [entity_state(entry) for entry in result["Passengers"]]
+ if "Attributes" in result:
+ result["Attributes"] = {entry["Name"]: {key: value for key, value in entry.items() if key != "Name"}
+ for entry in result["Attributes"]}
+ return result
+
+
+def capture(runtime, label, evidence=None):
+ definition = json.loads((runtime / "dimension-fixture.json").read_text())
+ record = json.loads((runtime / f"dimensions-{label}.json").read_text())
+ registry = enchantments(runtime)
+ level = nbtlib.load(runtime / WORLD / "level.dat")
+ legacy = int(level["Data"].get("DataVersion", -1)) < 1451
+ player_file = (evidence / "player.dat" if evidence else runtime / WORLD / "playerdata"
+ / (definition["playerUuid"] + ".dat"))
+ state = {"dimension": record["dimension"],
+ "file": semantic(nbtlib.load(player_file).unpack(), PLAYER_STATE, registry),
+ "entities": {}, "relationships": {}}
+ generator = record["generator"]
+ state["generator"] = {"name": generator["actualName"]}
+ if "tile" in generator:
+ state["generator"]["tile"] = normalized(plain(parse_nbt(generator["tile"])), registry)
+ state["generator"]["tile"].pop("initDone", None)
+ if record["playerAdapter"]:
+ state["adapter"] = semantic(parse_nbt(record["player"]), PLAYER_STATE, registry)
+ state["playerWorldDimension"] = record["playerWorldDimension"]
+ uuids = set()
+ names = set()
+ for entry in record["entities"]:
+ name = entry["witness"]
+ nbt = canonical_entities(normalized(plain(parse_nbt(entry)), registry), legacy)
+ identity = (nbt.get("UUIDMost"), nbt.get("UUIDLeast"))
+ if name in names or identity in uuids or None in identity:
+ raise AssertionError(f"Duplicate or missing entity UUID/witness: {name}")
+ names.add(name)
+ uuids.add(identity)
+ if name == "leash-knot":
+ if nbt.get("id") != "minecraft:leash_knot":
+ raise AssertionError("Unexpected transient helper identity")
+ continue
+ state["entities"][name] = entity_state(nbt)
+ state["relationships"][name] = {
+ "vehicleUuid": entry.get("vehicleUuid"),
+ "passengerUuids": sorted(entry.get("passengerUuids", [])),
+ "leashHolderType": entry.get("leashHolderType"),
+ "leashHolderPosition": entry.get("leashHolderPosition"),
+ }
+ if entry.get("leashHolderType") != "minecraft:leash_knot":
+ state["relationships"][name]["leashHolderUuid"] = entry.get("leashHolderUuid")
+ return state
+
+
+def verdict(expected, actual):
+ issues = differences(expected, actual)
+ issues.extend(exact_item_differences(expected, actual))
+ if set(expected["entities"]) != set(actual["entities"]):
+ issues.append({"path": "entities.keys", "expected": sorted(expected["entities"]),
+ "actual": sorted(actual["entities"])})
+ return {"status": "FAIL" if issues else "PASS", "differences": issues}
+
+
+def strict_expected(baseline, adapter=True):
+ result = {key: copy.deepcopy(value) for key, value in baseline.items()
+ if key != "relationships" and (adapter or key not in ("adapter", "playerWorldDimension"))}
+ return result
+
+
+def survival_expected(baseline):
+ result = strict_expected(baseline)
+ for nbt in result["entities"].values():
+ for key in ("Pos", "Motion", "Rotation", "Passengers"):
+ nbt.pop(key, None)
+ result["relationships"] = copy.deepcopy(baseline["relationships"])
+ return result
+
+
+def checkpoint(server, runtime, modern, operation, label, save=True):
+ command = (f"advdimensions {operation} {label}" if modern
+ else f"advprobe dimensions-{operation} {label}")
+ server.send(command)
+ server.wait("ADVDIMENSIONS COMPLETE " + label, 120)
+ if not save:
+ return None
+ server.send("save-all flush")
+ server.wait("Saved the", 30)
+ evidence = runtime / "dimension-evidence" / label
+ evidence.mkdir(parents=True, exist_ok=False)
+ definition = json.loads((runtime / "dimension-fixture.json").read_text())
+ shutil.copy2(runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat"), evidence / "player.dat")
+ shutil.copy2(runtime / WORLD / "level.dat", evidence / "level.dat")
+ state = capture(runtime, label, evidence)
+ write_json(runtime / f"dimension-semantic-{label}.json", state)
+ return state
+
+
+def start(runtime, profile, args):
+ launcher = boot(runtime, profile, args.java8, args.port)
+ properties = runtime / "server.properties"
+ properties.write_text(properties.read_text().replace("allow-nether=false", "allow-nether=true"))
+ return launcher
+
+
+def clone(source, target):
+ copy_fixture(source, target)
+ shutil.copy2(source / "dimension-fixture.json", target / "dimension-fixture.json")
+
+
+def read_phase(runtime, launcher, args, label, modern):
+ server = Server(runtime, args.java8, launcher, label)
+ try:
+ if args.native_load:
+ checkpoint(server, runtime, modern, "operate", label + "-release", save=False)
+ time.sleep(4)
+ return checkpoint(server, runtime, modern, "read", label)
+ finally:
+ server.close()
+
+
+def run(args):
+ with socket.socket() as check:
+ check.bind(("127.0.0.1", args.port))
+ profiles = selected_profiles(args)
+ valid_routes = {name + "-" + route for name in args.sources
+ for route in (["direct", "chained"] if name.endswith("110") else ["direct"])}
+ if args.routes and not set(args.routes).issubset(valid_routes):
+ raise ValueError("Unknown route subset")
+ sources = qualified_baselines()
+ originals = {name: fingerprint(sources[name] / WORLD) for name in args.sources}
+ for name, hashes in originals.items():
+ if hashes != json.loads((sources[name] / "fixture-hashes.json").read_text()):
+ raise AssertionError("Changed historical fixture: " + name)
+ args.output.mkdir(parents=True, exist_ok=False)
+ write_json(args.output / "profiles.json", {name: {"seed": str(p[0]), "launcher": p[1],
+ "jars": [{"path": str(j), "sha256": digest(j)} for j in p[2]]} for name, p in profiles.items()})
+ results = {}
+ for dimension in args.dimensions:
+ district = "nether" if dimension == -1 else "end"
+ root = args.output / district
+ root.mkdir()
+ for name in args.sources:
+ routes = [route for route in (["direct", "chained"] if name.endswith("110") else ["direct"])
+ if not args.routes or name + "-" + route in args.routes]
+ if not routes:
+ continue
+ source = root / name
+ launcher = start(source, profiles[name], args)
+ copy_fixture(sources[name], source)
+ write_json(source / "dimension-fixture.json", {"dimension": dimension})
+ print(f"BUILD {district}/{name}: source serializers", flush=True)
+ server = Server(source, args.java8, launcher, "source")
+ try:
+ baseline = checkpoint(server, source, False, "build", "source")
+ finally:
+ server.close()
+ if set(baseline["entities"]) != WITNESSES or baseline["file"]["Dimension"] != dimension:
+ raise AssertionError("Incomplete source dimension fixture")
+ immutable = fingerprint(source / WORLD)
+ write_json(source / "fixture-hashes.json", immutable)
+ for route in routes:
+ label = name + "-" + route
+ print("MIGRATE " + district + "/" + label, flush=True)
+ predecessor = source
+ checks = {}
+ if route == "chained":
+ predecessor = root / (label + "-112")
+ middle_launcher = start(predecessor, intermediate_profile(profiles), args)
+ clone(source, predecessor)
+ for phase in ("intermediate", "intermediate-restart"):
+ state = read_phase(predecessor, middle_launcher, args, phase, False)
+ checks[phase] = verdict(survival_expected(baseline) if args.native_load else strict_expected(baseline), state)
+ if not args.native_load:
+ checks[phase + "-frozen"] = freeze_result(json.loads((predecessor / f"dimensions-{phase}.json").read_text()))
+ else:
+ checks[phase + "-ticks"] = tick_result(json.loads((predecessor / f"dimensions-{phase}-release.json").read_text()),
+ json.loads((predecessor / f"dimensions-{phase}.json").read_text()))
+ target = root / (label + "-113")
+ target_launcher = start(target, profiles["target113"], args)
+ clone(predecessor, target)
+ player_id = json.loads((target / "dimension-fixture.json").read_text())["playerUuid"]
+ relative_player = Path("playerdata") / (player_id + ".dat")
+ original_player = digest(target / WORLD / relative_player)
+ server = Server(target, args.java8, target_launcher, "conversion")
+ try:
+ if args.native_load:
+ checkpoint(server, target, True, "operate", "conversion-release", save=False)
+ time.sleep(4)
+ converted = checkpoint(server, target, True, "snapshot", "converted")
+ expectation = survival_expected(baseline) if args.native_load else strict_expected(baseline, adapter=False)
+ expectation.pop("adapter", None)
+ expectation.pop("playerWorldDimension", None)
+ checks["conversion"] = verdict(expectation, converted)
+ if not args.native_load:
+ checks["conversion-frozen"] = freeze_result(json.loads((target / "dimensions-converted.json").read_text()))
+ else:
+ checks["conversion-ticks"] = tick_result(json.loads((target / "dimensions-conversion-release.json").read_text()),
+ json.loads((target / "dimensions-converted.json").read_text()))
+ backups = backup_state(target)
+ backup_file = target / WORLD / relative_player.with_name(relative_player.name + ".poweradvantage-legacy-backup")
+ checks["backup"] = {"status": "PASS" if backup_file.is_file() and digest(backup_file) == original_player else "FAIL",
+ "files": backups}
+ adapter = checkpoint(server, target, True, "read", "adapter")
+ checks["adapter"] = verdict(survival_expected(baseline) if args.native_load else strict_expected(baseline), adapter)
+ finally:
+ server.close()
+ server = Server(target, args.java8, target_launcher, "restart")
+ try:
+ if args.native_load:
+ checkpoint(server, target, True, "operate", "restart-load-release", save=False)
+ time.sleep(4)
+ restarted = checkpoint(server, target, True, "read", "restarted")
+ checks["restart"] = verdict(survival_expected(baseline) if args.native_load else strict_expected(baseline), restarted)
+ if not args.native_load:
+ checks["restart-frozen"] = freeze_result(json.loads((target / "dimensions-restarted.json").read_text()))
+ checks["idempotence"] = verdict(survival_expected(adapter) if args.native_load else strict_expected(adapter), restarted)
+ checkpoint(server, target, True, "operate", "released")
+ time.sleep(4)
+ operated = checkpoint(server, target, True, "read", "runtime")
+ checks["runtime"] = verdict(survival_expected(baseline), operated)
+ checks["runtime-ticks"] = tick_result(json.loads((target / "dimensions-released.json").read_text()),
+ json.loads((target / "dimensions-runtime.json").read_text()))
+ finally:
+ server.close()
+ server = Server(target, args.java8, target_launcher, "runtime-restart")
+ try:
+ checkpoint(server, target, True, "operate", "restart-released", save=not args.native_load)
+ time.sleep(4)
+ operated = checkpoint(server, target, True, "read", "runtime-restart")
+ checks["runtime-restart"] = verdict(survival_expected(baseline), operated)
+ checks["restart-ticks"] = tick_result(json.loads((target / "dimensions-restart-released.json").read_text()),
+ json.loads((target / "dimensions-runtime-restart.json").read_text()))
+ finally:
+ server.close()
+ checks["backup-stability"] = {"status": "PASS" if backup_state(target) == backups else "FAIL"}
+ results[district + "/" + label] = checks
+ write_json(target / "dimension-comparison.json", checks)
+ write_json(args.output / "dimension-results.json", results)
+ print(district + "/" + label + ": " + ", ".join(k + "=" + v["status"] for k, v in checks.items()), flush=True)
+ if args.fail_fast and any(v["status"] != "PASS" for v in checks.values()):
+ return 1
+ if fingerprint(source / WORLD) != immutable:
+ raise AssertionError("Generated source fixture changed")
+ for name, hashes in originals.items():
+ if fingerprint(sources[name] / WORLD) != hashes:
+ raise AssertionError("Historical source fixture changed")
+ write_json(args.output / "scope.json", {"real_client_login": "NOT_TESTED", "dimensions": args.dimensions,
+ "routes": len(results), "immutable_sources": "PASS",
+ "expected_route_ids": sorted(("nether" if dimension == -1 else "end") + "/" + route
+ for dimension in args.dimensions for route in (args.routes or valid_routes)),
+ "native_load": args.native_load,
+ "live_relationships": "Measured vehicle/passenger/leash UUIDs after native ticks and full restart"})
+ return int(any(v["status"] != "PASS" for checks in results.values() for v in checks.values()))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--workspace", type=Path, default=POWER.parents[1])
+ parser.add_argument("--profile-manifest", type=Path, required=True)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25622)
+ parser.add_argument("--sources", nargs="+", choices=list(qualified_baselines()), default=list(qualified_baselines()))
+ parser.add_argument("--routes", nargs="+")
+ parser.add_argument("--dimensions", nargs="+", type=int, choices=[-1, 1], default=[-1, 1])
+ parser.add_argument("--fail-fast", action="store_true")
+ parser.add_argument("--native-load", action="store_true", help="Permit vanilla entity initialization before the first save")
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/drill_restart.py b/test-rig/migration/drill_restart.py
new file mode 100644
index 00000000..fdd778b3
--- /dev/null
+++ b/test-rig/migration/drill_restart.py
@@ -0,0 +1,119 @@
+"""Prove both drills retain mid-cycle work across native process restarts."""
+import argparse
+import json
+from pathlib import Path
+import socket
+
+from connected import ConnectedServer, data, entries, operation, pins, prepare, read, state
+from matrix import POWER, WORLD, compare, fingerprint, write_json
+
+
+def progress(document, station):
+ return data(entries(document)[station + "-drill"])
+
+
+def first_tick(before, after):
+ failures = []
+ if (before["requestedTicks"], before["advancedTicks"], before["held"]) != (0, 0, True):
+ failures.append("Missing frozen load")
+ if (after["requestedTicks"], after["advancedTicks"], after["held"]) != (1, 1, True):
+ failures.append("Missing one native tick")
+ readings = {}
+ for station in ("C-SDRILL", "C-EDRILL"):
+ old, new = progress(before, station), progress(after, station)
+ readings[station] = dict(before=old.get("progress"), after=new.get("progress"))
+ low, high = (4, 18) if station == "C-SDRILL" else (64, 530)
+ if not low <= old.get("progress", 0) <= high:
+ failures.append(station + ": checkpoint is not mid-cycle")
+ if new.get("progress") != old.get("progress", 0) + 1:
+ failures.append(station + ": saved progress not continued")
+ if any(old.get(key) != new.get(key) for key in ("targetX", "targetY", "targetZ")):
+ failures.append(station + ": target changed during first tick")
+ a, b = entries(before)[station + "-drill"], entries(after)[station + "-drill"]
+ if a["objectIdentity"] != b["objectIdentity"] or a["witness"] != b["witness"]:
+ failures.append(station + ": drill replaced")
+ failures.extend(state(after)["failures"])
+ return dict(status="FAIL" if failures else "PASS", failures=failures, progress=readings)
+
+
+def ready(server, runtime, label):
+ for index in range(100):
+ server.observe("snapshot", label)
+ document = read(runtime, label)
+ steam = progress(document, "C-SDRILL").get("progress", 0)
+ electric = progress(document, "C-EDRILL").get("progress", 0)
+ if 4 <= steam <= 18 and 64 <= electric <= 530:
+ return document
+ server.observe("step", f"{label}-align-{index}", 7)
+ raise AssertionError("Cannot reach a naturally powered mid-cycle checkpoint for both drills")
+
+
+def run(args):
+ with socket.socket() as listener:
+ listener.bind(("127.0.0.1", args.port))
+ args.output.mkdir(parents=True, exist_ok=False)
+ available = pins(args.profile_manifest, args.output)
+ report = dict(status="RUNNING", targets={}, protocol="No NBT/energy/inventory writes; native alignment, frozen load, first tick, 620 work ticks, two complete restarts")
+ try:
+ for target in args.targets:
+ print("NATIVE RESTART " + target, flush=True)
+ source = args.fixture110 if target == "110" else args.fixture112
+ hashes = fingerprint(source / WORLD)
+ profile = available.get("native110", available["maintained110"]) if target == "110" else available["bridge112" if target == "112" else "target113"]
+ definition = json.loads((source / "connected-fixture.json").read_text())
+ runtime = args.output / target
+ launcher = prepare(runtime, profile, definition, args.port, source)
+ result = dict(source=str(source.resolve()), source_hashes=hashes, phases={})
+ server = ConnectedServer(runtime, args.java8, launcher, "alignment")
+ try:
+ server.observe("snapshot", "loaded-source")
+ result["initial_preservation"] = compare(source / "migration-source.json", runtime / "migration-loaded-source.json")
+ ready(server, runtime, "ready-0")
+ server.send("save-all flush")
+ finally:
+ server.close()
+ previous = "ready-0"
+ for number in (1, 2):
+ label = "restart-" + str(number)
+ server = ConnectedServer(runtime, args.java8, launcher, label)
+ try:
+ server.observe("snapshot", label + "-loaded")
+ server.observe("step", label + "-first", 1)
+ loaded, first = read(runtime, label + "-loaded"), read(runtime, label + "-first")
+ server.observe("step", label + "-worked", 620)
+ result["phases"][label] = dict(
+ preservation=compare(runtime / ("migration-" + previous + ".json"), runtime / ("migration-" + label + "-loaded.json")),
+ first_tick=first_tick(loaded, first),
+ operation=operation(first, read(runtime, label + "-worked"), 620))
+ if number == 1:
+ ready(server, runtime, "ready-1")
+ previous = "ready-1"
+ server.send("save-all flush")
+ finally:
+ server.close()
+ if fingerprint(source / WORLD) != hashes:
+ raise AssertionError("Native regression modified its source fixture")
+ result["status"] = "PASS" if result["initial_preservation"]["status"] == "PASS" and all(
+ gate["status"] == "PASS" for phase in result["phases"].values() for gate in phase.values()) else "FAIL"
+ report["targets"][target] = result
+ write_json(args.output / "drill-restart-results.json", report)
+ print(target + ": " + result["status"], flush=True)
+ report["status"] = "PASS" if report["targets"] and all(r["status"] == "PASS" for r in report["targets"].values()) else "FAIL"
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ write_json(args.output / "drill-restart-results.json", report)
+ return int(report["status"] != "PASS")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--profile-manifest", type=Path, required=True)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--targets", nargs="+", choices=("110", "112", "113"), default=["110", "112", "113"])
+ parser.add_argument("--fixture110", type=Path, default=POWER / "run-connected-migration/20261023-working-maintained110/maintained110")
+ parser.add_argument("--fixture112", type=Path, default=POWER / "run-connected-migration/20261023-working-112/maintained112")
+ parser.add_argument("--port", type=int, default=25659)
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/endurance.py b/test-rig/migration/endurance.py
new file mode 100644
index 00000000..08686342
--- /dev/null
+++ b/test-rig/migration/endurance.py
@@ -0,0 +1,342 @@
+"""Bounded repeat-batch endurance and restart checks on disposable Works worlds."""
+
+import argparse
+from collections import Counter
+from datetime import datetime, timezone
+import json
+import math
+from pathlib import Path
+import socket
+import struct
+import time
+import traceback
+
+import nbtlib
+from matrix import POWER, RIG, WORLD, Server, copy_fixture, digest, fingerprint, plain, profiles, selected_profiles, stage, write_json
+
+
+ACTIVE = ["W-01", "W-03", "W-04", "S-01", "S-02", "S-03", "S-04",
+ "E-01", "E-02", "E-03", "E-05", "P-API-01"]
+CHECKED = ACTIVE + ["S-06", "S-05", "E-04", "E-06"]
+CAPACITIES = {"lead_acid_battery": 1000, "nickel_hydride_battery": 2000,
+ "alkaline_battery": 4000, "lithium_battery": 8000}
+PROCESSING = [
+ ("steam-furnace", "S-04", (3, 1, 7), [(2, 1, 7), (4, 1, 7)], "iron_ore", "iron_ingot", 1, 4),
+ ("steam-crusher", "S-04", (3, 1, 13), [(2, 1, 13), (4, 1, 13)], "cobblestone", "gravel", 0, 4),
+ ("electric-furnace", "E-03", (6, 1, 6), [], "iron_ore", "iron_ingot", 0, 4),
+ ("electric-crusher", "E-03", (3, 1, 6), [], "cobblestone", "gravel", 0, 4),
+ ("electric-oven", "E-03", (9, 1, 6), [], "beef", "cooked_beef", 0, 4),
+]
+SLOTS = {"steam-furnace": 7, "steam-crusher": 6, "electric-furnace": 10,
+ "electric-crusher": 9, "electric-oven": 2}
+
+
+def require(condition, detail):
+ if not condition: raise AssertionError(detail)
+
+
+def parse_snapshot(document):
+ result = {}
+ for entry in document["snapshot"]:
+ result[tuple(entry["pos"])] = {**entry, "data": plain(nbtlib.parse_nbt(entry.get("nbt", "{}")))}
+ return result
+
+
+def item_counts(entries):
+ counts = Counter()
+ for entry in entries:
+ for stack in entry["data"].get("Items", []):
+ counts[stack["id"]] += int(stack["Count"])
+ return counts
+
+
+class Rig:
+ def __init__(self, name, runtime, java, launcher, definitions):
+ self.name, self.runtime, self.java, self.launcher = name, runtime, java, launcher
+ self.definitions, self.server = definitions, None
+ self.documents, self.snapshots, self.ledger = {}, {}, []
+ self.static_batteries = None
+
+ def pos(self, station, relative):
+ return tuple(a + b for a, b in zip(self.definitions[station]["origin"], relative))
+
+ def entry(self, station, relative):
+ return self.snapshots[station][self.pos(station, relative)]
+
+ def boot(self, phase):
+ self.server = Server(self.runtime, self.java, self.launcher, phase)
+
+ def command(self, command, response=None):
+ self.server.send(command)
+ if response: self.server.wait(response, 120)
+
+ def checkpoint(self, label):
+ for station in CHECKED:
+ self.command(f"advworks checkpoint {station} {label}", "Checkpoint written for " + station)
+ document = json.loads((self.runtime / "advantage-works-results" /
+ f"{station}-checkpoint-{label}.json").read_text())
+ self.documents[station], self.snapshots[station] = document, parse_snapshot(document)
+
+ def inventory(self, station, relative, items):
+ block = self.entry(station, relative)["block"]
+ slots = 27 if block == "minecraft:chest" else 1
+ for name, candidate, pos, *_ in PROCESSING:
+ if station == candidate and tuple(relative) == pos: slots = SLOTS[name]
+ # replaceitem uses IInventory setters, not TileEntity.read(), which would
+ # refresh conduit caches and could conceal a loaded-network defect.
+ x, y, z = self.pos(station, relative)
+ wanted = {slot: (name, count) for slot, name, count in items if count}
+ for slot in range(slots):
+ name, count = wanted.pop(slot, ("minecraft:air", 1))
+ self.command(f"replaceitem block {x} {y} {z} container.{slot} {name} {count}")
+ for slot, (name, count) in wanted.items():
+ self.command(f"replaceitem block {x} {y} {z} container.{slot} {name} {count}")
+
+ def start(self, build=False):
+ if build: self.command("advworks build all", "Advantage Works built")
+ for station in ACTIVE:
+ if station == "W-03": continue # Its first measured fill happens in seed().
+ if not build and station == "P-API-01":
+ # Its initial API offers deliberately fill the tank. Repeating
+ # those offers requires a reset after checking retained water.
+ self.command("advworks reset P-API-01", "Reset 1 station(s).")
+ self.command(f"advworks start {station}", "Started " + station)
+ # Warmed furnace fuel is independent of the steam bonus. The old station's
+ # input chest alone cannot commission a furnace without its fuel slot.
+ self.checkpoint("setup")
+ if build:
+ self.static_batteries = self.entry("E-02", (3, 1, 12))["data"].get("Items", [])
+ self.seed(initial=True)
+
+ def seed(self, initial=False):
+ for name, station, relative, neighbours, input_id, output_id, slot, count in PROCESSING:
+ for neighbour in neighbours: self.inventory(station, neighbour, [])
+ contents = []
+ if name == "steam-furnace":
+ fuel = 64 if initial else sum(int(s["Count"]) for s in self.entry(station, relative)["data"].get("Items", [])
+ if int(s["Slot"]) == 0 and s["id"] == "minecraft:coal")
+ contents.append((0, "minecraft:coal", fuel))
+ contents.append((slot, "minecraft:" + input_id, count))
+ self.inventory(station, relative, contents)
+ lanes = [(6, "dirt", 16), (9, "dirt", 8), (15, "dirt", 8), (18, "dirt", 8)]
+ if initial:
+ for x in (3, 4): self.inventory("W-04", (x, 1, 6), [])
+ for z in (9, 15, 18):
+ filters = [(int(s["Slot"]), s["id"], int(s["Count"]))
+ for s in self.entry("W-04", (3, 1, z))["data"].get("Items", [])
+ if int(s["Slot"]) != 0]
+ self.inventory("W-04", (3, 1, z), filters)
+ for z, _, _ in lanes:
+ destinations = [(5, 1, z)] if z == 6 else [(4, 1, z), (3, 0, z)]
+ for destination in destinations: self.inventory("W-04", destination, [])
+ source = [(0, "minecraft:dirt", 16)] if z == 6 else [(0, "minecraft:dirt", 8), (1, "minecraft:apple", 8)]
+ self.inventory("W-04", (2, 1, z), source)
+ # Start actions drain/refill through fluid handlers and IInventory.
+ self.command("advworks start W-03", "Started W-03")
+
+ def counts(self, station, positions):
+ return item_counts([self.entry(station, p) for p in positions])
+
+ def batch(self, label):
+ result = {"label": label, "outputs": {}, "world_time": self.documents["W-01"]["worldTime"]}
+ for name, station, relative, neighbours, input_id, output_id, _, count in PROCESSING:
+ counts = self.counts(station, [relative] + neighbours)
+ require(counts["minecraft:" + input_id] == 0, f"{self.name}/{name}: input remains: {counts}")
+ require(counts["minecraft:" + output_id] == count, f"{self.name}/{name}: incorrect yield: {counts}")
+ allowed = {"minecraft:" + output_id}
+ if name == "steam-furnace": allowed.add("minecraft:coal")
+ require(not (set(counts) - allowed), f"{self.name}/{name}: unexpected items: {counts}")
+ result["outputs"][name] = count
+ plain_conveyor = self.counts("W-04", [(2, 1, 6), (3, 1, 6), (4, 1, 6), (5, 1, 6)])
+ require(plain_conveyor == {"minecraft:dirt": 16}, f"{self.name}: conveyor conservation {plain_conveyor}")
+ require(self.counts("W-04", [(5, 1, 6)]) == {"minecraft:dirt": 16}, f"{self.name}: conveyor throughput")
+ result["outputs"]["conveyor"] = 16
+ for z, matched_dirt in ((9, True), (15, False), (18, True)):
+ above = self.counts("W-04", [(4, 1, z)])
+ below = self.counts("W-04", [(3, 0, z)])
+ require(above == ({"minecraft:apple": 8} if matched_dirt else {"minecraft:apple": 8, "minecraft:dirt": 8}),
+ f"{self.name}: filter {z} forward {above}")
+ require(below == ({"minecraft:dirt": 8} if matched_dirt else {}), f"{self.name}: filter {z} lower {below}")
+ require(not self.counts("W-04", [(2, 1, z)]), f"{self.name}: filter {z} source not exhausted")
+ require(not any(int(s["Slot"]) == 0 for s in self.entry("W-04", (3, 1, z))["data"].get("Items", [])),
+ f"{self.name}: filter {z} held input remains")
+ result["outputs"][f"filter-{z}"] = 16
+ # Shared bidirectional pipes may return refined oil to the supply tank.
+ outputs = [self.entry("W-03", p)["data"].get("Tank", {}) for p in ((4, 1, 8), (6, 1, 8), (8, 1, 8))]
+ outputs.append(self.entry("W-03", (6, 1, 8))["data"].get("InputTank", {}))
+ input_amount = sum(t.get("Amount", 0) for t in outputs if t.get("FluidName") == "poweradvantage:crude_oil")
+ require(input_amount == 0, f"{self.name}: oil input remaining {input_amount}")
+ require(sum(t.get("Amount", 0) for t in outputs) == 1000, f"{self.name}: oil output {outputs}")
+ require(all(t.get("FluidName") == "poweradvantage:refined_oil" for t in outputs if t.get("Amount", 0)),
+ f"{self.name}: wrong oil output identity")
+ result["outputs"]["distillation-mB"] = 1000
+ self.invariants()
+ self.ledger.append(result)
+ write_json(self.runtime / "endurance-ledger.json", self.ledger)
+ return result
+
+ def invariants(self):
+ for station, relative, amount in (("W-01", (13, 1, 7), 2000), ("S-06", (20, 5, 20), 1000), ("P-API-01", (8, 1, 7), 4000)):
+ tank = self.entry(station, relative)["data"].get("Tank", {})
+ require(tank.get("Amount") == amount and tank.get("FluidName") == "minecraft:water",
+ f"{self.name}/{station}: closed water witness changed: {tank}")
+ require(self.entry("S-01", (12, 1, 7))["data"].get("Temperature", 0) > 0,
+ f"{self.name}: geothermal temperature reset")
+ require(self.entry("E-02", (3, 1, 12))["data"].get("Items", []) == self.static_batteries,
+ f"{self.name}: stored battery items or their charge changed")
+ for station in CHECKED:
+ for entry in self.snapshots[station].values():
+ for bits in entry["data"].get("Energy", []):
+ energy = struct.unpack(">f", struct.pack(">I", int(bits) & 0xFFFFFFFF))[0]
+ require(math.isfinite(energy) and energy >= 0, f"{self.name}: invalid energy at {entry['pos']}: {energy}")
+ for stack in entry["data"].get("Items", []):
+ require(0 < int(stack["Count"]) <= 64, f"{self.name}: invalid stack size: {stack}")
+ name = stack["id"].split(":")[-1]
+ if name in CAPACITIES and "energy" in stack.get("tag", {}):
+ charge = stack["tag"]["energy"]
+ require(math.isfinite(charge) and 0 <= charge <= CAPACITIES[name],
+ f"{self.name}: out-of-range battery charge {name}: {charge}")
+ charge = self.entry("E-01", (4, 4, 12))["data"].get("Items", [{}])[0].get("tag", {}).get("energy", 0)
+ require(charge > 0, f"{self.name}: hydroelectric battery has not charged")
+
+ def structure(self):
+ statuses = {}
+ for station in CHECKED:
+ self.command(f"advworks check {station}", "Advantage Works check:")
+ status = json.loads((self.runtime / "advantage-works-results" / f"{station}-latest.json").read_text())["status"]
+ statuses[station] = status
+ require(status in ("PASS", "MANUAL_REQUIRED"), f"{self.name}: station {station}: {status}")
+ return statuses
+
+ def shutdown(self):
+ if self.server:
+ server, self.server = self.server, None
+ server.close()
+
+
+def run(args):
+ require(args.interval >= 60, "Batch interval must be at least 60 seconds")
+ require(args.seconds >= args.interval and args.seconds % args.interval == 0, "Duration must contain whole batch intervals")
+ current = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ for mod in current["mods"]:
+ if "sha256" in mod:
+ require(digest((RIG / mod["path"]).resolve()) == mod["sha256"], "Current artifact pin mismatch: " + mod["file"])
+ definition = json.loads((RIG / "src/main/resources/assets/advantageworkstest/works/works-1.13.2.json").read_text())
+ definitions = {s["id"]: s for s in definition["stations"]}
+ require(sum(bool(definitions[s].get("exclusive")) for s in ACTIVE) == 1, "Exclusive stations would interfere")
+ require((args.migrated / WORLD / "level.dat").is_file(), "Missing converted source world")
+ for port in (args.port, args.port + 1):
+ with socket.socket() as listener:
+ listener.bind(("127.0.0.1", port))
+ if args.profile_manifest:
+ args.workspace = POWER.parents[1]
+ profile = selected_profiles(args)["target113"]
+ else:
+ profile = profiles(POWER.parents[1])["target113"]
+ args.output.mkdir(parents=True, exist_ok=False)
+ rigs, source_hashes = [], fingerprint(args.migrated / WORLD)
+ report = {"status": "RUNNING", "duration_seconds": args.seconds, "interval_seconds": args.interval,
+ "feeding_protocol": "IInventory replaceitem and fluid handlers; no inventory NBT writes",
+ "active_stations": ACTIVE, "checked_stations": CHECKED, "profiles": {},
+ "artifacts": [{"file": jar.name, "sha256": digest(jar)} for jar in profile[2]],
+ "manual_checks": "Remain MANUAL_REQUIRED; no GUI, moving drill, lift, or weapon qualification"}
+ for index, name in enumerate(("fresh113", "original110-chained113")):
+ runtime = args.output / name
+ launcher = stage(runtime, profile)
+ if index: copy_fixture(args.migrated, runtime)
+ properties = runtime / "server.properties"
+ properties.write_text(properties.read_text().replace("server-port=25584", f"server-port={args.port + index}"))
+ (runtime / "migration-operate.allow").touch()
+ (runtime / "advantage-works.allow").touch()
+ rigs.append(Rig(name, runtime, args.java8, launcher, definitions))
+ try:
+ for rig in rigs:
+ rig.boot("endurance")
+ rig.start(build=True)
+ starting_ticks = {rig.name: rig.documents["W-01"]["worldTime"] for rig in rigs}
+ started = time.monotonic()
+ report["started_at_utc"] = datetime.now(timezone.utc).isoformat()
+ for cycle in range(1, args.seconds // args.interval + 1):
+ deadline = started + cycle * args.interval
+ while time.monotonic() < deadline:
+ for rig in rigs: require(rig.server.process.poll() is None, f"{rig.name}: server died")
+ time.sleep(min(1, max(0, deadline - time.monotonic())))
+ for rig in rigs:
+ rig.checkpoint(f"cycle-{cycle:03}")
+ rig.batch(f"cycle-{cycle:03}")
+ if cycle < args.seconds // args.interval: rig.seed()
+ report["profiles"][rig.name] = {"status": "RUNNING", "completed_batches": len(rig.ledger),
+ "latest_world_time": rig.documents["W-01"]["worldTime"]}
+ write_json(args.output / "endurance-results.json", report)
+ print(f"{cycle * args.interval}s: both profiles passed batch {cycle}", flush=True)
+ elapsed = time.monotonic() - started
+ ending_ticks = {rig.name: rig.documents["W-01"]["worldTime"] for rig in rigs}
+ for rig in rigs:
+ require(ending_ticks[rig.name] - starting_ticks[rig.name] >= args.seconds * 18,
+ f"{rig.name}: fewer than 18 game ticks/second during endurance")
+ for rig in rigs:
+ rig.structure()
+ rig.checkpoint("before-restart")
+ rig.batch("before-restart")
+ rig.command("save-all flush")
+ rig.shutdown()
+ rig.boot("endurance-restart")
+ rig.checkpoint("after-restart")
+ rig.batch("after-restart")
+ rig.structure()
+ # Keep the saved sources/pipes intact: restarting the stations would
+ # replace power blocks and mask a failure to reconnect after load.
+ rig.seed()
+ time.sleep(args.interval)
+ for rig in rigs:
+ rig.checkpoint("restart-resumed")
+ rig.batch("restart-resumed")
+ statuses = rig.structure()
+ rig.command("save-all flush")
+ rig.shutdown()
+ report["profiles"][rig.name] = {"status": "PASS", "elapsed_seconds": elapsed,
+ "endurance_batches": args.seconds // args.interval,
+ "total_verified_checkpoints": len(rig.ledger), "station_statuses": statuses,
+ "game_ticks": ending_ticks[rig.name] - starting_ticks[rig.name],
+ "restart": "PASS", "resumed_processing": "PASS"}
+ totals = Counter()
+ for batch in rig.ledger:
+ if batch["label"].startswith("cycle-"): totals.update(batch["outputs"])
+ report["profiles"][rig.name]["endurance_outputs"] = dict(totals)
+ diagnostics = []
+ for log in sorted(rig.runtime.glob("console-*.log")):
+ text = log.read_text(encoding="utf-8")
+ errors = [line for line in text.splitlines() if "/ERROR]" in line or "/FATAL]" in line]
+ diagnostics.append({"file": log.name, "error_lines": errors,
+ "keep_up_warnings": text.count("Can't keep up!")})
+ report["profiles"][rig.name]["log_diagnostics"] = diagnostics
+ require(fingerprint(args.migrated / WORLD) == source_hashes, "Converted source fixture changed")
+ report["source_fixture_unchanged"] = True
+ report["status"] = "PASS" if args.seconds >= 1800 else "SMOKE_PASS_NOT_ENDURANCE"
+ except BaseException as error:
+ report["status"], report["failure"] = "FAIL", str(error)
+ report["traceback"] = traceback.format_exc()
+ raise
+ finally:
+ shutdown_errors = []
+ for rig in rigs:
+ try: rig.shutdown()
+ except BaseException as error: shutdown_errors.append(f"{rig.name}: {error}")
+ if shutdown_errors:
+ report["status"], report["shutdown_errors"] = "FAIL", shutdown_errors
+ report["completed_at_utc"] = datetime.now(timezone.utc).isoformat()
+ write_json(args.output / "endurance-results.json", report)
+ require(report["status"] != "FAIL", "Endurance qualification failed; see the saved report")
+ print(f"{report['status']}: {args.output}", flush=True)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--migrated", type=Path, required=True, help="Immutable, already converted 1.13 runtime")
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--seconds", type=int, default=1800)
+ parser.add_argument("--interval", type=int, default=60)
+ parser.add_argument("--port", type=int, default=25584)
+ parser.add_argument("--profile-manifest", type=Path, help="Pinned matrix manifest with target113 launcher and jars")
+ run(parser.parse_args())
diff --git a/test-rig/migration/extended_lifecycle.py b/test-rig/migration/extended_lifecycle.py
new file mode 100644
index 00000000..2a8af80e
--- /dev/null
+++ b/test-rig/migration/extended_lifecycle.py
@@ -0,0 +1,427 @@
+"""Partial network lifecycles and repeated drill operation on disposable worlds."""
+import argparse
+from collections import Counter
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+import socket
+import time
+import traceback
+
+from endurance import require
+from lifecycle import FACINGS, LifecycleRun, at, contents, counts, move_result, offset, parse
+from matrix import POWER, RIG, WORLD, copy_fixture, digest, fingerprint, profiles, stage, write_json
+
+
+def fixtures():
+ result = []
+ for mod in ("steamadvantage", "electricadvantage"):
+ specs = [(name, direction, (0, 0, 1) if direction[0] else (1, 0, 0), False)
+ for name, direction in FACINGS.items()]
+ specs += [("vertical-up", FACINGS["north"], FACINGS["up"], False),
+ ("vertical-down", FACINGS["north"], FACINGS["down"], False),
+ ("junction", FACINGS["north"], FACINGS["down"], True)]
+ for name, direction, track, junction in specs:
+ base = 6144 + len(result) * 64
+ origin = [base + 14, 80, 2062]
+ route = [offset(origin, track, n) for n in range(5)]
+ axis = next(i for i in range(3) if not direction[i] and not track[i])
+ control = tuple(-1 if i == axis else 0 for i in range(3))
+ facing = next(k for k, v in FACINGS.items() if v == direction)
+ steam = mod == "steamadvantage"
+ entry = {"id": ("S-EXT-" if steam else "E-EXT-") + name,
+ "min": [base, 72, 2048], "max": [base + 31, 90, 2079],
+ "from": origin, "to": route[-1], "route": route,
+ "controls": [offset(pos, control) for pos in route], "hold": offset(route[-1], control),
+ "block": mod + (":steam_drill" if steam else ":electric_drill"),
+ "energyType": "steam" if steam else "electricity", "initialEnergy": 40 if steam else 400,
+ "facing": facing, "direction": direction, "track": track, "junction": junction,
+ "rear": offset(route[-1], direction, -1), "source": offset(origin, control, -1)}
+ if junction:
+ entry["alternatives"] = [offset(origin, FACINGS["up"]), offset(origin, FACINGS["east"])]
+ witnesses = route + entry["controls"] + [offset(pos, direction) for pos in route]
+ witnesses += [entry["rear"], entry["source"], offset(route[-1], direction, 2)]
+ center = offset(route[-1], direction)
+ plane_axes = [i for i, value in enumerate(direction) if not value]
+ if not steam:
+ for a in (-1, 0, 1):
+ for b in (-1, 0, 1):
+ pos = center.copy(); pos[plane_axes[0]] += a; pos[plane_axes[1]] += b
+ witnesses.append(pos)
+ witnesses += entry.get("alternatives", [])
+ entry["witnesses"] = list({tuple(p): p for p in witnesses}.values())
+ result.append(entry)
+ for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")):
+ base = 8192 + index * 64
+ result.append({"id": name, "min": [base, 72, 2048], "max": [base + 31, 90, 2079],
+ "witnesses": [[base + x, 80, 2056] for x in (13, 14, 15, 16, 17)],
+ "groups": {"supply": [[base >> 4, 2056 >> 4]],
+ "machine": [[(base >> 4) + 1, 2056 >> 4]]}})
+ return result
+
+
+def verdict(errors, **details):
+ return {"status": "FAIL" if errors else "PASS", "failures": errors, **details}
+
+
+def partial_result(definition, before, samples, loaded, group):
+ result = {}
+ old_chunks = {tuple(c["pos"]): c for c in before["chunks"]}
+ new_chunks = {tuple(c["pos"]): c for c in loaded["chunks"]}
+ for f in definition:
+ if "groups" not in f: continue
+ selected = [tuple(p) for p in f["groups"][group]]
+ other = "machine" if group == "supply" else "supply"
+ retained = [tuple(p) for p in f["groups"][other]]
+ errors, stable, transient = [], False, False
+ for sample_index, sample in enumerate(samples):
+ states = {tuple(c["pos"]): c for c in sample["chunks"]}
+ absent = all(not states[p]["loaded"] and states[p]["unloads"] > old_chunks[p]["unloads"] for p in selected)
+ previous_states = {tuple(c["pos"]): c for c in samples[max(0, sample_index - 1)]["chunks"]}
+ duration = sample.get("worldTime", 0) - samples[max(0, sample_index - 1)].get("worldTime", 0)
+ stable |= absent and sample.get("confirmed", False) and duration >= 40 and all(not previous_states[p]["loaded"] for p in selected)
+ transient |= any(states[p]["unloads"] > old_chunks[p]["unloads"] for p in selected)
+ if not all(states[p]["loaded"] and states[p]["unloads"] == old_chunks[p]["unloads"] for p in retained):
+ errors.append("retained endpoint also unloaded")
+ for entry in before["cases"][f["id"]]:
+ if (entry["pos"][0] >> 4, entry["pos"][2] >> 4) not in retained or "tileIdentity" not in entry: continue
+ now = at(sample, f["id"], entry["pos"])
+ if now.get("tileIdentity") != entry["tileIdentity"]: errors.append("retained tile identity changed")
+ if not stable: errors.append("selected endpoint did not stay genuinely unloaded for two seconds")
+ for p in selected:
+ if not new_chunks[p]["loaded"] or new_chunks[p]["loads"] <= old_chunks[p]["loads"]:
+ errors.append("missing real endpoint reload event")
+ for entry in before["cases"][f["id"]]:
+ now = at(loaded, f["id"], entry["pos"])
+ if entry["block"] != now.get("block") or entry["state"] != now.get("state"):
+ errors.append("network block/state changed")
+ if "Tank" in parse(entry) and parse(entry)["Tank"] != parse(now).get("Tank"):
+ errors.append("fluid contents changed")
+ result[f["id"]] = verdict(sorted(set(errors)), stable_unload=stable, real_unload_event=transient)
+ return result
+
+
+def movement_results(definition, snapshot):
+ result = {}
+ for f in definition:
+ if "route" not in f: continue
+ records = sorted((m for m in snapshot["movements"] if m["id"] == f["id"]), key=lambda m: m["step"])
+ errors, steps = [], []
+ if [m["step"] for m in records] != [1, 2, 3, 4]: errors.append("four consecutive moves were not witnessed")
+ for m in records:
+ n = m["step"]
+ local = {**f, "from": f["route"][n - 1], "to": f["route"][n]}
+ checked = move_result(local, m)
+ errors.extend(checked["failures"])
+ if m["before"]["pos"] != local["from"]: errors.append("wrong movement origin")
+ steps.append(checked)
+ result[f["id"]] = verdict(sorted(set(errors)), steps=steps)
+ return result
+
+
+def reserve_results(definition, before, after):
+ result = {}
+ for f in definition:
+ if "route" not in f: continue
+ a, b = at(before, f["id"], f["from"]), at(after, f["id"], f["from"])
+ errors = []
+ expected_reserve = 9 if f["energyType"] == "steam" else 249
+ if a.get("energy", {}).get(f["energyType"]) != expected_reserve:
+ errors.append("insufficient-energy fixture did not have its declared finite reserve")
+ if not 0 <= b.get("energy", {}).get(f["energyType"], -1) <= expected_reserve:
+ errors.append("stalled reserve increased or became invalid")
+ if a["block"] != f["block"] or b.get("block") != f["block"]: errors.append("drill moved with insufficient energy")
+ if contents(a) != contents(b): errors.append("inventory changed while stalled")
+ if any(m["id"] == f["id"] for m in after["movements"]): errors.append("unpaid movement was observed")
+ for pos in f["route"][1:]:
+ if at(after, f["id"], pos).get("block") != f["block"].replace("_drill", "_track"):
+ errors.append("track consumed without sufficient energy")
+ result[f["id"]] = verdict(errors, reserve_before=a["energy"][f["energyType"]], reserve_after=b["energy"][f["energyType"]])
+ return result
+
+
+def expected_items(f):
+ return Counter({"minecraft:dirt": 7, "minecraft:apple": 13,
+ "minecraft:gravel" if f["energyType"] == "steam" else "minecraft:cobblestone":
+ 1 if f["energyType"] == "steam" else 9})
+
+
+def output_results(definition, snapshot, blocked):
+ result = {}
+ for f in definition:
+ if "route" not in f: continue
+ drill, chest = (at(snapshot, f["id"], pos) for pos in (f["to"], f["rear"]))
+ expected = expected_items(f)
+ errors = []
+ ballast = 27 * 64 if blocked else 24 * 64
+ total = counts([drill, chest]); total.subtract({"minecraft:stone": ballast}); total += Counter()
+ if total != expected: errors.append("item loss, duplication or wrong mining yield")
+ if counts([chest])["minecraft:stone"] != ballast: errors.append("full-chest ballast changed")
+ if blocked and counts([drill]) != expected: errors.append("blocked output did not remain in the drill")
+ if not blocked and counts([drill]): errors.append("output did not resume after three slots were freed")
+ tagged = sum(int(s["Count"]) for e in (drill, chest) for s in contents(e)
+ if s.get("tag", {}).get("AdvWorksWitness") == f["id"])
+ if tagged != 7: errors.append("custom item tag lost")
+ result[f["id"]] = verdict(errors, conserved=dict(total), ballast=ballast)
+ return result
+
+
+def redstone_results(definition, before, held, after):
+ result = {}
+ for f in definition:
+ if "route" not in f: continue
+ a, b, c = (at(s, f["id"], f["to"]) for s in (before, held, after))
+ values = [parse(e).get("progress", 0) for e in (a, b, c)]
+ errors = []
+ if values[0] <= 0: errors.append("fixture was not mid-block before interruption")
+ if values[1] != 0: errors.append("redstone failed to cancel mining progress")
+ if values[2] <= 0: errors.append("mining failed to resume after redstone release")
+ if not (contents(a) == contents(b) == contents(c)): errors.append("redstone changed retained inventory")
+ if any(e.get("block") != f["block"] or "facing=" + f["facing"] not in e.get("state", "") for e in (a, b, c)):
+ errors.append("drill position or facing changed")
+ result[f["id"]] = verdict(errors, progress=values)
+ return result
+
+
+def restart_results(definition, before, after):
+ result = {}
+ for f in definition:
+ positions = [f["to"], f["rear"]] if "route" in f else f["witnesses"]
+ errors = []
+ for pos in positions:
+ a, b = at(before, f["id"], pos), at(after, f["id"], pos)
+ if a["block"] != b.get("block") or a["state"] != b.get("state"): errors.append("block state changed")
+ if contents(a) != contents(b): errors.append("inventory changed")
+ if "Tank" in parse(a) and parse(a)["Tank"] != parse(b).get("Tank"): errors.append("tank changed")
+ if "route" in f and pos == f["to"]:
+ if [parse(b).get(k) for k in ("x", "y", "z")] != f["to"]: errors.append("moved tile position changed")
+ result[f["id"]] = verdict(errors)
+ return result
+
+
+class ExtendedRun(LifecycleRun):
+ def build(self):
+ self.probe("load", "loaded")
+ for f in self.definition:
+ self.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace")
+ if "route" in f:
+ self.block(f["controls"][0], "minecraft:redstone_block")
+ self.block(f["from"], f["block"] + "[facing=" + f["facing"] + "]")
+ track = f["block"].replace("_drill", "_track")
+ for pos in f["route"][1:] + f.get("alternatives", []): self.block(pos, track)
+ for pos in f["route"]: self.block(offset(pos, f["direction"]), "minecraft:bedrock")
+ self.inventory(f["from"], 0, 'minecraft:dirt{AdvWorksWitness:"' + f["id"] + '"}', 7)
+ self.inventory(f["from"], 1, "minecraft:apple", 13)
+ elif f["id"] == "C-FLUID":
+ for pos in f["witnesses"][:4]: self.block(pos, "poweradvantage:fluid_pipe")
+ self.block(f["witnesses"][4], "poweradvantage:fluid_metal_tank")
+ else:
+ steam = f["id"] == "C-STEAM"
+ self.block(f["witnesses"][0], "poweradvantage:infinite_" + ("steam" if steam else "electricity"))
+ for pos in f["witnesses"][1:4]: self.block(pos, "steamadvantage:steam_pipe" if steam else "electricadvantage:electric_conduit")
+ self.block(f["witnesses"][4], "steamadvantage:steam_furnace" if steam else "electricadvantage:electric_furnace")
+ if steam: self.inventory(f["witnesses"][4], 0, "minecraft:coal", 64)
+ self.inventory(f["witnesses"][4], 1 if steam else 0, "minecraft:iron_ore", 4)
+ self.probe("offer", "commission-fluid")
+ self.probe("watch", "watching")
+
+ def release_drills(self, step):
+ for f in self.definition:
+ if "route" in f: self.block(f["controls"][step], "minecraft:air")
+
+ def hold_drills(self, step):
+ for f in self.definition:
+ if "route" in f: self.block(f["controls"][step], "minecraft:redstone_block")
+
+ def wait_step(self, step):
+ for attempt in range(30):
+ snap = self.probe("snapshot", f"step-{step}-wait-{attempt:02}")
+ if all(snap.get("routeSteps", {}).get(f["id"], 0) == step for f in self.definition if "route" in f):
+ return self.probe("snapshot", f"step-{step}-complete")
+ time.sleep(0.5)
+ raise AssertionError(f"Native drill step {step} did not complete; raw evidence retained")
+
+ def partial(self, group, cycle):
+ label = f"partial-{cycle}-{group}"
+ before = self.probe("snapshot", label + "-before")
+ self.command("save-all flush")
+ self.probe("release-" + group, label + "-released")
+ samples = []
+ # Bounded observation window includes real event counts even if a live source reloads its neighbour.
+ for n in range(8):
+ time.sleep(1)
+ samples.append(self.probe("snapshot", f"{label}-sample-{n}"))
+ time.sleep(2)
+ confirmed = self.probe("snapshot", label + "-confirmed")
+ # Absence must span both of the final observations, not just one lucky instant.
+ confirmed["confirmed"] = True
+ samples.append(confirmed)
+ loaded = self.probe("load-" + group, label + "-loaded")
+ result = partial_result(self.definition, before, samples, loaded, group)
+ return {"label": label, "group": group, "cases": result}
+
+ def install_blocked_output(self):
+ for f in self.definition:
+ if "route" not in f: continue
+ self.block(f["rear"], "minecraft:chest")
+ for slot in range(27): self.inventory(f["rear"], slot, "minecraft:stone", 64)
+ self.block(f["source"], "poweradvantage:infinite_" + f["energyType"])
+ self.target_plane(f, "minecraft:cobblestone")
+ self.block(offset(f["to"], f["direction"], 2), "minecraft:bedrock")
+ self.release_drills(4)
+
+ def freed_output(self):
+ for f in self.definition:
+ if "route" in f:
+ for slot in range(3): self.inventory(f["rear"], slot, "minecraft:air", 1)
+
+
+def run(args):
+ with socket.socket() as port: port.bind(("127.0.0.1", args.port))
+ profile = profiles(POWER.parents[1])["target113"]
+ manifest = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ for mod in manifest["mods"]:
+ if "sha256" in mod: require(digest((RIG / mod["path"]).resolve()) == mod["sha256"], "Artifact pin mismatch: " + mod["file"])
+ definition = fixtures()
+ args.output.mkdir(parents=True, exist_ok=False)
+ source_hashes = fingerprint(args.migrated / WORLD)
+ write_json(args.output / "source-fingerprint-before.json", source_hashes)
+ report = {"status": "RUNNING", "profiles": {}, "started_at_utc": datetime.now(timezone.utc).isoformat(),
+ "artifacts": [{"file": p.name, "sha256": digest(p)} for p in profile[2]],
+ "protocol": "Native repeated moves and chunk events; finite public reserve priming; external controls; no tile NBT injection"}
+ runner = None
+ try:
+ for index, name in enumerate(("fresh113", "original110-chained113")):
+ runtime = args.output / name
+ launcher = stage(runtime, profile)
+ if index: copy_fixture(args.migrated, runtime)
+ props = runtime / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"): (runtime / marker).touch()
+ write_json(runtime / "lifecycle-fixture.json", definition)
+ runner = ExtendedRun(runtime, args.java8, launcher, definition)
+ runner.boot("extended")
+ runner.build()
+ result = report["profiles"][name] = {"status": "RUNNING"}
+ low = runner.probe("low-prime", "low-before")
+ runner.release_drills(0); time.sleep(2)
+ stalled = runner.probe("snapshot", "low-stalled")
+ result["insufficient_energy"] = reserve_results(definition, low, stalled)
+ runner.hold_drills(0)
+ for step in range(4):
+ runner.probe("prime", f"prime-step-{step + 1}")
+ runner.release_drills(step)
+ moved = runner.wait_step(step + 1)
+ print(f"{name}: native drill step {step + 1}/4 observed", flush=True)
+ result["moves"] = movement_results(definition, moved)
+ time.sleep(50); runner.processing("network-commissioned", 1000)
+ result["partial_networks"] = []
+ for cycle, group in enumerate(("machine", "supply", "supply", "machine"), 1):
+ check = runner.partial(group, cycle)
+ result["partial_networks"].append(check)
+ runner.replenish(); runner.probe("offer", f"partial-fluid-{cycle}")
+ time.sleep(50); runner.processing(f"partial-resumed-{cycle}", (cycle + 1) * 1000)
+ print(f"{name}: {group}-only unload {cycle}, resumed processing PASS; stability recorded", flush=True)
+ write_json(args.output / "extended-results.json", report)
+ # Both endpoints absent, then reload in each order while retaining the loaded half.
+ result["reload_orders"] = []
+ for order in (("machine", "supply"), ("supply", "machine")):
+ label = "order-" + "-".join(order)
+ before = runner.probe("snapshot", label + "-before")
+ runner.probe("release-supply", label + "-release-supply")
+ runner.probe("release-machine", label + "-release-machine")
+ time.sleep(5)
+ absent = runner.probe("snapshot", label + "-absent")
+ first = runner.probe("load-" + order[0], label + "-first")
+ time.sleep(2)
+ settled = runner.probe("snapshot", label + "-settled")
+ loaded = runner.probe("load-" + order[1], label + "-loaded")
+ result["reload_orders"].append({"label": label, "order": list(order),
+ "cases": order_results(definition, before, absent, first, settled, loaded, order)})
+ runner.replenish(); time.sleep(50); runner.processing(label + "-resumed", 5000)
+ runner.install_blocked_output(); time.sleep(45)
+ blocked = runner.probe("snapshot", "blocked-output")
+ result["blocked_output"] = output_results(definition, blocked, True)
+ runner.command("save-all flush"); runner.close(); runner.boot("blocked-restart")
+ time.sleep(1)
+ retained = runner.probe("snapshot", "blocked-retained")
+ result["restart"] = restart_results(definition, blocked, retained)
+ result["blocked_after_restart"] = output_results(definition, retained, True)
+ runner.freed_output(); time.sleep(20)
+ result["resumed_output"] = output_results(definition, runner.probe("snapshot", "freed-output"), False)
+ for f in definition:
+ if "route" in f: runner.target_plane(f, "minecraft:obsidian")
+ time.sleep(4)
+ before = runner.probe("snapshot", "redstone-before")
+ runner.hold_drills(4); time.sleep(2)
+ held = runner.probe("snapshot", "redstone-held")
+ runner.release_drills(4); time.sleep(4)
+ after = runner.probe("snapshot", "redstone-resumed")
+ result["redstone"] = redstone_results(definition, before, held, after)
+ runner.hold_drills(4)
+ runner.command("save-all flush"); runner.close()
+ result["network_processing"] = "PASS"
+ result["status"] = qualification(result)
+ write_json(args.output / "extended-results.json", report)
+ print(f"{name}: {result['status']}; all observations retained", flush=True)
+ report["status"] = "PASS" if all(p["status"] == "PASS" for p in report["profiles"].values()) else "FAIL"
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error), traceback=traceback.format_exc())
+ raise
+ finally:
+ try:
+ if runner: runner.close()
+ except BaseException as error: report.update(status="FAIL", shutdown_error=str(error))
+ after_hashes = fingerprint(args.migrated / WORLD)
+ write_json(args.output / "source-fingerprint-after.json", after_hashes)
+ report["source_fixture_unchanged"] = source_hashes == after_hashes
+ if not report["source_fixture_unchanged"]: report["status"] = "FAIL"
+ for path in args.output.glob("*/input-jars.json"):
+ inputs = json.loads(path.read_text())
+ require(all(digest(path.parent / "mods" / p["file"]) == p["sha256"] for p in inputs), "Staged jar changed")
+ report.setdefault("log_diagnostics", {})[path.parent.name] = [
+ {"file": log.name, "errors": [line for line in log.read_text(encoding="utf-8").splitlines() if "/ERROR]" in line or "/FATAL]" in line],
+ "keep_up_warnings": log.read_text(encoding="utf-8").count("Can't keep up!")}
+ for log in sorted(path.parent.glob("console-*.log"))]
+ report["completed_at_utc"] = datetime.now(timezone.utc).isoformat()
+ write_json(args.output / "extended-results.json", report)
+ print(f"{report['status']}: {args.output}", flush=True)
+ require(report["status"] == "PASS", "Extended lifecycle acceptance failures; inspect retained reports")
+
+
+def order_results(definition, before, absent, first, settled, loaded, order):
+ result = {}
+ old = {tuple(c["pos"]): c for c in before["chunks"]}
+ for f in definition:
+ if "groups" not in f: continue
+ errors = []
+ for group in order:
+ for p in f["groups"][group]:
+ a = next(c for c in absent["chunks"] if c["pos"] == p)
+ b = next(c for c in loaded["chunks"] if c["pos"] == p)
+ if a["loaded"] or a["unloads"] <= old[tuple(p)]["unloads"]: errors.append("both ends were not genuinely absent")
+ if not b["loaded"] or b["loads"] <= old[tuple(p)]["loads"]: errors.append("missing endpoint reload event")
+ for p in f["groups"][order[1]]:
+ state = next(c for c in settled["chunks"] if c["pos"] == p)
+ if state["loaded"]: errors.append("first loaded endpoint automatically loaded the other endpoint")
+ for entry in first["cases"][f["id"]]:
+ if (entry["pos"][0] >> 4, entry["pos"][2] >> 4) not in map(tuple, f["groups"][order[0]]) or "tileIdentity" not in entry: continue
+ if at(loaded, f["id"], entry["pos"]).get("tileIdentity") != entry["tileIdentity"]:
+ errors.append("first endpoint was replaced while loading the second")
+ result[f["id"]] = verdict(sorted(set(errors)))
+ return result
+
+
+def qualification(result):
+ groups = [result[k] for k in ("insufficient_energy", "moves", "blocked_output", "restart", "blocked_after_restart", "resumed_output", "redstone")]
+ groups += [entry["cases"] for k in ("partial_networks", "reload_orders") for entry in result[k]]
+ return "PASS" if result["network_processing"] == "PASS" and all(c["status"] == "PASS" for g in groups for c in g.values()) else "FAIL"
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--migrated", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25587)
+ run(parser.parse_args())
diff --git a/test-rig/migration/lifecycle.py b/test-rig/migration/lifecycle.py
new file mode 100644
index 00000000..b611956d
--- /dev/null
+++ b/test-rig/migration/lifecycle.py
@@ -0,0 +1,434 @@
+"""Real chunk unload/reload and six-facing drill moves in disposable packaged worlds."""
+
+import argparse
+from collections import Counter
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+import socket
+import time
+import traceback
+
+import nbtlib
+from endurance import require
+from matrix import POWER, RIG, WORLD, Server, copy_fixture, digest, fingerprint, plain, profiles, stage, write_json
+
+
+FACINGS = {"down": (0, -1, 0), "up": (0, 1, 0), "north": (0, 0, -1),
+ "south": (0, 0, 1), "west": (-1, 0, 0), "east": (1, 0, 0)}
+
+
+def offset(pos, delta, steps=1):
+ return [a + steps * b for a, b in zip(pos, delta)]
+
+
+def fixtures():
+ result = []
+ for mod in ("steamadvantage", "electricadvantage"):
+ for facing, direction in FACINGS.items():
+ base = 2048 + len(result) * 64
+ origin = [base + 15, 80, 2063]
+ track = (0, 0, 1) if direction[0] else (1, 0, 0)
+ control = (0, 0, 1) if direction[1] else (0, 1, 0)
+ target = offset(origin, track)
+ entry = {"id": ("S-MOVE-" if mod == "steamadvantage" else "E-MOVE-") + facing,
+ "min": [base, 72, 2048], "max": [base + 31, 88, 2079], "from": origin, "to": target,
+ "hold": offset(target, control), "block": mod + (":steam_drill" if mod == "steamadvantage" else ":electric_drill"),
+ "energyType": "steam" if mod == "steamadvantage" else "electricity",
+ "initialEnergy": 40 if mod == "steamadvantage" else 400,
+ "facing": facing, "direction": direction, "control": offset(origin, control),
+ "rear": offset(target, direction, -1), "source": offset(origin, control),
+ "witnesses": [origin, target, offset(origin, direction), offset(target, direction),
+ offset(target, direction, 2), offset(target, direction, -1), offset(target, control)]}
+ if mod == "steamadvantage": entry["witnesses"].append(offset(target, direction, 3))
+ result.append(entry)
+ for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")):
+ base = 4096 + index * 64
+ result.append({"id": name, "min": [base, 72, 2048], "max": [base + 31, 88, 2079],
+ "witnesses": [[base + x, 80, 2056] for x in (13, 14, 15, 16, 17)]})
+ return result
+
+
+def parse(entry):
+ return plain(nbtlib.parse_nbt(entry.get("nbt", "{}")))
+
+
+def at(snapshot, name, pos):
+ return next(entry for entry in snapshot["cases"][name] if entry["pos"] == list(pos))
+
+
+def contents(entry):
+ return parse(entry).get("Items", [])
+
+
+def counts(entries):
+ result = Counter()
+ for entry in entries:
+ for stack in contents(entry): result[stack["id"]] += int(stack["Count"])
+ return result
+
+
+def require_unloaded(before, after):
+ require(len(before["chunks"]) == len(after["chunks"]) > 0, "Chunk witness count changed")
+ old = {tuple(e["pos"]): e for e in before["chunks"]}
+ for chunk in after["chunks"]:
+ require(not chunk["loaded"], f"Chunk did not unload: {chunk['pos']}")
+ require(chunk["unloads"] > old[tuple(chunk["pos"])]["unloads"], f"No real unload event: {chunk['pos']}")
+
+
+def move_result(fixture, movement):
+ before, after = movement["before"], movement["after"]
+ failures = []
+ def check(ok, detail):
+ if not ok: failures.append(detail)
+ check(after["pos"] == fixture["to"], "wrong destination")
+ check([parse(after).get(axis) for axis in ("x", "y", "z")] == fixture["to"], "wrong serialized tile position")
+ check("facing=" + fixture["facing"] in after["state"], "facing changed")
+ check(contents(before) == contents(after), "inventory or custom item NBT changed")
+ check(counts([after]) == {"minecraft:dirt": 7, "minecraft:apple": 13}, "seeded inventory lost or duplicated")
+ check(movement["oldPosition"]["block"] == ("steamadvantage:steam_pipe" if fixture["energyType"] == "steam"
+ else "electricadvantage:electric_conduit"), "old position is not a conduit")
+ old_energy = before["energy"][fixture["energyType"]]
+ new_energy = after["energy"][fixture["energyType"]]
+ expected = 10 if fixture["energyType"] == "steam" else 250
+ # Steam decays 1/16 per normal tick before its eight-tick power update.
+ decay = 0.0625 if fixture["energyType"] == "steam" else 0
+ check(abs((old_energy - new_energy) - expected - decay) < 0.001,
+ f"move energy debit {old_energy - new_energy}, expected {expected}+{decay}")
+ return {"status": "FAIL" if failures else "PASS", "failures": failures,
+ "energy_before": old_energy, "energy_after": new_energy, "expected_move_cost": expected,
+ "inventory_quantity": sum(counts([after]).values())}
+
+
+class LifecycleRun:
+ def __init__(self, runtime, java, launcher, definition):
+ self.runtime, self.java, self.launcher, self.definition = runtime, java, launcher, definition
+ self.server = None
+
+ def boot(self, label): self.server = Server(self.runtime, self.java, self.launcher, label)
+
+ def close(self):
+ if self.server:
+ server, self.server = self.server, None
+ server.close()
+
+ def command(self, command): self.server.send(command)
+
+ def probe(self, operation, label):
+ self.command(f"advlifecycle {operation} {label}")
+ self.server.wait(f"ADVLIFECYCLE COMPLETE {operation} {label}", 120)
+ return json.loads((self.runtime / "advantage-works-results" / f"lifecycle-{label}.json").read_text())
+
+ def block(self, pos, state): self.command("setblock " + " ".join(map(str, pos)) + " " + state)
+
+ def inventory(self, pos, slot, item, count):
+ self.command(f"replaceitem block {' '.join(map(str, pos))} container.{slot} {item} {count}")
+
+ def build(self):
+ self.probe("load", "loaded")
+ for f in self.definition:
+ self.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace")
+ if "from" in f:
+ mod = f["block"].split(":")[0]
+ self.block(f["control"], "minecraft:redstone_block")
+ self.block(f["from"], f["block"] + "[facing=" + f["facing"] + "]")
+ self.block(f["to"], mod + (":steam_track" if mod == "steamadvantage" else ":electric_track"))
+ self.block(offset(f["from"], f["direction"]), "minecraft:bedrock")
+ self.block(offset(f["to"], f["direction"]), "minecraft:bedrock")
+ self.inventory(f["from"], 0, 'minecraft:dirt{AdvWorksWitness:"' + f["id"] + '"}', 7)
+ self.inventory(f["from"], 1, "minecraft:apple", 13)
+ elif f["id"] == "C-FLUID":
+ self.block(f["witnesses"][0], "poweradvantage:fluid_pipe")
+ for pos in f["witnesses"][1:4]: self.block(pos, "poweradvantage:fluid_pipe")
+ self.block(f["witnesses"][4], "poweradvantage:fluid_metal_tank")
+ else:
+ steam = f["id"] == "C-STEAM"
+ self.block(f["witnesses"][0], "poweradvantage:infinite_" + ("steam" if steam else "electricity"))
+ for pos in f["witnesses"][1:4]: self.block(pos, "steamadvantage:steam_pipe" if steam else "electricadvantage:electric_conduit")
+ self.block(f["witnesses"][4], "steamadvantage:steam_furnace" if steam else "electricadvantage:electric_furnace")
+ if steam: self.inventory(f["witnesses"][4], 0, "minecraft:coal", 64)
+ self.inventory(f["witnesses"][4], 1 if steam else 0, "minecraft:iron_ore", 4)
+ self.probe("prime", "primed")
+ self.probe("offer", "first-fluid-offer")
+ self.probe("watch", "watching")
+ for f in self.definition:
+ if "from" in f: self.block(f["control"], "minecraft:air")
+
+ def wait_moves(self):
+ for attempt in range(30):
+ snapshot = self.probe("snapshot", f"moving-{attempt:02}")
+ if len(snapshot["movements"]) == 12: return snapshot
+ time.sleep(1)
+ raise AssertionError("Drills did not all move within 30 seconds")
+
+ def unload_reload(self, name):
+ before = self.probe("snapshot", name + "-before")
+ self.command("save-all flush")
+ self.probe("release", name + "-released")
+ for attempt in range(45):
+ absent = self.probe("snapshot", f"{name}-absent-{attempt:02}")
+ if all(not c["loaded"] for c in absent["chunks"]): break
+ time.sleep(1)
+ require_unloaded(before, absent)
+ # Prove they stay absent; the diagnostic itself must never acquire a chunk.
+ time.sleep(2)
+ absent = self.probe("snapshot", name + "-absent-confirmed")
+ require_unloaded(before, absent)
+ loaded = self.probe("load", name + "-loaded")
+ for old, new in zip(before["chunks"], loaded["chunks"]):
+ require(old["pos"] == new["pos"] and new["loaded"] and new["loads"] > old["loads"], "Missing chunk reload event")
+ for fixture in self.definition:
+ positions = [fixture["to"]] if "to" in fixture else fixture["witnesses"]
+ for pos in positions:
+ old, new = at(before, fixture["id"], pos), at(loaded, fixture["id"], pos)
+ require(old["block"] == new["block"] and old["state"] == new["state"], "Block state changed during chunk reload")
+ require(contents(old) == contents(new), f"Inventory changed during chunk reload: {fixture['id']}")
+ if "tileIdentity" in old:
+ require(new.get("tileIdentity") != old["tileIdentity"], "Old tile object reused, not a real reload")
+ if "to" in fixture:
+ require([parse(new).get(axis) for axis in ("x", "y", "z")] == fixture["to"], "Moved tile reverted its position")
+ energy_type = fixture["energyType"]
+ # Exact reserve comparison applies only to the held, isolated movement witness.
+ # Live mining both consumes and receives power during save/load command ticks.
+ if at(before, fixture["id"], fixture["hold"])["block"] == "minecraft:redstone_block":
+ require(abs(old["energy"][energy_type] - new["energy"][energy_type]) <= (1 if energy_type == "steam" else 0),
+ f"Moved drill reserve reset during chunk reload: {fixture['id']}")
+ require(0 <= new["energy"][energy_type] <= (50 if energy_type == "steam" else 500),
+ "Out-of-range drill energy")
+ if "Tank" in parse(old):
+ require(parse(old)["Tank"] == parse(new).get("Tank"), f"Tank changed during chunk reload: {fixture['id']}")
+ return loaded
+
+ def processing(self, label, fluid_amount):
+ snapshot = self.probe("snapshot", label)
+ for f in self.definition:
+ if f["id"] in ("C-STEAM", "C-ELECTRIC"):
+ items = counts([at(snapshot, f["id"], f["witnesses"][4])])
+ require(items["minecraft:iron_ingot"] == 4 and items["minecraft:iron_ore"] == 0,
+ f"{f['id']}: processing failed: {items}")
+ require(sum(at(snapshot, f["id"], f["witnesses"][4])["energy"].values()) > 0, "No restored power")
+ elif f["id"] == "C-FLUID":
+ tank = parse(at(snapshot, f["id"], f["witnesses"][4])).get("Tank", {})
+ require(tank.get("FluidName") == "minecraft:water" and tank.get("Amount") == fluid_amount, f"Fluid loss: {tank}")
+ return snapshot
+
+ def replenish(self):
+ for f in self.definition:
+ if f["id"] not in ("C-STEAM", "C-ELECTRIC"): continue
+ machine = f["witnesses"][4]
+ size = 7 if f["id"] == "C-STEAM" else 10
+ for slot in range(1 if f["id"] == "C-STEAM" else 0, size):
+ self.inventory(machine, slot, "minecraft:air", 1)
+ self.inventory(machine, 1 if f["id"] == "C-STEAM" else 0, "minecraft:iron_ore", 4)
+
+ def mine(self):
+ for f in self.definition:
+ if "to" not in f: continue
+ self.block(f["rear"], "minecraft:chest")
+ self.block(f["source"], "poweradvantage:infinite_" + f["energyType"])
+ self.target_plane(f, "minecraft:cobblestone")
+ self.block(offset(f["to"], f["direction"], 2), "minecraft:bedrock")
+ self.block(f["hold"], "minecraft:air")
+
+ def mining_results(self, snapshot):
+ result = {}
+ for f in self.definition:
+ if "to" not in f: continue
+ inventory = counts([at(snapshot, f["id"], pos) for pos in (f["to"], f["rear"])])
+ expected = Counter({"minecraft:dirt": 7, "minecraft:apple": 13,
+ "minecraft:gravel" if f["energyType"] == "steam" else "minecraft:cobblestone":
+ 1 if f["energyType"] == "steam" else 9})
+ errors = []
+ if inventory != expected: errors.append(f"incorrect mining yield or item conservation: {dict(inventory)}")
+ if counts([at(snapshot, f["id"], f["rear"])]) != expected: errors.append("not all output reached rear chest")
+ tagged = sum(int(s["Count"]) for pos in (f["to"], f["rear"])
+ for s in contents(at(snapshot, f["id"], pos))
+ if s.get("tag", {}).get("AdvWorksWitness") == f["id"])
+ if tagged != 7: errors.append("custom item NBT lost")
+ drill = at(snapshot, f["id"], f["to"])
+ if "facing=" + f["facing"] not in drill["state"]: errors.append("facing changed")
+ if drill["energy"][f["energyType"]] <= 0: errors.append("no reconnected energy")
+ result[f["id"]] = {"status": "FAIL" if errors else "PASS", "failures": errors, "items": dict(inventory)}
+ return result
+
+ def target_plane(self, fixture, block):
+ center = offset(fixture["to"], fixture["direction"])
+ if fixture["energyType"] == "steam":
+ self.block(center, block)
+ else:
+ axis = next(i for i, value in enumerate(fixture["direction"]) if value)
+ perpendicular = [i for i in range(3) if i != axis]
+ for a in (-1, 0, 1):
+ for b in (-1, 0, 1):
+ pos = center.copy(); pos[perpendicular[0]] += a; pos[perpendicular[1]] += b
+ self.block(pos, block)
+
+ def midblock(self):
+ # Obsidian lasts long enough that completion cannot explain a progress reset.
+ for f in self.definition:
+ if "to" in f: self.target_plane(f, "minecraft:obsidian")
+ time.sleep(5)
+ before = self.probe("snapshot", "midblock-before-chunk")
+ for f in self.definition:
+ if "to" in f: require(parse(at(before, f["id"], f["to"])).get("progress", 0) >= 20,
+ "Mid-block fixture did not begin mining: " + f["id"])
+ self.unload_reload("midblock-chunk")
+ time.sleep(0.25)
+ after = self.probe("snapshot", "midblock-after-chunk")
+ chunk_progress = progress_results(self.definition, before, after)
+ time.sleep(5)
+ before_restart = self.probe("snapshot", "midblock-before-restart")
+ self.command("save-all flush"); self.close()
+ self.boot("midblock-restart")
+ time.sleep(0.25)
+ after_restart = self.probe("snapshot", "midblock-after-restart")
+ restart_progress = progress_results(self.definition, before_restart, after_restart)
+ return {"chunk_reload": chunk_progress, "full_restart": restart_progress}
+
+ def save_read_guards(self):
+ before = self.probe("snapshot", "save-read-before")
+ legacy = self.probe("legacy-read", "save-read-legacy")
+ changed = self.probe("changed-target-read", "save-read-changed")
+ for f in self.definition:
+ if "to" in f:
+ require(contents(at(before, f["id"], f["to"])) == contents(at(changed, f["id"], f["to"])),
+ "Save-read guard changed drill inventory")
+ return {"legacy_read": progress_results(self.definition, before, legacy),
+ "changed_target_read": changed_target_results(self.definition, legacy, changed)}
+
+
+def progress_results(definition, before, after):
+ result = {}
+ for f in definition:
+ if "to" not in f: continue
+ old, new = parse(at(before, f["id"], f["to"])), parse(at(after, f["id"], f["to"]))
+ previous, current = old.get("progress", 0), new.get("progress", 0)
+ target_keys = ("targetX", "targetY", "targetZ")
+ same_target = all(old.get(key) == new.get(key) for key in target_keys)
+ result[f["id"]] = {"status": "PASS" if previous > 0 and current >= previous and same_target else "FAIL",
+ "before": previous, "after": current, "same_target": same_target}
+ return result
+
+
+def changed_target_results(definition, before, after):
+ result = {}
+ for f in definition:
+ if "to" not in f: continue
+ old, new = parse(at(before, f["id"], f["to"])), parse(at(after, f["id"], f["to"]))
+ previous, current = old.get("progress", 0), new.get("progress", 0)
+ cleared = all(key not in new for key in ("targetX", "targetY", "targetZ"))
+ result[f["id"]] = {"status": "PASS" if previous > 0 and current == 0 and cleared else "FAIL",
+ "before": previous, "after": current, "target_cleared": cleared}
+ return result
+
+
+def run(args):
+ with socket.socket() as port: port.bind(("127.0.0.1", args.port))
+ profile = profiles(POWER.parents[1])["target113"]
+ manifest = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ for mod in manifest["mods"]:
+ if "sha256" in mod: require(digest((RIG / mod["path"]).resolve()) == mod["sha256"], "Artifact pin mismatch: " + mod["file"])
+ definition = fixtures()
+ args.output.mkdir(parents=True, exist_ok=False)
+ source_hashes = fingerprint(args.migrated / WORLD)
+ report = {"status": "RUNNING", "started_at_utc": datetime.now(timezone.utc).isoformat(), "profiles": {},
+ "artifacts": [{"file": p.name, "sha256": digest(p)} for p in profile[2]],
+ "protocol": "Real chunk events; native inventory setters; finite movement energy; no tile NBT injection"}
+ runner = None
+ try:
+ for index, name in enumerate(("fresh113", "original110-chained113")):
+ runtime = args.output / name
+ launcher = stage(runtime, profile)
+ if index: copy_fixture(args.migrated, runtime)
+ properties = runtime / "server.properties"
+ properties.write_text(properties.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"):
+ (runtime / marker).touch()
+ write_json(runtime / "lifecycle-fixture.json", definition)
+ runner = LifecycleRun(runtime, args.java8, launcher, definition)
+ runner.boot("lifecycle")
+ runner.build()
+ moved = runner.wait_moves()
+ movements = {m["id"]: m for m in moved["movements"]}
+ movement_results = {f["id"]: move_result(f, movements[f["id"]]) for f in definition if "from" in f}
+ report["profiles"][name] = {"status": "RUNNING", "move_cases": movement_results, "chunk_cycles": 0}
+ write_json(args.output / "lifecycle-results.json", report)
+ time.sleep(60)
+ runner.processing("commissioned", 1000)
+ for cycle in range(1, 4):
+ runner.unload_reload(f"chunk-cycle-{cycle}")
+ runner.replenish()
+ runner.probe("offer", f"fluid-offer-{cycle}")
+ time.sleep(60)
+ runner.processing(f"chunk-resumed-{cycle}", (cycle + 1) * 1000)
+ report["profiles"][name]["chunk_cycles"] = cycle
+ write_json(args.output / "lifecycle-results.json", report)
+ print(f"{name}: chunk unload/reload {cycle} and resumed processing PASS", flush=True)
+ before = runner.probe("snapshot", "before-restart")
+ runner.command("save-all flush"); runner.close()
+ runner.boot("lifecycle-restart")
+ time.sleep(1) # Derived active flags/redstone caches settle through normal power updates.
+ after = runner.probe("snapshot", "after-restart")
+ for f in definition:
+ if "to" not in f: continue
+ old, new = at(before, f["id"], f["to"]), at(after, f["id"], f["to"])
+ require(contents(old) == contents(new) and old["block"] == new["block"]
+ and "facing=" + f["facing"] in new["state"], "Moved drill changed across full restart")
+ runner.processing("restart-retained", 4000)
+ runner.replenish(); runner.probe("offer", "restart-fluid-offer")
+ runner.mine()
+ time.sleep(60)
+ mined = runner.processing("restart-resumed", 5000)
+ mining_results = runner.mining_results(mined)
+ runner.unload_reload("mining-chunk-reload")
+ time.sleep(5)
+ mining_after = runner.mining_results(runner.probe("snapshot", "mining-retained"))
+ midblock_results = runner.midblock()
+ save_read_results = runner.save_read_guards()
+ runner.command("save-all flush"); runner.close()
+ result = {"status": "PASS", "chunk_cycles": 5, "move_cases": movement_results,
+ "full_restart": "PASS", "network_processing": "PASS", "fluid_mB": 5000,
+ "mining": mining_results, "mining_after_chunk_reload": mining_after,
+ "mid_block_progress": midblock_results, "save_read_guards": save_read_results}
+ if any(r["status"] != "PASS" for group in (movement_results, mining_results, mining_after,
+ midblock_results["chunk_reload"], midblock_results["full_restart"],
+ save_read_results["legacy_read"], save_read_results["changed_target_read"])
+ for r in group.values()): result["status"] = "FAIL"
+ report["profiles"][name] = result
+ write_json(args.output / "lifecycle-results.json", report)
+ print(f"{name}: {result['status']}; chunks/restart PASS; movement and mining results recorded", flush=True)
+ require(fingerprint(args.migrated / WORLD) == source_hashes, "Source fixture changed")
+ report["source_fixture_unchanged"] = True
+ report["status"] = "PASS" if all(p["status"] == "PASS" for p in report["profiles"].values()) else "FAIL"
+ except BaseException as error:
+ report["status"], report["failure"], report["traceback"] = "FAIL", str(error), traceback.format_exc()
+ raise
+ finally:
+ try:
+ if runner: runner.close()
+ except BaseException as error:
+ report["status"], report["shutdown_error"] = "FAIL", str(error)
+ report["source_fixture_unchanged"] = fingerprint(args.migrated / WORLD) == source_hashes
+ if not report["source_fixture_unchanged"]: report["status"] = "FAIL"
+ for runtime in sorted(args.output.glob("*/input-jars.json")):
+ inputs = json.loads(runtime.read_text())
+ require(all(digest(runtime.parent / "mods" / jar["file"]) == jar["sha256"] for jar in inputs), "Staged jar changed")
+ diagnostics = []
+ for log in sorted(runtime.parent.glob("console-*.log")):
+ text = log.read_text(encoding="utf-8")
+ diagnostics.append({"file": log.name, "errors": [line for line in text.splitlines() if "/ERROR]" in line or "/FATAL]" in line],
+ "keep_up_warnings": text.count("Can't keep up!")})
+ report.setdefault("log_diagnostics", {})[runtime.parent.name] = diagnostics
+ report["completed_at_utc"] = datetime.now(timezone.utc).isoformat()
+ write_json(args.output / "lifecycle-results.json", report)
+ print(f"{report['status']}: {args.output}", flush=True)
+ require(report["status"] == "PASS", "Lifecycle defects found; inspect retained results")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--migrated", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25586)
+ run(parser.parse_args())
diff --git a/test-rig/migration/matrix.py b/test-rig/migration/matrix.py
new file mode 100644
index 00000000..1e162865
--- /dev/null
+++ b/test-rig/migration/matrix.py
@@ -0,0 +1,412 @@
+"""Packaged, hash-checked migration qualification on disposable worlds only."""
+
+import argparse
+import base64
+import gzip
+import hashlib
+import io
+import json
+from pathlib import Path
+import queue
+import shutil
+import subprocess
+import threading
+import time
+import zipfile
+
+import nbtlib
+
+
+POWER = Path(__file__).resolve().parents[2]
+RIG = POWER / "test-rig"
+WORLD = "AdvantageWorksMigration"
+ORIGINAL_HASHES = {
+ "PowerAdvantage_1.10.2-2.3.0.jar": "47CD509C1141095BFE1C8C7DD689C2ECD3F99C89195E34A88CC9E0B5FC12413E",
+ "SteamAdvantage_1.10.2-2.2.0.jar": "34A0C46C7B36DEF313F35720BD788539BDEECAD3C30880D30D82BC84E35AD9A5",
+ "ElectricAdvantage_1.10.2-2.2.0.jar": "61AE5343A79F518F54F1717D508EA3F6DA7209EE69B0C4433E0B7A9722213EE0",
+}
+LEGACY_112_ELECTRIC_HASH = "CFF96BDCD60770D370E16BDCF34E8E08DF8139804437CA1D368D7080996E7537"
+
+
+def digest(path):
+ return hashlib.sha256(path.read_bytes()).hexdigest().upper()
+
+
+def fingerprint(root):
+ return {str(p.relative_to(root)): digest(p) for p in sorted(root.rglob("*")) if p.is_file()}
+
+
+def write_json(path, value):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(value, indent=2, sort_keys=True), encoding="utf-8")
+
+
+class Server:
+ def __init__(self, runtime, java, launcher, label):
+ self.lines = queue.Queue()
+ self.log = (runtime / f"console-{label}.log").open("w", encoding="utf-8")
+ self.process = subprocess.Popen(
+ [str(java), "-Xms512M", "-Xmx2G", "-Dfml.queryResult=confirm", "-jar", launcher, "nogui"],
+ cwd=runtime, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ text=True, encoding="utf-8", errors="replace", creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
+ )
+ self.thread = threading.Thread(target=self.read, daemon=True)
+ self.thread.start()
+ try:
+ self.wait("Done (", 300)
+ except BaseException:
+ self.close()
+ raise
+
+ def read(self):
+ for line in self.process.stdout:
+ self.log.write(line)
+ self.log.flush()
+ self.lines.put(line)
+
+ def wait(self, token, timeout=60):
+ deadline = time.monotonic() + timeout
+ while time.monotonic() < deadline:
+ try:
+ line = self.lines.get(timeout=0.5)
+ if token in line:
+ return
+ if any(failure in line for failure in ("Migration probe failed", "Actor probe failed", "Recipe probe failed", "Connected probe failed", "ADVRECIPES FAIL", "ADVPROGRESSION FAIL")):
+ raise RuntimeError(line.strip())
+ except queue.Empty:
+ if self.process.poll() is not None:
+ raise RuntimeError(f"Server exited ({self.process.returncode}); see {self.log.name}")
+ raise TimeoutError(f"Waiting for {token!r}; see {self.log.name}")
+
+ def send(self, command):
+ self.process.stdin.write(command + "\n")
+ self.process.stdin.flush()
+
+ def snapshot(self, label, build=False):
+ self.send(f"advprobe {'build' if build else 'snapshot'} {label}")
+ self.wait("ADVPROBE COMPLETE", 120)
+
+ def close(self):
+ if self.process.poll() is None:
+ try:
+ self.send("stop")
+ self.process.wait(timeout=90)
+ except (OSError, subprocess.TimeoutExpired):
+ self.process.kill()
+ self.process.wait(timeout=15)
+ self.thread.join(timeout=5)
+ self.log.close()
+ if self.process.returncode != 0:
+ raise RuntimeError(f"Unclean shutdown ({self.process.returncode}): {self.log.name}")
+
+
+def profiles(workspace):
+ old110 = workspace / "_tmp/pa110-migration-server"
+ old112 = workspace / "_tmp/pa112-smoke-server"
+ maintained110 = workspace / "PA110/PowerAdvantage/run-advantage-works/packaged-current-20260907-223929"
+ seed113 = POWER / "run-advantage-works/packaged-decoupled-20261005-184456"
+ current = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ modern_jars = [(RIG / mod["path"]).resolve() for mod in current["mods"]]
+ # Keep the qualified pre-repair source separate from a rebuilt bridge jar.
+ legacy_electric = POWER / "run-migration-matrix/legacy-release-jars/ElectricAdvantage-2.2.1.112021.jar"
+ if not legacy_electric.is_file():
+ legacy_electric = workspace / "PA112/ElectricAdvantage/ElectricAdvantage/build/libs/ElectricAdvantage-2.2.1.112021.jar"
+ if digest(legacy_electric) != LEGACY_112_ELECTRIC_HASH:
+ raise AssertionError(f"Qualified 1.12 Electric source jar changed: {legacy_electric}")
+ maintained112 = [
+ workspace / "PA112/PowerAdvantage/build/libs/PowerAdvantage-2.4.1.112021.jar",
+ workspace / "PA112/SteamAdvantage/SteamAdvantage/build/libs/SteamAdvantage-2.2.1.112021.jar",
+ legacy_electric,
+ ]
+ old112_jars = [p for p in (old112 / "mods").glob("*.jar") if "AdditionalLootTables" not in p.name]
+ dependencies112 = [p for p in old112_jars if "Advantage" not in p.name]
+ probe = RIG / "migration-probe/build"
+ result = {
+ "early110": (maintained110, "forge-1.10.2-12.18.3.2511-universal.jar",
+ sorted((old110 / "mods").glob("*.jar")) + [probe / "110/libs/AdvantageMigrationProbe-110-1.0.0.jar"]),
+ "maintained110": (maintained110, "forge-1.10.2-12.18.3.2511-universal.jar",
+ [p for p in (maintained110 / "mods").glob("*.jar") if "TestHarness" not in p.name]
+ + [probe / "110/libs/AdvantageMigrationProbe-110-1.0.0.jar"]),
+ "legacy112": (old112, "forge-1.12.2-14.23.5.2859.jar", old112_jars
+ + [probe / "112/libs/AdvantageMigrationProbe-112-1.0.0.jar"]),
+ "maintained112": (old112, "forge-1.12.2-14.23.5.2859.jar", maintained112 + dependencies112
+ + [probe / "112/libs/AdvantageMigrationProbe-112-1.0.0.jar"]),
+ "target113": (seed113, "forge-1.13.2-25.0.223.jar", modern_jars),
+ }
+ sylvester = workspace.parent / "MinecraftMineralogy/1 Sylvester/mods"
+ originals = [sylvester / "PowerAdvantage_1.10.2-2.3.0.jar",
+ sylvester / "SteamAdvantage_1.10.2-2.2.0.jar",
+ POWER / "run-migration-matrix/original-release-jars/ElectricAdvantage_1.10.2-2.2.0.jar"]
+ if all(p.is_file() for p in originals):
+ for jar in originals:
+ if digest(jar) != ORIGINAL_HASHES[jar.name]:
+ raise AssertionError(f"Original-release hash mismatch: {jar}")
+ with zipfile.ZipFile(jar) as archive:
+ if not any(name.startswith("cyano/") and name.endswith(".class") for name in archive.namelist()):
+ raise AssertionError(f"Not an original Cyano release: {jar}")
+ seed, launcher, jars = result["early110"]
+ result["original110"] = (seed, launcher, originals + [p for p in jars if "Advantage" not in p.name]
+ + [probe / "110/libs/AdvantageMigrationProbe-110-1.0.0.jar"])
+ return result
+
+
+def selected_profiles(args):
+ if args.profile_manifest is None: return profiles(args.workspace)
+ manifest = json.loads(args.profile_manifest.read_text())
+ result = {}
+ for name, entry in manifest.items():
+ jars = []
+ for jar in entry["jars"]:
+ path = Path(jar["path"])
+ if not path.is_absolute(): path = args.profile_manifest.parent / path
+ if digest(path) != jar["sha256"]:
+ raise AssertionError(f"Profile jar hash mismatch: {path}")
+ jars.append(path.resolve())
+ seed = Path(entry["seed"])
+ if not seed.is_absolute(): seed = args.profile_manifest.parent / seed
+ result[name] = (seed.resolve(), entry["launcher"], jars)
+ return result
+
+
+def intermediate_profile(available):
+ """An explicit bridge override must not replace the historical source profile."""
+ return available["bridge112"] if "bridge112" in available else available["maintained112"]
+
+
+def stage(runtime, profile):
+ seed, launcher, mods = profile
+ runtime.mkdir(parents=True, exist_ok=False)
+ shutil.copytree(seed / "libraries", runtime / "libraries")
+ for p in seed.glob("*.jar"):
+ if "installer" not in p.name:
+ shutil.copy2(p, runtime / p.name)
+ (runtime / "mods").mkdir()
+ identities = []
+ for jar in mods:
+ shutil.copy2(jar, runtime / "mods" / jar.name)
+ identities.append({"file": jar.name, "source": str(jar), "sha256": digest(jar)})
+ write_json(runtime / "input-jars.json", identities)
+ (runtime / "eula.txt").write_text("eula=true\n")
+ (runtime / "advantage-migration.allow").touch()
+ (runtime / "server.properties").write_text(
+ f"level-name={WORLD}\nlevel-type=FLAT\nonline-mode=false\nserver-ip=127.0.0.1\nserver-port=25584\n"
+ "spawn-protection=0\nview-distance=4\nmax-tick-time=60000\nallow-nether=false\n"
+ "generator-settings=3;minecraft:bedrock,2*minecraft:dirt,minecraft:grass;1;\n"
+ )
+ return launcher
+
+
+def parse_nbt(entry):
+ return nbtlib.File.parse(io.BytesIO(gzip.decompress(base64.b64decode(entry["nbtBase64"])))).unpack()
+
+
+def plain(value):
+ if isinstance(value, dict): return {k: plain(v) for k, v in value.items()}
+ if isinstance(value, (list, tuple)): return [plain(v) for v in value]
+ if hasattr(value, "tolist"): return value.tolist()
+ if hasattr(value, "item"): return value.item()
+ return value
+
+
+def normalized(value, enchantments=None):
+ if isinstance(value, dict):
+ result = {k: normalized(v, enchantments) for k, v in value.items() if not (k == "ForgeCaps" and not v)}
+ if "Empty" in result: result["Empty"] = True
+ for key in ("FluidName", "FluidID"):
+ if key in result and ":" not in result[key]:
+ result[key] = ("poweradvantage:" if result[key] in ("crude_oil", "refined_oil") else "minecraft:") + result[key]
+ identity = result.get("id", "")
+ if not isinstance(identity, str):
+ if "lvl" in result and enchantments is not None:
+ result["id"] = enchantments.get(identity, identity)
+ return result
+ if "advantage." in identity and "Count" not in result:
+ result["id"] = identity.replace("minecraft:", "").replace("advantage.", "advantage:", 1)
+ if identity == "Chest": result["id"] = "minecraft:chest"
+ if result.get("id") == "electricadvantage:l_e_d": result["id"] = "electricadvantage:led"
+ if "Count" in result:
+ if identity == "forge:bucketfilled" and "tag" in result:
+ fluid = result["tag"].get("FluidName")
+ if fluid in ("poweradvantage:crude_oil", "poweradvantage:refined_oil") and result["tag"].get("Amount") == 1000:
+ result["id"] = fluid + "_bucket"
+ result["tag"].pop("FluidName", None)
+ result["tag"].pop("Amount", None)
+ if not result["tag"]: result.pop("tag")
+ damage = result.pop("Damage", 0)
+ if damage:
+ result.setdefault("tag", {})["Damage"] = damage
+ if "tag" in result and "ench" in result["tag"]:
+ result["tag"]["Enchantments"] = result["tag"].pop("ench")
+ return result
+ if isinstance(value, list): return [normalized(v, enchantments) for v in value]
+ return value
+
+
+def differences(expected, actual, path=""):
+ result = []
+ if isinstance(expected, dict) and isinstance(actual, dict):
+ for key, value in expected.items():
+ if key not in actual: result.append({"path": f"{path}.{key}", "expected": value, "actual": ""})
+ else: result.extend(differences(value, actual[key], f"{path}.{key}"))
+ elif isinstance(expected, list) and isinstance(actual, list):
+ if len(expected) != len(actual): result.append({"path": path, "expected": expected, "actual": actual})
+ else:
+ for i, value in enumerate(expected): result.extend(differences(value, actual[i], f"{path}[{i}]"))
+ elif expected != actual:
+ result.append({"path": path, "expected": expected, "actual": actual})
+ return result
+
+
+def compare(source, target):
+ before = json.loads(source.read_text())
+ after = json.loads(target.read_text())
+ findings = []
+ if len(before) != len(after): raise AssertionError("Witness count changed")
+ counts = {"blocks": len(before), "tiles": 0, "items": 0}
+ def enchantment_ids(snapshot):
+ level = nbtlib.load(snapshot.parent / WORLD / "level.dat")
+ entries = level.get("FML", {}).get("Registries", {}).get("minecraft:enchantments", {}).get("ids", [])
+ return {int(e["V"]): str(e["K"]) for e in entries}
+ old_enchantments = enchantment_ids(source)
+ new_enchantments = enchantment_ids(target)
+ for old, new in zip(before, after):
+ pos = f"{old['name']}@{old['x']},{old['y']},{old['z']}"
+ if old["actualName"] != new["actualName"]:
+ findings.append({"path": pos, "expected": old["actualName"], "actual": new["actualName"]})
+ findings.extend(differences(old["properties"], new["properties"], pos + ".state"))
+ if "nbt" in old:
+ counts["tiles"] += 1
+ old_nbt = normalized(plain(parse_nbt(old)), old_enchantments)
+ counts["items"] += sum(int(v.get("Count", 0)) for v in old_nbt.get("Items", []))
+ if "nbt" not in new: findings.append({"path": pos + ".tile", "expected": "tile", "actual": ""})
+ else: findings.extend(differences(old_nbt, normalized(plain(parse_nbt(new)), new_enchantments), pos + ".nbt"))
+ return {"status": "FAIL" if findings else "PASS", "witnesses": counts, "differences": findings}
+
+
+def start_snapshot(runtime, java, launcher, label, build=False):
+ server = Server(runtime, java, launcher, label)
+ try:
+ server.snapshot(label, build)
+ server.send("save-all flush")
+ time.sleep(2)
+ finally:
+ server.close()
+
+
+def copy_fixture(source, destination):
+ shutil.copytree(source / WORLD, destination / WORLD)
+ shutil.copy2(source / "migration-fixture.json", destination / "migration-fixture.json")
+ if (source / "config").exists(): shutil.copytree(source / "config", destination / "config")
+
+
+def run(args):
+ available = selected_profiles(args)
+ if args.sources and not set(args.sources).issubset(available):
+ raise ValueError(f"Unavailable source profiles: {set(args.sources) - set(available)}")
+ root = args.output or POWER / "run-migration-matrix" / time.strftime("%Y%m%d-%H%M%S")
+ root.mkdir(parents=True, exist_ok=False)
+ write_json(root / "profiles.json", {key: {"seed": str(p[0]), "launcher": p[1], "jars": [{"path": str(j), "sha256": digest(j)} for j in p[2]]}
+ for key, p in available.items()})
+ sources = {}
+ results = {}
+ for name in ("original110", "early110", "maintained110", "legacy112", "maintained112"):
+ if name not in available: continue
+ if args.sources and name not in args.sources: continue
+ print(f"{'Reusing immutable' if args.fixtures else 'Building saved'} baseline: {name}", flush=True)
+ runtime = root / name
+ if args.fixtures:
+ runtime = args.fixtures / name
+ expected = json.loads((runtime / "fixture-hashes.json").read_text())
+ if fingerprint(runtime / WORLD) != expected: raise AssertionError(f"Source hash mismatch: {runtime}")
+ else:
+ launcher = stage(runtime, available[name])
+ start_snapshot(runtime, args.java8, launcher, "source", build=True)
+ write_json(runtime / "fixture-hashes.json", fingerprint(runtime / WORLD))
+ sources[name] = runtime
+ for name, source in sources.items():
+ routes = ["direct"] + (["chained"] if name.endswith("110") else [])
+ for route in routes:
+ label = name + "-" + route
+ print(f"Qualifying {label}", flush=True)
+ fixture = source
+ intermediate_result = None
+ if route == "chained":
+ intermediate = root / (label + "-112")
+ launcher = stage(intermediate, intermediate_profile(available))
+ copy_fixture(source, intermediate)
+ start_snapshot(intermediate, args.java8, launcher, "intermediate")
+ fixture = intermediate
+ intermediate_result = compare(source / "migration-source.json", intermediate / "migration-intermediate.json")
+ write_json(intermediate / "state-comparison.json", intermediate_result)
+ target = root / (label + "-113")
+ launcher = stage(target, available["target113"])
+ copy_fixture(fixture, target)
+ start_snapshot(target, args.java8, launcher, "converted")
+ first = compare(source / "migration-source.json", target / "migration-converted.json")
+ start_snapshot(target, args.java8, launcher, "restarted")
+ second = compare(source / "migration-source.json", target / "migration-restarted.json")
+ stable = compare(target / "migration-converted.json", target / "migration-restarted.json")
+ results[label] = {"conversion": first, "restart": second, "idempotence": stable}
+ if intermediate_result is not None: results[label]["intermediate"] = intermediate_result
+ write_json(target / "state-comparison.json", results[label])
+ write_json(root / "matrix-results.json", results)
+ print(f"{label}: conversion={first['status']} restart={second['status']} idempotence={stable['status']}", flush=True)
+ expected = json.loads((source / "fixture-hashes.json").read_text())
+ if fingerprint(source / WORLD) != expected: raise AssertionError(f"Immutable source changed: {source}")
+ print(f"Reports: {root}", flush=True)
+ if not results: raise AssertionError("No migration routes ran")
+ return 1 if any(r[stage]["status"] != "PASS" for r in results.values() for stage in r) else 0
+
+
+def operate(args):
+ """Commission the existing Works stations in a copy of a converted world."""
+ root = args.output
+ launcher = stage(root, selected_profiles(args)["target113"])
+ copy_fixture(args.operate, root)
+ (root / "migration-operate.allow").touch()
+ (root / "advantage-works.allow").touch()
+ stations = ["W-01", "W-04", "S-01", "S-04", "S-06", "E-01", "E-02", "E-03", "P-API-01"]
+ checked = stations + ["S-05", "E-06"]
+ source_hashes = fingerprint(args.operate / WORLD)
+ for phase in ("operation", "operation-restart"):
+ server = Server(root, args.java8, launcher, phase)
+ try:
+ if phase == "operation":
+ server.send("advworks build all")
+ server.wait("Advantage Works built", 120)
+ for station in stations:
+ server.send(f"advworks start {station}")
+ server.wait("Started " + station)
+ time.sleep(5)
+ server.send(f"advworks stop {station}")
+ server.wait("Stopped " + station)
+ time.sleep(5)
+ for station in checked:
+ server.send(f"advworks check {station}")
+ server.wait("Advantage Works check:")
+ server.send("save-all flush")
+ time.sleep(2)
+ finally:
+ server.close()
+ reports = root / "advantage-works-results"
+ statuses = {station: json.loads((reports / (station + "-latest.json")).read_text())["status"] for station in checked}
+ write_json(root / (phase + "-results.json"), statuses)
+ if any(s in ("FAIL", "EXPECTED_FAIL", "UNEXPECTED_PASS") for s in statuses.values()):
+ raise AssertionError(f"Station failure: {statuses}")
+ print(f"{phase}: automated assertions accepted for {len(statuses)} stations", flush=True)
+ if fingerprint(args.operate / WORLD) != source_hashes: raise AssertionError("Converted source fixture changed")
+ return 0
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--workspace", type=Path, default=POWER.parents[1])
+ parser.add_argument("--profile-manifest", type=Path, help="Hash-pinned input profiles; paths may be relative to this file")
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path)
+ parser.add_argument("--sources", nargs="+")
+ parser.add_argument("--fixtures", type=Path, help="Reuse an existing hash-checked immutable fixture set")
+ parser.add_argument("--operate", type=Path, help="Commission a disposable copy of this converted runtime")
+ args = parser.parse_args()
+ if args.operate and args.output is None: parser.error("--operate requires --output")
+ raise SystemExit(operate(args) if args.operate else run(args))
diff --git a/test-rig/migration/oil_transfer.py b/test-rig/migration/oil_transfer.py
new file mode 100644
index 00000000..017e832b
--- /dev/null
+++ b/test-rig/migration/oil_transfer.py
@@ -0,0 +1,154 @@
+"""Native oil buckets, drains, pipes and both containers across complete server restarts."""
+import argparse
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+import socket
+import time
+
+from endurance import require
+from lifecycle import LifecycleRun, at, parse
+from matrix import POWER, RIG, digest, stage, write_json
+
+
+def fixtures():
+ result = []
+ for index, (oil, tank) in enumerate((
+ (oil, tank) for oil in ("crude_oil", "refined_oil")
+ for tank in ("fluid_storage_tank", "fluid_metal_tank"))):
+ x, y, z = 11264 + index * 64, 80, 2056
+ source, drain = [x + 8, y + 1, z], [x + 8, y, z]
+ # Reproduce the player's descending pipe layout, then turn horizontally into the tank.
+ pipes = [[x + 8, y - step, z] for step in (1, 2, 3)]
+ pipes += [[x + step, y - 3, z] for step in (9, 10, 11)]
+ output = [x + 12, y - 3, z]
+ result.append({"id": f"OIL-{oil}-{tank}", "min": [x, 72, 2048], "max": [x + 31, 88, 2079],
+ "source": source, "drain": drain, "pipes": pipes, "output": output,
+ "fluid": "poweradvantage:" + oil, "bucket": "poweradvantage:" + oil + "_bucket",
+ "tank": "poweradvantage:" + tank, "witnesses": [source, drain, *pipes, output]})
+ return result
+
+
+def inspect(snapshot, definition, expected):
+ """Exact oracle: filling some tank with some fluid is not sufficient."""
+ rows = []
+ for f in definition:
+ source = at(snapshot, f["id"], f["source"])
+ drain = at(snapshot, f["id"], f["drain"])
+ output = at(snapshot, f["id"], f["output"])
+ require(source["block"] == "minecraft:air", f"Source not collected: {f['id']}")
+ require(drain["block"] == "poweradvantage:fluid_drain", f"Drain missing: {f['id']}")
+ require(output["block"] == f["tank"], f"Container changed: {f['id']}")
+ for pos in f["pipes"]:
+ require(at(snapshot, f["id"], pos)["block"] == "poweradvantage:fluid_pipe", f"Pipe missing: {f['id']}")
+ held = parse(drain).get("Tank", {}).get("Amount", 0)
+ tank = parse(output).get("Tank", {})
+ require(held == 0, f"Oil stuck in drain ({held} mB): {f['id']}")
+ require(tank.get("Amount") == expected, f"Lost/duplicated oil: {f['id']}: {tank}")
+ require(tank.get("FluidName") == f["fluid"], f"Oil changed identity: {f['id']}: {tank}")
+ rows.append({"id": f["id"], "status": "PASS", "fluid": f["fluid"], "tank": f["tank"],
+ "drain_mB": held, "container_mB": tank["Amount"], "total_mB": held + tank["Amount"]})
+ return rows
+
+
+def require_same_machines(before, after, definition):
+ for f in definition:
+ for pos in (f["drain"], f["output"]):
+ old, new = at(before, f["id"], pos), at(after, f["id"], pos)
+ require("tileIdentity" in old and old["tileIdentity"] == new.get("tileIdentity"),
+ f"Refill replaced a machine: {f['id']} {pos}")
+
+
+def run(args):
+ output = args.output.resolve()
+ require(output.is_relative_to(POWER.resolve()) and output != POWER.resolve(), "Output must be inside PowerAdvantage")
+ require(not output.exists(), "Use a new disposable output directory")
+ with socket.socket() as port:
+ port.bind(("127.0.0.1", args.port))
+ manifest = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ jars = []
+ for mod in manifest["mods"]:
+ jar = (RIG / mod["path"]).resolve()
+ require(digest(jar) == mod["sha256"], "Artifact pin mismatch: " + mod["file"])
+ jars.append(jar)
+ seed = POWER / "run-advantage-works/packaged-decoupled-20261005-184456"
+ launcher = stage(output, (seed, "forge-1.13.2-25.0.223.jar", jars))
+ props = output / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"):
+ (output / marker).touch()
+ definition = fixtures()
+ write_json(output / "lifecycle-fixture.json", definition)
+ runner = LifecycleRun(output, args.java8, launcher, definition)
+ report = {"status": "RUNNING", "phases": [], "full_process_restarts": 0,
+ "inputs": json.loads((output / "input-jars.json").read_text())}
+ try:
+ runner.boot("oil-first-fill")
+ runner.probe("load", "oil-loaded")
+ for f in definition:
+ runner.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace")
+ for pos in f["pipes"]: runner.block(pos, "poweradvantage:fluid_pipe")
+ runner.block(f["drain"], "poweradvantage:fluid_drain")
+ runner.block(f["output"], f["tank"])
+ x, y, z = f["source"]
+ for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1)):
+ runner.block([x + dx, y, z + dz], "minecraft:glass")
+ empty = runner.probe("snapshot", "oil-empty")
+ for f in definition:
+ for pos in (f["drain"], f["output"]):
+ require(parse(at(empty, f["id"], pos)).get("Tank", {}).get("Amount", 0) == 0, "Fixture was not empty")
+ runner.probe("place-buckets", "oil-first-buckets")
+ time.sleep(args.seconds)
+ first = runner.probe("snapshot", "oil-first-fill")
+ report["phases"].append({"phase": "first-fill", "cases": inspect(first, definition, 1000)})
+ require_same_machines(empty, first, definition)
+ runner.command("save-all flush")
+ runner.close()
+
+ runner.boot("oil-restart-refill")
+ report["full_process_restarts"] += 1
+ runner.probe("load", "oil-restart-loaded")
+ time.sleep(args.seconds)
+ restored = runner.probe("snapshot", "oil-restored")
+ report["phases"].append({"phase": "restored", "cases": inspect(restored, definition, 1000)})
+ # Only add new world-fluid sources: no tile.read(), block replacement, API offers or manual tank filling.
+ runner.probe("place-buckets", "oil-refill-buckets")
+ time.sleep(args.seconds)
+ refilled = runner.probe("snapshot", "oil-refilled")
+ report["phases"].append({"phase": "refilled", "cases": inspect(refilled, definition, 2000)})
+ require_same_machines(restored, refilled, definition)
+ runner.command("save-all flush")
+ runner.close()
+
+ runner.boot("oil-second-restart")
+ report["full_process_restarts"] += 1
+ runner.probe("load", "oil-second-restart-loaded")
+ time.sleep(args.seconds)
+ retained = runner.probe("snapshot", "oil-retained")
+ report["phases"].append({"phase": "retained", "cases": inspect(retained, definition, 2000)})
+ runner.command("save-all flush")
+ report["status"] = "PASS"
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ try:
+ runner.close()
+ except BaseException as error:
+ report.update(status="FAIL", shutdown_failure=str(error))
+ raise
+ finally:
+ write_json(output / "oil-transfer-results.json", report)
+ print(f"Oil transfer: PASS, four lanes, two full restarts. Evidence: {output}", flush=True)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True, help="Java 8 executable")
+ parser.add_argument("--output", type=Path, default=POWER / "run-oil-transfer" /
+ datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S"))
+ parser.add_argument("--port", type=int, default=25588)
+ parser.add_argument("--seconds", type=int, default=8)
+ args = parser.parse_args()
+ require(args.seconds >= 2, "Allow at least two seconds for the eight-tick distribution cycle")
+ run(args)
diff --git a/test-rig/migration/partial_network_trace.py b/test-rig/migration/partial_network_trace.py
new file mode 100644
index 00000000..08505e4b
--- /dev/null
+++ b/test-rig/migration/partial_network_trace.py
@@ -0,0 +1,72 @@
+"""Capture real chunk acquisition callers in isolated, marked network fixtures."""
+import argparse
+from datetime import datetime, timezone
+import json
+import socket
+import time
+from pathlib import Path
+
+from extended_lifecycle import ExtendedRun, fixtures, order_results
+from endurance import require
+from matrix import POWER, RIG, digest, profiles, stage, write_json
+
+
+def run(args):
+ with socket.socket() as port:
+ port.bind(("127.0.0.1", args.port))
+ manifest = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ for mod in manifest["mods"]:
+ if "sha256" in mod:
+ require(digest((RIG / mod["path"]).resolve()) == mod["sha256"], "Artifact pin mismatch: " + mod["file"])
+ definition = [f for f in fixtures() if "groups" in f]
+ launcher = stage(args.output, profiles(POWER.parents[1])["target113"])
+ props = args.output / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"):
+ (args.output / marker).touch()
+ write_json(args.output / "lifecycle-fixture.json", definition)
+ runner = ExtendedRun(args.output, args.java8, launcher, definition)
+ report = {"started_at_utc": datetime.now(timezone.utc).isoformat(), "partial_networks": [], "reload_orders": []}
+ try:
+ runner.boot("partial-trace")
+ runner.build()
+ time.sleep(50)
+ runner.processing("commissioned", 1000)
+ runner.probe("trace-loads", "trace-enabled")
+ for cycle, group in enumerate(("machine", "supply"), 1):
+ result = runner.partial(group, cycle)
+ report["partial_networks"].append(result)
+ print(result, flush=True)
+ for order in (("machine", "supply"), ("supply", "machine")):
+ label = "order-" + "-".join(order)
+ before = runner.probe("snapshot", label + "-before")
+ runner.probe("release-supply", label + "-release-supply")
+ runner.probe("release-machine", label + "-release-machine")
+ time.sleep(5)
+ absent = runner.probe("snapshot", label + "-absent")
+ first = runner.probe("load-" + order[0], label + "-first")
+ time.sleep(2)
+ settled = runner.probe("snapshot", label + "-settled")
+ loaded = runner.probe("load-" + order[1], label + "-loaded")
+ result = {"label": label, "cases": order_results(definition, before, absent, first, settled, loaded, order)}
+ report["reload_orders"].append(result)
+ print(result, flush=True)
+ report["chunk_events"] = loaded["chunkEvents"]
+ report["status"] = "PASS" if all(c["status"] == "PASS" for r in report["partial_networks"] + report["reload_orders"] for c in r["cases"].values()) else "FAIL"
+ runner.command("save-all flush")
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ runner.close()
+ report["completed_at_utc"] = datetime.now(timezone.utc).isoformat()
+ write_json(args.output / "partial-trace-results.json", report)
+ require(report["status"] == "PASS", "Partial-unload acceptance failures; raw load stacks retained")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25587)
+ run(parser.parse_args())
diff --git a/test-rig/migration/pin_dimension_profiles.py b/test-rig/migration/pin_dimension_profiles.py
new file mode 100644
index 00000000..4f0a78b5
--- /dev/null
+++ b/test-rig/migration/pin_dimension_profiles.py
@@ -0,0 +1,55 @@
+"""Replace only test probes in an existing hash-pinned qualification manifest."""
+
+import argparse
+import json
+from pathlib import Path
+import shutil
+import zipfile
+
+from matrix import POWER, digest, write_json
+
+
+def pin(base, output, archive=None):
+ profiles = json.loads(base.read_text())
+ expected_classes = {
+ "AdvantageMigrationProbe-110-1.0.0.jar": "zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.class",
+ "AdvantageMigrationProbe-112-1.0.0.jar": "zone/moddev/mc/advantageworks/migration/LegacyDimensionEntities.class",
+ "AdvantageWorksTestHarness-1.1.0.113021.jar": "zone/moddev/mc/advantageworks/DimensionEntityProbe.class",
+ }
+ for profile in profiles.values():
+ for jar in profile["jars"]:
+ previous = Path(jar["path"])
+ if previous.name in expected_classes:
+ if "MigrationProbe" in previous.name:
+ target = previous.name.split("-")[1]
+ path = POWER / "test-rig/migration-probe/build" / target / "libs" / previous.name
+ else:
+ path = POWER / "test-rig/build/libs" / previous.name
+ with zipfile.ZipFile(path) as contents:
+ if expected_classes[previous.name] not in contents.namelist():
+ raise AssertionError("Dimension probe missing: " + str(path))
+ if archive is not None:
+ staged = archive.resolve() / digest(path) / path.name
+ staged.parent.mkdir(parents=True, exist_ok=True)
+ if staged.exists() and digest(staged) != digest(path):
+ raise AssertionError("Archived probe changed: " + str(staged))
+ if not staged.exists():
+ shutil.copy2(path, staged)
+ path = staged
+ jar.update(path=str(path), sha256=digest(path))
+ elif digest(previous) != jar["sha256"]:
+ raise AssertionError("Production/dependency artifact changed: " + str(previous))
+ write_json(output, profiles)
+ write_json(output.with_suffix(".provenance.json"), {"base_manifest": str(base.resolve()),
+ "base_manifest_sha256": digest(base), "replacements": sorted(expected_classes),
+ "production_and_dependency_pins": "UNCHANGED"})
+ return profiles
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--base", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--archive", type=Path, help="Retain test probe bytes outside mutable build outputs")
+ args = parser.parse_args()
+ pin(args.base, args.output, args.archive)
diff --git a/test-rig/migration/pin_drill_profiles.py b/test-rig/migration/pin_drill_profiles.py
new file mode 100644
index 00000000..cbb47db6
--- /dev/null
+++ b/test-rig/migration/pin_drill_profiles.py
@@ -0,0 +1,100 @@
+"""Archive pre-fix artifacts or pin explicit repaired legacy runtime profiles."""
+import argparse
+import copy
+import json
+from pathlib import Path
+import re
+import shutil
+
+from matrix import POWER, digest, write_json
+
+WORKSPACE = POWER.parents[1]
+REPOS = {version + "-" + mod: WORKSPACE / ("PA" + version) / mod / mod
+ for version in ("110", "112") for mod in ("SteamAdvantage", "ElectricAdvantage")}
+
+
+def archive(path, output):
+ sha = digest(path)
+ target = output / "artifacts" / sha / path.name
+ target.parent.mkdir(parents=True, exist_ok=True)
+ if not target.exists():
+ shutil.copy2(path, target)
+ if digest(target) != sha:
+ raise AssertionError("Artifact changed during archival")
+ return dict(path=str(target.resolve()), sha256=sha)
+
+
+def run(args):
+ args.output.mkdir(parents=True, exist_ok=False)
+ if args.mode == "archive":
+ saved = []
+ for name, repo in REPOS.items():
+ for folder in ("build/libs", "build/libs-dev", "build/test-results/test"):
+ for path in (repo / folder).glob("*"):
+ if path.is_file() and path.suffix in (".jar", ".xml"):
+ saved.append(dict(repo=name, source=str(path), artifact=archive(path, args.output)))
+ write_json(args.output / "before-fix-artifacts.json", saved)
+ print("Archived", len(saved), "pre-fix artifacts and test records")
+ return
+ if args.mode == "workspace-profiles":
+ changes = []
+ for path in (WORKSPACE / "PA110/PowerAdvantage/test-rig/profiles").glob("*.json"):
+ profile = json.loads(path.read_text())
+ previous = digest(path)
+ updated = []
+ for mod in profile["mods"]:
+ if mod["id"] not in ("steamadvantage", "electricadvantage") or mod["version"] != "2.2.4.110021":
+ continue
+ name = "SteamAdvantage" if mod["id"] == "steamadvantage" else "ElectricAdvantage"
+ folder = "libs-dev" if mod["file"].endswith("-deobf.jar") else "libs"
+ for key in ("version", "file", "path"):
+ mod[key] = mod[key].replace("2.2.4.110021", "2.2.5.110021")
+ if "sha256" in mod:
+ mod["sha256"] = digest(REPOS["110-" + name] / "build" / folder / mod["file"])
+ updated.append(mod["id"])
+ if updated:
+ path.write_text(json.dumps(profile, indent=2) + "\n", encoding="utf-8")
+ changes.append(dict(path=str(path), before=previous, after=digest(path), mods=updated))
+ write_json(args.output / "workspace-profile-updates.json", changes)
+ print("Updated", len(changes), "mutable 1.10 profiles; archived migration profiles untouched")
+ return
+ profiles = json.loads(args.base.read_text())
+ before = copy.deepcopy(profiles)
+ profiles["native110"] = copy.deepcopy(profiles["maintained110"])
+ replacements = []
+ for version, profile_name in (("110", "native110"), ("112", "bridge112")):
+ for mod in ("SteamAdvantage", "ElectricAdvantage"):
+ jars = [p for p in (REPOS[version + "-" + mod] / "build/libs").glob(mod + "-*.jar")
+ if re.fullmatch(mod + r"-\d+\.\d+\.\d+\.\d+\.jar", p.name)]
+ if len(jars) != 1:
+ raise AssertionError("Build exactly one repaired release first: " + mod + version)
+ for index, jar in enumerate(profiles[profile_name]["jars"]):
+ if Path(jar["path"]).name.startswith(mod + "-"):
+ replacement = archive(jars[0], args.output)
+ replacements.append(dict(profile=profile_name, previous=jar, replacement=replacement))
+ profiles[profile_name]["jars"][index] = replacement
+ break
+ else:
+ raise AssertionError("Missing original mod in profile")
+ for profile in profiles.values():
+ for index, jar in enumerate(profile["jars"]):
+ path = Path(jar["path"])
+ if digest(path) != jar["sha256"]:
+ raise AssertionError("Changed pinned input")
+ profile["jars"][index] = archive(path, args.output)
+ for name in ("original110", "early110", "maintained110", "legacy112", "maintained112", "target113"):
+ if [j["sha256"] for j in before[name]["jars"]] != [j["sha256"] for j in profiles[name]["jars"]]:
+ raise AssertionError("Historical source or 1.13 target changed")
+ write_json(args.output / "profiles.json", profiles)
+ write_json(args.output / "replacement-provenance.json", dict(base=str(args.base.resolve()),
+ base_sha256=digest(args.base), replacements=replacements,
+ historical_sources_and_target113="UNCHANGED"))
+ print("Pinned four repaired artifacts; historical sources and 1.13 target unchanged")
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--mode", choices=("archive", "candidate", "workspace-profiles"), required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--base", type=Path, default=POWER / "run-connected-migration/20261023-working-legacy110/profiles.json")
+ run(parser.parse_args())
diff --git a/test-rig/migration/pipe_only_lifecycle.py b/test-rig/migration/pipe_only_lifecycle.py
new file mode 100644
index 00000000..8653fbf7
--- /dev/null
+++ b/test-rig/migration/pipe_only_lifecycle.py
@@ -0,0 +1,110 @@
+"""Test isolated pipe-only chunk bridges without replacing retained endpoints."""
+import argparse
+import json
+import socket
+import time
+from pathlib import Path
+
+from endurance import require
+from lifecycle import LifecycleRun, at, counts, parse
+from matrix import POWER, RIG, digest, profiles, stage, write_json
+
+
+def fixtures():
+ result = []
+ for index, name in enumerate(("C-STEAM", "C-ELECTRIC", "C-FLUID")):
+ base = 9216 + index * 64
+ origin = [base + 13, 80, 2062]
+ sink = [base + 9, 80, 2062]
+ path = [[base + 13, 80, z] for z in range(2063, 2067)]
+ path += [[base + x, 80, 2066] for x in range(12, 8, -1)]
+ path += [[base + 9, 80, z] for z in range(2065, 2062, -1)]
+ result.append({"id": name, "min": [base, 72, 2048], "max": [base + 31, 90, 2079],
+ "witnesses": [origin, sink] + path, "pipes": path,
+ "groups": {"bridge": [[base >> 4, 129]], "endpoints": [[base >> 4, 128]]}})
+ return result
+
+
+def run(args):
+ with socket.socket() as port:
+ port.bind(("127.0.0.1", args.port))
+ manifest = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ for mod in manifest["mods"]:
+ if "sha256" in mod:
+ require(digest((RIG / mod["path"]).resolve()) == mod["sha256"], "Artifact pin mismatch: " + mod["file"])
+ definition = fixtures()
+ launcher = stage(args.output, profiles(POWER.parents[1])["target113"])
+ props = args.output / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ for marker in ("migration-operate.allow", "advantage-works.allow", "advantage-lifecycle.allow"):
+ (args.output / marker).touch()
+ write_json(args.output / "lifecycle-fixture.json", definition)
+ runner = LifecycleRun(args.output, args.java8, launcher, definition)
+ report = {"status": "RUNNING", "cycles": []}
+ try:
+ runner.boot("pipe-only")
+ runner.probe("load", "loaded")
+ for f in definition:
+ runner.command("fill " + " ".join(map(str, f["min"] + f["max"])) + " minecraft:air replace")
+ fluid, steam = f["id"] == "C-FLUID", f["id"] == "C-STEAM"
+ pipe = "poweradvantage:fluid_pipe" if fluid else "steamadvantage:steam_pipe" if steam else "electricadvantage:electric_conduit"
+ runner.block(f["witnesses"][0], pipe if fluid else "poweradvantage:infinite_" + ("steam" if steam else "electricity"))
+ for pos in f["pipes"]: runner.block(pos, pipe)
+ runner.block(f["witnesses"][1], "poweradvantage:fluid_metal_tank" if fluid else "steamadvantage:steam_furnace" if steam else "electricadvantage:electric_furnace")
+ if steam: runner.inventory(f["witnesses"][1], 0, "minecraft:coal", 64)
+ runner.probe("trace-loads", "trace-enabled")
+ # Let normal powered-state updates settle before recording the first baseline.
+ time.sleep(2)
+ runner.probe("snapshot", "commissioned")
+ for cycle in range(3):
+ before = runner.probe("snapshot", f"bridge-{cycle}-before")
+ runner.command("save-all flush")
+ runner.probe("release-bridge", f"bridge-{cycle}-released")
+ time.sleep(3)
+ absent = runner.probe("isolated", f"bridge-{cycle}-isolated")
+ runner.probe("offer-blocked", f"bridge-{cycle}-blocked-offer")
+ time.sleep(3)
+ confirmed = runner.probe("snapshot", f"bridge-{cycle}-confirmed")
+ for f in definition:
+ bridge = f["groups"]["bridge"][0]
+ old = next(c for c in before["chunks"] if c["pos"] == bridge)
+ for snapshot in (absent, confirmed):
+ state = next(c for c in snapshot["chunks"] if c["pos"] == bridge)
+ require(not state["loaded"] and state["unloads"] > old["unloads"], "Bridge did not stay genuinely absent")
+ for pos in f["witnesses"][:2]:
+ require(at(snapshot, f["id"], pos).get("tileIdentity") == at(before, f["id"], pos).get("tileIdentity"), "Retained endpoint replaced")
+ runner.probe("load-bridge", f"bridge-{cycle}-loaded")
+ runner.probe("offer", f"bridge-{cycle}-reconnected-offer")
+ for f in definition:
+ if f["id"] == "C-FLUID": continue
+ machine = f["witnesses"][1]
+ for slot in range(1 if f["id"] == "C-STEAM" else 0, 7 if f["id"] == "C-STEAM" else 10):
+ runner.inventory(machine, slot, "minecraft:air", 1)
+ runner.inventory(machine, 1 if f["id"] == "C-STEAM" else 0, "minecraft:iron_ore", 4)
+ time.sleep(50)
+ resumed = runner.probe("snapshot", f"bridge-{cycle}-resumed")
+ for f in definition:
+ endpoint = at(resumed, f["id"], f["witnesses"][1])
+ if f["id"] == "C-FLUID":
+ require(parse(endpoint)["Tank"]["Amount"] == (cycle + 1) * 1000, "Fluid lost or duplicated")
+ else:
+ require(counts([endpoint])["minecraft:iron_ingot"] == 4 and counts([endpoint])["minecraft:iron_ore"] == 0, "Processing did not reconnect")
+ require(endpoint.get("tileIdentity") == at(before, f["id"], f["witnesses"][1]).get("tileIdentity"), "Reconnection replaced endpoint")
+ report["cycles"].append({"cycle": cycle, "status": "PASS"})
+ print(f"Pipe-only bridge cycle {cycle + 1}: PASS", flush=True)
+ report["status"] = "PASS"
+ runner.command("save-all flush")
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ runner.close()
+ write_json(args.output / "pipe-only-results.json", report)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25587)
+ run(parser.parse_args())
diff --git a/test-rig/migration/player_layout.py b/test-rig/migration/player_layout.py
new file mode 100644
index 00000000..9fa1946a
--- /dev/null
+++ b/test-rig/migration/player_layout.py
@@ -0,0 +1,80 @@
+"""Player bag/Ender Chest boundaries and recursively stored item stacks."""
+import argparse
+import copy
+import json
+from pathlib import Path
+import socket
+
+import nbtlib
+from actors import WORLD, boot, clone, phase
+from matrix import POWER, differences, fingerprint, intermediate_profile, selected_profiles, write_json
+
+
+def seed(runtime):
+ definition = json.loads((runtime / "actor-fixture.json").read_text())
+ path = runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat")
+ player = nbtlib.load(path)
+ slots = iter([0, 7, 8, 9, 18, 27, 35, 1, 2])
+ for stack in player["Inventory"]:
+ if 0 <= int(stack["Slot"]) <= 8: stack["Slot"] = nbtlib.Byte(next(slots))
+ for stack, slot in zip(player["EnderItems"], [0, 3, 5, 8, 13, 17, 20, 25, 26]): stack["Slot"] = nbtlib.Byte(slot)
+ musket = next(s for s in player["Inventory"] if str(s["id"]) == "steamadvantage:musket")
+ battery = next(s for s in player["Inventory"] if str(s["id"]) == "electricadvantage:lithium_battery")
+ oil = next(s for s in player["Inventory"] if str(s["id"]) in ("forge:bucketfilled", "poweradvantage:crude_oil"))
+ cargo = [copy.deepcopy(s) for s in (musket, battery, oil)]
+ for slot, stack in enumerate(cargo): stack["Slot"] = nbtlib.Byte(slot)
+ musket["tag"]["Nested"]["Items"] = nbtlib.List[nbtlib.Compound](cargo)
+ player.save()
+
+
+def player_verdict(expected, state):
+ issues = differences({key: expected[key] for key in ("file", "adapter")}, state)
+ return {"status": "FAIL" if issues else "PASS", "differences": issues}
+
+
+def run(args):
+ with socket.socket() as check: check.bind(("127.0.0.1", args.port))
+ root = args.output
+ root.mkdir(parents=True, exist_ok=False)
+ available = selected_profiles(args)
+ results = {}
+ for name in ("original110", "maintained110", "maintained112"):
+ immutable = args.fixtures / name
+ before = fingerprint(immutable / WORLD)
+ source = root / name
+ launcher = boot(source, available[name], args.java8, args.port)
+ clone(immutable, source)
+ seed(source)
+ baseline = phase(source, args.java8, launcher, False, "source")
+ write_json(source / "fixture-hashes.json", fingerprint(source / WORLD))
+ for route in (["direct", "chained"] if name == "original110" else ["direct"]):
+ label = name + "-" + route
+ predecessor = source
+ verdict = {}
+ if route == "chained":
+ predecessor = root / (label + "-112")
+ launcher112 = boot(predecessor, intermediate_profile(available), args.java8, args.port)
+ clone(source, predecessor)
+ verdict["intermediate"] = player_verdict(baseline, phase(predecessor, args.java8, launcher112, False, "intermediate"))
+ target = root / (label + "-113")
+ launcher113 = boot(target, available["target113"], args.java8, args.port)
+ clone(predecessor, target)
+ for checkpoint in ("converted", "restarted"):
+ verdict[checkpoint] = player_verdict(baseline, phase(target, args.java8, launcher113, True, checkpoint))
+ results[label] = verdict
+ write_json(root / "layout-results.json", results)
+ print(label + ": " + ", ".join(stage + "=" + value["status"] for stage, value in verdict.items()), flush=True)
+ if fingerprint(immutable / WORLD) != before: raise AssertionError("Source fixture changed")
+ if fingerprint(source / WORLD) != json.loads((source / "fixture-hashes.json").read_text()): raise AssertionError("Layout fixture changed")
+ return int(any(stage["status"] != "PASS" for route in results.values() for stage in route.values()))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--workspace", type=Path, default=POWER.parents[1])
+ parser.add_argument("--profile-manifest", type=Path, help="Hash-pinned profiles; optional bridge112 overrides only the intermediate runtime")
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--fixtures", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25618)
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/progression.py b/test-rig/migration/progression.py
new file mode 100644
index 00000000..8197d1d4
--- /dev/null
+++ b/test-rig/migration/progression.py
@@ -0,0 +1,145 @@
+"""Prove all survival Advantage items have manufacture paths from natural inputs."""
+import argparse
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+import socket
+
+from matrix import POWER, RIG, Server, digest, stage, write_json
+
+
+NATURAL_RESOURCES = [
+ "minecraft:oak_log", "minecraft:oak_sapling", "minecraft:stone", "minecraft:cobblestone",
+ "minecraft:dirt", "minecraft:coal", "minecraft:iron_ore", "minecraft:gold_ore",
+ "minecraft:sand", "minecraft:clay_ball", "minecraft:gravel", "minecraft:redstone",
+ "minecraft:diamond", "minecraft:emerald", "minecraft:obsidian", "minecraft:potato",
+ "minecraft:poisonous_potato", "minecraft:wheat", "minecraft:sugar_cane",
+ "minecraft:gunpowder", "minecraft:flint", "minecraft:leather", "minecraft:string",
+ "minecraft:feather", "minecraft:blaze_rod", "minecraft:ender_pearl", "minecraft:quartz",
+ "basemetals:copper_ore", "basemetals:tin_ore", "basemetals:zinc_ore", "basemetals:lead_ore",
+ "basemetals:nickel_ore", "basemetals:silver_ore", "electricadvantage:li_ore",
+ "electricadvantage:sulfur_ore",
+]
+
+EXCLUSIONS = {
+ "poweradvantage:infinite_steam": "Creative-only source, intentionally no survival recipe",
+ "poweradvantage:infinite_electricity": "Creative-only source, intentionally no survival recipe",
+ "poweradvantage:infinite_quantum": "Creative-only source, intentionally no survival recipe",
+ "poweradvantage:crude_oil": "World fluid block, placed and collected through its bucket",
+ "poweradvantage:refined_oil": "World fluid block, produced by distillation and placed through its bucket",
+ "poweradvantage:fluid_pipe_terminal": "Hidden state of an ordinary pipe adjacent to a fluid capability",
+ "steamadvantage:drillbit": "Hidden helper placed by a working steam drill; craft steam_drill_bit instead",
+ "steamadvantage:platform": "Elevator-generated helper with no block drops",
+ "steamadvantage:pump_pipe_steam": "Hidden helper created by a steam pump",
+ "electricadvantage:pump_pipe_electric": "Hidden helper created by an electric pump",
+ "electricadvantage:laser_turret_evil": "Hidden converted state of the craftable laser turret",
+}
+
+
+def fixture():
+ return {"naturalResources": NATURAL_RESOURCES, "exclusions": EXCLUSIONS,
+ "machineBounds": {"min": [16384, 78, 16384], "max": [16415, 88, 16415]}}
+
+
+def validate_tags(result, mode, rubber):
+ tags = result["tags"]
+ assert "electricadvantage:electric_conduit" in tags["wires"]
+ assert {"basemetals:coal_powder", "basemetals:charcoal_powder"} <= set(tags["dusts/carbon"])
+ assert ("poweradvantage:bioplastic_ingot" in tags["rubber"]) == rubber
+ assert {"poweradvantage:bioplastic_ingot", "electricadvantage:petrolplastic_ingot"} <= set(tags["plastic"])
+ assert "electricadvantage:control_circuit" in tags["circuit_boards"]
+ assert "electricadvantage:blank_circuit_board" not in tags["circuit_boards"]
+ assert "electricadvantage:solder" in tags["solder"]
+ for metal in ("lead", "tin", "silver"):
+ assert (f"basemetals:{metal}_ingot" in tags["solder"]) == (mode == "NORMAL")
+
+
+def run(args):
+ output = args.output.resolve()
+ if not output.is_relative_to(POWER.resolve()) or output == POWER.resolve() or output.exists():
+ raise ValueError("Use a new disposable output inside PowerAdvantage")
+ with socket.socket() as check:
+ check.bind(("127.0.0.1", args.port))
+ jars = []
+ if args.baseline:
+ # Immutable artifacts from the preceding discovery qualification, not rebuilt paths.
+ baseline = POWER / "run-recipe-unlocks/20261022-current-final"
+ for entry in json.loads((baseline / "input-jars.json").read_text()):
+ if "TestHarness" in entry["file"]:
+ continue
+ jar = baseline / "mods" / entry["file"]
+ if digest(jar) != entry["sha256"]:
+ raise AssertionError("Baseline hash mismatch: " + str(jar))
+ jars.append(jar)
+ jars.append(RIG / "build/libs/AdvantageWorksTestHarness-1.1.0.113021.jar")
+ else:
+ manifest = json.loads((RIG / "profiles/packaged-current.json").read_text())
+ for mod in manifest["mods"]:
+ jar = (RIG / mod["path"]).resolve()
+ if digest(jar) != mod["sha256"]:
+ raise AssertionError("Artifact pin mismatch: " + str(jar))
+ jars.append(jar)
+ launcher = stage(output, (POWER / "run-advantage-works/packaged-decoupled-20261005-184456",
+ "forge-1.13.2-25.0.223.jar", jars))
+ props = output / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ (output / "advantage-progression.allow").touch()
+ write_json(output / "progression-fixture.json", fixture())
+ pack = output / "AdvantageWorksMigration/datapacks/progression-override"
+ write_json(pack / "pack.mcmeta", {"pack": {"pack_format": 4, "description": "Disposable loot-override witness"}})
+ write_json(pack / "data/minecraft/loot_tables/chests/simple_dungeon.json", {
+ "pools": [{"name": "test_override", "rolls": 1, "entries": [{"type": "item", "name": "minecraft:dirt"}]}]})
+ report = {"status": "RUNNING", "baseline": args.baseline, "cases": [],
+ "inputs": json.loads((output / "input-jars.json").read_text())}
+ cases = [("NORMAL", True)] if args.baseline else [
+ (mode, rubber) for mode in ([args.mode] if args.mode else ("NORMAL", "TECH_PROGRESSION", "APOCALYPTIC"))
+ for rubber in (True, False)]
+ try:
+ for mode, rubber in cases:
+ config = output / "config/poweradvantage-common.toml"
+ config.parent.mkdir(exist_ok=True)
+ config.write_text(f'[options]\nrecipeMode = "{mode}"\nplasticEqualsRubber = {str(rubber).lower()}\n')
+ for phase in ("initial", "restart"):
+ label = mode.lower() + ("-rubber" if rubber else "-no-rubber") + "-" + phase
+ server = Server(output, args.java8, launcher, label)
+ try:
+ for reload in (False, True) if phase == "initial" and not args.baseline else (False,):
+ current = label + ("-reload" if reload else "")
+ if reload:
+ server.send("reload")
+ server.send("advprogression " + current)
+ server.wait("ADVPROGRESSION COMPLETE " + current, 180)
+ result = json.loads((output / ("progression-" + current + ".json")).read_text())
+ report["cases"].append({"mode": mode, "rubber": rubber, "phase": "reload" if reload else phase,
+ "status": result["status"], "blocked": result["blocked"],
+ "report": "progression-" + current + ".json"})
+ if args.baseline:
+ if result["status"] != "FAIL":
+ raise AssertionError("Baseline unexpectedly has complete manufacture paths")
+ else:
+ validate_tags(result, mode, rubber)
+ if result["status"] != "PASS":
+ raise AssertionError("Unobtainable components: " + ", ".join(
+ item["id"] for item in result["targets"] if item["status"] == "BLOCKED")
+ + "; blocked recipes: " + ", ".join(result.get("blockedRecipes", [])))
+ server.send("save-all flush")
+ finally:
+ server.close()
+ report["status"] = "EXPECTED_FAIL" if args.baseline else "PASS"
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ write_json(output / "progression-results.json", report)
+ print(f'Manufacturing progression {report["status"]}: {output}', flush=True)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, default=POWER / "run-progression" /
+ datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S"))
+ parser.add_argument("--port", type=int, default=25590)
+ parser.add_argument("--baseline", action="store_true")
+ parser.add_argument("--mode", choices=("NORMAL", "TECH_PROGRESSION", "APOCALYPTIC"))
+ run(parser.parse_args())
diff --git a/test-rig/migration/recipe_unlocks.py b/test-rig/migration/recipe_unlocks.py
new file mode 100644
index 00000000..936866fa
--- /dev/null
+++ b/test-rig/migration/recipe_unlocks.py
@@ -0,0 +1,127 @@
+"""Inventory discovery and recipe-book persistence on hash-pinned packaged servers."""
+import argparse
+from datetime import datetime, timezone
+import json
+from pathlib import Path
+import socket
+
+from matrix import POWER, RIG, Server, digest, stage, write_json
+
+
+MATERIALS = [
+ "minecraft:iron_ingot", "basemetals:steel_ingot", "basemetals:copper_ingot",
+ "basemetals:lead_ingot", "basemetals:brass_ingot", "basemetals:steel_bars",
+ "poweradvantage:steel_frame", "poweradvantage:fluid_pipe", "poweradvantage:item_conveyor",
+ "steamadvantage:steam_pipe", "poweradvantage:bioplastic_ingot", "basemetals:copper_rod",
+ "electricadvantage:silicon_ingot", "electricadvantage:integrated_circuit",
+ "basemetals:tin_powder", "minecraft:sand", "minecraft:gunpowder",
+ "basemetals:steel_plate", "electricadvantage:psu",
+ "poweradvantage:starch", "electricadvantage:li_ingot", "electricadvantage:li_nugget",
+ "electricadvantage:li_powder", "electricadvantage:li_ore",
+ "electricadvantage:silicon_mix", "electricadvantage:solder_mix",
+ "minecraft:potato",
+]
+
+
+def fixture(extensions="both"):
+ recipes = []
+ for mod, root in (
+ ("poweradvantage", POWER),
+ ("steamadvantage", POWER.parent / "SteamAdvantage/SteamAdvantage"),
+ ("electricadvantage", POWER.parent / "ElectricAdvantage/ElectricAdvantage"),
+ ):
+ if mod == "steamadvantage" and extensions not in ("both", "steam"):
+ continue
+ if mod == "electricadvantage" and extensions not in ("both", "electric"):
+ continue
+ data = root / "src/main/resources/data" / mod
+ for file in sorted((data / "recipes").glob("*.json")):
+ recipe = json.loads(file.read_text())
+ if recipe["type"] not in ("minecraft:crafting_shaped", "minecraft:crafting_shapeless", "minecraft:smelting"):
+ continue
+ unlock = json.loads((data / "advancements/recipes" / file.name).read_text())
+ recipes.append({"id": mod + ":" + file.stem, "ingredients": {
+ name: criterion["conditions"]["items"][0]
+ for name, criterion in unlock["criteria"].items()
+ if criterion["trigger"] == "minecraft:inventory_changed"
+ }})
+ expected = 35 + (24 if extensions in ("both", "steam") else 0) + (50 if extensions in ("both", "electric") else 0)
+ assert len(recipes) == expected, "Update the expected recipe-book inventory deliberately"
+ return {"recipes": recipes, "materials": MATERIALS}
+
+
+def run(args):
+ output = args.output.resolve()
+ if not output.is_relative_to(POWER.resolve()) or output == POWER.resolve() or output.exists():
+ raise ValueError("Use a new disposable output directory inside PowerAdvantage")
+ with socket.socket() as check:
+ check.bind(("127.0.0.1", args.port))
+ profile = "packaged-decoupled" if args.without_basemetals else "packaged-current"
+ manifest = json.loads((RIG / "profiles" / (profile + ".json")).read_text())
+ jars = []
+ for mod in manifest["mods"]:
+ if mod["id"] == "steamadvantage" and args.extensions not in ("both", "steam"):
+ continue
+ if mod["id"] == "electricadvantage" and args.extensions not in ("both", "electric"):
+ continue
+ jar = (RIG / mod["path"]).resolve()
+ if digest(jar) != mod["sha256"]:
+ raise AssertionError("Artifact pin mismatch: " + str(jar))
+ jars.append(jar)
+ seed = POWER / "run-advantage-works/packaged-decoupled-20261005-184456"
+ launcher = stage(output, (seed, "forge-1.13.2-25.0.223.jar", jars))
+ props = output / "server.properties"
+ props.write_text(props.read_text().replace("server-port=25584", f"server-port={args.port}"))
+ (output / "advantage-recipes.allow").touch()
+ definition = fixture(args.extensions)
+ write_json(output / "recipe-fixture.json", definition)
+ report = {"status": "RUNNING", "profile": profile, "extensions": args.extensions, "boots": [],
+ "inputs": json.loads((output / "input-jars.json").read_text())}
+
+ def boot(mode, label, operations):
+ config = output / "config/poweradvantage-common.toml"
+ config.parent.mkdir(exist_ok=True)
+ config.write_text(f'[options]\nrecipeMode = "{mode}"\n')
+ server = Server(output, args.java8, launcher, label)
+ row = {"mode": mode, "label": label, "operations": []}
+ report["boots"].append(row)
+ try:
+ for operation in operations:
+ name = label + "-" + operation
+ server.send(f"advrecipes {operation} {name}")
+ server.wait("ADVRECIPES COMPLETE " + operation + " " + name, 120)
+ result = json.loads((output / ("recipes-" + name + ".json")).read_text())
+ assert result["status"] == "PASS" and len(result["cases"]) == len(definition["recipes"])
+ if operation == "persist" and not args.without_basemetals:
+ assert not result["additionalMaterials"], "Human materials checklist has gaps"
+ row["operations"].append({"operation": operation, "report": "recipes-" + name + ".json",
+ "active": sum(case["recipeActive"] for case in result["cases"]),
+ "passed": sum(case["status"] == "PASS" for case in result["cases"])})
+ server.send("save-all flush")
+ finally:
+ server.close()
+
+ try:
+ boot("NORMAL", "normal", ("sweep", "persist"))
+ boot("NORMAL", "normal-restart", ("restore",))
+ boot("TECH_PROGRESSION", "tech", ("restore", "sweep", "persist"))
+ boot("APOCALYPTIC", "apocalyptic", ("restore", "sweep", "persist"))
+ boot("NORMAL", "normal-return", ("restore",))
+ report["status"] = "PASS"
+ except BaseException as error:
+ report.update(status="FAIL", failure=str(error))
+ raise
+ finally:
+ write_json(output / "recipe-unlock-results.json", report)
+ print(f"Recipe discovery PASS: {profile}, extensions={args.extensions}, all modes, five full process boots. Evidence: {output}", flush=True)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, default=POWER / "run-recipe-unlocks" /
+ datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S"))
+ parser.add_argument("--port", type=int, default=25589)
+ parser.add_argument("--without-basemetals", action="store_true")
+ parser.add_argument("--extensions", choices=("both", "steam", "electric", "none"), default="both")
+ run(parser.parse_args())
diff --git a/test-rig/migration/requirements.txt b/test-rig/migration/requirements.txt
new file mode 100644
index 00000000..96972e78
--- /dev/null
+++ b/test-rig/migration/requirements.txt
@@ -0,0 +1 @@
+nbtlib==2.0.4
diff --git a/test-rig/migration/respawn.py b/test-rig/migration/respawn.py
new file mode 100644
index 00000000..21522e61
--- /dev/null
+++ b/test-rig/migration/respawn.py
@@ -0,0 +1,153 @@
+"""Server death/respawn adapters, not a client death screen or login test."""
+
+import argparse
+from collections import Counter
+import itertools
+import json
+from pathlib import Path
+import shutil
+import socket
+
+import nbtlib
+
+from actors import enchantments, semantic
+from dimensions import PLAYER_STATE, clone, exact_item_differences, start
+from matrix import POWER, WORLD, Server, differences, fingerprint, normalized, parse_nbt, plain, selected_profiles, write_json
+
+
+def stacks(items):
+ result = Counter()
+ for original in items:
+ value = dict(original)
+ value.pop("Slot", None)
+ quantity = int(value.pop("Count", 0))
+ result[json.dumps(value, sort_keys=True)] += quantity
+ return result
+
+
+def spawn_reference(level_file):
+ data = nbtlib.load(level_file)["Data"]
+ return {"position": [int(data[key]) for key in ("SpawnX", "SpawnY", "SpawnZ")],
+ "radius": max(0, int(data.get("GameRules", {}).get("spawnRadius", "10")))}
+
+
+def respawn_verdict(record, registry, world_spawn=None):
+ data = record["respawn"]
+ before = semantic(parse_nbt(data["before"]), PLAYER_STATE, registry)
+ dead = semantic(parse_nbt(data["afterDeath"]), PLAYER_STATE, registry)
+ after = semantic(parse_nbt(data["after"]), PLAYER_STATE, registry)
+ issues = []
+ keep = data["keepInventory"]
+ expected_items = before["Inventory"] if keep else []
+ expected = {"Dimension": 0, "Health": 20.0, "Inventory": expected_items,
+ "EnderItems": before["EnderItems"], "UUIDMost": before["UUIDMost"], "UUIDLeast": before["UUIDLeast"],
+ "XpLevel": before["XpLevel"] if keep else 0, "XpTotal": before["XpTotal"] if keep else 0}
+ issues.extend(differences(expected, after, "respawn"))
+ issues.extend(exact_item_differences(expected, after, "respawn"))
+ issues.extend(differences({"Health": 0.0, "Inventory": expected_items}, dead, "death"))
+ issues.extend(differences({"fromDimension": record["dimension"], "toDimension": 0}, data, "world"))
+ if data.get("handler") != "NetHandlerPlayServer.PERFORM_RESPAWN":
+ issues.append({"path": "handler", "expected": "NetHandlerPlayServer.PERFORM_RESPAWN", "actual": data.get("handler")})
+ if not before["Inventory"] or not before["EnderItems"]:
+ issues.append({"path": "source-items", "expected": "Nonempty source inventories", "actual": before})
+ drops = [normalized(plain(parse_nbt(entry))["Item"], registry) for entry in data["drops"]]
+ expected_drops = Counter() if keep else stacks(before["Inventory"])
+ actual_drops = stacks(drops)
+ if expected_drops != actual_drops:
+ issues.append({"path": "drops", "expected": dict(expected_drops), "actual": dict(actual_drops)})
+ position = after["Pos"]
+ near_bed = abs(position[0] - 160.5) <= 3 and 99 <= position[1] <= 104 and abs(position[2] - 24.5) <= 3
+ if (data["bedMode"] != "missing") != near_bed:
+ issues.append({"path": "spawn-position", "expected": "bed area" if data["bedMode"] != "missing" else "world spawn",
+ "actual": position})
+ if data["bedMode"] == "missing":
+ if world_spawn is None:
+ issues.append({"path": "world-spawn-reference", "expected": "Native saved spawn position/radius", "actual": None})
+ else:
+ spawn = world_spawn["position"]
+ radius = world_spawn["radius"] + 1
+ if abs(position[0] - spawn[0]) > radius or abs(position[2] - spawn[2]) > radius or not 0 <= position[1] < 256:
+ issues.append({"path": "world-spawn-position", "expected": world_spawn, "actual": position})
+ if data["bedMode"] != "missing":
+ spawn = {key: before[key] for key in ("SpawnX", "SpawnY", "SpawnZ", "SpawnForced") if key in before}
+ issues.extend(differences(spawn, after, "saved-spawn"))
+ return {"status": "FAIL" if issues else "PASS", "differences": issues,
+ "inventory_quantity": sum(stacks(before["Inventory"]).values()), "drop_quantity": sum(actual_drops.values()),
+ "scope": data["scope"], "keepInventory": keep, "bedMode": data["bedMode"]}
+
+
+def run(args):
+ with socket.socket() as check:
+ check.bind(("127.0.0.1", args.port))
+ available = selected_profiles(args)
+ args.output.mkdir(parents=True, exist_ok=False)
+ results = {}
+ sources = {}
+ for district in args.districts:
+ for profile in args.profiles:
+ name = profile if profile != "target113" else args.target_fixture
+ fixture = args.fixtures / district / name
+ old_hashes = fingerprint(fixture / WORLD)
+ sources[district + "/" + profile] = {"path": str(fixture.resolve() / WORLD), "hashes": old_hashes}
+ write_json(args.output / "source-fingerprints.json", sources)
+ runtime = args.output / district / profile
+ launcher = start(runtime, available[profile], args)
+ clone(fixture, runtime)
+ definition = json.loads((runtime / "dimension-fixture.json").read_text())
+ player_file = runtime / WORLD / "playerdata" / (definition["playerUuid"] + ".dat")
+ original_player = player_file.read_bytes()
+ server = Server(runtime, args.java8, launcher, "respawn")
+ try:
+ for keep, mode in itertools.product([True, False], ["forced", "bed", "missing"]):
+ label = ("keep" if keep else "drop") + "-" + mode
+ # Independent cases start from the source-serialized player, never from a previous death result.
+ player_file.write_bytes(original_player)
+ definition["respawn"] = {"keepInventory": keep, "bedMode": mode}
+ write_json(runtime / "dimension-fixture.json", definition)
+ command = ("advdimensions respawn " if profile == "target113" else "advprobe dimensions-respawn ") + label
+ server.send(command)
+ server.wait("ADVDIMENSIONS COMPLETE " + label, 120)
+ server.send("save-all flush")
+ server.wait("Saved the", 30)
+ record = json.loads((runtime / f"dimensions-{label}.json").read_text())
+ result = respawn_verdict(record, enchantments(runtime), spawn_reference(runtime / WORLD / "level.dat"))
+ saved = semantic(nbtlib.load(player_file).unpack(), PLAYER_STATE, enchantments(runtime))
+ expected_saved = semantic(parse_nbt(record["respawn"]["after"]), PLAYER_STATE, enchantments(runtime))
+ saved_issues = differences(expected_saved, saved, "saved-player")
+ saved_issues.extend(exact_item_differences(expected_saved, saved, "saved-player"))
+ result["differences"].extend(saved_issues)
+ if saved_issues:
+ result["status"] = "FAIL"
+ results[district + "/" + profile + "/" + label] = result
+ evidence = runtime / "respawn-evidence" / label
+ evidence.mkdir(parents=True)
+ shutil.copy2(player_file, evidence / "player.dat")
+ shutil.copy2(runtime / WORLD / "level.dat", evidence / "level.dat")
+ write_json(args.output / "respawn-results.json", results)
+ print(district + "/" + profile + "/" + label + "=" + result["status"], flush=True)
+ finally:
+ server.close()
+ current = fingerprint(fixture / WORLD)
+ if current != old_hashes:
+ raise AssertionError("Source fixture changed")
+ write_json(args.output / "scope.json", {"real_client_login_and_death_screen": "NOT_TESTED", "cases": len(results),
+ "expected_case_ids": sorted(district + "/" + profile + "/" + ("keep" if keep else "drop") + "-" + mode
+ for district in args.districts for profile in args.profiles
+ for keep, mode in itertools.product([True, False], ["forced", "bed", "missing"])),
+ "source_fixtures": "UNCHANGED", "case_initialisation": "Restore source-serialized player.dat before each independent case",
+ "respawn_adapter": "Native onDeath and NetHandlerPlayServer.PERFORM_RESPAWN, server-only dummy connection"})
+ return int(any(value["status"] != "PASS" for value in results.values()))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--workspace", type=Path, default=POWER.parents[1])
+ parser.add_argument("--profile-manifest", type=Path, required=True)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--fixtures", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25622)
+ parser.add_argument("--districts", nargs="+", choices=["nether", "end"], default=["nether", "end"])
+ parser.add_argument("--profiles", nargs="+", default=["original110", "maintained112", "target113"])
+ parser.add_argument("--target-fixture", default="original110-chained-113")
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/test_actors.py b/test-rig/migration/test_actors.py
new file mode 100644
index 00000000..928da035
--- /dev/null
+++ b/test-rig/migration/test_actors.py
@@ -0,0 +1,75 @@
+import copy
+import unittest
+
+from actors import PLAYER_KEYS, compare, semantic, tick_result, freeze_result
+
+
+class ActorOracleTests(unittest.TestCase):
+ def setUp(self):
+ self.state = {"file": {"Inventory": [{"Slot": 0, "id": "electricadvantage:lithium_battery", "Count": 3,
+ "tag": {"energy": 123.0}}], "EnderItems": [], "XpTotal": 751},
+ "embedded": {}, "adapter": {},
+ "entities": {"hydroturbine": {"id": "electricadvantage:hydroturbine", "UUIDMost": 77,
+ "parentPos": [164, 81, 8]}}}
+
+ def changed(self, edit):
+ value = copy.deepcopy(self.state)
+ edit(value)
+ self.assertEqual("FAIL", compare(self.state, value)["status"])
+
+ def test_identical(self): self.assertEqual("PASS", compare(self.state, self.state)["status"])
+ def test_quantity_loss(self): self.changed(lambda v: v["file"]["Inventory"][0].update(Count=2))
+ def test_quantity_duplication(self): self.changed(lambda v: v["file"]["Inventory"][0].update(Count=4))
+ def test_extra_slot(self): self.changed(lambda v: v["file"]["Inventory"].append(copy.deepcopy(v["file"]["Inventory"][0])))
+ def test_battery_charge_loss(self): self.changed(lambda v: v["file"]["Inventory"][0]["tag"].update(energy=0))
+ def test_missing_entity(self): self.changed(lambda v: v["entities"].clear())
+ def test_duplicate_entity(self): self.changed(lambda v: v["entities"].update(copy=copy.deepcopy(v["entities"]["hydroturbine"])))
+ def test_parent_loss(self): self.changed(lambda v: v["entities"]["hydroturbine"].pop("parentPos"))
+ def test_uuid_changed(self): self.changed(lambda v: v["entities"]["hydroturbine"].update(UUIDMost=78))
+ def test_experience_loss(self): self.changed(lambda v: v["file"].update(XpTotal=0))
+ def test_world_ticks_do_not_prove_entity_ticks(self):
+ before = {"gameTime": 100, "entities": [{"witness": "cart", "ticksExisted": 0}]}
+ after = {"gameTime": 181, "entities": [{"witness": "cart", "ticksExisted": 0}]}
+ self.assertEqual("FAIL", tick_result(before, after)["status"])
+ after["entities"][0]["ticksExisted"] = 81
+ self.assertEqual("PASS", tick_result(before, after)["status"])
+
+ def test_freeze_requires_zero_measured_ticks(self):
+ record = {"frozen": True, "entities": [{"witness": "cart", "ticksExisted": 1}]}
+ self.assertEqual("FAIL", freeze_result(record)["status"])
+ record["entities"][0]["ticksExisted"] = 0
+ self.assertEqual("PASS", freeze_result(record)["status"])
+ record["frozen"] = False
+ self.assertEqual("FAIL", freeze_result(record)["status"])
+ def test_ender_chest_loss(self):
+ self.state["file"]["EnderItems"] = copy.deepcopy(self.state["file"]["Inventory"])
+ self.changed(lambda v: v["file"].update(EnderItems=[]))
+
+ def test_ownership_changed(self):
+ self.state["entities"]["wolf"] = {"OwnerUUID": "original-owner"}
+ self.changed(lambda v: v["entities"]["wolf"].update(OwnerUUID="different-owner"))
+
+ def test_custom_nbt_loss(self):
+ self.state["file"]["Inventory"][0]["tag"]["Nested"] = {"OwnerNote": "keep"}
+ self.changed(lambda v: v["file"]["Inventory"][0]["tag"].pop("Nested"))
+
+ def test_bucket_identity_and_custom_nbt(self):
+ old = {"Inventory": [{"Slot": 0, "id": "forge:bucketfilled", "Count": 1, "Damage": 0,
+ "tag": {"FluidName": "crude_oil", "Amount": 1000, "OwnerNote": "keep"}}]}
+ modern = {"Inventory": [{"Slot": 0, "id": "poweradvantage:crude_oil_bucket", "Count": 1,
+ "tag": {"OwnerNote": "keep"}}]}
+ self.assertEqual(modern, semantic(old, PLAYER_KEYS, {}))
+ modern["Inventory"][0]["id"] = "minecraft:bucket"
+ self.assertNotEqual(modern, semantic(old, PLAYER_KEYS, {}))
+
+ def test_enchantment_identity(self):
+ old = {"Inventory": [{"Slot": 0, "id": "steamadvantage:musket", "Count": 1, "Damage": 5,
+ "tag": {"ench": [{"id": 81, "lvl": 1}]}}]}
+ value = semantic(old, PLAYER_KEYS, {81: "steamadvantage:powder_charge"})
+ self.assertEqual("steamadvantage:powder_charge", value["Inventory"][0]["tag"]["Enchantments"][0]["id"])
+ self.assertEqual(5, value["Inventory"][0]["tag"]["Damage"])
+ wrong = semantic(old, PLAYER_KEYS, {81: "minecraft:sharpness"})
+ self.assertNotEqual(value, wrong)
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test-rig/migration/test_audit_connected.py b/test-rig/migration/test_audit_connected.py
new file mode 100644
index 00000000..abc7518b
--- /dev/null
+++ b/test-rig/migration/test_audit_connected.py
@@ -0,0 +1,48 @@
+"""The offline audit must not promote partial or incomplete evidence to a pass."""
+import json
+from pathlib import Path
+import tempfile
+import unittest
+
+from audit_connected import audit, expected_routes
+from connected import SOURCES
+
+
+class ConnectedAuditTest(unittest.TestCase):
+ def test_matrix_requires_eight_routes_including_three_bridges(self):
+ routes = expected_routes(SOURCES)
+ self.assertEqual(len(routes), 8)
+ self.assertEqual(sum(r.endswith("-chained") for r in routes), 3)
+ self.assertNotIn("legacy112-chained", routes)
+
+ def test_no_evidence_cannot_pass(self):
+ self.assertEqual(audit([])["status"], "FAIL")
+
+ def test_empty_success_report_cannot_pass(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ base = root / "base.json"
+ base.write_text("{}")
+ (root / "profiles.json").write_text("{}")
+ from matrix import digest
+ (root / "input-provenance.json").write_text(json.dumps(dict(base=str(base), sha256=digest(base))))
+ (root / "connected-results.json").write_text(json.dumps(dict(status="PASS", sources={}, routes={})))
+ result = audit([root])
+ self.assertEqual(result["status"], "FAIL")
+ self.assertEqual(result["routes"], [])
+
+ def test_unfinished_report_is_a_failure(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ base = root / "base.json"
+ base.write_text("{}")
+ (root / "profiles.json").write_text("{}")
+ from matrix import digest
+ (root / "input-provenance.json").write_text(json.dumps(dict(base=str(base), sha256=digest(base))))
+ (root / "connected-results.json").write_text(json.dumps(dict(status="RUNNING", sources={}, routes={})))
+ result = audit([root])
+ self.assertTrue(any(c["status"] == "FAIL" and "completed campaign" in c["name"] for c in result["checks"]))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_bridge_profiles.py b/test-rig/migration/test_bridge_profiles.py
new file mode 100644
index 00000000..ae05fd36
--- /dev/null
+++ b/test-rig/migration/test_bridge_profiles.py
@@ -0,0 +1,24 @@
+import unittest
+
+from matrix import intermediate_profile
+
+
+class BridgeProfileTests(unittest.TestCase):
+ def test_default_keeps_the_qualified_historical_bridge(self):
+ legacy = ("legacy-seed", "forge112.jar", ["original-electric.jar"])
+ self.assertIs(legacy, intermediate_profile({"maintained112": legacy}))
+
+ def test_override_leaves_the_historical_source_profile_unchanged(self):
+ legacy = ("legacy-seed", "forge112.jar", ["original-electric.jar"])
+ repaired = ("legacy-seed", "forge112.jar", ["repaired-electric.jar"])
+ available = {"maintained112": legacy, "bridge112": repaired}
+ self.assertIs(repaired, intermediate_profile(available))
+ self.assertIs(legacy, available["maintained112"])
+
+ def test_explicit_override_can_run_without_a_historical_bridge_entry(self):
+ repaired = ("legacy-seed", "forge112.jar", ["repaired-electric.jar"])
+ self.assertIs(repaired, intermediate_profile({"bridge112": repaired}))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_connected.py b/test-rig/migration/test_connected.py
new file mode 100644
index 00000000..a32ab3cc
--- /dev/null
+++ b/test-rig/migration/test_connected.py
@@ -0,0 +1,115 @@
+"""Connected tests must reject idle, rebuilt, emptied or duplicated machinery."""
+import base64
+import copy
+import gzip
+import io
+import unittest
+
+import nbtlib
+from connected import cold_state, entries, fixture, operation, state
+
+
+def nbt(entry, value):
+ stream = io.BytesIO()
+ nbtlib.File(nbtlib.parse_nbt(value)).write(stream)
+ entry["nbtBase64"] = base64.b64encode(gzip.compress(stream.getvalue())).decode()
+ entry["nbt"] = value
+
+
+def sample():
+ document = dict(held=True, requestedTicks=0, advancedTicks=0, buildCallsThisProcess=0,
+ snapshot=copy.deepcopy(fixture("test")["blocks"]))
+ for e in document["snapshot"]:
+ e["actualName"] = e["name"]
+ if "seed" in e or e["kind"] in ("drill", "rear", "output"):
+ nbt(e, e.get("seed", "{Items:[]}"))
+ e.update(witness=e["id"], objectIdentity=len(e["id"]))
+ return document
+
+
+def advanced(before):
+ after = copy.deepcopy(before)
+ after.update(requestedTicks=64, advancedTicks=64)
+ rows = entries(after)
+ nbt(rows["C-CONVEYOR-input"], '{Items:[{id:"minecraft:dirt",Count:1727}]}')
+ nbt(rows["C-CONVEYOR-output"], '{Items:[{id:"minecraft:dirt",Count:1b}]}')
+ for station, count in (("C-STEAM", 191), ("C-ELECTRIC", 63)):
+ nbt(rows[station + "-furnace"], '{Items:[{id:"minecraft:iron_ore",Count:%d},{id:"minecraft:iron_ingot",Count:1b}]}' % count)
+ nbt(rows["C-STEAM-crusher"], '{Items:[{id:"minecraft:cobblestone",Count:63b},{id:"minecraft:gravel",Count:1b}]}')
+ for station, count, item in (("C-SDRILL", 1, "gravel"), ("C-EDRILL", 9, "cobblestone")):
+ targets = [e for e in after["snapshot"] if e["station"] == station and e["kind"].startswith("target-")]
+ for target in targets[:count]:
+ target["actualName"] = "minecraft:air"
+ nbt(rows[station + "-drill"], '{Items:[{id:"minecraft:%s",Count:%db}]}' % (item, count))
+ return after
+
+
+class ConnectedTest(unittest.TestCase):
+ def test_definition_has_unique_bounded_placements_and_no_infinite_sources(self):
+ definition = fixture("test")
+ positions = {(e["x"], e["y"], e["z"]) for e in definition["blocks"]}
+ self.assertEqual(len(positions), len(definition["blocks"]))
+ self.assertTrue(all("infinite_" not in e["name"] for e in definition["blocks"]))
+ self.assertTrue(all("Energy" not in e.get("seed", "") for e in definition["blocks"]))
+
+ def test_valid_cold_fixture_is_finite_and_uncharged(self):
+ self.assertEqual(cold_state(sample())["status"], "PASS")
+
+ def test_empty_tank_setup_and_precharged_battery_fail(self):
+ doc = sample()
+ nbt(entries(doc)["C-STEAM-water-0"], '{Tank:{Empty:""}}')
+ self.assertEqual(cold_state(doc)["status"], "FAIL")
+ doc = sample()
+ nbt(entries(doc)["C-ELECTRIC-battery"], '{Items:[{id:"electricadvantage:lithium_battery",Count:1b,tag:{energy:10.0f}}]}')
+ self.assertEqual(cold_state(doc)["status"], "FAIL")
+
+ def test_live_work_with_exact_material_accounting_passes(self):
+ doc = sample()
+ self.assertEqual(operation(doc, advanced(doc), 64)["status"], "PASS")
+
+ def test_idle_equipment_is_not_an_operating_pass(self):
+ doc = sample()
+ idle = copy.deepcopy(doc)
+ idle.update(requestedTicks=64, advancedTicks=64)
+ self.assertEqual(operation(doc, idle, 64)["status"], "FAIL")
+
+ def test_inventory_loss_and_duplication_fail(self):
+ for count in (62, 64):
+ doc = advanced(sample())
+ nbt(entries(doc)["C-ELECTRIC-furnace"], '{Items:[{id:"minecraft:iron_ore",Count:%d},{id:"minecraft:iron_ingot",Count:1b}]}' % count)
+ self.assertEqual(state(doc)["status"], "FAIL")
+
+ def test_mined_blocks_without_retained_drops_fail(self):
+ doc = advanced(sample())
+ nbt(entries(doc)["C-EDRILL-drill"], '{Items:[]}')
+ self.assertEqual(state(doc)["status"], "FAIL")
+
+ def test_drills_preserve_their_distinct_products(self):
+ for station, wrong in (("C-SDRILL", "cobblestone"), ("C-EDRILL", "gravel")):
+ doc = advanced(sample())
+ count = 1 if station == "C-SDRILL" else 9
+ nbt(entries(doc)[station + "-drill"], '{Items:[{id:"minecraft:%s",Count:%db}]}' % (wrong, count))
+ self.assertEqual(state(doc)["status"], "FAIL")
+
+ def test_replacement_lost_witness_and_zero_ticks_fail(self):
+ before = sample()
+ for field, value in (("objectIdentity", -1), ("witness", "replaced")):
+ after = advanced(before)
+ entries(after)["C-ELECTRIC-furnace"][field] = value
+ self.assertEqual(operation(before, after, 64)["status"], "FAIL")
+ after = advanced(before)
+ after["advancedTicks"] = 0
+ self.assertEqual(operation(before, after, 64)["status"], "FAIL")
+
+ def test_fluid_creation_and_invalid_energy_fail(self):
+ before = sample()
+ after = advanced(before)
+ nbt(entries(after)["C-STEAM-water-0"], '{Tank:{FluidName:"water",Amount:10001}}')
+ self.assertEqual(operation(before, after, 64)["status"], "FAIL")
+ after = advanced(before)
+ nbt(entries(after)["C-ELECTRIC-furnace"], '{Energy:[I;2143289344]}')
+ self.assertEqual(state(after)["status"], "FAIL")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_connected_progress.py b/test-rig/migration/test_connected_progress.py
new file mode 100644
index 00000000..94f4a2c9
--- /dev/null
+++ b/test-rig/migration/test_connected_progress.py
@@ -0,0 +1,40 @@
+"""First-tick progress loss must remain visible even when later work succeeds."""
+import copy
+import unittest
+
+from audit_connected_progress import first_tick
+from connected import entries
+from test_connected import nbt, sample
+
+
+def checkpoints(progress):
+ loaded = sample()
+ nbt(entries(loaded)["C-EDRILL-drill"], '{progress:512s,targetX:270,targetY:80,targetZ:41,Items:[]}')
+ first = copy.deepcopy(loaded)
+ first.update(requestedTicks=1, advancedTicks=1)
+ nbt(entries(first)["C-EDRILL-drill"], '{progress:%ds,targetX:270,targetY:80,targetZ:41,Items:[]}' % progress)
+ return loaded, first
+
+
+class ConnectedProgressTest(unittest.TestCase):
+ def test_continuing_saved_work_passes(self):
+ self.assertEqual(first_tick(*checkpoints(513))["status"], "PASS")
+
+ def test_resetting_saved_work_fails(self):
+ self.assertEqual(first_tick(*checkpoints(1))["status"], "FAIL")
+
+ def test_replacement_is_rejected(self):
+ loaded, first = checkpoints(513)
+ entries(first)["C-EDRILL-drill"]["objectIdentity"] = -1
+ with self.assertRaises(AssertionError):
+ first_tick(loaded, first)
+
+ def test_unmeasured_observation_is_rejected(self):
+ loaded, first = checkpoints(513)
+ first["advancedTicks"] = 0
+ with self.assertRaises(AssertionError):
+ first_tick(loaded, first)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_dimensions.py b/test-rig/migration/test_dimensions.py
new file mode 100644
index 00000000..eef1cb7f
--- /dev/null
+++ b/test-rig/migration/test_dimensions.py
@@ -0,0 +1,117 @@
+import copy
+import unittest
+
+from dimensions import WITNESSES, canonical_entities, entity_state, runtime_errors, strict_expected, survival_expected, tick_result, verdict
+
+
+class DimensionOracleTests(unittest.TestCase):
+ def test_runtime_failures_cannot_hide_behind_clean_exit(self):
+ content = '[ERROR] Exception stopping the server\n[INFO] Stopping server\n[ERROR] "Silently" catching entity tracking error.'
+ self.assertEqual(2, len(runtime_errors(content)))
+
+ def test_known_external_diagnostics_are_not_relabelled_crashes(self):
+ self.assertEqual([], runtime_errors('[ERROR] basemetals_forge25_leaves_fixer\n[ERROR] Failed to delete temporary mod-pack directory'))
+
+ def setUp(self):
+ self.state = {"dimension": -1, "playerWorldDimension": -1,
+ "file": {"Dimension": -1, "SpawnDimension": 0, "Inventory": [{"id": "steamadvantage:musket", "Count": 1}]},
+ "adapter": {"Dimension": -1},
+ "entities": {"boat": {"id": "minecraft:boat", "UUIDMost": 1, "UUIDLeast": 2},
+ "horse": {"id": "minecraft:horse", "UUIDMost": 3, "UUIDLeast": 4,
+ "Leash": {"X": 165, "Y": 100, "Z": 8}},
+ "wolf": {"id": "minecraft:wolf", "OwnerUUID": "owner", "UUIDMost": 5}},
+ "relationships": {"wolf": {"vehicleUuid": "boat-uuid", "passengerUuids": [], "leashHolderUuid": None}}}
+
+ def changed(self, edit):
+ other = copy.deepcopy(self.state)
+ edit(other)
+ self.assertEqual("FAIL", verdict(self.state, other)["status"])
+
+ def test_identical(self):
+ self.assertEqual("PASS", verdict(self.state, self.state)["status"])
+
+ def test_dimension_reset(self):
+ self.changed(lambda s: s["file"].update(Dimension=0))
+
+ def test_wrong_live_world(self):
+ self.changed(lambda s: s.update(playerWorldDimension=0))
+
+ def test_passenger_detachment(self):
+ self.changed(lambda s: s["relationships"]["wolf"].update(vehicleUuid=None))
+
+ def test_leash_loss(self):
+ self.changed(lambda s: s["entities"]["horse"].pop("Leash"))
+
+ def test_inventory_loss(self):
+ self.changed(lambda s: s["file"].update(Inventory=[]))
+
+ def test_additional_item_tag_is_not_an_exact_item_match(self):
+ self.changed(lambda s: s["file"]["Inventory"][0].update(tag={"UnexpectedCapability": 999}))
+
+ def test_unknown_turbine_like_id_is_not_an_alias(self):
+ self.assertEqual("foreign:hydroturbine_fake", canonical_entities({"id": "foreign:hydroturbine_fake"})["id"])
+ self.assertEqual("electricadvantage:hydroturbine",
+ canonical_entities({"id": "electricadvantage:electricadvantage.Hydroturbine"})["id"])
+
+ def test_every_persistent_witness_needs_tick_evidence(self):
+ before = {"gameTime": 0, "entities": [{"witness": "horse", "ticksExisted": 0}]}
+ after = {"gameTime": 100, "entities": [{"witness": name, "ticksExisted": 100} for name in sorted(WITNESSES)]}
+ self.assertEqual("FAIL", tick_result(before, after)["status"])
+ before["entities"] = [{"witness": name, "ticksExisted": 0} for name in sorted(WITNESSES)]
+ self.assertEqual("PASS", tick_result(before, after)["status"])
+
+ def test_duplicate_helper_tick_record_fails(self):
+ before = {"gameTime": 0, "entities": [{"witness": name, "ticksExisted": 0} for name in sorted(WITNESSES)]}
+ after = {"gameTime": 100, "entities": [{"witness": name, "ticksExisted": 100} for name in sorted(WITNESSES)]}
+ after["entities"].extend([{"witness": "leash-knot", "ticksExisted": 100}] * 2)
+ self.assertEqual("FAIL", tick_result(before, after)["status"])
+
+ def test_owner_loss(self):
+ self.changed(lambda s: s["entities"]["wolf"].pop("OwnerUUID"))
+
+ def test_extra_entity(self):
+ self.changed(lambda s: s["entities"].update(duplicate=s["entities"]["wolf"]))
+
+ def test_respawn_point_loss(self):
+ self.changed(lambda s: s["file"].pop("SpawnDimension"))
+
+ def test_canonicalizes_nested_passenger_ids(self):
+ old = {"id": "Boat", "Passengers": [{"id": "Wolf", "OwnerUUID": "owner"}]}
+ new = {"id": "minecraft:boat", "Passengers": [{"id": "minecraft:wolf", "OwnerUUID": "owner"}]}
+ self.assertEqual(new, canonical_entities(old))
+ self.assertEqual("custom:unknown", canonical_entities({"id": "custom:unknown"})["id"])
+
+ def test_pending_links_only_excluded_from_zero_tick_comparison(self):
+ self.assertNotIn("relationships", strict_expected(self.state))
+ self.assertEqual(self.state["relationships"], survival_expected(self.state)["relationships"])
+
+ def test_movement_is_not_state_loss(self):
+ self.state["entities"]["horse"]["Pos"] = [1, 2, 3]
+ expected = survival_expected(self.state)
+ self.assertNotIn("Pos", expected["entities"]["horse"])
+ self.assertIn("Leash", expected["entities"]["horse"])
+
+ def test_legacy_collar_color_keeps_its_meaning(self):
+ self.assertEqual(14, canonical_entities({"id": "Wolf", "CollarColor": 1}, legacy=True)["CollarColor"])
+ self.assertEqual(14, canonical_entities({"id": "minecraft:wolf", "CollarColor": 14})["CollarColor"])
+
+ def test_passengers_keep_same_semantic_fields_as_roots(self):
+ nbt = {"id": "minecraft:boat", "Passengers": [{"id": "minecraft:wolf", "UUIDMost": 5,
+ "OwnerUUID": "owner", "Attributes": [{"Name": "generic.maxHealth", "Base": 20}]}]}
+ state = entity_state(nbt)
+ self.assertEqual(20, state["Passengers"][0]["Attributes"]["generic.maxHealth"]["Base"])
+ changed = copy.deepcopy(state)
+ changed["Passengers"][0]["OwnerUUID"] = "other"
+ self.assertEqual("FAIL", verdict({"entities": state}, {"entities": changed})["status"])
+
+ def test_new_attributes_allowed_but_changed_old_values_fail(self):
+ old = entity_state({"id": "minecraft:wolf", "Attributes": [{"Name": "generic.maxHealth", "Base": 20}]})
+ new = entity_state({"id": "minecraft:wolf", "Attributes": [{"Name": "forge.swimSpeed", "Base": 1},
+ {"Name": "generic.maxHealth", "Base": 20}]})
+ self.assertEqual("PASS", verdict({"entities": {"wolf": old}}, {"entities": {"wolf": new}})["status"])
+ new["Attributes"]["generic.maxHealth"]["Base"] = 10
+ self.assertEqual("FAIL", verdict({"entities": {"wolf": old}}, {"entities": {"wolf": new}})["status"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_drill_restart.py b/test-rig/migration/test_drill_restart.py
new file mode 100644
index 00000000..239b0fa7
--- /dev/null
+++ b/test-rig/migration/test_drill_restart.py
@@ -0,0 +1,47 @@
+"""Native first-tick checks cannot accept a reset or a newly primed drill."""
+import copy
+import unittest
+
+from connected import entries
+from drill_restart import first_tick
+from test_connected import nbt, sample
+
+
+def checkpoints():
+ before = sample()
+ for station, value in (("C-SDRILL", 17), ("C-EDRILL", 209)):
+ nbt(entries(before)[station + "-drill"], '{progress:%ds,targetX:1,targetY:80,targetZ:42,Items:[]}' % value)
+ after = copy.deepcopy(before)
+ after.update(requestedTicks=1, advancedTicks=1)
+ for station, value in (("C-SDRILL", 18), ("C-EDRILL", 210)):
+ nbt(entries(after)[station + "-drill"], '{progress:%ds,targetX:1,targetY:80,targetZ:42,Items:[]}' % value)
+ return before, after
+
+
+class DrillRestartTest(unittest.TestCase):
+ def test_both_midcycle_drills_continue(self):
+ self.assertEqual(first_tick(*checkpoints())["status"], "PASS")
+
+ def test_each_legacy_reset_is_rejected(self):
+ for station, value in (("C-SDRILL", 0), ("C-EDRILL", 1)):
+ before, after = checkpoints()
+ nbt(entries(after)[station + "-drill"], '{progress:%ds,targetX:1,targetY:80,targetZ:42,Items:[]}' % value)
+ self.assertEqual(first_tick(before, after)["status"], "FAIL")
+
+ def test_cold_drill_is_not_saved_work_evidence(self):
+ before, after = checkpoints()
+ nbt(entries(before)["C-SDRILL-drill"], '{progress:0s,targetX:1,targetY:80,targetZ:42,Items:[]}')
+ nbt(entries(after)["C-SDRILL-drill"], '{progress:1s,targetX:1,targetY:80,targetZ:42,Items:[]}')
+ self.assertEqual(first_tick(before, after)["status"], "FAIL")
+
+ def test_replacement_and_unmeasured_work_are_rejected(self):
+ before, after = checkpoints()
+ entries(after)["C-EDRILL-drill"]["objectIdentity"] = -1
+ self.assertEqual(first_tick(before, after)["status"], "FAIL")
+ before, after = checkpoints()
+ after["advancedTicks"] = 0
+ self.assertEqual(first_tick(before, after)["status"], "FAIL")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_endurance.py b/test-rig/migration/test_endurance.py
new file mode 100644
index 00000000..36bb43c8
--- /dev/null
+++ b/test-rig/migration/test_endurance.py
@@ -0,0 +1,151 @@
+"""Negative controls for the endurance oracle; no server or world writes."""
+
+from pathlib import Path
+import unittest
+import tempfile
+from unittest.mock import patch
+
+from endurance import CHECKED, PROCESSING, Rig, parse_snapshot
+from audit_endurance import audit, expected_inventory_acknowledgements
+from matrix import write_json
+
+
+class EnduranceOracleTests(unittest.TestCase):
+ def test_native_feed_acknowledgement_totals(self):
+ self.assertEqual(expected_inventory_acknowledgements(1), (448, 439))
+ self.assertEqual(expected_inventory_acknowledgements(30), (13179, 439))
+
+ def test_audit_rejects_old_nbt_fed_pass(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ write_json(root / "endurance-results.json", {"status": "PASS", "duration_seconds": 1800,
+ "interval_seconds": 60})
+ with self.assertRaisesRegex(AssertionError, "diagnostics only"): audit(root)
+
+ def setUp(self):
+ definitions = {name: {"origin": [0, 0, 0]} for name in CHECKED}
+ self.rig = Rig("test", Path("unused"), None, None, definitions)
+ self.rig.snapshots = {name: {} for name in CHECKED}
+ self.rig.documents = {"W-01": {"worldTime": 1200}}
+ self.rig.invariants = lambda: None
+ for _, station, pos, neighbours, _, output, _, count in PROCESSING:
+ self.put(station, pos, [("minecraft:" + output, count)])
+ for neighbour in neighbours: self.put(station, neighbour)
+ for x in (2, 3, 4): self.put("W-04", (x, 1, 6))
+ self.put("W-04", (5, 1, 6), [("minecraft:dirt", 16)])
+ for z in (9, 15, 18):
+ self.put("W-04", (2, 1, z))
+ self.put("W-04", (3, 1, z))
+ forward = [("minecraft:apple", 8)]
+ if z == 15: forward.append(("minecraft:dirt", 8))
+ self.put("W-04", (4, 1, z), forward)
+ self.put("W-04", (3, 0, z), [] if z == 15 else [("minecraft:dirt", 8)])
+ self.put("W-03", (4, 1, 8))
+ self.put("W-03", (6, 1, 8))
+ self.put("W-03", (8, 1, 8))
+ self.rig.entry("W-03", (8, 1, 8))["data"]["Tank"] = {
+ "FluidName": "poweradvantage:refined_oil", "Amount": 1000}
+
+ def put(self, station, pos, items=()):
+ self.rig.snapshots[station][pos] = {"pos": pos, "data": {"Items": [
+ {"Slot": slot, "id": name, "Count": count} for slot, (name, count) in enumerate(items)]}}
+
+ def check(self):
+ with patch("endurance.write_json"):
+ return self.rig.batch("test")
+
+ def test_valid_complete_batch(self):
+ self.assertEqual(self.check()["outputs"]["distillation-mB"], 1000)
+
+ def test_missing_item_fails(self):
+ self.put("E-03", (6, 1, 6), [("minecraft:iron_ingot", 3)])
+ with self.assertRaisesRegex(AssertionError, "incorrect yield"): self.check()
+
+ def test_duplicated_item_fails(self):
+ self.put("S-04", (3, 1, 13), [("minecraft:gravel", 5)])
+ with self.assertRaisesRegex(AssertionError, "incorrect yield"): self.check()
+
+ def test_unexpected_output_fails(self):
+ self.put("E-03", (3, 1, 6), [("minecraft:gravel", 4), ("minecraft:diamond", 1)])
+ with self.assertRaisesRegex(AssertionError, "unexpected items"): self.check()
+
+ def test_refined_oil_in_shared_supply_tank_is_counted(self):
+ tank = self.rig.entry("W-03", (8, 1, 8))["data"].pop("Tank")
+ self.rig.entry("W-03", (4, 1, 8))["data"]["Tank"] = tank
+ self.assertEqual(self.check()["outputs"]["distillation-mB"], 1000)
+
+ def test_unprocessed_input_fails(self):
+ self.put("E-03", (3, 1, 6), [("minecraft:cobblestone", 1), ("minecraft:gravel", 4)])
+ with self.assertRaisesRegex(AssertionError, "input remains"): self.check()
+
+ def test_wrong_filter_branch_fails(self):
+ self.put("W-04", (4, 1, 18), [("minecraft:dirt", 8)])
+ self.put("W-04", (3, 0, 18), [("minecraft:apple", 8)])
+ with self.assertRaisesRegex(AssertionError, "filter 18 forward"): self.check()
+
+ def test_conveyor_duplication_fails(self):
+ self.put("W-04", (3, 1, 6), [("minecraft:dirt", 1)])
+ with self.assertRaisesRegex(AssertionError, "conveyor conservation"): self.check()
+
+ def test_fluid_loss_fails(self):
+ self.rig.entry("W-03", (8, 1, 8))["data"]["Tank"]["Amount"] = 999
+ with self.assertRaisesRegex(AssertionError, "oil output"): self.check()
+
+ def test_wrong_fluid_identity_fails(self):
+ self.rig.entry("W-03", (8, 1, 8))["data"]["Tank"]["FluidName"] = "minecraft:water"
+ with self.assertRaisesRegex(AssertionError, "wrong oil output identity"): self.check()
+
+ def test_pending_oil_fails(self):
+ self.rig.entry("W-03", (6, 1, 8))["data"]["InputTank"] = {"Amount": 1, "FluidName": "poweradvantage:crude_oil"}
+ with self.assertRaisesRegex(AssertionError, "oil input remaining"): self.check()
+
+ def test_snbt_keeps_quantities_and_float_bits(self):
+ snapshot = parse_snapshot({"snapshot": [{"pos": [1, 2, 3], "nbt":
+ '{Items:[{Slot:0b,id:"minecraft:dirt",Count:16b}],Energy:[I;1065353216]}'}]})
+ self.assertEqual(snapshot[(1, 2, 3)]["data"]["Energy"], [1065353216])
+ self.assertEqual(snapshot[(1, 2, 3)]["data"]["Items"][0]["Count"], 16)
+
+ def invariant_fixture(self):
+ for station, pos, amount in (("W-01", (13, 1, 7), 2000), ("S-06", (20, 5, 20), 1000),
+ ("P-API-01", (8, 1, 7), 4000)):
+ self.put(station, pos)
+ self.rig.entry(station, pos)["data"]["Tank"] = {"FluidName": "minecraft:water", "Amount": amount}
+ self.put("S-01", (12, 1, 7))
+ self.rig.entry("S-01", (12, 1, 7))["data"]["Temperature"] = 900
+ self.put("E-02", (3, 1, 12), [("electricadvantage:lead_acid_battery", 1)])
+ self.rig.static_batteries = self.rig.entry("E-02", (3, 1, 12))["data"]["Items"]
+ self.put("E-01", (4, 4, 12), [("electricadvantage:lead_acid_battery", 1)])
+ self.rig.entry("E-01", (4, 4, 12))["data"]["Items"][0]["tag"] = {"energy": 100}
+
+ def test_closed_tank_loss_fails(self):
+ self.invariant_fixture()
+ self.rig.entry("S-06", (20, 5, 20))["data"]["Tank"]["Amount"] = 999
+ with self.assertRaisesRegex(AssertionError, "closed water witness"): Rig.invariants(self.rig)
+
+ def test_saved_battery_item_loss_fails(self):
+ self.invariant_fixture()
+ self.put("E-02", (3, 1, 12))
+ with self.assertRaisesRegex(AssertionError, "stored battery"): Rig.invariants(self.rig)
+
+ def test_negative_energy_fails(self):
+ self.invariant_fixture()
+ self.rig.entry("E-03", (6, 1, 6))["data"]["Energy"] = [-1082130432]
+ with self.assertRaisesRegex(AssertionError, "invalid energy"): Rig.invariants(self.rig)
+
+ def test_battery_overcharge_fails(self):
+ self.invariant_fixture()
+ self.rig.entry("E-01", (4, 4, 12))["data"]["Items"][0]["tag"]["energy"] = 1001
+ with self.assertRaisesRegex(AssertionError, "out-of-range battery"): Rig.invariants(self.rig)
+
+ def test_inventory_rearming_uses_inventory_commands_not_nbt(self):
+ self.rig.entry("E-03", (3, 1, 6))["block"] = "electricadvantage:electric_crusher"
+ commands = []
+ self.rig.command = lambda command, response=None: commands.append(command)
+ self.rig.inventory("E-03", (3, 1, 6), [(0, "minecraft:cobblestone", 4)])
+ self.assertEqual(len(commands), 9)
+ self.assertTrue(all(c.startswith("replaceitem block ") for c in commands))
+ self.assertIn("container.0 minecraft:cobblestone 4", commands[0])
+ self.assertTrue(all("minecraft:air 1" in c for c in commands[1:]))
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test-rig/migration/test_extended_lifecycle.py b/test-rig/migration/test_extended_lifecycle.py
new file mode 100644
index 00000000..9578fa40
--- /dev/null
+++ b/test-rig/migration/test_extended_lifecycle.py
@@ -0,0 +1,106 @@
+"""Negative controls for partial unload and extended drill acceptance."""
+import copy
+import unittest
+from extended_lifecycle import fixtures, output_results, partial_result, qualification, reserve_results, redstone_results, restart_results
+
+
+class ExtendedOracleTests(unittest.TestCase):
+ def test_geometry_and_coverage(self):
+ definitions = fixtures()
+ self.assertEqual(len(definitions), 21)
+ for f in definitions:
+ for p in f["witnesses"]:
+ self.assertTrue(all(a <= n <= b for a, n, b in zip(f["min"], p, f["max"])))
+ if "route" not in f: continue
+ self.assertEqual(len(f["route"]), 5)
+ for a, b in zip(f["route"], f["route"][1:]): self.assertEqual(sum(abs(x-y) for x,y in zip(a,b)), 1)
+ self.assertTrue(all(sum(abs(x-y) for x,y in zip(a,b)) == 1 for a,b in zip(f["route"], f["controls"])))
+ self.assertEqual(sum(f.get("junction", False) for f in definitions), 2)
+ self.assertEqual(sum(f.get("track", [0,0,0])[1] != 0 for f in definitions), 6)
+
+ def partial(self):
+ f = next(f for f in fixtures() if f["id"] == "C-FLUID")
+ source, receiver = f["groups"]["supply"][0], f["groups"]["machine"][0]
+ chunks = [{"pos": source, "loaded": True, "unloads": 0, "loads": 1},
+ {"pos": receiver, "loaded": True, "unloads": 0, "loads": 1}]
+ entries = [{"pos": p, "block": "pipe", "state": "pipe"} for p in f["witnesses"]]
+ entries[-1].update(tileIdentity=42, nbt='{Tank:{FluidName:"minecraft:water",Amount:1000}}')
+ before = {"worldTime": 0, "chunks": chunks, "cases": {f["id"]: entries}}
+ absent = copy.deepcopy(before); absent["chunks"][0].update(loaded=False, unloads=1)
+ loaded = copy.deepcopy(before); loaded["chunks"][0].update(unloads=1, loads=2)
+ confirmed = copy.deepcopy(absent); confirmed.update(confirmed=True, worldTime=40)
+ return f, before, [absent, confirmed], loaded
+
+ def test_true_partial_unload_passes(self):
+ f, before, samples, loaded = self.partial()
+ self.assertEqual(partial_result([f], before, samples, loaded, "supply")[f["id"]]["status"], "PASS")
+
+ def test_automatic_reload_is_not_partial_pass(self):
+ f, before, samples, loaded = self.partial()
+ for s in samples: s["chunks"][0]["loaded"] = True
+ r = partial_result([f], before, samples, loaded, "supply")[f["id"]]
+ self.assertEqual(r["status"], "FAIL"); self.assertTrue(r["real_unload_event"])
+
+ def test_snapshot_without_unload_event_fails(self):
+ f, before, samples, loaded = self.partial()
+ for s in samples: s["chunks"][0]["unloads"] = 0
+ self.assertEqual(partial_result([f], before, samples, loaded, "supply")[f["id"]]["status"], "FAIL")
+
+ def test_retained_endpoint_replacement_fails(self):
+ f, before, samples, loaded = self.partial(); samples[0]["cases"][f["id"]][-1]["tileIdentity"] = 43
+ self.assertEqual(partial_result([f], before, samples, loaded, "supply")[f["id"]]["status"], "FAIL")
+
+ def test_one_instant_absence_is_insufficient(self):
+ f, before, samples, loaded = self.partial(); samples[0]["chunks"][0]["loaded"] = True
+ self.assertEqual(partial_result([f], before, samples, loaded, "supply")[f["id"]]["status"], "FAIL")
+
+ def test_short_absence_is_not_two_seconds(self):
+ f, before, samples, loaded = self.partial(); samples[-1]["worldTime"] = 39
+ self.assertEqual(partial_result([f], before, samples, loaded, "supply")[f["id"]]["status"], "FAIL")
+
+ def test_redstone_needs_progress_cancel_and_resume(self):
+ f = fixtures()[0]
+ def snap(progress): return {"cases": {f["id"]: [{"pos": f["to"], "block": f["block"], "state": "drill[facing="+f["facing"]+"]", "nbt": "{progress:%ds,Items:[]}" % progress}]}}
+ self.assertEqual(redstone_results([f], snap(80), snap(0), snap(60))[f["id"]]["status"], "PASS")
+ self.assertEqual(redstone_results([f], snap(80), snap(0), snap(0))[f["id"]]["status"], "FAIL")
+
+ def test_failure_propagates_to_overall_qualification(self):
+ result = {k: {"case": {"status": "PASS"}} for k in ("insufficient_energy", "moves", "blocked_output", "restart", "blocked_after_restart", "resumed_output", "redstone")}
+ result.update(partial_networks=[], reload_orders=[], network_processing="PASS")
+ self.assertEqual(qualification(result), "PASS")
+ result["partial_networks"].append({"cases": {"case": {"status": "FAIL"}}})
+ self.assertEqual(qualification(result), "FAIL")
+
+ def output(self):
+ f = fixtures()[0]
+ tagged = '{Slot:0b,id:"minecraft:dirt",Count:7b,tag:{AdvWorksWitness:"' + f["id"] + '"}}'
+ drill = {"pos": f["to"], "block": f["block"], "state": "drill[facing=down]",
+ "nbt": '{Items:['+tagged+',{Slot:1b,id:"minecraft:apple",Count:13b},{Slot:2b,id:"minecraft:gravel",Count:1b}]}'}
+ stone = ','.join('{Slot:%db,id:"minecraft:stone",Count:64b}' % n for n in range(27))
+ chest = {"pos": f["rear"], "block": "minecraft:chest", "state": "chest", "nbt": '{Items:['+stone+']}'}
+ return f, {"cases": {f["id"]: [drill, chest]}}
+
+ def test_blocked_output_conserves_items(self):
+ f, snapshot = self.output()
+ self.assertEqual(output_results([f], snapshot, True)[f["id"]]["status"], "PASS")
+ snapshot["cases"][f["id"]][0]["nbt"] = snapshot["cases"][f["id"]][0]["nbt"].replace("Count:7b", "Count:6b")
+ self.assertEqual(output_results([f], snapshot, True)[f["id"]]["status"], "FAIL")
+
+ def test_unchanged_output_cannot_pass_resume(self):
+ f, snapshot = self.output()
+ self.assertEqual(output_results([f], snapshot, False)[f["id"]]["status"], "FAIL")
+
+ def test_custom_tag_loss_fails(self):
+ f, snapshot = self.output()
+ snapshot["cases"][f["id"]][0]["nbt"] = snapshot["cases"][f["id"]][0]["nbt"].replace(f["id"], "lost")
+ self.assertIn("custom item tag lost", output_results([f], snapshot, True)[f["id"]]["failures"])
+
+ def test_insufficient_reserve_is_measured(self):
+ f = fixtures()[0]
+ entry = {"pos": f["from"], "block": f["block"], "energy": {"steam": 40}, "nbt": "{Items:[]}"}
+ tracks = [{"pos": pos, "block": "steamadvantage:steam_track"} for pos in f["route"][1:]]
+ snap = {"movements": [], "cases": {f["id"]: [entry]+tracks}}
+ self.assertEqual(reserve_results([f], snap, snap)[f["id"]]["status"], "FAIL")
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test-rig/migration/test_lifecycle.py b/test-rig/migration/test_lifecycle.py
new file mode 100644
index 00000000..45a2bd24
--- /dev/null
+++ b/test-rig/migration/test_lifecycle.py
@@ -0,0 +1,86 @@
+"""Negative controls for the lifecycle oracle."""
+import unittest
+from lifecycle import LifecycleRun, changed_target_results, fixtures, move_result, progress_results, require_unloaded
+
+
+class LifecycleOracleTests(unittest.TestCase):
+ def test_all_facings_and_chunk_crossings(self):
+ cases = [f for f in fixtures() if "from" in f]
+ self.assertEqual(len(cases), 12)
+ for f in cases:
+ self.assertLessEqual(f["initialEnergy"], 50 if f["energyType"] == "steam" else 500)
+ self.assertNotEqual((f["from"][0] >> 4, f["from"][2] >> 4), (f["to"][0] >> 4, f["to"][2] >> 4))
+ for p in f["witnesses"]:
+ self.assertTrue(all(low <= a <= high for a, low, high in zip(p, f["min"], f["max"])))
+
+ def test_fake_unload_fails(self):
+ before = {"chunks": [{"pos": [1, 2], "loaded": True, "unloads": 0}]}
+ for loaded, count in ((True, 1), (False, 0)):
+ with self.assertRaises(AssertionError):
+ require_unloaded(before, {"chunks": [{"pos": [1, 2], "loaded": loaded, "unloads": count}]})
+ require_unloaded(before, {"chunks": [{"pos": [1, 2], "loaded": False, "unloads": 1}]})
+
+ def movement(self):
+ fixture = next(f for f in fixtures() if f["id"] == "E-MOVE-east")
+ nbt = '{x:%d,y:%d,z:%d,Items:[{Slot:0b,id:"minecraft:dirt",Count:7b},{Slot:1b,id:"minecraft:apple",Count:13b}]}'
+ before = {"nbt": nbt % tuple(fixture["from"]), "energy": {"electricity": 400}}
+ after = {"pos": fixture["to"], "state": "electric_drill[facing=east]", "nbt": nbt % tuple(fixture["to"]),
+ "energy": {"electricity": 150}}
+ return fixture, {"before": before, "after": after, "oldPosition": {"block": "electricadvantage:electric_conduit"}}
+
+ def test_valid_move(self):
+ fixture, move = self.movement()
+ self.assertEqual(move_result(fixture, move)["status"], "PASS")
+
+ def test_unpaid_energy_detected(self):
+ fixture, move = self.movement(); move["after"]["energy"]["electricity"] = 400
+ self.assertEqual(move_result(fixture, move)["status"], "FAIL")
+
+ def test_inventory_loss_detected(self):
+ fixture, move = self.movement(); move["after"]["nbt"] = move["after"]["nbt"].replace("Count:7b", "Count:6b")
+ self.assertEqual(move_result(fixture, move)["status"], "FAIL")
+
+ def test_wrong_position_and_facing_detected(self):
+ fixture, move = self.movement(); move["after"]["state"] = "electric_drill[facing=up]"
+ move["after"]["nbt"] = move["after"]["nbt"].replace("x:" + str(fixture["to"][0]), "x:0")
+ self.assertEqual(len(move_result(fixture, move)["failures"]), 2)
+
+ def test_midblock_progress_reset_detected(self):
+ fixture, _ = self.movement()
+ before = {"cases": {fixture["id"]: [{"pos": fixture["to"], "nbt": "{progress:80s,targetX:3,targetY:4,targetZ:5}"}]}}
+ after = {"cases": {fixture["id"]: [{"pos": fixture["to"], "nbt": "{progress:5s,targetX:3,targetY:4,targetZ:5}"}]}}
+ self.assertEqual(progress_results([fixture], before, after)[fixture["id"]]["status"], "FAIL")
+ after["cases"][fixture["id"]][0]["nbt"] = "{progress:81s,targetX:3,targetY:4,targetZ:5}"
+ self.assertEqual(progress_results([fixture], before, after)[fixture["id"]]["status"], "PASS")
+
+ def mining_snapshot(self):
+ fixture = fixtures()[0]
+ items = ('[{Slot:0b,id:"minecraft:dirt",Count:7b,tag:{AdvWorksWitness:"' + fixture["id"]
+ + '"}},{Slot:1b,id:"minecraft:apple",Count:13b},{Slot:2b,id:"minecraft:gravel",Count:1b}]')
+ drill = {"pos": fixture["to"], "state": "steam_drill[facing=down]", "energy": {"steam": 40}, "nbt": "{Items:[]}"}
+ chest = {"pos": fixture["rear"], "nbt": "{Items:" + items + "}"}
+ return fixture, {"cases": {fixture["id"]: [drill, chest]}}
+
+ def test_changed_target_must_clear_progress_and_coordinates(self):
+ fixture, _ = self.movement()
+ before = {"cases": {fixture["id"]: [{"pos": fixture["to"], "nbt": "{progress:80s,targetX:3,targetY:4,targetZ:5}"}]}}
+ after = {"cases": {fixture["id"]: [{"pos": fixture["to"], "nbt": "{progress:0s}"}]}}
+ self.assertEqual(changed_target_results([fixture], before, after)[fixture["id"]]["status"], "PASS")
+ for nbt in ("{progress:80s}", "{progress:0s,targetX:3,targetY:4,targetZ:5}"):
+ after["cases"][fixture["id"]][0]["nbt"] = nbt
+ self.assertEqual(changed_target_results([fixture], before, after)[fixture["id"]]["status"], "FAIL")
+
+ def test_rear_output_passes_with_all_quantities_and_tags(self):
+ fixture, snapshot = self.mining_snapshot()
+ runner = LifecycleRun(None, None, None, [fixture])
+ self.assertEqual(runner.mining_results(snapshot)[fixture["id"]]["status"], "PASS")
+
+ def test_items_retained_in_drill_do_not_pass_rear_output(self):
+ fixture, snapshot = self.mining_snapshot()
+ drill, chest = snapshot["cases"][fixture["id"]]
+ drill["nbt"], chest["nbt"] = chest["nbt"], drill["nbt"]
+ runner = LifecycleRun(None, None, None, [fixture])
+ self.assertEqual(runner.mining_results(snapshot)[fixture["id"]]["failures"], ["not all output reached rear chest"])
+
+
+if __name__ == "__main__": unittest.main()
diff --git a/test-rig/migration/test_matrix.py b/test-rig/migration/test_matrix.py
new file mode 100644
index 00000000..0d226137
--- /dev/null
+++ b/test-rig/migration/test_matrix.py
@@ -0,0 +1,47 @@
+"""The state oracle must detect loss rather than bless it as flattening."""
+
+import unittest
+from matrix import differences, normalized
+
+
+class ComparisonTest(unittest.TestCase):
+ def test_nested_inventory_loss_and_duplication_fail(self):
+ before = {"Items": [{"id": "steamadvantage:musket", "Count": 1, "tag": {"Damage": 5}}]}
+ self.assertTrue(differences(before, {"Items": []}))
+ self.assertTrue(differences(before, {"Items": before["Items"] * 2}))
+ self.assertTrue(differences(before, {"Items": [{"id": "minecraft:air", "Count": 1}]}))
+ self.assertTrue(differences(before, {"Items": [{"id": "steamadvantage:musket", "Count": 2}]}))
+
+ def test_fluid_energy_progress_and_temperature_loss_fail(self):
+ for field, value in (("Tank", {"FluidName": "water", "Amount": 750}),
+ ("Energy", [1120403456]), ("progress", 7), ("Temperature", 37.5)):
+ self.assertTrue(differences({field: value}, {}), field)
+ self.assertTrue(differences({field: value}, {field: 0}), field)
+
+ def test_legacy_enchantments_use_world_registry_not_vanilla_numbers(self):
+ old = {"id": "steamadvantage:musket", "Count": 1, "Damage": 5,
+ "tag": {"ench": [{"id": 11, "lvl": 2}]}}
+ modern = {"id": "steamadvantage:musket", "Count": 1,
+ "tag": {"Damage": 5, "Enchantments": [{"id": "steamadvantage:spreadshot", "lvl": 2}]}}
+ self.assertEqual(normalized(old, {11: "steamadvantage:spreadshot"}), modern)
+ self.assertTrue(differences(normalized(old, {11: "steamadvantage:spreadshot"}),
+ normalized(old, {11: "minecraft:binding_curse"})))
+
+ def test_only_complete_registered_oil_buckets_are_normalized(self):
+ old = {"id": "forge:bucketfilled", "Count": 1, "Damage": 0,
+ "tag": {"FluidName": "crude_oil", "Amount": 1000, "Witness": "keep"}}
+ expected = {"id": "poweradvantage:crude_oil_bucket", "Count": 1, "tag": {"Witness": "keep"}}
+ self.assertEqual(normalized(old), expected)
+ old["tag"]["Amount"] = 750
+ self.assertTrue(differences(normalized(old), expected))
+
+ def test_tile_aliases_and_empty_marker_do_not_hide_state_loss(self):
+ self.assertEqual(normalized({"id": "minecraft:steamadvantage.steam_pump"})["id"], "steamadvantage:steam_pump")
+ self.assertEqual(normalized({"id": "electricadvantage:l_e_d"})["id"], "electricadvantage:led")
+ self.assertEqual(normalized({"Tank": {"Empty": 0}}), {"Tank": {"Empty": True}})
+ self.assertTrue(differences(normalized({"Tank": {"FluidName": "water", "Amount": 750}}),
+ normalized({"Tank": {"Empty": 1}})))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_oil_transfer.py b/test-rig/migration/test_oil_transfer.py
new file mode 100644
index 00000000..350b41b1
--- /dev/null
+++ b/test-rig/migration/test_oil_transfer.py
@@ -0,0 +1,54 @@
+"""Negative controls for exact oil identity, conservation, and restart-refill oracles."""
+import copy
+import unittest
+
+from oil_transfer import fixtures, inspect, require_same_machines
+
+
+class OilTransferTests(unittest.TestCase):
+ def sample(self):
+ definition = fixtures()
+ snapshot = {"cases": {}}
+ for f in definition:
+ rows = [{"pos": f["source"], "block": "minecraft:air"}]
+ rows.append({"pos": f["drain"], "block": "poweradvantage:fluid_drain", "tileIdentity": 1, "nbt": "{Tank:{Empty:1b}}"})
+ rows.extend({"pos": p, "block": "poweradvantage:fluid_pipe"} for p in f["pipes"])
+ rows.append({"pos": f["output"], "block": f["tank"], "tileIdentity": 2,
+ "nbt": '{Tank:{Amount:1000,FluidName:"' + f["fluid"] + '"}}'})
+ snapshot["cases"][f["id"]] = rows
+ return definition, snapshot
+
+ def test_exact_lanes_pass(self):
+ definition, snapshot = self.sample()
+ self.assertEqual(len(inspect(snapshot, definition, 1000)), 4)
+
+ def test_changed_fluid_fails(self):
+ definition, snapshot = self.sample()
+ snapshot["cases"][definition[0]["id"]][-1]["nbt"] = '{Tank:{Amount:1000,FluidName:"minecraft:water"}}'
+ with self.assertRaises(AssertionError): inspect(snapshot, definition, 1000)
+
+ def test_loss_duplication_and_stuck_fluid_fail(self):
+ definition, original = self.sample()
+ for amount in (0, 999, 1001, 2000):
+ snapshot = copy.deepcopy(original)
+ snapshot["cases"][definition[0]["id"]][-1]["nbt"] = '{Tank:{Amount:' + str(amount) + ',FluidName:"poweradvantage:crude_oil"}}'
+ with self.assertRaises(AssertionError): inspect(snapshot, definition, 1000)
+ original["cases"][definition[0]["id"]][1]["nbt"] = '{Tank:{Amount:1}}'
+ with self.assertRaises(AssertionError): inspect(original, definition, 1000)
+
+ def test_refill_cannot_replace_endpoint(self):
+ definition, before = self.sample()
+ require_same_machines(before, copy.deepcopy(before), definition)
+ after = copy.deepcopy(before)
+ after["cases"][definition[0]["id"]][-1]["tileIdentity"] = 3
+ with self.assertRaises(AssertionError): require_same_machines(before, after, definition)
+
+ def test_lanes_are_bounded_separated_and_connected(self):
+ definition = fixtures()
+ self.assertEqual(len({f["bucket"] for f in definition}), 2)
+ self.assertEqual(len({f["tank"] for f in definition}), 2)
+ for f in definition:
+ for p in f["witnesses"]:
+ self.assertTrue(all(low <= value <= high for low, value, high in zip(f["min"], p, f["max"])))
+ route = [f["source"], f["drain"], *f["pipes"], f["output"]]
+ for a, b in zip(route, route[1:]): self.assertEqual(sum(abs(x-y) for x,y in zip(a,b)), 1)
diff --git a/test-rig/migration/test_pin_dimension_profiles.py b/test-rig/migration/test_pin_dimension_profiles.py
new file mode 100644
index 00000000..8b2e930d
--- /dev/null
+++ b/test-rig/migration/test_pin_dimension_profiles.py
@@ -0,0 +1,62 @@
+import json
+from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
+import zipfile
+
+from matrix import digest
+from pin_dimension_profiles import pin
+
+
+class DimensionProfilePins(unittest.TestCase):
+ def fixture(self, root):
+ probe = root / "test-rig/build/libs/AdvantageWorksTestHarness-1.1.0.113021.jar"
+ probe.parent.mkdir(parents=True)
+ with zipfile.ZipFile(probe, "w") as archive:
+ archive.writestr("zone/moddev/mc/advantageworks/DimensionEntityProbe.class", b"fixture-probe")
+ production = root / "PowerAdvantage.jar"
+ production.write_bytes(b"unchanged-production")
+ original = {"target113": {"seed": "unused", "launcher": "unused", "jars": [
+ {"path": str(production), "sha256": digest(production)},
+ {"path": str(probe), "sha256": "previous-probe-hash"}]}}
+ base = root / "profiles.json"
+ base.write_text(json.dumps(original))
+ return base, production, probe, original
+
+ def test_only_probes_are_replaced_and_archived(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ base, production, probe, original = self.fixture(root)
+ with patch("pin_dimension_profiles.POWER", root):
+ result = pin(base, root / "new/profiles.json", root / "archive")
+ jars = result["target113"]["jars"]
+ self.assertEqual(original["target113"]["jars"][0], jars[0])
+ archived = Path(jars[1]["path"])
+ self.assertNotEqual(probe, archived)
+ self.assertEqual(digest(probe), digest(archived))
+ self.assertEqual(original, json.loads(base.read_text()))
+ probe.write_bytes(b"later-build")
+ self.assertEqual(jars[1]["sha256"], digest(archived))
+ self.assertEqual(original["target113"]["jars"][0]["sha256"], digest(production))
+
+ def test_changed_production_jar_is_rejected(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ base, production, _, _ = self.fixture(root)
+ production.write_bytes(b"different-production")
+ with patch("pin_dimension_profiles.POWER", root), self.assertRaisesRegex(AssertionError, "Production/dependency"):
+ pin(base, root / "new/profiles.json")
+
+ def test_missing_dimension_probe_is_rejected(self):
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ base, _, probe, _ = self.fixture(root)
+ with zipfile.ZipFile(probe, "w") as archive:
+ archive.writestr("unrelated.class", b"not-a-dimension-probe")
+ with patch("pin_dimension_profiles.POWER", root), self.assertRaisesRegex(AssertionError, "probe missing"):
+ pin(base, root / "new/profiles.json")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_pipe_only_lifecycle.py b/test-rig/migration/test_pipe_only_lifecycle.py
new file mode 100644
index 00000000..e8273971
--- /dev/null
+++ b/test-rig/migration/test_pipe_only_lifecycle.py
@@ -0,0 +1,54 @@
+"""Check bridge geometry and the independent pipe-only oracle's negative controls."""
+import copy
+import unittest
+
+from audit_pipe_only import bridge_result
+from pipe_only_lifecycle import fixtures
+
+
+class PipeOnlyTests(unittest.TestCase):
+ def test_bridge_contains_only_pipes_and_breaks_the_only_path(self):
+ for f in fixtures():
+ bridge = tuple(f["groups"]["bridge"][0])
+ self.assertTrue(all((p[0] >> 4, p[2] >> 4) != bridge for p in f["witnesses"][:2]))
+ remaining = {tuple(p) for p in f["witnesses"] if (p[0] >> 4, p[2] >> 4) != bridge}
+ visited = {tuple(f["witnesses"][0])}
+ while True:
+ added = {p for p in remaining if any(sum(abs(a-b) for a,b in zip(p,q)) == 1 for q in visited)} - visited
+ if not added: break
+ visited |= added
+ self.assertNotIn(tuple(f["witnesses"][1]), visited)
+
+ def samples(self):
+ f = fixtures()[2]
+ endpoint = f["groups"]["endpoints"][0]
+ bridge = f["groups"]["bridge"][0]
+ before = {"worldTime": 0, "chunks": [{"pos": endpoint, "loaded": True, "unloads": 0, "loads": 1},
+ {"pos": bridge, "loaded": True, "unloads": 0, "loads": 1}],
+ "cases": {f["id"]: [{"pos": p, "block": "pipe", "state": "pipe"} for p in f["witnesses"]]}}
+ before["cases"][f["id"]][1].update(tileIdentity=1, nbt='{Tank:{FluidName:"minecraft:water",Amount:0}}')
+ absent = copy.deepcopy(before); absent["chunks"][1].update(loaded=False, unloads=1)
+ confirmed = copy.deepcopy(absent); confirmed["worldTime"] = 60
+ loaded = copy.deepcopy(before); loaded["chunks"][1].update(unloads=1, loads=2)
+ resumed = copy.deepcopy(loaded); resumed["cases"][f["id"]][1]["nbt"] = '{Tank:{FluidName:"minecraft:water",Amount:1000}}'
+ return f, [before, absent, confirmed, loaded, resumed]
+
+ def test_valid_bridge_passes(self):
+ f, samples = self.samples()
+ self.assertEqual(bridge_result([f], *samples, 0)["status"], "PASS")
+
+ def test_automatic_loading_fails(self):
+ f, samples = self.samples(); samples[2]["chunks"][1]["loaded"] = True
+ self.assertEqual(bridge_result([f], *samples, 0)["status"], "FAIL")
+
+ def test_missing_event_fails(self):
+ f, samples = self.samples(); samples[1]["chunks"][1]["unloads"] = 0
+ self.assertEqual(bridge_result([f], *samples, 0)["status"], "FAIL")
+
+ def test_endpoint_replacement_fails(self):
+ f, samples = self.samples(); samples[4]["cases"][f["id"]][1]["tileIdentity"] = 2
+ self.assertEqual(bridge_result([f], *samples, 0)["status"], "FAIL")
+
+ def test_fluid_duplication_fails(self):
+ f, samples = self.samples(); samples[4]["cases"][f["id"]][1]["nbt"] = '{Tank:{Amount:2000}}'
+ self.assertEqual(bridge_result([f], *samples, 0)["status"], "FAIL")
diff --git a/test-rig/migration/test_progression.py b/test-rig/migration/test_progression.py
new file mode 100644
index 00000000..2c71d105
--- /dev/null
+++ b/test-rig/migration/test_progression.py
@@ -0,0 +1,32 @@
+import unittest
+from progression import EXCLUSIONS, NATURAL_RESOURCES, fixture, validate_tags
+
+
+class ManufacturingOracleTest(unittest.TestCase):
+ def test_natural_basis_cannot_supply_finished_advantage_parts(self):
+ self.assertEqual(len(NATURAL_RESOURCES), len(set(NATURAL_RESOURCES)))
+ advantage = [item for item in NATURAL_RESOURCES if item.split(":")[0]
+ in ("poweradvantage", "steamadvantage", "electricadvantage")]
+ self.assertEqual(["electricadvantage:li_ore", "electricadvantage:sulfur_ore"], advantage)
+ self.assertTrue(all(item.endswith("_ore") for item in NATURAL_RESOURCES if item.startswith("basemetals:")))
+
+ def test_exclusions_do_not_hide_craftable_materials_or_machines(self):
+ for item in ("electricadvantage:psu", "poweradvantage:starch", "electricadvantage:li_nugget",
+ "poweradvantage:converter_rf_quantum", "steamadvantage:steam_drill"):
+ self.assertNotIn(item, EXCLUSIONS)
+ self.assertEqual([16384, 78, 16384], fixture()["machineBounds"]["min"])
+
+ def test_tag_oracle_rejects_the_original_empty_tags_and_wrong_board(self):
+ tags = {key: [] for key in ("wires", "dusts/carbon", "rubber", "plastic", "circuit_boards", "solder")}
+ with self.assertRaises(AssertionError):
+ validate_tags({"tags": tags}, "NORMAL", True)
+ tags.update({"wires": ["electricadvantage:electric_conduit"],
+ "dusts/carbon": ["basemetals:coal_powder", "basemetals:charcoal_powder"],
+ "rubber": ["poweradvantage:bioplastic_ingot"],
+ "plastic": ["poweradvantage:bioplastic_ingot", "electricadvantage:petrolplastic_ingot"],
+ "circuit_boards": ["electricadvantage:control_circuit"],
+ "solder": ["electricadvantage:solder"] + [f"basemetals:{m}_ingot" for m in ("lead", "tin", "silver")]})
+ validate_tags({"tags": tags}, "NORMAL", True)
+ tags["circuit_boards"].append("electricadvantage:blank_circuit_board")
+ with self.assertRaises(AssertionError):
+ validate_tags({"tags": tags}, "NORMAL", True)
diff --git a/test-rig/migration/test_recipe_unlocks.py b/test-rig/migration/test_recipe_unlocks.py
new file mode 100644
index 00000000..f10bc2ea
--- /dev/null
+++ b/test-rig/migration/test_recipe_unlocks.py
@@ -0,0 +1,32 @@
+import unittest
+from collections import Counter
+
+from recipe_unlocks import MATERIALS, fixture
+
+
+class RecipeUnlockRunnerTest(unittest.TestCase):
+ def test_fixture_covers_crafting_and_smelting_but_not_crushing(self):
+ recipes = fixture()["recipes"]
+ self.assertEqual(Counter(recipe["id"].split(":")[0] for recipe in recipes),
+ {"poweradvantage": 35, "steamadvantage": 24, "electricadvantage": 50})
+ self.assertEqual(109, len({recipe["id"] for recipe in recipes}))
+ self.assertTrue(all(":crushing_" not in recipe["id"] for recipe in recipes))
+
+ def test_every_case_exercises_inventory_predicates(self):
+ for recipe in fixture()["recipes"]:
+ self.assertTrue(recipe["ingredients"], recipe["id"])
+ self.assertTrue(all(set(predicate) in ({"item"}, {"tag"})
+ for predicate in recipe["ingredients"].values()), recipe["id"])
+
+ def test_standalone_expansions_do_not_require_the_other_expansion(self):
+ for extensions, count in (("none", 35), ("steam", 59), ("electric", 85)):
+ self.assertEqual(count, len(fixture(extensions)["recipes"]))
+
+ def test_human_kit_is_small_unique_and_uses_registered_id_shapes(self):
+ self.assertEqual(27, len(MATERIALS))
+ self.assertEqual(len(MATERIALS), len(set(MATERIALS)))
+ self.assertTrue(all(":" in item for item in MATERIALS))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_respawn.py b/test-rig/migration/test_respawn.py
new file mode 100644
index 00000000..6e74748b
--- /dev/null
+++ b/test-rig/migration/test_respawn.py
@@ -0,0 +1,113 @@
+from collections import Counter
+import copy
+import unittest
+from unittest.mock import patch
+
+from respawn import stacks, respawn_verdict
+
+
+class RespawnItemOracleTests(unittest.TestCase):
+ def test_slots_do_not_change_drop_identity(self):
+ item = {"id": "electricadvantage:lithium_battery", "Count": 3, "Slot": 2, "tag": {"energy": 123.0}}
+ drop = {key: value for key, value in item.items() if key != "Slot"}
+ self.assertEqual(stacks([item]), stacks([drop]))
+
+ def test_combined_stacks_conserve_quantity(self):
+ self.assertEqual(stacks([{"id": "minecraft:dirt", "Count": 4}]),
+ stacks([{"id": "minecraft:dirt", "Count": 1}, {"id": "minecraft:dirt", "Count": 3}]))
+
+ def test_battery_discharge_is_not_a_valid_drop(self):
+ self.assertNotEqual(stacks([{"id": "electricadvantage:lithium_battery", "Count": 1, "tag": {"energy": 123}}]),
+ stacks([{"id": "electricadvantage:lithium_battery", "Count": 1, "tag": {"energy": 0}}]))
+
+ def test_quantity_loss_and_duplication_fail(self):
+ before = stacks([{"id": "minecraft:dirt", "Count": 3}])
+ self.assertNotEqual(before, stacks([{"id": "minecraft:dirt", "Count": 2}]))
+ self.assertNotEqual(before, stacks([{"id": "minecraft:dirt", "Count": 4}]))
+ self.assertNotEqual(before, Counter())
+
+
+class RespawnOracleTests(unittest.TestCase):
+ def setUp(self):
+ before = {"Inventory": [{"id": "electricadvantage:lithium_battery", "Count": 3, "Slot": 0, "tag": {"energy": 123.0}}],
+ "EnderItems": [{"id": "steamadvantage:musket", "Count": 1, "Slot": 0}], "Dimension": -1,
+ "Pos": [144.5, 100, 8.5], "Health": 17.0, "UUIDMost": 1, "UUIDLeast": 2,
+ "SpawnX": 160, "SpawnY": 100, "SpawnZ": 24, "SpawnForced": 1,
+ "XpLevel": 23, "XpTotal": 751}
+ dead = copy.deepcopy(before)
+ dead.update(Health=0.0)
+ after = copy.deepcopy(before)
+ after.update(Dimension=0, Health=20.0, Pos=[160.5, 100, 24.5])
+ self.record = {"dimension": -1, "respawn": {"before": before, "afterDeath": dead, "after": after,
+ "keepInventory": True, "bedMode": "forced", "fromDimension": -1, "toDimension": 0,
+ "handler": "NetHandlerPlayServer.PERFORM_RESPAWN",
+ "drops": [], "scope": "server-only-dummy-network"}}
+
+ def run_oracle(self):
+ with patch("respawn.parse_nbt", side_effect=lambda value: value):
+ return respawn_verdict(self.record, {}, {"position": [0, 64, 0], "radius": 10})
+
+ def test_kept_inventory(self):
+ self.assertEqual("PASS", self.run_oracle()["status"])
+
+ def test_inventory_loss(self):
+ self.record["respawn"]["after"]["Inventory"] = []
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_extra_battery_payload_fails_exact_item_check(self):
+ self.record["respawn"]["after"]["Inventory"][0]["tag"]["extra"] = 999
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_missing_bed_must_use_native_world_spawn_area(self):
+ data = self.record["respawn"]
+ data["bedMode"] = "missing"
+ data["after"]["Pos"] = [0.5, 64, 0.5]
+ self.assertEqual("PASS", self.run_oracle()["status"])
+ data["after"]["Pos"] = [999, 64, 999]
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_uuid_replacement(self):
+ self.record["respawn"]["after"]["UUIDMost"] = 3
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_ender_items_lost(self):
+ self.record["respawn"]["after"]["EnderItems"] = []
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_false_death_evidence(self):
+ self.record["respawn"]["afterDeath"]["Health"] = 17.0
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_wrong_dimension(self):
+ self.record["respawn"]["after"]["Dimension"] = -1
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_saved_spawn_is_not_only_a_position_check(self):
+ self.record["respawn"]["after"].pop("SpawnX")
+ result = self.run_oracle()
+ self.assertEqual("FAIL", result["status"])
+ self.assertTrue(any(issue["path"] == "saved-spawn.SpawnX" for issue in result["differences"]))
+
+ def test_bed_failure_is_not_accepted_as_normal_respawn(self):
+ self.record["respawn"]["after"]["Pos"] = [0, 64, 0]
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_direct_recreation_cannot_claim_native_handler_coverage(self):
+ self.record["respawn"].pop("handler")
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+ def test_drop_items_conserved(self):
+ data = self.record["respawn"]
+ data["keepInventory"] = False
+ data["afterDeath"]["Inventory"] = []
+ data["after"].update(Inventory=[], XpLevel=0, XpTotal=0)
+ item = copy.deepcopy(data["before"]["Inventory"][0])
+ item.pop("Slot")
+ data["drops"] = [{"Item": item}]
+ self.assertEqual("PASS", self.run_oracle()["status"])
+ data["drops"][0]["Item"]["Count"] = 4
+ self.assertEqual("FAIL", self.run_oracle()["status"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/test_transformer_order.py b/test-rig/migration/test_transformer_order.py
new file mode 100644
index 00000000..d5edab27
--- /dev/null
+++ b/test-rig/migration/test_transformer_order.py
@@ -0,0 +1,181 @@
+"""Negative controls for packaged order proof and mixed-fixture construction."""
+
+import copy
+import io
+import json
+from pathlib import Path
+import struct
+import tempfile
+import unittest
+import zipfile
+import zlib
+
+import nbtlib
+
+from matrix import WORLD, digest, write_json
+from transformer_order import (ENGINE, METAL_BLOCKS, SUFFIXES, add_metal_blocks, metal_item,
+ backup_provenance, order_evidence, read_region, strict_equal, validate_patch)
+
+
+def hook(mod, name):
+ return {"method": "target()V", "index": 1, "owner": "zone/moddev/mc/" +
+ ("basemetals" if mod == "bm" else "poweradvantage") + "/migration/LegacyWorldDataHook",
+ "name": name, "descriptor": "()V"}
+
+
+class OrderEvidenceTests(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temp.cleanup)
+ self.root = Path(self.temp.name)
+ self.events = [{"event": "registration", "mode": "bm-first", "natural": [],
+ "forced": [mod + "_legacy_" + suffix for suffix in SUFFIXES
+ for mod in ("basemetals", "poweradvantage")]}]
+ for suffix, (class_name, methods) in SUFFIXES.items():
+ first = [hook("bm", name) for name in methods]
+ final = []
+ for name in methods:
+ pair = ("bm", "pa") if name == "finalizeLegacyChunk" else ("pa", "bm")
+ final.extend(hook(mod, name) for mod in pair)
+ for mod, before, after in (("basemetals", [], first), ("poweradvantage", first, final)):
+ dump = self.root / (suffix + "-" + mod + ".class")
+ dump.write_bytes(b"test-bytecode")
+ self.events.append({"event": "application", "class": class_name,
+ "transformer": mod + "_legacy_" + suffix, "before": before,
+ "after": after, "dump": dump.name, "sha256": digest(dump)})
+
+ def prove(self):
+ (self.root / "transformer-order-events.jsonl").write_text(
+ "\n".join(json.dumps(e) for e in self.events))
+ return order_evidence(self.root, "bm-first")
+
+ def test_observed_application_and_reversed_prefix_order_pass(self):
+ self.assertEqual("PASS", self.prove()["status"])
+
+ def test_source_registration_alone_is_not_proof(self):
+ self.events = self.events[:1]
+ with self.assertRaisesRegex(AssertionError, "application order"):
+ self.prove()
+
+ def test_wrong_application_order_fails(self):
+ self.events[1], self.events[2] = self.events[2], self.events[1]
+ with self.assertRaisesRegex(AssertionError, "application order"):
+ self.prove()
+
+ def test_missing_injection_fails_even_when_callback_returned(self):
+ self.events[1]["after"] = []
+ with self.assertRaisesRegex(AssertionError, "injection"):
+ self.prove()
+
+ def test_wrong_emitted_execution_order_fails(self):
+ self.events[2]["after"] = list(reversed(self.events[2]["after"]))
+ with self.assertRaisesRegex(AssertionError, "emitted hook order"):
+ self.prove()
+
+ def test_duplicate_callback_fails(self):
+ self.events.append(copy.deepcopy(self.events[1]))
+ with self.assertRaisesRegex(AssertionError, "application order"):
+ self.prove()
+
+ def test_changed_bytecode_dump_fails(self):
+ (self.root / self.events[1]["dump"]).write_bytes(b"changed")
+ with self.assertRaisesRegex(AssertionError, "hash changed"):
+ self.prove()
+
+ def test_disconnected_class_nodes_fail(self):
+ self.events[2]["before"] = []
+ with self.assertRaisesRegex(AssertionError, "same evolving class"):
+ self.prove()
+
+ def test_wrong_forced_request_fails(self):
+ self.events[0]["forced"] = list(reversed(self.events[0]["forced"]))
+ with self.assertRaisesRegex(AssertionError, "application order"):
+ self.prove()
+
+ def test_duplicate_prefix_injection_fails(self):
+ self.events[1]["after"].append(copy.deepcopy(self.events[1]["after"][0]))
+ with self.assertRaisesRegex(AssertionError, "injection"):
+ self.prove()
+
+
+class FixtureTests(unittest.TestCase):
+ def test_exact_comparison_detects_extra_fields(self):
+ self.assertEqual("FAIL", strict_equal({"count": 1}, {"count": 1, "extra": 2})["status"])
+
+ def test_damaged_item_keeps_typed_legacy_data(self):
+ item = metal_item(26)
+ self.assertIsInstance(item["Damage"], nbtlib.Short)
+ self.assertEqual(17, int(item["Damage"]))
+ self.assertEqual(26, int(item["Slot"]))
+ self.assertEqual("keep-exactly", str(item["tag"]["OrderingWitness"]))
+
+ def test_region_edits_only_requested_chunk_and_recovers_high_numeric_ids(self):
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ (root / "region").mkdir()
+ nbtlib.File({"FML": nbtlib.Compound({"Registries": nbtlib.Compound({
+ "minecraft:blocks": nbtlib.Compound({"ids": nbtlib.List[nbtlib.Compound]([
+ nbtlib.Compound({"K": nbtlib.String(name), "V": nbtlib.Int(304 + i)})
+ for i, name in enumerate(METAL_BLOCKS)])})})})}).save(root / "level.dat", gzipped=True)
+ chunk = nbtlib.File({"Level": nbtlib.Compound({"Sections": nbtlib.List[nbtlib.Compound]([
+ nbtlib.Compound({"Y": nbtlib.Byte(5), "Blocks": nbtlib.ByteArray([0] * 4096),
+ "Data": nbtlib.ByteArray([0] * 2048)})])})})
+ stream = io.BytesIO()
+ chunk.write(stream)
+ body = b"\x02" + zlib.compress(stream.getvalue())
+ payload = struct.pack(">I", len(body)) + body
+ header = bytearray(4096)
+ header[11 * 4:11 * 4 + 4] = (2 * 256 + 1).to_bytes(4, "big")
+ header[12 * 4:12 * 4 + 4] = (3 * 256 + 1).to_bytes(4, "big")
+ path = root / "region/r.0.0.mca"
+ padded = payload + b"\x00" * (4096 - len(payload))
+ path.write_bytes(header + b"\x00" * 4096 + padded + padded)
+ _, original = read_region(path)
+ definition = []
+ add_metal_blocks(root, definition)
+ _, updated = read_region(path)
+ self.assertEqual(original[12], updated[12])
+ self.assertNotEqual(original[11], updated[11])
+ fixed = nbtlib.File.parse(io.BytesIO(zlib.decompress(updated[11][5:])))
+ section = fixed["Level"]["Sections"][0]
+ for i in range(3):
+ index = (((81 + i) & 15) << 8) | (8 << 4) | 4
+ numeric = (int(section["Blocks"][index]) & 255) | (
+ ((int(section["Add"][index // 2]) & 255) >> ((index & 1) * 4) & 15) << 8)
+ self.assertEqual(304 + i, numeric)
+ with self.assertRaisesRegex(AssertionError, "occupied"):
+ add_metal_blocks(root, [])
+
+ def test_patch_audit_rejects_any_other_changed_library_entry(self):
+ with tempfile.TemporaryDirectory() as temp:
+ before, after = Path(temp) / "before.jar", Path(temp) / "after.jar"
+ with zipfile.ZipFile(before, "w") as archive:
+ archive.writestr(ENGINE, b"old")
+ archive.writestr("unrelated", b"original")
+ with zipfile.ZipFile(after, "w") as archive:
+ archive.writestr(ENGINE, b"new")
+ archive.writestr("unrelated", b"changed")
+ archive.writestr("qualification/order/TransformerOrderBridge.class", b"class")
+ archive.writestr("qualification/order/TransformerOrderBridge$Observed.class", b"class")
+ with self.assertRaisesRegex(AssertionError, "Unexpected coremods patch"):
+ validate_patch(before, after)
+
+ def test_original_byte_custody_passes_but_reports_intermediate_power_backup(self):
+ with tempfile.TemporaryDirectory() as temp:
+ fixture, runtime = Path(temp) / "fixture", Path(temp) / "runtime"
+ (fixture / WORLD / "playerdata").mkdir(parents=True)
+ (runtime / WORLD / "playerdata").mkdir(parents=True)
+ write_json(runtime / "actor-fixture.json", {"playerUuid": "test"})
+ for relative in (Path("level.dat"), Path("playerdata/test.dat")):
+ (fixture / WORLD / relative).write_bytes(b"original")
+ (runtime / WORLD / relative.with_name(relative.name + ".basemetals-legacy-backup")).write_bytes(b"original")
+ (runtime / WORLD / relative.with_name(relative.name + ".poweradvantage-legacy-backup")).write_bytes(b"intermediate")
+ proof = backup_provenance(fixture, runtime)
+ self.assertEqual("PASS", proof["status"])
+ self.assertFalse(proof["power_backup_matches_original"])
+ (runtime / WORLD / "level.dat.basemetals-legacy-backup").write_bytes(b"lost-original")
+ self.assertEqual("FAIL", backup_provenance(fixture, runtime)["status"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test-rig/migration/trace_connected_progress.py b/test-rig/migration/trace_connected_progress.py
new file mode 100644
index 00000000..0e198042
--- /dev/null
+++ b/test-rig/migration/trace_connected_progress.py
@@ -0,0 +1,60 @@
+"""Observe the first native drill tick in copies, with no progress restoration."""
+import argparse
+import json
+from pathlib import Path
+import socket
+
+from connected import (ConnectedServer, data, entries, operation, pins, prepare, read, state)
+from matrix import WORLD, compare, fingerprint, intermediate_profile, write_json
+
+
+def drill(document):
+ return data(entries(document)["C-EDRILL-drill"])
+
+
+def run(args):
+ with socket.socket() as listener:
+ listener.bind(("127.0.0.1", args.port))
+ before_hashes = fingerprint(args.fixture / WORLD)
+ args.output.mkdir(parents=True, exist_ok=False)
+ available = pins(args.profile_manifest, args.output)
+ definition = json.loads((args.fixture / "connected-fixture.json").read_text())
+ result = dict(controls={}, fixture=str(args.fixture.resolve()), source_hashes=before_hashes)
+ for name, profile in (("legacy112", intermediate_profile(available)), ("target113", available["target113"])):
+ runtime = args.output / name
+ launcher = prepare(runtime, profile, definition, args.port, args.fixture)
+ server = ConnectedServer(runtime, args.java8, launcher, "trace")
+ try:
+ server.observe("snapshot", "loaded")
+ server.observe("step", "first-tick", 1)
+ server.observe("step", "continued", 700)
+ loaded, first, continued = (read(runtime, label) for label in ("loaded", "first-tick", "continued"))
+ result["controls"][name] = dict(
+ preservation=compare(args.fixture / "migration-intermediate-restart-worked.json", runtime / "migration-loaded.json"),
+ loaded=drill(loaded), first_tick=drill(first), continued=drill(continued),
+ continued_operation=operation(first, continued, 700),
+ before=state(loaded), after=state(continued))
+ server.send("save-all flush")
+ finally:
+ server.close()
+ if fingerprint(args.fixture / WORLD) != before_hashes:
+ raise AssertionError("Trace modified source world")
+ controls = result["controls"]
+ legacy, target = controls["legacy112"], controls["target113"]
+ result["finding"] = "LEGACY_RESTART_PROGRESS_LOSS" if legacy["first_tick"]["progress"] < legacy["loaded"]["progress"] else "NOT_REPRODUCED"
+ result["target_preserves_first_tick"] = target["first_tick"]["progress"] > target["loaded"]["progress"]
+ result["control_protocol"] = "Frozen loaded snapshot, one native tick, 700 native ticks; no replacement, refill, energy or progress writes"
+ write_json(args.output / "progress-trace.json", result)
+ print(result["finding"], "target preserves:", result["target_preserves_first_tick"])
+ return int(result["finding"] != "LEGACY_RESTART_PROGRESS_LOSS" or not result["target_preserves_first_tick"]
+ or any(c["preservation"]["status"] != "PASS" or c["continued_operation"]["status"] != "PASS" for c in controls.values()))
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--java8", type=Path, required=True)
+ parser.add_argument("--profile-manifest", type=Path, required=True)
+ parser.add_argument("--fixture", type=Path, required=True)
+ parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--port", type=int, default=25658)
+ raise SystemExit(run(parser.parse_args()))
diff --git a/test-rig/migration/transformer-order-probe/TransformerOrderBridge.java b/test-rig/migration/transformer-order-probe/TransformerOrderBridge.java
new file mode 100644
index 00000000..c44147e6
--- /dev/null
+++ b/test-rig/migration/transformer-order-probe/TransformerOrderBridge.java
@@ -0,0 +1,232 @@
+package qualification.order;
+
+import cpw.mods.modlauncher.api.ITransformer;
+import cpw.mods.modlauncher.api.ITransformerVotingContext;
+import cpw.mods.modlauncher.api.TransformerVoteResult;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+/** Test-only registration forcing; production jars and transformer functions are unchanged. */
+public final class TransformerOrderBridge {
+ private static final String OWNER = "qualification/order/TransformerOrderBridge";
+ private static final String ENGINE = "net/minecraftforge/coremod/CoreModEngine.class";
+ private static final String[] SUFFIXES = {"world_info", "chunk_status", "worldgen_guard"};
+ private static final Gson JSON = new Gson();
+ private static int sequence;
+
+ private TransformerOrderBridge() {}
+
+ private static String name(ITransformer> transformer) {
+ String[] labels = transformer.labels();
+ return labels[labels.length - 1];
+ }
+
+ public static List> configure(List> input) throws IOException {
+ if (!Files.isRegularFile(Paths.get("advantage-migration.allow"))) {
+ throw new IllegalStateException("Unmarked order qualification runtime");
+ }
+ String mode = new String(Files.readAllBytes(Paths.get("transformer-order.mode")),
+ StandardCharsets.UTF_8).trim();
+ if (!Arrays.asList("natural", "bm-first", "pa-first").contains(mode)) {
+ throw new IllegalArgumentException("Unknown order: " + mode);
+ }
+ List> output = new ArrayList<>(input);
+ List natural = names(input);
+ for (String suffix : SUFFIXES) {
+ int bm = find(output, "basemetals_legacy_" + suffix);
+ int pa = find(output, "poweradvantage_legacy_" + suffix);
+ if (!mode.equals("natural") && ((bm < pa) != mode.equals("bm-first"))) {
+ ITransformer> saved = output.get(bm);
+ output.set(bm, output.get(pa));
+ output.set(pa, saved);
+ }
+ }
+ Map registration = new LinkedHashMap<>();
+ registration.put("event", "registration");
+ registration.put("mode", mode);
+ registration.put("natural", natural);
+ registration.put("forced", names(output));
+ record(registration);
+ for (int i = 0; i < output.size(); i++) {
+ ITransformer> transformer = output.get(i);
+ if (selected(name(transformer))) {
+ @SuppressWarnings("unchecked")
+ ITransformer delegate = (ITransformer) transformer;
+ output.set(i, new Observed(delegate));
+ }
+ }
+ return output;
+ }
+
+ private static int find(List> values, String expected) {
+ int found = -1;
+ for (int i = 0; i < values.size(); i++) {
+ if (!name(values.get(i)).equals(expected)) continue;
+ if (found != -1) throw new IllegalStateException("Duplicate transformer: " + expected);
+ found = i;
+ }
+ if (found == -1) throw new IllegalStateException("Missing transformer: " + expected);
+ return found;
+ }
+
+ private static List names(List> values) {
+ List result = new ArrayList<>();
+ for (ITransformer> value : values) result.add(name(value));
+ return result;
+ }
+
+ private static boolean selected(String name) {
+ for (String suffix : SUFFIXES) {
+ if (name.equals("basemetals_legacy_" + suffix)
+ || name.equals("poweradvantage_legacy_" + suffix)) return true;
+ }
+ return false;
+ }
+
+ private static synchronized void record(Map event) throws IOException {
+ event.put("sequence", ++sequence);
+ Files.write(Paths.get("transformer-order-events.jsonl"),
+ (JSON.toJson(event) + "\n").getBytes(StandardCharsets.UTF_8),
+ StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+ }
+
+ private static List