Minecraft Scripting API

Please note: Some examples on this page may be outdated or may not work as expected.

SuperDB - Basic Usage

SuperDB is a custom database system that makes data persistence simple. Instead of low-level scoreboard/dynamic property APIs, you work with familiar JavaScript objects - SuperDB handles saving/loading automatically.

Why SuperDB?

The problem: Minecraft only gives you scoreboards and dynamic properties - both terrible for complex data structures. You end up manually serializing/deserializing JSON, managing callbacks, and building wrapper functions.

SuperDB solution: Store JavaScript objects like you normally would, SuperDB handles persistence. You get:

  • Simple API: db.set(key, object) and db.get(key) - that's it
  • Complex data: Nested objects, arrays, mixed types - all work
  • Automatic save: Changes are written to disk immediately (if immediateWrite: true)
  • No learning curve: Just JavaScript objects, no SQL or weird serialization

Use it for: Player stats, inventories, achievements, economy, leveling, quest progress, settings - any data that should survive server restarts.

Key insight: SuperDB stores data as files in the world folder. When a player joins and asks for their data, you load from disk. When they change level or earn an achievement, you save immediately. Clean, simple, reliable.

Initialize SuperDB

Create database instances for different data types. Each instance gets its own file, so you can organize logically.

import { SuperDB } from "@minecraft/server"; // Custom module (part of your add-on)

// **Create a database for player stats/progress**
const playerDB = new SuperDB({
  name: "playerData",           // File name: playerData.json in world folder
  immediateWrite: true          // Save to disk immediately after each change
});

// **Create separate databases for different concerns**
// This keeps data organized and lets you manage each separately
const economyDB = new SuperDB({
  name: "economy",              // File name: economy.json
  immediateWrite: true
});

const achievementsDB = new SuperDB({
  name: "achievements",
  immediateWrite: true
});

// **Why separate databases?**
// - Organize data logically (player stats vs money vs achievements)
// - Load only what you need (don't load economy data if you only need player stats)
// - Easier to backup/export individual systems
// - Clear separation of concerns

// **Why immediateWrite: true?**
// - Changes are written to disk immediately after each set()
// - If server crashes, you don't lose recent data
// - Trade-off: slightly slower than batching writes
// - For critical data (money, achievements), always use immediateWrite
// - For non-critical caches, you could use immediateWrite: false to batch writes

Store Data

Store JavaScript objects under a unique key. Objects can be simple (flat) or complex (nested), with arrays and mixed types.

// **Store simple player data**
// Key = unique identifier (player UUID, name, etc.)
// Value = JavaScript object with their data
playerDB.set("player_uuid_1", {
  name: "PlayerName",        // Player's display name
  level: 10,                 // Progression level
  experience: 500,           // XP toward next level
  lastLogin: Date.now()      // Timestamp of last login
});

// **Store complex nested data**
// Same key, but object can have nested structures, arrays, etc.
playerDB.set("player_uuid_2", {
  name: "AnotherPlayer",
  
  // Nested object for grouped stats
  stats: {
    kills: 25,
    deaths: 5,
    playtime: 3600  // seconds
  },
  
  // Array of inventory items
  inventory: [
    { id: "diamond_sword", enchantments: ["sharpness:2"] },
    { id: "golden_apple", amount: 3 }
  ],
  
  // Array of achievement IDs
  achievements: ["first_kill", "diamond_miner"],
  
  // Mixed types - SuperDB handles it all
  isVIP: true,
  joinDate: Date.now(),
  lastLocationX: 100,
  lastLocationY: 64,
  lastLocationZ: 200
});

// **Why use nested objects?**
// - Cleaner than flat keys (stats.kills vs kills, stats.deaths vs deaths)
// - Easier to extend (add new stat types without renaming existing keys)
// - Represents real-world groupings (one player has many stats, inventory, achievements)
// - Less prone to naming conflicts

Retrieve Data

Read data from the database - single objects, with defaults, or iterate everything.

// **Get a specific player's data by key**
const playerData = playerDB.get("player_uuid_1");

// **Check if data exists** (get returns null if key doesn't exist)
if (playerData) {
  console.log(`${playerData.name} is level ${playerData.level}`);
} else {
  console.log("Player not found in database");
}

// **Get with default fallback**
// If key doesn't exist, use the default object instead
// This prevents null reference errors
const data = playerDB.get("player_uuid_1") || {
  name: "New Player",
  level: 1,
  experience: 0
};

// **Access nested data** (drill down the object)
const stats = playerData?.stats;  // Optional chaining - safe if playerData is null
const kills = stats?.kills || 0;  // Default to 0 if kills doesn't exist

// **Get all keys in database** - useful for iterating all players
const allPlayerIds = playerDB.keys();
console.log(`${allPlayerIds.length} players in database`);

// **Iterate over all entries** - get key and value
// Useful for leaderboards, migrations, bulk updates
playerDB.forEach((key, value) => {
  console.log(`${value.name} (${key})`);
  
  // Could do operations like:
  // - Check if player is inactive and delete
  // - Update all data format (migration)
  // - Build leaderboard
});

