diff --git a/dev images/Pasted image (2).png b/dev images/Pasted image (2).png new file mode 100644 index 0000000..09ca071 Binary files /dev/null and b/dev images/Pasted image (2).png differ diff --git a/dev images/Pasted image (3).png b/dev images/Pasted image (3).png new file mode 100644 index 0000000..e5b716f Binary files /dev/null and b/dev images/Pasted image (3).png differ diff --git a/dev images/Pasted image (4).png b/dev images/Pasted image (4).png new file mode 100644 index 0000000..015d3ec Binary files /dev/null and b/dev images/Pasted image (4).png differ diff --git a/dev images/Pasted image (5).png b/dev images/Pasted image (5).png new file mode 100644 index 0000000..273585a Binary files /dev/null and b/dev images/Pasted image (5).png differ diff --git a/dev images/Pasted image (6).png b/dev images/Pasted image (6).png new file mode 100644 index 0000000..8fb0758 Binary files /dev/null and b/dev images/Pasted image (6).png differ diff --git a/dev images/Pasted image (7).png b/dev images/Pasted image (7).png new file mode 100644 index 0000000..d618195 Binary files /dev/null and b/dev images/Pasted image (7).png differ diff --git a/dev images/Pasted image (8).png b/dev images/Pasted image (8).png new file mode 100644 index 0000000..ddea2e0 Binary files /dev/null and b/dev images/Pasted image (8).png differ diff --git a/dev images/Pasted image.png b/dev images/Pasted image.png index 7218961..b96e6ec 100644 Binary files a/dev images/Pasted image.png and b/dev images/Pasted image.png differ diff --git a/gradle.properties b/gradle.properties index 1f4060f..cc68152 100644 --- a/gradle.properties +++ b/gradle.properties @@ -36,7 +36,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.6.6.9 +mod_version=0.6.6.11 immersive_portals_version=6.0.7 terrablender_version=4.1.0.8 serene_seasons_version_id=pHEgQQUE diff --git a/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java b/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java index c99842c..9269fa0 100644 --- a/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java +++ b/src/main/java/net/dnorberg/rivencraft/block/BookReceptacleBlock.java @@ -97,7 +97,11 @@ public final class BookReceptacleBlock extends BaseEntityBlock { LinkTarget destination = stack.is(ModItems.LINKING_BOOK.get()) ? stack.get(ModDataComponents.LINK_TARGET.get()) : player instanceof ServerPlayer serverPlayer - ? AgeDimensionService.resolve(serverPlayer, stack.get(ModDataComponents.AGE_REFERENCE.get())).orElse(null) + ? AgeDimensionService.resolve(serverPlayer, stack.get(ModDataComponents.AGE_REFERENCE.get()), + net.dnorberg.rivencraft.writing.OrderedWriting.EMPTY, + !stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), + net.dnorberg.rivencraft.book.LinkProperties.NONE) + .has(net.dnorberg.rivencraft.book.LinkProperties.DISABLE_PLATFORM)).orElse(null) : null; if (destination == null || server.getServer().getLevel(destination.location().dimension()) == null) { player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true); diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java index 303d4c5..3f3fd6e 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeDimensionService.java @@ -96,6 +96,11 @@ public final class AgeDimensionService { public static Optional resolve(ServerPlayer player, AgeReference reference, OrderedWriting writing) { + return resolve(player, reference, writing, true); + } + + public static Optional resolve(ServerPlayer player, AgeReference reference, + OrderedWriting writing, boolean generatePlatform) { AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference, writing); if (AgeRegistrySavedData.get(player.serverLevel()).isDead(definition.id())) return Optional.empty(); ServerLevel level = ensureLoaded(player.getServer(), definition); @@ -106,7 +111,8 @@ public final class AgeDimensionService { BlockPos spawn = definition.spawn(); if (spawn == null) { applyInitialCelestialPhase(level, definition); - spawn = createSafeSpawn(level, definition); + spawn = createSafeSpawn(level, definition, generatePlatform); + if (spawn == null) return Optional.empty(); if (AgeFeatureSelection.resolved(definition.plan()).stream() .anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES)) || definition.randomFeatures().stream() @@ -219,21 +225,24 @@ public final class AgeDimensionService { return ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, definition.dimensionId()); } - private static BlockPos createSafeSpawn(ServerLevel level, AgeDefinition age) { + private static BlockPos createSafeSpawn(ServerLevel level, AgeDefinition age, boolean generatePlatform) { BlockPos floorCenter; if (age.generationProfile().equals(AgeGenerationProfiles.CAVE)) { // Cave Ages are enclosed. Searching from the world ceiling would // select or expose their roof, so begin with the floor beneath a // player standing at Y=64 and search downward from there. floorCenter = highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1) - .orElseGet(() -> fallbackSpawnFloor(level)); + .orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null); } else if (isNetherStyle(level)) { - floorCenter = netherSpawnFloor(level); + floorCenter = generatePlatform ? netherSpawnFloor(level) + : highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1).orElse(null); } else { - floorCenter = highestFootedSurface(level).orElseGet(() -> fallbackSpawnFloor(level)); + floorCenter = highestFootedSurface(level).orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null); } - for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) { + if (floorCenter == null) return null; + + if (generatePlatform) for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) { for (int z = -PLATFORM_RADIUS; z <= PLATFORM_RADIUS; z++) { BlockPos floor = floorCenter.offset(x, 0, z); level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 2); @@ -244,6 +253,11 @@ public final class AgeDimensionService { } BlockPos spawn = floorCenter.above(); + if (!generatePlatform) { + var safe = LinkingService.resolveDestination(level, spawn, true); + if (safe.isEmpty()) return null; + spawn = BlockPos.containing(safe.get()); + } level.setDefaultSpawnPos(spawn, 0.0F); return spawn; } diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java b/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java index 13fa9df..3bc3559 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgePersistenceGameTests.java @@ -23,6 +23,31 @@ public final class AgePersistenceGameTests { private AgePersistenceGameTests() { } + @GameTest(template = "empty") + public static void modifierSeedChangePersistsWithoutChangingAgeIdentity(GameTestHelper helper) { + UUID id = UUID.randomUUID(); + AgeDefinition original = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), + new AgeReference(id, "Reseed " + id)); + AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel()); + long newSeed = original.seed() == 987654321L ? 987654322L : 987654321L; + helper.assertTrue(registry.reseed(helper.getLevel().getServer(), id, newSeed), + "An unoccupied Age must accept a new seed"); + AgeDefinition changed = registry.find(id).orElseThrow(); + helper.assertValueEqual(changed.id(), original.id(), "Reseeding must preserve the Age identity"); + helper.assertValueEqual(changed.dimensionId(), original.dimensionId(), + "Reseeding must preserve its dimension and existing chunk location"); + helper.assertValueEqual(changed.seed(), newSeed, "The new generator seed must be saved"); + AgeDefinition restored = AgeRegistrySavedData.load( + registry.save(new CompoundTag(), helper.getLevel().registryAccess()), + helper.getLevel().registryAccess()).find(id).orElseThrow(); + helper.assertValueEqual(restored.seed(), newSeed, "The new seed must survive reload"); + var ageLevel = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), changed); + helper.assertTrue(ageLevel != null, "The reseeded Age must reload"); + helper.assertValueEqual(ageLevel.getSeed(), newSeed, + "The runtime level must use the saved seed for new chunks"); + helper.succeed(); + } + @GameTest(template = "empty") public static void retiredAgeIdentitySurvivesSaveAndCannotLoad(GameTestHelper helper) { UUID id = UUID.randomUUID(); diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java b/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java index 990a639..81d5ac5 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java +++ b/src/main/java/net/dnorberg/rivencraft/book/AgeRegistrySavedData.java @@ -16,6 +16,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.MinecraftServer; import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.world.level.saveddata.SavedData; import net.neoforged.fml.ModList; @@ -136,6 +137,32 @@ public final class AgeRegistrySavedData extends SavedData { return updated; } + /** Changes the generator for future chunks; existing chunk data is deliberately preserved. */ + public boolean reseed(MinecraftServer server, UUID ageId, long seed) { + AgeDefinition previous = ages.get(ageId); + if (previous == null || isDead(ageId)) return false; + if (previous.seed() == seed) return true; + ServerLevel loaded = server.getLevel(AgeDimensionService.dimensionKey(previous)); + if (loaded != null && !loaded.players().isEmpty()) return false; + + // Legacy changes the Age's seed, not its already-written pages or selected traits. + // Keeping the frozen plan and feature list prevents a visited Age from changing + // its identity while newly generated chunks use the revised terrain noise. + AgeGenerationPlan oldPlan = previous.plan(); + AgeGenerationPlan plan = oldPlan == null ? null : new AgeGenerationPlan(oldPlan.version(), seed, + oldPlan.writing(), oldPlan.symbols(), oldPlan.runtimeProfile(), oldPlan.biomeSelection(), + oldPlan.activeSymbolIndices(), oldPlan.writingInstability()); + AgeDefinition changed = new AgeDefinition(previous.id(), previous.name(), seed, + previous.generationProfile(), previous.generationVersion(), previous.createdGameTime(), + previous.dimensionId(), previous.spawn(), previous.traits(), plan, previous.seasonMode(), + previous.randomFeatures(), previous.instability(), previous.instabilityPlan()); + if (AgeDimensionService.unload(server, previous) == AgeDimensionService.UnloadResult.FAILED) + return false; + ages.put(ageId, changed); + setDirty(); + return true; + } + private AgeDefinition getOrCreate(AgeReference reference, OrderedWriting writing, long worldSeed, long createdGameTime, RegistryAccess registries) { AgeDefinition existing = ages.get(reference.id()); diff --git a/src/main/java/net/dnorberg/rivencraft/book/LinkProperties.java b/src/main/java/net/dnorberg/rivencraft/book/LinkProperties.java index f19afd2..22698ae 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/LinkProperties.java +++ b/src/main/java/net/dnorberg/rivencraft/book/LinkProperties.java @@ -11,11 +11,15 @@ import net.minecraft.network.chat.Component; /** Flags written into a Link Panel and carried by books assembled from it. */ public record LinkProperties(int flags) { public static final int DISARM = 1; - /** Retained only to read pre-0.4.7.1 test books; no longer generated or applied. */ + /** Retained to read older test books; a newly edited book uses DISABLE_PLATFORM instead. */ public static final int GENERATE_PLATFORM = 2; public static final int MAINTAIN_MOMENTUM = 4; public static final int INTRA_LINKING = 8; - private static final int ALL_FLAGS = DISARM | GENERATE_PLATFORM | MAINTAIN_MOMENTUM | INTRA_LINKING; + public static final int INTRA_LINKING_ONLY = 16; + public static final int RELATIVE_LINK = 32; + public static final int DISABLE_PLATFORM = 64; + private static final int ALL_FLAGS = DISARM | GENERATE_PLATFORM | MAINTAIN_MOMENTUM | INTRA_LINKING + | INTRA_LINKING_ONLY | RELATIVE_LINK | DISABLE_PLATFORM; public static final LinkProperties NONE = new LinkProperties(0); public static final Codec CODEC = Codec.intRange(0, ALL_FLAGS).xmap( LinkProperties::new, LinkProperties::flags); @@ -35,13 +39,16 @@ public record LinkProperties(int flags) { /** Intra Linking only governs Linking Books; a Descriptive Book treats that panel as ordinary. */ public LinkProperties forDescriptiveBook() { - return new LinkProperties(flags & ~INTRA_LINKING); + return new LinkProperties(flags & ~(INTRA_LINKING | INTRA_LINKING_ONLY)); } public void appendTooltip(List lines) { if (has(DISARM)) lines.add(propertyName("disarm")); if (has(MAINTAIN_MOMENTUM)) lines.add(propertyName("maintain_momentum")); if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking")); + if (has(INTRA_LINKING_ONLY)) lines.add(propertyName("intra_linking_only")); + if (has(RELATIVE_LINK)) lines.add(propertyName("relative_link")); + if (has(DISABLE_PLATFORM)) lines.add(propertyName("disable_platform")); } private static Component propertyName(String name) { @@ -54,6 +61,9 @@ public record LinkProperties(int flags) { case DISARM -> propertyName("disarm"); case MAINTAIN_MOMENTUM -> propertyName("maintain_momentum"); case INTRA_LINKING -> propertyName("intra_linking"); + case INTRA_LINKING_ONLY -> propertyName("intra_linking_only"); + case RELATIVE_LINK -> propertyName("relative_link"); + case DISABLE_PLATFORM -> propertyName("disable_platform"); default -> throw new IllegalArgumentException("Unknown link property flag: " + flag); }; } diff --git a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java index 0dc302c..7412742 100644 --- a/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java +++ b/src/main/java/net/dnorberg/rivencraft/book/LinkingService.java @@ -86,7 +86,10 @@ public final class LinkingService { if (destinationLevel == null) { return LinkResult.MISSING_DIMENSION; } - Optional destination = resolveDestination(destinationLevel, target.location().pos(), true); + if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel()) + return LinkResult.INTRA_ONLY; + BlockPos origin = landingOrigin(player, destinationLevel, target, properties); + Optional destination = resolveDestination(destinationLevel, origin, true); if (destination.isEmpty()) { return LinkResult.NO_SAFE_DESTINATION; } @@ -105,7 +108,10 @@ public final class LinkingService { ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension()); if (destinationLevel == null) return LinkResult.MISSING_DIMENSION; boolean instant = destinationChunksLoaded(destinationLevel, target.location().pos()); - Optional destination = resolveDestination(destinationLevel, target.location().pos(), true); + if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel()) + return LinkResult.INTRA_ONLY; + Optional destination = resolveDestination(destinationLevel, + landingOrigin(player, destinationLevel, target, properties), true); if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION; Vec3 momentum = player.getDeltaMovement(); if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player); @@ -121,11 +127,15 @@ public final class LinkingService { if (destinationLevel == null) { return LinkResult.MISSING_DIMENSION; } - if (destinationLevel == player.level() && !properties.has(LinkProperties.INTRA_LINKING)) + if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel()) + return LinkResult.INTRA_ONLY; + if (destinationLevel == player.level() && !properties.has(LinkProperties.INTRA_LINKING) + && !properties.has(LinkProperties.INTRA_LINKING_ONLY)) return LinkResult.INTRA_LINKING_REQUIRED; boolean instant = !portal || destinationChunksLoaded(destinationLevel, target.location().pos()); - Optional destination = resolveDestination(destinationLevel, target.location().pos(), requiresFooting); + Optional destination = resolveDestination(destinationLevel, + landingOrigin(player, destinationLevel, target, properties), requiresFooting); if (destination.isEmpty()) { return LinkResult.NO_SAFE_DESTINATION; } @@ -139,6 +149,17 @@ public final class LinkingService { return LinkResult.SUCCESS; } + /** Legacy relative links retain the traveller's offset from the source spawn. */ + private static BlockPos landingOrigin(ServerPlayer player, ServerLevel destinationLevel, + LinkTarget target, LinkProperties properties) { + if (!properties.has(LinkProperties.RELATIVE_LINK)) return target.location().pos(); + BlockPos sourceSpawn = player.serverLevel().getSharedSpawnPos(); + BlockPos destinationSpawn = destinationLevel.getSharedSpawnPos(); + BlockPos departure = player.blockPosition(); + return destinationSpawn.offset(departure.getX() - sourceSpawn.getX(), + departure.getY() - sourceSpawn.getY(), departure.getZ() - sourceSpawn.getZ()); + } + /** Drop each inventory stack as an item entity at the departure point. */ private static void dropInventoryAtSource(ServerPlayer player) { var inventory = player.getInventory(); @@ -263,6 +284,7 @@ public final class LinkingService { SUCCESS, MISSING_DIMENSION, INTRA_LINKING_REQUIRED, + INTRA_ONLY, NO_SAFE_DESTINATION } } diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java index d539099..295026c 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/LinkModifierScreen.java @@ -1,63 +1,121 @@ package net.dnorberg.rivencraft.client.screen; +import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.book.LinkProperties; import net.dnorberg.rivencraft.menu.LinkModifierMenu; import net.dnorberg.rivencraft.network.SetLinkModifierTitlePayload; +import net.dnorberg.rivencraft.network.SetLinkModifierSeedPayload; import net.dnorberg.rivencraft.registry.ModDataComponents; -import net.dnorberg.rivencraft.registry.ModItems; import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.item.ItemStack; import net.neoforged.neoforge.network.PacketDistributor; +/** Legacy single-slot layout, with server-owned link options. */ public final class LinkModifierScreen extends AbstractContainerScreen { + private static final ResourceLocation BACKGROUND = ResourceLocation.fromNamespaceAndPath( + Rivencraft.MOD_ID, "textures/gui/link_modifier_legacy.png"); + private static final int[] FLAGS = {LinkProperties.DISARM, LinkProperties.INTRA_LINKING, + LinkProperties.INTRA_LINKING_ONLY, LinkProperties.MAINTAIN_MOMENTUM, + LinkProperties.RELATIVE_LINK, LinkProperties.DISABLE_PLATFORM}; + private static final int[] IDS = {LinkModifierMenu.TOGGLE_DISARM, LinkModifierMenu.TOGGLE_INTRA, + LinkModifierMenu.TOGGLE_INTRA_ONLY, LinkModifierMenu.TOGGLE_MOMENTUM, + LinkModifierMenu.TOGGLE_RELATIVE, LinkModifierMenu.TOGGLE_PLATFORM}; private EditBox titleInput; - private Button disarm; - private Button momentum; - private Button intra; - private Button retire; + private EditBox seedInput; private ItemStack lastBook = ItemStack.EMPTY; + private long lastSeed = Long.MIN_VALUE; public LinkModifierScreen(LinkModifierMenu menu, Inventory inventory, Component title) { super(menu, inventory, title); imageWidth = 176; - imageHeight = 194; + imageHeight = 166; inventoryLabelX = 8; - inventoryLabelY = 99; + inventoryLabelY = 72; } @Override protected void init() { super.init(); - titleInput = new EditBox(font, leftPos + 8, topPos + 54, 160, 14, + titleInput = new EditBox(font, leftPos + 80, topPos + 56, 87, 14, Component.translatable("screen.rivencraft.link_modifier.title")); titleInput.setMaxLength(32); titleInput.setResponder(value -> PacketDistributor.sendToServer( new SetLinkModifierTitlePayload(menu.containerId, value))); addRenderableWidget(titleInput); - disarm = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.TOGGLE_DISARM)) - .bounds(leftPos + 8, topPos + 74, 76, 20).build()); - momentum = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.TOGGLE_MOMENTUM)) - .bounds(leftPos + 92, topPos + 74, 76, 20).build()); - intra = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.TOGGLE_INTRA)) - .bounds(leftPos + 8, topPos + 18, 64, 20).build()); - retire = addRenderableWidget(Button.builder(Component.empty(), button -> click(LinkModifierMenu.DELETE_AGE)) - .bounds(leftPos + 104, topPos + 18, 64, 20).build()); + seedInput = new EditBox(font, leftPos + 80, topPos + 15, 87, 14, + Component.translatable("screen.rivencraft.link_modifier.seed")); + seedInput.setMaxLength(21); + seedInput.setTooltip(Tooltip.create(Component.translatable("screen.rivencraft.link_modifier.seed_warning"))); + addRenderableWidget(seedInput); refreshControls(); } private void click(int id) { - if (minecraft != null && minecraft.gameMode != null) { + if (minecraft != null && minecraft.gameMode != null) minecraft.gameMode.handleInventoryButtonClick(menu.containerId, id); - } } @Override protected void containerTick() { refreshControls(); } + private void submitSeed() { + if (seedInput.visible && !seedInput.getValue().equals(Long.toString(menu.seed()))) + PacketDistributor.sendToServer(new SetLinkModifierSeedPayload(menu.containerId, seedInput.getValue())); + } + + @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (seedInput.isFocused()) { + if (keyCode == 257 || keyCode == 335) submitSeed(); + else if (keyCode != 256) seedInput.keyPressed(keyCode, scanCode, modifiers); + if (keyCode == 256 || keyCode == 257 || keyCode == 335) { + setFocused(null); + seedInput.setFocused(false); + } + return true; + } + if (titleInput.isFocused()) { + if (keyCode == 256) { + setFocused(null); + titleInput.setFocused(false); + } else titleInput.keyPressed(keyCode, scanCode, modifiers); + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (seedInput.isFocused() && (mouseX < seedInput.getX() || mouseX >= seedInput.getX() + seedInput.getWidth() + || mouseY < seedInput.getY() || mouseY >= seedInput.getY() + seedInput.getHeight())) { + submitSeed(); + setFocused(null); + seedInput.setFocused(false); + } + if (button == 0) { + for (int index = 0; index < FLAGS.length; index++) { + if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) { + if (!menu.book().isEmpty()) click(IDS[index]); + return true; + } + } + if (menu.canDelete(minecraft.player) + && inside(mouseX, mouseY, menu.awaitingDeletionConfirmation() ? 140 : 120, 32, 18, 18)) { + click(LinkModifierMenu.DELETE_AGE); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + private boolean inside(double mouseX, double mouseY, int x, int y, int width, int height) { + return mouseX >= leftPos + x && mouseX < leftPos + x + width + && mouseY >= topPos + y && mouseY < topPos + y + height; + } + private void refreshControls() { ItemStack book = menu.book(); if (!ItemStack.isSameItemSameComponents(book, lastBook) && !titleInput.isFocused()) { @@ -67,39 +125,59 @@ public final class LinkModifierScreen extends AbstractContainerScreen "message.rivencraft.linking_book.missing_dimension"; case INTRA_LINKING_REQUIRED -> "message.rivencraft.linking_book.intra_required"; + case INTRA_ONLY -> "message.rivencraft.linking_book.intra_only"; default -> "message.rivencraft.linking_book.unsafe"; }; player.displayClientMessage(Component.translatable(errorKey), true); diff --git a/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java b/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java index 42bf818..e8b2b28 100644 --- a/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java +++ b/src/main/java/net/dnorberg/rivencraft/menu/LinkModifierMenu.java @@ -29,12 +29,17 @@ public final class LinkModifierMenu extends AbstractContainerMenu { public static final int TOGGLE_MOMENTUM = 2; public static final int TOGGLE_INTRA = 3; public static final int DELETE_AGE = 4; + public static final int TOGGLE_INTRA_ONLY = 5; + public static final int TOGGLE_RELATIVE = 6; + public static final int TOGGLE_PLATFORM = 7; private static final int PLAYER_START = 1; private static final int MAX_TITLE_LENGTH = 32; private final LinkModifierBlockEntity modifier; private final Container bookSlot; private final boolean clientSide; + private final Player owner; + private final int[] seedWords = new int[4]; private int confirmation; private UUID pendingAge; @@ -48,11 +53,21 @@ public final class LinkModifierMenu extends AbstractContainerMenu { this.modifier = modifier; this.bookSlot = modifier == null ? new SimpleContainer(1) : modifier; this.clientSide = inventory.player.level().isClientSide; + this.owner = inventory.player; addDataSlot(new DataSlot() { @Override public int get() { return confirmation; } @Override public void set(int value) { confirmation = value; } }); - addSlot(new Slot(bookSlot, 0, 80, 30) { + for (int index = 0; index < seedWords.length; index++) { + final int word = index; + addDataSlot(new DataSlot() { + @Override public int get() { + return clientSide ? seedWords[word] : (int) ((seed() >>> (word * 16)) & 0xffffL); + } + @Override public void set(int value) { seedWords[word] = value & 0xffff; } + }); + } + addSlot(new Slot(bookSlot, 0, 80, 35) { @Override public boolean mayPlace(ItemStack stack) { return bookSlot.canPlaceItem(0, stack); } @Override public int getMaxStackSize() { return 1; } @Override public void setChanged() { @@ -62,12 +77,45 @@ public final class LinkModifierMenu extends AbstractContainerMenu { } }); for (int row = 0; row < 3; row++) for (int col = 0; col < 9; col++) - addSlot(new Slot(inventory, col + row * 9 + 9, 8 + col * 18, 112 + row * 18)); + addSlot(new Slot(inventory, col + row * 9 + 9, 8 + col * 18, 84 + row * 18)); for (int col = 0; col < 9; col++) - addSlot(new Slot(inventory, col, 8 + col * 18, 170)); + addSlot(new Slot(inventory, col, 8 + col * 18, 142)); } public ItemStack book() { return bookSlot.getItem(0); } + public boolean hasSeed() { + return book().is(ModItems.DESCRIPTIVE_BOOK.get()) + && book().has(ModDataComponents.AGE_REFERENCE.get()); + } + public long seed() { + if (clientSide) { + long result = 0; + for (int index = 0; index < seedWords.length; index++) + result |= (seedWords[index] & 0xffffL) << (index * 16); + return result; + } + if (!hasSeed()) return 0; + AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get()); + return AgeRegistrySavedData.get(((ServerPlayer) owner).serverLevel()).find(reference.id()) + .map(AgeDefinition::seed).orElse(0L); + } + + public boolean setSeed(Player player, String text) { + if (clientSide || !stillValid(player) || !hasSeed() || text == null || text.length() > 21 + || !(player instanceof ServerPlayer serverPlayer)) return false; + long requested; + try { requested = Long.parseLong(text.strip()); } + catch (NumberFormatException exception) { return false; } + AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get()); + if (reference == null) return false; + boolean changed = AgeRegistrySavedData.get(serverPlayer.serverLevel()) + .reseed(serverPlayer.getServer(), reference.id(), requested); + player.displayClientMessage(Component.translatable(changed + ? "message.rivencraft.link_modifier.seed_changed" + : "message.rivencraft.link_modifier.seed_failed"), false); + if (changed) broadcastChanges(); + return changed; + } public boolean awaitingDeletionConfirmation() { return confirmation == 1; } public boolean canDelete(Player player) { return player.hasPermissions(2) && book().is(ModItems.DESCRIPTIVE_BOOK.get()) @@ -92,11 +140,18 @@ public final class LinkModifierMenu extends AbstractContainerMenu { case TOGGLE_DISARM -> LinkProperties.DISARM; case TOGGLE_MOMENTUM -> LinkProperties.MAINTAIN_MOMENTUM; case TOGGLE_INTRA -> LinkProperties.INTRA_LINKING; + case TOGGLE_INTRA_ONLY -> LinkProperties.INTRA_LINKING_ONLY; + case TOGGLE_RELATIVE -> LinkProperties.RELATIVE_LINK; + case TOGGLE_PLATFORM -> LinkProperties.DISABLE_PLATFORM; default -> 0; }; - if (flag == 0 || (flag == LinkProperties.INTRA_LINKING + if (flag == 0 || ((flag == LinkProperties.INTRA_LINKING || flag == LinkProperties.INTRA_LINKING_ONLY) && book().is(ModItems.DESCRIPTIVE_BOOK.get()))) return false; int flags = book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE).flags(); + if (flag == LinkProperties.INTRA_LINKING_ONLY && (flags & LinkProperties.INTRA_LINKING_ONLY) == 0) + flags |= LinkProperties.INTRA_LINKING; + if (flag == LinkProperties.INTRA_LINKING && (flags & LinkProperties.INTRA_LINKING) != 0) + flags &= ~LinkProperties.INTRA_LINKING_ONLY; book().set(ModDataComponents.LINK_PROPERTIES.get(), new LinkProperties(flags ^ flag)); bookSlot.setChanged(); pendingAge = null; diff --git a/src/main/java/net/dnorberg/rivencraft/mixin/AgeDimensionSeedMixin.java b/src/main/java/net/dnorberg/rivencraft/mixin/AgeDimensionSeedMixin.java new file mode 100644 index 0000000..2c59d83 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/mixin/AgeDimensionSeedMixin.java @@ -0,0 +1,24 @@ +package net.dnorberg.rivencraft.mixin; + +import dev.galacticraft.dynamicdimensions.impl.DynamicDimensionRegistryImpl; +import net.dnorberg.rivencraft.book.AgeRegistrySavedData; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; +import net.minecraft.world.level.storage.WorldData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +/** Give Dynamic Dimensions the saved Age seed before it constructs the level and its chunk RandomState. */ +@Mixin(value = DynamicDimensionRegistryImpl.class, remap = false) +public abstract class AgeDimensionSeedMixin { + @ModifyVariable(method = "createDynamicLevel(Lnet/minecraft/resources/ResourceKey;Lnet/minecraft/world/level/storage/WorldData;Lnet/minecraft/world/level/dimension/LevelStem;Lnet/minecraft/server/level/ServerLevel;)Lnet/minecraft/server/level/ServerLevel;", + at = @At("STORE"), ordinal = 0, remap = false) + private long rivencraft$useSavedAgeSeed(long defaultSeed, ResourceKey dimension, + WorldData worldData, LevelStem stem, ServerLevel overworld) { + return AgeRegistrySavedData.get(overworld).findByDimension(dimension.location()) + .map(age -> age.seed()).orElse(defaultSeed); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java index 3ef7278..41c253a 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java +++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java @@ -1,6 +1,7 @@ package net.dnorberg.rivencraft.network; import net.dnorberg.rivencraft.book.LinkTarget; +import net.dnorberg.rivencraft.book.LinkProperties; import net.dnorberg.rivencraft.book.LinkingService; import net.dnorberg.rivencraft.book.AgeReference; import net.dnorberg.rivencraft.book.AgeRegistrySavedData; @@ -60,7 +61,7 @@ public final class ModNetworking { } public static void register(RegisterPayloadHandlersEvent event) { - PayloadRegistrar registrar = event.registrar("16"); + PayloadRegistrar registrar = event.registrar("17"); registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC, ModNetworking::requestDescriptiveBookScreen); registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC, @@ -88,6 +89,8 @@ public final class ModNetworking { ModNetworking::setBookBinderTitle); registrar.playToServer(SetLinkModifierTitlePayload.TYPE, SetLinkModifierTitlePayload.STREAM_CODEC, ModNetworking::setLinkModifierTitle); + registrar.playToServer(SetLinkModifierSeedPayload.TYPE, SetLinkModifierSeedPayload.STREAM_CODEC, + ModNetworking::setLinkModifierSeed); } private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) { @@ -106,6 +109,13 @@ public final class ModNetworking { } } + private static void setLinkModifierSeed(SetLinkModifierSeedPayload payload, IPayloadContext context) { + if (context.player() instanceof ServerPlayer player + && player.containerMenu instanceof LinkModifierMenu menu + && menu.containerId == payload.containerId()) + menu.setSeed(player, payload.seed()); + } + private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) { if (!(context.player() instanceof ServerPlayer player)) { return; @@ -174,7 +184,9 @@ public final class ModNetworking { try { AgeReference reference = new AgeReference(UUID.randomUUID(), title); if (AgeDimensionService.resolve(player, reference, access.stack() - .getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY)).isEmpty()) { + .getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY), + !access.stack().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE) + .has(LinkProperties.DISABLE_PLATFORM)).isEmpty()) { player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true); return; } @@ -280,7 +292,9 @@ public final class ModNetworking { LinkTarget target; if (payload.descriptiveBook() && stack.is(ModItems.DESCRIPTIVE_BOOK.get())) { AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get()); - target = age == null ? null : AgeDimensionService.resolve(player, age).orElse(null); + target = age == null ? null : AgeDimensionService.resolve(player, age, OrderedWriting.EMPTY, + !stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE) + .has(LinkProperties.DISABLE_PLATFORM)).orElse(null); } else if (!payload.descriptiveBook() && stack.is(ModItems.LINKING_BOOK.get())) { target = stack.get(ModDataComponents.LINK_TARGET.get()); } else { diff --git a/src/main/java/net/dnorberg/rivencraft/network/SetLinkModifierSeedPayload.java b/src/main/java/net/dnorberg/rivencraft/network/SetLinkModifierSeedPayload.java new file mode 100644 index 0000000..60cbfda --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/network/SetLinkModifierSeedPayload.java @@ -0,0 +1,19 @@ +package net.dnorberg.rivencraft.network; + +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; + +public record SetLinkModifierSeedPayload(int containerId, String seed) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_link_modifier_seed")); + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite(ByteBufCodecs.VAR_INT, SetLinkModifierSeedPayload::containerId, + ByteBufCodecs.STRING_UTF8, SetLinkModifierSeedPayload::seed, + SetLinkModifierSeedPayload::new); + + @Override public Type type() { return TYPE; } +} diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index 236ae3f..4649f5e 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -15,6 +15,9 @@ "item.rivencraft.link_property.disarm": "Disarm", "item.rivencraft.link_property.maintain_momentum": "Maintain Momentum", "item.rivencraft.link_property.intra_linking": "Intra Linking", + "item.rivencraft.link_property.intra_linking_only": "Intra-Linking Only", + "item.rivencraft.link_property.relative_link": "Relative Link", + "item.rivencraft.link_property.disable_platform": "Generate Platform: Off", "item.rivencraft.page.unresolved": "Unresolved Symbol Page", "item.rivencraft.notebook": "Notebook", "item.rivencraft.collation_folder": "Collation Folder", @@ -22,6 +25,10 @@ "block.rivencraft.link_modifier": "Link Modifier", "container.rivencraft.link_modifier": "Link Modifier", "screen.rivencraft.link_modifier.title": "Book title", + "screen.rivencraft.link_modifier.seed": "Age seed", + "screen.rivencraft.link_modifier.seed_warning": "Changing the seed preserves existing chunks but changes newly generated terrain. Press Enter to apply. The Age must be empty.", + "message.rivencraft.link_modifier.seed_changed": "Age seed changed. Existing chunks remain; newly generated chunks use the new seed.", + "message.rivencraft.link_modifier.seed_failed": "Could not change seed. Use a signed whole number and ensure no players are in the Age.", "screen.rivencraft.link_modifier.retire": "Retire Age", "screen.rivencraft.link_modifier.confirm": "Confirm!", "message.rivencraft.link_modifier.delete.retired": "Age retired. Its terrain was archived outside the active world.", @@ -100,6 +107,7 @@ "message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable", "message.rivencraft.linking_book.unsafe": "No safe destination was found near the link", "message.rivencraft.linking_book.intra_required": "This book needs an Intra Linking panel for same-dimension travel", + "message.rivencraft.linking_book.intra_only": "This book can only link within its current dimension", "message.rivencraft.descriptive_book.unwritten": "This Descriptive Book is unwritten", "message.rivencraft.descriptive_book.created": "Created Age: %s", "message.rivencraft.descriptive_book.unavailable": "That Age is unavailable", diff --git a/src/main/resources/assets/rivencraft/textures/gui/link_modifier_legacy.png b/src/main/resources/assets/rivencraft/textures/gui/link_modifier_legacy.png new file mode 100644 index 0000000..fff762d Binary files /dev/null and b/src/main/resources/assets/rivencraft/textures/gui/link_modifier_legacy.png differ diff --git a/src/main/resources/rivencraft.mixins.json b/src/main/resources/rivencraft.mixins.json index a0d089a..70567ba 100644 --- a/src/main/resources/rivencraft.mixins.json +++ b/src/main/resources/rivencraft.mixins.json @@ -4,6 +4,7 @@ "package": "net.dnorberg.rivencraft.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ + "AgeDimensionSeedMixin", "AgeDungeonPlacementMixin", "AgeStructureBiomeMixin", "StructurePlacementAccessor"