Kzeroko KTM2

Persistent state storage

Versioned save paths, world scope, key validation and the shared persistence helper.

Updated Sep 24, 2026 7 min read

Most project world-save records use the internal IsekaiPersistData helper and a versioned filename below data/isekaiexpansion/. The selected persistent-state manager determines world scope; a version in the filename does not itself migrate old data.

What it does

internal.persiststate.IsekaiPersistData extends Minecraft’s PersistentState, creates the parent directory and delegates loading/caching to PersistentStateManager.getOrCreate. It is an internal storage convention, not a public API facade or a transaction coordinator.

The server overload always uses the overworld manager. The manager overload uses exactly the supplied manager. Thus a server-wide record accessed from the Nether still lives in overworld data, while a world-scoped city or housing record lives in that world’s data directory.

Runtime model

Current file inventory

Paths below are relative to the selected manager’s data directory. Records are loaded on demand; listing a path does not imply its file already exists.

ScopeRelative fileOwner
Serverisekaiexpansion/blacksmith/commissions_v1.datForge commissions
Serverisekaiexpansion/dungeon/runtime_v2.datDungeon sessions, entrance allocations and participant progress
Serverisekaiexpansion/faction/reputation_v1.datPlayer reputation
Serverisekaiexpansion/faction/relations_v1.datFaction relation overrides
Serverisekaiexpansion/humanoid/paint_v1.datShared humanoid paint
Serverisekaiexpansion/npc/graves_v2.datNPC grave snapshots
Serverisekaiexpansion/npc/unique_marriage_v1.datUnique NPC marriage bindings
Serverisekaiexpansion/quest/meta_v2.datQuest storage metadata
Serverisekaiexpansion/quest/world_flags_v2.datQuest world flags
Serverisekaiexpansion/quest/shards/state_00_v2.dat through state_3f_v2.dat64 quest profile shards
Serverisekaiexpansion/scene/trigger_memory_v2.datPersistent scene-trigger replay memory
Serverisekaiexpansion/taming/companions_v1.datGlobal tame reservations and pending owner death notices
Selected worldisekaiexpansion/city/cities_v1.datCities and their substructures
Selected worldisekaiexpansion/structure/limited_structures_v1.datStructure placement limits
Selected worldisekaiexpansion/structure/spaces_v2.datHousing spaces and membership

Faction and scene state also expose manager overloads; calling those directly can choose a different scope. The listed server scope describes their normal runtime callers. Quest shards take a manager supplied by the server quest state, not the player’s current dimension.

Companion ledger

Use api.taming.TamedCompanionsAPI to read limit(player) and occupied(player) or to reserve and release ownership. Ownership mutations run on the server thread; a rejected reserve(entity, owner) must cancel the assignment. The facade supports TameableEntity and AbstractHorseEntity families.

internal.taming.TamedCompanionLedger uses the overworld helper record. It indexes entity UUID → owner UUID and owner UUID → occupied entities. TamedCompanionsComponent is an owner-only synchronised projection; it does not decide whether a slot is available. Reservations are checked and changed on the server thread before ownership changes. Repeating the same assignment consumes no extra slot.

Death removes the live reservation and queues a named grave notice. An online owner receives a component refresh and the notice immediately. An offline owner’s queue survives saving; login refreshes the projection from the ledger, sends the notices and clears their grave entries. Unloading a live entity or moving it between dimensions leaves its reservation intact. Explicit discard or release removes it. A dead entity loaded from a save cannot restore a live reservation.

FieldTypeDefaultWhat it does
Reservationsarray<object>[]Live reservations. A missing or incorrectly typed list reads as empty.
Reservations[].Entityarray<integer>requiredEntity UUID encoded as 4 integers; malformed UUID data fails loading.
Reservations[].Ownerarray<integer>requiredOwner UUID encoded as 4 integers. Later duplicate entity entries replace earlier ownership.
Gravesarray<object>[]Death notices awaiting an online owner; repeated death callbacks do not add duplicates.
Graves[].Entityarray<integer>requiredDead entity UUID encoded as 4 integers.
Graves[].Ownerarray<integer>requiredRecipient UUID encoded as 4 integers.
Graves[].Namestringempty textJSON-serialized Minecraft text. A decoded null becomes empty text; malformed text is not migrated.
Occupiedinteger0Player component count, clamped to at least 0 when read and reconciled from the ledger on login.

World-tree exception

world.terrain.placement.WorldTreeState directly extends PersistentState and uses isekaiexpansion_world_tree.dat at the manager’s data root. It does not use the versioned helper path. The saved payload has its own version and geometry checks.

Attachment restores a valid reservation, or chooses and records a new one before explicitly saving the manager. An invalid decoded reservation aborts attachment rather than silently selecting another tree location. Do not move this file into the helper directory or infer that all project persistent state follows the helper.

Contracts

Input or operationContract
DomainNonblank slash-separated segments; each segment matches [a-z0-9][a-z0-9_-]*. Nested domains such as quest/shards are supported.
NameA single segment matching the same expression. Dots, spaces, uppercase characters and path separators are rejected.
VersionInteger at least 1; invalid keys throw IllegalArgumentException.
Trailing domain slashJava’s default split discards trailing empty segments, so it normalizes away. Interior empty segments are rejected.
Generated IDisekaiexpansion/<domain>/<name>_v<version>; Minecraft adds the file suffix.
Directory creationResolves the actual manager directory through PersistentStateManagerAccessor, then creates the mod/domain directories.
Directory failureIOException or SecurityException is logged; the helper still calls getOrCreate. This is not proof that a later save will succeed.
Loading and mutationReader/factory and save format belong to the owning subsystem. The helper supplies no generic NBT validation, locks, dirty tracking or client synchronization.
Format changeA new key selects a different file. The helper neither reads the previous filename nor transforms its contents. Any migration must be explicit in the owning subsystem.

Use state mutations on the server thread, after the relevant world exists. Mark dirty when durable data changes; mutating an object returned from a getter does not automatically notify this helper. Keep entity components, client caches and server persistent records distinct, and use the owning service rather than editing live NBT as a gameplay operation.

Extending it

A project-internal state can follow this pattern. The example stores a counter and only marks changes dirty:

final class ExampleState extends IsekaiPersistData {
    private static final Key KEY = key("example", "counter", 1);
    private int count;

    static ExampleState get(MinecraftServer server) {
        return IsekaiPersistData.get(server, KEY, ExampleState::read, ExampleState::new);
    }
    private static ExampleState read(NbtCompound nbt) {
        var state = new ExampleState();
        state.count = nbt.getInt("Count");
        return state;
    }
    void increment() { count++; markDirty(); }
    @Override public NbtCompound writeNbt(NbtCompound nbt) {
        nbt.putInt("Count", count);
        return nbt;
    }
}

Choose server versus world scope before choosing a key. Keep keys stable for compatible formats and implement the reader’s missing/invalid-field behaviour deliberately. A cache added by the owning subsystem must also respect server/world lifetime.

Verifying

  • Load and mutate a server-wide state from different dimensions; confirm it resolves to the same overworld record.
  • Mutate the same world-scoped key in separate worlds and confirm independent records after restart.
  • Exercise invalid domain/name/version inputs and an unavailable storage directory; distinguish logged directory failure from successful durable saving.
  • Round-trip the owning NBT format, including missing and invalid fields. Test a version change against a disposable save and verify the intended migration or fresh-state policy.
  • For a saved world-tree reservation, test restoration and invalid geometry separately; attachment must not silently relocate the landmark.