From e386229b667817fd0ea83203438b95f5e552ba2c Mon Sep 17 00:00:00 2001 From: Sara Freimer Date: Tue, 14 Jun 2022 14:05:07 -0500 Subject: [PATCH] Replace StructureSpawnListGatherEvent with StructureModifiers (#8717) --- build.gradle | 1 + .../world/level/NaturalSpawner.java.patch | 9 + .../levelgen/structure/Structure.java.patch | 55 +++++ .../structure_modifier/modify_stronghold.json | 11 + .../net/minecraftforge/common/ForgeMod.java | 8 + .../common/data/JsonCodecProvider.java | 130 ++++++++++++ .../common/world/BiomeModifier.java | 1 - .../common/world/ModifiableStructureInfo.java | 128 +++++++++++ .../common/world/NoneStructureModifier.java | 29 +++ .../common/world/StructureModifier.java | 94 ++++++++ .../world/StructureSettingsBuilder.java | 200 ++++++++++++++++++ .../common/world/StructureSpawnManager.java | 112 ---------- .../world/StructureSpawnListGatherEvent.java | 142 ------------- .../ForgeDeferredRegistriesSetup.java | 2 + .../registries/ForgeRegistries.java | 7 + .../server/ServerLifecycleHooks.java | 21 +- .../resources/coremods/field_to_method.js | 10 + .../debug/world/BiomeModifierTest.java | 104 +++------ .../debug/world/StructureModifierTest.java | 128 +++++++++++ .../StructureSpawnListGatherEventTest.java | 47 ---- src/test/resources/META-INF/mods.toml | 2 + 21 files changed, 863 insertions(+), 378 deletions(-) create mode 100644 patches/minecraft/net/minecraft/world/level/levelgen/structure/Structure.java.patch create mode 100644 src/generated_test/resources/data/structure_modifiers_test/forge/structure_modifier/modify_stronghold.json create mode 100644 src/main/java/net/minecraftforge/common/data/JsonCodecProvider.java create mode 100644 src/main/java/net/minecraftforge/common/world/ModifiableStructureInfo.java create mode 100644 src/main/java/net/minecraftforge/common/world/NoneStructureModifier.java create mode 100644 src/main/java/net/minecraftforge/common/world/StructureModifier.java create mode 100644 src/main/java/net/minecraftforge/common/world/StructureSettingsBuilder.java delete mode 100644 src/main/java/net/minecraftforge/common/world/StructureSpawnManager.java delete mode 100644 src/main/java/net/minecraftforge/event/world/StructureSpawnListGatherEvent.java create mode 100644 src/test/java/net/minecraftforge/debug/world/StructureModifierTest.java delete mode 100644 src/test/java/net/minecraftforge/debug/world/StructureSpawnListGatherEventTest.java diff --git a/build.gradle b/build.gradle index bcf369f83e..09496c4a7c 100644 --- a/build.gradle +++ b/build.gradle @@ -1059,6 +1059,7 @@ project(':forge') { '--mod', 'custom_transformtype_test', '--mod', 'data_pack_registries_test', '--mod', 'biome_modifiers_test', + '--mod', 'structure_modifiers_test', '--output', rootProject.file('src/generated_test/resources/'), '--existing', sourceSets.main.resources.srcDirs[0], '--existing', sourceSets.test.resources.srcDirs[0] diff --git a/patches/minecraft/net/minecraft/world/level/NaturalSpawner.java.patch b/patches/minecraft/net/minecraft/world/level/NaturalSpawner.java.patch index 6ac5a859d7..4e2be8dbdf 100644 --- a/patches/minecraft/net/minecraft/world/level/NaturalSpawner.java.patch +++ b/patches/minecraft/net/minecraft/world/level/NaturalSpawner.java.patch @@ -35,6 +35,15 @@ return; } +@@ -266,7 +_,7 @@ + } + + private static WeightedRandomList m_220443_(ServerLevel p_220444_, StructureManager p_220445_, ChunkGenerator p_220446_, MobCategory p_220447_, BlockPos p_220448_, @Nullable Holder p_220449_) { +- return m_220455_(p_220448_, p_220444_, p_220447_, p_220445_) ? NetherFortressStructure.f_228517_ : p_220446_.m_223133_(p_220449_ != null ? p_220449_ : p_220444_.m_204166_(p_220448_), p_220445_, p_220447_, p_220448_); ++ return m_220455_(p_220448_, p_220444_, p_220447_, p_220445_) ? p_220445_.m_220521_().m_175515_(Registry.f_235725_).m_123013_(BuiltinStructures.f_209859_).m_226612_().get(MobCategory.MONSTER).f_210044_() : p_220446_.m_223133_(p_220449_ != null ? p_220449_ : p_220444_.m_204166_(p_220448_), p_220445_, p_220447_, p_220448_); + } + + public static boolean m_220455_(BlockPos p_220456_, ServerLevel p_220457_, MobCategory p_220458_, StructureManager p_220459_) { @@ -305,6 +_,13 @@ if (p_47052_ == SpawnPlacements.Type.NO_RESTRICTIONS) { return true; diff --git a/patches/minecraft/net/minecraft/world/level/levelgen/structure/Structure.java.patch b/patches/minecraft/net/minecraft/world/level/levelgen/structure/Structure.java.patch new file mode 100644 index 0000000000..c6bb057e58 --- /dev/null +++ b/patches/minecraft/net/minecraft/world/level/levelgen/structure/Structure.java.patch @@ -0,0 +1,55 @@ +--- a/net/minecraft/world/level/levelgen/structure/Structure.java ++++ b/net/minecraft/world/level/levelgen/structure/Structure.java +@@ -40,11 +_,12 @@ + public abstract class Structure { + public static final Codec f_226553_ = Registry.f_235740_.m_194605_().dispatch(Structure::m_213658_, StructureType::m_226884_); + public static final Codec> f_226554_ = RegistryFileCodec.m_135589_(Registry.f_235725_, f_226553_); +- protected final Structure.StructureSettings f_226555_; ++ //Forge: Make this field private so that the redirect coremod can target it ++ private final Structure.StructureSettings f_226555_; + + public static RecordCodecBuilder m_226567_(RecordCodecBuilder.Instance p_226568_) { + return Structure.StructureSettings.f_226688_.forGetter((p_226595_) -> { +- return p_226595_.f_226555_; ++ return p_226595_.modifiableStructureInfo().getOriginalStructureInfo().structureSettings(); // FORGE: Patch codec to ignore field redirect coremods. + }); + } + +@@ -56,6 +_,7 @@ + + protected Structure(Structure.StructureSettings p_226558_) { + this.f_226555_ = p_226558_; ++ this.modifiableStructureInfo = new net.minecraftforge.common.world.ModifiableStructureInfo(new net.minecraftforge.common.world.ModifiableStructureInfo.StructureInfo(p_226558_)); // FORGE: cache original structure info on construction so we can bypass our field read coremods where necessary + } + + public HolderSet m_226559_() { +@@ -149,6 +_,29 @@ + public abstract Optional m_214086_(Structure.GenerationContext p_226571_); + + public abstract StructureType m_213658_(); ++ ++ // FORGE START ++ ++ private final net.minecraftforge.common.world.ModifiableStructureInfo modifiableStructureInfo; ++ ++ /** ++ * {@return Cache of original structure data and structure data modified by structure modifiers} ++ * Modified structure data is set by server after datapacks and serverconfigs load. ++ * Settings field reads are coremodded to redirect to this. ++ **/ ++ public net.minecraftforge.common.world.ModifiableStructureInfo modifiableStructureInfo() ++ { ++ return this.modifiableStructureInfo; ++ } ++ ++ /** ++ * {@return The structure's settings, with modifications if called after modifiers are applied in server init.} ++ */ ++ public StructureSettings getModifiedStructureSettings() { ++ return this.modifiableStructureInfo().get().structureSettings(); ++ } ++ ++ // FORGE END + + public static record GenerationContext(RegistryAccess f_226621_, ChunkGenerator f_226622_, BiomeSource f_226623_, RandomState f_226624_, StructureTemplateManager f_226625_, WorldgenRandom f_226626_, long f_226627_, ChunkPos f_226628_, LevelHeightAccessor f_226629_, Predicate> f_226630_) { + public GenerationContext(RegistryAccess p_226632_, ChunkGenerator p_226633_, BiomeSource p_226634_, RandomState p_226635_, StructureTemplateManager p_226636_, long p_226637_, ChunkPos p_226638_, LevelHeightAccessor p_226639_, Predicate> p_226640_) { diff --git a/src/generated_test/resources/data/structure_modifiers_test/forge/structure_modifier/modify_stronghold.json b/src/generated_test/resources/data/structure_modifiers_test/forge/structure_modifier/modify_stronghold.json new file mode 100644 index 0000000000..86b69c4019 --- /dev/null +++ b/src/generated_test/resources/data/structure_modifiers_test/forge/structure_modifier/modify_stronghold.json @@ -0,0 +1,11 @@ +{ + "type": "structure_modifiers_test:test", + "category": "monster", + "spawn": { + "type": "minecraft:wither_skeleton", + "maxCount": 15, + "minCount": 5, + "weight": 100 + }, + "structures": "minecraft:stronghold" +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/common/ForgeMod.java b/src/main/java/net/minecraftforge/common/ForgeMod.java index 71f552ab2a..e47947a196 100644 --- a/src/main/java/net/minecraftforge/common/ForgeMod.java +++ b/src/main/java/net/minecraftforge/common/ForgeMod.java @@ -32,6 +32,8 @@ import net.minecraftforge.common.loot.CanToolPerformAction; import net.minecraftforge.common.loot.LootTableIdCondition; import net.minecraftforge.common.world.BiomeModifier; import net.minecraftforge.common.world.NoneBiomeModifier; +import net.minecraftforge.common.world.NoneStructureModifier; +import net.minecraftforge.common.world.StructureModifier; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fluids.FluidAttributes; import net.minecraftforge.fluids.ForgeFlowingFluid; @@ -93,6 +95,7 @@ public class ForgeMod private static final DeferredRegister ATTRIBUTES = DeferredRegister.create(ForgeRegistries.Keys.ATTRIBUTES, "forge"); private static final DeferredRegister> COMMAND_ARGUMENT_TYPES = DeferredRegister.create(Registry.COMMAND_ARGUMENT_TYPE_REGISTRY, "forge"); private static final DeferredRegister> BIOME_MODIFIER_SERIALIZERS = DeferredRegister.create(ForgeRegistries.Keys.BIOME_MODIFIER_SERIALIZERS, "forge"); + private static final DeferredRegister> STRUCTURE_MODIFIER_SERIALIZERS = DeferredRegister.create(ForgeRegistries.Keys.STRUCTURE_MODIFIER_SERIALIZERS, "forge"); @SuppressWarnings({ "unchecked", "rawtypes" }) private static final RegistryObject ENUM_COMMAND_ARGUMENT_TYPE = COMMAND_ARGUMENT_TYPES.register("enum", () -> @@ -130,6 +133,10 @@ public class ForgeMod * Noop biome modifier. Can be used in a biome modifier json with "type": "forge:none". */ public static final RegistryObject> NONE_BIOME_MODIFIER_TYPE = BIOME_MODIFIER_SERIALIZERS.register("none", () -> Codec.unit(NoneBiomeModifier.INSTANCE)); + /** + * Noop structure modifier. Can be used in a structure modifier json with "type": "forge:none". + */ + public static final RegistryObject> NONE_STRUCTURE_MODIFIER_TYPE = STRUCTURE_MODIFIER_SERIALIZERS.register("none", () -> Codec.unit(NoneStructureModifier.INSTANCE)); private static boolean enableMilkFluid = false; @@ -177,6 +184,7 @@ public class ForgeMod ATTRIBUTES.register(modEventBus); COMMAND_ARGUMENT_TYPES.register(modEventBus); BIOME_MODIFIER_SERIALIZERS.register(modEventBus); + STRUCTURE_MODIFIER_SERIALIZERS.register(modEventBus); MinecraftForge.EVENT_BUS.addListener(this::serverStopping); MinecraftForge.EVENT_BUS.addListener(this::missingSoundMapping); ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, ForgeConfig.clientSpec); diff --git a/src/main/java/net/minecraftforge/common/data/JsonCodecProvider.java b/src/main/java/net/minecraftforge/common/data/JsonCodecProvider.java new file mode 100644 index 0000000000..415e275448 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/data/JsonCodecProvider.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.data; + +import com.mojang.logging.LogUtils; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.JsonOps; + +import java.util.function.BiConsumer; +import cpw.mods.modlauncher.api.LamdbaExceptionUtils; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.DataProvider; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraftforge.common.data.ExistingFileHelper.ResourceType; +import net.minecraftforge.forge.event.lifecycle.GatherDataEvent; +import org.slf4j.Logger; + +/** + *

Dataprovider for using a Codec to generate jsons. + * Path names for jsons are derived from the given registry folder and each entry's namespaced id, in the format:

+ *
+ * {@code /entryid/registryfolder/entrypath.json }
+ * 
+ * + * @param the type of thing being generated. + */ +public final class JsonCodecProvider implements DataProvider +{ + private static final Logger LOGGER = LogUtils.getLogger(); + protected final DataGenerator dataGenerator; + protected final ExistingFileHelper existingFileHelper; + protected final String modid; + protected final DynamicOps dynamicOps; + protected final PackType packType; + protected final String directory; + protected final Codec codec; + protected final Map entries; + + /** + * @param dataGenerator DataGenerator provided by {@link GatherDataEvent}. + * @param dynamicOps DynamicOps to encode values to jsons with using the provided Codec, e.g. {@link JsonOps.INSTANCE}. + * @param packType PackType specifying whether to generate entries in assets or data. + * @param directory String representing the directory to generate jsons in, e.g. "dimension" or "cheesemod/cheese". + * @param codec Codec to encode values to jsons with using the provided DynamicOps. + * @param entries Map of named entries to serialize to jsons. Paths for values are derived from the ResourceLocation's entryid:entrypath as specified above. + */ + public JsonCodecProvider(DataGenerator dataGenerator, ExistingFileHelper existingFileHelper, String modid, DynamicOps dynamicOps, PackType packType, + String directory, Codec codec, Map entries) + { + // Track generated data so other dataproviders can validate if needed. + final ResourceType resourceType = new ResourceType(packType, ".json", directory); + for (ResourceLocation id : entries.keySet()) + { + existingFileHelper.trackGenerated(id, resourceType); + } + this.dataGenerator = dataGenerator; + this.existingFileHelper = existingFileHelper; + this.modid = modid; + this.dynamicOps = dynamicOps; + this.packType = packType; + this.directory = directory; + this.codec = codec; + this.entries = entries; + } + + /** + * {@return DatapackRegistryProvider that encodes using the registered loading codec for the provided registry key} + * Ensures the correct directory and codec are used. + * Only vanilla datapack registries enumerated in {@link RegistryAccess} and custom forge datapack registries can + * be generated this way. + * + * @param Registry element type, e.g. Biome + * @param dataGenerator DataGenerator provided by {@link GatherDataEvent}. + * @param modid namespace of the mod adding this DataProvider, for logging purposes. + * @param registryOps RegistryOps to encode values to json with. + * @param registryKey ResourceKey identifying the registry and its directory. + * @param entries Map of entries to encode and their ResourceLocations. Paths for values are derived from the ResourceLocation's entryid:entrypath. + */ + public static JsonCodecProvider forDatapackRegistry(DataGenerator dataGenerator, ExistingFileHelper existingFileHelper, String modid, + RegistryOps registryOps, ResourceKey> registryKey, Map entries) + { + final ResourceLocation registryId = registryKey.location(); + // Minecraft datapack registry folders are in data/json-namespace/registry-name/ + // Non-vanilla registry folders are data/json-namespace/registry-namespace/registry-name/ + final String registryFolder = registryId.getNamespace().equals("minecraft") + ? registryId.getPath() + : registryId.getNamespace() + "/" + registryId.getPath(); + final Codec codec = (Codec) RegistryAccess.REGISTRIES.get(registryKey).codec(); + return new JsonCodecProvider<>(dataGenerator, existingFileHelper, modid, registryOps, PackType.SERVER_DATA, registryFolder, codec, entries); + } + + @Override + public void run(final CachedOutput cache) throws IOException + { + final Path outputFolder = this.dataGenerator.getOutputFolder(); + final String dataFolder = this.packType.getDirectory(); + gather(LamdbaExceptionUtils.rethrowBiConsumer((id, value) -> { + final Path path = outputFolder.resolve(String.join("/", dataFolder, id.getNamespace(), this.directory, id.getPath() + ".json")); + JsonElement encoded = this.codec.encodeStart(this.dynamicOps, value) + .getOrThrow(false, msg -> LOGGER.error("Failed to encode {}: {}", path, msg)); + DataProvider.saveStable(cache, encoded, path); + })); + } + + protected void gather(BiConsumer consumer) + { + this.entries.forEach(consumer); + } + + @Override + public String getName() + { + return String.format("%s generator for %s", this.directory, this.modid); + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/common/world/BiomeModifier.java b/src/main/java/net/minecraftforge/common/world/BiomeModifier.java index 3b4a179d8e..16d39e6b25 100644 --- a/src/main/java/net/minecraftforge/common/world/BiomeModifier.java +++ b/src/main/java/net/minecraftforge/common/world/BiomeModifier.java @@ -37,7 +37,6 @@ public interface BiomeModifier * Codec for (de)serializing biome modifiers inline. * Mods can use this for data generation. */ - @SuppressWarnings("unchecked") Codec DIRECT_CODEC = ExtraCodecs.lazyInitializedCodec(() -> ForgeRegistries.BIOME_MODIFIER_SERIALIZERS.get().getCodec()) .dispatch(BiomeModifier::codec, Function.identity()); diff --git a/src/main/java/net/minecraftforge/common/world/ModifiableStructureInfo.java b/src/main/java/net/minecraftforge/common/world/ModifiableStructureInfo.java new file mode 100644 index 0000000000..2537ad890f --- /dev/null +++ b/src/main/java/net/minecraftforge/common/world/ModifiableStructureInfo.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.world; + +import java.util.List; +import java.util.Locale; +import net.minecraft.core.Holder; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.Structure.StructureSettings; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Holds lazy-evaluable modified structure info. + * Memoizers are not used because it's important to return null + * without evaluating the structure info if it's accessed outside of a server context. + */ +public class ModifiableStructureInfo +{ + @NotNull + private final StructureInfo originalStructureInfo; + @Nullable + private StructureInfo modifiedStructureInfo = null; + + /** + * @param originalStructureInfo StructureInfo representing the original state of a structure when the structure was constructed. + */ + public ModifiableStructureInfo(@NotNull final StructureInfo originalStructureInfo) + { + this.originalStructureInfo = originalStructureInfo; + } + + /** + * {@return The modified structure info if modified structure info has been generated, otherwise gets original structure info} + */ + @NotNull + public StructureInfo get() + { + return this.modifiedStructureInfo == null + ? originalStructureInfo + : modifiedStructureInfo; + } + + /** + * {@return The original structure info that the associated structure was created with} + */ + @NotNull + public StructureInfo getOriginalStructureInfo() + { + return this.originalStructureInfo; + } + + /** + * {@return Modified structure info; null if it hasn't been set yet} + */ + @Nullable + public StructureInfo getModifiedStructureInfo() + { + return this.modifiedStructureInfo; + } + + /** + * Internal forge method; the game will crash if mods invoke this. + * Creates and caches the modified structure info. + * @param structure named structure with original data. + * @param structureModifiers structure modifiers to apply. + * + * @throws IllegalStateException if invoked more than once. + */ + @ApiStatus.Internal + public void applyStructureModifiers(final Holder structure, final List structureModifiers) + { + if (this.modifiedStructureInfo != null) + throw new IllegalStateException(String.format(Locale.ENGLISH, "Structure %s already modified", structure)); + + StructureInfo original = this.getOriginalStructureInfo(); + final StructureInfo.Builder builder = StructureInfo.Builder.copyOf(original); + for (StructureModifier.Phase phase : StructureModifier.Phase.values()) + { + for (StructureModifier modifier : structureModifiers) + { + modifier.modify(structure, phase, builder); + } + } + this.modifiedStructureInfo = builder.build(); + } + + /** + * Record containing raw structure data. + * @param structureSettings Structure settings. + */ + public record StructureInfo(StructureSettings structureSettings) + { + public static class Builder + { + private StructureSettingsBuilder structureSettings; + + /** + * @param original Original structure information + * @return A ModifiedStructureInfo.StructureInfo.Builder with a copy of the structure's data + */ + public static Builder copyOf(final StructureInfo original) + { + final StructureSettingsBuilder structureBuilder = StructureSettingsBuilder.copyOf(original.structureSettings()); + return new Builder(structureBuilder); + } + + private Builder(final StructureSettingsBuilder structureSettings) + { + this.structureSettings = structureSettings; + } + + public StructureInfo build() + { + return new StructureInfo(this.structureSettings.build()); + } + + public StructureSettingsBuilder getStructureSettings() + { + return structureSettings; + } + } + } +} diff --git a/src/main/java/net/minecraftforge/common/world/NoneStructureModifier.java b/src/main/java/net/minecraftforge/common/world/NoneStructureModifier.java new file mode 100644 index 0000000000..89c404a810 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/world/NoneStructureModifier.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.world; + +import com.mojang.serialization.Codec; +import net.minecraft.core.Holder; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraftforge.common.ForgeMod; +import net.minecraftforge.common.world.ModifiableStructureInfo.StructureInfo.Builder; + +public class NoneStructureModifier implements StructureModifier +{ + public static final NoneStructureModifier INSTANCE = new NoneStructureModifier(); + + @Override + public void modify(Holder structure, Phase phase, Builder builder) + { + // NOOP - intended for datapack makers who want to disable a structure modifier + } + + @Override + public Codec codec() + { + return ForgeMod.NONE_STRUCTURE_MODIFIER_TYPE.get(); + } +} diff --git a/src/main/java/net/minecraftforge/common/world/StructureModifier.java b/src/main/java/net/minecraftforge/common/world/StructureModifier.java new file mode 100644 index 0000000000..82266bd464 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/world/StructureModifier.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.world; + +import com.mojang.serialization.Codec; +import java.util.function.Function; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.resources.RegistryFileCodec; +import net.minecraft.resources.RegistryOps; +import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraftforge.common.world.ModifiableStructureInfo.StructureInfo; +import net.minecraftforge.registries.ForgeRegistries; + +/** + * JSON-serializable structure modifier. + * Requires a {@link Codec} to deserialize structure modifiers from structure modifier jsons. + *

+ * Structure modifier jsons have the following json format: + *

+ * {
+ *   "type": "yourmod:yourserializer", // Indicates a registered structure modifier serializer
+ *   // Additional fields can be specified here according to the codec
+ * }
+ * 
+ *

+ * Datapacks can also disable a structure modifier by overriding the json and using {@code "type": "forge:none"}.

+ */ +public interface StructureModifier +{ + /** + * Codec for (de)serializing structure modifiers inline. + * Mods can use this for data generation. + */ + Codec DIRECT_CODEC = ExtraCodecs.lazyInitializedCodec(() -> ForgeRegistries.STRUCTURE_MODIFIER_SERIALIZERS.get().getCodec()) + .dispatch(StructureModifier::codec, Function.identity()); + + /** + * Codec for referring to structure modifiers by id in other datapack registry files. + * Can only be used with {@link RegistryOps}. + */ + Codec> REFERENCE_CODEC = RegistryFileCodec.create(ForgeRegistries.Keys.STRUCTURE_MODIFIERS, DIRECT_CODEC); + + /** + * Codec for referring to structure modifiers by id, list of id, or tags. + * Can only be used with {@link RegistryOps}. + */ + Codec> LIST_CODEC = RegistryCodecs.homogeneousList(ForgeRegistries.Keys.STRUCTURE_MODIFIERS, DIRECT_CODEC); + + /** + * Modifies the information via the provided structure builder. + * Allows mob spawns and world-gen features to be added or removed, + * and climate and client effects to be modified. + * + * @param structure the named structure being modified (with original data readable). + * @param phase structure modification phase. Structure modifiers apply in each phase in order of the enum constants. + * @param builder mutable structure info builder. Apply changes to this. + */ + void modify(Holder structure, Phase phase, StructureInfo.Builder builder); + + /** + * @return the codec which serializes and deserializes this structure modifier + */ + Codec codec(); + + enum Phase + { + /** + * Catch-all for anything that needs to run before standard phases. + */ + BEFORE_EVERYTHING, + /** + * Additional mob spawns, etc. + */ + ADD, + /** + * Removal of mob spawns, etc. + */ + REMOVE, + /** + * Alteration of values. + */ + MODIFY, + /** + * Catch-all for anything that needs to run after standard phases. + */ + AFTER_EVERYTHING + } +} diff --git a/src/main/java/net/minecraftforge/common/world/StructureSettingsBuilder.java b/src/main/java/net/minecraftforge/common/world/StructureSettingsBuilder.java new file mode 100644 index 0000000000..18cb6bca5c --- /dev/null +++ b/src/main/java/net/minecraftforge/common/world/StructureSettingsBuilder.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.common.world; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import net.minecraft.core.HolderSet; +import net.minecraft.util.random.WeightedRandomList; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.structure.Structure.StructureSettings; +import net.minecraft.world.level.levelgen.structure.StructureSpawnOverride; +import net.minecraft.world.level.levelgen.structure.TerrainAdjustment; +import org.jetbrains.annotations.Nullable; + +public class StructureSettingsBuilder +{ + private HolderSet biomes; + private final Map spawnOverrides; + private GenerationStep.Decoration step; + private TerrainAdjustment terrainAdaptation; + + /** + * @param settings Existing StructureSettings. + * @return A new builder with a copy of that StructureSettings's values. + */ + public static StructureSettingsBuilder copyOf(StructureSettings settings) + { + return new StructureSettingsBuilder(settings.biomes(), settings.spawnOverrides(), settings.step(), settings.terrainAdaptation()); + } + + private StructureSettingsBuilder(HolderSet biomes, Map spawnOverrides, GenerationStep.Decoration step, TerrainAdjustment terrainAdaptation) + { + this.biomes = biomes; + this.spawnOverrides = spawnOverrides.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> StructureSpawnOverrideBuilder.copyOf(entry.getValue()))); + this.step = step; + this.terrainAdaptation = terrainAdaptation; + } + + /** + * @return A new StructureSettings with the finalized values. + */ + public StructureSettings build() + { + Map overrides = Collections.unmodifiableMap( + this.spawnOverrides.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().build())) + ); + return new StructureSettings(this.biomes, overrides, this.step, this.terrainAdaptation); + } + + public HolderSet getBiomes() + { + return biomes; + } + + public void setBiomes(HolderSet biomes) + { + this.biomes = biomes; + } + + /** + * Gets a mutable builder for the spawn overrides of a given mob category or {@code null} if no overrides are defined for that category. + * @param category Mob category + */ + @Nullable + public StructureSpawnOverrideBuilder getSpawnOverrides(MobCategory category) + { + return spawnOverrides.get(category); + } + + /** + * Gets or creates a mutable builder for the spawn overrides of a given mob category. If the override needed to be created it will default to piece bounding. + * @param category Mob category + */ + public StructureSpawnOverrideBuilder getOrAddSpawnOverrides(MobCategory category) + { + return spawnOverrides.computeIfAbsent(category, c -> new StructureSpawnOverrideBuilder(StructureSpawnOverride.BoundingBoxType.PIECE, Collections.emptyList())); + } + + /** + * Removes the spawn overrides for the given mob category. + * @param category Mob category + */ + public void removeSpawnOverrides(MobCategory category) + { + this.spawnOverrides.remove(category); + } + + /** + * Gets the world generation decoration step the structure spawns during. + */ + public GenerationStep.Decoration getDecorationStep() + { + return step; + } + + /** + * Sets the world generation decoration step the structure spawns during. + */ + public void setDecorationStep(GenerationStep.Decoration step) + { + this.step = step; + } + + /** + * Gets the way the structure adapts to the terrain during generation. + */ + public TerrainAdjustment getTerrainAdaptation() + { + return terrainAdaptation; + } + + /** + * Sets the way the structure adapts to the terrain during generation. + * @param terrainAdaptation New terrain adjustment + */ + public void setTerrainAdaptation(TerrainAdjustment terrainAdaptation) + { + this.terrainAdaptation = terrainAdaptation; + } + + public static class StructureSpawnOverrideBuilder + { + private StructureSpawnOverride.BoundingBoxType boundingBox; + private final List spawns; + private final List spawnsView; + + /** + * @param override Existing spawn override data. + * @return A new builder with a copy of that StructureSpawnOverride's values. + */ + public static StructureSpawnOverrideBuilder copyOf(StructureSpawnOverride override) + { + return new StructureSpawnOverrideBuilder(override.boundingBox(), override.spawns().unwrap()); + } + + private StructureSpawnOverrideBuilder(StructureSpawnOverride.BoundingBoxType boundingBox, List spawns) + { + this.boundingBox = boundingBox; + this.spawns = new ArrayList<>(spawns); + this.spawnsView = Collections.unmodifiableList(this.spawns); + } + + /** + * Gets the type of bounding box for this structures spawn overrides. + */ + public StructureSpawnOverride.BoundingBoxType getBoundingBox() + { + return boundingBox; + } + + /** + * Sets the way the structure checks for spawn overrides. Whether it is on a piece by piece basis or within the bounds of the overall structure. + */ + public void setBoundingBox(StructureSpawnOverride.BoundingBoxType boundingBox) + { + this.boundingBox = boundingBox; + } + + /** + * Unmodifiable view of the possible spawns. + */ + public List getSpawns() + { + return spawnsView; + } + + /** + * Adds a spawn to the overrides. + */ + public void addSpawn(MobSpawnSettings.SpawnerData spawn) + { + this.spawns.add(spawn); + } + + /** + * Removes a given spawn from the list of overrides. Use {@link #getSpawns()} to get instances of spawn data to remove. + */ + public void removeSpawn(MobSpawnSettings.SpawnerData spawn) + { + this.spawns.remove(spawn); + } + + /** + * @return A new StructureSpawnOverride with the finalized values. + */ + public StructureSpawnOverride build() + { + return new StructureSpawnOverride(boundingBox, WeightedRandomList.create(spawns)); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/common/world/StructureSpawnManager.java b/src/main/java/net/minecraftforge/common/world/StructureSpawnManager.java deleted file mode 100644 index f46ed32cda..0000000000 --- a/src/main/java/net/minecraftforge/common/world/StructureSpawnManager.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.minecraftforge.common.world; - -//TODO-PATCHING: Reengineer this with configured features in mind as those are what handle spawning right now.s -/** - * Class to help manage entity spawns inside of structures - * / -public class StructureSpawnManager -{ - private static Map, StructureSpawnInfo> structuresWithSpawns = Collections.emptyMap(); - - /** - * Gathers potential entity spawns for all the different registered structures. - * @apiNote Internal - * / - public static void gatherEntitySpawns() - { - //We use a linked hash map to ensure that we check the structures in an order that if there are multiple structures a position satisfies - // then we have the same behavior as vanilla as vanilla checks, swamp huts, pillager outposts, ocean monuments, and nether fortresses in - // that order. - Map, StructureSpawnInfo> structuresWithSpawns = new LinkedHashMap<>(); - gatherEntitySpawns(structuresWithSpawns, StructureFeature.SWAMP_HUT); - gatherEntitySpawns(structuresWithSpawns, StructureFeature.PILLAGER_OUTPOST); - gatherEntitySpawns(structuresWithSpawns, StructureFeature.OCEAN_MONUMENT); - gatherEntitySpawns(structuresWithSpawns, StructureFeature.NETHER_BRIDGE); - for (StructureFeature structure : ForgeRegistries.STRUCTURE_FEATURES.getValues()) - { - if (structure != StructureFeature.SWAMP_HUT && structure != StructureFeature.PILLAGER_OUTPOST && structure != StructureFeature.OCEAN_MONUMENT && - structure != StructureFeature.NETHER_BRIDGE) - { - //If we didn't already gather the spawns already to ensure we do vanilla ones already - // gather the spawns for this structure - gatherEntitySpawns(structuresWithSpawns, structure); - } - } - StructureSpawnManager.structuresWithSpawns = structuresWithSpawns; - } - - private static void gatherEntitySpawns(Map, StructureSpawnInfo> structuresWithSpawns, StructureFeature structure) - { - StructureSpawnListGatherEvent event = new StructureSpawnListGatherEvent(structure); - MinecraftForge.EVENT_BUS.post(event); - ImmutableMap.Builder> builder = ImmutableMap.builder(); - event.getEntitySpawns().forEach((classification, spawns) -> { - if (!spawns.isEmpty()) - builder.put(classification, WeightedRandomList.create(spawns)); - }); - ImmutableMap> entitySpawns = builder.build(); - if (!entitySpawns.isEmpty()) - structuresWithSpawns.put(structure, new StructureSpawnInfo(entitySpawns, event.isInsideOnly())); - } - - /** - * Looks up if a given position is within a structure and returns any entity spawns that structure has for the given classification, or null if - * none are found. - * @param structureManager Structure Manager, used to check if a position is within a structure. - * @param classification Entity classification - * @param pos Position to get entity spawns of - * / - @Nullable - public static WeightedRandomList getStructureSpawns(StructureFeatureManager structureManager, MobCategory classification, BlockPos pos) - { - for (Entry, StructureSpawnInfo> entry : structuresWithSpawns.entrySet()) - { - StructureFeature structure = entry.getKey(); - StructureSpawnInfo spawnInfo = entry.getValue(); - //Note: We check if the structure has spawns for a type first before looking at the world as it should be a cheaper check - if (spawnInfo.spawns.containsKey(classification)) - { - boolean valid = spawnInfo.insideOnly - ? structureManager.getStructureWithPieceAt(pos, structure).isValid() - : structureManager.getStructureAt(pos, structure).isValid(); - if (valid) - return spawnInfo.spawns.get(classification); - } - } - return null; - } - - /** - * Gets the entity spawn lists for entities of a given classification for a given structure. - * @param structure The Structure - * @param classification The classification to lookup - * / - public static WeightedRandomList getSpawnList(StructureFeature structure, MobCategory classification) - { - StructureSpawnInfo info = structuresWithSpawns.get(structure); - if (info != null) - return info.spawns.getOrDefault(classification, WeightedRandomList.create()); - return WeightedRandomList.create(); - } - - /** - * Helper class to keep track of spawns and if the spawns should be restricted to inside the structure pieces. - * / - private static class StructureSpawnInfo - { - private final Map> spawns; - private final boolean insideOnly; - - private StructureSpawnInfo(ImmutableMap> spawns, boolean insideOnly) - { - this.spawns = spawns; - this.insideOnly = insideOnly; - } - } -} -*/ \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/event/world/StructureSpawnListGatherEvent.java b/src/main/java/net/minecraftforge/event/world/StructureSpawnListGatherEvent.java deleted file mode 100644 index 067efe94bf..0000000000 --- a/src/main/java/net/minecraftforge/event/world/StructureSpawnListGatherEvent.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.minecraftforge.event.world; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import net.minecraft.world.entity.MobCategory; -import net.minecraft.world.level.biome.MobSpawnSettings; -import net.minecraft.world.level.levelgen.structure.Structure; -import net.minecraftforge.eventbus.api.Event; -import net.minecraftforge.eventbus.api.EventPriority; - -/** - * This event fires when a Structure is gathering what mobs/creatures can spawn in it. - * - * In order to maintain the most compatibility possible with other mods' modifications to a structure, - * the event should be assigned a {@link net.minecraftforge.eventbus.api.EventPriority} as follows: - * - * - Additions : {@link EventPriority#HIGH} - * - Removals : {@link EventPriority#NORMAL} - * - Any other modification : {@link EventPriority#LOW} - * - * Be aware that another mod could have done an operation beforehand, so an expected value out of a vanilla structure might not - * always be the same, depending on other mods. - */ -public class StructureSpawnListGatherEvent extends Event -{ - - private final Structure structure; - private final Map> entitySpawns = new HashMap<>(); - private final Map> entitySpawnsUnmodifiableLists = new HashMap<>(); - private final Map> entitySpawnsUnmodifiable = Collections.unmodifiableMap(entitySpawnsUnmodifiableLists); - private boolean insideOnly; - - public StructureSpawnListGatherEvent(Structure structure) - { - this.structure = structure; - this.insideOnly = false; - /*this.insideOnly = this.structure.getDefaultRestrictsSpawnsToInside(); - - for (MobCategory cat : MobCategory.values()) { - List lst = this.structure.getDefaultSpawnList(cat); - if (!lst.isEmpty()) - addEntitySpawns(cat, lst); - }*/ - // TODO-PATCHING: follow how StructureSpawnManager is reengineered - } - - /** - * @return Structure to add or remove spawns to/from. - */ - public Structure getStructure() - { - return structure; - } - - /** - * Change if entity spawn location checks are done against the entire bounds of the structure or only the inside the pieces of the structure. - * @param insideOnly {@code true} to restrict the spawn checks to inside the pieces of the structure, {@code false} to allow spawns outside - */ - public void setInsideOnly(boolean insideOnly) - { - this.insideOnly = insideOnly; - } - - /** - * Checks if spawns for the structure are restricted to being inside the individual pieces of the structure. - */ - public boolean isInsideOnly() - { - return insideOnly; - } - - /** - * Gets an unmodifiable view of the the list representing the entity spawns for the given classification. - * @param classification Entity Classification - * @return The list of spawns for the given classification. - */ - public List getEntitySpawns(MobCategory classification) - { - return this.entitySpawnsUnmodifiableLists.getOrDefault(classification, Collections.emptyList()); - } - - /** - * Gets the internal spawn list for a given entity classification, or adds one if needed. (This includes adding it to the unmodifiable view) - */ - private List getOrCreateEntitySpawns(MobCategory classification) - { - return this.entitySpawns.computeIfAbsent(classification, c -> { - List spawners = new ArrayList<>(); - //If the classification isn't in entitySpawns yet, also add an unmodifiable view of the list to - // the unmodifiable list spawn map - this.entitySpawnsUnmodifiableLists.put(c, Collections.unmodifiableList(spawners)); - return spawners; - }); - } - - /** - * Adds a spawn to the list of spawns for the given classification. - * @param classification Entity Classification - * @param spawner Spawner - */ - public void addEntitySpawn(MobCategory classification, MobSpawnSettings.SpawnerData spawner) - { - getOrCreateEntitySpawns(classification).add(spawner); - } - - /** - * Adds spawns to the list of spawns for the given classification. - * @param classification Entity Classification - * @param spawners Spawners to add - */ - public void addEntitySpawns(MobCategory classification, List spawners) - { - getOrCreateEntitySpawns(classification).addAll(spawners); - } - - /** - * Removes a spawn from the list of spawns for the given classification. - * @param classification Entity Classification - * @param spawner Spawner - */ - public void removeEntitySpawn(MobCategory classification, MobSpawnSettings.SpawnerData spawner) - { - if (this.entitySpawns.containsKey(classification)) - this.entitySpawns.get(classification).remove(spawner); - } - - /** - * Gets an unmodifiable view of the map of spawns based on entity classification that is used to fill in the various spawn lists for the structure. - */ - public Map> getEntitySpawns() - { - return entitySpawnsUnmodifiable; - } -} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/registries/ForgeDeferredRegistriesSetup.java b/src/main/java/net/minecraftforge/registries/ForgeDeferredRegistriesSetup.java index 39732dfaed..b0e5e750ae 100644 --- a/src/main/java/net/minecraftforge/registries/ForgeDeferredRegistriesSetup.java +++ b/src/main/java/net/minecraftforge/registries/ForgeDeferredRegistriesSetup.java @@ -24,6 +24,8 @@ public class ForgeDeferredRegistriesSetup ForgeRegistries.DEFERRED_WORLD_TYPES.register(modEventBus); ForgeRegistries.DEFERRED_BIOME_MODIFIER_SERIALIZERS.register(modEventBus); ForgeRegistries.DEFERRED_BIOME_MODIFIERS.register(modEventBus); + ForgeRegistries.DEFERRED_STRUCTURE_MODIFIER_SERIALIZERS.register(modEventBus); + ForgeRegistries.DEFERRED_STRUCTURE_MODIFIERS.register(modEventBus); setup = true; } diff --git a/src/main/java/net/minecraftforge/registries/ForgeRegistries.java b/src/main/java/net/minecraftforge/registries/ForgeRegistries.java index 2d82acdde9..1dd4ed9e70 100644 --- a/src/main/java/net/minecraftforge/registries/ForgeRegistries.java +++ b/src/main/java/net/minecraftforge/registries/ForgeRegistries.java @@ -46,6 +46,7 @@ import net.minecraftforge.common.world.BiomeModifier; import net.minecraftforge.common.world.ForgeWorldPreset; import java.util.function.Supplier; +import net.minecraftforge.common.world.StructureModifier; /** * A class that exposes static references to all vanilla and Forge registries. @@ -102,10 +103,14 @@ public class ForgeRegistries public static final Supplier> WORLD_TYPES = DEFERRED_WORLD_TYPES.makeRegistry(GameData::getWorldTypesRegistryBuilder); static final DeferredRegister> DEFERRED_BIOME_MODIFIER_SERIALIZERS = DeferredRegister.create(Keys.BIOME_MODIFIER_SERIALIZERS, Keys.BIOME_MODIFIER_SERIALIZERS.location().getNamespace()); public static final Supplier>> BIOME_MODIFIER_SERIALIZERS = DEFERRED_BIOME_MODIFIER_SERIALIZERS.makeRegistry(() -> new RegistryBuilder>().disableSaving().disableSync()); + static final DeferredRegister> DEFERRED_STRUCTURE_MODIFIER_SERIALIZERS = DeferredRegister.create(Keys.STRUCTURE_MODIFIER_SERIALIZERS, Keys.STRUCTURE_MODIFIER_SERIALIZERS.location().getNamespace()); + public static final Supplier>> STRUCTURE_MODIFIER_SERIALIZERS = DEFERRED_STRUCTURE_MODIFIER_SERIALIZERS.makeRegistry(() -> new RegistryBuilder>().disableSaving().disableSync()); // Custom forge datapack registries static final DeferredRegister DEFERRED_BIOME_MODIFIERS = DeferredRegister.create(Keys.BIOME_MODIFIERS, "forge"); public static final Supplier> BIOME_MODIFIERS_BUILTIN = DEFERRED_BIOME_MODIFIERS.makeRegistry(() -> new RegistryBuilder().disableSaving().dataPackRegistry(BiomeModifier.DIRECT_CODEC)); + static final DeferredRegister DEFERRED_STRUCTURE_MODIFIERS = DeferredRegister.create(Keys.STRUCTURE_MODIFIERS, "forge"); + public static final Supplier> STRUCTURE_MODIFIERS_BUILTIN = DEFERRED_STRUCTURE_MODIFIERS.makeRegistry(() -> new RegistryBuilder().disableSaving().dataPackRegistry(StructureModifier.DIRECT_CODEC)); public static final class Keys { //Vanilla @@ -146,9 +151,11 @@ public class ForgeRegistries public static final ResourceKey>> LOOT_MODIFIER_SERIALIZERS = key("forge:loot_modifier_serializers"); public static final ResourceKey> WORLD_TYPES = key("forge:world_types"); public static final ResourceKey>> BIOME_MODIFIER_SERIALIZERS = key("forge:biome_modifier_serializers"); + public static final ResourceKey>> STRUCTURE_MODIFIER_SERIALIZERS = key("forge:structure_modifier_serializers"); // Forge Dynamic public static final ResourceKey> BIOME_MODIFIERS = key("forge:biome_modifier"); + public static final ResourceKey> STRUCTURE_MODIFIERS = key("forge:structure_modifier"); private static ResourceKey> key(String name) { diff --git a/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java b/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java index 47dbb9795f..e91a9c6033 100644 --- a/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java +++ b/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java @@ -26,6 +26,7 @@ import net.minecraft.network.chat.MutableComponent; import net.minecraft.server.packs.repository.PackSource; import net.minecraft.server.packs.repository.RepositorySource; import net.minecraft.world.level.storage.LevelResource; +import net.minecraftforge.common.world.StructureModifier; import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.common.util.LogicalSidedProvider; import net.minecraftforge.common.world.BiomeModifier; @@ -38,6 +39,7 @@ import net.minecraftforge.network.ServerStatusPing; import net.minecraftforge.network.NetworkHooks; import net.minecraftforge.network.NetworkRegistry; import net.minecraftforge.forgespi.locating.IModFile; +import net.minecraftforge.registries.ForgeRegistries.Keys; import net.minecraftforge.resource.PathResourcePack; import net.minecraftforge.server.permission.PermissionAPI; import org.apache.logging.log4j.LogManager; @@ -89,7 +91,7 @@ public class ServerLifecycleHooks // on the dedi server we need to force the stuff to setup properly LogicalSidedProvider.setServer(()->server); ConfigTracker.INSTANCE.loadConfigs(ModConfig.Type.SERVER, getServerConfigPath(server)); - runBiomeModifiers(server); + runModifiers(server); return !MinecraftForge.EVENT_BUS.post(new ServerAboutToStartEvent(server)); } @@ -211,20 +213,29 @@ public class ServerLifecycleHooks } } - private static void runBiomeModifiers(final MinecraftServer server) + private static void runModifiers(final MinecraftServer server) { final RegistryAccess registries = server.registryAccess(); - + // The order of holders() is the order modifiers were loaded in. - final List modifiers = registries.registryOrThrow(ForgeRegistries.Keys.BIOME_MODIFIERS) + final List biomeModifiers = registries.registryOrThrow(ForgeRegistries.Keys.BIOME_MODIFIERS) .holders() .map(Holder::value) .toList(); + final List structureModifiers = registries.registryOrThrow(Keys.STRUCTURE_MODIFIERS) + .holders() + .map(Holder::value) + .toList(); // Apply sorted biome modifiers to each biome. registries.registryOrThrow(Registry.BIOME_REGISTRY).holders().forEach(biomeHolder -> { - biomeHolder.value().modifiableBiomeInfo().applyBiomeModifiers(biomeHolder, modifiers); + biomeHolder.value().modifiableBiomeInfo().applyBiomeModifiers(biomeHolder, biomeModifiers); + }); + // Apply sorted structure modifiers to each structure. + registries.registryOrThrow(Registry.STRUCTURE_REGISTRY).holders().forEach(structureHolder -> + { + structureHolder.value().modifiableStructureInfo().applyStructureModifiers(structureHolder, structureModifiers); }); } } diff --git a/src/main/resources/coremods/field_to_method.js b/src/main/resources/coremods/field_to_method.js index 6c0082806d..17cd528184 100644 --- a/src/main/resources/coremods/field_to_method.js +++ b/src/main/resources/coremods/field_to_method.js @@ -13,6 +13,16 @@ function initializeCoreMod() { return classNode; } }, + 'structure': { + 'target': { + 'type': 'CLASS', + 'name': 'net.minecraft.world.level.levelgen.structure.Structure' + }, + 'transformer': function(classNode) { + ASMAPI.redirectFieldToMethod(classNode, ASMAPI.mapField('f_226555_'), 'getModifiedStructureSettings') + return classNode; + } + }, 'potion': { 'target': { 'type': 'CLASS', diff --git a/src/test/java/net/minecraftforge/debug/world/BiomeModifierTest.java b/src/test/java/net/minecraftforge/debug/world/BiomeModifierTest.java index 425fd2e4fe..81a77eab51 100644 --- a/src/test/java/net/minecraftforge/debug/world/BiomeModifierTest.java +++ b/src/test/java/net/minecraftforge/debug/world/BiomeModifierTest.java @@ -5,16 +5,12 @@ package net.minecraftforge.debug.world; -import java.io.IOException; -import java.nio.file.Path; import java.util.List; +import java.util.Map; -import net.minecraft.data.CachedOutput; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.mojang.serialization.Codec; import com.mojang.serialization.DataResult; @@ -31,7 +27,6 @@ import net.minecraft.data.worldgen.features.NetherFeatures; import net.minecraft.resources.RegistryOps; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.PackType; import net.minecraft.tags.BiomeTags; import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.biome.Biome; @@ -42,6 +37,8 @@ import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; import net.minecraft.world.level.levelgen.placement.BiomeFilter; import net.minecraft.world.level.levelgen.placement.CountOnEveryLayerPlacement; import net.minecraft.world.level.levelgen.placement.PlacedFeature; +import net.minecraftforge.common.data.ExistingFileHelper; +import net.minecraftforge.common.data.JsonCodecProvider; import net.minecraftforge.common.world.BiomeGenerationSettingsBuilder; import net.minecraftforge.common.world.BiomeModifier; import net.minecraftforge.common.world.ModifiableBiomeInfo.BiomeInfo.Builder; @@ -72,11 +69,13 @@ public class BiomeModifierTest public static final boolean ENABLED = true; public static final String BASALT_PILLARS = "large_basalt_columns"; public static final ResourceLocation BASALT_PILLARS_RL = new ResourceLocation(MODID, BASALT_PILLARS); + public static final ResourceKey BASALT_PILLARS_KEY = ResourceKey.create(Registry.PLACED_FEATURE_REGISTRY, BASALT_PILLARS_RL); public static final String TEST = "test"; public static final ResourceLocation ADD_FEATURES_TO_BIOMES_RL = new ResourceLocation(MODID, TEST); public static final String MODIFY_BADLANDS = "modify_badlands"; + public static final ResourceLocation MODIFY_BADLANDS_RL = new ResourceLocation(MODID, MODIFY_BADLANDS); + public static final ResourceKey MODIFY_BADLANDS_KEY = ResourceKey.create(ForgeRegistries.Keys.BIOME_MODIFIERS, MODIFY_BADLANDS_RL); - @SuppressWarnings("unchecked") public BiomeModifierTest() { if (!ENABLED) @@ -89,86 +88,49 @@ public class BiomeModifierTest serializers.register(modBus); serializers.register(TEST, TestModifier::makeCodec); - // Biome modifiers don't need to be registered when defined in json, but they do need to be registered if we are to datagenerate the jsons. - // We'll also datagenerate a placedfeature json (using our own placedfeature avoids feature cycle problems when we add it to biomes). - final DeferredRegister placedFeatures = DeferredRegister.create(Registry.PLACED_FEATURE_REGISTRY, MODID); - placedFeatures.register(modBus); - placedFeatures.register(BASALT_PILLARS, - () -> new PlacedFeature((Holder>) (Holder>)NetherFeatures.LARGE_BASALT_COLUMNS, - List.of(CountOnEveryLayerPlacement.of(1), BiomeFilter.biome()))); - modBus.addListener(this::onGatherData); } private void onGatherData(GatherDataEvent event) { // Example of how to datagen datapack registry objects. - // Datapack registry objects referred to by other datapack registry objects must be registered first. - DataGenerator generator = event.getGenerator(); - final Path outputFolder = generator.getOutputFolder(); - final RegistryOps ops = RegistryOps.create(JsonOps.INSTANCE, RegistryAccess.BUILTIN.get()); - final Gson gson = new GsonBuilder().setPrettyPrinting().create(); - final String directory = PackType.SERVER_DATA.getDirectory(); - Registry placedFeatures = ops.registry(Registry.PLACED_FEATURE_REGISTRY).get(); + final DataGenerator generator = event.getGenerator(); + final ExistingFileHelper existingFileHelper = event.getExistingFileHelper(); + + // Any reference holders our objects have must come from this same RegistryAccess instance, + // or encoding our objects will fail. + final RegistryAccess registries = RegistryAccess.builtinCopy(); + final RegistryOps ops = RegistryOps.create(JsonOps.INSTANCE, registries); + final Registry> configuredFeatures = ops.registry(Registry.CONFIGURED_FEATURE_REGISTRY).get(); + final Registry placedFeatures = ops.registry(Registry.PLACED_FEATURE_REGISTRY).get(); - // prepare to datagenerate our placed feature - final String featurePathString = String.join("/", directory, MODID, Registry.PLACED_FEATURE_REGISTRY.location().getPath(), BASALT_PILLARS + ".json"); - final Path featurePath = outputFolder.resolve(featurePathString); - final PlacedFeature feature = placedFeatures.get(BASALT_PILLARS_RL); + // Create our PlacedFeature so we can generate it. + final ResourceKey> configuredFeatureKey = NetherFeatures.LARGE_BASALT_COLUMNS.unwrapKey().get().cast(Registry.CONFIGURED_FEATURE_REGISTRY).get(); + final Holder> configuredFeatureHolder = configuredFeatures.getOrCreateHolderOrThrow(configuredFeatureKey); + final PlacedFeature placedFeature = new PlacedFeature(configuredFeatureHolder, List.of(CountOnEveryLayerPlacement.of(1), BiomeFilter.biome())); - // prepare to datagenerate our biome modifier - final ResourceLocation biomeModifiersRegistryID = ForgeRegistries.Keys.BIOME_MODIFIERS.location(); - final String biomeModifierPathString = String.join("/", directory, MODID, biomeModifiersRegistryID.getNamespace(), biomeModifiersRegistryID.getPath(), MODIFY_BADLANDS + ".json"); - final Path biomeModifierPath = outputFolder.resolve(biomeModifierPathString); + // Create our BiomeModifier so we can generate it. + final HolderSet placedFeatureHolderSet = + HolderSet.direct(placedFeatures.getOrCreateHolderOrThrow(BASALT_PILLARS_KEY)); final BiomeModifier biomeModifier = new TestModifier( new HolderSet.Named<>(ops.registry(Registry.BIOME_REGISTRY).get(), BiomeTags.IS_BADLANDS), Decoration.TOP_LAYER_MODIFICATION, - HolderSet.direct(placedFeatures.getOrCreateHolderOrThrow(ResourceKey.create(Registry.PLACED_FEATURE_REGISTRY, BASALT_PILLARS_RL))), + placedFeatureHolderSet, new SpawnerData(EntityType.MAGMA_CUBE, 100, 1, 4), Precipitation.SNOW, 0xFF0000 ); - generator.addProvider(event.includeServer(), new DataProvider() - { - @Override - public void run(final CachedOutput cache) throws IOException - { - PlacedFeature.DIRECT_CODEC.encodeStart(ops, feature) - .resultOrPartial(msg -> LOGGER.error("Failed to encode {}: {}", featurePathString, msg)) // Log error on encode failure. - .ifPresent(json -> // Output to file on encode success. - { - try - { - DataProvider.saveStable(cache, json, featurePath); - } - catch (IOException e) // The throws can't deal with this exception, because we're inside the ifPresent. - { - LOGGER.error("Failed to save " + featurePathString, e); - } - }); - - BiomeModifier.DIRECT_CODEC.encodeStart(ops, biomeModifier) - .resultOrPartial(msg -> LOGGER.error("Failed to encode {}: {}", biomeModifierPathString, msg)) // Log error on encode failure. - .ifPresent(json -> // Output to file on encode success. - { - try - { - DataProvider.saveStable(cache, json, biomeModifierPath); - } - catch (IOException e) // The throws can't deal with this exception, because we're inside the ifPresent. - { - LOGGER.error("Failed to save " + biomeModifierPathString, e); - } - }); - } - - @Override - public String getName() - { - return MODID + " data provider"; - } - }); + // Create and add our data providers. + final DataProvider placedFeatureProvider = + JsonCodecProvider.forDatapackRegistry(generator, existingFileHelper, MODID, ops, Registry.PLACED_FEATURE_REGISTRY, + Map.of(BASALT_PILLARS_RL, placedFeature)); + generator.addProvider(event.includeServer(), placedFeatureProvider); + + final DataProvider biomeModifierProvider = + JsonCodecProvider.forDatapackRegistry(generator, existingFileHelper, MODID, ops, ForgeRegistries.Keys.BIOME_MODIFIERS, + Map.of(MODIFY_BADLANDS_RL, biomeModifier)); + generator.addProvider(event.includeServer(), biomeModifierProvider); } public record TestModifier(HolderSet biomes, Decoration generationStage, HolderSet features, SpawnerData spawn, Precipitation precipitation, int waterColor) diff --git a/src/test/java/net/minecraftforge/debug/world/StructureModifierTest.java b/src/test/java/net/minecraftforge/debug/world/StructureModifierTest.java new file mode 100644 index 0000000000..52286f4f3d --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/world/StructureModifierTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.world; + +import com.google.gson.JsonElement; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.Map; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.RegistryCodecs; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.DataProvider; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.biome.MobSpawnSettings; +import net.minecraft.world.level.levelgen.structure.BuiltinStructures; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraftforge.common.data.JsonCodecProvider; +import net.minecraftforge.common.world.ModifiableStructureInfo.StructureInfo.Builder; +import net.minecraftforge.common.world.StructureModifier; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.forge.event.lifecycle.GatherDataEvent; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.ForgeRegistries.Keys; +import net.minecraftforge.registries.RegistryObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + *

This tests the requirements of structure modifier jsons::

+ *
    + *
  • A structure modifier json is created via datagen.
  • + *
  • The structure modifier adds a mob spawn of the given category to the given structures.
  • + *
+ *

If the structure modifier is applied correctly, then strongholds should have wither skeletons spawn in them.

+ */ +@Mod(StructureModifierTest.MODID) +public class StructureModifierTest +{ + private static final Codec> STRUCTURE_LIST_CODEC = RegistryCodecs.homogeneousList(Registry.STRUCTURE_REGISTRY, Structure.DIRECT_CODEC); + public static final Logger LOGGER = LogManager.getLogger(); + public static final String MODID = "structure_modifiers_test"; + public static final boolean ENABLED = true; + public static final String TEST = "test"; + public static final ResourceLocation ADD_SPAWNS_TO_STRUCTURE_RL = new ResourceLocation(MODID, TEST); + public static final String MODIFY_STRONGHOLD = "modify_stronghold"; + public static final ResourceLocation MODIFY_STRONGHOLD_RL = new ResourceLocation(MODID, MODIFY_STRONGHOLD); + + public StructureModifierTest() + { + if (!ENABLED) + return; + + IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus(); + + // Serializer types can be registered via deferred register. + final DeferredRegister> serializers = DeferredRegister.create(Keys.STRUCTURE_MODIFIER_SERIALIZERS, MODID); + serializers.register(modBus); + serializers.register(TEST, TestModifier::makeCodec); + + // Structure modifiers don't need to be registered when defined in json, but they do need to be registered if we are to datagenerate the jsons. + + modBus.addListener(this::onGatherData); + } + + private void onGatherData(GatherDataEvent event) + { + // Example of how to datagen datapack registry objects. + DataGenerator generator = event.getGenerator(); + final RegistryOps ops = RegistryOps.create(JsonOps.INSTANCE, RegistryAccess.BUILTIN.get()); + + // prepare to datagenerate our structure modifier + final StructureModifier structureModifier = new TestModifier( + HolderSet.direct(ops.registry(Registry.STRUCTURE_REGISTRY).get().getHolder(BuiltinStructures.STRONGHOLD).orElseThrow()), + MobCategory.MONSTER, + new MobSpawnSettings.SpawnerData(EntityType.WITHER_SKELETON, 100, 5, 15) + ); + + DataProvider structureModifierProvider = + JsonCodecProvider.forDatapackRegistry(generator, event.getExistingFileHelper(), MODID, ops, ForgeRegistries.Keys.STRUCTURE_MODIFIERS, + Map.of(MODIFY_STRONGHOLD_RL, structureModifier)); + generator.addProvider(event.includeServer(), structureModifierProvider); + } + + public record TestModifier(HolderSet structures, MobCategory category, MobSpawnSettings.SpawnerData spawn) + implements StructureModifier + { + private static final RegistryObject> SERIALIZER = RegistryObject.create(ADD_SPAWNS_TO_STRUCTURE_RL, ForgeRegistries.Keys.STRUCTURE_MODIFIER_SERIALIZERS, MODID); + + @Override + public void modify(Holder structure, Phase phase, Builder builder) + { + if (phase == Phase.ADD && this.structures.contains(structure)) + { + builder.getStructureSettings() + .getOrAddSpawnOverrides(category) + .addSpawn(spawn); + } + } + + @Override + public Codec codec() + { + return SERIALIZER.get(); + } + + private static Codec makeCodec() + { + return RecordCodecBuilder.create(builder -> builder.group( + STRUCTURE_LIST_CODEC.fieldOf("structures").forGetter(TestModifier::structures), + MobCategory.CODEC.fieldOf("category").forGetter(TestModifier::category), + MobSpawnSettings.SpawnerData.CODEC.fieldOf("spawn").forGetter(TestModifier::spawn) + ).apply(builder, TestModifier::new)); + } + } +} diff --git a/src/test/java/net/minecraftforge/debug/world/StructureSpawnListGatherEventTest.java b/src/test/java/net/minecraftforge/debug/world/StructureSpawnListGatherEventTest.java deleted file mode 100644 index 8553b66dae..0000000000 --- a/src/test/java/net/minecraftforge/debug/world/StructureSpawnListGatherEventTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.minecraftforge.debug.world; - -import net.minecraft.data.worldgen.Structures; -import net.minecraft.tags.StructureTags; -import net.minecraft.world.entity.MobCategory; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.level.biome.MobSpawnSettings; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.world.StructureSpawnListGatherEvent; -import net.minecraftforge.eventbus.api.EventPriority; -import net.minecraftforge.fml.common.Mod; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -@Mod(StructureSpawnListGatherEventTest.MODID) -public class StructureSpawnListGatherEventTest -{ - - public static final String MODID = "structure_spawn_list_event_test"; - private static final Logger LOGGER = LogManager.getLogger(MODID); - - public StructureSpawnListGatherEventTest() - { - MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGH, this::onStructureSpawnListGather); - } - - private void onStructureSpawnListGather(StructureSpawnListGatherEvent event) - { - // TODO-PATCHING: we likely want to get registry access here, so we can resolve tags instead of hardcoding structure objects - if (Structures.STRONGHOLD.get() == event.getStructure()) - { - event.addEntitySpawn(MobCategory.MONSTER, new MobSpawnSettings.SpawnerData(EntityType.WITHER_SKELETON, 100, 5, 15)); - LOGGER.info("Adding wither skeleton spawns to strong holds"); - } - else if (Structures.SHIPWRECK.get() == event.getStructure()) - { - event.setInsideOnly(false); - event.addEntitySpawn(MobCategory.MONSTER, new MobSpawnSettings.SpawnerData(EntityType.GUARDIAN, 100, 5, 15)); - LOGGER.info("Adding guardians spawns to shipwrecks"); - } - } -} diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml index 0c31baa07c..ae292bbf3d 100644 --- a/src/test/resources/META-INF/mods.toml +++ b/src/test/resources/META-INF/mods.toml @@ -8,5 +8,7 @@ license="LGPL v2.1" modId="client_command_test" [[mods]] modId="biome_modifiers_test" +[[mods]] + modId="structure_modifiers_test" # ADD ABOVE THIS LINE