1use crate::alloc_prelude::*;
11use alloc::{format, vec};
12use core::fmt::{self, Debug};
13
14use shuyi_assets::ContentError;
15use shuyi_assets::bundle::{Bundle, Layer};
16use shuyi_assets::keymap::ActionSet;
17use shuyi_core::Made;
18use shuyi_ui::basic::{BasicData, Own};
19use shuyi_ui::frame::ConsoleSpec;
20use shuyi_ui::module::{Module, ModuleError};
21use shuyi_ui::{Ctx, Game, GameData, GameHooks, Screen};
22
23use crate::app::{Card, CrashRecord, Start};
24use crate::app_info::AppInfo;
25use crate::platform::Look;
26
27pub type First<A, T> = fn(&mut Ctx<BasicData<A, T>>) -> Box<dyn Screen<BasicData<A, T>>>;
29
30pub type Screens<A, T> = fn(&mut Ctx<BasicData<A, T>>) -> Vec<Box<dyn Screen<BasicData<A, T>>>>;
33
34pub type Modules<A, T> = fn() -> Vec<Box<dyn Module<BasicData<A, T>>>>;
37
38pub const BASIC_CONSOLE: ConsoleSpec = ConsoleSpec::text(48, 24);
40
41pub struct BasicGame<A: ActionSet, T: Debug + 'static = ()> {
44 app: AppInfo,
45 files: Layer,
46 over: Option<Layer>,
48 own: Own<T>,
49 first: First<A, T>,
50 console: ConsoleSpec,
51 screens: Option<Screens<A, T>>,
52 modules: Option<Modules<A, T>>,
53 crash_record: Option<CrashRecord<BasicData<A, T>>>,
54}
55
56fn look<A: ActionSet, T: Debug + 'static>(data: &BasicData<A, T>) -> Look<'_> {
58 Look::new(&data.font, &data.images)
59}
60
61impl<A: ActionSet> BasicGame<A> {
62 pub fn new(app: AppInfo, files: Layer, first: First<A, ()>) -> Self {
65 Self::with_own(app, files, |_| Ok(()), first)
67 }
68}
69
70impl<A: ActionSet, T: Debug + 'static> BasicGame<A, T> {
71 pub fn with_own(app: AppInfo, files: Layer, own: Own<T>, first: First<A, T>) -> Self {
74 Self {
75 app,
76 files,
77 over: None,
78 own,
79 first,
80 console: BASIC_CONSOLE,
81 screens: None,
82 modules: None,
83 crash_record: None,
84 }
85 }
86
87 #[must_use]
89 pub fn console(mut self, console: ConsoleSpec) -> Self {
90 self.console = console;
91 self
92 }
93
94 #[must_use]
97 pub fn screens(mut self, screens: Screens<A, T>) -> Self {
98 self.screens = Some(screens);
99 self
100 }
101
102 #[must_use]
105 pub fn modules(mut self, modules: Modules<A, T>) -> Self {
106 self.modules = Some(modules);
107 self
108 }
109
110 #[must_use]
113 pub fn crash_record(mut self, hook: CrashRecord<BasicData<A, T>>) -> Self {
114 self.crash_record = Some(hook);
115 self
116 }
117
118 pub fn app(&self) -> AppInfo {
120 self.app
121 }
122
123 pub fn name(&self) -> &'static str {
125 self.app.name
126 }
127
128 pub fn storage_name(&self) -> &'static str {
130 self.app.storage
131 }
132
133 pub fn console_spec(&self) -> ConsoleSpec {
135 self.console
136 }
137
138 #[must_use]
141 pub fn over(mut self, layer: Layer) -> Self {
142 self.over = Some(layer);
143 self
144 }
145
146 pub fn bundle(&self) -> Bundle {
149 Bundle::new(self.over.into_iter().chain([self.files]).collect())
150 }
151
152 pub fn data(&self) -> Result<BasicData<A, T>, Vec<ContentError>> {
154 BasicData::load(self.bundle(), self.console, self.own)
155 }
156
157 pub fn ctx(&self) -> Result<Ctx<BasicData<A, T>>, String> {
160 let data = self.data().map_err(|errors| {
161 let lines: Vec<String> = errors.iter().map(ToString::to_string).collect();
162 lines.join("\n")
163 })?;
164 let mut ctx = Ctx::new(data)
165 .map_err(|missing| format!("the palette lacks colours: {}", missing.join(", ")))?;
166 if let Some(layout) = ctx.game.keymap_def().first_layout() {
167 ctx.use_layout(layout);
168 }
169 Ok(ctx)
170 }
171
172 pub fn game(
175 &self,
176 mut ctx: Ctx<BasicData<A, T>>,
177 ) -> Result<Game<BasicData<A, T>>, ModuleError> {
178 let first = (self.first)(&mut ctx);
179 let game = Game::new(ctx, first, GameHooks::default());
180 match self.modules {
181 Some(modules) => game.with_modules(modules()),
182 None => Ok(game),
183 }
184 }
185
186 pub fn every_screen(
189 &self,
190 ctx: &mut Ctx<BasicData<A, T>>,
191 ) -> Vec<Box<dyn Screen<BasicData<A, T>>>> {
192 match self.screens {
193 Some(screens) => screens(ctx),
194 None => vec![(self.first)(ctx)],
195 }
196 }
197
198 pub fn made(&self) -> Made {
201 let build = format!("{} {}", self.app.id, self.app.version);
202 Made::new(build, self.bundle().fingerprint())
203 }
204
205 pub fn start(self) -> Start<BasicData<A, T>> {
213 let made = self.made();
214 let crash_record = self.crash_record;
215 let start = Start::new(Card::text(self.app.name), look::<A, T>, move |setup| {
216 let ctx = self.ctx()?;
217 self.game(setup.ctx(ctx)).map_err(|e| e.to_string())
218 })
219 .made(made)
220 .state_hash(Game::state_hash);
221 match crash_record {
222 Some(hook) => start.crash_record(hook),
223 None => start,
224 }
225 }
226
227 #[cfg(any(test, feature = "harness"))]
234 pub fn harness(&self) -> crate::harness::Harness<BasicData<A, T>> {
235 let ctx = self
236 .ctx()
237 .unwrap_or_else(|errors| panic!("the game's files:\n{errors}"));
238 let game = self
239 .game(ctx)
240 .unwrap_or_else(|e| panic!("the game's modules: {e}"));
241 crate::harness::Harness::new(game)
242 }
243}
244
245impl<A: ActionSet, T: Debug + 'static> Clone for BasicGame<A, T> {
248 fn clone(&self) -> Self {
249 *self
250 }
251}
252
253impl<A: ActionSet, T: Debug + 'static> Copy for BasicGame<A, T> {}
254
255impl<A: ActionSet, T: Debug + 'static> Debug for BasicGame<A, T> {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 f.debug_struct("BasicGame")
258 .field("app", &self.app)
259 .field("files", &self.files.name)
260 .field("console", &self.console)
261 .finish_non_exhaustive()
262 }
263}
264
265#[cfg(test)]
266mod tests;