99 lines
6.6 KiB
Markdown
99 lines
6.6 KiB
Markdown
# Racetimer backend (2026)
|
||
|
||
REST API for the UNLOZE racetimer, reading the `unloze_racetimer_css_2026` database.
|
||
Java 8+, Jersey (JAX-RS 2.1), deployed as `racetimer_endpoints-1.0.war` on Tomcat 8.5, the same as before.
|
||
All paths below are under `https://<backend>/racetimer_endpoints-1.0/api/`.
|
||
|
||
## Deploying
|
||
|
||
1. Build: `mvn package`. The output is `target/racetimer_endpoints-1.0.war`.
|
||
2. Update `/opt/tomcat/race_backend_settings.json`. It is the same file the old backend used, with more keys; see `race_backend_settings.example.json`.
|
||
- `racetimerURL` must now point at the **`unloze_racetimer_css_2026`** database.
|
||
- Add the `sourcebans*`, `gidStaff` and `gidAdmin` keys. They work like `SBPP_DB_*`, `GID_STAFF` and `GID_ADMIN` in the entwatchbans panel.
|
||
- Set `jwtSecret` to at least 32 random characters. Without it, admins are signed out on every Tomcat restart.
|
||
- Set `publicBackendUrl` to this backend's public URL.
|
||
- Set `frontendUrl` to the React site's URL. Steam sends admins back there after they sign in.
|
||
- Move the Steam Web API key into `steamApiKey`. The old code had it hardcoded in `Facade.java`, which shipped in the repo, so generate a new key.
|
||
3. Deploy the WAR as before. The data loads on startup and reloads every `refreshMinutes` (default 30).
|
||
|
||
The settings path can also be given with `-Dracetimer.settings=/path/file.json` or the `RACETIMER_SETTINGS` environment variable.
|
||
|
||
## Points
|
||
|
||
Points are calculated per category, from each player's **best valid** time only. Tied times share a position.
|
||
|
||
| Rule | Detail |
|
||
|---|---|
|
||
| Who gets points | Everyone while a board has fewer than 200 completions. From 200 on, only the faster half (200 → top 100, 210 → top 105). |
|
||
| Base points | n − (position − 1): the fastest gets n (the number of completions), each position below gets one less. The slower half gets 0, so on big boards points drop from about n/2 straight to 0 at the cut. |
|
||
| Bonus multiplier | Only on boards with 100+ completions. 4× at #1, sliding smoothly to 2× at the top-1% mark, then to 1× at the top-5% mark. |
|
||
| CLASSIC RACETIMER | Final points ÷ 10. |
|
||
| Invalid category | Still shown with positions, but gives 0 points. |
|
||
| Invalid record | Listed separately under `invalidated` with no position and 0 points. Only that one run is removed; the player's previous valid time counts again. |
|
||
| Servers | ZE1 and ZE2 points and ranks are separate. Categories from any other tag (e.g. `dev`) are hidden. |
|
||
|
||
The old flat +2500 bonus for small boards is gone.
|
||
|
||
`GET` responses are computed from an in-memory snapshot. Admin changes rebuild it immediately.
|
||
|
||
## In-game plugins
|
||
|
||
`racetimer_rank.sp` and `toplvl.sp` keep working unchanged. `player/{steamid}` and `leaderboard/minified/{offset}` still return `PlayerPoints` (and `name`).
|
||
Without `?server=`, both endpoints use `defaultServerTag` (ze1). On ZE2, add `?server=ze2` to those two URLs so levels come from ZE2 points.
|
||
Levels drop for CLASSIC data, as intended.
|
||
|
||
## Endpoints
|
||
|
||
`{steamid}` accepts `STEAM_0:x:y`, `STEAM_1:x:y`, `[U:1:n]` or a SteamID64.
|
||
`?server=` is `ze1` or `ze2` and defaults to ze1.
|
||
Errors are JSON: `{"statusCode": 404, "errorMessage": "..."}`.
|
||
|
||
### Public
|
||
|
||
| Method & path | Returns |
|
||
|---|---|
|
||
| `GET timers/leaderboard/{offset}?server=` | 100 players ranked by that server's points. Fields: `steamID`, `steamID64`, `name`, `Avatar`, `Rank`, `PlayerPoints`, `Times`, `UrlBanners`, `server`, `servers` (`{"ze1": {points, rank, times}, "ze2": {...}}`), `badges`. |
|
||
| `GET timers/leaderboard/minified/{offset}?server=` | `[{name, PlayerPoints}]` (for `toplvl.sp`) |
|
||
| `GET timers/player/{steamid}?server=` | One player, same fields as the leaderboard. 404 if unknown. |
|
||
| `GET timers/player/badges/{steamid}` | `{badgesUrls, badges: [{name, url}]}` |
|
||
| `GET timers/player/maps/{steamid}/{offset}?server=` | 50 rows of the player's best per category. Fields: `recordId`, `categoryId`, `mapName`, `stage`, `categoryNumber`, `serverTag`, `isLegacy`, `categoryInvalid`, `time`, `position`, `completions`, `bonusMultiplier`, `points`, `recordedAt`. Without `server`, both servers are included. |
|
||
| `GET timers/player/history/{steamid}/{offset}?categoryId=` | 50 improvements, newest first. Fields: `time`, `previousTime`, `improvedBy` (seconds), `recordedAt`, `isInvalid`, `isCurrentBest`, plus the category fields. Legacy records are last, with `recordedAt: null`. |
|
||
| `GET timers/allmaps` | `[{mapName, allCategoriesInvalid, stages: [{stage, allCategoriesInvalid, categories: [category]}]}]` |
|
||
| `GET timers/map/{mapname}` | One map in the same shape (case-insensitive). |
|
||
| `GET timers/category/{id}/{offset}` | `{category, offset, pageSize, entries: [75], invalidated: [...]}` |
|
||
| `GET timers/searchplayers/{text}?server=` | Up to 100 players, matched on name or Steam ID. |
|
||
| `GET timers/searchmaps/{text}` | Maps whose name contains the text. |
|
||
|
||
`category` fields: `id`, `categoryNumber` (the same "Category N" as in-game), `mapName`, `stage`, `serverTag`, `serverCvars`, `cvars` (`[{name, value}]`, for showing what differs between categories), `isLegacy`, `isInvalid`, `givesPoints`, `completions`, `fastestTime`.
|
||
|
||
Board `entries` fields: `recordId`, `position`, `steamID`, `steamID64`, `name`, `avatar`, `badgesUrls`, `time`, `points`, `bonusMultiplier`, `recordedAt` (Unix seconds, `null` for legacy), `isLegacy`.
|
||
|
||
### Admin sign-in (Steam)
|
||
|
||
1. The site links the admin to `GET auth/steam/login`.
|
||
2. After Steam, the backend redirects to `frontendUrl` with either:
|
||
- `#token=<token>`
|
||
- or `#loginError=not_admin`, `#loginError=steam_verification_failed`, or `#loginError=admin_check_unavailable`
|
||
3. The site sends the token as `Authorization: Bearer <token>`. The old `x-access-token` header also works.
|
||
4. `GET auth/me` returns `{steamID, steamID64, name, role, expiresAt}`.
|
||
- The role is `admin` for `gidAdmin` groups and `staff` for `gidStaff` groups.
|
||
- A missing or expired token gives 401.
|
||
|
||
### Admin actions (staff and admin)
|
||
|
||
| Method & path | Body |
|
||
|---|---|
|
||
| `PUT admin/records/{recordId}` | `{"invalid": true}` or `{"invalid": false}` |
|
||
| `PUT admin/categories/{categoryId}` | same |
|
||
|
||
The response is `{id, invalid, changedBy}`. The schema has no audit table, so every change is written to the Tomcat log with the admin's name and Steam ID.
|
||
|
||
## Removed endpoints
|
||
|
||
`timers/mapsizecache/...` and the old `timers/map/{mapname}/{stage}/{offset}` are gone. Use `category.completions` and `timers/category/{id}/{offset}`.
|
||
The old username/password `login` endpoint and the JPA entities are gone too.
|
||
|
||
## Tests
|
||
|
||
`mvn test` runs the tests for the points formula, the snapshot builder, history, Steam IDs, tokens, Steam OpenID checks, and the whole API in memory. The API tests use no database.
|