TDD-003
DataStore Schema
Document Information
Purpose
Dokumen ini membuat konkret & typed skema DataStore yang menyimpan profil permanen satu pemain, berdasarkan "Data to Persist" dan "Data Structure" di GDD-012.
Revisi Besar (Version 2.0.0, Juli 2026). Tiga perubahan: (1) seluruh kepemilikan equipment dihapus dari skema — senjata, ammo, dan Utility kini isi paket Varian yang bersifat session-bound (GDD-004 v2.0.0); (2) Crystal masuk sebagai mata uang kedua; (3) dua kelompok field baru untuk state berjangka waktu — patroli Companion dan pengiriman Caravan (DN-010, DN-011).
Prinsip: Apa yang Tidak Masuk Skema
Batas ini sama pentingnya dengan field yang ada, karena ia yang menjaga model sewa tetap aman:
- SessionLoadout tidak pernah ditulis ke DataStore. Varian yang disewa, isi Backpack sesi, serta senjata dan ammo bawaan seluruhnya hidup di memori server. "Reset ke default" adalah konsekuensi otomatis dari tidak pernah disimpan — bukan aksi penghapusan yang bisa gagal.
- Karena itu, arah kegagalan sistem ini aman: bila server crash, pemain kehilangan sewa (rugi kecil, terkendali) alih-alih menyimpan Varian termahal selamanya (bocor ke ekonomi).
- Hasil patroli yang belum matang disimpan di server, tetapi tidak pernah dikirim ke client — lihat bagian Companion di bawah.
DataStore Key Convention
- DataStore name:
PlayerProfile_v1— angka versi di nama DataStore adalah mekanisme migrasi untuk perubahan struktur besar (buat DataStore baru, migrasi terkontrol), bukan untuk penambahan field kecil. - Key per pemain:
Player_<UserId>. - Penambahan field baru memakai default value aman langsung di kode baca (sesuai GDD-012 Data Migration) — tidak perlu bump nama DataStore untuk ini.
schemaVersion(number) disimpan di dalam data itu sendiri untuk melacak field mana yang sudah pernah di-backfill per profil.
Schema — profile & economy
| Field | Tipe | Catatan |
|---|---|---|
| schemaVersion | number | Untuk migrasi field, lihat Data Migration di bawah. |
| profile.userId | number | — |
| profile.username | string | Cache tampilan, bukan sumber kebenaran identitas. |
| profile.createdAt | number (unix timestamp) | — |
| profile.lastLoginAt | number (unix timestamp) | — |
| economy.coin | number | Tidak pernah ditulis langsung dari client. |
| economy.crystal | number | (baru v2.0.0) Mata uang kedua (GDD-006 Currency). Sumber: milestone Caravan, Weekly Mission, Daily Login, pembelian Robux. |
Schema — inventory
Direvisi total (v2.0.0). Seluruh kelompok equipment.* dihapus: ownedWeapons, ammoCounts, utilityCounts, dan activeLoadout. Tidak ada satu pun yang permanen lagi.
| Field | Tipe | Catatan |
|---|---|---|
| inventory.storage | array<{ animalId, count }> | Permanent Storage. Diisi lewat ekstraksi di Village dan Collect Companion — keduanya wajib lewat satu fungsi server yang sama supaya validasi dan logging tidak berbeda antar jalur. |
| inventory.consumables | map<itemId, count> | (baru v2.0.0) Consumable yang dibeli pemain dan dimiliki permanen (mis. Teleport Stone). Dipilih di layar Loadout untuk dibawa; sisa yang tidak terpakai kembali ke sini saat ekstraksi — bukan hangus, karena pemain sudah membelinya. Berbeda dari senjata dan ammo bawaan Varian yang session-bound dan selalu musnah. |
Schema — collection & progression
| Field | Tipe | Catatan |
|---|---|---|
| collection | map<speciesId, count> | Persis Collection Data MDD-007 — satu field per spesies, tidak ada weight/sub-loot. |
| progression.level | number | MDD-006. |
| progression.exp | number | MDD-006. |
| progression.lifetimePrestige | number | Catatan permanen, tidak pernah di-reset. Sumber seluruh unlock permanen (Item Delivery, Trading, Marketplace, Wraith Isle) — bukan lagi sumber Hunter Rank sejak DN-001 dipisah jadi dua counter. Rank tidak disimpan sebagai field terpisah — selalu dihitung ulang dari threshold MDD-006 saat dibaca, supaya tidak drift kalau threshold berubah. |
progression.seasonalPrestige | number | (baru Juli 2026) Diisi tangkapan yang sama dengan lifetimePrestige, tetapi di-reset tiap musim. Inilah sumber Hunter Rank (DN-001). |
progression.seasonId | string | (baru Juli 2026) Musim yang sedang berjalan saat profil terakhir ditulis. Reset dilakukan saat profil dibaca, bukan lewat sapuan massal: bila seasonId tidak cocok dengan musim aktif, seasonalPrestige dinolkan lalu seasonId diperbarui. Pola yang sama dipakai kematangan patroli Companion — pemain offline tidak pernah membebani server. |
progression.seasonHistory | array | (baru Juli 2026) Riwayat rank akhir tiap musim yang sudah lewat, sumber Medali Musim di Trophy Room (DN-001, MDD-007). Wajib ada sejak rilis meski baru terpakai setelah musim pertama berakhir — bila field ini belum ada saat Season 1 selesai, rank akhir seluruh pemain pada musim itu hilang selamanya dan tidak dapat dipulihkan. |
| achievements | map<achievementId, unlockedAt> | Post-MVP (GDD-012 Post-MVP Persistence Scope). |
Schema — mount & character
| Field | Tipe | Catatan |
|---|---|---|
| cosmetics.ownedMounts | array<mountId> | MNT0001–0003/0009 (beli-langsung) dan MNT0004–0006 (hasil evolusi) memakai field yang sama — tidak dibedakan sumbernya di schema, cukup ownership. |
| cosmetics.ownedCharacters | array<characterId> | CHR0001–0003 (Valdrik/Nayra/Bonoo, gratis) selalu ada default di profil baru; CHR0004–0005 (Freya/Ragnar) baru masuk array setelah dibeli lewat jalur Coin atau Robux. |
| cosmetics.ownedSkins | array<skinId> | Termasuk Skin Outfit & Skin Mount (MDD-003). |
| cosmetics.ownedAccessories | array<accessoryId> | ACC0001–0003 dst, universal ke semua Mount. |
| cosmetics.equipped | { mountId, characterId, mountSkinId, outfitSkinId, accessoryId } | Semua nullable/opsional. |
Dihapus (v2.0.0): companion.crystalCount — Crystal kini mata uang dan pindah ke economy.crystal. Field lama tidak digunakan ulang.
Schema — companion
Baru (v2.0.0, DN-010). Kelompok field untuk sistem berburu offline. Bagian paling sensitif keamanannya ada di activePatrol.slots.
| Field | Tipe | Catatan |
|---|---|---|
| companion.owned | array<{ companionId, level, biome, capacity }> | Level dan biome ditetapkan sekali saat diperoleh, tidak pernah berubah (tidak ada sistem EXP Companion). |
| companion.fuelCounts | map<fuelId, count> | Stok fuel per tier (FUL0001 dst, MDD-003). |
| companion.activePatrol | { companionId, startedAt, endsAt, slots } | null | Hanya satu patroli aktif per Companion. endsAt adalah timestamp selesai, bukan sisa waktu — supaya tetap benar setelah pemain logout. |
| companion.activePatrol.slots | array<{ animalId, ripeAt, collected }> | Seluruh isi sudah ditentukan sejak Start (seed = userId + startedAt). Server hanya boleh mengirim slot yang ripeAt-nya sudah terlewat ke client — sisanya tidak pernah keluar dari server, dalam bentuk apa pun. Ini yang mencegah exploit "mengintip" hasil. |
Schema — caravan
Baru (v2.0.0, DN-011). Seluruhnya state per-pemain — tidak ada state lintas server, sehingga asumsi "session locking antar-server tidak dibutuhkan untuk MVP" di bawah tetap berlaku utuh.
| Field | Tipe | Catatan |
|---|---|---|
| caravan.availableOrders | array<{ orderId, npcId, package, value, travelSeconds, expiresAt }> | Pesanan personal yang sedang ditawarkan ke pemain ini (acuan 2–3 sekaligus, berotasi). value dihitung server dari Base Price × pengali. |
| caravan.activeShipments | array<{ shipmentId, orderId, endsAt, claimed }> | Pengiriman berjalan. endsAt timestamp selesai, bukan hitung mundur. Jumlah dibatasi slot. |
| caravan.slotCount | number | Jumlah Caravan yang boleh berjalan bersamaan; dapat ditambah lewat pembelian. |
caravan.tradeValueAccumulated | Dihapus (Juli 2026) — field ini satu-satunya gunanya menggerakkan milestone Crystal, dan milestone itu dicabut (DN-011). Menyimpan akumulasi yang tidak dibaca siapa pun hanya menambah beban tulis tiap pengiriman. Nama tidak digunakan ulang. | |
Schema — trophy room, weekly mission & settings
| Field | Tipe | Catatan |
|---|---|---|
| trophyRoom.capacityTier | number | MDD-007 (5/15/28 slot). |
| trophyRoom.displayed | array<{ speciesId, pedestalType, slot }> | — |
| weeklyMission.weekId | string | Dipakai untuk deteksi reset mingguan. |
| weeklyMission.objectiveProgress | number | GDD-013. |
| weeklyMission.claimedThisWeek | boolean | — |
| settings | map<key, value> | Post-MVP, minimal untuk MVP (mis. toggle audio). |
Save Triggers
Menerjemahkan Save Triggers GDD-012 ke pola teknis konkret:
- Auto-save berkala — loop server per-player setiap 120 detik, cukup jarang untuk tidak membebani DataStore (CDR-008 "tidak terlalu sering"), cukup sering untuk membatasi kerugian saat crash.
- Dirty-flag setelah transaksi penting — jual-beli, rank up, tangkap hewan langka menandai profil "dirty" dan memicu save dalam debounce singkat (mis. 3 detik), bukan write instan per transaksi, supaya transaksi beruntun tidak spam DataStore.
- On Exit —
Players.PlayerRemovingmemaksa save langsung (bypass debounce), menunggu hasil sebelum instance profil dibuang dari memori. - On Safe State — ekstraksi di Village dan kembali ke Lobby memicu save (state paling "bersih" untuk dipulihkan, sesuai GDD-012).
- On Timed Action — (baru v2.0.0) memulai patroli Companion atau mengirim Caravan wajib memicu save langsung, karena keduanya menulis timestamp yang harus bertahan meski pemain logout sedetik kemudian.
- Server shutdown —
game:BindToClose()menunggu seluruh save pending pemain aktif selesai sebelum server ditutup.
Data Safety
- Kegagalan DataStore:GetAsync/SetAsync di-retry dengan backoff (mis. 3 percobaan), tidak langsung dianggap gagal permanen.
- Jika seluruh retry gagal saat save, profil ditandai tetap dirty dan dicoba lagi di siklus auto-save berikutnya — tidak pernah diam-diam membuang data (GDD-012 Data Safety).
- Session locking antar-server tidak dibutuhkan untuk MVP (single Roblox server per sesi pemain); dicatat sebagai Post-MVP jika fitur cross-server (mis. party lintas server) ditambahkan nanti.
- Recovery setelah disconnect memakai data hasil save terakhir yang berhasil, sesuai GDD-012 Disconnect Handling.
Data Migration
Field baru selalu punya default value aman dan ditambahkan lewat kode baca (bukan mengubah data lama secara massal). Perubahan struktur besar (mis. mengganti bentuk field yang sudah ada) memakai schemaVersion untuk menjalankan fungsi migrasi satu kali saat profil lama pertama kali dibaca, sebelum disimpan ulang dalam bentuk baru — konsisten dengan prinsip CDR-007 "Expandable Foundation" dan GDD-012 Data Migration.