// **Real-world example: build a leaderboard**
function getTopPlayersbyKills(limit = 10) {
  const topPlayers = [];
  
  playerDB.forEach((key, player) => {
    topPlayers.push({
      name: player.name,
      kills: player.stats?.kills || 0
    });
  });
  
  // Sort by kills, take top N
  return topPlayers
    .sort((a, b) => b.kills - a.kills)
    .slice(0, limit);
}

Update Data

Modify existing data using the read-modify-write pattern: get the object, change it, save it back.

// **Level up: read-modify-write pattern**
// 1. Read current data
const player = playerDB.get("player_uuid_1");

// 2. Check it exists
if (player) {
  // 3. Modify it
  player.level += 1;        // Increment level
  player.experience = 0;    // Reset XP
  
  // 4. Write back to database
  playerDB.set("player_uuid_1", player);
  // (with immediateWrite: true, this saves to disk immediately)
}

// **Track kills - nested update**
const player2 = playerDB.get("player_uuid_2");
if (player2) {
  // Modify nested objects
  player2.stats.kills += 1;        // Increment kill count
  player2.lastUpdated = Date.now(); // Track when this update happened
  
  // Save the entire updated object back
  playerDB.set("player_uuid_2", player2);
}

// **Why read-modify-write?**
// SuperDB stores the entire object, not individual fields
// You can't do partial updates like "just update player.level"
// You must: load full object → change properties → save full object

// **Safe defaults for modifications**
// Before modifying, check if property exists
const player3 = playerDB.get("player_uuid_3");
if (player3) {
  // Initialize property if it doesn't exist, then increment
  player3.killCount = (player3.killCount || 0) + 1;  // "|| 0" handles missing property
  
  // Initialize nested objects
  player3.achievements = player3.achievements || [];
  player3.achievements.push("new_achievement");
  
  playerDB.set("player_uuid_3", player3);
}

// **Real-world example: economy transaction**
function givePlayerMoney(playerId, amount) {
  const player = playerDB.get(playerId);
  if (!player) return false; // Player doesn't exist
  
  // Ensure wallet exists
  player.wallet = player.wallet || { balance: 0, transactions: [] };
  
  // Add money and log transaction
  player.wallet.balance += amount;
  player.wallet.transactions.push({
    type: "deposit",
    amount,
    timestamp: Date.now()
  });
  
  // Save updated player data
  playerDB.set(playerId, player);
  return true;
}

Delete Data

Remove entries from the database - individual keys or the entire database.

// **Delete a single player's data**
playerDB.delete("player_uuid_1"); // Removes that entry from database

// **Check if a key exists** - useful before operating on player data
if (playerDB.has("player_uuid_1")) {
  console.log("Player exists in database");
} else {
  console.log("Player not found - probably new player");
}

// **Clear entire database** - CAREFUL! Deletes everything
playerDB.clear();
// (Usually only used for testing or emergency resets)

// **Practical: archive old players**
// Delete players who haven't logged in for 30 days
function archiveInactivePlayers() {
  const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
  
  playerDB.forEach((key, player) => {
    if (player.lastLogin && player.lastLogin < thirtyDaysAgo) {
      console.log(`Archiving inactive player: ${player.name}`);
      playerDB.delete(key); // Remove from active database
      // (You'd probably save to archive DB first)
    }
  });
}

// **Alternative: mark as inactive instead of delete**
function markPlayerInactive(playerId) {
  const player = playerDB.get(playerId);
  if (player) {
    player.archived = true;  // Mark but keep data
    player.archivedDate = Date.now();
    playerDB.set(playerId, player);
  }
}

Integration with Players

Hook into player join/leave events to load and save data. This is the core of persistence - when player joins, load their data; when they leave, save current state.

import { world } from "@minecraft/server";

// **Player Joins - Load Their Data**
// Called when player enters the world
world.afterEvents.playerJoin.subscribe((event) => {
  const player = event.player;
  // Use player.id (UUID) instead of nameTag (can change if they rename)
  const playerId = player.id;
  
  // Check if this is a new player (no data saved yet)
  if (!playerDB.has(playerId)) {
    // **New player - initialize data**
    playerDB.set(playerId, {
      name: player.nameTag,
      level: 1,
      experience: 0,
      joinDate: Date.now(),      // When they first joined
      firstLogin: true,
      stats: {
        timesJoined: 1,
        totalPlaytime: 0
      }
    });
    player.sendMessage("§aWelcome! You are a new player!");
  } else {
    // **Returning player - load and update**
    const playerData = playerDB.get(playerId);
    playerData.timesJoined = (playerData.timesJoined || 0) + 1;
    playerData.lastLogin = Date.now();
    
    // Optional: apply buffs, set gamemode, teleport to spawn
    // player.teleport({ x: 0, y: 64, z: 0 });
    
    playerDB.set(playerId, playerData);
    player.sendMessage(`§aWelcome back! You are level ${playerData.level}.`);
  }
});

