Files
zkldi_Tachi/old-docs/docs/game-support/server-impl.md
T
zk e363bd2532 docs: migrate from mkdocs to mdbook (#1558)
* docs: migrate from mkdocs to mdbook

- Rename old mkdocs docs/ to old-docs/ for reference
- Set up new docs/ with mdbook (book.toml + src/ tree)
- Mirror full nav structure from mkdocs.yml into SUMMARY.md
- Add Justfile-docs with docs-serve, docs-build, docs-check, docs-install recipes
- Import Justfile-docs from root Justfile
- Rewrite .github/workflows/docs.yml: build step uses taiki-e/install-action
  to install mdbook, split into separate build + deploy jobs, PR builds
  run the check step too

* ci(docs): pin actions to SHAs, install mdbook via release binary

* ci(docs): install mdbook from apt instead of curling a release binary

* dev: replace mkdocs python stack with mdbook in dev image

* ci(docs): apt only works on Debian; restore release binary install for Ubuntu CI

* docs: fix duplicate file entries in SUMMARY.md

* docs: remove docs-install recipe

* docs: remove site-url from book.toml to fix asset loading

* dev: install mdbook from upstream release binary, not Debian apt

The Debian package (0.4.x+ds) strips bundled font assets, leaving the
built site without fonts/fonts.css. Use the upstream tarball (same as CI)
so the theme is complete. Handles x86_64 and aarch64.

* docs: vendor mdbook tarballs in dev/mdbook/, install from there

Dockerfile.dev uses COPY + tar to install the right arch at build time.
CI extracts the x86_64 tarball directly from the checkout.
No network access required for either — and no stripped-fonts Debian package.

* fix: unwritten
2026-05-22 20:43:07 +01:00

4.7 KiB

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 works, so you can re-use prudence functions here.

!!! example

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);
    	}
    },
    ```

## `scoreDeriver`

Any derived metrics you declared are produced by a single function
`(scoreData, chart) => derivedMetrics` that returns all derived fields at once.

!!! example
```ts
(scoreData, chart) => ({
	percent: (100 * scoreData.score) / (chart.data.notecount * 2),
	grade: GetGrade(G, scoreData.score),
});

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.

[
	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 each other!

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!