Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
# ctr-rs

ctr-rs is and online-focused port of Crash Team Racing (PS1, 1999), written in Rust.

It is heavily based on the [ctr-native](https://github.com/CTR-tools/ctr-native) project, which is used to verify the physics simulation and other mechanics remains as faithful to the original as possible. As we see it, the original CTR's feel and controls are truly S-tier, and as such, replicating them is the absolute top priority for this project.
ctr-rs is an online-focused reimplementation of Crash Team Racing (PS1, 1999), written in Rust.

## What's here

- **Exact physics** - Kart against ground and wall, and kart against kart, follow the original tick for tick. `vehicle/tests/tick.rs` checks them against the C reference, `ctr-native`. The weapons, crates and the level's objects follow the original closely.
- **Truly native performance, framerates/aspect ratio** - While the original wobbly graphics definitely has its own charm to it, the reimplementation has need for [PGXP](https://github.com/libretro/beetle-psx-libretro/tree/master/pgxp) or other workarounds, since we have an actual depth buffer.
- **Cross-platform** - The project runs and builds on any system that Rust builds on.
- **No emulator hacks, ROM patches or launchers** - The current "CTR online" solutions have quite a bit of jank to get running.
- **The modding horizon is unlimited** - Modding ctr-native, while certainly possible, is difficult to get right, and limited in scope due to the recomp-style codebase and the C programming language.
- **30 Hz simulation, hi-FPS rendering** - The game state advances at the original's 30 ticks a second, independent of the frame rate that is drawn.
- **Rollback netcode with no local delay** - Players connect directly (if possible) to each other with [iroh](https://github.com/n0-computer/iroh) and [the QUIC protocol](https://en.wikipedia.org/wiki/QUIC).
- **Custom tracks** - The `ctr-map` turns an xdelta patch of the disc into a `.ctrmap` file that the client can load through the lobby.
- **Exact, reference-checked physics** - Kart physics/collision follow the original tick for tick. We check them against the C decompilation, [ctr-native](https://github.com/CTR-tools/ctr-native). The weapons, crates and the level's objects follow the original closely.
- **Truly native performance, framerates/aspect ratio** - The original look definitely has its own charm to it, but the reimplementation has no need for [PGXP](https://github.com/libretro/beetle-psx-libretro/tree/master/pgxp) or other (awesome) emulator hacks, since we have full-precision vertices and a real depth buffer.
- **Cross-platform** - The project runs and builds on any system that Rust builds on, and on the browser too!
- **Hassle-free online play** - No patched ROMs, emulator builds or launchers required.
- **Peer-to-peer networking** - Players are directly connected to each other over [QUIC](https://en.wikipedia.org/wiki/QUIC), using [iroh](https://github.com/n0-computer/iroh).
- **No network setup** - No port forwarding, DMZ or dedicated servers. Connections punch through NAT automatically, and fall back to a relay when they can't.
- **30 Hz simulation, hi-FPS rendering** - The game state advances at the original's 30 ticks/s, independent of the rendering frame rate.
- **Custom tracks** - Just drag and drop an `.xdelta` (from e.g. [CTR Custom Tracks](https://ctrcustomtracks.com)) to the client window, and immediately play them with your friends.

## What's not here (not currently planned)

- **Adventure mode**
- **Time trial**
- **Relic race**
Expand All @@ -31,7 +30,7 @@ cargo run -p client

A build from the [releases](https://github.com/elipp/ctr-rs/releases) asks for the disc image the first time it starts. The macOS build is not signed, so macOS refuses to open it at first: allow it in System Settings → Privacy & Security ("Open Anyway"), or run `xattr -dr com.apple.quarantine ctr-rs.app`. On Linux, the `.AppImage` runs as it is once it is executable (`chmod +x`).

The client has no command line options. `]` and `[` switch the level, `.` and `,` switch the racer, and `/` commands go through the in-game console.
The client has no command line options. `]` and `[` switch the level, `.` and `,` switch the racer, and `Enter` opens the in-game console for `/` commands and chat.

`ctr-query` (`crates/query`) looks up a level's data from the disc, and `ctr-map` (`crates/map`) imports and describes custom tracks.

Expand All @@ -41,10 +40,10 @@ The client has no command line options. `]` and `[` switch the level, `.` and `,
cargo test --workspace --no-fail-fast
```

Some tests need files that are not in the repository, and without them they skip and pass without checking anything. Their names say which:
Some tests need files that are not in the repo, and without them they skip and pass without checking anything. Their names say which:

- `needs_disc_…`: the disc image (`assets/ctr-u.bin`, or `CTR_BIN`), or the folder prepared from it (`assets/disc`, or `CTR_DISC_DIR`).
- `needs_reference_…`: [ctr-native](https://github.com/CTR-tools/ctr-native) at the commit in [`docs/ctr-native.rev`](docs/ctr-native.rev), checked out at `reference/ctr-native` (git-ignored). Most of them also build the C side with `gcc -m32`, which on Debian and Ubuntu is the `gcc-multilib` package.
- `needs_reference_…`: [ctr-native](https://github.com/CTR-tools/ctr-native) at the commit in [`docs/ctr-native.rev`](docs/ctr-native.rev), checked out at `reference/ctr-native` (git-ignored). Most of them also build the C side with `gcc -m32` (x86-64 only), which on Debian and Ubuntu is the `gcc-multilib` package.
- `needs_disc_needs_reference_…`: both.

Filters match anywhere in the name, so `cargo test --workspace -- --skip needs_disc` skips every test that needs the disc (those that need both as well), and `--skip needs_reference` every test that needs ctr-native.
Expand All @@ -60,7 +59,7 @@ Another commit of ctr-native may not match what the tests expect (they fail to l

## Prior work

This project stands on the work of the CTR community, and could not exist without it.
This project stands on the work of the CTR community, and absolutely could not exist without it:

- [**ctr-native**](https://github.com/CTR-tools/ctr-native), a native PC port of the game built on the decompilation. It is our first reference for how the game behaves, and the oracle our physics tests compare against.
- [**CTR-ModSDK**](https://github.com/CTR-tools/CTR-ModSDK), and the decompilation, symbols and headers that came out of it. We use them for the layouts of the game's data and for what ctr-native lacks.
Expand Down
6 changes: 2 additions & 4 deletions crates/client/src/controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,8 @@ impl Controls {
pub fn taken(key: KeyCode) -> Option<&'static str> {
Some(match key {
KeyCode::Escape | KeyCode::KeyP => "opens the menu",
KeyCode::Enter | KeyCode::NumpadEnter => "opens the command line",
KeyCode::KeyR => "restarts the race",
KeyCode::Enter => "opens the command line",
KeyCode::F3 => "opens the effects panel",
KeyCode::F7 | KeyCode::F8 => "stalls the client",
KeyCode::Home => "resets the camera",
KeyCode::BracketLeft | KeyCode::BracketRight | KeyCode::PageUp | KeyCode::PageDown => {
"changes the level"
Expand Down Expand Up @@ -341,7 +339,7 @@ mod tests {
assert!(text.contains(r#""gas":["KeyW"]"#), "{text}");
assert_eq!(serde_json::from_str::<Controls>(&text).unwrap(), controls);

let text = r#"{"left": ["KeyA", "NoSuchKey"], "item": ["KeyR"]}"#;
let text = r#"{"left": ["KeyA", "NoSuchKey"], "item": ["KeyP"]}"#;
let read: Controls = serde_json::from_str(text).unwrap();
assert_eq!(read.keys(Control::Left), [KeyCode::KeyA]);
assert!(read.keys(Control::Item).is_empty());
Expand Down
29 changes: 5 additions & 24 deletions crates/client/src/drive/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ const MAX_LEAD: u32 = 60;
/// stays that long.
const WAIT_TOAST_TICKS: u32 = 90;

/// How long F7 and F8 freeze an online client for.
const STALLS_MS: [u64; 2] = [500, 2000];

/// Freezes this client for `time`. A browser cannot sleep: there it spins.
/// Freezes this client for `time` (`/debug stall`). A browser cannot sleep: there it spins.
fn stall(time: std::time::Duration) {
#[cfg(not(target_arch = "wasm32"))]
std::thread::sleep(time);
Expand Down Expand Up @@ -57,8 +54,6 @@ pub(super) struct Shown<'w, 's> {
#[derive(Default)]
pub(super) struct Memory {
was_paused: bool,
/// F7 and F8, for their edges.
keys_held: [bool; 2],
/// The ticks waited for a late player, and the buttons a spectator held last tick.
waits: (u32, u32),
}
Expand All @@ -85,11 +80,9 @@ pub(super) fn drive_tick(
} = shown;
let Memory {
was_paused,
keys_held,
waits,
} = &mut *memory;
let drive = &mut *drive;
let keys = &controller.keys;
// In an online race the sim is everyone's: no pause, no restarts.
let online = lobby.online.is_some();
// The level of the race is not up yet, nor the karts of the others.
Expand Down Expand Up @@ -118,10 +111,10 @@ pub(super) fn drive_tick(
return;
}

// R (or the menu's RESTART) is the grid: every start puts everything, the objects too, back
// The menu's RESTART is the grid: every start puts everything, the objects too, back
// as it was at tick 0.
let picked = std::mem::take(&mut hold.menu.restart);
let mut restart = (keys.pressed(KeyCode::KeyR) || picked) && !online;
let mut restart = picked && !online;
// The lobby closed, or the race failed: back to the grid with our kart alone.
if !online && sim.race.karts.len() != 1 {
restart = true;
Expand Down Expand Up @@ -150,19 +143,6 @@ pub(super) fn drive_tick(
each_motion(&sim, &mut motions, |motion, kart| motion.reset(kart));
}

// F7 and F8 freeze this client for a moment, to see the race put right after a hitch: the
// clock does not catch up (the frame is cut short), so the others run ahead and we wait.
// Edges are found here, not with `just_pressed`: that is cleared every rendered frame, and
// a frame can pass without a tick.
let stall_down = [KeyCode::F7, KeyCode::F8].map(|key| online && keys.pressed(key));
for (i, millis) in STALLS_MS.into_iter().enumerate() {
if stall_down[i] && !keys_held[i] {
debug!("(debug) stalling for {millis} ms");
stall(std::time::Duration::from_millis(millis));
}
}
*keys_held = stall_down;

// Tick 0 has to come first: the others must be ready as well.
if lobby.online.as_ref().is_some_and(|net| !net.go) {
for mut text in &mut net_label {
Expand Down Expand Up @@ -478,7 +458,8 @@ pub(super) fn debug_actions(
mut toasts: MessageWriter<Toast>,
) {
for action in actions.read() {
// Like F7 and F8, for as long as asked: the frame is cut short, so the others run ahead.
// Freeze for as long as asked, to see the race put right after a hitch: the frame is cut
// short, so the others run ahead.
if let Action::DebugStall(time) = action {
if lobby.online.is_some() {
debug!("(debug) stalling for {time:?}");
Expand Down
72 changes: 39 additions & 33 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ctr-rs</title>
<meta name="description" content="An online-focused port of Crash Team Racing (PS1, 1999), written in Rust.">
<meta name="description" content="An online-focused reimplementation of Crash Team Racing (PS1, 1999), written in Rust.">
<style>
@font-face {
font-family: "Press Start 2P";
Expand Down Expand Up @@ -122,8 +122,8 @@
<body>
<main>
<h1>CTR-RS</h1>
<p class="tagline">Crash Team Racing, ported to Rust.<br>Online first.</p>
<div class="screen" hidden>
<p class="tagline">Crash Team Racing (1999), rebuilt in Rust for online play.</p>
<div class="screen" hidden>
<a class="start" href="play/">PRESS START</a>
</div>

Expand All @@ -134,37 +134,36 @@ <h1>CTR-RS</h1>

<section>
<h2>About</h2>
<p>ctr-rs is an online-focused port of Crash Team Racing (PS1, 1999), written in Rust.</p>
<p>It is heavily based on the <a href="https://github.com/CTR-tools/ctr-native">ctr-native</a>
project, which is used to verify that the physics simulation and other mechanics remain as
faithful to the original as possible. As we see it, the original CTR's feel and controls are
truly S-tier, and replicating them is the absolute top priority for this project.</p>
<p>ctr-rs is an online-focused reimplementation of Crash Team Racing (PS1, 1999), written in
Rust.</p>
</section>

<section>
<h2>What's here</h2>
<ul>
<li><b>Exact physics</b> Kart against ground and wall, and kart against kart, follow the
original tick for tick. <code>vehicle/tests/tick.rs</code> checks them against the C
reference, ctr-native. The weapons, crates and the level's objects follow the original
closely.</li>
<li><b>Native performance, frame rates and aspect ratios</b> The original's wobbly graphics
have a charm of their own, but there is no need for
<li><b>Exact, reference-checked physics</b> Kart physics and collision follow the original
tick for tick. We check them against the C decompilation,
<a href="https://github.com/CTR-tools/ctr-native">ctr-native</a>. The weapons, crates and
the level's objects follow the original closely.</li>
<li><b>Truly native performance, frame rates and aspect ratios</b> The original look has a
charm of its own, but the reimplementation has no need for
<a href="https://github.com/libretro/beetle-psx-libretro/tree/master/pgxp">PGXP</a> or
other workarounds, since there is an actual depth buffer.</li>
other (awesome) emulator hacks, since it has full-precision vertices and a real depth
buffer.</li>
<li><b>Cross-platform</b> It runs and builds on any system that Rust builds on, and in the
browser.</li>
<li><b>No emulator hacks, ROM patches or launchers</b> The current "CTR online" solutions
have quite a bit of jank to get running.</li>
<li><b>Unlimited modding horizon</b> Modding ctr-native is certainly possible, but difficult
to get right, and limited in scope by its recomp-style codebase and the C language.</li>
<li><b>30 Hz simulation, high-FPS rendering</b> The game advances at the original's 30 ticks
a second, independent of the frame rate it is drawn at.</li>
<li><b>Rollback netcode with no local delay</b> Players connect directly (if possible) to
each other with <a href="https://github.com/n0-computer/iroh">iroh</a> and
<a href="https://en.wikipedia.org/wiki/QUIC">the QUIC protocol</a>.</li>
<li><b>Custom tracks</b> <code>ctr-map</code> turns an xdelta patch of the disc into a
<code>.ctrmap</code> file that the client loads through the lobby.</li>
browser too.</li>
<li><b>Hassle-free online play</b> No patched ROMs, emulator builds or launchers
required.</li>
<li><b>Peer-to-peer networking</b> Players are directly connected to each other over
<a href="https://en.wikipedia.org/wiki/QUIC">QUIC</a>, using
<a href="https://github.com/n0-computer/iroh">iroh</a>.</li>
<li><b>No network setup</b> No port forwarding, DMZ or dedicated servers. Connections punch
through NAT automatically, and fall back to a relay when they can't.</li>
<li><b>30 Hz simulation, high-FPS rendering</b> The game state advances at the original's
30 ticks a second, independent of the rendering frame rate.</li>
<li><b>Custom tracks</b> Drag and drop an <code>.xdelta</code> (from e.g.
<a href="https://ctrcustomtracks.com">CTR Custom Tracks</a>) onto the client window, and
play it with your friends right away.</li>
</ul>
</section>

Expand All @@ -184,16 +183,23 @@ <h2>How to play</h2>
<p><b>In the browser:</b> <a href="play/">press start</a> and drop the <code>.bin</code> on
the page. The image is read on your computer and is never uploaded. Needs a keyboard and a
browser with WebGL2; the first load is a big download.</p>
<p><b>Natively:</b> point <code>CTR_BIN</code> at the image, or put it at
<code>assets/ctr-u.bin</code>, and run</p>
<p><b>Natively:</b> a build from the
<a href="https://github.com/elipp/ctr-rs/releases">releases</a> asks for the image the first
time it starts. The macOS build is not signed, so allow it in System Settings &rarr; Privacy
&amp; Security ("Open Anyway"), or run
<code>xattr -dr com.apple.quarantine ctr-rs.app</code>. On Linux, make the
<code>.AppImage</code> executable (<code>chmod +x</code>) and run it.</p>
<p><b>From source:</b> the client asks for the image with a file dialog the first time; or
point <code>CTR_BIN</code> at it, or put it at <code>assets/ctr-u.bin</code>.</p>
<pre><code>cargo run -p client</code></pre>
<p><code>]</code> and <code>[</code> switch the level, <code>.</code> and <code>,</code>
switch the racer, and <code>/</code> commands go through the in-game console.</p>
switch the racer, and <code>Enter</code> opens the in-game console for <code>/</code>
commands and chat.</p>
</section>

<section>
<h2>Prior work</h2>
<p>This project stands on the work of the CTR community, and could not exist without it.</p>
<p>This project stands on the work of the CTR community, and absolutely could not exist without it.</p>
<ul>
<li><b><a href="https://github.com/CTR-tools/ctr-native">ctr-native</a></b> A native PC port
of the game built on the decompilation. Our first reference for how the game behaves, and
Expand All @@ -215,14 +221,14 @@ <h2>Notice</h2>
<p>ctr-rs is an independent fan project, not affiliated with or endorsed by Sony Interactive
Entertainment, Activision Publishing, Naughty Dog or any other rights holder. It ships no game
content: no art, models, textures, sounds, music, levels or disc contents. You provide your
own legally obtained copy of the game, and everything is read from it at runtime. Custom maps
own legally obtained copy of the game, and everything is read from it at runtime. Custom tracks
are not shipped either: you import them yourself from patches you have obtained.</p>
<p>Crash Team Racing was developed by Naughty Dog and published by Sony Computer Entertainment.
Crash Bandicoot and Crash Team Racing are trademarks of Activision Publishing, Inc.
PlayStation is a trademark of Sony Interactive Entertainment Inc.</p>
<p>Our own code is licensed under the
<a href="https://github.com/elipp/ctr-rs/blob/main/LICENSE">GNU General Public License,
version 3</a>, the license of ctr-native and CTR-ModSDK. <code>crates/gte</code> is also
version 3</a>, the license of ctr-native and CTR-ModSDK, the projects it is ported from. <code>crates/gte</code> is also
derived from MIT-licensed code; see
<a href="https://github.com/elipp/ctr-rs/blob/main/THIRD_PARTY_NOTICES.md">THIRD_PARTY_NOTICES.md</a>.</p>
<p class="note">LLM-based tooling was heavily used in the construction of this project.</p>
Expand Down
Loading