feat: first overhaul

This commit is contained in:
zkldi
2023-01-23 14:43:35 +00:00
parent 16b875c9b4
commit 377df6495b
53 changed files with 193 additions and 1510 deletions
@@ -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,22 @@
# 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 unfinished.
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).
*****
This documentation is unfinished. It is probably easier for you to [just read the config.ts file](https://github.com/TNG-dev/Tachi/tree/staging/common/src/config/config.ts)
@@ -0,0 +1,18 @@
# Goal ID implementation
Goal IDs exist to dedupe goals when a user creates
a new goal. For example, if a user wants to create a
HARD CLEAR Mei goal, but one already exists, we should
not insert two [Goal Documents](../../schemas/goal.md)
representing the same thing.
*****
## Hashing
To create the Goal ID, we perform a [json stable hash](https://github.com/zkldi/fast-json-stable-hash) on the
`game`, `playtype`, `charts` and `criteria` of this field.
This enforces that goals are unique on those fields.
The above hash is then returned, prefixed with `G`.
@@ -0,0 +1,115 @@
# Goals, Quests, Questlines
This page covers pretty much everything you need to know about interacting with goals
and quests.
Although these features are fairly straightforward on the surface, their implementation is a bit complex at points. We'll get to it.
## What is a Goal?
A goal is a set of instructions that are evaluated for a player if they are subscribed to it (and it is relevant to their current import, see [Getting Relevant Goals](../import/goals.md)).
!!! example
An example goal would be something like "AAA 100 charts in the X Folder".
Users must subscribe to goals for them to be evaluated when they import scores. This is
abstracted away from the user, as the operation to create a goal will automatically subscribe
them to the same goal.
### Identification
Goals are identified by a checksum of their instructions. If two users make the goal to "AAA Freedom Dive", the goalID will be the same.
This allows us to do things like see what goals are popular, and reduce the general duplication on the database.
## User Subscriptions
Users may create and subscribe to goals. This is done with the [UGPT Create Goals Endpoint](TODO). If they create a goal that already exists, they are just subscribed to that goal.
If they create a goal that does not already exist, it is created, and they are subscribed.
Users may unsubscribe from goals that they no longer care about getting pinged for.
Subscriptions to goals are stored in a [GoalSubscriptionDocument](../../schemas/goal-sub.md).
This document is uniquely identified by the joining of the `goalID` with the `userID`.
### Instant Direct Achievements
If a user creates/subscribes to a goal that they would instantly achieve, they are denied
from adding goal directly.
### Evaluating A User's Progress
Goals have two main components in terms of evaluation -- `progress` and `outOf`. If the progress is greater than or equal to the `outOf`, the goal is marked as achieved.
Both of these values are numbers, and are not intended for human consumption. For example, the goal 'HARD CLEAR Freedom Dive' would be represented by an `outOf` of `6`, which is the internal lampIndex for a BMS HARD CLEAR.
For human representation, these values are formatted and stored in the Goal Subscription Document as `progressHuman` and `outOfHuman`, respectively. These values are prettified for human users, and leverage game-specific formatting in some cases.
!!! note
Achieved goals are still calculated -- a user may actually find themselves un-achieving goals in some stranger cases.
If their goal is something like AAA 10% of a folder, and the folder gets larger, they
may find themselves losing the achieved status on this goal.
!!! warning
Goal Subscriptions are capped by the MAX_GOAL_SUBSCRIPTIONS [conf.json5](../setup/config.md) property, and defaults to 1000.
!!! danger
Unsubscribing from goals directly is only possible if they do not have any parent quests.
More on that below.
## What is a Quest?
Quests are groups of goals, defined in a structured form, with support for assigning
notes next to goalIDs, and other generally useful stuff.
Their purpose for users is to give them a quick way to assign a bunch of goals they might care about.
For example, a dedicated user may make their own "Kaiden Checklist", and create a set of goals
they think are useful for kaidens to aim for. Another user interested in this can just
subscribe to that quest, and all the goals will be managed for them.
### Identification
Quests are identified by their `questID`, which is a completely arbitrary string.
At the moment, users cannot create quests directly, they are hard-defined by the [Database Seeds](../infrastructure/database-seeds.md).
### Evaluating a User's Progress
Similarly to goals, user's can subscribe to quests.
Quest subscriptions are interesting in that the `progress` factor is always an integer -- how many goals in the quest the user has achieved, and the `outOf` factor is always an integer -- how many goals need to be achieved in the quest for the quest to be marked as achieved.
Similarly, the `achieved` status on quests is identical to `progress` being greater than or equal to `outOf`.
### Parenting Goal Subscriptions
When a quest is subscribed to, all the goals in the quest are also subscribed to.
When a user is subscribed to a quest, they *must* also be subscribed to all the goals in
that quest. If they aren't, they've desynced with the quest, and that's an awful
user experience.
As such, Tachi keeps track of all the quests that care about this goal subscription, and users are *prevented from unsubscribing from this goal* while any of their quest subscriptions parent the goal.
### Instant Indirect Achievements
Subscribing to a quest *mandates* that all of the goals are assigned. It is not rare for this to mean that you subscribe to goals that are instantly achieved.
This is fine. If this does happen though, `wasInstantlyAchieved` is set on the goal, and no
webhook event is emitted.
### Unsubscribing
For all `goalID`s in the quest, we check if this goal subscription has any other parent quests.
If this quest was the only reason the goal was assigned (i.e. it wasn't assigned directly, or isn't part of another quest), this quest will be unsubscribed from.
## Questlines
Questline are ordered lists of quests. Their purpose is to group quests together
visually in an ordered manner.
Users *can not* "subscribe" to questlines, but they can use questlines as a utility for subscribing to all the related quests.
@@ -0,0 +1,43 @@
# Score ID implementation
Score IDs exist to dedupe scores when a user re-submits
the same scores. This happens frequently with `file/`
and `api/` [Import Types](../import/import-types.md),
as they typically resubmit the same scores.
If a user only got a score once, we don't want to store
it twice.
Sadly, we can't depend on things like timestamps to assert
whether or whether not we've saw a score before. Many services
alter/tamper their timestamps such that they're unreliable.
!!! example
E-Amusement IIDX CSVs will change the timestamp of every single
score when a new version comes out to the second the user created
their new account.
*****
## Hashing
The score ID is created by joining the following properties:
- The `userID` that got this score
- The `chartID` that this score was on
- All [Provided Metrics](todo) for this GPT
- Any [Optional Metrics](todo) that have been marked as `partOfScoreID`
and then hashing them with SHA256.
This is then prefixed with `T`, and returned.
## Clobbering
Since a scoreID isn't necessarily all of the possible statistics for a score, it's
possible for users to "clobber" their scores, by importing a score without as many
pieces of info (i.e. no judgements), then trying to import that same score with
judgements later will not work, as the scoreID sees it as a duplicate.
See [Clobbering](todo).
@@ -0,0 +1,43 @@
# 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.
## User Searching
Searching users, on the other hand, has to use regex-based
searching.
The `$text` method attempts to break things up based on their
words, but that doesn't help with usernames, as they are all
too frequently `XxX_One_Long_Str1ng_xXx`.
Instead, we use a case insensitive regex - similar to SQL's
`LIKE`.
This means we do not have a `__textScore` property for this
search to sort on. Instead, we just constrict returns to
around 15, and have the user whittle their search down
better.
@@ -0,0 +1,135 @@
# 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,
},
"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
},
"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.