Compare commits

..
6 Commits
Author SHA1 Message Date
dnorberg 7d335f5952 Bug Fix: Enviromental Audio from Immersive Portals
Build / build (push) Canceled after 0s
2026-09-23 15:32:24 -04:00
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
dnorberg b8d23ee0c0 Define writing and symbol data model (#17)
Build / build (push) Canceled after 0s
2026-09-23 14:24:40 -04:00
dnorberg f276d412b9 Describe your changes
Build / build (push) Canceled after 0s
2026-09-23 06:30:15 -04:00
58 changed files with 1087 additions and 7 deletions

No files matched your search

+11 -4
View File
@@ -4,10 +4,17 @@ Rivencraft is a clean, incremental port of the ideas and gameplay of
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft
1.21.1 on NeoForge.
The bootstrap, books/linking, and Ages milestones are complete. The project
currently provides functional Linking Books and randomly seeded, persistent
vanilla-like Ages created from page-free Descriptive Books. Symbols,
instability, and the full writing mechanics have not been ported yet.
The bootstrap, books/linking, and Ages milestones are complete. Milestone 4 is
building the writing system on a data-pack symbol catalog and persistent
ordered-writing model. The project currently provides functional Linking Books
and randomly seeded, persistent vanilla-like Ages created from page-free
Descriptive Books. Instability and full symbol-to-world grammar interpretation
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
Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

+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.
+7
View File
@@ -34,6 +34,7 @@ acceptance issue and all required implementation issues are complete.
renderer compatibility. **Complete.** See
[Milestone 3](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/8).
4. **Writing system** — pages, symbols, notebooks, desks, and book assembly.
**In progress.**
5. **Age grammar** — translate symbol combinations into modern biome, noise,
dimension-type, and world-generation configuration. Optional integrations
may add composable biome regions and seasonal behavior.
@@ -61,9 +62,15 @@ The writing system and Age grammar will build on the existing page-free Age
path. Page-free books remain a useful deterministic fallback and test fixture
after written symbol combinations become available.
Milestone 4 begins with a versioned, data-pack-defined symbol catalog and an
immutable ordered-writing model. See [WRITING_DATA.md](WRITING_DATA.md). These
contracts preserve unknown symbol identifiers and store author intent; they do
not interpret symbols into Age generation, which remains Milestone 5 work.
## Provenance rules
- 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
in a nearby source comment or commit.
- Generated, binary, and third-party dependency content is not copied from the
+66
View File
@@ -0,0 +1,66 @@
# Writing and symbol data
Milestone 4 stores what a player wrote without deciding what that writing does
to an Age. Milestone 5 owns grammar validation, symbol interaction, instability,
and translation into biome, noise, dimension-type, and other world-generation
configuration.
## Symbol catalog
The authoritative server catalog loads JSON resources from
`data/<namespace>/rivencraft/symbols/<path>.json`. The resource ID is the
symbol ID. For example, `data/rivencraft/rivencraft/symbols/terrain/standard.json`
defines `rivencraft:terrain/standard`.
Schema version 1 contains display metadata only:
```json
{
"schema_version": 1,
"name": "symbol.rivencraft.terrain.standard",
"description": "symbol.rivencraft.terrain.standard.description",
"category": "rivencraft:terrain"
}
```
`name` and `description` are translation keys, not arbitrary formatted text or
executable behavior. `category` is a namespaced organizational identifier.
Invalid definitions are logged and excluded by the server. Menus and future
page synchronization must obtain resolved display metadata from the server;
clients never make a symbol valid by naming it.
Rivencraft ships a small catalog covering terrain, biomes, lighting, weather,
celestial behavior, and features. It exists to exercise the complete writing
workflow before Milestone 5 implements the full legacy grammar.
## Persistent writing
`SymbolReference` stores only a namespaced symbol ID. `WritingEntry` is either
a symbol reference or a baseline link-panel marker. `OrderedWriting` retains up
to 256 entries in exact player-authored order. All three contracts have both
persistent codecs and bounded network codecs.
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
the original namespaced ID for diagnostics and later restoration. This is
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.
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.3.0.0
mod_version=0.3.2.3
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.
# This should match the base package used for the mod sources.
@@ -18,6 +18,7 @@ import net.dnorberg.rivencraft.config.RivencraftClientConfig;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeWeatherService;
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
@@ -50,6 +51,7 @@ public final class Rivencraft {
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onServerStopping);
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
NeoForge.EVENT_BUS.addListener(AgeRuntimeProfiles::register);
NeoForge.EVENT_BUS.addListener(SymbolCatalog::register);
LOGGER.info("Initializing Rivencraft");
}
}
@@ -2,6 +2,10 @@ package net.dnorberg.rivencraft.client;
import net.dnorberg.rivencraft.Rivencraft;
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.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
@@ -21,6 +25,11 @@ public final class ClientEvents {
@SubscribeEvent
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::onRightClickBlock);
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPre);
@@ -42,7 +42,11 @@ public final class ImmersivePortalsDestinationPreviewRenderer implements Destina
@Override
public void accept(LinkPreviewPayload payload) {
if (destination != null) {
ImmersivePortalsPreviewAudioGuard.clear(destination.dimension());
}
destination = payload;
ImmersivePortalsPreviewAudioGuard.mute(payload.dimension());
resetView();
}
@@ -118,6 +122,7 @@ public final class ImmersivePortalsDestinationPreviewRenderer implements Destina
FRAMEBUFFER.clear(true);
minecraft.getMainRenderTarget().bindWrite(true);
MyRenderHelper.restoreViewPort();
ImmersivePortalsPreviewAudioGuard.markCameraRestorePending();
GuiPortalRendering.submitNextFrameRendering(info, FRAMEBUFFER);
hasCompletedFrame = true;
graphics.renderOutline(left, top, width, height, exploring ? 0xFFE0C36A : 0xFF7B6AA8);
@@ -234,5 +239,8 @@ public final class ImmersivePortalsDestinationPreviewRenderer implements Destina
public void close() {
clearMovement();
hasCompletedFrame = false;
if (destination != null) {
ImmersivePortalsPreviewAudioGuard.clear(destination.dimension());
}
}
}
@@ -0,0 +1,62 @@
package net.dnorberg.rivencraft.client.preview;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Camera;
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 static boolean cameraRestorePending;
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());
}
public static void markCameraRestorePending() {
cameraRestorePending = true;
}
/**
* Immersive Portals' GUI renderer leaves the shared main camera in the
* remote world after its end-of-frame render. Restore it before the next
* frame uses that camera for rain effects and the sound listener.
*/
public static void restorePlayerCamera() {
Minecraft minecraft = Minecraft.getInstance();
if (!cameraRestorePending) {
return;
}
cameraRestorePending = false;
if (minecraft.level == null || minecraft.getCameraEntity() == null) {
return;
}
Camera camera = minecraft.gameRenderer.getMainCamera();
camera.setup(
minecraft.level,
minecraft.getCameraEntity(),
!minecraft.options.getCameraType().isFirstPerson(),
minecraft.options.getCameraType().isMirrored(),
camera.getPartialTickTime()
);
}
}
@@ -0,0 +1,124 @@
package net.dnorberg.rivencraft.gametest;
import com.mojang.serialization.JsonOps;
import io.netty.buffer.Unpooled;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.OrderedWriting;
import net.dnorberg.rivencraft.writing.PageContent;
import net.dnorberg.rivencraft.writing.SymbolCatalog;
import net.dnorberg.rivencraft.writing.SymbolReference;
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.GameTestHelper;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
/** Acceptance coverage for Milestone 4's persistent writing contracts. */
@GameTestHolder(Rivencraft.MOD_ID)
@PrefixGameTestTemplate(false)
public final class WritingDataGameTests {
private WritingDataGameTests() {
}
@GameTest(template = "empty")
public static void orderedWritingPreservesKnownUnknownAndLinkPanelEntries(GameTestHelper helper) {
SymbolReference known = reference("rivencraft:terrain/standard");
SymbolReference unknown = reference("example:removed_symbol");
OrderedWriting original = new OrderedWriting(List.of(
WritingEntry.symbol(known),
WritingEntry.symbol(unknown),
WritingEntry.linkPanel()
));
var json = OrderedWriting.CODEC.encodeStart(JsonOps.INSTANCE, original).result().orElseThrow();
OrderedWriting persistentCopy = OrderedWriting.CODEC.parse(JsonOps.INSTANCE, json).result().orElseThrow();
helper.assertValueEqual(persistentCopy, original,
"Persistent writing codec must retain entry order and unknown symbol IDs");
FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer());
OrderedWriting.STREAM_CODEC.encode(buffer, original);
OrderedWriting networkCopy = OrderedWriting.STREAM_CODEC.decode(buffer);
helper.assertValueEqual(networkCopy, original,
"Network writing codec must retain entry order and unknown symbol IDs");
helper.succeed();
}
@GameTest(template = "empty")
public static void serverCatalogResolvesBuiltinsAndKeepsUnknownReferencesReadable(GameTestHelper helper) {
SymbolReference known = reference("rivencraft:terrain/standard");
SymbolReference unknown = reference("example:removed_symbol");
helper.assertTrue(SymbolCatalog.symbols().size() >= 6,
"The built-in catalog must exercise every Milestone 4 writing workflow");
helper.assertTrue(SymbolCatalog.isKnown(known),
"The built-in Standard Terrain symbol must be server-authoritative");
helper.assertFalse(SymbolCatalog.isKnown(unknown),
"An absent data-pack symbol must remain unresolved");
var unresolved = SymbolCatalog.display(unknown);
helper.assertFalse(unresolved.resolved(),
"Unknown symbol display metadata must be marked unresolved");
helper.assertValueEqual(unresolved.id(), unknown.id(),
"Unresolved display metadata must preserve the original namespaced ID");
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) {
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();
}
}
}
@@ -0,0 +1,18 @@
package net.dnorberg.rivencraft.mixin.client;
import net.dnorberg.rivencraft.client.preview.ImmersivePortalsPreviewAudioGuard;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Restores the player camera after Immersive Portals finishes GUI-world rendering. */
@Pseudo
@Mixin(targets = "qouteall.imm_ptl.core.render.GuiPortalRendering", remap = false)
public abstract class ImmersivePortalsGuiRenderingMixin {
@Inject(method = "_onGameRenderEnd", at = @At("RETURN"), remap = false)
private static void rivencraft$restorePlayerCamera(CallbackInfo callback) {
ImmersivePortalsPreviewAudioGuard.restorePlayerCamera();
}
}
@@ -19,6 +19,7 @@ public final class ModCreativeTabs {
.displayItems((parameters, output) -> {
output.accept(ModItems.DESCRIPTIVE_BOOK.get());
output.accept(ModItems.LINKING_BOOK.get());
output.accept(ModItems.PAGE.get());
})
.build());
@@ -3,6 +3,8 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.LinkTarget;
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.registries.Registries;
import net.minecraft.resources.ResourceLocation;
@@ -39,6 +41,18 @@ public final class ModDataComponents {
.networkSynchronized(ByteBufCodecs.STRING_UTF8)
.cacheEncoding()
.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() {
}
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.item.PageItem;
import net.minecraft.world.item.Item;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredItem;
@@ -21,6 +22,11 @@ public final class ModItems {
LinkingBookItem::new,
new Item.Properties().stacksTo(1)
);
public static final DeferredItem<PageItem> PAGE = ITEMS.registerItem(
"page",
PageItem::new,
new Item.Properties().stacksTo(64)
);
private ModItems() {
}
@@ -0,0 +1,47 @@
package net.dnorberg.rivencraft.writing;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import io.netty.buffer.ByteBuf;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
/** Immutable ordered writing shared by pages, notebooks, desks, and books. */
public record OrderedWriting(List<WritingEntry> entries) {
public static final int MAX_ENTRIES = 256;
public static final OrderedWriting EMPTY = new OrderedWriting(List.of());
public static final Codec<OrderedWriting> CODEC = WritingEntry.CODEC.listOf()
.validate(entries -> entries.size() <= MAX_ENTRIES
? DataResult.success(entries)
: DataResult.error(() -> "Writing exceeds " + MAX_ENTRIES + " entries"))
.xmap(OrderedWriting::new, OrderedWriting::entries);
public static final StreamCodec<ByteBuf, OrderedWriting> STREAM_CODEC = StreamCodec.of(
OrderedWriting::encode, OrderedWriting::decode);
public OrderedWriting {
entries = List.copyOf(Objects.requireNonNull(entries, "entries"));
if (entries.size() > MAX_ENTRIES) {
throw new IllegalArgumentException("Writing exceeds " + MAX_ENTRIES + " entries");
}
}
private static void encode(ByteBuf buffer, OrderedWriting writing) {
ByteBufCodecs.VAR_INT.encode(buffer, writing.entries.size());
writing.entries.forEach(entry -> WritingEntry.STREAM_CODEC.encode(buffer, entry));
}
private static OrderedWriting decode(ByteBuf buffer) {
int size = ByteBufCodecs.VAR_INT.decode(buffer);
if (size < 0 || size > MAX_ENTRIES) {
throw new IllegalArgumentException("Invalid writing entry count " + size);
}
List<WritingEntry> entries = new ArrayList<>(size);
for (int index = 0; index < size; index++) {
entries.add(WritingEntry.STREAM_CODEC.decode(buffer));
}
return new OrderedWriting(entries);
}
}
@@ -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
}
}
@@ -0,0 +1,67 @@
package net.dnorberg.rivencraft.writing;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.mojang.serialization.JsonOps;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
import net.minecraft.util.profiling.ProfilerFiller;
import net.neoforged.neoforge.event.AddReloadListenerEvent;
/** Authoritative server catalog loaded from data/&lt;namespace&gt;/rivencraft/symbols. */
public final class SymbolCatalog {
private static final Gson GSON = new GsonBuilder().create();
private static final String DIRECTORY = "rivencraft/symbols";
private static volatile Map<ResourceLocation, SymbolDefinition> symbols = Map.of();
private SymbolCatalog() {
}
public static void register(AddReloadListenerEvent event) {
event.addListener(new Loader());
}
public static Optional<SymbolDefinition> find(SymbolReference reference) {
return Optional.ofNullable(symbols.get(reference.id()));
}
public static boolean isKnown(SymbolReference reference) {
return symbols.containsKey(reference.id());
}
public static SymbolDisplayMetadata display(SymbolReference reference) {
SymbolDefinition definition = symbols.get(reference.id());
return definition == null
? SymbolDisplayMetadata.unresolved(reference)
: definition.display(reference.id());
}
public static Map<ResourceLocation, SymbolDefinition> symbols() {
return symbols;
}
private static final class Loader extends SimpleJsonResourceReloadListener {
private Loader() {
super(GSON, DIRECTORY);
}
@Override
protected void apply(Map<ResourceLocation, JsonElement> resources,
ResourceManager resourceManager,
ProfilerFiller profiler) {
Map<ResourceLocation, SymbolDefinition> loaded = new HashMap<>();
resources.forEach((id, json) -> SymbolDefinition.CODEC.parse(JsonOps.INSTANCE, json)
.resultOrPartial(error -> Rivencraft.LOGGER.error(
"Ignoring invalid writing symbol {}: {}", id, error))
.ifPresent(definition -> loaded.put(id, definition)));
symbols = Map.copyOf(loaded);
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
}
}
}
@@ -0,0 +1,47 @@
package net.dnorberg.rivencraft.writing;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.Objects;
import java.util.regex.Pattern;
import net.minecraft.resources.ResourceLocation;
/** Server-loaded, non-executable display metadata for a writing symbol. */
public record SymbolDefinition(
int schemaVersion,
String nameTranslationKey,
String descriptionTranslationKey,
ResourceLocation category
) {
public static final int CURRENT_SCHEMA_VERSION = 1;
private static final Pattern TRANSLATION_KEY = Pattern.compile("[a-z0-9_.-]+");
private static final Codec<String> TRANSLATION_KEY_CODEC = Codec.STRING.comapFlatMap(
value -> TRANSLATION_KEY.matcher(value).matches()
? DataResult.success(value)
: DataResult.error(() -> "Invalid translation key: " + value),
value -> value
);
public static final Codec<SymbolDefinition> CODEC = RecordCodecBuilder.<SymbolDefinition>create(instance -> instance.group(
Codec.INT.fieldOf("schema_version").forGetter(SymbolDefinition::schemaVersion),
TRANSLATION_KEY_CODEC.fieldOf("name").forGetter(SymbolDefinition::nameTranslationKey),
TRANSLATION_KEY_CODEC.fieldOf("description").forGetter(SymbolDefinition::descriptionTranslationKey),
ResourceLocation.CODEC.fieldOf("category").forGetter(SymbolDefinition::category)
).apply(instance, SymbolDefinition::new)).flatXmap(SymbolDefinition::validate, SymbolDefinition::validate);
public SymbolDefinition {
Objects.requireNonNull(nameTranslationKey, "nameTranslationKey");
Objects.requireNonNull(descriptionTranslationKey, "descriptionTranslationKey");
Objects.requireNonNull(category, "category");
}
public SymbolDisplayMetadata display(ResourceLocation id) {
return new SymbolDisplayMetadata(id, nameTranslationKey, descriptionTranslationKey, category, true);
}
private static DataResult<SymbolDefinition> validate(SymbolDefinition definition) {
return definition.schemaVersion == CURRENT_SCHEMA_VERSION
? DataResult.success(definition)
: DataResult.error(() -> "Unsupported symbol schema_version " + definition.schemaVersion);
}
}
@@ -0,0 +1,44 @@
package net.dnorberg.rivencraft.writing;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.netty.buffer.ByteBuf;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
/** Client-safe symbol presentation selected by the authoritative server catalog. */
public record SymbolDisplayMetadata(
ResourceLocation id,
String nameTranslationKey,
String descriptionTranslationKey,
ResourceLocation category,
boolean resolved
) {
public static final Codec<SymbolDisplayMetadata> CODEC = RecordCodecBuilder.create(instance -> instance.group(
ResourceLocation.CODEC.fieldOf("id").forGetter(SymbolDisplayMetadata::id),
Codec.STRING.fieldOf("name").forGetter(SymbolDisplayMetadata::nameTranslationKey),
Codec.STRING.fieldOf("description").forGetter(SymbolDisplayMetadata::descriptionTranslationKey),
ResourceLocation.CODEC.fieldOf("category").forGetter(SymbolDisplayMetadata::category),
Codec.BOOL.fieldOf("resolved").forGetter(SymbolDisplayMetadata::resolved)
).apply(instance, SymbolDisplayMetadata::new));
public static final StreamCodec<ByteBuf, SymbolDisplayMetadata> STREAM_CODEC = StreamCodec.composite(
ResourceLocation.STREAM_CODEC, SymbolDisplayMetadata::id,
ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::nameTranslationKey,
ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::descriptionTranslationKey,
ResourceLocation.STREAM_CODEC, SymbolDisplayMetadata::category,
ByteBufCodecs.BOOL, SymbolDisplayMetadata::resolved,
SymbolDisplayMetadata::new
);
public static SymbolDisplayMetadata unresolved(SymbolReference reference) {
return new SymbolDisplayMetadata(
reference.id(),
"symbol.rivencraft.unresolved",
"symbol.rivencraft.unresolved.description",
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "unresolved"),
false
);
}
}
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.writing;
import com.mojang.serialization.Codec;
import io.netty.buffer.ByteBuf;
import java.util.Objects;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
/** Stable namespaced identity for one writing symbol. */
public record SymbolReference(ResourceLocation id) {
public static final Codec<SymbolReference> CODEC = ResourceLocation.CODEC.xmap(
SymbolReference::new, SymbolReference::id);
public static final StreamCodec<ByteBuf, SymbolReference> STREAM_CODEC = ResourceLocation.STREAM_CODEC.map(
SymbolReference::new, SymbolReference::id);
public SymbolReference {
Objects.requireNonNull(id, "id");
}
/** Remains useful when the defining data pack has been removed. */
public String unresolvedName() {
return id.toString();
}
}
@@ -0,0 +1,82 @@
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;
/** One ordered unit of book writing. Interpretation belongs to Milestone 5. */
public record WritingEntry(Kind kind, Optional<SymbolReference> symbol) {
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 writing entry kind: " + value);
}
},
value -> value.name().toLowerCase(Locale.ROOT)
);
private static final Codec<WritingEntry> RECORD_CODEC = RecordCodecBuilder.create(instance -> instance.group(
KIND_CODEC.fieldOf("kind").forGetter(WritingEntry::kind),
SymbolReference.CODEC.optionalFieldOf("symbol").forGetter(WritingEntry::symbol)
).apply(instance, WritingEntry::new));
public static final Codec<WritingEntry> CODEC = RECORD_CODEC.flatXmap(
WritingEntry::validate, WritingEntry::validate);
public static final StreamCodec<ByteBuf, WritingEntry> STREAM_CODEC = StreamCodec.of(
WritingEntry::encode, WritingEntry::decode);
public WritingEntry {
Objects.requireNonNull(kind, "kind");
symbol = Objects.requireNonNull(symbol, "symbol");
if (kind == Kind.SYMBOL && symbol.isEmpty()) {
throw new IllegalArgumentException("Symbol writing entries require a symbol reference");
}
if (kind == Kind.LINK_PANEL && symbol.isPresent()) {
throw new IllegalArgumentException("Link-panel writing entries cannot contain a symbol reference");
}
}
public static WritingEntry symbol(SymbolReference symbol) {
return new WritingEntry(Kind.SYMBOL, Optional.of(symbol));
}
public static WritingEntry linkPanel() {
return new WritingEntry(Kind.LINK_PANEL, Optional.empty());
}
private static DataResult<WritingEntry> validate(WritingEntry entry) {
return entry.kind == Kind.SYMBOL && entry.symbol.isEmpty()
|| entry.kind == Kind.LINK_PANEL && entry.symbol.isPresent()
? DataResult.error(() -> "Writing entry contents do not match kind " + entry.kind)
: DataResult.success(entry);
}
private static void encode(ByteBuf buffer, WritingEntry entry) {
ByteBufCodecs.VAR_INT.encode(buffer, entry.kind.ordinal());
if (entry.kind == Kind.SYMBOL) {
SymbolReference.STREAM_CODEC.encode(buffer, entry.symbol.orElseThrow());
}
}
private static WritingEntry decode(ByteBuf buffer) {
int ordinal = ByteBufCodecs.VAR_INT.decode(buffer);
if (ordinal < 0 || ordinal >= Kind.values().length) {
throw new IllegalArgumentException("Unknown writing entry kind ordinal " + ordinal);
}
Kind kind = Kind.values()[ordinal];
return kind == Kind.SYMBOL
? symbol(SymbolReference.STREAM_CODEC.decode(buffer))
: linkPanel();
}
public enum Kind {
SYMBOL,
LINK_PANEL
}
}
@@ -6,6 +6,12 @@
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",
"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.unbound": "This Linking Book is unbound",
"message.rivencraft.linking_book.bound": "Linking Book bound as %s",
@@ -57,6 +63,20 @@
"trait.rivencraft.weather.snow": "Snow",
"trait.rivencraft.weather.storm": "Storm",
"trait.rivencraft.day_length.stopped": "Stopped",
"symbol.rivencraft.unresolved": "Unresolved Symbol",
"symbol.rivencraft.unresolved.description": "Missing symbol: %s",
"symbol.rivencraft.terrain.standard": "Standard Terrain",
"symbol.rivencraft.terrain.standard.description": "A familiar continental terrain foundation.",
"symbol.rivencraft.biome.native": "Native Biomes",
"symbol.rivencraft.biome.native.description": "Use the terrain profile's native biome climate.",
"symbol.rivencraft.lighting.normal": "Normal Lighting",
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness.",
"symbol.rivencraft.weather.normal": "Normal Weather",
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
"symbol.rivencraft.celestial.normal_time": "Normal Time",
"symbol.rivencraft.celestial.normal_time.description": "Use a normal-length celestial cycle.",
"symbol.rivencraft.feature.caves": "Caves",
"symbol.rivencraft.feature.caves.description": "Describe underground cave features.",
"generator.rivencraft.standard": "Standard",
"generator.rivencraft.amplified": "Amplified",
"generator.rivencraft.island": "Island",
@@ -1,6 +1,14 @@
{
"parent": "minecraft:item/generated",
"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"
}
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.biome.native",
"description": "symbol.rivencraft.biome.native.description",
"category": "rivencraft:biome"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.celestial.normal_time",
"description": "symbol.rivencraft.celestial.normal_time.description",
"category": "rivencraft:celestial"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.feature.caves",
"description": "symbol.rivencraft.feature.caves.description",
"category": "rivencraft:feature"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.lighting.normal",
"description": "symbol.rivencraft.lighting.normal.description",
"category": "rivencraft:lighting"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.terrain.standard",
"description": "symbol.rivencraft.terrain.standard.description",
"category": "rivencraft:terrain"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.weather.normal",
"description": "symbol.rivencraft.weather.normal.description",
"category": "rivencraft:weather"
}
@@ -4,6 +4,8 @@
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"client.ClientLevelSoundMixin",
"client.ImmersivePortalsGuiRenderingMixin",
"client.LevelRendererMixin"
],
"injectors": {