diff --git a/src/generated/resources/assets/anvilcraft/lang/en_ud.json b/src/generated/resources/assets/anvilcraft/lang/en_ud.json index 2eea8dc3f9..49ea55fe4e 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_ud.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_ud.json @@ -335,6 +335,7 @@ "block.anvilcraft.burning_heater": "ɹǝʇɐǝH ᵷuᴉuɹnᗺ", "block.anvilcraft.cake_base_block": "ʞɔoꞁᗺ ǝsɐᗺ ǝʞɐƆ", "block.anvilcraft.cake_block": "ʞɔoꞁᗺ ǝʞɐƆ", + "block.anvilcraft.celestial_back_gate": "ǝʇɐ⅁ ʞɔɐᗺ ꞁɐᴉʇsǝꞁǝƆ", "block.anvilcraft.celestial_forging_anvil": "ꞁᴉʌuⱯ ᵷuᴉᵷɹoℲ ꞁɐᴉʇsǝꞁǝƆ", "block.anvilcraft.celestial_forging_anvil_amplifier": "ɹǝᴉɟᴉꞁdɯⱯ ꞁᴉʌuⱯ ᵷuᴉᵷɹoℲ ꞁɐᴉʇsǝꞁǝƆ", "block.anvilcraft.celestial_forging_anvil_amplifier.need_anvil_corner": "ꞁᴉʌuⱯ ᵷuᴉᵷɹoℲ ꞁɐᴉʇsǝꞁǝƆ ǝɥʇ ɟo ɹǝuɹoɔ ǝɥʇ uo pǝɔɐꞁd ǝq oʇ spǝǝN", @@ -1314,6 +1315,9 @@ "screen.anvilcraft.cfa.mag.weak": "ʞɐǝM", "screen.anvilcraft.cfa.mass": "%s :ssɐW", "screen.anvilcraft.cfa.material_required": "%s × %s :sǝɹᴉnbǝᴚ", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf": "ǝɹǝɥdS uosʎᗡ ɟɹɐʍᗡ uʍoɹᗺ", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.description": "ɟɹɐʍp pǝɹ ɐ oʇuᴉ ʇᴉ uɹnʇ ʎꞁꞁɐnʇuǝʌǝ puɐ ʇndʇno ǝɥʇ ʎɟᴉꞁdɯɐ uɐɔ ɹǝʇʇɐɯ ꞁɐᴉpɹoɯᴉɹd ⸵ɹǝʍod ǝʇɐɹǝuǝᵷ oʇ ʎᵷɹǝuǝ s,ɟɹɐʍp uʍoɹq ɐ ᵷuᴉʇɔǝꞁꞁoƆ", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.usage": "ɹ§ɟɹɐʍp pǝɹ ɐ oʇuᴉ ɟɹɐʍp uʍoɹq ǝɥʇ suɹnʇ ᗺ008ᘔ⥝ ⸵sǝʇɐꞁnɯnɔɔɐ ᗺᘔ ǝʌoqɐ ɹǝʇʇɐWɟ§\n:sʇɔǝɟɟƎ ǝpᴉS\n———————\nɹ§ϛ×/Ɛ×/ᘔ×/ϛ˙⥝× sǝpᴉʌoɹd ʎꞁddns ǝꞁqɐʇs ⸵MW 009⥝/(²ᴚ×Ǝ)=Ԁɟ§\nɹ§[pᴉɹ⅁ ɹǝʍoԀ]ɐ§\n:sʇndʇnO\n———————\nɹ§ʇᵷ0߈ ɹoɟ ǝꞁqɐʇs 'ʞɔᴉʇ ɹǝd ᗺᘔ–ᗺɯ0ϛᘔ :ɹǝʇʇɐW ꞁɐᴉpɹoɯᴉɹԀɟ§\nɹ§[ʇuI nꞁℲ]q§\n:sǝɹᴉnbǝᴚ", "screen.anvilcraft.cfa.megastructure.dyson_sphere_large": "ǝɹǝɥdS uosʎᗡ ǝᵷɹɐꞀ", "screen.anvilcraft.cfa.megastructure.dyson_sphere_large.description": "ʎʇᴉɔᴉɹʇɔǝꞁǝ ɟo ʇunoɯɐ ǝᵷɹɐꞁ ɐ ǝʇɐɹǝuǝᵷ 'ɹɐʇs ɯoɹɟ ʎᵷɹǝuǝ ʇɥᵷᴉꞁ ᵷuᴉʇɔǝꞁꞁoƆ", "screen.anvilcraft.cfa.megastructure.dyson_sphere_large.usage": "ɹ§ǝuoNㄥ§\n:sʇɔǝɟɟƎ ǝpᴉS\n———————\nɹ§snᴉpɐɹ puɐ ǝɹnʇɐɹǝdɯǝʇ ɹɐꞁꞁǝʇs uo pǝsɐq ɹǝʍod sǝʇɐɹǝuǝ⅁ɟ§\nɹ§[pᴉɹ⅁ ɹǝʍoԀ]ɐ§\n:sʇndʇnO\n———————\nɹ§ǝuoNㄥ§\n:sǝɹᴉnbǝᴚ", @@ -1608,6 +1612,7 @@ "subtitles.anvilcraft.giant_anvil_shock": "ǝʌɐʍʞɔoɥs ꞁᴉʌuⱯ ʇuɐᴉ⅁", "subtitles.anvilcraft.plasma_jet": "ᵷuᴉɹɐoɹ ʇǝՐ ɐɯsɐꞁԀ", "subtitles.anvilcraft.plasma_jet_lava": "ʇsɹnq ʇǝՐ ɐɯsɐꞁԀ", + "subtitles.anvilcraft.quenched_out": "ʇno pǝɥɔuǝnꝹ", "subtitles.anvilcraft.smart_block_placer_extend": "pǝpuǝʇxǝ ɹǝɔɐꞁԀ ʞɔoꞁᗺ ʇɹɐɯS", "subtitles.anvilcraft.smart_block_placer_retract": "pǝʇɔɐɹʇǝɹ ɹǝɔɐꞁԀ ʞɔoꞁᗺ ʇɹɐɯS", "subtitles.anvilcraft.smart_block_placer_shulker_open": "sɹᴉɥʍ ɹǝɔɐꞁԀ ʞɔoꞁᗺ ʇɹɐɯS", @@ -1776,7 +1781,7 @@ "tooltip.anvilcraft.item.creative_crate.shift": "sɯǝʇᴉ ʇno ǝʞɐʇ oʇ ʞɔᴉꞁɔ-ʇɟǝꞁ sɹǝʎɐꞁd ꞁɐʌᴉʌɹnS\nuoᴉʇɐɹnᵷᴉɟuoɔ ǝɥʇ ɹɐǝꞁɔ oʇ ʞɔᴉꞁɔ-ʇɟǝꞁ sɹǝʎɐꞁd ǝʌᴉʇɐǝɹƆ\npǝɹnᵷᴉɟuoɔ sᴉ ɯǝʇᴉ ou uǝɥʍ sɯǝʇᴉ ʇnduᴉ ꞁꞁɐ sʎoɹʇsǝᗡ\nʇno uǝʞɐʇ uǝɥʍ pǝɯnsuoɔ ǝq ʇou ꞁꞁᴉʍ sɯǝʇI\nǝɹnᵷᴉɟuoɔ oʇ ǝpᴉsuᴉ sɯǝʇᴉ ǝɔɐꞁd :ǝdʎʇ ʇǝs ɐ ɟo sɯǝʇᴉ ǝʇᴉuᴉɟuᴉ sǝpᴉʌoɹԀ", "tooltip.anvilcraft.item.creative_fluid_tank": "ʎꞁddns puɐ ǝᵷɐɹoʇs pᴉnꞁɟ ǝʇᴉuᴉɟuI", "tooltip.anvilcraft.item.creative_fluid_tank.shift": "pǝɹnᵷᴉɟuoɔ sᴉ pᴉnꞁɟ ou uǝɥʍ pᴉnꞁɟ ʇnduᴉ ꞁꞁɐ sʎoɹʇsǝᗡ\npǝʇɔɐɹʇxǝ uǝɥʍ pǝɯnsuoɔ ǝq ʇou ꞁꞁᴉʍ pᴉnꞁℲ\nǝɹnᵷᴉɟuoɔ oʇ ǝpᴉsuᴉ pᴉnꞁɟ ꞁꞁᴉɟ :ǝdʎʇ ʇǝs ɐ ɟo pᴉnꞁɟ ǝʇᴉuᴉɟuᴉ sǝpᴉʌoɹԀ", - "tooltip.anvilcraft.item.creative_generator": "pɐoꞁ ɐ sɐ pǝsn ǝq osꞁɐ uɐɔ 'ɹǝʍod ɟo Mʞ ᘔ6⥝8 oʇ dn ǝpᴉʌoɹԀ", + "tooltip.anvilcraft.item.creative_generator": "pɐoꞁ ɐ sɐ pǝsn ǝq osꞁɐ uɐɔ 'ɹǝʍod ɟo Mʞ 9Ɛϛϛ9 oʇ dn ǝpᴉʌoɹԀ", "tooltip.anvilcraft.item.creative_laser": "ɯɐǝq ɹǝsɐꞁ ɐ sʇᴉɯƎ", "tooltip.anvilcraft.item.creative_laser.shift": "ǝuoʇspǝɹ ʎq ɟɟo pǝuɹnʇ ǝq uɐɔ 'ǝpoɯ ɐɯɯɐᵷ puɐ suǝꞁ 'ꞁǝʌǝꞁ ɹǝsɐꞁ ǝꞁqɐʇsnɾpⱯ", "tooltip.anvilcraft.item.crushing_table": "sǝdᴉɔǝɹ ᵷuᴉɥsnɹɔ ɯɹoɟɹǝԀ", @@ -2168,6 +2173,7 @@ "tooltip.anvilcraft.storage.types": ":sǝdʎ⟘", "tooltip.anvilcraft.storage.types.value": "%s / %s ", "tooltip.anvilcraft.thought": "oɟuᴉ ǝɹoɯ ɹoɟ [%s] ssǝɹԀ", + "tooltip.anvilcraft.waiting": "ᵷuᴉɔuʎs ɹoɟ ᵷuᴉʇᴉɐM", "tooltip.anvilcraft.wip_block.jade.recipe": "%s :ǝdᴉɔǝᴚ ꞁɐɹnpǝɔoɹԀ", "tooltip.anvilcraft.wip_block.jade.step_count": "%d :pǝʇnɔǝxƎ sdǝʇS", "tooltip.anvilcraft.working_progress.progress": "%%%2$s %1$s ", diff --git a/src/generated/resources/assets/anvilcraft/lang/en_us.json b/src/generated/resources/assets/anvilcraft/lang/en_us.json index ab75ad273e..41e679a698 100644 --- a/src/generated/resources/assets/anvilcraft/lang/en_us.json +++ b/src/generated/resources/assets/anvilcraft/lang/en_us.json @@ -335,6 +335,7 @@ "block.anvilcraft.burning_heater": "Burning Heater", "block.anvilcraft.cake_base_block": "Cake Base Block", "block.anvilcraft.cake_block": "Cake Block", + "block.anvilcraft.celestial_back_gate": "Celestial Back Gate", "block.anvilcraft.celestial_forging_anvil": "Celestial Forging Anvil", "block.anvilcraft.celestial_forging_anvil_amplifier": "Celestial Forging Anvil Amplifier", "block.anvilcraft.celestial_forging_anvil_amplifier.need_anvil_corner": "Needs to be placed on the corner of the Celestial Forging Anvil", @@ -1314,6 +1315,9 @@ "screen.anvilcraft.cfa.mag.weak": "Weak", "screen.anvilcraft.cfa.mass": "Mass: %s", "screen.anvilcraft.cfa.material_required": "Requires: %s × %s", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf": "Brown Dwarf Dyson Sphere", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.description": "Collecting a brown dwarf's energy to generate power; primordial matter can amplify the output and eventually turn it into a red dwarf", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.usage": "Requires:\n§b[Flu Int]§r\n§fPrimordial Matter: 250mB–2B per tick, stable for 40gt§r\n———————\nOutputs:\n§a[Power Grid]§r\n§fP=(E×R²)/1600 MW; stable supply provides ×1.5/×2/×3/×5§r\n———————\nSide Effects:\n§fMatter above 2B accumulates; 12800B turns the brown dwarf into a red dwarf§r", "screen.anvilcraft.cfa.megastructure.dyson_sphere_large": "Large Dyson Sphere", "screen.anvilcraft.cfa.megastructure.dyson_sphere_large.description": "Collecting light energy from star, generate a large amount of electricity", "screen.anvilcraft.cfa.megastructure.dyson_sphere_large.usage": "Requires:\n§7None§r\n———————\nOutputs:\n§a[Power Grid]§r\n§fGenerates power based on stellar temperature and radius§r\n———————\nSide Effects:\n§7None§r", @@ -1608,6 +1612,7 @@ "subtitles.anvilcraft.giant_anvil_shock": "Giant Anvil shockwave", "subtitles.anvilcraft.plasma_jet": "Plasma Jet roaring", "subtitles.anvilcraft.plasma_jet_lava": "Plasma Jet burst", + "subtitles.anvilcraft.quenched_out": "Quenched out", "subtitles.anvilcraft.smart_block_placer_extend": "Smart Block Placer extended", "subtitles.anvilcraft.smart_block_placer_retract": "Smart Block Placer retracted", "subtitles.anvilcraft.smart_block_placer_shulker_open": "Smart Block Placer whirs", @@ -1776,7 +1781,7 @@ "tooltip.anvilcraft.item.creative_crate.shift": "Provides infinite items of a set type: place items inside to configure\nItems will not be consumed when taken out\nDestroys all input items when no item is configured\nCreative players left-click to clear the configuration\nSurvival players left-click to take out items", "tooltip.anvilcraft.item.creative_fluid_tank": "Infinite fluid storage and supply", "tooltip.anvilcraft.item.creative_fluid_tank.shift": "Provides infinite fluid of a set type: fill fluid inside to configure\nFluid will not be consumed when extracted\nDestroys all input fluid when no fluid is configured", - "tooltip.anvilcraft.item.creative_generator": "Provide up to 8192 kW of power, can also be used as a load", + "tooltip.anvilcraft.item.creative_generator": "Provide up to 65536 kW of power, can also be used as a load", "tooltip.anvilcraft.item.creative_laser": "Emits a laser beam", "tooltip.anvilcraft.item.creative_laser.shift": "Adjustable laser level, lens and gamma mode, can be turned off by redstone", "tooltip.anvilcraft.item.crushing_table": "Perform crushing recipes", @@ -2168,6 +2173,7 @@ "tooltip.anvilcraft.storage.types": "Types:", "tooltip.anvilcraft.storage.types.value": " %s / %s", "tooltip.anvilcraft.thought": "Press [%s] for more info", + "tooltip.anvilcraft.waiting": "Waiting for syncing", "tooltip.anvilcraft.wip_block.jade.recipe": "Procedural Recipe: %s", "tooltip.anvilcraft.wip_block.jade.step_count": "Steps Executed: %d", "tooltip.anvilcraft.working_progress.progress": " %1$s %2$s%%", diff --git a/src/generated/resources/assets/anvilcraft/sounds.json b/src/generated/resources/assets/anvilcraft/sounds.json index 44bad0d952..1d40faed8d 100644 --- a/src/generated/resources/assets/anvilcraft/sounds.json +++ b/src/generated/resources/assets/anvilcraft/sounds.json @@ -81,6 +81,12 @@ ], "subtitle": "subtitles.anvilcraft.plasma_jet_lava" }, + "quenched_out": { + "sounds": [ + "anvilcraft:quenched_out" + ], + "subtitle": "subtitles.anvilcraft.quenched_out" + }, "smart_block_placer_extend": { "sounds": [ { diff --git a/src/generated/resources/data/anvilcraft/loot_table/blocks/celestial_forging_anvil.json b/src/generated/resources/data/anvilcraft/loot_table/blocks/celestial_forging_anvil.json index f203e55612..0de77f0c55 100644 --- a/src/generated/resources/data/anvilcraft/loot_table/blocks/celestial_forging_anvil.json +++ b/src/generated/resources/data/anvilcraft/loot_table/blocks/celestial_forging_anvil.json @@ -3,8 +3,27 @@ "pools": [ { "bonus_rolls": 0.0, - "entries": [], - "rolls": 0.0 + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "conditions": [ + { + "block": "anvilcraft:celestial_forging_anvil", + "condition": "minecraft:block_state_property", + "properties": { + "half": "bottom_center" + } + } + ], + "name": "anvilcraft:celestial_forging_anvil" + } + ], + "rolls": 1.0 } ], "random_sequence": "anvilcraft:blocks/celestial_forging_anvil" diff --git a/src/generated/resources/data/anvilcraft/recipe/planet_resource/offering.json b/src/generated/resources/data/anvilcraft/recipe/planet_resource/offering.json index 770a68fbd2..ab2978d20c 100644 --- a/src/generated/resources/data/anvilcraft/recipe/planet_resource/offering.json +++ b/src/generated/resources/data/anvilcraft/recipe/planet_resource/offering.json @@ -4,7 +4,24 @@ "offering": { "entries": [ { - "id": "anvilcraft:gem_block_random", + "choices": [ + { + "id": "minecraft:emerald_block", + "weight": 1 + }, + { + "id": "anvilcraft:topaz_block", + "weight": 1 + }, + { + "id": "anvilcraft:ruby_block", + "weight": 1 + }, + { + "id": "anvilcraft:sapphire_block", + "weight": 1 + } + ], "weight": 50 }, { @@ -20,7 +37,24 @@ "weight": 2 }, { - "id": "anvilcraft:gem_amulet_random", + "choices": [ + { + "id": "anvilcraft:emerald_amulet", + "weight": 1 + }, + { + "id": "anvilcraft:topaz_amulet", + "weight": 1 + }, + { + "id": "anvilcraft:ruby_amulet", + "weight": 1 + }, + { + "id": "anvilcraft:sapphire_amulet", + "weight": 1 + } + ], "weight": 2 }, { diff --git a/src/generated/resources/data/anvilcraft/recipe/special_celestial_body/overworld_like.json b/src/generated/resources/data/anvilcraft/recipe/special_celestial_body/overworld_like.json index 92abd7a6f0..bc72ea9f69 100644 --- a/src/generated/resources/data/anvilcraft/recipe/special_celestial_body/overworld_like.json +++ b/src/generated/resources/data/anvilcraft/recipe/special_celestial_body/overworld_like.json @@ -3,6 +3,12 @@ "axial_tilt": 0.0, "energy": 16, "has_atmosphere": true, + "landing": { + "coordinate_rule": { + "type": "same" + }, + "dimension": "anvilcraft:overworld_like" + }, "liquid_coverage": "medium", "magnetic_field": 2, "mass": 20, diff --git a/src/generated/resources/data/anvilcraft/tags/block/collision_immune.json b/src/generated/resources/data/anvilcraft/tags/block/collision_immune.json index ef5c94d73d..c9aa55a986 100644 --- a/src/generated/resources/data/anvilcraft/tags/block/collision_immune.json +++ b/src/generated/resources/data/anvilcraft/tags/block/collision_immune.json @@ -6,6 +6,7 @@ "anvilcraft:infinite_collector", "anvilcraft:celestial_forging_anvil", "anvilcraft:celestial_forging_anvil_portal", + "anvilcraft:celestial_back_gate", "anvilcraft:transcendium_block", "anvilcraft:confined_time_anvilon", "anvilcraft:confined_space_anvilon", diff --git a/src/generated/resources/data/minecraft/tags/block/dragon_immune.json b/src/generated/resources/data/minecraft/tags/block/dragon_immune.json index 8b2f70097a..2d8c4d9c28 100644 --- a/src/generated/resources/data/minecraft/tags/block/dragon_immune.json +++ b/src/generated/resources/data/minecraft/tags/block/dragon_immune.json @@ -14,6 +14,7 @@ "anvilcraft:celestial_forging_anvil_fluid_interface", "anvilcraft:celestial_forging_anvil_interface_placeholder", "anvilcraft:celestial_forging_anvil_portal", + "anvilcraft:celestial_back_gate", "anvilcraft:ember_metal_block", "anvilcraft:cut_ember_metal_block", "anvilcraft:cut_ember_metal_pillar", diff --git a/src/generated/resources/data/minecraft/tags/block/wither_immune.json b/src/generated/resources/data/minecraft/tags/block/wither_immune.json index e45133f7d7..d1fa1976e5 100644 --- a/src/generated/resources/data/minecraft/tags/block/wither_immune.json +++ b/src/generated/resources/data/minecraft/tags/block/wither_immune.json @@ -15,6 +15,7 @@ "anvilcraft:celestial_forging_anvil_laser_interface", "anvilcraft:celestial_forging_anvil_interface_placeholder", "anvilcraft:celestial_forging_anvil_portal", + "anvilcraft:celestial_back_gate", "anvilcraft:ember_metal_block", "anvilcraft:cut_ember_metal_block", "anvilcraft:cut_ember_metal_pillar", diff --git a/src/main/java/dev/dubhe/anvilcraft/AnvilCraft.java b/src/main/java/dev/dubhe/anvilcraft/AnvilCraft.java index c8c111b4d7..18af6aa411 100644 --- a/src/main/java/dev/dubhe/anvilcraft/AnvilCraft.java +++ b/src/main/java/dev/dubhe/anvilcraft/AnvilCraft.java @@ -15,10 +15,12 @@ import dev.dubhe.anvilcraft.data.AnvilCraftDatagen; import dev.dubhe.anvilcraft.dfu.AnvilCraftDfu; import dev.dubhe.anvilcraft.init.ModAttachments; +import dev.dubhe.anvilcraft.init.ModBiomeSources; import dev.dubhe.anvilcraft.init.ModCriterionTriggers; import dev.dubhe.anvilcraft.init.ModDataAttachments; import dev.dubhe.anvilcraft.init.ModDispenserBehavior; import dev.dubhe.anvilcraft.init.ModInspections; +import dev.dubhe.anvilcraft.init.ModMegastructures; import dev.dubhe.anvilcraft.init.ModMenuTypes; import dev.dubhe.anvilcraft.init.ModMobEffects; import dev.dubhe.anvilcraft.init.ModParticles; @@ -93,6 +95,7 @@ public class AnvilCraft { public AnvilCraft(IEventBus modEventBus, ModContainer modContainer) { MOD_BUS = modEventBus; NeoForgeMod.enableMilkFluid(); + ModBiomeSources.register(modEventBus); ModAttachments.register(modEventBus); ModItemGroups.register(modEventBus); ModBlocks.register(); @@ -101,6 +104,7 @@ public AnvilCraft(IEventBus modEventBus, ModContainer modContainer) { ModItems.register(); ModBlockEntities.register(); ModMenuTypes.register(); + ModMegastructures.register(modEventBus); ModComponents.register(modEventBus); ModVillagers.register(modEventBus); ModRecipeTypes.register(modEventBus); diff --git a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/HudTooltipManager.java b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/HudTooltipManager.java index 87214ede9f..0bd73c8119 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/HudTooltipManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/HudTooltipManager.java @@ -109,6 +109,12 @@ public void registerHandHeldItemTooltip(IHandHeldItemTooltipProvider provider) { handItemProviders.add(provider); } + public void resetClientState() { + for (ITooltipProvider.BlockEntityTooltipProvider provider : this.blockEntityProviders) { + provider.resetClientState(); + } + } + /** * 渲染方块的tooltip */ diff --git a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/ItemTooltipManager.java b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/ItemTooltipManager.java index 4c49aea7b0..4e11785679 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/ItemTooltipManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/ItemTooltipManager.java @@ -526,7 +526,7 @@ public class ItemTooltipManager { NORMAL.put(ModBlocks.ANCIENT_SEA_REEF.asItem(), "A chunk of sea reef – looks like it's hiding some treasure"); NORMAL.put(ModBlocks.VOID_MATTER_BLOCK.asItem(), "A chunk of void..."); NORMAL.put(ModBlocks.EXCITED_STATE_VOID_MATTER_BLOCK.asItem(), "A chunk of void... exciting void... comes from a Black Hole"); - NORMAL.put(ModBlocks.CREATIVE_GENERATOR.asItem(), "Provide up to 8192 kW of power, can also be used as a load"); + NORMAL.put(ModBlocks.CREATIVE_GENERATOR.asItem(), "Provide up to 65536 kW of power, can also be used as a load"); NORMAL.put(ModFoodItems.COCOA_BUTTER.asItem(), "One hundred percent natural pure cocoa butter!"); SHIFT.put( diff --git a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/CfaLogisticsInterfaceTooltipProvider.java b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/CfaLogisticsInterfaceTooltipProvider.java index ce0a598a5a..61d0641d43 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/CfaLogisticsInterfaceTooltipProvider.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/CfaLogisticsInterfaceTooltipProvider.java @@ -1,17 +1,32 @@ package dev.dubhe.anvilcraft.api.tooltip.impl; +import dev.anvilcraft.lib.v2.rpc.RPC; +import dev.anvilcraft.lib.v2.rpc.RpcTarget; import dev.dubhe.anvilcraft.api.tooltip.providers.ITooltipProvider; import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLogisticsInterfaceBlockEntity; +import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLogisticsInterfaceBlockEntity.TooltipData; +import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLogisticsInterfaceBlockEntity.TooltipSyncResult; import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +/** Displays a server snapshot of the logistics interface without block-entity sync spam. */ public class CfaLogisticsInterfaceTooltipProvider extends ITooltipProvider.BlockEntityTooltipProvider { + private static final int DATA_REFRESH_INTERVAL = 10; + private @Nullable CelestialForgingAnvilLogisticsInterfaceBlockEntity target; + private @Nullable CompletableFuture> pendingData; + private long nextDataRefresh; + private long dataVersion; + private @Nullable TooltipData data; + @Override public boolean accepts(BlockEntity value) { return value instanceof CelestialForgingAnvilLogisticsInterfaceBlockEntity; @@ -20,38 +35,40 @@ public boolean accepts(BlockEntity value) { @Override public List tooltip(BlockEntity value) { if (!(value instanceof CelestialForgingAnvilLogisticsInterfaceBlockEntity logistics)) return List.of(); - List lines = new ArrayList<>(); + this.refreshData(logistics); + TooltipData snapshot = this.data; + if (snapshot == null) { + return List.of(Component.literal("...").withStyle(ChatFormatting.DARK_GRAY)); + } - // Show temple demand if active and unsatisfied - var demandItem = logistics.getTempleDemandItem(); - if (!demandItem.isEmpty() && !logistics.isTempleDemandSatisfied()) { + List lines = new ArrayList<>(); + ItemStack demandItem = snapshot.templeDemandItem(); + if (!demandItem.isEmpty()) { lines.add(Component.translatable("screen.anvilcraft.cfa.temple_demand") .withStyle(ChatFormatting.GOLD)); - int progress = logistics.getTempleDemandProgress(); + int progress = snapshot.templeDemandProgress(); if (progress > 0) { lines.add(Component.literal(" · ") .append(demandItem.getHoverName()) - .append(Component.literal(" " + progress + "/" + logistics.getTempleDemandCount())) + .append(Component.literal(" " + progress + "/" + snapshot.templeDemandCount())) .withStyle(ChatFormatting.YELLOW)); } else { lines.add(Component.literal(" · ") .append(demandItem.getHoverName()) - .append(Component.literal(" ×" + logistics.getTempleDemandCount())) + .append(Component.literal(" ×" + snapshot.templeDemandCount())) .withStyle(ChatFormatting.YELLOW)); } lines.add(Component.literal("")); } - // Show collider info if active (pushed by CFA controller) - var colliderTargets = logistics.getColliderTargetItems(); - if (!colliderTargets.isEmpty() || logistics.isColliderProcessing() || logistics.isColliderStarMissing()) { - // Star missing warning — shown instead of target items - if (logistics.isColliderStarMissing()) { + List colliderTargets = snapshot.colliderTargetItems(); + if (!colliderTargets.isEmpty() || snapshot.colliderProcessing() || snapshot.colliderStarMissing()) { + if (snapshot.colliderStarMissing()) { lines.add(Component.translatable("screen.anvilcraft.cfa.collider_star_missing") .withStyle(ChatFormatting.RED)); lines.add(Component.literal("")); - } else if (logistics.isColliderProcessing()) { - var level = Minecraft.getInstance().level; + } else if (snapshot.colliderProcessing()) { + Level level = logistics.getLevel(); int dots = level != null ? (int) ((level.getGameTime() / 10) % 3) : 0; String base = Component.translatable("screen.anvilcraft.cfa.collider_processing").getString(); lines.add(Component.literal(base + ".".repeat(dots + 1) + "◇") @@ -60,8 +77,7 @@ public List tooltip(BlockEntity value) { lines.add(Component.translatable("screen.anvilcraft.cfa.collider_targets") .withStyle(ChatFormatting.AQUA)); } - // Only show target items when idle (not processing, not star missing) - if (!logistics.isColliderStarMissing() && !logistics.isColliderProcessing()) { + if (!snapshot.colliderStarMissing() && !snapshot.colliderProcessing()) { for (ItemStack target : colliderTargets) { if (!target.isEmpty()) { lines.add(Component.literal(" · ") @@ -73,25 +89,54 @@ public List tooltip(BlockEntity value) { lines.add(Component.literal("")); } - var handler = logistics.getItemHandler(); - boolean hasAny = false; - for (int i = 0; i < handler.getSlots(); i++) { - ItemStack stack = handler.getStackInSlot(i); - if (!stack.isEmpty()) { - hasAny = true; - lines.add(Component.literal(" · ") - .append(stack.getHoverName()) - .append(Component.literal(" ×" + stack.getCount())) - .withStyle(ChatFormatting.GRAY)); - } + for (ItemStack stack : snapshot.storedItems()) { + lines.add(Component.literal(" · ") + .append(stack.getHoverName()) + .append(Component.literal(" ×" + stack.getCount())) + .withStyle(ChatFormatting.GRAY)); } - if (!hasAny) { + if (snapshot.storedItems().isEmpty()) { lines.add(Component.translatable("screen.anvilcraft.cfa.interface.empty") .withStyle(ChatFormatting.DARK_GRAY)); } return lines; } + private void refreshData(CelestialForgingAnvilLogisticsInterfaceBlockEntity logistics) { + Level level = logistics.getLevel(); + if (level == null) return; + if (this.target != logistics) { + this.resetClientState(); + this.target = logistics; + this.nextDataRefresh = level.getGameTime(); + } + if (this.pendingData != null && this.pendingData.isDone()) { + this.pendingData.join().ifPresent(result -> { + this.dataVersion = result.version(); + if (result.data() != null) this.data = result.data(); + }); + this.pendingData = null; + } + if (this.pendingData != null || level.getGameTime() < this.nextDataRefresh) return; + this.nextDataRefresh = level.getGameTime() + DATA_REFRESH_INTERVAL; + this.pendingData = RPC.invoke( + RpcTarget.server(), + CelestialForgingAnvilLogisticsInterfaceBlockEntity::syncTooltipData, + logistics.getBlockPos(), + this.dataVersion + ).thenApply(Optional::of).exceptionally(ignored -> Optional.empty()); + } + + @Override + public void resetClientState() { + if (this.pendingData != null) this.pendingData.cancel(false); + this.target = null; + this.pendingData = null; + this.nextDataRefresh = 0; + this.dataVersion = 0; + this.data = null; + } + @Override public ItemStack icon(BlockEntity value) { return ItemStack.EMPTY; diff --git a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/HeatableBlockTooltipProvider.java b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/HeatableBlockTooltipProvider.java index dcc234a721..ec809491fd 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/HeatableBlockTooltipProvider.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/impl/HeatableBlockTooltipProvider.java @@ -1,6 +1,7 @@ package dev.dubhe.anvilcraft.api.tooltip.impl; -import dev.anvilcraft.lib.v2.util.Util; +import dev.anvilcraft.lib.v2.rpc.RPC; +import dev.anvilcraft.lib.v2.rpc.RpcTarget; import dev.dubhe.anvilcraft.api.heat.HeatRecorder; import dev.dubhe.anvilcraft.api.tooltip.providers.ITooltipProvider; import dev.dubhe.anvilcraft.block.entity.heatable.HeatableBlockEntity; @@ -9,12 +10,22 @@ import dev.dubhe.anvilcraft.util.FormattingUtil; import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import java.util.ArrayList; import java.util.List; +import java.util.OptionalInt; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; public class HeatableBlockTooltipProvider extends ITooltipProvider.BlockEntityTooltipProvider { + private static final int DURATION_REFRESH_INTERVAL = 10; + private @Nullable BlockEntity target; + private @Nullable CompletableFuture pendingDuration; + private long nextDurationRefresh; + private int duration = -1; + @Override public boolean accepts(BlockEntity entity) { return entity instanceof HeatableBlockEntity; @@ -24,23 +35,53 @@ public boolean accepts(BlockEntity entity) { public List tooltip(BlockEntity entity) { if (CompatUtil.HAS_JADE.get() && AnvilCraftClient.CONFIG.doNotShowTooltipWhenJadePresent) return null; List components = new ArrayList<>(); - HeatRecorder.getTier(entity.getLevel(), entity.getBlockPos(), entity.getBlockState()) + Level level = entity.getLevel(); + if (level == null) return components; + HeatRecorder.getTier(level, entity.getBlockPos(), entity.getBlockState()) .ifPresent(tier -> components.add(ITooltipProvider.withIndentAndMerge(Component.translatable( "tooltip.anvilcraft.heat.tier", tier.getDisplayName() ).withStyle(ChatFormatting.GRAY)))); - Util.castSafely(entity, HeatableBlockEntity.class) - .map(HeatableBlockEntity::getDuration) - .ifPresent(duration -> components.add(ITooltipProvider.withIndentAndMerge(Component.translatable( - "tooltip.anvilcraft.heat.duration", - FormattingUtil.toFormattedTime(Math.max(duration, 0), 1) - ).withStyle(ChatFormatting.GRAY)))); - if (!components.isEmpty()) { - components.addFirst(Component.translatable("tooltip.anvilcraft.heat.title").withStyle(ChatFormatting.BLUE)); - } + this.refreshDuration(entity); + Object duration = this.duration < 0 + ? Component.translatable("tooltip.anvilcraft.waiting") + : FormattingUtil.toFormattedTime(Math.max(this.duration, 0), 1); + components.add(ITooltipProvider.withIndentAndMerge(Component.translatable( + "tooltip.anvilcraft.heat.duration", + duration + ).withStyle(ChatFormatting.GRAY))); + components.addFirst(Component.translatable("tooltip.anvilcraft.heat.title").withStyle(ChatFormatting.BLUE)); return components; } + private void refreshDuration(BlockEntity entity) { + Level level = entity.getLevel(); + if (level == null) return; + if (this.target != entity) { + this.resetClientState(); + this.target = entity; + this.nextDurationRefresh = level.getGameTime(); + } + if (this.pendingDuration != null && this.pendingDuration.isDone()) { + this.pendingDuration.join().ifPresent(value -> this.duration = value); + this.pendingDuration = null; + } + if (this.pendingDuration != null || level.getGameTime() < this.nextDurationRefresh) return; + this.nextDurationRefresh = level.getGameTime() + DURATION_REFRESH_INTERVAL; + this.pendingDuration = RPC.invoke(RpcTarget.server(), HeatableBlockEntity::getDuration, entity.getBlockPos()) + .thenApply(OptionalInt::of) + .exceptionally(ignored -> OptionalInt.empty()); + } + + @Override + public void resetClientState() { + if (this.pendingDuration != null) this.pendingDuration.cancel(false); + this.target = null; + this.pendingDuration = null; + this.nextDurationRefresh = 0; + this.duration = -1; + } + @Override public int priority() { return 0; diff --git a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/providers/ITooltipProvider.java b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/providers/ITooltipProvider.java index d8417b2097..1e692f4336 100644 --- a/src/main/java/dev/dubhe/anvilcraft/api/tooltip/providers/ITooltipProvider.java +++ b/src/main/java/dev/dubhe/anvilcraft/api/tooltip/providers/ITooltipProvider.java @@ -73,6 +73,10 @@ public ItemStack icon(BlockEntity value) { } public abstract int priority(); + + /** Releases client-world state retained by a long-lived provider instance. */ + public void resetClientState() { + } } static Component withIndentAndMerge(Component... components) { diff --git a/src/main/java/dev/dubhe/anvilcraft/block/CelestialBackGateBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/CelestialBackGateBlock.java new file mode 100644 index 0000000000..bebbb4ad65 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/CelestialBackGateBlock.java @@ -0,0 +1,98 @@ +package dev.dubhe.anvilcraft.block; + +import com.mojang.serialization.MapCodec; +import dev.dubhe.anvilcraft.block.entity.CelestialBackGateBlockEntity; +import dev.dubhe.anvilcraft.init.block.ModBlockEntities; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.HorizontalDirectionalBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.Shapes; +import net.minecraft.world.phys.shapes.VoxelShape; + +import javax.annotation.Nullable; + +/** An unbreakable, non-inventory gate generated at a special-planet landing point. */ +public class CelestialBackGateBlock extends BaseEntityBlock { + public static final DirectionProperty FACING = HorizontalDirectionalBlock.FACING; + + public CelestialBackGateBlock(Properties properties) { + super(properties); + registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + protected MapCodec codec() { + return simpleCodec(CelestialBackGateBlock::new); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + public @Nullable BlockState getStateForPlacement(BlockPlaceContext context) { + return defaultBlockState().setValue(FACING, context.getHorizontalDirection()); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return ModBlockEntities.CELESTIAL_BACK_GATE.create(pos, state); + } + + @Override + public @Nullable BlockEntityTicker getTicker( + Level level, BlockState state, BlockEntityType type + ) { + if (level.isClientSide()) return null; + return createTickerHelper(type, ModBlockEntities.CELESTIAL_BACK_GATE.get(), + (lvl, pos, blockState, be) -> be.tick()); + } + + @Override + protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity) { + if (level.isClientSide()) return; + if (level.getBlockEntity(pos) instanceof CelestialBackGateBlockEntity gate) { + if (entity instanceof Player) { + gate.queueTouch(entity); + } else { + gate.tryTouch(entity); + } + } + } + + @Override + protected VoxelShape getCollisionShape( + BlockState state, BlockGetter level, BlockPos pos, CollisionContext context + ) { + return Shapes.empty(); + } + + @Override + public boolean onDestroyedByPlayer( + BlockState state, Level level, BlockPos pos, Player player, boolean willHarvest, FluidState fluid + ) { + return false; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java index 507d9d78d3..9c0dae44ee 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilBlock.java @@ -21,7 +21,6 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.Containers; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.ItemInteractionResult; @@ -41,12 +40,16 @@ import net.minecraft.world.level.block.state.properties.EnumProperty; import net.minecraft.world.level.block.state.properties.Property; import net.minecraft.world.level.pathfinder.PathComputationType; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.List; import javax.annotation.Nullable; public class CelestialForgingAnvilBlock @@ -243,10 +246,10 @@ public void onUnformed(Level level, MultiblockState state) { level.getBlockEntity(state.getControllerPos(), ModBlockEntities.CELESTIAL_FORGING_ANVIL.get()) .ifPresent(be -> { be.setAmplifierPresent(false); - if (be.getCelestialBodyData() instanceof StarData) { + if (be.getCelestialBodyData() instanceof StarData star && !star.specialRedDwarf()) { be.removeGravitySource(); /// 恒星不可见时立即移除引力 be.setLocked(true); - be.getSearchHistory().clear(); + be.clearSearchHistory(); /// 保持isAmplify为true以保留天体数据, /// 但渲染器会在增幅器缺失时隐藏天体显示 } else { @@ -267,50 +270,98 @@ public BlockPos correctPos(ServerLevel level, BlockPos pos, BlockState state) { return pos.offset(state.getValue(HALF).getOffset()).offset(this.getMainPartOffset()); } - @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") @Override - protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { - if (state.is(newState.getBlock())) return; - BlockPos mainPos = getMainPartPos(pos, state); - boolean isMain = state.hasProperty(HALF) && state.getValue(HALF) == Cube323PartHalf.BOTTOM_CENTER; - if (isMain && level.getBlockEntity(mainPos) instanceof CelestialForgingAnvilBlockEntity be) { - /// 1. 将所有库存物品掉落至世界中 - for (int i = 0; i < be.getAnvilInventory().getContainerSize(); i++) { - ItemStack stack = be.getAnvilInventory().getItem(i); - if (!stack.isEmpty()) { - Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, stack); - } - } - ItemStack matStack = be.getMaterialContainer().getItem(0); - if (!matStack.isEmpty()) { - Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, matStack); - } + protected List getDrops(BlockState state, LootParams.Builder params) { + List drops = super.getDrops(state, params); + BlockEntity blockEntity = params.getOptionalParameter(LootContextParams.BLOCK_ENTITY); + if (!(blockEntity instanceof CelestialForgingAnvilBlockEntity be)) { + return drops; + } - /// 2. 掉落方块物品,保留天体数据、巨构数据和匹配参数, - /// 以便在其他位置放置时天体能够重新出现。 - /// 运行时和位置相关的标志会从物品标签中清除。 - if (!level.isClientSide) { - ItemStack blockStack = new ItemStack(asItem()); - CompoundTag beTag = new CompoundTag(); - be.saveAdditional(beTag, level.registryAccess()); - - /// 清除与当前世界位置或瞬态运行时相关的数据 - beTag.remove("anvils"); /// 库存 — 已在上方掉落 - beTag.remove("materialFilter"); /// UI状态 — 关闭菜单时重置 - beTag.remove("materialLimit"); /// UI状态 - beTag.remove("searchHistory"); /// 搜索历史 — 不保留 - beTag.remove("searching"); /// 运行时 - beTag.remove("searchTicks"); /// 运行时 - beTag.remove("searchFailed"); /// 运行时 - beTag.remove("powerInsufficient"); /// 运行时 - beTag.remove("excavatorLaserActive"); /// 依赖于附近的激光方块 - beTag.remove("amplifierPresent"); /// 依赖于多方块结构 - - if (!beTag.isEmpty()) { - BlockItem.setBlockEntityData(blockStack, be.getType(), beTag); - } - Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, blockStack); - } + boolean dropsController = false; + for (ItemStack stack : drops) { + if (!stack.is(this.asItem())) continue; + dropsController = true; + this.saveBlockEntityDataToDrop(stack, be, params); + } + if (!dropsController) return drops; + + List result = new ArrayList<>(drops); + for (int i = 0; i < be.getAnvilInventory().getContainerSize(); i++) { + ItemStack stack = be.getAnvilInventory().getItem(i); + if (!stack.isEmpty()) result.add(stack.copy()); + } + ItemStack materialStack = be.getMaterialContainer().getItem(0); + if (!materialStack.isEmpty()) result.add(materialStack.copy()); + return result; + } + + private void saveBlockEntityDataToDrop( + ItemStack stack, + CelestialForgingAnvilBlockEntity blockEntity, + LootParams.Builder params + ) { + CompoundTag blockEntityTag = blockEntity.saveForDrop(params.getLevel().registryAccess()); + blockEntityTag.remove("anvils"); + blockEntityTag.remove("materialFilter"); + blockEntityTag.remove("materialLimit"); + blockEntityTag.remove("searchHistory"); + blockEntityTag.remove("searching"); + blockEntityTag.remove("searchTicks"); + blockEntityTag.remove("searchFailed"); + blockEntityTag.remove("powerInsufficient"); + blockEntityTag.remove("searchCapturedSeed"); + blockEntityTag.remove("searchSeedStateKnown"); + blockEntityTag.remove("historyBrowseIndex"); + blockEntityTag.remove("excavatorLaserActive"); + blockEntityTag.remove("penroseSphereLaserActive"); + blockEntityTag.remove("templeCycleDay"); + blockEntityTag.remove("templeLastDay"); + blockEntityTag.remove("templeDemand"); + blockEntityTag.remove("templeDemandCount"); + blockEntityTag.remove("templeDemandProgress"); + blockEntityTag.remove("templeDemandSatisfied"); + blockEntityTag.remove("acceleratorStage"); + blockEntityTag.remove("acceleratorTicksRemaining"); + blockEntityTag.remove("acceleratorTicksTotal"); + blockEntityTag.remove("acceleratorOriginalMass"); + blockEntityTag.remove("acceleratorOriginalEnergy"); + blockEntityTag.remove("acceleratorOriginalSize"); + blockEntityTag.remove("acceleratorDysonDestroyed"); + blockEntityTag.remove("acceleratorDysonDestroyTick"); + blockEntityTag.remove("collapseAnimTicks"); + blockEntityTag.remove("quenchedScheduled"); + blockEntityTag.remove("quenchedStartTick"); + blockEntityTag.remove("quenchedStarted"); + blockEntityTag.remove("quenchedCanceled"); + blockEntityTag.remove("quenchedSupernovaFired"); + blockEntityTag.remove("supernovaFlashTicks"); + blockEntityTag.remove("supernovaCenterY"); + blockEntityTag.remove("supernovaScale"); + blockEntityTag.remove("wormholeBodyUuid"); + blockEntityTag.remove("portals"); + blockEntityTag.remove("amplifierPresent"); + blockEntityTag.remove("activeMegastructureId"); + blockEntityTag.remove("activeMegastructure"); + blockEntityTag.remove("activeMegastructureName"); + blockEntityTag.remove("activeMegastructureRing"); + blockEntityTag.remove("colliderCooldown"); + blockEntityTag.remove("colliderCycleRemaining"); + blockEntityTag.remove("colliderActiveSpeed"); + blockEntityTag.remove("colliderLogisticsRoundRobin"); + blockEntityTag.remove("colliderReservedAnvil"); + blockEntityTag.remove("colliderReservedHitBlock"); + if (!blockEntityTag.isEmpty()) { + BlockItem.setBlockEntityData(stack, blockEntity.getType(), blockEntityTag); + } + } + + @Override + protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock()) + && state.getValue(HALF) == Cube323PartHalf.BOTTOM_CENTER + && level.getBlockEntity(pos) instanceof CelestialForgingAnvilBlockEntity blockEntity) { + blockEntity.prepareForPermanentRemoval(); } super.onRemove(state, level, pos, newState, movedByPiston); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java index f0b35a1125..37ca70016a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/cfa/CelestialForgingAnvilPortalBlock.java @@ -148,7 +148,7 @@ public BlockState updateShape(BlockState state, Direction direction, BlockState .setValue(HALF, DirectionGate331PartHalf.BOTTOM_CENTER) .setValue(FACING, dir.getOpposite()) .setValue(OPEN, false) - .setValue(WATERLOGGED, fluidState.getType() == Fluids.WATER); + .setValue(WATERLOGGED, fluidState.getType() == Fluids.WATER && fluidState.isSource()); } } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/cfa/interfaces/CelestialForgingAnvilInterfacePlaceholderBlock.java b/src/main/java/dev/dubhe/anvilcraft/block/cfa/interfaces/CelestialForgingAnvilInterfacePlaceholderBlock.java index cc07fed193..44e3cf52b8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/cfa/interfaces/CelestialForgingAnvilInterfacePlaceholderBlock.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/cfa/interfaces/CelestialForgingAnvilInterfacePlaceholderBlock.java @@ -1,6 +1,7 @@ package dev.dubhe.anvilcraft.block.cfa.interfaces; import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.util.ShapeUtil; import dev.dubhe.anvilcraft.api.hammer.IHammerRemovable; import dev.dubhe.anvilcraft.block.cfa.CelestialForgingAnvilBlock; import dev.dubhe.anvilcraft.block.cfa.item.CelestialForgingAnvilInterfaceBlockItem; @@ -25,10 +26,21 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; public class CelestialForgingAnvilInterfacePlaceholderBlock extends HorizontalDirectionalBlock implements IHammerRemovable { + public static final VoxelShape NORTH = ShapeUtil.merge( + CelestialForgingAnvilInterfaceBlock.BASE_NORTH, + Block.box(4, 4, 0, 12, 12, 8), + Block.box(4, 8, 6, 12, 16, 14), + Block.box(5, 12, 1, 11, 18, 7) + ); + public static final VoxelShape WEST = ShapeUtil.rotate(Direction.Axis.Y, 90, NORTH); + public static final VoxelShape SOUTH = ShapeUtil.rotate(Direction.Axis.Y, 180, NORTH); + public static final VoxelShape EAST = ShapeUtil.rotate(Direction.Axis.Y, 270, NORTH); public CelestialForgingAnvilInterfacePlaceholderBlock(Properties properties) { super(properties); @@ -56,6 +68,17 @@ protected boolean propagatesSkylightDown(BlockState state, BlockGetter level, Bl return true; } + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return switch (state.getValue(FACING)) { + case NORTH -> NORTH; + case SOUTH -> SOUTH; + case WEST -> WEST; + case EAST -> EAST; + default -> throw new IllegalArgumentException("Unsupported direction for horizontal facing"); + }; + } + @Override protected ItemInteractionResult useItemOn( ItemStack stack, BlockState state, Level level, BlockPos pos, diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/BaseLaserBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/BaseLaserBlockEntity.java index adf8db5066..867f0d6fa1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/BaseLaserBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/BaseLaserBlockEntity.java @@ -4,13 +4,10 @@ import dev.dubhe.anvilcraft.api.heat.HeaterManager; import dev.dubhe.anvilcraft.api.rendering.CacheableBERenderingPipeline; import dev.dubhe.anvilcraft.block.LensBlock; -import dev.dubhe.anvilcraft.block.RubyPrismBlock; -import dev.dubhe.anvilcraft.block.entity.heatable.HeatableBlockEntity; import dev.dubhe.anvilcraft.block.multipart.FlexibleMultiPartBlock; import dev.dubhe.anvilcraft.block.state.LensType; import dev.dubhe.anvilcraft.init.ModHeaterInfos; import dev.dubhe.anvilcraft.init.block.ModBlockTags; -import dev.dubhe.anvilcraft.init.block.ModBlocks; import dev.dubhe.anvilcraft.init.entity.ModDamageTypes; import dev.dubhe.anvilcraft.network.LaserEmitPacket; import dev.dubhe.anvilcraft.util.BlockMiningEffect; @@ -28,7 +25,6 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; @@ -47,6 +43,7 @@ import java.util.List; import java.util.Set; +@SuppressWarnings("checkstyle:JavadocParagraph") public abstract class BaseLaserBlockEntity extends BlockEntity { public static final int[] COOLDOWNS = { Integer.MAX_VALUE, @@ -74,6 +71,7 @@ public abstract class BaseLaserBlockEntity extends BlockEntity { protected int laserLinkRevision = 0; protected int irradiatedLaserTargetRevision = -1; private BlockMiningEffect lastEmittedMiningEffect = BlockMiningEffect.NORMAL; + private boolean lastEmittedGamma = false; @Getter protected int laserLevel = 0; /// 跟踪正在被伽马激光照射的方块位置及持续时间。 @@ -148,6 +146,17 @@ protected int getBaseLaserLevel() { return 1; } + /** + * Whether this emitter is currently producing a gamma beam. + * + * Keeping this on the common laser type lets receivers preserve the beam + * type for every emitter (including creative lasers), instead of coupling + * their validation to a list of concrete block entities. + */ + public boolean isEmittingGamma() { + return false; + } + protected int calculateLaserLevel() { return getBaseLaserLevel() + this.irradiateSelfLaserBlockSet.stream() @@ -223,11 +232,14 @@ public void emitLaser(Direction direction) { lastIrradiatedLaserBlockEntity.onCancelingIrradiation(this); } } + this.irradiatedLaserTarget = null; + this.irradiatedLaserTargetRevision = -1; } int newLaserLevel = this.calculateLaserLevel(); boolean laserLevelChanged = this.laserLevel != newLaserLevel; BlockMiningEffect miningEffect = getMiningEffect(); boolean miningEffectChanged = !lastEmittedMiningEffect.equals(miningEffect); + boolean gammaChanged = this.lastEmittedGamma; this.updateLaserLevel(newLaserLevel); if ( newLaserTarget != null @@ -237,7 +249,8 @@ public void emitLaser(Direction direction) { || targetEntityChanged || targetRevisionChanged || laserLevelChanged - || miningEffectChanged; + || miningEffectChanged + || gammaChanged; if (needsIrradiationUpdate && !newLaserTarget.getIgnoreFace().contains(direction)) { this.level.updateNeighborsAt(tempIrradiateBlockPos, getBlockState().getBlock()); newLaserTarget.onIrradiated(this); @@ -246,6 +259,7 @@ public void emitLaser(Direction direction) { } } this.lastEmittedMiningEffect = miningEffect; + this.lastEmittedGamma = false; this.updateIrradiateBlockPos(tempIrradiateBlockPos); if (!(this.level instanceof ServerLevel serverLevel)) return; @@ -469,7 +483,7 @@ public void updateLaserLevel(int value) { this.laserLevel = value; } - public void clientUpdate(BlockPos irradiateBlockPos, int laserLevel) { + public void clientUpdate(@Nullable BlockPos irradiateBlockPos, int laserLevel) { this.irradiateBlockPos = irradiateBlockPos; this.laserLevel = laserLevel; CacheableBERenderingPipeline.getInstance().update(this); @@ -491,61 +505,59 @@ protected void emitGammaLaserBeam(Direction direction) { int originalMaxDistance = this.maxTransmissionDistance; this.maxTransmissionDistance = 16; int gammaLevel = this.getGammaLaserLevel(); - BlockPos tempIrradiateBlockPos = this.getGammaIrradiateBlockPos(16, direction, this.getBlockPos()); + BlockPos gammaOrigin = this.getBlockPos(); if (this.isLaserOriginOffset()) { - tempIrradiateBlockPos = this.getGammaIrradiateBlockPos( - 16, direction, this.getBlockPos().relative(direction)); + gammaOrigin = gammaOrigin.relative(direction); } - this.destroyPrismsAlongPath(direction, tempIrradiateBlockPos); - if (!tempIrradiateBlockPos.equals(this.irradiateBlockPos)) { - if (this.irradiateBlockPos != null) { + BlockPos tempIrradiateBlockPos = CfaGammaLaserEffects.findTarget(this.level, gammaOrigin, direction); + CfaGammaLaserEffects.destroyPrisms(this.level, this.getBlockPos(), direction, tempIrradiateBlockPos); + BaseLaserBlockEntity newLaserTarget = + this.level.getBlockEntity(tempIrradiateBlockPos) instanceof BaseLaserBlockEntity target ? target : null; + boolean targetChanged = !tempIrradiateBlockPos.equals(this.irradiateBlockPos); + boolean targetEntityChanged = newLaserTarget != this.irradiatedLaserTarget; + boolean targetRevisionChanged = newLaserTarget != null + && newLaserTarget.laserLinkRevision != this.irradiatedLaserTargetRevision; + if (targetChanged || targetEntityChanged || targetRevisionChanged) { + if (this.irradiatedLaserTarget != null) { + this.irradiatedLaserTarget.onCancelingIrradiation(this); + } else if (targetChanged && this.irradiateBlockPos != null) { BlockEntity oldBe = this.level.getBlockEntity(this.irradiateBlockPos); if (oldBe instanceof BaseLaserBlockEntity lastIrradiated) { lastIrradiated.onCancelingIrradiation(this); } } + this.irradiatedLaserTarget = null; + this.irradiatedLaserTargetRevision = -1; } - if (this.level.getBlockEntity(tempIrradiateBlockPos) instanceof BaseLaserBlockEntity irradiated - && !this.isInIrradiateSelfLaserBlockSet(irradiated) - ) { - if (irradiated.getIgnoreFace().isEmpty()) { + boolean laserLevelChanged = this.laserLevel != gammaLevel; + BlockMiningEffect miningEffect = this.getMiningEffect(); + boolean miningEffectChanged = !this.lastEmittedMiningEffect.equals(miningEffect); + boolean gammaChanged = !this.lastEmittedGamma; + this.updateLaserLevel(gammaLevel); + if (newLaserTarget != null && !this.isInIrradiateSelfLaserBlockSet(newLaserTarget)) { + boolean needsIrradiationUpdate = targetChanged + || targetEntityChanged + || targetRevisionChanged + || laserLevelChanged + || miningEffectChanged + || gammaChanged; + if (needsIrradiationUpdate && !newLaserTarget.getIgnoreFace().contains(direction)) { this.level.updateNeighborsAt(tempIrradiateBlockPos, getBlockState().getBlock()); - irradiated.onIrradiated(this); - } else { - for (Direction dir : irradiated.getIgnoreFace()) { - if (direction != dir) { - this.level.updateNeighborsAt(tempIrradiateBlockPos, getBlockState().getBlock()); - irradiated.onIrradiated(this); - } - } + newLaserTarget.onIrradiated(this); + this.irradiatedLaserTarget = newLaserTarget; + this.irradiatedLaserTargetRevision = newLaserTarget.laserLinkRevision; } } + this.lastEmittedMiningEffect = miningEffect; + this.lastEmittedGamma = true; this.updateIrradiateBlockPos(tempIrradiateBlockPos); - this.updateLaserLevel(gammaLevel); if (!(this.level instanceof ServerLevel)) { this.maxTransmissionDistance = originalMaxDistance; return; } - int hurt = Math.min(16, gammaLevel - 4) * 16; - if (hurt > 0) { - Vec3 startPos = this.getBlockPos() - .relative(direction) - .getCenter() - .add(-0.0625, -0.0625, -0.0625); - AABB trackBoundingBox = new AABB( - startPos, - this.irradiateBlockPos.relative(direction.getOpposite()) - .getCenter() - .add(0.0625, 0.0625, 0.0625) - ); - this.level.getEntities( - EntityTypeTest.forClass(LivingEntity.class), - trackBoundingBox, - Entity::isAlive - ).forEach(livingEntity -> - livingEntity.hurt(ModDamageTypes.gammaLaser(this.level), hurt) - ); - } + CfaGammaLaserEffects.damageEntities( + this.level, this.getBlockPos(), this.irradiateBlockPos, direction, gammaLevel + ); BlockState irradiateBlock = this.level.getBlockState(this.irradiateBlockPos); int requiredExposure = GAMMA_EXPOSURE_TICKS[Math.clamp(gammaLevel / 4, 0, 4)]; BlockPos currentTarget = this.irradiateBlockPos.immutable(); @@ -573,105 +585,9 @@ protected void emitGammaLaserBeam(Direction direction) { } else { this.gammaExposureTicks = 0; } - this.tryHeatEmberMetal(direction); + CfaGammaLaserEffects.heatEmberMetal( + this.level, this.irradiateBlockPos, direction, gammaLevel, Block.UPDATE_CLIENTS + ); this.maxTransmissionDistance = originalMaxDistance; } - - /// 直接命中余烬金属块时,加热光束法向截面区域内的所有余烬金属方块。面积随伽马等级缩放:≥4→1×1,≥8→3×3,≥12→5×5×2,≥16→7×7×3。 - protected void tryHeatEmberMetal(Direction direction) { - if (this.level == null) return; - int gammaLevel = this.getGammaLaserLevel(); - if (gammaLevel < 4) return; - if (this.level.getGameTime() % 20 != 0) return; - BlockPos hitPos = this.irradiateBlockPos; - if (hitPos == null) return; - BlockState hitState = this.level.getBlockState(hitPos); - if (!hitState.is(ModBlocks.EMBER_METAL_BLOCK.get()) - && !hitState.is(ModBlocks.OVERHEATED_EMBER_METAL_BLOCK.get())) { - return; - } - int areaSize; - int thickness; - if (gammaLevel >= 16) { - areaSize = 7; - thickness = 3; - } else if (gammaLevel >= 12) { - areaSize = 5; - thickness = 2; - } else if (gammaLevel >= 8) { - areaSize = 3; - thickness = 1; - } else { - areaSize = 1; - thickness = 1; - } - int halfSize = areaSize / 2; - Direction[] perpendiculars = switch (direction.getAxis()) { - case X -> new Direction[]{Direction.UP, Direction.NORTH}; - case Z -> new Direction[]{Direction.UP, Direction.EAST}; - default -> new Direction[]{Direction.NORTH, Direction.EAST}; - }; - for (int depth = 0; depth < thickness; depth++) { - BlockPos depthPos = hitPos.relative(direction, depth); - for (int a = -halfSize; a <= halfSize; a++) { - for (int b = -halfSize; b <= halfSize; b++) { - BlockPos target = depthPos - .relative(perpendiculars[0], a) - .relative(perpendiculars[1], b); - tryHeatEmberMetalAt(target); - } - } - } - } - - /// 在给定位置升级或刷新单个余烬金属方块。 - protected void tryHeatEmberMetalAt(BlockPos pos) { - if (this.level == null) return; - BlockState state = this.level.getBlockState(pos); - if (state.is(ModBlocks.EMBER_METAL_BLOCK.get())) { - Block overheatedBlock = ModBlocks.OVERHEATED_EMBER_METAL_BLOCK.get(); - this.level.setBlock(pos, overheatedBlock.defaultBlockState(), Block.UPDATE_CLIENTS); - if (overheatedBlock instanceof EntityBlock entityBlock) { - BlockEntity be = entityBlock.newBlockEntity(pos, overheatedBlock.defaultBlockState()); - if (be instanceof HeatableBlockEntity heatable) { - this.level.setBlockEntity(heatable); - heatable.addDurationInTick(80); - } - } - } else if (state.is(ModBlocks.OVERHEATED_EMBER_METAL_BLOCK.get())) { - BlockEntity be = this.level.getBlockEntity(pos); - if (be instanceof HeatableBlockEntity heatable) { - heatable.addDurationInTick(80); - } - } - } - - /// 伽马激光只能穿过空气和可替换方块(高草丛等)。玻璃等其它方块都会阻挡伽马激光。 - protected boolean gammaCanPassThrough(BlockPos blockPos) { - if (this.level == null) return false; - BlockState blockState = this.level.getBlockState(blockPos); - return blockState.is(BlockTags.REPLACEABLE); - } - - /// 伽马专用目标查找器:仅穿透可替换方块,在第一个阻挡方块处停止。 - protected BlockPos getGammaIrradiateBlockPos(int expectedLength, Direction direction, BlockPos originPos) { - for (int length = 1; length <= expectedLength; length++) { - BlockPos checkPos = originPos.relative(direction, length); - if (!this.gammaCanPassThrough(checkPos)) return checkPos; - } - return originPos.relative(direction, expectedLength); - } - - /// 沿伽马激光路径摧毁棱镜。 - protected void destroyPrismsAlongPath(Direction direction, BlockPos targetPos) { - if (this.level == null) return; - BlockPos.MutableBlockPos checkPos = this.getBlockPos().relative(direction).mutable(); - while (!checkPos.equals(targetPos)) { - BlockState checkState = this.level.getBlockState(checkPos); - if (checkState.getBlock() instanceof RubyPrismBlock) { - this.level.destroyBlock(checkPos.immutable(), true); - } - checkPos.move(direction); - } - } -} +} \ No newline at end of file diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialBackGateBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialBackGateBlockEntity.java new file mode 100644 index 0000000000..dca65c9ac5 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialBackGateBlockEntity.java @@ -0,0 +1,134 @@ +package dev.dubhe.anvilcraft.block.entity; + +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelManager; +import lombok.Getter; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** Stores the source portal to which a generated return gate leads. */ +public class CelestialBackGateBlockEntity extends BlockEntity { + @Nullable + private ResourceKey returnDimension; + @Nullable + private BlockPos returnPortalPos; + @Getter + private Direction returnFacing = Direction.NORTH; + private final Set touchingEntities = new HashSet<>(); + private final Set pendingEntities = new HashSet<>(); + private boolean duplicateCleanupDone; + + public CelestialBackGateBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + } + + public void configure(ResourceKey dimension, BlockPos portalPos, Direction facing) { + this.returnDimension = dimension; + this.returnPortalPos = portalPos.immutable(); + this.returnFacing = facing; + this.duplicateCleanupDone = false; + setChanged(); + if (level != null) { + level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); + } + } + + @Nullable + public ResourceKey getReturnDimension() { + return returnDimension; + } + + @Nullable + public BlockPos getReturnPortalPos() { + return returnPortalPos; + } + + public void tick() { + if (!(level instanceof ServerLevel serverLevel)) return; + if (!duplicateCleanupDone && returnDimension != null && returnPortalPos != null) { + CelestialTravelManager.cleanupDuplicateGates( + serverLevel, worldPosition, returnDimension, returnPortalPos, returnFacing + ); + duplicateCleanupDone = true; + } + AABB gateBox = new AABB(worldPosition); + for (UUID uuid : Set.copyOf(pendingEntities)) { + Entity entity = serverLevel.getEntity(uuid); + if (entity != null && gateBox.intersects(entity.getBoundingBox())) tryTouch(entity); + } + pendingEntities.clear(); + for (Entity entity : serverLevel.getEntitiesOfClass(Entity.class, gateBox)) { + tryTouch(entity); + } + if (!touchingEntities.isEmpty()) { + Set present = serverLevel.getEntitiesOfClass(Entity.class, gateBox) + .stream().map(Entity::getUUID).collect(Collectors.toSet()); + touchingEntities.removeIf(uuid -> !present.contains(uuid)); + } + } + + public void tryTouch(Entity entity) { + UUID uuid = entity.getUUID(); + if (touchingEntities.contains(uuid)) return; + if (CelestialTravelManager.tryReturn(entity, this)) { + touchingEntities.add(uuid); + } + } + + /** Queue players for the next block-entity tick so movement packets are fully processed first. */ + public void queueTouch(Entity entity) { + pendingEntities.add(entity.getUUID()); + } + + @Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + if (returnDimension != null) { + tag.putString("returnDimension", returnDimension.location().toString()); + } + if (returnPortalPos != null) { + tag.putLong("returnPortalPos", returnPortalPos.asLong()); + } + tag.putString("returnFacing", returnFacing.getName()); + } + + @Override + protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + String dimensionName = tag.getString("returnDimension"); + if (!dimensionName.isEmpty()) { + try { + returnDimension = ResourceKey.create( + Registries.DIMENSION, ResourceLocation.parse(dimensionName) + ); + } catch (IllegalArgumentException ignored) { + returnDimension = null; + } + } else { + returnDimension = null; + } + returnPortalPos = tag.contains("returnPortalPos") + ? BlockPos.of(tag.getLong("returnPortalPos")) : null; + Direction parsedFacing = Direction.byName(tag.getString("returnFacing")); + returnFacing = parsedFacing != null && parsedFacing.getAxis().isHorizontal() + ? parsedFacing : Direction.NORTH; + duplicateCleanupDone = false; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java index eb080bbdd3..c6ea8c3787 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilBlockEntity.java @@ -5,37 +5,27 @@ import dev.dubhe.anvilcraft.api.power.IPowerProducer; import dev.dubhe.anvilcraft.api.power.PowerComponentType; import dev.dubhe.anvilcraft.api.power.PowerGrid; -import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyClass; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyData; -import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyMatcher; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialRefactorOption; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialRefactorRegistry; -import dev.dubhe.anvilcraft.block.entity.celestial.PlanetResourceGenerator; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialSearchHistory; import dev.dubhe.anvilcraft.block.entity.celestial.PlanetaryResourceSet; -import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyData; -import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyRecipe; import dev.dubhe.anvilcraft.block.entity.celestial.StarData; import dev.dubhe.anvilcraft.block.entity.megastructure.ExcavatorHandler; import dev.dubhe.anvilcraft.block.entity.megastructure.PenroseSphereHandler; import dev.dubhe.anvilcraft.block.entity.megastructure.WormholeStabilizerHandler; import dev.dubhe.anvilcraft.block.state.Cube323PartHalf; -import dev.dubhe.anvilcraft.entity.ThrownHeavyHalberdEntity; +import dev.dubhe.anvilcraft.init.ModMegastructures; import dev.dubhe.anvilcraft.init.ModMenuTypes; -import dev.dubhe.anvilcraft.init.block.ModBlocks; -import dev.dubhe.anvilcraft.init.entity.ModDamageTypes; -import dev.dubhe.anvilcraft.init.item.ModComponents; import dev.dubhe.anvilcraft.init.item.ModItems; -import dev.dubhe.anvilcraft.init.recipe.ModRecipeTypes; import dev.dubhe.anvilcraft.inventory.CelestialForgingAnvilMenu; import dev.dubhe.anvilcraft.item.DiskItem; -import dev.dubhe.anvilcraft.util.GravityManager; import lombok.Getter; import lombok.Setter; import net.minecraft.ChatFormatting; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.chat.Component; @@ -43,36 +33,27 @@ import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.MenuProvider; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.context.UseOnContext; -import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.Vec3; import java.util.ArrayList; -import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.UUID; import javax.annotation.Nullable; @@ -168,6 +149,14 @@ public int getActiveMegastructureIndex() { return megastructureManager.getActiveIndex(); } + public boolean hasActiveMegastructure() { + return this.megastructureManager.hasActiveMegastructure(); + } + + public @Nullable ResourceLocation getActiveMegastructureId() { + return this.megastructureManager.getActiveId(this); + } + /// 抽取器是否有有效的激光输入(用于模型切换)。委托给 ExcavatorHandler。 public boolean isExcavatorLaserActive() { ExcavatorHandler h = megastructureManager.findHandler(ExcavatorHandler.class); @@ -188,9 +177,6 @@ public UUID getWormholeParamsHash() { return wh.getBodyUuid(); } /// 此 CFA 当前是否已在虫洞网络中注册。 - /// 按立方体部件侧边映射到该侧放置的传送门 BlockPos。 - - private final Map portals = new EnumMap<>(Cube323PartHalf.class); /// 虫洞规范接口状态现已全局存储于 WormholeInterfaceStates(BetterSavedData),在整个网络组中共享。 /// === 神殿状态 === @@ -239,8 +225,8 @@ public CelestialForgingAnvilBlockEntity(BlockEntityType type, BlockPos pos, B @Override public int getInputPower() { - if (searching && searchTicksRemaining > 0) { - return isAmplify ? 4000 : 1000; + if (this.searchController.isSearching() && this.searchController.ticksRemaining() > 0) { + return isAmplify ? 32000 : 1000; } return megastructureManager.getInputPower(this); } @@ -448,6 +434,8 @@ public void setChanged() { /// 为给定的重构选项配置材料槽。玩家选择重构选项时在服务器端调用。 public void configureMaterialSlot(int optionIndex) { if (level == null || level.isClientSide()) return; + if (!this.locked) return; + if (this.isSearching() || this.isAcceleratorActive()) return; if (celestialBodyData == null) return; List options = getClientVisibleOptions(); if (optionIndex < 0 || optionIndex >= options.size()) { @@ -467,93 +455,43 @@ public void configureMaterialSlot(int optionIndex) { level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); } - /// 搜索计时器 - @Getter - private int searchTicksRemaining = 0; - @Getter - private boolean searching = false; - @Getter - @Setter - private boolean searchFailed = false; - @Getter - private boolean powerInsufficient = false; - private static final int SEARCH_TICKS = 200; /// 10 秒 - - /// 追踪搜索开始时消耗的种子物品(用于特殊天体匹配) - @javax.annotation.Nullable - private Item lastConsumedSeedItem = null; - @javax.annotation.Nullable - private CompoundTag lastConsumedSeedNbt = null; + /// 搜索状态委托给独立状态机;旧 getter/setter 在下方保留。 + private final CfaSearchController searchController = new CfaSearchController(); /// 电网 @Setter @Nullable private PowerGrid grid; - /// 引力源状态 - private boolean gravitySourceActive = false; - /// 基础引力影响半径(方块),对应 ringScale=6.0 时覆盖最外层束星环。 - private static final int BASE_GRAVITY_RADIUS = 4; + /// 引力源状态委托给独立控制器。 + private final CfaGravityController gravityController = new CfaGravityController(); public void startSearch() { - this.searchFailed = false; - this.powerInsufficient = false; + if (this.locked || this.isSearching() || this.isAcceleratorActive()) return; + this.searchController.start(this); + } - /// 检查是否有种子物品(用于跳过预检和消耗) - ItemStack seedStack = this.anvilInventory.getItem(4); - boolean hasSeedItem = !seedStack.isEmpty(); + public boolean isSearching() { + return this.searchController.isSearching(); + } - /// 服务端参数预检(有种子物品时跳过) - if (level != null && !level.isClientSide()) { - if (!hasSeedItem) { - var preCheck = CelestialBodyMatcher.match( - getAnvilCount(0), - getAnvilCount(1), - getAnvilCount(2), - getAnvilCount(3), - this.isAmplify, - level.getRandom() - ); - if (preCheck == null) { - this.searchFailed = true; - this.searching = false; - this.searchTicksRemaining = 0; - setChanged(); - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - return; - } - } - } + public int getSearchTicksRemaining() { + return this.searchController.ticksRemaining(); + } - /// 检查电力是否足够 - if (!hasEnoughPower()) { - this.powerInsufficient = true; - this.searching = false; - this.searchTicksRemaining = 0; - setChanged(); - if (level != null && !level.isClientSide()) { - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } - return; - } + public boolean isSearchFailed() { + return this.searchController.hasFailed(); + } - /// 捕获种子物品数据但暂不消耗(匹配成功后消耗) - if (hasSeedItem) { - this.lastConsumedSeedItem = seedStack.getItem(); - this.lastConsumedSeedNbt = extractSnapshot(seedStack); - } else { - this.lastConsumedSeedItem = null; - this.lastConsumedSeedNbt = null; - } + public boolean isPowerInsufficient() { + return this.searchController.isPowerInsufficient(); + } - /// 确认搜索将启动后才清除旧天体 - this.setCelestialBodyData(null); - /// 启动搜索 - this.searchTicksRemaining = SEARCH_TICKS; - this.searching = true; - setChanged(); - if (level != null && !level.isClientSide()) { - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); + /** Marks search state dirty and sends the small block-entity update used by the CFA screen. */ + void markSearchStateChanged() { + this.setChanged(); + if (this.level != null && !this.level.isClientSide()) { + this.level.sendBlockUpdated(this.worldPosition, this.getBlockState(), this.getBlockState(), Block.UPDATE_CLIENTS); } } @@ -563,45 +501,10 @@ public void serverTick() { if (supernovaFlashTicks > 0) { supernovaFlashTicks--; } - /// 持续刷新电力状态——电网恢复时清除过期的 powerInsufficient - boolean hasEnoughPower = hasEnoughPower(); - if (!hasEnoughPower && !this.powerInsufficient) { - this.powerInsufficient = true; - setChanged(); - Objects.requireNonNull(this.level).sendBlockUpdated(this.getBlockPos(), getBlockState(), getBlockState(), 3); - } else if (hasEnoughPower && this.powerInsufficient) { - this.powerInsufficient = false; - setChanged(); - Objects.requireNonNull(this.level).sendBlockUpdated(this.getBlockPos(), getBlockState(), getBlockState(), 3); - } - if (searchTicksRemaining > 0) { - /// 在搜索过程中检查电力是否仍然充足 - if (!hasEnoughPower) { - this.searching = false; - this.searchTicksRemaining = 0; - this.powerInsufficient = true; - setChanged(); - if (level != null) { - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } - } else { - searchTicksRemaining--; - if (searchTicksRemaining == 0) { - this.searching = false; - tryMatchCelestialBody(); - if (celestialBodyData == null) { - this.searchFailed = true; - } - setChanged(); - if (level != null) { - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } - } - } - } + this.searchController.serverTick(this); /// 管理恒星引力源 - updateGravitySource(); + this.updateGravitySource(); /// 巨构建造逻辑(委托给处理类) megastructureManager.serverTick(this); @@ -609,116 +512,32 @@ public void serverTick() { /// Updates the physical source from the same geometry used by rendering and contact damage. private void updateGravitySource() { - if (level == null || level.isClientSide()) return; - - boolean shouldHaveGravity = celestialBodyData != null - && stellarMass > 0 - && celestialBodyData.size() > 0 - && (!(celestialBodyData instanceof StarData) || amplifierPresent); - - if (shouldHaveGravity) { - double massRatio = Math.pow(2, (stellarMass - 12) / 2.0); - double strength = Math.log(1.0 + massRatio) / Math.log(2.0); - int radius = computeGravityRadius(); - double bodyRadius = computeGravityBodyRadius(); - float rawBodyScale = celestialBodyData.bodyScale(); - float bodyRadius0 = rawBodyScale / 2.0f; - if (bodyRadius0 > 1e-6f && bodyRadius > 1e-6) { - double ratio = bodyRadius / bodyRadius0; - strength *= ratio * ratio; - } - - Vec3 center = computeGravityCenter(); - GravityManager.GravitySourceType type = new GravityManager.GravitySourceType(strength, radius, bodyRadius); - GravityManager.GravitySourceManager.upsertSource(level, worldPosition, center, type); - gravitySourceActive = true; - } else if (gravitySourceActive) { - removeGravitySource(); - } + this.gravityController.tick( + this.level, + this.worldPosition, + this.isAmplify, + this.amplifierPresent, + this.celestialBodyData, + this.stellarMass, + this.getRedstoneSignal() + ); } /// 强制移除引力源。当增幅器被拆除时调用,确保引力立即消失。 public void removeGravitySource() { - if (level == null || level.isClientSide()) return; - GravityManager.GravitySourceManager.removeSource(level, worldPosition); - gravitySourceActive = false; + this.gravityController.remove(this.level, this.worldPosition); } public void handleEntityContact(Entity entity) { - if (!gravitySourceActive - || level == null - || level.isClientSide() - || entity.level() != level - || entity.isRemoved() - || isEternal(entity)) { - return; - } - - if (entity instanceof LivingEntity living) { - applyCelestialDamage(living); - } else { - entity.discard(); - } - } - - private static boolean isEternal(Entity entity) { - ItemStack stack = switch (entity) { - case ItemEntity itemEntity -> itemEntity.getItem(); - case ThrownHeavyHalberdEntity heavyHalberd -> heavyHalberd.getWeaponItem(); - default -> ItemStack.EMPTY; - }; - return stack.has(ModComponents.ETERNAL); - } - - /// Applies the corresponding damage when a living entity contacts the celestial body. - private void applyCelestialDamage(LivingEntity living) { - if (celestialBodyData instanceof StarData star) { - if (star.bodyClass() == CelestialBodyClass.BLACK_HOLE) { - living.hurt(ModDamageTypes.lostInTime(Objects.requireNonNull(level)), Float.MAX_VALUE); - } else { - /// 普通恒星 / 中子星使用火焰伤害。 - living.hurt(Objects.requireNonNull(level).damageSources().inFire(), STAR_CONTACT_DAMAGE); - } - } else { - /// 行星 / 特殊天体使用摔落伤害(类似撞上表面)。 - living.hurt(Objects.requireNonNull(level).damageSources().fall(), PLANET_CONTACT_DAMAGE); - } - } - - /// === 引力动态计算(委托给 CelestialBodyData 统一计算,渲染与引力共用) === - - private Vec3 computeGravityCenter() { - float redstoneFactor = getRedstoneSignal() / 15.0f; - float fullCenterY = CelestialBodyData.dynamicCenterY(celestialBodyData, isAmplify); - float baseCenterY = isAmplify ? 6.5f : 4.5f; - float centerY = baseCenterY + (fullCenterY - baseCenterY) * redstoneFactor; - return new Vec3(worldPosition.getX() + 0.5, worldPosition.getY() + centerY, worldPosition.getZ() + 0.5); - } - - /// 计算当前红石信号下引力影响半径(方块数)。 - /// ringScale=6.0 时半径 = BASE_GRAVITY_RADIUS=4,随环缩放线性变化。 - private int computeGravityRadius() { - float redstoneFactor = getRedstoneSignal() / 15.0f; - float fullRingScale = CelestialBodyData.ringSystemScale(celestialBodyData, isAmplify); - float ringScale = CelestialBodyData.BASE_RING_SCALE + (fullRingScale - CelestialBodyData.BASE_RING_SCALE) * redstoneFactor; - return Math.max(1, Math.round(BASE_GRAVITY_RADIUS * ringScale / CelestialBodyData.BASE_RING_SCALE)); - } - - /// 计算当前红石信号下天体视觉半径(方块),用于引力内部递减边界。 - /// 天体渲染为 1×1×1 立方体,缩放 bodyScaleMultiplier 后居中,视觉半径 = bodyScaleMultiplier / 2。 - private double computeGravityBodyRadius() { - if (celestialBodyData == null) return 0; - float redstoneFactor = getRedstoneSignal() / 15.0f; - float rawBodyScale = celestialBodyData.bodyScale(); - float fullBodyScale = rawBodyScale * CelestialBodyData.BODY_SCALE_FACTOR; - float bodyScaleMultiplier = rawBodyScale + (fullBodyScale - rawBodyScale) * redstoneFactor; - return bodyScaleMultiplier / 2.0; + this.gravityController.handleEntityContact(this.level, this.celestialBodyData, entity); } /// 搜索历史,最多 10 条。索引 0 = 最新。 @Getter private final List searchHistory = new ArrayList<>(); - private static final int MAX_HISTORY = 10; + private final CelestialSearchHistory searchHistoryController = new CelestialSearchHistory(this.searchHistory); + private @Nullable CompoundTag cachedDropData; + private boolean permanentRemovalPrepared; /// 一条搜索历史记录,将天体及其生成的资源捆绑在一起。 public record SearchHistoryEntry(CelestialBodyData body, @Nullable PlanetaryResourceSet resources) { @@ -741,12 +560,6 @@ public static SearchHistoryEntry fromTag(CompoundTag tag) { } } - /// 搜索历史的浏览索引:0 = 显示锁定的天体,1+ = 正在浏览。 - @Getter - private int historyBrowseIndex = 0; - @Nullable - private SearchHistoryEntry historyOriginalEntry; - @Getter private final SimpleContainer anvilInventory = new SimpleContainer(5) { @Override @@ -804,202 +617,44 @@ public void setAmplify(boolean amplify) { public void setRemoved() { super.setRemoved(); if (level != null && !level.isClientSide() && !PowerGrid.isServerClosing) { - if (gravitySourceActive) { - GravityManager.GravitySourceManager.removeSource(level, worldPosition); - gravitySourceActive = false; - } - /// 注销虫洞并清除巨构,使已连接的传送门关闭。 - /// 服务器关闭期间跳过,避免保存时访问已保存数据。 - megastructureManager.clearAllMegastructures(this); + this.removeGravitySource(); + this.megastructureManager.unload(this); } } - /// 从 bodySeed 派生出的可复现的 ±5% 随机偏移百分比。仅用于 UI 显示年龄/半径/质量值。index 为 0=时元,1=空间,2=质量。返回在 [-0.05, +0.05] 范围内的偏移值。 - public float getDisplayOffset(int index) { - if (bodySeed == 0) return 0f; - net.minecraft.util.RandomSource rand = net.minecraft.util.RandomSource.create(bodySeed + index * 7919L); - return (rand.nextFloat() - 0.5f) * 0.1f; - } - - @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") - public void tryMatchCelestialBody() { - if (level == null) return; - int time = getAnvilCount(0); - int space = getAnvilCount(1); - int mass = getAnvilCount(2); - int energy = getAnvilCount(3); - this.ageAnvilCount = time; - this.bodySeed = level.getRandom().nextLong(); - this.stellarMass = mass; - - /// 验证种子物品仍在——如果玩家在搜索期间移除了它, - /// 清除捕获的数据,以便回退到普通匹配,而不是在未扣除种子物品的情况下 - /// 授予特殊行星。 - if (lastConsumedSeedItem != null || lastConsumedSeedNbt != null) { - ItemStack seedStack = this.anvilInventory.getItem(4); - if (seedStack.isEmpty()) { - this.lastConsumedSeedItem = null; - this.lastConsumedSeedNbt = null; - } - } - - /// 第一步:检查种子物品快照(磁盘 / 奇点晶体) - if (lastConsumedSeedNbt != null && lastConsumedSeedNbt.contains("celestialBody")) { - applySnapshot(lastConsumedSeedNbt); - consumeSeedItem(); + public void prepareForPermanentRemoval() { + if (this.permanentRemovalPrepared + || this.level == null + || this.level.isClientSide() + || PowerGrid.isServerClosing) { return; } - - /// 第二步:检查玩家头颅种子物品 → 玩家头颅天体 - if (lastConsumedSeedItem == Items.PLAYER_HEAD - - ) { - ItemStack headSeedStack = this.anvilInventory.getItem(4); - CompoundTag profileTag = extractProfileNbt(headSeedStack); - if (profileTag != null) { - this.celestialBodyData = SpecialCelestialBodyData.fromPlayerHead(profileTag, space); - this.planetaryResourceSet = new PlanetaryResourceSet(); - addToSearchHistory(this.celestialBodyData, this.planetaryResourceSet); - consumeSeedItem(); - if (!level.isClientSide()) { - this.setChanged(); - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } - return; - } - } - - /// 第三步:通过种子物品检查特殊天体发现 - if (lastConsumedSeedItem != null) { - SpecialCelestialBodyData specialBody = tryMatchSpecialCelestialBody( - time, - space, - mass, - energy, - lastConsumedSeedItem, - ((ServerLevel) level).getSeed() - ); - if (specialBody != null) { - this.celestialBodyData = specialBody; - if (!level.isClientSide()) { - ResourceLocation recipeId = ResourceLocation.parse(specialBody.recipeId()); - level.getRecipeManager().byKey(recipeId).ifPresent(holder -> { - if (holder.value() instanceof SpecialCelestialBodyRecipe recipe) { - this.planetaryResourceSet = recipe.generateResources(); - } - }); - } - addToSearchHistory(this.celestialBodyData, this.planetaryResourceSet); - consumeSeedItem(); - if (!level.isClientSide()) { - this.setChanged(); - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } - return; - } - } - - /// 回退到普通三步匹配 - this.celestialBodyData = CelestialBodyMatcher.match(time, space, mass, energy, this.isAmplify, level.getRandom()); - if (this.celestialBodyData != null) { - /// 从 bodySeed 派生出 UUID 用于虫洞身份标识 - if (this.celestialBodyData instanceof StarData star && star.bodyUuid() == null) { - this.celestialBodyData = star.withBodyUuid(StarData.uuidFromBodySeed(this.bodySeed)); - } - /// 生成行星资源 - if (!level.isClientSide()) { - ResourceLocation seedItemId = lastConsumedSeedItem != null - ? BuiltInRegistries.ITEM.getKey(lastConsumedSeedItem) - : null; - this.planetaryResourceSet = PlanetResourceGenerator.generate( - this.celestialBodyData, - this.ageAnvilCount, - level, - this.bodySeed, - seedItemId - ); - } - addToSearchHistory(this.celestialBodyData, this.planetaryResourceSet); - } else { - this.planetaryResourceSet = null; - this.searchTicksRemaining = 0; /// 失败时停止计时器 - } - consumeSeedItem(); - - if (!level.isClientSide()) { - this.setChanged(); - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } + this.cachedDropData = this.saveCustomOnly(this.level.registryAccess()); + this.permanentRemovalPrepared = true; + this.removeGravitySource(); + this.megastructureManager.clearAllMegastructures(this); } - /// 从玩家头颅种子物品中提取 profile 数据并序列化为 NBT。 - @Nullable - private static CompoundTag extractProfileNbt(ItemStack stack) { - if (!stack.is(Items.PLAYER_HEAD)) return null; - net.minecraft.world.item.component.ResolvableProfile profile = stack.get( - net.minecraft.core.component.DataComponents.PROFILE - ); - if (profile == null) return null; - return (CompoundTag) net.minecraft.world.item.component.ResolvableProfile.CODEC - .encodeStart(net.minecraft.nbt.NbtOps.INSTANCE, profile) - .getOrThrow(); + @Override + public void clearRemoved() { + super.clearRemoved(); + this.cachedDropData = null; + this.permanentRemovalPrepared = false; } - /// 尝试根据砧子参数和消耗的种子物品来匹配一个特殊(隐藏)天体。种子物品必须是此世界种子的有效物品(使用与 RoyalPreference 相同的模式)。 - private void consumeSeedItem() { - if (level == null || level.isClientSide()) return; - ItemStack seed = this.anvilInventory.getItem(4); - if (!seed.isEmpty()) { - this.anvilInventory.setItem(4, ItemStack.EMPTY); - } + public CompoundTag saveForDrop(HolderLookup.Provider registries) { + return this.cachedDropData == null ? this.saveCustomOnly(registries) : this.cachedDropData.copy(); } - @javax.annotation.Nullable - private SpecialCelestialBodyData tryMatchSpecialCelestialBody( - int time, - int space, - int mass, - int energy, - Item consumedSeedItem, - long worldSeed - ) { - if (level == null) return null; - List recipes = level.getRecipeManager() - .getAllRecipesFor(ModRecipeTypes.SPECIAL_CELESTIAL_BODY_TYPE.get()) - .stream().map(RecipeHolder::value).toList(); - for (SpecialCelestialBodyRecipe recipe : recipes) { - if (recipe.time() == time && recipe.space() == space - && recipe.mass() == mass && recipe.energy() == energy - && recipe.isEffectiveSeedItem(consumedSeedItem, worldSeed) - ) { - /// 查找配方持有者以获取完整 ID - return level.getRecipeManager() - .getAllRecipesFor(ModRecipeTypes.SPECIAL_CELESTIAL_BODY_TYPE.get()) - .stream() - .filter(h -> h.value() == recipe) - .findFirst() - .map(h -> SpecialCelestialBodyData.fromRecipe(recipe, h.id().toString())) - .orElse(null); - } - } - return null; + /// 从 bodySeed 派生出的可复现的 ±5% 随机偏移百分比。仅用于 UI 显示年龄/半径/质量值。index 为 0=时元,1=空间,2=质量。返回在 [-0.05, +0.05] 范围内的偏移值。 + public float getDisplayOffset(int index) { + if (bodySeed == 0) return 0f; + net.minecraft.util.RandomSource rand = net.minecraft.util.RandomSource.create(bodySeed + index * 7919L); + return (rand.nextFloat() - 0.5f) * 0.1f; } - /// 从快照(磁盘 / 奇点晶体种子物品)加载天体。快照包含所有参数——砧子数量在匹配时被忽略。 - private void applySnapshot(CompoundTag tag) { - if (level == null) return; - this.celestialBodyData = CelestialBodyData.fromTag(tag.getCompound("celestialBody")); - this.bodySeed = tag.getLong("bodySeed"); - this.ageAnvilCount = tag.getInt("ageAnvilCount"); - this.stellarMass = tag.getInt("stellarMass"); - if (tag.contains("planetaryResources")) { - this.planetaryResourceSet = PlanetaryResourceSet.fromTag(tag.getCompound("planetaryResources")); - } - addToSearchHistory(this.celestialBodyData, this.planetaryResourceSet); - if (!level.isClientSide()) { - this.setChanged(); - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - } + public void tryMatchCelestialBody() { + this.searchController.match(this); } /// === 磁盘克隆接口 === @@ -1062,57 +717,18 @@ public InteractionResult useDisk(Level level, Player player, InteractionHand han /// 从种子物品堆中提取天体快照。 @javax.annotation.Nullable public static CompoundTag extractSnapshot(ItemStack stack) { - if (stack.getItem() instanceof DiskItem && DiskItem.hasDataStored(stack)) { - return DiskItem.getData(stack).copy(); - } - return loadSnapshotFromStack(stack); + return dev.dubhe.anvilcraft.block.entity.celestial.CelestialSnapshotCodec.extract(stack); } /// 从磁盘或奇点晶体中加载天体快照。 @javax.annotation.Nullable public static CompoundTag loadSnapshotFromStack(ItemStack stack) { - /// 磁盘 - if (stack.getItem() instanceof DiskItem && DiskItem.hasDataStored(stack)) { - CompoundTag data = DiskItem.getData(stack); - if (data.contains("celestialBody")) return data.copy(); - } - /// 奇点晶体 - if (stack.is(ModBlocks.SINGULARITY_CRYSTAL.asItem())) { - var customData = stack.getOrDefault( - net.minecraft.core.component.DataComponents.CUSTOM_DATA, - net.minecraft.world.item.component.CustomData.EMPTY - ); - CompoundTag tag = customData.copyTag(); - if (!tag.isEmpty() && tag.contains("celestialSnapshot")) { - CompoundTag snapshot = tag.getCompound("celestialSnapshot"); - if (snapshot.contains("celestialBody")) return snapshot.copy(); - } - } - return null; + return dev.dubhe.anvilcraft.block.entity.celestial.CelestialSnapshotCodec.load(stack); } /// 将快照保存到磁盘或奇点晶体中。 public static void saveSnapshotToStack(ItemStack stack, CompoundTag snapshot) { - if (stack.getItem() instanceof DiskItem) { - /// 极端天体(黑洞 / 中子星)不能存储在磁盘上 - if (snapshot.contains("celestialBody")) { - CompoundTag bodyTag = snapshot.getCompound("celestialBody"); - String bodyClass = bodyTag.getString("bodyClass"); - if ("BLACK_HOLE".equals(bodyClass) || "NEUTRON_STAR".equals(bodyClass)) { - return; /// 静默拒绝——极端天体需要奇点晶体 - } - } - CompoundTag diskTag = DiskItem.createData(stack); - diskTag.merge(snapshot); - } else if (stack.is(ModBlocks.SINGULARITY_CRYSTAL.asItem())) { - var oldCustom = stack.getOrDefault( - net.minecraft.core.component.DataComponents.CUSTOM_DATA, - net.minecraft.world.item.component.CustomData.EMPTY - ); - CompoundTag updated = oldCustom.copyTag(); - updated.put("celestialSnapshot", snapshot.copy()); - stack.set(net.minecraft.core.component.DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.of(updated)); - } + dev.dubhe.anvilcraft.block.entity.celestial.CelestialSnapshotCodec.save(stack, snapshot); } /// === CFA 方块交互 === @@ -1126,8 +742,7 @@ public void onLoad() { /// 如果虫洞稳定器处于活动状态,则重新注册到虫洞网络 /// 委托给 handler 的 onBuild 处理重新注册 WormholeStabilizerHandler wh = megastructureManager.getWormholeHandler(); - if (megastructureManager.getActiveIndex() >= 0 && getActiveMegastructureOption() != null && "wormhole_stabilizer".equals( - getActiveMegastructureOption().megastructure())) { + if (ModMegastructures.WORMHOLE_STABILIZER.getId().equals(megastructureManager.getActiveId(this))) { wh.onBuild(this); } this.setChanged(); @@ -1145,54 +760,27 @@ public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { tag.putBoolean("locked", this.locked); tag.putBoolean("amplifierPresent", this.amplifierPresent); - tag.putBoolean("searching", this.searching); - tag.putInt("searchTicks", this.searchTicksRemaining); - tag.putBoolean("searchFailed", this.searchFailed); - tag.putBoolean("powerInsufficient", this.powerInsufficient); + tag.putBoolean("searching", this.isSearching()); + tag.putInt("searchTicks", this.getSearchTicksRemaining()); + tag.putBoolean("searchFailed", this.isSearchFailed()); + tag.putBoolean("powerInsufficient", this.isPowerInsufficient()); + this.searchController.save(tag, registries); if (celestialBodyData != null) { tag.put("celestialBody", celestialBodyData.toTag()); } /// 搜索历史 - CompoundTag histTag = new CompoundTag(); - histTag.putInt("size", Math.min(searchHistory.size(), MAX_HISTORY)); - for (int i = 0; i < Math.min(searchHistory.size(), MAX_HISTORY); i++) { - histTag.put("h" + i, searchHistory.get(i).toTag()); - } - tag.put("searchHistory", histTag); + tag.put("searchHistory", this.searchHistoryController.toTag()); /// 砧子物品栏 - CompoundTag invTag = new CompoundTag(); - for (int i = 0; i < 5; i++) { - ItemStack stack = this.anvilInventory.getItem(i); - if (!stack.isEmpty()) { - invTag.put("s" + i, stack.save(registries)); - } - } - tag.put("anvils", invTag); + tag.put("anvils", CfaInventoryCodec.save(this.anvilInventory, registries)); /// 材料槽 if (!materialFilter.isEmpty()) { tag.put("materialFilter", materialFilter.save(registries)); } - if (!materialFilter.isEmpty()) { - tag.put("materialFilter", materialFilter.save(registries)); - } tag.putInt("materialLimit", materialLimit); tag.putInt("ageAnvilCount", this.ageAnvilCount); if (planetaryResourceSet != null) { tag.put("planetaryResources", planetaryResourceSet.toTag()); } - /// 虫洞稳定器状态(由 WormholeStabilizerHandler NBT 处理) - if (!portals.isEmpty()) { - CompoundTag portalTag = new CompoundTag(); - for (Map.Entry entry : portals.entrySet()) { - BlockPos p = entry.getValue(); - CompoundTag posTag = new CompoundTag(); - posTag.putInt("x", p.getX()); - posTag.putInt("y", p.getY()); - posTag.putInt("z", p.getZ()); - portalTag.put(entry.getKey().getSerializedName(), posTag); - } - tag.put("portals", portalTag); - } /// 神殿状态 tag.putInt("templeCycleDay", templeCycleDay); tag.putLong("templeLastDay", templeLastDay); @@ -1202,7 +790,7 @@ public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { tag.putInt("templeDemandCount", templeDemandCount); tag.putInt("templeDemandProgress", templeDemandProgress); tag.putBoolean("templeDemandSatisfied", templeDemandSatisfied); - tag.putInt("historyBrowseIndex", historyBrowseIndex); + tag.putInt("historyBrowseIndex", this.searchHistoryController.browseIndex()); /// 将巨构建造 NBT 委托给管理器 megastructureManager.saveAdditional(tag, registries); } @@ -1217,15 +805,14 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) this.stellarMass = tag.getInt("stellarMass"); this.locked = tag.getBoolean("locked"); this.amplifierPresent = tag.getBoolean("amplifierPresent"); - this.searching = tag.getBoolean("searching"); - this.searchTicksRemaining = tag.getInt("searchTicks"); - this.searchFailed = tag.getBoolean("searchFailed"); - this.powerInsufficient = tag.getBoolean("powerInsufficient"); - /// 如果 searching 为 true 但没有保存计时器(旧数据或新放置的), - /// 重置标志以防止搜索状态卡住 - if (this.searching && this.searchTicksRemaining <= 0) { - this.searching = false; - } + this.searchController.loadPersistent( + tag.getBoolean("searching"), + tag.getInt("searchTicks"), + tag.getBoolean("searchFailed"), + tag.getBoolean("powerInsufficient"), + tag, + registries + ); this.bodySeed = tag.getLong("bodySeed"); /// 捕获旧天体数据用于动画过渡检测 CelestialBodyData oldBodyData = this.celestialBodyData; @@ -1254,8 +841,17 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) this.animationPreviousBodyData = oldBodyData; } } - loadSearchHistory(tag); - loadInventory(tag, registries); + if (tag.contains("searchHistory")) { + this.searchHistoryController.load(tag.getCompound("searchHistory")); + } else { + this.searchHistoryController.clear(); + } + this.searchHistoryController.setBrowseIndex(tag.getInt("historyBrowseIndex")); + if (tag.contains("anvils")) { + CfaInventoryCodec.load(tag.getCompound("anvils"), this.anvilInventory, registries); + } else { + CfaInventoryCodec.load(new CompoundTag(), this.anvilInventory, registries); + } /// 材料过滤器 if (tag.contains("materialFilter")) { this.materialFilter = ItemStack.parse(registries, tag.getCompound("materialFilter")) @@ -1270,17 +866,6 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) } else { this.planetaryResourceSet = null; } - /// 虫洞稳定器状态(由 WormholeStabilizerHandler NBT 处理) - this.portals.clear(); - if (tag.contains("portals")) { - CompoundTag portalTag = tag.getCompound("portals"); - for (String key : portalTag.getAllKeys()) { - CompoundTag posTag = portalTag.getCompound(key); - Cube323PartHalf side = Cube323PartHalf.valueOf(key.toUpperCase()); - BlockPos pos = new BlockPos(posTag.getInt("x"), posTag.getInt("y"), posTag.getInt("z")); - portals.put(side, pos); - } - } /// 神殿状态 this.templeCycleDay = tag.getInt("templeCycleDay"); this.templeLastDay = tag.contains("templeLastDay") ? tag.getLong("templeLastDay") : -1; @@ -1293,7 +878,6 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) this.templeDemandProgress = tag.getInt("templeDemandProgress"); this.templeDemandSatisfied = tag.getBoolean("templeDemandSatisfied"); /// 对撞机运行时状态不持久化——加载时始终从干净状态开始 - this.historyBrowseIndex = tag.getInt("historyBrowseIndex"); /// 超新星闪光(客户端渲染)——运行时同步走 loadAdditional(onDataPacket→loadWithComponents), /// 故必须在此读取;仅在收到更大 ticks 时重启,避免覆盖客户端流畅递减。 if (tag.contains("supernovaFlashTicks")) { @@ -1306,6 +890,8 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) } /// 将巨构建造 NBT 委托给管理器(必须放在最后,以便管理器覆盖 BE 字段) megastructureManager.loadAdditional(tag, registries); + // Resolve legacy name/index data once the celestial context and handlers are loaded. + megastructureManager.getActiveId(this); if (level != null && !level.isClientSide()) { level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); } @@ -1322,20 +908,17 @@ public CompoundTag getUpdateTag(HolderLookup.Provider registries) { tag.putBoolean("locked", this.locked); tag.putBoolean("amplifierPresent", this.amplifierPresent); - tag.putBoolean("searching", this.searching); - tag.putInt("searchTicks", this.searchTicksRemaining); - tag.putBoolean("searchFailed", this.searchFailed); - tag.putBoolean("powerInsufficient", this.powerInsufficient); + tag.putBoolean("searching", this.isSearching()); + tag.putInt("searchTicks", this.getSearchTicksRemaining()); + tag.putBoolean("searchFailed", this.isSearchFailed()); + tag.putBoolean("powerInsufficient", this.isPowerInsufficient()); if (celestialBodyData != null) { tag.put("celestialBody", celestialBodyData.toTag()); } /// 搜索历史 - CompoundTag histTag = new CompoundTag(); - histTag.putInt("size", Math.min(searchHistory.size(), MAX_HISTORY)); - for (int i = 0; i < Math.min(searchHistory.size(), MAX_HISTORY); i++) { - histTag.put("h" + i, searchHistory.get(i).toTag()); - } - tag.put("searchHistory", histTag); + tag.put("searchHistory", this.searchHistoryController.toTag()); + /// 同步全部 5 个槽位,供客户端界面显示。 + tag.put("anvils", CfaInventoryCodec.save(this.anvilInventory, registries)); /// 材料过滤器同步 if (!materialFilter.isEmpty()) { tag.put("materialFilter", materialFilter.save(registries)); @@ -1355,7 +938,7 @@ public CompoundTag getUpdateTag(HolderLookup.Provider registries) { tag.putInt("templeDemandProgress", templeDemandProgress); tag.putBoolean("templeDemandSatisfied", templeDemandSatisfied); /// 对撞机运行时状态不同步到客户端 - tag.putInt("historyBrowseIndex", historyBrowseIndex); + tag.putInt("historyBrowseIndex", this.searchHistoryController.browseIndex()); /// 超新星闪光(客户端渲染) tag.putInt("supernovaFlashTicks", supernovaFlashTicks); tag.putDouble("supernovaCenterY", supernovaCenterY); @@ -1375,10 +958,12 @@ public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider lookupProvide this.stellarMass = tag.getInt("stellarMass"); this.locked = tag.getBoolean("locked"); this.amplifierPresent = tag.getBoolean("amplifierPresent"); - this.searching = tag.getBoolean("searching"); - this.searchTicksRemaining = tag.getInt("searchTicks"); - this.searchFailed = tag.getBoolean("searchFailed"); - this.powerInsufficient = tag.getBoolean("powerInsufficient"); + this.searchController.loadSynced( + tag.getBoolean("searching"), + tag.getInt("searchTicks"), + tag.getBoolean("searchFailed"), + tag.getBoolean("powerInsufficient") + ); this.bodySeed = tag.getLong("bodySeed"); /// 捕获旧天体数据用于动画过渡检测 @@ -1411,8 +996,17 @@ public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider lookupProvide this.animationPreviousBodyData = oldBodyData; } } - loadSearchHistory(tag); - loadInventory(tag, lookupProvider); + if (tag.contains("searchHistory")) { + this.searchHistoryController.load(tag.getCompound("searchHistory")); + } else { + this.searchHistoryController.clear(); + } + this.searchHistoryController.setBrowseIndex(tag.getInt("historyBrowseIndex")); + if (tag.contains("anvils")) { + CfaInventoryCodec.load(tag.getCompound("anvils"), this.anvilInventory, lookupProvider); + } else { + CfaInventoryCodec.load(new CompoundTag(), this.anvilInventory, lookupProvider); + } /// 材料过滤器(客户端——从同步中读取) if (tag.contains("materialFilter")) { this.materialFilter = ItemStack.parse(lookupProvider, tag.getCompound("materialFilter")) @@ -1439,7 +1033,6 @@ public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider lookupProvide this.templeDemandProgress = tag.getInt("templeDemandProgress"); this.templeDemandSatisfied = tag.getBoolean("templeDemandSatisfied"); /// 对撞机运行时状态不同步到客户端 - this.historyBrowseIndex = tag.getInt("historyBrowseIndex"); /// 超新星闪光(客户端渲染)——仅在收到更大值时重启,避免覆盖客户端流畅递减 int incomingFlash = tag.getInt("supernovaFlashTicks"); if (incomingFlash > this.supernovaFlashTicks) { @@ -1451,120 +1044,51 @@ public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider lookupProvide megastructureManager.readUpdateTag(tag, lookupProvider); } - private void loadInventory(CompoundTag tag, HolderLookup.Provider registries) { - if (tag.contains("anvils")) { - CompoundTag invTag = tag.getCompound("anvils"); - for (int i = 0; i < 5; i++) { - String key = "s" + i; - this.anvilInventory.setItem( - i, - invTag.contains(key) - ? ItemStack.parse(registries, Objects.requireNonNull(invTag.get(key))).orElse(ItemStack.EMPTY) - : ItemStack.EMPTY - ); - } - } - } - public int getAnvilCount(int slot) { return this.anvilInventory.getItem(slot).getCount(); } public void addToSearchHistory(CelestialBodyData data, @Nullable PlanetaryResourceSet resources) { - /// 去重:如果已是最新条目则不添加 - if (!searchHistory.isEmpty()) { - SearchHistoryEntry latest = searchHistory.getFirst(); - if (latest.body().toTag().toString().equals(data.toTag().toString())) return; - } - searchHistory.addFirst(new SearchHistoryEntry(data, resources)); - while (searchHistory.size() > MAX_HISTORY) { - searchHistory.removeLast(); - } + this.searchHistoryController.add(data, resources); } - private void loadSearchHistory(CompoundTag tag) { - searchHistory.clear(); - if (tag.contains("searchHistory")) { - CompoundTag histTag = tag.getCompound("searchHistory"); - int size = Math.min(histTag.getInt("size"), MAX_HISTORY); - for (int i = 0; i < size; i++) { - if (histTag.contains("h" + i)) { - CompoundTag entryTag = histTag.getCompound("h" + i); - if (entryTag.contains("body")) { - /// 新格式:SearchHistoryEntry - searchHistory.add(SearchHistoryEntry.fromTag(entryTag)); - } else { - /// 旧格式:裸 CelestialBodyData(未保存资源) - CelestialBodyData body = CelestialBodyData.fromTag(entryTag); - searchHistory.add(new SearchHistoryEntry(body, null)); - } - } - } - } + public void clearSearchHistory() { + this.searchHistoryController.clear(); } /// === 搜索历史浏览(服务端)=== public boolean hasPreviousHistory() { - int sz = searchHistory.size(); - return sz > 1 && historyBrowseIndex < sz; + return this.searchHistoryController.hasPrevious(); } public boolean hasNextHistory() { - return historyBrowseIndex > 0; + return this.searchHistoryController.hasNext(); } public void browseHistoryPrev() { if (level == null || level.isClientSide()) return; - int sz = searchHistory.size(); - /// 至少需要 2 条记录:索引 0 是当前锁定的天体 - if (sz <= 1 || historyBrowseIndex >= sz) return; - if (historyBrowseIndex == 0) { - historyOriginalEntry = new SearchHistoryEntry(Objects.requireNonNull(celestialBodyData), planetaryResourceSet); - historyBrowseIndex = 1; /// 跳过当前天体条目 - } - historyBrowseIndex++; - if (historyBrowseIndex > sz) return; - applyHistoryEntry(); + if (this.locked || this.isSearching() || this.isAcceleratorActive()) return; + SearchHistoryEntry entry = this.searchHistoryController.previous(celestialBodyData, planetaryResourceSet); + if (entry != null) this.applyHistoryEntry(entry); } public void browseHistoryNext() { if (level == null || level.isClientSide()) return; - if (historyBrowseIndex <= 0) return; - historyBrowseIndex--; - if (historyBrowseIndex == 0) { - if (historyOriginalEntry != null) { - celestialBodyData = historyOriginalEntry.body(); - planetaryResourceSet = historyOriginalEntry.resources(); - historyOriginalEntry = null; - } - setChanged(); - syncToClient(); - } else { - applyHistoryEntry(); - } + if (this.locked || this.isSearching() || this.isAcceleratorActive()) return; + SearchHistoryEntry entry = this.searchHistoryController.next(); + if (entry != null) this.applyHistoryEntry(entry); } - private void applyHistoryEntry() { - if (historyBrowseIndex > 0 && historyBrowseIndex <= searchHistory.size()) { - SearchHistoryEntry entry = searchHistory.get(historyBrowseIndex - 1); - celestialBodyData = entry.body(); - planetaryResourceSet = entry.resources(); - } + private void applyHistoryEntry(SearchHistoryEntry entry) { + this.celestialBodyData = entry.body(); + this.planetaryResourceSet = entry.resources(); setChanged(); syncToClient(); } private void syncToClient() { - if (level instanceof ServerLevel serverLevel) { - Packet packet = getUpdatePacket(); - for (ServerPlayer serverPlayer : serverLevel.getChunkSource().chunkMap.getPlayers( - serverLevel.getChunkAt(worldPosition) - .getPos(), false - )) { - serverPlayer.connection.send(packet); - } - } + CfaBlockEntitySync.sendToTracking(this, this.getUpdatePacket()); } @Override @@ -1601,14 +1125,14 @@ public void toggleLocked() { if (!this.locked) { /// 解锁:清除巨构和加速器,恢复为束星环 clearMegastructure(); - clearAcceleratorState(); + clearAuxiliaryMegastructures(); } this.setChanged(); level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); } - private void clearAcceleratorState() { - megastructureManager.getAcceleratorHandler().onClear(this); + private void clearAuxiliaryMegastructures() { + megastructureManager.clearAuxiliaryMegastructures(this); } /// 清除活动巨构及所有相关状态,恢复为束星环。 @@ -1621,15 +1145,15 @@ public void clearMegastructure() { PowerGrid.addComponent(this); } - /// 获取与客户端看到的匹配的选项列表(应用相同的过滤)。当巨构已建造时,仅加速器可见。 + /// 获取与客户端看到的匹配的选项列表(应用相同的过滤)。当主巨构已建造时,仅辅助巨构可见。 public List getClientVisibleOptions() { List options = CelestialRefactorRegistry.getOptions( celestialBodyData, isAmplify, this.planetaryResourceSet ); - if (megastructureManager.getActiveIndex() >= 0) { - options = options.stream().filter(opt -> "stellar_evolution_accelerator".equals(opt.megastructure())).toList(); + if (megastructureManager.hasActiveMegastructure()) { + options = options.stream().filter(CelestialRefactorOption::auxiliary).toList(); } return options; } @@ -1640,6 +1164,15 @@ public CelestialRefactorOption getActiveMegastructureOption() { return megastructureManager.getActiveOption(this); } + public boolean hasActiveAuxiliaryMegastructure() { + return this.megastructureManager.hasActiveAuxiliary(this); + } + + @Nullable + public CelestialRefactorOption getActiveAuxiliaryMegastructureOption(int ring) { + return this.megastructureManager.getActiveAuxiliaryOptionForRing(this, ring); + } + /// 获取放置在此 CFA 各侧的传送门(不可修改)。 public Map getPortals() { WormholeStabilizerHandler wh = megastructureManager.getWormholeHandler(); @@ -1649,17 +1182,23 @@ public Map getPortals() { /// 尝试建造巨构。玩家点击"开始重构"时在服务器端调用。optionIndex 为选中的重构选项索引。 public void buildMegastructure(int optionIndex) { if (level == null || level.isClientSide()) return; + if (!this.locked) return; + if (this.isSearching() || this.isAcceleratorActive()) return; if (celestialBodyData == null) return; List options = getClientVisibleOptions(); if (optionIndex < 0 || optionIndex >= options.size()) return; CelestialRefactorOption option = options.get(optionIndex); + // Validate the server-side option and handler before consuming any material. + if (!megastructureManager.canBuild(option, this)) return; + /// 先检查材料 if (option.needsMaterial()) { ItemStack contained = materialContainer.getItem(0); ItemStack required = option.material().copyWithCount(option.materialCount()); - if (!ItemStack.isSameItem(contained, required) || contained.getCount() < required.getCount()) { + if (!ItemStack.isSameItem(contained, required) + || contained.getCount() < required.getCount()) { return; } contained.shrink(required.getCount()); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilFluidInterfaceBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilFluidInterfaceBlockEntity.java index 637dff0f4f..4b8171915d 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilFluidInterfaceBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilFluidInterfaceBlockEntity.java @@ -17,12 +17,11 @@ import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.fluids.FluidStack; import net.neoforged.neoforge.fluids.capability.IFluidHandler; @@ -45,6 +44,7 @@ public class CelestialForgingAnvilFluidInterfaceBlockEntity extends BlockEntity @Setter @Nullable private PowerGrid grid; + private boolean suppressFluidSync; public CelestialForgingAnvilFluidInterfaceBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { super(type, pos, blockState); @@ -92,26 +92,36 @@ public void setRemoved() { /// 将方块实体数据同步到所有追踪的客户端。 public void syncToClients() { - if (level instanceof ServerLevel serverLevel) { - Packet packet = getUpdatePacket(); - if (packet != null) { - for (ServerPlayer player : serverLevel.getChunkSource().chunkMap - .getPlayers(serverLevel.getChunkAt(worldPosition).getPos(), false)) { - player.connection.send(packet); - } - } - } + CfaBlockEntitySync.sendToTracking(this, this.getUpdatePacket()); } @Override public void setChanged() { super.setChanged(); + if (suppressFluidSync) return; if (level != null && !level.isClientSide()) { level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); syncToClients(); } } + /** Drains all tanks containing the requested fluid and sends one combined update. */ + public int drainFluid(Fluid fluid) { + int drainedAmount = 0; + this.suppressFluidSync = true; + try { + for (FluidTank tank : tanks) { + FluidStack stored = tank.getFluid(); + if (stored.isEmpty() || !stored.is(fluid)) continue; + drainedAmount += tank.drain(stored.getAmount(), IFluidHandler.FluidAction.EXECUTE).getAmount(); + } + } finally { + this.suppressFluidSync = false; + } + if (drainedAmount > 0) this.setChanged(); + return drainedAmount; + } + @Override public @Nullable Packet getUpdatePacket() { return ClientboundBlockEntityDataPacket.create(this); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLaserInterfaceBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLaserInterfaceBlockEntity.java index 554591631b..5d2581e89f 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLaserInterfaceBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLaserInterfaceBlockEntity.java @@ -46,11 +46,14 @@ public class CelestialForgingAnvilLaserInterfaceBlockEntity extends BaseLaserBlo private boolean requiredGamma = false; /// 伽马激光状态(由 CFA 控制器设置,用于彭罗斯球输出) - @Getter private boolean emittingGamma = false; @Getter private int gammaLevel = 0; + /** A Penrose handler requests a beam for the next server tick. */ + private boolean gammaEmissionRequested = false; + private int gammaEmissionRequestLevel = 0; + /// 虫洞激光输出(由 CFA 控制器的 syncWormholeLasers 每 tick 设置) private int wormholeOutputLevel = 0; private boolean wormholeOutputGamma = false; @@ -98,10 +101,15 @@ public boolean isActive() { /// 设置虫洞激光输出等级和伽马标志。由 CFA 控制器的 syncWormholeLasers() 每 tick 调用。 public void setWormholeLaserOutput(int level, boolean gamma) { - this.wormholeOutputLevel = level; + this.wormholeOutputLevel = Math.max(0, level); this.wormholeOutputGamma = gamma; } + @Override + public boolean isEmittingGamma() { + return this.emittingGamma; + } + @Override public float getLaserOffset() { return 0.125f; @@ -111,8 +119,7 @@ public float getLaserOffset() { @Override public void onIrradiated(BaseLaserBlockEntity source) { int level = source.getLaserLevel(); - boolean gamma = source instanceof CelestialForgingAnvilLaserInterfaceBlockEntity cfaSource - && cfaSource.isEmittingGamma(); + boolean gamma = source.isEmittingGamma(); onLaserReceived(level, gamma, source.getMiningEffect()); /// 不进行链式传递——不调用 super.onIrradiated(source) } @@ -135,8 +142,12 @@ public Set getIgnoreFace() { /// === CFA 激光跟踪 === /// 设置此接口的激光需求,由 CFA 控制器调用。当 requiredLevel > 0 时,传入的激光将根据此需求进行验证。requiredLevel 为所需的最小激光等级,传 0 则清除需求;gamma 表示是否需要伽马激光。 + @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") public void setLaserRequirement(int requiredLevel, boolean gamma) { - this.requiredLaserLevel = requiredLevel; + boolean oldValid = this.laserValid; + int oldRequiredLevel = this.requiredLaserLevel; + boolean oldRequiredGamma = this.requiredGamma; + this.requiredLaserLevel = Math.max(0, requiredLevel); this.requiredGamma = gamma; /// 使用新需求重新评估有效性 if (requiredLaserLevel > 0 && receivedLaserLevel > 0) { @@ -145,24 +156,36 @@ public void setLaserRequirement(int requiredLevel, boolean gamma) { } else { this.laserValid = false; } - this.setChanged(); - } - - public void onLaserReceived(int level, boolean gamma) { - onLaserReceived(level, gamma, BlockMiningEffect.NORMAL); + if (oldRequiredLevel != this.requiredLaserLevel + || oldRequiredGamma != this.requiredGamma + || oldValid != this.laserValid) { + this.setChanged(); + } } + @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") public void onLaserReceived(int level, boolean gamma, BlockMiningEffect miningEffect) { + boolean oldValid = this.laserValid; + boolean changed = this.receivedLaserLevel != level + || this.receivedGamma != gamma + || !this.receivedMiningEffect.equals(miningEffect); this.receivedLaserLevel = level; this.receivedGamma = gamma; this.receivedMiningEffect = miningEffect; - this.laserValid = (requiredLaserLevel > 0 + boolean newValid = (requiredLaserLevel > 0 && level >= requiredLaserLevel && gamma == requiredGamma); - this.setChanged(); + this.laserValid = newValid; + if (changed || oldValid != newValid) this.setChanged(); } public void resetLaser() { + if (this.receivedLaserLevel == 0 + && !this.receivedGamma + && this.receivedMiningEffect.equals(BlockMiningEffect.NORMAL) + && !this.laserValid) { + return; + } this.receivedLaserLevel = 0; this.receivedGamma = false; this.receivedMiningEffect = BlockMiningEffect.NORMAL; @@ -174,9 +197,8 @@ public void resetLaser() { /// 由 CFA 控制器调用,使此接口发射伽马激光。 public void emitGammaLaser(int level) { - this.emittingGamma = true; - this.gammaLevel = level; - this.updateLaserLevel(level); + this.gammaEmissionRequestLevel = Math.max(0, level); + this.gammaEmissionRequested = this.gammaEmissionRequestLevel > 0; } @Override @@ -198,6 +220,7 @@ public void serverTick() { /// 如果正在接收传入激光,仅接收——绝不发射, /// 无论主动/被动模式或伽马状态如何。 if (receivedLaserLevel > 0) { + setGammaOutputState(false, 0); /// 被动模式:清除发射,因为我们正在接收 if (irradiateBlockPos != null) { BlockEntity oldBe = level.getBlockEntity(irradiateBlockPos); @@ -208,23 +231,20 @@ public void serverTick() { } clearIrradiateSelfLaserBlockSet(); updateLaserLevel(0); /// 为 HUD 清除过期的发射等级 - } else if (emittingGamma && gammaLevel > 0) { + } else if (gammaEmissionRequested && gammaEmissionRequestLevel > 0) { /// 发射伽马激光(彭罗斯球输出) + setGammaOutputState(true, gammaEmissionRequestLevel); Direction facing = getFacing(); emitGammaLaserBeam(facing); - /// 暂时不重置 emittingGamma——tickWithGamma 需要它来发送数据包 } else if (wormholeOutputGamma && wormholeOutputLevel > 0 && active) { /// 通过虫洞发射伽马激光(来自网络中被动接口的汇总)。借用 gammaLevel 用于发射,但之后恢复它以保留彭罗斯球状态。 - /// emittingGamma 保持为 true 以便 tickWithGamma 发送伽马数据包;它将在 serverTick() 末尾的清理中被重置。 - int savedGammaLevel = this.gammaLevel; - this.gammaLevel = wormholeOutputLevel; - this.emittingGamma = true; + setGammaOutputState(true, wormholeOutputLevel); Direction facing = getFacing(); emitGammaLaserBeam(facing); - this.gammaLevel = savedGammaLevel; } else if (active && getBaseLaserLevel() > 0) { /// 主动模式且有实际输出(虫洞普通激光)时才发射; /// 主动模式自身不再自发发射 1 级激光(仅切换模型)。 + setGammaOutputState(false, 0); Direction facing = getFacing(); /// 仅当尚未属于激光链时才发射 if (irradiateSelfLaserBlockSet.isEmpty()) { @@ -232,6 +252,7 @@ public void serverTick() { } } else { /// 被动模式或主动但无输出:清除激光发射 + setGammaOutputState(false, 0); if (irradiateBlockPos != null) { BlockEntity oldBe = level.getBlockEntity(irradiateBlockPos); if (oldBe instanceof BaseLaserBlockEntity lastIrradiated) { @@ -245,11 +266,8 @@ public void serverTick() { /// 自定义 tick,发送含伽马信息的数据包 tickWithGamma(level); - - /// 数据包发送后重置伽马发射状态 - if (emittingGamma) { - emittingGamma = false; - } + this.gammaEmissionRequested = false; + this.gammaEmissionRequestLevel = 0; /// 如果正在照射可加热方块,注册为热量生产者。BaseLaserBlockEntity.tick() 通常会处理此操作,但我们覆写了 tick() 且仅在客户端委托给 super,因此必须在服务器端手动处理。 if (level instanceof ServerLevel serverLevel @@ -279,9 +297,21 @@ private void tickWithGamma(Level level) { ); } } + // The CFA interface owns the server-side tick, so BaseLaserBlockEntity.tick() + // cannot clear this edge-triggered flag for us. + resetState(); this.tickCount++; } + private void setGammaOutputState(boolean emitting, int level) { + int normalizedLevel = emitting ? Math.max(0, level) : 0; + if (this.emittingGamma != emitting || this.gammaLevel != normalizedLevel) { + this.markChanged(); + } + this.emittingGamma = emitting; + this.gammaLevel = normalizedLevel; + } + /// 普通激光渲染的客户端更新。始终调用 super 以确保 irradiatePos=null 能正确清除渲染管线(例如移除红石信号时)。 @Override public void clientUpdate(@Nullable BlockPos irradiateBlockPos, int laserLevel) { @@ -366,22 +396,13 @@ private static BlockMiningEffect readMiningEffect(CompoundTag tag) { /// 将方块实体数据同步到所有追踪的客户端。 public void syncToClients() { - if (level instanceof ServerLevel serverLevel) { - Packet packet = getUpdatePacket(); - if (packet != null) { - for (ServerPlayer player : serverLevel.getChunkSource().chunkMap - .getPlayers(serverLevel.getChunkAt(worldPosition).getPos(), false)) { - player.connection.send(packet); - } - } - } + CfaBlockEntitySync.sendToTracking(this, this.getUpdatePacket()); } @Override public void setChanged() { super.setChanged(); if (level != null && !level.isClientSide()) { - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); syncToClients(); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLogisticsInterfaceBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLogisticsInterfaceBlockEntity.java index 8745a96e41..7a135ac97b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLogisticsInterfaceBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilLogisticsInterfaceBlockEntity.java @@ -1,9 +1,13 @@ package dev.dubhe.anvilcraft.block.entity; +import dev.anvilcraft.lib.v2.rpc.CallableParam; +import dev.anvilcraft.lib.v2.rpc.IRemoteCallableValidator; +import dev.anvilcraft.lib.v2.rpc.RemoteCallable; import dev.dubhe.anvilcraft.api.itemhandler.FilteredItemStackHandler; import dev.dubhe.anvilcraft.block.cfa.CelestialForgingAnvilBlock; import dev.dubhe.anvilcraft.block.cfa.interfaces.CelestialForgingAnvilInterfaceBlock; import dev.dubhe.anvilcraft.block.state.Cube323PartHalf; +import io.netty.buffer.ByteBuf; import lombok.Getter; import lombok.Setter; import net.minecraft.core.BlockPos; @@ -12,12 +16,14 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.ClientGamePacketListener; -import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; -import net.minecraft.server.level.ServerLevel; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.PacketFlow; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; @@ -27,16 +33,32 @@ import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.ItemHandlerHelper; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.IntStream; import javax.annotation.Nullable; /// 锻星砧的物流接口。最多存储 16 种不同的物品类型,每种类型一个堆叠。物品自动路由到对应类型的槽位,不会溢出到其他槽位。 public class CelestialForgingAnvilLogisticsInterfaceBlockEntity extends BlockEntity { private static final int TYPE_COUNT = 16; + private static final int MAX_COLLIDER_TOOLTIP_ITEMS = 256; + private static final int TOOLTIP_REQUEST_INTERVAL = 5; + private static final int TOOLTIP_REQUEST_EXPIRY = 200; + private static final StreamCodec POS_STREAM_CODEC = ByteBufCodecs.VAR_LONG + .map(BlockPos::of, BlockPos::asLong); @Setter private boolean syncing = false; /// 重入保护 + // Tooltip state is fetched on demand; coalesce changes made in one game tick. + private boolean tooltipDataDirty = false; + private long tooltipDataVersion = 1; + private @Nullable TooltipData cachedTooltipData; + private final Map tooltipRequestTicks = new HashMap<>(); private final FilteredItemStackHandler itemHandler = new FilteredItemStackHandler(TYPE_COUNT) { @Override @@ -66,31 +88,19 @@ public CelestialForgingAnvilLogisticsInterfaceBlockEntity(BlockEntityType typ super(type, pos, blockState); } - /// 将方块实体数据同步到所有追踪的客户端。 - public void syncToClients() { - if (level instanceof ServerLevel serverLevel) { - Packet packet = getUpdatePacket(); - if (packet != null) { - for (ServerPlayer player : serverLevel.getChunkSource().chunkMap - .getPlayers(serverLevel.getChunkAt(worldPosition).getPos(), false)) { - player.connection.send(packet); - } - } - } - } - @Override public void setChanged() { super.setChanged(); if (level != null && !level.isClientSide()) { - level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); - syncToClients(); + this.tooltipDataDirty = true; + this.cachedTooltipData = null; } } - @Override - public @Nullable Packet getUpdatePacket() { - return ClientboundBlockEntityDataPacket.create(this); + private void flushTooltipDataVersion() { + if (!this.tooltipDataDirty) return; + this.tooltipDataDirty = false; + this.tooltipDataVersion++; } @SuppressWarnings("unused") @@ -130,109 +140,233 @@ private BlockPos findParentCfa() { private static final int MAX_EJECT_PER_OP = 64; /// 每次弹出最多 1 组 public static final int EJECT_COOLDOWN = 8; /// 弹出间隔 8gt(类似磁力溜槽) - @Setter private int ejectCooldown = 0; private int lastEjectSlot = 0; /// 服务器端 tick。当主动模式(红石激活)时,每 8gt 自动将物品从内部库存向面向方向弹出,每次最多 1 组,速度类似于磁力溜槽。使用跨槽位轮询以防某个槽位被饿死。 public void serverTick() { if (level == null || level.isClientSide()) return; - BlockState state = getBlockState(); - if (!state.hasProperty(CelestialForgingAnvilInterfaceBlock.ACTIVE)) return; - if (!state.getValue(CelestialForgingAnvilInterfaceBlock.ACTIVE)) return; + try { + BlockState state = getBlockState(); + if (!state.hasProperty(CelestialForgingAnvilInterfaceBlock.ACTIVE)) return; + if (!state.getValue(CelestialForgingAnvilInterfaceBlock.ACTIVE)) return; - if (ejectCooldown > 0) { - ejectCooldown--; - return; - } + if (ejectCooldown > 0) { + ejectCooldown--; + return; + } - Direction facing = state.getValue(CelestialForgingAnvilInterfaceBlock.FACING); - BlockPos targetPos = worldPosition.relative(facing); - IItemHandler targetHandler = level.getCapability( - Capabilities.ItemHandler.BLOCK, targetPos, facing.getOpposite() - ); - Vec3 ejectPos = targetPos.getCenter(); - if (targetHandler == null) { - AABB ejectArea = new AABB( - ejectPos.add(-0.125, -0.125, -0.125), - ejectPos.add(0.125, 0.125, 0.125) + Direction facing = state.getValue(CelestialForgingAnvilInterfaceBlock.FACING); + BlockPos targetPos = worldPosition.relative(facing); + IItemHandler targetHandler = level.getCapability( + Capabilities.ItemHandler.BLOCK, targetPos, facing.getOpposite() ); - if (!level.noCollision(ejectArea)) return; - } - boolean ejected = false; - int totalSlots = itemHandler.getSlots(); - - /// 轮询:从 lastEjectSlot 开始,遍历所有槽位 - for (int offset = 0; offset < totalSlots; offset++) { - int slot = (lastEjectSlot + offset) % totalSlots; - ItemStack stack = itemHandler.getStackInSlot(slot); - if (stack.isEmpty()) continue; - int toExtract = Math.min(stack.getCount(), MAX_EJECT_PER_OP); + Vec3 ejectPos = targetPos.getCenter(); if (targetHandler == null) { - AABB itemDetectionArea = new AABB(targetPos).expandTowards(0, -0.5, 0); - int existingCount = level.getEntitiesOfClass(ItemEntity.class, itemDetectionArea).stream() - .map(ItemEntity::getItem) - .filter(existing -> ItemStack.isSameItemSameComponents(existing, stack)) - .mapToInt(ItemStack::getCount) - .sum(); - toExtract = Math.min(toExtract, stack.getMaxStackSize() - existingCount); - if (toExtract <= 0) continue; + AABB ejectArea = new AABB( + ejectPos.add(-0.125, -0.125, -0.125), + ejectPos.add(0.125, 0.125, 0.125) + ); + if (!level.noCollision(ejectArea)) return; } - ItemStack extracted = itemHandler.extractItem(slot, toExtract, false); - if (extracted.isEmpty()) continue; - - /// 尝试插入到目标容器中 - if (targetHandler != null) { - ItemStack remainder = ItemHandlerHelper.insertItem(targetHandler, extracted, false); - if (!remainder.isEmpty()) { - itemHandler.insertItem(slot, remainder, false); + boolean ejected = false; + int totalSlots = itemHandler.getSlots(); + + /// 轮询:从 lastEjectSlot 开始,遍历所有槽位 + for (int offset = 0; offset < totalSlots; offset++) { + int slot = (lastEjectSlot + offset) % totalSlots; + ItemStack stack = itemHandler.getStackInSlot(slot); + if (stack.isEmpty()) continue; + int toExtract = Math.min(stack.getCount(), MAX_EJECT_PER_OP); + if (targetHandler == null) { + AABB itemDetectionArea = new AABB(targetPos).expandTowards(0, -0.5, 0); + int existingCount = level.getEntitiesOfClass(ItemEntity.class, itemDetectionArea).stream() + .map(ItemEntity::getItem) + .filter(existing -> ItemStack.isSameItemSameComponents(existing, stack)) + .mapToInt(ItemStack::getCount) + .sum(); + toExtract = Math.min(toExtract, stack.getMaxStackSize() - existingCount); + if (toExtract <= 0) continue; } - if (remainder.getCount() < extracted.getCount()) { + ItemStack extracted = itemHandler.extractItem(slot, toExtract, false); + if (extracted.isEmpty()) continue; + + /// 尝试插入到目标容器中 + if (targetHandler != null) { + ItemStack remainder = ItemHandlerHelper.insertItem(targetHandler, extracted, false); + if (!remainder.isEmpty()) { + itemHandler.insertItem(slot, remainder, false); + } + if (remainder.getCount() < extracted.getCount()) { + ejected = true; + lastEjectSlot = (slot + 1) % totalSlots; + break; + } + } else { + /// 无目标容器——将物品以速度弹出到世界中 + Vec3 velocity = new Vec3( + facing.getStepX() * 0.25, + facing.getStepY() * 0.25, + facing.getStepZ() * 0.25 + ); + ItemEntity entity = new ItemEntity(level, ejectPos.x, ejectPos.y, ejectPos.z, extracted); + entity.setDeltaMovement(velocity); + entity.setDefaultPickUpDelay(); + level.addFreshEntity(entity); ejected = true; lastEjectSlot = (slot + 1) % totalSlots; break; } - } else { - /// 无目标容器——将物品以速度弹出到世界中 - Vec3 velocity = new Vec3( - facing.getStepX() * 0.25, - facing.getStepY() * 0.25, - facing.getStepZ() * 0.25 - ); - ItemEntity entity = new ItemEntity(level, ejectPos.x, ejectPos.y, ejectPos.z, extracted); - entity.setDeltaMovement(velocity); - entity.setDefaultPickUpDelay(); - level.addFreshEntity(entity); - ejected = true; - lastEjectSlot = (slot + 1) % totalSlots; - break; } - } - if (ejected) { - ejectCooldown = EJECT_COOLDOWN; - setChanged(); + if (ejected) { + ejectCooldown = EJECT_COOLDOWN; + setChanged(); + } + } finally { + this.flushTooltipDataVersion(); } } /// === 神殿需求显示(由 CFA 控制器推送)=== - @Getter @Setter + @Getter private ItemStack templeDemandItem = ItemStack.EMPTY; - @Getter @Setter + @Getter private int templeDemandCount = 0; - @Getter @Setter + @Getter private int templeDemandProgress = 0; - @Getter @Setter + @Getter private boolean templeDemandSatisfied = false; /// === 对撞机目标物品显示(由 CFA 控制器推送)=== - @Getter @Setter + @Getter private List colliderTargetItems = new ArrayList<>(); - @Getter @Setter + @Getter private boolean colliderProcessing = false; - @Getter @Setter + @Getter private boolean colliderStarMissing = false; + // Tooltip data is server-owned and fetched on demand by the client. + public void setTempleDemandItem(ItemStack templeDemandItem) { + if (ItemStack.matches(this.templeDemandItem, templeDemandItem)) return; + this.templeDemandItem = templeDemandItem.copy(); + this.setChanged(); + } + + public void setTempleDemandCount(int templeDemandCount) { + if (this.templeDemandCount == templeDemandCount) return; + this.templeDemandCount = templeDemandCount; + this.setChanged(); + } + + public void setTempleDemandProgress(int templeDemandProgress) { + if (this.templeDemandProgress == templeDemandProgress) return; + this.templeDemandProgress = templeDemandProgress; + this.setChanged(); + } + + public void setTempleDemandSatisfied(boolean templeDemandSatisfied) { + if (this.templeDemandSatisfied == templeDemandSatisfied) return; + this.templeDemandSatisfied = templeDemandSatisfied; + this.setChanged(); + } + + public void setColliderTargetItems(List colliderTargetItems) { + boolean unchanged = this.colliderTargetItems.size() == colliderTargetItems.size() + && IntStream.range(0, this.colliderTargetItems.size()) + .allMatch(index -> ItemStack.matches( + this.colliderTargetItems.get(index), colliderTargetItems.get(index) + )); + if (unchanged) return; + this.colliderTargetItems = new ArrayList<>(colliderTargetItems); + this.setChanged(); + } + + public void setColliderProcessing(boolean colliderProcessing) { + if (this.colliderProcessing == colliderProcessing) return; + this.colliderProcessing = colliderProcessing; + this.setChanged(); + } + + public void setColliderStarMissing(boolean colliderStarMissing) { + if (this.colliderStarMissing == colliderStarMissing) return; + this.colliderStarMissing = colliderStarMissing; + this.setChanged(); + } + + public void setEjectCooldown(int ejectCooldown) { + if (this.ejectCooldown == ejectCooldown) return; + this.ejectCooldown = ejectCooldown; + // Cooldown is server-side state and does not belong in tooltip snapshots. + super.setChanged(); + } + + @RemoteCallable(validator = TooltipDataValidator.class) + public static TooltipSyncResult syncTooltipData( + @CallableParam(clazz = CelestialForgingAnvilLogisticsInterfaceBlockEntity.class, field = "POS_STREAM_CODEC") BlockPos pos, + long knownVersion + ) { + CelestialForgingAnvilLogisticsInterfaceBlockEntity logistics = TooltipDataValidator.TARGET.get(); + try { + if (logistics == null || !logistics.getBlockPos().equals(pos)) return TooltipSyncResult.EMPTY; + logistics.flushTooltipDataVersion(); + return new TooltipSyncResult( + logistics.tooltipDataVersion, + knownVersion == logistics.tooltipDataVersion ? null : logistics.getOrCreateTooltipData() + ); + } finally { + TooltipDataValidator.TARGET.remove(); + } + } + + private TooltipData getOrCreateTooltipData() { + if (this.cachedTooltipData == null) { + this.cachedTooltipData = this.createTooltipData(); + } + return this.cachedTooltipData; + } + + private boolean allowTooltipRequest(ServerPlayer player) { + if (this.level == null || this.level.isClientSide()) return false; + long gameTime = this.level.getGameTime(); + Long lastRequest = this.tooltipRequestTicks.get(player.getUUID()); + if (lastRequest != null && gameTime >= lastRequest && gameTime - lastRequest < TOOLTIP_REQUEST_INTERVAL) { + return false; + } + if (this.tooltipRequestTicks.size() >= 64) { + this.tooltipRequestTicks.entrySet().removeIf(entry -> gameTime - entry.getValue() >= TOOLTIP_REQUEST_EXPIRY); + } + this.tooltipRequestTicks.put(player.getUUID(), gameTime); + return true; + } + + private TooltipData createTooltipData() { + List storedItems = new ArrayList<>(TYPE_COUNT); + for (int slot = 0; slot < this.itemHandler.getSlots(); slot++) { + ItemStack stack = this.itemHandler.getStackInSlot(slot); + if (!stack.isEmpty()) storedItems.add(stack.copy()); + } + + boolean hasTempleDemand = !this.templeDemandSatisfied && !this.templeDemandItem.isEmpty(); + List colliderTargets = new ArrayList<>(); + if (!this.colliderProcessing && !this.colliderStarMissing) { + int targetCount = Math.min(this.colliderTargetItems.size(), MAX_COLLIDER_TOOLTIP_ITEMS); + for (int index = 0; index < targetCount; index++) { + ItemStack target = this.colliderTargetItems.get(index); + if (!target.isEmpty()) colliderTargets.add(target.copy()); + } + } + return new TooltipData( + storedItems, + hasTempleDemand ? this.templeDemandItem.copy() : ItemStack.EMPTY, + hasTempleDemand ? this.templeDemandCount : 0, + hasTempleDemand ? this.templeDemandProgress : 0, + colliderTargets, + this.colliderProcessing, + this.colliderStarMissing + ); + } + @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); @@ -284,53 +418,119 @@ protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) this.colliderStarMissing = tag.getBoolean("colliderStarMissing"); } - @Override - public CompoundTag getUpdateTag(HolderLookup.Provider registries) { - CompoundTag tag = super.getUpdateTag(registries); - tag.put("inventory", itemHandler.serializeNBT(registries)); - if (!templeDemandItem.isEmpty()) { - tag.put("templeDemandItem", templeDemandItem.save(registries)); - } - tag.putInt("templeDemandCount", templeDemandCount); - tag.putInt("templeDemandProgress", templeDemandProgress); - tag.putBoolean("templeDemandSatisfied", templeDemandSatisfied); - if (!colliderTargetItems.isEmpty()) { - ListTag list = new ListTag(); - for (ItemStack stack : colliderTargetItems) { - if (!stack.isEmpty()) { - list.add(stack.save(registries)); - } + public record TooltipData( + List storedItems, + ItemStack templeDemandItem, + int templeDemandCount, + int templeDemandProgress, + List colliderTargetItems, + boolean colliderProcessing, + boolean colliderStarMissing + ) { + private static final int HAS_TEMPLE_DEMAND = 1; + private static final int COLLIDER_PROCESSING = 1 << 1; + private static final int COLLIDER_STAR_MISSING = 1 << 2; + private static final StreamCodec> STORED_ITEMS_STREAM_CODEC = + ItemStack.STREAM_CODEC.apply(ByteBufCodecs.list(TYPE_COUNT)); + private static final StreamCodec> COLLIDER_TARGETS_STREAM_CODEC = + ItemStack.STREAM_CODEC.apply(ByteBufCodecs.list(MAX_COLLIDER_TOOLTIP_ITEMS)); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + TooltipData::encode, + TooltipData::decode + ); + + private static void encode(RegistryFriendlyByteBuf buf, TooltipData data) { + TooltipData.STORED_ITEMS_STREAM_CODEC.encode(buf, data.storedItems); + int flags = 0; + if (!data.templeDemandItem.isEmpty()) flags |= TooltipData.HAS_TEMPLE_DEMAND; + if (data.colliderProcessing) flags |= TooltipData.COLLIDER_PROCESSING; + if (data.colliderStarMissing) flags |= TooltipData.COLLIDER_STAR_MISSING; + buf.writeByte(flags); + if ((flags & TooltipData.HAS_TEMPLE_DEMAND) != 0) { + ItemStack.STREAM_CODEC.encode(buf, data.templeDemandItem); + buf.writeVarInt(data.templeDemandCount); + buf.writeVarInt(data.templeDemandProgress); + } + if ((flags & (TooltipData.COLLIDER_PROCESSING | TooltipData.COLLIDER_STAR_MISSING)) == 0) { + TooltipData.COLLIDER_TARGETS_STREAM_CODEC.encode(buf, data.colliderTargetItems); } - tag.put("colliderTargetItems", list); } - tag.putBoolean("colliderProcessing", colliderProcessing); - tag.putBoolean("colliderStarMissing", colliderStarMissing); - return tag; + + private static TooltipData decode(RegistryFriendlyByteBuf buf) { + List storedItems = TooltipData.STORED_ITEMS_STREAM_CODEC.decode(buf); + int flags = buf.readUnsignedByte(); + boolean hasTempleDemand = (flags & TooltipData.HAS_TEMPLE_DEMAND) != 0; + ItemStack templeDemandItem = hasTempleDemand ? ItemStack.STREAM_CODEC.decode(buf) : ItemStack.EMPTY; + int templeDemandCount = hasTempleDemand ? buf.readVarInt() : 0; + int templeDemandProgress = hasTempleDemand ? buf.readVarInt() : 0; + boolean colliderProcessing = (flags & TooltipData.COLLIDER_PROCESSING) != 0; + boolean colliderStarMissing = (flags & TooltipData.COLLIDER_STAR_MISSING) != 0; + List colliderTargetItems = colliderProcessing || colliderStarMissing + ? List.of() + : TooltipData.COLLIDER_TARGETS_STREAM_CODEC.decode(buf); + return new TooltipData( + storedItems, + templeDemandItem, + templeDemandCount, + templeDemandProgress, + colliderTargetItems, + colliderProcessing, + colliderStarMissing + ); + } } - @Override - public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { - super.handleUpdateTag(tag, registries); - if (tag.contains("inventory")) { - itemHandler.deserializeNBT(registries, tag.getCompound("inventory")); + public record TooltipSyncResult(long version, @Nullable TooltipData data) { + public static final TooltipSyncResult EMPTY = new TooltipSyncResult(0, null); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + TooltipSyncResult::encode, + TooltipSyncResult::decode + ); + + private static void encode(RegistryFriendlyByteBuf buf, TooltipSyncResult result) { + buf.writeVarLong(result.version); + buf.writeBoolean(result.data != null); + if (result.data != null) TooltipData.STREAM_CODEC.encode(buf, result.data); } - if (tag.contains("templeDemandItem")) { - this.templeDemandItem = ItemStack.parse(registries, tag.getCompound("templeDemandItem")) - .orElse(ItemStack.EMPTY); - } else { - this.templeDemandItem = ItemStack.EMPTY; + + private static TooltipSyncResult decode(RegistryFriendlyByteBuf buf) { + long version = buf.readVarLong(); + return new TooltipSyncResult(version, buf.readBoolean() ? TooltipData.STREAM_CODEC.decode(buf) : null); } - this.templeDemandCount = tag.getInt("templeDemandCount"); - this.templeDemandProgress = tag.getInt("templeDemandProgress"); - this.templeDemandSatisfied = tag.getBoolean("templeDemandSatisfied"); - this.colliderTargetItems.clear(); - if (tag.contains("colliderTargetItems")) { - ListTag list = tag.getList("colliderTargetItems", Tag.TAG_COMPOUND); - for (int i = 0; i < list.size(); i++) { - ItemStack.parse(registries, list.getCompound(i)).ifPresent(colliderTargetItems::add); + } + + private static final class TooltipDataValidator implements IRemoteCallableValidator { + private static final ThreadLocal TARGET = + new ThreadLocal<>(); + + @Override + public boolean validate(IPayloadContext ctx, Method method, Object[] args) { + TooltipDataValidator.TARGET.remove(); + if ( + ctx.flow() != PacketFlow.SERVERBOUND + || !(ctx.player() instanceof ServerPlayer player) + || args.length != 2 + || !(args[0] instanceof BlockPos pos) + || !(args[1] instanceof Long knownVersion) + || knownVersion < 0 + ) { + return false; } + if (!player.level().isLoaded(pos)) return false; + BlockEntity blockEntity = player.level().getBlockEntity(pos); + if (!(blockEntity instanceof CelestialForgingAnvilLogisticsInterfaceBlockEntity logistics)) { + return false; + } + boolean accessible = AbstractContainerMenu.stillValid( + ContainerLevelAccess.create(player.level(), pos), + player, + logistics.getBlockState().getBlock() + ); + if (accessible && logistics.allowTooltipRequest(player)) { + TooltipDataValidator.TARGET.set(logistics); + return true; + } + return false; } - this.colliderProcessing = tag.getBoolean("colliderProcessing"); - this.colliderStarMissing = tag.getBoolean("colliderStarMissing"); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java index 7395b15e3e..b1b3d99ebf 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CelestialForgingAnvilPortalBlockEntity.java @@ -4,6 +4,9 @@ import dev.dubhe.anvilcraft.api.rendering.CacheableBERenderingPipeline; import dev.dubhe.anvilcraft.block.cfa.CelestialForgingAnvilBlock; import dev.dubhe.anvilcraft.block.cfa.CelestialForgingAnvilPortalBlock; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelData; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelManager; +import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyData; import dev.dubhe.anvilcraft.block.state.Cube323PartHalf; import dev.dubhe.anvilcraft.block.state.DirectionGate331PartHalf; import dev.dubhe.anvilcraft.init.ModHeaterInfos; @@ -12,7 +15,6 @@ import dev.dubhe.anvilcraft.network.LaserEmitPacket; import dev.dubhe.anvilcraft.saved.WormholeNetwork; import dev.dubhe.anvilcraft.util.BreakBlockUtil; -import lombok.Getter; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.HolderLookup; @@ -45,7 +47,6 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -73,7 +74,6 @@ boolean isJustPlacedWaterlogged() { private boolean wormholeLaserGamma = false; /// 客户端伽马激光束颜色渲染状态。 - @Getter private boolean emittingGamma = false; private int gammaLevel = 0; @@ -223,8 +223,7 @@ protected boolean isLaserOriginOffset() { @Override public void onIrradiated(BaseLaserBlockEntity source) { this.incomingLaserLevel = source.getLaserLevel(); - this.incomingLaserGamma = source instanceof CelestialForgingAnvilLaserInterfaceBlockEntity cfaSource - && cfaSource.isEmittingGamma(); + this.incomingLaserGamma = source.isEmittingGamma(); } @Override @@ -235,11 +234,18 @@ public void onCancelingIrradiation(BaseLaserBlockEntity source) { /// 设置来自已连接传送门的虫洞激光输出。由已连接传送门的 tick 通过虫洞同步调用。 public void setWormholeLaser(int level, boolean gamma) { - this.wormholeLaserLevel = level; + int normalizedLevel = Math.max(0, level); + if (this.wormholeLaserLevel == normalizedLevel && this.wormholeLaserGamma == gamma) return; + this.wormholeLaserLevel = normalizedLevel; this.wormholeLaserGamma = gamma; this.setChanged(); } + @Override + public boolean isEmittingGamma() { + return this.emittingGamma; + } + /// 传送门上伽马激光渲染的客户端更新。 public void clientUpdateGamma(@Nullable BlockPos irradiateBlockPos, int laserLevel) { this.emittingGamma = true; @@ -300,6 +306,13 @@ public void tick() { return; } + /// 可登陆特殊星球的传送门不依赖虫洞网络中的另一座 CFA。 + CelestialTravelData landing = getLandingData(parent); + if (landing != null) { + tickLandingPortal(state); + return; + } + Cube323PartHalf side = findSideFromParent(parent); if (side == null) return; @@ -326,8 +339,7 @@ public void tick() { updateLaserLevel(0); wormholeLaserLevel = 0; wormholeLaserGamma = false; - this.emittingGamma = false; - this.gammaLevel = 0; + setGammaOutputState(false, 0); this.gammaIrradiatingPos = null; this.gammaExposureTicks = 0; this.setChanged(); @@ -454,6 +466,7 @@ public void tick() { /// 如果此传送门有来自已连接传送门的虫洞激光设置,则发射激光。仅在传送门开启时发射——关闭的传送门不得转发激光。 if (wormholeLaserLevel > 0 && state.getValue(CelestialForgingAnvilPortalBlock.OPEN)) { Direction facing = getFacing(); + setGammaOutputState(wormholeLaserGamma, wormholeLaserLevel); if (irradiateSelfLaserBlockSet.isEmpty()) { if (wormholeLaserGamma) { emitGammaLaserBeam(facing); @@ -461,10 +474,9 @@ public void tick() { emitLaser(facing); } } - this.emittingGamma = wormholeLaserGamma; - this.gammaLevel = wormholeLaserLevel; } else { /// 停止激光发射 + setGammaOutputState(false, 0); if (irradiateBlockPos != null) { BlockEntity oldBe = level.getBlockEntity(irradiateBlockPos); if (oldBe instanceof BaseLaserBlockEntity lastIrradiated) { @@ -474,8 +486,6 @@ public void tick() { } clearIrradiateSelfLaserBlockSet(); updateLaserLevel(0); - this.emittingGamma = false; - this.gammaLevel = 0; this.gammaIrradiatingPos = null; this.gammaExposureTicks = 0; } @@ -506,7 +516,7 @@ public void tick() { } /// 清理已离开开口格的触碰记录 if (!touchingEntities.isEmpty()) { - Set present = level.getEntitiesOfClass(Entity.class, new AABB(worldPosition)) + Set present = level.getEntitiesOfClass(Entity.class, portalSpace) .stream().map(Entity::getUUID).collect(Collectors.toSet()); touchingEntities.removeIf(uuid -> !present.contains(uuid)); } @@ -524,12 +534,25 @@ public void tryTouchTeleport(Entity entity) { CelestialForgingAnvilBlockEntity parent = findParentCfa(); if (parent == null) return; + if (!(level instanceof ServerLevel sourceLevel)) return; + + CelestialTravelData landing = getLandingData(parent); + if (landing != null) { + if (CelestialTravelManager.tryLand(entity, sourceLevel, getAnchorPos(), getFacing(), landing)) { + touchingEntities.add(uuid); + } + return; + } + Cube323PartHalf side = findSideFromParent(parent); if (side == null) return; + UUID hash = parent.getWormholeParamsHash(); + if (hash == null) return; + WormholeNetwork network = WormholeNetwork.get(); List connected = network.getConnected( - Objects.requireNonNull(parent.getWormholeParamsHash()), Objects.requireNonNull(level).dimension(), parent.getBlockPos() + hash, sourceLevel.dimension(), parent.getBlockPos() ); /// 仅当恰好有另一个 CFA 在此同侧有传送门时才传送 List matching = connected.stream() @@ -540,7 +563,7 @@ public void tryTouchTeleport(Entity entity) { WormholeNetwork.Entry target = matching.getFirst(); if (!network.hasPortalAt(target.dimension(), target.pos(), side)) return; - ServerLevel targetLevel = level.getServer().getLevel(target.dimension()); + ServerLevel targetLevel = sourceLevel.getServer().getLevel(target.dimension()); if (targetLevel == null) return; Direction outwardFacing = CelestialForgingAnvilPortalBlock.getFacingFromSide(side); @@ -579,7 +602,7 @@ public void tryTouchTeleport(Entity entity) { /// 对于同维度传送,直接使用 teleportTo 以避免 changeDimension 重建实体,导致的弹射物停滞和速度丢失问题。 /// 对于 ServerPlayer 则还需调用 connection.teleport() 来同步客户端网络状态,否则客户端会从旧位置发送移动包,触发 "moved wrongly!" 反作弊警告。 /// 对于跨维度传送,使用 changeDimension(DimensionTransition)。 - if (targetLevel == level) { + if (targetLevel == sourceLevel) { entity.teleportTo(targetLevel, dx, dy, dz, java.util.Set.of(), targetYRot, entity.getXRot()); if (entity instanceof ServerPlayer player) { player.connection.teleport(dx, dy, dz, targetYRot, entity.getXRot()); @@ -606,6 +629,51 @@ public void tryTouchTeleport(Entity entity) { touchingEntities.add(uuid); } + @Nullable + private CelestialTravelData getLandingData(CelestialForgingAnvilBlockEntity parent) { + if (parent.getCelestialBodyData() instanceof SpecialCelestialBodyData special) { + return special.landing(); + } + return null; + } + + /** Opens a standalone landing gate and performs the same entity scan as a linked gate. */ + private void tickLandingPortal(BlockState state) { + if (!state.getValue(CelestialForgingAnvilPortalBlock.OPEN)) { + state = state.setValue(CelestialForgingAnvilPortalBlock.OPEN, true); + level.setBlock(worldPosition, state, 3); + } + /// A landing gate has no remote laser endpoint; discard stale wormhole output. + wormholeLaserLevel = 0; + wormholeLaserGamma = false; + setGammaOutputState(false, 0); + if (irradiateBlockPos != null) { + BlockEntity oldBe = level.getBlockEntity(irradiateBlockPos); + if (oldBe instanceof BaseLaserBlockEntity lastIrradiated) { + lastIrradiated.onCancelingIrradiation(this); + } + updateIrradiateBlockPos(null); + } + clearIrradiateSelfLaserBlockSet(); + updateLaserLevel(0); + this.gammaIrradiatingPos = null; + this.gammaExposureTicks = 0; + if (isAnchor()) { + AABB portalSpace = new AABB(worldPosition).expandTowards(0, 1, 0); + for (Entity entity : level.getEntitiesOfClass(Entity.class, portalSpace)) { + tryTouchTeleport(entity); + } + if (!touchingEntities.isEmpty()) { + Set present = level.getEntitiesOfClass(Entity.class, portalSpace) + .stream().map(Entity::getUUID).collect(Collectors.toSet()); + touchingEntities.removeIf(uuid -> !present.contains(uuid)); + } + } else { + touchingEntities.clear(); + } + sendLaserPackets(); + } + /// 查找虫洞另一侧已连接传送门中与本格相同高度的方块实体。本格为底层中心则返回对侧底层中心,本格为正中心则返回对侧正中心,如果未连接或目标传送门未找到则返回 null。 @Nullable private CelestialForgingAnvilPortalBlockEntity findConnectedPortal( @@ -697,25 +765,42 @@ public void deliverItem(List drops, Direction direction, BlockPos sou @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.saveAdditional(tag, registries); + tag.putBoolean("emittingGamma", this.emittingGamma); + tag.putInt("gammaLevel", this.gammaLevel); + tag.putBoolean("lastWaterlogged", this.lastWaterlogged); + } + + private void setGammaOutputState(boolean emitting, int level) { + int normalizedLevel = emitting ? Math.max(0, level) : 0; + if (this.emittingGamma != emitting || this.gammaLevel != normalizedLevel) { + this.markChanged(); + } + this.emittingGamma = emitting; + this.gammaLevel = normalizedLevel; } @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { super.loadAdditional(tag, registries); + this.emittingGamma = tag.getBoolean("emittingGamma"); + this.gammaLevel = tag.getInt("gammaLevel"); + this.lastWaterlogged = tag.getBoolean("lastWaterlogged"); } @Override public CompoundTag getUpdateTag(HolderLookup.Provider registries) { CompoundTag tag = super.getUpdateTag(registries); - tag.putBoolean("gamma", emittingGamma); - tag.putInt("gammaLevel", gammaLevel); + tag.putBoolean("emittingGamma", this.emittingGamma); + tag.putInt("gammaLevel", this.gammaLevel); return tag; } @Override public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { super.handleUpdateTag(tag, registries); - this.emittingGamma = tag.getBoolean("gamma"); + this.emittingGamma = tag.contains("emittingGamma") + ? tag.getBoolean("emittingGamma") + : tag.getBoolean("gamma"); this.gammaLevel = tag.getInt("gammaLevel"); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaBlockEntitySync.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaBlockEntitySync.java new file mode 100644 index 0000000000..97a24c171f --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaBlockEntitySync.java @@ -0,0 +1,22 @@ +package dev.dubhe.anvilcraft.block.entity; + +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.BlockEntity; + +import javax.annotation.Nullable; + +/** Shared server-to-client synchronization for CFA block entities. */ +final class CfaBlockEntitySync { + private CfaBlockEntitySync() { + } + + static void sendToTracking(BlockEntity blockEntity, @Nullable Packet packet) { + if (packet == null || !(blockEntity.getLevel() instanceof ServerLevel level)) return; + for (var player : level.getChunkSource().chunkMap.getPlayers( + level.getChunkAt(blockEntity.getBlockPos()).getPos(), false + )) { + player.connection.send(packet); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaGammaLaserEffects.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaGammaLaserEffects.java new file mode 100644 index 0000000000..19ffcc593a --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaGammaLaserEffects.java @@ -0,0 +1,105 @@ +package dev.dubhe.anvilcraft.block.entity; + +import dev.dubhe.anvilcraft.block.RubyPrismBlock; +import dev.dubhe.anvilcraft.block.entity.heatable.HeatableBlockEntity; +import dev.dubhe.anvilcraft.block.heatable.OverheatedEmberMetalBlock; +import dev.dubhe.anvilcraft.init.block.ModBlocks; +import dev.dubhe.anvilcraft.init.entity.ModDamageTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; + +/** Shared world effects for the CFA gamma beam emitters. */ +final class CfaGammaLaserEffects { + private static final int MAX_DISTANCE = 16; + + private CfaGammaLaserEffects() { + } + + static BlockPos findTarget(Level level, BlockPos origin, Direction direction) { + for (int distance = 1; distance <= MAX_DISTANCE; distance++) { + BlockPos candidate = origin.relative(direction, distance); + if (!level.getBlockState(candidate).is(BlockTags.REPLACEABLE)) return candidate; + } + return origin.relative(direction, MAX_DISTANCE); + } + + static void destroyPrisms(Level level, BlockPos origin, Direction direction, BlockPos target) { + BlockPos.MutableBlockPos current = origin.relative(direction).mutable(); + while (!current.equals(target)) { + if (level.getBlockState(current).getBlock() instanceof RubyPrismBlock) { + level.destroyBlock(current.immutable(), true); + } + current.move(direction); + } + } + + static void damageEntities(Level level, BlockPos origin, BlockPos target, Direction direction, int gammaLevel) { + int damage = Math.min(16, gammaLevel - 4) * 16; + if (damage <= 0) return; + Vec3 start = origin.relative(direction).getCenter().add(-0.0625, -0.0625, -0.0625); + Vec3 end = target.relative(direction.getOpposite()).getCenter().add(0.0625, 0.0625, 0.0625); + level.getEntities(EntityTypeTest.forClass(LivingEntity.class), new AABB(start, end), Entity::isAlive) + .forEach(entity -> entity.hurt(ModDamageTypes.gammaLaser(level), damage)); + } + + static void heatEmberMetal( + Level level, + @Nullable BlockPos target, + Direction direction, + int gammaLevel, + int updateFlags + ) { + if (target == null || gammaLevel < 4 || level.getGameTime() % 20 != 0) return; + BlockState hitState = level.getBlockState(target); + if (!hitState.is(ModBlocks.EMBER_METAL_BLOCK.get()) + && !hitState.is(ModBlocks.OVERHEATED_EMBER_METAL_BLOCK.get())) { + return; + } + int areaSize = gammaLevel >= 16 ? 7 : gammaLevel >= 12 ? 5 : gammaLevel >= 8 ? 3 : 1; + int thickness = gammaLevel >= 16 ? 3 : gammaLevel >= 12 ? 2 : 1; + int halfSize = areaSize / 2; + Direction[] perpendiculars = switch (direction.getAxis()) { + case X -> new Direction[]{Direction.UP, Direction.NORTH}; + case Z -> new Direction[]{Direction.UP, Direction.EAST}; + default -> new Direction[]{Direction.NORTH, Direction.EAST}; + }; + + for (int depth = 0; depth < thickness; depth++) { + BlockPos depthPos = target.relative(direction, depth); + for (int first = -halfSize; first <= halfSize; first++) { + for (int second = -halfSize; second <= halfSize; second++) { + heatEmberMetalAt(level, depthPos + .relative(perpendiculars[0], first) + .relative(perpendiculars[1], second), updateFlags); + } + } + } + } + + private static void heatEmberMetalAt(Level level, BlockPos pos, int updateFlags) { + BlockState state = level.getBlockState(pos); + if (state.is(ModBlocks.EMBER_METAL_BLOCK.get())) { + OverheatedEmberMetalBlock overheated = ModBlocks.OVERHEATED_EMBER_METAL_BLOCK.get(); + BlockState overheatedState = overheated.defaultBlockState(); + level.setBlock(pos, overheatedState, updateFlags); + BlockEntity blockEntity = overheated.newBlockEntity(pos, overheatedState); + if (blockEntity instanceof HeatableBlockEntity heatable) { + level.setBlockEntity(heatable); + heatable.addDurationInTick(80); + } + } else if (state.is(ModBlocks.OVERHEATED_EMBER_METAL_BLOCK.get()) + && level.getBlockEntity(pos) instanceof HeatableBlockEntity heatable) { + heatable.addDurationInTick(80); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaGravityController.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaGravityController.java new file mode 100644 index 0000000000..e5e7d924fd --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaGravityController.java @@ -0,0 +1,197 @@ +package dev.dubhe.anvilcraft.block.entity; + +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyClass; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.StarData; +import dev.dubhe.anvilcraft.entity.ThrownHeavyHalberdEntity; +import dev.dubhe.anvilcraft.init.entity.ModDamageTypes; +import dev.dubhe.anvilcraft.init.item.ModComponents; +import dev.dubhe.anvilcraft.util.GravityManager; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import javax.annotation.Nullable; + +/** + * Owns the physical gravity source and contact effects for one CFA. + * + *

The controller deliberately keeps the 1.21 geometry formulas. The 26.1 + * implementation uses a different redstone interpolation divisor, which would + * move the source outside the rendered body on this branch.

+ */ +final class CfaGravityController { + private static final int BASE_GRAVITY_RADIUS = 4; + private static final float PLANET_CONTACT_DAMAGE = 38.0f; + private static final float STAR_CONTACT_DAMAGE = 66.0f; + + private boolean active; + private double strength; + private int radius = BASE_GRAVITY_RADIUS; + private double bodyRadius; + private Vec3 center = Vec3.ZERO; + + void tick( + @Nullable Level level, + BlockPos controllerPos, + boolean amplified, + boolean amplifierPresent, + @Nullable CelestialBodyData body, + int stellarMass, + int redstoneSignal + ) { + if (level == null || level.isClientSide()) return; + + boolean shouldBeActive = body != null + && stellarMass > 0 + && body.size() > 0 + && (!(body instanceof StarData star) || amplifierPresent || star.specialRedDwarf()); + if (!shouldBeActive) { + this.remove(level, controllerPos); + return; + } + + int signal = Math.clamp(redstoneSignal, 0, 15); + double nextBodyRadius = computeBodyRadius(body, signal); + int nextRadius = computeGravityRadius(body, amplified, signal); + double nextStrength = computeStrength(body, stellarMass, nextBodyRadius); + Vec3 nextCenter = computeCenter(controllerPos, body, amplified, signal); + + if (!this.active + || Double.compare(this.strength, nextStrength) != 0 + || this.radius != nextRadius + || Double.compare(this.bodyRadius, nextBodyRadius) != 0 + || !this.center.equals(nextCenter)) { + GravityManager.GravitySourceType type = new GravityManager.GravitySourceType( + nextStrength, + nextRadius, + nextBodyRadius + ); + GravityManager.GravitySourceManager.upsertSource(level, controllerPos, nextCenter, type); + this.active = true; + this.strength = nextStrength; + this.radius = nextRadius; + this.bodyRadius = nextBodyRadius; + this.center = nextCenter; + } + + } + + void remove(@Nullable Level level, BlockPos controllerPos) { + if (level != null && !level.isClientSide() && this.active) { + GravityManager.GravitySourceManager.removeSource(level, controllerPos); + } + this.active = false; + this.strength = 0.0; + this.radius = BASE_GRAVITY_RADIUS; + this.bodyRadius = 0.0; + this.center = Vec3.ZERO; + } + + boolean isActive() { + return this.active; + } + + void handleEntityContact( + @Nullable Level level, + @Nullable CelestialBodyData body, + Entity entity + ) { + if (!this.active + || level == null + || level.isClientSide() + || entity.level() != level + || entity.isRemoved() + || isEternal(entity)) { + return; + } + if (entity instanceof LivingEntity living) { + applyCelestialDamage(level, body, living); + } else { + entity.discard(); + } + } + + private static boolean isEternal(Entity entity) { + ItemStack stack = switch (entity) { + case ItemEntity itemEntity -> itemEntity.getItem(); + case ThrownHeavyHalberdEntity halberd -> halberd.getWeaponItem(); + default -> ItemStack.EMPTY; + }; + return stack.has(ModComponents.ETERNAL); + } + + private static void applyCelestialDamage( + Level level, + @Nullable CelestialBodyData body, + LivingEntity living + ) { + if (body instanceof StarData star) { + if (star.bodyClass() == CelestialBodyClass.BLACK_HOLE) { + living.hurt(ModDamageTypes.lostInTime(level), Float.MAX_VALUE); + } else { + living.hurt(level.damageSources().inFire(), STAR_CONTACT_DAMAGE); + } + } else { + living.hurt(level.damageSources().fall(), PLANET_CONTACT_DAMAGE); + } + } + + private static double computeStrength(CelestialBodyData body, int stellarMass, double bodyRadius) { + double massRatio = Math.pow(2.0, (stellarMass - 12) / 2.0); + double strength = Math.log1p(massRatio) / Math.log(2.0); + double unscaledRadius = body.bodyScale() / 2.0; + if (unscaledRadius > 1.0E-6 && bodyRadius > 1.0E-6) { + double scaleRatio = bodyRadius / unscaledRadius; + strength *= scaleRatio * scaleRatio; + } + return strength; + } + + private static Vec3 computeCenter( + BlockPos controllerPos, + CelestialBodyData body, + boolean amplified, + int redstoneSignal + ) { + float factor = redstoneSignal / 15.0f; + float baseCenterY = amplified ? 6.5f : 4.5f; + float fullCenterY = CelestialBodyData.dynamicCenterY(body, amplified); + float centerY = baseCenterY + (fullCenterY - baseCenterY) * factor; + return new Vec3( + controllerPos.getX() + 0.5, + controllerPos.getY() + centerY, + controllerPos.getZ() + 0.5 + ); + } + + private static int computeGravityRadius( + CelestialBodyData body, + boolean amplified, + int redstoneSignal + ) { + float factor = redstoneSignal / 15.0f; + float fullRingScale = CelestialBodyData.ringSystemScale(body, amplified); + float ringScale = CelestialBodyData.BASE_RING_SCALE + + (fullRingScale - CelestialBodyData.BASE_RING_SCALE) * factor; + return Math.max(1, Math.round(BASE_GRAVITY_RADIUS * ringScale / CelestialBodyData.BASE_RING_SCALE)); + } + + private static double computeBodyRadius(CelestialBodyData body, int redstoneSignal) { + float factor = redstoneSignal / 15.0f; + float rawBodyScale = body.bodyScale(); + float fullBodyScale = rawBodyScale * CelestialBodyData.BODY_SCALE_FACTOR; + double bodyRadius = (rawBodyScale + (fullBodyScale - rawBodyScale) * factor) / 2.0; + // Error planets render as a quarter-sized body; keep their physical + // collision/gravity core aligned with that visual scale. + if (body instanceof SpecialCelestialBodyData special && special.isErrorPlanet()) { + bodyRadius *= 0.25; + } + return bodyRadius; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaInventoryCodec.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaInventoryCodec.java new file mode 100644 index 0000000000..7c5568715d --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaInventoryCodec.java @@ -0,0 +1,34 @@ +package dev.dubhe.anvilcraft.block.entity; + +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.ItemStack; + +/** Shared NBT codec for the five internal CFA anvil slots. */ +final class CfaInventoryCodec { + private CfaInventoryCodec() { + } + + static CompoundTag save(SimpleContainer inventory, HolderLookup.Provider registries) { + CompoundTag tag = new CompoundTag(); + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + ItemStack stack = inventory.getItem(slot); + if (!stack.isEmpty()) { + tag.put("s" + slot, stack.save(registries)); + } + } + return tag; + } + + static void load(CompoundTag tag, SimpleContainer inventory, HolderLookup.Provider registries) { + for (int slot = 0; slot < inventory.getContainerSize(); slot++) { + String key = "s" + slot; + ItemStack stack = ItemStack.EMPTY; + if (tag.get(key) instanceof CompoundTag stackTag) { + stack = ItemStack.parse(registries, stackTag).orElse(ItemStack.EMPTY); + } + inventory.setItem(slot, stack); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaMegastructureManager.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaMegastructureManager.java index 0e20770f3c..016599ae2d 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaMegastructureManager.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaMegastructureManager.java @@ -1,8 +1,10 @@ package dev.dubhe.anvilcraft.block.entity; +import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.api.power.PowerComponentType; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialRefactorOption; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialRefactorRegistry; +import dev.dubhe.anvilcraft.block.entity.celestial.Megastructure; import dev.dubhe.anvilcraft.block.entity.megastructure.AcceleratorHandler; import dev.dubhe.anvilcraft.block.entity.megastructure.ColliderHandler; import dev.dubhe.anvilcraft.block.entity.megastructure.DysonSphereHandler; @@ -16,116 +18,208 @@ import dev.dubhe.anvilcraft.block.entity.megastructure.PenroseSphereHandler; import dev.dubhe.anvilcraft.block.entity.megastructure.TempleHandler; import dev.dubhe.anvilcraft.block.entity.megastructure.WormholeStabilizerHandler; +import dev.dubhe.anvilcraft.init.registry.ModRegistries; import lombok.Getter; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import javax.annotation.Nullable; +/** Owns the runtime state and handlers for one Celestial Forging Anvil. */ public class CfaMegastructureManager { - private int activeMegastructureIndex = -1; + private @Nullable ResourceLocation activeMegastructureId; + /** Old index/name fields are kept so existing worlds and add-ons continue to load. */ + private int legacyActiveMegastructureIndex = -1; + private @Nullable String legacyActiveMegastructureName; + private int legacyActiveMegastructureRing = -1; - private final Map handlers = new LinkedHashMap<>(); + private final Map handlers = new LinkedHashMap<>(); @Getter private final AcceleratorHandler acceleratorHandler; public CfaMegastructureManager() { - registerHandler(new ExcavatorHandler()); - registerHandler(new ExtractorHandler()); - registerHandler(new GiantExtractorHandler()); - registerHandler(new ColliderHandler()); - registerHandler(new DysonSphereHandler("dyson_sphere_small")); - registerHandler(new DysonSphereHandler("dyson_sphere_large")); - registerHandler(new MagnetarCoilHandler()); - registerHandler(new PenroseSphereHandler()); - registerHandler(new MatterDecompressorHandler()); - registerHandler(new WormholeStabilizerHandler()); - registerHandler(new EcoStationHandler()); - registerHandler(new TempleHandler()); - this.acceleratorHandler = new AcceleratorHandler(); + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + IMegastructureHandler previous = this.handlers.put(definition.id(), definition.createHandler()); + if (previous != null) { + throw new IllegalStateException("Duplicate megastructure handler: " + definition.id()); + } + } + // A block entity can be constructed by a data-fix/test while the + // deferred registry is only partially filled. Fill every missing + // built-in handler without replacing handlers already created from the + // registry (or their runtime state). + this.registerLegacyHandlers(); + ResourceLocation acceleratorId = AnvilCraft.of("stellar_evolution_accelerator"); + IMegastructureHandler accelerator = this.handlers.get(acceleratorId); + if (!(accelerator instanceof AcceleratorHandler handler)) { + throw new IllegalStateException( + "Stellar evolution accelerator handler was not registered for " + acceleratorId + ); + } + this.acceleratorHandler = handler; + } + + private void registerLegacyHandlers() { + this.registerLegacy(new ExcavatorHandler()); + this.registerLegacy(new ExtractorHandler()); + this.registerLegacy(new GiantExtractorHandler()); + this.registerLegacy(new ColliderHandler()); + this.registerLegacy(new DysonSphereHandler("dyson_sphere_small")); + this.registerLegacy(new DysonSphereHandler("dyson_sphere_large")); + this.registerLegacy(new DysonSphereHandler("dyson_sphere_brown_dwarf")); + this.registerLegacy(new MagnetarCoilHandler()); + this.registerLegacy(new PenroseSphereHandler()); + this.registerLegacy(new MatterDecompressorHandler()); + this.registerLegacy(new WormholeStabilizerHandler()); + this.registerLegacy(new EcoStationHandler()); + this.registerLegacy(new TempleHandler()); + this.registerLegacy(new AcceleratorHandler()); } - private void registerHandler(IMegastructureHandler handler) { - handlers.put(handler.name(), handler); + private void registerLegacy(IMegastructureHandler handler) { + ResourceLocation id = AnvilCraft.of(handler.name()); + this.handlers.putIfAbsent(id, handler); } + /** + * Adds definitions that became visible after this manager was constructed. + * + *

Block entities can be instantiated by tests or data fixes before a + * deferred registry has finished filling. Keep already-created handlers + * (including the legacy fallback instances) so their runtime state is not + * replaced, while still making later add-on definitions usable.

+ */ + private void synchronizeRegistryHandlers() { + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!this.handlers.containsKey(definition.id())) { + this.handlers.put(definition.id(), definition.createHandler()); + } + } + } + + /** Returns the legacy option index when known, or -1 when no primary is active. */ public int getActiveIndex() { - return activeMegastructureIndex; + // A new-ID save may not have an index yet; callers use this value as + // an active-state flag while the exact index is resolved from the + // current definition list on the next option lookup. + return this.legacyActiveMegastructureIndex >= 0 || this.activeMegastructureId != null + ? Math.max(this.legacyActiveMegastructureIndex, 0) + : -1; + } + + public boolean hasActiveMegastructure() { + return this.activeMegastructureId != null + || this.legacyActiveMegastructureName != null + || this.legacyActiveMegastructureIndex >= 0; + } + + /** Resolves the stable ID, migrating old name/index state lazily. */ + public @Nullable ResourceLocation getActiveId(CelestialForgingAnvilBlockEntity be) { + this.resolveLegacyIdentity(be); + return this.activeMegastructureId; } @Nullable public IMegastructureHandler getActiveHandler(CelestialForgingAnvilBlockEntity be) { - CelestialRefactorOption option = getActiveOption(be); - if (option == null) return null; - return handlers.get(option.megastructure()); + this.synchronizeRegistryHandlers(); + ResourceLocation id = this.getActiveId(be); + return id == null ? null : this.handlers.get(id); } @Nullable public CelestialRefactorOption getActiveOption(CelestialForgingAnvilBlockEntity be) { - if (activeMegastructureIndex < 0 || be.getCelestialBodyData() == null) return null; - var options = CelestialRefactorRegistry.getOptions( + ResourceLocation id = this.getActiveId(be); + if (id == null) return null; + CelestialRefactorOption option = CelestialRefactorRegistry.getOption( + id, be.getCelestialBodyData(), be.isAmplify(), be.getPlanetaryResourceSet() ); - if (activeMegastructureIndex >= options.size()) return null; - return options.get(activeMegastructureIndex); + if (option != null) { + this.updateActiveIndex(be, option.id()); + } + return option; + } + + private void updateActiveIndex(CelestialForgingAnvilBlockEntity be, ResourceLocation id) { + List options = CelestialRefactorRegistry.getOptions( + be.getCelestialBodyData(), + be.isAmplify(), + be.getPlanetaryResourceSet() + ); + for (int index = 0; index < options.size(); index++) { + if (id.equals(options.get(index).id())) { + this.legacyActiveMegastructureIndex = index; + return; + } + } } @Nullable @SuppressWarnings("unchecked") public T findHandler(Class type) { - for (var handler : handlers.values()) { + this.synchronizeRegistryHandlers(); + for (IMegastructureHandler handler : this.handlers.values()) { if (type.isInstance(handler)) return (T) handler; } return null; } public WormholeStabilizerHandler getWormholeHandler() { - return findHandler(WormholeStabilizerHandler.class); + return Objects.requireNonNull( + this.findHandler(WormholeStabilizerHandler.class), + "Wormhole stabilizer handler is not registered" + ); } // === Tick === public void serverTick(CelestialForgingAnvilBlockEntity be) { - if (activeMegastructureIndex >= 0) { - IMegastructureHandler active = getActiveHandler(be); - if (active != null) { - active.serverTick(be); + IMegastructureHandler active = this.getActiveHandler(be); + if (active != null) { + active.serverTick(be); + } + if (this.hasActiveMegastructure()) this.syncLaserRequirements(be); + boolean acceleratorTicked = false; + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.auxiliary()) continue; + IMegastructureHandler handler = this.handlers.get(definition.id()); + if (handler != null && handler != active && handler.isAuxiliaryActive(be)) { + handler.serverTick(be); + acceleratorTicked |= handler == this.acceleratorHandler; } - syncLaserRequirements(be); } - if (acceleratorHandler.isActive()) { - acceleratorHandler.serverTick(be); + if (!acceleratorTicked + && this.acceleratorHandler != active + && this.acceleratorHandler.isAuxiliaryActive(be)) { + this.acceleratorHandler.serverTick(be); } } // === Laser requirements === public void syncLaserRequirements(CelestialForgingAnvilBlockEntity be) { - IMegastructureHandler active = getActiveHandler(be); + if (be.getLevel() == null) return; + IMegastructureHandler active = this.getActiveHandler(be); if (active == null) { - clearAllLaserRequirements(be); + this.clearAllLaserRequirements(be); return; } + IMegastructureHandler.LaserRequirement requirement = active.getLaserRequirement(); var lasers = CfaInterfaceScanner.findLaserInterfaces(be.getLevel(), be.getBlockPos()); - String name = active.name(); - if ("planet_excavator".equals(name)) { - for (var laser : lasers) { - laser.setLaserRequirement(16, false); - } - } else if ("matter_decompressor".equals(name)) { - for (var laser : lasers) { - laser.setLaserRequirement(1, true); - } - } else { - clearAllLaserRequirements(be); + for (var laser : lasers) { + laser.setLaserRequirement(requirement.level(), requirement.gamma()); } } private void clearAllLaserRequirements(CelestialForgingAnvilBlockEntity be) { + if (be.getLevel() == null) return; var lasers = CfaInterfaceScanner.findLaserInterfaces(be.getLevel(), be.getBlockPos()); for (var laser : lasers) { laser.setLaserRequirement(0, false); @@ -134,109 +228,290 @@ private void clearAllLaserRequirements(CelestialForgingAnvilBlockEntity be) { // === Build / Clear === + /** + * Validates a server-side build request before the CFA consumes its material. + * The option list is resolved again so a stale client index cannot select a + * definition that is no longer available for the current celestial body. + */ + public boolean canBuild(CelestialRefactorOption option, CelestialForgingAnvilBlockEntity be) { + this.synchronizeRegistryHandlers(); + if (!be.isLocked() || be.isSearching() || be.isAcceleratorActive() || be.getCelestialBodyData() == null) { + return false; + } + boolean currentOption = be.getClientVisibleOptions().stream() + .anyMatch(candidate -> candidate.id().equals(option.id()) && candidate.ring() == option.ring()); + if (!currentOption) return false; + IMegastructureHandler handler = this.handlers.get(option.id()); + if (handler == null) return false; + if (option.auxiliary()) { + return !handler.isAuxiliaryActive(be) && !this.isRingOccupied(option.ring(), be); + } + return !this.hasActiveMegastructure() + && this.getActiveAuxiliaryOptionForRing(be, option.ring()) == null; + } + + private boolean isRingOccupied(int ring, CelestialForgingAnvilBlockEntity be) { + CelestialRefactorOption primary = this.getActiveOption(be); + return (primary != null && primary.ring() == ring) + || this.getActiveAuxiliaryOptionForRing(be, ring) != null; + } + public void buildMegastructure(int optionIndex, CelestialForgingAnvilBlockEntity be) { - var body = be.getCelestialBodyData(); - if (body == null) return; - var options = be.getClientVisibleOptions(); + if (be.getCelestialBodyData() == null) return; + List options = be.getClientVisibleOptions(); if (optionIndex < 0 || optionIndex >= options.size()) return; - CelestialRefactorOption option = options.get(optionIndex); - if ("stellar_evolution_accelerator".equals(option.megastructure())) { - acceleratorHandler.onBuild(be); + this.buildMegastructure(options.get(optionIndex), optionIndex, be); + } + + /** Builds a resolved option; retained as an overload for 26.1-style callers. */ + public void buildMegastructure(CelestialRefactorOption option, CelestialForgingAnvilBlockEntity be) { + List options = be.getClientVisibleOptions(); + this.buildMegastructure(option, options.indexOf(option), be); + } + + private void buildMegastructure( + CelestialRefactorOption option, + int optionIndex, + CelestialForgingAnvilBlockEntity be + ) { + this.synchronizeRegistryHandlers(); + if (optionIndex < 0) return; + IMegastructureHandler handler = this.handlers.get(option.id()); + if (option.auxiliary()) { + if (handler != null + && !handler.isAuxiliaryActive(be) + && !this.isRingOccupied(option.ring(), be)) { + handler.onBuild(be); + } return; } - if (activeMegastructureIndex >= 0) return; - this.activeMegastructureIndex = optionIndex; - IMegastructureHandler handler = handlers.get(option.megastructure()); - if (handler != null) { - handler.onBuild(be); + if (this.hasActiveMegastructure() + || this.getActiveAuxiliaryOptionForRing(be, option.ring()) != null) { + return; } + this.activeMegastructureId = option.id(); + this.legacyActiveMegastructureIndex = Math.max(optionIndex, 0); + this.legacyActiveMegastructureName = null; + this.legacyActiveMegastructureRing = -1; + if (handler != null) handler.onBuild(be); } public void clearMegastructure(CelestialForgingAnvilBlockEntity be) { - if (activeMegastructureIndex >= 0) { - IMegastructureHandler handler = getActiveHandler(be); - if (handler != null) { + IMegastructureHandler handler = this.getActiveHandler(be); + if (handler != null) handler.onClear(be); + this.activeMegastructureId = null; + this.clearLegacyIdentity(); + this.clearAllLaserRequirements(be); + } + + public void clearAllMegastructures(CelestialForgingAnvilBlockEntity be) { + this.clearAuxiliaryMegastructures(be); + this.clearMegastructure(be); + } + + public void clearAuxiliaryMegastructures(CelestialForgingAnvilBlockEntity be) { + this.synchronizeRegistryHandlers(); + boolean acceleratorCleared = false; + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.auxiliary()) continue; + IMegastructureHandler handler = this.handlers.get(definition.id()); + if (handler != null && handler.isAuxiliaryActive(be)) { handler.onClear(be); + acceleratorCleared |= handler == this.acceleratorHandler; } } - this.activeMegastructureIndex = -1; - clearAllLaserRequirements(be); + if (!acceleratorCleared && this.acceleratorHandler.isAuxiliaryActive(be)) { + this.acceleratorHandler.onClear(be); + } } - public void clearAllMegastructures(CelestialForgingAnvilBlockEntity be) { - acceleratorHandler.onClear(be); - clearMegastructure(be); + public boolean hasActiveAuxiliary(CelestialForgingAnvilBlockEntity be) { + this.synchronizeRegistryHandlers(); + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.auxiliary()) continue; + IMegastructureHandler handler = this.handlers.get(definition.id()); + if (handler != null && handler.isAuxiliaryActive(be)) return true; + } + return this.acceleratorHandler.isAuxiliaryActive(be); + } + + public @Nullable CelestialRefactorOption getActiveAuxiliaryOptionForRing( + CelestialForgingAnvilBlockEntity be, + int ring + ) { + this.synchronizeRegistryHandlers(); + boolean acceleratorVisited = false; + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.auxiliary()) continue; + IMegastructureHandler handler = this.handlers.get(definition.id()); + if (handler == null || !handler.isAuxiliaryActive(be)) continue; + acceleratorVisited |= handler == this.acceleratorHandler; + CelestialRefactorOption option = CelestialRefactorRegistry.getOption( + definition.id(), + be.getCelestialBodyData(), + be.isAmplify(), + be.getPlanetaryResourceSet() + ); + if (option != null && option.ring() == ring) return option; + } + if (!acceleratorVisited && this.acceleratorHandler.isAuxiliaryActive(be)) { + CelestialRefactorOption option = CelestialRefactorRegistry.getOption( + AnvilCraft.of("stellar_evolution_accelerator"), + be.getCelestialBodyData(), + be.isAmplify(), + be.getPlanetaryResourceSet() + ); + if (option != null && option.ring() == ring) return option; + } + return null; + } + + /** Releases world-scoped runtime state while retaining data needed after a chunk reload. */ + public void unload(CelestialForgingAnvilBlockEntity be) { + this.synchronizeRegistryHandlers(); + for (IMegastructureHandler handler : this.handlers.values()) { + handler.onUnload(be); + } } // === NBT === public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { - tag.putInt("activeMegastructure", activeMegastructureIndex); - for (var handler : handlers.values()) { + this.synchronizeRegistryHandlers(); + if (this.activeMegastructureId != null) { + tag.putString("activeMegastructureId", this.activeMegastructureId.toString()); + } + tag.putInt("activeMegastructure", this.legacyActiveMegastructureIndex); + if (this.legacyActiveMegastructureName != null) { + tag.putString("activeMegastructureName", this.legacyActiveMegastructureName); + tag.putInt("activeMegastructureRing", this.legacyActiveMegastructureRing); + } + for (IMegastructureHandler handler : this.handlers.values()) { handler.saveAdditional(tag, registries); } - acceleratorHandler.saveAdditional(tag, registries); } public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { - this.activeMegastructureIndex = tag.contains("activeMegastructure") - ? tag.getInt("activeMegastructure") : -1; - for (var handler : handlers.values()) { + this.synchronizeRegistryHandlers(); + this.loadActiveIdentity( + tag.contains("activeMegastructureId") ? tag.getString("activeMegastructureId") : "", + tag.contains("activeMegastructureName") ? tag.getString("activeMegastructureName") : "", + tag.contains("activeMegastructureRing") ? tag.getInt("activeMegastructureRing") : -1, + tag.contains("activeMegastructure") ? tag.getInt("activeMegastructure") : -1 + ); + for (IMegastructureHandler handler : this.handlers.values()) { handler.loadAdditional(tag, registries); } - acceleratorHandler.loadAdditional(tag, registries); } public void writeUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { - tag.putInt("activeMegastructure", activeMegastructureIndex); - for (var handler : handlers.values()) { + this.synchronizeRegistryHandlers(); + if (this.activeMegastructureId != null) { + tag.putString("activeMegastructureId", this.activeMegastructureId.toString()); + } + tag.putInt("activeMegastructure", this.legacyActiveMegastructureIndex); + for (IMegastructureHandler handler : this.handlers.values()) { handler.writeUpdateTag(tag, registries); } - acceleratorHandler.writeUpdateTag(tag, registries); } public void readUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { - this.activeMegastructureIndex = tag.contains("activeMegastructure") - ? tag.getInt("activeMegastructure") : -1; - for (var handler : handlers.values()) { + this.synchronizeRegistryHandlers(); + this.loadActiveIdentity( + tag.contains("activeMegastructureId") ? tag.getString("activeMegastructureId") : "", + tag.contains("activeMegastructureName") ? tag.getString("activeMegastructureName") : "", + tag.contains("activeMegastructureRing") ? tag.getInt("activeMegastructureRing") : -1, + tag.contains("activeMegastructure") ? tag.getInt("activeMegastructure") : -1 + ); + for (IMegastructureHandler handler : this.handlers.values()) { handler.readUpdateTag(tag, registries); } - acceleratorHandler.readUpdateTag(tag, registries); + } + + private void resolveLegacyIdentity(CelestialForgingAnvilBlockEntity be) { + if (this.activeMegastructureId != null) return; + if (this.legacyActiveMegastructureName != null) { + this.activeMegastructureId = CelestialRefactorRegistry.findLegacyId( + this.legacyActiveMegastructureName, + this.legacyActiveMegastructureRing, + be.getCelestialBodyData(), + be.isAmplify(), + be.getPlanetaryResourceSet() + ); + } + if (this.activeMegastructureId == null && this.legacyActiveMegastructureIndex >= 0) { + this.activeMegastructureId = CelestialRefactorRegistry.findLegacyIdByIndex( + this.legacyActiveMegastructureIndex, + be.getCelestialBodyData(), + be.isAmplify(), + be.getPlanetaryResourceSet() + ); + } + if (this.activeMegastructureId != null) { + this.legacyActiveMegastructureName = null; + this.legacyActiveMegastructureRing = -1; + } + } + + private void loadActiveIdentity(String id, String legacyName, int legacyRing, int legacyIndex) { + ResourceLocation parsed = id.isEmpty() ? null : ResourceLocation.tryParse(id); + this.activeMegastructureId = parsed; + this.legacyActiveMegastructureIndex = legacyIndex; + if (parsed != null) { + this.legacyActiveMegastructureName = null; + this.legacyActiveMegastructureRing = -1; + } else { + this.legacyActiveMegastructureName = legacyName.isEmpty() ? null : legacyName; + this.legacyActiveMegastructureRing = legacyRing; + } + } + + private void clearLegacyIdentity() { + this.legacyActiveMegastructureIndex = -1; + this.legacyActiveMegastructureName = null; + this.legacyActiveMegastructureRing = -1; } // === Power === public int getInputPower(CelestialForgingAnvilBlockEntity be) { - IMegastructureHandler handler = getActiveHandler(be); - return handler != null ? handler.getInputPower(be) : 0; + IMegastructureHandler handler = this.getActiveHandler(be); + return handler == null ? 0 : handler.getInputPower(be); } public int getOutputPower(CelestialForgingAnvilBlockEntity be) { - IMegastructureHandler handler = getActiveHandler(be); - return handler != null ? handler.getOutputPower(be) : 0; + IMegastructureHandler handler = this.getActiveHandler(be); + return handler == null ? 0 : handler.getOutputPower(be); } public boolean isInfinitePower(CelestialForgingAnvilBlockEntity be) { - if (!be.isAcceleratorActive() || be.getAcceleratorStage() != 1 || !be.isAmplifierPresent()) { - return false; - } - CelestialRefactorOption option = getActiveOption(be); - return option != null && option.megastructure().contains("dyson_sphere"); + if (!be.isAcceleratorActive() || be.getAcceleratorStage() != 1 || !be.isAmplifierPresent()) return false; + ResourceLocation id = this.getActiveId(be); + return AnvilCraft.of("dyson_sphere_small").equals(id) + || AnvilCraft.of("dyson_sphere_large").equals(id); } public PowerComponentType getComponentType(CelestialForgingAnvilBlockEntity be) { - IMegastructureHandler handler = getActiveHandler(be); - if (handler != null) { - return handler.getComponentType(); - } - return PowerComponentType.CONSUMER; + IMegastructureHandler handler = this.getActiveHandler(be); + return handler == null ? PowerComponentType.CONSUMER : handler.getComponentType(); } public void gridTick(CelestialForgingAnvilBlockEntity be) { - IMegastructureHandler handler = getActiveHandler(be); - if (handler != null) { - handler.gridTick(be); + IMegastructureHandler active = this.getActiveHandler(be); + if (active != null) active.gridTick(be); + boolean acceleratorTicked = false; + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.auxiliary()) continue; + IMegastructureHandler handler = this.handlers.get(definition.id()); + if (handler != null && handler != active && handler.isAuxiliaryActive(be)) { + handler.gridTick(be); + acceleratorTicked |= handler == this.acceleratorHandler; + } + } + if (!acceleratorTicked + && this.acceleratorHandler != active + && this.acceleratorHandler.isAuxiliaryActive(be)) { + this.acceleratorHandler.gridTick(be); } - acceleratorHandler.gridTick(be); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java new file mode 100644 index 0000000000..e2dd80a4a8 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CfaSearchController.java @@ -0,0 +1,320 @@ +package dev.dubhe.anvilcraft.block.entity; + +import dev.dubhe.anvilcraft.api.power.PowerGrid; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyMatcher; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialSeedMatcher; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialSnapshotCodec; +import dev.dubhe.anvilcraft.block.entity.celestial.PlanetResourceGenerator; +import dev.dubhe.anvilcraft.block.entity.celestial.PlanetaryResourceSet; +import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.StarData; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +import javax.annotation.Nullable; + +/** State machine for the ten-second CFA celestial search. */ +final class CfaSearchController { + private static final int SEARCH_DURATION = 200; + private static final int SEED_SLOT = 4; + private static final String CAPTURED_SEED_KEY = "searchCapturedSeed"; + private static final String SEED_STATE_KNOWN_KEY = "searchSeedStateKnown"; + + private int ticksRemaining; + private boolean searching; + private boolean failed; + private boolean powerInsufficient; + private @Nullable Item capturedSeedItem; + private @Nullable ItemStack capturedSeedStack; + private @Nullable CompoundTag capturedSnapshot; + + void start(CelestialForgingAnvilBlockEntity owner) { + this.failed = false; + this.powerInsufficient = false; + + Level level = owner.getLevel(); + ItemStack seedStack = owner.getAnvilInventory().getItem(SEED_SLOT); + boolean hasSeed = !seedStack.isEmpty(); + if (level != null && !level.isClientSide() && !hasSeed + && CelestialBodyMatcher.match( + owner.getAnvilCount(0), + owner.getAnvilCount(1), + owner.getAnvilCount(2), + owner.getAnvilCount(3), + owner.isAmplify(), + level.getRandom() + ) == null) { + this.stop(true, false); + sync(owner); + return; + } + + if (!hasEnoughPower(owner)) { + this.stop(false, true); + sync(owner); + return; + } + + this.capturedSeedItem = hasSeed ? seedStack.getItem() : null; + this.capturedSeedStack = hasSeed ? seedStack.copy() : null; + this.capturedSnapshot = hasSeed ? CelestialSnapshotCodec.extract(seedStack) : null; + owner.setCelestialBodyData(null); + this.ticksRemaining = SEARCH_DURATION; + this.searching = true; + sync(owner); + } + + void serverTick(CelestialForgingAnvilBlockEntity owner) { + boolean enoughPower = hasEnoughPower(owner); + if (enoughPower != !this.powerInsufficient) { + this.powerInsufficient = !enoughPower; + sync(owner); + } + if (this.ticksRemaining <= 0) return; + if (!enoughPower) { + this.stop(false, true); + sync(owner); + return; + } + + this.ticksRemaining--; + if (this.ticksRemaining == 0) { + this.searching = false; + this.match(owner); + this.failed = owner.getCelestialBodyData() == null; + sync(owner); + } + } + + @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") + void match(CelestialForgingAnvilBlockEntity owner) { + Level level = owner.getLevel(); + if (level == null) return; + + int time = owner.getAnvilCount(0); + int space = owner.getAnvilCount(1); + int mass = owner.getAnvilCount(2); + int energy = owner.getAnvilCount(3); + owner.setAgeAnvilCount(time); + owner.setBodySeed(level.getRandom().nextLong()); + owner.setStellarMass(mass); + + ItemStack currentSeed = owner.getAnvilInventory().getItem(SEED_SLOT); + boolean capturedSeedMatches = this.capturedSeedStack != null + && !currentSeed.isEmpty() + && ItemStack.isSameItemSameComponents(currentSeed, this.capturedSeedStack); + if ((this.capturedSeedItem != null || this.capturedSnapshot != null) + && !capturedSeedMatches) { + this.capturedSeedItem = null; + this.capturedSeedStack = null; + this.capturedSnapshot = null; + } + + if (this.capturedSnapshot != null && this.capturedSnapshot.contains("celestialBody")) { + applySnapshot(owner, this.capturedSnapshot); + consumeSeed(owner, capturedSeedMatches); + sync(owner); + return; + } + + if (this.capturedSeedItem == Items.PLAYER_HEAD) { + SpecialCelestialBodyData playerHead = CelestialSeedMatcher.fromPlayerHead(currentSeed, space); + if (playerHead != null) { + applyResult(owner, playerHead, new PlanetaryResourceSet()); + consumeSeed(owner, capturedSeedMatches); + sync(owner); + return; + } + } + + if (this.capturedSeedItem != null && level instanceof ServerLevel serverLevel) { + CelestialSeedMatcher.Result special = CelestialSeedMatcher.match( + serverLevel, + time, + space, + mass, + energy, + this.capturedSeedItem + ); + if (special != null) { + applyResult(owner, special.body(), special.resources()); + consumeSeed(owner, capturedSeedMatches); + sync(owner); + return; + } + } + + CelestialBodyData body = CelestialBodyMatcher.match( + time, + space, + mass, + energy, + owner.isAmplify(), + level.getRandom() + ); + if (body instanceof StarData star && star.bodyUuid() == null) { + body = star.withBodyUuid(StarData.uuidFromBodySeed(owner.getBodySeed())); + } + PlanetaryResourceSet resources = body == null || level.isClientSide() + ? null + : PlanetResourceGenerator.generate( + body, + time, + level, + owner.getBodySeed(), + this.capturedSeedItem == null + ? null + : BuiltInRegistries.ITEM.getKey(this.capturedSeedItem) + ); + owner.setCelestialBodyData(body); + owner.setPlanetaryResourceSet(resources); + if (body != null) { + owner.addToSearchHistory(body, resources); + } else { + this.ticksRemaining = 0; + } + consumeSeed(owner, capturedSeedMatches); + sync(owner); + } + + void loadPersistent( + boolean searching, + int ticksRemaining, + boolean failed, + boolean powerInsufficient, + CompoundTag tag, + HolderLookup.Provider registries + ) { + this.loadState(searching, ticksRemaining, failed, powerInsufficient); + if (!this.searching) return; + if (!(tag.get(CAPTURED_SEED_KEY) instanceof CompoundTag seedTag)) { + if (!tag.getBoolean(SEED_STATE_KNOWN_KEY)) { + // Older saves did not persist the seed used by an in-flight search. + // Stop instead of granting a result without safely consuming that seed. + this.stop(false, false); + } + return; + } + ItemStack.parse(registries, seedTag).ifPresent(stack -> { + this.capturedSeedStack = stack; + this.capturedSeedItem = stack.getItem(); + this.capturedSnapshot = CelestialSnapshotCodec.extract(stack); + }); + if (this.capturedSeedStack == null || this.capturedSeedStack.isEmpty()) { + this.stop(false, false); + } + } + + void loadSynced( + boolean searching, + int ticksRemaining, + boolean failed, + boolean powerInsufficient + ) { + this.loadState(searching, ticksRemaining, failed, powerInsufficient); + } + + private void loadState( + boolean searching, + int ticksRemaining, + boolean failed, + boolean powerInsufficient + ) { + this.searching = searching && ticksRemaining > 0; + this.ticksRemaining = Math.max(0, ticksRemaining); + this.failed = failed; + this.powerInsufficient = powerInsufficient; + this.capturedSeedItem = null; + this.capturedSeedStack = null; + this.capturedSnapshot = null; + } + + void save(CompoundTag tag, HolderLookup.Provider registries) { + tag.remove(CAPTURED_SEED_KEY); + tag.remove(SEED_STATE_KNOWN_KEY); + if (this.searching) { + tag.putBoolean(SEED_STATE_KNOWN_KEY, true); + } + if (this.searching && this.capturedSeedStack != null) { + tag.put(CAPTURED_SEED_KEY, this.capturedSeedStack.save(registries)); + } + } + + int ticksRemaining() { + return this.ticksRemaining; + } + + boolean isSearching() { + return this.searching; + } + + boolean hasFailed() { + return this.failed; + } + + void setFailed(boolean failed) { + this.failed = failed; + } + + boolean isPowerInsufficient() { + return this.powerInsufficient; + } + + private static void applySnapshot(CelestialForgingAnvilBlockEntity owner, CompoundTag tag) { + owner.setCelestialBodyData(CelestialBodyData.fromTag(tag.getCompound("celestialBody"))); + owner.setBodySeed(tag.getLong("bodySeed")); + owner.setAgeAnvilCount(tag.getInt("ageAnvilCount")); + owner.setStellarMass(tag.getInt("stellarMass")); + PlanetaryResourceSet resources = tag.contains("planetaryResources") + ? PlanetaryResourceSet.fromTag(tag.getCompound("planetaryResources")) + : null; + owner.setPlanetaryResourceSet(resources); + if (owner.getCelestialBodyData() != null) { + owner.addToSearchHistory(owner.getCelestialBodyData(), resources); + } + } + + private static void applyResult( + CelestialForgingAnvilBlockEntity owner, + CelestialBodyData body, + @Nullable PlanetaryResourceSet resources + ) { + owner.setCelestialBodyData(body); + owner.setPlanetaryResourceSet(resources); + owner.addToSearchHistory(body, resources); + } + + private static void consumeSeed(CelestialForgingAnvilBlockEntity owner, boolean capturedSeedMatches) { + Level level = owner.getLevel(); + if (level == null || level.isClientSide() || !capturedSeedMatches) return; + if (!owner.getAnvilInventory().getItem(SEED_SLOT).isEmpty()) { + owner.getAnvilInventory().setItem(SEED_SLOT, ItemStack.EMPTY); + } + } + + private static boolean hasEnoughPower(CelestialForgingAnvilBlockEntity owner) { + PowerGrid grid = owner.getGrid(); + return grid != null && (owner.getInputPower() <= 0 || grid.isWorking()); + } + + private void stop(boolean failed, boolean powerInsufficient) { + this.failed = failed; + this.powerInsufficient = powerInsufficient; + this.searching = false; + this.ticksRemaining = 0; + this.capturedSeedItem = null; + this.capturedSeedStack = null; + this.capturedSnapshot = null; + } + + private static void sync(CelestialForgingAnvilBlockEntity owner) { + owner.markSearchStateChanged(); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeGeneratorBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeGeneratorBlockEntity.java index f18c6df6e5..3995014cf5 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeGeneratorBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeGeneratorBlockEntity.java @@ -28,9 +28,12 @@ @Getter public class CreativeGeneratorBlockEntity extends BlockEntity implements IPowerProducer, IPowerConsumer, MenuProvider { + public static final int MAX_POWER = 65536; + private static final int DEFAULT_POWER = 8192; + private @Nullable PowerGrid grid = null; - private int power = 8192; + private int power = DEFAULT_POWER; private int time = 0; private boolean previousSyncFailed = false; @@ -58,7 +61,7 @@ protected void saveAdditional(CompoundTag tag, HolderLookup.Provider provider) { @Override public void loadAdditional(CompoundTag tag, HolderLookup.Provider provider) { super.loadAdditional(tag, provider); - this.power = tag.getInt("power"); + this.power = Math.clamp(tag.getInt("power"), -MAX_POWER, MAX_POWER); } @Override @@ -103,7 +106,7 @@ public void writeClientSideData(AbstractContainerMenu menu, RegistryFriendlyByte } public void setPower(int power) { - this.power = power; + this.power = Math.clamp(power, -MAX_POWER, MAX_POWER); if (level instanceof ServerLevel) { if (grid != null) { this.grid.markChanged(); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeLaserBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeLaserBlockEntity.java index 05ff432c73..8c5b15474c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeLaserBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/CreativeLaserBlockEntity.java @@ -66,6 +66,11 @@ public void setGamma(boolean gamma) { this.setChanged(); } + @Override + public boolean isEmittingGamma() { + return this.gamma; + } + @Override protected int getBaseLaserLevel() { return this.configuredLevel; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/RubyPrismBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/RubyPrismBlockEntity.java index 74efa7f3d8..794f9b266b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/RubyPrismBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/RubyPrismBlockEntity.java @@ -6,6 +6,7 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; public class RubyPrismBlockEntity extends BaseLaserBlockEntity { private boolean enabled = false; @@ -66,7 +67,7 @@ public int getLaserLevel() { } @Override - public void clientUpdate(BlockPos irradiateBlockPos, int laserLevel) { + public void clientUpdate(@Nullable BlockPos irradiateBlockPos, int laserLevel) { enabled = laserLevel > 0; super.clientUpdate(irradiateBlockPos, laserLevel); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialBodyMatcher.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialBodyMatcher.java index e5d69090c3..b5f3e35455 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialBodyMatcher.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialBodyMatcher.java @@ -358,7 +358,7 @@ private static CelestialBodyData generateBrownDwarf(int space, int energy, Rando CelestialBodyClass.BROWN_DWARF, PressureType.GAS, WindSpeed.HIGH, RingType.NONE, size, baseRow, overlayRow, - randomAxialTilt(random), randomRotationSpeed(random), mag, true + randomAxialTilt(random), randomRotationSpeed(random), mag, true, energy ); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorOption.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorOption.java index c44f8a3356..7c33d14a87 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorOption.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorOption.java @@ -1,55 +1,148 @@ package dev.dubhe.anvilcraft.block.entity.celestial; +import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilBlockEntity; +import dev.dubhe.anvilcraft.block.entity.megastructure.BaseMegastructureHandler; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ItemLike; -/// 表示天体约束环的可能重构选项。 -/// 每个选项将一个环转化为巨构建筑。 -/// -/// ring - 要重构的环编号(R1-R6) -/// megastructure - 巨构模型名称后缀(例如"eco_station"、"dyson_sphere") -/// modelLocation - 巨构模型的位置。客户端渲染时转换为客户端模型标识。 -/// displayName - 巨构显示名称的翻译键 -/// material - 所需建筑材料的物品栈,若无则为 {@link ItemStack#EMPTY} -/// materialCount - 所需材料的数量 +import javax.annotation.Nullable; + +/** A megastructure definition resolved for one celestial body. */ public record CelestialRefactorOption( + Megastructure definition, + @Nullable Megastructure.Context context, int ring, - String megastructure, ResourceLocation modelLocation, String displayName, ItemStack material, int materialCount ) { - /// 创建一个不需要建筑材料的重构选项。 + /** Resolve a registered definition for the supplied CFA context. */ + public static CelestialRefactorOption resolve(Megastructure definition, Megastructure.Context context) { + int ring = definition.ring(context); + return new CelestialRefactorOption( + definition, + context, + ring, + definition.modelLocation(ring), + definition.displayName(), + definition.material(), + definition.materialCount() + ); + } + + /** Compatibility constructor for add-ons written against the pre-registry API. */ + public CelestialRefactorOption( + int ring, + String megastructure, + ResourceLocation modelLocation, + String displayName, + ItemStack material, + int materialCount + ) { + this( + legacyDefinition(ring, megastructure, modelLocation, displayName), + null, + ring, + modelLocation, + displayName, + material, + materialCount + ); + } + + public ResourceLocation id() { + return this.definition.id(); + } + + /** Legacy name accessor retained for handlers, renderers and add-ons. */ + public String megastructure() { + return this.definition.name(); + } + + public boolean auxiliary() { + return this.definition.auxiliary(); + } + + public float rotation(float baseRotation, float bodyRotation) { + if (this.context == null) return baseRotation; + return this.definition.rotation(this.context, this.ring, baseRotation, bodyRotation); + } + + public boolean needsMaterial() { + return this.materialCount > 0 && !this.material.isEmpty(); + } + + /** + * Creates an option in the old shape. New code should register a + * {@link Megastructure} instead, but keeping these factories avoids an + * unnecessary source break for add-ons targeting 1.21. + */ + @Deprecated public static CelestialRefactorOption noMaterial( int ring, String megastructure, ResourceLocation modelLocation, String displayName ) { return new CelestialRefactorOption( - ring, megastructure, modelLocation, displayName, ItemStack.EMPTY, 0 + ring, + megastructure, + modelLocation, + displayName, + ItemStack.EMPTY, + 0 ); } - /// 创建一个需要建筑材料的重构选项。 - /// - /// ring - 环编号(1-6) - /// megastructure - 巨构名称后缀 - /// modelLocation - 完整模型定位符 - /// displayName - 翻译键 - /// material - 所需物品 - /// materialCount - 所需物品数量 + @Deprecated public static CelestialRefactorOption withMaterial( - int ring, String megastructure, ResourceLocation modelLocation, String displayName, - ItemLike material, int materialCount + int ring, + String megastructure, + ResourceLocation modelLocation, + String displayName, + ItemLike material, + int materialCount ) { return new CelestialRefactorOption( - ring, megastructure, modelLocation, displayName, - new ItemStack(material), materialCount + ring, + megastructure, + modelLocation, + displayName, + new ItemStack(material), + materialCount ); } - public boolean needsMaterial() { - return materialCount > 0 && !material.isEmpty(); + private static Megastructure legacyDefinition( + int ring, + String name, + ResourceLocation modelLocation, + String displayName + ) { + Megastructure.Builder builder = Megastructure.builder(AnvilCraft.of(name), name) + .displayName(displayName) + .ring(ring) + .model(ring, modelLocation) + .handler(() -> new LegacyHandler(name)); + if ("stellar_evolution_accelerator".equals(name)) builder.auxiliary(); + return builder.build(); + } + + private static final class LegacyHandler extends BaseMegastructureHandler { + private final String name; + + private LegacyHandler(String name) { + this.name = name; + } + + @Override + public String name() { + return this.name; + } + + @Override + public void serverTick(CelestialForgingAnvilBlockEntity be) { + } } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorRegistry.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorRegistry.java index 31f98d5d81..c35d78bcd9 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorRegistry.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialRefactorRegistry.java @@ -3,6 +3,7 @@ import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.init.block.ModBlocks; import dev.dubhe.anvilcraft.init.item.ModItems; +import dev.dubhe.anvilcraft.init.registry.ModRegistries; import net.minecraft.resources.ResourceLocation; import java.util.ArrayList; @@ -10,208 +11,255 @@ import java.util.List; import javax.annotation.Nullable; -/// 将天体映射到其可能的天体约束环重构选项的注册表。 -/// 每种天体类型的最内层环: -/// - 小型岩石行星(size < 26):最内环 = R1 -/// - 小型气态行星(size < 26):最内环 = R2 -/// - 小型恒星(size < 26):最内环 = R4 -/// - 大型恒星(size >= 26):最内环 = R5 -/// 共有11种独特巨构建筑,分布在不同环上。 -/// 变体模型(如excavator_off、coil_fix)在世界中单独渲染; -/// UI中仅显示主要模型。 +/** Resolves registered megastructures for a Celestial Forging Anvil context. */ public final class CelestialRefactorRegistry { - private CelestialRefactorRegistry() { } - /// 获取给定天体的最内环编号。 - /// 启用增幅时,最小环始终为4(恒星级别)。 + /** + * Returns the options available for a locked celestial body. + * + *

Every definition is evaluated independently. This removes the old + * chain of name-based filters and lets add-ons contribute prerequisites, + * ring variants and materials through the registry.

+ */ + public static List getOptions( + @Nullable CelestialBodyData body, + boolean amplified, + @Nullable PlanetaryResourceSet resources + ) { + if (body == null) return Collections.emptyList(); + if (!hasRegisteredDefinitions()) { + return legacyOptions(body, amplified, resources); + } + + Megastructure.Context context = new Megastructure.Context(body, amplified, resources); + List options = new ArrayList<>(); + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.isAvailable(context)) continue; + int ring = definition.ring(context); + if (!isRingSupported(ring, amplified)) continue; + options.add(CelestialRefactorOption.resolve(definition, context)); + } + return options; + } + + /** Resolves one registered option by stable ID for persisted CFA state. */ + public static @Nullable CelestialRefactorOption getOption( + ResourceLocation id, + @Nullable CelestialBodyData body, + boolean amplified, + @Nullable PlanetaryResourceSet resources + ) { + if (body == null) return null; + Megastructure definition = get(id); + if (definition != null) { + Megastructure.Context context = new Megastructure.Context(body, amplified, resources); + // Availability is a build-time condition. An already-built + // structure remains resolvable while the celestial body evolves. + return CelestialRefactorOption.resolve(definition, context); + } + // Legacy/fallback registries may not expose a Holder for the ID. + for (CelestialRefactorOption option : getOptions(body, amplified, resources)) { + if (id.equals(option.id())) return option; + } + return null; + } + + /** Returns a definition by ID, or {@code null} when an add-on was removed. */ + public static @Nullable Megastructure get(ResourceLocation id) { + return ModRegistries.MEGASTRUCTURE.get(id); + } + + /** + * Maps the old name/ring pair used by 1.21 NBT to a stable definition ID. + */ + public static @Nullable ResourceLocation findLegacyId( + String name, + int ring, + @Nullable CelestialBodyData body, + boolean amplified, + @Nullable PlanetaryResourceSet resources + ) { + Megastructure.Context context = body == null ? null : new Megastructure.Context(body, amplified, resources); + ResourceLocation nameMatch = null; + for (Megastructure definition : ModRegistries.MEGASTRUCTURE) { + if (!definition.name().equals(name)) continue; + nameMatch = definition.id(); + if (context != null && definition.ring(context) == ring) return definition.id(); + } + if (nameMatch != null) return nameMatch; + for (CelestialRefactorOption option : getOptionsForRing(0, 6)) { + if (option.megastructure().equals(name) && option.ring() == ring) return option.id(); + } + return null; + } + + /** + * Resolves the numeric option index stored by pre-registry 1.21 worlds. + * The old index is tied to the former hard-coded option order and must not + * be interpreted using the iteration order of the extensible registry. + */ + public static @Nullable ResourceLocation findLegacyIdByIndex( + int index, + @Nullable CelestialBodyData body, + boolean amplified, + @Nullable PlanetaryResourceSet resources + ) { + if (index < 0 || body == null) return null; + List options = legacyOptions(body, amplified, resources); + return index < options.size() ? options.get(index).id() : null; + } + + /** Compatibility helper retained for blueprint/UI code and add-ons. */ public static int getInnermostRing(CelestialBodyData body, boolean amplified) { boolean isLarge = body.size() >= 48; int ring = switch (body) { - case StarData ignored -> isLarge ? 5 : 4; + case StarData star -> star.specialRedDwarf() ? 2 : (isLarge ? 5 : 4); case GiantPlanetData ignored -> 2; case RockyPlanetData ignored -> 1; - case SpecialCelestialBodyData s -> s.isErrorPlanet() ? 0 : 1; + case SpecialCelestialBodyData special -> special.isErrorPlanet() ? 0 : 1; }; - if (amplified) { - ring = Math.max(ring, 4); + return amplified ? Math.max(ring, 4) : ring; + } + + /** + * Compatibility view of the old hard-coded list. New code should use + * {@link #getOptions(CelestialBodyData, boolean, PlanetaryResourceSet)}. + */ + public static List getOptionsForRing(int innermostRing, int maxRing) { + List options = new ArrayList<>(); + String prefix = "screen.anvilcraft.cfa.megastructure."; + if (innermostRing <= 1 && 1 <= maxRing) { + options.add(CelestialRefactorOption.withMaterial(1, "planet_excavator", ringModel(1, "excavator"), + prefix + "planet_excavator", ModBlocks.RUBY_PRISM.asItem(), 16)); + options.add(CelestialRefactorOption.withMaterial(1, "planet_exctractor", ringModel(1, "exctractor"), + prefix + "planet_exctractor", ModBlocks.PUMP.asItem(), 16)); + options.add(CelestialRefactorOption.withMaterial(1, "eco_station", ringModel(1, "eco_station"), + prefix + "eco_station", ModBlocks.TEMPERING_GLASS.asItem(), 64)); + options.add(CelestialRefactorOption.withMaterial(1, "temple", ringModel(1, "temple"), + prefix + "temple", ModBlocks.ENCHANTED_GOLD_BLOCK.asItem(), 64)); + } + if (innermostRing <= 2 && 2 <= maxRing) { + options.add(CelestialRefactorOption.withMaterial(2, "giant_planet_exctractor", + ringModel(2, "exctractor"), prefix + "giant_planet_exctractor", ModBlocks.PUMP.asItem(), 32)); + options.add(CelestialRefactorOption.withMaterial(2, "dyson_sphere_brown_dwarf", + ringModel(2, "dyson_sphere"), prefix + "dyson_sphere_brown_dwarf", + ModItems.DYSON_SPHERE_COMPONENT.get(), 8)); + } + if (innermostRing <= 4 && 4 <= maxRing) { + options.add(CelestialRefactorOption.withMaterial(4, "stellar_ring_collider", ringModel(4, "collider"), + prefix + "stellar_ring_collider", ModItems.STELLAR_RING_COMPONENT.get(), 8)); + options.add(CelestialRefactorOption.withMaterial(4, "dyson_sphere_small", ringModel(4, "dyson_sphere"), + prefix + "dyson_sphere_small", ModItems.DYSON_SPHERE_COMPONENT.get(), 16)); + options.add(CelestialRefactorOption.withMaterial(4, "magnetar_coil", ringModel(4, "coil"), + prefix + "magnetar_coil", ModItems.MAGNETAR_COIL_COMPONENT.get(), 4)); + options.add(CelestialRefactorOption.withMaterial(4, "penrose_sphere", ringModel(4, "penrose_sphere"), + prefix + "penrose_sphere", ModItems.PENROSE_SPHERE_COMPONENT.get(), 8)); + options.add(CelestialRefactorOption.withMaterial(4, "matter_decompressor", + ringModel(4, "matter_decompressor"), prefix + "matter_decompressor", + ModItems.MATTER_DECOMPRESSOR_COMPONENT.get(), 2)); + options.add(CelestialRefactorOption.withMaterial(4, "wormhole_stabilizer", + ringModel(4, "wormhole_stabilizer"), prefix + "wormhole_stabilizer", + ModItems.WORMHOLE_STABILIZER_COMPONENT.get(), 4)); + options.add(CelestialRefactorOption.withMaterial(5, "stellar_evolution_accelerator", + ringModel(5, "stellar_evolution_accelerator"), prefix + "stellar_evolution_accelerator", + ModItems.STELLAR_EVOLUTION_ACCELERATOR_COMPONENT.get(), 8)); } - return ring; + if (innermostRing <= 5 && 5 <= maxRing) { + options.add(CelestialRefactorOption.withMaterial(5, "dyson_sphere_large", ringModel(5, "dyson_sphere"), + prefix + "dyson_sphere_large", ModItems.DYSON_SPHERE_COMPONENT.get(), 32)); + options.add(CelestialRefactorOption.withMaterial(6, "stellar_evolution_accelerator", + ringModel(6, "stellar_evolution_accelerator"), prefix + "stellar_evolution_accelerator", + ModItems.STELLAR_EVOLUTION_ACCELERATOR_COMPONENT.get(), 8)); + } + return options; } - /// 获取已锁定天体的可用重构选项。 - /// 非增幅锻星砧有环1-3 → 显示环1+2的巨构建筑。 - /// 增幅锻星砧有环3-5 → 显示环4+5的巨构建筑。 - /// resources - 行星资源集,用于根据资源可用性过滤选项; - /// 可以为null(最宽松模式,显示所有符合环条件的选项) - public static List getOptions(CelestialBodyData body, boolean amplified, - @Nullable PlanetaryResourceSet resources) { - if (body == null) return Collections.emptyList(); - /// 错误行星无法建造巨构 - if (body instanceof SpecialCelestialBodyData s && s.isErrorPlanet()) { + private static boolean hasRegisteredDefinitions() { + return ModRegistries.MEGASTRUCTURE.iterator().hasNext(); + } + + private static boolean isRingSupported(int ring, boolean amplified) { + return amplified ? ring >= 4 && ring <= 6 : ring >= 1 && ring <= 2; + } + + private static List legacyOptions( + CelestialBodyData body, + boolean amplified, + @Nullable PlanetaryResourceSet resources + ) { + if (body instanceof SpecialCelestialBodyData special && special.isErrorPlanet()) { return Collections.emptyList(); } int innermostRing = getInnermostRing(body, amplified); - int maxRing = amplified ? 5 : 2; + int maxRing = amplified ? 6 : 2; List options = getOptionsForRing(innermostRing, maxRing); - - /// 过滤行星开采器:岩石/特殊行星必须有液体 - if (!hasLiquid(body)) { - options.removeIf(opt -> "planet_exctractor".equals(opt.megastructure())); - } - - /// 过滤巨行星抽取器:仅对巨行星可用 + if (!hasLiquid(body)) options.removeIf(option -> "planet_exctractor".equals(option.megastructure())); if (!(body instanceof GiantPlanetData)) { - options.removeIf(opt -> "giant_planet_exctractor".equals(opt.megastructure())); + options.removeIf(option -> "giant_planet_exctractor".equals(option.megastructure())); + } + if (!((body instanceof GiantPlanetData brown && brown.brownDwarf()) + || (body instanceof StarData star && star.specialRedDwarf()))) { + options.removeIf(option -> "dyson_sphere_brown_dwarf".equals(option.megastructure())); } - - /// 过滤星环对撞机:仅对小型恒星可用(size < 48),黑洞和中子星不可用 if (!(body instanceof StarData star && star.size() < 48 - && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR - && star.bodyClass() != CelestialBodyClass.BLACK_HOLE)) { - options.removeIf(opt -> "stellar_ring_collider".equals(opt.megastructure())); + && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR + && star.bodyClass() != CelestialBodyClass.BLACK_HOLE)) { + options.removeIf(option -> "stellar_ring_collider".equals(option.megastructure())); } - - /// 过滤恒星演化加速器:恒星残骸不可用 - if (body instanceof StarData star - && (star.bodyClass() == CelestialBodyClass.WHITE_DWARF - || star.bodyClass() == CelestialBodyClass.NEUTRON_STAR - || star.bodyClass() == CelestialBodyClass.BLACK_HOLE)) { - options.removeIf(opt -> "stellar_evolution_accelerator".equals(opt.megastructure())); + if (body instanceof StarData star && (star.specialRedDwarf() || star.bodyClass() == CelestialBodyClass.WHITE_DWARF + || star.bodyClass() == CelestialBodyClass.NEUTRON_STAR + || star.bodyClass() == CelestialBodyClass.BLACK_HOLE)) { + options.removeIf(option -> "stellar_evolution_accelerator".equals(option.megastructure())); } - - /// 过滤磁星线圈:仅对中子星可用 if (!(body instanceof StarData star && star.bodyClass() == CelestialBodyClass.NEUTRON_STAR)) { - options.removeIf(opt -> "magnetar_coil".equals(opt.megastructure())); + options.removeIf(option -> "magnetar_coil".equals(option.megastructure())); } - - /// 按恒星大小过滤恒星演化加速器的环变体: - /// 小型恒星用环5模型,大型恒星用环6模型。 if (body instanceof StarData star) { - boolean isLarge = star.size() >= 48; - options.removeIf(opt -> "stellar_evolution_accelerator".equals(opt.megastructure()) - && ((isLarge && opt.ring() == 5) || (!isLarge && opt.ring() == 6))); + boolean large = star.size() >= 48; + options.removeIf(option -> "stellar_evolution_accelerator".equals(option.megastructure()) + && ((large && option.ring() == 5) || (!large && option.ring() == 6))); } - - /// 过滤戴森球:仅恒星可用(黑洞和中子星不可用),小型给小型恒星,大型给大型恒星 if (!(body instanceof StarData star - && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR - && star.bodyClass() != CelestialBodyClass.BLACK_HOLE)) { - options.removeIf(opt -> "dyson_sphere_small".equals(opt.megastructure()) - || "dyson_sphere_large".equals(opt.megastructure())); + && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR + && star.bodyClass() != CelestialBodyClass.BLACK_HOLE)) { + options.removeIf(option -> "dyson_sphere_small".equals(option.megastructure()) + || "dyson_sphere_large".equals(option.megastructure())); } else { - boolean isLarge = star.size() >= 48; - options.removeIf(opt -> "dyson_sphere_small".equals(opt.megastructure()) && isLarge); - options.removeIf(opt -> "dyson_sphere_large".equals(opt.megastructure()) && !isLarge); + boolean large = star.size() >= 48; + options.removeIf(option -> "dyson_sphere_small".equals(option.megastructure()) && large); + options.removeIf(option -> "dyson_sphere_large".equals(option.megastructure()) && !large); } - - /// 过滤彭罗斯球:仅对黑洞可用 if (!(body instanceof StarData star && star.bodyClass() == CelestialBodyClass.BLACK_HOLE)) { - options.removeIf(opt -> "penrose_sphere".equals(opt.megastructure())); + options.removeIf(option -> "penrose_sphere".equals(option.megastructure())); } - - /// 过滤虫洞稳定器:仅对增幅模式下的黑洞可用 if (!(body instanceof StarData star && star.bodyClass() == CelestialBodyClass.BLACK_HOLE && amplified)) { - options.removeIf(opt -> "wormhole_stabilizer".equals(opt.megastructure())); + options.removeIf(option -> "wormhole_stabilizer".equals(option.megastructure())); } - - /// 过滤物质解压器:仅对中子星或黑洞可用 - if (!(body instanceof StarData star - && (star.bodyClass() == CelestialBodyClass.NEUTRON_STAR - || star.bodyClass() == CelestialBodyClass.BLACK_HOLE))) { - options.removeIf(opt -> "matter_decompressor".equals(opt.megastructure())); + if (!(body instanceof StarData star && (star.bodyClass() == CelestialBodyClass.NEUTRON_STAR + || star.bodyClass() == CelestialBodyClass.BLACK_HOLE))) { + options.removeIf(option -> "matter_decompressor".equals(option.megastructure())); } - - /// 过滤生态站:需要生物资源且没有低级文明 if (resources != null) { - options.removeIf(opt -> "eco_station".equals(opt.megastructure()) - && !isEcoStationEligible(resources)); - /// 过滤神庙:需要低级文明 - options.removeIf(opt -> "temple".equals(opt.megastructure()) - && !resources.hasCivilization()); + options.removeIf(option -> "eco_station".equals(option.megastructure()) && !isEcoStationEligible(resources)); + options.removeIf(option -> "temple".equals(option.megastructure()) && !resources.hasCivilization()); } - return options; } - /// 生态站仅在行星拥有生物资源且没有低级文明时可用。 private static boolean hasLiquid(CelestialBodyData body) { if (body instanceof RockyPlanetData rocky) return rocky.liquidCoverage() != LiquidCoverage.NONE; - if (body instanceof SpecialCelestialBodyData s) { - LiquidCoverage lc = s.liquidCoverage(); - return lc != null && lc != LiquidCoverage.NONE; + if (body instanceof SpecialCelestialBodyData special) { + LiquidCoverage coverage = special.liquidCoverage(); + return coverage != null && coverage != LiquidCoverage.NONE; } return false; } private static boolean isEcoStationEligible(PlanetaryResourceSet resources) { - if (resources.hasCivilization()) return false; - return !resources.getBiologicalItems().isEmpty() - || !resources.getBiologicalFluids().isEmpty(); - } - - /// 获取给定环范围的可用巨构选项。 - /// 内层环可以建造外层环可以建造的任何巨构。 - public static List getOptionsForRing(int innermostRing, int maxRing) { - List options = new ArrayList<>(); - String prefix = "screen.anvilcraft.cfa.megastructure."; - - if (innermostRing <= 1 && 1 <= maxRing) { - /// 环1巨构(小型岩石行星的最内环) - options.add(CelestialRefactorOption.withMaterial(1, "planet_excavator", - ringModel(1, "excavator"), prefix + "planet_excavator", - ModBlocks.RUBY_PRISM.asItem(), 16)); - options.add(CelestialRefactorOption.withMaterial(1, "planet_exctractor", - ringModel(1, "exctractor"), prefix + "planet_exctractor", - ModBlocks.PUMP.asItem(), 16)); - options.add(CelestialRefactorOption.withMaterial(1, "eco_station", - ringModel(1, "eco_station"), prefix + "eco_station", - ModBlocks.TEMPERING_GLASS.asItem(), 64)); - options.add(CelestialRefactorOption.withMaterial(1, "temple", - ringModel(1, "temple"), prefix + "temple", - ModBlocks.ENCHANTED_GOLD_BLOCK.asItem(), 64)); - } - if (innermostRing <= 2 && 2 <= maxRing) { - /// 环2巨构(小型气态行星的最内环) - options.add(CelestialRefactorOption.withMaterial(2, "giant_planet_exctractor", - ringModel(2, "exctractor"), prefix + "giant_planet_exctractor", - ModBlocks.PUMP.asItem(), 32)); - } - if (innermostRing <= 4 && 4 <= maxRing) { - /// 环4巨构(小型恒星的最内环) - options.add(CelestialRefactorOption.withMaterial(4, "stellar_ring_collider", - ringModel(4, "collider"), prefix + "stellar_ring_collider", - ModItems.STELLAR_RING_COMPONENT, 8)); - options.add(CelestialRefactorOption.withMaterial(4, "dyson_sphere_small", - ringModel(4, "dyson_sphere"), prefix + "dyson_sphere_small", - ModItems.DYSON_SPHERE_COMPONENT, 16)); - options.add(CelestialRefactorOption.withMaterial(4, "magnetar_coil", - ringModel(4, "coil"), prefix + "magnetar_coil", - ModItems.MAGNETAR_COIL_COMPONENT, 4)); - options.add(CelestialRefactorOption.withMaterial(4, "penrose_sphere", - ringModel(4, "penrose_sphere"), prefix + "penrose_sphere", - ModItems.PENROSE_SPHERE_COMPONENT, 8)); - options.add(CelestialRefactorOption.withMaterial(4, "matter_decompressor", - ringModel(4, "matter_decompressor"), prefix + "matter_decompressor", - ModItems.MATTER_DECOMPRESSOR_COMPONENT, 2)); - options.add(CelestialRefactorOption.withMaterial(4, "wormhole_stabilizer", - ringModel(4, "wormhole_stabilizer"), prefix + "wormhole_stabilizer", - ModItems.WORMHOLE_STABILIZER_COMPONENT, 4)); - options.add(CelestialRefactorOption.withMaterial(5, "stellar_evolution_accelerator", - ringModel(5, "stellar_evolution_accelerator"), prefix + "stellar_evolution_accelerator", - ModItems.STELLAR_EVOLUTION_ACCELERATOR_COMPONENT, 8)); - } - if (innermostRing <= 5 && 5 <= maxRing) { - /// 环5巨构(大型恒星的最内环) - options.add(CelestialRefactorOption.withMaterial(5, "dyson_sphere_large", - ringModel(5, "dyson_sphere"), prefix + "dyson_sphere_large", - ModItems.DYSON_SPHERE_COMPONENT, 32)); - options.add(CelestialRefactorOption.withMaterial(6, "stellar_evolution_accelerator", - ringModel(6, "stellar_evolution_accelerator"), prefix + "stellar_evolution_accelerator", - ModItems.STELLAR_EVOLUTION_ACCELERATOR_COMPONENT, 8)); - } - return options; + return !resources.hasCivilization() + && (!resources.getBiologicalItems().isEmpty() || !resources.getBiologicalFluids().isEmpty()); } private static ResourceLocation ringModel(int ring, String megastructure) { diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSearchHistory.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSearchHistory.java new file mode 100644 index 0000000000..18ee0c45e1 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSearchHistory.java @@ -0,0 +1,123 @@ +package dev.dubhe.anvilcraft.block.entity.celestial; + +import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilBlockEntity; +import net.minecraft.nbt.CompoundTag; + +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; + +/** Maintains the bounded CFA search history and its browse cursor. */ +public final class CelestialSearchHistory { + private static final int MAX_ENTRIES = 10; + + private final List entries; + private int browseIndex; + private @Nullable CelestialForgingAnvilBlockEntity.SearchHistoryEntry originalEntry; + + public CelestialSearchHistory(List entries) { + this.entries = Objects.requireNonNull(entries); + } + + public List entries() { + return this.entries; + } + + public void clear() { + this.entries.clear(); + this.resetBrowsing(); + } + + public void add( + CelestialBodyData body, + @Nullable PlanetaryResourceSet resources + ) { + if (!this.entries.isEmpty() + && this.entries.getFirst().body().toTag().equals(body.toTag())) { + return; + } + this.entries.addFirst(new CelestialForgingAnvilBlockEntity.SearchHistoryEntry(body, resources)); + while (this.entries.size() > MAX_ENTRIES) this.entries.removeLast(); + this.resetBrowsing(); + } + + public boolean hasPrevious() { + return this.entries.size() > 1 && this.browseIndex < this.entries.size(); + } + + public boolean hasNext() { + return this.browseIndex > 0; + } + + public int browseIndex() { + return this.browseIndex; + } + + public void setBrowseIndex(int browseIndex) { + this.browseIndex = Math.clamp(browseIndex, 0, this.entries.size()); + this.originalEntry = null; + } + + public @Nullable CelestialForgingAnvilBlockEntity.SearchHistoryEntry previous( + @Nullable CelestialBodyData currentBody, + @Nullable PlanetaryResourceSet currentResources + ) { + int size = this.entries.size(); + if (size <= 1 || this.browseIndex >= size || currentBody == null) return null; + if (this.browseIndex == 0) { + this.originalEntry = new CelestialForgingAnvilBlockEntity.SearchHistoryEntry( + currentBody, + currentResources + ); + this.browseIndex = 1; + } + this.browseIndex++; + if (this.browseIndex > size) return null; + return this.entries.get(this.browseIndex - 1); + } + + public @Nullable CelestialForgingAnvilBlockEntity.SearchHistoryEntry next() { + if (this.browseIndex <= 0) return null; + this.browseIndex--; + if (this.browseIndex == 0) { + CelestialForgingAnvilBlockEntity.SearchHistoryEntry result = this.originalEntry; + if (result == null && !this.entries.isEmpty()) result = this.entries.getFirst(); + this.originalEntry = null; + return result; + } + return this.entries.get(this.browseIndex - 1); + } + + public CompoundTag toTag() { + CompoundTag tag = new CompoundTag(); + int size = Math.min(this.entries.size(), MAX_ENTRIES); + tag.putInt("size", size); + for (int index = 0; index < size; index++) { + tag.put("h" + index, this.entries.get(index).toTag()); + } + return tag; + } + + public void load(CompoundTag tag) { + this.clear(); + int size = Math.min(Math.max(tag.getInt("size"), 0), MAX_ENTRIES); + for (int index = 0; index < size; index++) { + String key = "h" + index; + if (!tag.contains(key)) continue; + CompoundTag entryTag = tag.getCompound(key); + if (entryTag.contains("body")) { + this.entries.add(CelestialForgingAnvilBlockEntity.SearchHistoryEntry.fromTag(entryTag)); + } else { + this.entries.add(new CelestialForgingAnvilBlockEntity.SearchHistoryEntry( + CelestialBodyData.fromTag(entryTag), + null + )); + } + } + } + + private void resetBrowsing() { + this.browseIndex = 0; + this.originalEntry = null; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSeedMatcher.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSeedMatcher.java new file mode 100644 index 0000000000..b39e4b5ed0 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSeedMatcher.java @@ -0,0 +1,60 @@ +package dev.dubhe.anvilcraft.block.entity.celestial; + +import dev.dubhe.anvilcraft.init.recipe.ModRecipeTypes; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.ResolvableProfile; +import net.minecraft.world.item.crafting.RecipeHolder; + +import javax.annotation.Nullable; + +/** Matches seed items against special celestial-body recipes. */ +public final class CelestialSeedMatcher { + private CelestialSeedMatcher() { + } + + public static @Nullable SpecialCelestialBodyData fromPlayerHead(ItemStack stack, int space) { + if (!stack.is(Items.PLAYER_HEAD)) return null; + ResolvableProfile profile = stack.get(DataComponents.PROFILE); + if (profile == null) return null; + CompoundTag profileTag = (CompoundTag) ResolvableProfile.CODEC + .encodeStart(NbtOps.INSTANCE, profile) + .getOrThrow(); + return SpecialCelestialBodyData.fromPlayerHead(profileTag, space); + } + + public static @Nullable Result match( + ServerLevel level, + int time, + int space, + int mass, + int energy, + Item seedItem + ) { + for (RecipeHolder holder : level.getRecipeManager() + .getAllRecipesFor(ModRecipeTypes.SPECIAL_CELESTIAL_BODY_TYPE.get())) { + SpecialCelestialBodyRecipe recipe = holder.value(); + if (recipe.time() != time + || recipe.space() != space + || recipe.mass() != mass + || recipe.energy() != energy + || !recipe.isEffectiveSeedItem(seedItem, level.getSeed())) { + continue; + } + SpecialCelestialBodyData body = SpecialCelestialBodyData.fromRecipe( + recipe, + holder.id().toString() + ); + return new Result(body, recipe.generateResources()); + } + return null; + } + + public record Result(SpecialCelestialBodyData body, PlanetaryResourceSet resources) { + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSnapshotCodec.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSnapshotCodec.java new file mode 100644 index 0000000000..5f5f96ecca --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialSnapshotCodec.java @@ -0,0 +1,61 @@ +package dev.dubhe.anvilcraft.block.entity.celestial; + +import dev.dubhe.anvilcraft.init.block.ModBlocks; +import dev.dubhe.anvilcraft.init.item.ModComponents; +import dev.dubhe.anvilcraft.item.DiskItem; +import dev.dubhe.anvilcraft.item.property.component.DiskData; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; + +import javax.annotation.Nullable; + +/** Reads and writes celestial snapshots carried by disks and singularity crystals. */ +public final class CelestialSnapshotCodec { + private static final String BODY_KEY = "celestialBody"; + private static final String SNAPSHOT_KEY = "celestialSnapshot"; + + private CelestialSnapshotCodec() { + } + + public static @Nullable CompoundTag extract(ItemStack stack) { + return load(stack); + } + + public static @Nullable CompoundTag load(ItemStack stack) { + if (stack.getItem() instanceof DiskItem && DiskItem.hasDataStored(stack)) { + CompoundTag data = DiskItem.getData(stack); + return data.contains(BODY_KEY) ? data.copy() : null; + } + if (!stack.is(ModBlocks.SINGULARITY_CRYSTAL.asItem())) return null; + CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY); + CompoundTag snapshot = customData.copyTag().getCompound(SNAPSHOT_KEY); + return snapshot.contains(BODY_KEY) ? snapshot.copy() : null; + } + + public static void save(ItemStack stack, CompoundTag snapshot) { + if (stack.getItem() instanceof DiskItem) { + if (containsExtremeBody(snapshot)) return; + CompoundTag diskTag = DiskItem.hasDataStored(stack) + ? DiskItem.getData(stack).copy() + : new CompoundTag(); + diskTag.merge(snapshot); + stack.set(ModComponents.DISK_DATA, new DiskData(diskTag)); + return; + } + if (stack.is(ModBlocks.SINGULARITY_CRYSTAL.asItem())) { + CustomData oldCustom = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY); + CompoundTag updated = oldCustom.copyTag(); + updated.put(SNAPSHOT_KEY, snapshot.copy()); + stack.set(DataComponents.CUSTOM_DATA, CustomData.of(updated)); + } + } + + private static boolean containsExtremeBody(CompoundTag snapshot) { + if (!snapshot.contains(BODY_KEY)) return false; + String bodyClass = snapshot.getCompound(BODY_KEY).getString("bodyClass"); + return CelestialBodyClass.BLACK_HOLE.name().equals(bodyClass) + || CelestialBodyClass.NEUTRON_STAR.name().equals(bodyClass); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelData.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelData.java new file mode 100644 index 0000000000..c5dc504c2e --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelData.java @@ -0,0 +1,238 @@ +package dev.dubhe.anvilcraft.block.entity.celestial; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import lombok.Getter; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; + +import java.util.Locale; +import javax.annotation.Nullable; + +/** + * Data-driven travel rules for a special celestial body. + * + *

The absence of this value means that the body cannot be reached through a + * Celestial Forging Anvil portal. Keeping the value optional lets datapacks + * opt a planet in without adding a second, easily desynchronised boolean.

+ */ +public record CelestialTravelData( + ResourceLocation dimension, + CoordinateRule coordinateRule, + ReturnRule returnRule +) { + public static final MapCodecHolder CODECS = new MapCodecHolder(); + + /** The coordinate used when a rule omits its optional position fields. */ + public record CoordinateRule(Type type, double scale, int x, int y, int z, int radius) { + public static final CoordinateRule DEFAULT = new CoordinateRule(Type.FIXED, 1.0, 0, 64, 0, 8); + + private static final Codec OBJECT_CODEC = RecordCodecBuilder.create(ins -> ins.group( + Type.CODEC.optionalFieldOf("type", Type.FIXED).forGetter(CoordinateRule::type), + Codec.DOUBLE.optionalFieldOf("scale", 1.0).forGetter(CoordinateRule::scale), + Codec.INT.optionalFieldOf("x", 0).forGetter(CoordinateRule::x), + Codec.INT.optionalFieldOf("y", 64).forGetter(CoordinateRule::y), + Codec.INT.optionalFieldOf("z", 0).forGetter(CoordinateRule::z), + Codec.INT.optionalFieldOf("radius", 8).forGetter(CoordinateRule::radius) + ).apply(ins, CoordinateRule::new)); + + /** Accept both the documented object form and a concise string rule in datapacks. */ + public static final Codec CODEC = Codec.either(OBJECT_CODEC, Type.CODEC).xmap( + either -> either.map(rule -> rule, type -> new CoordinateRule(type, 1.0, 0, 64, 0, 8)), + rule -> Either.left(rule) + ); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, + rule -> rule.type().getSerializedName(), + ByteBufCodecs.DOUBLE, + CoordinateRule::scale, + ByteBufCodecs.INT, + CoordinateRule::x, + ByteBufCodecs.INT, + CoordinateRule::y, + ByteBufCodecs.INT, + CoordinateRule::z, + ByteBufCodecs.INT, + CoordinateRule::radius, + (type, scale, x, y, z, radius) -> new CoordinateRule( + Type.fromName(type), scale, x, y, z, radius + ) + ); + + public CoordinateRule { + scale = Double.isFinite(scale) && scale > 0.0 ? scale : 1.0; + radius = Math.max(0, radius); + } + + @Getter + public enum Type { + SAME("same"), + SCALED("scaled"), + FIXED("fixed"), + RANDOM_SPAWN("random_spawn"); + + public static final Codec CODEC = Codec.STRING.xmap(Type::fromName, Type::getSerializedName); + + private final String serializedName; + + Type(String serializedName) { + this.serializedName = serializedName; + } + + public static Type fromName(String name) { + return switch (name.toLowerCase(Locale.ROOT)) { + case "same", "current", "equal" -> SAME; + case "scaled", "simple", "scale" -> SCALED; + case "fixed", "point" -> FIXED; + case "random_spawn", "random", "spawn" -> RANDOM_SPAWN; + default -> throw new IllegalArgumentException("Unknown celestial coordinate rule: " + name); + }; + } + } + } + + public record ReturnRule(Type type, int x, int y, int z, int radius) { + public static final ReturnRule DEFAULT = new ReturnRule(Type.ENTRY_PORTAL, 0, 64, 0, 8); + + private static final Codec OBJECT_CODEC = RecordCodecBuilder.create(ins -> ins.group( + Type.CODEC.optionalFieldOf("type", Type.ENTRY_PORTAL).forGetter(ReturnRule::type), + Codec.INT.optionalFieldOf("x", 0).forGetter(ReturnRule::x), + Codec.INT.optionalFieldOf("y", 64).forGetter(ReturnRule::y), + Codec.INT.optionalFieldOf("z", 0).forGetter(ReturnRule::z), + Codec.INT.optionalFieldOf("radius", 8).forGetter(ReturnRule::radius) + ).apply(ins, ReturnRule::new)); + + /** Accept both the documented object form and a concise string rule in datapacks. */ + public static final Codec CODEC = Codec.either(OBJECT_CODEC, Type.CODEC).xmap( + either -> either.map(rule -> rule, type -> new ReturnRule(type, 0, 64, 0, 8)), + rule -> Either.left(rule) + ); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, + rule -> rule.type().getSerializedName(), + ByteBufCodecs.INT, + ReturnRule::x, + ByteBufCodecs.INT, + ReturnRule::y, + ByteBufCodecs.INT, + ReturnRule::z, + ByteBufCodecs.INT, + ReturnRule::radius, + (type, x, y, z, radius) -> new ReturnRule(Type.fromName(type), x, y, z, radius) + ); + + public ReturnRule { + radius = Math.max(0, radius); + } + + @Getter + public enum Type { + ENTRY_PORTAL("entry_portal"), + RANDOM_PORTAL("random_portal"), + FIXED_PORTAL("fixed_portal"); + + public static final Codec CODEC = Codec.STRING.xmap(Type::fromName, Type::getSerializedName); + + private final String serializedName; + + Type(String serializedName) { + this.serializedName = serializedName; + } + + public static Type fromName(String name) { + return switch (name.toLowerCase(Locale.ROOT)) { + case "entry_portal", "entry", "entry_point" -> ENTRY_PORTAL; + case "random_portal", "random", "random_spawn" -> RANDOM_PORTAL; + case "fixed_portal", "fixed", "point" -> FIXED_PORTAL; + default -> throw new IllegalArgumentException("Unknown celestial return rule: " + name); + }; + } + } + } + + /** A small holder avoids exposing a MapCodec implementation detail in the record header. */ + public static final class MapCodecHolder { + public final Codec codec = RecordCodecBuilder.create(ins -> ins.group( + ResourceLocation.CODEC.fieldOf("dimension").forGetter(CelestialTravelData::dimension), + CoordinateRule.CODEC.optionalFieldOf("coordinate_rule", CoordinateRule.DEFAULT) + .forGetter(CelestialTravelData::coordinateRule), + ReturnRule.CODEC.optionalFieldOf("return_rule", ReturnRule.DEFAULT) + .forGetter(CelestialTravelData::returnRule) + ).apply(ins, CelestialTravelData::new)); + + public final StreamCodec streamCodec = StreamCodec.composite( + ResourceLocation.STREAM_CODEC, + CelestialTravelData::dimension, + CoordinateRule.STREAM_CODEC, + CelestialTravelData::coordinateRule, + ReturnRule.STREAM_CODEC, + CelestialTravelData::returnRule, + CelestialTravelData::new + ); + + private MapCodecHolder() { + } + } + + public static final Codec CODEC = CODECS.codec; + public static final StreamCodec STREAM_CODEC = CODECS.streamCodec; + + public CelestialTravelData { + coordinateRule = coordinateRule == null ? CoordinateRule.DEFAULT : coordinateRule; + returnRule = returnRule == null ? ReturnRule.DEFAULT : returnRule; + } + + public CompoundTag toTag() { + CompoundTag tag = new CompoundTag(); + tag.putString("dimension", dimension.toString()); + tag.putString("coordinateType", coordinateRule.type().getSerializedName()); + tag.putDouble("coordinateScale", coordinateRule.scale()); + tag.putInt("coordinateX", coordinateRule.x()); + tag.putInt("coordinateY", coordinateRule.y()); + tag.putInt("coordinateZ", coordinateRule.z()); + tag.putInt("coordinateRadius", coordinateRule.radius()); + tag.putString("returnType", returnRule.type().getSerializedName()); + tag.putInt("returnX", returnRule.x()); + tag.putInt("returnY", returnRule.y()); + tag.putInt("returnZ", returnRule.z()); + tag.putInt("returnRadius", returnRule.radius()); + return tag; + } + + @Nullable + public static CelestialTravelData fromTag(CompoundTag tag) { + String dimensionName = tag.getString("dimension"); + if (dimensionName.isEmpty()) return null; + try { + ResourceLocation dimension = ResourceLocation.parse(dimensionName); + CoordinateRule coordinateRule = new CoordinateRule( + CoordinateRule.Type.fromName(tag.contains("coordinateType") + ? tag.getString("coordinateType") : CoordinateRule.Type.FIXED.getSerializedName()), + tag.contains("coordinateScale") ? tag.getDouble("coordinateScale") : 1.0, + tag.getInt("coordinateX"), + tag.contains("coordinateY") ? tag.getInt("coordinateY") : 64, + tag.getInt("coordinateZ"), + tag.contains("coordinateRadius") ? tag.getInt("coordinateRadius") : 8 + ); + ReturnRule returnRule = new ReturnRule( + ReturnRule.Type.fromName(tag.contains("returnType") + ? tag.getString("returnType") : ReturnRule.Type.ENTRY_PORTAL.getSerializedName()), + tag.getInt("returnX"), + tag.contains("returnY") ? tag.getInt("returnY") : 64, + tag.getInt("returnZ"), + tag.contains("returnRadius") ? tag.getInt("returnRadius") : 8 + ); + return new CelestialTravelData(dimension, coordinateRule, returnRule); + } catch (IllegalArgumentException ignored) { + return null; + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java new file mode 100644 index 0000000000..7b2e0e669e --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/CelestialTravelManager.java @@ -0,0 +1,578 @@ +package dev.dubhe.anvilcraft.block.entity.celestial; + +import dev.dubhe.anvilcraft.block.CelestialBackGateBlock; +import dev.dubhe.anvilcraft.block.entity.CelestialBackGateBlockEntity; +import dev.dubhe.anvilcraft.init.block.ModBlocks; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.portal.DimensionTransition; +import net.minecraft.world.phys.Vec3; + +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nullable; + +/** Server-side implementation of the coordinate and return rules in {@link CelestialTravelData}. */ +public final class CelestialTravelManager { + public static final ResourceLocation OVERWORLD_LIKE_DIMENSION = + ResourceLocation.fromNamespaceAndPath("anvilcraft", "overworld_like"); + public static final ResourceKey OVERWORLD_LIKE_LEVEL = + ResourceKey.create(Registries.DIMENSION, OVERWORLD_LIKE_DIMENSION); + + private static final int SEARCH_RADIUS = 16; + private static final int SPAWN_SEARCH_RADIUS = 32; + private static final int GATE_REUSE_RADIUS = 8; + private static final int GATE_REUSE_VERTICAL_RADIUS = 8; + private static final int PORTAL_COOLDOWN_TICKS = 20; + private static final long RANDOM_SALT = 0x6A09E667F3BCC909L; + private static final long OVERWORLD_LIKE_SALT = 0x6A09E667F3BCC909L; + private static final long OVERWORLD_LIKE_MULTIPLIER = 0x9E3779B97F4A7C15L; + private static final long OVERWORLD_LIKE_ADDEND = 0xBF58476D1CE4E5B9L; + + private CelestialTravelManager() { + } + + public static boolean isOverworldLike(ResourceKey dimension) { + return OVERWORLD_LIKE_LEVEL.equals(dimension); + } + + /** + * Derives the seed used by the built-in overworld-like dimension. + * + *

The derivation is deliberately stable: all instances of the + * overworld-like special planet share one dimension, while every save gets + * terrain that is independent from its ordinary overworld.

+ */ + public static long overworldLikeSeed(long sourceSeed) { + long mixed = Long.rotateLeft(sourceSeed ^ OVERWORLD_LIKE_SALT, 23); + return mixed * OVERWORLD_LIKE_MULTIPLIER + OVERWORLD_LIKE_ADDEND; + } + + /** + * Sends an entity through a special-planet landing portal and records the + * source portal on the generated return gate. + */ + public static boolean tryLand( + Entity entity, + ServerLevel sourceLevel, + BlockPos sourcePortalPos, + Direction sourceFacing, + CelestialTravelData travel + ) { + if (entity.isOnPortalCooldown()) return false; + ResourceKey destinationKey = ResourceKey.create(Registries.DIMENSION, travel.dimension()); + ServerLevel destination = sourceLevel.getServer().getLevel(destinationKey); + if (destination == null) return false; + + BlockPos desired = landingOrigin(entity, destination, travel.coordinateRule()); + if (travel.returnRule().type() == CelestialTravelData.ReturnRule.Type.ENTRY_PORTAL) { + BlockPos existingGate = findMatchingGateNear( + destination, desired, sourceLevel.dimension(), sourcePortalPos, sourceFacing + ); + if (existingGate != null) { + return travelThroughGate( + entity, sourceLevel, destination, existingGate, existingGate, sourcePortalPos, sourceFacing, + travel.returnRule().type() + ); + } + } + boolean keepRequestedY = travel.coordinateRule().type() + == CelestialTravelData.CoordinateRule.Type.FIXED; + BlockPos landing = findSafeLandingOrSpawn(destination, desired, keepRequestedY); + boolean emergencyLanding = false; + if (landing == null) { + landing = createEmergencyLandingPlatform(destination, desired, keepRequestedY); + if (landing == null) return false; + emergencyLanding = true; + } + + BlockPos returnOrigin = returnOrigin(destination, entity, travel.returnRule(), landing); + boolean keepReturnY = travel.returnRule().type() == CelestialTravelData.ReturnRule.Type.FIXED_PORTAL + || (travel.returnRule().type() == CelestialTravelData.ReturnRule.Type.ENTRY_PORTAL && keepRequestedY); + boolean reuseExistingGate = travel.returnRule().type() + != CelestialTravelData.ReturnRule.Type.RANDOM_PORTAL; + BlockPos existingGate = reuseExistingGate + ? findMatchingGateNear( + destination, returnOrigin, sourceLevel.dimension(), sourcePortalPos, sourceFacing + ) : null; + BlockPos returnPos = existingGate != null + ? existingGate + : (travel.returnRule().type() == CelestialTravelData.ReturnRule.Type.ENTRY_PORTAL + && emergencyLanding + ? landing + : findSafeGateOrSpawn( + destination, + returnOrigin, + sourceFacing, + sourceLevel.dimension(), + sourcePortalPos, + keepReturnY + )); + if (returnPos == null) { + returnPos = createEmergencyLandingPlatform(destination, returnOrigin, keepReturnY); + } + if (returnPos == null) return false; + + return travelThroughGate( + entity, sourceLevel, destination, returnPos, landing, sourcePortalPos, sourceFacing, + travel.returnRule().type() + ); + } + + private static boolean travelThroughGate( + Entity entity, + ServerLevel sourceLevel, + ServerLevel destination, + BlockPos returnPos, + BlockPos landing, + BlockPos sourcePortalPos, + Direction sourceFacing, + CelestialTravelData.ReturnRule.Type returnType + ) { + Direction gateFacing = sourceFacing; + boolean reusingGate = destination.getBlockState(returnPos).getBlock() instanceof CelestialBackGateBlock; + BlockState gateState = ModBlocks.CELESTIAL_BACK_GATE.getDefaultState() + .setValue(CelestialBackGateBlock.FACING, gateFacing); + if (!reusingGate) { + destination.setBlock(returnPos, gateState, 3); + } else if (destination.getBlockState(returnPos).getValue(CelestialBackGateBlock.FACING) + != gateFacing) { + destination.setBlock(returnPos, gateState, 3); + } + if (!(destination.getBlockEntity(returnPos) instanceof CelestialBackGateBlockEntity gate)) { + if (!reusingGate) destination.removeBlock(returnPos, false); + return false; + } + + // An entry portal occupies the requested landing point, so arrive just + // in front of it. Other return rules place the gate independently and + // must preserve the coordinate rule's landing position. + BlockPos arrivalPos = returnType == CelestialTravelData.ReturnRule.Type.ENTRY_PORTAL + ? returnPos.relative(gateFacing) : landing; + Vec3 destinationPosition = Vec3.atBottomCenterOf(arrivalPos); + Vec3 momentum = reverseFacingMomentum(entity.getDeltaMovement(), sourceFacing); + Entity moved = move(entity, sourceLevel, destination, destinationPosition, momentum); + if (moved == null) { + if (!reusingGate) destination.removeBlock(returnPos, false); + return false; + } + moved.setPortalCooldown(PORTAL_COOLDOWN_TICKS); + gate.configure(sourceLevel.dimension(), sourcePortalPos, sourceFacing); + cleanupDuplicateGates(destination, returnPos, sourceLevel.dimension(), sourcePortalPos, sourceFacing); + return true; + } + + /** Sends an entity standing in a return gate back to its recorded source portal. */ + public static boolean tryReturn(Entity entity, CelestialBackGateBlockEntity gate) { + if (entity.isOnPortalCooldown()) return false; + if (!(gate.getLevel() instanceof ServerLevel sourceLevel)) return false; + ResourceKey returnDimension = gate.getReturnDimension(); + BlockPos returnPortal = gate.getReturnPortalPos(); + Direction returnFacing = gate.getReturnFacing(); + if (returnDimension == null || returnPortal == null) return false; + + ServerLevel destination = sourceLevel.getServer().getLevel(returnDimension); + if (destination == null) return false; + BlockPos desired = returnPortal.relative(returnFacing); + BlockPos landing = findSafeLandingOrSpawn(destination, desired, true); + if (landing == null) { + landing = createEmergencyLandingPlatform(destination, desired, true); + if (landing == null) return false; + } + + Vec3 destinationPosition = Vec3.atBottomCenterOf(landing); + Vec3 momentum = reverseFacingMomentum(entity.getDeltaMovement(), returnFacing); + Entity moved = move(entity, sourceLevel, destination, destinationPosition, momentum); + if (moved != null) moved.setPortalCooldown(PORTAL_COOLDOWN_TICKS); + return moved != null; + } + + private static BlockPos landingOrigin( + Entity entity, ServerLevel destination, CelestialTravelData.CoordinateRule rule + ) { + return switch (rule.type()) { + case SAME -> BlockPos.containing(entity.getX(), destination.getSharedSpawnPos().getY(), entity.getZ()); + case SCALED -> BlockPos.containing( + entity.getX() * rule.scale(), destination.getSharedSpawnPos().getY(), entity.getZ() * rule.scale() + ); + case FIXED -> new BlockPos(rule.x(), rule.y(), rule.z()); + case RANDOM_SPAWN -> randomSpawnOrigin(destination, rule.radius(), entity.getUUID()); + }; + } + + private static BlockPos returnOrigin( + ServerLevel destination, + Entity entity, + CelestialTravelData.ReturnRule rule, + BlockPos entry + ) { + return switch (rule.type()) { + case ENTRY_PORTAL -> entry; + case FIXED_PORTAL -> new BlockPos(rule.x(), rule.y(), rule.z()); + case RANDOM_PORTAL -> randomSpawnOrigin(destination, rule.radius(), entity.getUUID()); + }; + } + + private static BlockPos randomSpawnOrigin(ServerLevel level, int chunkRadius, UUID uuid) { + BlockPos spawn = level.getSharedSpawnPos(); + int radius = Math.max(0, Math.min(chunkRadius, 1024)) * 16; + long seed = uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits() + ^ level.getGameTime() ^ RANDOM_SALT; + int x = spawn.getX() + signedOffset(seed, radius); + int z = spawn.getZ() + signedOffset(Long.rotateLeft(seed, 21), radius); + return new BlockPos(x, spawn.getY(), z); + } + + private static int signedOffset(long seed, int radius) { + if (radius <= 0) return 0; + return (int) Math.floorMod(seed, (long) radius * 2L + 1L) - radius; + } + + /** Finds an air column with a solid floor near the requested coordinate. */ + @Nullable + public static BlockPos findSafeLandingPos(ServerLevel level, BlockPos origin) { + return findSafeLandingPos(level, origin, true); + } + + /** + * Finds a safe two-block column near an origin. Coordinate rules that + * describe X/Z only should use the heightmap instead of treating the + * origin's Y as a hard landing height. + */ + private static @Nullable BlockPos findSafeLandingPos( + ServerLevel level, BlockPos origin, boolean keepRequestedY + ) { + return findSafeLandingPos(level, origin, keepRequestedY, SEARCH_RADIUS); + } + + @Nullable + private static BlockPos findSafeLandingPos( + ServerLevel level, BlockPos origin, boolean keepRequestedY, int searchRadius + ) { + WorldBorder border = level.getWorldBorder(); + BlockPos clamped = border.clampToBounds(origin); + BlockPos direct = findSafeLandingInColumn( + level, origin.getX(), origin.getZ(), origin, keepRequestedY + ); + if (direct != null && border.isWithinBounds(direct.getX(), direct.getZ())) return direct; + for (BlockPos.MutableBlockPos column : BlockPos.spiralAround( + clamped, searchRadius, Direction.EAST, Direction.SOUTH + )) { + int x = column.getX(); + int z = column.getZ(); + if (!border.isWithinBounds(x, z)) continue; + BlockPos candidate = findSafeLandingInColumn(level, x, z, origin, keepRequestedY); + if (candidate != null) return candidate; + } + return null; + } + + /** + * Preserves the requested location whenever possible, then falls back to + * the destination spawn area when that location has no safe ground, such + * as the middle of a large ocean. + */ + @Nullable + private static BlockPos findSafeLandingOrSpawn(ServerLevel level, BlockPos origin, boolean keepRequestedY) { + BlockPos landing = findSafeLandingPos(level, origin, keepRequestedY); + if (landing != null) return landing; + return findSafeLandingPos(level, level.getSharedSpawnPos(), false, SPAWN_SEARCH_RADIUS); + } + + /** + * Builds a small obsidian platform when a coordinate rule resolves into an + * ocean or another area with no natural safe landing. This mirrors the + * fallback used by vanilla cross-dimension portals while keeping the + * generated return gate usable immediately. + */ + @Nullable + private static BlockPos createEmergencyLandingPlatform( + ServerLevel level, BlockPos origin, boolean keepRequestedY + ) { + WorldBorder border = level.getWorldBorder(); + BlockPos clamped = border.clampToBounds(origin); + int floorY = keepRequestedY + ? origin.getY() - 1 + : level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, clamped.getX(), clamped.getZ()) - 1; + if (floorY < level.getMinBuildHeight() || floorY > level.getMaxBuildHeight() - 3) return null; + + BlockPos floorCenter = new BlockPos(clamped.getX(), floorY, clamped.getZ()); + if (!canCreateEmergencyPlatform(level, floorCenter, border)) return null; + + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockPos floor = floorCenter.offset(x, 0, z); + if (!level.getBlockState(floor).isSolid()) { + level.setBlock(floor, Blocks.OBSIDIAN.defaultBlockState(), 3); + } + clearEmergencyPlatformSpace(level, floor.above()); + clearEmergencyPlatformSpace(level, floor.above(2)); + } + } + return floorCenter.above(); + } + + private static boolean canCreateEmergencyPlatform(ServerLevel level, BlockPos floorCenter, WorldBorder border) { + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + BlockPos floor = floorCenter.offset(x, 0, z); + if (!border.isWithinBounds(floor.getX(), floor.getZ())) return false; + BlockState floorState = level.getBlockState(floor); + if (!floorState.isSolid() && !canClearForEmergencyPlatform(floorState)) return false; + if (!canClearForEmergencyPlatform(level.getBlockState(floor.above())) + || !canClearForEmergencyPlatform(level.getBlockState(floor.above(2)))) { + return false; + } + } + } + return true; + } + + private static boolean canClearForEmergencyPlatform(BlockState state) { + return state.isAir() || !state.getFluidState().isEmpty() || state.canBeReplaced(); + } + + private static void clearEmergencyPlatformSpace(ServerLevel level, BlockPos pos) { + if (!level.isEmptyBlock(pos)) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), 3); + } + } + + @Nullable + private static BlockPos findSafeLandingInColumn( + ServerLevel level, int x, int z, BlockPos origin, boolean keepRequestedY + ) { + int height = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z); + int preferredY = keepRequestedY && origin.getY() > level.getMinBuildHeight() + ? origin.getY() : height; + int minY = Math.max(level.getMinBuildHeight() + 1, preferredY - 8); + int maxY = Math.min(level.getMaxBuildHeight() - 3, preferredY + 8); + for (int y = maxY; y >= minY; y--) { + BlockPos candidate = new BlockPos(x, y, z); + if (canStand(level, candidate)) return candidate; + } + return null; + } + + private static boolean canStand(ServerLevel level, BlockPos feet) { + BlockState floor = level.getBlockState(feet.below()); + return floor.isSolid() && level.isEmptyBlock(feet) && level.isEmptyBlock(feet.above()); + } + + /** Finds a gate position whose outward side is also a safe two-block landing column. */ + @Nullable + private static BlockPos findSafeGateOrSpawn( + ServerLevel level, + BlockPos origin, + Direction facing, + ResourceKey sourceDimension, + BlockPos sourcePortalPos, + boolean keepRequestedY + ) { + BlockPos gate = findSafeGatePos( + level, origin, facing, sourceDimension, sourcePortalPos, keepRequestedY, SEARCH_RADIUS + ); + if (gate != null) return gate; + return findSafeGatePos( + level, level.getSharedSpawnPos(), facing, sourceDimension, sourcePortalPos, false, SPAWN_SEARCH_RADIUS + ); + } + + /** Finds a gate position whose outward side is also a safe two-block landing column. */ + @Nullable + private static BlockPos findSafeGatePos( + ServerLevel level, + BlockPos origin, + Direction facing, + ResourceKey sourceDimension, + BlockPos sourcePortalPos, + boolean keepRequestedY, + int searchRadius + ) { + WorldBorder border = level.getWorldBorder(); + BlockPos clamped = border.clampToBounds(origin); + BlockPos direct = findSafeGateInColumn( + level, origin.getX(), origin.getZ(), origin, facing, sourceDimension, sourcePortalPos, keepRequestedY + ); + if (direct != null && border.isWithinBounds(direct.getX(), direct.getZ())) return direct; + for (BlockPos.MutableBlockPos column : BlockPos.spiralAround( + clamped, searchRadius, Direction.EAST, Direction.SOUTH + )) { + int x = column.getX(); + int z = column.getZ(); + if (!border.isWithinBounds(x, z)) continue; + BlockPos candidate = findSafeGateInColumn( + level, x, z, origin, facing, sourceDimension, sourcePortalPos, keepRequestedY + ); + if (candidate != null) return candidate; + } + return null; + } + + @Nullable + private static BlockPos findSafeGateInColumn( + ServerLevel level, + int x, + int z, + BlockPos origin, + Direction facing, + ResourceKey sourceDimension, + BlockPos sourcePortalPos, + boolean keepRequestedY + ) { + int height = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z); + int preferredY = keepRequestedY && origin.getY() > level.getMinBuildHeight() + ? origin.getY() : height; + int minY = Math.max(level.getMinBuildHeight() + 1, preferredY - 8); + int maxY = Math.min(level.getMaxBuildHeight() - 3, preferredY + 8); + for (int y = maxY; y >= minY; y--) { + BlockPos gate = new BlockPos(x, y, z); + BlockState gateState = level.getBlockState(gate); + boolean gateSpace = gateState.isAir() || gateState.getBlock() instanceof CelestialBackGateBlock; + if (gateState.getBlock() instanceof CelestialBackGateBlock + && !isReusableGate(level, gate, sourceDimension, sourcePortalPos, facing)) { + continue; + } + boolean gateSupport = level.getBlockState(gate.below()).isSolid() && level.isEmptyBlock(gate.above()); + if (gateSpace && gateSupport && canStand(level, gate.relative(facing))) return gate; + } + return null; + } + + private static boolean isReusableGate( + ServerLevel level, BlockPos pos, ResourceKey sourceDimension, BlockPos sourcePortalPos, + Direction sourceFacing + ) { + if (!(level.getBlockEntity(pos) instanceof CelestialBackGateBlockEntity gate)) return false; + return isMatchingGate(gate, sourceDimension, sourcePortalPos, sourceFacing); + } + + private static boolean isMatchingGate( + CelestialBackGateBlockEntity gate, ResourceKey sourceDimension, BlockPos sourcePortalPos, + Direction sourceFacing + ) { + ResourceKey returnDimension = gate.getReturnDimension(); + BlockPos returnPortalPos = gate.getReturnPortalPos(); + return returnDimension == null + || (returnDimension.equals(sourceDimension) + && sourcePortalPos.equals(returnPortalPos) + && gate.getReturnFacing() == sourceFacing); + } + + /** Finds an already generated gate before a new landing search can move it aside. */ + @Nullable + private static BlockPos findMatchingGateNear( + ServerLevel level, + BlockPos origin, + ResourceKey sourceDimension, + BlockPos sourcePortalPos, + Direction sourceFacing + ) { + BlockPos nearest = null; + long nearestDistance = Long.MAX_VALUE; + for (int dx = -GATE_REUSE_RADIUS; dx <= GATE_REUSE_RADIUS; dx++) { + for (int dy = -GATE_REUSE_VERTICAL_RADIUS; dy <= GATE_REUSE_VERTICAL_RADIUS; dy++) { + for (int dz = -GATE_REUSE_RADIUS; dz <= GATE_REUSE_RADIUS; dz++) { + BlockPos candidate = origin.offset(dx, dy, dz); + if (!level.hasChunkAt(candidate)) continue; + if (!(level.getBlockState(candidate).getBlock() instanceof CelestialBackGateBlock) + || !(level.getBlockEntity(candidate) instanceof CelestialBackGateBlockEntity gate) + || !isMatchingGate(gate, sourceDimension, sourcePortalPos, sourceFacing)) { + continue; + } + long distance = (long) dx * dx + (long) dy * dy + (long) dz * dz; + if (distance < nearestDistance) { + nearest = candidate.immutable(); + nearestDistance = distance; + } + } + } + } + return nearest; + } + + /** Removes only gates that point to the same source portal as the selected gate. */ + public static void cleanupDuplicateGates( + ServerLevel level, + BlockPos selected, + ResourceKey sourceDimension, + BlockPos sourcePortalPos, + Direction sourceFacing + ) { + if (!(level.getBlockState(selected).getBlock() instanceof CelestialBackGateBlock)) return; + for (int dx = -GATE_REUSE_RADIUS; dx <= GATE_REUSE_RADIUS; dx++) { + for (int dy = -GATE_REUSE_VERTICAL_RADIUS; dy <= GATE_REUSE_VERTICAL_RADIUS; dy++) { + for (int dz = -GATE_REUSE_RADIUS; dz <= GATE_REUSE_RADIUS; dz++) { + BlockPos candidate = selected.offset(dx, dy, dz); + if (!level.hasChunkAt(candidate)) continue; + if (candidate.equals(selected) + || !(level.getBlockState(candidate).getBlock() instanceof CelestialBackGateBlock) + || !(level.getBlockEntity(candidate) instanceof CelestialBackGateBlockEntity gate)) { + continue; + } + ResourceKey returnDimension = gate.getReturnDimension(); + BlockPos returnPortalPos = gate.getReturnPortalPos(); + boolean sameSource = returnDimension != null + && returnDimension.equals(sourceDimension) + && sourcePortalPos.equals(returnPortalPos) + && gate.getReturnFacing() == sourceFacing; + if (sameSource) level.removeBlock(candidate, false); + } + } + } + } + + private static Vec3 reverseFacingMomentum(Vec3 velocity, Direction facing) { + if (facing.getAxis() == Direction.Axis.X) { + return new Vec3(-velocity.x, velocity.y, velocity.z); + } + return new Vec3(velocity.x, velocity.y, -velocity.z); + } + + @Nullable + private static Entity move( + Entity entity, + ServerLevel source, + ServerLevel destination, + Vec3 position, + Vec3 momentum + ) { + float targetYRot = (entity.getYRot() + 180.0f) % 360.0f; + if (entity instanceof ServerPlayer player) { + player.teleportTo(destination, position.x, position.y, position.z, targetYRot, entity.getXRot()); + player.setDeltaMovement(momentum); + player.hasImpulse = true; + return player; + } + if (source == destination) { + entity.teleportTo(destination, position.x, position.y, position.z, Set.of(), targetYRot, entity.getXRot()); + entity.setDeltaMovement(momentum); + entity.hasImpulse = true; + return entity; + } + Entity moved = entity.changeDimension(new DimensionTransition( + destination, + position, + momentum, + targetYRot, + entity.getXRot(), + DimensionTransition.DO_NOTHING + )); + if (moved != null) { + moved.setDeltaMovement(momentum); + moved.hasImpulse = true; + } + return moved; + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/GiantPlanetData.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/GiantPlanetData.java index 569cd64d3e..bcc22c0cc1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/GiantPlanetData.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/GiantPlanetData.java @@ -13,9 +13,40 @@ public record GiantPlanetData( float axialTilt, int rotationSpeed, int magneticFieldStrength, - boolean brownDwarf + boolean brownDwarf, + int energy ) implements CelestialBodyData { + /** Backwards-compatible constructor for giant planets stored before energy was persisted. */ + public GiantPlanetData( + CelestialBodyClass bodyClass, + PressureType pressureType, + WindSpeed windSpeed, + RingType ringType, + int size, + int paletteBaseRow, + int paletteOverlayRow, + float axialTilt, + int rotationSpeed, + int magneticFieldStrength, + boolean brownDwarf + ) { + this( + bodyClass, + pressureType, + windSpeed, + ringType, + size, + paletteBaseRow, + paletteOverlayRow, + axialTilt, + rotationSpeed, + magneticFieldStrength, + brownDwarf, + 0 + ); + } + @Override public CelestialBodyType type() { return CelestialBodyType.GIANT_PLANET; @@ -36,6 +67,7 @@ public CompoundTag toTag() { tag.putInt("rotationSpeed", rotationSpeed); tag.putInt("magneticFieldStrength", magneticFieldStrength); tag.putBoolean("brownDwarf", brownDwarf); + tag.putInt("energy", energy); return tag; } @@ -43,6 +75,7 @@ public static GiantPlanetData fromTag(CompoundTag tag) { CelestialBodyClass cls = CelestialBodyData.readClass(tag, CelestialBodyType.GIANT_PLANET); int mag = tag.contains("magneticFieldStrength") ? tag.getInt("magneticFieldStrength") : 0; boolean bd = tag.contains("brownDwarf") && tag.getBoolean("brownDwarf"); + int energy = tag.contains("energy") ? tag.getInt("energy") : 0; return new GiantPlanetData( cls, PressureType.fromName(tag.getString("pressureType")), @@ -54,7 +87,8 @@ public static GiantPlanetData fromTag(CompoundTag tag) { tag.getFloat("axialTilt"), tag.getInt("rotationSpeed"), mag, - bd + bd, + energy ); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/Megastructure.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/Megastructure.java new file mode 100644 index 0000000000..68eff41db1 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/Megastructure.java @@ -0,0 +1,215 @@ +package dev.dubhe.anvilcraft.block.entity.celestial; + +import dev.dubhe.anvilcraft.block.entity.megastructure.IMegastructureHandler; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.function.ToIntFunction; +import javax.annotation.Nullable; + +/** + * Code-registered definition of a Celestial Forging Anvil megastructure. + * + *

The definition owns the rules that used to be spread over the option + * registry and the block entity manager. Add-on mods can therefore register + * a new structure without editing either of those classes.

+ */ +public final class Megastructure { + private final ResourceLocation id; + private final String name; + private final String displayName; + private final Prerequisite prerequisite; + private final ToIntFunction ringFunction; + private final RotationFunction rotationFunction; + private final Map modelLocations; + private final @Nullable ItemLike material; + private final int materialCount; + private final Supplier handlerFactory; + private final boolean auxiliary; + + private Megastructure(Builder builder) { + this.id = builder.id; + this.name = builder.name; + this.displayName = builder.displayName; + this.prerequisite = builder.prerequisite; + this.ringFunction = Objects.requireNonNull(builder.ringFunction, "Megastructure ring function was not set"); + this.rotationFunction = builder.rotationFunction; + this.modelLocations = Collections.unmodifiableMap(new LinkedHashMap<>(builder.modelLocations)); + this.material = builder.material; + this.materialCount = builder.materialCount; + this.handlerFactory = Objects.requireNonNull( + builder.handlerFactory, + "Megastructure handler factory was not set" + ); + this.auxiliary = builder.auxiliary; + if (this.modelLocations.isEmpty()) { + throw new IllegalStateException("Megastructure must provide at least one model: " + this.id); + } + } + + public static Builder builder(ResourceLocation id, String name) { + return new Builder(id, name); + } + + public ResourceLocation id() { + return this.id; + } + + /** Stable, human-readable name retained for old NBT and add-on APIs. */ + public String name() { + return this.name; + } + + public String displayName() { + return this.displayName; + } + + public Map modelLocations() { + return this.modelLocations; + } + + public ItemStack material() { + return this.material == null ? ItemStack.EMPTY : new ItemStack(this.material); + } + + public int materialCount() { + return this.materialCount; + } + + /** Auxiliary structures run alongside the selected primary structure. */ + public boolean auxiliary() { + return this.auxiliary; + } + + public boolean isAvailable(Context context) { + return this.prerequisite.test(context); + } + + public int ring(Context context) { + return this.ringFunction.applyAsInt(context); + } + + public ResourceLocation modelLocation(int ring) { + ResourceLocation location = this.modelLocations.get(ring); + if (location == null) { + throw new IllegalStateException( + "No model registered for megastructure " + this.id + " on ring " + ring + ); + } + return location; + } + + public float rotation(Context context, int ring, float baseRotation, float bodyRotation) { + return this.rotationFunction.apply(new RotationContext(context.body(), ring, baseRotation, bodyRotation)); + } + + public IMegastructureHandler createHandler() { + return this.handlerFactory.get(); + } + + public record Context( + CelestialBodyData body, + boolean amplified, + @Nullable PlanetaryResourceSet resources + ) { + } + + public record RotationContext( + CelestialBodyData body, + int ring, + float baseRotation, + float bodyRotation + ) { + } + + @FunctionalInterface + public interface Prerequisite { + boolean test(Context context); + } + + @FunctionalInterface + public interface RotationFunction { + float apply(RotationContext context); + } + + public static final class Builder { + private final ResourceLocation id; + private final String name; + private String displayName; + private Prerequisite prerequisite = context -> true; + private @Nullable ToIntFunction ringFunction; + private RotationFunction rotationFunction = RotationContext::baseRotation; + private final Map modelLocations = new LinkedHashMap<>(); + private @Nullable ItemLike material; + private int materialCount; + private @Nullable Supplier handlerFactory; + private boolean auxiliary; + + private Builder(ResourceLocation id, String name) { + this.id = Objects.requireNonNull(id); + this.name = Objects.requireNonNull(name); + this.displayName = "screen.anvilcraft.cfa.megastructure." + name; + } + + public Builder displayName(String displayName) { + this.displayName = Objects.requireNonNull(displayName); + return this; + } + + public Builder prerequisite(Prerequisite prerequisite) { + this.prerequisite = Objects.requireNonNull(prerequisite); + return this; + } + + public Builder ring(int ring) { + return this.ring(context -> ring); + } + + public Builder ring(ToIntFunction ringFunction) { + this.ringFunction = Objects.requireNonNull(ringFunction); + return this; + } + + public Builder rotation(RotationFunction rotationFunction) { + this.rotationFunction = Objects.requireNonNull(rotationFunction); + return this; + } + + public Builder model(int ring, ResourceLocation modelLocation) { + ResourceLocation previous = this.modelLocations.put(ring, Objects.requireNonNull(modelLocation)); + if (previous != null) { + throw new IllegalArgumentException("Duplicate model ring " + ring + " for megastructure " + this.id); + } + return this; + } + + public Builder material(ItemLike material, int count) { + if (count <= 0) { + throw new IllegalArgumentException("Megastructure material count must be positive"); + } + this.material = Objects.requireNonNull(material); + this.materialCount = count; + return this; + } + + public Builder handler(Supplier handlerFactory) { + this.handlerFactory = Objects.requireNonNull(handlerFactory); + return this; + } + + public Builder auxiliary() { + this.auxiliary = true; + return this; + } + + public Megastructure build() { + return new Megastructure(this); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceGenerator.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceGenerator.java index b5c6d092d1..cda68d34d9 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceGenerator.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceGenerator.java @@ -216,7 +216,7 @@ private static void generateGiantItems( if (gd != null) { for (PlanetResourceRecipe.WeightedEntry entry : gd.entries()) { set.addGiantItem(new PlanetaryResourceSet.WeightedItemStack( - entry.resourceId(), entry.weight() + entry.select(random), entry.weight() )); } } @@ -235,7 +235,7 @@ private static void generateGiantFluids( if (gd != null) { for (PlanetResourceRecipe.WeightedEntry entry : gd.entries()) { set.addGiantFluid(new PlanetaryResourceSet.WeightedFluidStack( - entry.resourceId(), entry.weight() + entry.select(random), entry.weight() )); } } @@ -285,7 +285,9 @@ private static boolean tryCivilization( if (random.nextInt(100) >= od.civilizationChance()) return false; for (PlanetResourceRecipe.WeightedEntry entry : od.entries()) { - ResourceLocation id = entry.resourceId(); + ResourceLocation id = entry.select(random); + // Keep accepting the old 1.21 pseudo entries when an existing + // datapack has not yet been regenerated to the nested-choice form. if ("anvilcraft:gem_amulet_random".equals(id.toString())) { ResourceLocation randomAmulet = pickRandomGemAmulet(random); if (randomAmulet != null) { @@ -374,7 +376,7 @@ private static void tryBiologicalLifeConfirmed( for (PlanetResourceRecipe.WeightedEntry entry : bd.mildExtraFluids()) { if (random.nextInt(100) < entry.weight()) { set.addBiologicalFluid(new PlanetaryResourceSet.WeightedFluidStack( - entry.resourceId(), 100 + entry.select(random), 100 )); } } @@ -400,7 +402,7 @@ private static void tryWasteland( set.setWasteland(); for (PlanetResourceRecipe.WeightedEntry entry : wd.entries()) { set.addWastelandItem(new PlanetaryResourceSet.WeightedItemStack( - entry.resourceId(), entry.weight() + entry.select(random), entry.weight() )); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceRecipe.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceRecipe.java index 32867da898..2972667161 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceRecipe.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/PlanetResourceRecipe.java @@ -1,24 +1,33 @@ package dev.dubhe.anvilcraft.block.entity.celestial; +import com.mojang.datafixers.util.Either; import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; import dev.dubhe.anvilcraft.init.recipe.ModRecipeTypes; import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.RandomSource; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.RecipeSerializer; import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.Level; +import net.minecraft.world.level.material.Fluid; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; /// 定义行星根据其天体参数产出哪些资源的配方。 /// @@ -63,26 +72,182 @@ public static Category fromName(String name) { /// === 加权条目 === - public record WeightedEntry(String id, int weight) { - public static final Codec CODEC = RecordCodecBuilder.create(ins -> ins.group( - Codec.STRING.fieldOf("id") - .forGetter(WeightedEntry::id), Codec.INT.fieldOf("weight").forGetter(WeightedEntry::weight) - ) - .apply(ins, WeightedEntry::new)); + /** A single candidate inside a possibly nested weighted entry. */ + public record WeightedChoice(String id, int weight) { + public static final Codec CODEC = RecordCodecBuilder.create(ins -> ins.group( + Codec.STRING.fieldOf("id").forGetter(WeightedChoice::id), + Codec.INT.fieldOf("weight").forGetter(WeightedChoice::weight) + ).apply(ins, WeightedChoice::new)); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + net.minecraft.network.codec.ByteBufCodecs.STRING_UTF8, + WeightedChoice::id, + ByteBufCodecs.INT, + WeightedChoice::weight, + WeightedChoice::new + ); - public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - ByteBufCodecs.STRING_UTF8, - WeightedEntry::id, - ByteBufCodecs.INT, - WeightedEntry::weight, - WeightedEntry::new - ); + public ResourceLocation resourceId() { + return ResourceLocation.parse(this.id); + } + } + + /** + * A weighted output entry. The direct {@code {id, weight}} form remains + * accepted so existing 1.21 datapacks continue to load; the {@code choices} + * form allows one entry to select uniformly/weighted among several outputs. + */ + public record WeightedEntry(List choices, int weight) { + private record DirectEntry(String id, int weight) { + private static final Codec CODEC = RecordCodecBuilder.create(ins -> ins.group( + Codec.STRING.fieldOf("id").forGetter(DirectEntry::id), + Codec.INT.fieldOf("weight").forGetter(DirectEntry::weight) + ).apply(ins, DirectEntry::new)); + } + + private record ChoiceEntry(List choices, int weight) { + private static final Codec CODEC = RecordCodecBuilder.create(ins -> ins.group( + WeightedChoice.CODEC.listOf().fieldOf("choices").forGetter(ChoiceEntry::choices), + Codec.INT.fieldOf("weight").forGetter(ChoiceEntry::weight) + ).apply(ins, ChoiceEntry::new)); + } + + public static final Codec CODEC = Codec.either(DirectEntry.CODEC, ChoiceEntry.CODEC) + .xmap( + either -> either.map( + direct -> new WeightedEntry(direct.id(), direct.weight()), + choice -> new WeightedEntry(choice.choices(), choice.weight()) + ), + entry -> entry.isDirect() + ? Either.left(new DirectEntry(entry.choices().getFirst().id(), entry.weight())) + : Either.right(new ChoiceEntry(entry.choices(), entry.weight())) + ); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + WeightedChoice.STREAM_CODEC.apply(ByteBufCodecs.list()), + WeightedEntry::choices, + ByteBufCodecs.INT, + WeightedEntry::weight, + WeightedEntry::new + ); + + public WeightedEntry { + choices = List.copyOf(choices); + if (choices.isEmpty()) { + throw new IllegalArgumentException("Planet resource entry must contain at least one choice"); + } + } + + /** Compatibility constructor for the original 1.21 direct-entry API. */ + public WeightedEntry(String id, int weight) { + this(List.of(new WeightedChoice(id, 1)), weight); + } + + public boolean isDirect() { + return this.choices.size() == 1; + } public ResourceLocation resourceId() { - return ResourceLocation.parse(id); + return this.choices.getFirst().resourceId(); + } + + public ResourceLocation select(RandomSource random) { + if (this.isDirect()) return this.resourceId(); + int totalWeight = 0; + for (WeightedChoice choice : this.choices) { + totalWeight += Math.max(0, choice.weight()); + } + if (totalWeight <= 0) return this.resourceId(); + int selected = random.nextInt(totalWeight); + for (WeightedChoice choice : this.choices) { + selected -= Math.max(0, choice.weight()); + if (selected < 0) return choice.resourceId(); + } + return this.choices.getLast().resourceId(); + } + } + + public static List entries(Consumer consumer) { + EntriesBuilder builder = new EntriesBuilder(); + consumer.accept(builder); + return builder.build(); + } + + public static final class EntriesBuilder { + private final List entries = new ArrayList<>(); + + public EntriesBuilder id(String id, int weight) { + return this.id(ResourceLocation.parse(id), weight); + } + + public EntriesBuilder id(ResourceLocation id, int weight) { + this.entries.add(new WeightedEntry(id.toString(), requirePositive(weight))); + return this; + } + + public EntriesBuilder item(ItemLike item, int weight) { + return this.id(BuiltInRegistries.ITEM.getKey(Objects.requireNonNull(item).asItem()), weight); + } + + public EntriesBuilder fluid(Fluid fluid, int weight) { + return this.id(BuiltInRegistries.FLUID.getKey(Objects.requireNonNull(fluid)), weight); + } + + public EntriesBuilder fluid(Supplier fluid, int weight) { + return this.fluid(Objects.requireNonNull(fluid).get(), weight); + } + + public EntriesBuilder chooseOne(int weight, Consumer consumer) { + ChoiceBuilder builder = new ChoiceBuilder(); + consumer.accept(builder); + this.entries.add(new WeightedEntry(builder.build(), requirePositive(weight))); + return this; + } + + public List build() { + return List.copyOf(this.entries); } } + public static final class ChoiceBuilder { + private final List choices = new ArrayList<>(); + + public ChoiceBuilder id(String id, int weight) { + return this.id(ResourceLocation.parse(id), weight); + } + + public ChoiceBuilder id(ResourceLocation id, int weight) { + this.choices.add(new WeightedChoice(id.toString(), requirePositive(weight))); + return this; + } + + public ChoiceBuilder item(ItemLike item, int weight) { + return this.id(BuiltInRegistries.ITEM.getKey(Objects.requireNonNull(item).asItem()), weight); + } + + public ChoiceBuilder fluid(Fluid fluid, int weight) { + return this.id(BuiltInRegistries.FLUID.getKey(Objects.requireNonNull(fluid)), weight); + } + + public ChoiceBuilder fluid(Supplier fluid, int weight) { + return this.fluid(Objects.requireNonNull(fluid).get(), weight); + } + + private List build() { + if (this.choices.isEmpty()) { + throw new IllegalStateException("Planet resource choice group cannot be empty"); + } + return List.copyOf(this.choices); + } + } + + private static int requirePositive(int weight) { + if (weight <= 0) throw new IllegalArgumentException("Planet resource weight must be positive"); + return weight; + } + /// === 生命概率 === public record LifeChances(int cold, int hot, int mild) { @@ -157,6 +322,24 @@ public record FluidData(String planetType, String temperature, String liquidMin, FluidData::outputFluid, FluidData::new ); + + public FluidData(String planetType, String temperature, String liquidMin, Fluid outputFluid) { + this( + planetType, + temperature, + liquidMin, + BuiltInRegistries.FLUID.getKey(Objects.requireNonNull(outputFluid)).toString() + ); + } + + public FluidData( + String planetType, + String temperature, + String liquidMin, + Supplier outputFluid + ) { + this(planetType, temperature, liquidMin, Objects.requireNonNull(outputFluid).get()); + } } /// 气态行星物品或流体资源。 @@ -255,6 +438,67 @@ public record WastelandData( ); } + /** Fluent construction API used by data generators and add-ons. */ + public static Builder builder(Category category) { + return new Builder(category); + } + + public static final class Builder { + private final Category category; + private Optional mineral = Optional.empty(); + private Optional fluid = Optional.empty(); + private Optional giant = Optional.empty(); + private Optional biological = Optional.empty(); + private Optional offering = Optional.empty(); + private Optional wasteland = Optional.empty(); + + private Builder(Category category) { + this.category = Objects.requireNonNull(category); + } + + public Builder mineral(MineralData mineral) { + this.mineral = Optional.of(Objects.requireNonNull(mineral)); + return this; + } + + public Builder fluid(FluidData fluid) { + this.fluid = Optional.of(Objects.requireNonNull(fluid)); + return this; + } + + public Builder giant(GiantData giant) { + this.giant = Optional.of(Objects.requireNonNull(giant)); + return this; + } + + public Builder biological(BiologicalData biological) { + this.biological = Optional.of(Objects.requireNonNull(biological)); + return this; + } + + public Builder offering(OfferingData offering) { + this.offering = Optional.of(Objects.requireNonNull(offering)); + return this; + } + + public Builder wasteland(WastelandData wasteland) { + this.wasteland = Optional.of(Objects.requireNonNull(wasteland)); + return this; + } + + public PlanetResourceRecipe build() { + return new PlanetResourceRecipe( + this.category, + this.mineral, + this.fluid, + this.giant, + this.biological, + this.offering, + this.wasteland + ); + } + } + /// === 编解码器 === public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(ins -> ins.group( diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/ShatteredPlanet.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/ShatteredPlanet.java index c3793d318e..f8e798d947 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/ShatteredPlanet.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/ShatteredPlanet.java @@ -24,6 +24,7 @@ public static SpecialCelestialBodyData createBody() { false, true, "planet_shattered", + null, null ); } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyData.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyData.java index 4f376463f0..e118a1b32c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyData.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyData.java @@ -22,9 +22,32 @@ public record SpecialCelestialBodyData( boolean isErrorPlanet, boolean needsCustomModel, String model, - @Nullable CompoundTag playerHeadProfile + @Nullable CompoundTag playerHeadProfile, + @Nullable CelestialTravelData landing ) implements CelestialBodyData { + /** Compatibility constructor for snapshots written before landing rules existed. */ + public SpecialCelestialBodyData( + String recipeId, + String name, + int size, + float axialTilt, + int rotationSpeed, + int magneticFieldStrength, + @Nullable Temperature temperature, + boolean hasAtmosphere, + @Nullable LiquidCoverage liquidCoverage, + boolean isErrorPlanet, + boolean needsCustomModel, + String model, + @Nullable CompoundTag playerHeadProfile + ) { + this( + recipeId, name, size, axialTilt, rotationSpeed, magneticFieldStrength, temperature, + hasAtmosphere, liquidCoverage, isErrorPlanet, needsCustomModel, model, playerHeadProfile, null + ); + } + /// 从配方和其资源路径ID创建。 public static SpecialCelestialBodyData fromRecipe(SpecialCelestialBodyRecipe recipe, String recipeId) { return new SpecialCelestialBodyData( @@ -40,7 +63,8 @@ public static SpecialCelestialBodyData fromRecipe(SpecialCelestialBodyRecipe rec recipe.isErrorPlanet(), recipe.needsCustomModel(), recipe.model(), - null + null, + recipe.landing().orElse(null) ); } @@ -60,7 +84,8 @@ public static SpecialCelestialBodyData fromPlayerHead(CompoundTag profileNbt, in false, true, "player_head", - profileNbt + profileNbt, + null ); } @@ -69,6 +94,27 @@ public boolean isPlayerHead() { return playerHeadProfile != null; } + /// Whether this discovered body has a data-driven landing destination. + public boolean isLandable() { + return landing != null; + } + + @Nullable + public CelestialTravelData landingData() { + return landing; + } + + @Nullable + public CelestialTravelData travelData() { + return landing; + } + + /** Compatibility alias for callers that use the older travel terminology. */ + @Nullable + public CelestialTravelData travel() { + return landing; + } + @Override public CelestialBodyType type() { return CelestialBodyType.SPECIAL; @@ -116,6 +162,9 @@ public CompoundTag toTag() { if (playerHeadProfile != null) { tag.put("playerHeadProfile", playerHeadProfile); } + if (landing != null) { + tag.put("landing", landing.toTag()); + } return tag; } @@ -140,10 +189,13 @@ public static SpecialCelestialBodyData fromTag(CompoundTag tag) { ? LiquidCoverage.fromName(tag.getString("liquidCoverage")) : null; CompoundTag playerHeadProfile = tag.contains("playerHeadProfile") ? tag.getCompound("playerHeadProfile") : null; + String landingKey = tag.contains("landing") ? "landing" : "travel"; + CelestialTravelData landing = tag.contains(landingKey) + ? CelestialTravelData.fromTag(tag.getCompound(landingKey)) : null; return new SpecialCelestialBodyData( recipeId, name, size, axialTilt, rotationSpeed, magneticFieldStrength, temperature, hasAtmosphere, liquidCoverage, - isErrorPlanet, needsCustomModel, model, playerHeadProfile + isErrorPlanet, needsCustomModel, model, playerHeadProfile, landing ); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyRecipe.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyRecipe.java index f5b89c7622..2d6249645f 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyRecipe.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/SpecialCelestialBodyRecipe.java @@ -61,9 +61,54 @@ public record SpecialCelestialBodyRecipe( List biologicalFluids, List offerings, List templeBlessings, - List templePunishments + List templePunishments, + Optional landing ) implements Recipe { + /** Compatibility constructor for integrations compiled before landing rules were added. */ + public SpecialCelestialBodyRecipe( + String name, + String model, + boolean needsCustomModel, + int time, + int space, + int mass, + int energy, + boolean hasAtmosphere, + Optional liquidCoverage, + int magneticFieldStrength, + int rotationSpeed, + float axialTilt, + List seedItems, + List minerals, + List fluids, + List biologicalItems, + List biologicalFluids, + List offerings, + List templeBlessings, + List templePunishments + ) { + this( + name, model, needsCustomModel, time, space, mass, energy, hasAtmosphere, liquidCoverage, + magneticFieldStrength, rotationSpeed, axialTilt, seedItems, minerals, fluids, + biologicalItems, biologicalFluids, offerings, templeBlessings, templePunishments, Optional.empty() + ); + } + + /** Alias used by integrations that describe the field as travel data. */ + public Optional landingData() { + return landing; + } + + /** Compatibility alias for datapack integrations that call this travel data. */ + public Optional travel() { + return landing; + } + + public boolean isLandable() { + return landing.isPresent(); + } + /// === WeightedEntry === public record WeightedEntry(String id, int weight) { @@ -154,7 +199,9 @@ private record ResourceFields( ResourceFields.CODEC.fieldOf("resources").forGetter( r -> new ResourceFields(r.minerals, r.fluids, r.biologicalItems, r.biologicalFluids, r.offerings, r.templeBlessings, r.templePunishments) - ) + ), + CelestialTravelData.CODEC.optionalFieldOf("landing").forGetter(SpecialCelestialBodyRecipe::landing), + CelestialTravelData.CODEC.optionalFieldOf("travel").forGetter(recipe -> Optional.empty()) ).apply(ins, SpecialCelestialBodyRecipe::fromCodec)); @SuppressWarnings("unused") @@ -163,7 +210,7 @@ private static SpecialCelestialBodyRecipe fromCodec( String texture, boolean hasAtmosphere, Optional liquidCoverage, int magneticField, int rotationSpeed, float axialTilt, List seedItems, boolean needsCustomModel, - ResourceFields res + ResourceFields res, Optional landing, Optional travel ) { return new SpecialCelestialBodyRecipe( name, texture, needsCustomModel, @@ -172,7 +219,8 @@ private static SpecialCelestialBodyRecipe fromCodec( magneticField, rotationSpeed, axialTilt, seedItems, res.minerals, res.fluids, res.biologicalItems, res.biologicalFluids, - res.offerings, res.templeBlessings, res.templePunishments + res.offerings, res.templeBlessings, res.templePunishments, + landing.isPresent() ? landing : travel ); } @@ -201,6 +249,7 @@ private static SpecialCelestialBodyRecipe fromCodec( List offerings = WeightedEntry.STREAM_CODEC.apply(ByteBufCodecs.list()).decode(buf); List templeBlessings = DemandEntry.STREAM_CODEC.apply(ByteBufCodecs.list()).decode(buf); List templePunishments = DemandEntry.STREAM_CODEC.apply(ByteBufCodecs.list()).decode(buf); + Optional landing = ByteBufCodecs.optional(CelestialTravelData.STREAM_CODEC).decode(buf); return new SpecialCelestialBodyRecipe( name, model, needsCustomModel, time, space, mass, energy, @@ -208,7 +257,8 @@ private static SpecialCelestialBodyRecipe fromCodec( magneticFieldStrength, rotationSpeed, axialTilt, seedItems, minerals, fluids, biologicalItems, biologicalFluids, - offerings, templeBlessings, templePunishments + offerings, templeBlessings, templePunishments, + landing ); } @@ -234,6 +284,7 @@ public void encode(RegistryFriendlyByteBuf buf, SpecialCelestialBodyRecipe r) { WeightedEntry.STREAM_CODEC.apply(ByteBufCodecs.list()).encode(buf, r.offerings()); DemandEntry.STREAM_CODEC.apply(ByteBufCodecs.list()).encode(buf, r.templeBlessings()); DemandEntry.STREAM_CODEC.apply(ByteBufCodecs.list()).encode(buf, r.templePunishments()); + ByteBufCodecs.optional(CelestialTravelData.STREAM_CODEC).encode(buf, r.landing()); } }; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/StarData.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/StarData.java index 0832647d90..726b50cec9 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/StarData.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/celestial/StarData.java @@ -15,9 +15,38 @@ public record StarData( int rotationSpeed, int magneticFieldStrength, int energy, - @Nullable UUID bodyUuid + @Nullable UUID bodyUuid, + boolean specialRedDwarf ) implements CelestialBodyData { + /** Backwards-compatible constructor for ordinary stars. */ + public StarData( + CelestialBodyClass bodyClass, + int size, + int colorR, + int colorG, + int colorB, + float axialTilt, + int rotationSpeed, + int magneticFieldStrength, + int energy, + @Nullable UUID bodyUuid + ) { + this( + bodyClass, + size, + colorR, + colorG, + colorB, + axialTilt, + rotationSpeed, + magneticFieldStrength, + energy, + bodyUuid, + false + ); + } + @Override public CelestialBodyType type() { return CelestialBodyType.STAR; @@ -30,7 +59,19 @@ public RingType ringType() { /// 创建一个带有新body UUID的副本,保留所有其他字段。 public StarData withBodyUuid(UUID uuid) { - return new StarData(bodyClass, size, colorR, colorG, colorB, axialTilt, rotationSpeed, magneticFieldStrength, energy, uuid); + return new StarData( + bodyClass, + size, + colorR, + colorG, + colorB, + axialTilt, + rotationSpeed, + magneticFieldStrength, + energy, + uuid, + specialRedDwarf + ); } /// 从bodySeed派生一个可复现的UUID。相同的bodySeed总是产生相同的UUID,这使得奇点水晶副本能够共享原始发现的虫洞身份。 @@ -51,6 +92,7 @@ public CompoundTag toTag() { tag.putInt("rotationSpeed", rotationSpeed); tag.putInt("magneticFieldStrength", magneticFieldStrength); tag.putInt("energy", energy); + tag.putBoolean("specialRedDwarf", specialRedDwarf); if (bodyUuid != null) { tag.putUUID("bodyUuid", bodyUuid); } @@ -72,6 +114,19 @@ public static StarData fromTag(CompoundTag tag) { int energy = tag.contains("energy") ? tag.getInt("energy") : 0; int rotSpeed = tag.contains("rotationSpeed") ? tag.getInt("rotationSpeed") : 0; UUID uuid = tag.contains("bodyUuid") ? tag.getUUID("bodyUuid") : null; - return new StarData(cls, size, r, g, b, tag.getFloat("axialTilt"), rotSpeed, mag, energy, uuid); + boolean specialRedDwarf = tag.contains("specialRedDwarf") && tag.getBoolean("specialRedDwarf"); + return new StarData( + cls, + size, + r, + g, + b, + tag.getFloat("axialTilt"), + rotSpeed, + mag, + energy, + uuid, + specialRedDwarf + ); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/heatable/HeatableBlockEntity.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/heatable/HeatableBlockEntity.java index 70d09e8e9b..f31c4e98ec 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/heatable/HeatableBlockEntity.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/heatable/HeatableBlockEntity.java @@ -1,32 +1,61 @@ package dev.dubhe.anvilcraft.block.entity.heatable; +import dev.anvilcraft.lib.v2.rpc.CallableParam; +import dev.anvilcraft.lib.v2.rpc.IRemoteCallableValidator; +import dev.anvilcraft.lib.v2.rpc.RemoteCallable; import dev.anvilcraft.lib.v2.util.Util; import dev.dubhe.anvilcraft.api.heat.HeaterManager; import dev.dubhe.anvilcraft.block.heatable.HeatableBlock; -import dev.dubhe.anvilcraft.network.HeatableSyncPacket; -import lombok.Getter; +import io.netty.buffer.ByteBuf; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.PacketFlow; import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.ChunkPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerLevelAccess; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Method; import java.util.Optional; public abstract class HeatableBlockEntity extends BlockEntity { + protected static final StreamCodec POS_STREAM_CODEC = ByteBufCodecs.VAR_LONG + .map(BlockPos::of, BlockPos::asLong); protected static final int MAX_DURATION = 1200 * 20; - @Getter protected int duration = 0; protected HeatableBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { super(type, pos, blockState); } + public int getDuration() { + return this.duration; + } + + @RemoteCallable(validator = Validator.class) + public static int getDuration( + @CallableParam(clazz = HeatableBlockEntity.class, field = "POS_STREAM_CODEC") BlockPos pos + ) { + ServerLevel level = Validator.LEVEL.get(); + try { + if (level == null) return -1; + return Util.castSafely(level.getBlockEntity(pos), HeatableBlockEntity.class) + .map(HeatableBlockEntity::getDuration) + .orElse(-1); + } finally { + Validator.LEVEL.remove(); + } + } + /** * 增加1秒 */ @@ -39,13 +68,9 @@ public void addDurationInTick(int tick) { } public void setDuration(int duration) { + if (this.duration == duration) return; this.duration = duration; this.setChanged(); - if (this.level == null || this.level.getGameTime() % 10 != 0) return; - if (this.level instanceof ServerLevel serverLevel) { - PacketDistributor.sendToPlayersTrackingChunk(serverLevel, new ChunkPos(this.getBlockPos()), - new HeatableSyncPacket(this.getBlockPos(), duration)); - } } public int getSignal() { @@ -76,10 +101,6 @@ public void onLoad() { public static void tick(Level level, BlockPos pos) { HeaterManager.addHeatableBlock(pos, level); - if (level.getGameTime() % 10 != 0) return; - PacketDistributor.sendToAllPlayers(new HeatableSyncPacket( - pos, Util.castSafely(level.getBlockEntity(pos), HeatableBlockEntity.class).map(HeatableBlockEntity::getDuration).orElse(0) - )); } public Optional getPrevTier(Level level, BlockPos pos) { @@ -87,4 +108,27 @@ public Optional getPrevTier(Level level, BlockPos pos) { if (!(state.getBlock() instanceof HeatableBlock heatable)) return Optional.empty(); return heatable.getPrevTier(level, pos, state); } + + private static final class Validator implements IRemoteCallableValidator { + private static final ThreadLocal<@Nullable ServerLevel> LEVEL = new ThreadLocal<>(); + + @Override + public boolean validate(IPayloadContext ctx, Method method, Object[] args) { + Validator.LEVEL.remove(); + if (ctx.flow() != PacketFlow.SERVERBOUND + || !(ctx.player() instanceof ServerPlayer player) + || args.length != 1 + || !(args[0] instanceof BlockPos pos) + || !(player.level() instanceof ServerLevel level) + || !level.isLoaded(pos) + || !(level.getBlockEntity(pos) instanceof HeatableBlockEntity blockEntity) + || !AbstractContainerMenu.stillValid( + ContainerLevelAccess.create(level, pos), player, blockEntity.getBlockState().getBlock() + )) { + return false; + } + Validator.LEVEL.set(level); + return true; + } + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/AcceleratorHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/AcceleratorHandler.java index 89b9e32df6..9821378482 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/AcceleratorHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/AcceleratorHandler.java @@ -5,11 +5,16 @@ import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyMatcher; import dev.dubhe.anvilcraft.block.entity.celestial.PlanetaryResourceSet; import dev.dubhe.anvilcraft.block.entity.celestial.StarData; +import dev.dubhe.anvilcraft.init.ModMegastructures; +import dev.dubhe.anvilcraft.network.QuenchedOutMusicPacket; import lombok.Getter; import lombok.Setter; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; +import net.neoforged.neoforge.network.PacketDistributor; public class AcceleratorHandler extends BaseMegastructureHandler { @@ -28,6 +33,22 @@ public class AcceleratorHandler extends BaseMegastructureHandler { @Getter private int collapseAnimTicks = 0; + /// === 淬灭序曲(超新星前奏曲)=== + /// 曲目全长约 71.77 秒(≈1435 刻),最后一秒(20 刻)恰为超新星爆发开始。 + /// 规则: + /// 1. 演化开始时若距超新星不足 1 分 12 秒(1440 刻),曲目无法完整播放则不播; + /// 2. 否则在爆炸开始前 1415 刻开始播放,使曲目尾音对准爆炸开始; + /// 3. 演化期间锻星砧或增幅器被破坏,音乐立即中断。 + private static final int QUENCHED_FULL_PLAY_TICKS = 1440; + private static final int QUENCHED_EXPLOSION_LEAD_TICKS = 1415; + private static final int SUPER_STAGE3_TICKS = 10; + + private boolean quenchedScheduled = false; + private long quenchedStartTick = -1; + private boolean quenchedStarted = false; + private boolean quenchedCanceled = false; + private boolean quenchedSupernovaFired = false; + @Override public String name() { return "stellar_evolution_accelerator"; @@ -37,9 +58,15 @@ public boolean isActive() { return stage >= 1 && stage <= 4; } + @Override + public boolean isAuxiliaryActive(CelestialForgingAnvilBlockEntity be) { + return this.isActive(); + } + @Override public void serverTick(CelestialForgingAnvilBlockEntity be) { if (be.getLevel() == null || be.getLevel().isClientSide()) return; + this.tickQuenchedOutMusic(be); if (!be.isAmplifierPresent()) return; if (stage < 1 || stage > 4) return; @@ -77,6 +104,8 @@ public void onBuild(CelestialForgingAnvilBlockEntity be) { initGiantPhase(be, ageX, energyY); } + scheduleQuenchedOut(be, cls, ageX, energyY); + be.setChanged(); be.getLevel().sendBlockUpdated(be.getBlockPos(), be.getBlockState(), be.getBlockState(), 3); } @@ -102,12 +131,79 @@ private void initGiantPhase(CelestialForgingAnvilBlockEntity be, int ageX, int e be.getLevel().sendBlockUpdated(be.getBlockPos(), be.getBlockState(), be.getBlockState(), 3); } + /// 在演化开始时决定是否播放入场曲,并预定开始时刻。 + /// 仅当演化路径真的会走向超新星(非 M 型主序星),且距爆炸足以完整播放曲目时才会预定。 + private void scheduleQuenchedOut(CelestialForgingAnvilBlockEntity be, CelestialBodyClass cls, int ageX, int energyY) { + quenchedScheduled = false; + quenchedStartTick = -1; + quenchedStarted = false; + quenchedCanceled = false; + if (cls == CelestialBodyClass.M_MAIN) { + /// M 型主序星走阶段 4 白矮星路线,无超新星,不播。 + return; + } + int predicted = predictedTicksUntilSupernova(cls, ageX, energyY); + if (predicted < QUENCHED_FULL_PLAY_TICKS) { + /// 距爆发不足 1 分 12 秒,曲目无法完整播放,不播。 + return; + } + quenchedScheduled = true; + quenchedStartTick = be.getLevel().getGameTime() + (long) predicted - QUENCHED_EXPLOSION_LEAD_TICKS; + } + + /// 预测从演化开始到超新星爆发开始的总刻数(与各阶段实际计时的算法一致)。 + private int predictedTicksUntilSupernova(CelestialBodyClass cls, int ageX, int energyY) { + int giantTicks = predictedGiantTicks(ageX, energyY); + if (cls.isMainSequence() && cls != CelestialBodyClass.M_MAIN) { + int mainTicks = CelestialBodyMatcher.countPixelsRightInAgeTemp(ageX, energyY) * 2400; + return mainTicks + giantTicks + SUPER_STAGE3_TICKS; + } + return giantTicks + SUPER_STAGE3_TICKS; + } + + /// 与 {@link #initGiantPhase} 相同的巨星级阶段时长计算。 + private int predictedGiantTicks(int ageX, int energyY) { + int pixelsDown = CelestialBodyMatcher.countPixelsDownInAgeTempSp(ageX, energyY); + int totalPixels = CelestialBodyMatcher.countTotalColoredPixelsInAgeTempSpColumn(ageX, energyY); + if (totalPixels <= 0) totalPixels = 1; + return Math.max((int) ((float) pixelsDown / totalPixels * 2400), 1); + } + + /// 每服务器刻推进音乐状态:定时开始、增幅器缺失时立即中断。 + private void tickQuenchedOutMusic(CelestialForgingAnvilBlockEntity be) { + if (stage < 1 || stage > 4) return; + if (!be.isAmplifierPresent()) { + /// 增幅器被破坏:已开播则立即中断,未开播则取消预定。 + if (quenchedStarted) { + quenchedStarted = false; + quenchedCanceled = true; + sendQuenchedOutMusic(be, false); + } else if (quenchedScheduled) { + quenchedCanceled = true; + quenchedScheduled = false; + } + return; + } + if (quenchedCanceled) return; + if (quenchedScheduled && !quenchedStarted && be.getLevel().getGameTime() >= quenchedStartTick) { + quenchedScheduled = false; + quenchedStarted = true; + sendQuenchedOutMusic(be, true); + } + } + + private void sendQuenchedOutMusic(CelestialForgingAnvilBlockEntity be, boolean start) { + if (!(be.getLevel() instanceof ServerLevel serverLevel)) return; + PacketDistributor.sendToPlayersTrackingChunk( + serverLevel, + new ChunkPos(be.getBlockPos()), + new QuenchedOutMusicPacket(be.getBlockPos(), start) + ); + } + private boolean isDysonSphereBuilt(CelestialForgingAnvilBlockEntity be) { - if (be.getActiveMegastructureIndex() < 0) return false; - var option = be.getActiveMegastructureOption(); - if (option == null) return false; - String name = option.megastructure(); - return "dyson_sphere_small".equals(name) || "dyson_sphere_large".equals(name); + return ModMegastructures.DYSON_SPHERE_SMALL.getId().equals(be.getActiveMegastructureId()) + || ModMegastructures.DYSON_SPHERE_LARGE.getId().equals(be.getActiveMegastructureId()); } private void tickStage1(CelestialForgingAnvilBlockEntity be) { @@ -213,6 +309,8 @@ private void triggerSupernova(CelestialForgingAnvilBlockEntity be) { ); } createRemnant(be); + /// 超新星已开始:曲目最后一秒正在播放,onClear 时不再强制中断。 + this.quenchedSupernovaFired = true; /// 通过管理器清除所有巨构 be.getMegastructureManager().clearAllMegastructures(be); be.setChanged(); @@ -442,6 +540,17 @@ public void onClear(CelestialForgingAnvilBlockEntity be) { this.dysonDestroyed = false; this.dysonDestroyTick = -1; this.collapseAnimTicks = 0; + if (quenchedSupernovaFired) { + /// 超新星已开始:曲目最后一秒正在播放,让它自然播完。 + quenchedSupernovaFired = false; + } else if (quenchedStarted) { + /// 演化被中断(解锁、锻星砧被破坏等):立即停止音乐。 + sendQuenchedOutMusic(be, false); + } + quenchedScheduled = false; + quenchedStartTick = -1; + quenchedStarted = false; + quenchedCanceled = false; } @Override @@ -454,6 +563,11 @@ public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { tag.putInt("acceleratorOriginalSize", originalSize); tag.putBoolean("acceleratorDysonDestroyed", dysonDestroyed); tag.putLong("acceleratorDysonDestroyTick", dysonDestroyTick); + tag.putBoolean("quenchedScheduled", quenchedScheduled); + tag.putLong("quenchedStartTick", quenchedStartTick); + tag.putBoolean("quenchedStarted", quenchedStarted); + tag.putBoolean("quenchedCanceled", quenchedCanceled); + tag.putBoolean("quenchedSupernovaFired", quenchedSupernovaFired); } @Override @@ -466,6 +580,11 @@ public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { this.originalSize = tag.getInt("acceleratorOriginalSize"); this.dysonDestroyed = tag.getBoolean("acceleratorDysonDestroyed"); this.dysonDestroyTick = tag.getLong("acceleratorDysonDestroyTick"); + this.quenchedScheduled = tag.getBoolean("quenchedScheduled"); + this.quenchedStartTick = tag.getLong("quenchedStartTick"); + this.quenchedStarted = tag.getBoolean("quenchedStarted"); + this.quenchedCanceled = tag.getBoolean("quenchedCanceled"); + this.quenchedSupernovaFired = tag.getBoolean("quenchedSupernovaFired"); } @Override diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/BaseMegastructureHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/BaseMegastructureHandler.java index 5c112e3129..78b903b97b 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/BaseMegastructureHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/BaseMegastructureHandler.java @@ -5,6 +5,7 @@ import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLaserInterfaceBlockEntity; import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLogisticsInterfaceBlockEntity; import dev.dubhe.anvilcraft.block.entity.CfaInterfaceScanner; +import dev.dubhe.anvilcraft.init.block.ModFluids; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.nbt.CompoundTag; @@ -60,6 +61,15 @@ protected List findFluidInterfac return CfaInterfaceScanner.findFluidInterfaces(be.getLevel(), be.getBlockPos()); } + /** Consumes every Primordial Matter stack exposed by the connected fluid interfaces. */ + protected int consumePrimordialMatter(CelestialForgingAnvilBlockEntity be) { + long consumed = 0L; + for (CelestialForgingAnvilFluidInterfaceBlockEntity fluidInterface : findFluidInterfaces(be)) { + consumed += fluidInterface.drainFluid(ModFluids.PRIMORDIAL_MATTER.get()); + } + return (int) Math.min(consumed, Integer.MAX_VALUE); + } + protected CfaInterfaceScanner.PrioritizedInterfaces findOutputLogisticsInterfaces( CelestialForgingAnvilBlockEntity be ) { diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ColliderHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ColliderHandler.java index 98e16ef551..dc8d0a3846 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ColliderHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ColliderHandler.java @@ -8,6 +8,8 @@ import dev.dubhe.anvilcraft.init.recipe.ModRecipeTypes; import dev.dubhe.anvilcraft.recipe.anvil.collision.AnvilCollisionCraftRecipe; import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.RecipeHolder; @@ -59,7 +61,6 @@ public void serverTick(CelestialForgingAnvilBlockEntity be) { if (starMissing) { if (cycleRemaining > 0 || !reservedAnvil.isEmpty() || !reservedHitBlock.isEmpty()) { outputColliderReservedItems(be); - resetColliderState(be); } broadcastColliderState(be, false, true); return; @@ -69,7 +70,6 @@ public void serverTick(CelestialForgingAnvilBlockEntity be) { if (be.isPowerInsufficient()) { outputColliderReservedItems(be); - resetColliderState(be); return; } @@ -98,7 +98,6 @@ private void broadcastColliderState(CelestialForgingAnvilBlockEntity be, boolean if (blockEntity instanceof CelestialForgingAnvilLogisticsInterfaceBlockEntity logiBe) { logiBe.setColliderProcessing(processing); logiBe.setColliderStarMissing(starMissing); - logiBe.setChanged(); } }, be ); @@ -111,7 +110,6 @@ private void broadcastColliderTargets(CelestialForgingAnvilBlockEntity be) { var blockEntity = be.getLevel().getBlockEntity(checkPos); if (blockEntity instanceof CelestialForgingAnvilLogisticsInterfaceBlockEntity logiBe) { logiBe.setColliderTargetItems(new ArrayList<>(targetItems)); - logiBe.setChanged(); } }, be ); @@ -258,7 +256,6 @@ private void markLogisticsProcessing(CelestialForgingAnvilBlockEntity be, BlockP if (blockEntity instanceof CelestialForgingAnvilLogisticsInterfaceBlockEntity logiBe) { logiBe.setColliderTargetItems(new ArrayList<>(targetItems)); logiBe.setColliderProcessing(processing); - logiBe.setChanged(); } } @@ -388,11 +385,39 @@ private void resetColliderState(CelestialForgingAnvilBlockEntity be) { reservedHitBlock = ItemStack.EMPTY; activeSpeed = 0; broadcastColliderState(be, false, false); + be.setChanged(); + } + + @Override + public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + tag.putInt("colliderCooldown", this.cooldown); + tag.putInt("colliderCycleRemaining", this.cycleRemaining); + tag.putInt("colliderActiveSpeed", this.activeSpeed); + tag.putInt("colliderLogisticsRoundRobin", this.logisticsRoundRobin); + if (!this.reservedAnvil.isEmpty()) { + tag.put("colliderReservedAnvil", this.reservedAnvil.save(registries)); + } + if (!this.reservedHitBlock.isEmpty()) { + tag.put("colliderReservedHitBlock", this.reservedHitBlock.save(registries)); + } + } + + @Override + public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + this.cooldown = Math.max(tag.getInt("colliderCooldown"), 0); + this.cycleRemaining = Math.max(tag.getInt("colliderCycleRemaining"), 0); + this.activeSpeed = tag.getInt("colliderActiveSpeed"); + this.logisticsRoundRobin = Math.max(tag.getInt("colliderLogisticsRoundRobin"), 0); + this.reservedAnvil = tag.contains("colliderReservedAnvil") + ? ItemStack.parse(registries, tag.getCompound("colliderReservedAnvil")).orElse(ItemStack.EMPTY) + : ItemStack.EMPTY; + this.reservedHitBlock = tag.contains("colliderReservedHitBlock") + ? ItemStack.parse(registries, tag.getCompound("colliderReservedHitBlock")).orElse(ItemStack.EMPTY) + : ItemStack.EMPTY; } @Override public void onClear(CelestialForgingAnvilBlockEntity be) { outputColliderReservedItems(be); - resetColliderState(be); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/DysonSphereHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/DysonSphereHandler.java index 880685e7cb..0fdadcfc01 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/DysonSphereHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/DysonSphereHandler.java @@ -2,12 +2,32 @@ import dev.dubhe.anvilcraft.api.power.PowerComponentType; import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilBlockEntity; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyClass; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyMatcher; +import dev.dubhe.anvilcraft.block.entity.celestial.GiantPlanetData; +import dev.dubhe.anvilcraft.block.entity.celestial.PlanetaryResourceSet; import dev.dubhe.anvilcraft.block.entity.celestial.StarData; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.Level; +/** Power production and primordial-matter processing for all Dyson spheres. */ public class DysonSphereHandler extends BaseMegastructureHandler { + private static final String BROWN_DWARF_NAME = "dyson_sphere_brown_dwarf"; + private static final String SMALL_NAME = "dyson_sphere_small"; + private static final int STABLE_SUPPLY_TICKS = 40; + private static final int MATTER_PER_BUCKET = 1_000; + private static final int BROWN_DWARF_TIER_1 = 250; + private static final int BROWN_DWARF_TIER_2 = 500; + private static final int BROWN_DWARF_TIER_3 = MATTER_PER_BUCKET; + private static final int BROWN_DWARF_TIER_4 = 2 * MATTER_PER_BUCKET; + private static final long RED_DWARF_MATTER = 12_800L * MATTER_PER_BUCKET; - private int cachedGridConsumption = 0; + private int cachedGridConsumption; private final String name; + private int stableSupplyTicks; + private int stableSupplyTier; + private long accumulatedExcessMatter; public DysonSphereHandler(String name) { this.name = name; @@ -18,25 +38,83 @@ public String name() { return name; } + @Override + public void onBuild(CelestialForgingAnvilBlockEntity be) { + resetSupplyState(); + } + + @Override + public void onClear(CelestialForgingAnvilBlockEntity be) { + resetSupplyState(); + } + @Override public void serverTick(CelestialForgingAnvilBlockEntity be) { - // Dyson Sphere is passive + Level level = be.getLevel(); + if (level == null || level.isClientSide()) return; + + boolean brownDwarfSphere = isBrownDwarfSphere(); + boolean smallDwarfSphere = isSmallDwarfSphere(be); + if (!brownDwarfSphere && !smallDwarfSphere) { + updateSupplyState(0, be); + return; + } + + int supplied = consumePrimordialMatter(be); + if (brownDwarfSphere) { + updateSupplyState(supplyTier(supplied), be); + if (supplied > BROWN_DWARF_TIER_4 + && be.getCelestialBodyData() instanceof GiantPlanetData brown + && brown.brownDwarf()) { + this.accumulatedExcessMatter += supplied - BROWN_DWARF_TIER_4; + be.setChanged(); + if (this.accumulatedExcessMatter >= RED_DWARF_MATTER) { + transformToRedDwarf(be, brown); + } + } + } else if (isSmallDwarfSphere(be)) { + updateSupplyState(supplied >= BROWN_DWARF_TIER_4 ? 1 : 0, be); + } else { + updateSupplyState(0, be); + } } @Override public int getOutputPower(CelestialForgingAnvilBlockEntity be) { - if (!be.isAmplifierPresent()) return 0; - if (!(be.getCelestialBodyData() instanceof StarData star)) return 0; - if (be.isAcceleratorActive() && be.getAcceleratorStage() == 1) { + boolean specialBrownStar = false; + boolean brownDwarf = false; + if (isBrownDwarfSphere()) { + specialBrownStar = be.getCelestialBodyData() instanceof StarData star && star.specialRedDwarf(); + brownDwarf = be.getCelestialBodyData() instanceof GiantPlanetData brown && brown.brownDwarf(); + if (!brownDwarf && !specialBrownStar) return 0; + } else if (!(be.getCelestialBodyData() instanceof StarData)) { + return 0; + } + if (!brownDwarf && !specialBrownStar && !be.isAmplifierPresent()) return 0; + + if (be.isAcceleratorActive() && be.getAcceleratorStage() == 1 && be.isAmplifierPresent()) { return Math.max(cachedGridConsumption * 2, cachedGridConsumption + 1); } - int e = star.energy(); - int r = star.size(); - if (e > 0 && r > 0) { - int powerMW = (e * r * r) / 800; - return powerMW * 1000; + + int energy; + int radius; + int denominator; + if (brownDwarf) { + GiantPlanetData brown = (GiantPlanetData) be.getCelestialBodyData(); + energy = effectiveEnergy(brown.energy(), be); + radius = Math.clamp(brown.size(), 0, 64); + denominator = 1_600; + } else { + StarData star = (StarData) be.getCelestialBodyData(); + energy = Math.clamp(star.energy(), 0, 64); + radius = Math.clamp(star.size(), 0, 64); + denominator = 800; } - return 0; + if (energy <= 0 || radius <= 0) return 0; + + long basePower = ((long) energy * radius * radius / denominator) * 1_000L; + long boostedPower = applyBoost(basePower, be); + return (int) Math.min(boostedPower, Integer.MAX_VALUE); } @Override @@ -49,8 +127,164 @@ public void gridTick(CelestialForgingAnvilBlockEntity be) { if (be.getGrid() != null && be.isAcceleratorActive() && be.getAcceleratorStage() == 1 - && be.getActiveMegastructureIndex() >= 0) { + && be.hasActiveMegastructure()) { this.cachedGridConsumption = be.getGrid().getConsume(); } } + + @Override + public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + writeState(tag); + } + + @Override + public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.loadAdditional(tag, registries); + readState(tag); + } + + @Override + public void writeUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { + super.writeUpdateTag(tag, registries); + writeState(tag); + } + + @Override + public void readUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { + super.readUpdateTag(tag, registries); + readState(tag); + } + + private void writeState(CompoundTag tag) { + String prefix = statePrefix(); + tag.putInt(prefix + "StableTicks", stableSupplyTicks); + tag.putInt(prefix + "StableTier", stableSupplyTier); + tag.putLong(prefix + "ExcessMatter", accumulatedExcessMatter); + } + + private void readState(CompoundTag tag) { + String prefix = statePrefix(); + stableSupplyTicks = Math.clamp(tag.getInt(prefix + "StableTicks"), 0, STABLE_SUPPLY_TICKS); + stableSupplyTier = Math.clamp(tag.getInt(prefix + "StableTier"), 0, 4); + accumulatedExcessMatter = Math.max(tag.getLong(prefix + "ExcessMatter"), 0L); + } + + private String statePrefix() { + return "dysonSphere_" + name + "_"; + } + + private boolean isBrownDwarfSphere() { + return BROWN_DWARF_NAME.equals(name); + } + + private boolean isSmallDwarfSphere(CelestialForgingAnvilBlockEntity be) { + if (!SMALL_NAME.equals(name) || !(be.getCelestialBodyData() instanceof StarData star)) return false; + return star.size() < 48 + && (star.bodyClass() == CelestialBodyClass.M_MAIN + || star.bodyClass() == CelestialBodyClass.K_MAIN + || star.bodyClass() == CelestialBodyClass.G_MAIN); + } + + private static int supplyTier(int amount) { + if (amount >= BROWN_DWARF_TIER_4) return 4; + if (amount >= BROWN_DWARF_TIER_3) return 3; + if (amount >= BROWN_DWARF_TIER_2) return 2; + if (amount >= BROWN_DWARF_TIER_1) return 1; + return 0; + } + + private void updateSupplyState(int tier, CelestialForgingAnvilBlockEntity be) { + int oldActiveTier = activeSupplyTier(); + int oldStableTicks = stableSupplyTicks; + int oldStableTier = stableSupplyTier; + if (tier <= 0) { + stableSupplyTicks = 0; + stableSupplyTier = 0; + } else if (tier != stableSupplyTier) { + stableSupplyTier = tier; + stableSupplyTicks = 1; + } else { + stableSupplyTicks = Math.min(stableSupplyTicks + 1, STABLE_SUPPLY_TICKS); + } + if (oldStableTicks != stableSupplyTicks || oldStableTier != stableSupplyTier) { + be.setChanged(); + } + if (oldActiveTier != activeSupplyTier()) { + markPowerStateChanged(be); + } + } + + private int activeSupplyTier() { + return stableSupplyTicks >= STABLE_SUPPLY_TICKS ? stableSupplyTier : 0; + } + + private long applyBoost(long basePower, CelestialForgingAnvilBlockEntity be) { + int tier = activeSupplyTier(); + if (tier == 0) return basePower; + + if (isBrownDwarfSphere() && be.getCelestialBodyData() instanceof StarData star + && star.specialRedDwarf()) { + return basePower * 2L; + } + if (isBrownDwarfSphere()) { + return switch (tier) { + case 1 -> basePower * 3L / 2L; + case 2 -> basePower * 2L; + case 3 -> basePower * 3L; + default -> basePower * 5L; + }; + } + if (!isSmallDwarfSphere(be) || !(be.getCelestialBodyData() instanceof StarData star)) return basePower; + return switch (star.bodyClass()) { + case M_MAIN -> basePower * 2L; + case K_MAIN -> basePower * 3L / 2L; + case G_MAIN -> basePower * 5L / 4L; + default -> basePower; + }; + } + + private static int effectiveEnergy(int storedEnergy, CelestialForgingAnvilBlockEntity be) { + if (storedEnergy > 0) return Math.clamp(storedEnergy, 0, 64); + return Math.clamp(be.getAnvilCount(3), 0, 64); + } + + private void transformToRedDwarf(CelestialForgingAnvilBlockEntity be, GiantPlanetData brown) { + int energy = effectiveEnergy(brown.energy(), be); + int[] rgb = CelestialBodyMatcher.getStarColor(energy); + be.setCelestialBodyData(new StarData( + CelestialBodyClass.M_MAIN, + Math.clamp(brown.size(), 1, 64), + rgb[0], + rgb[1], + rgb[2], + brown.axialTilt(), + brown.rotationSpeed(), + brown.magneticFieldStrength(), + energy, + StarData.uuidFromBodySeed(be.getBodySeed()), + true + )); + be.setPlanetaryResourceSet(new PlanetaryResourceSet()); + accumulatedExcessMatter = 0L; + stableSupplyTicks = 0; + stableSupplyTier = 0; + markPowerStateChanged(be); + } + + private void resetSupplyState() { + cachedGridConsumption = 0; + stableSupplyTicks = 0; + stableSupplyTier = 0; + accumulatedExcessMatter = 0L; + } + + private static void markPowerStateChanged(CelestialForgingAnvilBlockEntity be) { + be.setChanged(); + if (be.getGrid() != null) be.getGrid().markChanged(); + Level level = be.getLevel(); + if (level != null && !level.isClientSide()) { + level.sendBlockUpdated(be.getBlockPos(), be.getBlockState(), be.getBlockState(), 3); + } + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ExcavatorHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ExcavatorHandler.java index 215228e74d..1f4ce692e9 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ExcavatorHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/ExcavatorHandler.java @@ -41,6 +41,11 @@ public String name() { return "planet_excavator"; } + @Override + public LaserRequirement getLaserRequirement() { + return new LaserRequirement(LASER_THRESHOLD, false); + } + @SuppressWarnings("checkstyle:VariableDeclarationUsageDistance") @Override public void serverTick(CelestialForgingAnvilBlockEntity be) { diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/IMegastructureHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/IMegastructureHandler.java index efd87e4d03..218b29beb6 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/IMegastructureHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/IMegastructureHandler.java @@ -6,6 +6,7 @@ import net.minecraft.nbt.CompoundTag; public interface IMegastructureHandler { + LaserRequirement NO_LASER_REQUIREMENT = new LaserRequirement(0, false); String name(); @@ -15,6 +16,18 @@ public interface IMegastructureHandler { void onClear(CelestialForgingAnvilBlockEntity be); + /** + * Reports whether an auxiliary definition currently owns its ring. + * Auxiliary handlers must persist and synchronize the state used here. + */ + default boolean isAuxiliaryActive(CelestialForgingAnvilBlockEntity be) { + return false; + } + + /** Releases runtime registrations without clearing persistent megastructure state. */ + default void onUnload(CelestialForgingAnvilBlockEntity be) { + } + void saveAdditional(CompoundTag tag, HolderLookup.Provider registries); void loadAdditional(CompoundTag tag, HolderLookup.Provider registries); @@ -37,4 +50,12 @@ default PowerComponentType getComponentType() { default void gridTick(CelestialForgingAnvilBlockEntity be) { } + + /** Laser level and mode required by every interface connected to this structure. */ + default LaserRequirement getLaserRequirement() { + return IMegastructureHandler.NO_LASER_REQUIREMENT; + } + + record LaserRequirement(int level, boolean gamma) { + } } diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/MatterDecompressorHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/MatterDecompressorHandler.java index e1e16ecb13..31d2deca17 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/MatterDecompressorHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/MatterDecompressorHandler.java @@ -20,6 +20,11 @@ public String name() { return "matter_decompressor"; } + @Override + public LaserRequirement getLaserRequirement() { + return new LaserRequirement(1, true); + } + @Override public void serverTick(CelestialForgingAnvilBlockEntity be) { if (be.getLevel() == null || be.getLevel().isClientSide()) return; diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/TempleHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/TempleHandler.java index 29ee621f58..f566972702 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/TempleHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/TempleHandler.java @@ -91,7 +91,6 @@ public void pushTempleDemandToLogistics(CelestialForgingAnvilBlockEntity be) { logiBe.setTempleDemandCount(demandSatisfied ? 0 : demandCount); logiBe.setTempleDemandProgress(demandSatisfied ? 0 : demandProgress); logiBe.setTempleDemandSatisfied(demandSatisfied); - logiBe.setChanged(); } }, be ); diff --git a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/WormholeStabilizerHandler.java b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/WormholeStabilizerHandler.java index 86f6bb7239..555d2fe1d3 100644 --- a/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/WormholeStabilizerHandler.java +++ b/src/main/java/dev/dubhe/anvilcraft/block/entity/megastructure/WormholeStabilizerHandler.java @@ -135,6 +135,19 @@ public void onClear(CelestialForgingAnvilBlockEntity be) { cleanupWormholeChunkLoading(be.getLevel()); } + @Override + public void onUnload(CelestialForgingAnvilBlockEntity be) { + if (registered && be.getLevel() != null && !be.getLevel().isClientSide()) { + WormholeNetwork.get().unregister(be.getLevel(), be.getBlockPos()); + } + registered = false; + justReconnected = false; + lastSeenItems.clear(); + lastFluidSnapshot.clear(); + stopLocalLaserOutputs(be); + cleanupWormholeChunkLoading(be.getLevel()); + } + public void addPortal(Cube323PartHalf side, BlockPos portalPos, CelestialForgingAnvilBlockEntity be) { if (side != Cube323PartHalf.BOTTOM_N && side != Cube323PartHalf.BOTTOM_S && side @@ -191,6 +204,8 @@ public void syncLogisticsOnChange(BlockPos interfacePos, int changedSlot, Celest if (!ItemStack.matches(localStack, canonStack) || localStack.getCount() != canonStack.getCount()) { canonical.set(changedSlot, new UnlimitedItemStack(localStack)); states.setDirty(); + // Push the value just accepted into canonical state, not the stale snapshot. + canonStack = localStack.copy(); } WormholeNetwork network = WormholeNetwork.get(); @@ -510,7 +525,10 @@ private void clearLocalInterfaces(CelestialForgingAnvilBlockEntity be) { } } - /// 重置激光接口,使输出激光立即停止 + stopLocalLaserOutputs(be); + } + + private void stopLocalLaserOutputs(CelestialForgingAnvilBlockEntity be) { Map laserMap = getLaserInterfacesMap(be); for (var entry : laserMap.entrySet()) { CelestialForgingAnvilLaserInterfaceBlockEntity localBe = entry.getValue(); @@ -553,16 +571,7 @@ public void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { public void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) { this.bodyUuid = tag.contains("wormholeBodyUuid") ? tag.getUUID("wormholeBodyUuid") : null; this.registered = false; - this.portals.clear(); - if (tag.contains("portals")) { - CompoundTag portalTag = tag.getCompound("portals"); - for (String key : portalTag.getAllKeys()) { - CompoundTag posTag = portalTag.getCompound(key); - Cube323PartHalf side = Cube323PartHalf.valueOf(key.toUpperCase()); - BlockPos pos = new BlockPos(posTag.getInt("x"), posTag.getInt("y"), posTag.getInt("z")); - portals.put(side, pos); - } - } + this.loadPortals(tag); } @Override @@ -573,5 +582,29 @@ public void writeUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { @Override public void readUpdateTag(CompoundTag tag, HolderLookup.Provider registries) { this.bodyUuid = tag.contains("wormholeBodyUuid") ? tag.getUUID("wormholeBodyUuid") : null; + this.loadPortals(tag); + } + + /** Reads both the current handler-owned state and the legacy CFA portal tag. */ + private void loadPortals(CompoundTag tag) { + this.portals.clear(); + if (!tag.contains("portals")) return; + CompoundTag portalTag = tag.getCompound("portals"); + for (String key : portalTag.getAllKeys()) { + Cube323PartHalf side = null; + for (Cube323PartHalf candidate : Cube323PartHalf.values()) { + if (candidate.name().equalsIgnoreCase(key)) { + side = candidate; + break; + } + } + if (side == null) continue; + CompoundTag posTag = portalTag.getCompound(key); + this.portals.put(side, new BlockPos( + posTag.getInt("x"), + posTag.getInt("y"), + posTag.getInt("z") + )); + } } } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/event/ClientEventListener.java b/src/main/java/dev/dubhe/anvilcraft/client/event/ClientEventListener.java index 20f042b9c1..60ea5573f9 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/event/ClientEventListener.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/event/ClientEventListener.java @@ -4,13 +4,12 @@ import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.api.sound.SoundHelper; import dev.dubhe.anvilcraft.api.thought.ThoughtManager; +import dev.dubhe.anvilcraft.api.tooltip.HudTooltipManager; import dev.dubhe.anvilcraft.client.AnvilCraftClient; import dev.dubhe.anvilcraft.client.gui.screen.StorageScreen; import dev.dubhe.anvilcraft.client.gui.tooltip.FilterContentHoverWindow; import dev.dubhe.anvilcraft.client.init.ModKeyMappings; -import dev.dubhe.anvilcraft.client.rpc.SettingClientStub; import dev.dubhe.anvilcraft.client.rpc.StorageTerminalClientStub; -import dev.dubhe.anvilcraft.client.rpc.TerminalJeiStorageCache; import dev.dubhe.anvilcraft.client.support.AmuletSelectorSupport; import dev.dubhe.anvilcraft.client.support.StructureDiskPreviewSupport; import dev.dubhe.anvilcraft.client.support.TerminalRemoteOverlay; @@ -23,7 +22,6 @@ import dev.dubhe.anvilcraft.network.DragonRodStopDevourPacket; import dev.dubhe.anvilcraft.network.OpenHammerAnvilPacket; import dev.dubhe.anvilcraft.network.UsePillBoxPacket; -import dev.dubhe.anvilcraft.saved.setting.PlayerSettings; import dev.dubhe.anvilcraft.util.BlockHighlightUtil; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; @@ -43,6 +41,7 @@ import net.neoforged.neoforge.client.event.RenderLevelStageEvent; import net.neoforged.neoforge.client.event.RenderTooltipEvent; import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.event.level.LevelEvent; import net.neoforged.neoforge.network.PacketDistributor; import javax.annotation.Nullable; @@ -78,10 +77,15 @@ public static void onRenderBlockOverlay(RenderBlockScreenEffectEvent event) { public static void onClientPlayerDisconnect(ClientPlayerNetworkEvent.LoggingOut event) { SoundHelper.INSTANCE.clear(); StorageTerminalClientStub.clear(); - TerminalJeiStorageCache.clear(); TerminalRemoteOverlay.setHovering(ItemStack.EMPTY); - PlayerSettings.clearClient(); - SettingClientStub.clearCache(); + HudTooltipManager.INSTANCE.resetClientState(); + } + + @SubscribeEvent + public static void onClientLevelUnload(LevelEvent.Unload event) { + if (event.getLevel() instanceof ClientLevel) { + HudTooltipManager.INSTANCE.resetClientState(); + } } @SubscribeEvent diff --git a/src/main/java/dev/dubhe/anvilcraft/client/event/QuenchedOutMusicHandler.java b/src/main/java/dev/dubhe/anvilcraft/client/event/QuenchedOutMusicHandler.java new file mode 100644 index 0000000000..2b89f205a0 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/client/event/QuenchedOutMusicHandler.java @@ -0,0 +1,89 @@ +package dev.dubhe.anvilcraft.client.event; + +import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.init.ModSoundEvents; +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.core.BlockPos; +import net.minecraft.sounds.SoundSource; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.ClientTickEvent; + +import java.util.HashMap; +import java.util.Map; + +/** + * 淬灭序曲客户端播放器。 + * + *

+ * 在锻星砧位置以固定音源播放超新星前奏曲 {@code quenched_out}(约 1 分 12 秒,不循环)。 + * 由 {@link dev.dubhe.anvilcraft.network.QuenchedOutMusicPacket} 驱动: + * 演化进入最后 71 秒时开始播放,锻星砧/增幅器被破坏或加速器被清除时立即停止。 + *

+ */ +@EventBusSubscriber(modid = AnvilCraft.MOD_ID, value = Dist.CLIENT) +public final class QuenchedOutMusicHandler { + private static final Map ACTIVE = new HashMap<>(); + + private QuenchedOutMusicHandler() { + } + + /// 在客户端主线程开始播放(若同位置已有实例则先停止)。 + public static void start(BlockPos pos) { + stop(pos); + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.isPaused()) return; + QuenchedOutSound sound = new QuenchedOutSound(pos); + ACTIVE.put(pos, sound); + minecraft.getSoundManager().play(sound); + } + + /// 立即停止指定位置的播放。 + public static void stop(BlockPos pos) { + QuenchedOutSound sound = ACTIVE.remove(pos); + if (sound != null) { + sound.finish(); + } + } + + @SubscribeEvent + public static void onClientTick(ClientTickEvent.Post event) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.level == null || minecraft.isPaused()) return; + /// 自然播完的实例会被游戏引擎标记为已停止,这里清理残留引用。 + ACTIVE.entrySet().removeIf(entry -> entry.getValue().isStopped()); + } + + @SubscribeEvent + public static void onLogout(ClientPlayerNetworkEvent.LoggingOut event) { + ACTIVE.values().forEach(QuenchedOutSound::finish); + ACTIVE.clear(); + } + + /** 固定在锻星砧位置的单次播放实例,不跟随玩家。 */ + private static final class QuenchedOutSound extends AbstractTickableSoundInstance { + private QuenchedOutSound(BlockPos pos) { + super(ModSoundEvents.QUENCHED_OUT.get(), SoundSource.MUSIC, SoundInstance.createUnseededRandom()); + this.looping = false; + this.delay = 0; + this.volume = 1.0F; + this.pitch = 1.0F; + this.x = pos.getX() + 0.5; + this.y = pos.getY() + 0.5; + this.z = pos.getZ() + 0.5; + this.attenuation = SoundInstance.Attenuation.LINEAR; + } + + @Override + public void tick() { + } + + private void finish() { + this.stop(); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/dubhe/anvilcraft/client/event/RegisterAdditionalEventListener.java b/src/main/java/dev/dubhe/anvilcraft/client/event/RegisterAdditionalEventListener.java index b0d01c008c..c3ab6753e5 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/event/RegisterAdditionalEventListener.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/event/RegisterAdditionalEventListener.java @@ -2,6 +2,7 @@ import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.client.renderer.blockentity.CelestialForgingAnvilBlockEntityRenderer; +import dev.dubhe.anvilcraft.init.registry.ModRegistries; import net.minecraft.client.Minecraft; import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.resources.ResourceLocation; @@ -10,6 +11,9 @@ import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.neoforge.client.event.ModelEvent; +import java.util.HashSet; +import java.util.Set; + @SuppressWarnings("Linelength") @EventBusSubscriber(modid = AnvilCraft.MOD_ID, value = Dist.CLIENT) public class RegisterAdditionalEventListener { @@ -41,34 +45,42 @@ public static void registerModels(ModelEvent.RegisterAdditional event) { event.register(CelestialForgingAnvilBlockEntityRenderer.R5); event.register(CelestialForgingAnvilBlockEntityRenderer.R6); - // Celestial Restriction Ring megastructure models - Ring 1 + Set registeredMegastructureModels = new HashSet<>(); + for (var megastructure : ModRegistries.MEGASTRUCTURE) { + for (ResourceLocation modelLocation : megastructure.modelLocations().values()) { + if (registeredMegastructureModels.add(modelLocation)) { + event.register(ModelResourceLocation.standalone(modelLocation)); + } + } + } + + // Keep built-ins available if this event fires before the custom registry is populated. event.register(standaloneBlock("celestial_forging_anvil_ring_1_eco_station")); event.register(standaloneBlock("celestial_forging_anvil_ring_1_excavator")); - event.register(standaloneBlock("celestial_forging_anvil_ring_1_excavator_off")); event.register(standaloneBlock("celestial_forging_anvil_ring_1_exctractor")); event.register(standaloneBlock("celestial_forging_anvil_ring_1_temple")); - // Ring 2 event.register(standaloneBlock("celestial_forging_anvil_ring_2_exctractor")); - // Ring 4 + event.register(standaloneBlock("celestial_forging_anvil_ring_2_dyson_sphere")); event.register(standaloneBlock("celestial_forging_anvil_ring_4_coil")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_coil_fix")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_coil_ring")); event.register(standaloneBlock("celestial_forging_anvil_ring_4_collider")); event.register(standaloneBlock("celestial_forging_anvil_ring_4_dyson_sphere")); event.register(standaloneBlock("celestial_forging_anvil_ring_4_matter_decompressor")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_matter_decompressor_fix")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_matter_decompressor_ring")); event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere_fix")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere_laser")); - event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere_laser_off")); event.register(standaloneBlock("celestial_forging_anvil_ring_4_wormhole_stabilizer")); - // Ring 5 event.register(standaloneBlock("celestial_forging_anvil_ring_5_dyson_sphere")); event.register(standaloneBlock("celestial_forging_anvil_ring_5_stellar_evolution_accelerator")); - // Ring 6 event.register(standaloneBlock("celestial_forging_anvil_ring_6_stellar_evolution_accelerator")); + // Built-in state-dependent and split-layer models are not part of the primary definition. + event.register(standaloneBlock("celestial_forging_anvil_ring_1_excavator_off")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_coil_fix")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_coil_ring")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_matter_decompressor_fix")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_matter_decompressor_ring")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere_fix")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere_laser")); + event.register(standaloneBlock("celestial_forging_anvil_ring_4_penrose_sphere_laser_off")); + event.register(standaloneBlock("fire_cauldron_fire4")); event.register(standaloneBlock("smart_block_placer_base")); event.register(standaloneBlock("smart_block_placer_upperarm")); diff --git a/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/CelestialForgingAnvilScreen.java b/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/CelestialForgingAnvilScreen.java index e05f82a474..e2ecb67266 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/CelestialForgingAnvilScreen.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/CelestialForgingAnvilScreen.java @@ -362,7 +362,7 @@ private void renderRefactorSection(GuiGraphics guiGraphics, int guiLeft, int gui getMenu().getBlockEntity().isAmplify(), getMenu().getBlockEntity().getPlanetaryResourceSet() ).stream() - .filter(opt -> "stellar_evolution_accelerator".equals(opt.megastructure())) + .filter(CelestialRefactorOption::auxiliary) .toList(); } else if (!hasMegastructure && isActive) { refactorOptions = CelestialRefactorRegistry.getOptions( @@ -386,7 +386,7 @@ private void renderRefactorSection(GuiGraphics guiGraphics, int guiLeft, int gui /// 计算已建造巨构的换行使用说明文本 List wrappedUsageLines = List.of(); if (hasMegastructure && activeOption != null) { - String usageKey = "screen.anvilcraft.cfa.megastructure." + activeOption.megastructure() + ".usage"; + String usageKey = activeOption.displayName() + ".usage"; Component usageText = Component.translatable(usageKey); wrappedUsageLines = font.split(usageText, BMT_WRAP_W); } @@ -553,7 +553,10 @@ private void renderMegastructureModel(GuiGraphics guiGraphics, CelestialRefactor float scale = baseScale / divisor; poseStack.scale(scale, -scale, scale); poseStack.mulPose(com.mojang.math.Axis.XP.rotationDegrees(30)); - poseStack.mulPose(com.mojang.math.Axis.YP.rotationDegrees((previewRotTick * 2) % 360)); + float baseRotation = (previewRotTick * 2) % 360; + poseStack.mulPose(com.mojang.math.Axis.YP.rotationDegrees( + option.rotation(baseRotation, previewRotTick) + )); var bufferSource = guiGraphics.bufferSource(); var consumer = bufferSource.getBuffer(RenderType.cutout()); @@ -587,7 +590,9 @@ protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { private void renderPreviewArea(GuiGraphics guiGraphics) { /// 检查恒星天体是否缺少增幅器 CelestialBodyData body = getMenu().getBlockEntity().getCelestialBodyData(); - boolean missingAmplifier = body instanceof StarData && !getMenu().getBlockEntity().isAmplifierPresent(); + boolean missingAmplifier = body instanceof StarData star + && !star.specialRedDwarf() + && !getMenu().getBlockEntity().isAmplifierPresent(); if (missingAmplifier) { Component line1 = Component.translatable("screen.anvilcraft.cfa.missing_amplifier.line1"); Component line2 = Component.translatable("screen.anvilcraft.cfa.missing_amplifier.line2"); @@ -2051,7 +2056,8 @@ private void handleRefactorStart() { /// 材料槽是CFA的 materialContainer,在菜单中映射 ItemStack inSlot = be.getMaterialContainer().getItem(0); ItemStack required = option.material().copyWithCount(option.materialCount()); - if (!ItemStack.isSameItem(inSlot, required) || inSlot.getCount() < required.getCount()) { + if (!ItemStack.isSameItem(inSlot, required) + || inSlot.getCount() < required.getCount()) { showRefactorError(Component.translatable("screen.anvilcraft.cfa.insufficient_materials")); return; } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/SliderScreen.java b/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/SliderScreen.java index d5f7b8065b..296b270fd1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/SliderScreen.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/gui/screen/SliderScreen.java @@ -1,5 +1,6 @@ package dev.dubhe.anvilcraft.client.gui.screen; +import dev.dubhe.anvilcraft.block.entity.CreativeGeneratorBlockEntity; import dev.dubhe.anvilcraft.client.gui.component.Slider; import dev.dubhe.anvilcraft.client.gui.component.TexturedButton; import dev.dubhe.anvilcraft.constant.Constant; @@ -16,6 +17,8 @@ import net.neoforged.neoforge.network.PacketDistributor; public class SliderScreen extends AbstractContainerScreen { + private static final int POWER_LIMIT = CreativeGeneratorBlockEntity.MAX_POWER; + private static final int SLIDER_BOUND = Integer.numberOfTrailingZeros(POWER_LIMIT) + 1; public static final ResourceLocation BACKGROUND = SharedTextures.bg("misc", "slider_like"); public static final ResourceLocation BUTTON_MAX = SharedTextures.textureGui("misc/slider_like/button_max"); public static final ResourceLocation BUTTON_ADD = SharedTextures.textureGui("misc/slider_like/button_add"); @@ -38,7 +41,7 @@ protected void init() { int offsetX = (this.width - this.imageWidth) / 2; int offsetY = (this.height - this.imageHeight) / 2; this.value = new EditBox(this.font, offsetX + 50, offsetY + 47, 76, 8, Component.literal("value")); - this.slider = new Slider(8 + offsetX, 31 + offsetY, -14, 14, 160 - 16, this::update); + this.slider = new Slider(8 + offsetX, 31 + offsetY, -SLIDER_BOUND, SLIDER_BOUND, 160 - 16, this::update); this.value.setCanLoseFocus(true); this.value.setTextColor(-1); this.value.setTextColorUneditable(-1); @@ -55,7 +58,7 @@ protected void init() { 16, 16, 32, - (btn) -> this.slider.setValueWithUpdate(8192)); + (btn) -> this.slider.setValueWithUpdate(POWER_LIMIT)); TexturedButton add = new TexturedButton( 134 + offsetX, 43 + offsetY, @@ -67,8 +70,8 @@ protected void init() { 32, (btn) -> this.value.setValue("" + Math.clamp( Integer.parseInt(this.value.getValue()) + slider.getAddValue(Integer.parseInt(this.value.getValue())), - -8192, - 8192 + -POWER_LIMIT, + POWER_LIMIT ))); TexturedButton min = new TexturedButton( 8 + offsetX, @@ -79,7 +82,7 @@ protected void init() { 16, 16, 32, - (btn) -> this.slider.setValueWithUpdate(-8192)); + (btn) -> this.slider.setValueWithUpdate(-POWER_LIMIT)); TexturedButton minus = new TexturedButton( 26 + offsetX, 43 + offsetY, @@ -91,8 +94,8 @@ protected void init() { 32, (btn) -> this.value.setValue("" + Math.clamp( Integer.parseInt(this.value.getValue()) - slider.getAddValue(Integer.parseInt(this.value.getValue())), - -8192, - 8192 + -POWER_LIMIT, + POWER_LIMIT ))); this.addRenderableWidget(max); this.addRenderableWidget(add); @@ -124,8 +127,9 @@ private void onValueInput(String value) { this.value.setValue("" + this.slider.getValue()); return; } + v = Math.clamp(v, -POWER_LIMIT, POWER_LIMIT); this.slider.setValue(v); - PacketDistributor.sendToServer(new SliderUpdatePacket(Math.clamp(v, -8192, 8192))); + PacketDistributor.sendToServer(new SliderUpdatePacket(v)); } public void setMin(int min) { diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java index 65605d0353..21b0c4ffc4 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/blockentity/CelestialForgingAnvilBlockEntityRenderer.java @@ -8,6 +8,7 @@ import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilBlockEntity; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyClass; import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialRefactorOption; import dev.dubhe.anvilcraft.block.entity.celestial.GiantPlanetData; import dev.dubhe.anvilcraft.block.entity.celestial.RingType; import dev.dubhe.anvilcraft.block.entity.celestial.RockyPlanetData; @@ -17,6 +18,7 @@ import dev.dubhe.anvilcraft.client.init.ModRenderTypes; import dev.dubhe.anvilcraft.client.renderer.blockentity.celestial.CelestialBodyRenderer; import dev.dubhe.anvilcraft.client.renderer.blockentity.celestial.CelestialBodyTextureBakery; +import dev.dubhe.anvilcraft.init.ModMegastructures; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.LightTexture; import net.minecraft.client.renderer.MultiBufferSource; @@ -76,6 +78,8 @@ public class CelestialForgingAnvilBlockEntityRenderer implements BlockEntityRend "block/celestial_forging_anvil_ring_4_collider")); /// 戴森球巨构建材模型(替换第四环/第五环) + public static final ModelResourceLocation R2_DYSON_SPHERE = ModelResourceLocation.standalone(AnvilCraft.of( + "block/celestial_forging_anvil_ring_2_dyson_sphere")); public static final ModelResourceLocation R4_DYSON_SPHERE = ModelResourceLocation.standalone(AnvilCraft.of( "block/celestial_forging_anvil_ring_4_dyson_sphere")); public static final ModelResourceLocation R5_DYSON_SPHERE = ModelResourceLocation.standalone(AnvilCraft.of( @@ -125,6 +129,7 @@ public class CelestialForgingAnvilBlockEntityRenderer implements BlockEntityRend "anvilcraft", "block/celestial_body/black_hole" )); + private static final float PLAYER_HEAD_SCALE = 16.0f; private final BlockRenderDispatcher blockRenderer; private final BlockState whiteConcrete = Blocks.WHITE_CONCRETE.defaultBlockState(); @@ -159,6 +164,14 @@ public void render( float rot = blockEntity.getRotation() + (blockEntity.getRotation() - blockEntity.getPreRotation()) * partialTick; CelestialBodyData bodyData = blockEntity.getCelestialBodyData(); boolean isAmplify = blockEntity.isAmplify(); + float rotationBoost = blockEntity.getAnimationRotationBoost(partialTick); + float bodyRotation = (blockEntity.getBodyRotation() + partialTick) * rotationBoost; + int outerRing = isAmplify ? 6 : 3; + int middleRing = isAmplify ? 5 : 2; + int innerRing = isAmplify ? 4 : 1; + float outerRotation = getMegastructureRotation(blockEntity, outerRing, rot, bodyRotation); + float middleRotation = getMegastructureRotation(blockEntity, middleRing, rot, bodyRotation); + float innerRotation = getMegastructureRotation(blockEntity, innerRing, rot, bodyRotation); /// 红石信号控制缩放倍率:0 级时使用固定原始值(天体=最小,环=6,高度=4.5/6.5), /// 15 级时达到完整动态缩放。中间级别线性插值。 @@ -207,7 +220,7 @@ public void render( poseStack.scale(ringScale, ringScale, ringScale); /// outout + out:根骨骼 + 静态倾斜(外骨骼) - poseStack.mulPose(Axis.YP.rotationDegrees(-rot)); /// outout:Y 轴旋转 + poseStack.mulPose(Axis.YP.rotationDegrees(-outerRotation)); /// outout:Y 轴旋转 poseStack.mulPose(Axis.XP.rotationDegrees(14.5108f)); /// out:静态倾斜 X poseStack.mulPose(Axis.YP.rotationDegrees(-3.8411f)); /// out:静态倾斜 Y poseStack.mulPose(Axis.ZP.rotationDegrees(14.5109f)); /// out:静态倾斜 Z @@ -216,7 +229,8 @@ public void render( if (blockEntity.isAmplify()) { boolean isDysonSphereR5 = isDysonSphereActive(blockEntity, 5); boolean anyDysonSphere = isDysonSphereActive(blockEntity, 4) || isDysonSphereR5; - boolean hideOutermostForPenrose = isPenroseSphereActive(blockEntity) && !blockEntity.isAcceleratorActive(); + boolean hideOutermostForPenrose = isPenroseSphereActive(blockEntity) + && !blockEntity.hasActiveAuxiliaryMegastructure(); if (!anyDysonSphere && !hideOutermostForPenrose) { ModelResourceLocation r6Model = getRing6Model(blockEntity); renderRingMaybe( @@ -233,9 +247,9 @@ public void render( modelRenderer ); } - } else { + } else if (!isBrownDwarfDysonSphereActive(blockEntity)) { renderRingMaybe( - R3, + getRing3Model(blockEntity), 3, bodyData, prevBody, @@ -250,14 +264,16 @@ public void render( } /// mid:X 旋转(中骨骼) - poseStack.mulPose(Axis.XP.rotationDegrees(90.0f + rot)); + poseStack.mulPose(Axis.XP.rotationDegrees(90.0f + middleRotation)); /// === 中间圆环 —— "mid" 的子骨骼 === if (blockEntity.isAmplify()) { boolean anyDysonSphere = isDysonSphereActive(blockEntity, 4) || isDysonSphereActive(blockEntity, 5); /// 当彭罗斯球激活且第五环为最外层(小恒星)时隐藏第五环 boolean isSmallStar = bodyData != null && bodyData.size() < 48; - boolean hideMiddleForPenrose = isPenroseSphereActive(blockEntity) && isSmallStar && !blockEntity.isAcceleratorActive(); + boolean hideMiddleForPenrose = isPenroseSphereActive(blockEntity) + && isSmallStar + && !blockEntity.hasActiveAuxiliaryMegastructure(); if (!anyDysonSphere && !hideMiddleForPenrose) { ModelResourceLocation r5Model = getRing5Model(blockEntity); renderRingMaybe( @@ -274,7 +290,7 @@ public void render( modelRenderer ); } - } else { + } else if (!isBrownDwarfDysonSphereActive(blockEntity)) { ModelResourceLocation r2Model = getRing2Model(blockEntity); renderRingMaybe( r2Model, @@ -292,7 +308,7 @@ public void render( } /// in:Z 旋转(内骨骼) - poseStack.mulPose(Axis.ZP.rotationDegrees(rot)); + poseStack.mulPose(Axis.ZP.rotationDegrees(innerRotation)); /// === 最内层圆环 —— "in" 的子骨骼 === if (blockEntity.isAmplify()) { @@ -339,8 +355,7 @@ public void render( boolean isDysonSphereR4 = isDysonSphereActive(blockEntity, 4); boolean isDysonSphereR5 = isDysonSphereActive(blockEntity, 5); if ((isDysonSphereR4 || isDysonSphereR5) && bodyData instanceof StarData star) { - float rotationBoost = blockEntity.getAnimationRotationBoost(partialTick); - float bodyRot = (blockEntity.getBodyRotation() + partialTick) * rotationBoost; + float bodyRot = bodyRotation; renderDysonSphereRings( isDysonSphereR4, isDysonSphereR5, @@ -380,10 +395,28 @@ public void render( } } + /// 褐矮星戴森球和外环与褐矮星同步旋转,替换非增幅模式的 R2/R3 机械圆环。 + if (!blockEntity.isAmplify() + && isBrownDwarfDysonSphereActive(blockEntity) + && bodyData != null) { + renderBrownDwarfDysonSphereRings( + centerY, + bodyRotation, + bodyData, + animProgress, + redstoneFactor, + ringScale, + poseStack, + multiBufferSource, + packedOverlay, + modelRenderer, + blockEntity + ); + } + /// 渲染彭罗斯球圆环:fix(恒星同步)+ laser/off(机械旋转) if (blockEntity.isAmplify() && isPenroseSphereActive(blockEntity) && bodyData instanceof StarData star) { - float rotationBoost = blockEntity.getAnimationRotationBoost(partialTick); - float bodyRot = (blockEntity.getBodyRotation() + partialTick) * rotationBoost; + float bodyRot = bodyRotation; renderPenroseSphereRings(centerY, rot, bodyRot, star, animProgress, blockEntity.isPenroseSphereLaserActive(), redstoneFactor, poseStack, multiBufferSource, packedOverlay, modelRenderer); @@ -391,11 +424,10 @@ public void render( /// 渲染磁星线圈圆环:fix(恒星同步)+ ring(机械旋转) if (blockEntity.isAmplify() && isMagnetarCoilActive(blockEntity) && bodyData instanceof StarData star) { - float rotationBoost = blockEntity.getAnimationRotationBoost(partialTick); - float bodyRot = (blockEntity.getBodyRotation() + partialTick) * rotationBoost; + float bodyRot = bodyRotation; renderMagnetarCoilRings( centerY, - rot, + innerRotation, bodyRot, star, animProgress, @@ -409,11 +441,10 @@ public void render( /// 渲染物质解压器圆环:fix(恒星同步,类似戴森球)+ ring(机械旋转) if (blockEntity.isAmplify() && isMatterDecompressorActive(blockEntity) && bodyData instanceof StarData star) { - float rotationBoost = blockEntity.getAnimationRotationBoost(partialTick); - float bodyRot = (blockEntity.getBodyRotation() + partialTick) * rotationBoost; + float bodyRot = bodyRotation; renderMatterDecompressorRings( centerY, - rot, + innerRotation, bodyRot, star, animProgress, @@ -428,16 +459,16 @@ public void render( /// 使用有效天体数据(考虑 celestialBodyData 已置为 null 的逆向动画情形) CelestialBodyData effectiveBodyData = blockEntity.getEffectiveBodyDataForRendering(); boolean canRender = effectiveBodyData != null - && (!(effectiveBodyData instanceof StarData) || blockEntity.isAmplifierPresent()); + && (!(effectiveBodyData instanceof StarData star) + || blockEntity.isAmplifierPresent() + || star.specialRedDwarf()); if (canRender) { - float rotationBoost = blockEntity.getAnimationRotationBoost(partialTick); - float bodyRot = (blockEntity.getBodyRotation() + partialTick) * rotationBoost; renderTractorBeam(beamHeight, animProgress, poseStack, multiBufferSource); /// 天体缩放完全由 StarData.size() 驱动,坍缩期间通过 applyCollapseColor() 缩小 —— 无需额外的 pose 缩放。 renderCelestialBody( effectiveBodyData, centerY, - bodyRot, + bodyRotation, poseStack, multiBufferSource, packedOverlay, @@ -448,7 +479,7 @@ public void render( renderCelestialRing( effectiveBodyData, centerY, - bodyRot, + bodyRotation, poseStack, multiBufferSource, packedOverlay, @@ -465,118 +496,132 @@ public void render( } - /// 获取第一环对应的模型,考虑抽取器巨构替换。 + /// 获取第一环对应的模型,考虑巨构替换。 private ModelResourceLocation getRing1Model(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() >= 0) { - var option = blockEntity.getActiveMegastructureOption(); - if (option != null) { - if ("planet_excavator".equals(option.megastructure())) { - return blockEntity.isExcavatorLaserActive() ? R1_EXCAVATOR : R1_EXCAVATOR_OFF; - } - if ("planet_exctractor".equals(option.megastructure())) { - return R1_EXCTRACTOR; - } - if ("eco_station".equals(option.megastructure())) { - return R1_ECO_STATION; - } - if ("temple".equals(option.megastructure())) { - return R1_TEMPLE; - } - } - } - return R1; + ModelResourceLocation active = getActiveRingModel(blockEntity, 1); + if (active != null) return active; + ModelResourceLocation auxiliary = getActiveAuxiliaryRingModel(blockEntity, 1); + return auxiliary == null ? R1 : auxiliary; } - /// 获取第二环对应的模型,考虑巨行星提取器巨构替换。 + /// 获取第二环对应的模型,考虑巨构替换。 private ModelResourceLocation getRing2Model(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() >= 0) { - var option = blockEntity.getActiveMegastructureOption(); - if (option != null) { - if ("giant_planet_exctractor".equals(option.megastructure())) { - return R2_EXCTRACTOR; - } - } - } - return R2; + ModelResourceLocation active = getActiveRingModel(blockEntity, 2); + if (active != null) return active; + ModelResourceLocation auxiliary = getActiveAuxiliaryRingModel(blockEntity, 2); + return auxiliary == null ? R2 : auxiliary; + } + + private ModelResourceLocation getRing3Model(CelestialForgingAnvilBlockEntity blockEntity) { + ModelResourceLocation active = getActiveRingModel(blockEntity, 3); + if (active != null) return active; + ModelResourceLocation auxiliary = getActiveAuxiliaryRingModel(blockEntity, 3); + return auxiliary == null ? R3 : auxiliary; } /// 获取第四环对应的模型,考虑替换第四环的巨构。 private ModelResourceLocation getRing4Model(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() >= 0) { - var option = blockEntity.getActiveMegastructureOption(); - if (option != null) { - if ("stellar_ring_collider".equals(option.megastructure())) { - return R4_COLLIDER; - } - if ("dyson_sphere_small".equals(option.megastructure())) { - return R4_DYSON_SPHERE; - } - if ("wormhole_stabilizer".equals(option.megastructure())) { - return R4_WORMHOLE_STABILIZER; - } - } - } - return R4; + ModelResourceLocation active = getActiveRingModel(blockEntity, 4); + if (active != null) return active; + ModelResourceLocation auxiliary = getActiveAuxiliaryRingModel(blockEntity, 4); + return auxiliary == null ? R4 : auxiliary; } /// 获取第五环对应的模型,考虑替换第五环的巨构。 private ModelResourceLocation getRing5Model(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() >= 0) { - var option = blockEntity.getActiveMegastructureOption(); - if (option != null) { - if ("dyson_sphere_large".equals(option.megastructure())) { - return R5_DYSON_SPHERE; - } - } - } - /// 恒星演化加速器用于小恒星时替换第五环 - if (blockEntity.isAcceleratorActive() && blockEntity.getCelestialBodyData() instanceof StarData star && star.size() < 48) { - return R5_STELLAR_EVOLUTION_ACCELERATOR; - } - return R5; + ModelResourceLocation active = getActiveRingModel(blockEntity, 5); + if (active != null) return active; + ModelResourceLocation auxiliary = getActiveAuxiliaryRingModel(blockEntity, 5); + return auxiliary == null ? R5 : auxiliary; } /// 获取第六环对应的模型,考虑替换第六环的巨构。 private ModelResourceLocation getRing6Model(CelestialForgingAnvilBlockEntity blockEntity) { - /// 恒星演化加速器用于大恒星时替换第六环 - if (blockEntity.isAcceleratorActive() && blockEntity.getCelestialBodyData() instanceof StarData star && star.size() >= 48) { - return R6_STELLAR_EVOLUTION_ACCELERATOR; + ModelResourceLocation active = getActiveRingModel(blockEntity, 6); + if (active != null) return active; + ModelResourceLocation auxiliary = getActiveAuxiliaryRingModel(blockEntity, 6); + return auxiliary == null ? R6 : auxiliary; + } + + private @Nullable ModelResourceLocation getActiveRingModel( + CelestialForgingAnvilBlockEntity blockEntity, + int ring + ) { + CelestialRefactorOption option = blockEntity.getActiveMegastructureOption(); + if (option == null || option.ring() != ring) return null; + if (ModMegastructures.PLANET_EXCAVATOR.getId().equals(option.id()) + && !blockEntity.isExcavatorLaserActive()) { + return R1_EXCAVATOR_OFF; } - return R6; + return ModelResourceLocation.standalone(option.modelLocation()); + } + + /** + * Resolves the rotation for a mechanical ring, allowing both the active + * primary structure and an active auxiliary to provide custom + * rotation functions. A ring without a structure keeps the base bone + * animation used by the vanilla CFA model. + */ + private float getMegastructureRotation( + CelestialForgingAnvilBlockEntity blockEntity, + int ring, + float baseRotation, + float bodyRotation + ) { + CelestialRefactorOption active = blockEntity.getActiveMegastructureOption(); + if (active != null && active.ring() == ring) { + return active.rotation(baseRotation, bodyRotation); + } + CelestialRefactorOption auxiliary = blockEntity.getActiveAuxiliaryMegastructureOption(ring); + return auxiliary != null + ? auxiliary.rotation(baseRotation, bodyRotation) + : baseRotation; + } + + private @Nullable ModelResourceLocation getActiveAuxiliaryRingModel( + CelestialForgingAnvilBlockEntity blockEntity, + int ring + ) { + CelestialRefactorOption option = blockEntity.getActiveAuxiliaryMegastructureOption(ring); + if (option != null) return ModelResourceLocation.standalone(option.modelLocation()); + if (!blockEntity.isAcceleratorActive()) return null; + if (blockEntity.getCelestialBodyData() instanceof StarData star) { + if (ring == 5 && star.size() < 48) return R5_STELLAR_EVOLUTION_ACCELERATOR; + if (ring == 6 && star.size() >= 48) return R6_STELLAR_EVOLUTION_ACCELERATOR; + } + return null; } /// 检查指定环索引上的戴森球巨构是否激活。 private static boolean isDysonSphereActive(CelestialForgingAnvilBlockEntity blockEntity, int ring) { - if (blockEntity.getActiveMegastructureIndex() < 0) return false; - var option = blockEntity.getActiveMegastructureOption(); + CelestialRefactorOption option = blockEntity.getActiveMegastructureOption(); if (option == null) return false; - if (ring == 4) return "dyson_sphere_small".equals(option.megastructure()); - if (ring == 5) return "dyson_sphere_large".equals(option.megastructure()); + if (ring == 4) return ModMegastructures.DYSON_SPHERE_SMALL.getId().equals(option.id()); + if (ring == 5) return ModMegastructures.DYSON_SPHERE_LARGE.getId().equals(option.id()); return false; } + private static boolean isBrownDwarfDysonSphereActive(CelestialForgingAnvilBlockEntity blockEntity) { + CelestialRefactorOption option = blockEntity.getActiveMegastructureOption(); + return option != null && ModMegastructures.DYSON_SPHERE_BROWN_DWARF.getId().equals(option.id()); + } + /// 检查磁星线圈巨构是否激活。 private static boolean isMagnetarCoilActive(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() < 0) return false; - var option = blockEntity.getActiveMegastructureOption(); - if (option == null) return false; - return "magnetar_coil".equals(option.megastructure()); + CelestialRefactorOption option = blockEntity.getActiveMegastructureOption(); + return option != null && ModMegastructures.MAGNETAR_COIL.getId().equals(option.id()); } /// 检查彭罗斯球巨构是否激活。 private static boolean isPenroseSphereActive(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() < 0) return false; - var option = blockEntity.getActiveMegastructureOption(); - if (option == null) return false; - return "penrose_sphere".equals(option.megastructure()); + CelestialRefactorOption option = blockEntity.getActiveMegastructureOption(); + return option != null && ModMegastructures.PENROSE_SPHERE.getId().equals(option.id()); } /// 检查物质解压器巨构是否激活。 private static boolean isMatterDecompressorActive(CelestialForgingAnvilBlockEntity blockEntity) { - if (blockEntity.getActiveMegastructureIndex() < 0) return false; - var option = blockEntity.getActiveMegastructureOption(); - if (option == null) return false; - return "matter_decompressor".equals(option.megastructure()); + CelestialRefactorOption option = blockEntity.getActiveMegastructureOption(); + return option != null && ModMegastructures.MATTER_DECOMPRESSOR.getId().equals(option.id()); } /// 以恒星同步旋转(Y 旋转匹配恒星)渲染戴森球圆环, @@ -616,6 +661,41 @@ private void renderDysonSphereRings( poseStack.popPose(); } + private void renderBrownDwarfDysonSphereRings( + float centerY, + float bodyRot, + CelestialBodyData body, + float scale, + float redstoneFactor, + float ringScale, + PoseStack poseStack, + MultiBufferSource bufferSource, + int packedOverlay, + ModelBlockRenderer modelRenderer, + CelestialForgingAnvilBlockEntity blockEntity + ) { + if (scale < 0.001f) return; + + float fullRScale = ringSystemScale(body, false); + float rscale = 6.0f + (fullRScale - 6.0f) * redstoneFactor; + poseStack.pushPose(); + poseStack.translate(0.5, centerY, 0.5); + poseStack.scale(rscale, rscale, rscale); + poseStack.mulPose(Axis.XP.rotationDegrees(body.axialTilt())); + poseStack.mulPose(Axis.YP.rotationDegrees(bodyRot * CelestialBodyData.getVisualRotationSpeed(body.rotationSpeed()))); + renderRingCutout(R2_DYSON_SPHERE, poseStack, bufferSource, packedOverlay, modelRenderer); + poseStack.popPose(); + + /// 与大戴森球相同,R3 作为同步旋转的外环。 + poseStack.pushPose(); + poseStack.translate(0.5, centerY, 0.5); + poseStack.scale(ringScale, ringScale, ringScale); + poseStack.mulPose(Axis.XP.rotationDegrees(body.axialTilt())); + poseStack.mulPose(Axis.YP.rotationDegrees(bodyRot * CelestialBodyData.getVisualRotationSpeed(body.rotationSpeed()))); + renderRingCutout(getRing3Model(blockEntity), poseStack, bufferSource, packedOverlay, modelRenderer); + poseStack.popPose(); + } + /// 在 R4 位置用两个模型渲染磁星线圈圆环: /// - ring_4_coil_ring —— 机械旋转(与原 R4 内环相同的骨骼层级) /// - ring_4_coil_fix —— 恒星同步旋转(类似戴森球,相对恒星静止) @@ -748,6 +828,7 @@ private void renderMatterDecompressorRings( /// 判断机械圆环对给定天体数据是否应可见。 private static boolean isRingVisible(int ring, @Nullable CelestialBodyData bodyData, boolean isAmplify) { + if (isPlayerHead(bodyData)) return false; if (isAmplify) { return switch (ring) { case 4 -> bodyData == null || bodyData.size() < 48; @@ -766,6 +847,10 @@ private static boolean isRingVisible(int ring, @Nullable CelestialBodyData bodyD } } + private static boolean isPlayerHead(@Nullable CelestialBodyData bodyData) { + return bodyData instanceof SpecialCelestialBodyData special && special.isPlayerHead(); + } + /// 渲染机械圆环,当前后天体切换可见性变化时带有淡入淡出过渡。 private void renderRingMaybe( ModelResourceLocation modelId, @@ -780,6 +865,7 @@ private void renderRingMaybe( int packedOverlay, ModelBlockRenderer modelRenderer ) { + if (isPlayerHead(currBody) || (currBody == null && isPlayerHead(prevBody))) return; /// 第 1-3 环:非增幅模式,第 4-6 环:增幅模式 boolean isAmplify = ringIndex >= 4; boolean visibleNow = isRingVisible(ringIndex, currBody, isAmplify); @@ -901,8 +987,12 @@ private void renderCelestialBody( poseStack.pushPose(); poseStack.translate(0.5, centerY, 0.5); float baseScale = bodyScale; - if (bodyData instanceof SpecialCelestialBodyData s && s.isErrorPlanet()) { - baseScale *= 0.25f; + if (bodyData instanceof SpecialCelestialBodyData s) { + if (s.isPlayerHead()) { + baseScale *= PLAYER_HEAD_SCALE; + } else if (s.isErrorPlanet()) { + baseScale *= 0.25f; + } } float scale = baseScale * animProgress; if (scale < 0.001f) { @@ -1544,12 +1634,24 @@ public AABB getRenderBoundingBox(CelestialForgingAnvilBlockEntity blockEntity) { CelestialBodyData body = blockEntity.getCelestialBodyData(); float centerY = dynamicCenterY(body, blockEntity.isAmplify()); float bs = body != null ? bodyScale(body) * CelestialBodyData.BODY_SCALE_FACTOR : 6.0f; - float maxHeight = Math.max(centerY + bs * 1.5f, blockEntity.isAmplify() ? 18.0f : 12.0f); - float horizInset = bs * 0.8f; + if (isPlayerHead(body)) { + bs *= PLAYER_HEAD_SCALE; + } + // The mechanical rings can extend beyond the body, especially for small planets. + // Size the box from the larger of the body and the complete ring system. + float ringScale = ringSystemScale(body, blockEntity.isAmplify()); + float maxVisualScale = Math.max(bs, ringScale); + float maxHeight = Math.max( + centerY + maxVisualScale * 1.5f, + blockEntity.isAmplify() ? 18.0f : 12.0f + ); + float horizontalInset = maxVisualScale * 1.5f; /// 超新星爆发:水平闪光约 16 格 + 向四面八方(含下方)射出约 12 格光束, /// 用以天体中心为心的对称大包围盒覆盖,避免被裁剪。 if (blockEntity.getSupernovaFlashTicks() > 0) { - float reach = Math.max(SUPERNOVA_MAX_RADIUS, SUPERNOVA_RAY_LENGTH) * 1.5f + 2; + float explosionScale = Math.max(1.0f, blockEntity.getSupernovaScale()); + float reach = Math.max(SUPERNOVA_MAX_RADIUS, SUPERNOVA_RAY_LENGTH) + * explosionScale * 1.5f + 2; double cy = blockEntity.getSupernovaCenterY(); return new AABB( blockEntity.getBlockPos().getX() + 0.5, @@ -1562,16 +1664,16 @@ public AABB getRenderBoundingBox(CelestialForgingAnvilBlockEntity blockEntity) { } if (!blockEntity.isAmplify()) { AABB aabb = new AABB(blockEntity.getBlockPos().offset(state.getValue(CelestialForgingAnvilBlock.HALF).getOffset())).inflate( - Math.max(horizInset, 1), + Math.max(horizontalInset, 1), 0, - Math.max(horizInset, 1) + Math.max(horizontalInset, 1) ); return aabb.setMaxY(aabb.maxY + maxHeight); } AABB aabb = new AABB(blockEntity.getBlockPos().offset(state.getValue(CelestialForgingAnvilBlock.HALF).getOffset())).inflate( - Math.max(horizInset, 3), + Math.max(horizontalInset, 3), 0, - Math.max(horizInset, 3) + Math.max(horizontalInset, 3) ); return aabb.setMaxY(aabb.maxY + maxHeight); } diff --git a/src/main/java/dev/dubhe/anvilcraft/client/renderer/laser/LaserState.java b/src/main/java/dev/dubhe/anvilcraft/client/renderer/laser/LaserState.java index dbc5eb6ba1..12c0ced7e2 100644 --- a/src/main/java/dev/dubhe/anvilcraft/client/renderer/laser/LaserState.java +++ b/src/main/java/dev/dubhe/anvilcraft/client/renderer/laser/LaserState.java @@ -4,8 +4,6 @@ import dev.dubhe.anvilcraft.AnvilCraft; import dev.dubhe.anvilcraft.block.LensBlock; import dev.dubhe.anvilcraft.block.entity.BaseLaserBlockEntity; -import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLaserInterfaceBlockEntity; -import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilPortalBlockEntity; import dev.dubhe.anvilcraft.block.entity.CreativeLaserBlockEntity; import dev.dubhe.anvilcraft.block.state.LensType; import net.minecraft.client.Minecraft; @@ -45,12 +43,7 @@ public record LaserState( .getCenter() .distanceTo(blockEntity.getBlockPos().getCenter()) - 0.5); poseStack.mulPose(blockEntity.getFacing().getRotation()); - boolean gamma = (blockEntity instanceof CelestialForgingAnvilLaserInterfaceBlockEntity cfaLaser - && cfaLaser.isEmittingGamma()) - || (blockEntity instanceof CelestialForgingAnvilPortalBlockEntity portal - && portal.isEmittingGamma()) - || (blockEntity instanceof CreativeLaserBlockEntity creativeLaser - && creativeLaser.isGamma()); + boolean gamma = blockEntity.isEmittingGamma(); LensType lensType = blockEntity.getBlockState().getBlock() instanceof LensBlock ? blockEntity.getBlockState().getValue(LensBlock.TYPE) : LensType.NONE; diff --git a/src/main/java/dev/dubhe/anvilcraft/data/lang/OtherLang.java b/src/main/java/dev/dubhe/anvilcraft/data/lang/OtherLang.java index 21a76ccc8c..3525383120 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/lang/OtherLang.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/lang/OtherLang.java @@ -74,5 +74,6 @@ public static void init(RegistrumLangProvider provider) { provider.add("subtitles.anvilcraft.anvil_hammer_rotate_block", "Anvil Hammer rotates block"); provider.add("subtitles.anvilcraft.auto_enchanting_table.use", "Auto Enchanting Table used"); + provider.add("subtitles.anvilcraft.quenched_out", "Quenched out"); // 淬灭(超新星前奏曲) } } diff --git a/src/main/java/dev/dubhe/anvilcraft/data/lang/ScreenLang.java b/src/main/java/dev/dubhe/anvilcraft/data/lang/ScreenLang.java index c09a613455..edbf8e1c48 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/lang/ScreenLang.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/lang/ScreenLang.java @@ -130,6 +130,7 @@ public static void init(RegistrumLangProvider provider) { provider.add("tooltip.anvilcraft.heat.tier.incandescent", "Incandescent"); provider.add("tooltip.anvilcraft.heat.tier.overheated", "Overheated"); provider.add("tooltip.anvilcraft.heat.duration", "Duration: %s"); + provider.add("tooltip.anvilcraft.waiting", "Waiting for syncing"); provider.add("tooltip.anvilcraft.propel_piston.state", "Propel Piston State: "); provider.add("tooltip.anvilcraft.propel_piston.remaining_energy", " Remaining Energy: %s"); @@ -478,12 +479,13 @@ public static void init(RegistrumLangProvider provider) { provider.add("screen.anvilcraft.cfa.collider_processing", "◇ Processing"); // ◇ 正在加工 provider.add("screen.anvilcraft.cfa.collider_star_missing", "! Star Missing !"); // ! 天体丢失 ! - // Megastructure names (English) — 11 unique megastructures + // Megastructure names (English) provider.add("screen.anvilcraft.cfa.megastructure.planet_excavator", "Planet Excavator"); // 行星开采器 provider.add("screen.anvilcraft.cfa.megastructure.planet_exctractor", "Planet Exctractor"); // 行星抽取器 provider.add("screen.anvilcraft.cfa.megastructure.eco_station", "Ecological Station"); // 生态站 provider.add("screen.anvilcraft.cfa.megastructure.temple", "Temple"); // 神庙 provider.add("screen.anvilcraft.cfa.megastructure.giant_planet_exctractor", "Giant Planet Exctractor"); // 巨行星抽取器 + provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf", "Brown Dwarf Dyson Sphere"); // 戴森球(褐矮星) provider.add("screen.anvilcraft.cfa.megastructure.stellar_ring_collider", "Stellar Ring Collider"); // 星环对撞机 provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_small", "Small Dyson Sphere"); // 小戴森球 provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_large", "Large Dyson Sphere"); // 大戴森球 @@ -499,6 +501,7 @@ public static void init(RegistrumLangProvider provider) { provider.add("screen.anvilcraft.cfa.megastructure.eco_station.description", "Cultivate creatures on the planet and harvest biological resources"); provider.add("screen.anvilcraft.cfa.megastructure.temple.description", "Bestow blessings or mete out punishments to inferior civilization, and reap the resources they offer in homage"); provider.add("screen.anvilcraft.cfa.megastructure.giant_planet_exctractor.description", "Extracting resources from the atmosphere of giant planet. In addition to gases and liquids, solid products may also be produced"); + provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.description", "Collecting a brown dwarf's energy to generate power; primordial matter can amplify the output and eventually turn it into a red dwarf"); provider.add("screen.anvilcraft.cfa.megastructure.stellar_ring_collider.description", "Utilizing the gravitational and magnetic fields of star to accelerate the anvil to higher speeds or to frequently impact and manufacture items in batches"); provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_small.description", "Collecting light energy from star, generate a large amount of electricity"); provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_large.description", "Collecting light energy from star, generate a large amount of electricity"); @@ -582,6 +585,19 @@ public static void init(RegistrumLangProvider provider) { Side Effects: §7None§r""" ); + provider.add("screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.usage", + """ + Requires: + §b[Flu Int]§r + §fPrimordial Matter: 250mB–2B per tick, stable for 40gt§r + ——————— + Outputs: + §a[Power Grid]§r + §fP=(E×R²)/1600 MW; stable supply provides ×1.5/×2/×3/×5§r + ——————— + Side Effects: + §fMatter above 2B accumulates; 12800B turns the brown dwarf into a red dwarf§r""" + ); provider.add("screen.anvilcraft.cfa.megastructure.stellar_ring_collider.usage", """ Requires: diff --git a/src/main/java/dev/dubhe/anvilcraft/data/provider/ModSoundDefinitionsProvider.java b/src/main/java/dev/dubhe/anvilcraft/data/provider/ModSoundDefinitionsProvider.java index a8b9c3b66e..c780f9aa91 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/provider/ModSoundDefinitionsProvider.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/provider/ModSoundDefinitionsProvider.java @@ -71,5 +71,9 @@ public void registerSounds() { add(ModSoundEvents.AUTO_ENCHANTING_TABLE_USE, definition() .subtitle("subtitles.anvilcraft.auto_enchanting_table.use") .with(sound(ResourceLocation.withDefaultNamespace("block.enchantment_table.use"), SoundDefinition.SoundType.EVENT))); + + add(ModSoundEvents.QUENCHED_OUT, definition() + .subtitle("subtitles.anvilcraft.quenched_out") + .with(sound(AnvilCraft.of("quenched_out")))); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/data/recipe/PlanetResourceRecipeLoader.java b/src/main/java/dev/dubhe/anvilcraft/data/recipe/PlanetResourceRecipeLoader.java index 914c551bf2..9723c4963a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/recipe/PlanetResourceRecipeLoader.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/recipe/PlanetResourceRecipeLoader.java @@ -10,14 +10,8 @@ import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.resources.ResourceLocation; -import java.util.List; -import java.util.Optional; - -/** - * Data generation loader for planet resource recipes. - */ +/** Data generation loader for planet resource recipes. */ public class PlanetResourceRecipeLoader { - public static void init(RegistrumRecipeProvider provider) { createMineralRecipe(provider); createFluidRecipes(provider); @@ -37,139 +31,141 @@ private static void saveRecipe(RecipeOutput output, String name, PlanetResourceR output.accept(id, recipe, advancement.build(id.withPrefix("recipes/"))); } - // === Mineral === - private static void createMineralRecipe(RegistrumRecipeProvider provider) { - saveRecipe(provider, "mineral", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.MINERAL, - Optional.of(new PlanetResourceRecipe.MineralData( - "c:raw_materials", "anvilcraft:non_planetary_minerals", 10 - )), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty() - )); + saveRecipe( + provider, + "mineral", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.MINERAL) + .mineral(new PlanetResourceRecipe.MineralData( + "c:raw_materials", + "anvilcraft:non_planetary_minerals", + 10 + )) + .build() + ); } - // === Fluids === - private static void createFluidRecipes(RegistrumRecipeProvider provider) { - saveRecipe(provider, "fluid_water", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.FLUID, - Optional.empty(), - Optional.of(new PlanetResourceRecipe.FluidData( - "rocky_planet", "", "low", "minecraft:water" - )), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty() - )); - - saveRecipe(provider, "fluid_lava", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.FLUID, - Optional.empty(), - Optional.of(new PlanetResourceRecipe.FluidData( - "rocky_planet", "scorched", "low", "minecraft:lava" - )), - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty() - )); + saveRecipe( + provider, + "fluid_water", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.FLUID) + .fluid(new PlanetResourceRecipe.FluidData("rocky_planet", "", "low", "minecraft:water")) + .build() + ); + saveRecipe( + provider, + "fluid_lava", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.FLUID) + .fluid(new PlanetResourceRecipe.FluidData("rocky_planet", "scorched", "low", "minecraft:lava")) + .build() + ); } - // === Giant planet items === - private static void createGiantItemRecipes(RegistrumRecipeProvider provider) { - saveRecipe(provider, "giant_item_ice", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.GIANT_ITEM, - Optional.empty(), Optional.empty(), - Optional.of(new PlanetResourceRecipe.GiantData( - List.of( - new PlanetResourceRecipe.WeightedEntry("minecraft:ice", 50), - new PlanetResourceRecipe.WeightedEntry("minecraft:packed_ice", 30), - new PlanetResourceRecipe.WeightedEntry("minecraft:blue_ice", 20) - ), "ice" - )), - Optional.empty(), Optional.empty(), Optional.empty() - )); + saveRecipe( + provider, + "giant_item_ice", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.GIANT_ITEM) + .giant(new PlanetResourceRecipe.GiantData( + PlanetResourceRecipe.entries(entries -> entries + .id("minecraft:ice", 50) + .id("minecraft:packed_ice", 30) + .id("minecraft:blue_ice", 20)), + "ice" + )) + .build() + ); } - // === Giant planet fluids === - private static void createGiantFluidRecipes(RegistrumRecipeProvider provider) { - saveRecipe(provider, "giant_fluid_gas", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.GIANT_FLUID, - Optional.empty(), Optional.empty(), - Optional.of(new PlanetResourceRecipe.GiantData( - List.of( - new PlanetResourceRecipe.WeightedEntry("anvilcraft:primordial_matter", 100) - ), "gas" - )), - Optional.empty(), Optional.empty(), Optional.empty() - )); - - saveRecipe(provider, "giant_fluid_ice", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.GIANT_FLUID, - Optional.empty(), Optional.empty(), - Optional.of(new PlanetResourceRecipe.GiantData( - List.of( - new PlanetResourceRecipe.WeightedEntry("anvilcraft:primordial_matter", 90), - new PlanetResourceRecipe.WeightedEntry("minecraft:water", 10) - ), "ice" - )), - Optional.empty(), Optional.empty(), Optional.empty() - )); + saveRecipe( + provider, + "giant_fluid_gas", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.GIANT_FLUID) + .giant(new PlanetResourceRecipe.GiantData( + PlanetResourceRecipe.entries(entries -> entries.id("anvilcraft:primordial_matter", 100)), + "gas" + )) + .build() + ); + saveRecipe( + provider, + "giant_fluid_ice", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.GIANT_FLUID) + .giant(new PlanetResourceRecipe.GiantData( + PlanetResourceRecipe.entries(entries -> entries + .id("anvilcraft:primordial_matter", 90) + .id("minecraft:water", 10)), + "ice" + )) + .build() + ); } - // === Biological === - private static void createBiologicalRecipe(RegistrumRecipeProvider provider) { - saveRecipe(provider, "biological", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.BIOLOGICAL, - Optional.empty(), Optional.empty(), Optional.empty(), - Optional.of(new PlanetResourceRecipe.BiologicalData( - PlanetResourceRecipe.LifeChances.DEFAULT, - "anvilcraft:planetary_land_animals", - "anvilcraft:planetary_aquatic_animals", - "anvilcraft:non_planetary_mob_drops", - List.of( - new PlanetResourceRecipe.WeightedEntry("minecraft:milk", 50), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:honey", 50) - ) - )), - Optional.empty(), Optional.empty() - )); + saveRecipe( + provider, + "biological", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.BIOLOGICAL) + .biological(new PlanetResourceRecipe.BiologicalData( + PlanetResourceRecipe.LifeChances.DEFAULT, + "anvilcraft:planetary_land_animals", + "anvilcraft:planetary_aquatic_animals", + "anvilcraft:non_planetary_mob_drops", + PlanetResourceRecipe.entries(entries -> entries + .id("minecraft:milk", 50) + .id("anvilcraft:honey", 50)) + )) + .build() + ); } - // === Offering === - private static void createOfferingRecipe(RegistrumRecipeProvider provider) { - saveRecipe(provider, "offering", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.OFFERING, - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), - Optional.of(new PlanetResourceRecipe.OfferingData( - List.of( - new PlanetResourceRecipe.WeightedEntry("anvilcraft:gem_block_random", 50), - new PlanetResourceRecipe.WeightedEntry("minecraft:experience_bottle", 40), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:royal_steel_ingot", 5), - new PlanetResourceRecipe.WeightedEntry("minecraft:totem_of_undying", 2), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:gem_amulet_random", 2), - new PlanetResourceRecipe.WeightedEntry("minecraft:heart_of_the_sea", 1) - ), 50, 32, 43 - )), - Optional.empty() - )); + saveRecipe( + provider, + "offering", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.OFFERING) + .offering(new PlanetResourceRecipe.OfferingData( + PlanetResourceRecipe.entries(entries -> entries + .chooseOne(50, choices -> choices + .id("minecraft:emerald_block", 1) + .id("anvilcraft:topaz_block", 1) + .id("anvilcraft:ruby_block", 1) + .id("anvilcraft:sapphire_block", 1)) + .id("minecraft:experience_bottle", 40) + .id("anvilcraft:royal_steel_ingot", 5) + .id("minecraft:totem_of_undying", 2) + .chooseOne(2, choices -> choices + .id("anvilcraft:emerald_amulet", 1) + .id("anvilcraft:topaz_amulet", 1) + .id("anvilcraft:ruby_amulet", 1) + .id("anvilcraft:sapphire_amulet", 1)) + .id("minecraft:heart_of_the_sea", 1)), + 50, + 32, + 43 + )) + .build() + ); } - // === Wasteland === - private static void createWastelandRecipe(RegistrumRecipeProvider provider) { - saveRecipe(provider, "wasteland", new PlanetResourceRecipe( - PlanetResourceRecipe.Category.WASTELAND, - Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), - Optional.of(new PlanetResourceRecipe.WastelandData( - List.of( - new PlanetResourceRecipe.WeightedEntry("anvilcraft:reinforced_concrete_gray", 60), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:circuit_board", 30), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:processor", 5), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:raw_uranium", 3), - new PlanetResourceRecipe.WeightedEntry("anvilcraft:plutonium_nugget", 2) - ), 35, 10 - )) - )); + saveRecipe( + provider, + "wasteland", + PlanetResourceRecipe.builder(PlanetResourceRecipe.Category.WASTELAND) + .wasteland(new PlanetResourceRecipe.WastelandData( + PlanetResourceRecipe.entries(entries -> entries + .id("anvilcraft:reinforced_concrete_gray", 60) + .id("anvilcraft:circuit_board", 30) + .id("anvilcraft:processor", 5) + .id("anvilcraft:raw_uranium", 3) + .id("anvilcraft:plutonium_nugget", 2)), + 35, + 10 + )) + .build() + ); } } diff --git a/src/main/java/dev/dubhe/anvilcraft/data/recipe/SpecialCelestialBodyRecipeLoader.java b/src/main/java/dev/dubhe/anvilcraft/data/recipe/SpecialCelestialBodyRecipeLoader.java index 35055c41d0..a188bb47e8 100644 --- a/src/main/java/dev/dubhe/anvilcraft/data/recipe/SpecialCelestialBodyRecipeLoader.java +++ b/src/main/java/dev/dubhe/anvilcraft/data/recipe/SpecialCelestialBodyRecipeLoader.java @@ -2,6 +2,7 @@ import dev.anvilcraft.lib.v2.registrum.providers.RegistrumRecipeProvider; import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelData; import dev.dubhe.anvilcraft.block.entity.celestial.LiquidCoverage; import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyRecipe; import net.minecraft.advancements.Advancement; @@ -92,7 +93,16 @@ private static void createOverworldLike(RegistrumRecipeProvider provider) { List.of(item("minecraft:milk", 50)), List.of(), List.of(), - List.of() + List.of(), + Optional.of(new CelestialTravelData( + anvil("overworld_like"), + new CelestialTravelData.CoordinateRule( + CelestialTravelData.CoordinateRule.Type.SAME, 1.0, 0, 64, 0, 8 + ), + new CelestialTravelData.ReturnRule( + CelestialTravelData.ReturnRule.Type.ENTRY_PORTAL, 0, 64, 0, 8 + ) + )) ) ); } @@ -126,7 +136,8 @@ private static void createFleshPlanet(RegistrumRecipeProvider provider) { List.of(), List.of(), List.of(), - List.of() + List.of(), + Optional.empty() ) ); } @@ -153,7 +164,8 @@ private static void createIntelligentPlanet(RegistrumRecipeProvider provider) { List.of(), List.of(), List.of(), - List.of() + List.of(), + Optional.empty() ) ); } @@ -180,7 +192,8 @@ private static void createHollowPlanet(RegistrumRecipeProvider provider) { List.of(), List.of(), List.of(), - List.of() + List.of(), + Optional.empty() ) ); } @@ -207,7 +220,8 @@ private static void createErrorPlanet(RegistrumRecipeProvider provider) { List.of(), List.of(), List.of(), - List.of() + List.of(), + Optional.empty() ) ); } diff --git a/src/main/java/dev/dubhe/anvilcraft/init/ModBiomeSources.java b/src/main/java/dev/dubhe/anvilcraft/init/ModBiomeSources.java new file mode 100644 index 0000000000..060aabacd1 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/init/ModBiomeSources.java @@ -0,0 +1,29 @@ +package dev.dubhe.anvilcraft.init; + +import com.mojang.serialization.MapCodec; +import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.worldgen.VanillaOverworldBiomeSource; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.biome.BiomeSource; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredRegister; + +import java.util.function.Supplier; + +/** Registers codec types used by AnvilCraft's built-in dimensions. */ +public final class ModBiomeSources { + private static final DeferredRegister> REGISTER = DeferredRegister.create( + Registries.BIOME_SOURCE, AnvilCraft.MOD_ID + ); + + public static final Supplier> VANILLA_OVERWORLD = REGISTER.register( + "vanilla_overworld", () -> VanillaOverworldBiomeSource.CODEC + ); + + private ModBiomeSources() { + } + + public static void register(IEventBus modEventBus) { + REGISTER.register(modEventBus); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/init/ModMegastructures.java b/src/main/java/dev/dubhe/anvilcraft/init/ModMegastructures.java new file mode 100644 index 0000000000..09d0e4f9c5 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/init/ModMegastructures.java @@ -0,0 +1,271 @@ +package dev.dubhe.anvilcraft.init; + +import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyClass; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.GiantPlanetData; +import dev.dubhe.anvilcraft.block.entity.celestial.LiquidCoverage; +import dev.dubhe.anvilcraft.block.entity.celestial.Megastructure; +import dev.dubhe.anvilcraft.block.entity.celestial.PlanetaryResourceSet; +import dev.dubhe.anvilcraft.block.entity.celestial.RockyPlanetData; +import dev.dubhe.anvilcraft.block.entity.celestial.SpecialCelestialBodyData; +import dev.dubhe.anvilcraft.block.entity.celestial.StarData; +import dev.dubhe.anvilcraft.block.entity.megastructure.AcceleratorHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.ColliderHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.DysonSphereHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.EcoStationHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.ExcavatorHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.ExtractorHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.GiantExtractorHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.MagnetarCoilHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.MatterDecompressorHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.PenroseSphereHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.TempleHandler; +import dev.dubhe.anvilcraft.block.entity.megastructure.WormholeStabilizerHandler; +import dev.dubhe.anvilcraft.init.block.ModBlocks; +import dev.dubhe.anvilcraft.init.item.ModItems; +import dev.dubhe.anvilcraft.init.registry.ModRegistryKeys; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +import java.util.function.Function; +import javax.annotation.Nullable; + +/** Built-in Celestial Forging Anvil megastructure definitions. */ +public final class ModMegastructures { + private static final DeferredRegister REGISTER = DeferredRegister.create( + ModRegistryKeys.MEGASTRUCTURE, + AnvilCraft.MOD_ID + ); + + public static final DeferredHolder PLANET_EXCAVATOR = register( + "planet_excavator", + id -> Megastructure.builder(id, "planet_excavator") + .prerequisite(context -> isPlanet(context) && !isErrorPlanet(context)) + .ring(1) + .model(1, ringModel(1, "excavator")) + .material(ModBlocks.RUBY_PRISM.asItem(), 16) + .handler(ExcavatorHandler::new) + .build() + ); + public static final DeferredHolder PLANET_EXTRACTOR = register( + "planet_exctractor", + id -> Megastructure.builder(id, "planet_exctractor") + .prerequisite(context -> isPlanet(context) && !isErrorPlanet(context) && hasLiquid(context)) + .ring(1) + .model(1, ringModel(1, "exctractor")) + .material(ModBlocks.PUMP.asItem(), 16) + .handler(ExtractorHandler::new) + .build() + ); + public static final DeferredHolder ECO_STATION = register( + "eco_station", + id -> Megastructure.builder(id, "eco_station") + .prerequisite(context -> isPlanet(context) + && !isErrorPlanet(context) + && isEcoStationEligible(context.resources())) + .ring(1) + .model(1, ringModel(1, "eco_station")) + .material(ModBlocks.TEMPERING_GLASS.asItem(), 64) + .handler(EcoStationHandler::new) + .build() + ); + public static final DeferredHolder TEMPLE = register( + "temple", + id -> Megastructure.builder(id, "temple") + .prerequisite(context -> isPlanet(context) + && !isErrorPlanet(context) + && (context.resources() == null || context.resources().hasCivilization())) + .ring(1) + .model(1, ringModel(1, "temple")) + .material(ModBlocks.ENCHANTED_GOLD_BLOCK.asItem(), 64) + .handler(TempleHandler::new) + .build() + ); + public static final DeferredHolder GIANT_PLANET_EXTRACTOR = register( + "giant_planet_exctractor", + id -> Megastructure.builder(id, "giant_planet_exctractor") + .prerequisite(context -> context.body() instanceof GiantPlanetData) + .ring(2) + .model(2, ringModel(2, "exctractor")) + .material(ModBlocks.PUMP.asItem(), 32) + .handler(GiantExtractorHandler::new) + .build() + ); + public static final DeferredHolder DYSON_SPHERE_BROWN_DWARF = register( + "dyson_sphere_brown_dwarf", + id -> Megastructure.builder(id, "dyson_sphere_brown_dwarf") + .prerequisite(ModMegastructures::isBrownDwarfOrRemnant) + .ring(2) + .rotation(ModMegastructures::bodySynchronizedRotation) + .model(2, ringModel(2, "dyson_sphere")) + .material(ModItems.DYSON_SPHERE_COMPONENT.get(), 8) + .handler(() -> new DysonSphereHandler("dyson_sphere_brown_dwarf")) + .build() + ); + public static final DeferredHolder STELLAR_RING_COLLIDER = register( + "stellar_ring_collider", + id -> Megastructure.builder(id, "stellar_ring_collider") + .prerequisite(context -> context.body() instanceof StarData star + && star.size() < 48 + && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR + && star.bodyClass() != CelestialBodyClass.BLACK_HOLE) + .ring(4) + .model(4, ringModel(4, "collider")) + .material(ModItems.STELLAR_RING_COMPONENT.get(), 8) + .handler(ColliderHandler::new) + .build() + ); + public static final DeferredHolder DYSON_SPHERE_SMALL = register( + "dyson_sphere_small", + id -> Megastructure.builder(id, "dyson_sphere_small") + .prerequisite(context -> isOrdinaryStar(context, false)) + .ring(4) + .rotation(ModMegastructures::bodySynchronizedRotation) + .model(4, ringModel(4, "dyson_sphere")) + .material(ModItems.DYSON_SPHERE_COMPONENT.get(), 16) + .handler(() -> new DysonSphereHandler("dyson_sphere_small")) + .build() + ); + public static final DeferredHolder DYSON_SPHERE_LARGE = register( + "dyson_sphere_large", + id -> Megastructure.builder(id, "dyson_sphere_large") + .prerequisite(context -> isOrdinaryStar(context, true)) + .ring(5) + .rotation(ModMegastructures::bodySynchronizedRotation) + .model(5, ringModel(5, "dyson_sphere")) + .material(ModItems.DYSON_SPHERE_COMPONENT.get(), 32) + .handler(() -> new DysonSphereHandler("dyson_sphere_large")) + .build() + ); + public static final DeferredHolder MAGNETAR_COIL = register( + "magnetar_coil", + id -> Megastructure.builder(id, "magnetar_coil") + .prerequisite(context -> context.body() instanceof StarData star + && star.bodyClass() == CelestialBodyClass.NEUTRON_STAR) + .ring(4) + .model(4, ringModel(4, "coil")) + .material(ModItems.MAGNETAR_COIL_COMPONENT.get(), 4) + .handler(MagnetarCoilHandler::new) + .build() + ); + public static final DeferredHolder PENROSE_SPHERE = register( + "penrose_sphere", + id -> Megastructure.builder(id, "penrose_sphere") + .prerequisite(ModMegastructures::isBlackHole) + .ring(4) + .rotation(context -> -bodySynchronizedRotation(context)) + .model(4, ringModel(4, "penrose_sphere")) + .material(ModItems.PENROSE_SPHERE_COMPONENT.get(), 8) + .handler(PenroseSphereHandler::new) + .build() + ); + public static final DeferredHolder MATTER_DECOMPRESSOR = register( + "matter_decompressor", + id -> Megastructure.builder(id, "matter_decompressor") + .prerequisite(context -> context.body() instanceof StarData star + && (star.bodyClass() == CelestialBodyClass.NEUTRON_STAR + || star.bodyClass() == CelestialBodyClass.BLACK_HOLE)) + .ring(4) + .model(4, ringModel(4, "matter_decompressor")) + .material(ModItems.MATTER_DECOMPRESSOR_COMPONENT.get(), 2) + .handler(MatterDecompressorHandler::new) + .build() + ); + public static final DeferredHolder WORMHOLE_STABILIZER = register( + "wormhole_stabilizer", + id -> Megastructure.builder(id, "wormhole_stabilizer") + .prerequisite(context -> context.amplified() && isBlackHole(context)) + .ring(4) + .model(4, ringModel(4, "wormhole_stabilizer")) + .material(ModItems.WORMHOLE_STABILIZER_COMPONENT.get(), 4) + .handler(WormholeStabilizerHandler::new) + .build() + ); + public static final DeferredHolder STELLAR_EVOLUTION_ACCELERATOR = register( + "stellar_evolution_accelerator", + id -> Megastructure.builder(id, "stellar_evolution_accelerator") + .prerequisite(context -> context.body() instanceof StarData star + && !star.specialRedDwarf() + && star.bodyClass() != CelestialBodyClass.WHITE_DWARF + && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR + && star.bodyClass() != CelestialBodyClass.BLACK_HOLE) + .ring(context -> context.body().size() >= 48 ? 6 : 5) + .model(5, ringModel(5, "stellar_evolution_accelerator")) + .model(6, ringModel(6, "stellar_evolution_accelerator")) + .material(ModItems.STELLAR_EVOLUTION_ACCELERATOR_COMPONENT.get(), 8) + .handler(AcceleratorHandler::new) + .auxiliary() + .build() + ); + + private ModMegastructures() { + } + + public static void register(IEventBus modEventBus) { + REGISTER.register(modEventBus); + } + + private static DeferredHolder register( + String name, + Function factory + ) { + ResourceLocation id = AnvilCraft.of(name); + return REGISTER.register(name, () -> factory.apply(id)); + } + + private static ResourceLocation ringModel(int ring, String name) { + return AnvilCraft.of("block/celestial_forging_anvil_ring_" + ring + "_" + name); + } + + private static boolean isPlanet(Megastructure.Context context) { + return context.body() instanceof RockyPlanetData || context.body() instanceof SpecialCelestialBodyData; + } + + private static boolean isErrorPlanet(Megastructure.Context context) { + return context.body() instanceof SpecialCelestialBodyData special && special.isErrorPlanet(); + } + + private static boolean hasLiquid(Megastructure.Context context) { + if (context.body() instanceof RockyPlanetData rocky) { + return rocky.liquidCoverage() != LiquidCoverage.NONE; + } + if (context.body() instanceof SpecialCelestialBodyData special) { + LiquidCoverage coverage = special.liquidCoverage(); + return coverage != null && coverage != LiquidCoverage.NONE; + } + return false; + } + + private static boolean isEcoStationEligible(@Nullable PlanetaryResourceSet resources) { + if (resources == null) return true; + if (resources.hasCivilization()) return false; + return !resources.getBiologicalItems().isEmpty() || !resources.getBiologicalFluids().isEmpty(); + } + + private static boolean isOrdinaryStar(Megastructure.Context context, boolean large) { + return context.body() instanceof StarData star + && (star.size() >= 48) == large + && !star.specialRedDwarf() + && star.bodyClass() != CelestialBodyClass.NEUTRON_STAR + && star.bodyClass() != CelestialBodyClass.BLACK_HOLE; + } + + private static boolean isBrownDwarfOrRemnant(Megastructure.Context context) { + if (context.amplified()) return false; + if (context.body() instanceof GiantPlanetData brown) return brown.brownDwarf(); + return context.body() instanceof StarData star + && star.specialRedDwarf() + && star.bodyClass() == CelestialBodyClass.M_MAIN; + } + + private static boolean isBlackHole(Megastructure.Context context) { + return context.body() instanceof StarData star && star.bodyClass() == CelestialBodyClass.BLACK_HOLE; + } + + private static float bodySynchronizedRotation(Megastructure.RotationContext context) { + return context.bodyRotation() * CelestialBodyData.getVisualRotationSpeed(context.body().rotationSpeed()); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/init/ModSoundEvents.java b/src/main/java/dev/dubhe/anvilcraft/init/ModSoundEvents.java index c2939fddf2..24e5c3bdfe 100644 --- a/src/main/java/dev/dubhe/anvilcraft/init/ModSoundEvents.java +++ b/src/main/java/dev/dubhe/anvilcraft/init/ModSoundEvents.java @@ -65,6 +65,12 @@ public class ModSoundEvents { () -> SoundEvent.createVariableRangeEvent(AnvilCraft.of("block.anvilcraft.auto_enchanting_table.use")) ); + /// 淬灭序曲:锻星砧超新星爆发前播放的完整曲目(约 1 分 12 秒)。 + /// 固定 64 格听距,让围观超新星的玩家都能听到。 + public static final Supplier QUENCHED_OUT = REGISTER.register( + "quenched_out", () -> SoundEvent.createFixedRangeEvent(AnvilCraft.of("quenched_out"), 64.0f) + ); + public static void register(IEventBus modBus) { REGISTER.register(modBus); } diff --git a/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlockEntities.java b/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlockEntities.java index 8df0cbe2d4..0ad50995a1 100644 --- a/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlockEntities.java +++ b/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlockEntities.java @@ -8,6 +8,7 @@ import dev.dubhe.anvilcraft.block.entity.AutoEnchantingTableBlockEntity; import dev.dubhe.anvilcraft.block.entity.BlackHoleBlockEntity; import dev.dubhe.anvilcraft.block.entity.BurningHeaterBlockEntity; +import dev.dubhe.anvilcraft.block.entity.CelestialBackGateBlockEntity; import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilBlockEntity; import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilFluidInterfaceBlockEntity; import dev.dubhe.anvilcraft.block.entity.CelestialForgingAnvilLaserInterfaceBlockEntity; @@ -494,6 +495,11 @@ public class ModBlockEntities { .renderer(() -> LaserBlockRenderer::new) .register(); + public static final BlockEntityEntry CELESTIAL_BACK_GATE = REGISTRUM + .blockEntity("celestial_back_gate", CelestialBackGateBlockEntity::new) + .validBlock(ModBlocks.CELESTIAL_BACK_GATE) + .register(); + public static final BlockEntityEntry TRADING_STATION = REGISTRUM .blockEntity("trading_station", TradingStationBlockEntity::new) .validBlock(ModBlocks.TRADING_STATION) diff --git a/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java b/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java index fb13412047..f922dc6849 100644 --- a/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java +++ b/src/main/java/dev/dubhe/anvilcraft/init/block/ModBlocks.java @@ -19,6 +19,7 @@ import dev.dubhe.anvilcraft.block.BurningHeaterBlock; import dev.dubhe.anvilcraft.block.CakeBaseBlock; import dev.dubhe.anvilcraft.block.CakeBlock; +import dev.dubhe.anvilcraft.block.CelestialBackGateBlock; import dev.dubhe.anvilcraft.block.CementCauldronBlock; import dev.dubhe.anvilcraft.block.ChargeCollectorBlock; import dev.dubhe.anvilcraft.block.ChargerBlock; @@ -1856,14 +1857,7 @@ public boolean doesSneakBypassUse(ItemStack stack, LevelReader level, BlockPos p .isValidSpawn(Blocks::never) .explosionResistance(1200) .emissiveRendering(ModBlocks::always)) - .loot((tables, block) -> { - // Generate empty loot table (rolls=0) so datagen doesn't break. - // Actual drop (with NBT) is handled manually in onRemove. - tables.add( - block, - LootTable.lootTable().withPool(LootPool.lootPool().setRolls(ConstantValue.exactly(0.0f))) - ); - }) + .loot(SimpleMultiPartBlock::loot) .tag((BlockTags.MINEABLE_WITH_PICKAXE), BlockTags.WITHER_IMMUNE, BlockTags.DRAGON_IMMUNE, ModBlockTags.COLLISION_IMMUNE) .item(CelestialForgingAnvilBlockItem::new) .properties(properties -> properties.stacksTo(1).rarity(Rarity.EPIC)) @@ -1976,6 +1970,22 @@ public boolean doesSneakBypassUse(ItemStack stack, LevelReader level, BlockPos p .recipe(RegistrumBlockRecipeLoader::celestialForgingAnvilPortal) .register(); + public static final BlockEntry CELESTIAL_BACK_GATE = REGISTRUM + .block("celestial_back_gate", CelestialBackGateBlock::new) + .lang("Celestial Back Gate") + .initialProperties(() -> Blocks.IRON_BLOCK) + .properties(properties -> properties + .noOcclusion() + .noCollission() + .isViewBlocking(ModBlocks::never) + .isValidSpawn(Blocks::never) + .strength(-1.0F, 3600000.0F) + .pushReaction(PushReaction.BLOCK) + .noLootTable()) + .blockstate(DataGenUtil::noExtraModelOrState) + .tag(BlockTags.WITHER_IMMUNE, BlockTags.DRAGON_IMMUNE, ModBlockTags.COLLISION_IMMUNE) + .register(); + public static final BlockEntry MAGNETO_ELECTRIC_CORE_BLOCK = REGISTRUM.block( "magnetoelectric_core", MagnetoElectricCoreBlock::new diff --git a/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistries.java b/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistries.java index 3ea5b283e5..9fcbbb8a5e 100644 --- a/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistries.java +++ b/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistries.java @@ -6,6 +6,7 @@ import dev.dubhe.anvilcraft.api.recipe.data.ICustomDataComponent; import dev.dubhe.anvilcraft.api.recipe.number.INumberProvider; import dev.dubhe.anvilcraft.api.recipe.result.modifier.IResultModifier; +import dev.dubhe.anvilcraft.block.entity.celestial.Megastructure; import dev.dubhe.anvilcraft.item.property.component.amulet.IAmulet; import dev.dubhe.anvilcraft.saved.storage.category.ICategory; import net.minecraft.core.Registry; @@ -38,6 +39,10 @@ public class ModRegistries { public static final Registry> TARGET_POINTER_TYPE = ModRegistries.simple( ModRegistryKeys.TARGET_POINTER_TYPE ); + /** Code-registered CFA megastructure definitions. */ + public static final Registry MEGASTRUCTURE = ModRegistries.simple( + ModRegistryKeys.MEGASTRUCTURE + ); @SubscribeEvent public static void registerRegistries(NewRegistryEvent event) { @@ -48,6 +53,7 @@ public static void registerRegistries(NewRegistryEvent event) { event.register(ModRegistries.NUMBER_PROVIDER_TYPE); event.register(ModRegistries.CATEGORY_TYPE); event.register(ModRegistries.TARGET_POINTER_TYPE); + event.register(ModRegistries.MEGASTRUCTURE); } private static Registry simple(ResourceKey> key) { diff --git a/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistryKeys.java b/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistryKeys.java index f017d4b1ae..9992ed9a1c 100644 --- a/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistryKeys.java +++ b/src/main/java/dev/dubhe/anvilcraft/init/registry/ModRegistryKeys.java @@ -6,6 +6,7 @@ import dev.dubhe.anvilcraft.api.recipe.data.ICustomDataComponent; import dev.dubhe.anvilcraft.api.recipe.number.INumberProvider; import dev.dubhe.anvilcraft.api.recipe.result.modifier.IResultModifier; +import dev.dubhe.anvilcraft.block.entity.celestial.Megastructure; import dev.dubhe.anvilcraft.block.placement.BlockPlacementRuleSet; import dev.dubhe.anvilcraft.item.property.component.amulet.IAmulet; import dev.dubhe.anvilcraft.saved.storage.category.ICategory; @@ -27,6 +28,7 @@ public class ModRegistryKeys { public static final ResourceKey> CATEGORY = key("category"); public static final ResourceKey>> TARGET_POINTER_TYPE = key("target_pointer"); public static final ResourceKey> BLOCK_PLACEMENT_RULES = key("block_placement_rules"); + public static final ResourceKey> MEGASTRUCTURE = key("megastructure"); @SubscribeEvent public static void registerRegistries(DataPackRegistryEvent.NewRegistry event) { @@ -42,4 +44,4 @@ public static void registerRegistries(DataPackRegistryEvent.NewRegistry event) { private static ResourceKey> key(String name) { return ResourceKey.createRegistryKey(AnvilCraft.of(name)); } -} \ No newline at end of file +} diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/ChunkGeneratorMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/ChunkGeneratorMixin.java new file mode 100644 index 0000000000..4fccfd8317 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/ChunkGeneratorMixin.java @@ -0,0 +1,93 @@ +package dev.dubhe.anvilcraft.mixin; + +import dev.dubhe.anvilcraft.worldgen.VanillaOverworldBiomeSource; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.chunk.ChunkGenerator; +import net.minecraft.world.level.levelgen.structure.StructureSet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import java.util.function.Function; + +/** Keeps the overworld-like generator independent from data-driven mod world generation. */ +@Mixin(ChunkGenerator.class) +public class ChunkGeneratorMixin { + @ModifyVariable( + method = "(Lnet/minecraft/world/level/biome/BiomeSource;Ljava/util/function/Function;)V", + at = @At("HEAD"), + argsOnly = true, + index = 2 + ) + private static Function, BiomeGenerationSettings> anvilcraft$useOriginalBiomeSettings( + Function, BiomeGenerationSettings> settingsGetter, BiomeSource biomeSource + ) { + return biomeSource instanceof VanillaOverworldBiomeSource + ? VanillaOverworldBiomeSource::originalGenerationSettings : settingsGetter; + } + + @ModifyArg( + method = "createState", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;createForNormal(" + + "Lnet/minecraft/world/level/levelgen/RandomState;J" + + "Lnet/minecraft/world/level/biome/BiomeSource;" + + "Lnet/minecraft/core/HolderLookup;" + + ")Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;" + ), + index = 3 + ) + private HolderLookup anvilcraft$keepOnlyVanillaStructureSets( + HolderLookup structureSets + ) { + ChunkGenerator generator = (ChunkGenerator) (Object) this; + return generator.getBiomeSource() instanceof VanillaOverworldBiomeSource + ? new VanillaStructureSetLookup(structureSets) : structureSets; + } + + private static final class VanillaStructureSetLookup implements HolderLookup { + private final HolderLookup parent; + + private VanillaStructureSetLookup(HolderLookup parent) { + this.parent = parent; + } + + @Override + public java.util.Optional> get(ResourceKey key) { + return parent.get(key).filter(VanillaStructureSetLookup::isVanillaOnly); + } + + @Override + public java.util.Optional> get( + net.minecraft.tags.TagKey key + ) { + return parent.get(key); + } + + @Override + public java.util.stream.Stream> listElements() { + return parent.listElements().filter(VanillaStructureSetLookup::isVanillaOnly); + } + + @Override + public java.util.stream.Stream> listTags() { + return parent.listTags(); + } + + private static boolean isVanillaOnly(Holder.Reference structureSet) { + return structureSet.unwrapKey() + .map(key -> "minecraft".equals(key.location().getNamespace())) + .orElse(false) + && structureSet.value().structures().stream().allMatch(entry -> entry.structure().unwrapKey() + .map(key -> "minecraft".equals(key.location().getNamespace())) + .orElse(false)); + } + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000000..1e39316dbe --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/MinecraftServerMixin.java @@ -0,0 +1,39 @@ +package dev.dubhe.anvilcraft.mixin; + +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelManager; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.biome.BiomeManager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +/** Gives the built-in overworld-like level an independent biome zoom seed. */ +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + @ModifyArgs( + method = "createLevels", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;(" + + "Lnet/minecraft/server/MinecraftServer;" + + "Ljava/util/concurrent/Executor;" + + "Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;" + + "Lnet/minecraft/world/level/storage/ServerLevelData;" + + "Lnet/minecraft/resources/ResourceKey;" + + "Lnet/minecraft/world/level/dimension/LevelStem;" + + "Lnet/minecraft/server/level/progress/ChunkProgressListener;" + + "ZJ" + + "Ljava/util/List;" + + "Z" + + "Lnet/minecraft/world/RandomSequences;)V" + ) + ) + private void anvilcraft$useOverworldLikeBiomeSeed(Args args) { + if (!CelestialTravelManager.OVERWORLD_LIKE_LEVEL.equals(args.get(4))) return; + MinecraftServer server = args.get(0); + long sourceSeed = server.getWorldData().worldGenOptions().seed(); + long dimensionSeed = CelestialTravelManager.overworldLikeSeed(sourceSeed); + args.set(8, BiomeManager.obfuscateSeed(dimensionSeed)); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/mixin/ServerLevelMixin.java b/src/main/java/dev/dubhe/anvilcraft/mixin/ServerLevelMixin.java index 712e6dd3ba..ebe335650a 100644 --- a/src/main/java/dev/dubhe/anvilcraft/mixin/ServerLevelMixin.java +++ b/src/main/java/dev/dubhe/anvilcraft/mixin/ServerLevelMixin.java @@ -2,6 +2,7 @@ import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.dubhe.anvilcraft.block.entity.celestial.CelestialTravelManager; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.ExperienceOrb; @@ -11,6 +12,7 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(ServerLevel.class) @@ -22,6 +24,40 @@ public class ServerLevelMixin { @Unique boolean anvilcraft$shouldCheckDiscarded; + /** Give the built-in overworld-like dimension a deterministic seed of its own. */ + @Inject(method = "getSeed", at = @At("HEAD"), cancellable = true) + private void anvilcraft$useOverworldLikeSeed(CallbackInfoReturnable cir) { + ServerLevel level = (ServerLevel) (Object) this; + if (!CelestialTravelManager.isOverworldLike(level.dimension())) return; + long sourceSeed = level.getServer().getWorldData().worldGenOptions().seed(); + cir.setReturnValue(CelestialTravelManager.overworldLikeSeed(sourceSeed)); + } + + /** Keep structure-presence checks on the same seed as terrain generation. */ + @ModifyArg( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/levelgen/structure/StructureCheck;(" + + "Lnet/minecraft/world/level/chunk/storage/ChunkScanAccess;" + + "Lnet/minecraft/core/RegistryAccess;" + + "Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;" + + "Lnet/minecraft/resources/ResourceKey;" + + "Lnet/minecraft/world/level/chunk/ChunkGenerator;" + + "Lnet/minecraft/world/level/levelgen/RandomState;" + + "Lnet/minecraft/world/level/LevelHeightAccessor;" + + "Lnet/minecraft/world/level/biome/BiomeSource;" + + "JLcom/mojang/datafixers/DataFixer;)V" + ), + index = 8 + ) + private long anvilcraft$useOverworldLikeStructureSeed(long seed) { + ServerLevel level = (ServerLevel) (Object) this; + if (!CelestialTravelManager.isOverworldLike(level.dimension())) return seed; + long sourceSeed = level.getServer().getWorldData().worldGenOptions().seed(); + return CelestialTravelManager.overworldLikeSeed(sourceSeed); + } + @Inject( method = "addFreshEntity", at = @At(value = "HEAD") diff --git a/src/main/java/dev/dubhe/anvilcraft/network/HeatableSyncPacket.java b/src/main/java/dev/dubhe/anvilcraft/network/HeatableSyncPacket.java deleted file mode 100644 index ecfe840896..0000000000 --- a/src/main/java/dev/dubhe/anvilcraft/network/HeatableSyncPacket.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.dubhe.anvilcraft.network; - -import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; -import dev.anvilcraft.lib.v2.network.packet.IPacket; -import dev.dubhe.anvilcraft.AnvilCraft; -import dev.dubhe.anvilcraft.block.entity.heatable.HeatableBlockEntity; -import io.netty.buffer.ByteBuf; -import net.minecraft.core.BlockPos; -import net.minecraft.network.codec.ByteBufCodecs; -import net.minecraft.network.codec.StreamCodec; -import net.minecraft.world.entity.player.Player; - -public record HeatableSyncPacket(BlockPos pos, int duration) implements IClientboundPacket { - public static final Type TYPE = IPacket.type(AnvilCraft.of("heatable_sync")); - public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - BlockPos.STREAM_CODEC, - HeatableSyncPacket::pos, - ByteBufCodecs.VAR_INT, - HeatableSyncPacket::duration, - HeatableSyncPacket::new - ); - - @Override - public Type type() { - return TYPE; - } - - @Override - public void handleOnClient(Player player) { - if (!(player.level().getBlockEntity(this.pos) instanceof HeatableBlockEntity heatable)) return; - heatable.setDuration(this.duration); - } -} diff --git a/src/main/java/dev/dubhe/anvilcraft/network/QuenchedOutMusicPacket.java b/src/main/java/dev/dubhe/anvilcraft/network/QuenchedOutMusicPacket.java new file mode 100644 index 0000000000..60c02c7e4f --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/network/QuenchedOutMusicPacket.java @@ -0,0 +1,47 @@ +package dev.dubhe.anvilcraft.network; + +import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.dubhe.anvilcraft.AnvilCraft; +import dev.dubhe.anvilcraft.client.event.QuenchedOutMusicHandler; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.BlockPos; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.world.entity.player.Player; + +/** + * 淬灭序曲音乐包(Server → Client)。 + * + *

+ * 服务端在演化将进入超新星前预定时刻通知客户端开始播放 {@code quenched_out} 曲目; + * 在演化被中断(锻星砧/增幅器被破坏、加速器被清除)时通知客户端立即停止。 + *

+ */ +public record QuenchedOutMusicPacket(BlockPos pos, boolean start) implements IClientboundPacket { + + public static final Type TYPE = IPacket.type(AnvilCraft.of("quenched_out_music")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + BlockPos.STREAM_CODEC, + QuenchedOutMusicPacket::pos, + ByteBufCodecs.BOOL, + QuenchedOutMusicPacket::start, + QuenchedOutMusicPacket::new + ); + + @Override + public Type type() { + return TYPE; + } + + @Override + public void handleOnClient(Player player) { + if (start) { + QuenchedOutMusicHandler.start(pos); + } else { + QuenchedOutMusicHandler.stop(pos); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/dubhe/anvilcraft/worldgen/VanillaOverworldBiomeSource.java b/src/main/java/dev/dubhe/anvilcraft/worldgen/VanillaOverworldBiomeSource.java new file mode 100644 index 0000000000..677b788dfc --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/worldgen/VanillaOverworldBiomeSource.java @@ -0,0 +1,59 @@ +package dev.dubhe.anvilcraft.worldgen; + +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.RegistryOps; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.BiomeGenerationSettings; +import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.biome.Climate; +import net.minecraft.world.level.biome.MultiNoiseBiomeSource; +import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterList; + +import java.util.List; +import java.util.stream.Stream; + +/** Vanilla overworld climate layout with access to the original biome data. */ +public class VanillaOverworldBiomeSource extends BiomeSource { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group(RegistryOps.retrieveGetter(Registries.BIOME)) + .apply(instance, VanillaOverworldBiomeSource::new) + ); + + private final MultiNoiseBiomeSource delegate; + + public VanillaOverworldBiomeSource(HolderGetter biomes) { + MultiNoiseBiomeSourceParameterList parameters = new MultiNoiseBiomeSourceParameterList( + MultiNoiseBiomeSourceParameterList.Preset.OVERWORLD, biomes + ); + delegate = MultiNoiseBiomeSource.createFromList(parameters.parameters()); + } + + public static BiomeGenerationSettings originalGenerationSettings(Holder biome) { + return biome.value().modifiableBiomeInfo().getOriginalBiomeInfo().generationSettings(); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected Stream> collectPossibleBiomes() { + return delegate.possibleBiomes().stream(); + } + + @Override + public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) { + return delegate.getNoiseBiome(x, y, z, sampler); + } + + @Override + public void addDebugInfo(List info, BlockPos pos, Climate.Sampler sampler) { + delegate.addDebugInfo(info, pos, sampler); + } +} diff --git a/src/main/java/dev/dubhe/anvilcraft/worldgen/package-info.java b/src/main/java/dev/dubhe/anvilcraft/worldgen/package-info.java new file mode 100644 index 0000000000..4fd55c5ad5 --- /dev/null +++ b/src/main/java/dev/dubhe/anvilcraft/worldgen/package-info.java @@ -0,0 +1,9 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +@FieldsAreNonnullByDefault +package dev.dubhe.anvilcraft.worldgen; + +import com.mojang.logging.annotations.FieldsAreNonnullByDefault; +import com.mojang.logging.annotations.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/src/main/resources/anvilcraft.mixins.json b/src/main/resources/anvilcraft.mixins.json index 4760b75308..9210922277 100644 --- a/src/main/resources/anvilcraft.mixins.json +++ b/src/main/resources/anvilcraft.mixins.json @@ -14,6 +14,7 @@ "BlockStateMixin", "BuddingAmethystBlockMixin", "CandleCakeBlockMixin", + "ChunkGeneratorMixin", "ComparatorBlockEntityMixin", "ComponentSerializationMixin", "CraftingMenuMixin", @@ -43,6 +44,7 @@ "LightningBoltMixin", "LivingEntityMixin", "LootItemRandomChanceWithEnchantedBonusConditionMixin", + "MinecraftServerMixin", "NearestAttackableTargetGoalMixin", "PhantomGoalMixin", "PigMixin", diff --git a/src/main/resources/assets/anvilcraft/blockstates/celestial_back_gate.json b/src/main/resources/assets/anvilcraft/blockstates/celestial_back_gate.json new file mode 100644 index 0000000000..a210a87c17 --- /dev/null +++ b/src/main/resources/assets/anvilcraft/blockstates/celestial_back_gate.json @@ -0,0 +1,8 @@ +{ + "variants": { + "facing=north": { "model": "anvilcraft:block/back_gate" }, + "facing=east": { "model": "anvilcraft:block/back_gate", "y": 90 }, + "facing=south": { "model": "anvilcraft:block/back_gate", "y": 180 }, + "facing=west": { "model": "anvilcraft:block/back_gate", "y": 270 } + } +} diff --git a/src/main/resources/assets/anvilcraft/lang/zh_cn.json b/src/main/resources/assets/anvilcraft/lang/zh_cn.json index 9eda703cca..c0d2cdf75c 100644 --- a/src/main/resources/assets/anvilcraft/lang/zh_cn.json +++ b/src/main/resources/assets/anvilcraft/lang/zh_cn.json @@ -1721,6 +1721,7 @@ "screen.anvilcraft.cfa.megastructure.dyson_sphere_small": "戴森球(小)", "screen.anvilcraft.cfa.megastructure.eco_station": "生态站", "screen.anvilcraft.cfa.megastructure.giant_planet_exctractor": "巨行星抽取器", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf": "戴森球(褐矮星)", "screen.anvilcraft.cfa.megastructure.magnetar_coil": "磁星线圈", "screen.anvilcraft.cfa.megastructure.matter_decompressor": "物质解压器", "screen.anvilcraft.cfa.megastructure.penrose_sphere": "彭罗斯球", @@ -1815,6 +1816,7 @@ "screen.anvilcraft.cfa.megastructure.dyson_sphere_small.description": "收集恒星的光能产生大量电力", "screen.anvilcraft.cfa.megastructure.eco_station.description": "养殖星球上的生物,收获生物资源", "screen.anvilcraft.cfa.megastructure.giant_planet_exctractor.description": "抽取星球上的流体资源,此举可能破坏星球生态", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.description": "收集褐矮星能量发电,可消耗原始物质增强发电并最终将其转化为红矮星", "screen.anvilcraft.cfa.megastructure.magnetar_coil.description": "利用磁星高速变化的磁场发电", "screen.anvilcraft.cfa.megastructure.matter_decompressor.description": "通过伽马激光激活奇异引力场抽取中子星内部或黑洞视界内的物质", "screen.anvilcraft.cfa.megastructure.penrose_sphere.description": "利用黑洞高速自转产生的能层强化激光能量", @@ -1837,6 +1839,7 @@ "screen.anvilcraft.cfa.megastructure.dyson_sphere_small.usage": "需求:\n§7无§r\n———————\n产出:\n§a[电力功率]§r\n§f根据恒星温度与半径发电§r\n———————\n副作用:\n§7无§r", "screen.anvilcraft.cfa.megastructure.eco_station.usage": "需求:\n§a[电力功率]§r\n§f1000 kW§r\n———————\n产出:\n§e[物流接口]§r\n§f行星生物资源§r\n§b[流体接口]§r\n§f行星生物资源§r\n———————\n副作用:\n§7无§r", "screen.anvilcraft.cfa.megastructure.giant_planet_exctractor.usage": "需求:\n§7无§r\n———————\n产出:\n§e[物流接口]§r\n§f巨行星固体资源§r\n§b[流体接口]§r\n§f巨行星流体资源§r\n———————\n副作用:\n§7无§r", + "screen.anvilcraft.cfa.megastructure.dyson_sphere_brown_dwarf.usage": "需求:\n§b[流体接口]§r\n§f每刻原始物质 250mB–2B,稳定供给40gt§r\n———————\n产出:\n§a[电力功率]§r\n§fP=(E×R²)/1600 MW;稳定供给倍率为×1.5/×2/×3/×5§r\n———————\n副作用:\n§f超过2B的物质累积,12800B后褐矮星转化为红矮星§r", "screen.anvilcraft.cfa.megastructure.magnetar_coil.usage": "需求:\n§7无§r\n———————\n产出:\n§a[电力]§r\n§f根据中子星磁场强度与转速发电§r\n———————\n副作用:\n§7无§r", "screen.anvilcraft.cfa.megastructure.matter_decompressor.usage": "需求:\n§c[激光接口]§r\n§d伽马激光 Lv.1+§r§f (每级伽马激光提供更高工作效率)§r\n———————\n产出:\n§e[物流接口]§r\n§f根据天体类型产出对应物质§r\n———————\n副作用:\n§7无§r", "screen.anvilcraft.cfa.megastructure.penrose_sphere.usage": "需求:\n§c[激光接口]§r\n§c激光 Lv.1+ §r§f(不能在锻星砧侧面的中间)§r\n———————\n产出:\n§c[激光接口]§r\n§d伽马激光 Lv.= §r§f(输出的接口在输入接口同侧对位)§r\n———————\n副作用:\n§7无§r", @@ -2132,7 +2135,7 @@ "tooltip.anvilcraft.item.chocolate_black": "可口的巧克力,纯可可脂的,吃下获得急迫效果", "tooltip.anvilcraft.item.chocolate_white": "可口的巧克力,纯可可脂的,吃下获得跳跃提升效果", "tooltip.anvilcraft.item.controllable_sand": "可以被红石控制上升或下降的沙子", - "tooltip.anvilcraft.item.creative_generator": "提供最多 8192 kW 的能量,也可以作为负载使用", + "tooltip.anvilcraft.item.creative_generator": "提供最多 65536 kW 的能量,也可以作为负载使用", "tooltip.anvilcraft.item.crushing_table": "执行粉碎配方", "tooltip.anvilcraft.item.ember_deco_block": "余烬金属制装饰方块,似乎因为含量太低而不能防爆", "tooltip.anvilcraft.item.ember_deco_outline": "余烬金属制装饰方块,似乎因为含量太低而不能防爆", diff --git a/src/main/resources/assets/anvilcraft/models/block/back_gate.json b/src/main/resources/assets/anvilcraft/models/block/back_gate.json index 965fdcda61..9b52e1b8dc 100644 --- a/src/main/resources/assets/anvilcraft/models/block/back_gate.json +++ b/src/main/resources/assets/anvilcraft/models/block/back_gate.json @@ -10,13 +10,17 @@ }, "elements": [ { - "from": [-8, -8, 8], - "to": [24, 24, 8], + "from": [-8, -8, 7.9], + "to": [24, 24, 8.1], "shade": false, "rotation": {"angle": 0, "axis": "z", "origin": [8, 8, 8]}, "faces": { "north": {"uv": [0, 0, 16, 16], "rotation": 180, "texture": "#4"}, - "south": {"uv": [0, 0, 16, 16], "rotation": 180, "texture": "#4"} + "south": {"uv": [0, 0, 16, 16], "rotation": 180, "texture": "#4"}, + "east": {"uv": [0, 0, 0.2, 16], "texture": "#4"}, + "west": {"uv": [0, 0, 0.2, 16], "texture": "#4"}, + "up": {"uv": [0, 0, 16, 0.2], "texture": "#4"}, + "down": {"uv": [0, 0, 16, 0.2], "texture": "#4"} } } ], @@ -64,4 +68,4 @@ "scale": [0.4, 0.4, 0.4] } } -} \ No newline at end of file +} diff --git a/src/main/resources/data/anvilcraft/dimension/overworld_like.json b/src/main/resources/data/anvilcraft/dimension/overworld_like.json new file mode 100644 index 0000000000..ee1470c937 --- /dev/null +++ b/src/main/resources/data/anvilcraft/dimension/overworld_like.json @@ -0,0 +1,10 @@ +{ + "type": "minecraft:overworld", + "generator": { + "type": "minecraft:noise", + "biome_source": { + "type": "anvilcraft:vanilla_overworld" + }, + "settings": "minecraft:overworld" + } +}