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
- Create a data class extending
PixelPlayerData - Create a data modifier extending
PixelAutoDataModifier - Load data from SQL
- Save data to SQL
- 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
PixelPlayerDatadefines the data structurePixelAutoDataModifierhandles loading and savinggetFromSQL(...)loads dataupdateSQLData(...)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]]