docs: the one where he writes the docs

This commit is contained in:
zkldi
2023-01-24 02:51:15 +00:00
parent 26dd656dd2
commit 43cb45d6bb
50 changed files with 1270 additions and 79 deletions
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const BMS_CONF = {
defaultPlaytype: "7K",
name: "BMS",
playtypes: ["7K", "14K"],
songData: z.strictObject({
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const CHUNITHM_CONF = {
defaultPlaytype: "Single",
name: "CHUNITHM",
playtypes: ["Single"],
songData: z.strictObject({
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const GITADORA_CONF = {
defaultPlaytype: "Dora",
name: "GITADORA",
playtypes: ["Gita", "Dora"],
songData: z.strictObject({
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const IIDX_CONF = {
defaultPlaytype: "SP",
name: "beatmania IIDX",
playtypes: ["SP", "DP"],
songData: z.strictObject({
-1
View File
@@ -5,7 +5,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const ITG_CONF = {
defaultPlaytype: "Stamina",
name: "ITG",
playtypes: ["Stamina"],
songData: z.strictObject({
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const JUBEAT_CONF = {
defaultPlaytype: "Single",
name: "jubeat",
playtypes: ["Single"],
songData: z.strictObject({
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const MAIMAI_DX_CONF = {
defaultPlaytype: "Single",
name: "maimai DX",
playtypes: ["Single"],
songData: z.strictObject({
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const MUSECA_CONF = {
defaultPlaytype: "Single",
name: "MÚSECA",
playtypes: ["Single"],
songData: z.strictObject({
+1 -3
View File
@@ -1,13 +1,11 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { BMS_7K_CONF } from "./bms";
import { FmtScoreNoCommas, FmtPercent } from "../../utils/util";
import { FmtPercent, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const PMS_CONF = {
defaultPlaytype: "Controller",
name: "PMS",
playtypes: ["Controller", "Keyboard"],
songData: z.strictObject({
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const POPN_CONF = {
defaultPlaytype: "9B",
name: "pop'n music",
playtypes: ["9B"],
songData: z.strictObject({
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const SDVX_CONF = {
defaultPlaytype: "Single",
name: "SOUND VOLTEX",
playtypes: ["Single"],
songData: z.strictObject({
-1
View File
@@ -7,7 +7,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const USC_CONF = {
defaultPlaytype: "Controller",
name: "USC",
playtypes: ["Controller", "Keyboard"],
songData: z.strictObject({}),
-1
View File
@@ -6,7 +6,6 @@ import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
export const WACCA_CONF = {
defaultPlaytype: "Single",
name: "WACCA",
playtypes: ["Single"],
songData: z.strictObject({
-1
View File
@@ -70,6 +70,5 @@ export type INTERNAL_GAME_PT_CONFIG = Readonly<{
export type INTERNAL_GAME_CONFIG<PT extends string = string> = Readonly<{
name: string;
playtypes: ReadonlyArray<PT>;
defaultPlaytype: PT;
songData: AnyZodObject;
}>;
-1
View File
@@ -82,7 +82,6 @@ export interface ConfEnumScoreMetric<V extends string> {
type: "ENUM";
values: ReadonlyArray<V>;
// todo document this
minimumRelevantValue: V;
}
+1 -1
View File
@@ -124,7 +124,7 @@ function createConfigDocumentation(game: Game, playtype: Playtype) {
This game has the internal GPTString of \`${gptString}\`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+162
View File
@@ -0,0 +1,162 @@
# Client Implementation
As you probably can expect, the client implementation is entirely
## Where do Client Implementations go?
Implementations should be written in `client/src/lib/game-implementations.tsx`.
It's fine to inline implementations here, but feel free to break out into a separate file (See what IIDX does for a reference) if you need to.
## `enumColours`
For all the ENUM metrics in this game, give all of their values colours.
!!! tip
The `COLOUR_SET` global is useful for this, as it provides consistent identity
throughout tachi.
## `enumIcons`
What [Font Awesome v5](https://fontawesome.com/v5/search) icon should we use to
represent each enum?
## `difficultyColours`
If this game uses fixed difficulties, give each difficulty name a colour here.
## `classColours`
Give each class value a colour. These are used to render the class badges.
## `ratingSystems`
Although all games have `level` and `levelNum`, some optional properties may be useful here.
This is an array of functions that take a chart and return information about it.
These are used to sort charts in tables when the `difficulty` header is used to sort.
!!! example
The `CreateRatingSys` util is used for this:
```ts
[
CreateRatingSys(
"NC Tier",
"Tierlist Ratings for Normal Clears.",
(c) => c.data.ncTier?.value,
(c) => c.data.ncTier?.text,
(c) => c.data.ncTier?.individualDifference
),
CreateRatingSys(
"HC Tier",
"Tierlist Ratings for Hard Clears.",
(c) => c.data.hcTier?.value,
(c) => c.data.hcTier?.text,
(c) => c.data.hcTier?.individualDifference
),
CreateRatingSys(
"EXHC Tier",
"Tierlist Ratings for EX-HARD Clears.",
(c) => c.data.exhcTier?.value,
(c) => c.data.exhcTier?.text,
(c) => c.data.exhcTier?.individualDifference
),
]
```
!!! example
ITG also uses this to leverage the sorting abilities of this: we want to sort
things on level, but if the level is the same, we want to break ties on BPM.
```ts
[
CreateRatingSys(
"BPM",
"How fast are the streams in this chart?",
(c) => c.data.streamBPM,
(c) => c.data.streamBPM?.toString()
),
]
```
## `scoreHeaders`
What should the headers be for the score cells when rendering scores for this GPT?
!!! example
```ts
[
["Score", "Score", NumericSOV((x) => x.scoreData.percent)],
["Deltas", "Deltas", NumericSOV((x) => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV((x) => x.scoreData.enumIndexes.lamp)],
]
```
will correspond to the headers in the red box
![](../images/headers.png)
## `scoreCoreCells`
When rendering a score row, how should we render the actual score information cells?
This function gets `sc`, which is either a score or a PB for this GPT, and `chart`; the chart this score was on.
!!! important
The amount of cells returned should be **EXACTLY** the same length as the headers.
!!! example
```ts
({ sc }) => (
<>
<MillionsScoreCell
score={sc.scoreData.score}
grade={sc.scoreData.grade}
colour={GetEnumColour(sc, "grade")}
/>
<PopnJudgementCell score={sc} />
<PopnLampCell score={sc} />
</>
),
```
will correspond to the cells in these columns.
![](../images/cells.png)
## `ratingCell`
How should we render the rating cell for this GPT?
This is a function that takes in the aforementioned `sc` and `chart`, alongside `rating`, which is the currently selected score rating algorithm.
!!! example
```ts
({ sc, chart, rating }) => (
<>
{rating === "blockRating" ? (
<td>
<strong>
{chart.data.rankedLevel === null
? "Unranked Chart."
: sc.calculatedData.blockRating === null
? "Failed"
: sc.calculatedData.blockRating}
</strong>
</td>
) : (
<RatingCell score={sc} rating={rating} />
)}
</>
),
```
will correspond to the cells in this column.
![](../images/ratingcell.png)
## That's it!
Congrats! If you've done this, the [Server Implementation](./server-impl.md) and the [Common Configuration](./common-config/index.md), you've just added full support for a game to Tachi! Nice job!
@@ -0,0 +1,414 @@
# Common Configuration
This page documents how support for a game is written in `common/`.
There are two kinds of configs here.
## Where do configurations go?
Configurations should be written in `common/src/config/game-support/GAMENAME.ts`.
Once you have written a config, go to `common/src/config/config.ts` and import it. Mount the game configuration on `GAME_CONFIGS`, and the GPT configuration on `GAME_PT_CONFIGS`.
## A quick note on Games vs GPTs.
A game config is configuration for a game. Games in Tachi aren't actually the meaty part of support, instead, games form a kind of "group" for their playtypes.
For example, in IIDX there are two kinds of playtypes - Single Play and Double Play. Although these are completely separate kinds of games (it wouldn't make sense to share scores between them).
The game part - in this case `iidx` - defines things that are shared between all of its playtypes - things like what to call the game and what the song documents look like.
Whereas the game + playtype (typically shortened to GPT) is the part that contains almost all of the actual configuration. This distinction will become more obvious when you see the difference in size between the two configs.
## Game Configurations
Here is an example configuration - taken from our actual implementation of IIDX.
```ts
export const IIDX_CONF = {
name: "beatmania IIDX",
playtypes: ["SP", "DP"],
songData: z.strictObject({
genre: z.string(),
displayVersion: z.nullable(z.string()),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
```
It's important to note that `game` in Tachi is pretty much *just* used to group related types of game that **share songs**. This is fairly common in arcade games, but for home games it's rare for things across playtypes to share songs.
### `name`
This is the name for the game that will be displayed to end users. This should be formatted generally how the game formats things.
### `playtypes`
The list of playtypes this game supports. For games that only have one playtype (and likely will never have another), "Single" is typically used.
!!! example
For SDVX, we use `playtypes: ["Single"]`. Since SDVX doesn't normally have any sort of separate playtypes.
### `songData`
What game-specific properties should exist on a song?
This is written as a [Zod](https://github.com/colinhacks/zod) schema, which allows us to simultaneously declare the type of additional data, and how to validate it.
This game-specific information is stored on the `data` field of a song.
!!! example
Here's an example IIDX song:
```json
{
"altTitles": [],
"artist": "dj nagureo",
"data": {
"displayVersion": "1",
"genre": "PIANO AMBIENT"
},
"id": 1,
"searchTerms": [],
"title": "5.1.1."
},
```
Note the `data` field, which allows for game-specific metadata.
## GPT Configurations
This is the *real* meat-and-potatoes for implementing something for Tachi.
Here is our actual implementation for WACCA's Single playtype.
!!! note
WACCA only has one playtype - "Single".
```ts
export const WACCA_SINGLE_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
validate: p.isBetween(0, 1_000_000),
formatter: FmtNum,
description: "The score value. This is between 0 and 1 million.",
},
lamp: {
type: "ENUM",
values: ["FAILED", "CLEAR", "MISSLESS", "FULL COMBO", "ALL MARVELOUS"],
minimumRelevantValue: "CLEAR",
description: "The type of clear this score was.",
},
},
derivedMetrics: {
grade: {
type: "ENUM",
values: [
"D",
"C",
"B",
"A",
"AA",
"AAA",
"S",
"S+",
"SS",
"SS+",
"SSS",
"SSS+",
"MASTER",
],
minimumRelevantValue: "S",
description: "The grade this score was.",
},
},
optionalMetrics: {
...FAST_SLOW_MAXCOMBO,
},
defaultMetric: "score",
preferredDefaultEnum: "grade",
scoreRatingAlgs: {
rate: {
description: "Rating as it's implemented in game.",
},
},
profileRatingAlgs: {
naiveRate: {
description: "A naive rating algorithm that just sums your 50 best scores.",
},
rate: {
description:
"Rating as it's implemented in game, taking 15 scores from the latest version and 35 from all old versions.",
},
},
sessionRatingAlgs: {
rate: { description: "The average of your best 10 ratings this session." },
},
defaultScoreRatingAlg: "rate",
defaultProfileRatingAlg: "naiveRate",
defaultSessionRatingAlg: "rate",
difficulties: {
type: "FIXED",
order: ["NORMAL", "HARD", "EXPERT", "INFERNO"],
shorthand: {
NORMAL: "NRM",
HARD: "HRD",
EXPERT: "EXP",
INFERNO: "INF",
},
default: "EXPERT",
},
classes: {
stageUp: {
type: "PROVIDED",
values: WaccaStageUps,
},
colour: {
type: "DERIVED",
values: WaccaColours,
},
},
orderedJudgements: ["marvelous", "great", "good", "miss"],
versions: {
reverse: "REVERSE",
},
chartData: z.strictObject({
isHot: z.boolean(),
}),
preferences: z.strictObject({}),
scoreMeta: z.strictObject({ mirror: z.boolean().optional() }),
supportedMatchTypes: ["songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
```
We'll go over each bit of this.
## Metrics
See [Metrics](./metrics.md) and [Metric Groups](./metric-groups.md).
## `defaultMetric`
What should the default metric for this GPT be? This is used for chart leaderboards,
and should *ideally* be an `INTEGER` or `DECIMAL` metric. It is not legal for this
to be a `GRAPH` or `NULLABLE_GRAPH` metric, as those are not sanely comparable.
## `preferredDefaultEnum`
If the user has no preferences overriding this, what should this GPT default to showing
when showing enum graphs?
!!! example
![](../images/../../images/default-enum.png)
Here, we see that the site has picked lamps by default to show. This is because
the default enum for IIDX is `lamp`.
!!! note
Users may override this preference in their settings for this GPT.
## Score, Session, Profile Rating Algorithms
These are all the exact same idea, but appear in different parts of Tachi.
All of these define the names of rating algorithms and a description.
Rating algorithms are functions that return a number or null. This can be used
to implement things like VOLFORCE (SDVX, USC) or tierlist ratings.
Optionally, a rating algorithm's config may specify a `formatter:` field, which is
a function that transforms the number into a string somehow. If this is not specified,
this will default to a function that rounds the number to two decimal places.
## Difficulties
There are two kinds of difficulties available in Tachi. `"FIXED"`, which is a more traditional approach defining a fixed set of possible difficulties for your song (NORMAL, HYPER, ANOTHER, etc.), and `"DYNAMIC"`, which allows any arbitrary string as a difficulty name.
### Dynamic Difficulties
Dynamic difficulties are intended for use in games where a song may have any number of possible charts.
An example of this would be something like `osu!`, where a song may have as many difficulties as it wants, with *any* string as their name.
### Fixed Difficulties
For games where songs can only have a certain amount of difficulties (most arcade games)
a fixed config likely makes more sense.
With a fixed config, you define the `order:` in which these difficulties appear in,a default difficulty to redirect to if the song is selected in the search bar and
optionally, some shorthand (generally for mobile view).
!!! example
```ts
difficulties: {
type: "FIXED",
order: ["NORMAL", "HARD", "EXPERT", "INFERNO"],
shorthand: {
NORMAL: "NRM",
HARD: "HRD",
EXPERT: "EXP",
INFERNO: "INF",
},
default: "EXPERT",
}
```
## Classes
Classes are enums for profiles. These allow you to store discrete values on a user's
profile. What values are supported are controlled by the `values:` field.
You can use the `ClassValue` helper function to help define values.
There are two types of classes:
### "DERIVED"
In which the value of the class is derived from the user's profile metrics.
!!! example
A common example of this would be things like `colour` rankings in konami arcade
games. These are cutoffs like `>1500 jubility = ORANGE, >2000 jubility = GREEN`.
### "PROVIDED"
In which the value of the class is provided by a score import somehow.
!!! example
A common example of this would be things like dans. These are not functions
of existing state, and must be stated in an import method.
!!! note
"DERIVED" classes are allowed to go back down - if the deriving implementation
says the class is now worse, the class the user has will decrease.
However, "PROVIDED" classes cannot be superceded by worse ones - if the user was
10th dan and then cleared 3rd dan, their dan will *not* be overwritten. The only
way for a "PROVIDED" class to go down is to contact an admin at the moment.
Maybe the in the future, users could have the ability to manually wipe their own
classes, in-case they corrupt their profile.
## `orderedJudgements`
An ordered list (best to worst) of strings, representing the name of judgements (hit windows) for this GPT.
## Versions
See [Versions](./versions.md), as whether your GPT needs this or not requires some assessment.
## Chart Data
The `chartData` field allows you to define GPT-specific fields on chart documents.
For example, in IIDX we *need* to store the `notecount` of a chart somewhere (it's used to calculate `percent` and `grade`).
The `chartData` field is a [Zod](https://github.com/colinhacks/zod) schema indicating the structure of this additional information.
!!! example
```ts
chartData: z.strictObject({
inGameID: z.number().int().nonnegative(),
clearTier: z.strictObject({
value: z.number(),
text: z.string(),
individualDifference: z.boolean(),
}).nullable(),
}),
```
declares that a chart for this GPT should look like:
```ts
{
"chartID": "5088a4d0e1ee9d0cc2f625934306e45b1a60699b",
"data": {
"inGameID": 1,
"clearTier": { value: 12.5, text: "12C", individualDifference: false }
},
"difficulty": "ADV",
"isPrimary": true,
"level": "10",
"levelNum": 10,
"playtype": "Single",
"songID": 1,
"versions": ["exceed", "konaste"]
},
```
Note the GPT-specific properties in the `data` field now.
## Preferences
If this GPT should have specific preferences (for BMS, you can set a list of tables you don't want to see, for example), list them here as a Zod schema.
!!! example
A `preferences` of
```ts
preferences: z.strictObject({ showCustomCharts: z.boolean() })
```
corresponds to a settings document of:
```ts
[
{
"userID": 1,
"game": "iidx",
"playtype": "SP",
"preferences": {
"preferredScoreAlg": null,
"preferredSessionAlg": null,
"preferredProfileAlg": null,
"preferredDefaultEnum": null,
"defaultTable": null,
"preferredRanking": null,
"stats": [],
"gameSpecific": {
"showCustomCharts": false
}
},
"rivals": []
}
]
```
Note the `preferences.gameSpecific` part, which holds these game-specific
preferences.
## Score Metadata
Sometimes, we want to store stuff about a score that isn't really related to a metric.
A common example of this would be something like the mods the player was using - were
they using RANDOM, MIRROR, etc?
!!! note
Score Metadata **DOES NOT** appear on PBs. This metadata is *specific* to the score
itself, and would never make sense to merge.
If a PB is comprised of a best score using RANDOM, and a best lamp using MIRROR,
what should the PB merge to? It doesn't make any sense as an operation.
This, similarly, is a Zod schema, and appears on a score documents `scoreMeta` field.
## Supported Match Types
This is for [BATCH MANUAL](todo) usage. BATCH MANUAL has to specify a `matchType` - how should it resolve a given identifier?
Some import methods might want to use in game IDs, hashes of a certain kind, etc.
For a list of all possible match types, see [Match Types](./match-types.md).
You should list the available `matchTypes` in this field of the config.
@@ -0,0 +1,61 @@
# Match Types
For [BATCH MANUAL](./todo) imports, we need a way of resolving given identifiers
into charts on Tachi. These may be through a variety of methods, such as chart hashes,
in game IDs, etc.
This is a list of **all** available match types. However, what match types are available
depend on what game you're importing scores for.
## Identifier Only
The following match types only need an `identifier` defined.
### `bmsChartHash`
Uses `identifier` as an MD5 or SHA25 hash.
### `itgChartHash`
Uses `identifier` as a GroovestatsV3 hash.
### `popnChartHash`
Uses `identifier` as a SHA256 hash, for pop'n file contents.
### `uscChartHash`
Uses `identifier` as a SHA1 hash, since that's what USC uses.
## Identifier + Difficulty
These match types need both `identifier` and a `difficulty` defined.
### `inGameID`
Looks up on the in game ID for this chart.
### `sdvxInGameID`
Looks up on the in game ID for this chart, but allows a special difficulty string - `"ANY_INF"` to be passed.
If `"ANY_INF"` is the difficulty, then that difficulty will try to find a chart with this in game ID where the difficulty is any of INF, GRV, HVN, VVD or XCD.
### `songTitle`
Looks up the song on its title or any of its defined `altTitles`.
!!! warning
This *requires* that your game never has two songs with the same title. By enabling
this, the seeds tests will check and enforce this for you.
You should generally *never* enable this option, as you *really* can't guarantee
the uniqueness of song titles short of the game being dead.
### `tachiSongID`
Looks up the song on Tachi's defined `songID`.
## Adding a new Match Type
Does your GPT implementation need its own match type? See [Adding a new match type](./COOKBOOK TODO).
@@ -0,0 +1,103 @@
# Metric Groups
Metrics for a GPT are grouped into three categories:
## Provided Metrics
These metrics **must** be provided in a score import. These are, in effect, **MANDATORY** metrics.
These metrics go into the scoreID.
!!! example
```ts
providedMetrics: {
score: {
type: "INTEGER",
validate: p.isBetween(0, 1_000_000),
formatter: FmtNum,
description: "The score value. This is between 0 and 1 million.",
},
lamp: {
type: "ENUM",
values: ["FAILED", "CLEAR", "MISSLESS", "FULL COMBO", "ALL MARVELOUS"],
minimumRelevantValue: "CLEAR",
description: "The type of clear this score was.",
},
},
```
Any score import for a GPT like this *must* provide a compliant `score:` and `lamp:` value.
## Derived Metrics
These metrics are derived from the provided metrics (and the chart the score was on), but stored on scores for conveniences sake. These will **ALWAYS** be present on a score.
It may also be the case that this derived form of a metric is the default metric for a game, in which case, we'd definitely want to store it for leaderboards!
These metrics **DO NOT** go into the scoreID, as they are merely functions of other captured state.
!!! example
```ts
derivedMetrics: {
percent: {
type: "DECIMAL",
validate: p.isBetween(0, 100),
formatter: FmtPercent,
description: "EX Score divided by the maximum possible EX Score on this chart.",
},
grade: {
type: "ENUM",
values: ["F", "E", "D", "C", "B", "A", "AA", "AAA", "MAX-", "MAX"],
minimumRelevantValue: "A",
description:
"Grades as they are in IIDX. We also add MAX- (94.44...%) and MAX (100%) as their own grades for convenience.",
},
},
```
In IIDX, the derived metrics are `percent` (as it's exScore / maximumEXOnThisChart)
and `grade`, which is also derivable.
We want to store these values for convenience and displaying on the UI, so we will.
## Optional Metrics
Sometimes, we want to store some metrics that not all score imports could provide. For that, we have optional metrics.
These metrics are defined like any other, but are all allowed to be not-provided by an importer.
!!! example
```ts
optionalMetrics: {
bp: {
type: "INTEGER",
validate: p.isPositive,
formatter: FmtScoreNoCommas,
description: "The total bads + poors in this score.",
},
gauge: {
type: "DECIMAL",
validate: p.isBetween(0, 100),
formatter: FmtPercent,
description:
"The life in percent (between 0 and 100) that was on the gauge at the end of the chart.",
},
}
```
For IIDX, not all importing methods might know these values, and we don't want to exclude users of those services from being able to import their scores.
As such, we mark these as optional.
!!! warning
It's possible, but rare, that an optional metric should be part of the scoreID.
For these cases, you can mark an optional metric with `partOfScoreID: true`.
This metric will now - as you'd expect - be part of the scoreID.
We actually use this in SDVX for `exScore`. EX Score might not be available, as it
doesn't necessarily have to be enabled by the user - but if it does exist, we want
to track it properly. That is to say, if the user raises their `exScore` but nothing
else, we should still count that as a new score - despite `exScore` being optional!
@@ -0,0 +1,205 @@
# Metrics
Metrics are what kind of values we want to track on this GPT's scores. Common things for this are values like `score`, `lamp`, maybe even `grade`.
A metric definition looks like this:
```ts
{
type: "INTEGER",
validate: p.isBetween(0, 100_000),
formatter: FmtNum,
description: "The score value.",
}
```
!!! note
All metrics look like t
## Metric Types
We're allowed 5 types of metrics:
- `DECIMAL`
This metric is expected to be a decimal.
!!! example
```ts
percent: {
type: "DECIMAL",
validate: p.isBetween(0, 100),
formatter: FmtPercent,
description: "EX Score divided by the maximum possible EX Score on this chart.",
},
```
- `INTEGER`
This metric is expected to be a whole number.
!!! example
```ts
{
type: "INTEGER",
validate: p.isBetween(0, 100_000),
formatter: FmtNum,
description: "The score value.",
}
```
- `ENUM`
This metric is expected to be a string in a provided ordered list of strings. This is used to implement things like `"FAILED", "CLEAR", "FULL COMBO", "PERFECT"` lamps.
!!! example
```ts
{
type: "ENUM",
values: [
"NO PLAY",
"FAILED",
"ASSIST CLEAR",
"EASY CLEAR",
"CLEAR",
"HARD CLEAR",
"EX HARD CLEAR",
"FULL COMBO",
],
minimumRelevantValue: "EASY CLEAR",
description: "The type of clear this was.",
},
```
- `GRAPH`
This metric is an array of numbers.
- `NULLABLE_GRAPH`
This metric is an array of numbers or null.
!!! example
```ts
gaugeHistory: {
type: "NULLABLE_GRAPH",
validate: p.isBetween(0, 100),
description:
"A snapshot of the gauge percent throughout the chart. The values should be null from the point the user dies until the end of the chart.",
},
```
Importantly: something like `[99, 74, 12, null, null, null]` would be legal as a nullable graph.
This does not mean the field itself is nullable, only the values inside the array!
## Enum Metrics
Metrics of type `ENUM` are special in many ways. Most importantly, they don't require
any sort of validation - the input is either a member of `values` or it isn't.
An ENUM metric looks like this:
```ts
{
type: "ENUM",
values: [
"NO PLAY",
"FAILED",
"ASSIST CLEAR",
"EASY CLEAR",
"CLEAR",
"HARD CLEAR",
"EX HARD CLEAR",
"FULL COMBO",
],
minimumRelevantValue: "EASY CLEAR",
description: "The type of clear this was.",
},
```
The order of `values` is extremely important: the first value should be the worst possible value in the metric, and the last value should be the best possible value.
`minimumRelevantValue` indicates the smallest value in this enum that a user will
ever realistically care about getting. This is used in the UI and other places to
hide useless ENUM values from the user.
!!! example
![](../../images/min-relevant-value.png)
Despite the fact this user failed a bunch of stuff this session, the UI won't
show me my new `FAILED` or `ASSIST CLEAR` scores!
## Integer and Decimal Metrics
These kinds of metrics, given their more open-ended nature, require some validation.
### Validation
There are two kinds of metric validations. If a metric can be validated, it should have
a property called `validate` which returns true on success, and a string representing
an error message on failure:
!!! example
```ts
{
type: "INTEGER",
validate: (value) => {
if (value > 100_000) { return "Score cannot be greater than 100k." }
return true;
},
formatter: FmtNum,
description: "The score value.",
}
```
!!! note
The type of input is already checked for you. You don't need to check whether your
input is an integer or not if you declare `type: "INTEGER"`. The same goes for
`type: "GRAPH"`, the validator instead runs on each element in the array.
!!! tip
The reason for this rather strange type signature (`string | true`) is because we
can use our validation library - [Prudence](https://github.com/zkldi/Prudence) - to create validators for us. Instead
of writing out that validation code, we can use `p.isBetween(0, 100_000)`!
However, it might not be possible to validate a metric without knowing what chart
the score is on. For example, in IIDX the maximum amount of score you can get is
the chart's notecount * 2.
Since we don't know what this GPT chart's look like at this time (we haven't defined them yet!)
we instead don't declare a validator and put down `chartDependentMax: true` instead.
We will have to write the validator later in the server implementation.
### Formatting
We also want to format these metrics into a string. For example, a `percent` metric may want to stringify into `92.45%` instead of `92.49938222293842`.
More subtle formatting rules can be defined here too, such as how in jubeat, scores are
easier to read written like `903,283` instead of `903283`.
## Graph Metrics
Graph metrics (`GRAPH` and `NULLABLE_GRAPH`) also have to define a validator, but the validator is called on each element of their respective arrays.
To validate that all of the values in a graph metric are between 0 and 10, you could
write a validator like:
```ts
function validate(value: number) {
if (value < 0) return "too small";
if (value > 10) return "too big";
return true;
}
```
This is remarkably similar to how validators work for `INTEGER` and `DECIMAL` metrics.
Furthermore, graph metrics can specify a `size` function, which validates the size of
the array too. You can use this to enforce a certain array size (such as exactly 100 entries, or similar).
## Now what?
This is how metrics work, but there's more detail in how they're defined in each game's config.
For example, some metrics (like `grade` for a lot of games) are merely functions of other metrics. Making every import method handle this themselves seems silly. How do we tackle this? This, and more, is answered in [Metric Groups](./metric-groups.md)!
@@ -0,0 +1,32 @@
# Versions
Versions are a method for us marking charts as being available in a certain "version" of the game.
The predominant use for this is for arcade games, where new versions of the game generally come out and add/remove charts.
It's useful for us to know what charts are available in what version for two purposes.
The first allows us to create folders - users playing on a certain version of a game don't want to see charts they can't play!
The second is more complex.
## Disambiguation and `isPrimary`
Sadly, the real world isn't easy to deal with. It's possible for a chart to exist in
version A of a game, get removed in version B, and then come back in version C with
new, completely different charts.
**Worse still**, it's possible for that song to use the *exact same* in game ID it used
before. If we use `inGameID` to identify things, now we've got a critical ambiguity!
Does `inGameID: 1, difficulty: ANOTHER` refer to the version A chart, or the version C one? For games where metrics are chart-dependent, this could be *lethal* to the leaderboards.
It's much easier to get a good score in IIDX if the chart thinks it has less notes than it actually has!
To fix this, we also use versions as an identifer for disambiguation. If a score comes
in, it **MAY** pass what version the score was attained on.
This allows us to immediately disambiguate the previous case.
If a version is **not** passed in, we assume that the score is for the chart marked as
`isPrimary: true`. Furthermore, seeds tests **enforce** that duplicates on certain IDs expected-to-be-unique cannot be true unless all-but-one-of-them is marked as `isPrimary: false`.
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `bms:14K`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `bms:7K`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `chunithm:Single`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `gitadora:Dora`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `gitadora:Gita`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `iidx:DP`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `iidx:SP`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `itg:Stamina`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `jubeat:Single`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `maimaidx:Single`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `museca:Single`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `pms:Controller`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `pms:Keyboard`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `popn:9B`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `sdvx:Single`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
@@ -3,7 +3,7 @@
This game has the internal GPTString of `usc:Controller`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `usc:Keyboard`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+1 -1
View File
@@ -3,7 +3,7 @@
This game has the internal GPTString of `wacca:Single`.
!!! note
For information on what each section means, please see [Common Config](../../common-config.md).
For information on what each section means, please see [Common Config](../common-config/index.md).
## Metrics
+11 -23
View File
@@ -6,33 +6,21 @@ be enabled on this Tachi instance.
## What games are supported?
In the Tachi repo, the following games have modules:
- [beatmania IIDX](./games/iidx.md)
- [MUSECA](./games/museca.md)
- [SDVX](./games/sdvx.md)
- [BMS](./games/bms.md)
- [CHUNITHM](./games/chunithm.md)
- [USC](./games/usc.md)
- [WACCA](./games/wacca.md)
- [pop'n music](./games/popn.md)
- [jubeat](./games/jubeat.md)
- [PMS](./games/pms.md)
- [GITADORA](./games/gitadora.md)
- [maimai DX](./games/maimaidx.md)
- [ITG](./games/itg.md)
For more information on what each module has (such as score metrics, chart specific data, etc.), click on the game.
See 'Game Information' in the sidebar for a list of all supported games and their configurations.
## How do I write support for a game?
Adding support for a game requires configuration in three places and a loading of `database-seeds`.
You will need:
You will need to:
- [A configuration in `common/`](./config/common.md)
- [A configuration in `server/`](./config/server.md)
- [A configuration in `client/`](./config/client.md)
- [Some database seeds for songs and charts](./config/seeds.md)
- [Create a configuration in common](./common-config/index.md).
- [Implement it in the server](./server-impl.md).
- [Implement it in the client](./client-impl.md).
- [Load songs, charts, folders, etc.](./seeds.md).
For more information on how to write each module and what they contain, click on the specific item.
For more information on how to write each module and what they contain, click on the specific item.
## How do I enable that support?
In your `server`'s `conf.json5` file, add the game to `TACHI_CONFIG.GAMES`. This will enable this module.
+98
View File
@@ -0,0 +1,98 @@
# Adding Seeds
With a [Common Configuration](./common-config/index.md) defined, we know what the songs and charts for this game should look like.
Lets load them into the database seeds.
## Quick Primer
The database seeds are a folder in the monorepo: `database-seeds/collections`, which contain JSON files.
These JSON files contain the state of a lot of our databases that need to be loaded. When changes are made to these seeds and committed to the main repository, a script will automatically apply those changes to the database.
!!! info
For local usage, you can use `pnpm sync-database-local` in the terminal to sync
the database with your local seeds.
## Adding songs and charts
If they don't already exist, create new files for `songs-GAMENAME.json` and `charts-GAMENAME.json`. Place `[]` inside those files, as they should be arrays.
It's left as an exercise for the reader to source the song and chart data for their game. You will likely need to write your own scripts.
Once you've gotten that data, you need to convert it into Tachi's song/chart format.
## Writing the files
You can modify the JSON files however you want. It really doesn't matter. However, there is a `database-seeds/scripts/` folder with a bunch of scripts you can use
to ease this process.
For things you only want to run a single time, place the script in the `database-seeds/scripts/single-time` folder.
For things you want to keep around, place the script in the `database-seeds/rerunners` folder. Simple.
The file `util.js` contains a bunch of miscellaneous utils for helping out, like `CreateChartID` or `MutateCollection`.
## What do songs and charts look like?
A song in Tachi looks like this:
```json
{
"altTitles": [],
"artist": "dj nagureo",
"data": {
// the things you defined in GAME_CONFIG.songData go here
},
"id": 1,
"searchTerms": [],
"title": "5.1.1."
},
```
For information on what each of these properties mean, see [Song Document](../schemas/song.md).
A chart in Tachi looks like this:
```json
{
// This is a randomly generated 20 byte string.
// The utility function `CreateChartID` should be used.
"chartID": "70b80da02a2037d556026b412c386b2fd1e57dbd",
"data": {
// this should be what you defined in GPT_CONFIG.chartData.
},
// If your difficulties are "FIXED", this should be one of the expected difficulties.
// Otherwise, any string goes here.
"difficulty": "Green",
"level": "3",
"levelNum": 3,
// this should be one of the playtypes for your game.
"playtype": "Single",
"songID": 1,
// This should be an array of the versions this chart appears in.
// For more information, see Common Config's Versions documentation.
"versions": [
"1.5",
"1.5-b"
],
// See Common Config's Versions documentation.
"isPrimary": true
}
```
## Tables and Folders
You'll probably want to create atleast one table and some folders for your game.
There are various utilities for this, like `scripts/rerunners/add-level-version-folders.js` for creating a traditional "Level 1, Level 2, Level 3" kind of table.
## Loading the seeds
Once you've modified the database seeds, test them with `pnpm test` inside the `database-seeds/scripts` folder. This will check a bunch of properties about the songs and charts you just made.
If they fail, read why and make appropriate changes. If they pass, move to the root of the Tachi repository and run `pnpm sync-database-local`. This will load the changes into your MongoDB instance.
+152
View File
@@ -0,0 +1,152 @@
# Implementing on the Server
Now that we've got a config defined in `common/` for a GPT, we need to implement
parts of it on the server.
## Where do Server Implementations go?
Implementations should be written in `server/src/game-implementations/games/GAMENAME.ts`.
Once you have written a config, go to `server/src/game-implementations/game-implementations.ts` and import it. Mount the game configuration on `GPT_SERVER_IMPLEMENTATIONS`.
## `chartSpecificValidators`
Any metrics you declared as being `chartSpecificMax: true` in the config need
an implementation here. You can declare a function that takes in the metric's value
and the chart the score is on and validate accordingly.
These functions should return a string on error, and true on success. This is aligned
with how [Prudence](https://github.com/zkldi/Prudence) works, so you can re-use prudence
functions here.
!!! example
```ts
musicRate: (rate, chart) => {
switch (chart.difficulty) {
case "BSC":
case "ADV":
case "EXT":
return p.isBetween(0, 100)(rate);
case "HARD BSC":
case "HARD ADV":
case "HARD EXT":
return p.isBetween(0, 120)(rate);
}
},
```
## `derivers`
Any derived metrics you declared need derivers implemented here. This is a function
that takes in the provided metrics and the chart for this score and should return
the metric value we expect.
!!! example
```ts
percent: (metrics, chart) => (100 * metrics.score) / (chart.data.notecount * 2);
```
## `scoreCalcs`, `sessionCalcs`, `profileCalcs`
For any `{score, session, profile}RatingAlgs` you defined, implement them here.
## `classDerivers`
For all the classes you declared with `type: "DERIVED"`, implement the derivers here.
## `goalCriteriaFormatters`
When creating a goal on a metric, how should we format the title?
```
Get a score of 1234 on 5.1.1 SP ANOTHER
^^^^^^^^^^^^^^^^^^^^^^
this bit
```
## `goalOutOfFormatters`
When creating a goal on a metric, how should we format the "outOf" part?
```
HARD CLEAR/FULL COMBO
^^^^^^^^
this bit
```
## `goalProgressFormatters`
How should we format the progress of this goal?
```
HARD CLEAR/FULL COMBO
^^^^^^^^
this bit
```
## `pbMergeFunctions`
How should we combine scores into one PB? There is an *extraordinarily* useful helper
function for this called `CreatePBMergeFor`.
The way PBs are merged works like a chain: the first score is the best score this user
has on this chart for the `defaultMetric` declared in the config. Then, every
merge function defined in this pipeline is ran on the score and mutates the original.
Eventually, you have a fully merged PB document.
The below code defines a PB merger that gets the largest lamp. It will then run the
final function with the base score and the score it just fetched.
In the event it doesn't find a score (i.e. the user has no scores with `optional.bp`)
the function will simply not be called.
```ts
[
CreatePBMergeFor("largest", "enumIndexes.lamp", "Best Lamp", (base, lamp) => {
base.scoreData.lamp = lamp.scoreData.lamp;
base.scoreData.optional.gsmEasy = lamp.scoreData.optional.gsmEasy;
base.scoreData.optional.gsmNormal = lamp.scoreData.optional.gsmNormal;
base.scoreData.optional.gsmHard = lamp.scoreData.optional.gsmHard;
base.scoreData.optional.gsmEXHard = lamp.scoreData.optional.gsmEXHard;
base.scoreData.optional.gauge = lamp.scoreData.optional.gauge;
base.scoreData.optional.gaugeHistory = lamp.scoreData.optional.gaugeHistory;
base.scoreData.optional.comboBreak = lamp.scoreData.optional.comboBreak;
}),
CreatePBMergeFor("smallest", "optional.bp", "Lowest BP", (base, bp) => {
base.scoreData.optional.bp = bp.scoreData.optional.bp;
}),
];
```
## `defaultMergeRefName`
As mentioned above, the chain of PB functions starts by plucking the best score this
user has on this chart under the `defaultMetric`. What should we call that score?
!!! example
For IIDX, this is "Best Score". For something like GITADORA, which only has percent,
this might be called "Best Percent".
## `scoreValidators`
Out of the box, Tachi will assume complete independence of all variables in a score.
However, this is often not the case, and there are certain things you expect to be true
between the metrics of a score.
For example, you shouldn't be able to submit a `PERFECT ULTIMATE CHAIN` if your score
isn't 10 million - the two imply eachother!
In this part of the server implementation, you may specify as many validation functions
as you like. These assert relations between the fields on a score, and allow you
to restrict certain things.
Like real life, passing judgements subjects you to more scrutiny -- getting a `FULL COMBO` with misses should likely fail a validation function.
## That's it!
The only thing left is to define the [Client Implementation](./client-impl.md)!
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

+11 -13
View File
@@ -34,11 +34,19 @@ nav:
- "contributing/components/documentation.md"
- "contributing/components/seeds.md"
- Game Support:
- "game-support/common-config.md"
- Supporting New Games:
- "game-support/index.md"
- Common Config:
- "game-support/common-config/index.md"
- "game-support/common-config/metrics.md"
- "game-support/common-config/metric-groups.md"
- "game-support/common-config/match-types.md"
- "game-support/common-config/versions.md"
- "game-support/server-impl.md"
- "game-support/client-impl.md"
- "game-support/database-seeds.md"
- Game Information:
- "game-support/games/iidx-SP.md"
- "game-support/games/iidx-DP.md"
@@ -69,16 +77,6 @@ nav:
- "wiki/lamps.md"
- "wiki/score-oddities.md"
- Documents:
- "schemas/index.md"
- "schemas/user.md"
- "schemas/session.md"
- "schemas/score.md"
- "schemas/song.md"
- "schemas/chart.md"
- "schemas/goal.md"
- "schemas/goal-sub.md"
- API Reference:
- "api/index.md"
- "api/auth.md"
@@ -1,10 +1,4 @@
import {
GoalFmtPercent,
GoalFmtScore,
GoalOutOfFmtPercent,
GoalOutOfFmtScore,
GradeGoalFormatter,
} from "./_common";
import { GoalFmtScore, GoalOutOfFmtScore, GradeGoalFormatter } from "./_common";
import db from "external/mongo/db";
import { CreatePBMergeFor } from "game-implementations/utils/pb-merge";
import { ProfileSumBestN } from "game-implementations/utils/profile-calc";