This document explains the data formats of server communication and the database.
The system works with format numbers, starting from 1 as the first version. They differentiate themselves by the organization and quantity of features that can be stored with them.
-
Format 1
- Stores:
userident,levelid,levelversion,practice,x,y,percentage - Sends for list:
x,y,percentage - Sends for analysis:
userident,levelversion,practice,x,y,percentage
- Stores:
-
Format 2
- Additionally Stores:
coins,itemdata - Additionally sends for analysis:
coins,itemdata - Currently unused by the mod, but available
- Additionally Stores:
Format 2 is accepted for submissions and treated the same as format 1 for listing, but ignored for analysis
The database contains one table per format version, in order to store the corresponding incoming deaths in the correct version table, and sending deaths combined from all formats.
| Column Name | Data Type | Constraints | Description |
|---|---|---|---|
| userident | CHAR(40) | NOT NULL | A unique identification of player & level. See § userident |
| levelid | INT | UNSIGNED NOT NULL | The id of the level. |
| levelversion | TINYINT | UNSIGNED DEFAULT 1 | Version number of the level. |
| practice | BIT | DEFAULT 0 | If the death was done in practice. |
| x | FLOAT(10,2) | NOT NULL | The x-position of the player at the time of death. |
| y | FLOAT(10,2) | NOT NULL | The y-position of the player at the time of death. |
| percentage | SMALLINT | UNSIGNED NOT NULL | For normal levels, the percentage of the player 0-100, and 101 for a level finish. For platformer levels, the time of death in seconds. |
| Column Name | Data Type | Constraints | Description |
|---|---|---|---|
| ... | ... | ... | Identical to format1, with the addition of: |
| coins | TINYINT | DEFAULT 0 | Bitfield:1 << 0 = 1st coin,1 << 1 = 2nd coin,1 << 2 = 3rd coin. |
| itemdata | DOUBLE | DEFAULT 0 | The value of item 0 at the time of death. Can be used by level creators to encode specific information about the player. |
When a player finishes the level, an entry with percentage = 100 is added. Although this is not a death in the sense of the word, it serves an analytical purpose for detecting if a given player (based on userident) has ended up beating the level, as well as discovering potential unintended secret ways.
To speed up queries for specific levels, the database also employs an index for every table:
CREATE INDEX IF NOT EXISTS format1index ON format1 (levelid);
CREATE INDEX IF NOT EXISTS format1index ON format2 (levelid);
The userident is used to group together deaths from an individual player playing a specific level. This is anonymized in order to make grouping across levels difficult. The three identification components are: Account Name, Level ID, User ID, which are arranged as [account name]_[user id]_[level id] and then hashed using the SHA-1 algorithm.
Yes SHA-1 has been cryptographically broken and is vulnerable to certain attacks, but come on, this is a fucking death collection mod. Who cares. It also has the smallest output size and storage space is limited. Unless this proves to become an actual danger, it stays.
Since the userident is stored with each death and could easily be computed to figure out a specific player's deaths (though not the other way), upon sending the deaths for analysis (when the useridents would be exposed), they are hashed again, this time with a random salt generated per analysis. As a result, the userident stays consistent at grouping together a single player's death, but nobody can identify a specific player's death.
The server accepts either account name and user id to be sent OR a complete and proper userident. Geometry Dash and Geode cannot natively hash arbitrary strings, and - in my eyes - a dependency for it is unnecessary. The server creates the userident and immediately forgets about the user's data.
- Player: RobTop (User-ID 16)
- Level: Bloodbath, ID 10565740
[account name]_[user id]_[level id]
⇒ RobTop_16_10565740
⇒ cba4a35e4ee458178b18d4c8ebb836a518b4df4b ← This is the way it is stored in the database
Upon analysis:
Select salt: e.g. ZqQhF28asA <- Different every time a level analysis is requested
⇒ cba4a35e4ee458178b18d4c8ebb836a518b4df4b_ZqQhF28asA
⇒ aed5ab073efad9fe738eacb2bebeb174b2ceae6b ← This is what is sent from /analysis
In principle, we're accessing GameManager::m_playerUserID and GameManager::m_playerName which are the user id (a separate id from your account id and assigned to logged out users as well) and account name (same as, say GJAccountManager::m_username, and if not logged in, taking the name in the icon kit).
Everyone playing logged in will be uniquely identified. Everyone else is assumed an individual player if they play on one device and do not change their name.
Base URL: https://deathmarkers.lol/
Parameter(s):
levelid: The ID of the level requested.platformer(boolean): Iffalse, ignores entries with percentage > 100. See § Table DEATHS.- Optional:
practice(boolean): Iffalse, ignores deaths that occurred in practice mode (defaulttrue) - Optional:
response: responds using specified data format, One of:csv(default),bin
Delivers (text/csv):
x,y,percentagefloat,float,int...
This endpoint is intended for a regular playthrough to display Mario Maker-style death pins and a bar graph on the percentage.
Parameter(s):
levelid: The ID of the level requested.- Optional:
response: responds using specified data format, One of:csv(default),bin
Delivers (text/csv):
userident,levelversion,practice,x,y,percentagestring,int,0 or 1,float,float,int...
See § Table DEATHS for information on each property.
This endpoint is intended for level creators to analyze the deaths in their level to improve the gameplay. It is not restricted to the actual level creator to allow anyone to learn from others' levels.
This parameter in particular is not meant for public access. It should only be used by the mod and is only documented for contributors.
Parameter(s):
levelid: The ID of the level requested.
Body Data (In JSON, i.e. MIME application/json):
format: (int): Format Version Number, for the following always 1.- Fallback:
levelid(int): The ID of the level. (Required only if equivalent parameter is not given) - Optional:
levelversion(int): Version number of the level. 0 by default. - One of:
userident(string): See § userident-
playername(string): Player Nameuserid(int): Player's User ID The hash will be calculated by the server.
The following can either be part of the root object or individual objects in an array deaths:
- Optional:
practice(bool): Whether the death occurred in practice mode.falseby default. x(float): x-positiony(float): y-positionpercentage(int): For normal levels, the percentage of the player 0-100. For platformer levels, the time of death in seconds.
Delivers: 4xx or 204 Status. 4xx responses supply a human-readable error source.
The server limits requests per IP to 2 requests per 8 seconds. (only applies to /list, /analysis and /submit). If one IP breaks this limit for 2 cycles in a row, it is blocked for one hour.
Using &response=bin on /list and /analysis yields the requested data in binary format for faster parsing and ~1/3 the transmission size. Due to the constant-size data types of death objects, the structure of binary responses is simple:
Example CSV response for comparison:userident,levelversion,practice,x,y,percentage472457f3142e4a5c17c6d37e60297c5c99e61382,2,1,5296.254532601192,2415.0162601709135,24
xandyare encoded using binary32 (IEEE 754) (aka. float) into 4 bytes in little endian.percentageis a little endian 2-byte/16-bit integer.- The very first byte of the response is a versioning byte for future compatibility, deaths only start after.
v1.4.0 is the first version to replace a set of settings with a new way to control the same stuff. To preserve the player's chosen behaviour, the old settings have to be ported to the new settings scheme. Below is an overview of the steps taken in each "settings version" (which is stored in the mod's saved values as offered by Geode). This translation is done in the $execute directive at the end of main.cpp.
Changed: Use local deaths (boolean) -> Use local death (enum)
| Old Value | New Value |
|---|---|
false |
"Never" |
| "Demons only" | |
true |
"Always |
Changed: Always show Markers & Markers in Practice -> When to draw in normal mode & "-" practice mode
| Always show Markers | Markers in Practice | When to draw in normal mode | "-" practice mode |
|---|---|---|---|
false |
false |
"On Death" | "Never" |
false |
true |
"On Death" | "On Death" |
true |
false |
"Always" | "Never" |
true |
true |
"Always" | "Always" |
Local deaths are stored in the mod's data folder (%localappdata%/GeometryDash/geode/mods/freakyrobot.deathmarkers) as files named the ID of the level it belongs to.
They are formatted similar to CSV, storing a specific order of data points about the level. However, the file contains no header line and lines may contain a different number of values.
Lines may contain 2 or 3 (for regular markers) or 5 or 6 (for ghost markers) values. 3 and 6 contain an extra value at the end for percentage, only if the level is a normal mode level.
Values on regular markers:
- x-Position
- y-Position
- (Percentage)
Values on ghost markers:
- x-Position
- y-Position
- Rotation
- Gamemode (as internal value of the enum IconType)
- boolean metadata bitfield:
- Bit 0 (value 1): is second player
- Bit 1 (value 2): is mini
- Bit 2 (value 4): gravity is flipped
- Bit 3 (value 8): gameplay is mirrored
- (Percentage)
