Skip to main content

shuyi_cli/contract/
codes.rs

1//! Problem codes (ADR-0028): every error the contract gives carries a
2//! stable code, `SH` and four digits, whose first digit is its class, and
3//! the class sets the exit code. Each code has a row here, with a help
4//! line, and a page in `crates/cli/errors/` that `shuyi explain <code>`
5//! prints.
6//!
7//! | Class | Codes | Exit code | Means |
8//! | ----- | ----- | --------- | ----- |
9//! | usage | `SH0nnn` | 2 | the arguments or the input are wrong: change the command |
10//! | setup | `SH1nnn` | 3 | the project file or the toolchain needs fixing: fix it, then run the same command again |
11//! | content | `SH2nnn` | 1 | a file of the game's content has a problem |
12//! | operation | `SH3nnn` | 1 | the operation failed on something else: the game's tools, a target, a file it reads or writes |
13//!
14//! A code is never reused or renumbered: a retired one keeps its row and
15//! its page. A new error gets a new code, its row and its page in the same
16//! change. The content codes of the game's files are
17//! `shuyi_assets::error::codes`; those of its code, `SH21nn` (`lint`), and
18//! the tools' own are here.
19
20use std::fmt::Write as _;
21
22use schemars::JsonSchema;
23use serde::{Deserialize, Serialize};
24pub use shuyi_assets::error::Code;
25use shuyi_assets::error::codes as content;
26
27use super::Problem;
28
29/// `wrong-arguments`: the arguments or the input are wrong.
30pub const WRONG_ARGUMENTS: Code = Code("SH0001");
31/// `no-session`: an MCP session tool was called with no game started.
32pub const NO_SESSION: Code = Code("SH0002");
33/// `unknown-module`: `add` was given a name that is no module the game's
34/// engine builds.
35pub const UNKNOWN_MODULE: Code = Code("SH0003");
36/// `link-token`: `attach` gave a token the build doesn't have, and it
37/// closed the link.
38pub const LINK_TOKEN: Code = Code("SH0004");
39/// `no-value`: `scene --path` was given a JSON Pointer that names nothing
40/// in the scene's data.
41pub const NO_VALUE: Code = Code("SH0005");
42/// `no-project`: no `shuyi.ron` here or above.
43pub const NO_PROJECT: Code = Code("SH1001");
44/// `project-file`: `shuyi.ron` doesn't read, or lacks what the command
45/// needs.
46pub const PROJECT_FILE: Code = Code("SH1002");
47/// `toolchain`: a piece of the toolchain is missing or doesn't start.
48pub const TOOLCHAIN: Code = Code("SH1003");
49/// `engine-dependency`: the game's `Cargo.toml` has no `shuyi`
50/// dependency, or the engine checkout it names can't be read.
51pub const ENGINE_DEPENDENCY: Code = Code("SH1004");
52/// `no-build`: `attach` found no build listening at the address.
53pub const NO_BUILD: Code = Code("SH1005");
54/// `link-version`: the build speaks another link version than this
55/// `shuyi`.
56pub const LINK_VERSION: Code = Code("SH1006");
57/// `hard-coded-key`: the game's code or text names a key (`lint`'s
58/// `check-keys`).
59pub const HARD_CODED_KEY: Code = Code("SH2101");
60/// `screen-text`: a screen's text is written in its code, or a screen
61/// reads a data name itself (`lint`'s `check-text`).
62pub const SCREEN_TEXT: Code = Code("SH2102");
63/// `rules-fence`: the rules crate uses what its fence bans: a hash map, a
64/// float, a clock, file I/O, outside luck (`lint`).
65pub const RULES_FENCE: Code = Code("SH2103");
66/// `rules-dependency`: the rules crate depends on a screen, device or
67/// tool crate, or on outside luck (`lint`).
68pub const RULES_DEPENDENCY: Code = Code("SH2104");
69/// `not-offered`: the game's tools don't offer the operation.
70pub const NOT_OFFERED: Code = Code("SH3001");
71/// `game-error`: the game's tools failed.
72pub const GAME_ERROR: Code = Code("SH3002");
73/// `target`: no such target, or the target profiles don't load.
74pub const TARGET: Code = Code("SH3003");
75/// `file`: a file the operation reads or writes can't be.
76pub const FILE: Code = Code("SH3004");
77/// `game-program`: the game's tool program didn't build, or stopped
78/// before it answered.
79pub const GAME_PROGRAM: Code = Code("SH3005");
80/// `link-lost`: the build's link is held by another client, closed, or
81/// answered what `attach` can't read.
82pub const LINK_LOST: Code = Code("SH3006");
83/// `other-stamp`: `replay` was given a record made by another engine,
84/// another build of the game or other content.
85pub const OTHER_STAMP: Code = Code("SH3007");
86/// `target-check`: `export` ran the target's check, which found errors,
87/// and built nothing.
88pub const TARGET_CHECK: Code = Code("SH3008");
89/// `build-path`: `export` (or `web --release`) found a path of the build
90/// machine in the built program, and refused the package.
91pub const BUILD_PATH: Code = Code("SH3009");
92/// `no-exporter`: the target passes its check, but has no exporter, or no
93/// run step, yet.
94pub const NO_EXPORTER: Code = Code("SH3010");
95/// `package`: `export`'s build, or writing its package, failed.
96pub const PACKAGE: Code = Code("SH3011");
97/// `disclosure`: `export` found what the game's `disclosure` part doesn't
98/// allow in the final package, and removed it.
99pub const DISCLOSURE: Code = Code("SH3012");
100
101/// A code's class: what kind of fix it asks for.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
103#[serde(rename_all = "kebab-case")]
104pub enum Class {
105    /// The arguments or the input are wrong: change the command.
106    Usage,
107    /// The project file or the toolchain needs fixing: fix it, then run
108    /// the same command again.
109    Setup,
110    /// A file of the game's content has a problem.
111    Content,
112    /// The operation failed on something else.
113    Operation,
114}
115
116impl Class {
117    /// Every class, by its digit.
118    pub const ALL: [Class; 4] = [Class::Usage, Class::Setup, Class::Content, Class::Operation];
119
120    /// The process exit code of an error of this class.
121    pub fn exit_code(self) -> u8 {
122        match self {
123            Class::Usage => 2,
124            Class::Setup => 3,
125            Class::Content | Class::Operation => 1,
126        }
127    }
128
129    /// The first digit of its codes.
130    pub fn digit(self) -> char {
131        match self {
132            Class::Usage => '0',
133            Class::Setup => '1',
134            Class::Content => '2',
135            Class::Operation => '3',
136        }
137    }
138
139    /// Its name (`usage`).
140    pub fn name(self) -> &'static str {
141        match self {
142            Class::Usage => "usage",
143            Class::Setup => "setup",
144            Class::Content => "content",
145            Class::Operation => "operation",
146        }
147    }
148
149    /// The class of `code`, by its first digit.
150    pub fn of(code: &str) -> Option<Class> {
151        let digit = code.strip_prefix("SH")?.chars().next()?;
152        Class::ALL.into_iter().find(|class| class.digit() == digit)
153    }
154}
155
156/// A code's row, read field by field (ADR-0039).
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158#[non_exhaustive]
159pub struct Entry {
160    /// The code.
161    pub code: Code,
162    /// Its kebab-case name, as stable as the code (`missing-file`).
163    pub name: &'static str,
164    /// Its class.
165    pub class: Class,
166    /// What went wrong, in a few words.
167    pub title: &'static str,
168    /// How to fix it, in a line.
169    pub help: &'static str,
170    /// Its page, `crates/cli/errors/<code>.md`.
171    pub page: &'static str,
172    /// Whether it is retired: no error gives it any more.
173    pub retired: bool,
174}
175
176/// The row of `code`, named `name`, in `class`.
177macro_rules! row {
178    ($code:expr, $name:literal, $class:ident, $file:literal, $title:literal, $help:literal) => {
179        Entry {
180            code: $code,
181            name: $name,
182            class: Class::$class,
183            title: $title,
184            help: $help,
185            page: include_str!(concat!("../../errors/", $file, ".md")),
186            retired: false,
187        }
188    };
189}
190
191/// Every code, in order.
192pub static TABLE: &[Entry] = &[
193    row!(
194        WRONG_ARGUMENTS,
195        "wrong-arguments",
196        Usage,
197        "SH0001",
198        "The command's arguments or input are wrong",
199        "read the usage, change the command, and run it again"
200    ),
201    row!(
202        NO_SESSION,
203        "no-session",
204        Usage,
205        "SH0002",
206        "No game is running in the MCP session",
207        "call `start` first (with the game's folder as `project`), then the session's other tools"
208    ),
209    row!(
210        UNKNOWN_MODULE,
211        "unknown-module",
212        Usage,
213        "SH0003",
214        "No module of that name is built in the game's engine",
215        "pass one of the names the message lists (`shuyi modules` lists them); a module the \
216         catalog plans isn't built yet"
217    ),
218    row!(
219        LINK_TOKEN,
220        "link-token",
221        Usage,
222        "SH0004",
223        "The build closed the link at its hello: the token is wrong",
224        "pass the token of the build's log line `shuyi link: listening on <address>, token \
225         <token>` as --token (it changes each start, unless SHUYI_LINK_TOKEN sets it)"
226    ),
227    row!(
228        NO_VALUE,
229        "no-value",
230        Usage,
231        "SH0005",
232        "The JSON Pointer names no value in the scene's data",
233        "pass a pointer to a key the message lists (`/hero/x`); `scene --json` shows the whole \
234         data, and `scene --shape` its shape"
235    ),
236    row!(
237        NO_PROJECT,
238        "no-project",
239        Setup,
240        "SH1001",
241        "No shuyi.ron here or in a folder above",
242        "run the command in a game's folder (one holding shuyi.ron, or below it), or make a game \
243         with `shuyi new`"
244    ),
245    row!(
246        PROJECT_FILE,
247        "project-file",
248        Setup,
249        "SH1002",
250        "shuyi.ron doesn't read, or lacks what the command needs",
251        "fix shuyi.ron as the message says, then run the same command again"
252    ),
253    row!(
254        TOOLCHAIN,
255        "toolchain",
256        Setup,
257        "SH1003",
258        "A piece of the toolchain is missing",
259        "run `shuyi doctor` and fix each failed check as its help says, then run the same \
260         command again"
261    ),
262    row!(
263        ENGINE_DEPENDENCY,
264        "engine-dependency",
265        Setup,
266        "SH1004",
267        "The game doesn't depend on the engine through the shuyi crate",
268        "depend on the engine's `shuyi` crate in the game's Cargo.toml, as `shuyi new` writes \
269         it, and check that the engine it names is there; then run the same command again"
270    ),
271    row!(
272        NO_BUILD,
273        "no-build",
274        Setup,
275        "SH1005",
276        "No build is listening at the address",
277        "start the game's debug build with its `link` feature and SHUYI_LINK set (`SHUYI_LINK=0 \
278         cargo run --features link`), then attach to the address its log line gives"
279    ),
280    row!(
281        LINK_VERSION,
282        "link-version",
283        Setup,
284        "SH1006",
285        "The build speaks another link version than this shuyi",
286        "attach with the shuyi of the engine the build was made with (the game's own `cargo \
287         shuyi`), or build the game again on this engine, then attach again"
288    ),
289    row!(
290        content::PARSE,
291        "parse",
292        Content,
293        "SH2001",
294        "A text file's syntax is wrong",
295        "fix the file at the line and column given: the message says what was expected there"
296    ),
297    row!(
298        content::NOT_TEXT,
299        "not-text",
300        Content,
301        "SH2002",
302        "A text file isn't valid UTF-8",
303        "save the file as UTF-8 text"
304    ),
305    row!(
306        content::MISSING_FILE,
307        "missing-file",
308        Content,
309        "SH2003",
310        "A file isn't there",
311        "add the file at that path, or fix the name that asks for it"
312    ),
313    row!(
314        content::AMBIGUOUS_FILE,
315        "ambiguous-file",
316        Content,
317        "SH2004",
318        "Several files could be the asset",
319        "name the file meant with its extension, or remove the others"
320    ),
321    row!(
322        content::NO_FORMAT,
323        "no-format",
324        Content,
325        "SH2005",
326        "No format reads the file",
327        "give the file an extension a format of its kind claims, or register a format that \
328         reads it"
329    ),
330    row!(
331        content::TOO_BIG,
332        "too-big",
333        Content,
334        "SH2006",
335        "A file is bigger than its format reads",
336        "make the file smaller, or split it"
337    ),
338    row!(
339        content::PACK_DIFFERS,
340        "pack-differs",
341        Content,
342        "SH2007",
343        "The pack differs from its sources",
344        "bake the pack again with `shuyi bake`; if it still differs, the kind's packed format \
345         loses something"
346    ),
347    row!(
348        content::UNKNOWN_NAME,
349        "unknown-name",
350        Content,
351        "SH2008",
352        "A name in the file isn't one it may use",
353        "use one of the names the message lists, or fix the spelling"
354    ),
355    row!(
356        content::DUPLICATE,
357        "duplicate",
358        Content,
359        "SH2009",
360        "Something given once is given twice",
361        "keep one of the two: remove or rename the other"
362    ),
363    row!(
364        content::OUT_OF_RANGE,
365        "out-of-range",
366        Content,
367        "SH2010",
368        "A number or a size is out of range",
369        "bring it within the range the message gives"
370    ),
371    row!(
372        HARD_CODED_KEY,
373        "hard-coded-key",
374        Content,
375        "SH2101",
376        "The game's code or text names a key",
377        "react to an action instead, and name a key in text through the player's keymap"
378    ),
379    row!(
380        SCREEN_TEXT,
381        "screen-text",
382        Content,
383        "SH2102",
384        "A screen's text is written in its code",
385        "put the text in the language file and ask for it by key; show a data name through the game's words helpers"
386    ),
387    row!(
388        RULES_FENCE,
389        "rules-fence",
390        Content,
391        "SH2103",
392        "The rules use what keeps them from being pure and deterministic",
393        "use what the message's reason names: BTreeMap for a hash map, whole numbers for a float, turns or a Command for the time, crates/game for files, the state's SimRng for luck"
394    ),
395    row!(
396        RULES_DEPENDENCY,
397        "rules-dependency",
398        Content,
399        "SH2104",
400        "The rules depend on a screen, device or tool crate",
401        "remove the dependency from the rules (the command shows the way in) and move the code that needs it to crates/game"
402    ),
403    row!(
404        NOT_OFFERED,
405        "not-offered",
406        Operation,
407        "SH3001",
408        "The game's tools don't offer the operation",
409        "write the GameTools method the message names in the game's tool program"
410    ),
411    row!(
412        GAME_ERROR,
413        "game-error",
414        Operation,
415        "SH3002",
416        "The game's tools failed",
417        "fix what the message says in the game's content or tools, then run the command again"
418    ),
419    row!(
420        TARGET,
421        "target",
422        Operation,
423        "SH3003",
424        "No such target, or the target profiles don't load",
425        "name a target the message lists, or fix the target profiles' files"
426    ),
427    row!(
428        FILE,
429        "file",
430        Operation,
431        "SH3004",
432        "A file the operation reads or writes can't be",
433        "check the path and the folder's permissions: the message gives the system's reason"
434    ),
435    row!(
436        GAME_PROGRAM,
437        "game-program",
438        Operation,
439        "SH3005",
440        "The game's tool program didn't build, or stopped before it answered",
441        "fix what its output in the message says (a compile error, a panic) in the game's tool \
442         program, or in the project's own MCP server when the message names that, then call the \
443         tool again; the session, if one ran, has ended: `start` it again"
444    ),
445    row!(
446        LINK_LOST,
447        "link-lost",
448        Operation,
449        "SH3006",
450        "The build's link is busy, closed, or answered what shuyi can't read",
451        "detach the other client attached to the build, or start the build again, then attach \
452         again"
453    ),
454    row!(
455        OTHER_STAMP,
456        "other-stamp",
457        Operation,
458        "SH3007",
459        "The record was made by another build or with other content",
460        "make the record again with this build, or replay it with --any-stamp to run it anyway"
461    ),
462    row!(
463        TARGET_CHECK,
464        "target-check",
465        Operation,
466        "SH3008",
467        "The target's check found errors, so nothing was exported",
468        "fix each error the message lists (`check --target <profile>` shows them too), then \
469         export again"
470    ),
471    row!(
472        BUILD_PATH,
473        "build-path",
474        Operation,
475        "SH3009",
476        "The built program holds a path of the build machine",
477        "find the code that writes the path into the program as a value (env!(\"CARGO_MANIFEST_DIR\"), \
478         OUT_DIR) and keep it out of what ships, and keep CARGO_HOME and the target folder where \
479         cargo puts them"
480    ),
481    row!(
482        NO_EXPORTER,
483        "no-exporter",
484        Operation,
485        "SH3010",
486        "The target has no exporter, or no run step, yet",
487        "export for a target the message lists, or run the package by hand"
488    ),
489    row!(
490        PACKAGE,
491        "package",
492        Operation,
493        "SH3011",
494        "The export's build, or writing its package, failed",
495        "fix what the message says (a compile error, a tool that failed, a folder that can't be \
496         written), then export again"
497    ),
498    row!(
499        DISCLOSURE,
500        "disclosure",
501        Operation,
502        "SH3012",
503        "The final package holds what the game's disclosure part doesn't allow",
504        "for each file the message names, take it out of what is built, or add it to the part if \
505         it is meant to ship, then export again"
506    ),
507];
508
509/// The row of `code`.
510pub fn entry(code: &str) -> Option<&'static Entry> {
511    TABLE.iter().find(|entry| entry.code.0 == code)
512}
513
514/// The help line of `code`'s row.
515pub fn help_of(code: &str) -> Option<&'static str> {
516    entry(code).map(|entry| entry.help)
517}
518
519/// The exit code of an error with `code`: its class's, 1 without one.
520pub fn exit_code(code: Option<&str>) -> u8 {
521    code.and_then(Class::of).map_or(1, Class::exit_code)
522}
523
524/// An error of `name` (an op or a command) for people: `name:
525/// error[CODE]: message`, then its `help` (multi-line help indented under
526/// it) and the `shuyi explain` that prints its page.
527pub fn said(name: &str, code: &str, message: &str, help: Option<&str>) -> String {
528    let mut out = format!("{name}: error[{code}]: {message}\n");
529    if let Some(help) = help {
530        let mut lines = help.trim_end().lines();
531        out.push_str("  help: ");
532        out.push_str(lines.next().unwrap_or_default());
533        for line in lines {
534            out.push('\n');
535            if !line.is_empty() {
536                out.push_str("        ");
537                out.push_str(line);
538            }
539        }
540        out.push('\n');
541    }
542    // Writing to a `String` can't fail.
543    let _ = writeln!(out, "  see: shuyi explain {code}");
544    out
545}
546
547/// After a list of `problems`: one `help[CODE]: …` line per code they
548/// used, in the order first used.
549pub fn help_lines(problems: &[Problem]) -> String {
550    let mut seen: Vec<&str> = Vec::new();
551    let mut out = String::new();
552    for problem in problems {
553        if let (Some(code), Some(help)) = (&problem.code, &problem.help)
554            && !seen.contains(&code.as_str())
555        {
556            seen.push(code);
557            // Writing to a `String` can't fail.
558            let _ = writeln!(out, "help[{code}]: {help}");
559        }
560    }
561    out
562}
563
564#[cfg(test)]
565mod tests;