use serde_json::{json, Value};
use crate::diagnostics::CODES;
use crate::schema::OPERATIONS;
const PAGES: &[(&str, &str, &str)] = &[
(
"manual/getting-started",
"manual",
include_str!("../docs/getting-started.md"),
),
(
"manual/concepts",
"manual",
include_str!("../docs/concepts.md"),
),
(
"manual/scripting",
"manual",
include_str!("../docs/scripting.md"),
),
(
"manual/determinism",
"manual",
include_str!("../docs/determinism.md"),
),
(
"manual/dev-environment",
"manual",
include_str!("../docs/dev-environment.md"),
),
(
"tutorial/hello-cube",
"tutorial",
include_str!("../docs/tutorials/01-hello-cube.md"),
),
(
"tutorial/coin-run-cli",
"tutorial",
include_str!("../docs/tutorials/02-coin-run-cli.md"),
),
(
"tutorial/gameplay-plugin",
"tutorial",
include_str!("../docs/tutorials/03-gameplay-plugin.md"),
),
(
"tutorial/gpu-cube-rain",
"tutorial",
include_str!("../docs/tutorials/04-gpu-cube-rain.md"),
),
(
"guide/gpu-condition-shaders",
"guide",
include_str!("../docs/gpu-condition-shaders.md"),
),
("guide/audio", "guide", include_str!("../docs/audio.md")),
("guide/cameras", "guide", include_str!("../docs/cameras.md")),
(
"guide/look-and-feel",
"guide",
include_str!("../docs/look-and-feel.md"),
),
(
"guide/menus-and-ui",
"guide",
include_str!("../docs/menus-and-ui.md"),
),
(
"guide/agent-skill",
"guide",
include_str!("../skills/rusting-game/SKILL.md"),
),
(
"guide/project-agents",
"guide",
include_str!("project_agents.md"),
),
("guide/effects", "guide", include_str!("../docs/effects.md")),
(
"guide/animation",
"guide",
include_str!("../docs/animation.md"),
),
(
"guide/lighting",
"guide",
include_str!("../docs/lighting.md"),
),
(
"guide/pitfalls",
"guide",
include_str!("../docs/pitfalls.md"),
),
(
"guide/networking",
"guide",
include_str!("../docs/networking.md"),
),
(
"guide/godot-migration",
"guide",
include_str!("../docs/godot-migration.md"),
),
(
"guide/plugin-authoring",
"guide",
include_str!("../docs/plugin-authoring.md"),
),
];
macro_rules! samples {
($($name:literal),* $(,)?) => {
&[$((
$name,
include_str!(concat!("../docs/samples/", $name, "/README.md")),
include_str!(concat!("../docs/samples/", $name, "/main.rs")),
)),*]
};
}
const SAMPLES: &[(&str, &str, &str)] = samples![
"brick_bounce",
"core_defense",
"crate_keeper",
"ember_arena",
"forever_bear_booth",
"hammer_run",
"lantern_grid",
"night_vault",
"putt_course",
"sky_hop",
"snake_trail",
"target_range",
"tower_topple",
];
pub struct DocItem {
pub id: String,
pub kind: &'static str,
pub title: String,
pub summary: String,
pub text: String,
}
#[must_use]
pub fn tokens(text: &str) -> usize {
text.chars().count().div_ceil(4)
}
fn first_sentence(text: &str) -> String {
let end = text
.match_indices(['.'])
.map(|(index, _)| index)
.find(|&index| {
text[index + 1..].starts_with([' ', '\n'])
&& !text[..index].ends_with('.')
})
.map_or(text.len(), |index| index + 1);
text[..end].trim().replace('\n', " ")
}
fn page(id: &str, kind: &'static str, text: &str) -> DocItem {
let text = text.trim_start_matches('\u{feff}');
let title = text
.lines()
.find_map(|line| line.strip_prefix("# "))
.unwrap_or(id)
.trim()
.to_owned();
let described = text.strip_prefix("---").and_then(|rest| {
let front = rest.split("\n---").next()?;
front
.lines()
.find_map(|line| line.strip_prefix("description:"))
.map(|description| first_sentence(description.trim()))
});
let summary = described.unwrap_or_else(|| {
let paragraph: Vec<&str> = text
.lines()
.map(str::trim)
.skip_while(|line| {
line.is_empty()
|| line.starts_with(['#', '`', '-', '|', '>', '<', '!'])
})
.take_while(|line| !line.is_empty())
.collect();
first_sentence(¶graph.join(" "))
});
DocItem {
id: id.to_owned(),
kind,
title,
summary,
text: text.to_owned(),
}
}
fn scenario_reference() -> DocItem {
let section = crate::schema::catalog_entry("scenario")
.expect("the catalog has a scenario section");
let line = |value: &Value| match value {
Value::String(text) => text.clone(),
other => other.to_string(),
};
let mut text = String::from(
"# Scenario file reference\n\nEvery field of a `rusting test` \
scenario file and every step kind, generated from \
`rusting schema scenario`.\n",
);
for (key, value) in section.as_object().into_iter().flatten() {
text += &format!("\n## {key}\n\n");
match value {
Value::Object(fields) => {
for (name, field) in fields {
text += &format!("- `{name}`: {}\n", line(field));
}
}
other => text += &format!("{}\n", line(other)),
}
}
page("reference/scenario", "reference", &text)
}
fn reflected_items() -> Vec<DocItem> {
let catalog = crate::schema::catalog();
let scene = &catalog["scene"];
let groups = [
("component", &scene["entity_sections"]),
("component", &scene["components"]),
("resource", &catalog["resources"]),
("asset", &catalog["asset_types"]),
];
let mut items = Vec::new();
for (kind, group) in groups {
for entry in group.as_array().into_iter().flatten() {
let key = entry["key"].as_str().unwrap_or_default();
let summary = entry["summary"].as_str().map_or_else(
|| format!("The `{key}` {kind} type and its fields."),
str::to_owned,
);
let mut text = format!("# {key}\n\n{summary}\n");
if let Some(gpu) = entry["gpu_cost"].as_str() {
text += &format!("\nGPU: {gpu}\n");
}
text += "\nFields:\n\n";
for field in entry["fields"].as_array().into_iter().flatten() {
let path = field["path"].as_str().unwrap_or_default();
text += &format!(
"- `{}`",
if path.is_empty() { "/" } else { path }
);
let hints: Vec<&str> = ["unit", "range"]
.iter()
.filter_map(|hint| field[hint].as_str())
.filter(|hint| !hint.is_empty())
.collect();
if !hints.is_empty() {
text += &format!(" ({})", hints.join(", "));
}
match field["doc"].as_str() {
Some(doc) if !doc.is_empty() => {
text += &format!(": {doc}\n")
}
_ => text.push('\n'),
}
}
for part in ["default", "example"] {
if !entry[part].is_null() {
text += &format!(
"\n{}{}: `{}`\n",
part[..1].to_uppercase(),
&part[1..],
entry[part]
);
}
}
items.push(DocItem {
id: format!("{kind}/{key}"),
kind,
title: key.to_owned(),
summary: first_sentence(&summary),
text,
});
}
}
items
}
#[must_use]
pub fn items() -> Vec<DocItem> {
let mut items: Vec<DocItem> = PAGES
.iter()
.map(|(id, kind, text)| page(id, kind, text))
.collect();
for operation in OPERATIONS {
let mut text = format!(
"rusting {}\n\n{}\n\nGPU: {}\n",
operation.usage, operation.summary, operation.gpu
);
for (flag, value) in operation.defaults {
text += &format!("Default {flag}: {value}\n");
}
text += &format!("\nExample: rusting {}\n", operation.example);
items.push(DocItem {
id: format!("command/{}", operation.name.replace(' ', "-")),
kind: "command",
title: format!("rusting {}", operation.usage),
summary: first_sentence(operation.summary),
text,
});
}
for info in CODES {
items.push(DocItem {
id: format!("code/{}", info.code),
kind: "code",
title: info.code.to_owned(),
summary: info.summary.to_owned(),
text: format!(
"{}\n\n{}\n\nFix: {}\n\nExample: {}\n",
info.code, info.summary, info.fix, info.example
),
});
}
items.push(scenario_reference());
items.extend(reflected_items());
for (name, source) in crate::cookbook::COOKBOOK {
let header: Vec<&str> = source
.lines()
.map_while(|line| line.strip_prefix("//!"))
.map(str::trim_start)
.collect();
let mut item = page(
&format!("cookbook/{name}"),
"cookbook",
&format!("# {}", header.join("\n")),
);
item.summary.clone_from(&item.title);
item.text += &format!("\n\n```rust\n{source}```\n");
items.push(item);
}
for (name, readme, code) in SAMPLES {
let mut item = page(&format!("sample/{name}"), "sample", readme);
item.text += &format!("\n## src/main.rs\n\n```rust\n{code}```\n");
items.push(item);
}
let api = api_items();
for owner in API_TYPES {
let methods: Vec<&DocItem> = api
.iter()
.filter(|item| item.title.split("::").next() == Some(owner))
.collect();
let fields = if API_SOURCE.contains(&format!("pub struct {owner} {{")) {
struct_item(owner, API_SOURCE).text + "\n"
} else {
format!("# {owner}\n\n")
};
let mut text = format!(
"{fields}Every public method, {} in all. \
`rusting docs show api/{owner}::<method>` prints one.\n\n",
methods.len()
);
for item in &methods {
text += &format!("- {}: {}\n", item.title, item.summary);
}
items.push(DocItem {
id: format!("api/{owner}"),
kind: "api",
title: owner.to_owned(),
summary: format!("Index of every public {owner} method."),
text,
});
}
items.extend(api);
items.push(struct_item(
"PlayerController",
include_str!("runtime/player.rs"),
));
items.push(struct_item(
"GpuStateField",
include_str!("runtime/hybrid_physics.rs"),
));
items.push(struct_item("RayHit", include_str!("project_runner.rs")));
items.push(struct_item(
"GpuBodySettings",
include_str!("project_runner.rs"),
));
items.push(struct_item(
"GpuConditionShader",
include_str!("runtime/hybrid_physics.rs"),
));
items.push(struct_item("MaterialAsset", include_str!("assets/mod.rs")));
items.push(struct_item("Sound", include_str!("runtime/audio.rs")));
items.push(struct_item(
"ParticleEmitter",
include_str!("runtime/particles.rs"),
));
items.push(struct_item(
"Animation",
include_str!("runtime/animation.rs"),
));
items.push(struct_item(
"AnimationEvent",
include_str!("runtime/animation.rs"),
));
items.push(struct_item("Skin", include_str!("runtime/skinning.rs")));
items.push(struct_item("Morph", include_str!("runtime/skinning.rs")));
items.push(struct_item("Ik", include_str!("runtime/ik.rs")));
items.push(struct_item("Ragdoll", include_str!("runtime/ragdoll.rs")));
items.push(struct_item(
"HudElement",
include_str!("runtime/game_feel.rs"),
));
items.push(struct_item(
"HudAnchor",
include_str!("runtime/game_feel.rs"),
));
items.push(struct_item(
"PhysicsSyncMode",
include_str!("runtime/hybrid_physics.rs"),
));
items.push(struct_item("BusEffect", include_str!("runtime/audio.rs")));
items
}
fn struct_item(name: &str, source: &str) -> DocItem {
let enum_start = source.find(&format!("pub enum {name} {{"));
let start = enum_start
.or_else(|| source.find(&format!("pub struct {name} {{")))
.expect("type is in its source file");
let head = &source[..start];
let doc: Vec<&str> = head
.lines()
.rev()
.skip_while(|line| {
!line.starts_with("///") && !line.is_empty() && *line != "}"
})
.take_while(|line| line.starts_with("///"))
.filter_map(|line| line.strip_prefix("///"))
.map(str::trim)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let list = if enum_start.is_some() {
"Variants"
} else {
"Fields"
};
let mut text = format!("# {name}\n\n{}\n\n{list}:\n\n", doc.join(" "));
let mut field_doc: Vec<&str> = Vec::new();
for line in source[start..].lines().skip(1).map(str::trim) {
if line == "}" {
break;
}
if let Some(doc) = line.strip_prefix("///") {
field_doc.push(doc.trim());
} else if let Some(field) = line.strip_prefix("pub ").or(enum_start
.map(|_| line)
.filter(|line| !line.starts_with('#')))
{
let field = field.trim_end_matches(',');
text += &format!("- `{field}`");
if !field_doc.is_empty() {
text += &format!(": {}", field_doc.join(" "));
}
text.push('\n');
field_doc.clear();
}
}
DocItem {
id: format!("api/{name}"),
kind: "api",
title: name.to_owned(),
summary: first_sentence(&doc.join(" ")),
text,
}
}
const API_TYPES: [&str; 8] = [
"GameScene",
"GameObject",
"CubeSpawn",
"SphereSpawn",
"GpuCondition",
"GpuFieldCondition",
"BeatClock",
"WaypointGraph",
];
const API_SOURCE: &str = concat!(
include_str!("project_runner.rs"),
include_str!("runtime/hybrid_physics.rs"),
include_str!("runtime/audio.rs"),
include_str!("runtime/waypoints.rs")
);
fn api_items() -> Vec<DocItem> {
let source = API_SOURCE;
let mut items = Vec::new();
let mut owner = None;
let mut docs: Vec<&str> = Vec::new();
let mut signature: Option<String> = None;
for line in source.lines() {
if let Some(sig) = signature.as_mut() {
sig.push(' ');
sig.push_str(line.trim());
} else if let Some(rest) = line.strip_prefix("impl ") {
owner = API_TYPES.iter().copied().find(|name| {
rest.strip_prefix(name)
.is_some_and(|r| r.starts_with(['<', ' ']))
});
docs.clear();
continue;
} else if line == "}" {
owner = None;
continue;
} else if let Some(doc) = line.trim().strip_prefix("///") {
docs.push(doc.strip_prefix(' ').unwrap_or(doc));
continue;
} else if line.starts_with(" pub fn ") {
signature = Some(line.trim().to_owned());
} else {
if !line.trim().starts_with('#') {
docs.clear();
}
continue;
}
if line.trim_end().ends_with(['{', ';']) {
let sig = signature.take().unwrap_or_default();
let sig = sig.trim_end_matches(['{', ';']).trim().to_owned();
items.extend(api_item(owner, &sig, &docs));
docs.clear();
}
}
items
}
fn api_item(
owner: Option<&str>,
signature: &str,
docs: &[&str],
) -> Option<DocItem> {
let owner = owner?;
let name = signature
.strip_prefix("pub fn ")?
.split(['(', '<'])
.next()?
.trim();
let text = docs.join("\n");
Some(DocItem {
id: format!("api/{owner}::{name}"),
kind: "api",
title: format!("{owner}::{name}"),
summary: first_sentence(text.trim()),
text: format!("{signature}\n\n{}\n", text.trim()),
})
}
#[must_use]
pub fn find(id: &str) -> Option<DocItem> {
let wanted = id.to_ascii_lowercase();
let mut matches = items().into_iter().filter(|item| {
let name = item.id.to_ascii_lowercase();
name == wanted || name.split_once('/').is_some_and(|(_, n)| n == wanted)
});
let first = matches.next()?;
matches.next().is_none().then_some(first)
}
#[must_use]
pub fn search(query: &str, limit: usize) -> (Vec<Value>, usize) {
let words: Vec<String> =
query.split_whitespace().map(str::to_lowercase).collect();
if words.is_empty() {
return (Vec::new(), 0);
}
let mut scored: Vec<(usize, DocItem)> = items()
.into_iter()
.filter_map(|item| {
let text = item.text.to_lowercase();
let head = format!("{} {}", item.id, item.title).to_lowercase();
let mut score = 0;
for word in &words {
let hits = text.matches(word.as_str()).count();
let in_head = head.contains(word.as_str());
if hits == 0 && !in_head {
return None;
}
score += hits.min(20) + if in_head { 25 } else { 0 };
}
Some((score, item))
})
.collect();
scored.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.id.cmp(&b.1.id)));
let total = scored.len();
let matches = scored
.into_iter()
.take(limit)
.map(|(score, item)| {
let found = |line: &str| {
let line = line.to_lowercase();
words
.iter()
.filter(|word| line.contains(word.as_str()))
.count()
};
let snippet = item
.text
.lines()
.skip(1)
.enumerate()
.filter(|(_, line)| found(line) > 0)
.max_by_key(|(index, line)| {
(found(line), std::cmp::Reverse(*index))
})
.map(|(_, line)| line)
.or_else(|| item.text.lines().find(|line| found(line) > 0))
.map(|line| line.trim().chars().take(160).collect::<String>());
json!({
"id": item.id,
"kind": item.kind,
"title": item.title,
"score": score,
"snippet": snippet,
})
})
.collect();
(matches, total)
}
#[must_use]
pub fn within_budget(text: &str, budget: usize) -> (String, bool) {
if tokens(text) <= budget {
return (text.to_owned(), false);
}
let mut out = String::new();
for line in text.lines() {
if !out.is_empty() && tokens(&format!("{out}{line}\n")) > budget {
break;
}
out += line;
out.push('\n');
}
(out, true)
}
#[must_use]
pub fn brief(budget: usize) -> (String, usize, usize) {
let head = "# Rusting engine\n\n\
Rust game engine with a Vulkan renderer, ECS, and a CLI built for \
agents: every command has `--json`, every error has a code.\n\n\
Look up: `rusting docs search <words>`, `rusting docs show <id>`, \
`rusting explain <CODE>`, `rusting schema --json`.\n\n";
let items = items();
let mut text = head.to_owned();
let mut listed = 0;
let mut kind = "";
let room = budget.saturating_sub(FOOTER_TOKENS);
for item in &items {
let heading = if item.kind == kind {
String::new()
} else {
format!("\n## {}s\n", item.kind)
};
let heading = if listed == 0 {
heading.trim_start().to_owned()
} else {
heading
};
let line = format!("{heading}- {}: {}\n", item.id, item.summary);
if listed > 0 && tokens(&text) + tokens(&line) > room {
break;
}
text += &line;
kind = item.kind;
listed += 1;
}
if listed < items.len() {
text += &format!(
"\n{} more items; `rusting docs` lists them all.\n",
items.len() - listed
);
}
(text, listed, items.len())
}
const FOOTER_TOKENS: usize = 20;
#[must_use]
pub fn component_list() -> String {
crate::schema::component_summaries()
.map(|(key, summary)| {
format!("- `{key}`: {}\n", first_sentence(summary))
})
.collect()
}
#[must_use]
pub fn pitfall_list() -> String {
let mut list = String::new();
for section in include_str!("../docs/pitfalls.md").split("\n## ").skip(1) {
let heading = section.lines().next().unwrap_or_default();
let titles: Vec<&str> = section
.split("\n**")
.skip(1)
.filter_map(|entry| entry.split_once("**"))
.map(|(title, _)| title.trim_end_matches('.'))
.collect();
list += &format!("- {heading}: {}.\n", titles.join("; "));
}
list
}
const API_GROUPS: &[(&str, &[&str])] = &[
(
"find, move, parent",
&["object", "try_object", "reparent", "name_of", "world"],
),
("contacts", &["touching"]),
(
"counters",
&[
"counter",
"counter_value",
"counter_or",
"set_counter",
"add_to_counter",
"spend",
"counter_complete",
"counters",
],
),
(
"cooldowns",
&["cooldown_ready", "start_cooldown", "cooldown_left"],
),
("state machines", &["set_state", "state", "state_seconds"]),
(
"health",
&[
"damage",
"health",
"same_team",
"add_status",
"has_status",
"status_left",
"clear_status",
],
),
(
"input",
&[
"pressed",
"held",
"press_tick",
"stick",
"clicked",
"cursor",
"keys_pressed",
"binding",
"rebind",
"window_focused",
"viewport_size",
],
),
(
"rays",
&[
"raycast",
"raycast_skipping",
"raycast_only",
"raycast_visible",
"aim",
"camera_ray",
"pointer_ray",
"on_screen",
],
),
(
"cameras",
&[
"set_active_camera",
"set_camera",
"set_camera_fov",
"camera_fov",
"basis",
"set_mouse_look",
],
),
(
"physics",
&[
"set_body_kind",
"set_linear_velocity",
"linear_velocity",
"set_angular_velocity",
"angular_velocity",
"reset_body",
],
),
("player", &["set_look", "set_player", "player", "dash"]),
(
"create, remove",
&[
"spawn_cube",
"spawn_sphere",
"spawn_cube_with_material",
"spawn_sphere_with_material",
"spawn_copy",
"spawn_copy_at_root",
"spawn_numbered",
"spawn_prefab",
"despawn",
],
),
(
"classes",
&["in_class", "has_class", "add_class", "remove_class"],
),
(
"sound",
&[
"play_sound",
"play_sound_looped",
"play_sound_with",
"play_sound_on",
"set_sound_rate",
"set_sound_position",
"set_sound_volume",
"pause_sound",
"resume_sound",
"pause_sounds",
"resume_sounds",
"seek_sound",
"stop_sound",
"stop_all_sounds",
"playing_sounds",
"sounds_requested",
"set_master_volume",
"set_bus_volume",
"mute_bus",
"solo_bus",
"set_bus_effect",
"set_bus_voice_limit",
"set_listener",
"set_captions",
],
),
(
"look",
&[
"set_hud",
"set_visible",
"color",
"set_color",
"set_emissive",
"create_material",
"set_material",
"set_text",
"set_text_in_font",
"set_light",
"set_background_color",
"set_reflections",
"set_exposure",
"set_hard_shadows",
"create_texture",
"edit_texture",
],
),
(
"game feel",
&[
"flash",
"squash",
"add_trauma",
"hit_stop",
"particles",
"trigger",
"tween",
],
),
(
"animation",
&[
"play_animation",
"crossfade",
"stop_animation",
"is_playing",
"set_animation_speed",
"set_animation_parameter",
"animation_events",
"take_root_motion",
"set_ragdoll",
"set_ragdoll_muscle",
"reset_ragdoll",
"is_limp",
],
),
(
"rounds, levels",
&[
"once",
"restart",
"load_scene",
"initial",
"snapshot",
"restore",
"state_hash",
"seed",
"random",
"roll_loot",
"roll_loot_table",
],
),
(
"menus, saves",
&[
"ui",
"set_text_scale",
"text_scale",
"set_paused",
"paused",
"set_time_scale",
"time_scale",
"quit",
"save_data",
"load_data",
"delete_data",
"saved_keys",
"save_counters",
"save_objects",
"load_objects",
"load_counters",
"load_text",
],
),
(
"video settings",
&[
"set_render_scale",
"render_scale",
"set_pixelated",
"set_upscale_sharpness",
"set_vsync",
"set_max_fps",
"set_fullscreen",
"fullscreen",
"set_window_size",
"set_window_title",
],
),
("translations", &["set_locale", "locale", "tr", "tr_count"]),
(
"dialogue",
&["start_dialogue", "dialogue_line", "advance_dialogue"],
),
("tiles, fields", &["tile", "set_tile", "set_field"]),
(
"GPU bodies",
&[
"apply_gpu_physics_to_class",
"watch_gpu_class",
"watch_gpu_object",
"set_gpu_condition_shaders",
"gpu_events",
"gpu_state",
"count_gpu_bodies_in_box",
"gpu_command",
],
),
("other", &[]),
];
fn api_call(item: &DocItem) -> String {
let (_, name) = item.title.split_once("::").unwrap_or(("", &item.title));
let signature = item.text.lines().next().unwrap_or_default();
let after = &signature[signature.find(name).unwrap_or(0) + name.len()..];
let (mut depth, mut args, mut list) = (0, Vec::new(), String::new());
let mut previous = ' ';
for c in after.chars() {
let arrow = previous == '-';
previous = c;
match c {
'<' | '[' => depth += 1,
'>' | ']' if !arrow => depth -= 1,
'(' if depth == 0 && list.is_empty() && args.is_empty() => {
depth = 100;
continue;
}
'(' => depth += 1,
')' if depth == 100 => break,
')' => depth -= 1,
',' if depth == 100 => {
args.push(std::mem::take(&mut list));
continue;
}
_ => {}
}
if depth >= 100 {
list.push(c);
}
}
args.push(list);
let args: Vec<&str> = args
.iter()
.filter_map(|arg| arg.split(':').next())
.map(|arg| {
arg.trim()
.trim_start_matches("mut ")
.trim_start_matches('_')
})
.filter(|arg| !arg.is_empty() && !arg.ends_with("self"))
.collect();
format!("`{name}({})`", args.join(", "))
}
#[must_use]
pub fn api_table() -> String {
let api = api_items();
let scene: Vec<&DocItem> = api
.iter()
.filter(|item| item.title.starts_with("GameScene::"))
.collect();
let method = |item: &&DocItem| item.title["GameScene::".len()..].to_owned();
let mut table = String::from("| Need | Call |\n| --- | --- |\n");
for (need, names) in API_GROUPS {
let calls: Vec<String> = if names.is_empty() {
let grouped: Vec<&str> = API_GROUPS
.iter()
.flat_map(|(_, n)| n.iter().copied())
.collect();
scene
.iter()
.filter(|item| !grouped.contains(&method(item).as_str()))
.map(|item| api_call(item))
.collect()
} else {
names
.iter()
.map(|name| {
let item = scene
.iter()
.find(|item| method(item) == *name)
.unwrap_or_else(|| {
panic!("`{name}` is not a GameScene method")
});
api_call(item)
})
.collect()
};
if !calls.is_empty() {
table += &format!("| {need} | {} |\n", calls.join(", "));
}
}
let object: Vec<String> = api
.iter()
.filter(|item| item.title.starts_with("GameObject::"))
.map(api_call)
.collect();
table += &format!("| on `object(name)` | {} |\n", object.join(", "));
table
}
#[cfg(test)]
mod tests {
#[test]
fn sample_doc_copies_match_the_samples() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for (name, readme, code) in super::SAMPLES {
let dir = root.join("samples").join(name);
if !dir.exists() {
continue; }
let stale = "is out of date; run scripts/sync_sample_docs.sh";
let real = std::fs::read_to_string(dir.join("README.md")).unwrap();
assert_eq!(*readme, real, "docs/samples/{name}/README.md {stale}");
let real =
std::fs::read_to_string(dir.join("src/main.rs")).unwrap();
assert_eq!(*code, real, "docs/samples/{name}/main.rs {stale}");
}
}
#[test]
fn api_index_lists_documented_gameplay_methods() {
let once = super::find("api/GameScene::once").expect("once indexed");
assert!(once.text.starts_with("pub fn once("), "{}", once.text);
assert!(once.summary.contains("once per round"), "{}", once.summary);
let api: Vec<_> = super::items()
.into_iter()
.filter(|i| i.kind == "api")
.collect();
assert!(api.len() > 30, "only {} api items", api.len());
assert!(api.iter().all(|i| !i.summary.is_empty()), "undocumented");
let lighting = super::find("guide/lighting").expect("lighting");
for wanted in ["1024", "Eco", "4 GiB", "shadows", "dropped_lights"] {
assert!(lighting.text.contains(wanted), "{wanted}");
}
let graph = super::find("api/WaypointGraph").expect("waypoints");
for wanted in ["nodes", "nearest", "disconnect", "path", "length"] {
assert!(graph.text.contains(wanted), "{wanted}: {}", graph.text);
}
for (page, field) in [
("api/GpuBodySettings", "- `collision_layers:"),
(
"api/GpuConditionShader",
"- `params: Vec<[f32; 4]>`: Values",
),
("api/MaterialAsset", "- `roughness: f32`: 0 is a mirror"),
] {
let item = super::find(page).expect(page);
assert!(item.text.contains(field), "{page}: {}", item.text);
}
let player = super::find("api/PlayerController").expect("player");
assert!(
player.summary.starts_with("Walk, jump"),
"{}",
player.summary
);
assert!(
player.text.contains("- `wall: Option<Entity>`: The body"),
"{}",
player.text
);
assert!(!player.text.contains("`: \n"), "{}", player.text);
let event = super::find("api/AnimationEvent").expect("animation event");
for field in [
"tick: u64",
"entity: Entity",
"object: String",
"clip",
"name",
] {
assert!(event.text.contains(field), "{}", event.text);
}
}
use super::*;
#[test]
fn every_item_has_a_unique_id_a_title_and_a_summary() {
let items = items();
let mut ids: Vec<_> = items.iter().map(|item| &item.id).collect();
ids.sort();
ids.dedup();
assert_eq!(ids.len(), items.len(), "ids are unique");
for item in &items {
assert!(!item.title.is_empty(), "{} has no title", item.id);
assert!(!item.summary.is_empty(), "{} has no summary", item.id);
assert!(item.id.contains('/'), "{} has no kind", item.id);
}
assert_eq!(
items.iter().filter(|item| item.kind == "command").count(),
OPERATIONS.len()
);
assert_eq!(
items.iter().filter(|item| item.kind == "code").count(),
CODES.len()
);
}
#[test]
fn the_scenario_reference_lists_every_step_kind() {
let page = find("scenario").unwrap();
assert_eq!(page.id, "reference/scenario");
for step in ["expect_pixels", "expect_screen", "capture", "log", "set"]
{
assert!(page.text.contains(&format!("- `{step}`:")), "{step}");
}
assert!(page.text.contains("## budgets"));
}
#[test]
fn every_reflected_type_has_a_reference_page() {
let items = items();
for key in crate::schema::component_summaries().map(|(key, _)| key) {
let id = format!("component/{key}");
assert!(items.iter().any(|item| item.id == id), "{id}");
}
let fog = find("component/rusting.fog").unwrap();
assert!(fog.text.contains("- `/density` (1/m, 0..1): extinction"));
assert!(fog.text.contains("\nDefault: `{"));
assert!(find("component/transform").is_some());
assert!(find("resource/rusting.physics_settings").is_some());
assert!(find("asset/rusting.material").is_some());
}
#[test]
fn find_accepts_a_bare_name_only_when_it_is_unambiguous() {
assert_eq!(find("scene_conflict").unwrap().id, "code/SCENE_CONFLICT");
assert_eq!(find("command/scene-patch").unwrap().kind, "command");
assert!(find("no-such-item").is_none());
}
#[test]
fn search_needs_every_word_and_ranks_head_hits_first() {
let (found, _) = search("scene patch", 5);
assert!(!found.is_empty());
assert_eq!(found[0]["id"], "command/scene-patch");
assert!(search("zzzznotaword", 5).0.is_empty());
let (found, _) = search("text texture", 50);
let guide =
found.iter().find(|item| item["id"] == "guide/menus-and-ui");
let snippet =
guide.unwrap()["snippet"].as_str().unwrap().to_lowercase();
assert!(snippet.contains("text_texture"), "{snippet}");
assert!(search(" ", 5).0.is_empty());
let (three, total) = search("scene", 3);
assert_eq!(three.len(), 3);
assert!(total > 3, "total counts matches past the limit");
}
#[test]
fn level_of_detail_is_found_by_its_usual_names() {
for query in ["lod", "level of detail", "rlod"] {
let (found, _) = search(query, 50);
assert!(
found.iter().any(|item| item["id"] == "guide/look-and-feel"),
"{query}"
);
}
}
#[test]
fn the_headless_render_size_is_found_by_its_usual_names() {
for query in ["resolution", "render size", "1080p"] {
let (found, _) = search(query, 50);
assert!(
found.iter().any(|item| item["id"] == "guide/look-and-feel"),
"{query}"
);
}
}
#[test]
fn the_lighting_guide_explains_brightness_and_direction() {
for query in ["illuminance intensity", "spot light axis"] {
let (found, _) = search(query, 50);
assert!(
found.iter().any(|item| item["id"] == "guide/lighting"),
"{query}"
);
}
}
#[test]
fn bus_effects_and_press_tick_are_documented() {
let items = items();
let page = |id: &str| {
items
.iter()
.find(|item| item.id == id)
.unwrap_or_else(|| panic!("no {id}"))
.text
.clone()
};
let bus = page("api/BusEffect");
assert!(
bus.contains("LowPass") && bus.contains("cutoff_hz"),
"{bus}"
);
let press = page("api/GameScene::press_tick");
assert!(press.contains("\"at\""), "{press}");
}
#[test]
fn counter_or_is_documented() {
let (found, _) = search("counter_or default without a warning", 50);
assert!(found.iter().any(|item| item["id"] == "guide/menus-and-ui"));
}
#[test]
fn entity_order_effect_on_physics_is_documented() {
let (found, _) = search("unrelated scene edit toppled", 50);
assert!(found.iter().any(|item| item["id"] == "manual/determinism"));
}
#[test]
fn when_perf_render_is_filled_is_documented() {
let (found, _) = search("perf.render cameras null budget", 50);
assert!(found.iter().any(|item| item["id"] == "manual/concepts"));
let schema = crate::schema::catalog().to_string();
assert!(
schema.contains("no capture step needed"),
"split-signal F12"
);
}
#[test]
fn keep_going_and_its_variable_are_documented() {
let (found, _) = search("RUSTING_KEEP_GOING", 50);
assert!(found.iter().any(|item| item["id"] == "manual/concepts"));
}
#[test]
fn build_time_budget_and_shared_target_are_documented() {
let (found, _) = search("CARGO_TARGET_DIR shared build", 50);
assert!(found.iter().any(|item| item["id"] == "manual/concepts"));
}
#[test]
fn primitive_uvs_and_child_scale_are_explained() {
for (query, page) in [
("cylinder cap texture disc", "guide/look-and-feel"),
("child transform relative parent scale", "manual/concepts"),
] {
let (found, _) = search(query, 50);
assert!(found.iter().any(|item| item["id"] == page), "{query}");
}
use crate::assets::{procedural_primitive_mesh, PrimitiveShape};
for shape in [PrimitiveShape::Cylinder, PrimitiveShape::Cone] {
let mesh = procedural_primitive_mesh(shape);
let caps: Vec<_> = mesh
.vertices
.iter()
.filter(|v| v.normal[0] == 0.0 && v.normal[2] == 0.0)
.collect();
assert!(!caps.is_empty(), "{shape:?}");
for v in caps {
let u = 0.5 + v.position[0] * v.normal[1];
let w = 0.5 + v.position[2];
assert!((v.uv[0] - u).abs() < 1e-5, "{shape:?} {v:?}");
assert!((v.uv[1] - w).abs() < 1e-5, "{shape:?} {v:?}");
}
}
for (shape, half) in [
(PrimitiveShape::Cylinder, 0.5),
(PrimitiveShape::Cone, 0.5),
(PrimitiveShape::Capsule, 1.0),
] {
let mesh = procedural_primitive_mesh(shape);
for v in mesh
.vertices
.iter()
.filter(|v| v.normal[0] != 0.0 || v.normal[2] != 0.0)
{
let expected = (half - v.position[1]) / (2.0 * half);
assert!((v.uv[1] - expected).abs() < 1e-4, "{shape:?} {v:?}");
}
}
let plane = procedural_primitive_mesh(PrimitiveShape::Plane);
let uvs: Vec<_> = plane.vertices.iter().map(|v| v.uv).collect();
assert_eq!(uvs, [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]);
}
#[test]
fn many_body_costs_are_explained() {
for query in ["subdivisions triangles", "state_hash checkpoint"] {
let (found, _) = search(query, 50);
assert!(
found.iter().any(|item| item["id"] == "manual/concepts"),
"{query}"
);
}
for n in [3, 6, 16] {
let mesh = crate::assets::procedural_sphere_mesh(n);
assert_eq!(mesh.indices.len() as u32 / 3, 4 * n * n);
}
}
#[test]
fn contact_grid_overflow_is_explained() {
let (found, _) = search("contact grid overflow", 50);
assert!(
found.iter().any(|item| item["id"] == "manual/concepts"),
"{found:?}"
);
}
#[test]
fn budget_cuts_at_a_line_and_the_brief_fits_it() {
let (cut, truncated) = within_budget("aaaa\nbbbb\ncccc\n", 3);
assert!(truncated);
assert_eq!(cut, "aaaa\nbbbb\n");
assert_eq!(within_budget("abc", 5), ("abc".to_owned(), false));
for budget in [300, 1000, 4000] {
let (text, listed, total) = brief(budget);
assert!(tokens(&text) <= budget, "{budget}: {}", tokens(&text));
assert!(listed >= 1 && listed <= total);
assert_eq!(
text.contains(" more items;"),
listed < total,
"the brief says what it left out"
);
}
let (_, listed, total) = brief(1_000_000);
assert_eq!(listed, total, "a big budget lists everything");
}
#[test]
fn the_brief_names_only_commands_that_exist() {
let (text, _, _) = brief(1_000_000);
for word in ["docs", "explain", "schema"] {
assert!(
OPERATIONS.iter().any(|operation| operation.name == word),
"{word}"
);
assert!(text.contains(&format!("`rusting {word}")));
}
}
#[test]
fn documented_commands_name_real_operations_and_flags() {
let mut commands: Vec<(String, String)> = Vec::new();
for operation in OPERATIONS {
commands.push((
format!("catalog {}", operation.name),
format!("rusting {}", operation.example),
));
}
for item in items().into_iter().filter(|item| item.kind != "api") {
for line in item.text.lines() {
let mut rest = line;
while let Some(start) = rest.find("`rusting ") {
let after = &rest[start + 1..];
let Some(end) = after.find('`') else { break };
commands.push((item.id.clone(), after[..end].to_owned()));
rest = &after[end..];
}
if let Some(command) = line
.trim_start()
.strip_prefix("$ ")
.filter(|c| c.starts_with("rusting "))
{
commands.push((item.id.clone(), command.to_owned()));
}
}
}
assert!(commands.len() > 40, "only {} commands", commands.len());
let mut stale = Vec::new();
for (source, command) in commands {
let words: Vec<&str> = command
.split_whitespace()
.skip(1)
.take_while(|w| !w.starts_with(['-', '<', '[', '.']))
.collect();
let operation = OPERATIONS
.iter()
.filter(|op| {
let name: Vec<&str> = op.name.split(' ').collect();
words.len() >= name.len() && words[..name.len()] == name[..]
})
.max_by_key(|op| op.name.len());
let Some(operation) = operation else {
if !words.is_empty() {
stale
.push(format!("{source}: `{command}` unknown command"));
}
continue;
};
for flag in command
.split_whitespace()
.filter(|w| w.starts_with("--"))
.map(|w| w.trim_end_matches([',', '.', ';', ')', '`']))
{
if !matches!(flag, "--json" | "--help")
&& !operation.usage.contains(flag)
{
stale.push(format!(
"{source}: `{command}` flag {flag} not in `{}`",
operation.usage
));
}
}
}
assert!(stale.is_empty(), "stale examples:\n{}", stale.join("\n"));
}
#[test]
fn gameplay_methods_do_not_return_new_borrows() {
const KNOWN: [&str; 4] = [
"api/GameScene::object",
"api/GameScene::try_object",
"api/GameScene::counter",
"api/GameScene::world",
];
let mut found = Vec::new();
for item in items().into_iter().filter(|item| item.kind == "api") {
let signature = item.text.lines().next().unwrap_or_default();
let Some((_, returns)) = signature.split_once("->") else {
continue;
};
let chaining = item.id.starts_with("api/GameObject::")
&& returns.trim() == "&mut Self";
if !chaining && returns.contains(['&', '\'']) {
found.push(item.id);
}
}
let new: Vec<_> = found
.iter()
.filter(|id| !KNOWN.contains(&id.as_str()))
.collect();
assert!(new.is_empty(), "returns a borrow: {new:?}");
let gone: Vec<_> = KNOWN
.iter()
.filter(|id| !found.iter().any(|f| f == *id))
.collect();
assert!(gone.is_empty(), "fixed; remove from KNOWN: {gone:?}");
}
#[test]
fn preferred_calls_exist_in_the_index() {
let ids: Vec<String> = items()
.into_iter()
.filter(|item| item.kind == "api")
.map(|item| item.id)
.collect();
let mut marked = 0;
for item in items().into_iter().filter(|item| item.kind == "api") {
let owner = item.id.split("::").next().unwrap_or_default();
let Some(start) = item.text.find("Preferred:") else {
continue;
};
let paragraph = item.text[start..].split("\n\n").next().unwrap();
marked += 1;
for name in paragraph.split("Self::").skip(1) {
let name: String = name
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
assert!(
ids.contains(&format!("{owner}::{name}")),
"{}: `{name}` is not in the API index",
item.id
);
}
}
assert!(marked >= 2, "only {marked} calls mark a preferred one");
}
#[test]
fn pitfall_catalog_names_only_real_codes_and_every_entry_has_a_fix() {
let page = super::find("guide/pitfalls").expect("pitfalls");
let mut named = 0;
for line in page.text.lines().filter(|l| l.starts_with("Code: ")) {
let codes = line.split('`').skip(1).step_by(2);
for code in codes.filter(|c| !c.contains(' ')) {
assert!(crate::diagnostics::lookup(code).is_some(), "{code}");
named += 1;
}
}
assert!(named >= 15, "only {named} codes");
let entries = page.text.matches("\n**").count();
assert_eq!(page.text.matches("\nCode: ").count(), entries);
assert_eq!(page.text.matches("\nFix: ").count(), entries);
}
#[test]
fn agent_guides_cover_every_scene_method_and_name_only_real_ones() {
let api = super::api_items();
let methods: Vec<&str> = api
.iter()
.filter_map(|item| item.title.split_once("::").map(|(_, m)| m))
.collect();
let skill = include_str!("../skills/rusting-game/SKILL.md");
let agents = include_str!("project_agents.md");
let guides: String = PAGES.iter().map(|(_, _, text)| *text).collect();
let named = |text: &str, name: &str| {
text.match_indices(name).any(|(at, _)| {
let word = |c: char| c.is_ascii_alphanumeric() || c == '_';
!text[..at].ends_with(word)
&& !text[at + name.len()..].starts_with(word)
})
};
let unnamed: Vec<_> = api
.iter()
.filter_map(|item| item.title.strip_prefix("GameScene::"))
.filter(|m| !named(&guides, m) && !named(agents, m))
.collect();
assert!(unnamed.is_empty(), "no guide names {unnamed:?}");
let std_calls = ["atan2"];
for text in [skill, agents] {
for code in text.split('`').skip(1).step_by(2) {
let call = code.trim_start_matches("scene.");
let Some((name, _)) = call.split_once('(') else {
continue;
};
if name.starts_with(|c: char| c.is_ascii_lowercase())
&& name.chars().all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'
})
{
assert!(
methods.contains(&name) || std_calls.contains(&name),
"`{code}` names no API method"
);
}
}
}
}
#[test]
fn agent_guides_hold_their_generated_parts() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let (skill, agents) =
("skills/rusting-game/SKILL.md", "src/project_agents.md");
for (marker, generated, file) in [
("api-table", super::api_table(), skill),
("api-table", super::api_table(), agents),
("components", super::component_list(), skill),
("pitfalls", super::pitfall_list(), skill),
] {
let start = format!("<!-- {marker}:start -->\n");
let end = format!("<!-- {marker}:end -->");
let text = std::fs::read_to_string(root.join(file)).unwrap();
let (head, rest) = text.split_once(&start).expect(&start);
let (old, tail) = rest.split_once(&end).expect(&end);
if old == generated {
continue;
}
if std::env::var_os(crate::scenario::UPDATE_GOLDEN_ENV).is_some() {
let text = format!("{head}{start}{generated}{end}{tail}");
std::fs::write(root.join(file), text).unwrap();
continue;
}
panic!(
"{file}: the generated {marker} is stale; run with {}=1 to rewrite it",
crate::scenario::UPDATE_GOLDEN_ENV
);
}
}
}