Keeping a character's stats, skills and perks
Everything the game’s character screen shows is the game mode’s: the stats (strength, agility, vitality, speech, the story’s progress and the Blacksmith’s prestige), the skills, the experience gathered toward each next level, the perk points not spent yet, the perks, and two states of the character, the nourishment and the energy. By default a server keeps none of it between visits: every visit starts with the game’s own fresh character.
A game mode can keep it. The server keeps a record of each player’s progression as their game reports it, the mode
saves that record in its own database and puts it back at the next login. It needs the mode’s
database. The names are in
the Stats, skills, XP and perks group of the server API. The examples are
Lua; a C# mode has the same calls on IServerApi.
1. What is kept
Section titled “1. What is kept”GetPlayerProgress(pid) answers the record in one table:
local p = GetPlayerProgress(pid)-- p.stats.strength { level = 12, xp = 40.5, next = 500, points = 1 }-- p.skills.weapon_sword { level = 8, xp = 0, next = 290, points = 2 }-- p.stats.mainlevel { level = 9, xp = 0, next = 0, points = 3 } - the main level, and the pool of points of no stat or skill-- p.perks { "2993585c-40c9-42e3-ac45-b837f3bc50f7", ... } - the perks the character holds, by ID-- p.nourishment 98.2 - nil until the player's game has reported-- p.energy 97.0| Part | What it is |
|---|---|
| a stat or skill | level from 0 to 30, xp gathered toward the next level, next what the next level takes in all, points not spent yet (the names) |
stats.mainlevel |
the main level - the game works it out from the other levels, so it is read only; its points are the pool that belongs to no stat or skill |
skills.fencing |
Warfare - the game works it out from the weapon skills, so only its points are the mode’s |
perks |
the perks the character holds, by ID (the perks) |
nourishment, energy |
how fed and how rested the character is |
The player’s game reports its progression a few seconds after each spawn, the moment a level, a perk or perk points change, within seconds for experience and the two states, and the whole of it again every minute. A stat or skill the server has not heard of yet reads as level 0, so read the record after the game has reported, not before.
2. The experience players earn
Section titled “2. The experience players earn”Nobody gains experience unless the mode says so. The game awards experience for a fight, a ride, a book or a drink; the
server’s rate multiplies every award, and the rate is 0 by default.
SetXPRate(rate) sets it for everyone - 1 is the game’s own, 2 twice as much, up to
100 - and SetPlayerXPRate(pid, rate) for one player (nil gives them back the
server-wide rate).
function OnGameModeInit() SetXPRate(1) -- the game's own experience countsendA mode that wants a word on every award defines
OnPlayerGainXP(pid, kind, name, xp). kind is "stat" or "skill", xp the
award already times the rate. Return nothing to give it, false to refuse it, or a number to give that much instead:
local apprentices = {} -- the mode's own: pid -> true for the players learning at the guild
function OnPlayerGainXP(pid, kind, name, xp) if name == "drinking" then return false end -- no drinking experience on this server if apprentices[pid] and kind == "skill" then return xp * 2 endendA rate of 0 gives nothing and asks nothing, so a mode that defines the callback sets a rate too. Experience the mode hands out
itself goes the game’s way, with the level-up notice and the perk points:
AddPlayerStatXP(pid, stat, xp) and
AddPlayerSkillXP(pid, skill, xp). A level a player earns reaches
OnPlayerLevelUp(pid, kind, name, level) - the main level too, as the stat
mainlevel.
3. Saving a character
Section titled “3. Saving a character”OnPlayerProgressChange(pid, levels) says the record changed. levels is
true when a level, a perk or perk points changed - save now, it is the character - and false when only experience or the
two states moved, which a timer can save. It also runs once for the first report after the player joins. The mode’s own sets
do not call it.
Never write before the restore has gone out. Until the saved progression is put back (section 4) the record holds the game’s fresh character, and writing that over the stored one would lose it. A character with nothing saved yet starts from its game’s own first report.
The mode below keeps one row per stat and skill and one per perk. It keys a character by name for brevity (a mode with accounts uses its account’s id) and uses the database calls of the databases guide:
local db = GetDatabase()-- pid -> { who = the character, ready = may be written, fresh = nothing saved yet, dirty = moved since the last save,-- stored = { ["stat:strength"] = "12:40.5:1", perks = "id,id,..." } - what the tables hold now }local players = {}
local function key(v) return string.format("%d:%.1f:%d", v.level, v.xp, v.points) end
function OnGameModeInit() SetXPRate(1) if not db then Log("no database: progression is not kept") return end db:ExecuteSync("CREATE TABLE IF NOT EXISTS player_progress (who VARCHAR(24) NOT NULL, kind VARCHAR(8) NOT NULL, " .. "name VARCHAR(32) NOT NULL, level SMALLINT NOT NULL, xp DOUBLE PRECISION NOT NULL, points SMALLINT NOT NULL, " .. "PRIMARY KEY (who, kind, name))") db:ExecuteSync("CREATE TABLE IF NOT EXISTS player_perks (who VARCHAR(24) NOT NULL, perk VARCHAR(36) NOT NULL, " .. "PRIMARY KEY (who, perk))") SetTimer(saveAllProgress, 60000, true) -- every minute, for whoever gained experience sinceend
function OnPlayerConnect(pid) players[pid] = { stored = {} } restoreProgress(pid, GetPlayerName(pid):lower()) -- section 4; with passwords, once the password is rightend
function OnPlayerProgressChange(pid, levels) local p = players[pid] if not p then return end if p.fresh then p.ready = true end -- a new character: its game's first report is where it starts if levels then saveProgress(pid) else p.dirty = true endend
function OnPlayerDisconnect(pid, reason) saveProgress(pid) players[pid] = nilend
function OnGameModeExit() -- the server stops, or the mode is reloaded for pid in pairs(players) do saveProgress(pid) endend
function saveProgress(pid) local p = players[pid] local now = p and p.who and p.ready and GetPlayerProgress(pid) if not now then return end -- not back yet: nothing is written p.dirty = nil local batch = {} for _, group in ipairs({ { "stat", now.stats }, { "skill", now.skills } }) do for name, v in pairs(group[2]) do local id = group[1] .. ":" .. name if p.stored[id] ~= key(v) then -- only what differs from the last save p.stored[id] = key(v) batch[#batch + 1] = { "DELETE FROM player_progress WHERE who = @w AND kind = @k AND name = @n", { w = p.who, k = group[1], n = name } } batch[#batch + 1] = { "INSERT INTO player_progress (who, kind, name, level, xp, points) VALUES (@w, @k, @n, @l, @x, @p)", { w = p.who, k = group[1], n = name, l = v.level, x = v.xp, p = v.points } } end end end local perks = table.concat(now.perks, ",") if perks ~= p.stored.perks then p.stored.perks = perks batch[#batch + 1] = { "DELETE FROM player_perks WHERE who = @w", { w = p.who } } for _, perk in ipairs(now.perks) do batch[#batch + 1] = { "INSERT INTO player_perks (who, perk) VALUES (@w, @g)", { w = p.who, g = perk } } end end if #batch == 0 then return end db:Batch(batch, function(ok, err) if ok then return end Log("progression not saved: " .. tostring(err)) p.stored, p.dirty = {}, true -- the next round writes it all again end)end
function saveAllProgress() for pid, p in pairs(players) do if p.dirty then saveProgress(pid) end endend4. Putting it back at the login
Section titled “4. Putting it back at the login”SetPlayerProgress(pid, progress) takes a table of the shape
GetPlayerProgress gives, and every part is optional. It is a whole restore and exact:
- every stat and skill listed is set as it is - its level, its experience and its perk points (none counts as
0), lowering too, and without the game’s level-up notice. An entry without alevelis skipped; stats.mainlevelputs back only the pool of perk points,skills.fencingonly Warfare’s points: the game works both levels out from the others;perks, when given, makes the character’s perks exactly that list;nourishmentandenergyare set when given.
The server takes it at once (GetPlayerProgress right afterwards answers it) and sends it to the player’s game. A restore made
before the player is in the world waits for their spawn, and a report their game sent before the restore landed never
overwrites it.
-- when the mode knows which character this is: OnPlayerConnect on a server without passwords, else once the password is rightfunction restoreProgress(pid, who) local p = players[pid] if not (db and p) then return end p.who = who db:Query("SELECT kind, name, level, xp, points FROM player_progress WHERE who = @w", { w = who }, function(rows, err) if err then Log("progression not read: " .. err) return end -- nothing is written this visit if players[pid] ~= p then return end -- they left meanwhile if #rows == 0 then p.fresh = true -- nothing saved: the game's own start is the first save return end local progress = { stats = {}, skills = {} } for _, r in ipairs(rows) do local v = { level = r.level, xp = r.xp, points = r.points } progress[r.kind == "stat" and "stats" or "skills"][r.name] = v p.stored[r.kind .. ":" .. r.name] = key(v) end db:Query("SELECT perk FROM player_perks WHERE who = @w", { w = who }, function(perkRows, perkErr) if perkErr or players[pid] ~= p then return end progress.perks = {} for _, r in ipairs(perkRows) do progress.perks[#progress.perks + 1] = r.perk end table.sort(progress.perks) p.stored.perks = table.concat(progress.perks, ",") SetPlayerProgress(pid, progress) p.ready = true -- from here on the record is the saved character end) end)endThe perk list replaces the character’s perks. Every player is given the perfect block, the riposte and the master strikes
when they first spawn, and the list GetPlayerProgress gives holds them. A list the mode writes itself takes them away unless it
names them too. The perks a fresh character holds beyond the list are taken away only once the server has heard which perks
they hold - a few seconds after the spawn. A restore made before that adds the missing ones; a second SetPlayerProgress after
the first OnPlayerProgressChange makes the list exact.
5. Changing a character during play
Section titled “5. Changing a character during play”| Call | What it does |
|---|---|
SetPlayerStat(pid, stat, level [, xp]), SetPlayerSkill(pid, skill, level [, xp]) |
the level and the experience exactly, lowering too, no notice; the perk points stay |
AddPlayerStatXP, AddPlayerSkillXP |
experience the game’s way: the level-up notice and the perk points |
SetPlayerPerkPoints(pid, name, points) |
a stat’s or a skill’s unspent points, or the main level’s pool (mainlevel) |
AddPlayerPerk, RemovePlayerPerk, HasPlayerPerk |
a perk by ID, or by name once the game’s tables are exported; GetPerkName answers the name |
SetPlayerNourishment, SetPlayerEnergy |
how fed and how rested, up to the maximum the game reported |
A stat or skill a mode raises shows on the other players’ screens too: the character’s body there takes the same level, so the game treats it the same way. A bow’s draw, for one, holds or drops by the archer’s strength against the bow on every screen.
6. Horses
Section titled “6. Horses”A horse has strength, agility, vitality and courage of its own:
SetHorseStat(id, stat, level) sets one from 0 to 30 on every copy of the horse - its
rider’s and everyone’s who sees it - and carry capacity, speed, stamina and courage follow at once.
GetHorseStat and GetHorseStats read
them, 0 or an empty table for a horse never set (it has the game’s own). The stats live as long as the horse: a mode that
keeps its players’ horses saves them with the horse’s row and sets them again when it makes the horse anew.
The example modes
Section titled “The example modes”basicrp.lua in the server folder is the worked example: it saves each character’s stats, skills, experience, perk points,
perks, nourishment and energy (player_progress, player_perks and two columns of its players table), puts them back at the
login, and keeps the stats an admin gave a horse (horse_stats). Its PROGRESS table holds the rate (xp_rate = 1). Everyone
has /skills [name]; admins have /setlevel, /givexp, /perk, /perkpoints, /xprate and /horsestat. freeroam.lua
gives the game’s own experience, /skills and the admin commands but /horsestat, and keeps nothing; duel_arena.lua keeps
every duelist equal with a rate of 0.
What to know
Section titled “What to know”- The rate is 0 by default. A mode of your own that never calls
SetXPRatehas nobody gaining experience. [spawn] stat_levelinserver.tomlraises the four core stats at every spawn and never lowers them. A mode that keeps its players’ levels leaves it at0.- The progression is checked. The record is what the player’s game reports, and the server’s
audit judges it against what it can explain. With
[audit] mode = "enforce"a rise nothing explains is put back. A server whose story quests award experience vouches for it inOnPlayerProgressViolation. The server ships with[audit] mode = "observe", which judges and logs but puts nothing back: a value a player’s game forges would be the one the record holds and your mode saves. A mode that keeps characters should not rely on the record until the server owner runsenforce. - Perk names need the tables. Perk IDs work on every server; names (
AddPlayerPerk(pid, "Iron Grip")) need the exported tables of this version.
