lemur/src/window/game.rs

426 lines
15 KiB
Rust

use std::sync::mpsc;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
persistence::Persistence,
};
use egui::{
menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui,
Vec2, ViewportBuilder, ViewportCommand, ViewportId, Window,
};
use egui_toast::{Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy;
use super::{
game_screen::{DisplayMode, GameScreen},
utils::UiExt as _,
AppWindow,
};
const COLOR_PRESETS: [[Color32; 2]; 3] = [
[
Color32::from_rgb(0xff, 0x00, 0x00),
Color32::from_rgb(0x00, 0xc6, 0xf0),
],
[
Color32::from_rgb(0x00, 0xb4, 0x00),
Color32::from_rgb(0xc8, 0x00, 0xff),
],
[
Color32::from_rgb(0xb4, 0x9b, 0x00),
Color32::from_rgb(0x00, 0x00, 0xff),
],
];
pub struct GameWindow {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
sim_id: SimId,
config: GameConfig,
screen: Option<GameScreen>,
messages: Option<mpsc::Receiver<Toast>>,
color_picker: Option<ColorPickerState>,
}
impl GameWindow {
pub fn new(
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
sim_id: SimId,
) -> Self {
let config = load_config(&persistence, sim_id);
Self {
client,
proxy,
persistence,
sim_id,
config,
screen: None,
messages: None,
color_picker: None,
}
}
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
ui.menu_button("ROM", |ui| {
if ui.button("Open ROM").clicked() {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
ui.close_menu();
}
if ui.button("Quit").clicked() {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
});
ui.menu_button("Emulation", |ui| {
let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running;
let can_resume = is_ready && state == EmulatorState::Paused;
if state == EmulatorState::Running {
if ui.add_enabled(can_pause, Button::new("Pause")).clicked() {
self.client.send_command(EmulatorCommand::Pause);
ui.close_menu();
}
} else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() {
self.client.send_command(EmulatorCommand::Resume);
ui.close_menu();
}
if ui.add_enabled(is_ready, Button::new("Reset")).clicked() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
}
});
ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
ui.menu_button("Multiplayer", |ui| {
let has_player_2 = self.client.sim_state(SimId::Player2) != SimState::Uninitialized;
if self.sim_id == SimId::Player1
&& !has_player_2
&& ui.button("Open Player 2").clicked()
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
ui.close_menu();
}
if has_player_2 {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
ui.close_menu();
}
if !linked && ui.button("Link").clicked() {
self.client.send_command(EmulatorCommand::Link);
ui.close_menu();
}
}
});
ui.menu_button("Tools", |ui| {
if ui.button("GDB Server").clicked() {
self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Character Data").clicked() {
self.proxy
.send_event(UserEvent::OpenCharacterData(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Background Maps").clicked() {
self.proxy
.send_event(UserEvent::OpenBgMap(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Objects").clicked() {
self.proxy
.send_event(UserEvent::OpenObjects(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("Worlds").clicked() {
self.proxy
.send_event(UserEvent::OpenWorlds(self.sim_id))
.unwrap();
ui.close_menu();
}
});
ui.menu_button("Help", |ui| {
if ui.button("About").clicked() {
self.proxy.send_event(UserEvent::OpenAbout).unwrap();
ui.close_menu();
}
});
}
fn show_options_menu(&mut self, ctx: &Context, ui: &mut Ui) {
ui.menu_button("Video", |ui| {
ui.menu_button("Screen Size", |ui| {
let current_dims = self.config.dimensions;
for scale in 1..=4 {
let label = format!("x{scale}");
let scale = scale as f32;
let dims = {
let Vec2 { x, y } = self.config.display_mode.proportions();
Vec2::new(x * scale, y * scale + 22.0)
};
if ui
.selectable_button((current_dims - dims).length() < 1.0, label)
.clicked()
{
ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims));
ui.close_menu();
}
}
});
ui.menu_button("Display Mode", |ui| {
let old_proportions = self.config.display_mode.proportions();
let mut changed = false;
let mut display_mode = self.config.display_mode;
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::Anaglyph, "Anaglyph")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::LeftEye, "Left Eye")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::RightEye, "Right Eye")
.clicked();
changed |= ui
.selectable_option(&mut display_mode, DisplayMode::SideBySide, "Side by Side")
.clicked();
if !changed {
return;
}
let current_dims = self.config.dimensions;
let new_proportions = display_mode.proportions();
let scale = new_proportions / old_proportions;
if scale != Vec2::new(1.0, 1.0) {
ctx.send_viewport_cmd(ViewportCommand::InnerSize(current_dims * scale));
}
self.update_config(|c| {
c.display_mode = display_mode;
c.dimensions = current_dims * scale;
});
ui.close_menu();
});
ui.menu_button("Colors", |ui| {
for preset in COLOR_PRESETS {
if ui.color_pair_button(preset[0], preset[1]).clicked() {
self.update_config(|c| c.colors = preset);
ui.close_menu();
}
}
ui.with_layout(ui.layout().with_cross_align(egui::Align::Center), |ui| {
if ui.button("Custom").clicked() {
let color_str = |color: Color32| {
format!("{:02x}{:02x}{:02x}", color.r(), color.g(), color.b())
};
let is_running = self.client.emulator_state() == EmulatorState::Running;
if is_running {
self.client.send_command(EmulatorCommand::Pause);
}
let color_codes = [
color_str(self.config.colors[0]),
color_str(self.config.colors[1]),
];
self.color_picker = Some(ColorPickerState {
color_codes,
just_opened: true,
unpause_on_close: is_running,
});
ui.close_menu();
}
});
});
});
ui.menu_button("Audio", |ui| {
let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
if ui.selectable_button(p1_enabled, "Player 1").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
ui.close_menu();
}
if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
ui.close_menu();
}
});
ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() {
self.proxy.send_event(UserEvent::OpenInput).unwrap();
ui.close_menu();
}
});
}
fn show_color_picker(&mut self, ui: &mut Ui) {
let mut colors = self.config.colors;
let Some(state) = self.color_picker.as_mut() else {
return;
};
let (open, updated) = ui
.horizontal(|ui| {
let left_color = ui.color_picker(&mut colors[0], &mut state.color_codes[0]);
if state.just_opened {
left_color.request_focus();
state.just_opened = false;
}
let right_color = ui.color_picker(&mut colors[1], &mut state.color_codes[1]);
let open = left_color.has_focus() || right_color.has_focus();
let updated = left_color.changed() || right_color.changed();
(open, updated)
})
.inner;
if !open {
if state.unpause_on_close {
self.client.send_command(EmulatorCommand::Resume);
}
self.color_picker = None;
}
if updated {
self.update_config(|c| c.colors = colors);
}
}
fn update_config(&mut self, update: impl FnOnce(&mut GameConfig)) {
let mut new_config = self.config.clone();
update(&mut new_config);
if self.config != new_config {
let _ = self
.persistence
.save_config(config_filename(self.sim_id), &new_config);
}
self.config = new_config;
}
}
fn config_filename(sim_id: SimId) -> &'static str {
match sim_id {
SimId::Player1 => "config_p1",
SimId::Player2 => "config_p2",
}
}
fn load_config(persistence: &Persistence, sim_id: SimId) -> GameConfig {
if let Ok(config) = persistence.load_config(config_filename(sim_id)) {
return config;
}
GameConfig {
display_mode: DisplayMode::Anaglyph,
colors: COLOR_PRESETS[0],
dimensions: DisplayMode::Anaglyph.proportions() + Vec2::new(0.0, 22.0),
}
}
impl AppWindow for GameWindow {
fn viewport_id(&self) -> ViewportId {
match self.sim_id {
SimId::Player1 => ViewportId::ROOT,
SimId::Player2 => ViewportId::from_hash_of("Player2"),
}
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Lemur")
.with_inner_size(self.config.dimensions)
}
fn show(&mut self, ctx: &Context) {
let dimensions = {
let bounds = ctx.available_rect();
bounds.max - bounds.min
};
self.update_config(|c| c.dimensions = dimensions);
let mut toasts = Toasts::new()
.anchor(Align2::LEFT_BOTTOM, (10.0, 10.0))
.direction(Direction::BottomUp);
if let Some(messages) = self.messages.as_mut() {
while let Ok(toast) = messages.try_recv() {
toasts.add(toast);
}
}
TopBottomPanel::top("menubar")
.exact_height(22.0)
.show(ctx, |ui| {
menu::bar(ui, |ui| {
self.show_menu(ctx, ui);
});
});
if self.color_picker.is_some() {
Window::new("Color Picker")
.title_bar(false)
.resizable(false)
.anchor(Align2::CENTER_CENTER, Vec2::ZERO)
.show(ctx, |ui| {
self.show_color_picker(ui);
});
}
let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show(ctx, |ui| {
if let Some(screen) = self.screen.as_mut() {
screen.update(self.config.display_mode, self.config.colors);
ui.add(screen);
}
});
toasts.show(ctx);
}
fn on_init(&mut self, _ctx: &Context, render_state: &egui_wgpu::RenderState) {
let (screen, sink) = GameScreen::init(render_state);
let (message_sink, message_source) = mpsc::channel();
self.client.send_command(EmulatorCommand::ConnectToSim(
self.sim_id,
sink,
message_sink,
));
self.screen = Some(screen);
self.messages = Some(message_source);
}
fn on_destroy(&mut self) {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
}
struct ColorPickerState {
color_codes: [String; 2],
just_opened: bool,
unpause_on_close: bool,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)]
struct GameConfig {
display_mode: DisplayMode,
colors: [Color32; 2],
dimensions: Vec2,
}