cheats.rsannotatedcheats.rssource592 lines · 18.8 KB · raw
1use crate::app::{App, OpenWindow};
2use egui::text::CCursorRange;
3use egui::{
4    Button, CentralPanel, Context, FontFamily, Key, KeyboardShortcut, Label, Modifiers, Panel,
5    Response, RichText, ScrollArea, Sense, TextEdit, TextStyle, Ui, ViewportCommand, Window,
6};
7use egui_extras::{Column, TableBuilder};
8use genesis_config::cheats::{GenesisCheat, GenesisCheatCodeType, GenesisCheats};
9use jgenesis_common::cheats::ByteCheatCodeU16Address;
10use jgenesis_native_driver::extensions::Console;
11use serde::Serialize;
12use smsgg_config::cheats::{SmsGgCheat, SmsGgCheatCodeType, SmsGgCheats};
13use std::borrow::Cow;
14use std::path::Path;
15use std::sync::{Arc, LazyLock};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ActiveCheats {
19    None,
20    SmsGg(SmsGgCheats),
21    Genesis(GenesisCheats),
22}
23
24impl ActiveCheats {
25    pub fn genesis_or_default(&self) -> &GenesisCheats {
26        static DEFAULT: LazyLock<GenesisCheats> = LazyLock::new(GenesisCheats::default);
27
28        match self {
29            Self::Genesis(cheats) => cheats,
30            _ => &DEFAULT,
31        }
32    }
33
34    pub fn smsgg_or_default(&self) -> &SmsGgCheats {
35        static DEFAULT: LazyLock<SmsGgCheats> = LazyLock::new(SmsGgCheats::default);
36
37        match self {
38            Self::SmsGg(cheats) => cheats,
39            _ => &DEFAULT,
40        }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub struct CheatState {
46    name: String,
47    enabled: bool,
48    codes: Vec<String>,
49    codes_buffer: String,
50    code_messages: Vec<Cow<'static, str>>,
51    cursor_range: Option<CCursorRange>,
52}
53
54impl CheatState {
55    fn new() -> Self {
56        Self {
57            name: String::new(),
58            enabled: true,
59            codes: vec![],
60            codes_buffer: String::new(),
61            code_messages: vec![],
62            cursor_range: None,
63        }
64    }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum CheatConsole {
69    Genesis,
70    SmsGg,
71}
72
73impl CheatConsole {
74    pub fn from_console(console: Console) -> Option<Self> {
75        match console {
76            Console::Genesis | Console::SegaCd | Console::Sega32X | Console::SegaCd32X => {
77                Some(Self::Genesis)
78            }
79            Console::MasterSystem | Console::GameGear | Console::Sg1000 => Some(Self::SmsGg),
80            _ => None,
81        }
82    }
83}
84
85trait SystemCheats: PartialEq + Eq + Serialize {
86    type MemoryOverrides: PartialEq + Eq;
87
88    fn from_active_or_default(active: &ActiveCheats) -> &Self;
89
90    fn from_state(state: &CheatWindowState) -> Self;
91
92    fn into_active(self) -> ActiveCheats;
93
94    fn to_memory_overrides(&self) -> Self::MemoryOverrides;
95}
96
97impl SystemCheats for GenesisCheats {
98    type MemoryOverrides = Vec<(u32, u16)>;
99
100    fn from_active_or_default(active: &ActiveCheats) -> &Self {
101        active.genesis_or_default()
102    }
103
104    fn from_state(state: &CheatWindowState) -> Self {
105        Self {
106            cheats: state
107                .cheats
108                .iter()
109                .map(|cheat| GenesisCheat {
110                    name: cheat.name.clone(),
111                    enabled: cheat.enabled,
112                    codes: cheat.codes.clone(),
113                })
114                .collect(),
115        }
116    }
117
118    fn into_active(self) -> ActiveCheats {
119        ActiveCheats::Genesis(self)
120    }
121
122    fn to_memory_overrides(&self) -> Self::MemoryOverrides {
123        self.to_memory_override_vec()
124    }
125}
126
127impl SystemCheats for SmsGgCheats {
128    type MemoryOverrides = Vec<ByteCheatCodeU16Address>;
129
130    fn from_active_or_default(active: &ActiveCheats) -> &Self {
131        active.smsgg_or_default()
132    }
133
134    fn from_state(state: &CheatWindowState) -> Self {
135        Self {
136            cheats: state
137                .cheats
138                .iter()
139                .map(|cheat| SmsGgCheat {
140                    name: cheat.name.clone(),
141                    enabled: cheat.enabled,
142                    codes: cheat.codes.clone(),
143                })
144                .collect(),
145        }
146    }
147
148    fn into_active(self) -> ActiveCheats {
149        ActiveCheats::SmsGg(self)
150    }
151
152    fn to_memory_overrides(&self) -> Self::MemoryOverrides {
153        self.to_memory_override_vec()
154    }
155}
156
157#[derive(Debug, Clone)]
158pub struct CheatWindowState {
159    cheats: Vec<CheatState>,
160    selected_cheat: usize,
161    active: Arc<ActiveCheats>,
162    active_console: Option<(Console, CheatConsole)>,
163}
164
165impl CheatWindowState {
166    pub fn new() -> Self {
167        Self {
168            cheats: vec![],
169            selected_cheat: 0,
170            active: Arc::new(ActiveCheats::None),
171            active_console: None,
172        }
173    }
174}
175
176impl App {
177    pub(super) fn render_cheats_window(&mut self, ctx: &Context) {
178        const WINDOW: OpenWindow = OpenWindow::Cheats;
179
180        let mut open = true;
181        Window::new(WINDOW.title()).open(&mut open).default_width(800.0).show(ctx, |ui| {
182            let cheats_state = &mut self.state.cheats;
183
184            Panel::top("cheats_top_panel").show_inside(ui, |ui| {
185                ui.checkbox(&mut self.config.common.cheats_enabled, "Cheats enabled");
186            });
187
188            let emu_runner_status = self.emu_runner.status();
189            if !emu_runner_status.is_running() {
190                render_centered_message(ui, "Load a game to edit cheats");
191                return;
192            }
193
194            let Some((_, console)) = cheats_state.active_console else {
195                render_centered_message(ui, "Cheats are not currently supported for this console");
196                return;
197            };
198
199            ui.add_enabled_ui(self.config.common.cheats_enabled, |ui| {
200                let mut cheat_just_created = false;
201
202                Panel::left("cheats_left_panel").show_inside(ui, |ui| {
203                    render_left_panel(ui, cheats_state, &mut cheat_just_created);
204                });
205
206                CentralPanel::default().show_inside(ui, |ui| {
207                    let Some(cheat_state) =
208                        cheats_state.cheats.get_mut(cheats_state.selected_cheat)
209                    else {
210                        return;
211                    };
212
213                    render_central_panel(ui, console, cheat_state, cheat_just_created);
214                });
215            });
216
217            self.update_active_cheats();
218        });
219        if !open {
220            self.state.open_windows.remove(&WINDOW);
221        }
222    }
223
224    fn update_active_cheats(&mut self) {
225        match self.state.cheats.active_console {
226            Some((console, CheatConsole::Genesis)) => {
227                self.update_system_active_cheats::<GenesisCheats>(console);
228            }
229            Some((console, CheatConsole::SmsGg)) => {
230                self.update_system_active_cheats::<SmsGgCheats>(console);
231            }
232            None => {}
233        }
234    }
235
236    fn update_system_active_cheats<Cheats: SystemCheats>(&mut self, console: Console) {
237        let prev_active_cheats = Arc::clone(&self.state.cheats.active);
238        let prev_active_cheats = Cheats::from_active_or_default(&prev_active_cheats);
239
240        let system_cheats = Cheats::from_state(&self.state.cheats);
241
242        let changed = *prev_active_cheats != system_cheats;
243        if changed {
244            if let Err(err) = self.config.save_cheats(
245                &self.config_path,
246                &self.state.current_file_path,
247                console.standard_extension(),
248                &system_cheats,
249            ) {
250                log::error!("Error saving cheats file: {err}");
251            }
252
253            let new_memory_overrides = system_cheats.to_memory_overrides();
254            self.state.cheats.active = Arc::new(system_cheats.into_active());
255
256            let prev_memory_overrides = prev_active_cheats.to_memory_overrides();
257            if prev_memory_overrides != new_memory_overrides {
258                self.reload_config();
259            }
260        }
261    }
262
263    pub(super) fn check_cheat_names_for_cjk(&mut self, ctx: &Context) {
264        self.load_cjk_fonts_if_needed(ctx, |app, predicate| match app.active_cheats().as_ref() {
265            ActiveCheats::None => None,
266            ActiveCheats::SmsGg(cheats) => {
267                cheats.cheats.iter().find_map(|cheat| predicate(&cheat.name))
268            }
269            ActiveCheats::Genesis(cheats) => {
270                cheats.cheats.iter().find_map(|cheat| predicate(&cheat.name))
271            }
272        });
273    }
274
275    pub(super) fn load_cheats_for_game(&mut self, console: Console, rom_file_path: &Path) {
276        match CheatConsole::from_console(console) {
277            Some(CheatConsole::Genesis) => {
278                let cheats = self
279                    .config
280                    .try_load_cheats::<GenesisCheats>(
281                        &self.config_path,
282                        rom_file_path,
283                        console.standard_extension(),
284                    )
285                    .unwrap_or_default();
286
287                self.state.cheats = CheatWindowState {
288                    cheats: cheats
289                        .cheats
290                        .iter()
291                        .map(|cheat| {
292                            let codes_buffer = cheat.codes.join("\n");
293                            let code_messages =
294                                cheat.codes.iter().map(|code| genesis_code_message(code)).collect();
295
296                            CheatState {
297                                name: cheat.name.clone(),
298                                enabled: cheat.enabled,
299                                codes: cheat.codes.clone(),
300                                codes_buffer,
301                                code_messages,
302                                cursor_range: None,
303                            }
304                        })
305                        .collect(),
306                    selected_cheat: 0,
307                    active: Arc::new(ActiveCheats::Genesis(cheats)),
308                    active_console: Some((console, CheatConsole::Genesis)),
309                };
310            }
311            Some(CheatConsole::SmsGg) => {
312                let cheats = self
313                    .config
314                    .try_load_cheats::<SmsGgCheats>(
315                        &self.config_path,
316                        rom_file_path,
317                        console.standard_extension(),
318                    )
319                    .unwrap_or_default();
320
321                self.state.cheats = CheatWindowState {
322                    cheats: cheats
323                        .cheats
324                        .iter()
325                        .map(|cheat| {
326                            let codes_buffer = cheat.codes.join("\n");
327                            let code_messages =
328                                cheat.codes.iter().map(|code| smsgg_code_message(code)).collect();
329
330                            CheatState {
331                                name: cheat.name.clone(),
332                                enabled: cheat.enabled,
333                                codes: cheat.codes.clone(),
334                                codes_buffer,
335                                code_messages,
336                                cursor_range: None,
337                            }
338                        })
339                        .collect(),
340                    selected_cheat: 0,
341                    active: Arc::new(ActiveCheats::SmsGg(cheats)),
342                    active_console: Some((console, CheatConsole::SmsGg)),
343                };
344            }
345            None => {
346                self.state.cheats.active = Arc::new(ActiveCheats::None);
347                self.state.cheats.active_console = None;
348            }
349        }
350    }
351
352    pub(super) fn active_cheats(&self) -> &Arc<ActiveCheats> {
353        static NONE: LazyLock<Arc<ActiveCheats>> = LazyLock::new(|| Arc::new(ActiveCheats::None));
354
355        if !self.config.common.cheats_enabled {
356            return &NONE;
357        }
358
359        &self.state.cheats.active
360    }
361}
362
363fn render_left_panel(
364    ui: &mut Ui,
365    cheats_state: &mut CheatWindowState,
366    cheat_just_created: &mut bool,
367) {
368    ui.add_space(5.0);
369
370    if ui.button("Add New").clicked() {
371        cheats_state.cheats.push(CheatState::new());
372
373        cheats_state.selected_cheat = cheats_state.cheats.len() - 1;
374        *cheat_just_created = true;
375    }
376
377    ui.add_space(5.0);
378
379    let mut rows_to_remove = Vec::<usize>::new();
380
381    TableBuilder::new(ui)
382        .column(Column::auto())
383        .column(Column::auto().at_least(200.0))
384        .column(Column::remainder().at_least(60.0))
385        .sense(Sense::click())
386        .body(|body| {
387            body.rows(15.0, cheats_state.cheats.len(), |mut row| {
388                let row_index = row.index();
389                row.set_selected(row_index == cheats_state.selected_cheat);
390
391                let cheat = &mut cheats_state.cheats[row_index];
392
393                row.col(|ui| {
394                    ui.checkbox(&mut cheat.enabled, "");
395                });
396
397                row.col(|ui| {
398                    let name = if !cheat.name.is_empty() { &cheat.name } else { "(No name)" };
399                    ui.add(Label::new(name).selectable(false));
400                });
401
402                if row.response().clicked() {
403                    cheats_state.selected_cheat = row_index;
404                }
405
406                row.col(|ui| {
407                    if ui.button("Delete").clicked() {
408                        rows_to_remove.push(row_index);
409                    }
410                });
411            });
412        });
413
414    for i in rows_to_remove {
415        if i < cheats_state.cheats.len() {
416            cheats_state.cheats.remove(i);
417
418            if cheats_state.selected_cheat > i {
419                cheats_state.selected_cheat -= 1;
420            }
421        }
422    }
423}
424
425fn render_central_panel(
426    ui: &mut Ui,
427    console: CheatConsole,
428    cheat_state: &mut CheatState,
429    cheat_just_created: bool,
430) {
431    const TEXTEDIT_WIDTH: f32 = 150.0;
432    const TEXTEDIT_ROWS: usize = 10;
433
434    ui.horizontal(|ui| {
435        ui.label("Name");
436
437        let resp = ui.add(TextEdit::singleline(&mut cheat_state.name).hint_text("New Cheat"));
438        if cheat_just_created {
439            resp.request_focus();
440        }
441    });
442
443    ui.add_space(10.0);
444
445    ui.label("Codes");
446
447    ScrollArea::vertical().show(ui, |ui| {
448        ui.horizontal(|ui| {
449            let cheats_resp = text_edit_with_copy_paste_menu(
450                TextEdit::multiline(&mut cheat_state.codes_buffer)
451                    .font(TextStyle::Monospace)
452                    .desired_width(TEXTEDIT_WIDTH)
453                    .desired_rows(TEXTEDIT_ROWS),
454                &mut cheat_state.cursor_range,
455                ui,
456            );
457
458            if cheats_resp.changed() {
459                cheat_state.codes.clear();
460                cheat_state.code_messages.clear();
461
462                for line in cheat_state.codes_buffer.split('\n') {
463                    let line = line.trim();
464                    if line.is_empty() {
465                        cheat_state.code_messages.push("".into());
466                        continue;
467                    }
468
469                    cheat_state.codes.push(line.into());
470
471                    let message = match console {
472                        CheatConsole::Genesis => genesis_code_message(line),
473                        CheatConsole::SmsGg => smsgg_code_message(line),
474                    };
475                    cheat_state.code_messages.push(message);
476                }
477            }
478
479            ui.vertical(|ui| {
480                ui.add_space(3.0);
481
482                ui.spacing_mut().item_spacing.y = 0.0;
483
484                for message in &cheat_state.code_messages {
485                    ui.label(RichText::new(message.as_ref()).family(FontFamily::Monospace));
486                }
487            });
488        });
489
490        ui.add_space(5.0);
491
492        ui.label("Enter one code per line.");
493        ui.add_space(5.0);
494        ui.label("Supported code formats:");
495
496        match console {
497            CheatConsole::Genesis => {
498                ui.label(RichText::new(" • Game Genie (ABCD-EGHJ)").monospace());
499                ui.label(
500                    RichText::new(" • Pro Action Replay (AAAAA AVVVV) (Little-endian value)")
501                        .monospace(),
502                );
503                ui.label(
504                    RichText::new(" • Memory address/value (AAAAAA:VVVV) (Big-endian value)")
505                        .monospace(),
506                );
507            }
508            CheatConsole::SmsGg => {
509                ui.label(RichText::new(" • Game Genie (ABC-DEF or ABC-DEF-012)").monospace());
510                ui.label(RichText::new(" • Pro Action Replay (00AA AAVV)").monospace());
511            }
512        }
513    });
514}
515
516fn text_edit_with_copy_paste_menu(
517    text_edit: TextEdit<'_>,
518    cursor_range: &mut Option<CCursorRange>,
519    ui: &mut Ui,
520) -> Response {
521    let mut resp = text_edit.show(ui);
522
523    // Workaround for: https://github.com/emilk/egui/issues/4393
524    //
525    // egui's builtin TextEdit widget does not distinguish between left and right clicks when updating
526    // the cursor position, which is obviously problematic for a right click copy/paste menu
527    let right_mouse_down = resp.response.secondary_clicked()
528        || ui.input(|i| i.pointer.secondary_down() && !i.pointer.is_decidedly_dragging());
529    let cursor_range_selected = cursor_range.is_some_and(|range| range.primary != range.secondary);
530    if right_mouse_down && cursor_range_selected {
531        resp.state.cursor.set_char_range(*cursor_range);
532        resp.state.store(ui, resp.response.id);
533    } else {
534        *cursor_range = resp.cursor_range;
535    }
536
537    resp.response.context_menu(|ui| {
538        for (label, key, command) in [
539            ("Cut", Key::X, ViewportCommand::RequestCut),
540            ("Copy", Key::C, ViewportCommand::RequestCopy),
541            ("Paste", Key::V, ViewportCommand::RequestPaste),
542        ] {
543            let button = Button::new(label)
544                .shortcut_text(ui.format_shortcut(&KeyboardShortcut::new(Modifiers::CTRL, key)));
545            if ui.add(button).clicked() {
546                resp.response.request_focus();
547                ui.send_viewport_cmd(command);
548            }
549        }
550    });
551
552    resp.response.response
553}
554
555const INVALID_CODE: &str = "Invalid code";
556
557fn genesis_code_message(line: &str) -> Cow<'static, str> {
558    let Some(code_type) = GenesisCheatCodeType::guess_from(line) else {
559        return INVALID_CODE.into();
560    };
561
562    let Some((address, value)) = code_type.decode(line) else {
563        return INVALID_CODE.into();
564    };
565
566    format!("${address:06X} = 0x{value:04X}").into()
567}
568
569fn smsgg_code_message(line: &str) -> Cow<'static, str> {
570    let Some(code_type) = SmsGgCheatCodeType::guess_from(line) else {
571        return INVALID_CODE.into();
572    };
573
574    let Some(code) = code_type.decode(line) else {
575        return INVALID_CODE.into();
576    };
577
578    match code.reference {
579        Some(reference) => {
580            format!("${:04X} = 0x{:02X} (if = 0x{reference:02X})", code.address, code.value).into()
581        }
582        None => format!("${:04X} = 0x{:02X}", code.address, code.value).into(),
583    }
584}
585
586fn render_centered_message(ui: &mut Ui, message: &str) {
587    CentralPanel::default().show_inside(ui, |ui| {
588        ui.centered_and_justified(|ui| {
589            ui.label(message);
590        });
591    });
592}