Skip to content

Create persistent player data

This example shows how to create persistent player data and store it in SQL.

Player data is used to attach custom data to a player and keep it synchronized across servers.

→ Deep dive: [[Player Data|Core-API/Player/Player-Data]]


Steps

  1. Create a data class extending PixelPlayerData
  2. Create a data modifier extending PixelAutoDataModifier
  3. Load data from SQL
  4. Save data to SQL
  5. Register the data modifier

Data class

public class ExamplePlayerData extends PixelPlayerData {

    // always overwrite, since you have mismatches otherwise cross servers
    @Serial
    private static final long serialVersionUID = 0L;

    @NotNull
    private String name;

    @NotNull
    private final Timestamp firstJoin;

    public ExamplePlayerData(@NotNull String name, @NotNull Timestamp firstJoin) {
        this.name = name;
        this.firstJoin = firstJoin;
    }

    public @NotNull String getName() {
        return name;
    }

    public void setName(@NotNull String name) {
        this.name = name;
    }

    public @NotNull Timestamp getFirstJoin() {
        return firstJoin;
    }

    @Override
    public PlayerData clone() {
        // always make sure to have a deep-copy here!
        return new ExamplePlayerData(name, new Timestamp(firstJoin.getTime()));
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) return false;
        ExamplePlayerData that = (ExamplePlayerData) o;
        return Objects.equals(name, that.name) && Objects.equals(firstJoin, that.firstJoin);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, firstJoin);
    }

    @Override
    public String toString() {
        return "ExamplePlayerData{" +
                "name='" + name + '\'' +
                ", firstJoin=" + firstJoin +
                '}';
    }

}

Data modifier

public class ExamplePlayerDataModifier extends PixelAutoDataModifier<ExamplePlayerData> {

    public ExamplePlayerDataModifier() {
        // put the system name always first, then a "-" and then the name of the data
        // priority is loading from 0 to 100. If your data modifier depend on each other, define priority properly
        super("Example-PlayerData", 90);
    }

    @Override
    public @NotNull Class<ExamplePlayerData> getPlayerDataType() {
        return ExamplePlayerData.class;
    }

    @Nullable
    @Override
    public ExamplePlayerData getFromSQL(@NotNull SQLUser sqlUser, @NotNull Connection connection, int playerId)
            throws SQLException {
        String table = ExampleTableAdapter.BASE_PLAYER_DATA.getTable();

        SQLResult sqlResult = sqlUser.getSQLResult(
                connection,
                "SELECT name, first_join" +
                        "   FROM " + table +
                        "   WHERE player_id = ?",
                playerId
        );

        SQLRow sqlRow = sqlResult.getFirstOrNull();
        if (sqlRow == null) return null;

        String name = sqlRow.getString("name");
        Timestamp firstJoin = sqlRow.getTimestamp("first_join");

        return new ExamplePlayerData(name, firstJoin);
    }

    @Override
    public boolean updateSQLData(@NotNull SQLUser sqlUser, @NotNull Connection connection, @NotNull PixelPlayer<?> player, @NotNull ExamplePlayerData playerData)
            throws SQLException {
        int playerId = player.getId();

        String table = ExampleTableAdapter.BASE_PLAYER_DATA.getTable();

        sqlUser.executeQuery(
                connection,
                "INSERT INTO " + table +
                        "   (player_id, name, first_join)" +
                        "   VALUES (?, ?, ?)" +
                        "   ON DUPLICATE KEY UPDATE name = ?;",
                playerId,
                playerData.getName(),
                playerData.getFirstJoin(),
                playerData.getName()
        );

        return true;
    }

    // should it only call the update if the data isn't equal to the join data state?
    @Override
    public boolean doEqualCheckBeforeSaving() {
        return true;
    }

    // loading the data before the player actually joins - his join gets delayed until data finished loading
    @Override
    public LoadingState getLoadingState() {
        return LoadingState.PRE_LOGIN;
    }

    @Override
    public QuitAction getQuitAction() {
        return QuitAction.REDIS_AND_SQL_UPDATE;
    }

}

Registering the data modifier

DataHandler dataHandler = BukkitCoreLibrary.getDataHandler();

ExamplePlayerDataModifier dataModifier = new ExamplePlayerDataModifier();
dataHandler.registerDataModifier(dataModifier);

Usage

if (player.hasData(ExamplePlayerData.class)) {
    ExamplePlayerData data = player.getData(ExamplePlayerData.class);
    data.setName("NewName");
}

Explanation

  • PixelPlayerData defines the data structure
  • PixelAutoDataModifier handles loading and saving
  • getFromSQL(...) loads data
  • updateSQLData(...) saves data
  • Data is automatically synced across servers

Notes

  • Always implement a deep copy in clone()
  • Use meaningful names for your data modifier
  • Keep SQL logic clean and consistent

Next step

[[Create an offline player command|Core-API/Build-your-first-feature/Systems/Create-an-offline-player-command]]