Public Access
This commit is contained in:
1 parent
b1d563fb84
commit
a17083309d
7 files changed
+218
-14
No files matched your search
@@ -0,0 +1,27 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** Immutable, server-owned generation definition for one Age. */
|
||||
public record AgeDefinition(
|
||||
UUID id,
|
||||
String name,
|
||||
long seed,
|
||||
ResourceLocation generationProfile,
|
||||
int generationVersion,
|
||||
long createdGameTime
|
||||
) {
|
||||
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "random_vanilla"
|
||||
);
|
||||
public static final int CURRENT_GENERATION_VERSION = 1;
|
||||
|
||||
public AgeDefinition {
|
||||
Objects.requireNonNull(id, "id");
|
||||
Objects.requireNonNull(name, "name");
|
||||
Objects.requireNonNull(generationProfile, "generationProfile");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.datafix.DataFixTypes;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
/** Server-wide persistent mapping from stable book UUIDs to immutable Age definitions. */
|
||||
public final class AgeRegistrySavedData extends SavedData {
|
||||
private static final String DATA_NAME = "rivencraft_ages";
|
||||
private static final String AGES_TAG = "Ages";
|
||||
private static final String ID_TAG = "Id";
|
||||
private static final String NAME_TAG = "Name";
|
||||
private static final String SEED_TAG = "Seed";
|
||||
private static final String PROFILE_TAG = "GenerationProfile";
|
||||
private static final String VERSION_TAG = "GenerationVersion";
|
||||
private static final String CREATED_TIME_TAG = "CreatedGameTime";
|
||||
private static final Factory<AgeRegistrySavedData> FACTORY = new Factory<>(
|
||||
AgeRegistrySavedData::new,
|
||||
AgeRegistrySavedData::load,
|
||||
DataFixTypes.SAVED_DATA_MAP_DATA
|
||||
);
|
||||
|
||||
private final Map<UUID, AgeDefinition> ages = new HashMap<>();
|
||||
|
||||
public static AgeRegistrySavedData get(ServerLevel level) {
|
||||
return level.getServer().overworld().getDataStorage().computeIfAbsent(FACTORY, DATA_NAME);
|
||||
}
|
||||
|
||||
/** Resolves a book identity, creating its deterministic random definition exactly once. */
|
||||
public static AgeDefinition resolveOrCreate(ServerLevel level, AgeReference reference) {
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
return get(overworld).getOrCreate(reference, overworld.getSeed(), overworld.getGameTime());
|
||||
}
|
||||
|
||||
public Optional<AgeDefinition> find(UUID ageId) {
|
||||
return Optional.ofNullable(ages.get(ageId));
|
||||
}
|
||||
|
||||
private AgeDefinition getOrCreate(AgeReference reference, long worldSeed, long createdGameTime) {
|
||||
AgeDefinition existing = ages.get(reference.id());
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
AgeDefinition created = new AgeDefinition(
|
||||
reference.id(),
|
||||
normalizedName(reference.name()),
|
||||
ageSeed(worldSeed, reference.id()),
|
||||
AgeDefinition.RANDOM_VANILLA_PROFILE,
|
||||
AgeDefinition.CURRENT_GENERATION_VERSION,
|
||||
createdGameTime
|
||||
);
|
||||
ages.put(created.id(), created);
|
||||
setDirty();
|
||||
return created;
|
||||
}
|
||||
|
||||
private static AgeRegistrySavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AgeRegistrySavedData data = new AgeRegistrySavedData();
|
||||
ListTag serializedAges = tag.getList(AGES_TAG, CompoundTag.TAG_COMPOUND);
|
||||
for (int index = 0; index < serializedAges.size(); index++) {
|
||||
CompoundTag entry = serializedAges.getCompound(index);
|
||||
if (!entry.hasUUID(ID_TAG)) {
|
||||
continue;
|
||||
}
|
||||
ResourceLocation profile = ResourceLocation.tryParse(entry.getString(PROFILE_TAG));
|
||||
if (profile == null) {
|
||||
profile = AgeDefinition.RANDOM_VANILLA_PROFILE;
|
||||
}
|
||||
UUID id = entry.getUUID(ID_TAG);
|
||||
data.ages.put(id, new AgeDefinition(
|
||||
id,
|
||||
normalizedName(entry.getString(NAME_TAG)),
|
||||
entry.getLong(SEED_TAG),
|
||||
profile,
|
||||
entry.contains(VERSION_TAG) ? entry.getInt(VERSION_TAG) : 1,
|
||||
entry.getLong(CREATED_TIME_TAG)
|
||||
));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
ListTag serializedAges = new ListTag();
|
||||
ages.values().stream()
|
||||
.sorted((left, right) -> left.id().compareTo(right.id()))
|
||||
.forEach(age -> {
|
||||
CompoundTag entry = new CompoundTag();
|
||||
entry.putUUID(ID_TAG, age.id());
|
||||
entry.putString(NAME_TAG, age.name());
|
||||
entry.putLong(SEED_TAG, age.seed());
|
||||
entry.putString(PROFILE_TAG, age.generationProfile().toString());
|
||||
entry.putInt(VERSION_TAG, age.generationVersion());
|
||||
entry.putLong(CREATED_TIME_TAG, age.createdGameTime());
|
||||
serializedAges.add(entry);
|
||||
});
|
||||
tag.put(AGES_TAG, serializedAges);
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static String normalizedName(String name) {
|
||||
String normalized = name.strip();
|
||||
return normalized.isEmpty() ? "Unnamed Age" : normalized;
|
||||
}
|
||||
|
||||
private static long ageSeed(long worldSeed, UUID ageId) {
|
||||
long value = worldSeed ^ ageId.getMostSignificantBits();
|
||||
value = mix64(value + ageId.getLeastSignificantBits());
|
||||
return mix64(value ^ 0x524956454E435241L);
|
||||
}
|
||||
|
||||
/** SplitMix64 finalizer, used only to derive a stable seed from world and Age identities. */
|
||||
private static long mix64(long value) {
|
||||
value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL;
|
||||
return value ^ (value >>> 31);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,9 @@ public final class VanillaAgeResolver {
|
||||
}
|
||||
|
||||
public static Optional<LinkTarget> resolve(ServerPlayer player, AgeReference age) {
|
||||
// Milestone 3 owns Age identity and generation data even while this
|
||||
// temporary adapter continues to provide a vanilla travel destination.
|
||||
AgeRegistrySavedData.resolveOrCreate(player.serverLevel(), age);
|
||||
ResourceKey<Level> dimension = VANILLA_DIMENSIONS.get(Math.floorMod(age.id().hashCode(), VANILLA_DIMENSIONS.size()));
|
||||
ServerLevel level = player.getServer().getLevel(dimension);
|
||||
if (level == null) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.dnorberg.rivencraft.gametest;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeDefinition;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.dnorberg.rivencraft.book.LinkTarget;
|
||||
import net.dnorberg.rivencraft.book.LinkingService;
|
||||
import net.dnorberg.rivencraft.book.AgeVisitSavedData;
|
||||
@@ -75,6 +78,30 @@ public final class LinkingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void ageDefinitionIsCreatedOnceWithDeterministicData(GameTestHelper helper) {
|
||||
UUID ageId = UUID.randomUUID();
|
||||
AgeReference reference = new AgeReference(ageId, "Test Age");
|
||||
|
||||
AgeDefinition first = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(), reference);
|
||||
AgeDefinition second = AgeRegistrySavedData.resolveOrCreate(
|
||||
helper.getLevel(), new AgeReference(ageId, "A later book name")
|
||||
);
|
||||
AgeDefinition another = AgeRegistrySavedData.resolveOrCreate(
|
||||
helper.getLevel(), new AgeReference(UUID.randomUUID(), "Another Age")
|
||||
);
|
||||
|
||||
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.generationProfile(), AgeDefinition.RANDOM_VANILLA_PROFILE,
|
||||
"A page-free book must use the random vanilla generation profile");
|
||||
helper.assertValueEqual(first.generationVersion(), AgeDefinition.CURRENT_GENERATION_VERSION,
|
||||
"New Ages must record the current generation algorithm version");
|
||||
helper.assertTrue(first.seed() != another.seed(), "Distinct Age UUIDs must receive distinct random seeds");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void travelUsesLeafyDestinationWithoutFloor(GameTestHelper helper) {
|
||||
helper.setBlock(ORIGIN.below(), Blocks.AIR);
|
||||
|
||||
Reference in new issue
Block a user