1use std::fmt::Write as _;
20use std::fs;
21use std::path::Path;
22
23use schemars::JsonSchema;
24use serde::{Deserialize, Serialize};
25
26use super::{Answer, Op, codes};
27use crate::catalog::{self, Catalog, SkillCopy};
28use crate::project::Project;
29
30pub const BESIDE: &str = "beside-its-alternatives";
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
37#[serde(deny_unknown_fields)]
38#[non_exhaustive]
41pub struct AddInput {
42 pub module: String,
45}
46
47impl AddInput {
48 pub fn new(module: impl Into<String>) -> Self {
50 Self {
51 module: module.into(),
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct AddOutput {
60 pub module: String,
62 pub feature: String,
64 pub changed: bool,
67 pub skill: Option<String>,
70 pub about: String,
72 pub alternatives_on: Vec<String>,
74 pub beside: Option<String>,
76 pub install: Option<String>,
79 pub formats: Option<String>,
81}
82
83pub fn add(project: &Project, input: &AddInput) -> Answer<AddOutput> {
85 match catalog::engine_root(&project.root).and_then(|root| Catalog::read(&root)) {
86 Ok(catalog) => add_in(&project.root, &catalog, input),
87 Err(e) => Answer::failed(Op::Add, codes::ENGINE_DEPENDENCY, e),
88 }
89}
90
91pub fn add_in(game: &Path, catalog: &Catalog, input: &AddInput) -> Answer<AddOutput> {
93 match added(game, catalog, input) {
94 Ok((output, limits)) => Answer::new(Op::Add, true, output, limits),
95 Err((code, e)) => Answer::failed(Op::Add, code, e),
96 }
97}
98
99pub(crate) fn unknown(catalog: &Catalog, name: &str) -> String {
102 let module = if name.starts_with("shuyi-") {
103 name.to_owned()
104 } else {
105 catalog::module_of(name)
106 };
107 let planned: Vec<String> = catalog
108 .concerns
109 .answered_by(&module)
110 .map(|(_, answer)| {
111 format!(
112 "{} is {} ({})",
113 answer.name,
114 answer.status.name(),
115 answer.tickets.join(", ")
116 )
117 })
118 .collect();
119 if !planned.is_empty() {
120 return format!(
121 "{module} is in the catalog but not built yet: {}",
122 planned.join("; ")
123 );
124 }
125 let known: Vec<&str> = catalog.modules.iter().map(|m| m.name.as_str()).collect();
126 if known.is_empty() {
127 format!("no module {name}: the game's engine builds no module yet")
128 } else {
129 format!(
130 "no module {name}: the game's engine builds {}",
131 known.join(", ")
132 )
133 }
134}
135
136fn added(
139 game: &Path,
140 catalog: &Catalog,
141 input: &AddInput,
142) -> Result<(AddOutput, Vec<String>), (codes::Code, String)> {
143 let module = catalog
144 .module(&input.module)
145 .ok_or_else(|| (codes::UNKNOWN_MODULE, unknown(catalog, &input.module)))?;
146 let about = catalog::readme_paragraph(&module.dir).map_err(|e| (codes::FILE, e))?;
147 let on = super::modules::on_in(game)?;
148 let manifest = game.join("Cargo.toml");
149 let text = fs::read_to_string(&manifest)
150 .map_err(|e| (codes::FILE, format!("{}: {e}", manifest.display())))?;
151 let edited =
152 catalog::turn_on(&text, module.feature()).map_err(|e| (codes::ENGINE_DEPENDENCY, e))?;
153 if let Some(edited) = &edited {
154 fs::write(&manifest, edited)
155 .map_err(|e| (codes::FILE, format!("{}: {e}", manifest.display())))?;
156 }
157
158 let mut limits =
159 vec!["only the game's root Cargo.toml was read for the modules it has on".to_owned()];
160 let skill = match catalog::skill_of(&module.dir, &module.name) {
161 None => {
162 limits.push(format!("{} has no skill to copy", module.name));
163 None
164 }
165 Some(skill) => match catalog::copy_skill(&skill, game, &module.name) {
166 Ok(SkillCopy::Copied(_)) => Some(format!("{}/{}", catalog::SKILLS, module.name)),
167 Ok(SkillCopy::AlreadyThere(_)) => {
168 limits.push(format!(
169 "the game already has a skill {} in {}: it was left as it is",
170 module.name,
171 catalog::SKILLS
172 ));
173 None
174 }
175 Ok(SkillCopy::NoSkills) => {
176 limits.push(format!(
177 "the game keeps no skills ({} isn't there): {}'s skill wasn't copied",
178 catalog::SKILLS,
179 module.name
180 ));
181 None
182 }
183 Err(e) => return Err((codes::FILE, e)),
184 },
185 };
186
187 let alternatives_on: Vec<String> = on
188 .iter()
189 .map(|feature| catalog::module_of(feature))
190 .filter(|other| module.info.alternatives.contains(other))
191 .collect();
192 let beside = (!alternatives_on.is_empty()).then(|| {
193 let readme = module.dir.join("README.md");
194 format!("{}#{BESIDE}", readme.to_string_lossy().replace('\\', "/"))
195 });
196 let install = (!catalog::installs_modules(game)).then(|| {
197 "add `.modules(shuyi::enabled_modules)` to the game's `BasicGame`, or \
198 `.with_modules(shuyi::enabled_modules())` to its `Game`: every module it turns on then \
199 joins it"
200 .to_owned()
201 });
202 let formats = module.info.formats.as_ref().map(|function| {
203 format!(
204 "call `{function}` in the game's `formats()`: its formats are read with the game's \
205 data, before its `Game` exists"
206 )
207 });
208 let output = AddOutput {
209 module: module.name.clone(),
210 feature: module.feature().to_owned(),
211 changed: edited.is_some(),
212 skill,
213 about,
214 alternatives_on,
215 beside,
216 install,
217 formats,
218 };
219 Ok((output, limits))
220}
221
222pub fn usage(command: &str) -> String {
224 format!(
225 "usage: {command} add <module> [--json]\n\n\
226 Turns on a module of the game's engine (`physics` or `shuyi-physics`): its feature\n\
227 of `shuyi` in Cargo.toml, its skill copied, what it is for. `{command} modules`\n\
228 lists them."
229 )
230}
231
232pub fn parse_args(args: &[String]) -> Result<AddInput, String> {
234 match args {
235 [module] if !module.starts_with("--") => Ok(AddInput::new(module.clone())),
236 _ => Err("one <module>, nothing else".to_owned()),
237 }
238}
239
240pub fn text(output: &AddOutput) -> String {
242 let mut out = if output.changed {
243 format!(
244 "add: {} on (the feature `{}` of shuyi in Cargo.toml)\n",
245 output.module, output.feature
246 )
247 } else {
248 format!("add: {} was already on: nothing changed\n", output.module)
249 };
250 for line in output.about.lines() {
251 let _ = writeln!(out, " {line}");
253 }
254 if let Some(skill) = &output.skill {
255 let _ = writeln!(out, "skill: copied to {skill}");
256 }
257 if !output.alternatives_on.is_empty() {
258 let _ = writeln!(
259 out,
260 "warning: the game already has {} on, an alternative: two alternatives never drive \
261 the same thing in one stage{}",
262 output.alternatives_on.join(", "),
263 output
264 .beside
265 .as_ref()
266 .map(|beside| format!("; see {beside}"))
267 .unwrap_or_default()
268 );
269 }
270 for to_do in output.install.iter().chain(&output.formats) {
271 let _ = writeln!(out, "to do: {to_do}");
272 }
273 out
274}
275
276#[cfg(test)]
277mod tests;