├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── FantasqueSansMono-Bold.ttf ├── ship-flame.png └── ship.png ├── training-1 ├── level.asm └── src │ ├── main.rs │ └── ship.rs └── vm ├── Cargo.toml └── src ├── command.rs ├── config.rs ├── console.rs ├── game_core.rs ├── lib.rs ├── position.rs ├── text.rs └── vm.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock 7 | Cargo.lock 8 | 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | install: 5 | - wget https://www.libsdl.org/release/SDL2-2.0.4.tar.gz -O sdl2.tar.gz 6 | - tar xzf sdl2.tar.gz 7 | - pushd SDL2-2.0.4 && ./configure && make && sudo make install && popd 8 | - wget -q http://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.0.12.tar.gz 9 | - wget -q http://www.libsdl.org/projects/SDL_image/release/SDL2_image-2.0.0.tar.gz 10 | - wget -q http://internode.dl.sourceforge.net/project/sdl2gfx/SDL2_gfx-1.0.1.tar.gz 11 | - tar xzf SDL2_ttf-*.tar.gz 12 | - tar xzf SDL2_image-*.tar.gz 13 | - tar xzf SDL2_gfx-*.tar.gz 14 | - pushd SDL2_ttf-* && ./configure && make && sudo make install && popd 15 | - pushd SDL2_image-* && ./configure && make && sudo make install && popd 16 | - pushd SDL2_gfx-* && ./autogen.sh && ./configure && make && sudo make install && popd 17 | env: 18 | global: 19 | - LD_LIBRARY_PATH: "/usr/local/lib" -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking an interest in contributing to hakka! 4 | 5 | Any and all help is most welcome on this project. Rust beginners, Rust intermediates, Rust experts.. even non-Rustaceans. If you're keen to learn Rust, learn 6502 Assembly or just help out in general, there is something for you to do here. 6 | 7 | ## How can I contribute? 8 | 9 | Contributions take many forms. 10 | 11 | * Submit a Pull Request. 12 | * Report a bug. 13 | * Suggest an improvement. 14 | 15 | ...or, the big one: **Create a new level**. 16 | 17 | ### Submit a Pull Request 18 | 19 | Before submitting a Pull Request, it is important that the feature/bugfix/improvement/cleanup is tracked as an Issue. This stops multiple people working on the same item. 20 | 21 | So please open an Issue to discuss your intended Pull Request or comment on an existing Issue to let us know that you're planning to work on it. 22 | 23 | ### Creating a level 24 | 25 | If you have an idea for a level, [open an Issue and we can discuss it!](https://github.com/simon-whitehead/hakka/issues/new?title=Level%20suggestion:). 26 | 27 | Hakka levels are stand-alone binaries that encapsulate a single scenario. 28 | 29 | The Rust wrapper can have as little or as much control over the environment as necessary to create a fun playing experience. Just how far you go with game-logic responsibility is entirely up to you when creating a level. 30 | 31 | #### Level documentation 32 | 33 | Perhaps just as important as creating the level, is some accompanying documentation. Hakka levels require some hints where appropriate for players to pass. Unless your level is targeted at seasoned hackers, its probably best that you provide some documentation about the inner workings of the level. 34 | 35 | ### Report a bug 36 | 37 | If you've found a bug, please [open an issue and let us know what we broke!](https://github.com/simon-whitehead/hakka/issues/new?title=Bug:) 38 | 39 | ### Suggest an improvement 40 | 41 | We are always looking to improve the player experience! Please [open an issue and let us know what we can do to make the game better!](https://github.com/simon-whitehead/hakka/issues/new?title=Suggestion:) -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hakka" 3 | version = "0.2.2" 4 | authors = ["Simon Whitehead "] 5 | 6 | [lib] 7 | name = "vm" 8 | path = "vm/src/lib.rs" 9 | 10 | [[bin]] 11 | name = "training-1" 12 | path = "training-1/src/main.rs" 13 | 14 | [dependencies] 15 | byteorder = "0.5.3" 16 | rs6502 = "0.3.0" 17 | find_folder = "0.3.0" 18 | app_dirs = "1.1.1" 19 | rustc-serialize = "0.3.22" 20 | 21 | [dependencies.sdl2] 22 | version = "^0.29" 23 | features = ["image", "ttf"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Simon Whitehead 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hakka [![Build Status](https://travis-ci.org/simon-whitehead/hakka.svg?branch=master)](https://travis-ci.org/simon-whitehead/hakka) 2 | A game that you can't pass simply by playing the game. You have to hack it. 3 | 4 | **NOTE: This is currently in "proof-of-concept" stage** and is being actively developed. This early version 5 | is being released to test the waters to see if people find it interesting. 6 | 7 | ## Building the game 8 | 9 | This game is built using [Rust](https://www.rust-lang.org/). You will need to [download and install Rust](https://www.rust-lang.org/en-US/install.html) before being able to build this game. 10 | 11 | Hakka requires `SDL2`, `SDL2_image` and `SDL2_ttf`. 12 | 13 | [You can see instructions for installing the SDL2 development libraries here](https://github.com/AngryLawyer/rust-sdl2#sdl20-development-libraries) 14 | 15 | If you're on OSX and have Homebrew installed, it should be as simple as: 16 | 17 | ``` 18 | brew install SDL2 19 | brew install SDL2_image 20 | brew install SDL2_ttf 21 | ``` 22 | 23 | Once the SDL dependencies are installed, you can run the game via: 24 | 25 | cargo run --bin training-1 26 | 27 | ## How to play 28 | 29 | Toggle the in-game Console via the Backtick/Grave/Tilde key (`~` on English keyboards. The key _under_ escape on non-English keyboards). 30 | 31 | (There is an in-game `help` command which gives a basic overview of the available commands) 32 | 33 | The goal of the training level is simple. Fly the ship up to the finish line: 34 | 35 | ![screen shot 2016-12-22 at 7 42 18 pm](https://cloud.githubusercontent.com/assets/2499070/21419963/fc9f33f6-c87e-11e6-8e31-0c9a39ccbece.png) 36 | 37 | You can fly it to the finish line with the `Up` arrow. It should be pretty simple.. 38 | 39 | ## Hacking the game 40 | 41 | The game includes terminal support for hacking the game. The game runs via an emulated 6502 Microprocessor and 42 | the terminal supports features to interrogate the virtual machine. 43 | 44 | ### Look at the game code 45 | 46 | ![screen shot 2016-12-22 at 7 43 09 pm](https://cloud.githubusercontent.com/assets/2499070/21419974/10e8113e-c87f-11e6-815b-85349ccc2550.png) 47 | 48 | ### Set breakpoints and step through the code 49 | 50 | ![screen shot 2016-12-22 at 7 43 35 pm](https://cloud.githubusercontent.com/assets/2499070/21419976/11bcc3b6-c87f-11e6-8f93-a5bd144ddd22.png) 51 | 52 | ### ..or alter the game memory directly! 53 | 54 | 55 | ## Contributing 56 | 57 | I would LOVE contributions. This is currently a single "training" level. I plan on expanding this repository 58 | with new levels in future. If you would like to clean the code up, add documentation, __implement levels__, or anything 59 | else, please do open an Issue and lets discuss it! 60 | 61 | 62 | ## LICENSE 63 | MIT licensed. I hope you learn something from it. 64 | 65 | ## Credits 66 | 67 | The spaceship sprite was created by [MillionthVector and is hosted for free on his/her website](http://millionthvector.blogspot.com.au/p/free-sprites.html). Check it out! 68 | 69 | The font is the [FantasqueSansMono font developed by GitHub user belluzj](https://github.com/belluzj/fantasque-sans). 70 | -------------------------------------------------------------------------------- /assets/FantasqueSansMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simon-whitehead/hakka/d3e08004be83e808c6d3d748b2d03b0f2c515439/assets/FantasqueSansMono-Bold.ttf -------------------------------------------------------------------------------- /assets/ship-flame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simon-whitehead/hakka/d3e08004be83e808c6d3d748b2d03b0f2c515439/assets/ship-flame.png -------------------------------------------------------------------------------- /assets/ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simon-whitehead/hakka/d3e08004be83e808c6d3d748b2d03b0f2c515439/assets/ship.png -------------------------------------------------------------------------------- /training-1/level.asm: -------------------------------------------------------------------------------- 1 | 2 | ; This code runs Training Level 1 of the game, hakka. 3 | 4 | ; 16-bit Ship X position 5 | X_0 = $00 6 | X_1 = $01 7 | 8 | ; 16-bit Ship Y position 9 | Y_0 = $02 10 | Y_1 = $03 11 | 12 | ; The keycode of the last key pressed 13 | KEY = $04 14 | 15 | ; 16-bit movement speed 16 | MOV_0 = $05 17 | MOV_1 = $06 18 | 19 | GameLoop 20 | 21 | JSR UpArrow 22 | JSR DownArrow 23 | JSR Flame 24 | 25 | JMP GameLoop 26 | 27 | DownArrow: 28 | 29 | LDA KEY 30 | CMP #40 ; Was the down arrow pressed? 31 | BNE DownArrowEnd 32 | 33 | ; Add the movement speed to the Y position 34 | CLC 35 | SEI 36 | LDA Y_0 37 | ADC MOV_0 38 | STA Y_0 39 | LDA Y_1 40 | ADC MOV_1 41 | STA Y_1 42 | CLI 43 | 44 | DownArrowEnd: 45 | RTS 46 | 47 | UpArrow: 48 | 49 | LDA KEY 50 | CMP #38 ; Was the up arrow pressed? 51 | BNE UpArrowEnd 52 | 53 | SEC 54 | SEI 55 | LDA Y_0 56 | SBC MOV_0 57 | STA Y_0 58 | LDA Y_1 59 | SBC MOV_1 60 | STA Y_1 61 | 62 | ; clamp the Y position so it can't be bigger than
63 | 64 | UpArrowEnd: 65 | CLI 66 | RTS 67 | 68 | ; Toggle the flame based on key state 69 | Flame: 70 | LDA KEY 71 | CMP #$00 72 | BEQ FlameOff 73 | LDA #$01 74 | STA $07 75 | JMP FlameEnd 76 | FlameOff: 77 | LDA #$00 78 | STA $07 79 | 80 | FlameEnd: 81 | RTS 82 | 83 | END: -------------------------------------------------------------------------------- /training-1/src/main.rs: -------------------------------------------------------------------------------- 1 | 2 | extern crate byteorder; 3 | extern crate find_folder; 4 | extern crate rs6502; 5 | extern crate sdl2; 6 | extern crate vm; 7 | 8 | mod ship; 9 | 10 | use std::path::Path; 11 | 12 | use byteorder::{ByteOrder, LittleEndian}; 13 | 14 | use find_folder::Search; 15 | 16 | use sdl2::event::Event; 17 | use sdl2::keyboard::*; 18 | use sdl2::image::LoadTexture; 19 | use sdl2::pixels::Color; 20 | use sdl2::rect::Rect; 21 | use sdl2::render::{Renderer, TextureQuery}; 22 | 23 | use rs6502::{Assembler, CodeSegment, Cpu}; 24 | use vm::{Position, Text, GameCore}; 25 | 26 | const FPS_STEP: u32 = 1000 / 60; 27 | 28 | fn main() { 29 | let window_width = 1280; 30 | let window_height = 720; 31 | 32 | let sdl_context = sdl2::init().unwrap(); 33 | let ttf_context = sdl2::ttf::init().unwrap(); 34 | let video_subsystem = sdl_context.video().unwrap(); 35 | 36 | let window = video_subsystem.window("hakka", window_width, window_height) 37 | .build() 38 | .unwrap(); 39 | 40 | let (window_width, _) = window.size(); 41 | 42 | let mut renderer = window.renderer() 43 | .accelerated() 44 | .build() 45 | .unwrap(); 46 | 47 | let local = Search::Parents(3).for_folder("training-1").unwrap(); 48 | let assets = Search::KidsThenParents(3, 3).for_folder("assets").unwrap(); 49 | 50 | let ship_texture = renderer.load_texture(&assets.join("ship.png")).unwrap(); 51 | let ship_flame_texture = renderer.load_texture(&assets.join("ship-flame.png")) 52 | .unwrap(); 53 | 54 | let font = assets.join("FantasqueSansMono-Bold.ttf"); 55 | 56 | let finish_text = Text::new(&ttf_context, 57 | &mut renderer, 58 | "FINISH", 59 | Position::HorizontalCenter((window_width / 2) as i32, 25), 60 | 56, 61 | Color::RGBA(0, 0, 0, 255), 62 | font.to_str().unwrap()); 63 | let win_text = Text::new(&ttf_context, 64 | &mut renderer, 65 | "PASSED", 66 | Position::HorizontalCenter((window_width / 2) as i32, 330), 67 | 64, 68 | Color::RGBA(0, 0, 0, 255), 69 | font.to_str().unwrap()); 70 | 71 | let mut game_core = GameCore::new(&ttf_context, &mut renderer, font.to_str().unwrap()); 72 | 73 | let TextureQuery { width: ship_width, .. } = ship_texture.query(); 74 | init_cpu_mem(&mut game_core.vm.cpu, &mut renderer, ship_width); 75 | 76 | let segments = assemble(local.join("level.asm")); 77 | game_core.vm.load_code_segments(segments); 78 | game_core.vm.cpu.reset(); 79 | 80 | let mut events = sdl_context.event_pump().unwrap(); 81 | 82 | let mut level_complete = false; 83 | let mut ship = ship::Ship::new(ship_texture, 84 | ship_flame_texture, 85 | Position::HorizontalCenter((window_width / 2) as i32, 500)); 86 | let mut last_fps = 0; 87 | let mut monitor_last = 0; 88 | 89 | 'running: loop { 90 | 91 | for event in events.poll_iter() { 92 | game_core.process_event(&event); 93 | 94 | if !game_core.vm.console.visible { 95 | match event { 96 | Event::Quit { .. } => break 'running, 97 | Event::KeyUp { keycode, .. } => { 98 | match keycode { 99 | Some(Keycode::Up) | 100 | Some(Keycode::Down) => { 101 | game_core.vm.cpu.memory[0x04] = 0; 102 | } 103 | _ => (), 104 | } 105 | } 106 | Event::KeyDown { keycode, .. } => { 107 | match keycode { 108 | Some(Keycode::Escape) => break 'running, 109 | 110 | // Movement 111 | Some(Keycode::Up) => { 112 | game_core.vm.cpu.memory[0x04] = 38; 113 | } 114 | Some(Keycode::Down) => { 115 | game_core.vm.cpu.memory[0x04] = 40; 116 | } 117 | _ => (), 118 | } 119 | } 120 | _ => (), 121 | } 122 | } 123 | } 124 | 125 | if !level_complete { 126 | ship.process(&game_core.vm.cpu.memory[..]); 127 | 128 | // Pull the ship back so it can't go past a certain spot 129 | if ship.y <= 0x190 && ship.y >= 0x100 && game_core.vm.cpu.memory[0x04] != 0 { 130 | game_core.vm.cpu.memory[0x02] = 0x90; 131 | game_core.vm.cpu.memory[0x03] = 0x01; 132 | } 133 | } 134 | 135 | let now = sdl_context.timer().unwrap().ticks(); 136 | let delta = now - last_fps; 137 | if delta < FPS_STEP { 138 | sdl_context.timer().unwrap().delay(FPS_STEP - delta); 139 | } else { 140 | game_core.update(); 141 | 142 | // Rendering only the background when interrupts are disabled results in a horrible 143 | // flickering; therefore only render when we're either in single stepping mode or 144 | // interrupts are enabled 145 | if game_core.vm.is_debugging() || !game_core.vm.cpu.flags.interrupt_disabled { 146 | renderer.set_draw_color(Color::RGBA(0, 0, 0, 255)); 147 | renderer.clear(); 148 | 149 | // Render complete game screen only if interrupts are enabled 150 | if !game_core.vm.cpu.flags.interrupt_disabled { 151 | if level_complete { 152 | draw_passed_background(&mut renderer); 153 | win_text.render(&mut renderer); 154 | } 155 | draw_finish_background(&mut renderer); 156 | finish_text.render(&mut renderer); 157 | if game_core.vm.cpu.memory[0x07] > 0 { 158 | ship.render_flame(&mut renderer); 159 | } 160 | ship.render(&mut renderer); 161 | if ship.y <= 0x8C { 162 | level_complete = true; 163 | } 164 | } 165 | game_core.vm.render(&mut renderer); 166 | renderer.present(); 167 | last_fps = now; 168 | } 169 | } 170 | 171 | // Dump the CPU memory at 1 second intervals if the monitor is enabled 172 | let delta = now - monitor_last; 173 | if delta > 1000 && game_core.vm.monitor.enabled { 174 | game_core.vm.dump_memory(); 175 | monitor_last = now; 176 | } 177 | } 178 | } 179 | 180 | fn assemble

