Astral Realms Documentation Help

Creating a Custom Adapter

This guide walks through building a custom SnapshotAdapter that saves and restores plugin-specific player data. The example tracks kill and death counts.

Step 1 — Create the Data Class

Define a plain Java class to hold the data you want to persist.

public class PlayerStats { private int kills; private int deaths; public PlayerStats() {} public PlayerStats(int kills, int deaths) { this.kills = kills; this.deaths = deaths; } public int getKills() { return kills; } public int getDeaths() { return deaths; } public void setKills(int kills) { this.kills = kills; } public void setDeaths(int deaths) { this.deaths = deaths; } }

Step 2 — Implement SnapshotAdapter

Implement all six methods of SnapshotAdapter<T>.

import com.astralrealms.sync.adapter.SnapshotAdapter; import com.astralrealms.sync.model.holder.DataHolder; import net.kyori.adventure.key.Key; import org.bukkit.entity.Player; public class PlayerStatsAdapter implements SnapshotAdapter<PlayerStats> { private static final Key KEY = Key.key("myplugin", "stats"); /** * Unique identifier for this adapter. * Use your plugin's name as the namespace. */ @Override public Key key() { return KEY; } /** * Called for brand-new players who have no snapshot yet. * Return sensible defaults. */ @Override public PlayerStats create(Player player) { return new PlayerStats(0, 0); } /** * Called just before serialization to capture current state. * Update the object in-place and return it. */ @Override public PlayerStats update(Player player, PlayerStats stats) { // If stats are stored in memory elsewhere (e.g. a local cache), // pull them in here. Otherwise just return the object as-is. return stats; } /** * Called on the main server thread after deserialization. * Apply the loaded data to the player. */ @Override public void apply(Player player, PlayerStats stats) { // Example: store in a local cache so your plugin can read it quickly StatsCache.put(player.getUniqueId(), stats); } /** * Read the data object from the binary stream. * Order must match serialize(). */ @Override public PlayerStats deserialize(DataHolder holder, BinaryMessage message) { int kills = message.readInt(); int deaths = message.readInt(); return new PlayerStats(kills, deaths); } /** * Write the data object to the binary stream. * Order must match deserialize(). */ @Override public void serialize(DataHolder holder, PlayerStats stats, BinaryMessage message) { message.writeInt(stats.getKills()); message.writeInt(stats.getDeaths()); } }

Step 3 — Register in onEnable()

Register the adapter in your plugin's onEnable(). This must happen before any player joins.

import com.astralrealms.sync.SyncAPI; public class MyPlugin extends JavaPlugin { @Override public void onEnable() { SyncAPI.registerAdapter(new PlayerStatsAdapter()); } }

Step 4 — Access Data at Runtime

Read the data using SyncAPI. Safe to call after PlayerDataLoadedEvent fires.

@EventHandler public void onDataLoaded(PlayerDataLoadedEvent event) { Player player = event.getPlayer(); SyncAPI.findData(player.getUniqueId(), PlayerStats.class).ifPresent(stats -> { player.sendMessage("Kills: " + stats.getKills() + " | Deaths: " + stats.getDeaths()); }); }

Or by key:

Key key = Key.key("myplugin", "stats"); SyncAPI.<PlayerStats>findData(player.getUniqueId(), key).ifPresent(stats -> { ... });

Optional: Adding Versioning

If your data format may change in a future plugin update, implement VersionedSnapshotAdapter instead. This lets you add a v2 while still reading old v1 snapshots from the database.

Your data class must implement VersionedData:

import com.astralrealms.sync.model.serializer.VersionedData; public class PlayerStatsV2 implements VersionedData { private int kills; private int deaths; private long playtimeSeconds; // new field in v2 @Override public int version() { return 2; } // getters / setters ... }

The adapter declares its version:

import com.astralrealms.sync.adapter.VersionedSnapshotAdapter; public class PlayerStatsAdapterV2 implements VersionedSnapshotAdapter<PlayerStatsV2> { @Override public int version() { return 2; } @Override public Key key() { return Key.key("myplugin", "stats"); } @Override public PlayerStatsV2 deserialize(DataHolder holder, BinaryMessage message) { int kills = message.readInt(); int deaths = message.readInt(); long playtime = message.readLong(); // new in v2 return new PlayerStatsV2(kills, deaths, playtime); } @Override public void serialize(DataHolder holder, PlayerStatsV2 stats, BinaryMessage message) { message.writeInt(stats.getKills()); message.writeInt(stats.getDeaths()); message.writeLong(stats.getPlaytimeSeconds()); } // ... create(), update(), apply() }

Register both adapters so that old v1 snapshots can still be read:

@Override public void onEnable() { SyncAPI.registerAdapter(new PlayerStatsAdapterV1()); // reads old snapshots SyncAPI.registerAdapter(new PlayerStatsAdapterV2()); // used for all new saves }

Optional: Cleanup on Unload

If your adapter allocates resources that must be released when a player disconnects, implement UnloadableSnapshotAdapter:

import com.astralrealms.sync.adapter.UnloadableSnapshotAdapter; public class PlayerStatsAdapter implements UnloadableSnapshotAdapter<PlayerStats> { // ... SnapshotAdapter methods ... /** * Called automatically when SnapshotCause.shouldUnload() == true * (i.e. DISCONNECT or SERVER_SHUTDOWN). */ @Override public void unload(DataHolder holder, Player player, PlayerStats stats) { StatsCache.remove(player.getUniqueId()); } }

You do not need to listen to quit events or call unload() yourself — AstralSync invokes it at the correct time.

Checklist

  • [ ] Data class has a no-arg constructor (or is constructed inside create())

  • [ ] key() uses your plugin's name as the namespace (not astralsync)

  • [ ] serialize() and deserialize() write/read fields in the same order

  • [ ] Adapter is registered in onEnable() before any player joins

  • [ ] Data is read only after PlayerDataLoadedEvent fires or hasFinishedLoading() is true

  • [ ] If using versioning, all old adapters remain registered alongside the new one

24 April 2026