Compare commits

...
3 Commits
Author SHA1 Message Date
dnorberg 211aa0733b Mute Immersive Portals preview world audio
Build / build (push) Canceled after 0s
2026-09-23 15:20:07 -04:00
dnorberg 8b418ce01c Use legacy unbound linking book texture
Build / build (push) Canceled after 0s
2026-09-23 15:07:55 -04:00
dnorberg 62693c2229 Add blank symbol and link-panel pages (#18)
Build / build (push) Canceled after 0s
2026-09-23 14:49:44 -04:00
43 changed files with 546 additions and 3 deletions

No files matched your search

+5
View File
@@ -11,6 +11,11 @@ and randomly seeded, persistent vanilla-like Ages created from page-free
Descriptive Books. Instability and full symbol-to-world grammar interpretation Descriptive Books. Instability and full symbol-to-world grammar interpretation
have not been ported yet. have not been ported yet.
Milestone 4 now includes stackable blank Pages plus server-created symbol,
unresolved-symbol, and baseline link-panel states. Page visuals follow the
legacy runtime-composition rules; written-page creation will become available
through the Writing Desk work that follows.
## Linking Book ## Linking Book
Craft a Linking Book from a vanilla book and an ender pearl, then use it in the Craft a Linking Book from a vanilla book and an ender pearl, then use it in the
+16
View File
@@ -0,0 +1,16 @@
# Asset provenance
Rivencraft reuses selected assets from
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) under that
project's GNU LGPL v3 license. Rivencraft itself uses the same license.
| Rivencraft asset | Upstream asset | Upstream revision | Changes |
| --- | --- | --- | --- |
| `assets/rivencraft/textures/item/page.png` | `assets/mystcraft/textures/items/page_background.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
| `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample |
| `assets/rivencraft/textures/item/page_symbol*.png` and `page_unresolved.png` | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Static 1.21.1 renditions of the legacy runtime-composed four-word symbol pages; `normal_time` uses the legacy Full Length poem |
Mystcraft Legacy identifies XCompWiz as its project developer and includes the
full LGPL v3 text in its root `LICENSE` file. Rivencraft's root `LICENSE`
provides the same license terms.
+1
View File
@@ -70,6 +70,7 @@ not interpret symbols into Age generation, which remains Milestone 5 work.
## Provenance rules ## Provenance rules
- New code uses the `net.dnorberg.rivencraft` package. - New code uses the `net.dnorberg.rivencraft` package.
- Reused legacy art is recorded in [ASSET_PROVENANCE.md](ASSET_PROVENANCE.md).
- Adapted Mystcraft code retains its upstream copyright notice and is tracked - Adapted Mystcraft code retains its upstream copyright notice and is tracked
in a nearby source comment or commit. in a nearby source comment or commit.
- Generated, binary, and third-party dependency content is not copied from the - Generated, binary, and third-party dependency content is not copied from the
+20
View File
@@ -44,3 +44,23 @@ Removing a data pack never removes its IDs from saved writing. A missing ID is
resolved as client-safe **Unresolved Symbol** display metadata while retaining resolved as client-safe **Unresolved Symbol** display metadata while retaining
the original namespaced ID for diagnostics and later restoration. This is the original namespaced ID for diagnostics and later restoration. This is
deliberately different from treating an unknown symbol as valid grammar. deliberately different from treating an unknown symbol as valid grammar.
## Pages
The single stackable `rivencraft:page` item has a bounded, persistent
`rivencraft:page_content` component. Its explicit states are blank, symbol,
unresolved symbol, and baseline link panel. Symbol and unresolved-symbol pages
retain their namespaced symbol reference; unresolved pages can be inspected and
removed but are rejected as usable writing until their data pack returns.
Only server-side writing operations may turn a blank page into a known symbol
page or link panel. The shared `PageItem` creation and validation gates reject
new pages for identifiers absent from the server catalog. Later Writing Desk
and Book Binder packets must call these gates rather than accepting page
contents supplied by a client.
The item model uses server-assigned custom-model data derived from the symbol
identifier. Built-in pages reproduce Mystcraft Legacy's four-word poem
composition from its page background and symbol-component sheet. Data-pack
symbols without a bundled resource-pack model use the legacy question-mark
fallback while keeping their real identifier and display metadata.
+1 -1
View File
@@ -35,7 +35,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.3.1.0 mod_version=0.3.2.2
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
@@ -2,6 +2,10 @@ package net.dnorberg.rivencraft.client;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.registry.ModBlockEntities; import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.renderer.item.ItemProperties;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent; import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber; import net.neoforged.fml.common.EventBusSubscriber;
@@ -21,6 +25,11 @@ public final class ClientEvents {
@SubscribeEvent @SubscribeEvent
public static void clientSetup(FMLClientSetupEvent event) { public static void clientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> ItemProperties.register(
ModItems.LINKING_BOOK.get(),
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "bound"),
(stack, level, entity, seed) -> stack.has(ModDataComponents.LINK_TARGET.get()) ? 1.0F : 0.0F
));
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem); NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickItem);
NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock); NeoForge.EVENT_BUS.addListener(ClientBookInteractionHandler::onRightClickBlock);
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPre); NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPre);
@@ -42,7 +42,11 @@ public final class ImmersivePortalsDestinationPreviewRenderer implements Destina
@Override @Override
public void accept(LinkPreviewPayload payload) { public void accept(LinkPreviewPayload payload) {
if (destination != null) {
ImmersivePortalsPreviewAudioGuard.clear(destination.dimension());
}
destination = payload; destination = payload;
ImmersivePortalsPreviewAudioGuard.mute(payload.dimension());
resetView(); resetView();
} }
@@ -234,5 +238,8 @@ public final class ImmersivePortalsDestinationPreviewRenderer implements Destina
public void close() { public void close() {
clearMovement(); clearMovement();
hasCompletedFrame = false; hasCompletedFrame = false;
if (destination != null) {
ImmersivePortalsPreviewAudioGuard.clear(destination.dimension());
}
} }
} }
@@ -0,0 +1,31 @@
package net.dnorberg.rivencraft.client.preview;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
/** Prevents an Immersive Portals preview world from contributing sounds to a book screen. */
public final class ImmersivePortalsPreviewAudioGuard {
private static ResourceKey<Level> mutedDimension;
private ImmersivePortalsPreviewAudioGuard() {
}
public static void mute(ResourceKey<Level> dimension) {
mutedDimension = dimension;
}
public static void clear(ResourceKey<Level> dimension) {
if (dimension.equals(mutedDimension)) {
mutedDimension = null;
}
}
public static boolean shouldMute(ClientLevel level) {
Minecraft minecraft = Minecraft.getInstance();
return mutedDimension != null
&& level.dimension().equals(mutedDimension)
&& (minecraft.player == null || level != minecraft.player.level());
}
}
@@ -5,13 +5,18 @@ import io.netty.buffer.Unpooled;
import java.util.List; import java.util.List;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.OrderedWriting; import net.dnorberg.rivencraft.writing.OrderedWriting;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolCatalog; import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolReference; import net.dnorberg.rivencraft.writing.SymbolReference;
import net.dnorberg.rivencraft.writing.WritingEntry; import net.dnorberg.rivencraft.writing.WritingEntry;
import net.dnorberg.rivencraft.item.PageItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.minecraft.gametest.framework.GameTest; import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper; import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.gametest.GameTestHolder; import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
@@ -63,6 +68,56 @@ public final class WritingDataGameTests {
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void pageStatesPersistAndNetworkRoundTrip(GameTestHelper helper) {
SymbolReference known = reference("rivencraft:terrain/standard");
SymbolReference removed = reference("example:removed_symbol");
List<PageContent> states = List.of(
PageContent.BLANK,
PageContent.symbol(known),
PageContent.unresolved(removed),
PageContent.LINK_PANEL
);
for (PageContent state : states) {
var json = PageContent.CODEC.encodeStart(JsonOps.INSTANCE, state).result().orElseThrow();
PageContent persistentCopy = PageContent.CODEC.parse(JsonOps.INSTANCE, json).result().orElseThrow();
helper.assertValueEqual(persistentCopy, state, "Page content must persist exactly");
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer());
PageContent.STREAM_CODEC.encode(buffer, state);
helper.assertValueEqual(PageContent.STREAM_CODEC.decode(buffer), state,
"Page content must network-sync exactly");
}
helper.succeed();
}
@GameTest(template = "empty")
public static void pageItemPreservesContentAndRejectsForgedUnknownSymbols(GameTestHelper helper) {
SymbolReference known = reference("rivencraft:terrain/standard");
SymbolReference unknown = reference("example:forged_symbol");
ItemStack page = PageItem.createSymbolPage(known);
CompoundTag serialized = (CompoundTag) page.saveOptional(helper.getLevel().registryAccess());
ItemStack restored = ItemStack.parseOptional(helper.getLevel().registryAccess(), serialized);
helper.assertValueEqual(restored.get(ModDataComponents.PAGE_CONTENT.get()), PageContent.symbol(known),
"Symbol page content must survive an ItemStack save/load");
helper.assertValueEqual(
restored.get(ModDataComponents.PAGE_SYMBOL_DISPLAY.get()),
SymbolCatalog.display(known),
"Server-selected display metadata must survive for remote clients"
);
ItemStack recovered = PageItem.createRecoveredPage(unknown);
helper.assertFalse(PageItem.isUsableWriting(recovered),
"An unresolved page must remain recoverable without becoming valid writing");
try {
PageItem.createSymbolPage(unknown);
helper.fail("The server must reject new pages for unknown symbols");
} catch (IllegalArgumentException expected) {
helper.succeed();
}
}
private static SymbolReference reference(String id) { private static SymbolReference reference(String id) {
return new SymbolReference(ResourceLocation.parse(id)); return new SymbolReference(ResourceLocation.parse(id));
} }
@@ -0,0 +1,131 @@
package net.dnorberg.rivencraft.item;
import java.util.List;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import net.dnorberg.rivencraft.writing.SymbolReference;
import net.minecraft.ChatFormatting;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.component.CustomModelData;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
/** One stackable Page item whose server-owned component identifies its writing state. */
public final class PageItem extends Item {
public PageItem(Properties properties) {
super(properties);
}
public static ItemStack createSymbolPage(SymbolReference reference) {
if (!SymbolCatalog.isKnown(reference)) {
throw new IllegalArgumentException("Cannot create a page for unknown symbol " + reference.id());
}
return create(PageContent.symbol(reference));
}
public static ItemStack createLinkPanelPage() {
return create(PageContent.LINK_PANEL);
}
/** Restores saved unknown references without granting them validity for new writing operations. */
public static ItemStack createRecoveredPage(SymbolReference reference) {
return create(PageContent.unresolved(reference));
}
public static PageContent content(ItemStack stack) {
PageContent content = stack.get(ModDataComponents.PAGE_CONTENT.get());
return content == null ? PageContent.BLANK : content;
}
/** Server-side gate for Desk and Binder inputs; unresolved pages remain removable but not usable. */
public static boolean isUsableWriting(ItemStack stack) {
if (!stack.is(ModItems.PAGE.get())) {
return false;
}
PageContent resolved = content(stack).resolve();
return resolved.kind() == PageContent.Kind.BLANK
|| resolved.kind() == PageContent.Kind.LINK_PANEL
|| resolved.isResolvedSymbol();
}
@Override
public Component getName(ItemStack stack) {
PageContent content = content(stack);
return switch (content.kind()) {
case BLANK -> Component.translatable("item.rivencraft.page.blank");
case LINK_PANEL -> Component.translatable("item.rivencraft.page.link_panel");
case UNRESOLVED_SYMBOL -> Component.translatable("item.rivencraft.page.unresolved");
case SYMBOL -> symbolName(stack, content.symbol().orElseThrow());
};
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context,
List<Component> tooltipComponents, TooltipFlag tooltipFlag) {
PageContent content = content(stack);
if (content.kind() == PageContent.Kind.BLANK) {
tooltipComponents.add(Component.translatable("item.rivencraft.page.blank.description")
.withStyle(ChatFormatting.GRAY));
return;
}
if (content.kind() == PageContent.Kind.LINK_PANEL) {
tooltipComponents.add(Component.translatable("item.rivencraft.page.link_panel.description")
.withStyle(ChatFormatting.GRAY));
return;
}
SymbolReference reference = content.symbol().orElseThrow();
SymbolDisplayMetadata metadata = content.kind() == PageContent.Kind.SYMBOL
? stack.getOrDefault(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), SymbolCatalog.display(reference))
: SymbolDisplayMetadata.unresolved(reference);
if (metadata.resolved()) {
tooltipComponents.add(Component.translatable(metadata.descriptionTranslationKey())
.withStyle(ChatFormatting.GRAY));
} else {
tooltipComponents.add(Component.translatable(
"symbol.rivencraft.unresolved.description", reference.unresolvedName())
.withStyle(ChatFormatting.RED));
}
tooltipComponents.add(Component.literal(reference.id().toString()).withStyle(ChatFormatting.DARK_GRAY));
}
private static ItemStack create(PageContent content) {
ItemStack stack = new ItemStack(ModItems.PAGE.get());
stack.set(ModDataComponents.PAGE_CONTENT.get(), content);
stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(modelIndex(content)));
if (content.kind() == PageContent.Kind.SYMBOL) {
stack.set(ModDataComponents.PAGE_SYMBOL_DISPLAY.get(),
SymbolCatalog.display(content.symbol().orElseThrow()));
}
return stack;
}
private static Component symbolName(ItemStack stack, SymbolReference reference) {
SymbolDisplayMetadata metadata = stack.getOrDefault(
ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), SymbolCatalog.display(reference));
return metadata.resolved()
? Component.translatable(metadata.nameTranslationKey())
: Component.translatable("item.rivencraft.page.unresolved");
}
private static int modelIndex(PageContent content) {
return switch (content.kind()) {
case BLANK -> 0;
case LINK_PANEL -> 1;
case UNRESOLVED_SYMBOL -> 2;
case SYMBOL -> switch (content.symbol().orElseThrow().id().toString()) {
case "rivencraft:terrain/standard" -> 100;
case "rivencraft:biome/native" -> 101;
case "rivencraft:lighting/normal" -> 102;
case "rivencraft:weather/normal" -> 103;
case "rivencraft:celestial/normal_time" -> 104;
case "rivencraft:feature/caves" -> 105;
default -> 3;
};
};
}
}
@@ -0,0 +1,59 @@
package net.dnorberg.rivencraft.mixin.client;
import net.dnorberg.rivencraft.client.preview.ImmersivePortalsPreviewAudioGuard;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.Holder;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Mutes only the remote client world displayed by a Rivencraft book preview. */
@Mixin(value = ClientLevel.class, priority = 2000)
public abstract class ClientLevelSoundMixin {
@Inject(
method = "playSound(DDDLnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FFZJ)V",
at = @At("HEAD"),
cancellable = true
)
private void rivencraft$mutePreviewPositionSound(
double x, double y, double z, SoundEvent sound, SoundSource source,
float volume, float pitch, boolean distanceDelay, long seed, CallbackInfo callback
) {
rivencraft$cancelPreviewSound(callback);
}
@Inject(
method = "playLocalSound(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FF)V",
at = @At("HEAD"),
cancellable = true
)
private void rivencraft$mutePreviewLocalEntitySound(
Entity entity, SoundEvent sound, SoundSource source, float volume, float pitch,
CallbackInfo callback
) {
rivencraft$cancelPreviewSound(callback);
}
@Inject(
method = "playSeededSound(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/Holder;Lnet/minecraft/sounds/SoundSource;FFJ)V",
at = @At("HEAD"),
cancellable = true
)
private void rivencraft$mutePreviewSeededEntitySound(
Player player, Entity entity, Holder<SoundEvent> sound, SoundSource source,
float volume, float pitch, long seed, CallbackInfo callback
) {
rivencraft$cancelPreviewSound(callback);
}
private void rivencraft$cancelPreviewSound(CallbackInfo callback) {
if (ImmersivePortalsPreviewAudioGuard.shouldMute((ClientLevel)(Object)this)) {
callback.cancel();
}
}
}
@@ -19,6 +19,7 @@ public final class ModCreativeTabs {
.displayItems((parameters, output) -> { .displayItems((parameters, output) -> {
output.accept(ModItems.DESCRIPTIVE_BOOK.get()); output.accept(ModItems.DESCRIPTIVE_BOOK.get());
output.accept(ModItems.LINKING_BOOK.get()); output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.PAGE.get());
}) })
.build()); .build());
@@ -3,6 +3,8 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.AgeReference; import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@@ -39,6 +41,18 @@ public final class ModDataComponents {
.networkSynchronized(ByteBufCodecs.STRING_UTF8) .networkSynchronized(ByteBufCodecs.STRING_UTF8)
.cacheEncoding() .cacheEncoding()
.build()); .build());
public static final DeferredHolder<DataComponentType<?>, DataComponentType<PageContent>> PAGE_CONTENT =
DATA_COMPONENTS.register("page_content", () -> DataComponentType.<PageContent>builder()
.persistent(PageContent.CODEC)
.networkSynchronized(PageContent.STREAM_CODEC)
.cacheEncoding()
.build());
public static final DeferredHolder<DataComponentType<?>, DataComponentType<SymbolDisplayMetadata>> PAGE_SYMBOL_DISPLAY =
DATA_COMPONENTS.register("page_symbol_display", () -> DataComponentType.<SymbolDisplayMetadata>builder()
.persistent(SymbolDisplayMetadata.CODEC)
.networkSynchronized(SymbolDisplayMetadata.STREAM_CODEC)
.cacheEncoding()
.build());
private ModDataComponents() { private ModDataComponents() {
} }
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.neoforged.bus.api.IEventBus; import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredItem; import net.neoforged.neoforge.registries.DeferredItem;
@@ -21,6 +22,11 @@ public final class ModItems {
LinkingBookItem::new, LinkingBookItem::new,
new Item.Properties().stacksTo(1) new Item.Properties().stacksTo(1)
); );
public static final DeferredItem<PageItem> PAGE = ITEMS.registerItem(
"page",
PageItem::new,
new Item.Properties().stacksTo(64)
);
private ModItems() { private ModItems() {
} }
@@ -0,0 +1,95 @@
package net.dnorberg.rivencraft.writing;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.netty.buffer.ByteBuf;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
/** Persistent contents of a Page item. Unknown symbols remain explicit and recoverable. */
public record PageContent(Kind kind, Optional<SymbolReference> symbol) {
public static final PageContent BLANK = new PageContent(Kind.BLANK, Optional.empty());
public static final PageContent LINK_PANEL = new PageContent(Kind.LINK_PANEL, Optional.empty());
private static final Codec<Kind> KIND_CODEC = Codec.STRING.comapFlatMap(
value -> {
try {
return DataResult.success(Kind.valueOf(value.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException exception) {
return DataResult.error(() -> "Unknown page content kind: " + value);
}
},
value -> value.name().toLowerCase(Locale.ROOT)
);
private static final Codec<PageContent> RECORD_CODEC = RecordCodecBuilder.create(instance -> instance.group(
KIND_CODEC.fieldOf("kind").forGetter(PageContent::kind),
SymbolReference.CODEC.optionalFieldOf("symbol").forGetter(PageContent::symbol)
).apply(instance, PageContent::new));
public static final Codec<PageContent> CODEC = RECORD_CODEC.flatXmap(PageContent::validate, PageContent::validate);
public static final StreamCodec<ByteBuf, PageContent> STREAM_CODEC = StreamCodec.of(
PageContent::encode, PageContent::decode);
public PageContent {
Objects.requireNonNull(kind, "kind");
symbol = Objects.requireNonNull(symbol, "symbol");
boolean needsSymbol = kind == Kind.SYMBOL || kind == Kind.UNRESOLVED_SYMBOL;
if (needsSymbol != symbol.isPresent()) {
throw new IllegalArgumentException("Page contents do not match kind " + kind);
}
}
public static PageContent symbol(SymbolReference symbol) {
return new PageContent(Kind.SYMBOL, Optional.of(symbol));
}
public static PageContent unresolved(SymbolReference symbol) {
return new PageContent(Kind.UNRESOLVED_SYMBOL, Optional.of(symbol));
}
/** Re-resolves persisted data against the authoritative current server catalog. */
public PageContent resolve() {
if (kind != Kind.SYMBOL && kind != Kind.UNRESOLVED_SYMBOL) {
return this;
}
SymbolReference reference = symbol.orElseThrow();
return SymbolCatalog.isKnown(reference) ? symbol(reference) : unresolved(reference);
}
public boolean isResolvedSymbol() {
return kind == Kind.SYMBOL;
}
private static DataResult<PageContent> validate(PageContent content) {
boolean needsSymbol = content.kind == Kind.SYMBOL || content.kind == Kind.UNRESOLVED_SYMBOL;
return needsSymbol == content.symbol.isPresent()
? DataResult.success(content)
: DataResult.error(() -> "Page contents do not match kind " + content.kind);
}
private static void encode(ByteBuf buffer, PageContent content) {
ByteBufCodecs.VAR_INT.encode(buffer, content.kind.ordinal());
content.symbol.ifPresent(reference -> SymbolReference.STREAM_CODEC.encode(buffer, reference));
}
private static PageContent decode(ByteBuf buffer) {
int ordinal = ByteBufCodecs.VAR_INT.decode(buffer);
if (ordinal < 0 || ordinal >= Kind.values().length) {
throw new IllegalArgumentException("Unknown page content kind ordinal " + ordinal);
}
Kind kind = Kind.values()[ordinal];
Optional<SymbolReference> symbol = kind == Kind.SYMBOL || kind == Kind.UNRESOLVED_SYMBOL
? Optional.of(SymbolReference.STREAM_CODEC.decode(buffer))
: Optional.empty();
return new PageContent(kind, symbol);
}
public enum Kind {
BLANK,
SYMBOL,
UNRESOLVED_SYMBOL,
LINK_PANEL
}
}
@@ -6,6 +6,12 @@
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind", "item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)", "item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
"item.rivencraft.linking_book.return": "Return Link", "item.rivencraft.linking_book.return": "Return Link",
"item.rivencraft.page": "Page",
"item.rivencraft.page.blank": "Blank Page",
"item.rivencraft.page.blank.description": "Ready for writing at a Writing Desk.",
"item.rivencraft.page.link_panel": "Link Panel Page",
"item.rivencraft.page.link_panel.description": "The baseline panel required to assemble a Descriptive Book.",
"item.rivencraft.page.unresolved": "Unresolved Symbol Page",
"message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location", "message.rivencraft.linking_book.bind_hint": "Sneak-use to bind this book to your current location",
"message.rivencraft.linking_book.unbound": "This Linking Book is unbound", "message.rivencraft.linking_book.unbound": "This Linking Book is unbound",
"message.rivencraft.linking_book.bound": "Linking Book bound as %s", "message.rivencraft.linking_book.bound": "Linking Book bound as %s",
@@ -1,6 +1,14 @@
{ {
"parent": "minecraft:item/generated", "parent": "minecraft:item/generated",
"textures": { "textures": {
"layer0": "rivencraft:item/linking_book" "layer0": "rivencraft:item/linking_book_unwritten"
} },
"overrides": [
{
"predicate": {
"rivencraft:bound": 1.0
},
"model": "rivencraft:item/linking_book_bound"
}
]
} }
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/linking_book"
}
}
@@ -0,0 +1,17 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/page"
},
"overrides": [
{ "predicate": { "custom_model_data": 1 }, "model": "rivencraft:item/page_link_panel" },
{ "predicate": { "custom_model_data": 2 }, "model": "rivencraft:item/page_unresolved" },
{ "predicate": { "custom_model_data": 3 }, "model": "rivencraft:item/page_symbol" },
{ "predicate": { "custom_model_data": 100 }, "model": "rivencraft:item/page_symbol_terrain_standard" },
{ "predicate": { "custom_model_data": 101 }, "model": "rivencraft:item/page_symbol_biome_native" },
{ "predicate": { "custom_model_data": 102 }, "model": "rivencraft:item/page_symbol_lighting_normal" },
{ "predicate": { "custom_model_data": 103 }, "model": "rivencraft:item/page_symbol_weather_normal" },
{ "predicate": { "custom_model_data": 104 }, "model": "rivencraft:item/page_symbol_celestial_normal_time" },
{ "predicate": { "custom_model_data": 105 }, "model": "rivencraft:item/page_symbol_feature_caves" }
]
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/page_link_panel"
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/page_symbol"
}
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/page_symbol_biome_native" }
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/page_symbol_celestial_normal_time" }
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/page_symbol_feature_caves" }
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/page_symbol_lighting_normal" }
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/page_symbol_terrain_standard" }
}
@@ -0,0 +1,4 @@
{
"parent": "minecraft:item/generated",
"textures": { "layer0": "rivencraft:item/page_symbol_weather_normal" }
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "rivencraft:item/page_unresolved"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,13 @@
{
"type": "minecraft:crafting_shapeless",
"category": "misc",
"ingredients": [
{
"item": "minecraft:paper"
}
],
"result": {
"count": 1,
"id": "rivencraft:page"
}
}
@@ -4,6 +4,7 @@
"package": "net.dnorberg.rivencraft.mixin", "package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"client": [ "client": [
"client.ClientLevelSoundMixin",
"client.LevelRendererMixin" "client.LevelRendererMixin"
], ],
"injectors": { "injectors": {