From d39f677cf805a15ed665ac1b1ee7adc5296808a2 Mon Sep 17 00:00:00 2001 From: Thom van den Akker Date: Wed, 1 Apr 2026 19:30:30 +0200 Subject: [PATCH 1/5] Schematic index - part 1 --- .../com/ldtteam/structurize/Structurize.java | 2 + .../ldtteam/structurize/api/Registries.java | 13 + .../structurize/commands/EntryPoint.java | 6 +- .../commands/PackIndexCommand.java | 113 +++++++ .../structurize/event/EventSubscriber.java | 21 ++ .../event/LifecycleSubscriber.java | 12 + .../structurize/index/PackManager.java | 186 +++++++++++ .../structurize/index/models/Pack.java | 103 ++++++ .../index/models/PackSchematic.java | 104 ++++++ .../index/packtypes/PackTypesRegistry.java | 28 ++ .../index/packtypes/models/PackType.java | 67 ++++ .../packtypes/models/PackTypeRequirement.java | 137 ++++++++ .../models/PackTypeSchematicRequirement.java | 305 ++++++++++++++++++ .../PackTypeSchematicRequirementSeverity.java | 30 ++ .../structurize/items/ItemPackIndexDebug.java | 68 ++++ .../ldtteam/structurize/items/ModItems.java | 5 + .../messages/SyncPackManagerMessage.java | 43 +++ 17 files changed, 1241 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/ldtteam/structurize/api/Registries.java create mode 100644 src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java create mode 100644 src/main/java/com/ldtteam/structurize/index/PackManager.java create mode 100644 src/main/java/com/ldtteam/structurize/index/models/Pack.java create mode 100644 src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java create mode 100644 src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java create mode 100644 src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java create mode 100644 src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java create mode 100644 src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java create mode 100644 src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirementSeverity.java create mode 100644 src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java create mode 100644 src/main/java/com/ldtteam/structurize/network/messages/SyncPackManagerMessage.java diff --git a/src/main/java/com/ldtteam/structurize/Structurize.java b/src/main/java/com/ldtteam/structurize/Structurize.java index e88cddd40..49424bcdf 100644 --- a/src/main/java/com/ldtteam/structurize/Structurize.java +++ b/src/main/java/com/ldtteam/structurize/Structurize.java @@ -14,6 +14,7 @@ import com.ldtteam.structurize.event.ClientLifecycleSubscriber; import com.ldtteam.structurize.event.EventSubscriber; import com.ldtteam.structurize.event.LifecycleSubscriber; +import com.ldtteam.structurize.index.packtypes.PackTypesRegistry; import com.ldtteam.structurize.items.ModItemGroups; import com.ldtteam.structurize.items.ModItems; import com.ldtteam.structurize.blockentities.ModBlockEntities; @@ -58,6 +59,7 @@ public Structurize(final FMLModContainer modContainer, final Dist dist) ModItems.ITEMS.register(modBus); ModBlockEntities.BLOCK_ENTITIES.register(modBus); ModItemGroups.TAB_REG.register(modBus); + PackTypesRegistry.DEFERRED_REGISTER.register(modBus); modBus.register(LifecycleSubscriber.class); forgeBus.register(EventSubscriber.class); diff --git a/src/main/java/com/ldtteam/structurize/api/Registries.java b/src/main/java/com/ldtteam/structurize/api/Registries.java new file mode 100644 index 000000000..c1a7397fc --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/api/Registries.java @@ -0,0 +1,13 @@ +package com.ldtteam.structurize.api; + +import com.ldtteam.structurize.api.constants.Constants; +import com.ldtteam.structurize.index.packtypes.models.PackType; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; + +public class Registries +{ + public static final ResourceKey> SCHEMATIC_INDEX_PACK_TYPES = + ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, ("schematic_index_pack_types"))); +} diff --git a/src/main/java/com/ldtteam/structurize/commands/EntryPoint.java b/src/main/java/com/ldtteam/structurize/commands/EntryPoint.java index bdc56490e..49c4a8013 100644 --- a/src/main/java/com/ldtteam/structurize/commands/EntryPoint.java +++ b/src/main/java/com/ldtteam/structurize/commands/EntryPoint.java @@ -44,9 +44,11 @@ public static void register(final CommandDispatcher dispatch .addNode(UpdateSchematicsCommand::build, () -> CommandSelection.INTEGRATED) .addNode(ScanCommand::build, AbstractCommand::getEnvironmentType) .addNode(PasteCommand::build, AbstractCommand::getEnvironmentType) - .addNode(PasteFolderCommand::build, AbstractCommand::getEnvironmentType) + .addNode(PasteFolderCommand::build, AbstractCommand::getEnvironmentType) .addNode(UpgradeCommand.ToDO::build, () -> CommandSelection.ALL) - .addNode(UpdateSchematicPackCommand::build, () -> CommandSelection.ALL); + .addNode(UpdateSchematicPackCommand::build, () -> CommandSelection.ALL) + // TODO: Remove before publication — debug command only + .addNode(PackIndexCommand::build, PackIndexCommand::getEnvironmentType); structurizeRoot.register(dispatcher, environment); } diff --git a/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java b/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java new file mode 100644 index 000000000..1c51f60e1 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java @@ -0,0 +1,113 @@ +// TODO: Remove before publication — debug command only +package com.ldtteam.structurize.commands; + +import com.ldtteam.structurize.index.PackManager; +import com.ldtteam.structurize.index.models.PackSchematic; +import com.ldtteam.structurize.index.packtypes.PackTypesRegistry; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands.CommandSelection; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class PackIndexCommand extends AbstractCommand +{ + private static final Logger LOGGER = LogManager.getLogger(); + + public static final String NAME = "packindex"; + + protected static CommandSelection getEnvironmentType() + { + return CommandSelection.ALL; + } + + protected static LiteralArgumentBuilder build() + { + return newLiteral(NAME) + .then(newLiteral("add") + .then(newLiteral("pack") + .then(newArgument("packName", StringArgumentType.word()) + .executes(ctx -> addPack(ctx.getSource(), StringArgumentType.getString(ctx, "packName"))))) + .then(newLiteral("schematic") + .then(newArgument("packName", StringArgumentType.word()) + .then(newArgument("schematicName", StringArgumentType.word()) + .then(newArgument("path", StringArgumentType.word()) + .then(newArgument("level", IntegerArgumentType.integer(0)) + .executes(ctx -> addSchematic( + ctx.getSource(), + StringArgumentType.getString(ctx, "packName"), + StringArgumentType.getString(ctx, "schematicName"), + StringArgumentType.getString(ctx, "path"), + IntegerArgumentType.getInteger(ctx, "level"))))))))); + } + + private static ServerLevel getOverworld(final CommandSourceStack source) + { + return source.getServer().getLevel(Level.OVERWORLD); + } + + private static int addPack(final CommandSourceStack source, final String packName) + { + final ServerLevel overworld = getOverworld(source); + if (overworld == null) + { + return 0; + } + + final PackManager manager = overworld.getDataStorage().computeIfAbsent(PackManager.FACTORY, PackManager.DATA_NAME); + final String newId = manager.addPack(packName, PackTypesRegistry.DEFAULT_PACK_TYPE); + + if (newId != null) + { + LOGGER.info("[PackIndex] Added pack '{}'", packName); + source.sendSuccess(() -> net.minecraft.network.chat.Component.literal("Added pack: " + packName), false); + } + else + { + LOGGER.info("[PackIndex] Pack not added: '{}', duplicate name", packName); + source.sendSuccess(() -> net.minecraft.network.chat.Component.literal("Couldn't add pack: " + packName + ", duplicate name"), false); + } + return 1; + } + + private static int addSchematic( + final CommandSourceStack source, + final String packName, + final String schematicName, + final String path, + final int level) + { + final ServerLevel overworld = getOverworld(source); + if (overworld == null) + { + return 0; + } + + if (PackManager.getServerPacks(overworld).stream().noneMatch(p -> p.getName().equals(packName))) + { + source.sendFailure(net.minecraft.network.chat.Component.literal("Pack not found: " + packName)); + return 0; + } + + final PackSchematic schematic = new PackSchematic( + path, + schematicName, + level, + BlockPos.ZERO, + BlockPos.ZERO, + null, + source.getLevel().dimension()); + + overworld.getDataStorage().computeIfAbsent(PackManager.FACTORY, PackManager.DATA_NAME) + .addSchematic(packName, schematic); + + LOGGER.info("[PackIndex] Added schematic '{}' to pack '{}'", schematicName, packName); + source.sendSuccess(() -> net.minecraft.network.chat.Component.literal("Added schematic '" + schematicName + "' to pack '" + packName + "'"), false); + return 1; + } +} diff --git a/src/main/java/com/ldtteam/structurize/event/EventSubscriber.java b/src/main/java/com/ldtteam/structurize/event/EventSubscriber.java index ef97526ec..bd1479fea 100644 --- a/src/main/java/com/ldtteam/structurize/event/EventSubscriber.java +++ b/src/main/java/com/ldtteam/structurize/event/EventSubscriber.java @@ -1,12 +1,15 @@ package com.ldtteam.structurize.event; import com.ldtteam.structurize.commands.EntryPoint; +import com.ldtteam.structurize.index.PackManager; import com.ldtteam.structurize.management.Manager; import com.ldtteam.structurize.util.BlockUtils; import com.ldtteam.structurize.util.IOPool; import net.minecraft.server.level.ServerLevel; import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.level.LevelEvent; import net.neoforged.neoforge.event.server.ServerStoppingEvent; import net.neoforged.neoforge.event.tick.LevelTickEvent; import org.jetbrains.annotations.NotNull; @@ -31,6 +34,24 @@ private EventSubscriber() * * @param event event */ + @SubscribeEvent + public static void onLevelLoad(final LevelEvent.Load event) + { + PackManager.onLevelLoad(event); + } + + @SubscribeEvent + public static void onLevelUnload(final LevelEvent.Unload event) + { + PackManager.onLevelUnload(event); + } + + @SubscribeEvent + public static void onPlayerJoin(final PlayerEvent.PlayerLoggedInEvent event) + { + PackManager.onPlayerJoin(event); + } + @SubscribeEvent public static void onRegisterCommands(final RegisterCommandsEvent event) { diff --git a/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java b/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java index 1d4679d15..d26e111ba 100644 --- a/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java +++ b/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java @@ -2,6 +2,7 @@ import com.ldtteam.common.language.LanguageHandler; import com.ldtteam.structurize.api.constants.Constants; +import com.ldtteam.structurize.network.messages.SyncPackManagerMessage; import com.ldtteam.structurize.datagen.BlockEntityTagProvider; import com.ldtteam.structurize.datagen.BlockTagProvider; import com.ldtteam.structurize.datagen.EntityTagProvider; @@ -22,12 +23,16 @@ import net.neoforged.neoforge.data.event.GatherDataEvent; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforge.registries.NewRegistryEvent; +import net.neoforged.neoforge.registries.RegistryBuilder; import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.IdentityHashMap; import java.util.Set; +import static com.ldtteam.structurize.api.Registries.SCHEMATIC_INDEX_PACK_TYPES; + public class LifecycleSubscriber { @SubscribeEvent @@ -54,6 +59,7 @@ public static void onNetworkRegistry(final RegisterPayloadHandlersEvent event) ScanToolTeleportMessage.TYPE.register(registry); SetTagInTool.TYPE.register(registry); ShowScanMessage.TYPE.register(registry); + SyncPackManagerMessage.TYPE.register(registry); SyncPreviewCacheToClient.TYPE.register(registry); SyncPreviewCacheToServer.TYPE.register(registry); SyncSettingsToServer.TYPE.register(registry); @@ -102,4 +108,10 @@ public static void registerCaps(final RegisterCapabilitiesEvent event) } PlacementHandlers.ContainerPlacementHandler.CONTAINERS = containerBlocks; } + + @SubscribeEvent + public static void registerNewRegistries(final NewRegistryEvent event) + { + event.create(new RegistryBuilder<>(SCHEMATIC_INDEX_PACK_TYPES).sync(true)); + } } diff --git a/src/main/java/com/ldtteam/structurize/index/PackManager.java b/src/main/java/com/ldtteam/structurize/index/PackManager.java new file mode 100644 index 000000000..4646a1148 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/PackManager.java @@ -0,0 +1,186 @@ +package com.ldtteam.structurize.index; + +import com.ldtteam.structurize.index.models.Pack; +import com.ldtteam.structurize.index.models.PackSchematic; +import com.ldtteam.structurize.index.packtypes.models.PackType; +import com.ldtteam.structurize.network.messages.SyncPackManagerMessage; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.level.LevelEvent; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * Global SavedData stored in the overworld, holding all packs across the game. + * Synced to clients on player join. + */ +public class PackManager extends SavedData +{ + public static final String DATA_NAME = "structurize_pack_manager"; + + public static final SavedData.Factory FACTORY = new SavedData.Factory<>(PackManager::new, PackManager::deserialize); + + /** + * Client-side pack list, populated via {@link SyncPackManagerMessage}. + */ + private static Map clientPacks = new HashMap<>(); + + private static final String NBT_PACKS = "packs"; + + private final Map packs = new HashMap<>(); + + private PackManager() {} + + private static PackManager deserialize(final @NotNull CompoundTag compound, final @NotNull HolderLookup.Provider provider) + { + final PackManager manager = new PackManager(); + final ListTag list = compound.getList(NBT_PACKS, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) + { + final Pack pack = Pack.load(list.getCompound(i), provider); + manager.packs.put(pack.getId(), pack); + } + return manager; + } + + @Override + @NotNull + public CompoundTag save(final @NotNull CompoundTag tag, final @NotNull HolderLookup.Provider provider) + { + final ListTag list = new ListTag(); + for (final Pack pack : packs.values()) + { + list.add(pack.save(provider)); + } + tag.put(NBT_PACKS, list); + return tag; + } + + /** + * Loads the PackManager from the overworld data storage. + * Call on overworld load only. + */ + public static void onLevelLoad(final LevelEvent.Load event) + { + if (!(event.getLevel() instanceof final ServerLevel serverLevel)) + { + return; + } + + if (serverLevel.dimension() != Level.OVERWORLD) + { + return; + } + + serverLevel.getDataStorage().computeIfAbsent(FACTORY, DATA_NAME); + } + + /** + * Clears client-side pack list on overworld unload. + */ + public static void onLevelUnload(final LevelEvent.Unload event) + { + if (!(event.getLevel() instanceof final ServerLevel serverLevel)) + { + return; + } + + if (serverLevel.dimension() != Level.OVERWORLD) + { + return; + } + + clientPacks.clear(); + } + + /** + * Syncs the pack list to a player when they join. + */ + public static void onPlayerJoin(final PlayerEvent.PlayerLoggedInEvent event) + { + if (!(event.getEntity() instanceof final ServerPlayer serverPlayer)) + { + return; + } + + final ServerLevel overworld = serverPlayer.getServer().getLevel(Level.OVERWORLD); + if (overworld == null) + { + return; + } + + final PackManager manager = overworld.getDataStorage().computeIfAbsent(FACTORY, DATA_NAME); + PacketDistributor.sendToPlayer(serverPlayer, new SyncPackManagerMessage(manager.packs)); + } + + /** + * Returns the server-sided pack list from the overworld data storage. + */ + @NotNull + public static List getServerPacks(final @NotNull ServerLevel overworld) + { + final Map packs = overworld.getDataStorage().computeIfAbsent(FACTORY, DATA_NAME).packs; + return packs.values().stream().sorted(Comparator.comparing(Pack::getName)).toList(); + } + + /** + * Returns the client-sided pack list from the overworld data storage. + */ + @NotNull + public static List getClientPacks() + { + return clientPacks.values().stream().sorted(Comparator.comparing(Pack::getName)).toList(); + } + + /** + * Called by {@link SyncPackManagerMessage} on the client to store the synced pack list. + */ + public static void onClientSync(final @NotNull Map packs) + { + clientPacks = new HashMap<>(packs); + } + + @Override + public void setDirty(boolean value) + { + super.setDirty(value); + if (value) + { + PacketDistributor.sendToAllPlayers(new SyncPackManagerMessage(packs)); + } + } + + @Nullable + public String addPack(final @NotNull String name, final @NotNull Holder packType) + { + final String id = name.toLowerCase(Locale.ROOT).replace(" ", "_"); + if (packs.containsKey(id)) + { + return null; + } + packs.put(id, new Pack(id, name, packType)); + setDirty(); + return id; + } + + public void addSchematic(final @NotNull String packId, final @NotNull PackSchematic schematic) + { + final Pack pack = packs.get(packId); + if (pack != null) + { + pack.addSchematic(schematic); + setDirty(); + } + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/models/Pack.java b/src/main/java/com/ldtteam/structurize/index/models/Pack.java new file mode 100644 index 000000000..5abc34c5d --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/models/Pack.java @@ -0,0 +1,103 @@ +package com.ldtteam.structurize.index.models; + +import com.ldtteam.structurize.api.Registries; +import com.ldtteam.structurize.index.packtypes.models.PackType; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.RegistryFixedCodec; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Pack +{ + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("id").forGetter(p -> p.id), + Codec.STRING.fieldOf("name").forGetter(p -> p.name), + RegistryFixedCodec.create(Registries.SCHEMATIC_INDEX_PACK_TYPES).fieldOf("type").forGetter(p -> p.type), + PackSchematic.CODEC.listOf().fieldOf("schematics").forGetter(p -> p.schematics) + ).apply(instance, Pack::new)); + + public static final StreamCodec STREAM_CODEC = + ByteBufCodecs.fromCodecWithRegistries(CODEC); + + public static final StreamCodec> MAP_STREAM_CODEC = + ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, STREAM_CODEC); + + @NotNull + private final String id; + + @NotNull + private final String name; + + @NotNull + private final Holder type; + + @NotNull + private final List schematics; + + public Pack(final @NotNull String id, final @NotNull String name, final @NotNull Holder type) + { + this.id = id; + this.name = name; + this.type = type; + this.schematics = new ArrayList<>(); + } + + private Pack(final @NotNull String id, final @NotNull String name, final @NotNull Holder type, final @NotNull List schematics) + { + this.id = id; + this.name = name; + this.type = type; + this.schematics = new ArrayList<>(schematics); + } + + public static Pack load(final @NotNull CompoundTag compound, final @NotNull HolderLookup.Provider provider) + { + return CODEC.parse(provider.createSerializationContext(NbtOps.INSTANCE), compound).getOrThrow(); + } + + public CompoundTag save(final @NotNull HolderLookup.Provider provider) + { + return (CompoundTag) CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), this).getOrThrow(); + } + + @NotNull + public String getId() + { + return id; + } + + @NotNull + public String getName() + { + return name; + } + + @NotNull + public Holder getType() + { + return type; + } + + @NotNull + public List getSchematics() + { + return schematics; + } + + public void addSchematic(final @NotNull PackSchematic schematic) + { + schematics.add(schematic); + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java b/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java new file mode 100644 index 000000000..d38007b0f --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java @@ -0,0 +1,104 @@ +package com.ldtteam.structurize.index.models; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class PackSchematic +{ + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("path").forGetter(s -> s.path), + Codec.STRING.fieldOf("name").forGetter(s -> s.name), + Codec.INT.fieldOf("level").forGetter(s -> s.level), + BlockPos.CODEC.fieldOf("pos1").forGetter(s -> s.pos1), + BlockPos.CODEC.fieldOf("pos2").forGetter(s -> s.pos2), + BlockPos.CODEC.optionalFieldOf("anchor").forGetter(s -> Optional.ofNullable(s.anchor)), + Level.RESOURCE_KEY_CODEC.fieldOf("dimension").forGetter(s -> s.world) + ).apply(instance, (path, name, level, pos1, pos2, anchor, world) -> new PackSchematic(path, name, level, pos1, pos2, anchor.orElse(null), world))); + + + @NotNull + private final String path; + + @NotNull + private final String name; + + private final int level; + + @NotNull + private final BlockPos pos1; + + @NotNull + private final BlockPos pos2; + + @Nullable + private final BlockPos anchor; + + @NotNull + private final ResourceKey world; + + public PackSchematic( + final @NotNull String path, + final @NotNull String name, + final int level, + final @NotNull BlockPos pos1, + final @NotNull BlockPos pos2, + final @Nullable BlockPos anchor, + final @NotNull ResourceKey world) + { + this.path = path; + this.name = name; + this.level = level; + this.pos1 = pos1; + this.pos2 = pos2; + this.anchor = anchor; + this.world = world; + } + + @NotNull + public String getPath() + { + return path; + } + + @NotNull + public String getName() + { + return name; + } + + public int getLevel() + { + return level; + } + + @NotNull + public BlockPos getPos1() + { + return pos1; + } + + @NotNull + public BlockPos getPos2() + { + return pos2; + } + + @Nullable + public BlockPos getAnchor() + { + return anchor; + } + + @NotNull + public ResourceKey getWorld() + { + return world; + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java b/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java new file mode 100644 index 000000000..b97a9f099 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java @@ -0,0 +1,28 @@ +package com.ldtteam.structurize.index.packtypes; + +import com.ldtteam.structurize.api.constants.Constants; +import com.ldtteam.structurize.index.packtypes.models.PackType; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +import java.util.function.Function; + +import static com.ldtteam.structurize.api.Registries.SCHEMATIC_INDEX_PACK_TYPES; + +public class PackTypesRegistry +{ + public static final DeferredRegister DEFERRED_REGISTER = DeferredRegister.create(SCHEMATIC_INDEX_PACK_TYPES, Constants.MOD_ID); + + public static final DeferredHolder DEFAULT_PACK_TYPE = + register(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "default"), builder -> builder.withName(Component.literal("Default"))); + + public static DeferredHolder register( + final ResourceLocation packId, + final Function configure) + { + final PackType.Builder builder = configure.apply(new PackType.Builder(packId)); + return DEFERRED_REGISTER.register(packId.getPath(), builder::build); + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java new file mode 100644 index 000000000..bc5481b5a --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java @@ -0,0 +1,67 @@ +package com.ldtteam.structurize.index.packtypes.models; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.List; + +public class PackType +{ + private final ResourceLocation packId; + + private final Component name; + + private final List packRequirements; + + private final List schematicRequirements; + + private PackType( + final ResourceLocation packId, final Component name, final List packRequirements, + final List schematicRequirements) + { + this.packId = packId; + this.name = name; + this.packRequirements = packRequirements; + this.schematicRequirements = schematicRequirements; + } + + public static class Builder + { + private final ResourceLocation packId; + + private Component name; + + private final List packRequirements = new ArrayList<>(); + + private final List schematicRequirements = new ArrayList<>(); + + public Builder(final ResourceLocation packId) + { + this.packId = packId; + } + + public Builder withName(final Component name) + { + this.name = name; + return this; + } + + public Builder addPackRequirement(final PackTypeRequirement requirement) + { + this.packRequirements.add(requirement); + return this; + } + + public Builder addSchematicRequirement(final PackTypeSchematicRequirement requirement) + { + this.schematicRequirements.add(requirement); + return this; + } + + public PackType build() + { + return new PackType(this.packId, this.name, this.packRequirements, this.schematicRequirements); + } + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java new file mode 100644 index 000000000..f5a1539f2 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java @@ -0,0 +1,137 @@ +package com.ldtteam.structurize.index.packtypes.models; + +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +/** + * A requirement that the pack as a whole must satisfy. + * + *

Requirements can assert that a schematic exists matching one of the following criteria: + *

    + *
  • A specific schematic name
  • + *
  • A specific schematic path
  • + *
  • A specific anchor block type
  • + *
+ * + *

By default, requirements are mandatory, shown with an exclamation mark in the UI. + * Use {@link Builder#optional()} to display a requirement without marking it as mandatory. + */ +public class PackTypeRequirement +{ + /** + * If non-null, a schematic with this name must exist in the pack. + */ + @Nullable + private final String requiredName; + + /** + * If non-null, a schematic at this path must exist in the pack. + */ + @Nullable + private final String requiredPath; + + /** + * If non-null, a schematic with this anchor block type must exist in the pack. + */ + @Nullable + private final BlockState requiredAnchor; + + /** + * Whether this requirement is optional. Optional requirements are shown in the UI but not flagged as mandatory. + */ + private final boolean optional; + + private PackTypeRequirement(final @Nullable String requiredName, final @Nullable String requiredPath, final @Nullable BlockState requiredAnchor, final boolean optional) + { + this.requiredName = requiredName; + this.requiredPath = requiredPath; + this.requiredAnchor = requiredAnchor; + this.optional = optional; + } + + /** + * Builder for {@link PackTypeRequirement}. + */ + public static class Builder + { + /** + * @see PackTypeRequirement#requiredName + */ + @Nullable + private String requiredName; + + /** + * @see PackTypeRequirement#requiredPath + */ + @Nullable + private String requiredPath; + + /** + * @see PackTypeRequirement#requiredAnchor + */ + @Nullable + private BlockState requiredAnchor; + + /** + * @see PackTypeRequirement#optional + */ + private boolean optional = false; + + /** + * Requires a schematic with the given name to exist in the pack. + * + * @param name the required schematic name + * @return this builder + */ + public Builder requiresName(final String name) + { + this.requiredName = name; + return this; + } + + /** + * Requires a schematic at the given path to exist in the pack. + * + * @param path the required schematic path + * @return this builder + */ + public Builder requiresPath(final String path) + { + this.requiredPath = path; + return this; + } + + /** + * Requires a schematic with the given anchor block type to exist in the pack. + * + * @param anchor the required anchor block state + * @return this builder + */ + public Builder requiresAnchor(final BlockState anchor) + { + this.requiredAnchor = anchor; + return this; + } + + /** + * Marks this requirement as optional. It will still appear in the UI, but will not be flagged as mandatory. + * + * @return this builder + */ + public Builder optional() + { + this.optional = true; + return this; + } + + /** + * Builds the {@link PackTypeRequirement}. + * + * @return the constructed requirement + */ + public PackTypeRequirement build() + { + return new PackTypeRequirement(requiredName, requiredPath, requiredAnchor, optional); + } + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java new file mode 100644 index 000000000..589103dd8 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java @@ -0,0 +1,305 @@ +package com.ldtteam.structurize.index.packtypes.models; + +import com.ldtteam.structurize.blueprints.v1.Blueprint; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +import static com.ldtteam.structurize.api.constants.Constants.GROUNDLEVEL_TAG; +import static com.ldtteam.structurize.blockentities.interfaces.IBlueprintDataProviderBE.TAG_BLUEPRINTDATA; + +/** + * A requirement checked against individual schematics to determine whether they are "valid". + * + *

Each requirement may match globally or be scoped to specific schematics via filters: + *

    + *
  • Schematic name
  • + *
  • Schematic path
  • + *
  • Anchor block type
  • + *
  • One or more tags
  • + *
+ * + *

Matched schematics are then validated against: + *

    + *
  • One or more required tags on the anchor block
  • + *
  • One or more blocks that must be present in the schematic
  • + *
  • Any number of custom predicates operating on the full {@link Blueprint} data
  • + *
+ * + *

Each requirement carries a {@link PackTypeSchematicRequirementSeverity severity level} indicating + * how problematic a violation is. + */ +public class PackTypeSchematicRequirement +{ + public static final PackTypeSchematicRequirement ANCHOR_NO_GROUND_LEVEL = new PackTypeSchematicRequirement.Builder().addRequiredTag(GROUNDLEVEL_TAG) + .addAdditionalBlueprintCheck(blueprint -> blueprint.getBlockInfoAsList() + .stream() + .filter(blockInfo -> blockInfo.hasTileEntityData() && blockInfo.getTileEntityData().contains(TAG_BLUEPRINTDATA)) + .toList() + .size() > 1) + .markAsIssue() + .build(); + + public static final PackTypeSchematicRequirement MULTIPLE_ANCHORS = new PackTypeSchematicRequirement.Builder().addRequiredTag(GROUNDLEVEL_TAG) + .addAdditionalBlueprintCheck(blueprint -> blueprint.getBlockInfoAsList() + .stream() + .filter(blockInfo -> blockInfo.hasTileEntityData() && blockInfo.getTileEntityData().contains(TAG_BLUEPRINTDATA)) + .toList() + .size() > 1) + .markAsIssue() + .build(); + + /** + * If non-null, this requirement only applies to schematics whose name matches this value. + */ + @Nullable + private final String matchesName; + + /** + * If non-null, this requirement only applies to schematics whose path matches this value. + */ + @Nullable + private final String matchesPath; + + /** + * If non-null, this requirement only applies to schematics with this anchor block type. + */ + @Nullable + private final BlockState matchesAnchor; + + /** + * If non-null, this requirement only applies to schematics that have all of these tags. + */ + @Nullable + private final List matchesTags; + + /** + * Tags that must be present on the anchor block of any matched schematic. + */ + @NotNull + private final List requiredTags; + + /** + * Blocks that must be present in any matched schematic, mapped to their minimum required count. + */ + @NotNull + private final Map requiredBlocks; + + /** + * Additional custom predicates evaluated against the full {@link Blueprint} of any matched schematic. + * All predicates must return {@code true} for the schematic to be considered valid. + */ + @NotNull + private final List> additionalBlueprintChecks; + + /** + * The severity of a violation of this requirement. + */ + private final PackTypeSchematicRequirementSeverity warningSeverity; + + private PackTypeSchematicRequirement( + final @Nullable String matchesName, + final @Nullable String matchesPath, + final @Nullable BlockState matchesAnchor, + final @Nullable List matchesTags, + final @NotNull List requiredTags, + final @NotNull Map requiredBlocks, + final @NotNull List> additionalBlueprintChecks, + final PackTypeSchematicRequirementSeverity warningSeverity) + { + this.matchesName = matchesName; + this.matchesPath = matchesPath; + this.matchesAnchor = matchesAnchor; + this.matchesTags = matchesTags; + this.requiredTags = requiredTags; + this.requiredBlocks = requiredBlocks; + this.additionalBlueprintChecks = additionalBlueprintChecks; + this.warningSeverity = warningSeverity; + } + + /** + * Builder for {@link PackTypeSchematicRequirement}. + */ + public static class Builder + { + /** + * @see PackTypeSchematicRequirement#matchesName + */ + @Nullable + private String matchesName; + + /** + * @see PackTypeSchematicRequirement#matchesPath + */ + @Nullable + private String matchesPath; + + /** + * @see PackTypeSchematicRequirement#matchesAnchor + */ + @Nullable + private BlockState matchesAnchor; + + /** + * @see PackTypeSchematicRequirement#matchesTags + */ + @NotNull + private final List matchesTags = new ArrayList<>(); + + /** + * @see PackTypeSchematicRequirement#requiredTags + */ + @NotNull + private final List requiredTags = new ArrayList<>(); + + /** + * @see PackTypeSchematicRequirement#requiredBlocks + */ + @NotNull + private final Map requiredBlocks = new HashMap<>(); + + /** + * @see PackTypeSchematicRequirement#additionalBlueprintChecks + */ + @NotNull + private final List> additionalBlueprintChecks = new ArrayList<>(); + + /** + * @see PackTypeSchematicRequirement#warningSeverity + */ + private PackTypeSchematicRequirementSeverity warningSeverity = PackTypeSchematicRequirementSeverity.INFORMATIONAL; + + /** + * Restricts this requirement to schematics whose name matches the given value. + * + * @param name the schematic name to match + * @return this builder + */ + public Builder matchesName(final String name) + { + this.matchesName = name; + return this; + } + + /** + * Restricts this requirement to schematics whose path matches the given value. + * + * @param path the schematic path to match + * @return this builder + */ + public Builder matchesPath(final String path) + { + this.matchesPath = path; + return this; + } + + /** + * Restricts this requirement to schematics that use the given anchor block type. + * + * @param anchor the anchor block state to match + * @return this builder + */ + public Builder matchesAnchor(final BlockState anchor) + { + this.matchesAnchor = anchor; + return this; + } + + /** + * Requires the anchor block of a matched schematic to have the given tag. + * May be called multiple times to require several tags. + * + * @param tag the tag that must be present on the anchor block + * @return this builder + */ + public Builder addRequiredTag(final String tag) + { + this.requiredTags.add(tag); + return this; + } + + /** + * Requires at least one instance of the given block to be present in a matched schematic. + * Equivalent to {@code addRequiredBlock(block, 1)}. + * + * @param block the block that must be present + * @return this builder + */ + public Builder addRequiredBlock(final BlockState block) + { + return addRequiredBlock(block, 1); + } + + /** + * Requires at least {@code count} instances of the given block to be present in a matched schematic. + * + * @param block the block that must be present + * @param count the minimum number of occurrences required + * @return this builder + */ + public Builder addRequiredBlock(final BlockState block, final int count) + { + this.requiredBlocks.put(block, count); + return this; + } + + /** + * Adds a custom predicate evaluated against the full {@link Blueprint} of any matched schematic. + * All added predicates must return {@code true} for the schematic to be considered valid. + * May be called multiple times to add several checks. + * + * @param additionalCheck a predicate receiving the full blueprint data + * @return this builder + */ + public Builder addAdditionalBlueprintCheck(final Predicate additionalCheck) + { + this.additionalBlueprintChecks.add(additionalCheck); + return this; + } + + /** + * Sets the warning severity to {@link PackTypeSchematicRequirementSeverity#ISSUE}. + * + * @return this builder + */ + public Builder markAsIssue() + { + this.warningSeverity = PackTypeSchematicRequirementSeverity.ISSUE; + return this; + } + + /** + * Sets the warning severity to {@link PackTypeSchematicRequirementSeverity#ERROR}. + * + * @return this builder + */ + public Builder markAsError() + { + this.warningSeverity = PackTypeSchematicRequirementSeverity.ERROR; + return this; + } + + /** + * Builds the {@link PackTypeSchematicRequirement}. + * + * @return the constructed requirement + */ + public PackTypeSchematicRequirement build() + { + return new PackTypeSchematicRequirement(matchesName, + matchesPath, + matchesAnchor, + matchesTags, + requiredTags, + requiredBlocks, + additionalBlueprintChecks, + warningSeverity); + } + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirementSeverity.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirementSeverity.java new file mode 100644 index 000000000..b0385ff5d --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirementSeverity.java @@ -0,0 +1,30 @@ +package com.ldtteam.structurize.index.packtypes.models; + +/** + * Severity level for a schematic requirement violation. + */ +public enum PackTypeSchematicRequirementSeverity +{ + /** + * Purely informational — no immediate problem, but worth noting. + * + *

Example: No {@code groundlevel} tag on a schematic with an anchor block. + * This is fine because the ground level defaults to the block below the anchor. + */ + INFORMATIONAL, + + /** + * A potential problem — not immediately broken, but may not behave as intended. + * + *

Example: No {@code groundlevel} tag on a schematic without an anchor block. + * This causes the bottom of the full schematic to be used as the ground level. + */ + ISSUE, + + /** + * A hard error that will cause problems in-game and must be fixed before release. These issues are blocking and will not allow a scan to be made! + * + *

Example: Two anchor blocks in a schematic without an explicitly defined anchor position. + */ + ERROR +} diff --git a/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java b/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java new file mode 100644 index 000000000..1ed49b043 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java @@ -0,0 +1,68 @@ +// TODO: Remove before publication — debug item only +package com.ldtteam.structurize.items; + +import com.ldtteam.structurize.index.PackManager; +import com.ldtteam.structurize.index.models.Pack; +import com.ldtteam.structurize.index.models.PackSchematic; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ItemPackIndexDebug extends Item +{ + private static final Logger LOGGER = LogManager.getLogger(); + + public ItemPackIndexDebug() + { + super(new Properties().stacksTo(1)); + } + + @Override + public @NotNull InteractionResultHolder use(final @NotNull Level level, final @NotNull Player player, final @NotNull InteractionHand hand) + { + final List packs = level.isClientSide + ? PackManager.getClientPacks() + : PackManager.getServerPacks(level.getServer().getLevel(Level.OVERWORLD)); + + final String side = level.isClientSide ? "CLIENT" : "SERVER"; + + if (packs.isEmpty()) + { + LOGGER.info("[PackIndex][{}] No packs registered.", side); + } + else + { + LOGGER.info("[PackIndex][{}] {} pack(s):", side, packs.size()); + for (final Pack pack : packs) + { + LOGGER.info("[PackIndex][{}] Pack: '{}' | type: {}", + side, + pack.getName(), + pack.getType().unwrapKey().map(k -> k.location().toString()).orElse("unknown")); + + for (final PackSchematic schematic : pack.getSchematics()) + { + LOGGER.info("[PackIndex][{}] Schematic: '{}' | path: {} | level: {} | world: {} | pos1: {} | pos2: {} | anchor: {}", + side, + schematic.getName(), + schematic.getPath(), + schematic.getLevel(), + schematic.getWorld().location(), + schematic.getPos1(), + schematic.getPos2(), + schematic.getAnchor()); + } + } + } + + return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide); + } +} diff --git a/src/main/java/com/ldtteam/structurize/items/ModItems.java b/src/main/java/com/ldtteam/structurize/items/ModItems.java index 6efaa8947..1a04d5ba6 100644 --- a/src/main/java/com/ldtteam/structurize/items/ModItems.java +++ b/src/main/java/com/ldtteam/structurize/items/ModItems.java @@ -25,6 +25,9 @@ private ModItems() { /* prevent construction */ } public static final DeferredItem caliper; public static final DeferredItem blockTagSubstitution; + // TODO: Remove before publication — debug item only + public static final DeferredItem packIndexDebug; + static { buildTool = ITEMS.register("sceptergold", ItemBuildTool::new); @@ -33,5 +36,7 @@ private ModItems() { /* prevent construction */ } tagTool = ITEMS.register("sceptertag", (Supplier) ItemTagTool::new); caliper = ITEMS.register("caliper", ItemCaliper::new); blockTagSubstitution = ITEMS.register("blockTagSubstitution".toLowerCase(), ItemTagSubstitution::new); + // TODO: Remove before publication — debug item only + packIndexDebug = ITEMS.register("packindexdebug", ItemPackIndexDebug::new); } } diff --git a/src/main/java/com/ldtteam/structurize/network/messages/SyncPackManagerMessage.java b/src/main/java/com/ldtteam/structurize/network/messages/SyncPackManagerMessage.java new file mode 100644 index 000000000..5143e0ca4 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/network/messages/SyncPackManagerMessage.java @@ -0,0 +1,43 @@ +package com.ldtteam.structurize.network.messages; + +import com.ldtteam.common.network.AbstractClientPlayMessage; +import com.ldtteam.common.network.PlayMessageType; +import com.ldtteam.structurize.api.constants.Constants; +import com.ldtteam.structurize.index.PackManager; +import com.ldtteam.structurize.index.models.Pack; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.util.Map; + +public class SyncPackManagerMessage extends AbstractClientPlayMessage +{ + public static final PlayMessageType TYPE = PlayMessageType.forClient(Constants.MOD_ID, "sync_pack_manager", SyncPackManagerMessage::new); + + private final Map packs; + + public SyncPackManagerMessage(final Map packs) + { + super(TYPE); + this.packs = packs; + } + + protected SyncPackManagerMessage(final RegistryFriendlyByteBuf buf, final PlayMessageType type) + { + super(buf, type); + this.packs = Pack.MAP_STREAM_CODEC.decode(buf); + } + + @Override + protected void toBytes(final RegistryFriendlyByteBuf buf) + { + Pack.MAP_STREAM_CODEC.encode(buf, packs); + } + + @Override + protected void onExecute(final IPayloadContext context, final Player player) + { + PackManager.onClientSync(packs); + } +} From a0e657983177b778ff60a5d9362cb6bbc8e5df2a Mon Sep 17 00:00:00 2001 From: Thom van den Akker Date: Fri, 3 Apr 2026 17:51:24 +0200 Subject: [PATCH 2/5] Storage update --- .../commands/PackIndexCommand.java | 49 +---- .../structurize/index/LevelPackData.java | 102 ++++++++++ .../structurize/index/PackManager.java | 176 ++++++++++-------- .../structurize/index/models/Pack.java | 81 +++----- .../index/models/PackSchematic.java | 110 +++-------- .../structurize/items/ItemPackIndexDebug.java | 23 ++- 6 files changed, 267 insertions(+), 274 deletions(-) create mode 100644 src/main/java/com/ldtteam/structurize/index/LevelPackData.java diff --git a/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java b/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java index 1c51f60e1..b4ce757a5 100644 --- a/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java +++ b/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java @@ -10,8 +10,6 @@ import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands.CommandSelection; import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.world.level.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -34,33 +32,21 @@ protected static LiteralArgumentBuilder build() .then(newArgument("packName", StringArgumentType.word()) .executes(ctx -> addPack(ctx.getSource(), StringArgumentType.getString(ctx, "packName"))))) .then(newLiteral("schematic") - .then(newArgument("packName", StringArgumentType.word()) + .then(newArgument("packId", StringArgumentType.word()) .then(newArgument("schematicName", StringArgumentType.word()) .then(newArgument("path", StringArgumentType.word()) .then(newArgument("level", IntegerArgumentType.integer(0)) .executes(ctx -> addSchematic( ctx.getSource(), - StringArgumentType.getString(ctx, "packName"), + StringArgumentType.getString(ctx, "packId"), StringArgumentType.getString(ctx, "schematicName"), StringArgumentType.getString(ctx, "path"), IntegerArgumentType.getInteger(ctx, "level"))))))))); } - private static ServerLevel getOverworld(final CommandSourceStack source) - { - return source.getServer().getLevel(Level.OVERWORLD); - } - private static int addPack(final CommandSourceStack source, final String packName) { - final ServerLevel overworld = getOverworld(source); - if (overworld == null) - { - return 0; - } - - final PackManager manager = overworld.getDataStorage().computeIfAbsent(PackManager.FACTORY, PackManager.DATA_NAME); - final String newId = manager.addPack(packName, PackTypesRegistry.DEFAULT_PACK_TYPE); + final String newId = PackManager.addPack(packName, PackTypesRegistry.DEFAULT_PACK_TYPE); if (newId != null) { @@ -77,37 +63,22 @@ private static int addPack(final CommandSourceStack source, final String packNam private static int addSchematic( final CommandSourceStack source, - final String packName, + final String packId, final String schematicName, final String path, final int level) { - final ServerLevel overworld = getOverworld(source); - if (overworld == null) - { - return 0; - } - - if (PackManager.getServerPacks(overworld).stream().noneMatch(p -> p.getName().equals(packName))) + if (PackManager.getPack(packId) == null) { - source.sendFailure(net.minecraft.network.chat.Component.literal("Pack not found: " + packName)); + source.sendFailure(net.minecraft.network.chat.Component.literal("Pack not found: " + packId)); return 0; } - final PackSchematic schematic = new PackSchematic( - path, - schematicName, - level, - BlockPos.ZERO, - BlockPos.ZERO, - null, - source.getLevel().dimension()); - - overworld.getDataStorage().computeIfAbsent(PackManager.FACTORY, PackManager.DATA_NAME) - .addSchematic(packName, schematic); + final PackSchematic schematic = new PackSchematic(path, schematicName, level, BlockPos.ZERO, BlockPos.ZERO, null); + PackManager.addSchematic(packId, schematic); - LOGGER.info("[PackIndex] Added schematic '{}' to pack '{}'", schematicName, packName); - source.sendSuccess(() -> net.minecraft.network.chat.Component.literal("Added schematic '" + schematicName + "' to pack '" + packName + "'"), false); + LOGGER.info("[PackIndex] Added schematic '{}' to pack '{}'", schematicName, packId); + source.sendSuccess(() -> net.minecraft.network.chat.Component.literal("Added schematic '" + schematicName + "' to pack '" + packId + "'"), false); return 1; } } diff --git a/src/main/java/com/ldtteam/structurize/index/LevelPackData.java b/src/main/java/com/ldtteam/structurize/index/LevelPackData.java new file mode 100644 index 000000000..2639b50f6 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/LevelPackData.java @@ -0,0 +1,102 @@ +package com.ldtteam.structurize.index; + +import com.ldtteam.structurize.index.models.Pack; +import com.ldtteam.structurize.network.messages.SyncPackManagerMessage; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * Per-level SavedData storing the packs that belong to a specific level's data folder. + * One instance is loaded per {@link net.minecraft.server.level.ServerLevel}; the merged view is managed by {@link PackManager}. + */ +public class LevelPackData extends SavedData +{ + public static final String DATA_NAME = "structurize_pack_manager"; + + private static final String NBT_PACKS = "packs"; + + private final ResourceKey dimension; + + private final Map ownedPacks = new HashMap<>(); + + private LevelPackData(final ResourceKey dimension) + { + this.dimension = dimension; + } + + /** + * Returns a factory for the given dimension. Must be constructed per call-site since the + * dimension key is needed for the empty-constructor supplier. + */ + public static SavedData.Factory factoryFor(final ResourceKey dimension) + { + return new SavedData.Factory<>(() -> new LevelPackData(dimension), (tag, provider) -> deserialize(tag, provider, dimension)); + } + + private static LevelPackData deserialize(final @NotNull CompoundTag compound, final @NotNull HolderLookup.Provider provider, final ResourceKey dimension) + { + final LevelPackData data = new LevelPackData(dimension); + final ListTag list = compound.getList(NBT_PACKS, Tag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) + { + final Pack pack = Pack.load(list.getCompound(i), provider); + data.ownedPacks.put(pack.id(), pack); + } + return data; + } + + @Override + @NotNull + public CompoundTag save(final @NotNull CompoundTag tag, final @NotNull HolderLookup.Provider provider) + { + final ListTag list = new ListTag(); + for (final Pack pack : ownedPacks.values()) + { + list.add(pack.save(provider)); + } + tag.put(NBT_PACKS, list); + return tag; + } + + @Override + public void setDirty(final boolean value) + { + super.setDirty(value); + if (value) + { + PacketDistributor.sendToAllPlayers(new SyncPackManagerMessage(PackManager.getServerPacksMap())); + } + } + + public ResourceKey getDimension() + { + return dimension; + } + + /** + * Read-only view of packs owned by this level, used during load merging. + */ + public Map getOwnedPacks() + { + return ownedPacks; + } + + /** + * Mutable access for PackManager to insert newly created packs into this level's owned set. + * Package-private intentionally. + */ + Map getOwnedPacksMutable() + { + return ownedPacks; + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/PackManager.java b/src/main/java/com/ldtteam/structurize/index/PackManager.java index 4646a1148..e35ccf5db 100644 --- a/src/main/java/com/ldtteam/structurize/index/PackManager.java +++ b/src/main/java/com/ldtteam/structurize/index/PackManager.java @@ -5,14 +5,10 @@ import com.ldtteam.structurize.index.packtypes.models.PackType; import com.ldtteam.structurize.network.messages.SyncPackManagerMessage; import net.minecraft.core.Holder; -import net.minecraft.core.HolderLookup; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.Level; -import net.minecraft.world.level.saveddata.SavedData; import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.level.LevelEvent; import net.neoforged.neoforge.network.PacketDistributor; @@ -22,54 +18,31 @@ import java.util.*; /** - * Global SavedData stored in the overworld, holding all packs across the game. - * Synced to clients on player join. + * Static utility managing the global merged pack list across all loaded levels. + * Each {@link ServerLevel} owns a {@link LevelPackData} that loads/saves its packs independently; + * this class merges them into a single in-memory view and handles client synchronisation. */ -public class PackManager extends SavedData +public final class PackManager { - public static final String DATA_NAME = "structurize_pack_manager"; + /** + * Tracks which dimension each pack was loaded from, for cleanup on level unload. + */ + private static final Map> packOwnerDimension = new HashMap<>(); - public static final SavedData.Factory FACTORY = new SavedData.Factory<>(PackManager::new, PackManager::deserialize); + /** + * Live {@link LevelPackData} instances, one per currently loaded {@link ServerLevel}. + */ + private static final Map, LevelPackData> loadedLevelData = new HashMap<>(); /** * Client-side pack list, populated via {@link SyncPackManagerMessage}. */ private static Map clientPacks = new HashMap<>(); - private static final String NBT_PACKS = "packs"; - - private final Map packs = new HashMap<>(); - private PackManager() {} - private static PackManager deserialize(final @NotNull CompoundTag compound, final @NotNull HolderLookup.Provider provider) - { - final PackManager manager = new PackManager(); - final ListTag list = compound.getList(NBT_PACKS, Tag.TAG_COMPOUND); - for (int i = 0; i < list.size(); i++) - { - final Pack pack = Pack.load(list.getCompound(i), provider); - manager.packs.put(pack.getId(), pack); - } - return manager; - } - - @Override - @NotNull - public CompoundTag save(final @NotNull CompoundTag tag, final @NotNull HolderLookup.Provider provider) - { - final ListTag list = new ListTag(); - for (final Pack pack : packs.values()) - { - list.add(pack.save(provider)); - } - tag.put(NBT_PACKS, list); - return tag; - } - /** - * Loads the PackManager from the overworld data storage. - * Call on overworld load only. + * Loads each level's packs into the global map when the level loads. */ public static void onLevelLoad(final LevelEvent.Load event) { @@ -78,16 +51,19 @@ public static void onLevelLoad(final LevelEvent.Load event) return; } - if (serverLevel.dimension() != Level.OVERWORLD) + final ResourceKey dimension = serverLevel.dimension(); + final LevelPackData data = serverLevel.getDataStorage().computeIfAbsent(LevelPackData.factoryFor(dimension), LevelPackData.DATA_NAME); + + loadedLevelData.put(dimension, data); + + for (final String packId : data.getOwnedPacks().keySet()) { - return; + packOwnerDimension.put(packId, dimension); } - - serverLevel.getDataStorage().computeIfAbsent(FACTORY, DATA_NAME); } /** - * Clears client-side pack list on overworld unload. + * Removes the unloaded level's packs from the global map. */ public static void onLevelUnload(final LevelEvent.Unload event) { @@ -96,12 +72,14 @@ public static void onLevelUnload(final LevelEvent.Unload event) return; } - if (serverLevel.dimension() != Level.OVERWORLD) + final ResourceKey dimension = serverLevel.dimension(); + loadedLevelData.remove(dimension); + packOwnerDimension.entrySet().removeIf(entry -> entry.getValue().equals(dimension)); + + if (loadedLevelData.isEmpty()) { - return; + clientPacks.clear(); } - - clientPacks.clear(); } /** @@ -114,33 +92,40 @@ public static void onPlayerJoin(final PlayerEvent.PlayerLoggedInEvent event) return; } - final ServerLevel overworld = serverPlayer.getServer().getLevel(Level.OVERWORLD); - if (overworld == null) - { - return; - } + PacketDistributor.sendToPlayer(serverPlayer, new SyncPackManagerMessage(getServerPacksMap())); + } - final PackManager manager = overworld.getDataStorage().computeIfAbsent(FACTORY, DATA_NAME); - PacketDistributor.sendToPlayer(serverPlayer, new SyncPackManagerMessage(manager.packs)); + /** + * Returns the server-sided pack list, sorted by name. + */ + @NotNull + public static List getServerPacks() + { + return getServerPacksMap().values().stream().sorted(Comparator.comparing(Pack::name)).toList(); } /** - * Returns the server-sided pack list from the overworld data storage. + * Returns an unmodifiable merged view of all loaded levels' packs. + * Used by {@link LevelPackData#setDirty} to populate sync messages. */ @NotNull - public static List getServerPacks(final @NotNull ServerLevel overworld) + static Map getServerPacksMap() { - final Map packs = overworld.getDataStorage().computeIfAbsent(FACTORY, DATA_NAME).packs; - return packs.values().stream().sorted(Comparator.comparing(Pack::getName)).toList(); + final Map merged = new LinkedHashMap<>(); + for (final LevelPackData data : loadedLevelData.values()) + { + merged.putAll(data.getOwnedPacks()); + } + return Collections.unmodifiableMap(merged); } /** - * Returns the client-sided pack list from the overworld data storage. + * Returns the client-sided pack list, sorted by name. */ @NotNull public static List getClientPacks() { - return clientPacks.values().stream().sorted(Comparator.comparing(Pack::getName)).toList(); + return clientPacks.values().stream().sorted(Comparator.comparing(Pack::name)).toList(); } /** @@ -151,36 +136,65 @@ public static void onClientSync(final @NotNull Map packs) clientPacks = new HashMap<>(packs); } - @Override - public void setDirty(boolean value) + /** + * Adds a new pack, owned by the overworld's data file. + * + * @return the new pack's ID, or {@code null} if a pack with that name already exists or the overworld is not loaded. + */ + @Nullable + public static String addPack(final @NotNull String name, final @NotNull Holder packType) { - super.setDirty(value); - if (value) + final String id = name.toLowerCase(Locale.ROOT).replace(" ", "_"); + + final LevelPackData target = loadedLevelData.get(Level.OVERWORLD); + if (target == null) { - PacketDistributor.sendToAllPlayers(new SyncPackManagerMessage(packs)); + return null; } - } - @Nullable - public String addPack(final @NotNull String name, final @NotNull Holder packType) - { - final String id = name.toLowerCase(Locale.ROOT).replace(" ", "_"); - if (packs.containsKey(id)) + if (target.getOwnedPacks().containsKey(id)) { return null; } - packs.put(id, new Pack(id, name, packType)); - setDirty(); + + final Pack pack = new Pack(id, name, packType); + packOwnerDimension.put(id, Level.OVERWORLD); + target.getOwnedPacksMutable().put(id, pack); + target.setDirty(); return id; } - public void addSchematic(final @NotNull String packId, final @NotNull PackSchematic schematic) + /** + * Returns the pack with the given ID, or {@code null} if not loaded. + */ + @Nullable + public static Pack getPack(final @NotNull String packId) + { + final ResourceKey owner = packOwnerDimension.get(packId); + final LevelPackData data = owner != null ? loadedLevelData.get(owner) : null; + return data != null ? data.getOwnedPacks().get(packId) : null; + } + + /** + * Adds or replaces a schematic in the given pack and marks that pack's owning level dirty. + * If a schematic with the same path, name, and level already exists it is replaced. + */ + public static void addSchematic(final @NotNull String packId, final @NotNull PackSchematic schematic) { - final Pack pack = packs.get(packId); - if (pack != null) + final ResourceKey owner = packOwnerDimension.get(packId); + final LevelPackData data = owner != null ? loadedLevelData.get(owner) : null; + if (data == null) { - pack.addSchematic(schematic); - setDirty(); + return; } + + final Pack pack = data.getOwnedPacks().get(packId); + if (pack == null) + { + return; + } + + data.getOwnedPacksMutable().put(packId, pack.withSchematic(schematic)); + data.setDirty(); } } diff --git a/src/main/java/com/ldtteam/structurize/index/models/Pack.java b/src/main/java/com/ldtteam/structurize/index/models/Pack.java index 5abc34c5d..dc4bb087d 100644 --- a/src/main/java/com/ldtteam/structurize/index/models/Pack.java +++ b/src/main/java/com/ldtteam/structurize/index/models/Pack.java @@ -14,18 +14,21 @@ import net.minecraft.resources.RegistryFixedCodec; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; -public class Pack +public record Pack( + @NotNull String id, + @NotNull String name, + @NotNull Holder type, + @NotNull Set schematics) { + private static final Codec> SCHEMATICS_CODEC = PackSchematic.CODEC.listOf().xmap(HashSet::new, ArrayList::new); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.STRING.fieldOf("id").forGetter(p -> p.id), - Codec.STRING.fieldOf("name").forGetter(p -> p.name), - RegistryFixedCodec.create(Registries.SCHEMATIC_INDEX_PACK_TYPES).fieldOf("type").forGetter(p -> p.type), - PackSchematic.CODEC.listOf().fieldOf("schematics").forGetter(p -> p.schematics) + Codec.STRING.fieldOf("id").forGetter(Pack::id), + Codec.STRING.fieldOf("name").forGetter(Pack::name), + RegistryFixedCodec.create(Registries.SCHEMATIC_INDEX_PACK_TYPES).fieldOf("type").forGetter(Pack::type), + SCHEMATICS_CODEC.fieldOf("schematics").forGetter(Pack::schematics) ).apply(instance, Pack::new)); public static final StreamCodec STREAM_CODEC = @@ -34,32 +37,14 @@ public class Pack public static final StreamCodec> MAP_STREAM_CODEC = ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, STREAM_CODEC); - @NotNull - private final String id; - - @NotNull - private final String name; - - @NotNull - private final Holder type; - - @NotNull - private final List schematics; - - public Pack(final @NotNull String id, final @NotNull String name, final @NotNull Holder type) + public Pack(@NotNull final String id, @NotNull final String name, @NotNull final Holder type) { - this.id = id; - this.name = name; - this.type = type; - this.schematics = new ArrayList<>(); + this(id, name, type, Collections.emptySet()); } - private Pack(final @NotNull String id, final @NotNull String name, final @NotNull Holder type, final @NotNull List schematics) + public Pack { - this.id = id; - this.name = name; - this.type = type; - this.schematics = new ArrayList<>(schematics); + schematics = Set.copyOf(schematics); } public static Pack load(final @NotNull CompoundTag compound, final @NotNull HolderLookup.Provider provider) @@ -72,32 +57,14 @@ public CompoundTag save(final @NotNull HolderLookup.Provider provider) return (CompoundTag) CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), this).getOrThrow(); } - @NotNull - public String getId() - { - return id; - } - - @NotNull - public String getName() - { - return name; - } - - @NotNull - public Holder getType() - { - return type; - } - - @NotNull - public List getSchematics() - { - return schematics; - } - - public void addSchematic(final @NotNull PackSchematic schematic) + /** + * Returns a new {@link Pack} with the given schematic added, replacing any existing schematic with the same identity. + */ + public Pack withSchematic(final @NotNull PackSchematic schematic) { - schematics.add(schematic); + final Set updated = new HashSet<>(schematics); + updated.remove(schematic); + updated.add(schematic); + return new Pack(id, name, type, updated); } } diff --git a/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java b/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java index d38007b0f..ac1f0a1d9 100644 --- a/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java +++ b/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java @@ -3,102 +3,42 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.BlockPos; -import net.minecraft.resources.ResourceKey; -import net.minecraft.world.level.Level; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Objects; import java.util.Optional; -public class PackSchematic +public record PackSchematic( + String path, + String name, + int level, + BlockPos pos1, + BlockPos pos2, + @Nullable BlockPos anchor) { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.STRING.fieldOf("path").forGetter(s -> s.path), - Codec.STRING.fieldOf("name").forGetter(s -> s.name), - Codec.INT.fieldOf("level").forGetter(s -> s.level), - BlockPos.CODEC.fieldOf("pos1").forGetter(s -> s.pos1), - BlockPos.CODEC.fieldOf("pos2").forGetter(s -> s.pos2), - BlockPos.CODEC.optionalFieldOf("anchor").forGetter(s -> Optional.ofNullable(s.anchor)), - Level.RESOURCE_KEY_CODEC.fieldOf("dimension").forGetter(s -> s.world) - ).apply(instance, (path, name, level, pos1, pos2, anchor, world) -> new PackSchematic(path, name, level, pos1, pos2, anchor.orElse(null), world))); + Codec.STRING.fieldOf("path").forGetter(PackSchematic::path), + Codec.STRING.fieldOf("name").forGetter(PackSchematic::name), + Codec.INT.fieldOf("level").forGetter(PackSchematic::level), + BlockPos.CODEC.fieldOf("pos1").forGetter(PackSchematic::pos1), + BlockPos.CODEC.fieldOf("pos2").forGetter(PackSchematic::pos2), + BlockPos.CODEC.optionalFieldOf("anchor").forGetter(s -> Optional.ofNullable(s.anchor())) + ).apply(instance, (path, name, level, pos1, pos2, anchor) -> new PackSchematic(path, name, level, pos1, pos2, anchor.orElse(null)))); - @NotNull - private final String path; - - @NotNull - private final String name; - - private final int level; - - @NotNull - private final BlockPos pos1; - - @NotNull - private final BlockPos pos2; - - @Nullable - private final BlockPos anchor; - - @NotNull - private final ResourceKey world; - - public PackSchematic( - final @NotNull String path, - final @NotNull String name, - final int level, - final @NotNull BlockPos pos1, - final @NotNull BlockPos pos2, - final @Nullable BlockPos anchor, - final @NotNull ResourceKey world) - { - this.path = path; - this.name = name; - this.level = level; - this.pos1 = pos1; - this.pos2 = pos2; - this.anchor = anchor; - this.world = world; - } - - @NotNull - public String getPath() - { - return path; - } - - @NotNull - public String getName() - { - return name; - } - - public int getLevel() - { - return level; - } - - @NotNull - public BlockPos getPos1() - { - return pos1; - } - - @NotNull - public BlockPos getPos2() - { - return pos2; - } - - @Nullable - public BlockPos getAnchor() + @Override + public boolean equals(final Object o) { - return anchor; + if (!(o instanceof PackSchematic other)) + { + return false; + } + return level == other.level && path.equals(other.path) && name.equals(other.name); } - @NotNull - public ResourceKey getWorld() + @Override + public int hashCode() { - return world; + return Objects.hash(path, name, level); } } diff --git a/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java b/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java index 1ed49b043..f81c9516c 100644 --- a/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java +++ b/src/main/java/com/ldtteam/structurize/items/ItemPackIndexDebug.java @@ -30,7 +30,7 @@ public ItemPackIndexDebug() { final List packs = level.isClientSide ? PackManager.getClientPacks() - : PackManager.getServerPacks(level.getServer().getLevel(Level.OVERWORLD)); + : PackManager.getServerPacks(); final String side = level.isClientSide ? "CLIENT" : "SERVER"; @@ -45,20 +45,19 @@ public ItemPackIndexDebug() { LOGGER.info("[PackIndex][{}] Pack: '{}' | type: {}", side, - pack.getName(), - pack.getType().unwrapKey().map(k -> k.location().toString()).orElse("unknown")); + pack.name(), + pack.type().unwrapKey().map(k -> k.location().toString()).orElse("unknown")); - for (final PackSchematic schematic : pack.getSchematics()) + for (final PackSchematic schematic : pack.schematics()) { - LOGGER.info("[PackIndex][{}] Schematic: '{}' | path: {} | level: {} | world: {} | pos1: {} | pos2: {} | anchor: {}", + LOGGER.info("[PackIndex][{}] Schematic: '{}' | path: {} | level: {} | pos1: {} | pos2: {} | anchor: {}", side, - schematic.getName(), - schematic.getPath(), - schematic.getLevel(), - schematic.getWorld().location(), - schematic.getPos1(), - schematic.getPos2(), - schematic.getAnchor()); + schematic.name(), + schematic.path(), + schematic.level(), + schematic.pos1(), + schematic.pos2(), + schematic.anchor()); } } } From 881ca417ff3f81e29ee5f5a246f877ae5a384b83 Mon Sep 17 00:00:00 2001 From: Thom van den Akker Date: Sun, 21 Jun 2026 13:33:07 +0200 Subject: [PATCH 3/5] Port some changes from part 2 into part 1 --- .../api/constants/TranslationConstants.java | 15 + .../blueprints/v1/IBlueprintDetails.java | 74 ++++ .../event/LifecycleSubscriber.java | 18 - .../index/packtypes/models/PackType.java | 110 +++++- .../packtypes/models/PackTypeRequirement.java | 128 ++++++- .../models/PackTypeSchematicRequirement.java | 341 ++++++++++++------ .../packtypes/models/SchematicPredicate.java | 23 ++ .../assets/structurize/lang/en_us.json | 10 +- 8 files changed, 577 insertions(+), 142 deletions(-) create mode 100644 src/main/java/com/ldtteam/structurize/blueprints/v1/IBlueprintDetails.java create mode 100644 src/main/java/com/ldtteam/structurize/index/packtypes/models/SchematicPredicate.java diff --git a/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java b/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java index 864805469..c275c67f0 100644 --- a/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java +++ b/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java @@ -18,6 +18,21 @@ public final class TranslationConstants @NonNls public static final String GUI_SWITCH_PACK_DISABLED_TEXT = "com.ldtteam.structurize.gui.switchpack.pack_disabled.hover_text"; + @NonNls + public static final String PACK_TYPE_VALIDATION_REQUIRED_SCHEMATIC = "com.ldtteam.structurize.pack_type.validation.missing_required_schematic"; + @NonNls + public static final String PACK_TYPE_VALIDATION_OPTIONAL_SCHEMATIC = "com.ldtteam.structurize.pack_type.validation.missing_optional_schematic"; + @NonNls + public static final String PACK_TYPE_VALIDATION_REQUIREMENT_PART_NAME = "com.ldtteam.structurize.pack_type.validation.requirement_part_name"; + @NonNls + public static final String PACK_TYPE_VALIDATION_REQUIREMENT_PART_PATH = "com.ldtteam.structurize.pack_type.validation.requirement_part_path"; + @NonNls + public static final String PACK_TYPE_VALIDATION_REQUIREMENT_PART_ANCHOR = "com.ldtteam.structurize.pack_type.validation.requirement_part_anchor"; + @NonNls + public static final String PACK_TYPE_VALIDATION_REQUIREMENT_PART_AND = "com.ldtteam.structurize.pack_type.validation.requirement_part_and"; + @NonNls + public static final String PACK_TYPE_VALIDATION_MISSING_LEVEL = "com.ldtteam.structurize.pack_type.validation.missing_level"; + private TranslationConstants() { //empty default diff --git a/src/main/java/com/ldtteam/structurize/blueprints/v1/IBlueprintDetails.java b/src/main/java/com/ldtteam/structurize/blueprints/v1/IBlueprintDetails.java new file mode 100644 index 000000000..467448f1f --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/blueprints/v1/IBlueprintDetails.java @@ -0,0 +1,74 @@ +package com.ldtteam.structurize.blueprints.v1; + +import net.minecraft.core.BlockPos; +import org.jetbrains.annotations.Nullable; + +/** + * Provides the details required to create a {@link Blueprint} from a world region + * via {@link BlueprintUtil#createBlueprint(IBlueprintDetails, net.minecraft.server.level.ServerLevel)}. + * + *

Implemented by both {@link com.ldtteam.structurize.index.models.PackSchematic} (for validation + * of stored schematics) and scan GUI providers (for live scans via {@link ScanUtil}). + */ +public interface IBlueprintDetails +{ + /** + * Returns one corner of the bounding box. + * + * @return the first corner position + */ + BlockPos getPos1(); + + /** + * Returns the opposite corner of the bounding box. + * + * @return the second corner position + */ + BlockPos getPos2(); + + /** + * Returns the relative folder path of the schematic, empty for root-level schematics. + * + * @return the schematic folder path, never {@code null} + */ + String getSchematicPath(); + + /** + * Returns the schematic file name without extension. + * + * @return the schematic name, never {@code null} + */ + String getSchematicName(); + + /** + * Returns the building level this schematic represents, or {@code null} if no level suffix + * should be appended to the file name. + * + * @return the schematic level, or {@code null} + */ + @Nullable + Integer getSchematicLevel(); + + /** + * Returns the full schematic path including the file name and optional level digit (without extension). + * For example: {@code "huts/miner2"} for level 2, or {@code "huts/miner"} when level is {@code null}. + * + * @return the full schematic path + */ + default String getFullSchematicPath() + { + final String path = getSchematicPath(); + final Integer level = getSchematicLevel(); + final String nameWithLevel = level != null ? getSchematicName() + level : getSchematicName(); + return path.isEmpty() ? nameWithLevel : path + "/" + nameWithLevel; + } + + /** + * Returns the world position of the anchor block, or {@code null} if none is set. + * + * @return the anchor position, or {@code null} + */ + @Nullable + BlockPos getAnchor(); + +} diff --git a/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java b/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java index a3324ed63..661055ea3 100644 --- a/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java +++ b/src/main/java/com/ldtteam/structurize/event/LifecycleSubscriber.java @@ -21,10 +21,6 @@ import net.neoforged.neoforge.registries.RegistryBuilder; import org.jetbrains.annotations.NotNull; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.Set; - import static com.ldtteam.structurize.api.Registries.SCHEMATIC_INDEX_PACK_TYPES; public class LifecycleSubscriber @@ -89,20 +85,6 @@ public static void onDatagen(@NotNull final GatherDataEvent event) generator.addProvider(event.includeClient(), new EntityTagProvider(event.getGenerator().getPackOutput(), Registries.ENTITY_TYPE, event.getLookupProvider(), event.getExistingFileHelper())); } - @SubscribeEvent(priority = EventPriority.LOWEST) - public static void registerCaps(final RegisterCapabilitiesEvent event) - { - final Set containerBlocks = Collections.newSetFromMap(new IdentityHashMap<>()); - for (final Block block : BuiltInRegistries.BLOCK) - { - if (event.isBlockRegistered(Capabilities.ItemHandler.BLOCK, block)) - { - containerBlocks.add(block); - } - } - PlacementHandlers.ContainerPlacementHandler.CONTAINERS = containerBlocks; - } - @SubscribeEvent public static void registerNewRegistries(final NewRegistryEvent event) { diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java index bc5481b5a..17fff9a48 100644 --- a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackType.java @@ -6,9 +6,16 @@ import java.util.ArrayList; import java.util.List; +/** + * Defines the type of schematic pack, including its display name and the requirements that packs + * and individual schematics of this type must satisfy. + * + *

Instances are registered via NeoForge's registry using {@link com.ldtteam.structurize.index.packtypes.PackTypesRegistry}. + * Use {@link Builder} to construct instances. + */ public class PackType { - private final ResourceLocation packId; + private final ResourceLocation id; private final Component name; @@ -17,51 +24,128 @@ public class PackType private final List schematicRequirements; private PackType( - final ResourceLocation packId, final Component name, final List packRequirements, + final ResourceLocation id, + final Component name, + final List packRequirements, final List schematicRequirements) { - this.packId = packId; + this.id = id; this.name = name; this.packRequirements = packRequirements; this.schematicRequirements = schematicRequirements; } + /** + * Returns the registry ID of this pack type. + * + * @return the resource location identifier + */ + public ResourceLocation getId() + { + return id; + } + + /** + * Returns the localized display name of this pack type shown in the UI. + * + * @return the display name component + */ + public Component getName() + { + return name; + } + + /** + * Returns the pack-level requirements that a {@link com.ldtteam.structurize.index.models.Pack} of this type must satisfy. + * These are checked against the set of schematics as a whole (e.g., required schematic names and level counts). + * + * @return an unmodifiable list of pack requirements + */ + public List getPackRequirements() + { + return packRequirements; + } + + /** + * Returns the per-schematic requirements evaluated against individual schematics when a full validation is run. + * + * @return an unmodifiable list of schematic requirements + */ + public List getSchematicRequirements() + { + return schematicRequirements; + } + + /** + * Builder for {@link PackType}. + */ public static class Builder { - private final ResourceLocation packId; + private final ResourceLocation id; private Component name; private final List packRequirements = new ArrayList<>(); - private final List schematicRequirements = new ArrayList<>(); - - public Builder(final ResourceLocation packId) + private final List schematicRequirements = new ArrayList<>(List.of( + PackTypeSchematicRequirement.ANCHOR_OUTSIDE_BOUNDS, + PackTypeSchematicRequirement.EXCEEDS_BLOCK_LIMIT + )); + + /** + * Creates a builder for a pack type with the given registry ID. + * + * @param id the resource location that will be used to register this pack type + */ + public Builder(final ResourceLocation id) { - this.packId = packId; + this.id = id; } + /** + * Sets the localized display name shown in the UI. + * + * @param name the display name component + * @return this builder + */ public Builder withName(final Component name) { this.name = name; return this; } - public Builder addPackRequirement(final PackTypeRequirement requirement) + /** + * Adds a pack-level requirement using the given builder. + * + * @param requirement the requirement builder to build and add + * @return this builder + */ + public Builder addPackRequirement(final PackTypeRequirement.Builder requirement) { - this.packRequirements.add(requirement); + this.packRequirements.add(requirement.build()); return this; } - public Builder addSchematicRequirement(final PackTypeSchematicRequirement requirement) + /** + * Adds a per-schematic requirement using the given builder. + * + * @param requirement the requirement builder to build and add + * @return this builder + */ + public Builder addSchematicRequirement(final PackTypeSchematicRequirement.Builder requirement) { - this.schematicRequirements.add(requirement); + this.schematicRequirements.add(requirement.build()); return this; } + /** + * Builds the {@link PackType}. + * + * @return the constructed pack type + */ public PackType build() { - return new PackType(this.packId, this.name, this.packRequirements, this.schematicRequirements); + return new PackType(this.id, this.name, this.packRequirements, this.schematicRequirements); } } } diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java index f5a1539f2..279c89cb6 100644 --- a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeRequirement.java @@ -1,20 +1,33 @@ package com.ldtteam.structurize.index.packtypes.models; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; + +import static com.ldtteam.structurize.api.constants.TranslationConstants.*; + /** * A requirement that the pack as a whole must satisfy. * - *

Requirements can assert that a schematic exists matching one of the following criteria: + *

Requirements assert that one or more schematics exist in the pack matching all the + * specified criteria: *

    *
  • A specific schematic name
  • *
  • A specific schematic path
  • *
  • A specific anchor block type
  • *
* - *

By default, requirements are mandatory, shown with an exclamation mark in the UI. - * Use {@link Builder#optional()} to display a requirement without marking it as mandatory. + *

If {@link #requiredLevelCount} is set, matching schematics must collectively cover each + * level from 1 through N. For example, a count of 3 requires level 1, 2, and 3 to each be + * present among matching schematics. If not set, at least one matching schematic at any level + * is sufficient. + * + *

By default, requirements are mandatory. Use {@link Builder#optional()} to mark a requirement + * as optional — it will still appear in the UI but will not be flagged as a hard error. */ public class PackTypeRequirement { @@ -30,6 +43,13 @@ public class PackTypeRequirement @Nullable private final String requiredPath; + /** + * If non-null, matching schematics must collectively cover each level from 1 through this value. + * If null, any single matching schematic at any level satisfies the requirement. + */ + @Nullable + private final Integer requiredLevelCount; + /** * If non-null, a schematic with this anchor block type must exist in the pack. */ @@ -41,14 +61,92 @@ public class PackTypeRequirement */ private final boolean optional; - private PackTypeRequirement(final @Nullable String requiredName, final @Nullable String requiredPath, final @Nullable BlockState requiredAnchor, final boolean optional) + private PackTypeRequirement( + final @Nullable String requiredName, + final @Nullable String requiredPath, + final @Nullable Integer requiredLevelCount, + final @Nullable BlockState requiredAnchor, + final boolean optional) { this.requiredName = requiredName; this.requiredPath = requiredPath; + this.requiredLevelCount = requiredLevelCount; this.requiredAnchor = requiredAnchor; this.optional = optional; } + @Nullable + public String getRequiredName() + { + return requiredName; + } + + @Nullable + public String getRequiredPath() + { + return requiredPath; + } + + @Nullable + public Integer getRequiredLevelCount() + { + return requiredLevelCount; + } + + @Nullable + public BlockState getRequiredAnchor() + { + return requiredAnchor; + } + + public boolean isOptional() + { + return optional; + } + + /** + * Builds a {@link Component} describing this requirement's failure when no matching schematic + * exists at all, including whether it was required or optional. + * + * @return the failure message component + */ + public Component getFailureDescription() + { + return Component.translatable(optional ? PACK_TYPE_VALIDATION_OPTIONAL_SCHEMATIC : PACK_TYPE_VALIDATION_REQUIRED_SCHEMATIC, buildCriteria()); + } + + /** + * Builds a {@link Component} describing a missing level for a requirement that is otherwise + * partially satisfied — i.e. some matching schematics exist, but the given level is absent. + * + * @param missingLevel the level number that is missing + * @return the missing-level message component + */ + public Component getMissingLevelDescription(final int missingLevel) + { + return Component.translatable(PACK_TYPE_VALIDATION_MISSING_LEVEL, missingLevel, buildCriteria()); + } + + private Component buildCriteria() + { + final List parts = new ArrayList<>(); + if (requiredName != null) + { + parts.add(Component.translatable(PACK_TYPE_VALIDATION_REQUIREMENT_PART_NAME, requiredName)); + } + if (requiredPath != null) + { + parts.add(Component.translatable(PACK_TYPE_VALIDATION_REQUIREMENT_PART_PATH, requiredPath)); + } + if (requiredAnchor != null) + { + parts.add(Component.translatable(PACK_TYPE_VALIDATION_REQUIREMENT_PART_ANCHOR, requiredAnchor)); + } + + final Component and = Component.translatable(PACK_TYPE_VALIDATION_REQUIREMENT_PART_AND); + return parts.stream().reduce((a, b) -> a.append(" ").append(and).append(" ").append(b)).orElse(Component.empty()); + } + /** * Builder for {@link PackTypeRequirement}. */ @@ -66,6 +164,12 @@ public static class Builder @Nullable private String requiredPath; + /** + * @see PackTypeRequirement#requiredLevelCount + */ + @Nullable + private Integer requiredLevelCount; + /** * @see PackTypeRequirement#requiredAnchor */ @@ -101,6 +205,20 @@ public Builder requiresPath(final String path) return this; } + /** + * Requires that matching schematics cover each level from 1 through {@code count}. + * For example, a count of 3 means level 1, 2, and 3 must each be present among matching + * schematics. If not called, any single matching schematic at any level is sufficient. + * + * @param count the number of consecutive levels required, starting from 1 + * @return this builder + */ + public Builder requiresLevelCount(final int count) + { + this.requiredLevelCount = count; + return this; + } + /** * Requires a schematic with the given anchor block type to exist in the pack. * @@ -131,7 +249,7 @@ public Builder optional() */ public PackTypeRequirement build() { - return new PackTypeRequirement(requiredName, requiredPath, requiredAnchor, optional); + return new PackTypeRequirement(requiredName, requiredPath, requiredLevelCount, requiredAnchor, optional); } } } diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java index 589103dd8..81aa7bb74 100644 --- a/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/PackTypeSchematicRequirement.java @@ -1,18 +1,19 @@ package com.ldtteam.structurize.index.packtypes.models; -import com.ldtteam.structurize.blueprints.v1.Blueprint; +import com.ldtteam.structurize.Structurize; +import com.ldtteam.structurize.api.BlockPosUtil; +import net.minecraft.network.chat.Component; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.structure.BoundingBox; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.function.Predicate; +import java.util.function.Supplier; -import static com.ldtteam.structurize.api.constants.Constants.GROUNDLEVEL_TAG; -import static com.ldtteam.structurize.blockentities.interfaces.IBlueprintDataProviderBE.TAG_BLUEPRINTDATA; +import static com.ldtteam.structurize.api.constants.TranslationConstants.ANCHOR_POS_OUTSIDE_SCHEMATIC; +import static com.ldtteam.structurize.api.constants.TranslationConstants.MAX_SCHEMATIC_SIZE_REACHED; /** * A requirement checked against individual schematics to determine whether they are "valid". @@ -21,40 +22,70 @@ *

    *
  • Schematic name
  • *
  • Schematic path
  • + *
  • Schematic level
  • *
  • Anchor block type
  • - *
  • One or more tags
  • + *
  • One or more tags on the anchor block
  • + *
  • Any number of custom {@link SchematicPredicate}s
  • *
* *

Matched schematics are then validated against: *

    *
  • One or more required tags on the anchor block
  • *
  • One or more blocks that must be present in the schematic
  • - *
  • Any number of custom predicates operating on the full {@link Blueprint} data
  • + *
  • Any number of custom {@link SchematicPredicate}s
  • *
* + *

Each check carries an optional {@link Component} message override. If absent, a default + * message is generated (e.g. including the tag name or block name). For additional checks + * the message is mandatory since no meaningful default can be generated. + * *

Each requirement carries a {@link PackTypeSchematicRequirementSeverity severity level} indicating * how problematic a violation is. */ public class PackTypeSchematicRequirement { - public static final PackTypeSchematicRequirement ANCHOR_NO_GROUND_LEVEL = new PackTypeSchematicRequirement.Builder().addRequiredTag(GROUNDLEVEL_TAG) - .addAdditionalBlueprintCheck(blueprint -> blueprint.getBlockInfoAsList() - .stream() - .filter(blockInfo -> blockInfo.hasTileEntityData() && blockInfo.getTileEntityData().contains(TAG_BLUEPRINTDATA)) - .toList() - .size() > 1) - .markAsIssue() + /** + * Fires when a schematic has an explicit anchor position that lies outside its bounding box. + */ + public static final PackTypeSchematicRequirement ANCHOR_OUTSIDE_BOUNDS = new PackTypeSchematicRequirement.Builder() + .requiresAdditionalCheck( + (blueprint, details) -> details.getAnchor() == null + || BlockPosUtil.isInbetween(details.getAnchor(), details.getPos1(), details.getPos2()), + Component.translatable(ANCHOR_POS_OUTSIDE_SCHEMATIC)) + .markAsError() .build(); - public static final PackTypeSchematicRequirement MULTIPLE_ANCHORS = new PackTypeSchematicRequirement.Builder().addRequiredTag(GROUNDLEVEL_TAG) - .addAdditionalBlueprintCheck(blueprint -> blueprint.getBlockInfoAsList() - .stream() - .filter(blockInfo -> blockInfo.hasTileEntityData() && blockInfo.getTileEntityData().contains(TAG_BLUEPRINTDATA)) - .toList() - .size() > 1) - .markAsIssue() + /** + * Fires when a schematic's bounding box exceeds the configured {@code schematicBlockLimit}. + */ + public static final PackTypeSchematicRequirement EXCEEDS_BLOCK_LIMIT = new PackTypeSchematicRequirement.Builder() + .requiresAdditionalCheck( + (blueprint, details) -> { + final BoundingBox box = BoundingBox.fromCorners(details.getPos1(), details.getPos2()); + return (long) box.getXSpan() * box.getYSpan() * box.getZSpan() <= Structurize.getConfig().getServer().schematicBlockLimit.get(); + }, + () -> Component.translatable(MAX_SCHEMATIC_SIZE_REACHED, Structurize.getConfig().getServer().schematicBlockLimit.get())) + .markAsError() .build(); + /** + * A required anchor tag paired with an optional message override. + * If {@code message} is {@code null} a default is generated from the tag name. + */ + public record TagCheck(@NotNull String tag, @Nullable Component message) {} + + /** + * A required block paired with its minimum count and an optional message override. + * If {@code message} is {@code null} a default is generated from the block name and counts. + */ + public record BlockCheck(@NotNull BlockState block, int count, @Nullable Component message) {} + + /** + * A required additional check paired with a mandatory failure message, since no meaningful + * default can be generated for an arbitrary predicate. + */ + public record AdditionalCheck(@NotNull SchematicPredicate predicate, @NotNull Supplier message) {} + /** * If non-null, this requirement only applies to schematics whose name matches this value. */ @@ -67,6 +98,12 @@ public class PackTypeSchematicRequirement @Nullable private final String matchesPath; + /** + * If non-null, this requirement only applies to schematics whose level matches this value. + */ + @Nullable + private final Integer matchesLevel; + /** * If non-null, this requirement only applies to schematics with this anchor block type. */ @@ -74,29 +111,36 @@ public class PackTypeSchematicRequirement private final BlockState matchesAnchor; /** - * If non-null, this requirement only applies to schematics that have all of these tags. + * If non-empty, this requirement only applies to schematics that have all of these tags on their anchor block. */ - @Nullable + @NotNull private final List matchesTags; + /** + * If non-empty, this requirement only applies to schematics for which all of these predicates + * return {@code true}. This is the blueprint-level equivalent of the tag/name/path/level/anchor + * filter criteria, for cases that cannot be expressed with those simpler filters. + */ + @NotNull + private final List matchesAdditionalChecks; + /** * Tags that must be present on the anchor block of any matched schematic. */ @NotNull - private final List requiredTags; + private final List requiredTags; /** - * Blocks that must be present in any matched schematic, mapped to their minimum required count. + * Blocks that must be present in any matched schematic, with their minimum required counts. */ @NotNull - private final Map requiredBlocks; + private final List requiredBlocks; /** - * Additional custom predicates evaluated against the full {@link Blueprint} of any matched schematic. - * All predicates must return {@code true} for the schematic to be considered valid. + * Custom predicates evaluated against any matched schematic, each with a mandatory failure message. */ @NotNull - private final List> additionalBlueprintChecks; + private final List requiredAdditionalChecks; /** * The severity of a violation of this requirement. @@ -106,80 +150,122 @@ public class PackTypeSchematicRequirement private PackTypeSchematicRequirement( final @Nullable String matchesName, final @Nullable String matchesPath, + final @Nullable Integer matchesLevel, final @Nullable BlockState matchesAnchor, - final @Nullable List matchesTags, - final @NotNull List requiredTags, - final @NotNull Map requiredBlocks, - final @NotNull List> additionalBlueprintChecks, + final @NotNull List matchesTags, + final @NotNull List matchesAdditionalChecks, + final @NotNull List requiredTags, + final @NotNull List requiredBlocks, + final @NotNull List requiredAdditionalChecks, final PackTypeSchematicRequirementSeverity warningSeverity) { this.matchesName = matchesName; this.matchesPath = matchesPath; + this.matchesLevel = matchesLevel; this.matchesAnchor = matchesAnchor; this.matchesTags = matchesTags; + this.matchesAdditionalChecks = matchesAdditionalChecks; this.requiredTags = requiredTags; this.requiredBlocks = requiredBlocks; - this.additionalBlueprintChecks = additionalBlueprintChecks; + this.requiredAdditionalChecks = requiredAdditionalChecks; this.warningSeverity = warningSeverity; } + @Nullable + public String getMatchesName() + { + return matchesName; + } + + @Nullable + public String getMatchesPath() + { + return matchesPath; + } + + @Nullable + public Integer getMatchesLevel() + { + return matchesLevel; + } + + @Nullable + public BlockState getMatchesAnchor() + { + return matchesAnchor; + } + + @NotNull + public List getMatchesTags() + { + return matchesTags; + } + + @NotNull + public List getMatchesAdditionalChecks() + { + return matchesAdditionalChecks; + } + + @NotNull + public List getRequiredTags() + { + return requiredTags; + } + + @NotNull + public List getRequiredBlocks() + { + return requiredBlocks; + } + + @NotNull + public List getRequiredAdditionalChecks() + { + return requiredAdditionalChecks; + } + + public PackTypeSchematicRequirementSeverity getWarningSeverity() + { + return warningSeverity; + } + /** * Builder for {@link PackTypeSchematicRequirement}. */ public static class Builder { - /** - * @see PackTypeSchematicRequirement#matchesName - */ @Nullable private String matchesName; - /** - * @see PackTypeSchematicRequirement#matchesPath - */ @Nullable private String matchesPath; - /** - * @see PackTypeSchematicRequirement#matchesAnchor - */ + @Nullable + private Integer matchesLevel; + @Nullable private BlockState matchesAnchor; - /** - * @see PackTypeSchematicRequirement#matchesTags - */ @NotNull private final List matchesTags = new ArrayList<>(); - /** - * @see PackTypeSchematicRequirement#requiredTags - */ @NotNull - private final List requiredTags = new ArrayList<>(); + private final List matchesAdditionalChecks = new ArrayList<>(); - /** - * @see PackTypeSchematicRequirement#requiredBlocks - */ @NotNull - private final Map requiredBlocks = new HashMap<>(); + private final List requiredTags = new ArrayList<>(); - /** - * @see PackTypeSchematicRequirement#additionalBlueprintChecks - */ @NotNull - private final List> additionalBlueprintChecks = new ArrayList<>(); + private final List requiredBlocks = new ArrayList<>(); + + @NotNull + private final List requiredAdditionalChecks = new ArrayList<>(); - /** - * @see PackTypeSchematicRequirement#warningSeverity - */ private PackTypeSchematicRequirementSeverity warningSeverity = PackTypeSchematicRequirementSeverity.INFORMATIONAL; /** * Restricts this requirement to schematics whose name matches the given value. - * - * @param name the schematic name to match - * @return this builder */ public Builder matchesName(final String name) { @@ -189,9 +275,6 @@ public Builder matchesName(final String name) /** * Restricts this requirement to schematics whose path matches the given value. - * - * @param path the schematic path to match - * @return this builder */ public Builder matchesPath(final String path) { @@ -199,11 +282,17 @@ public Builder matchesPath(final String path) return this; } + /** + * Restricts this requirement to schematics whose level matches the given value. + */ + public Builder matchesLevel(final Integer level) + { + this.matchesLevel = level; + return this; + } + /** * Restricts this requirement to schematics that use the given anchor block type. - * - * @param anchor the anchor block state to match - * @return this builder */ public Builder matchesAnchor(final BlockState anchor) { @@ -211,62 +300,105 @@ public Builder matchesAnchor(final BlockState anchor) return this; } + /** + * Restricts this requirement to schematics that have the given tag on their anchor block. + * May be called multiple times; all tags must be present for the requirement to apply. + */ + public Builder matchesTag(final String tag) + { + this.matchesTags.add(tag); + return this; + } + + /** + * Restricts this requirement to schematics for which the given predicate returns {@code true}. + * May be called multiple times; all predicates must match for the requirement to apply. + */ + public Builder matchesAdditionalCheck(final SchematicPredicate predicate) + { + this.matchesAdditionalChecks.add(predicate); + return this; + } + + /** + * Requires the anchor block of a matched schematic to have the given tag. + * Uses a default generated failure message. + */ + public Builder requiresTag(final String tag) + { + return requiresTag(tag, null); + } + /** * Requires the anchor block of a matched schematic to have the given tag. - * May be called multiple times to require several tags. - * - * @param tag the tag that must be present on the anchor block - * @return this builder + * Uses the provided message on failure instead of the default generated one. */ - public Builder addRequiredTag(final String tag) + public Builder requiresTag(final String tag, final Component message) { - this.requiredTags.add(tag); + this.requiredTags.add(new TagCheck(tag, message)); return this; } /** * Requires at least one instance of the given block to be present in a matched schematic. - * Equivalent to {@code addRequiredBlock(block, 1)}. - * - * @param block the block that must be present - * @return this builder + * Uses a default generated failure message. */ - public Builder addRequiredBlock(final BlockState block) + public Builder requiresBlock(final BlockState block) { - return addRequiredBlock(block, 1); + return requiresBlock(block, 1, null); + } + + /** + * Requires at least one instance of the given block to be present in a matched schematic. + * Uses the provided message on failure instead of the default generated one. + */ + public Builder requiresBlock(final BlockState block, final Component message) + { + return requiresBlock(block, 1, message); } /** * Requires at least {@code count} instances of the given block to be present in a matched schematic. - * - * @param block the block that must be present - * @param count the minimum number of occurrences required - * @return this builder + * Uses a default generated failure message. + */ + public Builder requiresBlock(final BlockState block, final int count) + { + return requiresBlock(block, count, null); + } + + /** + * Requires at least {@code count} instances of the given block to be present in a matched schematic. + * Uses the provided message on failure instead of the default generated one. + */ + public Builder requiresBlock(final BlockState block, final int count, final @Nullable Component message) + { + this.requiredBlocks.add(new BlockCheck(block, count, message)); + return this; + } + + /** + * Adds a custom predicate evaluated against any matched schematic. + * The message is mandatory since no meaningful default can be generated for an arbitrary predicate. */ - public Builder addRequiredBlock(final BlockState block, final int count) + public Builder requiresAdditionalCheck(final SchematicPredicate predicate, final Component message) { - this.requiredBlocks.put(block, count); + this.requiredAdditionalChecks.add(new AdditionalCheck(predicate, () -> message)); return this; } /** - * Adds a custom predicate evaluated against the full {@link Blueprint} of any matched schematic. - * All added predicates must return {@code true} for the schematic to be considered valid. - * May be called multiple times to add several checks. - * - * @param additionalCheck a predicate receiving the full blueprint data - * @return this builder + * Adds a custom predicate evaluated against any matched schematic, with a lazily-evaluated + * message supplier. Use this when the message content depends on runtime state (e.g. config values) + * that is not available at class initialization time. */ - public Builder addAdditionalBlueprintCheck(final Predicate additionalCheck) + public Builder requiresAdditionalCheck(final SchematicPredicate predicate, final Supplier message) { - this.additionalBlueprintChecks.add(additionalCheck); + this.requiredAdditionalChecks.add(new AdditionalCheck(predicate, message)); return this; } /** * Sets the warning severity to {@link PackTypeSchematicRequirementSeverity#ISSUE}. - * - * @return this builder */ public Builder markAsIssue() { @@ -276,8 +408,6 @@ public Builder markAsIssue() /** * Sets the warning severity to {@link PackTypeSchematicRequirementSeverity#ERROR}. - * - * @return this builder */ public Builder markAsError() { @@ -287,18 +417,19 @@ public Builder markAsError() /** * Builds the {@link PackTypeSchematicRequirement}. - * - * @return the constructed requirement */ public PackTypeSchematicRequirement build() { - return new PackTypeSchematicRequirement(matchesName, + return new PackTypeSchematicRequirement( + matchesName, matchesPath, + matchesLevel, matchesAnchor, matchesTags, + matchesAdditionalChecks, requiredTags, requiredBlocks, - additionalBlueprintChecks, + requiredAdditionalChecks, warningSeverity); } } diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/models/SchematicPredicate.java b/src/main/java/com/ldtteam/structurize/index/packtypes/models/SchematicPredicate.java new file mode 100644 index 000000000..a6cfa9058 --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/models/SchematicPredicate.java @@ -0,0 +1,23 @@ +package com.ldtteam.structurize.index.packtypes.models; + +import com.ldtteam.structurize.blueprints.v1.Blueprint; +import com.ldtteam.structurize.blueprints.v1.IBlueprintDetails; + +/** + * A predicate evaluated against a schematic's {@link Blueprint} and its associated {@link IBlueprintDetails}. + * + *

Used both as a filter (to determine whether a {@link PackTypeSchematicRequirement} applies to a + * given schematic) and as a content check (to determine whether a matched schematic is valid). + */ +@FunctionalInterface +public interface SchematicPredicate +{ + /** + * Evaluates this predicate against the given blueprint and its details. + * + * @param blueprint the blueprint data + * @param details the blueprint details providing path, name, level, and anchor + * @return {@code true} if the predicate is satisfied + */ + boolean test(Blueprint blueprint, IBlueprintDetails details); +} \ No newline at end of file diff --git a/src/main/resources/assets/structurize/lang/en_us.json b/src/main/resources/assets/structurize/lang/en_us.json index 18610a1f3..9b5f5c211 100644 --- a/src/main/resources/assets/structurize/lang/en_us.json +++ b/src/main/resources/assets/structurize/lang/en_us.json @@ -238,5 +238,13 @@ "com.ldtteam.tag.tooltip.invisible": "Hide Blueprint in Buildtool like Integrated Buildings or Mineshafts", "com.ldtteam.structurize.gui.switchpack.authors": "Authors: %s", "com.ldtteam.structurize.gui.switchpack.list.empty": "There are no structure packs found, please install some", - "com.ldtteam.structurize.gui.switchpack.pack_disabled.hover_text": "This pack is disabled because it only exists on the client. In order to use this pack, the server must enable the config option '%s' on the server." + "com.ldtteam.structurize.gui.switchpack.pack_disabled.hover_text": "This pack is disabled because it only exists on the client. In order to use this pack, the server must enable the config option '%s' on the server.", + + "com.ldtteam.structurize.pack_type.validation.missing_required_schematic": "Pack is missing required schematic with %s", + "com.ldtteam.structurize.pack_type.validation.missing_optional_schematic": "Pack is missing optional schematic with %s", + "com.ldtteam.structurize.pack_type.validation.requirement_part_name": "name \"%s\"", + "com.ldtteam.structurize.pack_type.validation.requirement_part_path": "path \"%s\"", + "com.ldtteam.structurize.pack_type.validation.requirement_part_anchor": "anchor \"%s\"", + "com.ldtteam.structurize.pack_type.validation.requirement_part_and": "and", + "com.ldtteam.structurize.pack_type.validation.missing_level": "Missing level %s for schematic with %s" } From da199225c310c8de9fa25e185422820a7b4f574e Mon Sep 17 00:00:00 2001 From: Thom van den Akker Date: Sun, 21 Jun 2026 13:49:00 +0200 Subject: [PATCH 4/5] Port some changes back --- .../commands/PackIndexCommand.java | 18 ++-- .../structurize/index/PackManager.java | 11 ++- .../PackSchematicValidationCollector.java | 45 +++++++++ .../index/models/PackSchematic.java | 61 +++++++++++- .../models/PackSchematicValidationState.java | 93 +++++++++++++++++++ .../index/models/PackValidationState.java | 75 +++++++++++++++ 6 files changed, 285 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/ldtteam/structurize/index/PackSchematicValidationCollector.java create mode 100644 src/main/java/com/ldtteam/structurize/index/models/PackSchematicValidationState.java create mode 100644 src/main/java/com/ldtteam/structurize/index/models/PackValidationState.java diff --git a/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java b/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java index b4ce757a5..7f47f7345 100644 --- a/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java +++ b/src/main/java/com/ldtteam/structurize/commands/PackIndexCommand.java @@ -3,6 +3,7 @@ import com.ldtteam.structurize.index.PackManager; import com.ldtteam.structurize.index.models.PackSchematic; +import com.ldtteam.structurize.index.models.PackSchematicValidationState; import com.ldtteam.structurize.index.packtypes.PackTypesRegistry; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; @@ -10,6 +11,7 @@ import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands.CommandSelection; import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -29,12 +31,12 @@ protected static LiteralArgumentBuilder build() return newLiteral(NAME) .then(newLiteral("add") .then(newLiteral("pack") - .then(newArgument("packName", StringArgumentType.word()) - .executes(ctx -> addPack(ctx.getSource(), StringArgumentType.getString(ctx, "packName"))))) + .then(newArgument("packName", StringArgumentType.string()) + .executes(ctx -> addPack(ctx.getSource(), StringArgumentType.getString(ctx, "packName"), ctx.getSource().getLevel())))) .then(newLiteral("schematic") - .then(newArgument("packId", StringArgumentType.word()) - .then(newArgument("schematicName", StringArgumentType.word()) - .then(newArgument("path", StringArgumentType.word()) + .then(newArgument("packId", StringArgumentType.string()) + .then(newArgument("schematicName", StringArgumentType.string()) + .then(newArgument("path", StringArgumentType.string()) .then(newArgument("level", IntegerArgumentType.integer(0)) .executes(ctx -> addSchematic( ctx.getSource(), @@ -44,9 +46,9 @@ protected static LiteralArgumentBuilder build() IntegerArgumentType.getInteger(ctx, "level"))))))))); } - private static int addPack(final CommandSourceStack source, final String packName) + private static int addPack(final CommandSourceStack source, final String packName, final ServerLevel level) { - final String newId = PackManager.addPack(packName, PackTypesRegistry.DEFAULT_PACK_TYPE); + final String newId = PackManager.addPack(packName, PackTypesRegistry.DEFAULT_PACK_TYPE, level); if (newId != null) { @@ -74,7 +76,7 @@ private static int addSchematic( return 0; } - final PackSchematic schematic = new PackSchematic(path, schematicName, level, BlockPos.ZERO, BlockPos.ZERO, null); + final PackSchematic schematic = new PackSchematic(path, schematicName, level, BlockPos.ZERO, BlockPos.ZERO, null, new PackSchematicValidationState()); PackManager.addSchematic(packId, schematic); LOGGER.info("[PackIndex] Added schematic '{}' to pack '{}'", schematicName, packId); diff --git a/src/main/java/com/ldtteam/structurize/index/PackManager.java b/src/main/java/com/ldtteam/structurize/index/PackManager.java index e35ccf5db..487b9eee0 100644 --- a/src/main/java/com/ldtteam/structurize/index/PackManager.java +++ b/src/main/java/com/ldtteam/structurize/index/PackManager.java @@ -137,16 +137,16 @@ public static void onClientSync(final @NotNull Map packs) } /** - * Adds a new pack, owned by the overworld's data file. + * Adds a new pack, owned by the given level's data file. * - * @return the new pack's ID, or {@code null} if a pack with that name already exists or the overworld is not loaded. + * @return the new pack's ID, or {@code null} if a pack with that name already exists or the level is not loaded. */ @Nullable - public static String addPack(final @NotNull String name, final @NotNull Holder packType) + public static String addPack(final @NotNull String name, final @NotNull Holder packType, final ServerLevel level) { final String id = name.toLowerCase(Locale.ROOT).replace(" ", "_"); - final LevelPackData target = loadedLevelData.get(Level.OVERWORLD); + final LevelPackData target = loadedLevelData.get(level.dimension()); if (target == null) { return null; @@ -158,8 +158,9 @@ public static String addPack(final @NotNull String name, final @NotNull Holder

The constructor is package-private, so only classes within {@code com.ldtteam.structurize.index} + * can create instances. This prevents external code from getting write access to a + * {@link com.ldtteam.structurize.index.models.PackSchematicValidationState}. + */ +public final class PackSchematicValidationCollector +{ + private final Map> issues = new EnumMap<>(PackTypeSchematicRequirementSeverity.class); + + PackSchematicValidationCollector() {} + + /** + * Appends an issue message at the given severity level. + * + * @param severity the severity of the issue + * @param message the issue message + */ + public void addIssue(final PackTypeSchematicRequirementSeverity severity, final Component message) + { + issues.computeIfAbsent(severity, k -> new ArrayList<>()).add(message); + } + + /** + * Returns the accumulated issues map. + * + * @return the issues map + */ + public Map> getIssues() + { + return issues; + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java b/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java index ac1f0a1d9..865b4c731 100644 --- a/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java +++ b/src/main/java/com/ldtteam/structurize/index/models/PackSchematic.java @@ -1,30 +1,81 @@ package com.ldtteam.structurize.index.models; +import com.ldtteam.structurize.blueprints.v1.IBlueprintDetails; import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.BlockPos; -import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.Optional; +/** + * Immutable record representing a single schematic entry within a {@link Pack}. + * + *

Equality and hashing are based on {@link #path}, {@link #name}, and {@link #level} so that + * replacing a schematic (e.g. after rescanning) correctly overwrites the old entry in a set. + * + * @param path the relative folder path of the schematic file, may be empty for root-level schematics + * @param name the schematic file name (without extension) + * @param level the building level this schematic represents (1-based); defaults to 1 when omitted from storage + * @param pos1 one corner of the bounding box in world space + * @param pos2 the opposite corner of the bounding box in world space + * @param anchor the world position of the anchor block, or {@code null} if not set + * @param validationState the most recent per-schematic validation result + */ public record PackSchematic( String path, String name, int level, BlockPos pos1, BlockPos pos2, - @Nullable BlockPos anchor) + Optional anchor, + PackSchematicValidationState validationState) implements IBlueprintDetails { public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.STRING.fieldOf("path").forGetter(PackSchematic::path), Codec.STRING.fieldOf("name").forGetter(PackSchematic::name), - Codec.INT.fieldOf("level").forGetter(PackSchematic::level), + Codec.INT.optionalFieldOf("level", 1).forGetter(PackSchematic::level), BlockPos.CODEC.fieldOf("pos1").forGetter(PackSchematic::pos1), BlockPos.CODEC.fieldOf("pos2").forGetter(PackSchematic::pos2), - BlockPos.CODEC.optionalFieldOf("anchor").forGetter(s -> Optional.ofNullable(s.anchor())) - ).apply(instance, (path, name, level, pos1, pos2, anchor) -> new PackSchematic(path, name, level, pos1, pos2, anchor.orElse(null)))); + BlockPos.CODEC.optionalFieldOf("anchor").forGetter(PackSchematic::anchor), + PackSchematicValidationState.CODEC.fieldOf("validation-state").forGetter(PackSchematic::validationState) + ).apply(instance, PackSchematic::new)); + @Override + public BlockPos getPos1() + { + return pos1; + } + + @Override + public BlockPos getPos2() + { + return pos2; + } + + @Override + public String getSchematicPath() + { + return path; + } + + @Override + public String getSchematicName() + { + return name; + } + + @Override + public Integer getSchematicLevel() + { + return level; + } + + @Override + public BlockPos getAnchor() + { + return anchor.orElse(null); + } @Override public boolean equals(final Object o) diff --git a/src/main/java/com/ldtteam/structurize/index/models/PackSchematicValidationState.java b/src/main/java/com/ldtteam/structurize/index/models/PackSchematicValidationState.java new file mode 100644 index 000000000..ce305d41e --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/models/PackSchematicValidationState.java @@ -0,0 +1,93 @@ +package com.ldtteam.structurize.index.models; + +import com.ldtteam.structurize.index.PackSchematicValidationCollector; +import com.ldtteam.structurize.index.packtypes.models.PackTypeSchematicRequirement; +import com.ldtteam.structurize.index.packtypes.models.PackTypeSchematicRequirementSeverity; +import com.mojang.serialization.Codec; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Holds the per-schematic result of a validation run, keyed by + * {@link PackTypeSchematicRequirementSeverity}. + * + *

Unlike {@link PackValidationState} (which holds pack-level warnings and errors), + * this class captures issues found against individual schematics by + * {@link PackTypeSchematicRequirement}s. + * + *

Instances are written to via {@link #apply(PackSchematicValidationCollector)}, which requires + * a {@link PackSchematicValidationCollector} that can only be constructed within + * {@code com.ldtteam.structurize.index}. + */ +public class PackSchematicValidationState +{ + private static final Codec>> ISSUES_CODEC = + Codec.unboundedMap( + Codec.STRING.xmap(PackTypeSchematicRequirementSeverity::valueOf, Enum::name), + ComponentSerialization.CODEC.listOf() + ); + + public static final Codec CODEC = ISSUES_CODEC.xmap(issues -> { + final PackSchematicValidationState state = new PackSchematicValidationState(); + state.issues = Map.copyOf(issues); + return state; + }, s -> s.issues); + + private Map> issues = Collections.emptyMap(); + + /** + * Commits the issues accumulated in the given {@link PackSchematicValidationCollector} into + * this state, replacing any previously stored results. + * + *

Only classes within {@code com.ldtteam.structurize.index} can create a + * {@link PackSchematicValidationCollector}, so this method cannot be meaningfully called by + * external code. + * + * @param collector the collector holding the accumulated issues + */ + public void apply(final PackSchematicValidationCollector collector) + { + this.issues = Map.copyOf(collector.getIssues()); + } + + /** + * Returns the full issues map, keyed by severity. + * + * @return an unmodifiable map of severity to issue messages + */ + public Map> getIssues() + { + return issues; + } + + /** + * Returns all issue messages for the given severity, or an empty list if none exist. + * + * @param severity the severity to query + * @return the list of issue messages for the given severity, never {@code null} + */ + public List getIssues(final PackTypeSchematicRequirementSeverity severity) + { + return issues.getOrDefault(severity, Collections.emptyList()); + } + + @Override + public boolean equals(final Object o) + { + if (!(o instanceof PackSchematicValidationState other)) + { + return false; + } + return issues.equals(other.issues); + } + + @Override + public int hashCode() + { + return issues.hashCode(); + } +} diff --git a/src/main/java/com/ldtteam/structurize/index/models/PackValidationState.java b/src/main/java/com/ldtteam/structurize/index/models/PackValidationState.java new file mode 100644 index 000000000..40dc372be --- /dev/null +++ b/src/main/java/com/ldtteam/structurize/index/models/PackValidationState.java @@ -0,0 +1,75 @@ +package com.ldtteam.structurize.index.models; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Holds the result of a validation run for a {@link Pack}. + */ +public class PackValidationState +{ + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ComponentSerialization.CODEC.listOf().optionalFieldOf("optional-warnings", List.of()).forGetter(s -> s.optionalWarnings), + ComponentSerialization.CODEC.listOf().optionalFieldOf("required-errors", List.of()).forGetter(s -> s.requiredErrors) + ).apply(instance, (optional, required) -> { + final PackValidationState state = new PackValidationState(); + state.setOptionalWarnings(optional); + state.setRequiredErrors(required); + return state; + })); + + @NotNull + private List optionalWarnings = new ArrayList<>(); + + @NotNull + private List requiredErrors = new ArrayList<>(); + + /** + * Replaces the list of optional (non-blocking) warnings with the given list. + * + * @param warnings the new optional warning messages + */ + public void setOptionalWarnings(final @NotNull List warnings) + { + this.optionalWarnings = new ArrayList<>(warnings); + } + + /** + * Replaces the list of required (blocking) errors with the given list. + * + * @param errors the new required error messages + */ + public void setRequiredErrors(final @NotNull List errors) + { + this.requiredErrors = new ArrayList<>(errors); + } + + /** + * Returns an unmodifiable view of the optional warning messages produced by the last validation run. + * + * @return the optional warnings, never {@code null} + */ + @NotNull + public List getOptionalWarnings() + { + return Collections.unmodifiableList(optionalWarnings); + } + + /** + * Returns an unmodifiable view of the required error messages produced by the last validation run. + * + * @return the required errors, never {@code null} + */ + @NotNull + public List getRequiredErrors() + { + return Collections.unmodifiableList(requiredErrors); + } +} From 9ce428047481fa4b76afdea6f3ea7feb529b1e2e Mon Sep 17 00:00:00 2001 From: Thom van den Akker Date: Sun, 21 Jun 2026 14:01:57 +0200 Subject: [PATCH 5/5] Port some changes back --- .../api/constants/TranslationConstants.java | 2 +- .../structurize/index/LevelPackData.java | 12 ++++ .../structurize/index/PackManager.java | 47 ++++++++++--- .../structurize/index/models/Pack.java | 67 ++++++++++++++++--- .../index/packtypes/PackTypesRegistry.java | 24 ++++++- 5 files changed, 131 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java b/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java index c275c67f0..eb9dcc803 100644 --- a/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java +++ b/src/main/java/com/ldtteam/structurize/api/constants/TranslationConstants.java @@ -14,7 +14,7 @@ public final class TranslationConstants public static final String ANCHOR_POS_OUTSIDE_SCHEMATIC = "item.sceptersteel.badanchorpos"; @NonNls - public static final String GUI_SWITCH_PACK_AUTHORS = "com.ldtteam.structurize.gui.switchpack.authors"; + public static final String GUI_SWITCH_PACK_AUTHORS = "com.ldtteam.structurize.gui.switchpack.authors"; @NonNls public static final String GUI_SWITCH_PACK_DISABLED_TEXT = "com.ldtteam.structurize.gui.switchpack.pack_disabled.hover_text"; diff --git a/src/main/java/com/ldtteam/structurize/index/LevelPackData.java b/src/main/java/com/ldtteam/structurize/index/LevelPackData.java index 2639b50f6..f73971bf1 100644 --- a/src/main/java/com/ldtteam/structurize/index/LevelPackData.java +++ b/src/main/java/com/ldtteam/structurize/index/LevelPackData.java @@ -68,16 +68,28 @@ public CompoundTag save(final @NotNull CompoundTag tag, final @NotNull HolderLoo return tag; } + /** + * Marks this data as dirty and, if dirtied, invalidates the merged pack cache and broadcasts + * an updated {@link SyncPackManagerMessage} to all connected players. + * + * @param value {@code true} to mark dirty and trigger a sync; {@code false} to clear the flag + */ @Override public void setDirty(final boolean value) { super.setDirty(value); if (value) { + PackManager.invalidateServerPacksSorted(); PacketDistributor.sendToAllPlayers(new SyncPackManagerMessage(PackManager.getServerPacksMap())); } } + /** + * Returns the dimension key that identifies the {@link net.minecraft.server.level.ServerLevel} this data belongs to. + * + * @return the dimension key + */ public ResourceKey getDimension() { return dimension; diff --git a/src/main/java/com/ldtteam/structurize/index/PackManager.java b/src/main/java/com/ldtteam/structurize/index/PackManager.java index 487b9eee0..fbf0210b5 100644 --- a/src/main/java/com/ldtteam/structurize/index/PackManager.java +++ b/src/main/java/com/ldtteam/structurize/index/PackManager.java @@ -34,10 +34,20 @@ public final class PackManager */ private static final Map, LevelPackData> loadedLevelData = new HashMap<>(); + /** + * Cached sorted view of the server-side merged pack map, invalidated whenever packs are added or levels load/unload. + */ + private static List serverPacksSorted = List.of(); + /** * Client-side pack list, populated via {@link SyncPackManagerMessage}. */ - private static Map clientPacks = new HashMap<>(); + private static final Map clientPacks = new HashMap<>(); + + /** + * Cached sorted view of {@link #clientPacks}, invalidated whenever the map changes. + */ + private static List clientPacksSorted = List.of(); private PackManager() {} @@ -60,6 +70,8 @@ public static void onLevelLoad(final LevelEvent.Load event) { packOwnerDimension.put(packId, dimension); } + + invalidateServerPacksSorted(); } /** @@ -76,9 +88,12 @@ public static void onLevelUnload(final LevelEvent.Unload event) loadedLevelData.remove(dimension); packOwnerDimension.entrySet().removeIf(entry -> entry.getValue().equals(dimension)); + invalidateServerPacksSorted(); + if (loadedLevelData.isEmpty()) { clientPacks.clear(); + clientPacksSorted = List.of(); } } @@ -101,7 +116,16 @@ public static void onPlayerJoin(final PlayerEvent.PlayerLoggedInEvent event) @NotNull public static List getServerPacks() { - return getServerPacksMap().values().stream().sorted(Comparator.comparing(Pack::name)).toList(); + return serverPacksSorted; + } + + /** + * Rebuilds the cached sorted server pack list from the current merged map. + * Called whenever packs are added or levels load/unload. + */ + static void invalidateServerPacksSorted() + { + serverPacksSorted = getServerPacksMap().values().stream().sorted(Comparator.comparing(Pack::name)).toList(); } /** @@ -125,7 +149,7 @@ static Map getServerPacksMap() @NotNull public static List getClientPacks() { - return clientPacks.values().stream().sorted(Comparator.comparing(Pack::name)).toList(); + return clientPacksSorted; } /** @@ -133,7 +157,9 @@ public static List getClientPacks() */ public static void onClientSync(final @NotNull Map packs) { - clientPacks = new HashMap<>(packs); + clientPacks.clear(); + clientPacks.putAll(packs); + clientPacksSorted = clientPacks.values().stream().sorted(Comparator.comparing(Pack::name)).toList(); } /** @@ -171,8 +197,7 @@ public static String addPack(final @NotNull String name, final @NotNull Holder

owner = packOwnerDimension.get(packId); - final LevelPackData data = owner != null ? loadedLevelData.get(owner) : null; + final LevelPackData data = getLevelPackData(packId); return data != null ? data.getOwnedPacks().get(packId) : null; } @@ -182,8 +207,7 @@ public static Pack getPack(final @NotNull String packId) */ public static void addSchematic(final @NotNull String packId, final @NotNull PackSchematic schematic) { - final ResourceKey owner = packOwnerDimension.get(packId); - final LevelPackData data = owner != null ? loadedLevelData.get(owner) : null; + final LevelPackData data = getLevelPackData(packId); if (data == null) { return; @@ -198,4 +222,11 @@ public static void addSchematic(final @NotNull String packId, final @NotNull Pac data.getOwnedPacksMutable().put(packId, pack.withSchematic(schematic)); data.setDirty(); } + + @Nullable + private static LevelPackData getLevelPackData(final @NotNull String packId) + { + final ResourceKey owner = packOwnerDimension.get(packId); + return owner != null ? loadedLevelData.get(owner) : null; + } } diff --git a/src/main/java/com/ldtteam/structurize/index/models/Pack.java b/src/main/java/com/ldtteam/structurize/index/models/Pack.java index dc4bb087d..8a063ac31 100644 --- a/src/main/java/com/ldtteam/structurize/index/models/Pack.java +++ b/src/main/java/com/ldtteam/structurize/index/models/Pack.java @@ -16,30 +16,46 @@ import java.util.*; +/** + * Immutable record representing a schematic pack — a named, typed collection of {@link PackSchematic}s + * together with a cached {@link PackValidationState}. + * + *

Packs are owned by a specific {@link net.minecraft.server.level.ServerLevel} via + * {@link com.ldtteam.structurize.index.LevelPackData} and are merged into a global view by + * {@link com.ldtteam.structurize.index.PackManager}. + * + *

Equality and hashing are based solely on {@link #id} so that a pack can be located in sets + * and maps without caring about its mutable validation state. + */ public record Pack( @NotNull String id, @NotNull String name, @NotNull Holder type, - @NotNull Set schematics) + @NotNull Set schematics, + @NotNull PackValidationState validationState) { private static final Codec> SCHEMATICS_CODEC = PackSchematic.CODEC.listOf().xmap(HashSet::new, ArrayList::new); - public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( - Codec.STRING.fieldOf("id").forGetter(Pack::id), + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group(Codec.STRING.fieldOf("id").forGetter(Pack::id), Codec.STRING.fieldOf("name").forGetter(Pack::name), RegistryFixedCodec.create(Registries.SCHEMATIC_INDEX_PACK_TYPES).fieldOf("type").forGetter(Pack::type), - SCHEMATICS_CODEC.fieldOf("schematics").forGetter(Pack::schematics) - ).apply(instance, Pack::new)); + SCHEMATICS_CODEC.fieldOf("schematics").forGetter(Pack::schematics), + PackValidationState.CODEC.fieldOf("validation-state").forGetter(Pack::validationState)).apply(instance, Pack::new)); - public static final StreamCodec STREAM_CODEC = - ByteBufCodecs.fromCodecWithRegistries(CODEC); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistries(CODEC); - public static final StreamCodec> MAP_STREAM_CODEC = - ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, STREAM_CODEC); + public static final StreamCodec> MAP_STREAM_CODEC = ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, STREAM_CODEC); + /** + * Convenience constructor for a new, empty pack with no schematics and a fresh validation state. + * + * @param id the unique pack identifier (typically a lowercase, underscore-separated string) + * @param name the human-readable display name + * @param type the {@link PackType} governing which schematics and requirements apply to this pack + */ public Pack(@NotNull final String id, @NotNull final String name, @NotNull final Holder type) { - this(id, name, type, Collections.emptySet()); + this(id, name, type, Collections.emptySet(), new PackValidationState()); } public Pack @@ -47,11 +63,24 @@ public Pack(@NotNull final String id, @NotNull final String name, @NotNull final schematics = Set.copyOf(schematics); } + /** + * Deserializes a {@link Pack} from the given NBT compound using the provided registry context. + * + * @param compound the NBT compound to read from + * @param provider the registry lookup provider for holder resolution + * @return the deserialized pack + */ public static Pack load(final @NotNull CompoundTag compound, final @NotNull HolderLookup.Provider provider) { return CODEC.parse(provider.createSerializationContext(NbtOps.INSTANCE), compound).getOrThrow(); } + /** + * Serializes this pack to an NBT compound using the provided registry context. + * + * @param provider the registry lookup provider for holder serialization + * @return the serialized NBT compound + */ public CompoundTag save(final @NotNull HolderLookup.Provider provider) { return (CompoundTag) CODEC.encodeStart(provider.createSerializationContext(NbtOps.INSTANCE), this).getOrThrow(); @@ -65,6 +94,22 @@ public Pack withSchematic(final @NotNull PackSchematic schematic) final Set updated = new HashSet<>(schematics); updated.remove(schematic); updated.add(schematic); - return new Pack(id, name, type, updated); + return new Pack(id, name, type, updated, validationState); + } + + @Override + public boolean equals(final Object o) + { + if (!(o instanceof Pack other)) + { + return false; + } + return id.equals(other.id); + } + + @Override + public int hashCode() + { + return id.hashCode(); } } diff --git a/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java b/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java index b97a9f099..1e48c7fb1 100644 --- a/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java +++ b/src/main/java/com/ldtteam/structurize/index/packtypes/PackTypesRegistry.java @@ -2,6 +2,7 @@ import com.ldtteam.structurize.api.constants.Constants; import com.ldtteam.structurize.index.packtypes.models.PackType; +import com.ldtteam.structurize.index.packtypes.models.PackTypeRequirement; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.neoforged.neoforge.registries.DeferredHolder; @@ -11,13 +12,34 @@ import static com.ldtteam.structurize.api.Registries.SCHEMATIC_INDEX_PACK_TYPES; +/** + * Holds the deferred register and built-in {@link PackType} registrations for the schematic index system. + * + *

Third-party mods can register additional pack types by calling + * {@link #register(ResourceLocation, Function)} after getting a reference to this class, + * provided they do so before the registry freezes. + */ public class PackTypesRegistry { public static final DeferredRegister DEFERRED_REGISTER = DeferredRegister.create(SCHEMATIC_INDEX_PACK_TYPES, Constants.MOD_ID); + /** + * The built-in default pack type, used as a fallback and for testing. + * Requires a schematic named {@code "test"} at levels 1 and 2, and optionally one named {@code "flex"} at level 1. + */ public static final DeferredHolder DEFAULT_PACK_TYPE = - register(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "default"), builder -> builder.withName(Component.literal("Default"))); + register(ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "default"), + builder -> builder.withName(Component.literal("Default")) + .addPackRequirement(new PackTypeRequirement.Builder().requiresName("test").requiresLevelCount(2)) + .addPackRequirement(new PackTypeRequirement.Builder().requiresName("flex").requiresLevelCount(1).optional())); + /** + * Registers a new {@link PackType} with the given ID, configured via the provided function. + * + * @param packId the resource location used as the registry key + * @param configure a function that receives a pre-initialized {@link PackType.Builder} and returns the configured builder + * @return the deferred holder for the registered pack type + */ public static DeferredHolder register( final ResourceLocation packId, final Function configure)