diff --git a/docs/PORTING.md b/docs/PORTING.md
index edd8705..2d581e0 100644
--- a/docs/PORTING.md
+++ b/docs/PORTING.md
@@ -44,7 +44,7 @@ the symbol grammar is introduced.
- [x] Server-authoritative cross-dimension travel with world-border, collision,
fluid, and floor checks.
- [x] Nearby landing search and clear failure messages.
-- [ ] Descriptive Book identity and Age reference components.
+- [x] Descriptive Book identity and Age reference components.
- [ ] Return-link creation as part of Descriptive Book travel.
- [ ] Lectern/book-stand interaction.
- [ ] Automated in-game tests for binding, persistence, and travel.
@@ -53,6 +53,10 @@ The temporary shapeless Linking Book recipe makes the linking subsystem easy
to test. It can move into the writing/bookbinder progression when that system
is implemented.
+Age references use UUIDs rather than dimension keys. Milestone 3 will own the
+mapping from that stable identity to server data and a runtime dimension. This
+keeps saved books valid if an Age is migrated or regenerated.
+
## Version policy
Versions follow `x.y.z`:
diff --git a/gradle.properties b/gradle.properties
index e9d8b44..5c64f25 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -32,7 +32,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. See https://semver.org/
-mod_version=0.1.1
+mod_version=0.1.2
# 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.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
diff --git a/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java b/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java
new file mode 100644
index 0000000..e6a2c37
--- /dev/null
+++ b/src/main/java/net/dnorberg/rivencraft/book/AgeReference.java
@@ -0,0 +1,27 @@
+package net.dnorberg.rivencraft.book;
+
+import com.mojang.serialization.Codec;
+import io.netty.buffer.ByteBuf;
+import java.util.UUID;
+import net.minecraft.core.UUIDUtil;
+import net.minecraft.network.codec.StreamCodec;
+
+/**
+ * Stable identity for an Age, independent of its eventual dimension key.
+ *
+ *
Milestone 3 will resolve this identifier through server-owned Age data.
+ * Books only retain the identifier so dimension regeneration or migration does
+ * not invalidate every existing book.
+ */
+public record AgeReference(UUID id) {
+ public static final Codec CODEC = UUIDUtil.STRING_CODEC.xmap(AgeReference::new, AgeReference::id);
+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite(
+ UUIDUtil.STREAM_CODEC,
+ AgeReference::id,
+ AgeReference::new
+ );
+
+ public String shortId() {
+ return id.toString().substring(0, 8);
+ }
+}
diff --git a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java
index d0bf781..42f4469 100644
--- a/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java
+++ b/src/main/java/net/dnorberg/rivencraft/item/DescriptiveBookItem.java
@@ -1,8 +1,11 @@
package net.dnorberg.rivencraft.item;
import java.util.List;
+import net.dnorberg.rivencraft.book.AgeReference;
+import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
@@ -19,6 +22,11 @@ public final class DescriptiveBookItem extends Item {
super(properties);
}
+ @Override
+ public boolean isFoil(ItemStack stack) {
+ return stack.has(ModDataComponents.AGE_REFERENCE.get()) || super.isFoil(stack);
+ }
+
@Override
public void appendHoverText(
ItemStack stack,
@@ -26,7 +34,10 @@ public final class DescriptiveBookItem extends Item {
List tooltipComponents,
TooltipFlag tooltipFlag
) {
- tooltipComponents.add(Component.translatable("item.rivencraft.descriptive_book.unwritten")
- .withStyle(ChatFormatting.GRAY));
+ AgeReference age = stack.get(ModDataComponents.AGE_REFERENCE.get());
+ MutableComponent status = age == null
+ ? Component.translatable("item.rivencraft.descriptive_book.unwritten")
+ : Component.translatable("item.rivencraft.descriptive_book.age", age.shortId());
+ tooltipComponents.add(status.withStyle(ChatFormatting.GRAY));
}
}
diff --git a/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java b/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java
index 6f4efa5..0c2f3d0 100644
--- a/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java
+++ b/src/main/java/net/dnorberg/rivencraft/registry/ModDataComponents.java
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.registry;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.LinkTarget;
+import net.dnorberg.rivencraft.book.AgeReference;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.neoforged.bus.api.IEventBus;
@@ -18,6 +19,12 @@ public final class ModDataComponents {
.networkSynchronized(LinkTarget.STREAM_CODEC)
.cacheEncoding()
.build());
+ public static final DeferredHolder, DataComponentType> AGE_REFERENCE =
+ DATA_COMPONENTS.register("age_reference", () -> DataComponentType.builder()
+ .persistent(AgeReference.CODEC)
+ .networkSynchronized(AgeReference.STREAM_CODEC)
+ .cacheEncoding()
+ .build());
private ModDataComponents() {
}
diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json
index 2dbe910..13b6484 100644
--- a/src/main/resources/assets/rivencraft/lang/en_us.json
+++ b/src/main/resources/assets/rivencraft/lang/en_us.json
@@ -1,6 +1,7 @@
{
"item.rivencraft.descriptive_book": "Descriptive Book",
"item.rivencraft.descriptive_book.unwritten": "Unwritten",
+ "item.rivencraft.descriptive_book.age": "Age %s",
"item.rivencraft.linking_book": "Linking Book",
"item.rivencraft.linking_book.unbound": "Unbound — sneak-use to bind",
"item.rivencraft.linking_book.destination": "%s (%s, %s, %s)",