package de.lucas.litematica.placement; import de.lucas.litematica.schematic.SchematicFiles; import de.lucas.litematica.schematic.SchematicManager; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; /** * All placements of the current world/server. Main thread only. Placements are persisted per world and restored * on the next login, see {@link PlacementStorage}. */ public final class PlacementManager { private static final PlacementManager INSTANCE = new PlacementManager(); private static final int SAVE_DELAY_TICKS = 40; private final List placements = new ArrayList<>(); @Nullable private UUID selected; @Nullable private String worldKey; private int saveCountdown = -1; public static PlacementManager get() { return INSTANCE; } public List all() { return Collections.unmodifiableList(this.placements); } /** Placements in the player's current dimension. */ public List inCurrentDimension() { Minecraft mc = Minecraft.getInstance(); if (mc.level == null) { return List.of(); } ResourceLocation dim = mc.level.dimension().location(); return this.placements.stream().filter(p -> p.dimension().equals(dim)).toList(); } @Nullable public SchematicPlacement selected() { for (SchematicPlacement p : this.placements) { if (p.id().equals(this.selected)) { return p; } } return null; } public void select(@Nullable SchematicPlacement placement) { this.selected = placement == null ? null : placement.id(); this.markDirty(); } /** Selects the next placement in the current dimension, wrapping around. */ public void selectNext() { List list = this.inCurrentDimension(); if (list.isEmpty()) { return; } int index = list.indexOf(this.selected()); this.select(list.get((index + 1) % list.size())); } /** Places a schematic file with its minimum corner at {@code origin}. The file is loaded in the background. */ public SchematicPlacement place(Path file, BlockPos origin) { Minecraft mc = Minecraft.getInstance(); ResourceLocation dim = mc.level != null ? mc.level.dimension().location() : ResourceLocation.withDefaultNamespace("overworld"); SchematicPlacement placement = new SchematicPlacement(UUID.randomUUID(), SchematicFiles.relativeName(file), dim, SchematicFiles.baseName(file), origin.immutable(), origin.immutable(), PlacementTransform.IDENTITY, true); this.placements.add(placement); this.selected = placement.id(); this.loadSchematic(placement, true); this.markDirty(); return placement; } public void remove(SchematicPlacement placement) { this.placements.remove(placement); if (placement.id().equals(this.selected)) { this.selected = this.placements.isEmpty() ? null : this.placements.getLast().id(); } this.markDirty(); } public void move(SchematicPlacement placement, int dx, int dy, int dz) { this.setOrigin(placement, placement.origin().offset(dx, dy, dz)); } public void setOrigin(SchematicPlacement placement, BlockPos origin) { placement.setOrigin(origin); this.markDirty(); } public void resetOrigin(SchematicPlacement placement) { this.setOrigin(placement, placement.originalOrigin()); } /** Makes the current position the one {@link #resetOrigin} returns to. */ public void storeOriginAsOriginal(SchematicPlacement placement) { placement.setOriginalOrigin(placement.origin()); this.markDirty(); } public void setTransform(SchematicPlacement placement, PlacementTransform transform) { placement.setTransform(transform); this.markDirty(); } public void setEnabled(SchematicPlacement placement, boolean enabled) { placement.setEnabled(enabled); this.markDirty(); } public void rename(SchematicPlacement placement, String name) { if (!name.isBlank()) { placement.setName(name.trim()); this.markDirty(); } } /** Reloads all placements that use the given file, e.g. after it was replaced on disk. */ public void reloadFile(Path file) { String relative = SchematicFiles.relativeName(file); SchematicManager.get().invalidate(file); for (SchematicPlacement p : this.placements) { if (p.schematicFile().equals(relative)) { this.loadSchematic(p, false); } } } public void tick() { if (this.saveCountdown >= 0 && --this.saveCountdown < 0) { this.save(); } } public void onLogin() { this.placements.clear(); this.worldKey = PlacementStorage.currentWorldKey(); PlacementStorage.Loaded loaded = PlacementStorage.load(this.worldKey); this.placements.addAll(loaded.placements()); this.selected = loaded.selected(); for (SchematicPlacement p : this.placements) { this.loadSchematic(p, false); } } public void onLogout() { this.save(); this.placements.clear(); this.selected = null; this.worldKey = null; } private void markDirty() { this.saveCountdown = SAVE_DELAY_TICKS; } private void save() { this.saveCountdown = -1; if (this.worldKey != null) { PlacementStorage.save(this.worldKey, this.placements, this.selected); } } private void loadSchematic(SchematicPlacement placement, boolean announce) { Minecraft mc = Minecraft.getInstance(); Path file = SchematicFiles.resolve(placement.schematicFile()); SchematicManager.get().load(file).whenComplete((schematic, error) -> mc.execute(() -> { if (!this.placements.contains(placement)) { return; } if (error != null) { String message = SchematicManager.errorMessage(error); placement.setSchematic(null, message); notify(Component.translatable("neolitematica.message.load_failed", placement.schematicFile(), message).withStyle(ChatFormatting.RED)); } else { placement.setSchematic(schematic, null); if (announce) { notify(Component.translatable("neolitematica.message.placed", placement.name(), placement.origin().getX(), placement.origin().getY(), placement.origin().getZ())); } } })); } private static void notify(Component message) { Minecraft mc = Minecraft.getInstance(); if (mc.player != null) { mc.player.displayClientMessage(message, false); } } }