Public Access
This commit is contained in:
1 parent
b1d563fb84
commit
a17083309d
7 files changed
+218
-14
No files matched your search
@@ -62,15 +62,17 @@ The built mod is written to `build/libs/`.
|
||||
|
||||
## Versioning
|
||||
|
||||
Rivencraft versions use `x.y.z`:
|
||||
Rivencraft versions use `x.y.z.b`:
|
||||
|
||||
- `x` changes for a major public release.
|
||||
- `y` changes when a development milestone is completed.
|
||||
- `z` changes for iterations within the current milestone and for bug fixes.
|
||||
- `z` changes for each release slice within the current milestone.
|
||||
- `b` changes for bug fixes and additional work within that release slice.
|
||||
|
||||
The patch component resets to zero when a milestone is completed. For example,
|
||||
milestone 2 development follows `0.1.1`, `0.1.2`, and so on; completing that
|
||||
milestone produces `0.2.0`.
|
||||
The build component resets to zero when a new release slice begins. Completing
|
||||
a milestone increments `y` and resets both `z` and `b` to zero. For example,
|
||||
milestone 2 development can progress from `0.1.1.0` to `0.1.1.1`, then
|
||||
`0.1.2.0`; completing the milestone produces `0.2.0.0`.
|
||||
|
||||
## Porting approach
|
||||
|
||||
|
||||
+25
-7
@@ -12,10 +12,20 @@ split into independently testable milestones.
|
||||
2. **Books and linking** — descriptive/linking book data components, lectern-like
|
||||
interaction, safe teleportation, and return links. **Complete.**
|
||||
3. **Ages** — dimension definitions backed by data packs, deterministic seeds,
|
||||
lifecycle management, and persistence.
|
||||
lifecycle management, and persistence. Make the legacy-style animated
|
||||
linking panel the built-in destination preview. Provide optional
|
||||
Create/Ponder and Immersive Portals integrations as alternative rotating-
|
||||
snapshot renderers. The configurable preview modes are `AUTO`,
|
||||
`IMMERSIVE_PORTALS`, `PONDER`, and `LEGACY`; `AUTO` prefers Immersive
|
||||
Portals when it is installed, then Create/Ponder, and finally the built-in
|
||||
legacy animation.
|
||||
4. **Writing system** — pages, symbols, notebooks, desks, and book assembly.
|
||||
5. **Age grammar** — translate symbol combinations into modern biome, noise,
|
||||
dimension-type, and worldgen configuration.
|
||||
dimension-type, and worldgen configuration. Optional integrations with
|
||||
TerraBlender provide composable biome-region generation, while Serene
|
||||
Seasons and Ecliptic Seasons allow Age generation to participate in their
|
||||
respective seasonal systems. None of these integrations are required for
|
||||
Rivencraft's core world generation.
|
||||
6. **Instability** — deterministic validation and effects with server-safe
|
||||
behavior.
|
||||
7. **Structures and polish** — libraries, crystals, portals, recipes, models,
|
||||
@@ -174,20 +184,28 @@ Return Link only for that initial arrival.
|
||||
Version 0.2.8 makes Return Link creation a server setting: `ON_CREATION`
|
||||
(default), `ALWAYS`, or `DISABLED`.
|
||||
|
||||
Version 0.2.9.0 begins Milestone 3 with a server-owned, world-persistent Age
|
||||
registry. Each Age UUID resolves exactly once to an immutable random seed,
|
||||
generation profile, generation-version marker, canonical name, and creation
|
||||
time. The temporary vanilla-dimension adapter remains in place until runtime
|
||||
Age dimensions are introduced by a later release slice.
|
||||
|
||||
The preview code is separated behind a renderer interface and uses an isolated
|
||||
Ponder level populated from server-authoritative destination snapshots.
|
||||
|
||||
## Version policy
|
||||
|
||||
Versions follow `x.y.z`:
|
||||
Versions follow `x.y.z.b`:
|
||||
|
||||
- `x`: major release point.
|
||||
- `y`: number of completed milestones.
|
||||
- `z`: iteration within the active milestone, including bug-fix releases.
|
||||
- `z`: release slice within the active milestone.
|
||||
- `b`: bug-fix and additional-work iteration within that release slice.
|
||||
|
||||
Starting work on a milestone increments `z`. Completing it increments `y` and
|
||||
resets `z` to zero. The first milestone-2 slice is therefore `0.1.1`, and
|
||||
milestone 2 completion will be `0.2.0`.
|
||||
Starting a release slice increments `z` and resets `b` to zero. Completing a
|
||||
milestone increments `y` and resets both `z` and `b` to zero. The first
|
||||
milestone-2 slice is therefore `0.1.1.0`; a fix on that slice is `0.1.1.1`;
|
||||
and milestone 2 completion is `0.2.0.0`.
|
||||
|
||||
## Milestone 1 acceptance record
|
||||
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ mod_id=rivencraft
|
||||
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.2.8
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.2.9.0
|
||||
# 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
|
||||
|
||||
@@ -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