diff --git a/README.md b/README.md index 3fbdbb5..719b38e 100644 --- a/README.md +++ b/README.md @@ -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** @@ -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. @@ -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. @@ -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. diff --git a/crates/client/src/controls.rs b/crates/client/src/controls.rs index 0d6ab8a..e7cb9e8 100644 --- a/crates/client/src/controls.rs +++ b/crates/client/src/controls.rs @@ -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" @@ -341,7 +339,7 @@ mod tests { assert!(text.contains(r#""gas":["KeyW"]"#), "{text}"); assert_eq!(serde_json::from_str::(&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()); diff --git a/crates/client/src/drive/tick.rs b/crates/client/src/drive/tick.rs index ea21ec0..6c1a9b9 100644 --- a/crates/client/src/drive/tick.rs +++ b/crates/client/src/drive/tick.rs @@ -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); @@ -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), } @@ -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. @@ -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; @@ -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 { @@ -478,7 +458,8 @@ pub(super) fn debug_actions( mut toasts: MessageWriter, ) { 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:?}"); diff --git a/web/index.html b/web/index.html index af1d21f..5857e57 100644 --- a/web/index.html +++ b/web/index.html @@ -4,7 +4,7 @@ ctr-rs - +