1use crate::prelude::*;
31use alloc::collections::BTreeMap;
32
33use shuyi_assets::keymap::{ActionSet, ButtonChord, KeymapDef, PadKeys};
34use shuyi_assets::target::TargetProfile;
35
36use crate::ctx::GameData;
37use crate::screen::Screen;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ScreenActions<A> {
42 pub screen: &'static str,
44 pub actions: Option<Vec<A>>,
47}
48
49impl<A: ActionSet> ScreenActions<A> {
50 pub fn of<G: GameData<Action = A>>(screen: &dyn Screen<G>) -> Self {
52 Self {
53 screen: screen.name(),
54 actions: screen.actions(),
55 }
56 }
57
58 pub fn declared(screen: &'static str, actions: Vec<A>) -> Self {
60 Self {
61 screen,
62 actions: Some(actions),
63 }
64 }
65
66 fn required(&self) -> impl Iterator<Item = A> + '_ {
68 A::ALL.iter().copied().filter(|a| {
69 a.is_required()
70 && self
71 .actions
72 .as_ref()
73 .is_none_or(|declared| declared.contains(a))
74 })
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
80pub enum FitRule {
81 NoLayout,
83 NotOnTarget,
86 MenuSlot,
88 Unreachable,
90 Shared,
92 Undeclared,
95}
96
97impl FitRule {
98 pub fn is_error(self) -> bool {
100 self != Self::Undeclared
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct FitProblem {
107 pub rule: FitRule,
109 pub screen: Option<&'static str>,
111 pub actions: Vec<&'static str>,
113 pub inputs: Vec<String>,
116 pub message: String,
118}
119
120impl FitProblem {
121 pub fn is_error(&self) -> bool {
123 self.rule.is_error()
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129enum Offer {
130 Input,
132 Menu,
134 Lacking,
136 PastMenu,
138}
139
140fn offer(profile: &TargetProfile, binding: ButtonChord) -> Offer {
142 let input = &profile.input;
143 if binding.buttons().is_some() {
144 let listed = input
145 .chords
146 .iter()
147 .any(|&[a, b]| ButtonChord::pair(a, b) == Some(binding));
148 return if listed { Offer::Input } else { Offer::Lacking };
149 }
150 let button = binding.first();
151 match button.system_menu_slot() {
152 Some(slot) if slot < input.system_menu_slots => Offer::Menu,
153 Some(_) => Offer::PastMenu,
154 None if input.buttons.contains(&button) => Offer::Input,
155 None => Offer::Lacking,
156 }
157}
158
159fn quoted_list(names: &[&str]) -> String {
161 let quoted: Vec<String> = names.iter().map(|n| format!("\"{n}\"")).collect();
162 match quoted.split_last() {
163 Some((last, rest)) if !rest.is_empty() => format!("{} and {last}", rest.join(", ")),
164 Some((last, _)) => last.clone(),
165 None => String::new(),
166 }
167}
168
169pub fn controls_fit<A: ActionSet>(
174 profile: &TargetProfile,
175 layout: &PadKeys<A>,
176 screens: &[ScreenActions<A>],
177) -> Vec<FitProblem> {
178 let target = profile.id.as_str();
179 let mut out = Vec::new();
180 for (&action, bindings) in layout {
181 for &binding in bindings {
182 let name = action.name();
183 let (rule, message) = match offer(profile, binding) {
184 Offer::Input | Offer::Menu => continue,
185 Offer::Lacking if binding.buttons().is_some() => (
186 FitRule::NotOnTarget,
187 format!(
188 "action \"{name}\" is bound to the chord {binding}, which {target} \
189 doesn't list"
190 ),
191 ),
192 Offer::Lacking => (
193 FitRule::NotOnTarget,
194 format!("action \"{name}\" is bound to {binding}, which {target} doesn't have"),
195 ),
196 Offer::PastMenu => (
197 FitRule::MenuSlot,
198 format!(
199 "action \"{name}\" is bound to {binding}, but {target}'s system menu \
200 holds {} item(s) of the game's",
201 profile.input.system_menu_slots
202 ),
203 ),
204 };
205 out.push(FitProblem {
206 rule,
207 screen: None,
208 actions: vec![name],
209 inputs: vec![binding.to_string()],
210 message,
211 });
212 }
213 }
214 for screen in screens {
215 let name = screen.screen;
216 let mut on: BTreeMap<ButtonChord, Vec<&'static str>> = BTreeMap::new();
217 for action in screen.required() {
218 let bindings = layout.get(&action).map_or(&[][..], Vec::as_slice);
219 let usable: Vec<ButtonChord> = bindings
220 .iter()
221 .copied()
222 .filter(|&b| offer(profile, b) == Offer::Input)
223 .collect();
224 if usable.is_empty() {
225 let why = if bindings.is_empty() {
226 "it has no button".to_owned()
227 } else if bindings.iter().all(|&b| offer(profile, b) == Offer::Menu) {
228 "a system menu item holds optional actions only".to_owned()
229 } else {
230 let shown: Vec<String> = bindings.iter().map(ToString::to_string).collect();
231 format!("it is bound to {}", shown.join(", "))
232 };
233 out.push(FitProblem {
234 rule: FitRule::Unreachable,
235 screen: Some(name),
236 actions: vec![action.name()],
237 inputs: bindings.iter().map(ToString::to_string).collect(),
238 message: format!(
239 "screen \"{name}\": required action \"{}\" has no input {target} offers \
240 ({why})",
241 action.name()
242 ),
243 });
244 }
245 for binding in usable {
246 on.entry(binding).or_default().push(action.name());
247 }
248 }
249 for (binding, actions) in on {
250 if actions.len() > 1 {
251 out.push(FitProblem {
252 rule: FitRule::Shared,
253 screen: Some(name),
254 message: format!(
255 "screen \"{name}\": required actions {} share {binding} on {target}",
256 quoted_list(&actions)
257 ),
258 actions,
259 inputs: vec![binding.to_string()],
260 });
261 }
262 }
263 }
264 out
265}
266
267pub trait Controls {
270 fn fit(&self, profile: &TargetProfile) -> Vec<FitProblem>;
273}
274
275#[derive(Debug, Clone)]
277pub struct GameControls<A: ActionSet> {
278 keymap: KeymapDef<A>,
279 screens: Vec<ScreenActions<A>>,
280}
281
282impl<A: ActionSet> GameControls<A> {
283 pub fn new(keymap: KeymapDef<A>) -> Self {
285 Self {
286 keymap,
287 screens: Vec::new(),
288 }
289 }
290
291 #[must_use]
294 pub fn with(mut self, screen: ScreenActions<A>) -> Self {
295 self.screens.push(screen);
296 self
297 }
298
299 pub fn screens(&self) -> &[ScreenActions<A>] {
301 &self.screens
302 }
303
304 pub fn fit(&self, profile: &TargetProfile) -> Vec<FitProblem> {
310 let target = profile.id.as_str();
311 let mut out: Vec<FitProblem> = self
312 .screens
313 .iter()
314 .filter(|s| s.actions.is_none())
315 .map(|s| FitProblem {
316 rule: FitRule::Undeclared,
317 screen: Some(s.screen),
318 actions: Vec::new(),
319 inputs: Vec::new(),
320 message: format!(
321 "screen \"{}\" doesn't declare its actions (Screen::actions): every action \
322 counts as its",
323 s.screen
324 ),
325 })
326 .collect();
327 match self.keymap.target_buttons(target) {
328 Some(layout) => out.extend(controls_fit(profile, layout, &self.screens)),
329 None if profile.input.keyboard => {}
330 None => out.push(FitProblem {
331 rule: FitRule::NoLayout,
332 screen: None,
333 actions: Vec::new(),
334 inputs: Vec::new(),
335 message: format!(
336 "the keymap file has no buttons for {target}, which has no keyboard: give \
337 it a table in `targets` (\"{target}\")"
338 ),
339 }),
340 }
341 out
342 }
343}
344
345impl<A: ActionSet> Controls for GameControls<A> {
346 fn fit(&self, profile: &TargetProfile) -> Vec<FitProblem> {
347 GameControls::fit(self, profile)
348 }
349}
350
351#[cfg(test)]
352mod tests;