Name Age save folders after player titles
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-21 15:07:41 -04:00
1 parent 2da52f2658
commit 3242eb8192
6 files changed
+65 -8

No files matched your search

+4
View File
@@ -46,6 +46,10 @@ available, the platform is generated at Y=63. Reopen the book and click its prev
Copies of the written book continue to resolve to the same persistent Age
across server restarts.
New Age dimension folders use a Minecraft-safe lowercase form of the assigned
name. Conflicts receive incrementing suffixes such as `_2` and `_3`; existing
Age folders are never renamed.
Linking creates a bound Return Link to the exact source position and orientation
according to the configured Return Link policy.
+5
View File
@@ -210,6 +210,11 @@ Version 0.2.10.4 restores Mystcraft Legacy's standard linking sound for both
Linking and Descriptive Books. Successful travel plays the sound at its source
and destination with the legacy volume and pitch variation.
Version 0.2.10.5 derives each new Age's saved dimension folder from its
player-assigned name. Names are converted to safe lowercase resource paths,
and folder conflicts receive `_2`, `_3`, and later numeric suffixes. Existing
UUID-based dimension folders remain unchanged for save compatibility.
The preview code is separated behind a renderer interface and uses an isolated
Ponder level populated from server-authoritative destination snapshots.
+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.2.10.4
mod_version=0.2.10.5
# 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
@@ -33,6 +33,28 @@ public record AgeDefinition(
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + ageId);
}
public static ResourceLocation namedDimensionId(String ageName) {
StringBuilder pathName = new StringBuilder();
boolean lastWasSeparator = false;
for (int index = 0; index < ageName.length(); index++) {
char character = Character.toLowerCase(ageName.charAt(index));
if (character >= 'a' && character <= 'z' || character >= '0' && character <= '9') {
pathName.append(character);
lastWasSeparator = false;
} else if (!lastWasSeparator && !pathName.isEmpty()) {
pathName.append('_');
lastWasSeparator = true;
}
}
while (!pathName.isEmpty() && pathName.charAt(pathName.length() - 1) == '_') {
pathName.deleteCharAt(pathName.length() - 1);
}
if (pathName.isEmpty()) {
pathName.append("unnamed_age");
}
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + pathName);
}
public AgeDefinition withSpawn(BlockPos spawn) {
return new AgeDefinition(id, name, seed, generationProfile, generationVersion,
createdGameTime, dimensionId, spawn.immutable());
@@ -71,14 +71,16 @@ public final class AgeRegistrySavedData extends SavedData {
return existing;
}
String name = normalizedName(reference.name());
ResourceLocation dimensionId = availableDimensionId(name);
AgeDefinition created = new AgeDefinition(
reference.id(),
normalizedName(reference.name()),
ageSeed(worldSeed, reference.id()),
name,
ageSeed(worldSeed, dimensionId),
AgeDefinition.RANDOM_VANILLA_PROFILE,
AgeDefinition.CURRENT_GENERATION_VERSION,
createdGameTime,
AgeDefinition.dimensionId(reference.id()),
dimensionId,
null
);
ages.put(created.id(), created);
@@ -150,9 +152,25 @@ public final class AgeRegistrySavedData extends SavedData {
return normalized.isEmpty() ? "Unnamed Age" : normalized;
}
private static long ageSeed(long worldSeed, UUID ageId) {
private ResourceLocation availableDimensionId(String ageName) {
ResourceLocation base = AgeDefinition.namedDimensionId(ageName);
ResourceLocation candidate = base;
int suffix = 2;
while (dimensionIdInUse(candidate)) {
candidate = ResourceLocation.fromNamespaceAndPath(
base.getNamespace(), base.getPath() + "_" + suffix++
);
}
return candidate;
}
private boolean dimensionIdInUse(ResourceLocation candidate) {
return ages.values().stream().anyMatch(age -> age.dimensionId().equals(candidate));
}
private static long ageSeed(long worldSeed, ResourceLocation dimensionId) {
// Dynamic Dimensions AR derives each level seed from the primary world
// seed and the dimension key. Persist the same value as Age metadata.
return worldSeed + AgeDefinition.dimensionId(ageId).hashCode();
return worldSeed + dimensionId.hashCode();
}
}
@@ -82,7 +82,8 @@ public final class LinkingGameTests {
@GameTest(template = "empty")
public static void ageDefinitionIsCreatedOnceWithDeterministicData(GameTestHelper helper) {
UUID ageId = UUID.randomUUID();
AgeReference reference = new AgeReference(ageId, "Test Age");
String requestedName = "Test Age " + ageId;
AgeReference reference = new AgeReference(ageId, requestedName);
AgeDefinition first = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), reference);
AgeDefinition second = AgeRegistrySavedData.resolveOrCreate(
@@ -91,10 +92,17 @@ public final class LinkingGameTests {
AgeDefinition another = AgeRegistrySavedData.resolveOrCreate(
helper.getLevel(), new AgeReference(UUID.randomUUID(), "Another Age")
);
AgeDefinition conflicting = AgeRegistrySavedData.resolveOrCreate(
helper.getLevel(), new AgeReference(UUID.randomUUID(), requestedName)
);
helper.assertValueEqual(second, first,
"An existing Age UUID must retain its original immutable generation definition");
helper.assertValueEqual(first.name(), "Test Age", "The first book name must become the canonical Age name");
helper.assertValueEqual(first.name(), requestedName, "The first book name must become the canonical Age name");
helper.assertValueEqual(first.dimensionId(), AgeDefinition.namedDimensionId(requestedName),
"A new Age dimension folder must derive from the player-assigned name");
helper.assertValueEqual(conflicting.dimensionId().getPath(), first.dimensionId().getPath() + "_2",
"A conflicting Age dimension folder must receive an incrementing suffix");
helper.assertValueEqual(first.generationProfile(), AgeDefinition.RANDOM_VANILLA_PROFILE,
"A page-free book must use the random vanilla generation profile");
helper.assertValueEqual(first.generationVersion(), AgeDefinition.CURRENT_GENERATION_VERSION,