▶ Try the live stats player — watch a real replay play back and stats accumulate frame-by-frame, right in your browser.
subtr-actor turns raw boxcars replay data into
higher-level game state, derived replay events, structured frame payloads, and
dense numeric features for analytics and ML workflows.
- Higher-level game state modeled from the raw actor graph
- Frame-by-frame structured data ready for JSON export and playback UIs
- Dense numeric feature matrices for ML, built from a string-addressable feature registry
- Derived events and cumulative stats — touches, boost pickups, dodge refreshes, goals, demolishes, and more
- One pipeline, three languages — the same Rust core drives the Python and JavaScript/WASM bindings
ReplayProcessorwalks the replay's network frames, models actor state, and tracks derived replay events such as touches, boost pad pickups, dodge refreshes, goals, player stat events, and demolishes.Collectoris the core extension point. Collectors observe the replay frame by frame and can either process every frame or control sampling viaTimeAdvance.ReplayProcessor::process_alllets multiple collectors share a single replay pass when you want to build several outputs at once.FrameRateDecoratorandCallbackCollectorprovide lightweight utilities for downsampling a collector or attaching side-effectful hooks such as progress reporting and debugging.
ReplayDataCollectorbuilds a serde-friendly replay payload with frame data, replay metadata, and derived event streams suitable for JSON export and playback UIs.NDArrayCollectoremits a densendarray::Array2with replay metadata and headers. It supports both explicit feature adders and the string-based registry exposed throughNDArrayCollector::from_stringsandNDArrayCollector::from_strings_typed.StatsCollectoraccumulates graph-backed replay statistics as a module-keyed dynamic payload suitable for builtin module selection and JSON export.StatsTimelineEventCollectoraccumulates graph-backed replay statistics as event streams plus lightweight frame scaffolding. This is the preferred timeline export when callers do not need to serialize full per-frame partial sums.StatsTimelineCollectorpreserves the legacy full snapshot timeline form (ReplayStatsTimeline) for parity checks and compatibility.
The stats module houses analysis calculators, graph nodes, stat
event calculators, and the labeled stat-aggregation types
(LabeledCounts, LabeledFloatSums) consumed by the stats collectors.
use boxcars::ParserBuilder;
use subtr_actor::ReplayDataCollector;
let bytes = std::fs::read("replay.replay").unwrap();
let replay = ParserBuilder::new(&bytes)
.must_parse_network_data()
.on_error_check_crc()
.parse()
.unwrap();
let replay_data = ReplayDataCollector::new().get_replay_data(&replay).unwrap();
println!("frames: {}", replay_data.frame_data.frame_count());
println!("touches: {}", replay_data.touch_events.len());use boxcars::ParserBuilder;
use subtr_actor::{Collector, FrameRateDecorator, NDArrayCollector};
let bytes = std::fs::read("replay.replay").unwrap();
let replay = ParserBuilder::new(&bytes)
.must_parse_network_data()
.on_error_check_crc()
.parse()
.unwrap();
let mut collector = NDArrayCollector::<f32>::from_strings(
&["BallRigidBody", "CurrentTime"],
&["PlayerRigidBody", "PlayerBoost", "PlayerAnyJump"],
)
.unwrap();
FrameRateDecorator::new_from_fps(30.0, &mut collector)
.process_replay(&replay)
.unwrap();
let (meta, features) = collector.get_meta_and_ndarray().unwrap();
println!("players: {}", meta.replay_meta.player_count());
println!("shape: {:?}", features.raw_dim());use boxcars::ParserBuilder;
use subtr_actor::StatsTimelineEventCollector;
let bytes = std::fs::read("replay.replay").unwrap();
let replay = ParserBuilder::new(&bytes)
.must_parse_network_data()
.on_error_check_crc()
.parse()
.unwrap();
let timeline = StatsTimelineEventCollector::new()
.get_replay_stats_timeline_scaffold(&replay)
.unwrap();
println!("timeline frames: {}", timeline.frames.len());
let rush_events = timeline
.events
.events
.iter()
.filter(|event| event.meta.stream == "rush")
.count();
println!("rush events: {rush_events}");- Rust:
subtr-actor - Python:
subtr-actor-py - JavaScript / WASM bindings:
@rlrml/subtr-actor - JavaScript replay player:
@rlrml/player - JavaScript stats player:
@rlrml/stats-player(see the live demo above)
cargo add subtr-actorpip install subtr-actor-py
# or, with uv:
uv add subtr-actor-py
# or, with Poetry:
poetry add subtr-actor-pynpm install @rlrml/subtr-actornpm install @rlrml/player threeThe Rust examples above carry over to the bindings: you choose feature adders by
name and get back replay metadata plus a numeric array. PlayerBoost is exposed
in raw replay units (0-255), not a percentage.
import subtr_actor
meta, ndarray = subtr_actor.get_ndarray_with_info_from_replay_filepath(
"example.replay",
global_feature_adders=["BallRigidBody", "SecondsRemaining"],
player_feature_adders=["PlayerRigidBody", "PlayerBoost", "PlayerAnyJump"],
fps=10.0,
dtype="float32",
)
print(ndarray.shape)
print(meta["column_headers"]["player_headers"][:5])import init, {
get_ndarray_with_info,
validate_replay,
} from "@rlrml/subtr-actor";
await init();
const replayData = new Uint8Array(
await fetch("example.replay").then((response) => response.arrayBuffer()),
);
const validation = validate_replay(replayData);
if (!validation.valid) {
throw new Error(validation.error ?? "Replay is not valid");
}
const result = get_ndarray_with_info(
replayData,
["BallRigidBody", "SecondsRemaining"],
["PlayerRigidBody", "PlayerBoost", "PlayerAnyJump"],
10.0,
);
console.log(result.shape);
console.log(result.metadata.column_headers.player_headers.slice(0, 5));These string identifiers select feature adders through the Python and JavaScript bindings:
- Global:
BallRigidBody,CurrentTime,SecondsRemaining - Player:
PlayerRigidBody,PlayerBoost,PlayerAnyJump,PlayerJump,PlayerEvent:touch
Analysis-backed player event indicators use PlayerEvent:<event_name> and emit
1 for a sampled frame when that player has a new event, otherwise 0.
- Rust API docs: https://docs.rs/subtr-actor
- Changelog: CHANGELOG.md
- Python package README: python/PYTHON-README.md
- JavaScript package README: js/README.md
- JavaScript player README: js/player/README.md
- Stat definitions: docs/event-definitions.md
- Statistic confidence: docs/stat-confidence.md
- Release notes and process: docs/RELEASING.md
just build
just test
just fmt
just clippy
just check # fast lint/format/compile gate — run clean before committingThese just recipes enter the flake dev shell, so they use the Rust toolchain
from nix develop instead of any older cargo/rustc on your ambient PATH.
Bindings:
just build-python
just build-jsjust build-js builds the repo-local bundler target into js/pkg. To build the web-target package that matches npm publish, run npm --prefix js install once and then npm --prefix js run build.
The crate-level docs in src/lib.rs are the source of truth for the overview
section above. Run just readme after editing them to regenerate this file;
just check fails if the two drift apart.
MIT