#Data & Persistence
#One profile per player
DataService owns everything that survives a session. Profiles live in the SAE_Profiles_v1 DataStore, keyed per user id, and are cached in memory while the player is in the server.
| Setting | Value |
|---|---|
| DataStore name | SAE_Profiles_v1 |
Config.PROFILE_SCHEMA_VERSION | 2 |
| Load retries / delay | 4 attempts, 2 seconds apart |
| Autosave interval | Config.Economy.SAVE_INTERVAL = 60 s |
| Save on shutdown | DataService.bindToClose() |
| Offline income | Config.Economy.OFFLINE_INCOME = false |
The bootstrap loads a profile before anything else happens: PlayerStateService.initRuntime → DataService.load → plot assignment → speed, treadmill, passes, friend bonus, tutorial, growth and pet rebuild. A player whose profile fails to load is warned and skipped rather than half-initialised.
#Profile shape
DataService.defaultProfile() defines a fresh profile:
| Field | Meaning |
|---|---|
schemaVersion | Migration marker, currently 2 |
createdAt, lastSeen | Unix timestamps |
cash, speed | Currencies (a new profile starts with 10 Speed) |
unlockedZones | { Forest = true } and whatever the Speed requirement unlocked |
treadmillId, ownedTreadmills | Equipped treadmill and the owned set |
activeTrail, ownedTrails | Equipped trail and the owned set |
batId, bats | Equipped bat and the owned set |
penLevel | Index into Config.Pen.Levels |
pets | Inventory: uid → pet record |
placedPets | uid → placement, the set that earns income |
growing | uid → grow entry (petId, weightMul, sizeId, pos, startedAt, finishAt) |
eggItems | Eggs stored as inventory items |
index, indexClaimed | Discovered pets and claimed reward keys |
settings | music, sfx, showOwnPets, showOtherPets, slowMode |
stats | totalEarned, totalStolen, totalHatched, totalFused, bestIncome, playTime |
events | Event progress: riftTokens, mutationShards, sakuraCrystals, bossMastery, freeGiftClaimed, incubatorCharge, boost timestamps and more |
cooldowns | Timed gates such as growAllFree |
purchases | Record of processed receipts |
#Writes, saves and lifetime
- Services mutate the cached profile and call
DataService.markDirty(player). DataService.stripRuntimeremoves runtime-only helpers before the record is written, so a save never contains live Instances or transient flags.- The autosave loop flushes dirty profiles every 60 seconds;
bindToCloseflushes again on shutdown. DataService.release(player)drops the cache entry when the player leaves.
#Migrations
Two functions keep old profiles valid:
migrate(profile)upgrades a profile whoseschemaVersionis behind the current one.reconcile(profile)fills in fields that did not exist when the profile was created — this is what keeps the growingeventsandsettingstables consistent across updates.
When you add a field, add it to defaultProfile and to reconcile, otherwise old profiles will be missing it at runtime.
#Studio behaviour
In Studio, DataService.isPersistent() is false unless the place has API access enabled (Game Settings → Security → Enable Studio Access to API Services). Without it, profiles are kept in memory only: everything works, nothing is saved, and the admin datastore command reports the problem. DataService.selfTest() performs a real read/write check and is what that command calls.
#Extras worth knowing
DataService.ProfileLoadedandDataService.ProfileReleasingareBindableEvents other services listen to.- The test harness needs fake profiles, which is why
__setStoreForTest,__injectProfileForTestand friends exist on the service. - Per-player rates are clamped when read (
EconomyService.sane,Guard.flag), so a corrupted or hostile value cannot be written back into the store in a worse state.