Compare commits

...
2 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
60 changed files with 1176 additions and 57 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
@@ -36,7 +36,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.6.6.8
mod_version=0.6.6.11
immersive_portals_version=6.0.7
terrablender_version=4.1.0.8
serene_seasons_version_id=pHEgQQUE
@@ -97,7 +97,11 @@ public final class BookReceptacleBlock extends BaseEntityBlock {
LinkTarget destination = stack.is(ModItems.LINKING_BOOK.get())
? stack.get(ModDataComponents.LINK_TARGET.get())
: player instanceof ServerPlayer serverPlayer
? AgeDimensionService.resolve(serverPlayer, stack.get(ModDataComponents.AGE_REFERENCE.get())).orElse(null)
? AgeDimensionService.resolve(serverPlayer, stack.get(ModDataComponents.AGE_REFERENCE.get()),
net.dnorberg.rivencraft.writing.OrderedWriting.EMPTY,
!stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(),
net.dnorberg.rivencraft.book.LinkProperties.NONE)
.has(net.dnorberg.rivencraft.book.LinkProperties.DISABLE_PLATFORM)).orElse(null)
: null;
if (destination == null || server.getServer().getLevel(destination.location().dimension()) == null) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
@@ -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.ModItems;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.OrderedWriting;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.WritingGrammar;
@@ -96,7 +97,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
case COVER -> validCover(stack);
case LINK_PANEL -> stack.is(ModItems.PAGE.get())
&& 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);
};
}
@@ -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. */
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();
if (pages.isEmpty()) return ItemStack.EMPTY;
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.PageItem;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.InkVialExchange;
import net.dnorberg.rivencraft.writing.WritingInkService;
import net.dnorberg.rivencraft.writing.PageContent;
@@ -87,7 +88,7 @@ public final class WritingDeskBlockEntity extends BlockEntity implements Contain
worldPosition.getZ() + 0.5) <= 64.0;
}
@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)
.pages().stream().noneMatch(page -> PageItem.content(page).kind() == PageContent.Kind.LINK_PANEL)
|| 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) -> {
ServerLevel overworld = server.overworld();
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
if (AgeRegistrySavedData.get(overworld).isDead(age.id())) continue;
if (!canLoadProfile(server, age)) continue;
ServerLevel level = loader.loadDynamicDimension(
age.dimensionId(),
@@ -95,7 +96,13 @@ public final class AgeDimensionService {
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference reference,
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);
if (AgeRegistrySavedData.get(player.serverLevel()).isDead(definition.id())) return Optional.empty();
ServerLevel level = ensureLoaded(player.getServer(), definition);
if (level == null) {
return Optional.empty();
@@ -104,7 +111,8 @@ public final class AgeDimensionService {
BlockPos spawn = definition.spawn();
if (spawn == null) {
applyInitialCelestialPhase(level, definition);
spawn = createSafeSpawn(level, definition);
spawn = createSafeSpawn(level, definition, generatePlatform);
if (spawn == null) return Optional.empty();
if (AgeFeatureSelection.resolved(definition.plan()).stream()
.anyMatch(choice -> choice.id().equals(AgeFeatureSelection.STAR_FISSURES))
|| definition.randomFeatures().stream()
@@ -122,6 +130,7 @@ public final class AgeDimensionService {
}
public static ServerLevel ensureLoaded(MinecraftServer server, AgeDefinition definition) {
if (AgeRegistrySavedData.get(server.overworld()).isDead(definition.id())) return null;
ResourceKey<Level> key = dimensionKey(definition);
ServerLevel existing = server.getLevel(key);
if (existing != null) {
@@ -216,21 +225,24 @@ public final class AgeDimensionService {
return ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, definition.dimensionId());
}
private static BlockPos createSafeSpawn(ServerLevel level, AgeDefinition age) {
private static BlockPos createSafeSpawn(ServerLevel level, AgeDefinition age, boolean generatePlatform) {
BlockPos floorCenter;
if (age.generationProfile().equals(AgeGenerationProfiles.CAVE)) {
// Cave Ages are enclosed. Searching from the world ceiling would
// select or expose their roof, so begin with the floor beneath a
// player standing at Y=64 and search downward from there.
floorCenter = highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1)
.orElseGet(() -> fallbackSpawnFloor(level));
.orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null);
} else if (isNetherStyle(level)) {
floorCenter = netherSpawnFloor(level);
floorCenter = generatePlatform ? netherSpawnFloor(level)
: highestFootedSurface(level, CAVE_SPAWN_SEARCH_Y - 1).orElse(null);
} else {
floorCenter = highestFootedSurface(level).orElseGet(() -> fallbackSpawnFloor(level));
floorCenter = highestFootedSurface(level).orElseGet(() -> generatePlatform ? fallbackSpawnFloor(level) : null);
}
for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) {
if (floorCenter == null) return null;
if (generatePlatform) for (int x = -PLATFORM_RADIUS; x <= PLATFORM_RADIUS; x++) {
for (int z = -PLATFORM_RADIUS; z <= PLATFORM_RADIUS; z++) {
BlockPos floor = floorCenter.offset(x, 0, z);
level.setBlock(floor, ModBlocks.CLOUD_PLATFORM.get().defaultBlockState(), 2);
@@ -241,6 +253,11 @@ public final class AgeDimensionService {
}
BlockPos spawn = floorCenter.above();
if (!generatePlatform) {
var safe = LinkingService.resolveDestination(level, spawn, true);
if (safe.isEmpty()) return null;
spawn = BlockPos.containing(safe.get());
}
level.setDefaultSpawnPos(spawn, 0.0F);
return spawn;
}
@@ -23,6 +23,49 @@ public final class AgePersistenceGameTests {
private AgePersistenceGameTests() {
}
@GameTest(template = "empty")
public static void modifierSeedChangePersistsWithoutChangingAgeIdentity(GameTestHelper helper) {
UUID id = UUID.randomUUID();
AgeDefinition original = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
new AgeReference(id, "Reseed " + id));
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
long newSeed = original.seed() == 987654321L ? 987654322L : 987654321L;
helper.assertTrue(registry.reseed(helper.getLevel().getServer(), id, newSeed),
"An unoccupied Age must accept a new seed");
AgeDefinition changed = registry.find(id).orElseThrow();
helper.assertValueEqual(changed.id(), original.id(), "Reseeding must preserve the Age identity");
helper.assertValueEqual(changed.dimensionId(), original.dimensionId(),
"Reseeding must preserve its dimension and existing chunk location");
helper.assertValueEqual(changed.seed(), newSeed, "The new generator seed must be saved");
AgeDefinition restored = AgeRegistrySavedData.load(
registry.save(new CompoundTag(), helper.getLevel().registryAccess()),
helper.getLevel().registryAccess()).find(id).orElseThrow();
helper.assertValueEqual(restored.seed(), newSeed, "The new seed must survive reload");
var ageLevel = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), changed);
helper.assertTrue(ageLevel != null, "The reseeded Age must reload");
helper.assertValueEqual(ageLevel.getSeed(), newSeed,
"The runtime level must use the saved seed for new chunks");
helper.succeed();
}
@GameTest(template = "empty")
public static void retiredAgeIdentitySurvivesSaveAndCannotLoad(GameTestHelper helper) {
UUID id = UUID.randomUUID();
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")
public static void authoredPlanRetainsResolvedMeaningAndAgeIdentity(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of(
@@ -5,6 +5,9 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
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.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
@@ -13,6 +16,7 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.level.saveddata.SavedData;
import net.neoforged.fml.ModList;
@@ -21,6 +25,7 @@ import net.neoforged.fml.ModList;
public final class AgeRegistrySavedData extends SavedData {
private static final String DATA_NAME = "rivencraft_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 NAME_TAG = "Name";
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 Set<UUID> deadAges = new HashSet<>();
public static AgeRegistrySavedData get(ServerLevel level) {
return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME);
@@ -110,6 +116,16 @@ public final class AgeRegistrySavedData extends SavedData {
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) {
AgeDefinition existing = ages.get(ageId);
if (existing == null) {
@@ -121,6 +137,32 @@ public final class AgeRegistrySavedData extends SavedData {
return updated;
}
/** Changes the generator for future chunks; existing chunk data is deliberately preserved. */
public boolean reseed(MinecraftServer server, UUID ageId, long seed) {
AgeDefinition previous = ages.get(ageId);
if (previous == null || isDead(ageId)) return false;
if (previous.seed() == seed) return true;
ServerLevel loaded = server.getLevel(AgeDimensionService.dimensionKey(previous));
if (loaded != null && !loaded.players().isEmpty()) return false;
// Legacy changes the Age's seed, not its already-written pages or selected traits.
// Keeping the frozen plan and feature list prevents a visited Age from changing
// its identity while newly generated chunks use the revised terrain noise.
AgeGenerationPlan oldPlan = previous.plan();
AgeGenerationPlan plan = oldPlan == null ? null : new AgeGenerationPlan(oldPlan.version(), seed,
oldPlan.writing(), oldPlan.symbols(), oldPlan.runtimeProfile(), oldPlan.biomeSelection(),
oldPlan.activeSymbolIndices(), oldPlan.writingInstability());
AgeDefinition changed = new AgeDefinition(previous.id(), previous.name(), seed,
previous.generationProfile(), previous.generationVersion(), previous.createdGameTime(),
previous.dimensionId(), previous.spawn(), previous.traits(), plan, previous.seasonMode(),
previous.randomFeatures(), previous.instability(), previous.instabilityPlan());
if (AgeDimensionService.unload(server, previous) == AgeDimensionService.UnloadResult.FAILED)
return false;
ages.put(ageId, changed);
setDirty();
return true;
}
private AgeDefinition getOrCreate(AgeReference reference, OrderedWriting writing,
long worldSeed, long createdGameTime, RegistryAccess registries) {
AgeDefinition existing = ages.get(reference.id());
@@ -161,6 +203,11 @@ public final class AgeRegistrySavedData extends SavedData {
static AgeRegistrySavedData load(CompoundTag tag, HolderLookup.Provider registries) {
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);
for (int index = 0; index < serializedAges.size(); index++) {
CompoundTag entry = serializedAges.getCompound(index);
@@ -264,6 +311,9 @@ public final class AgeRegistrySavedData extends SavedData {
serializedAges.add(entry);
});
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;
}
@@ -11,11 +11,15 @@ import net.minecraft.network.chat.Component;
/** Flags written into a Link Panel and carried by books assembled from it. */
public record LinkProperties(int flags) {
public static final int DISARM = 1;
/** Retained only to read pre-0.4.7.1 test books; no longer generated or applied. */
/** Retained to read older test books; a newly edited book uses DISABLE_PLATFORM instead. */
public static final int GENERATE_PLATFORM = 2;
public static final int MAINTAIN_MOMENTUM = 4;
public static final int INTRA_LINKING = 8;
private static final int ALL_FLAGS = DISARM | GENERATE_PLATFORM | MAINTAIN_MOMENTUM | INTRA_LINKING;
public static final int INTRA_LINKING_ONLY = 16;
public static final int RELATIVE_LINK = 32;
public static final int DISABLE_PLATFORM = 64;
private static final int ALL_FLAGS = DISARM | GENERATE_PLATFORM | MAINTAIN_MOMENTUM | INTRA_LINKING
| INTRA_LINKING_ONLY | RELATIVE_LINK | DISABLE_PLATFORM;
public static final LinkProperties NONE = new LinkProperties(0);
public static final Codec<LinkProperties> CODEC = Codec.intRange(0, ALL_FLAGS).xmap(
LinkProperties::new, LinkProperties::flags);
@@ -35,13 +39,16 @@ public record LinkProperties(int flags) {
/** Intra Linking only governs Linking Books; a Descriptive Book treats that panel as ordinary. */
public LinkProperties forDescriptiveBook() {
return new LinkProperties(flags & ~INTRA_LINKING);
return new LinkProperties(flags & ~(INTRA_LINKING | INTRA_LINKING_ONLY));
}
public void appendTooltip(List<Component> lines) {
if (has(DISARM)) lines.add(propertyName("disarm"));
if (has(MAINTAIN_MOMENTUM)) lines.add(propertyName("maintain_momentum"));
if (has(INTRA_LINKING)) lines.add(propertyName("intra_linking"));
if (has(INTRA_LINKING_ONLY)) lines.add(propertyName("intra_linking_only"));
if (has(RELATIVE_LINK)) lines.add(propertyName("relative_link"));
if (has(DISABLE_PLATFORM)) lines.add(propertyName("disable_platform"));
}
private static Component propertyName(String name) {
@@ -54,6 +61,9 @@ public record LinkProperties(int flags) {
case DISARM -> propertyName("disarm");
case MAINTAIN_MOMENTUM -> propertyName("maintain_momentum");
case INTRA_LINKING -> propertyName("intra_linking");
case INTRA_LINKING_ONLY -> propertyName("intra_linking_only");
case RELATIVE_LINK -> propertyName("relative_link");
case DISABLE_PLATFORM -> propertyName("disable_platform");
default -> throw new IllegalArgumentException("Unknown link property flag: " + flag);
};
}
@@ -86,7 +86,10 @@ public final class LinkingService {
if (destinationLevel == null) {
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()) {
return LinkResult.NO_SAFE_DESTINATION;
}
@@ -105,7 +108,10 @@ public final class LinkingService {
ServerLevel destinationLevel = player.getServer().getLevel(target.location().dimension());
if (destinationLevel == null) return LinkResult.MISSING_DIMENSION;
boolean instant = destinationChunksLoaded(destinationLevel, target.location().pos());
Optional<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;
Vec3 momentum = player.getDeltaMovement();
if (properties.has(LinkProperties.DISARM)) dropInventoryAtSource(player);
@@ -121,11 +127,15 @@ public final class LinkingService {
if (destinationLevel == null) {
return LinkResult.MISSING_DIMENSION;
}
if (destinationLevel == player.level() && !properties.has(LinkProperties.INTRA_LINKING))
if (properties.has(LinkProperties.INTRA_LINKING_ONLY) && destinationLevel != player.serverLevel())
return LinkResult.INTRA_ONLY;
if (destinationLevel == player.level() && !properties.has(LinkProperties.INTRA_LINKING)
&& !properties.has(LinkProperties.INTRA_LINKING_ONLY))
return LinkResult.INTRA_LINKING_REQUIRED;
boolean instant = !portal || destinationChunksLoaded(destinationLevel, target.location().pos());
Optional<Vec3> destination = resolveDestination(destinationLevel, target.location().pos(), requiresFooting);
Optional<Vec3> destination = resolveDestination(destinationLevel,
landingOrigin(player, destinationLevel, target, properties), requiresFooting);
if (destination.isEmpty()) {
return LinkResult.NO_SAFE_DESTINATION;
}
@@ -139,6 +149,17 @@ public final class LinkingService {
return LinkResult.SUCCESS;
}
/** Legacy relative links retain the traveller's offset from the source spawn. */
private static BlockPos landingOrigin(ServerPlayer player, ServerLevel destinationLevel,
LinkTarget target, LinkProperties properties) {
if (!properties.has(LinkProperties.RELATIVE_LINK)) return target.location().pos();
BlockPos sourceSpawn = player.serverLevel().getSharedSpawnPos();
BlockPos destinationSpawn = destinationLevel.getSharedSpawnPos();
BlockPos departure = player.blockPosition();
return destinationSpawn.offset(departure.getX() - sourceSpawn.getX(),
departure.getY() - sourceSpawn.getY(), departure.getZ() - sourceSpawn.getZ());
}
/** Drop each inventory stack as an item entity at the departure point. */
private static void dropInventoryAtSource(ServerPlayer player) {
var inventory = player.getInventory();
@@ -263,6 +284,7 @@ public final class LinkingService {
SUCCESS,
MISSING_DIMENSION,
INTRA_LINKING_REQUIRED,
INTRA_ONLY,
NO_SAFE_DESTINATION
}
}
@@ -26,6 +26,7 @@ import net.dnorberg.rivencraft.client.screen.NotebookScreen;
import net.dnorberg.rivencraft.client.screen.WritingDeskScreen;
import net.dnorberg.rivencraft.client.screen.BookBinderScreen;
import net.dnorberg.rivencraft.client.screen.InkMixerScreen;
import net.dnorberg.rivencraft.client.screen.LinkModifierScreen;
import net.dnorberg.rivencraft.registry.ModMenus;
@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.BOOK_BINDER.get(), BookBinderScreen::new);
event.register(ModMenus.INK_MIXER.get(), InkMixerScreen::new);
event.register(ModMenus.LINK_MODIFIER.get(), LinkModifierScreen::new);
}
@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.Tooltip;
import net.minecraft.network.chat.Component;
import net.minecraft.core.component.DataComponents;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvents;
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 final AgeReference age;
private final Component displayName;
private final ResourceLocation generationProfile;
private final AgeTraits traits;
private final OrderedWriting writing;
@@ -68,6 +70,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
List<SymbolDisplayMetadata> writingSymbols) {
super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get());
displayName = book.get(DataComponents.CUSTOM_NAME);
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
this.traits = traits;
writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY);
@@ -82,6 +85,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
List<SymbolDisplayMetadata> writingSymbols) {
super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get());
displayName = book.get(DataComponents.CUSTOM_NAME);
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
this.traits = traits;
writing = book.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY);
@@ -245,7 +249,8 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
}
return;
}
int nameEnd = drawWrappedWithoutShadow(graphics, Component.literal(age.name()),
int nameEnd = drawWrappedWithoutShadow(graphics,
displayName == null ? Component.literal(age.name()) : displayName,
x, top + 40, 110);
int detailsY = drawWrappedWithoutShadow(graphics,
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() {
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()) {
return index + 1;
}
@@ -12,6 +12,7 @@ import net.dnorberg.rivencraft.menu.WritingDeskMenu;
import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import net.minecraft.client.gui.GuiGraphics;
@@ -234,7 +235,7 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private List<ItemStack> pages(int collectionIndex) {
ItemStack collection = menu.getSlot(collectionIndex).getItem();
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 List.of();
@@ -301,9 +302,9 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
private boolean canMovePage(DisplayPage source, DisplayPage target) {
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(target.collection()).getItem().is(ModItems.NOTEBOOK.get())
&& (CollationFolderItem.isCollection(menu.getSlot(target.collection()).getItem())
|| menu.getSlot(target.collection()).getItem().is(ModItems.PAGE.get()))
&& (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;
ItemStack current = slot.getItem();
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(
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;
} else if (index > WritingDeskMenu.COPY_RESULT && ItemStack.isSameItemSameComponents(current, held)
&& current.getCount() < slot.getMaxStackSize(held)) return index;
@@ -112,8 +112,9 @@ public final class RivencraftCommands {
source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false);
for (AgeDefinition age : ages) {
boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null;
boolean retired = AgeRegistrySavedData.get(source.getLevel()).isDead(age.id());
source.sendSuccess(() -> Component.literal(
(loaded ? "[loaded] " : "[unloaded] ")
(retired ? "[retired] " : loaded ? "[loaded] " : "[unloaded] ")
+ age.name() + " — " + age.dimensionId()
+ (age.seasonMode() == null ? "" : " (season: "
+ age.seasonMode().name().toLowerCase(java.util.Locale.ROOT) + ")")
@@ -14,6 +14,26 @@ import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
public final class 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")
public static void linkSoundPriorityMatchesLegacy(GameTestHelper helper) {
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 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) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
return false;
@@ -69,8 +72,6 @@ public final class DescriptiveBookItem extends Item {
ServerLevel sourceLevel = player.serverLevel();
Vec3 sourcePosition = player.position();
LinkProperties properties = stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.forDescriptiveBook();
LinkingService.LinkResult result = portal
? LinkingService.travelDescriptivePortal(player, ageTarget, properties)
: hand == null ? LinkingService.travelWithFooting(player, ageTarget, properties)
@@ -79,6 +79,7 @@ public final class LinkingBookItem extends Item {
String errorKey = switch (result) {
case MISSING_DIMENSION -> "message.rivencraft.linking_book.missing_dimension";
case INTRA_LINKING_REQUIRED -> "message.rivencraft.linking_book.intra_required";
case INTRA_ONLY -> "message.rivencraft.linking_book.intra_only";
default -> "message.rivencraft.linking_book.unsafe";
};
player.displayClientMessage(Component.translatable(errorKey), true);
@@ -16,7 +16,7 @@ import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
public final class NotebookItem extends Item {
public class NotebookItem extends Item {
public NotebookItem(Properties properties) {
super(properties);
}
@@ -39,7 +39,9 @@ public final class NotebookItem extends Item {
NotebookContents contents = stack.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY);
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) {
tooltip.add(Component.translatable("item.rivencraft.notebook.unresolved",
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.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.registry.ModMenus;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.WritingGrammar;
@@ -172,7 +173,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
private boolean clickStrip(int index) {
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,
binder.nextPageIndex()));
if (emptied.isEmpty()) return false;
@@ -227,7 +228,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
moved = moveItemStackTo(source, PLAYER_START, slots.size(), true);
} else if (source.is(Items.LEATHER) || BookBinderBlockEntity.validCover(source)) {
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());
if (emptied.isEmpty()) return ItemStack.EMPTY;
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.PageContent;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.minecraft.core.NonNullList;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.Container;
@@ -28,6 +29,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
private final InteractionHand hand;
private final int openedHotbarSlot;
private final NotebookContainer contents;
private final int pageCapacity;
public NotebookMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf data) {
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.hand = hand;
this.openedHotbarSlot = inventory.selected;
this.pageCapacity = CollationFolderItem.capacity(inventory.player.getItemInHand(hand));
this.contents = new NotebookContainer(inventory, hand);
for (int page = 0; page < NOTEBOOK_SLOTS; page++) {
// 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 column = 0; column < 9; column++) {
@@ -61,11 +64,13 @@ public final class NotebookMenu extends AbstractContainerMenu {
: new Slot(playerInventory, inventoryIndex, x, y);
}
public int pageCapacity() { return pageCapacity; }
@Override
public boolean stillValid(Player player) {
return player.isAlive()
&& (hand == InteractionHand.OFF_HAND || player.getInventory().selected == openedHotbarSlot)
&& player.getItemInHand(hand).is(ModItems.NOTEBOOK.get());
&& CollationFolderItem.isCollection(player.getItemInHand(hand));
}
@Override
@@ -108,7 +113,7 @@ public final class NotebookMenu extends AbstractContainerMenu {
} else {
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()) {
return false;
}
@@ -132,14 +137,18 @@ public final class NotebookMenu extends AbstractContainerMenu {
}
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);
this.enabled = enabled;
}
@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;
}
@Override public boolean mayPickup(Player player) { return enabled; }
@Override public int getMaxStackSize() { return 1; }
}
@@ -184,11 +193,12 @@ public final class NotebookMenu extends AbstractContainerMenu {
public void setChanged() {
super.setChanged();
ItemStack notebook = inventory.player.getItemInHand(hand);
if (loading || !notebook.is(ModItems.NOTEBOOK.get())) {
if (loading || !CollationFolderItem.isCollection(notebook)) {
return;
}
NonNullList<ItemStack> saved = NonNullList.withSize(NOTEBOOK_SLOTS, ItemStack.EMPTY);
for (int index = 0; index < NOTEBOOK_SLOTS; index++) {
int capacity = CollationFolderItem.capacity(notebook);
NonNullList<ItemStack> saved = NonNullList.withSize(capacity, ItemStack.EMPTY);
for (int index = 0; index < capacity; index++) {
ItemStack page = getItem(index);
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.ModMenus;
import net.dnorberg.rivencraft.writing.NotebookContents;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
@@ -107,7 +108,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
public int pageCapacity(int collectionIndex) {
if (collectionIndex < 0 || collectionIndex >= WritingDeskBlockEntity.COLLECTIONS) return 0;
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;
}
public List<SymbolDisplayMetadata> catalog() { return catalog; }
@@ -183,7 +184,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
ItemStack carried = getCarried();
if (clientSide || !storablePage(carried)) return false;
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(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
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;
ItemStack sourceBook = desk.getItem(sourceCollection);
ItemStack targetBook = desk.getItem(targetCollection);
boolean sourceNotebook = sourceBook.is(ModItems.NOTEBOOK.get());
boolean targetNotebook = targetBook.is(ModItems.NOTEBOOK.get());
boolean sourceNotebook = CollationFolderItem.isCollection(sourceBook);
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))
|| (!targetNotebook && (!targetBook.is(ModItems.PAGE.get()) || targetPage != 0))) return false;
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) {
if (!getCarried().isEmpty() || targetSlot < -1 || targetSlot >= slots.size()) return false;
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;
var sourcePages = sourceNotebook ? new java.util.ArrayList<>(sourceBook.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages()) : null;
@@ -263,12 +268,12 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
Slot target = slots.get(targetSlot);
if (!target.isActive() || !target.mayPlace(moving)) return false;
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
: new java.util.ArrayList<>(current.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages());
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()) {
position = i;
break;
@@ -342,7 +347,7 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
private boolean moveIntoDesk(ItemStack moving) {
if (moving.is(ModItems.INK_VIAL.get())) return moveItemStackTo(moving,
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))
return moveItemStackTo(moving, WritingDeskBlockEntity.PAPER,
WritingDeskBlockEntity.PAPER + 1, false);
@@ -374,7 +379,8 @@ public final class WritingDeskMenu extends AbstractContainerMenu {
|| pageIndex < 0 || pageIndex >= NotebookContents.MAX_PAGES) return ItemStack.EMPTY;
ItemStack collection = desk.getItem(collectionIndex);
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(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
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;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkProperties;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
@@ -18,6 +19,7 @@ import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.menu.BookBinderMenu;
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.OrderedWriting;
@@ -59,7 +61,7 @@ public final class ModNetworking {
}
public static void register(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("15");
PayloadRegistrar registrar = event.registrar("17");
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
ModNetworking::requestDescriptiveBookScreen);
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
@@ -85,6 +87,10 @@ public final class ModNetworking {
ClientWritingDeskCatalog::handle);
registrar.playToServer(SetBookBinderTitlePayload.TYPE, SetBookBinderTitlePayload.STREAM_CODEC,
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) {
@@ -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) {
if (!(context.player() instanceof ServerPlayer player)) {
return;
@@ -163,7 +184,9 @@ public final class ModNetworking {
try {
AgeReference reference = new AgeReference(UUID.randomUUID(), title);
if (AgeDimensionService.resolve(player, reference, access.stack()
.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY)).isEmpty()) {
.getOrDefault(ModDataComponents.BOOK_WRITING.get(), OrderedWriting.EMPTY),
!access.stack().getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.has(LinkProperties.DISABLE_PLATFORM)).isEmpty()) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
return;
}
@@ -269,7 +292,9 @@ public final class ModNetworking {
LinkTarget target;
if (payload.descriptiveBook() && stack.is(ModItems.DESCRIPTIVE_BOOK.get())) {
AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
target = age == null ? null : AgeDimensionService.resolve(player, age).orElse(null);
target = age == null ? null : AgeDimensionService.resolve(player, age, OrderedWriting.EMPTY,
!stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.has(LinkProperties.DISABLE_PLATFORM)).orElse(null);
} else if (!payload.descriptiveBook() && stack.is(ModItems.LINKING_BOOK.get())) {
target = stack.get(ModDataComponents.LINK_TARGET.get());
} else {
@@ -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.WritingDeskBlockEntity;
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.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
@@ -31,6 +32,9 @@ public final class ModBlockEntities {
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<InkMixerBlockEntity>> INK_MIXER =
BLOCK_ENTITIES.register("ink_mixer", () -> BlockEntityType.Builder
.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 =
BLOCK_ENTITIES.register("star_fissure", () -> BlockEntityType.Builder
.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.WritingDeskBlock;
import net.dnorberg.rivencraft.block.InkMixerBlock;
import net.dnorberg.rivencraft.block.LinkModifierBlock;
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
import net.dnorberg.rivencraft.block.StarFissureBlock;
import net.dnorberg.rivencraft.block.DecayBlock;
@@ -40,6 +41,9 @@ public final class ModBlocks {
"ink_mixer", InkMixerBlock::new,
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",
Block::new, BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK).mapColor(MapColor.COLOR_BLUE));
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.LINKING_BOOK.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.WRITING_DESK.get());
output.accept(ModItems.INK_MIXER.get());
output.accept(ModItems.LINK_MODIFIER.get());
output.accept(ModItems.BOOK_BINDER.get());
output.accept(ModItems.CRYSTAL_BLOCK.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.PageItem;
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.minecraft.world.item.Item;
import net.minecraft.world.item.BlockItem;
@@ -35,6 +37,10 @@ public final class ModItems {
NotebookItem::new,
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(
"ink_vial",
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> BOOK_BINDER = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_BINDER);
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> BOOK_RECEPTACLE = ITEMS.registerSimpleBlockItem(ModBlocks.BOOK_RECEPTACLE);
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.BookBinderMenu;
import net.dnorberg.rivencraft.menu.InkMixerMenu;
import net.dnorberg.rivencraft.menu.LinkModifierMenu;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.inventory.MenuType;
import net.neoforged.bus.api.IEventBus;
@@ -24,6 +25,8 @@ public final class ModMenus {
MENUS.register("book_binder", () -> IMenuTypeExtension.create(BookBinderMenu::new));
public static final DeferredHolder<MenuType<?>, MenuType<InkMixerMenu>> INK_MIXER =
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() {
}
@@ -9,6 +9,7 @@ import net.dnorberg.rivencraft.block.entity.WritingDeskBlockEntity;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.item.CollationFolderItem;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
@@ -24,7 +25,7 @@ public final class WritingDeskCatalog {
for (int slot = 0; slot < WritingDeskBlockEntity.COLLECTIONS; slot++) {
ItemStack collection = desk.getItem(slot);
if (collection.is(ModItems.PAGE.get())) add(unique, collection);
if (collection.is(ModItems.NOTEBOOK.get())) {
if (CollationFolderItem.isCollection(collection)) {
List<ItemStack> pages = collection.getOrDefault(
ModDataComponents.NOTEBOOK_CONTENTS.get(), NotebookContents.EMPTY).pages();
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.maintain_momentum": "Maintain Momentum",
"item.rivencraft.link_property.intra_linking": "Intra Linking",
"item.rivencraft.link_property.intra_linking_only": "Intra-Linking Only",
"item.rivencraft.link_property.relative_link": "Relative Link",
"item.rivencraft.link_property.disable_platform": "Generate Platform: Off",
"item.rivencraft.page.unresolved": "Unresolved Symbol Page",
"item.rivencraft.notebook": "Notebook",
"item.rivencraft.collation_folder": "Collation Folder",
"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.unresolved": "Unresolved pages: %s",
"item.rivencraft.ink_vial": "Ink Vial",
@@ -88,6 +107,7 @@
"message.rivencraft.linking_book.missing_dimension": "That dimension is unavailable",
"message.rivencraft.linking_book.unsafe": "No safe destination was found near the link",
"message.rivencraft.linking_book.intra_required": "This book needs an Intra Linking panel for same-dimension travel",
"message.rivencraft.linking_book.intra_only": "This book can only link within its current dimension",
"message.rivencraft.descriptive_book.unwritten": "This Descriptive Book is unwritten",
"message.rivencraft.descriptive_book.created": "Created Age: %s",
"message.rivencraft.descriptive_book.unavailable": "That Age is unavailable",
@@ -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,
"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": "minecraft:book", "weight": 3 },
{ "type": "minecraft:item", "name": "rivencraft:sealed_notebook", "weight": 20 },
{ "type": "minecraft:item", "name": "minecraft:experience_bottle", "weight": 1 }
]
},
@@ -17,7 +17,7 @@
"rolls": { "type": "minecraft:uniform", "min": 1, "max": 2 },
"entries": [
{ "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 }
]
}
@@ -4,6 +4,7 @@
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
"AgeDimensionSeedMixin",
"AgeDungeonPlacementMixin",
"AgeStructureBiomeMixin",
"StructurePlacementAccessor"