(path: P) -> Vec 181 | where P: AsRef 182 | { 183 | let mut assembler = Assembler::new(); 184 | assembler.assemble_file(path, 0xC000).unwrap() 185 | } 186 | 187 | fn init_cpu_mem(cpu: &mut Cpu, renderer: &mut Renderer, ship_width: u32) { 188 | cpu.flags.interrupt_disabled = false; 189 | 190 | LittleEndian::write_u16(&mut cpu.memory[0..], 191 | renderer.window().unwrap().size().0 as u16 / 2 - 192 | (ship_width as u16 / 2)); 193 | cpu.memory[0x02] = 0xFF; 194 | cpu.memory[0x03] = 0x01; 195 | cpu.memory[0x05] = 0x05; 196 | cpu.memory[0x06] = 0x00; 197 | } 198 | 199 | fn draw_text_background(renderer: &mut Renderer, color: Color, y: i32) { 200 | let width = renderer.window().unwrap().size().0; 201 | renderer.set_draw_color(color); 202 | renderer.fill_rect(Rect::new(0, y, width, 120)).unwrap(); 203 | } 204 | 205 | fn draw_finish_background(renderer: &mut Renderer) { 206 | draw_text_background(renderer, Color::RGB(0, 144, 192), 0); 207 | } 208 | 209 | fn draw_passed_background(renderer: &mut Renderer) { 210 | draw_text_background(renderer, Color::RGB(0, 255, 0), 300); 211 | } 212 | -------------------------------------------------------------------------------- /training-1/src/ship.rs: -------------------------------------------------------------------------------- 1 | use sdl2::pixels::Color; 2 | use sdl2::rect::Rect; 3 | use sdl2::render::{Renderer, Texture, TextureQuery}; 4 | 5 | use vm::Position; 6 | 7 | pub struct Ship { 8 | pub x: i32, 9 | pub y: i32, 10 | width: u32, 11 | height: u32, 12 | flame_width: u32, 13 | flame_height: u32, 14 | texture: Texture, 15 | flame_texture: Texture, 16 | } 17 | 18 | impl Ship { 19 | pub fn new(texture: Texture, flame_texture: Texture, position: Position) -> Ship { 20 | let TextureQuery { width: ship_width, height: ship_height, .. } = texture.query(); 21 | let TextureQuery { width: flame_width, height: flame_height, .. } = flame_texture.query(); 22 | 23 | let (x, y) = match position { 24 | Position::HorizontalCenter(x, y) => (x - ship_width as i32 / 2, y), 25 | Position::XY(x, y) => (x, y), 26 | }; 27 | 28 | Ship { 29 | x: x, 30 | y: y, 31 | width: ship_width, 32 | height: ship_height, 33 | texture: texture, 34 | flame_width: flame_width, 35 | flame_height: flame_height, 36 | flame_texture: flame_texture, 37 | } 38 | } 39 | 40 | pub fn process(&mut self, memory: &[u8]) { 41 | // println!("Mem: {:?}", &memory[0x00..0x05]); 42 | let mut x: u16 = memory[0x00] as u16; 43 | x |= (memory[0x01] as u16) << 8; 44 | self.x = x as i32; 45 | 46 | let mut y: u16 = memory[0x02] as u16; 47 | y |= (memory[0x03] as u16) << 8; 48 | self.y = y as i32; 49 | } 50 | 51 | pub fn render(&self, renderer: &mut Renderer) { 52 | renderer.set_draw_color(Color::RGBA(0, 0, 0, 255)); 53 | renderer.copy(&self.texture, 54 | None, 55 | Some(Rect::new(self.x, self.y, self.width, self.height))) 56 | .unwrap(); 57 | } 58 | 59 | pub fn render_flame(&self, renderer: &mut Renderer) { 60 | renderer.copy(&self.flame_texture, 61 | None, 62 | Some(Rect::new(self.x - 10, 63 | self.y + 150, 64 | self.flame_width, 65 | self.flame_height))) 66 | .unwrap(); 67 | 68 | renderer.copy(&self.flame_texture, 69 | None, 70 | Some(Rect::new(self.x + 77, 71 | self.y + 150, 72 | self.flame_width, 73 | self.flame_height))) 74 | .unwrap(); 75 | } 76 | } -------------------------------------------------------------------------------- /vm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vm" 3 | version = "0.2.0" 4 | authors = ["Simon Whitehead "] 5 | 6 | [dependencies] 7 | rs6502 = "0.3.0" 8 | find_folder = "0.3.0" 9 | app_dirs = "1.1.1" 10 | rustc-serialize = "0.3.22" 11 | 12 | [dependencies.sdl2] 13 | version = "*" 14 | features = ["image", "ttf"] 15 | -------------------------------------------------------------------------------- /vm/src/command.rs: -------------------------------------------------------------------------------- 1 | 2 | use std; 3 | use std::io::Write; 4 | use vm::VirtualMachine; 5 | 6 | pub type UnblockEvent = Box; 7 | 8 | pub struct CommandSystem { 9 | commands: Vec>, 10 | } 11 | impl CommandSystem { 12 | pub fn new() -> CommandSystem { 13 | let mut system = CommandSystem { 14 | commands: Vec::new() 15 | }; 16 | 17 | system.add_command(HelpCommand); 18 | system.add_command(ClearCommand); 19 | system.add_command(SourceCommand); 20 | system.add_command(ListCommand); 21 | system.add_command(RegistersCommand); 22 | system.add_command(StepCommand); 23 | system.add_command(ContinueCommand); 24 | system.add_command(BreakCommand); 25 | system.add_command(FlagsCommand); 26 | system.add_command(MemdmpCommand); 27 | system.add_command(MemsetCommand); 28 | system.add_command(MonitorCommand); 29 | system.add_command(ExitCommand); 30 | 31 | system 32 | } 33 | 34 | pub fn add_command(&mut self, command: C) 35 | where C: Command + 'static 36 | { 37 | self.commands.push(Box::new(command)); 38 | } 39 | 40 | pub fn execute(&self, command: S, mut vm: &mut VirtualMachine) -> (CommandResult, Option) 41 | where S: Into 42 | { 43 | let command = command.into(); 44 | let parts = command.split_whitespace().map(String::from).collect::>(); 45 | 46 | for command in self.commands.iter() { 47 | if command.matches_name(parts[0].clone()) { 48 | let result = command.execute(parts[1..].to_owned(), &self, &mut vm); 49 | if let CommandResult::SucessBlock = result { 50 | return (result, Some(command.on_unblock_event())); 51 | } else { 52 | return (result, None); 53 | } 54 | } 55 | } 56 | 57 | (CommandResult::NotFound, None) 58 | } 59 | } 60 | 61 | pub enum CommandResult { 62 | NotFound, 63 | InvalidArgs, 64 | Sucess, 65 | SucessBlock, 66 | } 67 | 68 | pub trait Command { 69 | fn execute(&self, args: Vec, system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult; 70 | 71 | fn get_names(&self) -> Vec<&str>; 72 | 73 | fn get_arg_info(&self) -> Option<&str> { 74 | None 75 | } 76 | 77 | fn get_help(&self) -> &str { 78 | "No help text provided!" 79 | } 80 | 81 | fn matches_name(&self, name: String) -> bool { 82 | for actual_name in self.get_names() { 83 | if actual_name == name { 84 | return true; 85 | } 86 | } 87 | return false 88 | } 89 | 90 | fn on_unblock_event(&self) -> UnblockEvent { 91 | Box::new(|_| {}) 92 | } 93 | } 94 | 95 | 96 | struct HelpCommand; 97 | impl Command for HelpCommand { 98 | fn execute(&self, _args: Vec, system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 99 | writeln!(vm.console, "Commands:").unwrap(); 100 | 101 | // Creates strings containing all names, e.g. "help, h, ?" 102 | for command in system.commands.iter() { 103 | let name = command.get_names().join(", "); 104 | write!(vm.console, " {}", name).unwrap(); 105 | if let Some(arg_info) = command.get_arg_info() { 106 | write!(vm.console, ": {}", arg_info).unwrap(); 107 | } 108 | writeln!(vm.console, "").unwrap(); 109 | 110 | let help = command.get_help(); 111 | let help_lines = help.trim().lines().map(|line| line.trim()).collect::>(); 112 | if !help_lines.is_empty() { 113 | for help_line in &help_lines { 114 | writeln!(vm.console, " {}", help_line).unwrap();; 115 | } 116 | } 117 | } 118 | 119 | CommandResult::Sucess 120 | } 121 | 122 | fn get_names(&self) -> Vec<&str> { 123 | vec!["help", "h", "?"] 124 | } 125 | 126 | fn get_help(&self) -> &str { 127 | "Prints this help message" 128 | } 129 | } 130 | 131 | struct ClearCommand; 132 | impl Command for ClearCommand { 133 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 134 | vm.console.clear(); 135 | CommandResult::Sucess 136 | } 137 | 138 | fn get_names(&self) -> Vec<&str> { 139 | vec!["clear", "cls"] 140 | } 141 | 142 | fn get_help(&self) -> &str { 143 | "Clears the console" 144 | } 145 | } 146 | 147 | struct SourceCommand; 148 | impl Command for SourceCommand { 149 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 150 | vm.dump_disassembly(); 151 | CommandResult::Sucess 152 | } 153 | 154 | fn get_names(&self) -> Vec<&str> { 155 | vec!["source"] 156 | } 157 | 158 | fn get_help(&self) -> &str { 159 | "Lists the code currently running in the virtual 160 | machine. A '>' symbol indicates the current 161 | program counter." 162 | } 163 | } 164 | 165 | struct ListCommand; 166 | impl Command for ListCommand { 167 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 168 | vm.dump_local_disassembly(); 169 | CommandResult::Sucess 170 | } 171 | 172 | fn get_names(&self) -> Vec<&str> { 173 | vec!["list"] 174 | } 175 | 176 | fn get_help(&self) -> &str { 177 | "Lists the code surrounding the current 178 | program counter." 179 | } 180 | } 181 | 182 | struct RegistersCommand; 183 | impl Command for RegistersCommand { 184 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 185 | vm.dump_registers(); 186 | CommandResult::Sucess 187 | } 188 | 189 | fn get_names(&self) -> Vec<&str> { 190 | vec!["registers", "reg"] 191 | } 192 | 193 | fn get_help(&self) -> &str { 194 | "Lists the CPU registers and their current 195 | values." 196 | } 197 | } 198 | 199 | struct MonitorCommand; 200 | impl Command for MonitorCommand { 201 | fn execute(&self, args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 202 | if args.len() != 2 { 203 | writeln!(vm.console, "Expected 2 arguments, found {}", args.len()).unwrap(); 204 | return CommandResult::InvalidArgs; 205 | } 206 | 207 | let start = usize::from_str_radix(&args[0].replace("0x", "")[..], 16); 208 | if start.is_err() { 209 | writeln!(vm.console, "Expected hexadecimal memory address, found {}", args[0]).unwrap(); 210 | return CommandResult::InvalidArgs; 211 | } 212 | let start = start.unwrap(); 213 | 214 | let end = usize::from_str_radix(&args[1].replace("0x", "")[..], 16); 215 | if end.is_err() { 216 | writeln!(vm.console, "Expected hexadecimal memory address, found {}", args[1]).unwrap(); 217 | return CommandResult::InvalidArgs; 218 | } 219 | let end = end.unwrap(); 220 | 221 | vm.enable_memory_monitor(start..end); 222 | 223 | CommandResult::SucessBlock 224 | } 225 | 226 | fn on_unblock_event(&self) -> UnblockEvent { 227 | Box::new(|vm| { 228 | vm.disable_memory_monitor(); 229 | writeln!(vm.console, "Disabled memory monitor").unwrap(); 230 | }) 231 | } 232 | 233 | fn get_names(&self) -> Vec<&str> { 234 | vec!["monitor", "mon"] 235 | } 236 | 237 | fn get_arg_info(&self) -> Option<&str> { 238 | Some("start end") 239 | } 240 | 241 | fn get_help(&self) -> &str { 242 | "Dumps the memory between and 243 | (inclusive) every seconds. Press ENTER to 244 | stop monitoring." 245 | } 246 | } 247 | 248 | struct MemsetCommand; 249 | impl Command for MemsetCommand { 250 | fn execute(&self, args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 251 | if args.len() < 2 { 252 | writeln!(vm.console, 253 | "Expected 2 arguments. E.g.: memset 0x00 0x01 stores 0x01 at address 0x00" 254 | ).unwrap(); 255 | return CommandResult::InvalidArgs; 256 | } 257 | 258 | let start = usize::from_str_radix(&args[0].replace("0x", "")[..], 16); 259 | if start.is_err() { 260 | writeln!(vm.console, "Expected hexadecimal memory address, found {}", args[0]).unwrap(); 261 | return CommandResult::InvalidArgs; 262 | } 263 | let start = start.unwrap(); 264 | 265 | let bytes = { 266 | let mut bytes = Vec::new(); 267 | for index in 1..args.len() { 268 | let byte = u8::from_str_radix(&args[index].replace("0x", "")[..], 16); 269 | if byte.is_err() { 270 | writeln!(vm.console, "Expected hexadecimal byte, found {}", args[index]).unwrap(); 271 | return CommandResult::InvalidArgs; 272 | } 273 | bytes.push(byte.unwrap()); 274 | } 275 | bytes 276 | }; 277 | 278 | for (index, byte) in bytes.iter().enumerate() { 279 | vm.cpu.memory[start + index] = *byte; 280 | } 281 | 282 | CommandResult::Sucess 283 | } 284 | 285 | fn get_names(&self) -> Vec<&str> { 286 | vec!["memset", "set"] 287 | } 288 | 289 | fn get_arg_info(&self) -> Option<&str> { 290 | Some("addres value [values, ...]") 291 | } 292 | 293 | fn get_help(&self) -> &str { 294 | "Writes the given value to the given address. 295 | Multiple values will be written to consequent 296 | addresses." 297 | } 298 | } 299 | 300 | struct MemdmpCommand; 301 | impl Command for MemdmpCommand { 302 | fn execute(&self, args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 303 | if args.is_empty() || args.len() > 2 { 304 | writeln!(vm.console, "Expected either 1 or 2 arguments, found {}", args.len()).unwrap(); 305 | return CommandResult::InvalidArgs; 306 | } 307 | 308 | // Dump a page 309 | if args.len() == 1 { 310 | let page = usize::from_str_radix(&args[0].replace("0x", "")[..], 16); 311 | if page.is_err() { 312 | writeln!(vm.console, "Expected page index, found {}", args[0]).unwrap(); 313 | return CommandResult::InvalidArgs; 314 | } 315 | let page = page.unwrap(); 316 | 317 | vm.dump_memory_page(page); 318 | 319 | // Dump a range 320 | } else if args.len() == 2 { 321 | let start = usize::from_str_radix(&args[0].replace("0x", "")[..], 16); 322 | if start.is_err() { 323 | writeln!(vm.console, "Expected hexadecimal memory address, found {}", args[0]).unwrap(); 324 | return CommandResult::InvalidArgs; 325 | } 326 | let start = start.unwrap(); 327 | 328 | let end = usize::from_str_radix(&args[1].replace("0x", "")[..], 16); 329 | if end.is_err() { 330 | writeln!(vm.console, "Expected hexadecimal memory address, found {}", args[1]).unwrap(); 331 | return CommandResult::InvalidArgs; 332 | } 333 | let end = end.unwrap(); 334 | 335 | vm.dump_memory_range(start, end); 336 | } 337 | 338 | CommandResult::Sucess 339 | } 340 | 341 | fn get_names(&self) -> Vec<&str> { 342 | vec!["memdmp", "dmp"] 343 | } 344 | 345 | fn get_arg_info(&self) -> Option<&str> { 346 | Some("page OR start end") 347 | } 348 | 349 | fn get_help(&self) -> &str { 350 | "Dumps a single memory page, or a specified memory 351 | range from to ." 352 | } 353 | } 354 | 355 | struct FlagsCommand; 356 | impl Command for FlagsCommand { 357 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 358 | vm.dump_flags(); 359 | CommandResult::Sucess 360 | } 361 | 362 | fn get_names(&self) -> Vec<&str> { 363 | vec!["flags"] 364 | } 365 | 366 | fn get_help(&self) -> &str { 367 | "Not yet implemented" 368 | } 369 | } 370 | 371 | struct BreakCommand; 372 | impl Command for BreakCommand { 373 | fn execute(&self, args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 374 | if args.len() > 1 { 375 | writeln!(vm.console, "Expected 0 or 1 arguments, found {}", args.len()).unwrap(); 376 | return CommandResult::InvalidArgs; 377 | } 378 | 379 | // Break at the given address 380 | if args.len() == 1 { 381 | let address = usize::from_str_radix(&args[0].replace("0x", "")[..], 16); 382 | if address.is_err() { 383 | writeln!(vm.console, "Expected hexadecimal memory address, found {}", args[0]).unwrap(); 384 | } 385 | let address = address.unwrap(); 386 | 387 | if address <= u16::max_value() as usize { 388 | if vm.toggle_breakpoint(address) { 389 | writeln!(vm.console, "Added breakpoint at {:04X}", address).unwrap(); 390 | } else { 391 | writeln!(vm.console, "Removed breakpoint at {:04X}", address).unwrap(); 392 | } 393 | } else { 394 | writeln!(vm.console, "Address outside addressable range.").unwrap(); 395 | } 396 | 397 | // Break at current program counter 398 | } else { 399 | vm.break_execution(); 400 | writeln!(vm.console, "Breaking execution at {:04X}", vm.cpu.registers.PC).unwrap(); 401 | } 402 | 403 | CommandResult::Sucess 404 | } 405 | 406 | fn get_names(&self) -> Vec<&str> { 407 | vec!["break", "b"] 408 | } 409 | 410 | fn get_arg_info(&self) -> Option<&str> { 411 | Some("[address]") 412 | } 413 | 414 | fn get_help(&self) -> &str { 415 | "Toggles a breakpoint at the specified

. 416 | If the program counter hits this address, execution 417 | stops. If no address is specified, execution will 418 | be stopped at the current point, without inserting 419 | a breakpoint." 420 | } 421 | } 422 | 423 | struct ContinueCommand; 424 | impl Command for ContinueCommand { 425 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 426 | vm.continue_execution(); 427 | CommandResult::Sucess 428 | } 429 | 430 | fn get_names(&self) -> Vec<&str> { 431 | vec!["continue", "c"] 432 | } 433 | 434 | fn get_help(&self) -> &str { 435 | "Resumes program execution after the program 436 | has stopped at a breakpoint." 437 | } 438 | } 439 | 440 | struct StepCommand; 441 | impl Command for StepCommand { 442 | fn execute(&self, _args: Vec, _system: &CommandSystem, vm: &mut VirtualMachine) -> CommandResult { 443 | vm.step_execution(); 444 | CommandResult::Sucess 445 | } 446 | 447 | fn get_names(&self) -> Vec<&str> { 448 | vec!["step", "s"] 449 | } 450 | 451 | fn get_help(&self) -> &str { 452 | "Executes a single instruction, then stops 453 | execution" 454 | } 455 | } 456 | 457 | struct ExitCommand; 458 | impl Command for ExitCommand { 459 | fn execute(&self, _args: Vec, _system: &CommandSystem, _vm: &mut VirtualMachine) -> CommandResult { 460 | std::process::exit(0); 461 | } 462 | 463 | fn get_names(&self) -> Vec<&str> { 464 | vec!["exit"] 465 | } 466 | 467 | fn get_help(&self) -> &str { 468 | "Quits the game" 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /vm/src/config.rs: -------------------------------------------------------------------------------- 1 | 2 | use sdl2::keyboard::Scancode; 3 | use rustc_serialize::json; 4 | use std::path::Path; 5 | use std::fs::File; 6 | use std::io::{Read, Write}; 7 | use std::io; 8 | 9 | const DEFAULT_CONSOLE_TOGGLE: Scancode = Scancode::Grave; 10 | 11 | #[derive(RustcDecodable, RustcEncodable, Debug)] 12 | pub struct Configuration { 13 | console_toggle: i32, 14 | } 15 | 16 | impl Configuration { 17 | pub fn default() -> Configuration { 18 | Configuration { 19 | console_toggle: DEFAULT_CONSOLE_TOGGLE as i32 20 | } 21 | } 22 | 23 | pub fn store(&self, target: &Path) -> Result<(), ConfigError> { 24 | let encoded = json::as_pretty_json(&self); 25 | let mut file = File::create(target)?; 26 | write!(file, "{}", encoded)?; 27 | writeln!(file, "")?; // End file with newline 28 | Ok(()) 29 | } 30 | 31 | pub fn load(target: &Path) -> Result { 32 | let mut file = File::open(target)?; 33 | let mut buffer = String::new(); 34 | file.read_to_string(&mut buffer)?; 35 | let config = json::decode(&buffer)?; 36 | Ok(config) 37 | } 38 | 39 | pub fn get_scancode(&self) -> Scancode { 40 | Scancode::from_i32(self.console_toggle).unwrap_or(DEFAULT_CONSOLE_TOGGLE) 41 | } 42 | } 43 | 44 | #[derive(Debug)] 45 | pub enum ConfigError { 46 | File(io::Error), 47 | Serialization(json::EncoderError), 48 | Deserialization(json::DecoderError), 49 | } 50 | 51 | impl From for ConfigError { 52 | fn from(e: io::Error) -> ConfigError { 53 | ConfigError::File(e) 54 | } 55 | } 56 | impl From for ConfigError { 57 | fn from(e: json::EncoderError) -> ConfigError { 58 | ConfigError::Serialization(e) 59 | } 60 | } 61 | impl From for ConfigError { 62 | fn from(e: json::DecoderError) -> ConfigError { 63 | ConfigError::Deserialization(e) 64 | } 65 | } 66 | 67 | 68 | -------------------------------------------------------------------------------- /vm/src/console.rs: -------------------------------------------------------------------------------- 1 | 2 | use std; 3 | use std::path::Path; 4 | use std::io::Write; 5 | 6 | use sdl2::event::Event; 7 | use sdl2::keyboard::*; 8 | use sdl2::pixels::{Color, PixelFormatEnum}; 9 | use sdl2::rect::{Point, Rect}; 10 | use sdl2::render::{BlendMode, Renderer, Texture, TextureQuery}; 11 | use sdl2::surface::Surface; 12 | use sdl2::ttf::{Font, Sdl2TtfContext, STYLE_BOLD}; 13 | 14 | use app_dirs::*; 15 | 16 | use position::Position; 17 | use text::Text; 18 | use config::{Configuration, ConfigError}; 19 | 20 | const APP_INFO: AppInfo = AppInfo { name: "hakka", author: "simon-whitehead" }; 21 | const CONFIG_FILE: &'static str = "config.json"; 22 | 23 | const BORDER_COLOR: Color = Color::RGBA(255, 255, 255, 64); 24 | 25 | const PADDING: i32 = 10; 26 | 27 | const FONT_COLOR: Color = Color::RGBA(45, 200, 45, 255); 28 | const FONT_SIZE: u16 = 18; 29 | 30 | pub struct Console<'a> { 31 | pub visible: bool, 32 | pub input_blocked: bool, 33 | visible_start_time: u32, /* Used to ensure that the KeyDown event that opens the console does not trigger text input */ 34 | 35 | config: Configuration, 36 | 37 | font_file: &'a str, 38 | leader: Text, 39 | input_buffer: String, 40 | last_command: String, 41 | command_history: Vec, 42 | history_position: usize, 43 | cursor_position: usize, 44 | buffer: Vec, 45 | backbuffer_y: i32, 46 | texture: Texture, 47 | ttf_context: &'a Sdl2TtfContext, 48 | size: (u32, u32), 49 | font: Font<'a, 'a>, 50 | } 51 | 52 | impl<'a> Console<'a> { 53 | /// Creates a new empty Console 54 | pub fn new(ttf_context: &'a Sdl2TtfContext, 55 | mut renderer: &mut Renderer, 56 | font_file: &'a str) 57 | -> Console<'a> { 58 | 59 | let (width, height) = renderer.window().unwrap().size(); 60 | let mut texture = 61 | renderer.create_texture_streaming(PixelFormatEnum::RGBA8888, width / 2, height) 62 | .unwrap(); 63 | texture.with_lock(None, |buffer: &mut [u8], pitch: usize| { 64 | for y in 0..height { 65 | for x in 0..width / 2 { 66 | let x = x as usize; 67 | let y = y as usize; 68 | let offset = y * pitch + x * 4; 69 | buffer[offset] = 182; 70 | buffer[offset + 1] = 0; 71 | buffer[offset + 2] = 0; 72 | buffer[offset + 3] = 0; 73 | } 74 | } 75 | }) 76 | .unwrap(); 77 | 78 | let mut font = ttf_context.load_font(Path::new(font_file), FONT_SIZE).unwrap(); 79 | font.set_style(STYLE_BOLD); 80 | 81 | let config_root = app_root(AppDataType::UserConfig, &APP_INFO).unwrap(); 82 | let config_file = { 83 | let mut config_file = config_root.clone(); 84 | config_file.push(CONFIG_FILE); 85 | config_file 86 | }; 87 | 88 | if !config_file.exists() { 89 | let default_config = Configuration::default(); 90 | default_config.store(&config_file).unwrap(); 91 | } 92 | 93 | let config = match Configuration::load(&config_file) { 94 | Err(err) => match err { 95 | ConfigError::Deserialization(err) => { 96 | // Something happend during deserialization, indicating that the file has invalid content 97 | println!("config.json could not be deserialized. Replacing with default ({:?})", err); 98 | let default_config = Configuration::default(); 99 | default_config.store(&config_file).unwrap(); 100 | default_config 101 | }, 102 | _ => { 103 | panic!("Unable to load the configuration file! {:?}", err); 104 | } 105 | }, 106 | Ok(config) => { 107 | config 108 | } 109 | }; 110 | 111 | Console { 112 | visible: false, 113 | visible_start_time: 0, 114 | 115 | config: config, 116 | 117 | font_file: font_file, 118 | leader: Text::new(ttf_context, 119 | &mut renderer, 120 | "hakka>", 121 | Position::XY(PADDING, height as i32 - FONT_SIZE as i32 - PADDING), 122 | FONT_SIZE, 123 | FONT_COLOR, 124 | font_file), 125 | input_buffer: "".into(), 126 | last_command: "".into(), 127 | command_history: Vec::new(), 128 | history_position: 0, 129 | cursor_position: 0, 130 | buffer: Vec::new(), 131 | backbuffer_y: 0, 132 | texture: texture, 133 | ttf_context: ttf_context, 134 | size: (width / 2, height), 135 | font: font, 136 | input_blocked: false, 137 | } 138 | } 139 | 140 | pub fn process(&mut self, event: &Event) { 141 | // Used to check if no modifiers are held when toggeling console 142 | let no_mods = |keymod: Mod| 143 | !keymod.intersects(LALTMOD | LCTRLMOD | LSHIFTMOD | RALTMOD | RCTRLMOD | RSHIFTMOD); 144 | 145 | if !self.visible { 146 | if let Event::KeyDown { scancode, keymod, timestamp, .. } = *event { 147 | if no_mods(keymod) && scancode == Some(self.config.get_scancode()) { 148 | self.toggle(timestamp); 149 | return; 150 | } 151 | } 152 | return; 153 | } 154 | 155 | // Main event processing, only run if visible 156 | match *event { 157 | Event::TextInput { ref text, timestamp, .. } => { 158 | if self.visible && timestamp > self.visible_start_time + 50 && !self.input_blocked { 159 | self.add_text(text); 160 | } 161 | } 162 | Event::MouseWheel { y, .. } => { 163 | if self.visible && 164 | self.buffer.len() * FONT_SIZE as usize > (self.size.1 - (FONT_SIZE as u32 * 2)) as usize { 165 | self.backbuffer_y += y * 6; 166 | if self.backbuffer_y < 0 { 167 | self.backbuffer_y = 0; 168 | } 169 | } 170 | } 171 | Event::KeyDown { keycode, scancode, timestamp, keymod, .. } => { 172 | if self.visible { 173 | if no_mods(keymod) && scancode == Some(self.config.get_scancode()) { 174 | self.toggle(timestamp); 175 | return; 176 | } else if !self.input_blocked { 177 | match keycode { 178 | Some(Keycode::C) => { 179 | if keymod.intersects(LCTRLMOD | RCTRLMOD) { 180 | self.input_buffer.push_str("^C"); 181 | self.commit(false); 182 | } 183 | } 184 | Some(Keycode::Left) => { 185 | self.cursor_left(); 186 | } 187 | Some(Keycode::Right) => { 188 | self.cursor_right(); 189 | } 190 | Some(Keycode::Backspace) => { 191 | self.backspace(); 192 | } 193 | Some(Keycode::Delete) => { 194 | if self.cursor_position < self.input_buffer.len() { 195 | self.cursor_position += 1; 196 | self.backspace(); 197 | } 198 | } 199 | _ => (), 200 | } 201 | } 202 | } 203 | } 204 | Event::KeyUp { keycode, timestamp, .. } => { 205 | if self.visible && !self.input_blocked { 206 | match keycode { 207 | Some(Keycode::Up) => { 208 | // Special check that an automatic console toggle 209 | // does not cause history navigation when holding the 210 | // up arrow. 211 | if self.visible_start_time > 0 { 212 | self.history_navigate_back(); 213 | } else { 214 | self.visible_start_time = timestamp; 215 | } 216 | } 217 | Some(Keycode::Down) => { 218 | // Special check that an automatic console toggle 219 | // does not cause history navigation when holding the 220 | // down arrow. 221 | if self.visible_start_time > 0 { 222 | self.history_navigate_forward(); 223 | } else { 224 | self.visible_start_time = timestamp; 225 | } 226 | } 227 | Some(Keycode::Return) => { 228 | self.commit(true); 229 | } 230 | Some(Keycode::End) => { 231 | self.cursor_position = self.input_buffer.len(); 232 | } 233 | Some(Keycode::Home) => { 234 | self.cursor_position = 0; 235 | } 236 | _ => (), 237 | } 238 | } 239 | } 240 | _ => (), 241 | } 242 | } 243 | 244 | pub fn process_command(&mut self) { 245 | let command = self.input_buffer.clone(); 246 | if !command.is_empty() { 247 | self.command_history.push(command.clone()); 248 | self.last_command = command.clone(); 249 | } 250 | } 251 | 252 | pub fn clear(&mut self) { 253 | self.buffer.clear(); 254 | } 255 | 256 | fn history_navigate_back(&mut self) { 257 | if self.history_position > 0 { 258 | self.input_buffer = self.command_history[self.history_position - 1].clone(); 259 | self.cursor_position = self.input_buffer.len(); 260 | 261 | if self.history_position > 0 { 262 | self.history_position -= 1; 263 | } 264 | } 265 | } 266 | 267 | fn history_navigate_forward(&mut self) { 268 | if !self.command_history.is_empty() && 269 | self.history_position < self.command_history.len() - 1 { 270 | self.input_buffer = self.command_history[self.history_position + 1].clone(); 271 | self.cursor_position = self.input_buffer.len(); 272 | if self.history_position < self.command_history.len() { 273 | self.history_position += 1; 274 | } 275 | } 276 | } 277 | 278 | pub fn get_next_command(&mut self) -> Option { 279 | if !self.last_command.is_empty() { 280 | let cmd = self.last_command.clone(); 281 | self.last_command.clear(); 282 | Some(cmd) 283 | } else { 284 | None 285 | } 286 | } 287 | 288 | /// Toggles the visibility of the Console 289 | pub fn toggle(&mut self, time: u32) { 290 | self.visible = !self.visible; 291 | if self.visible { 292 | self.visible_start_time = time; 293 | } 294 | } 295 | 296 | pub fn add_text(&mut self, input: &str) { 297 | self.input_buffer.insert(self.cursor_position, input.chars().next().unwrap()); 298 | self.cursor_position += input.len(); 299 | } 300 | 301 | pub fn commit(&mut self, execute: bool) { 302 | let command = self.input_buffer.clone(); 303 | writeln!(self, "hakka> {}", command).unwrap(); 304 | 305 | if execute { 306 | self.process_command(); 307 | } 308 | 309 | self.input_buffer.clear(); 310 | self.cursor_position = 0; 311 | self.history_position = self.command_history.len(); 312 | } 313 | 314 | pub fn cursor_left(&mut self) { 315 | if self.cursor_position > 0 { 316 | self.cursor_position -= 1; 317 | while !self.input_buffer.is_char_boundary(self.cursor_position) { 318 | self.cursor_position -= 1; 319 | } 320 | } 321 | } 322 | 323 | pub fn cursor_right(&mut self) { 324 | if self.cursor_position < self.input_buffer.len() { 325 | self.cursor_position += 1; 326 | while !self.input_buffer.is_char_boundary(self.cursor_position) { 327 | self.cursor_position += 1; 328 | } 329 | } 330 | } 331 | 332 | pub fn backspace(&mut self) { 333 | if self.visible && self.cursor_position > 0 { 334 | self.cursor_position -= 1; 335 | while !self.input_buffer.is_char_boundary(self.cursor_position) { 336 | self.cursor_position -= 1; 337 | } 338 | self.input_buffer.remove(self.cursor_position); 339 | } 340 | } 341 | 342 | /// Renders the Console 343 | pub fn render(&mut self, mut renderer: &mut Renderer) { 344 | if self.visible { 345 | 346 | renderer.set_blend_mode(BlendMode::Blend); 347 | self.texture.set_blend_mode(BlendMode::Blend); 348 | renderer.copy(&self.texture, 349 | None, 350 | Some(Rect::new(0, 0, self.size.0, self.size.1))) 351 | .unwrap(); 352 | self.generate_backbuffer_texture(&mut renderer); 353 | 354 | if !self.input_blocked { 355 | self.render_leader(&mut renderer); 356 | // Insert the cursor via a dodgy vertical line 357 | let cursor_x = 358 | 60 + PADDING as i16 + 359 | self.font.size_of(&self.input_buffer[..self.cursor_position]).unwrap().0 as i16; 360 | // Draw a dodgy cursor 361 | renderer.draw_line(Point::new(cursor_x as i32, 362 | self.size.1 as i32 - FONT_SIZE as i32 - PADDING as i32), 363 | Point::new(cursor_x as i32, 364 | self.size.1 as i32 - PADDING as i32)) 365 | .unwrap(); 366 | 367 | if !self.input_buffer.is_empty() { 368 | let text = Text::new(self.ttf_context, 369 | &mut renderer, 370 | &self.input_buffer[..], 371 | Position::XY(60 + PADDING, 372 | self.size.1 as i32 - FONT_SIZE as i32 - PADDING), 373 | FONT_SIZE, 374 | FONT_COLOR, 375 | self.font_file); 376 | text.render(&mut renderer); 377 | } 378 | } else { 379 | let text = Text::new(self.ttf_context, 380 | &mut renderer, 381 | "Press Ctrl+C or ENTER to cancel", 382 | Position::XY(PADDING, 383 | self.size.1 as i32 - FONT_SIZE as i32 - PADDING), 384 | FONT_SIZE, 385 | FONT_COLOR, 386 | self.font_file); 387 | text.render(&mut renderer); 388 | } 389 | 390 | self.render_border(&mut renderer); 391 | } 392 | } 393 | 394 | fn render_border(&self, mut renderer: &mut Renderer) { 395 | // Render the border 396 | renderer.set_draw_color(Color::RGBA(255, 255, 255, 255)); 397 | // North 398 | renderer.draw_line(Point::new(0, 0), Point::new(self.size.0 as i32, 0)).unwrap(); 399 | 400 | // East 401 | renderer.draw_line(Point::new(self.size.0 as i32, 402 | 0), 403 | Point::new(self.size.0 as i32, 404 | self.size.1 as i32)) 405 | .unwrap(); 406 | 407 | // South 408 | renderer.draw_line(Point::new(0, 409 | self.size.1 as i32 - 1), 410 | Point::new(self.size.0 as i32, 411 | self.size.1 as i32 - 1)) 412 | .unwrap(); 413 | } 414 | 415 | fn render_leader(&self, mut renderer: &mut Renderer) { 416 | // Render a black background behind it so the buffer scrolling looks 417 | // nicer. 418 | let rect_y = self.size.1 as i32 - FONT_SIZE as i32 - PADDING; 419 | renderer.set_draw_color(Color::RGBA(0, 0, 0, 255)); 420 | renderer.fill_rect(Rect::new(0, rect_y, self.size.0, rect_y as u32)).unwrap(); 421 | self.leader.render(&mut renderer); 422 | } 423 | 424 | fn generate_backbuffer_texture(&mut self, mut renderer: &mut Renderer) { 425 | let mut main_surface = Surface::new(self.size.0, 426 | (self.size.1 - (FONT_SIZE as u32)), 427 | PixelFormatEnum::RGBA8888) 428 | .unwrap(); 429 | let mut counter = 2; 430 | // TODO: Make the line render limit here configurable 431 | for (index, line) in self.buffer.iter().rev().take(200).enumerate() { 432 | // index 0 is the last line, b/c the iterator is reversed. writeln! 433 | // outputs a newline at the end of what is written, creating a new 434 | // string in the buffer, which we do not want to render 435 | if index == 0 && line.is_empty() { 436 | continue; 437 | } 438 | 439 | let y_pos = self.size.1 as i32 - (FONT_SIZE as i32 * counter) + self.backbuffer_y; 440 | counter += 1; 441 | 442 | if line.trim().is_empty() { 443 | continue; 444 | } 445 | 446 | let surface = self.font 447 | .render(line) 448 | .blended(FONT_COLOR) 449 | .unwrap(); 450 | surface.blit(None, 451 | &mut main_surface, 452 | Some(Rect::new(PADDING, y_pos - PADDING, self.size.1, FONT_SIZE as u32))) 453 | .unwrap(); 454 | } 455 | let texture = renderer.create_texture_from_surface(&main_surface) 456 | .unwrap(); 457 | 458 | let TextureQuery { width, height, .. } = texture.query(); 459 | 460 | renderer.copy(&texture, None, Some(Rect::new(0, 0, width, height))).unwrap(); 461 | } 462 | } 463 | 464 | impl<'a> Write for Console<'a> { 465 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 466 | if self.buffer.is_empty() { 467 | self.buffer.push(String::new()); 468 | } 469 | 470 | let mut text = String::from(std::str::from_utf8(buf).unwrap()); 471 | while !text.is_empty() { 472 | if let Some(index) = text.find('\n') { 473 | let substring: String = text.drain(..index+1).filter(|c| *c != '\n' && *c != '\r').collect(); 474 | 475 | if let Some(last) = self.buffer.last_mut() { 476 | last.push_str(&substring); 477 | } 478 | 479 | self.buffer.push(String::new()); 480 | } else { 481 | if let Some(last) = self.buffer.last_mut() { 482 | last.push_str(&text); 483 | } 484 | text.drain(..); 485 | } 486 | } 487 | 488 | Ok(buf.len()) 489 | } 490 | 491 | fn flush(&mut self) -> std::io::Result<()> { 492 | Ok(()) 493 | } 494 | } 495 | 496 | -------------------------------------------------------------------------------- /vm/src/game_core.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::io::Write; 3 | 4 | use vm::VirtualMachine; 5 | use command::{CommandSystem, UnblockEvent, CommandResult}; 6 | 7 | use sdl2::render::Renderer; 8 | use sdl2::ttf::Sdl2TtfContext; 9 | use sdl2::event::Event; 10 | use sdl2::keyboard::{Keycode, LCTRLMOD, RCTRLMOD}; 11 | 12 | use rs6502::Cpu; 13 | 14 | pub struct GameCore<'a> { 15 | pub vm: VirtualMachine<'a>, 16 | pub command_system: CommandSystem, 17 | unblock_event: Option, 18 | } 19 | 20 | impl<'a> GameCore<'a> { 21 | pub fn new(ttf_context: &'a Sdl2TtfContext, 22 | mut renderer: &mut Renderer, 23 | font_file: &'a str) 24 | -> GameCore<'a> 25 | { 26 | let cpu = Cpu::new(); 27 | let vm = VirtualMachine::new(cpu, 150, &ttf_context, &mut renderer, font_file); 28 | 29 | GameCore { 30 | vm: vm, 31 | command_system: CommandSystem::new(), 32 | unblock_event: None, 33 | } 34 | } 35 | 36 | pub fn process_event(&mut self, event: &Event) { 37 | match *event { 38 | // Stop a blocking event 39 | Event::KeyDown { keycode, keymod, .. } 40 | if (keycode == Some(Keycode::C) && keymod.intersects(LCTRLMOD | RCTRLMOD) || keycode == Some(Keycode::Return)) && 41 | self.unblock_event.is_some() => { 42 | if let Some(ref unblock_event) = self.unblock_event { 43 | unblock_event(&mut self.vm); 44 | } 45 | self.vm.console.input_blocked = false; 46 | self.unblock_event = None; 47 | }, 48 | // Let the console handle the event 49 | _ => self.vm.console.process(event) 50 | } 51 | } 52 | 53 | pub fn update(&mut self) { 54 | if let Some(cmd) = self.vm.console.get_next_command() { 55 | let (result, unblock_event) = self.command_system.execute(cmd, &mut self.vm); 56 | 57 | if let CommandResult::NotFound = result { 58 | writeln!(self.vm.console, "Command not recognized, type 'help' for a list of commands").unwrap(); 59 | } 60 | 61 | if unblock_event.is_some() { 62 | self.unblock_event = unblock_event; 63 | self.vm.console.input_blocked = true; 64 | } else { 65 | self.unblock_event = None; 66 | } 67 | } 68 | 69 | self.vm.cycle(); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /vm/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | extern crate find_folder; 3 | extern crate rs6502; 4 | extern crate rustc_serialize; 5 | extern crate app_dirs; 6 | extern crate sdl2; 7 | 8 | mod console; 9 | mod position; 10 | mod text; 11 | mod config; 12 | mod command; 13 | mod vm; 14 | mod game_core; 15 | 16 | pub use self::position::Position; 17 | pub use self::text::Text; 18 | pub use self::vm::VirtualMachine; 19 | pub use self::command::{CommandSystem, Command}; 20 | pub use self::game_core::GameCore; 21 | -------------------------------------------------------------------------------- /vm/src/position.rs: -------------------------------------------------------------------------------- 1 | 2 | pub enum Position { 3 | HorizontalCenter(i32, i32), 4 | XY(i32, i32), 5 | } -------------------------------------------------------------------------------- /vm/src/text.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use sdl2::pixels::Color; 4 | use sdl2::rect::Rect; 5 | use sdl2::render::{Renderer, Texture, TextureQuery}; 6 | use sdl2::ttf::{Sdl2TtfContext, STYLE_BOLD}; 7 | 8 | use position::Position; 9 | 10 | pub struct Text { 11 | x: i32, 12 | y: i32, 13 | width: u32, 14 | height: u32, 15 | texture: Texture, 16 | } 17 | 18 | impl Text { 19 | pub fn new(ttf_context: &Sdl2TtfContext, 20 | renderer: &mut Renderer, 21 | text: S, 22 | position: Position, 23 | font_size: u16, 24 | color: Color, 25 | path: P) 26 | -> Text 27 | where S: Into, 28 | P: AsRef 29 | { 30 | let mut font = ttf_context.load_font(path.as_ref(), font_size).unwrap(); 31 | font.set_style(STYLE_BOLD); 32 | let surface = font.render(&text.into()) 33 | .blended(color) 34 | .unwrap(); 35 | let texture = renderer.create_texture_from_surface(&surface) 36 | .unwrap(); 37 | 38 | let TextureQuery { width, height, .. } = texture.query(); 39 | 40 | let (x, y) = match position { 41 | Position::HorizontalCenter(x, y) => (x - width as i32 / 2, y), 42 | Position::XY(x, y) => (x, y), 43 | }; 44 | 45 | Text { 46 | x: x, 47 | y: y, 48 | width: width, 49 | height: height, 50 | texture: texture, 51 | } 52 | } 53 | 54 | pub fn render(&self, renderer: &mut Renderer) { 55 | renderer.copy(&self.texture, 56 | None, 57 | Some(Rect::new(self.x, self.y, self.width, self.height))) 58 | .unwrap(); 59 | } 60 | } -------------------------------------------------------------------------------- /vm/src/vm.rs: -------------------------------------------------------------------------------- 1 | 2 | use rs6502::{CodeSegment, Cpu, Disassembler}; 3 | use sdl2::render::Renderer; 4 | use sdl2::ttf::Sdl2TtfContext; 5 | use console::Console; 6 | use std::io::Write; 7 | use std::ops::Range; 8 | 9 | #[derive(Debug)] 10 | pub struct MemoryMonitor { 11 | pub enabled: bool, 12 | start_addr: usize, 13 | end_addr: usize, 14 | } 15 | 16 | pub struct VirtualMachine<'a> { 17 | pub cpu: Cpu, 18 | pub monitor: MemoryMonitor, 19 | pub console: Console<'a>, 20 | segments: Vec, 21 | clock_rate: Option, 22 | breakpoints: [u8; 64 * 1024], 23 | broken: bool, 24 | step: bool, 25 | } 26 | 27 | impl<'a> VirtualMachine<'a> { 28 | pub fn new(cpu: Cpu, 29 | clock_rate: CR, 30 | ttf_context: &'a Sdl2TtfContext, 31 | mut renderer: &mut Renderer, 32 | font_file: &'a str) 33 | -> VirtualMachine<'a> 34 | where CR: Into> 35 | { 36 | let mut console = Console::new(ttf_context, renderer, font_file); 37 | 38 | writeln!(console, "Welcome to hakka. Type 'help' for instructions").unwrap(); 39 | writeln!(console, "").unwrap(); 40 | 41 | VirtualMachine { 42 | cpu: cpu, 43 | console: console, 44 | segments: Vec::new(), 45 | clock_rate: clock_rate.into(), 46 | monitor: MemoryMonitor { 47 | enabled: false, 48 | start_addr: 0, 49 | end_addr: 0, 50 | }, 51 | breakpoints: [0; 64 * 1024], 52 | broken: false, 53 | step: false, 54 | } 55 | } 56 | 57 | pub fn render(&mut self, mut renderer: &mut Renderer) { 58 | self.console.render(renderer); 59 | } 60 | 61 | pub fn load_code_segments(&mut self, segments: Vec) { 62 | if segments.is_empty() { 63 | return; 64 | } 65 | self.segments = segments; 66 | for segment in &self.segments { 67 | self.cpu.load(&segment.code, segment.address).unwrap(); 68 | } 69 | 70 | self.cpu.registers.PC = self.segments[0].address; 71 | } 72 | 73 | /// Cycles the Virtual Machine CPU according to the clock rate 74 | pub fn cycle(&mut self) { 75 | if let Some(clock_rate) = self.clock_rate { 76 | let mut n = 0; 77 | while (n < clock_rate && !self.broken) || self.step { 78 | n += self.cpu.step().expect("SEGFAULT") as u32; 79 | if self.breakpoints[self.cpu.registers.PC as usize] > 0 { 80 | self.broken = true; 81 | writeln!(self.console, "").unwrap(); 82 | writeln!(self.console, "BREAKPOINT hit at {:04x}", self.cpu.registers.PC).unwrap(); 83 | // We are supposed to pass the current timestamp to prevent the keys which are 84 | // used to toggle the console from inputing text into the console. As no key 85 | // is pressed to open the console in this instance, passing the time is not 86 | // strictly necesarry 87 | self.console.toggle(0); 88 | } 89 | // If we stepped, dump the local disassembly 90 | if self.step { 91 | self.dump_local_disassembly(); 92 | } 93 | self.step = false; 94 | } 95 | } else { 96 | self.cpu.step().expect("SEGFAULT"); 97 | if self.step { 98 | self.dump_local_disassembly(); 99 | } 100 | self.step = false; 101 | if self.breakpoints[self.cpu.registers.PC as usize] > 0 { 102 | self.broken = true; 103 | writeln!(self.console, "").unwrap(); 104 | writeln!(self.console, "BREAKPOINT hit at {:04x}", self.cpu.registers.PC).unwrap(); 105 | self.console.toggle(0); 106 | } 107 | } 108 | } 109 | 110 | pub fn enable_memory_monitor(&mut self, range: Range) { 111 | self.monitor.start_addr = range.start; 112 | self.monitor.end_addr = range.end; 113 | self.monitor.enabled = true; 114 | } 115 | pub fn is_memory_monitor_enabled(&self) -> bool { 116 | self.monitor.enabled 117 | } 118 | pub fn disable_memory_monitor(&mut self) { 119 | self.monitor.enabled = false; 120 | } 121 | 122 | pub fn is_debugging(&self) -> bool { 123 | self.broken 124 | } 125 | 126 | pub fn break_execution(&mut self) { 127 | self.broken = true; 128 | } 129 | pub fn continue_execution(&mut self) { 130 | self.broken = false; 131 | } 132 | pub fn step_execution(&mut self) { 133 | self.broken = true; 134 | self.step = true; 135 | } 136 | pub fn toggle_breakpoint(&mut self, address: usize) -> bool { 137 | if self.breakpoints[address] > 0 { 138 | self.breakpoints[address] = 0; 139 | return false; 140 | } else { 141 | self.breakpoints[address] = 1; 142 | return true; 143 | } 144 | } 145 | 146 | pub fn dump_disassembly(&mut self) { 147 | writeln!(self.console, " ").unwrap(); 148 | 149 | for segment in &self.segments { 150 | writeln!(self.console, ".ORG ${:04X}", segment.address).unwrap(); 151 | let disassembler = Disassembler::with_offset(segment.address); 152 | let pairs = disassembler.disassemble_with_addresses(&segment.code); 153 | let lines = self.highlight_lines(self.cpu.registers.PC as usize, 154 | pairs, 155 | segment.address, 156 | false); 157 | for line in lines { 158 | write!(self.console, "{}", line).unwrap(); 159 | } 160 | } 161 | 162 | writeln!(self.console, " ").unwrap(); 163 | } 164 | 165 | pub fn dump_local_disassembly(&mut self) { 166 | writeln!(self.console, " ").unwrap(); 167 | 168 | let result = { 169 | let pc = self.cpu.registers.PC as usize; 170 | let local_segment = self.get_local_segment(pc); 171 | let disassembler = Disassembler::with_offset(local_segment.address); 172 | let pairs = disassembler.disassemble_with_addresses(&local_segment.code); 173 | self.highlight_lines(pc, pairs, local_segment.address, true) 174 | }; 175 | for line in result { 176 | write!(self.console, "{}", line).unwrap(); 177 | } 178 | writeln!(self.console, "").unwrap(); 179 | } 180 | 181 | pub fn dump_memory_page(&mut self, page: usize) { 182 | let mut addr = page * 0x100; 183 | for chunk in self.cpu.memory[page * 0x100..(page * 0x100) + 0x100].chunks(8) { 184 | write!(self.console, "{:04X}: ", addr).unwrap(); 185 | for b in chunk { 186 | write!(self.console, "{:02X} ", *b).unwrap(); 187 | } 188 | writeln!(self.console, "").unwrap(); 189 | addr += 0x08; 190 | } 191 | writeln!(self.console, "").unwrap(); 192 | } 193 | 194 | pub fn dump_memory(&mut self) { 195 | for chunk in self.cpu.memory[self.monitor.start_addr..self.monitor.end_addr + 0x01] 196 | .chunks(8) { 197 | for b in chunk { 198 | write!(self.console, "{:02X} ", *b).unwrap(); 199 | } 200 | writeln!(self.console, "").unwrap(); 201 | } 202 | } 203 | 204 | pub fn dump_memory_range(&mut self, start: usize, end: usize) { 205 | for chunk in self.cpu.memory[start..end + 0x01].chunks(8) { 206 | for b in chunk { 207 | write!(self.console, "{:02X} ", *b).unwrap(); 208 | } 209 | writeln!(self.console, "").unwrap(); 210 | } 211 | writeln!(self.console, "").unwrap(); 212 | } 213 | 214 | pub fn dump_registers(&mut self) { 215 | writeln!(self.console, " ").unwrap(); 216 | writeln!(self.console,"A: {} ({:04X})", self.cpu.registers.A, self.cpu.registers.A).unwrap(); 217 | writeln!(self.console, "X: {} ({:04X})", self.cpu.registers.X, self.cpu.registers.X).unwrap(); 218 | writeln!(self.console, "Y: {} ({:04X})", self.cpu.registers.Y, self.cpu.registers.Y).unwrap(); 219 | writeln!(self.console, "PC: {} ({:04X})", self.cpu.registers.PC, self.cpu.registers.PC).unwrap(); 220 | writeln!(self.console, "S: {} ({:04X})", self.cpu.stack.pointer, self.cpu.stack.pointer).unwrap(); 221 | writeln!(self.console, " ").unwrap(); 222 | } 223 | 224 | pub fn dump_flags(&mut self) { 225 | writeln!(self.console, " ").unwrap(); 226 | writeln!(self.console, "Carry: {}", self.cpu.flags.carry).unwrap(); 227 | writeln!(self.console, "Zero: {}", self.cpu.flags.zero).unwrap(); 228 | writeln!(self.console, "Interrupts disabled: {}", self.cpu.flags.interrupt_disabled).unwrap(); 229 | writeln!(self.console, "Decimal mode: {}", self.cpu.flags.decimal).unwrap(); 230 | writeln!(self.console, "Break: {}", self.cpu.flags.breakpoint).unwrap(); 231 | writeln!(self.console, "Overflow: {}", self.cpu.flags.overflow).unwrap(); 232 | writeln!(self.console, "Sign: {}", self.cpu.flags.sign).unwrap(); 233 | writeln!(self.console, "Unused: {}", self.cpu.flags.unused).unwrap(); 234 | writeln!(self.console, " ").unwrap(); 235 | } 236 | 237 | fn get_local_segment(&self, pc: usize) -> &CodeSegment { 238 | for segment in &self.segments { 239 | let addr = segment.address as usize; 240 | if pc >= addr && pc <= addr + segment.code.len() { 241 | return segment; 242 | } 243 | } 244 | 245 | &self.segments[0] 246 | } 247 | 248 | fn highlight_lines(&self, 249 | pc: usize, 250 | pairs: Vec<(String, u16)>, 251 | segment_start: u16, 252 | limit_results: bool) 253 | -> Vec { 254 | let mut result = Vec::new(); 255 | 256 | let base = pc as isize - segment_start as isize; 257 | 258 | for pair in pairs { 259 | if limit_results { 260 | let start = if base > 0x0A { base - 0x0A } else { 0 }; 261 | if (pair.1 as isize) < start || (pair.1 as isize) > base + 0x0A { 262 | continue; 263 | } 264 | } 265 | let current_line = pc as u16 == segment_start + pair.1; 266 | let breakpoint = self.breakpoints[segment_start as usize + pair.1 as usize] > 0x00; 267 | 268 | if breakpoint && current_line { 269 | result.push(format!("> * {}", pair.0)); 270 | } else if breakpoint && !current_line { 271 | result.push(format!(" * {}", pair.0)); 272 | } else if !breakpoint && current_line { 273 | result.push(format!("> {}", pair.0)); 274 | } else { 275 | result.push(format!(" {}", pair.0)); 276 | } 277 | } 278 | 279 | result 280 | } 281 | } 282 | --------------------------------------------------------------------------------