No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 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/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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.0
|
||||||
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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 297 B |
|
After Width: | Height: | Size: 941 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||