Database: Difference between revisions
DrinkyBird (talk | contribs) No edit summary |
DrinkyBird (talk | contribs) (Add back to ACS functions category) Tag: Source edit |
||
(6 intermediate revisions by 2 users not shown) | |||
Line 54: | Line 54: | ||
|- | |- | ||
| [[GetDBEntryRank]] | | [[GetDBEntryRank]] | ||
| string ''namespace'', string ''key'', bool '' | | string ''namespace'', string ''key'', bool ''descending'' | ||
| int ''position'' | | int ''position'' | ||
| Sort a namespace by value and return the position of the given key in that namespace. | | Sort a namespace by value and return the position of the given key in that namespace. | ||
Line 64: | Line 64: | ||
|- | |- | ||
| [[SortDBEntries]] | | [[SortDBEntries]] | ||
| string ''namespace'', int ''limit'', int ''offset'', bool '' | | string ''namespace'', int ''limit'', int ''offset'', bool ''descending'' | ||
| resource ''results'' | | resource ''results'' | ||
| Return a resource that contains a pointer to rows in the namespace sorted by value. | | Return a resource that contains a pointer to rows in the namespace sorted by value. <br>''Limit'' is the maximum number of rows to return.<br>''Offset'' is the starting position to return in the sorted table (a value of 1 will cause the returned database to start at the second highest value). | ||
|- | |- | ||
| [[CountDBResults]] | | [[CountDBResults]] | ||
Line 128: | Line 128: | ||
== Examples == | == Examples == | ||
Lets say we want to make a script that keeps track of player deaths. | |||
[[Category: | The following script increments (or if it doesn't exist, creates) a namespace with the player's authentication account name and initializes a section called "Deaths". Whenever a player dies, if they are logged in, their death will be recorded. | ||
One namespace can store multiple sections, so you could use the same player name to store "Deaths" and "Frags". The namespace does not need to be the player's authentication name, you could use their ingame name instead. This is a little bit unsafe, however, as it might cause problems when players share the same names. | |||
<syntaxhighlight lang="c" line="1"> | |||
script "IncrementPlayerDeaths" DEATH | |||
{ | |||
if (PlayerIsLoggedIn(PlayerNumber())) | |||
IncrementDBEntry(GetPlayerAccountName(PlayerNumber()), "Deaths", 1); | |||
} | |||
</syntaxhighlight> | |||
However, instead of incrementing this value every time a player dies, you can do this before the map changes with a transaction. This is benefitial for performance on large databases with many players. Be aware that if a player leaves the game before the map change, their data will not be saved! | |||
<syntaxhighlight lang="c" line="1"> | |||
#define MAXPLAYERS 64 | |||
int global_playerdeaths[MAXPLAYERS]; | |||
script "IncrementPlayerDeaths" DEATH | |||
{ | |||
global_playerdeaths[PlayerNumber()]++; | |||
} | |||
script "StorePlayerDeaths" UNLOADING | |||
{ | |||
BeginDBTransaction(); | |||
for (int i=0; i<MAXPLAYERS; i++) | |||
if (PlayerIsLoggedIn(i)) | |||
IncrementDBEntry(GetPlayerAccountName(i)), "Deaths", global_playerdeaths[i]); | |||
EndDBTransaction(); | |||
} | |||
</syntaxhighlight> | |||
And the following script sorts the database and prints the name of the player with the most deaths, as well as how many times they died | |||
<syntaxhighlight lang="c" line="1"> | |||
script "ShowMostDeaths" (void) | |||
{ | |||
int result = GetDBEntries("Deaths"); | |||
int count = CountDBResults(result); | |||
// Make sure the database has values | |||
if (count != 0) | |||
{ | |||
// Free the memory allocated by the first GetDBEntries call | |||
FreeDBResults(result); | |||
// Get the list of deaths, sort it, and then print who died the most | |||
result = SortDBEntries("Deaths", count, 0, true); | |||
PrintBold(s:"Player ", s:GetDBResultKeyString(result, 0), s:" has the most deaths, having died ", d:GetDBResultValue(result, 0), " time(s)."); | |||
} | |||
// Free the memory allocated by SortDBEntries, or by CountDBResults if it returned zero. | |||
FreeDBResults(result); | |||
} | |||
</syntaxhighlight> | |||
== See also == | |||
* [[Accounts]] | |||
* Account-related functions: [[PlayerIsLoggedIn]], [[GetPlayerAccountName]] | |||
* [https://zandronum.com/forum/viewtopic.php?f=94&t=6993 Databases, accounts, and you. Part 1: basic tutorial.] | |||
[[Category:Modding]] | |||
[[Category:ACS]] | |||
[[Category:ACS functions]] |
Latest revision as of 16:53, 2 July 2022
This article documents a Zandronum-specific ACS feature which may not be supported by ZDoom and its other child ports. |
Zandronum has database support, allowing you to persist data across server restarts.
Note: You need to have port 10666 forwarded to test this online.
Database Functions
Data retrieval and saving
Function | Parameters | Return | Description |
---|---|---|---|
GetDBEntry | string namespace, string key | int value | Retrieve a numeric value out of the database. |
SetDBEntry | string namespace, string key, int value | void | Set a database row to a specific numeric value. |
GetDBEntryString | string namespace, string key | string value | Retrieve a string value out of the database. |
SetDBEntryString | string namespace, string key, string value | void | Set a database row to a specific string value. |
IncrementDBEntry | string namespace, string key, int value | void | Increment a database row by a specific number. |
Reading and writing data to a database is done through an easy-to-use namespace/key/value system. If you put data into the database with a specific namespace and key, you can retrieve that same data by supplying the same namespace and key later.
Namespaces are intended to be roughly analogous to "tables" in other databases, used to split up different sets of data by concern. Namespaces can also be iterated through and sorted.
Iteration and Sorting
Function | Parameters | Return | Description |
---|---|---|---|
GetDBEntryRank | string namespace, string key, bool descending | int position | Sort a namespace by value and return the position of the given key in that namespace. |
GetDBEntries | string namespace | resource results | Return a resource that contains a pointer to all rows in a namespace. |
SortDBEntries | string namespace, int limit, int offset, bool descending | resource results | Return a resource that contains a pointer to rows in the namespace sorted by value. Limit is the maximum number of rows to return. Offset is the starting position to return in the sorted table (a value of 1 will cause the returned database to start at the second highest value). |
CountDBResults | resource results | int count | Count the number of rows a resource contains. |
GetDBResultKeyString | resource results, int row | string key | Returns the key at the specific row of a resource. |
GetDBResultValueString | resource results, int row | string value | Returns the value at the specific row of a resource as a string. |
GetDBResultValue | resource results, int row | int value | Returns the value at the specific row of a resource as an integer. |
FreeDBResults | resource results | void | Free a resource from memory. |
Most of these functions deal with a resource. A resource is simply a number that points to results of a database query. The queries that Zandronum allows you to make is either retrieving all rows in a namespace at once, or retrieving a limited subset of them in a sorted fashion. Once you have a query result as a resource, you then use functions to figure out what exactly that result contains.
Transactions
Function | Parameters | Return | Description |
---|---|---|---|
BeginDBTransaction | void | void | Begin a database transaction. |
EndDBTransaction | void | void | Commit the database transaction. |
A transaction prevents functions that normally write to the database from writing immediately. Instead, they are kept in memory until you end the transaction, at which point it writes everything at once. If you need to write lots of rows to the database, writing them all at once in a transaction is much faster than writing them one by one. Transactions are also guaranteed to be atomic, which means that if Zandronum crashes during the transaction, either everything in a transaction is written to the database or nothing gets written - the database will never be left in a state where only half the rows were written.
A word of warning
Using any ACS database function stops the entire gamesim while it runs the database query. This is normally not much of an issue, but if you start to see performance issues with your modification, looking at how many database queries you're running is a good place to start. If your database queries are causing performance issues, try batching your queries up and running them all at once with a transaction.
Remote Database support
Zandronum does not support connecting to a remote database server over a network. Due to limitations of ACS, database functions lock up the entire game simulation until a result is found - such a scheme would be unworkable over a something as fickle and with as much latency as the Internet.
However, if you know a little bit of non-ACS programming or scripting, you can connect to the on-disk database with any language that supports SQLite3 and do whatever you want with it, including sending it someplace else over a network. If this sounds like fun, but you have no programming experience aside from ACS, this author recommends Python since it works on Windows, Linux and OSX and comes with sqlite3 support out of the box.
Examples
Lets say we want to make a script that keeps track of player deaths.
The following script increments (or if it doesn't exist, creates) a namespace with the player's authentication account name and initializes a section called "Deaths". Whenever a player dies, if they are logged in, their death will be recorded. One namespace can store multiple sections, so you could use the same player name to store "Deaths" and "Frags". The namespace does not need to be the player's authentication name, you could use their ingame name instead. This is a little bit unsafe, however, as it might cause problems when players share the same names.
script "IncrementPlayerDeaths" DEATH
{
if (PlayerIsLoggedIn(PlayerNumber()))
IncrementDBEntry(GetPlayerAccountName(PlayerNumber()), "Deaths", 1);
}
However, instead of incrementing this value every time a player dies, you can do this before the map changes with a transaction. This is benefitial for performance on large databases with many players. Be aware that if a player leaves the game before the map change, their data will not be saved!
#define MAXPLAYERS 64
int global_playerdeaths[MAXPLAYERS];
script "IncrementPlayerDeaths" DEATH
{
global_playerdeaths[PlayerNumber()]++;
}
script "StorePlayerDeaths" UNLOADING
{
BeginDBTransaction();
for (int i=0; i<MAXPLAYERS; i++)
if (PlayerIsLoggedIn(i))
IncrementDBEntry(GetPlayerAccountName(i)), "Deaths", global_playerdeaths[i]);
EndDBTransaction();
}
And the following script sorts the database and prints the name of the player with the most deaths, as well as how many times they died
script "ShowMostDeaths" (void)
{
int result = GetDBEntries("Deaths");
int count = CountDBResults(result);
// Make sure the database has values
if (count != 0)
{
// Free the memory allocated by the first GetDBEntries call
FreeDBResults(result);
// Get the list of deaths, sort it, and then print who died the most
result = SortDBEntries("Deaths", count, 0, true);
PrintBold(s:"Player ", s:GetDBResultKeyString(result, 0), s:" has the most deaths, having died ", d:GetDBResultValue(result, 0), " time(s).");
}
// Free the memory allocated by SortDBEntries, or by CountDBResults if it returned zero.
FreeDBResults(result);
}
See also
- Accounts
- Account-related functions: PlayerIsLoggedIn, GetPlayerAccountName
- Databases, accounts, and you. Part 1: basic tutorial.