skills/hytopia-persisted-data/SKILL.md
Helps save and load persistent data in HYTOPIA SDK games. Use when users need to save player progress, leaderboards, game state, or any data that persists across sessions. Covers PersistenceManager, global data, and player data.
npx skillsauth add abstrucked/hytopia-skills hytopia-persisted-dataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill helps you save and load persistent data in HYTOPIA SDK games.
Documentation: https://dev.hytopia.com/sdk-guides/persisted-data
Use this skill when the user:
| Type | Scope | Use Cases | |------|-------|-----------| | Global Data | All game instances | Leaderboards, game config, shared state | | Player Data | Per player | Progress, inventory, stats, preferences |
Shared across all running game instances.
import { PersistenceManager } from 'hytopia';
// Save global data
await PersistenceManager.instance.setGlobalData('leaderboard', [
{ name: 'Player1', score: 1000 },
{ name: 'Player2', score: 950 },
{ name: 'Player3', score: 900 }
]);
// Save game config
await PersistenceManager.instance.setGlobalData('game-config', {
maxPlayers: 20,
roundDuration: 300,
difficulty: 'hard'
});
import { PersistenceManager } from 'hytopia';
// Get leaderboard
const leaderboard = await PersistenceManager.instance.getGlobalData('leaderboard');
console.log('Top scores:', leaderboard);
// Get with default
const config = await PersistenceManager.instance.getGlobalData('game-config');
if (!config) {
// Use defaults
}
Persisted per player across sessions.
import { Player } from 'hytopia';
// Save player progress
await player.setPersistedData('level', 15);
await player.setPersistedData('xp', 2500);
await player.setPersistedData('gold', 1000);
// Save complex data
await player.setPersistedData('inventory', [
{ id: 'sword', quantity: 1 },
{ id: 'potion', quantity: 5 },
{ id: 'key', quantity: 3 }
]);
await player.setPersistedData('unlocks', {
skins: ['default', 'warrior', 'mage'],
maps: ['forest', 'desert'],
achievements: ['first-kill', 'speedrun']
});
import { Player } from 'hytopia';
// Get player progress
const level = await player.getPersistedData('level') || 1;
const xp = await player.getPersistedData('xp') || 0;
const gold = await player.getPersistedData('gold') || 100;
// Get inventory
const inventory = await player.getPersistedData('inventory') || [];
// Load player on join
world.onPlayerJoin = async (player) => {
const savedData = await player.getPersistedData('progress');
if (savedData) {
player.setData('level', savedData.level);
player.setData('xp', savedData.xp);
player.setHealth(savedData.health);
console.log(`Loaded ${player.username}'s progress`);
} else {
// New player - set defaults
player.setData('level', 1);
player.setData('xp', 0);
console.log(`New player: ${player.username}`);
}
};
When updating object data, HYTOPIA performs shallow merging at the root level.
// Initial data
await player.setPersistedData('stats', {
kills: 10,
deaths: 5,
playtime: 3600
});
// Update only kills - other fields preserved
await player.setPersistedData('stats', {
kills: 15
});
// Result: { kills: 15, deaths: 5, playtime: 3600 }
// WARNING: Nested objects are replaced entirely
await player.setPersistedData('settings', {
audio: { music: 0.5, sfx: 1.0 },
video: { quality: 'high' }
});
await player.setPersistedData('settings', {
audio: { music: 0.3 } // sfx is LOST!
});
// Result: { audio: { music: 0.3 }, video: { quality: 'high' } }
// Always fetch, modify, and save for nested data
async function updateNestedSetting(player: Player, path: string, value: any) {
const settings = await player.getPersistedData('settings') || {};
// Deep update
const keys = path.split('.');
let obj = settings;
for (let i = 0; i < keys.length - 1; i++) {
obj[keys[i]] = obj[keys[i]] || {};
obj = obj[keys[i]];
}
obj[keys[keys.length - 1]] = value;
await player.setPersistedData('settings', settings);
}
// Usage
await updateNestedSetting(player, 'audio.music', 0.3);
import { PersistenceManager } from 'hytopia';
async function updateLeaderboard(playerName: string, score: number) {
// Get current leaderboard
const leaderboard = await PersistenceManager.instance.getGlobalData('leaderboard') || [];
// Check if player already on board
const existingIndex = leaderboard.findIndex(e => e.name === playerName);
if (existingIndex !== -1) {
// Update if new score is higher
if (score > leaderboard[existingIndex].score) {
leaderboard[existingIndex].score = score;
}
} else {
// Add new entry
leaderboard.push({ name: playerName, score });
}
// Sort and keep top 100
leaderboard.sort((a, b) => b.score - a.score);
const top100 = leaderboard.slice(0, 100);
await PersistenceManager.instance.setGlobalData('leaderboard', top100);
return top100;
}
async function getLeaderboard(limit: number = 10) {
const leaderboard = await PersistenceManager.instance.getGlobalData('leaderboard') || [];
return leaderboard.slice(0, limit);
}
class AutoSave {
private saveInterval: number = 60000; // 1 minute
private dirty: Set<string> = new Set();
constructor() {
setInterval(() => this.saveAll(), this.saveInterval);
}
markDirty(playerId: string) {
this.dirty.add(playerId);
}
async saveAll() {
for (const playerId of this.dirty) {
const player = world.getPlayer(playerId);
if (player) {
await this.savePlayer(player);
}
}
this.dirty.clear();
console.log(`Auto-saved ${this.dirty.size} players`);
}
async savePlayer(player: Player) {
await player.setPersistedData('progress', {
level: player.getData('level'),
xp: player.getData('xp'),
gold: player.getData('gold'),
inventory: player.getData('inventory'),
lastSaved: Date.now()
});
}
}
const autoSave = new AutoSave();
// Mark player dirty when they change
function addXP(player: Player, amount: number) {
const currentXP = player.getData('xp') || 0;
player.setData('xp', currentXP + amount);
autoSave.markDirty(player.id);
}
world.onPlayerLeave = async (player) => {
// Save all player data before they leave
await player.setPersistedData('progress', {
level: player.getData('level'),
xp: player.getData('xp'),
position: player.position,
inventory: player.getData('inventory'),
lastPlayed: Date.now()
});
console.log(`Saved ${player.username}'s progress`);
};
async function unlockItem(player: Player, category: string, itemId: string) {
const unlocks = await player.getPersistedData('unlocks') || {};
if (!unlocks[category]) {
unlocks[category] = [];
}
if (!unlocks[category].includes(itemId)) {
unlocks[category].push(itemId);
await player.setPersistedData('unlocks', unlocks);
player.sendMessage(`Unlocked: ${itemId}!`);
return true;
}
return false; // Already unlocked
}
async function hasUnlock(player: Player, category: string, itemId: string) {
const unlocks = await player.getPersistedData('unlocks') || {};
return unlocks[category]?.includes(itemId) || false;
}
// Usage
await unlockItem(player, 'skins', 'golden-armor');
const hasSkin = await hasUnlock(player, 'skins', 'golden-armor');
Data persists in auto-generated dev/ directory between restarts.
Tip: Use single browser tabs during local testing - player IDs are assigned sequentially starting at 1 after server restarts.
Set environment variables:
NODE_ENV=productionHYTOPIA_API_KEY - Your API keyHYTOPIA_GAME_ID - Your game IDHYTOPIA_LOBBY_ID - Your lobby IDUpon deployment, HYTOPIA automatically configures persistence services.
const CURRENT_VERSION = 2;
async function loadPlayerData(player: Player) {
const data = await player.getPersistedData('progress');
if (!data) {
return getDefaultData();
}
// Migrate old data formats
if (!data.version || data.version < CURRENT_VERSION) {
const migrated = migrateData(data);
await player.setPersistedData('progress', migrated);
return migrated;
}
return data;
}
function migrateData(data: any) {
let migrated = { ...data };
// v1 -> v2: inventory format changed
if (!data.version || data.version < 2) {
if (Array.isArray(data.inventory)) {
migrated.inventory = data.inventory.map(item =>
typeof item === 'string'
? { id: item, quantity: 1 }
: item
);
}
}
migrated.version = CURRENT_VERSION;
return migrated;
}
development
Helps build and manage worlds in HYTOPIA SDK. Use when users need to create terrain, place blocks, manage chunks, or work with the world editor integration. Covers blocks, chunk loading, world generation, and build.hytopia.com workflow.
tools
Helps create and use plugins in HYTOPIA SDK games. Use when users need to add NPM packages, create reusable modules, or extend game functionality. Covers plugin requirements, installation, and best practices.
development
Helps implement physics and collision in HYTOPIA SDK games. Use when users need rigid bodies, collision detection, raycasting, forces, or physics-based gameplay. Covers PhysicsComponent, colliders, raycasting, and physics simulation.
development
Helps implement multiplayer features in HYTOPIA SDK games. Use when users need player management, server-authoritative gameplay, networking, or state synchronization. Covers Player class, server authority, network optimization, and player data.