Other ScriptCore 3 scripts on the same server can read ZitroStats data - points, ranks, kills, ratings and more - by calling functions of the ZitroStats library. The values come from the statistics ZitroStats keeps in memory, so no files are read.
The API was tested from a second script on Soldat Server 2.8.2 (Windows).
- ZitroStats 5.2 is installed and running; its
main.passtarts the library. - The server allows libraries for scripts:
server.ini, section[ScriptCore3],AllowDlls=1. - Your script's
config.inicontainsSandboxed = 0andAllowDlls = 1.
Copy these declarations into your script. ScriptCore 3 expects 'FunctionName@path cdecl', with the path relative to the server folder and without spaces.
function ZS_Is_Ready(): Integer;
external {$IFDEF WIN32} 'ZS_Is_Ready@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_Is_Ready@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_Get_Api_Version(): Integer;
external {$IFDEF WIN32} 'ZS_Get_Api_Version@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_Get_Api_Version@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Player_Stat(SlotID, StatID: Integer): Double;
external {$IFDEF WIN32} 'ZS_API_Get_Player_Stat@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Player_Stat@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Player_Name(SlotID: Integer): PChar;
external {$IFDEF WIN32} 'ZS_API_Get_Player_Name@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Player_Name@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Player_Rank(SlotID: Integer): Integer;
external {$IFDEF WIN32} 'ZS_API_Get_Player_Rank@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Player_Rank@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Player_HUD(SlotID: Integer): Integer;
external {$IFDEF WIN32} 'ZS_API_Get_Player_HUD@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Player_HUD@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Profile_Stat(ProfileID, StatID: Integer): Double;
external {$IFDEF WIN32} 'ZS_API_Get_Profile_Stat@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Profile_Stat@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Profile_Count(): Integer;
external {$IFDEF WIN32} 'ZS_API_Get_Profile_Count@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Profile_Count@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Top_Player(Rank: Integer): PChar;
external {$IFDEF WIN32} 'ZS_API_Get_Top_Player@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Top_Player@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};
function ZS_API_Get_Server_Peak(): Integer;
external {$IFDEF WIN32} 'ZS_API_Get_Server_Peak@scripts/ZitroStats-Core/zitrostats_dll.dll cdecl' {$ELSE} 'ZS_API_Get_Server_Peak@scripts/ZitroStats-Core/zitrostats_dll.so cdecl' {$ENDIF};Use exactly the library path ZitroStats uses. A copy of the library in another folder is a separate instance without any data and returns only zeros.
| Function | Result |
|---|---|
ZS_Is_Ready(): Integer |
1 when ZitroStats has loaded its data, otherwise 0 |
ZS_Get_Api_Version(): Integer |
14 for ZitroStats 5.2 |
ZS_API_Get_Player_Stat(SlotID, StatID): Double |
A statistic of the account used by the player in slot 1-32 |
ZS_API_Get_Player_Name(SlotID): PChar |
Name of the player in the slot |
ZS_API_Get_Player_Rank(SlotID): Integer |
Position of the player's account in the points ranking (1 = best) |
ZS_API_Get_Player_HUD(SlotID): Integer |
1 when the player has the ZitroStats HUD switched on (!hud) and the server shows it ([HUD] Enabled) |
ZS_API_Get_Profile_Stat(ProfileID, StatID): Double |
A statistic of the account with number ProfileID (#id), online or not |
ZS_API_Get_Profile_Count(): Integer |
Number of accounts |
ZS_API_Get_Top_Player(Rank): PChar |
Name of the account at this position of the points ranking |
ZS_API_Top_Player_Copy(Rank, Buf, Size): Integer |
For another library on a thread of its own, not for scripts (it waits for the statistics): name, tab and SteamID of the account at this ranking position, copied into Buf (Size bytes with the closing zero); the result is the length, 0 = nobody. AntiFake uses it for /visitsi <position> and the other i forms |
ZS_API_Get_Server_Peak(): Integer |
Highest number of players online at the same time |
When there is no value, numeric functions return 0 and text functions return an empty string. That happens:
- before
ZS_Is_Readyreturns 1; - for an empty slot, or a player who has no account assigned yet;
- for an unknown account number or ranking position;
- for an unknown StatID.
The account number of an online player is ZS_API_Get_Player_Stat(SlotID, 37).
| StatID | Value | Unit / notes |
|---|---|---|
| 0 | Points | ranking points |
| 1 | Kills | |
| 2 | Deaths | |
| 3 | K/D | kills / deaths; kills when there are no deaths |
| 4 | Rating | the performance rating shown in !stats and !top, calculated for the current game mode |
| 5 | Rank | position in the points ranking, 0 = not ranked |
| 6 | Time played | seconds |
| 7 | Headshot kills | all weapons |
| 8 | Accuracy | percent, 0-100 |
| 9 | Flag captures | |
| 10 | Assists | |
| 11 | Clutches won | Survival rounds won after being the team's last player alive against at least two enemies (team modes only; free-for-all Survival counts no clutches) |
| 12 | Best kill streak | |
| 13 | Damage dealt | hit points |
| 14 | Damage received | hit points |
| 15 | Prone kills | |
| 16 | Jetpack time | seconds |
| 17 | Camp kills | kills after staying within [Mechanics] CampRadius for CampSeconds |
| 18 | Bink recovery kills | kills of a player who hit the killer within [Mechanics] BinkSeconds and did at least BinkDamage to the killer in the killer's life |
| 19 | Killing sprees | times a streak reached 5 kills |
| 20 | Rampages | times a streak reached 10 kills |
| 21 | Dominating | times a streak reached 15 kills |
| 22 | Unstoppable | times a streak reached 20 kills |
| 23 | Godlike | times a streak reached 25 kills |
| 24 | Shutdowns | kills of players who had a streak of 5 or more |
| 25 | Finisher kills | kills with USSOCOM, Desert Eagles, knife (also thrown), chainsaw, LAW, flamer or fists within [Mechanics] FinisherSeconds of the killer's last hit on the victim with MP5, AK-74, AUG, Spas-12, Ruger, M79, Barrett, Minimi or Minigun |
| 26 | Fastest capture | seconds from the capper's last grab; 0 = no timed capture (a capture under 0.5 s is not timed) |
| 27 | Escort kills | team modes: kills near a living human teammate who carries a flag ([Mechanics] EscortRadius) |
| 28 | Flag juggles | grabs within [Mechanics] JuggleSeconds after dropping or throwing the flag |
| 29 | Contested holds | Hold the Flag only ([Mechanics] ContestedHoldSeconds) |
| 30 | Longest kill | meters |
| 31 | Wins | matches |
| 32 | Losses | matches |
| 33 | Protection days | |
| 34 | Season points | points won or lost in the current season |
| 35 | Glicko-2 value | rating - ConservativeFactor x deviation, for the family (FFA, Team, CTF) of the current game mode; calculated whatever RankingSystem is selected |
| 36 | OpenSkill value | mu - 3 x sigma, for the family of the current game mode; calculated whatever RankingSystem is selected |
| 37 | Account number | the #id used by commands |
| 38 | Registered | 1 = the account is protected with a password |
| 39 | MMR | Heroes of the Storm style match rating for the family of the current game mode (the start value, 1500 by default, before the first rated match); calculated whatever RankingSystem is selected |
Constants to copy:
const
ZS_STAT_POINTS = 0; ZS_STAT_KILLS = 1; ZS_STAT_DEATHS = 2;
ZS_STAT_KD = 3; ZS_STAT_RATING = 4; ZS_STAT_RANK = 5;
ZS_STAT_TIME_PLAYED = 6; ZS_STAT_HEADSHOTS = 7; ZS_STAT_ACCURACY = 8;
ZS_STAT_FLAGS = 9; ZS_STAT_ASSISTS = 10; ZS_STAT_CLUTCHES = 11;
ZS_STAT_MAX_STREAK = 12; ZS_STAT_DMG_DEALT = 13; ZS_STAT_DMG_RECEIVED = 14;
ZS_STAT_PRONE_KILLS = 15; ZS_STAT_JET_TIME = 16; ZS_STAT_CAMP_KILLS = 17;
ZS_STAT_BINK_KILLS = 18; ZS_STAT_SPREES = 19; ZS_STAT_RAMPAGES = 20;
ZS_STAT_DOMINATING = 21; ZS_STAT_UNSTOPPABLE = 22; ZS_STAT_GODLIKE = 23;
ZS_STAT_SHUTDOWNS = 24; ZS_STAT_FINISHERS = 25; ZS_STAT_FASTEST_CAP = 26;
ZS_STAT_ESCORT_KILLS = 27; ZS_STAT_FLAG_JUGGLES = 28; ZS_STAT_CONTESTED = 29;
ZS_STAT_LONGEST_KILL = 30; ZS_STAT_WINS = 31; ZS_STAT_LOSSES = 32;
ZS_STAT_PROTECT_DAYS = 33; ZS_STAT_SEASON_POINTS = 34; ZS_STAT_GLICKO = 35;
ZS_STAT_OPENSKILL = 36; ZS_STAT_ACCOUNT_ID = 37; ZS_STAT_REGISTERED = 38;
ZS_STAT_MMR = 39;The examples use the declarations and constants above.
A joining player may not have an account assigned yet (for example while the Steam login is checked), so the example waits for the first respawn that returns a rank.
var
Announced: array[1..32] of Boolean;
procedure OnRespawn(Player: TActivePlayer);
var
Rank: Integer;
begin
if Announced[Player.ID] or (ZS_Is_Ready() = 0) then
Exit;
Rank := ZS_API_Get_Player_Rank(Player.ID);
if Rank = 0 then
Exit;
Announced[Player.ID] := True;
Players.WriteConsole(Player.Name + ' is ranked #' + IntToStr(Rank) + ' with ' +
IntToStr(Round(ZS_API_Get_Player_Stat(Player.ID, ZS_STAT_POINTS))) + ' points.', $FFAA00);
end;
procedure OnLeave(Player: TActivePlayer; Kicked: Boolean);
begin
Announced[Player.ID] := False;
end;
var
i: Integer;
begin
for i := 1 to 32 do
Players[i].OnAfterRespawn := @OnRespawn;
Game.OnLeave := @OnLeave;
end.function TeamPoints(Team: Integer): Integer;
var
i: Integer;
begin
Result := 0;
for i := 1 to 32 do
if Players[i].Active and (Players[i].Team = Team) then
Result := Result + Round(ZS_API_Get_Player_Stat(i, ZS_STAT_POINTS));
end;
var
NextReport: Integer;
procedure OnTick(Ticks: Integer);
begin
if (Ticks < NextReport) or (ZS_Is_Ready() = 0) then
Exit;
NextReport := Ticks + 60 * 120; // every two minutes
Players.WriteConsole('Team points - Alpha: ' + IntToStr(TeamPoints(1)) +
', Bravo: ' + IntToStr(TeamPoints(2)), $AAAAFF);
end;
begin
Game.OnClockTick := @OnTick;
end.ZitroStats handles only its own commands, so a script can react to other chat text, here !best.
procedure OnSpeak(Player: TActivePlayer; Text: string);
var
i: Integer;
Name: string;
begin
if (Text <> '!best') or (ZS_Is_Ready() = 0) then
Exit;
for i := 1 to 3 do
begin
Name := ZS_API_Get_Top_Player(i);
if Name <> '' then
Player.WriteConsole('#' + IntToStr(i) + ' ' + Name, $FFFFFF);
end;
end;
var
i: Integer;
begin
for i := 1 to 32 do
Players[i].OnSpeak := @OnSpeak;
end.Kills := Round(ZS_API_Get_Profile_Stat(42, ZS_STAT_KILLS)); // account #42- Timing. ZitroStats applies game events on its own thread a moment after they happen. Inside your own
OnKillhandler, the kill may not be counted yet. - Locking. Each call copies one value under a short lock. It never waits: when the engine is busy the call returns the value it read last for the same arguments (0 or an empty string when it has none), so the game tick is not held up.
/zs_perfshows how long the engine takes per event. Read values when you need them or cache them per second, rather than reading every statistic of every slot on every tick. - Strings. A returned
PCharpoints to a buffer inside the library that the next text call replaces. Assign it to astringstraight away and never free it. Names use the server's code page. - Numbers. Counters are whole numbers returned as
Double; useRoundorFormatFloatto display them. - Reserved functions. Only ZitroStats' own
main.pasmay call the other exports:ZS_Init,ZS_Shutdown,ZS_Claim_Token,ZS_Reload_Config,ZS_Ev_*(includingZS_Ev_Ray,ZS_Ev_Move_ResultandZS_Ev_Integrity),ZS_Team_Join,ZS_CommandandZS_Action_*. Calling them from another script adds false statistics or takes output away from ZitroStats. The admin flag ofZS_Ev_JoinandZS_Commandand every console command need the token thatZS_Claim_Tokenhands out once, so another script cannot run admin commands through the library.
These are the reserved calls of the Notes above. They are described so that a script of your own does not get in their way.
- The reserved damage event carries the health the victim had before the hit, so damage statistics never count more than the victim could lose.
main.pastells the library withMap.RayCastwhether the killer still had a clear line to the victim (reserved kill event). The library can also askmain.pasabout any line: it queues the reserved actionACT_RAYCAST(7;ZS_Action_Int1 = request id, 2-3 = start, 6-7 = end) andmain.pasanswers with the reservedZS_Ev_Ray(RequestId, Answer, Tick)(1 clear, 0 blocked, -1 not calibrated). ZitroStats uses it for "Cover (shot)", the line from where the lethal shot was fired to where it hit.- The team balance (
[Balance]insettings.ini) works through two reserved calls.main.passets aGame.Teams[n].OnBeforeJoinhandler for every team and asksZS_Team_Join(Slot, Team, OldTeam, Tick)which team a joining player should enter (-1 keeps a player in their team; a new player,OldTeam255, is never refused). The library moves players with the reserved actionACT_MOVE(8;ZS_Action_Int1 = team, 2 = 1 when only a dead player may be moved) andmain.pasanswers withZS_Ev_Move_Result(Slot, Team, Result, Tick)(1 moved, 2 still alive, 3 carries a flag, 4 refused, 5 already in the team). A script of your own that moves players withChangeTeamgoes through the same handlers, so the balance may place those players elsewhere; turn[Balance] AssignJoinsoff if your script decides the teams. AChangeTeamto the player's own team - a respawn, the only kindScriptCoreitself has - is let through bymain.pasuntouched and never reachesZS_Team_Join. - The score guard (
[Integrity]) works inmain.pas: it sets team scores the server gave for a join, a team change, a double capture or a match without opponents back, puts players a map change left dead back on their feet and keeps/pausefrom throwing a running map change away. It tells the library with the reserved callZS_Ev_Integrity, and the library sends it its settings with the reserved actionACT_INTEGRITY(9). A script of your own that changes team scores may see them set back: turn[Integrity] Enabledoff if your script keeps the score. main.pasreports every change of the ammunition in hand with the reservedZS_Ev_Ammo(the reload key only in the tick a magazine runs empty), and the ammunition of both weapons with the reserved weapon change; the library tells shots from reloads (README, Reloads). A script of your own that takes ammunition away from a player (Primary.Ammo,ForceWeapon) may be counted as shots or as a reload; ammunition a script adds is not counted.main.pasadds up the healing of human players and sends it once a second and when the player leaves (nothing when there was no healing), with the reservedZS_Ev_Heal(slot, kits that healed, tick, health from regenerating polygons, health from kits): a regenerating polygon reports as damage below zero, whichmain.pasturns into health up to the full health (150, or 65 in realistic mode); a medical, berserker or predator kit fills the health beforeOnKitPickupruns, and the difference to the health of the previous tick is what it healed.- The map vote (
[MapVote]insettings.ini) works through the reservedZS_Ev_Vote(Slot, Kind, Target, Tick)and four reserved actions.main.pasreports votes withZS_Ev_Vote: kind 1 a map vote started (OnVoteMapStart), 2 a kick vote started (OnVoteKickStart; not counted), 3 a vote cast for the map vote that shows (OnVoteMap: the vote menu or/votemap). With[MapVote] Enabled,OnVoteMapStartreturnsTrue- so Soldat's own vote never starts - and the library starts its own vote for that map instead; with it off these handlers only report, a map vote another script starts withGame.StartVoteMap(which never passesOnVoteMapStart) is counted from its first vote cast, and a vote a script counts entirely on its own (chat commands such as!vthat never touch Soldat's vote) is not seen at all, only the map change it makes. The library then drivesmain.paswithACT_VOTE_START(10; text = the map:main.paskeepsGame.VotePercent, sets it to 200 and callsGame.StartVoteMap, so the clients see the vote and F12 works while Soldat itself never lets it pass),ACT_VOTE_END(11; the percent goes back once Soldat's own vote dialog - always 20 seconds on the clients, whatever[MapVote] VoteSecondssays - has run out;Int 1= the percent to use when the one from before the vote is not known, 0 = leave it) andACT_MAP_CHANGE(12; text = the map;Int 1= 1 when it isGame.NextMap, soMap.NextMapmoves the maps list on, otherwiseMap.SetMap).ACT_MAPVOTE(13) carries the settings the vote needs:Int 1=MV_*flags (MV_ENABLED= 1, the vote is on) andInt 2=[MapVote] RestoreVotePercent. The reservedZS_Ev_Map_List(Count, CurrentId, Reason, Tick, Names)sends the server's maps list the same way, at the start, after a map change, after/addmap,/delmapor/loadlist, and when a check once a second sees another list (Reason1-4 in that order:MAPLIST_START,MAPLIST_MAPCHANGE,MAPLIST_ADMIN,MAPLIST_CHANGED). - A script that is loaded again (a recompile, or a change of the game mode, which reloads every script) may claim the admin token again; the library notices the new
ZS_Initand hands it out once more. - StatID 4 ("Rating") follows
[Ranking] RatingMode; the defaultskillis a value of roughly 0 to 100 that does not depend on the points. - Points, Rating, MMR, Glicko-2 and OpenSkill are all kept for every account;
[Ranking] RankingSystemonly decides which one the ranking and!topskilluse.
- The function names and StatIDs 0-30 are the same; StatIDs 31-39,
ZS_Is_Ready,ZS_Get_Api_VersionandZS_API_Get_Player_HUDdid not exist in 4.5. - The library is loaded from
scripts/ZitroStats-Core/, not from the server folder. - The 4.5 document used declarations (
cdecl; external 'lib') that ScriptCore 3 does not accept; use the declarations above. - Some statistics are measured differently, for example the kill distance; see README.md.