No files matched your search
@@ -5,13 +5,18 @@ 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;
|
||||
|
||||
@@ -63,6 +68,56 @@ public final class WritingDataGameTests {
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,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.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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||