1use 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
29pub const WRONG_ARGUMENTS: Code = Code("SH0001");
31pub const NO_SESSION: Code = Code("SH0002");
33pub const UNKNOWN_MODULE: Code = Code("SH0003");
36pub const LINK_TOKEN: Code = Code("SH0004");
39pub const NO_VALUE: Code = Code("SH0005");
42pub const NO_PROJECT: Code = Code("SH1001");
44pub const PROJECT_FILE: Code = Code("SH1002");
47pub const TOOLCHAIN: Code = Code("SH1003");
49pub const ENGINE_DEPENDENCY: Code = Code("SH1004");
52pub const NO_BUILD: Code = Code("SH1005");
54pub const LINK_VERSION: Code = Code("SH1006");
57pub const HARD_CODED_KEY: Code = Code("SH2101");
60pub const SCREEN_TEXT: Code = Code("SH2102");
63pub const RULES_FENCE: Code = Code("SH2103");
66pub const RULES_DEPENDENCY: Code = Code("SH2104");
69pub const NOT_OFFERED: Code = Code("SH3001");
71pub const GAME_ERROR: Code = Code("SH3002");
73pub const TARGET: Code = Code("SH3003");
75pub const FILE: Code = Code("SH3004");
77pub const GAME_PROGRAM: Code = Code("SH3005");
80pub const LINK_LOST: Code = Code("SH3006");
83pub const OTHER_STAMP: Code = Code("SH3007");
86pub const TARGET_CHECK: Code = Code("SH3008");
89pub const BUILD_PATH: Code = Code("SH3009");
92pub const NO_EXPORTER: Code = Code("SH3010");
95pub const PACKAGE: Code = Code("SH3011");
97pub const DISCLOSURE: Code = Code("SH3012");
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
103#[serde(rename_all = "kebab-case")]
104pub enum Class {
105 Usage,
107 Setup,
110 Content,
112 Operation,
114}
115
116impl Class {
117 pub const ALL: [Class; 4] = [Class::Usage, Class::Setup, Class::Content, Class::Operation];
119
120 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158#[non_exhaustive]
159pub struct Entry {
160 pub code: Code,
162 pub name: &'static str,
164 pub class: Class,
166 pub title: &'static str,
168 pub help: &'static str,
170 pub page: &'static str,
172 pub retired: bool,
174}
175
176macro_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
191pub 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
509pub fn entry(code: &str) -> Option<&'static Entry> {
511 TABLE.iter().find(|entry| entry.code.0 == code)
512}
513
514pub fn help_of(code: &str) -> Option<&'static str> {
516 entry(code).map(|entry| entry.help)
517}
518
519pub fn exit_code(code: Option<&str>) -> u8 {
521 code.and_then(Class::of).map_or(1, Class::exit_code)
522}
523
524pub 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 let _ = writeln!(out, " see: shuyi explain {code}");
544 out
545}
546
547pub 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 let _ = writeln!(out, "help[{code}]: {help}");
559 }
560 }
561 out
562}
563
564#[cfg(test)]
565mod tests;