Merge pull request #6 from zkldi/develop

Update Everything.
This commit is contained in:
zkldi
2021-06-22 15:50:18 +01:00
committed by GitHub
36 changed files with 3666 additions and 42 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
/build
.vscode
.vscode
site
+3 -1
View File
@@ -1,6 +1,6 @@
# Internal Authentication
*****
These endpoints relate to internal authentication methods. Read the warning below.
!!! danger
This is **NOT** for external use. You should **NEVER**
@@ -11,6 +11,8 @@
This is documented for completeness' sake.
*****
## Login with username and password.
```POST /api/v1/login```
+2 -2
View File
@@ -57,7 +57,7 @@ scoreData=<file data>
#### Response
```json
```js
{
"importType": "file/eamusement-iidx-csv",
"idStrings": [
@@ -80,6 +80,6 @@ scoreData=<file data>
"classDeltas": [],
"goalInfo": [],
"milestoneInfo": [],
"userIntent": true
"userIntent": false, // if X-User-Intent was set, this would be true.
}
```
+3
View File
@@ -1,5 +1,8 @@
# Status Checks
These endpoints are generally for programmers checking their code works.
They can also be used to check the status of the server.
*****
## Check server status.
+529
View File
@@ -0,0 +1,529 @@
# Individual User on Specific Game
This endpoints are for specific users information on specific game + playtype combinations.
*****
## Get information about a user's plays on a game + playtype.
`GET /api/v1/users/:userID/games/:game/:playtype`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `gameStats` | UserGameStatsDocument | The User's GameStats for this game + playtype. |
| `firstScore` | ScoreDocument or Null | The user's first score for this game + playtype. This is null if the user has no scores with timestamps. |
| `mostRecentScore` | ScoreDocument or Null | The user's most recent score. This is null if the user has no scores with timestamps. |
| `totalScores` | integer | The total amount of scores this user has. |
| `rankingData` | { ranking: integer, outOf: integer } | The position of this player on the default leaderboards for this game, and how many players it is out of. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP
```
#### Response
```js
{
gameStats: {
userID: 1,
game: "iidx",
playtype: "SP",
ratings: {
ktRating: 15
},
classes: {
dan: 14
}
},
firstScore: null,
mostRecentScore: null,
totalScores: 5,
rankingData: {
ranking: 3,
outOf: 18
}
}
```
*****
## Retrieve a user's goals for this game + playtype.
`GET /api/v1/users/:userID/games/:game/:playtype/goals`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `unachieved` (Optional) | Presence | If present, only unachieved goals are returned. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `userGoals` | Array&lt;UserGoalDocument&gt; | The array of user-subscriptions to goals this user has. |
| `goals` | Array&lt;GoalDocument&gt; | The array of goal documents this user has a subscription to. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/goals
```
#### Response
```js
{
userGoals: [
{
userID: 1,
goalID: "foobar",
progress: 4,
progressHuman: "CLEAR",
outOf: 5,
outOfHuman: "HARD CLEAR",
// ... so on
}
],
goals: [
{
goalID: "foobar",
title: "Hard Clear 5.1.1. SP ANOTHER",
// ... so on
}
]
}
```
*****
## Retrieve a user's milestones for this game + playtype.
`GET /api/v1/users/:userID/games/:game/:playtype/milestones`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `unachieved` (Optional) | Presence | If present, only unachieved milestones are returned. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `userMilestones` | Array&lt;UserMilestoneDocument&gt; | The array of user-subscriptions to milestones this user has. |
| `milestones` | Array&lt;MilestoneDocument&gt; | The array of milestone documents this user has a subscription to. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/milestones
```
#### Response
```js
{
userMilestones: [
{
userID: 1,
milestoneID: "foobar",
// ... so on
}
],
milestones: [
{
milestoneID: "foobar",
title: "IIDX SP 9th Dan Milestone",
// ... so on
}
]
}
```
*****
## Search a user's personal bests.
`GET /api/v1/users/:userID/games/:game/:playtype/pbs`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `search` | String | Limits the returned scores to those where the corresponding song is most similar to this query. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `songs` | Array&lt;SongDocument&gt; | The array of songs this search returned. |
| `charts` | Array&lt;ChartDocument&gt; | The array of charts this search returned. |
| `pbs` | Array&lt;PBDocument&gt; | The array of personal bests this search returned. This is limited to 30 returns. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/pbs?search=Verfl
```
#### Response
```js
{
pbs: [{
userID: 1,
scoreData: {
score: 123,
// ...
}
}],
songs: [{
title: "Verflucht",
// ...
}],
charts: [{
songID: 123,
playtype: "SP",
difficulty: "ANOTHER",
// ...
}, {
songID: 123,
playtype: "SP",
difficulty: "HYPER",
}]
}
```
## Get a user's best 100 personal bests.
`GET /api/v1/users/:userID/games/:game/:playtype/pbs/best`
This returns the users' best 100 personal bests according
to the [Default Rating Algorithm](todo) for this game.
This is returned in descending sorted order.
The query parameter `alg` can be used to specify a
different rating algorithm to sort under.
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `alg` | String | An overriding rating algorithm to use instead of the default. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `songs` | Array&lt;SongDocument&gt; | The array of songs this search returned. |
| `charts` | Array&lt;ChartDocument&gt; | The array of charts this search returned. |
| `pbs` | Array&lt;PBDocument&gt; | The array of personal bests this search returned. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/pbs/best?alg=BPI
```
#### Response
```js
{
pbs: [{
userID: 1,
scoreData: {
// ...
},
calculatedData: {
ktRating: 15,
BPI: 4.4
}
}, {
userID: 1,
scoreData: {
// ...
},
calculatedData: {
ktRating: 19,
BPI: 4.2
}
}],
songs: [{
title: "Verflucht",
// ...
}, {
title: "AA",
// ...
}],
charts: [{
songID: 123,
playtype: "SP",
difficulty: "ANOTHER",
// ...
}, {
songID: 14,
playtype: "SP",
difficulty: "ANOTHER",
}]
}
```
*****
## Search a user's individual scores.
`GET /api/v1/users/:userID/games/:game/:playtype/scores`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `search` | String | Limits the returned scores to those where the corresponding song is most similar to this query. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `songs` | Array&lt;SongDocument with __textScore&gt; | The array of songs this search returned. |
| `charts` | Array&lt;ChartDocument&gt; | The array of charts this search returned. |
| `scores` | Array&lt;ScoreDocument&gt; | The array of scores this search returned. This is limited to 30 returns. |
!!! info
All `songs` returned also have the `__textScore`
property. This property describes how close the query
was to the actual text, and is mostly internal.
You can read more into the details of this at [Search Implementation](../../codebase/implementation-details/search.md)
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/scores?search=Verfl
```
#### Response
```js
{
scores: [{
userID: 1,
scoreData: {
score: 123,
// ...
}
}],
songs: [{
title: "Verflucht",
// ...
}],
charts: [{
songID: 123,
playtype: "SP",
difficulty: "ANOTHER",
// ...
}, {
songID: 123,
playtype: "SP",
difficulty: "HYPER",
}]
}
```
*****
## Get a user's most recent 100 scores.
`GET /api/v1/users/:userID/games/:game/:playtype/scores/recent`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `songs` | Array&lt;SongDocument&gt; | The array of songs this search returned. |
| `charts` | Array&lt;ChartDocument&gt; | The array of charts this search returned. |
| `scores` | Array&lt;ScoreDocument&gt; | The array of scores this search returned. This is limited to 30 returns. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/scores/recent
```
#### Response
```js
{
scores: [{
userID: 1,
scoreData: {
score: 123,
// ...
}
}],
songs: [{
title: "Verflucht",
// ...
}],
charts: [{
songID: 123,
playtype: "SP",
difficulty: "ANOTHER",
// ...
}, {
songID: 123,
playtype: "SP",
difficulty: "HYPER",
}]
}
```
*****
## Search a user's sessions.
`GET /api/v1/users/:userID/games/:game/:playtype/sessions`
Searches the names of sessions from a given user. This
does not search session descriptions, nor does it search
song titles of played songs inside sessions.
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `search` | string | The session name to search for. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `<body>` | Array&lt;SessionDocument&gt; | The array of sessions that matched this query. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/sessions?search=epic%20session
```
#### Response
```js
[
{
name: "My Epic Session!!!",
userID: 1,
game: "iidx",
playtype: "SP",
// ...
}
]
```
## Get a user's best 100 sessions.
`GET /api/v1/users/:userID/games/:game/:playtype/sessions/best`
Retrieves a user's best 100 sessions according to the
game + playtypes default algorithm. The algorithm can
be overrode with the `alg` query string parameter.
These are returned in descending order.
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The ID or username of the user to retrieve information from. |
| `:game` | URL Parameter | The game to retrieve information from. Must be a supported game. |
| `:playtype` | URL Parameter | The playtype to retrieve information for. Must be a supported playtype of the previous game. |
| `alg` (Optional) | string | The name of the algorithm to use instead of the default. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `<body>` | Array&lt;SessionDocument&gt; | The array of the users best sessions. |
### Example
#### Request
```
GET /api/v1/users/zkldi/games/iidx/SP/sessions/best
```
#### Response
!!! info
The default rating algorithm for IIDX:SP is `ktRating`.
```js
[
{
userID: 1,
game: "iidx",
playtype: "SP",
calculatedData: {
ktRating: 14,
bpi: 3
}
// ... more properties
},
{
userID: 1,
game: "iidx",
playtype: "SP",
calculatedData: {
ktRating: 13.2,
bpi: 4
}
}
]
```
+140
View File
@@ -0,0 +1,140 @@
# Users
These endpoints are related to users in general.
*****
## List Users
` /api/v1/users`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `online` (Optional) | Presence | If present, this limits the returned users to those that are currently online. |
| `username` (Optional) | String | If present, this endpoint works like a search engine for usernames. Users will be returned in their proximity to the original text. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `<body>` | Array&lt;UserDocument&gt; | The array of up to 100 users returned. |
### Example
#### Request
```
GET /api/v1/users
```
#### Response
```js
[{
"userID": 1,
"username": "zkldi",
// ... continued
}]
```
*****
## Retrieve user with ID
`GET /api/v1/users/:userID`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The user's userID or their username. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `<body>` | UserDocument | The user this ID/username corresponds to. |
### Example
!!! note
`zkldi` is the username for the user with userID 1.
it's also the username of the person writing these
docs. Hi!
#### Request
```
GET /api/v1/users/zkldi
OR
GET /api/v1/users/1
```
#### Response
```js
{
userID: 1,
username: "zkldi",
// ... so on
}
```
*****
## Retrieve user's statistics on all games.
`GET /api/v1/users/:userID/game-stats`
### Parameters
| Property | Type | Description |
| :: | :: | :: |
| `:userID` | URL Parameter | The user ID or username to fetch the data of. |
### Response
| Property | Type | Description |
| :: | :: | :: |
| `<body>` | Array&lt;UserGameStatsDocument&gt; | The array of User Game Stats this user has. |
### Example
#### Request
```
GET /api/v1/users/zkldi/stats
OR
GET /api/v1/users/1/stats
```
#### Response
```js
[{
userID: 1,
game: "iidx",
playtype: "SP",
ratings: {
ktRating: 15
},
classes: {
dan: 14
},
}, {
userID: 1,
game: "gitadora",
playtype: "Dora",
ratings: {
skill: 1404
},
classes: {
skillColour: 1
}
}]
```
!!! info
In the event a user has played no games, this will
return an empty array.
+149
View File
@@ -0,0 +1,149 @@
# What is BATCH-MANUAL?
BATCH-MANUAL is a JSON format that Tachi accepts.
This format can be submitted as [a file](../../api/routes/import.md#import-scores-from-a-file)
using the `file/batch-manual` [Import Type](../import/import-types.md), or it can be submitted as a
[HTTP request body](todo)
*****
## Motivation
Instead of Tachi writing new support for every kind of
possible export, and bothering other service providers
to write exports, we could write a generic format we accept
and then users with a bit of scripting knowledge can import
their own scores.
This has the additional advantage of allowing extremely
obscure imports, and reduces the workload on Tachi.
## Format
The format is incredibly simple JSON.
It is comprised of two base keys, `head` and `body`.
### Head
The `head` key contains metadata, and looks like this:
```json
{
"game": "iidx",
"playtype": "SP",
"service": "foobar"
}
```
The fields have the following values:
| Property | Type | Description |
| :: | :: | :: |
| `game` | Any Game Identifier | The game this import is for. |
| `playtype` | Any Playtype for the above game. | The playtype this import is for. |
| `service` | string | A humanised string to explain where these scores are from. This must be between 2 and 15 characters. |
| `version` (Optional) | string | Optionally, you can specify a version of the game this import is for. This should be used when conflicting versions of songs exist, or when this import is rather old. Most of the time, this does not need to be present. |
### Body
The body is an array of Batch Manual Scores. An example
score is as follows:
```json
{
"score": 500,
"lamp": "HARD CLEAR",
"matchType": "songTitle",
"identifier": "5.1.1.",
"difficulty": "ANOTHER",
"timeAchieved": 1624324467489
}
```
The properties are described as this:
| Property | Type | Description |
| :: | :: | :: |
| `score` | Number | The score for this, well, score. This should use the default scoring algorithm for this game. |
| `lamp` | Lamp | The lamp for this score. This should be one of the lamps as described in the config for your game + playtype. |
| `matchType` | "songTitle" \| "ddrSongHash" \| "tachiSongID" \| "bmsChartHash" | This determines how `identifier` will be used to match your scores' chart with Tachi's database of songs and charts. |
| `identifier` | string | A string that Tachi uses to identify what chart this is for. How this is used depends on the `matchType`. |
| `difficulty` (Conditional) | string | If `matchType` is "tachiSongID" or "songTitle", this field must be present, and describe the difficulty of the chart this score is for. |
| `timeAchieved` (Optional) | integer \| null | This is *when* the score was achieved in unix milliseconds. This should be provided if possible, as Tachi uses it for a LOT of features. |
| `comment` (Optional) | string \| null | A comment from the user about this score. |
| `judgements` (Optional) | Record&lt;Game Judgement, integer&gt; | This should be a record of the judgements for your game + playtype, and the integer indicating how often they occured. |
| `hitMeta` (Optional) | See [Game Specific Hit Meta](../documents/score.md#game-specific) | This can be a partial record of various `hitMeta` props for this game. |
#### Match Type
There are four match types, and they all use identifier
in a different way.
- songTitle
As the name implies, this searches for a song who's title
resembles `identifier`. **THIS IS NOT FUZZY MATCHING**,
and is by far the least reliable way to send scores to
Tachi. However, in most scenarios, it will work.
This match type *necessitates* that `difficulty` be defined
and set to a valid difficulty for this game + playtype.
- tachiSongID
This uses `identifier` as if it were an integer, to match
songs based on the `id` field of a tachi song.
This match type *necessitates* that `difficulty` be defined
and set to a valid difficulty for this game + playtype.
- bmsChartHash
As the name implies, this looks for the chart hash BMS
uses. This can be either the MD5 hash or the SHA256 hash,
both will match.
This match type can only be used for BMS.
- ddrSongHash
This looks for the DDR 'song hash'. This is an identifier
used on the e-amusement website, and references a song,
not a chart. As such:
This match type *necessitates* that `difficulty` be defined
and set to a valid difficulty for DDR.
This match type can only be used for DDR.
## Example
A final example of a simple BATCH MANUAL format
looks like this:
```json
{
"head": {
"game": "iidx",
"playtype": "SP",
"service": "My Service"
},
"body": [{
"score": 500,
"lamp": "HARD CLEAR",
"matchType": "songTitle",
"identifier": "5.1.1.",
"difficulty": "ANOTHER"
}, {
"score": 123,
"lamp": "FAILED",
"matchType": "tachiSongID",
"identifier": "1",
"difficulty": "HYPER",
"comment": "This score sucked!",
"hitMeta": {
"bp": 5
}
}]
}
```
+14 -1
View File
@@ -53,6 +53,20 @@ is open sourced under AGPLv3.
Contributions to this will be under increased scrutiny as
I am trying to keep the codebase well organised and tidy.
!!! tip
If you're setting up a development environment locally,
commit `47c981f` converted the codebase from spaces to tabs.
This revision is hidden using .git-blame-ignore-revs.
You can fix it with this command.
```
git config blame.ignoreRevsFile .git-blame-ignore-revs
```
!!! warning
This only works on git 2.23 or greater. Your package manager may not have a version
this recent. See [Git Installation for Linux](https://git-scm.com/download/linux).
### Pull Requests
You can contribute to `tachi-server` by going to the
@@ -115,4 +129,3 @@ Non-Specific bug reports will be closed immediately and marked as invalid.
- Documentation issues go in the other repo.
Documentation issues should go [here](https://github.com/zkldi/tachi-docs) instead.
+28
View File
@@ -0,0 +1,28 @@
# Documents
Tachi's database has specific documents for specific
things. This part of the documentation covers the
shape of those documents.
!!! note
Quite often, the API exposes these documents as-is,
without running a projection step on the fields.
This means that the API documentation will
involve frequent links to this part of the
codebase documentation.
*****
## Definition
This part of the documentation will define the shape of the
document, and explain fields where necessary.
Depending on whats easiest, this may just be a raw
TypeScript interface definition, or it may be a table.
## Example
This part of the documentation will show an example document,
with some explainations if necessary.
+331
View File
@@ -0,0 +1,331 @@
# Score Document
- Stored in `scores`.
!!! info
The same kind of score document is used for every game.
As such, this document is rather complex.
To make parsing this document easier, we're going to
define the "Base Score" document, then define all
the game-specific fields and properties that appear.
*****
## Definition
```ts
interface PublicUserDocument {
service: string;
game: "iidx" | "bms" // ...etc;
playtype: __GameSpecific;
userID: integer;
scoreData: {
score: number;
lamp: __GameSpecific;
percent: number;
grade: __GameSpecific;
lampIndex: integer;
gradeIndex: integer;
esd: number | null;
judgements: __GameSpecific;
hitMeta: __GameSpecific;
};
scoreMeta: __GameSpecific;
calculatedData: __GameSpecific;
timeAchieved: integer | null;
songID: integer;
chartID: string;
isPrimary: boolean;
highlight: boolean;
comment: string | null;
timeAdded: integer;
scoreID: string;
importType: ImportTypes | null;
}
```
!!! note
All fields marked with __GameSpecific change
depending on the game and playtype this score
is for. We'll go over all of those in a bit.
The base score document is structured as follows:
| Property | Description |
| :: | :: |
| `game` | This is the game this score is for. |
| `service` | This is a humanised string for representing the place this score came from. This is primarily used by [Batch-Manual](../batch-manual/overview.md) formats to declare where the scores are coming from. |
| `userID` | The user that got this score. |
| `timeAchieved` | This is the time this score was actually achieved. This is **NOT NECESSARILY** the time this score was inserted into the database. If this is not known, it can be set to null. |
| `timeAdded` | This is the time the score was added to the Tachi database. This is **NOT NECESSARILY** the time the score was achieved. |
| `songID` | The song this score is on. Even though this can be derived from `chartID`, it's kept next to the score for certain query optimisations. You can read on the difference between charts and songs [here](../implementation-details/songs-charts.md). |
| `chartID` | The chart this score was achieved on. |
| `isPrimary` | Whether this score is was achieved on a "primary" chart or not. You can read more on what a primary chart is [here](../implementation-details/songs-charts.md#isPrimary). |
| `highlight` | Whether this individual score was highlighted or not by the user. This is one of the few mutable fields on the score document. |
| `comment` | A comment left by the user on this score. If one is not present, it is left as `null`. Comments are capped at 240 characters. |
| `scoreID` | A unique identifier for this score. Score IDs are prefixed with `R`. This identifier is derived from the content of the score, and thus can be used to dedupe scores. See [Score IDs](todo). |
| `importType` | The import type used to import this score. For more on this, see [Import Types](../import/import-types.md) |
Now, absolutely none of the above fields contain
information about the actual *score* the user got,
as in, the numbers they achieved! (Other than the
`timeAchieved` property, maybe).
That is stored in the below sub-documents.
### `scoreData`
The below table describes the properties of the `scoreData`
sub-document.
| Property | Description |
| :: | :: |
| `score` | A number describing the "score" the user got. Depending on the game, this may be bounded between various numbers. |
| `lamp` | The lamp the user got. For more information on what a lamp is, see [What are Lamps?](../../user/terminology/lamps.md)
| `percent` | The 'percent' the user got. That is, their score scaled to the total amount of score they could have possibly got. There are some oddities with this field.[^1]. |
| `grade` | The grade the user got. For most games, this is a set of discrete cutoffs for the score's `percent`.[^2] |
| `lampIndex`, `gradeIndex` | While `lamp` and `grade` are both strings, these are the raw enum values for those fields. This can be used for filters (select scores where lampIndex > lamps.HARD_CLEAR), or other query methods. |
| `esd` | Some games support ESD. This field contains the ESD for that score. For more information on what ESD is, see [What is ESD?](../../user/stats/esd.md)
| `judgements` | A record of Judgement->Integer values. The keys for this property depend on the game the score is for. |
| `hitMeta` | 'Meta' information about the user's *hits*. That is, things that aren't *literally* about the score, but are related to how well the user played. This contains properties such as `maxCombo` and `fast` and `slow` counts. All the fields here are optional and nullable. Some games extend this to provide things like `gauge`. |
### `scoreMeta`
The counterpart to `scoreData` is `scoreMeta`, which is
an entirely game-specific record containing meta information
about how the play was achieved.
!!! warning
It's easy to confuse this with `hitMeta`.
`hitMeta` is for meta information about **HOW**
the user performed on the score. Things that
aren't literally the score or percent, but are
important properties nonetheless.
`scoreMeta` is for completely meta information
about the **PLAY** the user made, such as the
mods and options they had on when playing.
Since `scoreMeta`'s properties are entirely game-specific,
they will be covered in more detail in the below section.
## Example Score
The below document is an example IIDX - SP score.
```json
{
"service": "e-amusement",
"comment": null,
"game": "iidx",
"importType": "file/eamusement-iidx-csv",
"scoreMeta": {},
"timeAchieved": 1608317520000,
"scoreData": {
"lampIndex": 7,
"gradeIndex": 8,
"esd": 10.15625,
"score": 1729,
"lamp": "FULL COMBO",
"hitData": {
"pgreat": 821,
"great": 87
},
"hitMeta": {
"bp": 0
},
"percent": 95,
"grade": "MAX-"
},
"highlight": false,
"timeAdded": 1623955294917,
"userID": 1,
"calculatedData": {
"BPI": null,
"K%": null,
"ktRating": 9.110264135494702,
"ktLampRating": 8
},
"songID": 212,
"chartID": "efa36c73259409ea6a86c689f5750a2de395143b",
"scoreID": "Rcf644b6c07830c0948d3a8b457427c1e449f22bf60a7bbed6c3970171e0fa969",
"playtype": "SP",
"isPrimary": true
}
```
## Game Specific
Every game has its own specific properties it adds to
or changes about the base score document.
For all games, the following changes are applied:
| Property | Change |
| :: | :: |
| `scoreData.lamp` | This field is restricted to only Lamps for that game. For more information, see [Game Enums](../implementation-details/game-configuration.md). |
| `scoreData.grade` | This field is restricted to only Grades for that game. For more information, see [Game Enums](../implementation-details/game-configuration.md). |
| `scoreData.judgements` | The keys of this field are set to only valid Judgements for that game. For more information, see [Game Judgements](../implementation-details/game-configuration.md).
!!! info
All games implicitly have `fast`, `slow` and `maxCombo`
as `integer | null` in their hitMeta.
!!! warning
As mentioned above, all properties inside ScoreMeta
and HitMeta are optional.
### IIDX:SP
```ts
interface HitMeta {
bp: integer | null;
gauge: number | null;
gaugeHistory: (number | null)[] | null;
scoreHistory: number[] | null;
comboBreak: integer | null;
gsm: {
EASY: (number | null)[];
NORMAL: (number | null)[];
HARD: (number | null)[];
EX_HARD: (number | null)[];
} | null;
}
```
| Property | Description |
| :: | :: |
| `bp` | The total amount of bads this user got plus the total amount of poors. This is generally used by players as a more continuous form of evaluating lamp ability. |
| `gauge` | The gauge the user had at the end of the chart. |
| `gaugeHistory` | An array, describing the gauge the user had at each measure in the chart. If the user dies, null is used until the end of the array. |
| `scoreHistory` | An array describing the exscore the user had at each measure in the chart. |
| `comboBreak` | The amount of times this score broke combo. Only some poors actually cause combo breaks! |
| `gsm` | Data for GAUGE_SHIFT_MANEUVER. This is `gaugeHistory` replicated for all possible gauges at once. |
```ts
interface ScoreMeta {
random: "NONRAN" | "RANDOM" | "R-RANDOM" | "S-RANDOM" | "MIRROR" | null;
assist: "NO ASSIST" | "AUTO SCRATCH" | "LEGACY NOTE" | "FULL ASSIST" | null;
range: "NONE" | "SUDDEN+" | "HIDDEN+" | "SUD+ HID+" | "LIFT" | "LIFT SUD+" | null;
gauge: "ASSISTED EASY" | "EASY" | "NORMAL" | "HARD" | "EX HARD" | null;
}
```
| Property | Description |
| :: | :: |
| `random` | The RANDOM option that was used during this score. |
| `assist` | The ASSIST option that was used during this score. |
| `range` | The RANGE option that was used during this score. |
| `gauge` | The GAUGE option that was used during this score. |
### IIDX:DP
Same as IIDX:SP, but with the following changes to Hit Meta:
| Property | Description |
| :: | :: |
| `random` | Is now an array of [LEFT_HAND_RANDOM, RIGHT_HAND_RANDOM], instead of just the single option.
### SDVX
```ts
interface HitMeta {
gauge: number | null;
btnRate: number | null;
holdRate: number | null;
laserRate: number | null;
}
```
| Property | Description |
| :: | :: |
| `gauge` | The gauge the user had when the chart finished. |
| `btnRate`, `holdRate`, `laserRate` | The values the user had for the "rate" bars. These are not of much interest to anyone. |
```ts
interface ScoreMeta {
inSkillAnalyser: boolean | null
}
```
| Property | Description |
| :: | :: |
| `inSkillAnalyser` | Whether or whether not this score was achieved inside the skill analyser mode. |
### USC
```ts
interface HitMeta {
gauge: number | null;
btnRate: number | null;
holdRate: number | null;
laserRate: number | null;
}
```
| Property | Description |
| :: | :: |
| `gauge` | The gauge the user had when the chart finished. |
| `btnRate`, `holdRate`, `laserRate` | The values the user had for the "rate" bars. These are not of much interest to anyone. |
```ts
interface ScoreMeta {
noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN" | null;
gaugeMod: "NORMAL" | "HARD" | null;
}
```
| Property | Description |
| :: | :: |
| `noteMod` | The NOTE modifier used on this score. |
| `gaugeMod` | The gauge modifier used on this score. |
### BMS:7K
```ts
type BMSJudgePermutations = `${"e" | "l"}${"bd" | "pr" | "gd" | "gr" | "pg"}`;
type BMSHitMeta = BASE_VALID_HIT_META &
{
[K in BMSJudgePermutations]: integer;
} & {
bp: integer | null;
gauge: number | null;
};
```
| Property | Description |
| :: | :: |
| `epg`, `lpg`... | These are the (E)arly and (L)ate judgements used by beatoraja internally. |
| `bp` | The total bads + the total poors of this score. |
| `gauge` | The gauge the user had at the end of this chart. |
```ts
interface ScoreMeta {
random: "NONRAN" | "RANDOM" | "R-RANDOM" | "S-RANDOM" | "MIRROR" | null;
inputDevice: "KEYBOARD" | "BM_CONTROLLER" | null;
client: "LR2" | "beatoraja" | "lr2oraja";
lntype: null | "LN" | "CN";
}
```
| Property | Description |
| :: | :: |
| `random` | The random option this user selected. |
| `inputDevice` | Whether this score was achieved on a keyboard or a beatmania controller. If null, this is not known. |
| `client` | What client this score was performed on. Scores achieved on beatoraja have a warning next to them indicating differences in their gauge implementation. |
| `lnType` | What type of LNs this user was using. This only applies to beatoraja and lr2oraja, where CNs are an option. |
### DDR, maimai, MÚSECA, CHUNITHM
These games have no extended properties on its `hitMeta` object.
These games also have no properties on its `scoreMeta` object.
*****
[^1]: SEGA games, such as maimai and CHUNITHM have percents that go greater than 100%. For this, we just let the field overflow 100. There's no reason it can't! It is still strange for a percent to be over 100, however. In some scenarios, the `score` property for a score may be identical to the `percent` property. This occurs when a games primary score indicator is also their percent property, or when their default `score` property is useless (such as in maimai).
[^2]: Some games, however, have special grade requirements, and that is why this is a separate field (and not just derived from `percent`.)
+65
View File
@@ -0,0 +1,65 @@
# User Document
- Stored in `users`.
*****
## Definition
```ts
interface PublicUserDocument {
username: string;
usernameLowercase: string;
id: integer;
socialMedia: {
discord?: string | null;
twitter?: string | null;
github?: string | null;
steam?: string | null;
youtube?: string | null;
twitch?: string | null;
};
lastSeen: integer;
about: string;
customPfp: boolean;
customBanner: boolean;
clan: string | null;
}
```
| Property | Description |
| :: | :: |
| `username` | This is guaranteed to be unique even across casings. Valid usernames match `/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/`. |
| `usernameLowercase` | Lowercased versions of usernames are stored so we can efficiently query whether a username exists. This means we don't have to do case insensitive searches! |
| `id` | This is a unique auto-incrementing integer for the user, and is completely immutable. |
| `socialMedia` | Contains popular social media sites and a way of referencing that user. |
| `lastSeen` | The last time this user made a request to Tachi. |
| `about` | This user's about me. This is evaluated as markdown. |
| `customPfp`, `customBanner` | Whether this user has a custom profile picture and banner. |
| `clan` | Currently unused. This will store a users clan tag if they are in a clan. |
## Example
```json
{
"username": "test_zkldi",
"usernameLowercase": "test_zkldi",
"id": 1,
"socialMedia": {
"discord": "test_zkldi#1234",
"steam": null
},
"lastSeen": null,
"about": "test_user_not_real",
"customPfp": true,
"customBanner": true,
"clan": null
}
```
!!! note
An extension of the user document - `PrivateUserDocument`,
is what is actually stored in the database.
This appends two fields - `email` and `password`,
and is not exposed over the API for obvious reasons.
@@ -0,0 +1,8 @@
# About
As you'd expect, this section contains implementation details. Although the entire
codebase reference is technically about implementation details, this part is
for miscellaneous implementation details that wouldn't fit anywhere else.
*****
@@ -0,0 +1,98 @@
# ESD Implementation
ESD uses the fact that [A binomial distribution can approximate a normal one](https://en.wikipedia.org/wiki/Central_limit_theorem) in order to derive an estimate for standard deviations.
*****
## Method Outline
Our overview is as follows. We are given a percent and
judgement windows for a game.
From that, we want to return the standard deviation that
would result in that percent - given that game's judgement
windows.
### Deriving Percent From Standard Deviation
We assume that the mean of the players hits is always 0.
Then, we work backwards. With the knowledge of
the judgement windows for a game, we can estimate the
percent a standard deviation would typically give.
We construct a distribution with a mean of 0 and a standard
deviation of S, and then see roughly where hits would
end up on that distribution.
We multiply how many hits we'd *expect* to be within a
certain judgement window by the value of that judgement.
So in our scenario of S standard deviation, we would
expect X% of hits to be between, say, -16.67 and +16.67 (IIDX's PGREAT window).
!!! note
To calculate that percent we need to use the
cumulative distribution function. That is not covered
here, but guides are all over the internet.
We can multiply that percent by the value of a PGREAT (100%).
Then, we repeat for the great window at 50%, and so on.
When we've summed all that up, we get an estimate of
the percent this standard deviation is worth.
### Reversing That
This is good, but this is backwards!
Turns out, there's no algebraic way to reverse this function!
So, let's do a little approximating.
We can start with an ESD of 100, which is halfway between
the lowest ESD (0), and the highest (200).
```ts
for (let i = 0; i < MAX_ITERATIONS; i++) {
const estimatedPercent = StdDeviationToPercent(judgements, estSD, largestValue);
if (Math.abs(estimatedPercent - percent) < ACCEPTABLE_ERROR) {
return estSD;
}
if (estimatedPercent < percent) {
maxSD = estSD;
} else {
minSD = estSD;
}
if (estSD === (minSD + maxSD) / 2) {
// if it isn't moving, just terminate
break;
}
estSD = (minSD + maxSD) / 2;
}
```
This code is then ran to approximate the standard deviation
needed to get a percent *like* the one we were given.
`ACCEPTABLE_ERROR` is set to `0.001` by default.
`MAX_ITERATIONS` is set to `50`.
With this, we can "intelligently brute force" standard deviations,
getting closer to our provided percent until its
within 0.001%. Then, we can return the standard deviation
we used to get that percent!
!!! note
Performance of this is incredibly fast, while
"intelligently brute forcing" isn't ideal, 50
iterations are almost never hit, and most ESDs
are calculated in about 10 iterations.
All of this happens in significantly under 1 milisecond,
so it is not exactly a significant performance hit.
@@ -0,0 +1,895 @@
# Game Configuration
Tachi has two sets of game configurations.
The first is on the game level, which contains things
like the humanised name for the game (i.e. `iidx -> beatmania IIDX`).
The second is for each individual game + playtype
combination, which contains things like the list of
lamps for the game.
!!! help
This page is currently unformatted, and is just a reference
to the codebase at points.
It would be really appreciated if someone formatted
the configurations for every game! For the time being,
the below documentation is just the raw configuration
for each game. Please see [Contributing to Tachi](../contributing.md).
*****
## Game Configurations
```ts
const GAME_CONFIGS: GameConfigs = {
iidx: {
defaultPlaytype: "SP",
name: "beatmania IIDX",
internalName: "iidx",
validPlaytypes: ["SP", "DP"],
},
museca: {
defaultPlaytype: "Single",
name: "MÚSECA",
internalName: "museca",
validPlaytypes: ["Single"],
},
chunithm: {
defaultPlaytype: "Single",
name: "CHUNITHM",
internalName: "chunithm",
validPlaytypes: ["Single"],
},
ddr: {
defaultPlaytype: "SP",
name: "DDR", // used to be 'Dance Dance Revolution', is now DDR for space reasons.
internalName: "ddr",
validPlaytypes: ["SP", "DP"],
},
bms: {
defaultPlaytype: "7K",
name: "BMS",
internalName: "bms",
validPlaytypes: ["7K", "14K"],
},
gitadora: {
defaultPlaytype: "Dora",
name: "GITADORA",
internalName: "gitadora",
validPlaytypes: ["Gita", "Dora"],
},
maimai: {
defaultPlaytype: "Single",
name: "maimai",
internalName: "maimai",
validPlaytypes: ["Single"],
},
sdvx: {
defaultPlaytype: "Single",
name: "SOUND VOLTEX",
internalName: "sdvx",
validPlaytypes: ["Single"],
},
usc: {
defaultPlaytype: "Single",
name: "unnamed_sdvx_clone",
internalName: "usc",
validPlaytypes: ["Single"],
},
};
```
## Game + Playtype Configurations
```ts
const GAME_PT_CONFIGS: GamePTConfigs = {
"iidx:SP": {
idString: "iidx:SP",
percentMax: 100,
defaultScoreRatingAlg: "ktRating",
defaultSessionRatingAlg: "ktRating",
defaultProfileRatingAlg: "ktRating",
difficulties: ["BEGINNER", "NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"],
defaultDifficulty: "ANOTHER",
difficultyColours: {
BEGINNER: COLOUR_SET.paleGreen,
NORMAL: COLOUR_SET.blue,
HYPER: COLOUR_SET.orange,
ANOTHER: COLOUR_SET.red,
LEGGENDARIA: COLOUR_SET.purple,
},
grades: ["F", "E", "D", "C", "B", "A", "AA", "AAA", "MAX-", "MAX"],
gradeColours: {
F: COLOUR_SET.gray,
E: COLOUR_SET.red,
D: COLOUR_SET.maroon,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleBlue,
A: COLOUR_SET.green,
AA: COLOUR_SET.blue,
AAA: COLOUR_SET.gold,
"MAX-": COLOUR_SET.teal,
MAX: COLOUR_SET.white,
},
clearGrade: "A",
gradeBoundaries: [0, 22.22, 33.33, 44.44, 55.55, 66.66, 77.77, 88.88, 94.44, 100.0],
lamps: [
"NO PLAY",
"FAILED",
"ASSIST CLEAR",
"EASY CLEAR",
"CLEAR",
"HARD CLEAR",
"EX HARD CLEAR",
"FULL COMBO",
],
lampColours: {
"NO PLAY": COLOUR_SET.gray,
FAILED: COLOUR_SET.red,
"ASSIST CLEAR": COLOUR_SET.purple,
"EASY CLEAR": COLOUR_SET.green,
CLEAR: COLOUR_SET.blue,
"HARD CLEAR": COLOUR_SET.orange,
"EX HARD CLEAR": COLOUR_SET.gold,
"FULL COMBO": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: true,
judgementWindows: [
{ name: "PGREAT", msBorder: 16.667, value: 2 },
{ name: "GREAT", msBorder: 33.333, value: 1 },
{ name: "GOOD", msBorder: 116.667, value: 0 },
],
judgements: ["pgreat", "great", "good", "bad", "poor"],
defaultTable: "Levels (N-1)",
scoreBucket: "lamp",
},
"iidx:DP": {
idString: "iidx:DP",
percentMax: 100,
defaultScoreRatingAlg: "ktRating",
defaultSessionRatingAlg: "ktRating",
defaultProfileRatingAlg: "ktRating",
difficulties: ["NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"],
defaultDifficulty: "ANOTHER",
difficultyColours: {
NORMAL: COLOUR_SET.blue,
HYPER: COLOUR_SET.orange,
ANOTHER: COLOUR_SET.red,
LEGGENDARIA: COLOUR_SET.purple,
},
grades: ["F", "E", "D", "C", "B", "A", "AA", "AAA", "MAX-", "MAX"],
gradeColours: {
F: COLOUR_SET.gray,
E: COLOUR_SET.red,
D: COLOUR_SET.maroon,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleBlue,
A: COLOUR_SET.green,
AA: COLOUR_SET.blue,
AAA: COLOUR_SET.gold,
"MAX-": COLOUR_SET.teal,
MAX: COLOUR_SET.white,
},
clearGrade: "A",
gradeBoundaries: [0, 22.22, 33.33, 44.44, 55.55, 66.66, 77.77, 88.88, 94.44, 100.0],
lamps: [
"NO PLAY",
"FAILED",
"ASSIST CLEAR",
"EASY CLEAR",
"CLEAR",
"HARD CLEAR",
"EX HARD CLEAR",
"FULL COMBO",
],
lampColours: {
"NO PLAY": COLOUR_SET.gray,
FAILED: COLOUR_SET.red,
"ASSIST CLEAR": COLOUR_SET.purple,
"EASY CLEAR": COLOUR_SET.green,
CLEAR: COLOUR_SET.blue,
"HARD CLEAR": COLOUR_SET.orange,
"EX HARD CLEAR": COLOUR_SET.gold,
"FULL COMBO": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: true,
judgementWindows: [
{ name: "PGREAT", msBorder: 16.667, value: 2 },
{ name: "GREAT", msBorder: 33.333, value: 1 },
{ name: "GOOD", msBorder: 116.667, value: 0 },
],
judgements: ["pgreat", "great", "good", "bad", "poor"],
defaultTable: "Levels (N-1)",
scoreBucket: "lamp",
},
"chunithm:Single": {
idString: "chunithm:Single",
percentMax: 101,
defaultScoreRatingAlg: "rating",
defaultSessionRatingAlg: "naiveRating",
defaultProfileRatingAlg: "naiveRating",
difficulties: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "WORLD'S END"],
defaultDifficulty: "MASTER",
difficultyColours: {
BASIC: COLOUR_SET.blue,
ADVANCED: COLOUR_SET.orange,
EXPERT: COLOUR_SET.red,
MASTER: COLOUR_SET.purple,
"WORLD'S END": COLOUR_SET.vibrantYellow,
},
grades: ["D", "C", "B", "BB", "BBB", "A", "AA", "AAA", "S", "SS", "SSS"],
gradeColours: {
D: COLOUR_SET.red,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleBlue,
BB: COLOUR_SET.blue,
BBB: COLOUR_SET.vibrantBlue,
A: COLOUR_SET.paleGreen,
AA: COLOUR_SET.green,
AAA: COLOUR_SET.vibrantGreen,
S: COLOUR_SET.vibrantOrange,
SS: COLOUR_SET.vibrantYellow,
SSS: COLOUR_SET.teal,
},
clearGrade: "A",
gradeBoundaries: [0, 50, 60, 70, 80, 90, 92.5, 95.0, 97.5, 100, 107.5, 101],
lamps: ["FAILED", "CLEAR", "FULL COMBO", "ALL JUSTICE", "ALL JUSTICE CRITICAL"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.paleGreen,
"FULL COMBO": COLOUR_SET.paleBlue,
"ALL JUSTICE": COLOUR_SET.gold,
"ALL JUSTICE CRITICAL": COLOUR_SET.white,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["jcrit", "justice", "attack", "miss"],
defaultTable: "Levels (N-1)",
scoreBucket: "grade",
},
"sdvx:Single": {
idString: "sdvx:Single",
percentMax: 100,
defaultScoreRatingAlg: "VF6",
defaultSessionRatingAlg: "ProfileVF6",
defaultProfileRatingAlg: "VF6",
difficulties: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "MXM"],
defaultDifficulty: "EXH",
difficultyColours: {
NOV: COLOUR_SET.purple, // colour set dark purple
ADV: COLOUR_SET.vibrantYellow,
EXH: COLOUR_SET.red,
INF: "TODO", // colour set light pink
GRV: COLOUR_SET.orange,
HVN: COLOUR_SET.teal,
VVD: "TODO", // colour set pink
MXM: COLOUR_SET.white,
},
grades: ["D", "C", "B", "A", "A+", "AA", "AA+", "AAA", "AAA+", "S"],
gradeColours: {
D: COLOUR_SET.gray,
C: COLOUR_SET.red,
B: COLOUR_SET.maroon,
A: COLOUR_SET.paleBlue,
"A+": COLOUR_SET.blue,
AA: COLOUR_SET.paleGreen,
"AA+": COLOUR_SET.green,
AAA: COLOUR_SET.gold,
"AAA+": COLOUR_SET.vibrantYellow,
S: COLOUR_SET.teal,
},
clearGrade: "A",
gradeBoundaries: [0, 70, 80, 87, 90, 93, 95, 97, 98, 99],
lamps: ["FAILED", "CLEAR", "EXCESSIVE CLEAR", "ULTIMATE CHAIN", "PERFECT ULTIMATE CHAIN"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.green,
"EXCESSIVE CLEAR": COLOUR_SET.orange,
"ULTIMATE CHAIN": COLOUR_SET.teal,
"PERFECT ULTIMATE CHAIN": COLOUR_SET.gold,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["critical", "near", "miss"],
defaultTable: "Levels (N-1)",
scoreBucket: "grade",
},
"usc:Single": {
idString: "usc:Single",
percentMax: 100,
defaultScoreRatingAlg: "VF6",
defaultSessionRatingAlg: "ProfileVF6",
defaultProfileRatingAlg: "VF6",
difficulties: ["NOV", "ADV", "EXH", "INF"],
defaultDifficulty: "EXH",
difficultyColours: {
NOV: COLOUR_SET.purple, // colour set dark purple
ADV: COLOUR_SET.vibrantYellow,
EXH: COLOUR_SET.red,
INF: "TODO", // colour set light pink
},
grades: ["D", "C", "B", "A", "A+", "AA", "AA+", "AAA", "AAA+", "S"],
gradeColours: {
D: COLOUR_SET.gray,
C: COLOUR_SET.red,
B: COLOUR_SET.maroon,
A: COLOUR_SET.paleBlue,
"A+": COLOUR_SET.blue,
AA: COLOUR_SET.paleGreen,
"AA+": COLOUR_SET.green,
AAA: COLOUR_SET.gold,
"AAA+": COLOUR_SET.vibrantYellow,
S: COLOUR_SET.teal,
},
clearGrade: "A",
gradeBoundaries: [0, 70, 80, 87, 90, 93, 95, 97, 98, 99],
lamps: ["FAILED", "CLEAR", "EXCESSIVE CLEAR", "ULTIMATE CHAIN", "PERFECT ULTIMATE CHAIN"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.green,
"EXCESSIVE CLEAR": COLOUR_SET.orange,
"ULTIMATE CHAIN": COLOUR_SET.teal,
"PERFECT ULTIMATE CHAIN": COLOUR_SET.gold,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["critical", "near", "miss"],
defaultTable: "Levels (N-1)",
scoreBucket: "grade",
},
"museca:Single": {
idString: "museca:Single",
percentMax: 100,
defaultScoreRatingAlg: "ktRating",
defaultSessionRatingAlg: "ktRating",
defaultProfileRatingAlg: "ktRating",
difficulties: ["Green", "Yellow", "Red"],
defaultDifficulty: "Red",
difficultyColours: {
Green: COLOUR_SET.green,
Yellow: COLOUR_SET.vibrantYellow,
Red: COLOUR_SET.red,
},
grades: ["没", "拙", "凡", "佳", "良", "優", "秀", "傑", "傑G"],
gradeColours: {
没: COLOUR_SET.gray,
拙: COLOUR_SET.maroon,
凡: COLOUR_SET.red,
佳: COLOUR_SET.paleGreen,
良: COLOUR_SET.paleBlue,
優: COLOUR_SET.green,
秀: COLOUR_SET.blue,
傑: COLOUR_SET.teal,
傑G: COLOUR_SET.gold,
},
clearGrade: "良",
gradeBoundaries: [0, 60, 70, 80, 85, 90, 95, 97.5, 100],
lamps: ["FAILED", "CLEAR", "CONNECT ALL", "PERFECT CONNECT ALL"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.green,
"CONNECT ALL": COLOUR_SET.teal,
"PERFECT CONNECT ALL": COLOUR_SET.gold,
},
clearLamp: "CLEAR",
supportsESD: true,
judgementWindows: [
{ name: "CRITICAL", msBorder: 33.333, value: 2 },
{ name: "NEAR", msBorder: 66.667, value: 1 },
],
judgements: ["critical", "near", "miss"],
defaultTable: "Levels (N-1)",
scoreBucket: "grade",
},
"bms:7K": {
idString: "bms:7K",
percentMax: 100,
defaultScoreRatingAlg: "sieglinde",
defaultSessionRatingAlg: "sieglinde",
defaultProfileRatingAlg: "sieglinde",
difficulties: ["CHART"],
defaultDifficulty: "CHART",
difficultyColours: {
CHART: null,
},
grades: ["F", "E", "D", "C", "B", "A", "AA", "AAA", "MAX-", "MAX"],
gradeColours: {
F: COLOUR_SET.gray,
E: COLOUR_SET.red,
D: COLOUR_SET.maroon,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleBlue,
A: COLOUR_SET.green,
AA: COLOUR_SET.blue,
AAA: COLOUR_SET.gold,
"MAX-": COLOUR_SET.teal,
MAX: COLOUR_SET.white,
},
clearGrade: "A",
gradeBoundaries: [0, 22.22, 33.33, 44.44, 55.55, 66.66, 77.77, 88.88, 94.44, 100.0],
lamps: [
"NO PLAY",
"FAILED",
"ASSIST CLEAR",
"EASY CLEAR",
"CLEAR",
"HARD CLEAR",
"EX HARD CLEAR",
"FULL COMBO",
],
lampColours: {
"NO PLAY": COLOUR_SET.gray,
FAILED: COLOUR_SET.red,
"ASSIST CLEAR": COLOUR_SET.purple,
"EASY CLEAR": COLOUR_SET.green,
CLEAR: COLOUR_SET.blue,
"HARD CLEAR": COLOUR_SET.orange,
"EX HARD CLEAR": COLOUR_SET.gold,
"FULL COMBO": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["pgreat", "great", "good", "bad", "poor"],
defaultTable: "Insane",
scoreBucket: "lamp",
},
"bms:14K": {
idString: "bms:14K",
percentMax: 100,
defaultScoreRatingAlg: "sieglinde",
defaultSessionRatingAlg: "sieglinde",
defaultProfileRatingAlg: "sieglinde",
difficulties: ["CHART"],
defaultDifficulty: "CHART",
difficultyColours: {
CHART: null,
},
grades: ["F", "E", "D", "C", "B", "A", "AA", "AAA", "MAX-", "MAX"],
gradeColours: {
F: COLOUR_SET.gray,
E: COLOUR_SET.red,
D: COLOUR_SET.maroon,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleBlue,
A: COLOUR_SET.green,
AA: COLOUR_SET.blue,
AAA: COLOUR_SET.gold,
"MAX-": COLOUR_SET.teal,
MAX: COLOUR_SET.white,
},
clearGrade: "A",
gradeBoundaries: [0, 22.22, 33.33, 44.44, 55.55, 66.66, 77.77, 88.88, 94.44, 100.0],
lamps: [
"NO PLAY",
"FAILED",
"ASSIST CLEAR",
"EASY CLEAR",
"CLEAR",
"HARD CLEAR",
"EX HARD CLEAR",
"FULL COMBO",
],
lampColours: {
"NO PLAY": COLOUR_SET.gray,
FAILED: COLOUR_SET.red,
"ASSIST CLEAR": COLOUR_SET.purple,
"EASY CLEAR": COLOUR_SET.green,
CLEAR: COLOUR_SET.blue,
"HARD CLEAR": COLOUR_SET.orange,
"EX HARD CLEAR": COLOUR_SET.gold,
"FULL COMBO": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["pgreat", "great", "good", "bad", "poor"],
defaultTable: "Insane",
scoreBucket: "lamp",
},
"ddr:SP": {
idString: "ddr:SP",
percentMax: 100,
defaultScoreRatingAlg: "ktRating",
defaultSessionRatingAlg: "ktRating",
defaultProfileRatingAlg: "ktRating",
difficulties: ["BEGINNER", "BASIC", "DIFFICULT", "EXPERT", "CHALLENGE"],
defaultDifficulty: "EXPERT",
difficultyColours: {
BEGINNER: COLOUR_SET.paleBlue,
BASIC: COLOUR_SET.orange,
DIFFICULT: COLOUR_SET.red,
EXPERT: COLOUR_SET.green,
CHALLENGE: COLOUR_SET.purple,
},
grades: [
"D",
"D+",
"C-",
"C",
"C+",
"B-",
"B",
"B+",
"A-",
"A",
"A+",
"AA-",
"AA",
"AA+",
"AAA",
],
gradeColours: {
D: COLOUR_SET.gray,
"D+": COLOUR_SET.maroon,
"C-": COLOUR_SET.red,
C: COLOUR_SET.purple,
"C+": COLOUR_SET.vibrantPurple,
"B-": COLOUR_SET.paleBlue,
B: COLOUR_SET.blue,
"B+": COLOUR_SET.vibrantBlue,
"A-": COLOUR_SET.paleGreen,
A: COLOUR_SET.green,
"A+": COLOUR_SET.vibrantGreen,
"AA-": COLOUR_SET.paleOrange,
AA: COLOUR_SET.orange,
"AA+": COLOUR_SET.vibrantOrange,
AAA: COLOUR_SET.gold,
},
clearGrade: "A",
gradeBoundaries: [0, 55, 59, 60, 65, 69, 70, 75, 79, 80, 85, 89, 90, 95, 99],
lamps: [
"FAILED",
"CLEAR",
"LIFE4",
"FULL COMBO",
"GREAT FULL COMBO",
"PERFECT FULL COMBO",
"MARVELOUS FULL COMBO",
],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.paleGreen,
LIFE4: COLOUR_SET.orange,
"FULL COMBO": COLOUR_SET.paleBlue,
"GREAT FULL COMBO": COLOUR_SET.green,
"PERFECT FULL COMBO": COLOUR_SET.gold,
"MARVELOUS FULL COMBO": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: true,
judgementWindows: [
{ name: "MARVELOUS", msBorder: 15, value: 3 },
{ name: "PERFECT", msBorder: 30, value: 2 },
{ name: "GREAT", msBorder: 59, value: 1 },
{ name: "GOOD", msBorder: 89, value: 0 },
{ name: "BAD", msBorder: 119, value: 0 },
],
judgements: ["marvelous", "perfect", "great", "good", "boo", "miss", "ok", "ng"],
defaultTable: "Levels (N-1)",
scoreBucket: "lamp",
},
"ddr:DP": {
idString: "ddr:DP",
percentMax: 100,
defaultScoreRatingAlg: "ktRating",
defaultSessionRatingAlg: "ktRating",
defaultProfileRatingAlg: "ktRating",
difficulties: ["BASIC", "DIFFICULT", "EXPERT", "CHALLENGE"],
defaultDifficulty: "EXPERT",
difficultyColours: {
BASIC: COLOUR_SET.orange,
DIFFICULT: COLOUR_SET.red,
EXPERT: COLOUR_SET.green,
CHALLENGE: COLOUR_SET.purple,
},
grades: [
"D",
"D+",
"C-",
"C",
"C+",
"B-",
"B",
"B+",
"A-",
"A",
"A+",
"AA-",
"AA",
"AA+",
"AAA",
],
gradeColours: {
D: COLOUR_SET.gray,
"D+": COLOUR_SET.maroon,
"C-": COLOUR_SET.red,
C: COLOUR_SET.purple,
"C+": COLOUR_SET.vibrantPurple,
"B-": COLOUR_SET.paleBlue,
B: COLOUR_SET.blue,
"B+": COLOUR_SET.vibrantBlue,
"A-": COLOUR_SET.paleGreen,
A: COLOUR_SET.green,
"A+": COLOUR_SET.vibrantGreen,
"AA-": COLOUR_SET.paleOrange,
AA: COLOUR_SET.orange,
"AA+": COLOUR_SET.vibrantOrange,
AAA: COLOUR_SET.gold,
},
clearGrade: "A",
gradeBoundaries: [0, 55, 59, 60, 65, 69, 70, 75, 79, 80, 85, 89, 90, 95, 99],
lamps: [
"FAILED",
"CLEAR",
"LIFE4",
"FULL COMBO",
"GREAT FULL COMBO",
"PERFECT FULL COMBO",
"MARVELOUS FULL COMBO",
],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.paleGreen,
LIFE4: COLOUR_SET.orange,
"FULL COMBO": COLOUR_SET.paleBlue,
"GREAT FULL COMBO": COLOUR_SET.green,
"PERFECT FULL COMBO": COLOUR_SET.gold,
"MARVELOUS FULL COMBO": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: true,
judgementWindows: [
{ name: "MARVELOUS", msBorder: 15, value: 3 },
{ name: "PERFECT", msBorder: 30, value: 2 },
{ name: "GREAT", msBorder: 59, value: 1 },
{ name: "GOOD", msBorder: 89, value: 0 },
{ name: "BAD", msBorder: 119, value: 0 },
],
judgements: ["marvelous", "perfect", "great", "good", "boo", "miss", "ok", "ng"],
defaultTable: "Levels (N-1)",
scoreBucket: "lamp",
},
"maimai:Single": {
idString: "maimai:Single",
percentMax: 120, // a safe estimate?
defaultScoreRatingAlg: "ktRating",
defaultSessionRatingAlg: "ktRating",
defaultProfileRatingAlg: "ktRating",
difficulties: ["Easy", "Basic", "Advanced", "Expert", "Master", "Re:Master"],
defaultDifficulty: "Master",
difficultyColours: {
Easy: COLOUR_SET.blue,
Basic: COLOUR_SET.green,
Advanced: COLOUR_SET.orange,
Expert: COLOUR_SET.red,
Master: COLOUR_SET.purple,
"Re:Master": COLOUR_SET.white,
},
grades: ["F", "E", "D", "C", "B", "A", "AA", "AAA", "S", "S+", "SS", "SS+", "SSS", "SSS+"],
gradeColours: {
F: COLOUR_SET.gray,
E: COLOUR_SET.red,
D: COLOUR_SET.maroon,
C: COLOUR_SET.purple,
B: COLOUR_SET.paleGreen,
A: COLOUR_SET.green,
AA: COLOUR_SET.paleBlue,
AAA: COLOUR_SET.blue,
S: COLOUR_SET.gold,
"S+": COLOUR_SET.vibrantYellow,
SS: COLOUR_SET.paleOrange,
"SS+": COLOUR_SET.orange,
SSS: COLOUR_SET.teal,
"SSS+": COLOUR_SET.white,
},
clearGrade: "A",
// @hack Maimai's top grade depends on the chart's maximum percent
// we just set it at percentMax, but it's not technically correct
gradeBoundaries: [0, 10, 20, 40, 60, 80, 90, 94, 97, 98, 99, 99.5, 100, 120],
lamps: ["FAILED", "CLEAR", "FULL COMBO", "ALL PERFECT", "ALL PERFECT+"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.green,
"FULL COMBO": COLOUR_SET.blue,
"ALL PERFECT": COLOUR_SET.gold,
"ALL PERFECT+": COLOUR_SET.teal,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["perfect", "great", "good", "miss"],
defaultTable: "Levels",
scoreBucket: "grade",
},
"gitadora:Gita": {
idString: "gitadora:Gita",
percentMax: 100,
defaultScoreRatingAlg: "skill",
defaultSessionRatingAlg: "skill",
defaultProfileRatingAlg: "skill",
difficulties: [
"BASIC",
"ADVANCED",
"EXTREME",
"MASTER",
"BASS BASIC",
"BASS ADVANCED",
"BASS EXTREME",
"BASS MASTER",
],
defaultDifficulty: "EXTREME",
difficultyColours: {
BASIC: COLOUR_SET.blue,
ADVANCED: COLOUR_SET.orange,
EXTREME: COLOUR_SET.red,
MASTER: COLOUR_SET.purple,
"BASS BASIC": COLOUR_SET.vibrantBlue,
"BASS ADVANCED": COLOUR_SET.vibrantOrange,
"BASS EXTREME": "todo", // colourset vibrant red
"BASS MASTER": COLOUR_SET.vibrantPurple,
},
grades: ["C", "B", "A", "S", "SS", "MAX"],
gradeColours: {
C: COLOUR_SET.purple,
B: COLOUR_SET.blue,
A: COLOUR_SET.green,
S: COLOUR_SET.orange,
SS: COLOUR_SET.gold,
MAX: COLOUR_SET.white,
},
clearGrade: "A",
gradeBoundaries: [0, 63, 73, 80, 95, 100],
lamps: ["FAILED", "CLEAR", "FULL COMBO", "EXCELLENT"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.blue,
"FULL COMBO": COLOUR_SET.teal,
EXCELLENT: COLOUR_SET.gold,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["perfect", "great", "good", "ok", "miss"],
defaultTable: "Levels (N-1)",
scoreBucket: "grade",
},
"gitadora:Dora": {
idString: "gitadora:Dora",
percentMax: 100,
defaultScoreRatingAlg: "skill",
defaultSessionRatingAlg: "skill",
defaultProfileRatingAlg: "skill",
difficulties: ["BASIC", "ADVANCED", "EXTREME", "MASTER"],
defaultDifficulty: "EXTREME",
difficultyColours: {
BASIC: COLOUR_SET.blue,
ADVANCED: COLOUR_SET.orange,
EXTREME: COLOUR_SET.red,
MASTER: COLOUR_SET.purple,
},
grades: ["C", "B", "A", "S", "SS", "MAX"],
gradeColours: {
C: COLOUR_SET.purple,
B: COLOUR_SET.blue,
A: COLOUR_SET.green,
S: COLOUR_SET.orange,
SS: COLOUR_SET.gold,
MAX: COLOUR_SET.white,
},
clearGrade: "A",
gradeBoundaries: [0, 63, 73, 80, 95, 100],
lamps: ["FAILED", "CLEAR", "FULL COMBO", "EXCELLENT"],
lampColours: {
FAILED: COLOUR_SET.red,
CLEAR: COLOUR_SET.blue,
"FULL COMBO": COLOUR_SET.teal,
EXCELLENT: COLOUR_SET.gold,
},
clearLamp: "CLEAR",
supportsESD: false,
judgements: ["perfect", "great", "good", "ok", "miss"],
defaultTable: "Levels (N-1)",
scoreBucket: "grade",
},
};
```
@@ -0,0 +1,26 @@
# Search Implementation
Tachi's search implementation uses MongoDB's $text index. This breaks a query into words
and compares each of them to the provided text fields.
*****
## __textScore
For our code, we mutate the documents we want to return with a special field: `__textScore`.
This field declares how 'close' the provided query was to the $text fields in this document.
This is sometimes exposed in the API for sorting reasons.
!!! bug
MongoDB's $text matching algorithm isn't great for fuzzy matches - It doesn't
like song titles like 'A', as it thinks 'a' is an article, and doesn't match it properly as
a word.
!!! info
Why not regex for fuzzy matches?
Regex has performance issues on larger datasets and we
want to avoid it. Most regexes cannot use indexes, and therefore invoke a COLLSCAN, which
we want to avoid.
@@ -0,0 +1,137 @@
# Songs And Charts
Tachi structures its song and chart data in a specific
manner in order to avoid copying properties all over
the place.
*****
## Songs
Songs look like this:
```json
{
"title": "5.1.1.",
"artist": "dj nagureo",
"id": 1,
"firstVersion": "0",
"alt-titles": [],
"search-titles": [],
"data": {
"genre": "PIANO AMBIENT"
}
}
```
They're relatively small documents, and contain some metadata.
Depending on the game, the `data` prop will contain
game-specific properties (i.e. not all games have
song genres!)
Songs, however, don't contain any information about
their *charts*. The charts are what people actually play.
In short, songs are *just* a collection of metadata that
parents a chart document!
*****
## Chart Documents
Chart Documents **MUST** belong to a song. In the below
chart, `songID` refers to the above song document:
```json
{
"rgcID": null,
"chartID": "c2311194e3897ddb5745b1760d2c0141f933e683",
"difficulty": "ANOTHER",
"songID": 1,
"playtype": "SP",
"levelNum": 10,
"level": "10",
"flags": {
"IN BASE GAME": true,
"OMNIMIX": true,
"N-1": true,
"2dxtra": false
},
"data": {
"inGameID": 1000,
"notecount": 786,
"arcChartID": "CYjwAuz7Yq9"
},
"isPrimary": true,
"versions": [
"27-omni",
"26-omni",
"27",
"26",
"inf",
"16-cs",
"12-cs",
"10-cs",
"8-cs",
"7-cs",
"bmus"
]
}
```
Songs can have multiple charts, but charts can only have
one song.
## Primary
Sometimes, games like to rechart things and release them
under the exact same song. Sometimes, they even do this
under the *exact* same internal songID!
A 'Primary' chart refers to a chart that is the *current*
variant of that chart for this game. Non-Primary charts
are not eligible for any rating calculations, nor do they
contribute to profile rating.
To check if a chart is primary or not, the `isPrimary` property
handles that.
For example:
```json
{
"rgcID": null,
"chartID": "103ff8bb004e1a8a005f808c025c3feb",
"difficulty": "ANOTHER",
"songID": 1,
"playtype": "SP",
"levelNum": 5,
"level": "5",
"flags": {
"IN BASE GAME": true,
"OMNIMIX": true,
"N-1": true,
"2dxtra": false
},
"data": {
"inGameID": 1000,
"notecount": 433,
"arcChartID": "foobar"
},
"isPrimary": false,
"versions": [
"1"
]
}
```
Notice that the chartID is different, and `isPrimary`
is set to false.
Even though this chart is "5.1.1 (SP ANOTHER)", it isn't
the primary chart for this songID + playtype + difficulty.
For the UI, Tachi will hide non-primary charts by default.
Realistically, they only exist to support legacy scores
without having to throw them away when a rechart occurs.
@@ -0,0 +1,178 @@
# Statistic Implementation
This page documents the maths behind various algorithms
in Tachi.
*****
## BPI
Our implementation of Poyashi BPI is leveraged from [here](https://github.com/potakusan/iidx_score_manager/blob/f21ba6b85fcc0bf8b7ca888fa2239a3951a9c9c2/src/components/bpi/index.tsx#L120).
To be honest, I do not really understand *why* BPI looks
like this. I couldn't justify basically any line of this
function, nor any of the magic numbers it references.
## MFCP
MFCP is implemented as follows.
If the score is not an MFC, it is worth `null`.
If the score on a BEGINNER or BASIC chart, it is worth `null`.
If the level of the chart is worth less than 8, it is worth `null`.
Else, it follows this table:
| Levels | MFCP |
| :: | :: |
| 8, 9, 10 | 1 |
| 11, 12 | 2 |
| 13 | 4 |
| 14 | 8 |
| 15 | 15 |
| 16, 17, 18, 19, 20 | 25 |
## VF6
VF6 is calculated as follows.
The grade of the score is converted into a coefficent
according to this table.
```ts
const VF5GradeCoefficients = {
S: 1.05,
"AAA+": 1.02,
AAA: 1.0,
"AA+": 0.97,
AA: 0.94,
"A+": 0.91, // everything below this point (incl. this) is marked with a (?) in bemaniwiki.
A: 0.88,
B: 0.85,
C: 0.82,
D: 0.8,
};
```
Lamps are converted into a coefficent similarly.
```ts
const VF5LampCoefficients = {
"PERFECT ULTIMATE CHAIN": 1.1,
"ULTIMATE CHAIN": 1.05,
"EXCESSIVE CLEAR": 1.02,
CLEAR: 1.0,
FAILED: 0.5,
};
```
Then, we perform the following calculation:
$$
f(l, p, c1, c2) = 2l * p * c1 * c2 * 0.01
$$
Where L is the chart's level, P is the percent of the score,
C1 is the grade coefficent, and C2 is the lamp coefficient.
For VF6, this result is returned floored to 3 decimal places.
For VF5, this result is returned floored to **2** decimal places.
## KtRating
KtRating is a generic exponential algorithm that takes
three tunable parameters. These are changed depending
on the game that uses this algorithm.
!!! note
This is not meant to be a perfect algorithm for all
scenarios. It's meant to cover for games that don't
have a sensible default rating algorithm.
The three parameters are as follows:
| Parameter | Description |
| :: | :: |
| `pivotPercent` | The percent below which a score is considered a 'fail', and should be negatively punished. |
| `failHarshnessMultiplier` | By how much fails should be punished. A higher value implies fails are worth less. |
| `clearExpMultiplier` | How much to exponentially reward clears. A higher value implies that timing at the highest level is more difficult. |
If the score's percent is below the `pivotPercent`, the
fail calculator is invoked, which uses the following
function:
```ts
percentDiv100 ** (parameters.failHarshnessMultiplier * levelNum) *
(levelNum / parameters.pivotPercent ** (parameters.failHarshnessMultiplier * levelNum))
```
In mathematical notation, this is represented as:
$$
f(x, f, l, c) = \left(x^{f}\cdot\left(\frac{l}{c^{f}}\right)\right)
$$
Where X is the percent divided by 100,
F is the `failHarshnessMultiplier` multiplied by L,
L is the level of the chart and
C is the `pivotPercent`.
If the score is above the `pivotPercent`, then the
clear calculator is invoked, which uses the following
function:
```ts
Math.cosh(
parameters.clearExpMultiplier * levelNum * (percentDiv100 - parameters.pivotPercent)
) +
(levelNum - 1);
```
In mathematical notation, this is expressed as:
$$
f(x, c, l) = \cosh\left(n\left(x-c\right)\right)+l-1
$$
Where X is the percent divided by 100,
L is the level of the chart and
C is the `pivotPercent`.
Cosh is used because it's essentially e^x, and easier
to work with in this (contrived) scenario.
!!! note
L will be replaced with the timing difficulty
declared for that chart in the tierlists.
If one does not exist, it will fall back to
the provided level as a number.
## KtLampRating
Unlike KTRating, KTLampRating is a fairly sensible
function.
It has no tunable parameters.
It starts by getting the tierlist information for the
given chart.
If there is tierlist information, then we iterate
over the lamp ratings they declare.
We select the largest lamp rating that the lamp meets
the requirements of.
This is to fix things like Hard Clears sometimes being worth
less than Normal Clears (especially in IIDX). This also
means that we safely fall back to lower values if the
users lamp was unsupported - i.e. a FULL COMBO will fall
down to an EX HARD CLEAR.
If there is no tierlist data available for this chart,
we fall to a simple question of whether the score was
considered a clear or not. If it is, give the level
of the chart as a number as points.
+1 -1
View File
@@ -12,7 +12,7 @@ This function can be found at `src/lib/score-import/framework/score-import-main.
| Argument | Type | Description |
| :: | :: | :: |
| `user` | PublicUserDocument | The user that is making this import request. |
| `userIntent` | boolean | Whether this import was performed with User Intent - See [Import Types](./import-types.md#User%20Intent) |
| `userIntent` | boolean | Whether this import was performed with User Intent - See [Import Types](./import-types.md#user-intent) |
| `importType` | ImportType | What kind of import "type" this is. For more on this, see [Import Types](./import-types.md)
| `InputParser` | Function | The parser function to call. For more info, see [Parsing and Converting](./parse-conv.md)
| `providedImportObjects` (Optional) | { logger, importID } | Optionally, a logger and existing importID can be passed here. This is used for scenarios where the logger and importID have already been created before importMain was called. |
+2 -2
View File
@@ -51,7 +51,7 @@ to.
### Implementation
As an example, lets say we had a CSV being sent to us as a file.
As an example, let's say we had a CSV being sent to us as a file.
We could write a parser for that as follows:
@@ -256,7 +256,7 @@ function CreateIIDXFileParser(request) {
## Converters
Lets say we have a parser for SDVX that returns:
let's say we have a parser for SDVX that returns:
```js
[{
score: 9000000,
+1 -1
View File
@@ -75,7 +75,7 @@ You may recall that in [Parsers and Converters](./parse-conv.md) it was document
ClassHandler. The ClassHandler is intended to handle those
explicit changes.
For example, lets say we have an import type that tells us:
For example, let's say we have an import type that tells us:
```js
{
scores: [{score: 1000, songID: 1, diff: "spa"}],
@@ -0,0 +1,51 @@
# Versioning
*****
## Semver
`tachi-server` and `tachi-client` follow [Semantic Versioning](https://semver.org).
The two repositories have completely separate versioning, but generally will move in
lockstep with one-another.
!!! example
For quick reference, MAJOR, MINOR and PATCH correspond as follows:
```2.3.1 -> MAJOR.MINOR.PATCH```
## `master` Branch
Every push to `master` must involve a change to the versioning of that repository. If
it's a hotfix, it should bump the PATCH version. If it's a feature, it should update the
MINOR version.
The MAJOR version will only be bumped if there are significant changes to almost everything,
which I don't anticipate.
## Version Names
`tachi-server` and `tachi-client` have version names that change in correspondence with
their `MINOR` versions.
The version names follow the song titles of an album. In `tachi-server`'s case, it follows
[Portishead - Dummy](https://en.wikipedia.org/wiki/Dummy_(album))
For `tachi-client`, we follow [The Cure - Disintegration](https://en.wikipedia.org/wiki/Disintegration_(The_Cure_album))
### Why?
Versions following album songs saves the hassle of having to come up with nice sounding version
names. It's also an excuse to show off albums I really like.
As for why these specific albums:
Portishead's Dummy was chosen because Tachi V1 followed
[Massive Attack - Mezzanine](https://en.wikipedia.org/wiki/Mezzanine_(album)). The two albums
are both standout albums in the same genre, so it was fitting to follow it up.
The Cure's Disintegration was chosen because I wanted another album with
the same amount of tracks as Dummy, and it's also a great album.
!!! tip
These albums are great. Do yourself a favour and check them out.
+2 -1
View File
@@ -21,11 +21,12 @@ correspond to, how `thing` works, etc.
## Repos and Licenses
Tachi is made up of three components:
Tachi is made up of four components:
- `tachi-common`: Common types and values for Tachi. [GitHub](https://github.com/zkldi/tachi-common). This is licensed under MIT.
- `tachi-server`: The API, IR implementations and 'business logic' behind Tachi. [GitHub](https://github.com/zkldi/tachi-server). This is licensed under AGPLv3.
- `tachi-client`: The front-end code for Tachi. This is closed source.
- `tachi-docs`: The documentation you're reading right now! [GitHub](https://github.com/zkldi/tachi-docs). This is licensed under MIT.
### What's with `tachi-client`?
+10 -3
View File
@@ -19,7 +19,8 @@ the main benefits for us are as follows:
```js
{
MONGO_BASE_URL: "127.0.0.1",
MONGO_CONNECTION_URL: "127.0.0.1:27017",
MONGO_DATABASE_NAME: "somedb",
LOG_LEVEL: "info",
CAPTCHA_SECRET_KEY: "google_given_secret_key",
SESSION_SECRET: "some_secret_key",
@@ -43,16 +44,22 @@ the main benefits for us are as follows:
All properties are required.
### MONGO_BASE_URL
### MONGO_CONNECTION_URL
- Type: String
Where your MongoDB server is located. For most cases, this
is `127.0.0.1` or `localhost`.
is `127.0.0.1:27017` or `localhost:27017`.
For more creative scenarios such as running through WSL, you
might need to change this.
### MONGO_DATABASE_NAME
- Type: String
What collection to use for your database.
### LOG_LEVEL
- Type: Log Level
+9 -5
View File
@@ -34,6 +34,14 @@ Various scripts for interacting with `tachi-server`, such
as single-use scripts for importing some data, or
frequently used scripts such as updating BMS tables.
!!! danger
The scripts in here are not regularly maintained,
**especially** the ones inside `single-use`. You should
**ABSOLUTELY NOT** run those if you do not know what
they do.
Seriously, you could destroy your server.
## TypeScript Source Code
All of these are inside `/src`.
@@ -127,8 +135,4 @@ in cases where an API call needs to do a lot of things.
## Test Files
Test files are to be located in the same folder as the file
they're testing, and should only ever test the exports
of one file.
They should have the filename of the file they're testing, with the extension `.test.ts`.
This documentation has been moved to [its own page](./testing.md)!
+16 -7
View File
@@ -17,9 +17,12 @@ ESLint is set up to automatically perform all of these changes when ran.
## Prettier Rules
- 4 Spaces Indenting.
- Tab Indenting.
I'd prefer to use tabs, honestly, but Prettier and JSDoc like to align things with spaces and it messes with them.
<del>I'd prefer to use tabs, honestly, but Prettier and JSDoc like to align things with spaces and it messes with them.</del>
It turns out there's only one rare scenario where prettier mixes tabs and spaces (Rare as in, it happens
once in an obscure place in the entire codebase), so we've switched to tabs.
- Semicolons.
@@ -27,7 +30,8 @@ No-Semicolons causes issues with IIFEs.
- Try to keep things under 100 characters.
Absolutely do not insert random line breaks to keep stuff under 100 characters. It's fine for things to go a bit over.
Absolutely **DO NOT** insert random line breaks to keep stuff under 100 characters. It's fine for things to go a bit over.
Seriously, your editor is definitely capable of wrapping text if it goes too far.
Prettier has its own opinions on where these line breaks should happen, just trust them.
@@ -35,11 +39,11 @@ Prettier has its own opinions on where these line breaks should happen, just tru
JSON does it and that's pretty much the only reason why.
- Line Break is LF, not CRLF
- Line Break is LF, **not CRLF**
Your editor will handle this properly. If it does not
automatically set, check the bottom right of your editor.
For Atom, VSCode and some others it will let you switch between
For Atom, VSCode and most others it will let you switch between
CRLF and LF.
## Commenting Style
@@ -70,10 +74,10 @@ function sd(arr: number[]) {
This is bad code. Very bad code. It is not at all clear what
this code does from any of the variable names, and the function signature barely helps.
Lets try and make this code more self documenting.
First, let's try and make this code more self documenting.
We'll give everything proper variable names, and then
expand the second `reduce` call into a simpler for loop.
expand the second `reduce` call into a simpler for loop.[^1]
```ts
function CalculateStandardDeviation(dataset: number[]) {
@@ -137,3 +141,8 @@ The list of directives and their meaning is here:
Don't worry about this too much, At the end of the day, as
long as the code is understandable and the linter is happy,
it's good.
[^1]: JS's ES6 array methods are the devil if used improperly. For some reason, lots of people in
react and react-adjacent scenes seem to love (ab)using these array methods for everything. Complex
`reduce` operations should always be turned into a `for loop`, and that's to say nothing of my opinions
on `forEach`.
@@ -18,6 +18,16 @@ pnpm fulltest
This will execute every test, and also perform coverage
analysis.
!!! bug
This may or may not impact you depending on how
dependencies install, but `tap` requires `ts-node`
and `typescript` to also be installed.
Otherwise, you will get a
`cannot use import outside of module`
error, which indicates that our typescript hasn't
been transpiled.
## Parallel Tests
You can run tests in parallel with:
+1 -1
View File
@@ -42,7 +42,7 @@ If you want to contribute to this documentation, you can find the repository [he
If you want to contribute to the Tachi backend code, you can find the repository [here!](https://github.com/zkldi/tachi-server)
!!! note
You should read the [Contributing To Tachi-Server](#) page beforehand.
You should read the [Contributing To Tachi-Server](./codebase/contributing.md) page beforehand.
## Acknowledgements
+240
View File
@@ -0,0 +1,240 @@
# Feature List
This page lists all of the features Tachi has.
Some games may utilise these better than others.
*****
## Sessions
Sessions are a feature in Tachi that group up your
scores depending on *when* they were achieved.
Sessions are meant to mimic the colloquial use of
the term - Players typically refer to their scores
as being part of a session, so why don't we show
their scores in that fashion aswell?
### What is a session?
A session is a group of scores that all happened
around the same time.
When you get a new score (and don't have a session nearby),
a session is automatically created!
Then, if you get a score within two hours of that last score,
that score is added to the session - this will repeat
until eventually you spend more than two hours between
your score and the last score.
In practice, the only time you're spending more than
two hours between a score is when you're either not playing
anymore, or taking a significant enough break that you're
probably no longer warm.
!!! info
The reason we use the two-hour rule instead of just
splitting on a day is that not everyone lives in the
same timezone, and their session may get unexpectedly
split in two!
### Why bother?
Since players use the term "sessions" so frequently, it
makes sense to let them view scores how they already
think about them.
Advantages of sessions also include being able to name and
categorise them - so you can search back on them in the
future.
You can also share a single link to your finished session,
instead of having to squish all your great scores into one
long twitter thread!
Another small advantages include statistics - we can
calculate your "ability" that session, and graph it
over time - so you can see your real time improvement!
### What if I play multiple games?
**Sessions can only store scores of the same game and playtype**.
That is, if you have an IIDX SP session "ongoing", and get
an IIDX DP score - you will have two sessions ongoing at
the same time!
Your IIDX DP score will **not** be added to your SP session.
!!! note
For home players, this is likely to not ever be an
issue, but for arcade players - where people typically
throw a couple credits into different games every day,
it can become a problem!
### What does this performance statistic mean?
You can read all about session statistics [here](./stats/tachi.md#session-ratings).
### Summary
That's it for sessions! As a quick summary:
- Sessions group your scores based on when they've happened.
- Sessions are easy to share with other players.
- Sessions are a nice way of displaying all the scores you did!
*****
## Goals
!!! note
This feature goes hand-in-hand with [Milestones](#milestones).
Goals are a built-in way of setting targets for yourself.
A lot of the time, people have goals they're aiming for in
rhythm games, but at the moment, people generally have to
keep said goals in their head.
Tachi lets you set and track your own goals - so you don't
have to remember them!
### Advantages
With your goals set and stored, we can see other users with
similar goals - which might give you good ideas for other
goals to aim for.
You can also be automatically notified when a goal is
achieved, or when you've made some progress towards it.
If you've ever been in the middle of a session and not
known what to play, you could always look at your list of
goals and try to check off some older ones, too!
### What kind of goals can I set?
There are two different parameters that control a goal.
#### Criteria
The criteria determines what our goal actually is.
You can set goals for Lamps, Grades, Percents and score.
!!! example
Get 950'000 on FREEDOM DiVE.
SS FREEDOM DiVE.
Clear FREEDOM DiVE
Get 95% on FREEDOM DiVE.
We can also control *how many* scores need to match the
given criteria. This makes more sense when goals apply
to more than one chart.
!!! example
AAA 50 Charts in the Level 12 folder.
FULL COMBO 10% of the Level 11 folder.
Clear either FREEDOM DiVE or Blue Zenith.
#### Charts
You can also control what charts your goal applies to.
The most simple option is to only select one chart.
!!! example
Full Combo xi - FREEDOM DiVE (FOUR DIMENSIONS)
You can also select multiple, fixed charts.
!!! example
Full Combo xi - FREEDOM DiVE or NOMA - BRAIN POWER.
Get 95% on 2 of the following, FREEDOM DiVE, Elemental Creation, BRAIN POWER or Blue Zenith.
Alternatively, you can set goals on folders.
!!! example
Full combo any chart in the Level 12 folder.
Clear 50% of the Level 12 folder.
AAA 100 charts in the Level 12 folder.
Or, remove the chart restriction entirely!
!!! example
AAA any chart.
AAA 100 charts.
Full Combo Every Chart.
### Summary
That's it for goals! As a quick summary:
- Goals can be set for charts, folders, or anything!
- You can set goals for lamps, scores, percents and grades.
- You can share your goals with other users, and see what your rivals have set!
This is nice, but there's something missing...
## Milestones
As mentioned above, Goals were designed to go hand-in-hand
with this feature.
### Issues with Just Goals.
The main issue with goals on their own is that we're not
good at setting our own goals.
What if someone doesn't *really* know what they should be
aiming for, or is just lazy and doesn't like setting goals?
In general, we're bad at setting our own goals. How can
we fix that?
### Why Milestones?
Milestones are **pre-made groups of goals**. As an example,
we might bundle together some goals aimed at SOUND VOLTEX
11 dan players. You can then subscribe to that milestone,
and all those goals will be merged with your list of goals!
This solves the problem of having to set and manage *loads*
of goals on your own - since other people can come up with
and debate good goals for your skill level, and you can
just seamlessly integrate them with your play!
### Who makes the Milestones?
The community! People are free to create their own
milestones and share them.
### Why are they called Milestones?
Milestones work as - well - milestones!
They're achievable, just like goals, which means the
milestone creator can set criteria.
A basic example would be counting the milestone as
achieved when you've achieved *all* of the goals in that
milestone.
More advanced options include achieving X goals inside a
milestone, or X% of the goals inside a milestone.
### Summary
That's all for milestones! As a quick summary:
- Since milestones are achievables, you'll be automatically notified when you tick another milestone off!
- You can also see other user's milestones and their progress.
- Milestones reduce the pain of having to set your goals yourself!
+3 -3
View File
@@ -34,8 +34,8 @@ that display your scores. I think that scores are integral
to the rhythm game experience, and that displaying them
properly is *just* as important.
The benefits of Tachi include features like [Sessions](./features/sessions.md), which break your scores up into
groups of when they were played, and [Goals](./features/goals.md) which let you set automatically updating targets for yourself!
The benefits of Tachi include features like [Sessions](./features.md#sessions), which break your scores up into
groups of when they were played, and [Goals](./features.md#goals) which let you set automatically updating targets for yourself!
There are way more features that Tachi has, and you can
read about all of them [here](./features.md).
@@ -57,7 +57,7 @@ Tachi's analytics!
Yes.
You should familiarise yourself with the rules before
using any variant of Tachi.
using any distribution of Tachi.
The rules can be found [here](./rules.md).
+1 -1
View File
@@ -117,7 +117,7 @@ interactions **outside of Tachi**, which includes, **but is not limited to**:
## You are only allowed one account.
Do not make multiple accounts, we track account IPs and it
lets us know. This is to keep the leaderboards fair
let's us know. This is to keep the leaderboards fair
and avoid one player from taking multiple spaces.
## Do not set NSFW artwork as your avatar or banner.
-11
View File
@@ -1,11 +0,0 @@
# Tachi Statistics
This page serves as a glossary for all the statistics
used in Tachi, alongside their pros and cons.
*****
## IIDX
### SP
+52
View File
@@ -0,0 +1,52 @@
# What is ESD?
ESD is short for "Estimated Standard Deviation".
Standard deviation is a way of measuring how *dispersed*
data is. [Wikipedia](https://en.wikipedia.org/wiki/Standard_deviation) explains what this is and why it works.
*****
## Motivation
Standard Deviation is an interesting statistic for rhythm
games, as the dispersion of a users hits is quite interesting.
That is, if a user is hitting all over the place timing wise,
they probably aren't doing very good. Vice versa,
if a user has incredibly close together hits timing wise,
they're probably good.
ESD let's us derive standard deviation from just the *percent*
of a score, and the judgement windows for a game!
It's surprisingly accurate, and is a useful statistic
to derive other statistics from.
!!! note
Only some games support ESD. These are only games that
have strict hit windows that correlate perfectly with
percent.
An example would be IIDX, where percent is only
derived from EX Score, and hit windows are constant.
BMS cannot support ESD as it has dynamic hit windows,
depending on the chart.
GITADORA cannot support ESD as it's percent is influenced
by combo-based scoring.
SDVX cannot support ESD because things like holds
count as multiple repeated hits, but you do not have
to time those repeated hits!
In short, one hit needs to correspond to one judgement,
and every hit has to involve a timing window.
For details on the implementation of ESD, you can see
[here](../../codebase/implementation-details/esd.md).
!!! warning
Implementation details about ESD require some
external knowledge about statistics and distributions.
+585
View File
@@ -0,0 +1,585 @@
# Tachi Statistics
Tachi has a lot of statistics involved. This page documents all of the
statistics used for each game, and what they mean.
!!! note
These explainations brush over the technical details a bit. If you're interested
in that, you might want to see the [Implementation Details](../../codebase/implementation-details/statistics.md).
*****
## Statistics Overview
Tachi needs statistics in three main places. The first
place is on each *individual* score.
As an example, if I get a score, it should have a rating
attached onto it.
The second place it needs a rating algorithm is for
a user's profile. This is likely to be combined from
individual score statistics, either by averaging or
totalling.
The third place it needs a rating algorithm is for
a user's sessions. This is also likely to be combined
from individual score statistics.
## Score Statistics
The below statistics apply to individual scores.
!!! note
Every game needs to have a default rating algorithm. A default rating
algorithm needs to work well on all scores from all skill levels.
This means that some more 'pro' oriented statistics cannot be the default.
Some games might not have *any* rating algorithms. If we dont have
a good built-in contender for a default rating algorithm, we have to invent our own.
*****
### ktRating (IIDX)
- **Default for IIDX SP and IIDX DP**
Pros:
- Works on all scores.
- Takes tierlists into account.
- Accuracy Based.
Cons:
- Has a poor understanding of high acc levels (MAX- and beyond).
KtRating (Kamaitachi Rating) is our custom rolled rating algorithm for IIDX.
The issue we have is that IIDX doesn't have any metrics that are a good contender
for our default rating algorithm.
DJ Points is worthless because it depends on the notecount of the chart instead of its level
and BPI only really has any meaning to kaidens, so we're stuck having to roll our own statistic.
KtRating was designed to be a generic **timing** rating algorithm for IIDX. It isn't perfect
by any means, and is generally worse than BPI at 12s. It also struggles with things like
MAX- and stronger scores.
Regardless, it works decently for most charts and most players. It uses a curve such that
an AA on a chart rated X will be worth X points.
!!! note
X will be influenced by the tierlist. For example, mosaic SPA is rated a 12 internally,
but has a lower tierlist timing rating value of around 10.5.
*****
### BPI (IIDX)
Pros:
- Estimates timing difficulty for a chart reasonably.
- Is an understood standard by Kaiden and Post-Kaiden players.
Cons:
- Depends on Kaiden Average and World Record, which can be highly fluctuative.
- Not able to accurately cross-compare values (i.e. 20BPI on one song is often not equivalent in 'skill' to 20BPI on another.)
- Only practically works on 12s. It can be extended to 11s, but it doesn't work as well. It does not work at all below 11.
There are two implementations of BPI - We'll call them Nori BPI and Poyashi BPI, Tachi uses the
more recent Poyashi BPI.
!!! info
As mentioned above in the cons of BPI, Kaiden Average and WR can be rather unreliable estimates
of difficulty. When HV came out, 120hz caused almost all WRs to jump up significantly, which
pretty much completely broke Nori's BPI. To fix this, some parameters were adjusted for
poyashi's BPI, which is implemented [here](https://bpi.poyashi.me).
Namely, reducing the impact of high WRs on average BPI.
As a consequence, Poyashi BPI is significantly easier than Nori BPI in every circumstance.
Whether this is an issue or not is up to you, and I think most players enjoy seeing the
larger number.
BPI looks at the kaiden average and world record for a chart, and constructs an exponential
graph between the two points. A BPI of 0 is equivalent to Kaiden Average, a BPI of 100 is
equivalent to the world record.
!!! info
For scores less than the Kaiden Average, Poyashi BPI uses a negative extension that caps
at -15. This appears to be arbitrary.
For Nori BPI, scores less than the Kaiden Average become [Complex Numbers](https://en.wikipedia.org/wiki/Complex_number).
BPI is intended for use for kaidens and players significantly beyond kaiden. For that, it works
decently. As mentioned above in the cons, BPI is not very cross-comparable. 20BPI on one song
is not necessarily as good as 20BPI on another.
!!! example
20BPI on Verflucht Leggendaria is AAA+66. 20BPI on FAKE TIME is also AAA+66.
*****
### ktLampRating (IIDX)
Pros:
- Corresponds 100% with well-agreed-upon tierlists
- Works on all charts, and uses tierlists down to SP10.
Cons:
- Does not support additional points for Full Combos (will give EXHard points) or Easy Clears (will give 0).
KtLampRating (Kamaitachi Lamp Rating) is a generic algorithm that gives points based
on the quality of the lamp.
This is entirely done with tierlists that are converted into decimal form. So, if a chart
is marked as 11.3 for Hard Clear, HCing it will give 11.3 points.
BP is not taken into account, and the only lamps that give rating are Normal, Hard and EXHard.
!!! note
In the scenario where, say, a NC is worth more than a HC, HCing the chart will give the NC rating.
*****
### VF6 (SDVX, USC)
- **Default for SDVX and USC**
Pros:
- Built-in to the game, and understood by all players.
- Unlike VF4, doesn't massively reward fails on high level charts
- Unlike VF5, has more than 33 unique values.
Cons:
- Values on an individual score are small decimals, which can be difficult to parse.
VF6 (Volforce 6) is the Volforce algorithm used in SDVX 6.
This algorithm is identical to VF5, but with the addition
of another decimal place. This fixes a long standing issue
with VF5 where there were only 33 possible values for a
given score, which made the function painfully discrete.
!!! note
VF5 and VF4 are deprecated in Tachi, and not displayed
anywhere.
VF5 is deprecated because VF6 is strictly better.
VF4 is deprecated because it's 4 years old at this
point, and its flaws make it incredibly abusable.
An implementation of them is still in the
codebase, but is unused and commented out.
*****
### ktRating (DDR)
- **Default for DDR SP and DDR DP**
Pros:
- Works on all scores.
- Takes tierlists into account.
Cons:
- Isn't good at evaluating scores at all.
In a similar vein to [IIDX's KTRating](#ktrating-iidx),
we need a generic rating algorithm for DDR's scores.
However, I don't play DDR at all, and have no idea
of how to properly rate scores.
!!! help
If you want to help out with Tachi, have a bit
of maths knowledge and also play DDR, feel free
to make an issue describing a new default DDR
algorithm.
*****
### MFCP (DDR)
Pros:
- Understood by the community because of its inclusion in [LIFE4](https://life4ddr.com/).
Cons:
- Too Discrete.
- Only applies to a very small subset of scores.
MFCP (Marvelous Full Combo Points) are a scoring system
used by LIFE4 for its challenges. As the name implies,
you only get points for MFCs on a chart. And of that, the
chart must be DIFFICULT or higher, and rated higher than
level 8.
As such, it only applies to a rather small subset of
players and an even smaller subset of their scores.
*****
### KtRating (maimai)
- **Default for maimai**
Pros:
- Works for all scores.
- Doesn't reward weak passes on high rated charts.
- Takes tierlists into account.
Cons:
- Not well understood by players.
- Not amazingly accurate when it comes to high acc scores.
- Hypothetically broken by charts with lots (lots) of breaks.
In a similar vein to [IIDX's KTRating](#ktrating-iidx),
we need a generic rating algorithm for maimai's scores.
Maimai has a built-in rating algorithm, but it is not
implemented, nor is it known to me how it works.
This is an adapted version of IIDX's KTRating to work
for maimai. Since maimai is a very accuracy oriented game,
it punishes low-accuracy scores heavily.
Scores below 90% are heavily nerfed. Timing is rewarded
significantly. Clear type is ignored.
### KtRating (MÚSECA)
- **Default for MÚSECA**
Pros:
- Works on all scores.
- Doesn't reward weak passes on high rated charts.
- Takes tierlists into account.
Cons:
- Not well understood by players.
In a similar vein to [IIDX's KTRating](#ktrating-iidx),
we need a generic rating algorithm for MÚSECA's scores.
MÚSECA's built-in CURATOR RANK is built in a similar
vein to Volforce, but is broken by some
questionable chart rating decisions. This makes it
undesirable for score comparison.
This is an adapted version of the IIDX KTRating
algorithm (again!), with parameters tuned for MÚSECA.
Scores below 900k are heavily nerfed. Timing is rewarded
significantly. Clear type is ignored.
### Sieglinde (BMS)
- **Default for BMS 7K and BMS 14K**
Pros:
- Derives how difficult a lamp is to get by scores on LR2IR and Mocha IR.
- An update to walkure without certain vulnerabilities.
- Only gives rating for popular tables.
Cons:
- Does not support Groove Clears, EX Hard Clears or FCs.
- Likely contentious for individual difference stuff. (What algorithm isn't!)
Sieglinde is a modern implementation of Walkure which
aims to fix a couple of issues with Walkure as an
individual score rating algorithm.
It uses data from IRs to derive an EC and HC value for
a chart, which is then given if you get EC or HC
respectively.
!!! note
Due to poor data on LR2IR, and complete lack of support
in LR2, Groove Clears and EX Hard Clears will be
treated as Easy Clears and Hard Clears, respectively.
Full Combos are similarly removed due to incredibly
poor data. Grinding Full Combos on low level insane
charts was the best way to raise your walkure!
!!! info
For 7K, the currently supported tables are:
- Insane1
- Insane2
- Normal1
- Normal2
- Satellite
- Stella
- Overjoy
For 14K, the currently supported tables are:
- Normal
- Insane
- Satellite
### Rating (CHUNITHM)
- **Default for CHUNITHM**
Pros:
- Built-in to the game.
- Universally understood by players.
Cons:
- ???
Rating refers to CHUNITHM's built-in rating algorithm.
This takes into account an internal tierlist, and looks
at the accuracy of the provided score.
!!! note
I don't play CHUNITHM, so I don't really know the
implementation flaws of this algorithm for individual
scores.
I imagine it probably overrates fails on high level
charts.
### Skill (Gitadora)
- **Default for GITADORA**
Pros:
- Built-in to the game.
- Universally understood by players.
Cons:
- Algorithm is very naive, and doesn't increase exponentially with respect to accuracy.
Gitadora's Skill algorithm is the in-game algorithm
for determining a players 'skill' on a given chart.
The algorithm itself is incredibly simple, and likely
breaks horribly for a lot of scenarios, but, it works
decently for what it is.
!!! note
Unlike almost every other algorithm on this page,
the GITADORA Skill algorithm can be expressed in a single line.
$$
f(p,l) = 0.01p * 20l
$$
Where P is the users percent, and L is the level
of the chart.
*****
## Profile Statistics
This section explains the statistics on a user's profile.
These are almost always derived from individual score
statistics as listed above.
*****
### IIDX
The below statistics apply to SP and DP.
- BPI
The average of your highest 20 BPIs.
- KtRating **(Default)**
The average of your highest 20 ktRatings.
- KtLampRating
The average of your highest 20 ktLampRatings.
*****
### SDVX, USC
The below statistics apply to both SDVX and USC.
- VF6 **(Default)**
Your highest 50 VF6s added together. This has the benefit
of making VF6 not a small decimal, and is generally how
people talk about their volforce.
*****
### DDR
The below statistics apply to both SP and DP.
- MFCP
**ALL** of your MFCP added together.
- KtRating **(Default)**
The average of your highest 20 ktRatings.
*****
### maimai, museca
The below statistics apply to maimai and museca.
- KtRating **(Default)**
The average of your highest 20 ktRatings.
*****
### BMS
- Sieglinde **(Default)**
The average of your best 50 Sieglinde scores.
*****
### CHUNITHM
- Naive Rating **(Default)**
The average of your highest 20 CHUNITHM Ratings.
!!! warning
This is *different* to what you'd expect! CHUNITHM
has a built-in profile rating mechanism, but it has
some awful flaws with respect to losing rating after
playing poorly, and is generally *very* hard to
implement.
*****
### GITADORA
- Skill **(Default)**
Your profile skill as it appears in game. This is the
sum of all your skills on 50 HOT songs and 50 'NOT HOT'
songs.
!!! info
This might be slightly different to your in-game
skill. This may be due to rerates or things like
certain songs no longer being hot.
*****
## Session Ratings
This section explains the statistics on a session.
These are almost always derived from individual score
statistics as listed above.
If a session has less than 10 scores, all of the below
statistics are marked as N/A[^1] except for MFCP.
*****
### IIDX
The below information applies to SP and DP.
- BPI, KtRating, KtLampRating
The average of your highest 10 values for that statistic.
*****
### SDVX, USC
- VF6
The average of your highest 10 VF6's that session.
- Profile VF6 **(Default)**
The above statistic, but multiplied by 50. This is
to scale it up to what you'd normally see on a profile.
The reason for this multiplication is that, generally,
people don't like dealing with decimals. Furthermore,
SDVX players generally talk about their profile volforce,
rather than their individual volforce.
*****
### DDR
The below information applies to SP and DP.
- MFCP
The total MFCP you achieved this session.
!!! warning
Unlike all other statistics, this is not marked
as N/A if you have less than 10 scores.
- KtRating **(Default)**
The average of the highest 10 KtRatings that session.
*****
### maimai, MÚSECA
- KtRating **(Default)**
The average of the highest 10 KtRatings that session.
### BMS
The below information applies to both 7K and 14K.
- Sieglinde **(Default)**
The average of the highest 10 Sieglinde ratings that session.
### CHUNITHM
- Naive Rating **(Default)**
The average of the highest 10 ratings that session.
### GITADORA
- Skill **(Default)**
The average of the highest 10 skills achieved that session.
!!! note
Unlike SDVX, we do not have a ProfileSkill here.
There's no good reason for this, though.
If people want it, it can be added! Feel
free to report it as an issue if you think it
should be added.
[^1]: Internally, they are marked as `null`.
+20
View File
@@ -0,0 +1,20 @@
# What are Lamps?
Lamps are a term borrowed mainly from Arcade rhythm games.
Their name is derived from the visual implementation in
IIDX and DDR, where a lamp would light up a specific
colour depending on your, well, lamp.
A synonym for this that might make more sense is "clear type", and some games use this name internally.
*****
## Examples
In most games, you have a "FULL COMBO", which means you
didn't miss. That would be a different lamp to a "CLEAR",
which would be - well - a clear.
Another common lamp is "FAILED", which is exactly what
you think it would be.
+16
View File
@@ -0,0 +1,16 @@
window.MathJax = {
tex: {
inlineMath: [["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true,
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex",
},
};
document$.subscribe(() => {
MathJax.typesetPromise();
});
+38 -1
View File
@@ -29,6 +29,14 @@ nav:
- "user/overview.md"
- "user/rules.md"
- "user/games.md"
- "user/features.md"
- Terminology:
- "user/terminology/lamps.md"
- Statistics:
- "user/stats/tachi.md"
- "user/stats/esd.md"
- API Reference:
- "api/overview.md"
@@ -39,6 +47,9 @@ nav:
- "api/routes/status.md"
- "api/routes/import.md"
- "api/routes/auth.md"
- "api/routes/users.md"
- "api/routes/user-gamept.md"
- Codebase Reference:
- "codebase/overview.md"
- "codebase/contributing.md"
@@ -51,11 +62,15 @@ nav:
- "codebase/infrastructure/toolchain.md"
- "codebase/infrastructure/logging.md"
- "codebase/infrastructure/branches.md"
- "codebase/infrastructure/testing.md"
- "codebase/infrastructure/versions.md"
- Structure:
- "codebase/structure/style.md"
- "codebase/structure/filesystem.md"
- "codebase/structure/testing.md"
- BATCH-MANUAL:
- "codebase/batch-manual/overview.md"
- Score Importing:
- "codebase/import/overview.md"
@@ -73,6 +88,19 @@ nav:
- "codebase/import/milestones.md"
- "codebase/import/import-doc-time.md"
- Implementation Details:
- "codebase/implementation-details/details.md"
- "codebase/implementation-details/search.md"
- "codebase/implementation-details/statistics.md"
- "codebase/implementation-details/songs-charts.md"
- "codebase/implementation-details/game-configuration.md"
- "codebase/implementation-details/esd.md"
- Documents:
- "codebase/documents/overview.md"
- "codebase/documents/user.md"
- "codebase/documents/score.md"
markdown_extensions:
- admonition
- pymdownx.highlight
@@ -80,3 +108,12 @@ markdown_extensions:
- abbr
- pymdownx.snippets
- footnotes
- toc:
toc_depth: 2
permalink: true
- pymdownx.arithmatex:
generic: true
extra_javascript:
- https://polyfill.io/v3/polyfill.min.js?features=es6
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js