This is an explanation with REAL code examples of how the stat's system works on Soltide! Please note that this is NOT a tutorial, I originally made Soltide as a way to practice with code, and this code is NOT high quality at all, but I thought it'd be neat to share for anyone interested!
Hints: Variables are pieces of data that represent something else, for example, a player variable represents a player, and a PlayerStats variable represents all of the player's stats.
Functions are chunks of code that can be ran (called) in other pieces of code! If you're executing the same lines of code multiple times you'd want to put it in a function so you don't have to write it every time!
Minecraft Plugin (Java):
Plugins are addon to Minecraft's server software.
Code explanations are written as Java code comments! You can tell a line in the code is a code comment if it starts with //.
Let's start off! When a player joins, this code is executed:
Player player = playerJoin.getPlayer();
player.sendMessage("§7Attempting to load player data...");
Long time = System.currentTimeMillis();
String group = Soltide.getPermissions().getPrimaryGroup(null, player);
try{
PlayerStats playerStats = getPlayerStatsFromDatabase(player);
playerStats.setLastLogin(new Date());
playerStats.setPlayerName(player.getName());
playerStats.setRank(group);
Soltide.getInstance().getDb().updatePlayerStats(playerStats);
player.sendMessage("§fSol§btide §8|§f Loaded player data in §b"+(System.currentTimeMillis() - time)+"ms");
Database.addPlayerStatsToMetadata(player, playerStats);
} catch (SQLException e) {
this.plugin.getLogger().warning(e.toString());
player.kickPlayer("§cAn error occurred whilst fetching player data! \n§cPlease try again later.");
}
Let's break this down:
1. Initialization:
This phase loads all the initial variables before actually fetching any player data.
//This sets a player variable from the join event.
Player player = playerJoin.getPlayer();
//This sends the data loading message to the player.
player.sendMessage("§7Attempting to load player data...");
//This sets a variable is used to count the time it took to load.
Long time = System.currentTimeMillis();
-
Getting and Setting: This phase fetches the player data and updates .
//The try and catch handles any database exceptions. try{ //This tells the database to fetch the player's stats. PlayerStats playerStats = getPlayerStatsFromDatabase(player); //These update any values that might've changed since last log out. playerStats.setLastLogin(new Date()); playerStats.setPlayerName(player.getName()); playerStats.setRank(group); //This connects to the database again and tells the database to add the new values! Soltide.getInstance().getDb().updatePlayerStats(playerStats); player.sendMessage("§fSol§btide §8|§f Loaded player data in §b"+(System.currentTimeMillis() - time)+"ms"); Database.addPlayerStatsToMetadata(player, playerStats); }Wait, why does it update the player's name? Player names can be changed, so it can't be used to identify a player, so we use unique user identifiers (UUIDs) to make sure that your stats don't get deleted when you change your name! The reason we also store the player name is so that when you do the /stats command in the discord it can look it up using their username, as it's a lot easier to remember "pbsi" as opposed to "473d773d-d2ab-42e8-b139-a2452568a48d" (My UUID)! -
Error Handling: This phase takes care of any errors .
//Catches any SQLExceptions (Database errors) catch (SQLException e) { //This code sends a warning to console and kicks the player. this.plugin.getLogger().warning(e.toString()); player.kickPlayer("§cAn error occurred whilst fetching player data! \n§cPlease try again later."); }
But how does it actually get and set the values?
These functions!
Getting Player Stats:
public PlayerStats findPlayerStatsByUUID(String uuid) throws SQLException
{
//This execute a database query from all values where the player's uuid is equal to the string!
PreparedStatement statement = getConnection().prepareStatement("SELECT * FROM player_stats WHERE uuid = ?");
statement.setString(1, uuid);
ResultSet resultSet = statement.executeQuery();
PlayerStats playerStats;
if(resultSet.next()){
//If there's a result (if resultSet.next() is true), this creates a PlayerStats object and populates it with the values from the database!
playerStats = new PlayerStats(resultSet.getString("uuid"), resultSet.getInt("deaths"), resultSet.getInt("kills"), resultSet.getDouble("balance"), resultSet.getInt("play_time"), resultSet.getDate("first_login"), resultSet.getDate("last_logout"), resultSet.getString("rank"), resultSet.getString("user_name"), resultSet.getInt("points"));
//Closes out the query.
statement.close();
//This returns the object from the function, being the player's stats!
return playerStats;
}
statement.close();
return null;
}
This function basically searches the database for a result, and if there is one it makes a new PlayerStats object and fills it out with the details from the database!
Updating Player Stats
public void updatePlayerStats(PlayerStats playerStats) throws SQLException {
//This statement finds a player and then sets *every* value!
PreparedStatement statement = getConnection().prepareStatement("UPDATE player_stats SET deaths = ?, kills = ?, balance = ?, play_time = ?, first_login = ?, last_logout = ?, `rank` = ?, `user_name` = ?, points = ? WHERE uuid = ?");
//This gets every value from the PlayerStats object that was passed and sets it!
//Since player stats are updated by getting the whole object and change a few values, this allows it to retain all previously set values when the object is passed, while still being able to update multiple values simultaneously.
statement.setInt(1, playerStats.getDeaths());
statement.setInt(2, playerStats.getKills());
statement.setDouble(3, playerStats.getBalance());
statement.setDouble(4, playerStats.getPlayTime());
statement.setDate(5, new Date(playerStats.getFirstLogin().getTime()));
statement.setDate(6, new Date(playerStats.getLastLogout().getTime()));
statement.setString(7, playerStats.getRank());
statement.setString(8, playerStats.getUserName());
statement.setInt(9, playerStats.getPoints());
//This is the WHERE clause, and identifies which player to update, hence why it's seperated.
statement.setString(10, playerStats.getPlayerUUID());
//This executes the database query!
statement.executeUpdate();
statement.close();
}
*When a player dies code similar to this code is ran, but it updates the kills and deaths. *
This function takes an input of a PlayerStats variable, and as is required, the PlayerStats variable must have every value is set. Since every value is guaranteed to be set, it goes ahead and replaces all the values in the database with the new PlayerStats variable, this allows multiple values to be changed at once while keeping all original values!
Wait, what IS a PlayerStats variable?
public PlayerStats(String playerUUID, int deaths, int kills, double balance, int playTime, Date firstLogin, Date lastLogout, String rank, String userName, int points) {
this.playerUUID = playerUUID;
this.deaths = deaths;
this.kills = kills;
this.balance = balance;
this.playTime = playTime;
this.firstLogin = firstLogin;
this.lastLogout = lastLogout;
this.rank = rank;
this.userName = userName;
this.points = points;
}
This code defines what must be in every instance of the PlayerStats variable. Every time you see a variable that starts is defined as "PlayerStats", it contains every piece of this information!
Great! We got and update the player's stats! What do we do now?
How about the SCOREBOARD! Doesn't that sound fun?
Skript
Yes I could write this in java, but making scoreboards in java is reallyyy hard :(
To setup the scoreboard, the following script is run:
while player is online:
wait 20 ticks
set title of player's fastboard to "&f&lSol&b<ide"
set line 1 of player's fastboard to ""
set line 2 of player's fastboard to "%player's prefix%%player's name%&7:"
set line 3 of player's fastboard to " &8|&r &7Kills:&b %metadata value "kills" of player%"
set line 4 of player's fastboard to " &8|&r &7Deaths:&b %metadata value "deaths" of player%"
set line 5 of player's fastboard to " &8|&r &7KDR:&b %kdr(metadata value "kills" of player + 0, metadata value "deaths" of player + 0)%"
set line 6 of player's fastboard to "&8-"
set line 7 of player's fastboard to " &8|&r &7Level:&6 %getLevel(player)%✳"
set line 8 of player's fastboard to " &8|&r &7Balance:&a %player's balance%"
set line 9 of player's fastboard to ""
set line 10 of player's fastboard to ""
set line 11 of player's fastboard to "&7%now formatted as "MM/dd/YYYY"% &8| &fSoltide.&bnet"
To keep the data in the scoreboard up to date, this code is ran every 20 ticks, or every second (1 tick is 1/20th of a second). Most of it is fairly self explanatory it set's the line of the player's scoreboard (fastboard) to the text inside of the quotations. But how does it dynamically update with changes in player stats? METADATA!!!!! Metadata is invisible data that any plugin can store inside of a player! It is lost once the server restarts, hence why the initial values need to be set when the player joins every time. Each piece has a name, or identifier, and a value! For example, if you got the metadata with the identifies "kills" from me it'd return the number 5!
The reason I use metadata is that it's the easiest to set in Java and access in Skript.
If you notice inside of the quotes, there's text wrapped in %s, what's that about?
That is Skript code that is ran directly within the line, and is replaced by the out!
The code metadata value "kills" of player get's the number of kills from the player!
But how is the metadata set?
If we go back to the plugin code that is ran when a player joins, the last line of that is:
Database.addPlayerStatsToMetadata(player, playerStats);.
This calls a function that get's all the relevant values from the playerStats objects and adds every single value into the player's metadata, here's the code!
public static void addPlayerStatsToMetadata(Player player, PlayerStats stats)
{
//The text in the quotes is the name of the metadata, then the stats.get part gets the actual value to put in the metadata.
player.setMetadata("kills", new FixedMetadataValue(Soltide.getInstance(), stats.getKills()));
player.setMetadata("deaths", new FixedMetadataValue(Soltide.getInstance(), stats.getDeaths()));
player.setMetadata("points", new FixedMetadataValue(Soltide.getInstance(), stats.getPoints()));
//Wait, where's balance? A player's balance can be easily got in Skript by writing %player's balance%, and is stored by the economy plugin, so there's really no reason to fetch it from the database.
}
Fairly simple, it just sets a few values at once, and that's how they're grabbed from within the scoreboard!
Hope you enjoyed my brief explanation on how a random feature on Soltide works! I'll explain how the Discord bot works in another post sometime later :D