diff --git a/.gitignore b/.gitignore index ab75dc1..9374d18 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /user* *.zip version.txt +perf.data* +flamegraph.svg diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cb141..a707217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,33 @@ # Marble Machinations Change Log Game store page: https://crispypin.itch.io/marble-machinations -## [unreleased] +## v0.3.3 - 2025-04-21 +### added +- option to display power direction while overlay is enabled +- show level name in end popup + +### changed +- hide tick timing numbers by default +- when multiple I/O silos (or multiple directions of one) are activated in the same tick, they will all output the same value instead of pulling input in an arbitrary order + +### fixed +- input bytes are consumed even if the marble can't be created because another one was taking its place +- keybindings activated even when typing in a text field, making especially renaming blueprints difficult +- grid rendering broken on right edge of the screen at some zoom levels and window sizes +- crash when saving config if no user dir exists +- bindings did not properly take into account order of pressing, so Shift+A and A+Shift were treated as the same thing +- after removing a binding that was a superset of another, the remaining one did not stop being blocked by the removed ones additional modifiers until another binding was added or edited + +## v0.3.2 - 2025-04-14 +### added +- "weird machines" chapter with levels for [deadfish](https://esolangs.org/wiki/Deadfish) and [brainfuck](https://esolangs.org/wiki/Brainfuck) +- "missing levels" section giving access to solutions to levels that are no longer available +- click to collapse chapters in level list +- input bindings for eraser (X), selection (B), blueprint list (Ctrl B), no tool (no default binding) +### fixed +- invalid action ids in the config file key bindings caused everything to revert to default. +- when start and stop are bound to the same thing (as by default), only start works +- when two input bindings had the same trigger but one has a strict subset of the others modifiers, both would activate when the one with more modifiers was pressed. For example (Ctrl+S -> Save) would also trigger (S -> Wire Tool). Now, Shift+S will still trigger Wire Tool, unless Shift+S (or eg. Shift+Ctrl+S) is bound to something else. ## v0.3.1 - 2025-04-05 ### fixed @@ -11,7 +37,7 @@ Game store page: https://crispypin.itch.io/marble-machinations ### added - score number: bounding area - configurable key bindings for many editor actions -- QWERTY+ASDFGH keybindings for the tile tools by default +- QWERTY+ASDFGH key bindings for the tile tools by default - OS clipboard copy/paste, with fallback to old behavior when copying - cut selection - in-grid text comments (not yet editable in-game) diff --git a/Cargo.lock b/Cargo.lock index 0481cd8..e27bf23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -213,7 +213,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "marble-machinations" -version = "0.3.1" +version = "0.3.3" dependencies = [ "arboard", "raylib", diff --git a/Cargo.toml b/Cargo.toml index d7d32d7..9fd920c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "marble-machinations" -version = "0.3.1" +version = "0.3.3" edition = "2021" default-run = "marble-machinations" @@ -10,6 +10,10 @@ raylib = "5.5" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" +[features] +# affects simulation sub-steps, used for profiling +inline_less = [] + [[bin]] name = "bench" path = "src/benchmark.rs" diff --git a/Makefile b/Makefile index 69bf9cf..e5a2446 100644 --- a/Makefile +++ b/Makefile @@ -22,3 +22,9 @@ windows: rm -rf ${RELEASE_DIRNAME}_win all: windows linux + +flamegraph: + cargo flamegraph --release --bin bench --features "inline_less" + +bench: + cargo run --release --bin bench diff --git a/README.md b/README.md index e6d4c93..d29aba8 100644 --- a/README.md +++ b/README.md @@ -7,41 +7,52 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble ## todo ### meta - engine tests -- blag post about marble movement logic +- blag post about marble movement logic? +- standardise terminology (cycle/step/tick) ### bugs -- modifier-less bindings trigger when typing in a text box, makes renaming existing blueprints basically impossible + ### features +#### 0.3.x +- more levels + - packet routing? + - game of life sim (width;height;steps;grid -> grid) +- shrink button +#### 0.4.x +- UI layout engine + - global scale multiplier, affected by window size + - background colour setting (requires color picker => after UI rework) + - light theme +#### unspecified - comments - editing - add to all intro levels - highlight regions with background colours -- UI layout engine - - global scale setting - button + binding to flip selection that is being pasted -- accessibility - - background colour setting - - hotkeys for everything (no mouse needed to play) - - menu navigation (requires UI rework) - - speed up/down - - grid cursor movement and placement - - grid zoom and pan -- more levels -- scroll output bytes +- hotkeys for everything (no mouse needed to play) + - menu navigation (requires UI rework) + - speed up/down + - keybinds for specific tool variants + - grid cursor movement and placement + - grid zoom and pan +- config settings page categories (mostly for keybindings) +- UI: scroll output bytes - timestamps in solutions and blueprints - display tool variant more clearly (it's not obvious there are more states) - better text rendering - font selection (probably a lot of work) ### online stuff - store scores in server -- validate solutions in server (with limits) +- validate solutions in server (with compute limits) - show histograms - author name in solutions and blueprints ### undecided +- option to skip (fast-forward through with settable multiplier) first N stages or cycles, for when you are debugging something that happens in later stages - hide some tile tools in early levels to make it less overwhelming - footprint score (tiles that were non-empty at any point in the run) - option to use 8-bit marbles? -- blueprint rotation? +- 32 bit input/output? - settable marble start direction? + - allows blueprint rotation ## playtesting observations - 'loops' introduces too many things (powering, redirection, generating zeroes) diff --git a/levels/chapter_05.json b/levels/chapter_05.json new file mode 100644 index 0000000..e08eeb8 --- /dev/null +++ b/levels/chapter_05.json @@ -0,0 +1,44 @@ +{ + "title": "5. Weird machines", + "levels": [ + { + "id": "deadfish", + "name": "Deadfish", + "description": "Deadfish is a very small joke programming language. It is often used as a test program for other esoteric programming languages. Marble machinations is now one of them.\n\n There are four commands:\ni: increment the accumulator\nd: decrement the accumulator\ns: square the accumulator\no: output the accumulator\n\n if the value becomes -1 or 256, it should be set to zero.\n\nThe test cases are taken from esolangs.org/wiki/Deadfish", + "stages": [{ + "input": "iiso", + "output": "4" + },{ + "input": "iissso", + "output": "0" + },{ + "input": "diissisdo", + "output": "288" + },{ + "input": "iissisdddddddddddddddddddddddddddddddddo", + "output": "0" + }] + }, + { + "id": "brainfuck", + "name": "Brainfuck", + "description": "The language brainfuck operates on a 'tape' of bytes, with a pointer that can be moved left and right. Level input is formatted as '#'.\n(separator # is 35)\n\n+ (43): increment\n- (45): decrement\n< (60): move left\n> (62): move right\n, (44): input\n. (46): output\n[ (91): skip to matching ] if current value is zero\n] (93): jump to matching [ if current value is not zero\n\nmaximum memory needed: 32 bytes\nlongest program: 106 commands", + "stages":[{ + "input": ">><[]++++++++-.#", + "output": [7] + },{ + "input": ",>,>,>,.<.<.<.#woem", + "output": "meow" + },{ + "input": "++++[++++.]#", + "output": [8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, 148, 152, 156, 160, 164, 168, 172, 176, 180, 184, 188, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240, 244, 248, 252, 0] + },{ + "input": "-[>,]<+[-.<+]-#reverse cat program", + "output": "margorp tac esrever" + },{ + "input": "++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.#", + "output": "Hello World!\n" + }] + } + ] +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 8200b00..674611f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,21 @@ use raylib::prelude::*; use serde::{Deserialize, Serialize}; -use crate::{input::Input, theme::FG_CHAPTER_TITLE, ui::text_button, util::Scroll, Globals}; +use crate::{ + input::Input, + theme::FG_CHAPTER_TITLE, + ui::{text_button, toggle_button}, + util::Scroll, + Globals, +}; #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Config { pub input: Input, + #[serde(default)] + pub show_debug_timing: bool, + #[serde(default)] + pub show_power_direction: bool, #[serde(skip)] scroll_offset: u32, } @@ -25,20 +35,31 @@ impl Config { Some(Scroll::Up) => self.scroll_offset = self.scroll_offset.saturating_sub(64), None => (), } - let y = -(self.scroll_offset as i32); + let mut y = -(self.scroll_offset as i32) + 15; + d.draw_text("Settings", 16, y, 30, FG_CHAPTER_TITLE); + y += 40; - d.draw_text("Settings", 16, y + 16, 30, FG_CHAPTER_TITLE); - - if text_button(d, &globals.mouse, 10, y + 60, 80, "apply") { + if text_button(d, &globals.mouse, 10, y, 80, "apply") { return MenuReturn::StaySave; } - if text_button(d, &globals.mouse, 100, y + 60, 80, "done") { + if text_button(d, &globals.mouse, 100, y, 80, "done") { return MenuReturn::ReturnSave; } - if text_button(d, &globals.mouse, 190, y + 60, 80, "cancel") { + if text_button(d, &globals.mouse, 190, y, 80, "cancel") { return MenuReturn::ReturnCancel; } + y += 40; + let mut toggle = |value, text| { + toggle_button((d, &globals.mouse), 10, y, 30, 30, value); + d.draw_text(text, 50, y + 5, 20, Color::WHITE); + y += 40; + }; + + toggle(&mut self.show_power_direction, "show power directions"); + toggle(&mut self.show_debug_timing, "show debug timing"); + + // self.input.update(d); self.input.draw_edit(d, globals, y); MenuReturn::Stay } diff --git a/src/editor.rs b/src/editor.rs index 4db5372..e3a34fa 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -24,7 +24,7 @@ const HEADER_HEIGHT: i32 = 40; const FOOTER_HEIGHT: i32 = 95; const SIDEBAR_WIDTH: i32 = 200 + 32 * 2 + 5 * 4; const END_POPUP_WIDTH: i32 = 320; -const END_POPUP_HEIGHT: i32 = 225; +const END_POPUP_HEIGHT: i32 = 255; const MAX_ZOOM: f32 = 8.; const MIN_ZOOM: f32 = 0.25; @@ -74,8 +74,8 @@ pub struct Editor { undo_history: Vec, undo_index: usize, // debug/profiling - step_time: u128, - max_step_time: u128, + step_time: usize, + max_step_time: usize, start_time: Instant, } @@ -85,10 +85,10 @@ enum Action { SetArea(ResizeDeltas, Pos, Board, Board), } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone)] enum Popup { None, - Success, + Success(Score), Failure, LevelInfo, PauseMenu, @@ -316,7 +316,7 @@ impl Editor { self.machine.step(); if let Some(i) = self.stage { - if matches!(self.popup, Popup::Failure | Popup::Success) { + if matches!(self.popup, Popup::Failure | Popup::Success(_)) { self.popup = Popup::None; self.dismissed_end = true; } @@ -328,15 +328,16 @@ impl Editor { self.total_steps += self.machine.step_count(); self.reset_machine(); } else { - self.popup = Popup::Success; println!("completed in {:?}", self.start_time.elapsed()); self.exit_state = ExitState::Save; self.sim_state = SimState::Stepping; - self.score = Some(Score { + let score = Score { cycles: self.total_steps + self.machine.step_count(), tiles: self.source_board.grid.count_tiles(), bounds_area: self.source_board.grid.used_bounds_area(), - }); + }; + self.score = Some(score.clone()); + self.popup = Popup::Success(score); } } else if !stage.output().as_bytes().starts_with(self.machine.output()) { self.popup = Popup::Failure; @@ -439,7 +440,7 @@ impl Editor { if globals.is_pressed(ActionId::ToggleMenu) { self.popup = match self.popup { - Popup::Success | Popup::Failure => { + Popup::Success(_) | Popup::Failure => { self.dismissed_end = true; Popup::None } @@ -472,28 +473,31 @@ impl Editor { .as_micros() .checked_div(steps_taken) .unwrap_or_default(); - self.step_time = avg_step_time; - self.max_step_time = avg_step_time.max(self.max_step_time); + self.step_time = avg_step_time as usize; + self.max_step_time = self.step_time.max(self.max_step_time); } if globals.is_pressed(ActionId::StepSim) { self.step_pressed() } - if globals.is_pressed(ActionId::StartSim) { - match self.sim_state { - SimState::Editing => { + match self.sim_state { + SimState::Editing => { + if globals.is_pressed(ActionId::StartSim) { self.init_sim(); self.sim_state = SimState::Running; } - SimState::Stepping => self.sim_state = SimState::Running, - SimState::Running => (), } - } else if globals.is_pressed(ActionId::StopSim) { - match self.sim_state { - SimState::Running | SimState::Stepping => { + SimState::Stepping => { + if globals.is_pressed(ActionId::StartSim) { + self.sim_state = SimState::Running + } else if globals.is_pressed(ActionId::StopSim) { self.sim_state = SimState::Editing; self.popup = Popup::None; } - SimState::Editing => (), + } + SimState::Running => { + if globals.is_pressed(ActionId::StopSim) { + self.sim_state = SimState::Editing; + } } } @@ -571,27 +575,48 @@ impl Editor { } } - fn draw_board(&self, d: &mut RaylibDrawHandle, textures: &Textures) { + fn draw_board(&self, d: &mut RaylibDrawHandle, globals: &Globals) { + let draw_power = globals.config.show_power_direction && self.draw_overlay; if self.sim_state == SimState::Editing { - self.source_board - .grid - .draw(d, textures, self.view_offset, self.zoom); + self.source_board.grid.draw( + d, + &globals.textures, + self.view_offset, + self.zoom, + draw_power, + ); } else { if self.machine.debug_subticks.is_empty() { - self.machine - .grid() - .draw(d, textures, self.view_offset, self.zoom); + self.machine.grid().draw( + d, + &globals.textures, + self.view_offset, + self.zoom, + draw_power, + ); } else { let subframe = &self.machine.debug_subticks[self.machine.subtick_index]; - subframe.grid.draw(d, textures, self.view_offset, self.zoom); + subframe.grid.draw( + d, + &globals.textures, + self.view_offset, + self.zoom, + draw_power, + ); if let Some(pos) = subframe.pos { let p = self.pos_to_screen(pos.to_vec()); - d.draw_texture_ex(textures.get("selection"), p, 0., self.zoom, Color::ORANGE); + d.draw_texture_ex( + globals.get_tex("selection"), + p, + 0., + self.zoom, + Color::ORANGE, + ); } } if self.draw_overlay { self.machine - .draw_marble_values(d, textures, self.view_offset, self.zoom); + .draw_marble_values(d, &globals.textures, self.view_offset, self.zoom); } } if self.draw_overlay { @@ -607,23 +632,25 @@ impl Editor { let tile_size = TILE_TEXTURE_SIZE * self.zoom; let grid_spill_x = (self.view_offset.x).rem(tile_size) - tile_size; let grid_spill_y = (self.view_offset.y).rem(tile_size) - tile_size; - for y in 0..=(d.get_screen_height() / tile_size as i32) { + let hlines = d.get_screen_height() / tile_size as i32 + 3; + let vlines = d.get_screen_width() / tile_size as i32 + 3; + for y in 0..hlines { let y = y * tile_size as i32 + grid_spill_y as i32; d.draw_line(0, y, d.get_screen_width(), y, FG_GRID); } - for x in 0..=(d.get_screen_width() / tile_size as i32) { + for x in 0..vlines { let x = x * tile_size as i32 + grid_spill_x as i32; d.draw_line(x, 0, x, d.get_screen_height(), FG_GRID); } } - self.draw_board(d, &globals.textures); - self.board_overlay(d, &globals.textures); + self.draw_board(d, globals); + self.board_overlay(d, globals); self.draw_bottom_bar(d, globals); self.draw_top_bar(d, globals); if self.active_tool == Tool::Blueprint { - self.draw_blueprint_sidebar(d, &globals.textures); + self.draw_blueprint_sidebar(d, globals); } self.mouse.update(d); @@ -640,7 +667,7 @@ impl Editor { } match self.popup { - Popup::Success | Popup::Failure => { + Popup::Success(_) | Popup::Failure => { self.draw_end_popup(d, &globals.textures); } Popup::LevelInfo => { @@ -673,7 +700,7 @@ impl Editor { self.tooltip.draw(d); } - fn draw_blueprint_sidebar(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { + fn draw_blueprint_sidebar(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals) { let sidebar_height = d.get_screen_height() - FOOTER_HEIGHT - HEADER_HEIGHT - 40; d.draw_rectangle( 0, @@ -697,7 +724,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (5, y), - textures.get("rubbish"), + globals.get_tex("rubbish"), (&mut self.tooltip, "Delete"), ) { b.remove_file(); @@ -708,7 +735,7 @@ impl Editor { let mut text_selected = is_selected && self.blueprint_name_selected; text_input( d, - &self.mouse, + globals, Rectangle::new(42., y as f32, 200., 32.), &mut b.name, &mut text_selected, @@ -727,7 +754,7 @@ impl Editor { ); d.draw_texture_ex( - textures.get("blueprint"), + globals.get_tex("blueprint"), Vector2::new((42 + 205) as f32, y as f32), 0., 2., @@ -740,38 +767,35 @@ impl Editor { fn draw_end_popup(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { let bounds = screen_centered_rect(d, END_POPUP_WIDTH, END_POPUP_HEIGHT); let x = bounds.x as i32; - let y = bounds.y as i32; + let mut y = bounds.y as i32 + 10; d.draw_rectangle_rec(bounds, BG_DARK); - if self.popup == Popup::Success { - d.draw_text("Level Complete!", x + 45, y + 10, 30, Color::LIME); - if let Some(score) = &self.score { - d.draw_text("cycles", x + 15, y + 40, 20, Color::WHITE); - draw_usize(d, textures, score.cycles, (x + 110, y + 40), 9, 2); - d.draw_text("tiles", x + 15, y + 80, 20, Color::WHITE); - draw_usize(d, textures, score.tiles, (x + 110, y + 80), 9, 2); - d.draw_text("bounds", x + 15, y + 120, 20, Color::WHITE); - draw_usize(d, textures, score.bounds_area, (x + 110, y + 120), 9, 2); - } - let y = y + 60; - if simple_button((d, &self.mouse), x + 10, y + 110, 140, 45) { + if let Popup::Success(score) = &self.popup { + d.draw_text("Level Complete!", x + 45, y, 30, Color::LIME); + y += 30; + d.draw_text(self.level.name(), x + 10, y, 20, Color::GRAY); + y += 30; + d.draw_text("cycles", x + 15, y, 20, Color::WHITE); + draw_usize(d, textures, score.cycles, (x + 110, y), 9, 2); + y += 40; + d.draw_text("tiles", x + 15, y, 20, Color::WHITE); + draw_usize(d, textures, score.tiles, (x + 110, y), 9, 2); + y += 40; + d.draw_text("bounds", x + 15, y, 20, Color::WHITE); + draw_usize(d, textures, score.bounds_area, (x + 110, y), 9, 2); + y += 40; + if simple_button((d, &self.mouse), x + 10, y, 140, 50) { self.popup = Popup::None; self.dismissed_end = true; } - d.draw_text("continue\nediting", x + 15, y + 115, 20, Color::WHITE); + d.draw_text("continue\nediting", x + 15, y + 5, 20, Color::WHITE); - if simple_button( - (d, &self.mouse), - x + END_POPUP_WIDTH / 2 + 5, - y + 110, - 140, - 45, - ) { + if simple_button((d, &self.mouse), x + END_POPUP_WIDTH / 2 + 5, y, 140, 50) { self.exit_state = ExitState::ExitAndSave; } d.draw_text( "return to\nlevel list", x + END_POPUP_WIDTH / 2 + 10, - y + 115, + y + 5, 20, Color::WHITE, ); @@ -786,8 +810,7 @@ impl Editor { } } - fn draw_top_bar(&mut self, d: &mut RaylibDrawHandle, globals: &Globals) { - let textures = &globals.textures; + fn draw_top_bar(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals) { // background d.draw_rectangle( 0, @@ -800,7 +823,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (4, 4), - textures.get("exit"), + globals.get_tex("exit"), (&mut self.tooltip, "exit"), ) { if self.exit_menu { @@ -813,7 +836,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (40, 4), - textures.get("cancel"), + globals.get_tex("cancel"), (&mut self.tooltip, "cancel"), ) { self.exit_menu = false; @@ -821,7 +844,7 @@ impl Editor { } else if tex32_button( (d, &self.mouse), (40, 4), - textures.get("save"), + globals.get_tex("save"), (&mut self.tooltip, "save"), ) || globals.is_pressed(ActionId::Save) { @@ -841,7 +864,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (150, 4), - textures.get(undo_icon), + globals.get_tex(undo_icon), (&mut self.tooltip, "Undo"), ) { self.undo() @@ -855,7 +878,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (186, 4), - textures.get(redo_icon), + globals.get_tex(redo_icon), (&mut self.tooltip, "Redo"), ) { self.redo() @@ -870,7 +893,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (223, 4), - textures.get(overlay_btn_icon), + globals.get_tex(overlay_btn_icon), (&mut self.tooltip, "Toggle overlay"), ) { self.draw_overlay = !self.draw_overlay; @@ -880,7 +903,7 @@ impl Editor { if tex32_button( (d, &self.mouse), (260, 4), - textures.get("pause"), + globals.get_tex("pause"), (&mut self.tooltip, "Pause"), ) { self.sim_state = SimState::Stepping; @@ -888,7 +911,7 @@ impl Editor { } else if tex32_button( (d, &self.mouse), (260, 4), - textures.get("play"), + globals.get_tex("play"), (&mut self.tooltip, "Start"), ) { if self.sim_state == SimState::Editing { @@ -901,7 +924,7 @@ impl Editor { && tex32_button( (d, &self.mouse), (296, 4), - textures.get("stop"), + globals.get_tex("stop"), (&mut self.tooltip, "Stop"), ) { self.sim_state = SimState::Editing; @@ -911,14 +934,21 @@ impl Editor { if tex32_button( (d, &self.mouse), (332, 4), - textures.get("step"), + globals.get_tex("step"), (&mut self.tooltip, "Step"), ) { self.step_pressed(); } self.tooltip.add(368, 4, 48, 32, "Speed"); - draw_usize(d, textures, 1 << self.sim_speed, (368, 4), SPEED_DIGITS, 1); + draw_usize( + d, + &globals.textures, + 1 << self.sim_speed, + (368, 4), + SPEED_DIGITS, + 1, + ); slider( (d, &self.mouse), rect(368, 24, 48, 12), @@ -928,20 +958,36 @@ impl Editor { ); self.tooltip.add(420, 4, 180, 32, "Steps"); - draw_usize(d, textures, self.machine.step_count(), (420, 4), 9, 2); + draw_usize( + d, + &globals.textures, + self.machine.step_count(), + (420, 4), + 9, + 2, + ); if self.stage > Some(0) { self.tooltip.add(420, 44, 180, 32, "Total steps"); let total_steps = self.total_steps + self.machine.step_count(); - draw_usize(d, textures, total_steps, (420, 44), 9, 2); + draw_usize(d, &globals.textures, total_steps, (420, 44), 9, 2); } - draw_usize(d, textures, self.step_time as usize, (260, 42), 9, 1); - draw_usize(d, textures, self.max_step_time as usize, (260, 60), 9, 1); - #[cfg(debug_assertions)] - { - draw_usize(d, textures, self.machine.subtick_index, (260, 80), 9, 1); - let subtick_count = self.machine.debug_subticks.len(); - draw_usize(d, textures, subtick_count, (260, 100), 9, 1); + if globals.config.show_debug_timing { + draw_usize(d, &globals.textures, self.step_time, (260, 42), 9, 1); + draw_usize(d, &globals.textures, self.max_step_time, (260, 60), 9, 1); + #[cfg(debug_assertions)] + { + draw_usize( + d, + &globals.textures, + self.machine.subtick_index, + (260, 80), + 9, + 1, + ); + let subtick_count = self.machine.debug_subticks.len(); + draw_usize(d, &globals.textures, subtick_count, (260, 100), 9, 1); + } } d.draw_text("input:", 603, 8, 10, Color::WHITE); @@ -964,7 +1010,7 @@ impl Editor { } if text_input( d, - &self.mouse, + globals, Rectangle::new(input_x as f32, 5., (width - input_x - 5) as f32, 30.), &mut input_text, &mut self.input_text_selected, @@ -1018,7 +1064,7 @@ impl Editor { hide_tile_tools = true; text_input( d, - &self.mouse, + globals, Rectangle::new(100., footer_top + 10., 240., 30.), &mut self.new_blueprint_name, &mut self.blueprint_name_selected, @@ -1060,7 +1106,7 @@ impl Editor { self.tooltip.add(276, y, 40, 40, "Delete"); if simple_button((d, &self.mouse), 276, y, 40, 40) - || globals.is_pressed(ActionId::Erase) + || globals.is_pressed(ActionId::EraseSelection) { erase_selection = true; } @@ -1091,7 +1137,7 @@ impl Editor { texture: &str, tooltip: &'static str, tool_option: Tool, - action: Option| { + action: ActionId| { let border = 4.; let gap = 2.; let button_size = 32. + border * 2.; @@ -1111,20 +1157,26 @@ impl Editor { tool_option, &mut self.active_tool, border, - action.map(|a| globals.is_pressed(a)).unwrap_or(false), + globals.is_pressed(action), ) }; - tool_button((0, -2), "eraser", "Eraser", Tool::Erase, None); + tool_button((0, -2), "eraser", "Eraser", Tool::Erase, ActionId::Eraser); tool_button( (1, -2), "selection", "Select", Tool::SelectArea(Selection::default()), - None, + ActionId::Selection, ); - tool_button((0, -1), "blueprint", "Blueprints", Tool::Blueprint, None); - tool_button((1, -1), "transparent", "None", Tool::None, None); + tool_button( + (0, -1), + "blueprint", + "Blueprints", + Tool::Blueprint, + ActionId::Blueprints, + ); + tool_button((1, -1), "transparent", "None", Tool::None, ActionId::NoTool); if !hide_tile_tools { tool_button( @@ -1132,42 +1184,42 @@ impl Editor { "block", "Block", Tool::SetTile(Tile::from_char('#')), - Some(ActionId::TileBlock), + ActionId::TileBlock, ); tool_button( (0, 1), "silo_off", "Silo", Tool::SetTile(Tile::from_char('B')), - Some(ActionId::TileSilo), + ActionId::TileSilo, ); tool_button( (0, 2), "button_off", "Button", Tool::SetTile(Tile::from_char('*')), - Some(ActionId::TileButton), + ActionId::TileButton, ); tool_button( (0, 3), "io_tile_off", "Input/Output silo", Tool::SetTile(Tile::from_char('I')), - Some(ActionId::TileIOSilo), + ActionId::TileIOSilo, ); tool_button( (0, 4), "flipper_off", "Flipper", Tool::SetTile(Tile::from_char('F')), - Some(ActionId::TileFlipper), + ActionId::TileFlipper, ); tool_button( (0, 5), "digit_tool", "Digit", Tool::Digits(None), - Some(ActionId::TileDigit), + ActionId::TileDigit, ); tool_button( @@ -1175,14 +1227,14 @@ impl Editor { "marble", "Marble", Tool::SetTile(Tile::from_char('o')), - Some(ActionId::TileMarble), + ActionId::TileMarble, ); match tool_button( (1, 1), self.tool_wire.texture_name_off(), self.tool_wire.human_name(), Tool::Wire, - Some(ActionId::TileGroupWire), + ActionId::TileGroupWire, ) { Some(Scroll::Down) => self.tool_wire.next(), Some(Scroll::Up) => self.tool_wire.prev(), @@ -1194,7 +1246,7 @@ impl Editor { self.tool_arrow.arrow_tile_texture_name(), self.tool_arrow.arrow_tile_human_name(), Tool::Arrow, - Some(ActionId::TileGroupArrow), + ActionId::TileGroupArrow, ) { Some(Scroll::Down) => self.tool_arrow = self.tool_arrow.right(), Some(Scroll::Up) => self.tool_arrow = self.tool_arrow.left(), @@ -1205,7 +1257,7 @@ impl Editor { self.tool_mirror.texture_name(), self.tool_mirror.human_name(), Tool::Mirror, - Some(ActionId::TileGroupMirror), + ActionId::TileGroupMirror, ) .is_some() { @@ -1216,7 +1268,7 @@ impl Editor { self.tool_math.texture_name_off(), self.tool_math.human_name(), Tool::Math, - Some(ActionId::TileGroupMath), + ActionId::TileGroupMath, ) { Some(Scroll::Down) => self.tool_math.next(), Some(Scroll::Up) => self.tool_math.prev(), @@ -1227,7 +1279,7 @@ impl Editor { self.tool_comparator.texture_name_off(), self.tool_comparator.human_name(), Tool::Comparator, - Some(ActionId::TileGroupCompare), + ActionId::TileGroupCompare, ) { Some(Scroll::Down) => self.tool_comparator.next(), Some(Scroll::Up) => self.tool_comparator.prev(), @@ -1308,7 +1360,7 @@ impl Editor { } } - fn board_overlay(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { + fn board_overlay(&mut self, d: &mut RaylibDrawHandle, globals: &Globals) { let footer_top = (d.get_screen_height() - FOOTER_HEIGHT) as f32; let tile_size = TILE_TEXTURE_SIZE * self.zoom; @@ -1327,7 +1379,9 @@ impl Editor { offset.x -= offset.x.rem(tile_size); offset.y -= offset.y.rem(tile_size); offset += view_offset; - board.grid.draw(d, textures, offset, self.zoom); + board + .grid + .draw(d, &globals.textures, offset, self.zoom, false); board.draw_comments(d, offset, self.zoom); if self.mouse.left_click() { let tile_pos = (self.mouse.pos() - self.view_offset) / tile_size; @@ -1341,7 +1395,7 @@ impl Editor { if let Tool::Digits(Some(pos)) = &mut self.active_tool { let tile_screen_pos = pos.to_vec() * tile_size + self.view_offset; d.draw_texture_ex( - textures.get("selection"), + globals.get_tex("selection"), tile_screen_pos, 0., self.zoom, @@ -1394,7 +1448,7 @@ impl Editor { }; d.draw_texture_ex( - textures.get(tex), + globals.get_tex(tex), tile_screen_pos, 0., self.zoom, @@ -1464,7 +1518,9 @@ impl Editor { offset.x -= offset.x.rem(tile_size); offset.y -= offset.y.rem(tile_size); offset += view_offset; - bp.board.grid.draw(d, textures, offset, self.zoom); + bp.board + .grid + .draw(d, &globals.textures, offset, self.zoom, false); bp.board.draw_comments(d, offset, self.zoom); } if self.mouse.pos().x < SIDEBAR_WIDTH as f32 { diff --git a/src/input.rs b/src/input.rs index 4d695b0..a769625 100644 --- a/src/input.rs +++ b/src/input.rs @@ -23,7 +23,7 @@ pub enum ActionId { Copy, Cut, Paste, - Erase, + EraseSelection, ToggleMenu, Save, StartSim, @@ -32,6 +32,10 @@ pub enum ActionId { CycleGroup, CycleGroupRevMod, + Eraser, + Selection, + Blueprints, + NoTool, TileBlock, TileSilo, TileButton, @@ -54,6 +58,7 @@ impl Default for Input { let mut bindings = [(); ActionId::SIZE].map(|_| Vec::new()); let mut bind_key = |action, mods, trigger| { bindings[action as usize].push(Binding { + blocking_modifiers: Vec::new(), modifiers: mods, trigger, }); @@ -64,7 +69,7 @@ impl Default for Input { bind_key(ActionId::Copy, vec![LCtrl], C); bind_key(ActionId::Cut, vec![LCtrl], X); bind_key(ActionId::Paste, vec![LCtrl], V); - bind_key(ActionId::Erase, vec![], Backspace); + bind_key(ActionId::EraseSelection, vec![], Backspace); bind_key(ActionId::ToggleMenu, vec![], Escape); bind_key(ActionId::Save, vec![LCtrl], S); bind_key(ActionId::StartSim, vec![], Enter); @@ -72,6 +77,9 @@ impl Default for Input { bind_key(ActionId::StepSim, vec![], Space); bind_key(ActionId::CycleGroup, vec![], Tab); bind_key(ActionId::CycleGroupRevMod, vec![], LShift); + bind_key(ActionId::Eraser, vec![], X); + bind_key(ActionId::Blueprints, vec![LCtrl], B); + bind_key(ActionId::Selection, vec![], B); bind_key(ActionId::TileBlock, vec![], Q); bind_key(ActionId::TileSilo, vec![], W); bind_key(ActionId::TileButton, vec![], E); @@ -86,9 +94,11 @@ impl Default for Input { bind_key(ActionId::TileGroupCompare, vec![], H); Self { - bindings, - states: Default::default(), + action_bindings: bindings, + action_states: [BindingState::Off; ActionId::SIZE], + key_states: [BindingState::Off; Button::SIZE], editing_binding: None, + in_text_edit: false, } } } @@ -102,14 +112,16 @@ enum BindingState { Released, } -type InputMap = BTreeMap>; +type InputMap = BTreeMap>; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(from = "InputMap", into = "InputMap")] pub struct Input { - bindings: [Vec; ActionId::SIZE], - states: [BindingState; ActionId::SIZE], + action_bindings: [Vec; ActionId::SIZE], + action_states: [BindingState; ActionId::SIZE], + key_states: [BindingState; Button::SIZE], editing_binding: Option<(ActionId, usize, BindingEdit)>, + pub in_text_edit: bool, } #[derive(Clone, Debug)] @@ -120,8 +132,7 @@ enum BindingEdit { } impl Input { - pub fn draw_edit(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals, y: i32) { - let mut y = y + 96; + pub fn draw_edit(&mut self, d: &mut RaylibDrawHandle, globals: &mut Globals, mut y: i32) { if self.editing_binding.is_some() { globals.mouse.clear(); } @@ -132,36 +143,48 @@ impl Input { for action_index in 0..ActionId::SIZE { let action = ActionId::from_usize(action_index).unwrap(); + // if self.action_states[action_index] == BindingState::Pressed { + // d.draw_rectangle(200, y, 20, 20, Color::LIMEGREEN); + // } d.draw_text(&format!("{action:?}"), 16, y, 20, Color::ORANGE); - for (binding_index, binding) in self.bindings[action_index].iter().enumerate() { + for (binding_index, binding) in self.action_bindings[action_index].iter().enumerate() { if text_button(d, &globals.mouse, buttons_x, y, 80, "remove") { - self.bindings[action_index].remove(binding_index); + self.action_bindings[action_index].remove(binding_index); + self.update_modifier_blocks(); return; } if text_button(d, &globals.mouse, buttons_x + 85, y, 45, "edit") { self.editing_binding = Some((action, binding_index, BindingEdit::Init)); } + // let trigger = format!("{:?}", binding.trigger); - d.draw_text(&trigger, binding_text_x, y + 5, 20, Color::LIMEGREEN); - let x = binding_text_x + 10 + d.measure_text(&trigger, 20); + let mut x = binding_text_x; + d.draw_text(&trigger, x, y + 5, 20, Color::LIMEGREEN); + x += 10 + d.measure_text(&trigger, 20); + // let modifiers = format!("{:?}", binding.modifiers); d.draw_text(&modifiers, x, y + 5, 20, Color::LIGHTBLUE); - let conflicts = conflicts(&self.bindings, binding, action); + x += 10 + d.measure_text(&modifiers, 20); + // + let conflicts = conflicts(&self.action_bindings, binding, action); if !conflicts.is_empty() { - let x = x + 10 + d.measure_text(&modifiers, 20); - d.draw_text( - &format!("also used by: {conflicts:?}"), - x, - y + 5, - 20, - Color::ORANGERED, - ); + let conflict_text = format!("also used by: {conflicts:?}"); + d.draw_text(&conflict_text, x, y + 5, 20, Color::ORANGERED); + x += 10 + d.measure_text(&conflict_text, 20); + } + // + if !binding.blocking_modifiers.is_empty() { + let blocking_text = format!("not while: {:?}", binding.blocking_modifiers); + d.draw_text(&blocking_text, x, y + 5, 20, Color::GRAY); } y += 32; } if text_button(d, &globals.mouse, buttons_x, y, 130, "add binding") { - self.editing_binding = - Some((action, self.bindings[action_index].len(), BindingEdit::Init)); + self.editing_binding = Some(( + action, + self.action_bindings[action_index].len(), + BindingEdit::Init, + )); } y += 45; } @@ -193,6 +216,7 @@ impl Input { BindingEdit::Init => { if key.just_pressed(d) { *edit_state = BindingEdit::Adding(Binding { + blocking_modifiers: Vec::new(), modifiers: Vec::new(), trigger: key, }); @@ -218,6 +242,7 @@ impl Input { globals.mouse.is_over(ok_btn_rect) && key == Button::MouseLeft; if key.just_pressed(d) && !clicking_ok { *edit_state = BindingEdit::Adding(Binding { + blocking_modifiers: Vec::new(), modifiers: Vec::new(), trigger: key, }); @@ -235,7 +260,7 @@ impl Input { let text = format!("{:?} + {:?}", b.modifiers, b.trigger); d.draw_text(&text, x + 5, y + 5, 20, colour); - let conflicts = conflicts(&self.bindings, b, *action); + let conflicts = conflicts(&self.action_bindings, b, *action); if !conflicts.is_empty() { d.draw_text( &format!("conflicts: {conflicts:?}"), @@ -248,13 +273,14 @@ impl Input { } if text_button(d, &globals.mouse, ok_btn_x, ok_btn_y, ok_btn_width, "ok") { if let BindingEdit::Releasing(binding) = edit_state { - let binding_list = &mut self.bindings[*action as usize]; + let binding_list = &mut self.action_bindings[*action as usize]; if *binding_index < binding_list.len() { binding_list[*binding_index] = binding.clone(); } else { binding_list.push(binding.clone()); } self.editing_binding = None; + self.update_modifier_blocks(); } } if text_button(d, &globals.mouse, x + 100, y + 40, 80, "cancel") { @@ -264,16 +290,10 @@ impl Input { } pub fn update(&mut self, rl: &RaylibHandle) { - for i in 0..ActionId::SIZE { - let bindings = &self.bindings[i]; - let mut is_active = false; - for binding in bindings { - if binding.modifiers.iter().all(|&m| m.is_down(rl)) { - is_active |= binding.trigger.is_down(rl); - } - } - let state = &mut self.states[i]; - *state = if is_active { + for i in 0..Button::SIZE { + let button = Button::from_usize(i).unwrap(); + let state = &mut self.key_states[i]; + *state = if button.is_down(rl) { match state { BindingState::Off | BindingState::Released => BindingState::Pressed, BindingState::Pressed | BindingState::Held => BindingState::Held, @@ -285,19 +305,67 @@ impl Input { } } } + for i in 0..ActionId::SIZE { + let bindings = &self.action_bindings[i]; + let mut is_active = BindingState::Off; + if !self.in_text_edit { + for binding in bindings { + if binding + .modifiers + .iter() + .all(|&m| self.key_states[m as usize] == BindingState::Held) + && binding + .blocking_modifiers + .iter() + .all(|&m| self.key_states[m as usize] == BindingState::Off) + { + is_active = is_active.or(self.key_states[binding.trigger as usize]); + } + } + } + self.action_states[i] = is_active; + } + self.in_text_edit = false; } pub fn is_pressed(&self, action: ActionId) -> bool { - self.states[action as usize] == BindingState::Pressed + self.action_states[action as usize] == BindingState::Pressed } pub fn is_held(&self, action: ActionId) -> bool { - self.states[action as usize] == BindingState::Pressed - || self.states[action as usize] == BindingState::Held + self.action_states[action as usize] == BindingState::Pressed + || self.action_states[action as usize] == BindingState::Held } pub fn is_released(&self, action: ActionId) -> bool { - self.states[action as usize] == BindingState::Released + self.action_states[action as usize] == BindingState::Released + } + + /// Must be called after any binding has changed. + /// Ensures a binding "S" is not triggered by "Ctrl+S", if "Ctrl+S" is bound to something else. + pub fn update_modifier_blocks(&mut self) { + for i in 0..ActionId::SIZE { + let bindings = &self.action_bindings[i]; + for binding_index in 0..bindings.len() { + let binding = &self.action_bindings[i][binding_index]; + let mut blocking_mods = Vec::new(); + for i in 0..ActionId::SIZE { + let other_bindings = &self.action_bindings[i]; + for other_binding in other_bindings { + if other_binding.trigger == binding.trigger { + for modifier in &other_binding.modifiers { + if !blocking_mods.contains(modifier) + && !binding.modifiers.contains(modifier) + { + blocking_mods.push(*modifier); + } + } + } + } + } + self.action_bindings[i][binding_index].blocking_modifiers = blocking_mods; + } + } } } @@ -338,15 +406,38 @@ impl ActionId { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Binding { + #[serde(skip)] + blocking_modifiers: Vec