Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0x-unkwn0wn/simterm/llms.txt

Use this file to discover all available pages before exploring further.

Simterm ships two validation modes for campaign authors. Running them regularly — especially after structural changes — catches broken references and out-of-range values before you discover them mid-playtest.

Two validation modes

ModeWhat it checksExits non-zero on
--checkParses the RON file and confirms at least one mission is present.Parse errors, missing missions.
--doctorFull semantic analysis: dangling references, duplicate IDs, unreachable content, out-of-range values, and trigger collisions.Any error-level finding.
# Basic load check — does it parse and have missions?
cargo run -p simterm -- --check --campaign ./my_campaign

# Deeper semantic check — dangling refs, bad ranges, collisions
cargo run -p simterm -- --doctor --campaign ./my_campaign
--doctor prints errors and warnings to stdout. Errors are things that break the campaign; warnings are things that smell wrong but still load. Fix all errors before distributing a campaign.
--doctor exits with a non-zero status code when there are any errors. This makes it safe to use in CI pipelines — a failing campaign blocks the build.

--doctor errors

These conditions break the campaign and must be resolved:
ErrorCause
Duplicate mission IDTwo or more Mission entries share the same id.
Empty mission IDA Mission.id is an empty or whitespace-only string.
Campaign with no missionsThe missions list is empty.
Objective not in VFSMission.objective (or NetHost.objective) points to a path with no matching file in that host’s filesystem.
affected_service port not in servicesA Vulnerability.affected_service port does not appear in the same host’s services list.
Service.requires token never obtainableA service gated with requires: Some("token") but that token is not granted by any Loot.foothold_token, Loot.hash.yields, or Binary.yields anywhere in the campaign.
Duplicate network hostnameTwo NetHost entries in the same network list share the same hostname.
Invalid links referenceA NetHost.links entry names a hostname that does not exist in the mission’s network.
Duplicate achievement IDTwo CampaignAchievement entries share the same id.
Achievement trigger: missing missionA CompleteMission or ChooseEnding trigger references a Mission.id that does not exist.
Achievement trigger: choice out of rangeA ChooseEnding trigger has a choice value less than 1 or greater than the number of endings in the referenced mission.
Command effect: missing achievementA CommandEffect::UnlockAchievement("id") references an achievement id that is not defined in the campaign.
Command condition: missing missionA CommandCondition::Mission("id") references a Mission.id that does not exist.
Command condition: invalid phaseA CommandCondition::Phase("name") uses a phase name other than recon, enum, exploit, or post.

--doctor warnings

These conditions are suspicious but do not break the campaign. They still load and run, but they often indicate design mistakes:
WarningCause
skill out of rangeA mission’s skill value is outside 0.0..=1.0. The engine clamps it, but it likely isn’t what you intended.
root_difficulty out of rangeA mission’s root_difficulty is outside 1..=10.
detection_limit ≤ 0A detection_limit is zero or negative — the mission would be unwinnable.
time_limit is Some(0)A time limit of zero ticks causes instant defeat.
Vulnerability difficulty out of rangeA Vulnerability.difficulty is outside 1..=10.
accepts_token never obtainableA host’s accepts_token value is not produced by any loot anywhere in the campaign. login will never work on that host.
Network has no entry hostNo NetHost in a network list has entry: true. The engine falls back to the first host, which may not be what you intended.
Easter egg defines no triggerAn EasterEgg block has an empty triggers list and can never fire.
Easter egg trigger collides with built-inAn EasterEgg.triggers entry matches a built-in verb. The easter egg will be shadowed and never fire.
Campaign command defines no triggerA CampaignCommand block has an empty triggers list and can never fire.
Campaign command trigger collides with built-inA CampaignCommand.triggers entry matches a built-in verb.
Campaign command trigger collides with easter eggA CampaignCommand.triggers entry also appears in an easter egg — the command takes priority, which may not be intended.
Duplicate trigger across campaign commandsThe same trigger string appears in more than one CampaignCommand.
Achievement ReadFile path not in any VFSA ReadFile trigger path doesn’t match any file in any host’s filesystem — the achievement can never unlock.
FlagSet condition for unset flagA CommandCondition::FlagSet("flag") references a flag that no command ever activates with SetFlag. The command can never become available.
Empty env key nameA key in the env map is an empty or whitespace-only string.
Terminal command defines no triggerA TerminalCommand block has an empty triggers list and can never fire.
Terminal command trigger collides with built-inA TerminalCommand.triggers entry matches a built-in or system command.
Terminal command trigger collides with declarative commandA TerminalCommand.triggers entry also appears in a CampaignCommand — the declarative command takes priority.
Duplicate trigger across terminal commandsThe same trigger string appears in more than one TerminalCommand.
TerminalCommand.exit out of rangeAn exit code is outside 0..=255.
{env:NAME} references undefined variableA TerminalCommand output template references an env variable that is neither in campaign.env nor automatically derived (USER, LOGNAME, HOME, PWD, SHELL, HOSTNAME).

Troubleshooting

SymptomLikely cause
RON parse error at startupMissing comma, mismatched parenthesis, or malformed Some(...). Check the line number in the error message.
--check fails with “no missions”The missions field is missing or the list is empty.
Mission never completesThe objective path doesn’t exist in the VFS, or no deterministic root route exists (no privesc_key loot and no local_privesc).
login always fails on a hostThe accepts_token value on that host is never produced by any foothold_token, hash yields, or binary yields in the campaign.
Command or easter egg never firesThe trigger collides with a built-in verb, or a conditions guard references a flag that is never set.
Achievement never unlocksThe ReadFile path doesn’t match any VFS file, the CompleteMission mission ID doesn’t exist, or the ChooseEnding choice index is out of range.
Player stuck — can’t escalateThe privesc_key file has root: true, or there is no local_privesc defined and no privesc_key loot at all.
Network pivot never worksA NetHost.links entry doesn’t match any other host’s short name or FQDN in the same mission network.

The validate_campaign API

--doctor’s logic is exposed as a public Rust function in simterm_engine:
use simterm_engine::validate_campaign;

let report = validate_campaign(&campaign, &reserved_verbs);

if report.has_errors() {
    for issue in &report.errors {
        eprintln!("[ERROR] {}: {}", issue.location, issue.message);
    }
}
for issue in &report.warnings {
    eprintln!("[WARN]  {}: {}", issue.location, issue.message);
}
The second argument, reserved_verbs, is a slice of built-in verb strings from your frontend. Passing them lets the engine detect trigger collisions without the engine needing to know the specific command set. Pass &[] to skip collision checking. This API lets custom frontends, campaign editors, and CI tools reuse the same validation logic as the --doctor flag without spawning a subprocess.

Build docs developers (and LLMs) love