Compare commits

...
3 Commits
Author SHA1 Message Date
dnorberg 09d2b0800c Complete Link Modifier controls and seed-driven Age generation (#90)
Build / build (push) Canceled after 0s
2026-10-21 08:11:26 -04:00
dnorberg ee905cab68 Add Legacy missing items and OP Link Modifier (#90)
Build / build (push) Canceled after 0s
2026-10-20 17:50:09 -04:00
dnorberg 5c08f391a2 Update optional Serene Seasons compatibility to 10.1.0.9
Build / build (push) Canceled after 0s
2026-10-20 17:33:07 -04:00
62 changed files with 1209 additions and 59 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 47 KiB

+1 -1
View File
@@ -31,7 +31,7 @@ release V0.6.6.0.
| Ponder (bundled with Create) | 1.0.82 | 1.0.87 library artifact | [Maven](https://maven.createmod.net/net/createmod/ponder/ponder-neoforge/maven-metadata.xml) has a newer artifact, but Create 6.0.10 still bundles 1.0.82. Do not treat this as a separately installed Create update. | | Ponder (bundled with Create) | 1.0.82 | 1.0.87 library artifact | [Maven](https://maven.createmod.net/net/createmod/ponder/ponder-neoforge/maven-metadata.xml) has a newer artifact, but Create 6.0.10 still bundles 1.0.82. Do not treat this as a separately installed Create update. |
| Immersive Portals (optional) | 6.0.7 | 6.0.7 | No newer matching [Modrinth](https://modrinth.com/mod/immersive-portals-neoforge/versions) release found. | | Immersive Portals (optional) | 6.0.7 | 6.0.7 | No newer matching [Modrinth](https://modrinth.com/mod/immersive-portals-neoforge/versions) release found. |
| TerraBlender (optional) | 4.1.0.8 | 4.1.0.8 | No newer matching [Modrinth](https://modrinth.com/mod/terrablender/versions) release found. | | TerraBlender (optional) | 4.1.0.8 | 4.1.0.8 | No newer matching [Modrinth](https://modrinth.com/mod/terrablender/versions) release found. |
| Serene Seasons (optional) | 10.1.0.8 compile target | 10.1.0.9 | Separate update issue; the user's Prism instance already contains 10.1.0.9, but that is not a recorded compatibility pass. | | Serene Seasons (optional) | 10.1.0.8 at #82 review | 10.1.0.9 | Updated in #88 after the player's full seasonal matrix passed; see [Serene review](MILESTONE_7_SERENE_REVIEW.md). |
| GlitchCore (Serene dependency) | 2.1.0.2 | 2.1.0.2 | No newer matching [Modrinth](https://modrinth.com/mod/glitchcore/versions) release found. | | GlitchCore (Serene dependency) | 2.1.0.2 | 2.1.0.2 | No newer matching [Modrinth](https://modrinth.com/mod/glitchcore/versions) release found. |
| Biomes O' Plenty (TerraBlender test addon) | 21.1.0.14 in user's Prism instance | 21.1.0.14 | No newer matching [Modrinth](https://modrinth.com/mod/biomes-o-plenty/versions) release found. | | Biomes O' Plenty (TerraBlender test addon) | 21.1.0.14 in user's Prism instance | 21.1.0.14 | No newer matching [Modrinth](https://modrinth.com/mod/biomes-o-plenty/versions) release found. |
+31
View File
@@ -0,0 +1,31 @@
# Serene Seasons 10.1.0.9 compatibility review (#88)
Reviewed 2026-10-20 for Minecraft 1.21.1 and NeoForge 21.1.264. Rivencraft
now compiles against [Serene Seasons 10.1.0.9](https://modrinth.com/mod/serene-seasons/version/pHEgQQUE)
with [GlitchCore 2.1.0.2](https://modrinth.com/mod/glitchcore/version/S2TfWrZR).
Both remain optional. The Serene update fixes a precipitation-mixin recursion;
it does not introduce a new dynamic-dimension API.
The 10.1.0.8 and 10.1.0.9 bytecode for Rivencraft's version-sensitive
`SeasonsConfig`, `SeasonHandler`, `SeasonSavedData`, and `SeasonTime` integration
classes is identical. In particular, the dimension whitelist field, per-level
saved season clock, and season update method have unchanged signatures and
implementations. The adapter still writes the whitelist through GlitchCore's
config API and sends Rivencraft's whitelist update to already connected clients.
Verification:
| Check | Result |
| --- | --- |
| Build against 10.1.0.9 and GlitchCore 2.1.0.2 | Passed. |
| Real Prism client with Serene 10.1.0.9, GlitchCore 2.1.0.2, and Rivencraft 0.6.6.7 | Both mods loaded and the player joined an Age. |
| No Seasons, normal Seasons, and all four Eternal modes in new and saved Ages | Player reported the full in-game matrix passed. |
| Reconnect, restart, and Overworld independence | Player reported the full in-game matrix passed. |
| Isolated dedicated-server startup without Serene/GlitchCore | Reached `Done` on port 25568 with only required runtime mods, then saved and stopped. |
| Isolated dedicated-server startup with Serene/GlitchCore | Reached `Done` on port 25569 with 10.1.0.9/2.1.0.2, then saved and stopped. |
Serene's sync packet is incompatible with GameTest mock players, so its
in-game behavior must be checked with a real client and server. The seasonal
test path in [SERENE_SEASONS_COMPATIBILITY.md](SERENE_SEASONS_COMPATIBILITY.md)
remains the regression checklist for future upgrades. Rivencraft does not
bundle or require Serene Seasons or GlitchCore.
+2 -2
View File
@@ -36,10 +36,10 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # 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 mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.6.6.7 mod_version=0.6.6.11
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
terrablender_version=4.1.0.8 terrablender_version=4.1.0.8
serene_seasons_version_id=B6nTjh4b serene_seasons_version_id=pHEgQQUE
glitchcore_version_id=S2TfWrZR glitchcore_version_id=S2TfWrZR
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
@@ -97,7 +97,11 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
LinkTarget destination = stack.is(ModItems.LINKING_BOOK.get()) LinkTarget destination = stack.is(ModItems.LINKING_BOOK.get())
? stack.get(ModDataComponents.LINK_TARGET.get()) ? stack.get(ModDataComponents.LINK_TARGET.get())
: player instanceof ServerPlayer serverPlayer : 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; : null;
if (destination == null || server.getServer().getLevel(destination.location().dimension()) == null) { if (destination == null || server.getServer().getLevel(destination.location().dimension()) == null) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true); player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
@@ -0,0 +1,63 @@
package net.dnorberg.rivencraft.block;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity;
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Containers;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.BaseEntityBlock;
import net.minecraft.world.level.block.RenderShape;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
public final class LinkModifierBlock extends BaseEntityBlock {
public static final MapCodec<LinkModifierBlock> CODEC = simpleCodec(LinkModifierBlock::new);
public static final DirectionProperty FACING = BlockStateProperties.HORIZONTAL_FACING;
public LinkModifierBlock(Properties properties) {
super(properties);
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
}
@Override protected MapCodec<? extends BaseEntityBlock> codec() { return CODEC; }
@Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block,
BlockState> builder) { builder.add(FACING); }
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new LinkModifierBlockEntity(pos, state);
}
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
Player player, BlockHitResult hit) {
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer
&& level.getBlockEntity(pos) instanceof LinkModifierBlockEntity modifier) {
serverPlayer.openMenu(new SimpleMenuProvider(
(id, inventory, ignored) -> new LinkModifierMenu(id, inventory, modifier),
Component.translatable("container.rivencraft.link_modifier")),
buffer -> buffer.writeBlockPos(pos));
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
@Override protected void onRemove(BlockState state, Level level, BlockPos pos,
BlockState replacement, boolean moving) {
if (!state.is(replacement.getBlock()) && !level.isClientSide
&& level.getBlockEntity(pos) instanceof LinkModifierBlockEntity modifier) {
Containers.dropContents(level, pos, modifier);
}
super.onRemove(state, level, pos, replacement, moving);
}
}
@@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.OrderedWriting; import net.dnorberg.rivencraft.writing.OrderedWriting;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.WritingGrammar; import net.dnorberg.rivencraft.writing.WritingGrammar;
@@ -96,7 +97,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
case COVER -> validCover(stack); case COVER -> validCover(stack);
case LINK_PANEL -> stack.is(ModItems.PAGE.get()) case LINK_PANEL -> stack.is(ModItems.PAGE.get())
&& PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL; && PageItem.content(stack).kind() == PageContent.Kind.LINK_PANEL;
case NOTEBOOK -> stack.is(ModItems.NOTEBOOK.get()); case NOTEBOOK -> CollationFolderItem.isCollection(stack);
default -> slot >= FIRST_PAGE && slot < SIZE && validPage(stack); default -> slot >= FIRST_PAGE && slot < SIZE && validPage(stack);
}; };
} }
@@ -296,7 +297,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
/** Import a Notebook from either its stored slot or the player's cursor. */ /** Import a Notebook from either its stored slot or the player's cursor. */
public ItemStack importNotebookFromStack(ItemStack notebook, int index) { public ItemStack importNotebookFromStack(ItemStack notebook, int index) {
if (!notebook.is(ModItems.NOTEBOOK.get()) || notebook.getCount() != 1) return ItemStack.EMPTY; if (!CollationFolderItem.isCollection(notebook) || notebook.getCount() != 1) return ItemStack.EMPTY;
List<ItemStack> pages = notebook.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); List<ItemStack> pages = notebook.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
if (pages.isEmpty()) return ItemStack.EMPTY; if (pages.isEmpty()) return ItemStack.EMPTY;
int end = nextPageIndex(); int end = nextPageIndex();
@@ -0,0 +1,63 @@
package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
/** One persisted book slot; edits are performed by the server menu. */
public final class LinkModifierBlockEntity extends BlockEntity implements Container {
private NonNullList<ItemStack> items = NonNullList.withSize(1, ItemStack.EMPTY);
public LinkModifierBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.LINK_MODIFIER.get(), pos, state);
}
@Override public int getContainerSize() { return 1; }
@Override public boolean isEmpty() { return items.getFirst().isEmpty(); }
@Override public ItemStack getItem(int slot) { return items.get(slot); }
@Override public ItemStack removeItem(int slot, int amount) {
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
if (!removed.isEmpty()) setChanged();
return removed;
}
@Override public ItemStack removeItemNoUpdate(int slot) {
ItemStack removed = ContainerHelper.takeItem(items, slot);
if (!removed.isEmpty()) setChanged();
return removed;
}
@Override public void setItem(int slot, ItemStack stack) {
items.set(slot, stack.copyWithCount(Math.min(1, stack.getCount())));
setChanged();
}
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
return slot == 0 && (stack.is(ModItems.LINKING_BOOK.get())
|| stack.is(ModItems.DESCRIPTIVE_BOOK.get()));
}
@Override public boolean stillValid(Player player) {
return level != null && level.isLoaded(worldPosition) && level.getBlockEntity(worldPosition) == this
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
worldPosition.getZ() + 0.5) <= 64.0;
}
@Override public void clearContent() {
items = NonNullList.withSize(1, ItemStack.EMPTY);
setChanged();
}
@Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
items = NonNullList.withSize(1, ItemStack.EMPTY);
ContainerHelper.loadAllItems(tag, items, registries);
}
@Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
ContainerHelper.saveAllItems(tag, items, registries);
}
}
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.item.InkVialItem; import net.dnorberg.rivencraft.item.InkVialItem;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.InkVialExchange; import net.dnorberg.rivencraft.writing.InkVialExchange;
import net.dnorberg.rivencraft.writing.WritingInkService; import net.dnorberg.rivencraft.writing.WritingInkService;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
@@ -87,7 +88,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
worldPosition.getZ() + 0.5) <= 64.0; worldPosition.getZ() + 0.5) <= 64.0;
} }
@Override public boolean canPlaceItem(int slot, ItemStack stack) { @Override public boolean canPlaceItem(int slot, ItemStack stack) {
if (slot < COLLECTIONS) return stack.is(ModItems.NOTEBOOK.get()) if (slot < COLLECTIONS) return CollationFolderItem.isCollection(stack)
&& stack.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY) && stack.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY)
.pages().stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL) .pages().stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL)
|| stack.is(ModItems.PAGE.get()) || stack.is(ModItems.PAGE.get())
@@ -0,0 +1,56 @@
package net.dnorberg.rivencraft.book;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.AtomicMoveNotSupportedException;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.storage.LevelResource;
/** Retires an exact Age and moves its unloaded region data out of the live world. */
public final class AgeDeletionService {
public enum Result { RETIRED, ALREADY_DEAD, OCCUPIED, UNLOAD_FAILED, STORAGE_FAILED }
private AgeDeletionService() { }
public static Result retire(MinecraftServer server, AgeDefinition age) {
AgeRegistrySavedData registry = AgeRegistrySavedData.get(server.overworld());
if (registry.isDead(age.id())) return Result.ALREADY_DEAD;
var level = server.getLevel(AgeDimensionService.dimensionKey(age));
if (level != null && !level.players().isEmpty()) return Result.OCCUPIED;
if (AgeDimensionService.unload(server, age) == AgeDimensionService.UnloadResult.FAILED)
return Result.UNLOAD_FAILED;
Path world = server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
Path ageRoot = world.resolve("dimensions/rivencraft/age").normalize();
Path data = world.resolve("dimensions")
.resolve(age.dimensionId().getNamespace())
.resolve(age.dimensionId().getPath()).normalize();
// Exact Rivencraft Age only; never move the shared age/ directory.
if (!data.startsWith(ageRoot) || data.equals(ageRoot)
|| !"rivencraft".equals(age.dimensionId().getNamespace())
|| !age.dimensionId().getPath().startsWith("age/")) return Result.STORAGE_FAILED;
try {
if (Files.isSymbolicLink(world.resolve("dimensions"))
|| Files.isSymbolicLink(world.resolve("dimensions/rivencraft"))
|| Files.isSymbolicLink(ageRoot) || Files.isSymbolicLink(data)) return Result.STORAGE_FAILED;
if (Files.exists(data, LinkOption.NOFOLLOW_LINKS)) {
Path archive = world.getParent().resolve(world.getFileName() + "-retired-ages")
.resolve(age.id().toString());
if (Files.exists(archive, LinkOption.NOFOLLOW_LINKS)) return Result.STORAGE_FAILED;
Files.createDirectories(archive.getParent());
try { Files.move(data, archive, StandardCopyOption.ATOMIC_MOVE); }
catch (AtomicMoveNotSupportedException exception) { Files.move(data, archive); }
Rivencraft.LOGGER.warn("Retired Age {} data moved from {} to {}", age.id(), data, archive);
}
} catch (IOException exception) {
Rivencraft.LOGGER.error("Cannot archive data for Age {}", age.id(), exception);
return Result.STORAGE_FAILED;
}
registry.markDead(age.id());
return Result.RETIRED;
}
}
@@ -74,6 +74,7 @@ public final class AgeDimensionService {
DynamicDimensionLoadCallback.register((server, loader) -> { DynamicDimensionLoadCallback.register((server, loader) -> {
ServerLevel overworld = server.overworld(); ServerLevel overworld = server.overworld();
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) { for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
if (AgeRegistrySavedData.get(overworld).isDead(age.id())) continue;
if (!canLoadProfile(server, age)) continue; if (!canLoadProfile(server, age)) continue;
ServerLevel level = loader.loadDynamicDimension( ServerLevel level = loader.loadDynamicDimension(
age.dimensionId(), age.dimensionId(),
@@ -95,7 +96,13 @@ public final class AgeDimensionService {
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference, public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
OrderedWriting writing) { OrderedWriting writing) {
return resolve(player, reference, writing, true);
}
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
OrderedWriting writing, boolean generatePlatform) {
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), reference, writing); 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); ServerLevel level = ensureLoaded(player.getServer(), definition);
if (level == null) { if (level == null) {
return Optional.empty(); return Optional.empty();
@@ -104,7 +111,8 @@ public final class AgeDimensionService {
BlockPos spawn = definition.spawn(); BlockPos spawn = definition.spawn();
if (spawn == null) { if (spawn == null) {
applyInitialCelestialPhase(level, definition); applyInitialCelestialPhase(level, definition);
spawn = createSafeSpawn(level, definition); spawn = createSafeSpawn(level, definition, generatePlatform);
if (spawn == null) return Optional.empty();
if (AgeFeatureSelection.resolved(definition.plan()).stream() if (AgeFeatureSelection.resolved(definition.plan()).stream()
.anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES)) .anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES))
|| definition.randomFeatures().stream() || definition.randomFeatures().stream()
@@ -122,6 +130,7 @@ public final class AgeDimensionService {
} }
public static ServerLevel ensureLoaded(MinecraftServer server, AgeDefinition definition) { public static ServerLevel ensureLoaded(MinecraftServer server, AgeDefinition definition) {
if (AgeRegistrySavedData.get(server.overworld()).isDead(definition.id())) return null;
ResourceKey<Level> key = dimensionKey(definition); ResourceKey<Level> key = dimensionKey(definition);
ServerLevel existing = server.getLevel(key); ServerLevel existing = server.getLevel(key);
if (existing != null) { if (existing != null) {
@@ -216,21 +225,24 @@ public final class AgeDimensionService {
return ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, definition.dimensionId()); 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; BlockPos floorCenter;
if (age.generationProfile().equals(AgeGenerationProfiles.CAVE)) { if (age.generationProfile().equals(AgeGenerationProfiles.CAVE)) {
// Cave Ages are enclosed. Searching from the world ceiling would // Cave Ages are enclosed. Searching from the world ceiling would
// select or expose their roof, so begin with the floor beneath a // select or expose their roof, so begin with the floor beneath a
// player standing at Y=64 and search downward from there. // player standing at Y=64 and search downward from there.
floorCenter = highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1) floorCenter = highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1)
.orElseGet(() -> fallbackSpawnFloor(level)); .orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null);
} else if (isNetherStyle(level)) { } else if (isNetherStyle(level)) {
floorCenter = netherSpawnFloor(level); floorCenter = generatePlatform ? netherSpawnFloor(level)
: highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1).orElse(null);
} else { } 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++) { for (int z = -PLATFORM_RADIUS; z <= PLATFORM_RADIUS; z++) {
BlockPos floor = floorCenter.offset(x, 0, z); BlockPos floor = floorCenter.offset(x, 0, z);
level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 2); level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 2);
@@ -241,6 +253,11 @@ public final class AgeDimensionService {
} }
BlockPos spawn = floorCenter.above(); 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); level.setDefaultSpawnPos(spawn, 0.0F);
return spawn; return spawn;
} }
@@ -23,6 +23,49 @@ public final class AgePersistenceGameTests {
private 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();
AgeReference reference = new AgeReference(id, "Retired " + id);
AgeDefinition age = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), reference);
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
helper.assertTrue(registry.markDead(id), "The first retirement must set the tombstone");
helper.assertTrue(!registry.markDead(id), "Retirement must be idempotent");
helper.assertTrue(AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), age) == null,
"A retired Age must never load from an old book");
AgeRegistrySavedData restored = AgeRegistrySavedData.load(
registry.save(new CompoundTag(), helper.getLevel().registryAccess()),
helper.getLevel().registryAccess());
helper.assertTrue(restored.isDead(id) && restored.find(id).isPresent(),
"Both the retired flag and reserved Age identity must survive reload");
helper.succeed();
}
@GameTest(template = "empty") @GameTest(template = "empty")
public static void authoredPlanRetainsResolvedMeaningAndAgeIdentity(GameTestHelper helper) { public static void authoredPlanRetainsResolvedMeaningAndAgeIdentity(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of( OrderedWriting writing = new OrderedWriting(List.of(
@@ -5,6 +5,9 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.List; import java.util.List;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.nbt.StringTag;
import net.dnorberg.rivencraft.writing.OrderedWriting; import net.dnorberg.rivencraft.writing.OrderedWriting;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
@@ -13,6 +16,7 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag; import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.datafix.DataFixTypes; import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.level.saveddata.SavedData; import net.minecraft.world.level.saveddata.SavedData;
import net.neoforged.fml.ModList; import net.neoforged.fml.ModList;
@@ -21,6 +25,7 @@ import net.neoforged.fml.ModList;
public final class AgeRegistrySavedData extends SavedData { public final class AgeRegistrySavedData extends SavedData {
private static final String DATA_NAME = "rivencraft_ages"; private static final String DATA_NAME = "rivencraft_ages";
private static final String AGES_TAG = "Ages"; private static final String AGES_TAG = "Ages";
private static final String DEAD_AGES_TAG = "DeadAges";
private static final String ID_TAG = "Id"; private static final String ID_TAG = "Id";
private static final String NAME_TAG = "Name"; private static final String NAME_TAG = "Name";
private static final String SEED_TAG = "Seed"; private static final String SEED_TAG = "Seed";
@@ -50,6 +55,7 @@ public final class AgeRegistrySavedData extends SavedData {
); );
private final Map<UUID, AgeDefinition> ages = new HashMap<>(); private final Map<UUID, AgeDefinition> ages = new HashMap<>();
private final Set<UUID> deadAges = new HashSet<>();
public static AgeRegistrySavedData get(ServerLevel level) { public static AgeRegistrySavedData get(ServerLevel level) {
return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME); return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME);
@@ -110,6 +116,16 @@ public final class AgeRegistrySavedData extends SavedData {
return List.copyOf(ages.values()); return List.copyOf(ages.values());
} }
public boolean isDead(UUID ageId) { return deadAges.contains(ageId); }
/** Keep the ID and definition reserved so an old book cannot regenerate a retired Age. */
public boolean markDead(UUID ageId) {
if (!ages.containsKey(ageId)) return false;
boolean changed = deadAges.add(ageId);
if (changed) setDirty();
return changed;
}
public AgeDefinition setSpawn(UUID ageId, BlockPos spawn) { public AgeDefinition setSpawn(UUID ageId, BlockPos spawn) {
AgeDefinition existing = ages.get(ageId); AgeDefinition existing = ages.get(ageId);
if (existing == null) { if (existing == null) {
@@ -121,6 +137,32 @@ public final class AgeRegistrySavedData extends SavedData {
return updated; 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, private AgeDefinition getOrCreate(AgeReference reference, OrderedWriting writing,
long worldSeed, long createdGameTime, RegistryAccess registries) { long worldSeed, long createdGameTime, RegistryAccess registries) {
AgeDefinition existing = ages.get(reference.id()); AgeDefinition existing = ages.get(reference.id());
@@ -161,6 +203,11 @@ public final class AgeRegistrySavedData extends SavedData {
static AgeRegistrySavedData load(CompoundTag tag, HolderLookup.Provider registries) { static AgeRegistrySavedData load(CompoundTag tag, HolderLookup.Provider registries) {
AgeRegistrySavedData data = new AgeRegistrySavedData(); AgeRegistrySavedData data = new AgeRegistrySavedData();
ListTag deadTags = tag.getList(DEAD_AGES_TAG, StringTag.TAG_STRING);
for (int index = 0; index < deadTags.size(); index++) {
try { data.deadAges.add(UUID.fromString(deadTags.getString(index))); }
catch (IllegalArgumentException ignored) { }
}
ListTag serializedAges = tag.getList(AGES_TAG, CompoundTag.TAG_COMPOUND); ListTag serializedAges = tag.getList(AGES_TAG, CompoundTag.TAG_COMPOUND);
for (int index = 0; index < serializedAges.size(); index++) { for (int index = 0; index < serializedAges.size(); index++) {
CompoundTag entry = serializedAges.getCompound(index); CompoundTag entry = serializedAges.getCompound(index);
@@ -264,6 +311,9 @@ public final class AgeRegistrySavedData extends SavedData {
serializedAges.add(entry); serializedAges.add(entry);
}); });
tag.put(AGES_TAG, serializedAges); tag.put(AGES_TAG, serializedAges);
ListTag deadTags = new ListTag();
deadAges.stream().sorted().forEach(id -> deadTags.add(StringTag.valueOf(id.toString())));
tag.put(DEAD_AGES_TAG, deadTags);
return tag; return tag;
} }
@@ -11,11 +11,15 @@ import net.minecraft.network.chat.Component;
/** Flags written into a Link Panel and carried by books assembled from it. */ /** Flags written into a Link Panel and carried by books assembled from it. */
public record LinkProperties(int flags) { public record LinkProperties(int flags) {
public static final int DISARM = 1; 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 GENERATE_PLATFORM = 2;
public static final int MAINTAIN_MOMENTUM = 4; public static final int MAINTAIN_MOMENTUM = 4;
public static final int INTRA_LINKING = 8; 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 LinkProperties NONE = new LinkProperties(0);
public static final Codec<LinkProperties> CODEC = Codec.intRange(0, ALL_FLAGS).xmap( public static final Codec<LinkProperties> CODEC = Codec.intRange(0, ALL_FLAGS).xmap(
LinkProperties::new, LinkProperties::flags); 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. */ /** Intra Linking only governs Linking Books; a Descriptive Book treats that panel as ordinary. */
public LinkProperties forDescriptiveBook() { public LinkProperties forDescriptiveBook() {
return new LinkProperties(flags & ~INTRA_LINKING); return new LinkProperties(flags & ~(INTRA_LINKING | INTRA_LINKING_ONLY));
} }
public void appendTooltip(List<Component> lines) { public void appendTooltip(List<Component> lines) {
if (has(DISARM)) lines.add(propertyName("disarm")); if (has(DISARM)) lines.add(propertyName("disarm"));
if (has(MAINTAIN_MOMENTUM)) lines.add(propertyName("maintain_momentum")); if (has(MAINTAIN_MOMENTUM)) lines.add(propertyName("maintain_momentum"));
if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking")); 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) { private static Component propertyName(String name) {
@@ -54,6 +61,9 @@ public record LinkProperties(int flags) {
case DISARM -> propertyName("disarm"); case DISARM -> propertyName("disarm");
case MAINTAIN_MOMENTUM -> propertyName("maintain_momentum"); case MAINTAIN_MOMENTUM -> propertyName("maintain_momentum");
case INTRA_LINKING -> propertyName("intra_linking"); 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); default -> throw new IllegalArgumentException("Unknown link property flag: " + flag);
}; };
} }
@@ -86,7 +86,10 @@ public final class LinkingService {
if (destinationLevel == null) { if (destinationLevel == null) {
return LinkResult.MISSING_DIMENSION; return LinkResult.MISSING_DIMENSION;
} }
Optional<Vec3> 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<Vec3> destination = resolveDestination(destinationLevel, origin, true);
if (destination.isEmpty()) { if (destination.isEmpty()) {
return LinkResult.NO_SAFE_DESTINATION; return LinkResult.NO_SAFE_DESTINATION;
} }
@@ -105,7 +108,10 @@ public final class LinkingService {
ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension()); ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension());
if (destinationLevel == null) return LinkResult.MISSING_DIMENSION; if (destinationLevel == null) return LinkResult.MISSING_DIMENSION;
boolean instant = destinationChunksLoaded(destinationLevel, target.location().pos()); boolean instant = destinationChunksLoaded(destinationLevel, target.location().pos());
Optional<Vec3> destination = resolveDestination(destinationLevel, target.location().pos(), true); if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel())
return LinkResult.INTRA_ONLY;
Optional<Vec3> destination = resolveDestination(destinationLevel,
landingOrigin(player, destinationLevel, target, properties), true);
if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION; if (destination.isEmpty()) return LinkResult.NO_SAFE_DESTINATION;
Vec3 momentum = player.getDeltaMovement(); Vec3 momentum = player.getDeltaMovement();
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player); if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
@@ -121,11 +127,15 @@ public final class LinkingService {
if (destinationLevel == null) { if (destinationLevel == null) {
return LinkResult.MISSING_DIMENSION; 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; return LinkResult.INTRA_LINKING_REQUIRED;
boolean instant = !portal || destinationChunksLoaded(destinationLevel, target.location().pos()); boolean instant = !portal || destinationChunksLoaded(destinationLevel, target.location().pos());
Optional<Vec3> destination = resolveDestination(destinationLevel, target.location().pos(), requiresFooting); Optional<Vec3> destination = resolveDestination(destinationLevel,
landingOrigin(player, destinationLevel, target, properties), requiresFooting);
if (destination.isEmpty()) { if (destination.isEmpty()) {
return LinkResult.NO_SAFE_DESTINATION; return LinkResult.NO_SAFE_DESTINATION;
} }
@@ -139,6 +149,17 @@ public final class LinkingService {
return LinkResult.SUCCESS; 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. */ /** Drop each inventory stack as an item entity at the departure point. */
private static void dropInventoryAtSource(ServerPlayer player) { private static void dropInventoryAtSource(ServerPlayer player) {
var inventory = player.getInventory(); var inventory = player.getInventory();
@@ -263,6 +284,7 @@ public final class LinkingService {
SUCCESS, SUCCESS,
MISSING_DIMENSION, MISSING_DIMENSION,
INTRA_LINKING_REQUIRED, INTRA_LINKING_REQUIRED,
INTRA_ONLY,
NO_SAFE_DESTINATION NO_SAFE_DESTINATION
} }
} }
@@ -26,6 +26,7 @@ import net.dnorberg.rivencraft.client.screen.NotebookScreen;
import net.dnorberg.rivencraft.client.screen.WritingDeskScreen; import net.dnorberg.rivencraft.client.screen.WritingDeskScreen;
import net.dnorberg.rivencraft.client.screen.BookBinderScreen; import net.dnorberg.rivencraft.client.screen.BookBinderScreen;
import net.dnorberg.rivencraft.client.screen.InkMixerScreen; import net.dnorberg.rivencraft.client.screen.InkMixerScreen;
import net.dnorberg.rivencraft.client.screen.LinkModifierScreen;
import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.registry.ModMenus;
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT) @EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
@@ -47,6 +48,7 @@ public final class ClientEvents {
event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new); event.register(ModMenus.WRITING_DESK.get(), WritingDeskScreen::new);
event.register(ModMenus.BOOK_BINDER.get(), BookBinderScreen::new); event.register(ModMenus.BOOK_BINDER.get(), BookBinderScreen::new);
event.register(ModMenus.INK_MIXER.get(), InkMixerScreen::new); event.register(ModMenus.INK_MIXER.get(), InkMixerScreen::new);
event.register(ModMenus.LINK_MODIFIER.get(), LinkModifierScreen::new);
} }
@SubscribeEvent @SubscribeEvent
@@ -27,6 +27,7 @@ import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundEvents;
import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.FormattedCharSequence;
@@ -48,6 +49,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
private static final Component EXPLORE_ICON = Component.literal("👁"); private static final Component EXPLORE_ICON = Component.literal("👁");
private final AgeReference age; private final AgeReference age;
private final Component displayName;
private final ResourceLocation generationProfile; private final ResourceLocation generationProfile;
private final AgeTraits traits; private final AgeTraits traits;
private final OrderedWriting writing; private final OrderedWriting writing;
@@ -68,6 +70,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
List<SymbolDisplayMetadata> writingSymbols) { List<SymbolDisplayMetadata> writingSymbols) {
super(book.getHoverName()); super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get()); age = book.get(ModDataComponents.AGE_REFERENCE.get());
displayName = book.get(DataComponents.CUSTOM_NAME);
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
this.traits = traits; this.traits = traits;
writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY); writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY);
@@ -82,6 +85,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
List<SymbolDisplayMetadata> writingSymbols) { List<SymbolDisplayMetadata> writingSymbols) {
super(book.getHoverName()); super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get()); age = book.get(ModDataComponents.AGE_REFERENCE.get());
displayName = book.get(DataComponents.CUSTOM_NAME);
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
this.traits = traits; this.traits = traits;
writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY); writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY);
@@ -245,7 +249,8 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
} }
return; return;
} }
int nameEnd = drawWrappedWithoutShadow(graphics, Component.literal(age.name()), int nameEnd = drawWrappedWithoutShadow(graphics,
displayName == null ? Component.literal(age.name()) : displayName,
x, top + 40, 110); x, top + 40, 110);
int detailsY = drawWrappedWithoutShadow(graphics, int detailsY = drawWrappedWithoutShadow(graphics,
author == null || author.isBlank() author == null || author.isBlank()
@@ -0,0 +1,183 @@
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.minecraft.client.gui.GuiGraphics;
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<LinkModifierMenu> {
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 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 = 166;
inventoryLabelX = 8;
inventoryLabelY = 72;
}
@Override protected void init() {
super.init();
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);
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)
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()) {
Component name = book.get(DataComponents.CUSTOM_NAME);
titleInput.setValue(name == null ? "" : name.getString());
lastBook = book.copy();
}
boolean present = !book.isEmpty();
titleInput.setEditable(present);
seedInput.visible = menu.hasSeed();
seedInput.setEditable(menu.hasSeed());
if (seedInput.visible && !seedInput.isFocused() && (lastSeed != menu.seed()
|| !ItemStack.isSameItemSameComponents(book, lastBook))) {
lastSeed = menu.seed();
seedInput.setValue(Long.toString(lastSeed));
}
}
@Override protected void renderBg(GuiGraphics graphics, float partialTick, int mouseX, int mouseY) {
graphics.blit(BACKGROUND, leftPos, topPos, 0, 0, imageWidth, imageHeight, 256, 256);
LinkProperties properties = menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE);
for (int index = 0; index < FLAGS.length; index++)
square(graphics, 5 + (index / 3) * 20, 10 + (index % 3) * 20,
properties.has(FLAGS[index]), false);
if (menu.canDelete(minecraft.player)) {
square(graphics, 120, 32, menu.awaitingDeletionConfirmation(), false);
if (menu.awaitingDeletionConfirmation()) square(graphics, 140, 32, false, true);
}
}
private void square(GuiGraphics graphics, int x, int y, boolean selected, boolean danger) {
int left = leftPos + x;
int top = topPos + y;
graphics.fill(left, top, left + 18, top + 18, 0xFFAAAAAA);
graphics.fill(left + 2, top + 2, left + 17, top + 17,
danger ? 0xFFAA5555 : selected ? 0xFFBBBBBB : 0xFF777777);
}
@Override protected void renderLabels(GuiGraphics graphics, int mouseX, int mouseY) {
graphics.drawString(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0x404040, false);
}
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
super.render(graphics, mouseX, mouseY, partialTick);
renderTooltip(graphics, mouseX, mouseY);
if (!menu.book().isEmpty()) {
for (int index = 0; index < FLAGS.length; index++) {
if (inside(mouseX, mouseY, 5 + (index / 3) * 20, 10 + (index % 3) * 20, 18, 18)) {
Component label = FLAGS[index] == LinkProperties.DISABLE_PLATFORM
? Component.literal("Generate Platform: "
+ (menu.book().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.has(LinkProperties.DISABLE_PLATFORM) ? "Off" : "On"))
: LinkProperties.name(FLAGS[index]);
graphics.renderTooltip(font, label, mouseX, mouseY);
return;
}
}
}
if (menu.canDelete(minecraft.player)
&& inside(mouseX, mouseY, menu.awaitingDeletionConfirmation() ? 140 : 120, 32, 18, 18))
graphics.renderTooltip(font, Component.translatable(menu.awaitingDeletionConfirmation()
? "screen.rivencraft.link_modifier.confirm" : "screen.rivencraft.link_modifier.retire"),
mouseX, mouseY);
}
}
@@ -182,9 +182,9 @@ public final class NotebookScreen extends AbstractContainerScreen<NotebookMenu>
private int displayedPageSlots() { private int displayedPageSlots() {
if (menu.getCarried().is(ModItems.PAGE.get())) { if (menu.getCarried().is(ModItems.PAGE.get())) {
return NotebookMenu.NOTEBOOK_SLOTS; return menu.pageCapacity();
} }
for (int index = NotebookMenu.NOTEBOOK_SLOTS - 1; index >= 0; index--) { for (int index = menu.pageCapacity() - 1; index >= 0; index--) {
if (!menu.getSlot(index).getItem().isEmpty()) { if (!menu.getSlot(index).getItem().isEmpty()) {
return index + 1; return index + 1;
} }
@@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
@@ -234,7 +235,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private List<ItemStack> pages(int collectionIndex) { private List<ItemStack> pages(int collectionIndex) {
ItemStack collection = menu.getSlot(collectionIndex).getItem(); ItemStack collection = menu.getSlot(collectionIndex).getItem();
if (collection.is(ModItems.PAGE.get())) return List.of(collection); if (collection.is(ModItems.PAGE.get())) return List.of(collection);
if (collection.is(ModItems.NOTEBOOK.get())) { if (CollationFolderItem.isCollection(collection)) {
return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); return collection.getOrDefault(ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
} }
return List.of(); return List.of();
@@ -301,9 +302,9 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private boolean canMovePage(DisplayPage source, DisplayPage target) { private boolean canMovePage(DisplayPage source, DisplayPage target) {
return !source.stack().isEmpty() return !source.stack().isEmpty()
&& (menu.getSlot(source.collection()).getItem().is(ModItems.NOTEBOOK.get()) && (CollationFolderItem.isCollection(menu.getSlot(source.collection()).getItem())
|| menu.getSlot(source.collection()).getItem().is(ModItems.PAGE.get())) || menu.getSlot(source.collection()).getItem().is(ModItems.PAGE.get()))
&& (menu.getSlot(target.collection()).getItem().is(ModItems.NOTEBOOK.get()) && (CollationFolderItem.isCollection(menu.getSlot(target.collection()).getItem())
|| menu.getSlot(target.collection()).getItem().is(ModItems.PAGE.get())) || menu.getSlot(target.collection()).getItem().is(ModItems.PAGE.get()))
&& (source.collection() != target.collection() || source.index() != target.index()); && (source.collection() != target.collection() || source.index() != target.index());
} }
@@ -317,10 +318,10 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
if (!slot.mayPlace(held)) return -1; if (!slot.mayPlace(held)) return -1;
ItemStack current = slot.getItem(); ItemStack current = slot.getItem();
if (current.isEmpty()) return index; if (current.isEmpty()) return index;
if (index < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) { if (index < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) {
List<ItemStack> pages = current.getOrDefault( List<ItemStack> pages = current.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
if (pages.size() < NotebookContents.MAX_PAGES || pages.stream().anyMatch(ItemStack::isEmpty)) if (pages.size() < CollationFolderItem.capacity(current) || pages.stream().anyMatch(ItemStack::isEmpty))
return index; return index;
} else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held) } else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held)
&& current.getCount() < slot.getMaxStackSize(held)) return index; && current.getCount() < slot.getMaxStackSize(held)) return index;
@@ -112,8 +112,9 @@ public final class RivencraftCommands {
source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false); source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false);
for (AgeDefinition age : ages) { for (AgeDefinition age : ages) {
boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null; boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null;
boolean retired = AgeRegistrySavedData.get(source.getLevel()).isDead(age.id());
source.sendSuccess(() -> Component.literal( source.sendSuccess(() -> Component.literal(
(loaded ? "[loaded] " : "[unloaded] ") (retired ? "[retired] " : loaded ? "[loaded] " : "[unloaded] ")
+ age.name() + " — " + age.dimensionId() + age.name() + " — " + age.dimensionId()
+ (age.seasonMode() == null ? "" : " (season: " + (age.seasonMode() == null ? "" : " (season: "
+ age.seasonMode().name().toLowerCase(java.util.Locale.ROOT) + ")") + age.seasonMode().name().toLowerCase(java.util.Locale.ROOT) + ")")
@@ -14,6 +14,26 @@ import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
public final class Milestone7PolishGameTests { public final class Milestone7PolishGameTests {
private Milestone7PolishGameTests() {} private Milestone7PolishGameTests() {}
@GameTest(template = "empty")
public static void modifierFlagsKeepLegacyIntraRules(GameTestHelper helper) {
LinkProperties linking = new LinkProperties(LinkProperties.DISARM | LinkProperties.INTRA_LINKING
| LinkProperties.INTRA_LINKING_ONLY | LinkProperties.RELATIVE_LINK
| LinkProperties.DISABLE_PLATFORM);
helper.assertTrue(linking.has(LinkProperties.INTRA_LINKING_ONLY)
&& linking.has(LinkProperties.RELATIVE_LINK)
&& linking.has(LinkProperties.DISABLE_PLATFORM),
"The new link options must retain independent flag values");
LinkProperties descriptive = linking.forDescriptiveBook();
helper.assertTrue(!descriptive.has(LinkProperties.INTRA_LINKING)
&& !descriptive.has(LinkProperties.INTRA_LINKING_ONLY),
"Intra-only controls must not constrain Descriptive Books");
helper.assertTrue(descriptive.has(LinkProperties.DISARM)
&& descriptive.has(LinkProperties.RELATIVE_LINK)
&& descriptive.has(LinkProperties.DISABLE_PLATFORM),
"Shared link options must remain active on Descriptive Books");
helper.succeed();
}
@GameTest(template = "empty") @GameTest(template = "empty")
public static void linkSoundPriorityMatchesLegacy(GameTestHelper helper) { public static void linkSoundPriorityMatchesLegacy(GameTestHelper helper) {
helper.assertTrue(LinkVisualEffects.sound(LinkProperties.NONE, false) == ModSounds.LINK.get(), helper.assertTrue(LinkVisualEffects.sound(LinkProperties.NONE, false) == ModSounds.LINK.get(),
@@ -0,0 +1,23 @@
package net.dnorberg.rivencraft.item;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.minecraft.world.item.ItemStack;
/** Legacy's compact page collection, using the existing Notebook page editor. */
public final class CollationFolderItem extends NotebookItem {
public static final int MAX_PAGES = 27;
public CollationFolderItem(Properties properties) {
super(properties);
}
public static boolean isCollection(ItemStack stack) {
return stack.is(ModItems.NOTEBOOK.get()) || stack.is(ModItems.COLLATION_FOLDER.get());
}
public static int capacity(ItemStack stack) {
return stack.is(ModItems.COLLATION_FOLDER.get()) ? MAX_PAGES
: stack.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES : 0;
}
}
@@ -61,7 +61,10 @@ public final class DescriptiveBookItem extends Item {
} }
LinkTarget returnTarget = LinkTarget.from(player); LinkTarget returnTarget = LinkTarget.from(player);
LinkTarget ageTarget = AgeDimensionService.resolve(player, age).orElse(null); LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.forDescriptiveBook();
LinkTarget ageTarget = AgeDimensionService.resolve(player, age, net.dnorberg.rivencraft.writing.OrderedWriting.EMPTY,
!properties.has(LinkProperties.DISABLE_PLATFORM)).orElse(null);
if (ageTarget == null) { if (ageTarget == null) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true); player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
return false; return false;
@@ -69,8 +72,6 @@ public final class DescriptiveBookItem extends Item {
ServerLevel sourceLevel = player.serverLevel(); ServerLevel sourceLevel = player.serverLevel();
Vec3 sourcePosition = player.position(); Vec3 sourcePosition = player.position();
LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.forDescriptiveBook();
LinkingService.LinkResult result = portal LinkingService.LinkResult result = portal
? LinkingService.travelDescriptivePortal(player, ageTarget, properties) ? LinkingService.travelDescriptivePortal(player, ageTarget, properties)
: hand == null ? LinkingService.travelWithFooting(player, ageTarget, properties) : hand == null ? LinkingService.travelWithFooting(player, ageTarget, properties)
@@ -79,6 +79,7 @@ public final class LinkingBookItem extends Item {
String errorKey = switch (result) { String errorKey = switch (result) {
case MISSING_DIMENSION -> "message.rivencraft.linking_book.missing_dimension"; case MISSING_DIMENSION -> "message.rivencraft.linking_book.missing_dimension";
case INTRA_LINKING_REQUIRED -> "message.rivencraft.linking_book.intra_required"; 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"; default -> "message.rivencraft.linking_book.unsafe";
}; };
player.displayClientMessage(Component.translatable(errorKey), true); player.displayClientMessage(Component.translatable(errorKey), true);
@@ -16,7 +16,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
public final class NotebookItem extends Item { public class NotebookItem extends Item {
public NotebookItem(Properties properties) { public NotebookItem(Properties properties) {
super(properties); super(properties);
} }
@@ -39,7 +39,9 @@ public final class NotebookItem extends Item {
NotebookContents contents = stack.getOrDefault( NotebookContents contents = stack.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
tooltip.add(Component.translatable("item.rivencraft.notebook.pages", tooltip.add(Component.translatable("item.rivencraft.notebook.pages",
contents.pageCount(), NotebookContents.MAX_PAGES).withStyle(ChatFormatting.GRAY)); contents.pageCount(), stack.is(net.dnorberg.rivencraft.registry.ModItems.COLLATION_FOLDER.get())
? CollationFolderItem.MAX_PAGES : NotebookContents.MAX_PAGES)
.withStyle(ChatFormatting.GRAY));
if (contents.unresolvedCount() > 0) { if (contents.unresolvedCount() > 0) {
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved", tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
contents.unresolvedCount()).withStyle(ChatFormatting.RED)); contents.unresolvedCount()).withStyle(ChatFormatting.RED));
@@ -0,0 +1,62 @@
package net.dnorberg.rivencraft.item;
import java.util.ArrayList;
import java.util.List;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolReference;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
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;
/** Opens into a 16-page Collation Folder, as Legacy's booster item did. */
public final class SealedNotebookItem extends Item {
private static final int PAGE_COUNT = 16;
public SealedNotebookItem(Properties properties) {
super(properties);
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack sealed = player.getItemInHand(hand);
if (level.isClientSide) return InteractionResultHolder.sidedSuccess(sealed, true);
if (!(player instanceof ServerPlayer serverPlayer)) return InteractionResultHolder.pass(sealed);
ItemStack folder = open(serverPlayer);
if (folder.isEmpty()) return InteractionResultHolder.fail(sealed);
if (!player.hasInfiniteMaterials()) sealed.shrink(1);
if (sealed.isEmpty()) {
player.setItemInHand(hand, folder);
} else if (!player.getInventory().add(folder)) {
player.drop(folder, false);
}
return InteractionResultHolder.success(player.getItemInHand(hand));
}
static ItemStack open(ServerPlayer player) {
// Current symbol definitions have no Legacy rank. Draw from the authored
// symbol pool, excluding virtual block/biome pages and corrupted pages.
List<ResourceLocation> candidates = SymbolCatalog.symbols().entrySet().stream()
.filter(entry -> entry.getKey().getNamespace().equals("rivencraft")
&& !entry.getKey().getPath().startsWith("block/")
&& !entry.getKey().getPath().startsWith("biome/")
&& !entry.getValue().category().getPath().equals("corrupted"))
.map(java.util.Map.Entry::getKey).sorted().toList();
if (candidates.isEmpty()) return ItemStack.EMPTY;
List<ItemStack> pages = new ArrayList<>(PAGE_COUNT);
for (int i = 0; i < PAGE_COUNT; i++) {
ResourceLocation id = candidates.get(player.getRandom().nextInt(candidates.size()));
pages.add(PageItem.createSymbolPage(new SymbolReference(id)));
}
ItemStack folder = new ItemStack(ModItems.COLLATION_FOLDER.get());
folder.set(ModDataComponents.NOTEBOOK_CONTENTS.get(), new NotebookContents(pages));
return folder;
}
}
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.registry.ModMenus;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.WritingGrammar; import net.dnorberg.rivencraft.writing.WritingGrammar;
@@ -172,7 +173,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
private boolean clickStrip(int index) { private boolean clickStrip(int index) {
ItemStack carried = getCarried(); ItemStack carried = getCarried();
if (carried.is(ModItems.NOTEBOOK.get())) { if (CollationFolderItem.isCollection(carried)) {
ItemStack emptied = binder.importNotebookFromStack(carried, Math.min(index == 0 ? 0 : index - 1, ItemStack emptied = binder.importNotebookFromStack(carried, Math.min(index == 0 ? 0 : index - 1,
binder.nextPageIndex())); binder.nextPageIndex()));
if (emptied.isEmpty()) return false; if (emptied.isEmpty()) return false;
@@ -227,7 +228,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
moved = moveItemStackTo(source, PLAYER_START, slots.size(), true); moved = moveItemStackTo(source, PLAYER_START, slots.size(), true);
} else if (source.is(Items.LEATHER) || BookBinderBlockEntity.validCover(source)) { } else if (source.is(Items.LEATHER) || BookBinderBlockEntity.validCover(source)) {
moved = moveItemStackTo(source, BookBinderBlockEntity.COVER, BookBinderBlockEntity.COVER + 1, false); moved = moveItemStackTo(source, BookBinderBlockEntity.COVER, BookBinderBlockEntity.COVER + 1, false);
} else if (source.is(ModItems.NOTEBOOK.get())) { } else if (CollationFolderItem.isCollection(source)) {
ItemStack emptied = binder.importNotebookFromStack(source, binder.nextPageIndex()); ItemStack emptied = binder.importNotebookFromStack(source, binder.nextPageIndex());
if (emptied.isEmpty()) return ItemStack.EMPTY; if (emptied.isEmpty()) return ItemStack.EMPTY;
sourceSlot.setByPlayer(emptied); sourceSlot.setByPlayer(emptied);
@@ -0,0 +1,200 @@
package net.dnorberg.rivencraft.menu;
import java.util.UUID;
import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity;
import net.dnorberg.rivencraft.book.AgeDefinition;
import net.dnorberg.rivencraft.book.AgeDeletionService;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModMenus;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
/** Server-owned book edits and a two-stage, OP-only Age retirement control. */
public final class LinkModifierMenu extends AbstractContainerMenu {
public static final int TOGGLE_DISARM = 1;
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;
public LinkModifierMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
instanceof LinkModifierBlockEntity found ? found : null);
}
public LinkModifierMenu(int id, Inventory inventory, LinkModifierBlockEntity modifier) {
super(ModMenus.LINK_MODIFIER.get(), id);
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; }
});
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() {
super.setChanged();
pendingAge = null;
confirmation = 0;
}
});
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, 84 + row * 18));
for (int col = 0; col < 9; col++)
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())
&& book().has(ModDataComponents.AGE_REFERENCE.get());
}
public boolean setTitle(Player player, String title) {
if (clientSide || !stillValid(player) || title == null || title.length() > MAX_TITLE_LENGTH
|| book().isEmpty()) return false;
String clean = title.strip();
if (clean.isEmpty()) book().remove(DataComponents.CUSTOM_NAME);
else book().set(DataComponents.CUSTOM_NAME, Component.literal(clean));
bookSlot.setChanged();
broadcastChanges();
return true;
}
@Override public boolean clickMenuButton(Player player, int id) {
if (clientSide || !stillValid(player) || book().isEmpty()) return false;
if (id == DELETE_AGE) return retireAge(player);
int flag = switch (id) {
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 || 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;
confirmation = 0;
broadcastChanges();
return true;
}
private boolean retireAge(Player player) {
if (!(player instanceof ServerPlayer serverPlayer) || !canDelete(player)) return false;
AgeReference reference = book().get(ModDataComponents.AGE_REFERENCE.get());
if (reference == null) return false;
AgeDefinition age = AgeRegistrySavedData.get(serverPlayer.serverLevel()).find(reference.id()).orElse(null);
if (age == null) return false;
if (confirmation != 1 || !reference.id().equals(pendingAge)) {
pendingAge = reference.id();
confirmation = 1;
broadcastChanges();
return true;
}
confirmation = 0;
pendingAge = null;
AgeDeletionService.Result result = AgeDeletionService.retire(serverPlayer.getServer(), age);
player.displayClientMessage(Component.translatable(
"message.rivencraft.link_modifier.delete." + result.name().toLowerCase()), false);
broadcastChanges();
return result == AgeDeletionService.Result.RETIRED;
}
@Override public boolean stillValid(Player player) {
return modifier != null && modifier.stillValid(player);
}
@Override public ItemStack quickMoveStack(Player player, int index) {
Slot slot = slots.get(index);
if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack source = slot.getItem();
ItemStack copy = source.copy();
if (index == 0) {
if (!moveItemStackTo(source, PLAYER_START, slots.size(), true)) return ItemStack.EMPTY;
} else if (!moveItemStackTo(source, 0, 1, false)) return ItemStack.EMPTY;
if (source.isEmpty()) slot.set(ItemStack.EMPTY);
else slot.setChanged();
return copy;
}
}
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModMenus;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.minecraft.core.NonNullList; import net.minecraft.core.NonNullList;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.Container; import net.minecraft.world.Container;
@@ -28,6 +29,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
private final InteractionHand hand; private final InteractionHand hand;
private final int openedHotbarSlot; private final int openedHotbarSlot;
private final NotebookContainer contents; private final NotebookContainer contents;
private final int pageCapacity;
public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) { public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) {
this(containerId, inventory, data.readBoolean() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND); this(containerId, inventory, data.readBoolean() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND);
@@ -38,11 +40,12 @@ public final class NotebookMenu extends AbstractContainerMenu {
this.playerInventory = inventory; this.playerInventory = inventory;
this.hand = hand; this.hand = hand;
this.openedHotbarSlot = inventory.selected; this.openedHotbarSlot = inventory.selected;
this.pageCapacity = CollationFolderItem.capacity(inventory.player.getItemInHand(hand));
this.contents = new NotebookContainer(inventory, hand); this.contents = new NotebookContainer(inventory, hand);
for (int page = 0; page < NOTEBOOK_SLOTS; page++) { for (int page = 0; page < NOTEBOOK_SLOTS; page++) {
// The client draws these pages on a filtered, scrollable surface instead. // The client draws these pages on a filtered, scrollable surface instead.
addSlot(new PageSlot(contents, page, -1000, -1000)); addSlot(new PageSlot(contents, page, -1000, -1000, page < pageCapacity));
} }
for (int row = 0; row < 3; row++) { for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) { for (int column = 0; column < 9; column++) {
@@ -61,11 +64,13 @@ public final class NotebookMenu extends AbstractContainerMenu {
: new Slot(playerInventory, inventoryIndex, x, y); : new Slot(playerInventory, inventoryIndex, x, y);
} }
public int pageCapacity() { return pageCapacity; }
@Override @Override
public boolean stillValid(Player player) { public boolean stillValid(Player player) {
return player.isAlive() return player.isAlive()
&& (hand == InteractionHand.OFF_HAND || player.getInventory().selected == openedHotbarSlot) && (hand == InteractionHand.OFF_HAND || player.getInventory().selected == openedHotbarSlot)
&& player.getItemInHand(hand).is(ModItems.NOTEBOOK.get()); && CollationFolderItem.isCollection(player.getItemInHand(hand));
} }
@Override @Override
@@ -108,7 +113,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
} else { } else {
return false; return false;
} }
if (source < 0 || source >= NOTEBOOK_SLOTS || target < 0 || target >= NOTEBOOK_SLOTS if (source < 0 || source >= pageCapacity || target < 0 || target >= pageCapacity
|| contents.getItem(source).isEmpty()) { || contents.getItem(source).isEmpty()) {
return false; return false;
} }
@@ -132,14 +137,18 @@ public final class NotebookMenu extends AbstractContainerMenu {
} }
private static final class PageSlot extends Slot { private static final class PageSlot extends Slot {
private PageSlot(Container container, int slot, int x, int y) { private final boolean enabled;
private PageSlot(Container container, int slot, int x, int y, boolean enabled) {
super(container, slot, x, y); super(container, slot, x, y);
this.enabled = enabled;
} }
@Override public boolean mayPlace(ItemStack stack) { @Override public boolean mayPlace(ItemStack stack) {
return stack.is(ModItems.PAGE.get()) return enabled && stack.is(ModItems.PAGE.get())
&& PageItem.content(stack).kind() != PageContent.Kind.LINK_PANEL; && PageItem.content(stack).kind() != PageContent.Kind.LINK_PANEL;
} }
@Override public boolean mayPickup(Player player) { return enabled; }
@Override public int getMaxStackSize() { return 1; } @Override public int getMaxStackSize() { return 1; }
} }
@@ -184,11 +193,12 @@ public final class NotebookMenu extends AbstractContainerMenu {
public void setChanged() { public void setChanged() {
super.setChanged(); super.setChanged();
ItemStack notebook = inventory.player.getItemInHand(hand); ItemStack notebook = inventory.player.getItemInHand(hand);
if (loading || !notebook.is(ModItems.NOTEBOOK.get())) { if (loading || !CollationFolderItem.isCollection(notebook)) {
return; return;
} }
NonNullList<ItemStack> saved = NonNullList.withSize(NOTEBOOK_SLOTS, ItemStack.EMPTY); int capacity = CollationFolderItem.capacity(notebook);
for (int index = 0; index < NOTEBOOK_SLOTS; index++) { NonNullList<ItemStack> saved = NonNullList.withSize(capacity, ItemStack.EMPTY);
for (int index = 0; index < capacity; index++) {
ItemStack page = getItem(index); ItemStack page = getItem(index);
saved.set(index, page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1)); saved.set(index, page.isEmpty() ? ItemStack.EMPTY : page.copyWithCount(1));
} }
@@ -7,6 +7,7 @@ import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.registry.ModMenus; import net.dnorberg.rivencraft.registry.ModMenus;
import net.dnorberg.rivencraft.writing.NotebookContents; import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.PageContent; import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolCatalog; import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata; import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
@@ -107,7 +108,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
public int pageCapacity(int collectionIndex) { public int pageCapacity(int collectionIndex) {
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0; if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0;
ItemStack collection = desk.getItem(collectionIndex); ItemStack collection = desk.getItem(collectionIndex);
return collection.is(ModItems.NOTEBOOK.get()) ? NotebookContents.MAX_PAGES return CollationFolderItem.isCollection(collection) ? CollationFolderItem.capacity(collection)
: collection.is(ModItems.PAGE.get()) ? 1 : 0; : collection.is(ModItems.PAGE.get()) ? 1 : 0;
} }
public List<SymbolDisplayMetadata> catalog() { return catalog; } public List<SymbolDisplayMetadata> catalog() { return catalog; }
@@ -183,7 +184,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
ItemStack carried = getCarried(); ItemStack carried = getCarried();
if (clientSide || !storablePage(carried)) return false; if (clientSide || !storablePage(carried)) return false;
ItemStack collection = desk.getItem(collectionIndex); ItemStack collection = desk.getItem(collectionIndex);
if (collection.is(ModItems.NOTEBOOK.get())) { if (CollationFolderItem.isCollection(collection)) {
if (pageIndex < 0 || pageIndex >= CollationFolderItem.capacity(collection)) return false;
var pages = new java.util.ArrayList<>(collection.getOrDefault( var pages = new java.util.ArrayList<>(collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
while (pages.size() <= pageIndex) pages.add(ItemStack.EMPTY); while (pages.size() <= pageIndex) pages.add(ItemStack.EMPTY);
@@ -207,8 +209,10 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
if (!getCarried().isEmpty() || sourceCollection == targetCollection && sourcePage == targetPage) return false; if (!getCarried().isEmpty() || sourceCollection == targetCollection && sourcePage == targetPage) return false;
ItemStack sourceBook = desk.getItem(sourceCollection); ItemStack sourceBook = desk.getItem(sourceCollection);
ItemStack targetBook = desk.getItem(targetCollection); ItemStack targetBook = desk.getItem(targetCollection);
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get()); boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
boolean targetNotebook = targetBook.is(ModItems.NOTEBOOK.get()); boolean targetNotebook = CollationFolderItem.isCollection(targetBook);
if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)
|| targetNotebook && targetPage >= CollationFolderItem.capacity(targetBook)) return false;
if ((!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) if ((!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0))
|| (!targetNotebook && (!targetBook.is(ModItems.PAGE.get()) || targetPage != 0))) return false; || (!targetNotebook && (!targetBook.is(ModItems.PAGE.get()) || targetPage != 0))) return false;
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault( var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
@@ -247,7 +251,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private boolean extractPage(int sourceCollection, int sourcePage, int targetSlot) { private boolean extractPage(int sourceCollection, int sourcePage, int targetSlot) {
if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false; if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false;
ItemStack sourceBook = desk.getItem(sourceCollection); ItemStack sourceBook = desk.getItem(sourceCollection);
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get()); boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
if (sourceNotebook && sourcePage >= CollationFolderItem.capacity(sourceBook)) return false;
if (!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) return false; if (!sourceNotebook && (!sourceBook.is(ModItems.PAGE.get()) || sourcePage != 0)) return false;
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault( var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null; ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
@@ -263,12 +268,12 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
Slot target = slots.get(targetSlot); Slot target = slots.get(targetSlot);
if (!target.isActive() || !target.mayPlace(moving)) return false; if (!target.isActive() || !target.mayPlace(moving)) return false;
ItemStack current = target.getItem(); ItemStack current = target.getItem();
if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && current.is(ModItems.NOTEBOOK.get())) { if (targetSlot < WritingDeskBlockEntity.COLLECTIONS && CollationFolderItem.isCollection(current)) {
var targetPages = targetSlot == sourceCollection ? sourcePages var targetPages = targetSlot == sourceCollection ? sourcePages
: new java.util.ArrayList<>(current.getOrDefault( : new java.util.ArrayList<>(current.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
int position = -1; int position = -1;
for (int i = 0; i < NotebookContents.MAX_PAGES; i++) { for (int i = 0; i < CollationFolderItem.capacity(current); i++) {
if (i >= targetPages.size() || targetPages.get(i).isEmpty()) { if (i >= targetPages.size() || targetPages.get(i).isEmpty()) {
position = i; position = i;
break; break;
@@ -342,7 +347,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private boolean moveIntoDesk(ItemStack moving) { private boolean moveIntoDesk(ItemStack moving) {
if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving, if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving,
WritingDeskBlockEntity.INK, WritingDeskBlockEntity.INK + 1, false); WritingDeskBlockEntity.INK, WritingDeskBlockEntity.INK + 1, false);
if (moving.is(ModItems.NOTEBOOK.get())) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false); if (CollationFolderItem.isCollection(moving)) return moveItemStackTo(moving, firstVisibleTab, firstVisibleTab + VISIBLE_TABS, false);
if (moving.is(Items.PAPER)) if (moving.is(Items.PAPER))
return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER, return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER,
WritingDeskBlockEntity.PAPER + 1, false); WritingDeskBlockEntity.PAPER + 1, false);
@@ -374,7 +379,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|| pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY; || pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY;
ItemStack collection = desk.getItem(collectionIndex); ItemStack collection = desk.getItem(collectionIndex);
if (collection.is(ModItems.PAGE.get())) return pageIndex == 0 ? collection : ItemStack.EMPTY; if (collection.is(ModItems.PAGE.get())) return pageIndex == 0 ? collection : ItemStack.EMPTY;
if (collection.is(ModItems.NOTEBOOK.get())) { if (CollationFolderItem.isCollection(collection)) {
if (pageIndex >= CollationFolderItem.capacity(collection)) return ItemStack.EMPTY;
List<ItemStack> pages = collection.getOrDefault( List<ItemStack> pages = collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
return pageIndex < pages.size() ? pages.get(pageIndex) : ItemStack.EMPTY; return pageIndex < pages.size() ? pages.get(pageIndex) : ItemStack.EMPTY;
@@ -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<Level> dimension,
WorldData worldData, LevelStem stem, ServerLevel overworld) {
return AgeRegistrySavedData.get(overworld).findByDimension(dimension.location())
.map(age -> age.seed()).orElse(defaultSeed);
}
}
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.network; package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.book.LinkingService; import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.book.AgeReference; import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData; import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
@@ -18,6 +19,7 @@ import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.menu.BookBinderMenu; import net.dnorberg.rivencraft.menu.BookBinderMenu;
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.OrderedWriting; import net.dnorberg.rivencraft.writing.OrderedWriting;
@@ -59,7 +61,7 @@ public final class ModNetworking {
} }
public static void register(RegisterPayloadHandlersEvent event) { public static void register(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("15"); PayloadRegistrar registrar = event.registrar("17");
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC, registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
ModNetworking::requestDescriptiveBookScreen); ModNetworking::requestDescriptiveBookScreen);
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC, registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
@@ -85,6 +87,10 @@ public final class ModNetworking {
ClientWritingDeskCatalog::handle); ClientWritingDeskCatalog::handle);
registrar.playToServer(SetBookBinderTitlePayload.TYPE, SetBookBinderTitlePayload.STREAM_CODEC, registrar.playToServer(SetBookBinderTitlePayload.TYPE, SetBookBinderTitlePayload.STREAM_CODEC,
ModNetworking::setBookBinderTitle); 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) { private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) {
@@ -95,6 +101,21 @@ public final class ModNetworking {
} }
} }
private static void setLinkModifierTitle(SetLinkModifierTitlePayload payload, IPayloadContext context) {
if (context.player() instanceof ServerPlayer player
&& player.containerMenu instanceof LinkModifierMenu menu
&& menu.containerId == payload.containerId()) {
menu.setTitle(player, payload.title());
}
}
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) { private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) { if (!(context.player() instanceof ServerPlayer player)) {
return; return;
@@ -163,7 +184,9 @@ public final class ModNetworking {
try { try {
AgeReference reference = new AgeReference(UUID.randomUUID(), title); AgeReference reference = new AgeReference(UUID.randomUUID(), title);
if (AgeDimensionService.resolve(player, reference, access.stack() 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); player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
return; return;
} }
@@ -269,7 +292,9 @@ public final class ModNetworking {
LinkTarget target; LinkTarget target;
if (payload.descriptiveBook() && stack.is(ModItems.DESCRIPTIVE_BOOK.get())) { if (payload.descriptiveBook() && stack.is(ModItems.DESCRIPTIVE_BOOK.get())) {
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.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())) { } else if (!payload.descriptiveBook() && stack.is(ModItems.LINKING_BOOK.get())) {
target = stack.get(ModDataComponents.LINK_TARGET.get()); target = stack.get(ModDataComponents.LINK_TARGET.get());
} else { } else {
@@ -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<SetLinkModifierSeedPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_link_modifier_seed"));
public static final StreamCodec<RegistryFriendlyByteBuf, SetLinkModifierSeedPayload> STREAM_CODEC =
StreamCodec.composite(ByteBufCodecs.VAR_INT, SetLinkModifierSeedPayload::containerId,
ByteBufCodecs.STRING_UTF8, SetLinkModifierSeedPayload::seed,
SetLinkModifierSeedPayload::new);
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
}
@@ -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 SetLinkModifierTitlePayload(int containerId, String title) implements CustomPacketPayload {
public static final Type<SetLinkModifierTitlePayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_link_modifier_title"));
public static final StreamCodec<RegistryFriendlyByteBuf, SetLinkModifierTitlePayload> STREAM_CODEC =
StreamCodec.composite(ByteBufCodecs.VAR_INT, SetLinkModifierTitlePayload::containerId,
ByteBufCodecs.STRING_UTF8, SetLinkModifierTitlePayload::title,
SetLinkModifierTitlePayload::new);
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
}
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity; import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity; import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity; import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
import net.dnorberg.rivencraft.block.entity.LinkModifierBlockEntity;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity; import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity; import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity; import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
@@ -31,6 +32,9 @@ public final class ModBlockEntities {
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<InkMixerBlockEntity>> INK_MIXER = public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<InkMixerBlockEntity>> INK_MIXER =
BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder
.of(InkMixerBlockEntity::new, ModBlocks.INK_MIXER.get()).build(null)); .of(InkMixerBlockEntity::new, ModBlocks.INK_MIXER.get()).build(null));
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<LinkModifierBlockEntity>> LINK_MODIFIER =
BLOCK_ENTITIES.register("link_modifier", () -> BlockEntityType.Builder
.of(LinkModifierBlockEntity::new, ModBlocks.LINK_MODIFIER.get()).build(null));
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<StarFissureBlockEntity>> STAR_FISSURE = public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<StarFissureBlockEntity>> STAR_FISSURE =
BLOCK_ENTITIES.register("star_fissure", () -> BlockEntityType.Builder BLOCK_ENTITIES.register("star_fissure", () -> BlockEntityType.Builder
.of(StarFissureBlockEntity::new, ModBlocks.STAR_FISSURE.get()).build(null)); .of(StarFissureBlockEntity::new, ModBlocks.STAR_FISSURE.get()).build(null));
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.block.BookStandBlock;
import net.dnorberg.rivencraft.block.BookBinderBlock; import net.dnorberg.rivencraft.block.BookBinderBlock;
import net.dnorberg.rivencraft.block.WritingDeskBlock; import net.dnorberg.rivencraft.block.WritingDeskBlock;
import net.dnorberg.rivencraft.block.InkMixerBlock; import net.dnorberg.rivencraft.block.InkMixerBlock;
import net.dnorberg.rivencraft.block.LinkModifierBlock;
import net.dnorberg.rivencraft.block.BuddingCrystalBlock; import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
import net.dnorberg.rivencraft.block.StarFissureBlock; import net.dnorberg.rivencraft.block.StarFissureBlock;
import net.dnorberg.rivencraft.block.DecayBlock; import net.dnorberg.rivencraft.block.DecayBlock;
@@ -40,6 +41,9 @@ public final class ModBlocks {
"ink_mixer", InkMixerBlock::new, "ink_mixer", InkMixerBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion() BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion()
); );
public static final DeferredBlock<LinkModifierBlock> LINK_MODIFIER = BLOCKS.registerBlock(
"link_modifier", LinkModifierBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block", public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
Block::new, BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK).mapColor(MapColor.COLOR_BLUE)); Block::new, BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK).mapColor(MapColor.COLOR_BLUE));
public static final DeferredBlock<BookReceptacleBlock> BOOK_RECEPTACLE = BLOCKS.registerBlock("book_receptacle", public static final DeferredBlock<BookReceptacleBlock> BOOK_RECEPTACLE = BLOCKS.registerBlock("book_receptacle",
@@ -27,9 +27,12 @@ public final class ModCreativeTabs {
output.accept(ModItems.DESCRIPTIVE_BOOK.get()); output.accept(ModItems.DESCRIPTIVE_BOOK.get());
output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.NOTEBOOK.get()); output.accept(ModItems.NOTEBOOK.get());
output.accept(ModItems.COLLATION_FOLDER.get());
output.accept(ModItems.SEALED_NOTEBOOK.get());
output.accept(ModItems.INK_VIAL.get()); output.accept(ModItems.INK_VIAL.get());
output.accept(ModItems.WRITING_DESK.get()); output.accept(ModItems.WRITING_DESK.get());
output.accept(ModItems.INK_MIXER.get()); output.accept(ModItems.INK_MIXER.get());
output.accept(ModItems.LINK_MODIFIER.get());
output.accept(ModItems.BOOK_BINDER.get()); output.accept(ModItems.BOOK_BINDER.get());
output.accept(ModItems.CRYSTAL_BLOCK.get()); output.accept(ModItems.CRYSTAL_BLOCK.get());
output.accept(ModItems.BOOK_RECEPTACLE.get()); output.accept(ModItems.BOOK_RECEPTACLE.get());
@@ -5,6 +5,8 @@ import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.NotebookItem; import net.dnorberg.rivencraft.item.NotebookItem;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.item.SealedNotebookItem;
import net.dnorberg.rivencraft.item.InkVialItem; import net.dnorberg.rivencraft.item.InkVialItem;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.BlockItem;
@@ -35,6 +37,10 @@ public final class ModItems {
NotebookItem::new, NotebookItem::new,
new Item.Properties().stacksTo(1) new Item.Properties().stacksTo(1)
); );
public static final DeferredItem<CollationFolderItem> COLLATION_FOLDER = ITEMS.registerItem(
"collation_folder", CollationFolderItem::new, new Item.Properties().stacksTo(1));
public static final DeferredItem<SealedNotebookItem> SEALED_NOTEBOOK = ITEMS.registerItem(
"sealed_notebook", SealedNotebookItem::new, new Item.Properties().stacksTo(16));
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem( public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
"ink_vial", "ink_vial",
InkVialItem::new, InkVialItem::new,
@@ -43,6 +49,7 @@ public final class ModItems {
public static final DeferredItem<BlockItem> WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK); public static final DeferredItem<BlockItem> WRITING_DESK = ITEMS.registerSimpleBlockItem(ModBlocks.WRITING_DESK);
public static final DeferredItem<BlockItem> BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER); public static final DeferredItem<BlockItem> BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER);
public static final DeferredItem<BlockItem> INK_MIXER = ITEMS.registerSimpleBlockItem(ModBlocks.INK_MIXER); public static final DeferredItem<BlockItem> INK_MIXER = ITEMS.registerSimpleBlockItem(ModBlocks.INK_MIXER);
public static final DeferredItem<BlockItem> LINK_MODIFIER = ITEMS.registerSimpleBlockItem(ModBlocks.LINK_MODIFIER);
public static final DeferredItem<BlockItem> CRYSTAL_BLOCK = ITEMS.registerSimpleBlockItem(ModBlocks.CRYSTAL_BLOCK); public static final DeferredItem<BlockItem> CRYSTAL_BLOCK = ITEMS.registerSimpleBlockItem(ModBlocks.CRYSTAL_BLOCK);
public static final DeferredItem<BlockItem> BOOK_RECEPTACLE = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_RECEPTACLE); public static final DeferredItem<BlockItem> BOOK_RECEPTACLE = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_RECEPTACLE);
public static final DeferredItem<BlockItem> BUDDING_CRYSTAL = ITEMS.registerSimpleBlockItem(ModBlocks.BUDDING_CRYSTAL); public static final DeferredItem<BlockItem> BUDDING_CRYSTAL = ITEMS.registerSimpleBlockItem(ModBlocks.BUDDING_CRYSTAL);
@@ -5,6 +5,7 @@ import net.dnorberg.rivencraft.menu.NotebookMenu;
import net.dnorberg.rivencraft.menu.WritingDeskMenu; import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.menu.BookBinderMenu; import net.dnorberg.rivencraft.menu.BookBinderMenu;
import net.dnorberg.rivencraft.menu.InkMixerMenu; import net.dnorberg.rivencraft.menu.InkMixerMenu;
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.neoforged.bus.api.IEventBus; import net.neoforged.bus.api.IEventBus;
@@ -24,6 +25,8 @@ public final class ModMenus {
MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new)); MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new));
public static final DeferredHolder<MenuType<?>, MenuType<InkMixerMenu>> INK_MIXER = public static final DeferredHolder<MenuType<?>, MenuType<InkMixerMenu>> INK_MIXER =
MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new)); MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new));
public static final DeferredHolder<MenuType<?>, MenuType<LinkModifierMenu>> LINK_MODIFIER =
MENUS.register("link_modifier", () -> IMenuTypeExtension.create(LinkModifierMenu::new));
private ModMenus() { private ModMenus() {
} }
@@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.item.PageItem; import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModDataComponents; import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems; import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@@ -24,7 +25,7 @@ public final class WritingDeskCatalog {
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) { for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
ItemStack collection = desk.getItem(slot); ItemStack collection = desk.getItem(slot);
if (collection.is(ModItems.PAGE.get())) add(unique, collection); if (collection.is(ModItems.PAGE.get())) add(unique, collection);
if (collection.is(ModItems.NOTEBOOK.get())) { if (CollationFolderItem.isCollection(collection)) {
List<ItemStack> pages = collection.getOrDefault( List<ItemStack> pages = collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages(); ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
pages.forEach(page -> add(unique, page)); pages.forEach(page -> add(unique, page));
@@ -0,0 +1,8 @@
{
"variants": {
"facing=north": { "model": "rivencraft:block/link_modifier" },
"facing=east": { "model": "rivencraft:block/link_modifier", "y": 90 },
"facing=south": { "model": "rivencraft:block/link_modifier", "y": 180 },
"facing=west": { "model": "rivencraft:block/link_modifier", "y": 270 }
}
}
@@ -15,8 +15,27 @@
"item.rivencraft.link_property.disarm": "Disarm", "item.rivencraft.link_property.disarm": "Disarm",
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum", "item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
"item.rivencraft.link_property.intra_linking": "Intra Linking", "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.page.unresolved": "Unresolved Symbol Page",
"item.rivencraft.notebook": "Notebook", "item.rivencraft.notebook": "Notebook",
"item.rivencraft.collation_folder": "Collation Folder",
"item.rivencraft.sealed_notebook": "Sealed Notebook",
"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.",
"message.rivencraft.link_modifier.delete.already_dead": "This Age is already retired.",
"message.rivencraft.link_modifier.delete.occupied": "Cannot retire an Age while players are inside it.",
"message.rivencraft.link_modifier.delete.unload_failed": "Could not unload this Age; its data is unchanged.",
"message.rivencraft.link_modifier.delete.storage_failed": "Could not archive this Age's data; it was not retired.",
"item.rivencraft.notebook.pages": "Pages: %s/%s", "item.rivencraft.notebook.pages": "Pages: %s/%s",
"item.rivencraft.notebook.unresolved": "Unresolved pages: %s", "item.rivencraft.notebook.unresolved": "Unresolved pages: %s",
"item.rivencraft.ink_vial": "Ink Vial", "item.rivencraft.ink_vial": "Ink Vial",
@@ -88,6 +107,7 @@
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable", "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.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_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.unwritten": "This Descriptive Book is unwritten",
"message.rivencraft.descriptive_book.created": "Created Age: %s", "message.rivencraft.descriptive_book.created": "Created Age: %s",
"message.rivencraft.descriptive_book.unavailable": "That Age is unavailable", "message.rivencraft.descriptive_book.unavailable": "That Age is unavailable",
@@ -0,0 +1,120 @@
{
"parent": "block/block",
"textures": {
"particle": "rivencraft:block/link_modifier_base",
"0": "rivencraft:block/link_modifier_base",
"1": "rivencraft:block/link_modifier_details"
},
"elements": [
{
"from": [ 0.0, 0.0, 0.0 ],
"to": [ 16.0, 12.0, 16.0 ],
"faces": {
"north": { "texture": "#0", "uv": [ 0.0, 4.0, 4.0, 7.0 ] },
"east": { "texture": "#0", "uv": [ 4.0, 4.0, 8.0, 7.0 ] },
"south": { "texture": "#0", "uv": [ 8.0, 4.0, 12.0, 7.0 ] },
"west": { "texture": "#0", "uv": [ 12.0, 4.0, 16.0, 7.0 ] },
"up": { "texture": "#0", "uv": [ 4.0, 0.0, 8.0, 4.0 ] },
"down": { "texture": "#0", "uv": [ 8.0, 0.0, 12.0, 4.0 ] }
}
},
{
"from": [ 1.5, 12.0, 11.0 ],
"to": [ 3.5, 13.0, 13.0 ],
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
}
},
{
"from": [ 3.7, 12.0, 11.0 ],
"to": [ 5.7, 13.0, 13.0 ],
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
}
},
{
"from": [ 5.9, 12.0, 11.0 ],
"to": [ 7.9, 13.0, 13.0 ],
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
}
},
{
"from": [ 8.1, 12.0, 11.0 ],
"to": [ 10.1, 13.0, 13.0 ],
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
}
},
{
"from": [ 10.3, 12.0, 11.0 ],
"to": [ 12.3, 13.0, 13.0 ],
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
}
},
{
"from": [ 12.5, 12.0, 11.0 ],
"to": [ 14.5, 13.0, 13.0 ],
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 15.0, 1.0, 15.5 ] },
"east": { "texture": "#1", "uv": [ 0.0, 14.0, 1.0, 14.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 13.0, 1.0, 13.5 ] },
"west": { "texture": "#1", "uv": [ 1.0, 13.0, 1.0, 13.5 ] },
"up": { "texture": "#1", "uv": [ 2.0, 14.0, 3.0, 15.0 ] },
"down": { "texture": "#1", "uv": [ 1.0, 13.0, 2.0, 14.0 ] }
}
},
{
"from": [ 2.0, 9.4, 1.0 ],
"to": [ 14.0, 12.4, 8.0 ],
"rotation": { "origin": [ 8.0, 8.0, 8.0 ], "axis": "x", "angle": 22.5 },
"faces": {
"north": { "texture": "#1", "uv": [ 0.0, 4.0, 6.0, 5.5 ] },
"east": { "texture": "#1", "uv": [ 6.0, 4.0, 9.5, 5.5 ] },
"south": { "texture": "#1", "uv": [ 0.0, 6.0, 6.0, 7.5 ] },
"west": { "texture": "#1", "uv": [ 6.0, 6.0, 9.5, 7.5 ] },
"up": { "texture": "#1", "uv": [ 0.0, 0.0, 6.0, 3.5 ] },
"down": { "texture": "#1", "uv": [ 6.0, 0.0, 12.0, 3.5 ] }
}
},
{
"from": [ 4.0, 11.5, 3.5 ],
"to": [ 12.0, 15.5, 5.5 ],
"rotation": { "origin": [ 8.0, 8.0, 8.0 ], "axis": "x", "angle": 22.5 },
"faces": {
"north": { "texture": "#1", "uv": [ 1.0, 9.0, 5.0, 11.0 ] },
"east": { "texture": "#1", "uv": [ 0.0, 8.0, 1.0, 11.0 ] },
"south": { "texture": "#1", "uv": [ 6.0, 9.0, 10.0, 11.0 ] },
"west": { "texture": "#1", "uv": [ 5.0, 9.0, 6.0, 11.0 ] },
"up": { "texture": "#1", "uv": [ 1.0, 8.0, 5.0, 9.0 ] },
"down": { "texture": "#1", "uv": [ 5.0, 8.0, 9.0, 9.0 ] }
}
}
]
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/collation_folder" }
}
@@ -0,0 +1 @@
{ "parent": "rivencraft:block/link_modifier" }
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/sealed_notebook" }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

@@ -0,0 +1,4 @@
{
"type": "minecraft:block",
"pools": [{ "rolls": 1, "entries": [{ "type": "minecraft:item", "name": "rivencraft:link_modifier" }] }]
}
@@ -9,7 +9,7 @@
{ "type": "minecraft:item", "name": "minecraft:leather", "weight": 5, { "type": "minecraft:item", "name": "minecraft:leather", "weight": 5,
"functions": [{ "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 1, "max": 3 } }] }, "functions": [{ "function": "minecraft:set_count", "count": { "type": "minecraft:uniform", "min": 1, "max": 3 } }] },
{ "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 4 }, { "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 4 },
{ "type": "minecraft:item", "name": "minecraft:book", "weight": 3 }, { "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 20 },
{ "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 } { "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 }
] ]
}, },
@@ -17,7 +17,7 @@
"rolls": { "type": "minecraft:uniform", "min": 1, "max": 2 }, "rolls": { "type": "minecraft:uniform", "min": 1, "max": 2 },
"entries": [ "entries": [
{ "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 3 }, { "type": "minecraft:item", "name": "rivencraft:ink_vial", "weight": 3 },
{ "type": "minecraft:item", "name": "minecraft:book", "weight": 2 }, { "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 2 },
{ "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 } { "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 }
] ]
} }
@@ -4,6 +4,7 @@
"package": "net.dnorberg.rivencraft.mixin", "package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"mixins": [ "mixins": [
"AgeDimensionSeedMixin",
"AgeDungeonPlacementMixin", "AgeDungeonPlacementMixin",
"AgeStructureBiomeMixin", "AgeStructureBiomeMixin",
"StructurePlacementAccessor" "StructurePlacementAccessor"