// **Player Leaves - Save Their Data**
// Called when player disconnects
world.afterEvents.playerLeave.subscribe((event) => {
  const player = event.player;
  const playerId = player.id;
  
  const playerData = playerDB.get(playerId);
  
  if (playerData) {
    // Update their final state before saving
    playerData.lastLogout = Date.now();
    // Could also save position: playerData.lastLocationX = player.location.x, etc.
    
    // Save updated player data
    playerDB.set(playerId, playerData);
    console.log(`Saved ${playerData.name}'s data`);
  }
});

// **Why this pattern?**
// - Join event: player enters → load their data into memory → can use it for rest of session
// - Leave event: player exits → save any changes they made during session
// - Between: data is in memory, you modify it as they play
// - Server crash: only lose changes since last save (mitigated by immediateWrite: true)

## Backup & Export

Create snapshots of your data or migrate to other systems.

```javascript
// **Export all data to JSON string**
// Useful for backups or transferring to other systems
function exportDatabase(dbName) {
  const db = new SuperDB({ name: dbName });
  const exported = {};
  
  // Iterate all entries and copy to plain object
  db.forEach((key, value) => {
    exported[key] = value;
  });
  
  // Convert to JSON string (can save to file or send to server)
  return JSON.stringify(exported, null, 2); // Pretty-printed
}

// Usage:
const playerDataBackup = exportDatabase("playerData");
// Now you have all player data as a JSON string
// Could log it, send to external service, etc.

// **Get database statistics**
function getDatabaseSize(db) {
  let count = 0;
  db.forEach(() => count++);
  return count;
}

console.log(`Database has ${getDatabaseSize(playerDB)} players`);

// **Advanced: migration between formats**
function migrateDatabase(oldDb, newDb) {
  const count = 0;
  oldDb.forEach((key, value) => {
    // Transform data if needed
    const migrated = {
      ...value,
      // Add new fields if needed
      schemaVersion: 2,
      migratedAt: Date.now()
    };
    newDb.set(key, migrated);
    count++;
  });
  
  console.log(`Migrated ${count} entries`);
}

Best Practices

  • Use unique identifiers - Use player.id (UUID) instead of nameTag (can change). Prevents data loss if player renames
  • Structure data - Use nested objects to group related data (stats.kills instead of just kills)
  • Save frequently - Enable immediateWrite: true for critical data (money, progression, achievements)
  • Validate on load - Check data integrity when loading (schema version, required fields)
  • Default safely - Use || default pattern when accessing potentially missing properties
  • Archive old data - Periodically clean up or archive inactive players to keep database lean
  • Version your schema - Add schemaVersion field so you can handle migrations when data format changes
  • Backup regularly - Periodically export critical databases as backup

Common Patterns

Recurring operations you'll do often - increment counters, manage arrays, search data.

// **Pattern 1: Increment counter** (used for kills, deaths, playtime, etc.)
// Read → modify → write pattern
function addKill(playerId) {
  const player = playerDB.get(playerId);
  
  // Initialize to 0 if doesn't exist yet
  player.killCount = (player.killCount || 0) + 1;
  
  playerDB.set(playerId, player);
}

// **Pattern 2: Array operations** (inventory, achievements, etc.)
function addItem(playerId, itemId) {
  const player = playerDB.get(playerId);
  
  // Initialize array if doesn't exist
  player.items = player.items || [];
  
  // Add item to array
  player.items.push(itemId);
  
  playerDB.set(playerId, player);
}

function removeItem(playerId, itemId) {
  const player = playerDB.get(playerId);
  
  if (player.items) {
    // Filter out the item (keep everything except the one we're removing)
    player.items = player.items.filter(id => id !== itemId);
  }
  
  playerDB.set(playerId, player);
}

// **Pattern 3: Check array contains** (achievement unlocked, owned item, etc.)
function hasItem(playerId, itemId) {
  const player = playerDB.get(playerId);
  return player?.items?.includes(itemId) || false;
}

// **Pattern 4: Search by property** (find player by name, find highest level, etc.)
function findPlayerByName(name) {
  let found = null;
  
  playerDB.forEach((key, player) => {
    if (player.name === name) {
      found = player;
    }
  });
  
  return found;  // null if not found
}

// **Pattern 5: Conditional counter** (increment only if condition met)
function giveExperienceIfNotEnough(playerId, amount) {
  const player = playerDB.get(playerId);
  
  // Only give XP if player doesn't already have enough
  player.experience = player.experience || 0;
  if (player.experience < 1000) {
    player.experience += amount;
  }
  
  playerDB.set(playerId, player);
}

// **Pattern 6: Batch update** (level up all players, reset stats, etc.)
function seasonResetStats() {
  playerDB.forEach((key, player) => {
    // Reset all stats to 0, but keep level and name
    player.kills = 0;
    player.deaths = 0;
    player.money = 100; // Reset with starting balance
    player.lastSeasonReset = Date.now();
    
    playerDB.set(key, player);
  });
  
  console.log("Season reset complete");
}
Navigation