Skip to content

Latest commit

 

History

History
219 lines (149 loc) · 10.8 KB

File metadata and controls

219 lines (149 loc) · 10.8 KB

DeathMarkers Documentation

This document explains the data formats of server communication and the database.

Formats

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
  • Format 2

    • Additionally Stores: coins, itemdata
    • Additionally sends for analysis: coins, itemdata
    • Currently unused by the mod, but available

Format 2 is accepted for submissions and treated the same as format 1 for listing, but ignored for analysis

Database

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.

Table format1

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.

Table format2

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.

Indexes

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);

userident

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.

Example

  • 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

But what about people playing without an account?

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.

API

Base URL: https://deathmarkers.lol/

GET /list

Parameter(s):

  • levelid: The ID of the level requested.
  • platformer (boolean): If false, ignores entries with percentage > 100. See § Table DEATHS.
  • Optional: practice (boolean): If false, ignores deaths that occurred in practice mode (default true)
  • Optional: response: responds using specified data format, One of: csv (default), bin

Delivers (text/csv):

x,y,percentage float,float,int ...

This endpoint is intended for a regular playthrough to display Mario Maker-style death pins and a bar graph on the percentage.

GET /analysis

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,percentage string,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.

POST /submit

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:
    1. userident (string): See § userident
      • playername (string): Player Name
      • userid (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. false by default.
  • x (float): x-position
  • y (float): y-position
  • percentage (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.

Rate Limits

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.

Binary transmission

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,percentage
472457f3142e4a5c17c6d37e60297c5c99e61382,2,1,5296.254532601192,2415.0162601709135,24

Binary Arrangement Breakdown

  • x and y are encoded using binary32 (IEEE 754) (aka. float) into 4 bytes in little endian.
  • percentage is 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.

Upgrading Settings

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.

Settings Version 0 -> 1

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

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)