- Rust 97.4%
- Shell 2.5%
- Dockerfile 0.1%
The first measurement took only the coloured face of each note and came out 10% small, because our notes draw their black outline inside the radius. With the outline a small note is 0.44 of the lane's height, and YataiDON's Nijiiro skin agrees: 109 px and 163 px at 1080p, so a big note is 1.5 times a small one. reference-constants.md now lists the official demo and YataiDON as sources for the playfield's proportions, GPL-3.0 and so used for numbers only, and records the measurements in a new section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| crates | ||
| docs | ||
| scripts | ||
| test-charts | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| implementation-plan.md | ||
| LICENSE | ||
| plan.md | ||
| README.md | ||
Simple Taiko
A simple, stable, low-latency Taiko rhythm game that plays songs from an existing OpenTaiko library, in place and unmodified.
Status: v0.9.4 — playable, and it looks the part. Browse a library, pick a chart, play it, see your score. Scoring matches OpenTaiko exactly, scores persist, and it plays on keyboard or pad with latency calibration. Every screen sits on a drifting pattern, the notes and gauge wear colours measured out of OpenTaiko's own skin, and the difficulty picker is a row of cards. Controls are rebindable from the Bindings screen, on whatever you play on. Song select opens on what you last played, screens change with a wipe, and anything that goes wrong is said on screen rather than in a terminal. Not yet done: the modes listed in docs/v1-scope.md.
See CHANGELOG.md for what each release contains, and implementation-plan.md for where it is going.
Docs
| plan.md | The idea and the feature list |
| implementation-plan.md | Phases, gates, decisions |
| docs/format-notes.md | TJA / box.def / library layout, measured from a real 506-chart corpus; skin and hit-sound investigation |
| docs/shapeshifter-jist.md | One chart's gimmicks explained by the person who wrote them — the source for how branching really behaves |
| docs/timing-notes.md | Toolchain, audio clock measurements, latency budget |
| docs/reference-constants.md | Judge windows, score formula, OFFSET semantics, read from OpenTaiko source; playfield proportions from the official game and YataiDON |
| docs/v1-scope.md | What v1 does and, more importantly, does not do |
| CHANGELOG.md | What changed in each release |
The format itself is documented outside this repository, and better than we could:
IepIweidieng's tja.md
gives each command's scope and effect time and records where the simulators disagree. It is
the first thing to read before changing the parser.
Layout
crates/tja/ TJA parsing — pure, no raylib
crates/library/ song tree scanning — pure, no raylib
crates/timing/ the master clock — pure, no raylib
crates/play/ judgement and scoring — pure, no raylib
crates/save/ scores on disk — pure, no raylib
crates/taiko/ the game — the only crate that may touch graphics/audio/input
The split is deliberate: it makes the compiler enforce that the parser stays headless and testable against the whole chart corpus without opening a window.
Getting songs
Simple Taiko needs two things: charts, and a pair of drum samples. Neither is bundled — they belong to other people — but both are a download away, and you do not need to install OpenTaiko: the samples are two files in its repository.
Songs
The OpenTaiko community soundtrack is a plain git repository of chart folders, which is exactly the layout the scanner expects:
git clone --depth 1 https://github.com/OpenTaiko/OpenTaiko-Soundtrack Songs
That is around 500 songs. --depth 1 matters: the full history is roughly 3.4 GB on top of
a 1.2 GB working tree, and none of it is any use for playing.
Hit sounds
The drum samples are in the OpenTaiko repository itself, under
OpenTaiko/Global/HitSounds/<set>/, and in the release archive under
publish/Global/HitSounds/. Two files out of either is the whole job:
mkdir -p Global/HitSounds/Taiko
base=https://raw.githubusercontent.com/0auBSQ/OpenTaiko/main/OpenTaiko/Global/HitSounds/Taiko
curl -L -o Global/HitSounds/Taiko/dong.wav "$base/dong.wav"
curl -L -o Global/HitSounds/Taiko/ka.wav "$base/ka.wav"
Taiko is one of nine sets — Old Taiko, Konga, Electro, Break, Clap,
Gorgeous Taiko, Bol and '08 Dual are the rest, of which Bol and Old Taiko are .ogg
rather than .wav. Or supply your own: any directory holding dong.wav and ka.wav works —
.ogg is read too — and the game runs, silently, without them.
The layout
Put both next to the binary and it runs with no arguments:
simple-taiko/
├── taiko
├── Songs/
│ ├── 01 OpenTaiko Chapter I/
│ │ ├── box.def category name, colours, description
│ │ ├── 001 - CITYSCAPE/
│ │ │ ├── CITYSCAPE.tja the chart
│ │ │ ├── CITYSCAPE.ogg the audio, named by the chart's WAVE: header
│ │ │ ├── cityscape.png jacket, named by its PREIMAGE: header
│ │ │ └── uniqueID.json stable song id — what your scores are keyed on
│ │ └── 002 - .../
│ └── 02 OpenTaiko Chapter II/
└── Global/
└── HitSounds/
└── Taiko/
├── dong.wav
└── ka.wav
Categories may nest as deeply as you like, and a flat folder of song folders works too.
If OpenTaiko is already installed you need not lay any of this out: its
publish/directory is this layout, withSongs/andGlobal/HitSounds/both in place. Point at it with--base /path/to/OpenTaiko/publish, as below, or drop thetaikobinary intopublish/and run it with no arguments — a binary's own directory is one of the places it looks for content. Either way you get the hit sounds too, which--songson its own does not. Dropping the binary in is the only thing that puts a file in OpenTaiko's folder; the game itself still writes nothing there.
Or point at what you already have
If OpenTaiko is already installed, use it in place — nothing is copied and nothing is written to it:
taiko --base ~/Applications/OpenTaiko/publish
Or mix sources, which is the point of --songs being repeatable:
taiko --songs ~/OpenTaiko-Soundtrack --songs ~/my-charts \
--hitsounds ~/Applications/OpenTaiko/publish/Global/HitSounds/Taiko
Skins are not used. OpenTaiko's skin repository is another 1.4 GB and Simple Taiko draws its own playfield, so there is no reason to clone it — see docs/format-notes.md for why that format was turned down.
Play
cargo run --bin taiko -- --base ~/Applications/OpenTaiko/publish
cargo run --bin taiko -- --songs "<songs folder>" # one or more libraries
cargo run --bin taiko -- "<song folder>" oni # jump straight into one chart
The main menu offers Play, Settings and Quit. Settings holds calibration, audio, video, song select, bindings and an input test.
In song select: Up/Down move, Enter open, Backspace return, C calibrate, Esc return. Playing: F/J hit the drum centre (don), D/K the rim (ka), Esc returns to the list.
The drum works the menus too, as it does on the cabinet: the rim moves — D up, K down — and either don key, F or J, chooses. That matters most to anyone playing on a real drum, where those four inputs are the whole interface. It is why D no longer moves the cursor right: one key cannot both move and adjust on a screen that reads them together.
And the drum answers: the rim clicks as the cursor moves, and the centre thumps to choose something or to come back out. Since those are the same keys, a player on a real drum hears the drum they are actually hitting rather than a menu beep imitating one — which also means the hit-sound volume in Settings auditions itself as you change it.
On a controller the arcade layout applies: every face button is the drum — d-pad for the left hand, A/B/X/Y for the right — and the shoulders are the rim, L1/L2 left and R1/R2 right. The pad drives the menus too.
A library can be nested in categories or a flat folder of song folders; both work. Above them, once you have finished a song, is Recently Played: the last 20 songs you finished, newest first. It is built from your scores rather than from a folder, and song select opens on it. Settings → Song select changes how many it holds, or turns it off.
When something goes wrong, the game says so on screen, at the top right — a song that will not start and why, a score or setting that would not save. The same lines go to stderr. With no audio device the game still starts, silently, and says "No audio device found"; songs cannot be played until it is restarted with one.
Settings
~/.config/taiko/config.toml is written on first run, with comments. It holds key and gamepad
bindings, the latency offset, colours, volumes, video settings and how many songs Recently
Played holds. --list-keys prints every name
it accepts.
On a variable-refresh display, a stutter that appears fullscreen and not in a window is
worth suspecting the display for rather than the game. Hyprland's misc:vrr = 2 applies VRR
to fullscreen windows only, which produces exactly that shape. Turning VRR off is the known
cure; V-Sync is not one, and was measured not to be. See
docs/timing-notes.md §6, including why the game's own frame timings
cannot settle this.
Calibrate before playing seriously: press C in song select and tap along with the metronome. Every audio setup is late by some constant amount, and that is what the offset removes.
Settings → Bindings changes any of them without a keyboard or a text editor: Select on a row waits for an input and adds it to that action, Left drops the last one, and a Reset to defaults row puts every action back. Adding rather than replacing is deliberate — the defaults put five inputs on each don, and binding a sixth should not cost you the other five. The five actions that move around the menus keep their last input whatever you do, so there is no way to strand yourself on a handheld with no config file in reach.
Every control is an action with a list of inputs bound to it, under [input.bindings].
Actions are don_left, don_right, ka_left, ka_right, menu_up, menu_down,
menu_page_up, menu_page_down, confirm, back and debug_hud. Inputs are written F,
pad:RIGHT_FACE_DOWN, pad2:MIDDLE for one specific device, or axis:LEFT_Y+. Anything you
omit keeps its default. --save-config rewrites the file with every binding spelled out, and
--list-keys prints everything accepted.
input.gamepad defaults to "all", which listens to every device the system offers. That is
deliberate: a controller shows up as several devices — an 8BitDo Ultimate 2C presents its
keyboard interface first and the actual joystick second — and the indices shift as things are
plugged in and out. Set it to "none", an index, or part of a device name if you need to pin it.
If input is not doing what you expect, run --test-input: it lists every device being listened to
and names each key and button as it arrives.
Fonts
Japanese titles need a font that has Japanese in it, and no such font is shipped: they are several megabytes with their own licences, and every desktop already has one. On startup the game asks fontconfig what is installed and picks two — the sans the rest of your desktop uses for the alphabet, and whichever installed font covers the most of your song titles for everything else. Each is checked after loading, so a font that turns out not to have the characters it advertises is dropped rather than drawn as boxes. The startup line names what was chosen:
font NotoSans-Regular.ttf + HanaMinA.ttf (461 glyphs)
To choose yourself, set path under [font] in the config. It must be a single font file —
.ttf or .otf, not a .ttc collection, which cannot be read. Anything it lacks still falls
back to the automatic choice.
Wayland
Linux builds include GLFW's Wayland backend alongside the X11 one. One binary serves both:
GLFW picks Wayland when WAYLAND_DISPLAY is set and X11 otherwise, and the startup log says
which it chose (PLATFORM: DESKTOP (GLFW - Wayland) or (GLFW - X11)). Wayland's libraries
are loaded by name at runtime, like X11's, so a machine without them still runs the binary
over X11.
cargo build --no-default-features gives an X11-only build, which still runs under XWayland.
One known cost: GLFW's Wayland backend cannot report window position, so raylib cannot tell
which monitor the window is on and assumes the first — for fullscreen, and for the refresh
rate the frame cap follows. Set monitor under [video] in the config to the name the
startup monitors line prints, and fullscreen goes there. Or run over XWayland instead by
starting the game with WAYLAND_DISPLAY unset: env -u WAYLAND_DISPLAY ./taiko. See
implementation-plan.md step 16.
Steam Deck
scripts/build-deck.sh produces a binary that runs there. Built on a current distribution
it will not: Arch's glibc gives the binary a requirement of 2.43 and the Deck has 2.41, so
it refuses to start with a message about a version not found. The script builds inside
Valve's Steam Runtime "sniper" SDK instead, which targets glibc 2.31, and refuses to finish
if the result ever asks for more than the Deck provides.
Confirmed running on a Deck as of 0.5.0. It needs Docker, and downloads about 1.4 GB of SDK the first time. docs/steam-deck-build.md covers how the container is put together and how to check a binary yourself.
scripts/build-deck.sh # build, and report the glibc it needs
scripts/build-deck.sh --package # tarball in dist/, ready to copy across
Steam is not required to run the result. Old glibc is a link-time floor rather than a runtime dependency, and X11, Wayland and GL are loaded by name rather than linked, so the binary needs nothing on the Deck but libc, libm and libgcc_s. Copy it across with a song library and run it.
Running natively on Wayland in Gaming Mode. Gamescope hides its Wayland socket from the
games it runs unless it was started with --expose-wayland, which SteamOS does not do. So by
default the game runs through gamescope's XWayland. To run it as a native Wayland client,
add the game to Steam as a non-Steam game and set its launch options to:
WAYLAND_DISPLAY=gamescope-0 %command%
Gamescope always serves xdg-shell on that socket, so the game connects and the startup log
says (GLFW - Wayland). This is not a setup Valve documents or supports. If the game shows
no window or fails to start, remove the launch option. Desktop Mode is plain X11 unless the
Plasma Wayland session is chosen, and there the game picks Wayland on its own.
Windows
cargo build --release --target x86_64-pc-windows-gnu produces taiko.exe, and
scripts/release.sh builds and attaches it unless told --no-windows. Nothing extra ships
beside it: every DLL it imports is a Windows system one. scripts/make-pack.sh --windows
puts one in a folder with songs and hit sounds, for carrying to a Windows machine.
Two things differ from Linux and both are handled. Config and scores go to %APPDATA% and
%LOCALAPPDATA% rather than ~/.config and ~/.local/share — XDG_CONFIG_HOME and
XDG_DATA_HOME still win if set, which is what makes running it under Wine behave. And
fonts are found in C:\Windows\Fonts rather than through fontconfig, including the .ttc
collections that Windows keeps its Japanese faces in; those are unpacked a face at a time
by sfnt.rs, because a rasteriser handed a collection whole
quietly renders nothing.
Confirmed booting and reaching charts on Windows as of 0.9.2, on someone else's machine. Everything up to starting a song is therefore known to work; what nobody has checked yet is how it plays — audio latency in particular, which is the thing a rhythm game is judged on and the one thing testing under Wine says nothing about. Calibrate before believing the timing.
The startup lines are worth pasting back if you run it: they name the fonts that were found and where scores are being written, which are the two things that differ from Linux.
Where it looks for things
Content — songs, hit sounds — comes from a content root, the first of these that exists
and contains Songs/ or Global/HitSounds/:
--base <dir>$TAIKO_BASE- the directory holding the executable — so a copied folder is a self-contained install
~/.local/share/taiko
--songs <dir> (repeatable) and --hitsounds <dir> override individual pieces, and pointing
--base at an existing OpenTaiko installation works without copying anything.
Scores and config are always written to ~/.local/share/taiko and ~/.config/taiko
(%LOCALAPPDATA%\taiko and %APPDATA%\taiko on Windows), never into the content root —
which may be read-only, or somebody else's game folder. The game prints every resolved path
at startup.
During development, set TAIKO_BASE once rather than passing --base each time; cargo clean
deletes target/, so assets next to the debug binary would not survive.
F10 shows the debug readout: frame times as median / 99th / worst over the last few
seconds, then how many frames have missed their budget for the whole run and the worst one.
A stutter is a tail rather than an average, which a frames-per-second number cannot show. It
is a binding like any other, so debug_hud in config.toml moves it or unbinds it.
Development flags: --windowed opens in a window even if the config says fullscreen — worth
knowing before you run a screenshot on the machine you are working on. --auto plays it back automatically (a correct parse → clock → judge chain
scores every note GOOD), --screenshot <file> captures a frame, --seconds <n> exits after a
while, --pick opens the difficulty picker at startup. --help lists everything.
Two tests need a real library, and skip without one: every song reachable by navigation, and every listed difficulty loading as the difficulty that was listed:
TAIKO_BASE=~/Applications/OpenTaiko/publish cargo test --bin taiko
Releasing
scripts/release.sh <version> tests, builds, packages, tags, and publishes to Forgejo with
fj. Three binaries go out: one built here, one built against glibc 2.31 in the Steam
Runtime SDK for the Steam Deck and other older systems, and one
cross-compiled for Windows. Each travels with the README, the changelog and the
licence, and with nothing else — no songs and no hit sounds, which are not ours to
redistribute.
scripts/release.sh 0.9.4 # do it
scripts/release.sh 0.9.4 --dry # build and package only; no tag, no publish
scripts/release.sh 0.9.4 --no-deck # skip the Deck build (it needs Docker)
scripts/release.sh 0.9.4 --no-windows # skip the Windows build (needs the mingw-w64 target)
Three things have to be true first, and each is checked rather than trusted:
Cargo.tomlalready says the new version. The argument only confirms it, and the built binary is asked its--versionafterwards, so a tag can never ship a binary reporting a different number.- The working tree is clean. Bumping the version rewrites
Cargo.lock, so commit that with the bump — the script says as much when the lock is the only difference. CHANGELOG.mdhas a section for the version. The release notes are read from it, so the two cannot drift, and a missing section stops the release rather than publishing nothing.
The branch is pushed before the tag, so a release never points at a commit no branch contains.
When SteamOS has had a major update, check the Deck's glibc with pacman -Q glibc on the
device and update DECK_GLIBC in scripts/build-deck.sh. That number is the most the Deck
build is allowed to require. A stale one breaks nothing today — it just holds the build to an
older glibc than the Deck now has, and one day refuses a build that would have been fine.
See docs/steam-deck-build.md.
Build
cargo build
cargo test
raylib-sys builds raylib from vendored source, so the first build is slow and needs cmake and a C
toolchain.
Tools
# Measure the raw audio clock's resolution and drift.
cargo run --bin clockprobe -- <file.ogg> [seconds] [poll_hz] [buffer_frames]
# Check the smoothed master clock against real hardware (Step 2's gate).
cargo run --bin clocktest -- <file.ogg> [seconds] [poll_hz]
# Parse every chart in a library and report failures (Step 1's gate).
cargo run -p tja --example corpus -- <songs_dir>
# Scan a library into categories and songs.
cargo run -p library --example scan -- <songs_dir> [--tree]
Licence
MIT. No OpenTaiko assets are redistributed here — hit sounds and any skin art are read from the user's own installation at runtime.