From bbc91949480221120a5bb3d415e52ebb241f8c99 Mon Sep 17 00:00:00 2001 From: Wenxuan Zhao Date: Sun, 1 Sep 2024 23:40:08 +0800 Subject: [PATCH] WIP: refactor, add markup parse and rendering, use cxx crate --- Cargo.lock | 17 +- Cargo.toml | 1 + src/config.rs | 9 +- src/cxxstring.rs | 293 ------------------- src/df/common.rs | 60 ++++ src/df/enabler.rs | 6 + src/df/enums.rs | 62 ++++ src/{enums.rs => df/flags.rs} | 0 src/df/game.rs | 63 ++++ src/df/graphic.rs | 45 +++ src/df/mod.rs | 12 + src/df/offsets.rs | 24 ++ src/df/renderer.rs | 20 ++ src/df/utils.rs | 15 + src/encodings/cjk.rs | 12 + src/{ => encodings}/cp437.rs | 0 src/encodings/mod.rs | 2 + src/font.rs | 26 +- src/global.rs | 28 ++ src/hooks.rs | 173 ++++++++--- src/lib.rs | 7 +- src/markup.rs | 530 ++++++++++++++++++++++++++++++++++ src/raw.rs | 11 - src/screen.rs | 203 ------------- src/screen/constants.rs | 2 + src/screen/data.rs | 29 ++ src/screen/mod.rs | 153 ++++++++++ src/screen/text.rs | 66 +++++ src/watchdog.rs | 1 + 29 files changed, 1290 insertions(+), 580 deletions(-) delete mode 100644 src/cxxstring.rs create mode 100644 src/df/common.rs create mode 100644 src/df/enabler.rs create mode 100644 src/df/enums.rs rename src/{enums.rs => df/flags.rs} (100%) create mode 100644 src/df/game.rs create mode 100644 src/df/graphic.rs create mode 100644 src/df/mod.rs create mode 100644 src/df/offsets.rs create mode 100644 src/df/renderer.rs create mode 100644 src/df/utils.rs create mode 100644 src/encodings/cjk.rs rename src/{ => encodings}/cp437.rs (100%) create mode 100644 src/encodings/mod.rs create mode 100644 src/markup.rs delete mode 100644 src/raw.rs delete mode 100644 src/screen.rs create mode 100644 src/screen/constants.rs create mode 100644 src/screen/data.rs create mode 100644 src/screen/mod.rs create mode 100644 src/screen/text.rs diff --git a/Cargo.lock b/Cargo.lock index 828f73c..fedcb1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,9 +194,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.94" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +checksum = "3c4eae4b7fc8dcb0032eb3b1beee46b38d371cdeaf2d0c64b9944f6f69ad7755" dependencies = [ "cc", "cxxbridge-flags", @@ -221,15 +221,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.94" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" +checksum = "719d6197dc016c88744aff3c0d0340a01ecce12e8939fc282e7c8f583ee64bc6" [[package]] name = "cxxbridge-macro" -version = "1.0.94" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +checksum = "35de3b547387863c8f82013c4f79f1c2162edee956383e4089e1d04c18c4f16c" dependencies = [ "proc-macro2", "quote", @@ -260,6 +260,7 @@ dependencies = [ "bitflags 2.6.0", "checksum", "chrono", + "cxx", "device_query", "dlopen2", "exe", @@ -451,9 +452,9 @@ dependencies = [ [[package]] name = "link-cplusplus" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" dependencies = [ "cc", ] diff --git a/Cargo.toml b/Cargo.toml index b584c24..34f916c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ device_query = "2.1.0" sdl2 = "0.37.0" fontdue = "0.9.2" bitflags = "2.6.0" +cxx = "1.0.126" [target.'cfg(target_os = "windows")'.dependencies] exe = "0.5.6" diff --git a/src/config.rs b/src/config.rs index 3ed81ad..1bc20c1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,10 +57,8 @@ pub struct OffsetsMetadata { #[derive(Deserialize)] pub struct OffsetsValues { pub enabler: Option, - pub enabler_offset_curses_glyph_texture: Option, + pub game: Option, pub gps: Option, - pub gps_offset_dimension: Option, - pub renderer_offset_screen_info: Option, pub addst: Option, pub addst_top: Option, pub addst_flag: Option, @@ -68,6 +66,10 @@ pub struct OffsetsValues { pub gps_allocate: Option, pub update_all: Option, pub update_tile: Option, + pub get_key_display: Option, + pub mtb_process_string_to_lines: Option, + pub mtb_set_width: Option, + pub render_help_dialog: Option, } #[allow(dead_code)] @@ -81,6 +83,7 @@ pub struct SymbolsValues { pub gps_allocate: Option>, pub update_all: Option>, pub update_tile: Option>, + pub get_key_display: Option>, } impl Config { diff --git a/src/cxxstring.rs b/src/cxxstring.rs deleted file mode 100644 index eadb3c5..0000000 --- a/src/cxxstring.rs +++ /dev/null @@ -1,293 +0,0 @@ -use std::alloc::{alloc_zeroed, realloc, Layout}; -use std::ops::{Index, IndexMut}; - -#[cfg(target_os = "linux")] -#[repr(C)] -pub struct CxxString { - pub ptr: *mut u8, - pub len: usize, - pub sso: CxxSSO, -} - -#[cfg(target_os = "linux")] -#[repr(C)] -pub union CxxSSO { - pub capa: usize, - pub buf: [u8; 16], -} - -#[cfg(target_os = "linux")] -impl CxxString { - pub unsafe fn new(ptr: *mut T, size: usize) -> Self { - if size >= 16 { - return Self { - ptr: ptr as *mut u8, - len: size, - sso: CxxSSO { capa: size }, - }; - } - let array_ptr: *const [u8; 16] = ptr as *const [u8; 16]; - Self { - ptr: ptr as *mut u8, - len: size, - sso: CxxSSO { - buf: std::mem::transmute(*array_ptr), - }, - } - } - - // TODO: maybe wrong, not tested - pub unsafe fn resize(&mut self, size: usize) { - if size > self.len { - match (size >= 16, self.len) { - (true, v) if v < 16 => { - let new_array = alloc_zeroed(Layout::array::(32).unwrap()); - std::ptr::copy_nonoverlapping(self.sso.buf.as_ptr(), new_array, 16); - self.ptr = new_array; - self.sso.capa = 32; - } - (true, v) if v >= 16 && size > self.sso.capa => { - self.ptr = realloc( - self.ptr, - Layout::array::(self.sso.capa).unwrap(), - self.sso.capa + 16, - ); - self.sso.capa += 16; - } - (_, _) => (), - } - } else { - match (size >= 16, self.len) { - (true, _) => { - let target = self.ptr as usize + size; - let slice = std::slice::from_raw_parts_mut(target as *mut u8, 1); - slice[0] = 0; - } - (false, v) if v >= 16 => { - std::ptr::copy_nonoverlapping(self.ptr, self.sso.buf.as_mut_ptr(), 16); - } - (_, _) => {} - } - } - self.len = size; - } - - pub unsafe fn from_ptr(ptr: *const u8) -> &'static mut Self { - std::mem::transmute(ptr) - } - - pub unsafe fn as_ptr(&mut self) -> *const u8 { - std::mem::transmute(self) - } - - pub unsafe fn as_mut_ptr(&mut self) -> *mut u8 { - std::mem::transmute(self) - } - - pub unsafe fn to_str(&mut self) -> Result<&'static str, Box> { - match std::ffi::CStr::from_bytes_with_nul(std::slice::from_raw_parts(self.ptr, self.len + 1)) { - Ok(value) => match value.to_str() { - Ok(value) => Ok(value), - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } - - pub unsafe fn to_bytes_without_nul(&mut self) -> &[u8] { - std::slice::from_raw_parts(self.ptr, self.len) - } - - pub fn size(&self) -> usize { - self.len - } - - pub unsafe fn pop_back(&mut self) { - let index = self.len; - self[index] = 0; - self.resize(self.len - 1); - } - - pub unsafe fn push_back(&mut self, symbol: u8) { - let index = self.len; - self.resize(index + 1); - self[index] = symbol; - } -} - -#[cfg(target_os = "linux")] -impl Index for CxxString { - type Output = u8; - - fn index(&self, index: usize) -> &Self::Output { - unsafe { - let mut data: *const u8 = self.sso.buf.as_ptr(); - if self.len >= 16 { - data = self.ptr; - } - let target = data as usize + index; - let slice = std::slice::from_raw_parts(target as *const u8, 1); - &slice[0] - } - } -} - -#[cfg(target_os = "linux")] -impl IndexMut for CxxString { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - unsafe { - let mut data: *mut u8 = self.sso.buf.as_mut_ptr(); - if self.len >= 16 { - data = self.ptr; - } - let target = data as usize + index; - let slice = std::slice::from_raw_parts_mut(target as *mut u8, 1); - &mut slice[0] - } - } -} - -#[cfg(target_os = "windows")] -#[repr(C)] -pub struct CxxString { - pub data: CxxStringContent, - pub len: usize, - pub capa: usize, -} - -#[cfg(target_os = "windows")] -#[repr(C)] -pub union CxxStringContent { - pub buf: [u8; 16], - pub ptr: *mut u8, -} - -#[cfg(target_os = "windows")] -impl CxxString { - pub unsafe fn new(ptr: *mut T, size: usize) -> Self { - if size >= 16 { - return Self { - data: CxxStringContent { ptr: ptr as *mut u8 }, - len: size, - capa: size, - }; - } - let array_ptr: *const [u8; 16] = ptr as *const [u8; 16]; - Self { - data: CxxStringContent { - buf: std::mem::transmute(*array_ptr), - }, - len: size, - capa: 15, - } - } - - pub unsafe fn resize(&mut self, size: usize) { - if size > self.len { - match (size >= 16, self.capa) { - (true, v) if v == 15 => { - let new_array = alloc_zeroed(Layout::array::(32).unwrap()); - std::ptr::copy_nonoverlapping(self.data.buf.as_ptr(), new_array, 16); - self.data.ptr = new_array; - self.capa = 32; - } - (true, v) if v < size => { - self.data.ptr = realloc(self.data.ptr, Layout::array::(self.capa).unwrap(), self.capa + 16); - self.capa += 16; - } - (_, _) => (), - } - } else { - if self.capa >= 16 { - let target = self.data.ptr as usize + size; - let slice = std::slice::from_raw_parts_mut(target as *mut u8, 1); - slice[0] = 0; - } else { - self.data.buf[size] = 0; - } - } - self.len = size; - } - - pub unsafe fn from_ptr(ptr: *const u8) -> &'static mut Self { - std::mem::transmute(ptr) - } - - pub unsafe fn to_str(&mut self) -> Result<&'static str, Box> { - let mut data: *const u8 = self.data.buf.as_ptr(); - if self.capa >= 16 { - data = self.data.ptr; - } - match std::ffi::CStr::from_bytes_with_nul(std::slice::from_raw_parts(data, self.len + 1)) { - Ok(value) => match value.to_str() { - Ok(value) => Ok(value), - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } - - pub unsafe fn to_bytes_without_nul(&mut self) -> &[u8] { - let mut data: *const u8 = self.data.buf.as_ptr(); - if self.capa >= 16 { - data = self.data.ptr; - } - std::slice::from_raw_parts(data, self.len) - } - - pub unsafe fn as_ptr(&mut self) -> *const u8 { - std::mem::transmute(self) - } - - pub unsafe fn as_mut_ptr(&mut self) -> *mut u8 { - std::mem::transmute(self) - } - - pub fn size(&self) -> usize { - self.len - } - - pub unsafe fn pop_back(&mut self) { - let index = self.len; - self[index] = 0; - self.resize(self.len - 1); - } - - pub unsafe fn push_back(&mut self, symbol: u8) { - let index = self.len; - self.resize(index + 1); - self[index] = symbol; - } -} - -#[cfg(target_os = "windows")] -impl Index for CxxString { - type Output = u8; - - fn index(&self, index: usize) -> &Self::Output { - unsafe { - let mut data: *const u8 = self.data.buf.as_ptr(); - if self.capa >= 16 { - data = self.data.ptr; - } - let target = data as usize + index; - let slice = std::slice::from_raw_parts(target as *const u8, 1); - &slice[0] - } - } -} - -#[cfg(target_os = "windows")] -impl IndexMut for CxxString { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - unsafe { - let mut data: *mut u8 = self.data.buf.as_mut_ptr(); - if self.capa >= 16 { - data = self.data.ptr; - } - let target = data as usize + index; - let slice = std::slice::from_raw_parts_mut(target as *mut u8, 1); - &mut slice[0] - } - } -} diff --git a/src/df/common.rs b/src/df/common.rs new file mode 100644 index 0000000..69a0694 --- /dev/null +++ b/src/df/common.rs @@ -0,0 +1,60 @@ +use super::utils; + +#[repr(C)] +#[derive(Debug, Default)] +pub struct Coord { + pub x: T, + pub y: T, +} + +impl Coord { + pub fn at(addr: usize) -> Self { + utils::deref(addr) + } +} + +pub type Dimension = Coord; + +#[repr(C)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, +} + +impl Color { + pub fn at(addr: usize) -> Self { + utils::deref(addr) + } + + pub fn rgb(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b } + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct Vector { + pub begin: usize, + pub end: usize, + pub capacity: usize, +} + +impl Vector { + pub fn first_address(&self) -> Option { + if self.begin == 0 || self.begin == self.end { + None + } else { + Some(unsafe { *(self.begin as *const usize) }) + } + } + + pub fn first(&self) -> Option<&'static T> { + self.first_address().map(|addr| unsafe { &*(addr as *const T) }) + } + + pub fn first_mut(&self) -> Option<&'static mut T> { + self.first_address().map(|addr| unsafe { &mut *(addr as *mut T) }) + } +} diff --git a/src/df/enabler.rs b/src/df/enabler.rs new file mode 100644 index 0000000..0e4f7c4 --- /dev/null +++ b/src/df/enabler.rs @@ -0,0 +1,6 @@ +use super::{offsets, utils}; + +pub fn deref_curses_surface(addr: usize, code: u8) -> usize { + let texture_base: usize = utils::deref(addr + offsets::ENABLER_TEXTURES); + utils::deref(texture_base + (code as usize * 8)) +} diff --git a/src/df/enums.rs b/src/df/enums.rs new file mode 100644 index 0000000..4502f09 --- /dev/null +++ b/src/df/enums.rs @@ -0,0 +1,62 @@ +#[allow(dead_code)] +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +pub enum CursesColor { + Black = 0x0, + Blue = 0x1, + Green = 0x2, + Aqua = 0x3, + Red = 0x4, + Purple = 0x5, + Yellow = 0x6, + White = 0x7, + Gray = 0x8, + LightBlue = 0x9, + LightGreen = 0xa, + LightAqua = 0xb, + LightRed = 0xc, + LightPurple = 0xd, + LightYellow = 0xe, + BrightWhite = 0xf, +} + +impl From for CursesColor { + fn from(value: i32) -> Self { + unsafe { std::mem::transmute::(value & 0xf) } + } +} + +impl CursesColor { + pub fn light(self) -> Self { + ((self as i32) & 0x7 | 0x8).into() + } + pub fn dark(self) -> Self { + ((self as i32) & 0x7).into() + } + pub fn with_bright(self, bright: bool) -> Self { + if bright { + self.light() + } else { + self.dark() + } + } +} + +#[allow(dead_code, non_camel_case_types)] +#[repr(i32)] +#[derive(Debug, Clone, Copy)] +pub enum LinkType { + NONE = -1, + HIST_FIG = 0, + SITE = 1, + ARTIFACT = 2, + BOOK = 3, + SUBREGION = 4, + FEATURE_LAYER = 5, + ENTITY = 6, + ABSTRACT_BUILDING = 7, + ENTITY_POPULATION = 8, + ART_IMAGE = 9, + ERA = 10, + HEC = 11, +} diff --git a/src/enums.rs b/src/df/flags.rs similarity index 100% rename from src/enums.rs rename to src/df/flags.rs diff --git a/src/df/game.rs b/src/df/game.rs new file mode 100644 index 0000000..4290805 --- /dev/null +++ b/src/df/game.rs @@ -0,0 +1,63 @@ +use super::{common, offsets}; + +#[derive(Debug)] +#[repr(C)] +pub struct MarkupTextWord { + pub str: [u8; 32], + pub red: u8, + pub green: u8, + pub blue: u8, + pub link_index: i32, + pub px: i32, + pub py: i32, + pub flags: u32, +} + +#[derive(Debug)] +#[repr(C)] +pub struct MarkupTextBox { + pub word: common::Vector, + pub link: common::Vector, + pub current_width: i32, + pub max_y: i32, + pub environment: usize, +} + +impl MarkupTextBox { + pub fn at_mut(addr: usize) -> &'static mut Self { + unsafe { &mut *(addr as *mut Self) } + } + + pub fn ptr(&self) -> usize { + self as *const MarkupTextBox as usize + } +} + +#[derive(Debug)] +#[repr(C)] +pub struct GameMainInterfaceHelp { + pub open: bool, + pub flag: u32, + pub context_flag: u32, + pub context: u32, + pub header: [u8; 32], + pub text: [MarkupTextBox; 20], +} + +impl GameMainInterfaceHelp { + pub fn borrow_at(addr: usize) -> &'static Self { + unsafe { &*((addr) as *const GameMainInterfaceHelp) } + } + + pub fn borrow_from(addr: usize) -> &'static Self { + Self::borrow_at(addr + offsets::GAME_MAIN_INTERFACE_HELP) + } + + pub fn borrow_mut_at(addr: usize) -> &'static mut Self { + unsafe { &mut *((addr) as *mut GameMainInterfaceHelp) } + } + + pub fn borrow_mut_from(addr: usize) -> &'static mut Self { + Self::borrow_mut_at(addr + offsets::GAME_MAIN_INTERFACE_HELP) + } +} diff --git a/src/df/graphic.rs b/src/df/graphic.rs new file mode 100644 index 0000000..009b4a8 --- /dev/null +++ b/src/df/graphic.rs @@ -0,0 +1,45 @@ +use super::{common, enums, offsets, utils}; + +pub fn deref_coord(addr: usize) -> common::Coord { + common::Coord::at(addr + offsets::GRAPHIC_SCREENX) +} + +pub fn deref_dim(addr: usize) -> common::Dimension { + common::Dimension::at(addr + offsets::GRAPHIC_DIMX) +} + +#[derive(Debug)] +#[repr(C)] +pub struct ColorInfo { + pub screenf: u8, + pub screenb: u8, + pub screenbright: bool, + pub use_old_16_colors: bool, + pub screen_color_r: u8, + pub screen_color_g: u8, + pub screen_color_b: u8, +} + +pub fn deref_color(addr: usize) -> common::Color { + let ColorInfo { + use_old_16_colors, + screenf, + screenbright, + screen_color_r: r, + screen_color_g: g, + screen_color_b: b, + .. + } = utils::deref(addr + offsets::GRAPHIC_SCREENF); + + if use_old_16_colors { + let fg = (screenf + if screenbright { 8 } else { 0 }) as usize; + let uccolor_base = addr + offsets::GRAPHIC_SCREENF + offsets::GRAPHIC_SCREENF_UCCOLOR; + common::Color::at(uccolor_base + fg * 3) + } else { + common::Color::rgb(r, g, b) + } +} + +pub fn get_uccolor(addr: usize, color: enums::CursesColor) -> common::Color { + common::Color::at(addr + offsets::GRAPHIC_UCCOLOR + 3 * color as usize) +} diff --git a/src/df/mod.rs b/src/df/mod.rs new file mode 100644 index 0000000..b227078 --- /dev/null +++ b/src/df/mod.rs @@ -0,0 +1,12 @@ +pub mod enums; +pub mod flags; +pub mod offsets; + +pub mod utils; + +pub mod common; + +pub mod enabler; +pub mod game; +pub mod graphic; +pub mod renderer; diff --git a/src/df/offsets.rs b/src/df/offsets.rs new file mode 100644 index 0000000..102516f --- /dev/null +++ b/src/df/offsets.rs @@ -0,0 +1,24 @@ +#[cfg(target_os = "linux")] +pub const ENABLER_TEXTURES: usize = 0x388; +#[cfg(target_os = "windows")] +pub const ENABLER_TEXTURES: usize = 0x348; + +#[cfg(target_os = "linux")] +pub const GAME_MAIN_INTERFACE_HELP: usize = 0x5d40; +#[cfg(target_os = "windows")] +pub const GAME_MAIN_INTERFACE_HELP: usize = 0x5a70; + +pub const GRAPHIC_SCREENX: usize = 0x84; +pub const GRAPHIC_SCREENF: usize = 0x8c; +pub const GRAPHIC_SCREENF_UCCOLOR: usize = 0xcc; // TODO: remove this, use GRAPHIC_UCCOLOR instead +pub const GRAPHIC_UCCOLOR: usize = 0x158; +#[cfg(target_os = "linux")] +pub const GRAPHIC_DIMX: usize = 0xa00; +#[cfg(target_os = "windows")] +pub const GRAPHIC_DIMX: usize = 0x6cc; + +pub const RENDERER_SDL_RENDERER: usize = 0x108; +#[cfg(target_os = "linux")] +pub const RENDERER_DISPX_Z: usize = 0x160; +#[cfg(target_os = "windows")] +pub const RENDERER_DISPX_Z: usize = 0x168; diff --git a/src/df/renderer.rs b/src/df/renderer.rs new file mode 100644 index 0000000..8f1ac90 --- /dev/null +++ b/src/df/renderer.rs @@ -0,0 +1,20 @@ +use sdl2::sys as sdl; + +use super::{offsets, utils}; + +#[derive(Debug)] +#[repr(C)] +pub struct ScreenInfo { + pub dispx_z: i32, + pub dispy_z: i32, + pub origin_x: i32, + pub origin_y: i32, +} + +pub fn deref_screen_info(addr: usize) -> ScreenInfo { + utils::deref(addr + offsets::RENDERER_DISPX_Z) +} + +pub fn deref_sdl_renderer(addr: usize) -> *mut sdl::SDL_Renderer { + utils::deref(addr + offsets::RENDERER_SDL_RENDERER) +} diff --git a/src/df/utils.rs b/src/df/utils.rs new file mode 100644 index 0000000..3219c92 --- /dev/null +++ b/src/df/utils.rs @@ -0,0 +1,15 @@ +use cxx::CxxString; + +use crate::encodings; + +pub fn deref(addr: usize) -> T { + // TODO: avoid copy + unsafe { (addr as *const T).read() } +} + +pub fn deref_string(addr: usize) -> String { + let bytes = unsafe { std::mem::transmute::(addr).as_bytes() }; + let result: Vec = + bytes.into_iter().flat_map(|&byte| encodings::cp437::CP437_TO_UTF8_BYTES[byte as usize].to_owned()).collect(); + String::from_utf8_lossy(&result).into_owned() +} diff --git a/src/encodings/cjk.rs b/src/encodings/cjk.rs new file mode 100644 index 0000000..f87f328 --- /dev/null +++ b/src/encodings/cjk.rs @@ -0,0 +1,12 @@ +use super::cp437; + +pub fn is_cjk(ch: char) -> bool { + !cp437::UTF8_CHAR_TO_CP437.contains_key(&ch) +} + +pub fn is_cjk_punctuation(ch: char) -> bool { + match ch { + '。' | ',' | '?' | '!' => true, + _ => false, + } +} diff --git a/src/cp437.rs b/src/encodings/cp437.rs similarity index 100% rename from src/cp437.rs rename to src/encodings/cp437.rs diff --git a/src/encodings/mod.rs b/src/encodings/mod.rs new file mode 100644 index 0000000..12d648c --- /dev/null +++ b/src/encodings/mod.rs @@ -0,0 +1,2 @@ +pub mod cjk; +pub mod cp437; diff --git a/src/font.rs b/src/font.rs index 5e9ee50..78aabd5 100644 --- a/src/font.rs +++ b/src/font.rs @@ -5,9 +5,8 @@ use std::io::Read; use std::{mem, ptr}; use crate::config::CONFIG; -use crate::cp437::UTF8_CHAR_TO_CP437; use crate::global::ENABLER; -use crate::{raw, utils}; +use crate::{df, encodings, utils}; pub const CURSES_FONT_WIDTH: u32 = 16; pub const CJK_FONT_SIZE: u32 = 24; @@ -39,13 +38,9 @@ impl Font { } } - fn get(&mut self, ch: char) -> (usize, bool) { - let enabler = ENABLER.to_owned(); - let curses_surface_base = - raw::deref::(enabler + CONFIG.offset.as_ref().unwrap().enabler_offset_curses_glyph_texture.unwrap()); - - if let Some(&code) = UTF8_CHAR_TO_CP437.get(&ch) { - return (raw::deref::(curses_surface_base + (code as usize * 8)), true); + pub fn get(&mut self, ch: char) -> (usize, bool) { + if let Some(&code) = encodings::cp437::UTF8_CHAR_TO_CP437.get(&ch) { + return (df::enabler::deref_curses_surface(ENABLER.to_owned(), code), true); }; if !self.cache.contains_key(&ch) { @@ -54,7 +49,7 @@ impl Font { let mut surface = Surface::new(CJK_FONT_SIZE, CJK_FONT_SIZE, PixelFormatEnum::RGBA32).unwrap(); surface.with_lock_mut(|buffer| { let dx = metrics.xmin; - let dy = (CJK_FONT_SIZE as i32 - metrics.height as i32) - (metrics.ymin + 3); // Note: only for the "NotoSansMonoCJKsc-Bold" font + let dy = (CJK_FONT_SIZE as i32 - metrics.height as i32) - (metrics.ymin + 4); // Note: only for the "NotoSansMonoCJKsc-Bold" font let dy = if dy < 0 { 0 } else { dy }; for y in 0..metrics.height { for x in 0..metrics.width { @@ -78,11 +73,10 @@ impl Font { return (surface_ptr.to_owned(), false); } else { // fallback to curses space glyph - return (raw::deref::(curses_surface_base + (32 * 8)), true); + return (df::enabler::deref_curses_surface(ENABLER.to_owned(), ' ' as u8), true); } } - // FIXME: also returns the real width pub fn render(&mut self, string: String) -> (usize, u32) { let width = CJK_FONT_SIZE * string.chars().count() as u32; let height = CJK_FONT_SIZE; @@ -112,3 +106,11 @@ impl Font { fontdue::Font::from_bytes(data, fontdue::FontSettings::default()).map_err(|err| anyhow::anyhow!(err)) } } + +pub fn get_width(ch: char) -> u32 { + if encodings::cjk::is_cjk(ch) { + CJK_FONT_SIZE + } else { + CURSES_FONT_WIDTH + } +} diff --git a/src/global.rs b/src/global.rs index 0e38423..4f089e6 100644 --- a/src/global.rs +++ b/src/global.rs @@ -1,3 +1,4 @@ +// TODO: move to df::globals use crate::config::CONFIG; use crate::utils; @@ -22,6 +23,14 @@ pub static ENABLER: usize = { } }; +#[static_init::dynamic] +pub static GAME: usize = { + match CONFIG.offset.is_some() { + true => utils::address(CONFIG.offset.as_ref().unwrap().game.unwrap()), + false => 0 as usize, + } +}; + #[cfg(target_os = "linux")] #[static_init::dynamic] pub static GPS: usize = unsafe { @@ -39,3 +48,22 @@ pub static GPS: usize = { false => 0 as usize, } }; + +#[allow(non_upper_case_globals)] +#[cfg(target_os = "linux")] +#[static_init::dynamic] +pub static get_key_display: fn(usize, usize, i32) = unsafe { + match CONFIG.symbol.is_some() { + true => { + let symbol = CONFIG.symbol.as_ref().unwrap().get_key_display.as_ref().unwrap(); + utils::symbol_handle::(&symbol[0], &symbol[1]) + } + false => move |_, _, _| {}, + } +}; + +#[allow(non_upper_case_globals)] +#[cfg(target_os = "windows")] +pub fn get_key_display(str_ptr: usize, enabler: usize, binding: i32) { + log::warn!("FIXME: get_key_display(0x{str_ptr:x}, 0x{enabler:x}, {binding}) doesn't work"); +} diff --git a/src/hooks.rs b/src/hooks.rs index b8ec0c3..eb19034 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -1,13 +1,13 @@ use anyhow::Result; +use cxx::let_cxx_string; use retour::static_detour; use crate::config::CONFIG; -use crate::cxxstring::CxxString; use crate::dictionary::DICTIONARY; -use crate::enums::ScreenTexPosFlag; -use crate::global::GPS; -use crate::screen::{SCREEN, SCREEN_TOP}; -use crate::{raw, utils}; +use crate::global::{GAME, GPS}; +use crate::markup::MARKUP; +use crate::screen; +use crate::{df, utils}; use r#macro::hook; @@ -19,6 +19,9 @@ pub unsafe fn attach_all() -> Result<()> { attach_gps_allocate()?; attach_update_all()?; attach_update_tile()?; + attach_mtb_process_string_to_lines()?; + attach_mtb_set_width()?; + attach_render_help_dialog()?; Ok(()) } @@ -31,6 +34,9 @@ pub unsafe fn enable_all() -> Result<()> { enable_gps_allocate()?; enable_update_all()?; enable_update_tile()?; + // always enable mtb_process_string_to_lines: + enable_mtb_set_width()?; + enable_render_help_dialog()?; Ok(()) } @@ -43,28 +49,15 @@ pub unsafe fn disable_all() -> Result<()> { disable_gps_allocate()?; disable_update_all()?; disable_update_tile()?; + // always enable mtb_process_string_to_lines: + disable_mtb_set_width()?; + disable_render_help_dialog()?; Ok(()) } -fn gps_get_screen_coord(addr: usize) -> (i32, i32) { - ( - raw::deref::(addr + 0x84), // gps.screenx - raw::deref::(addr + 0x88), // gps.screeny - ) -} - -fn dummy_content(width: usize) -> CxxString { - let mut dummy: Vec = Vec::new(); - dummy.resize(width + 1, 32); - dummy[width] = 0; - let (ptr, len, _) = dummy.into_raw_parts(); - unsafe { CxxString::new(ptr, len - 1) } -} - -// FIXME: render the font, get real width, divided by 2, ceil it to curses font width fn translate(string: usize) -> String { - let mut content = raw::deref_string(string); + let mut content = df::utils::deref_string(string); if let Some(translated) = DICTIONARY.get(&content) { content = translated.to_owned(); } @@ -74,37 +67,61 @@ fn translate(string: usize) -> String { #[cfg_attr(target_os = "linux", hook(by_symbol))] #[cfg_attr(target_os = "windows", hook(by_offset))] fn addst(gps: usize, string: usize, just: u8, space: i32) { - let (x, y) = gps_get_screen_coord(gps); let content = translate(string); - let width = SCREEN.write().add(gps, x, y, content, 0); - unsafe { original!(gps, dummy_content(width).as_ptr() as usize, just, space) }; + let text = screen::Text::new(content).by_graphic(gps); + let width = screen::SCREEN.write().add_text(text); + + let_cxx_string!(dummy = " ".repeat(width)); + let dummy_ptr: usize = unsafe { core::mem::transmute(dummy) }; + unsafe { original!(gps, dummy_ptr, just, space) }; } #[cfg_attr(target_os = "linux", hook(by_symbol))] #[cfg_attr(target_os = "windows", hook(by_offset))] fn addst_top(gps: usize, string: usize, just: u8, space: i32) { - let (x, y) = gps_get_screen_coord(gps); let content = translate(string); - let width = SCREEN_TOP.write().add(gps, x, y, content, 0); - unsafe { original!(gps, dummy_content(width).as_ptr() as usize, just, space) }; + // in order to get the correct coord for help markup text, + // we need to render it here and skip the content from original text. + let help = df::game::GameMainInterfaceHelp::borrow_from(GAME.to_owned()); + for text in &help.text { + if let Some(word) = text.word.first_address() { + // if we're rendering a help text - rendering its first word + if string == word.to_owned() { + MARKUP.write().render(gps, text.ptr()); + return; + } + } + } + + let text = screen::Text::new(content).by_graphic(gps); + let width = screen::SCREEN_TOP.write().add_text(text); + + let_cxx_string!(dummy = " ".repeat(width)); + let dummy_ptr: usize = unsafe { core::mem::transmute(dummy) }; + unsafe { original!(gps, dummy_ptr, just, space) }; } #[cfg_attr(target_os = "linux", hook(by_symbol))] #[cfg_attr(target_os = "windows", hook(by_offset))] fn addst_flag(gps: usize, string: usize, just: u8, space: i32, sflag: u32) { - let (x, y) = gps_get_screen_coord(gps); let content = translate(string); - let width = SCREEN.write().add(gps, x, y, content, sflag); - unsafe { original!(gps, dummy_content(width).as_ptr() as usize, just, space, sflag) }; + let text = screen::Text::new(content).by_graphic(gps).with_sflag(sflag); + let width = screen::SCREEN.write().add_text(text); + + let_cxx_string!(dummy = " ".repeat(width)); + let dummy_ptr: usize = unsafe { core::mem::transmute(dummy) }; + unsafe { original!(gps, dummy_ptr, just, space, sflag) }; } #[cfg_attr(target_os = "linux", hook(bypass))] #[cfg_attr(target_os = "windows", hook(by_offset))] fn addchar_flag(gps: usize, c: u8, advance: i8, sflag: u32) { - if ScreenTexPosFlag::from_bits_retain(sflag).contains(ScreenTexPosFlag::TOP_OF_TEXT) { + // skip top-half character for Windows + let flag = df::flags::ScreenTexPosFlag::from_bits_retain(sflag); + if flag.contains(df::flags::ScreenTexPosFlag::TOP_OF_TEXT) { return; } @@ -113,37 +130,101 @@ fn addchar_flag(gps: usize, c: u8, advance: i8, sflag: u32) { #[cfg_attr(target_os = "linux", hook(by_symbol))] #[cfg_attr(target_os = "windows", hook(by_offset))] -fn gps_allocate(renderer: usize, w: u32, h: u32, screen_x: u32, screen_y: u32, tile_dim_x: u32, tile_dim_y: u32) { - unsafe { original!(renderer, w, h, screen_x, screen_y, tile_dim_x, tile_dim_y) }; - SCREEN.write().resize(w, h); - SCREEN_TOP.write().resize(w, h); +fn gps_allocate(renderer: usize, x: i32, y: i32, screen_x: u32, screen_y: u32, tile_dim_x: u32, tile_dim_y: u32) { + // graphicst::resize is inlined in Windows, hook gps_allocate instead + unsafe { original!(renderer, x, y, screen_x, screen_y, tile_dim_x, tile_dim_y) }; + screen::SCREEN.write().resize(x, y); + screen::SCREEN_TOP.write().resize(x, y); } #[cfg_attr(target_os = "linux", hook(by_symbol))] #[cfg_attr(target_os = "windows", hook(by_offset))] fn update_all(renderer: usize) { unsafe { original!(renderer) }; - SCREEN_TOP.write().render(renderer); - SCREEN.write().clear(); - SCREEN_TOP.write().clear(); -} + screen::SCREEN_TOP.write().render(renderer); -struct Dimension { - x: i32, - y: i32, + // always clear the buffer layer after a full-render + screen::SCREEN.write().clear(); + screen::SCREEN_TOP.write().clear(); } #[cfg_attr(target_os = "linux", hook(by_symbol))] #[cfg_attr(target_os = "windows", hook(by_offset))] fn update_tile(renderer: usize, x: i32, y: i32) { unsafe { original!(renderer, x, y) }; - let dim = raw::deref::(GPS.to_owned() + CONFIG.offset.as_ref().unwrap().gps_offset_dimension.unwrap()); + let dim = df::graphic::deref_dim(GPS.to_owned()); // hack to render text after the last update_tile in update_all - // TODO: consider re-write update_all function completely according to g_src if (x != dim.x - 1 || y != dim.y - 1) { return; } - SCREEN.write().render(renderer); + screen::SCREEN.write().render(renderer); +} + +#[cfg_attr(target_os = "linux", hook(by_offset))] +#[cfg_attr(target_os = "windows", hook(by_offset))] +fn mtb_process_string_to_lines(text: usize, src: usize) { + let content = translate(src); + + unsafe { original!(text, src) }; + + // TODO: may need regexp for some scenarios like world generation status (0x22fa459) + // TODO: log unknown text (during world generation) + // examples: (they are coming from "data/vanilla/vanilla_buildings/objects/building_custom.txt") + // * 0x7ffda475bbb8 Use tallow (rendered fat) or oil here with lye to make soap. 24 + // * 0x7ffda4663918 A useful workshop for pressing liquids from various sources. Some plants might need to be milled first before they can be used. Empty jugs are required to store the liquid products. 24 + + MARKUP.write().add(text, &content); +} + +#[cfg_attr(target_os = "linux", hook(by_offset))] +#[cfg_attr(target_os = "windows", hook(by_offset))] +fn mtb_set_width(text_address: usize, current_width: i32) { + let max_y = MARKUP.write().layout(text_address, current_width); + + // skip original function for help texts + let help = df::game::GameMainInterfaceHelp::borrow_mut_from(GAME.to_owned()); + for text in &mut help.text { + // if we're rendering a help text + if text as *const df::game::MarkupTextBox as usize == text_address { + // adjust the px and py to 0 (was -1 before original function call), + // this helps the screen coord is correct for addst_top. + if let Some(word) = text.word.first_mut::() { + word.px = 0; + word.py = 0; + } + + // set to 0 to ensure mtb_set_width is called in every loop + text.current_width = 0; + // use the max_y from markup layout + text.max_y = max_y; + + return; + } + } + + unsafe { original!(text_address, current_width) }; +} + +#[cfg_attr(target_os = "linux", hook(by_offset))] +#[cfg_attr(target_os = "windows", hook(by_offset))] +fn render_help_dialog(help_address: usize) { + let help = df::game::GameMainInterfaceHelp::borrow_mut_at(help_address); + + // save end offset of word vector of each text, + // and leave only one word in the vector to get screen coord for addst_top. + let mut stored_end = [0; 20]; + for (i, text) in &mut help.text.iter_mut().enumerate() { + stored_end[i] = text.word.end; + text.word.end = text.word.begin + 8; + } + + unsafe { original!(help_address) }; + + // restore saved end offset of word vector of each text, + // so the translation can be disabled at any point. + for (i, text) in &mut help.text.iter_mut().enumerate() { + text.word.end = stored_end[i]; + } } diff --git a/src/lib.rs b/src/lib.rs index e6bb46c..6b6d930 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,14 +6,13 @@ extern crate toml; mod config; mod constants; -mod cp437; -mod cxxstring; +mod df; mod dictionary; -mod enums; +mod encodings; mod font; mod global; mod hooks; -mod raw; +mod markup; mod screen; mod utils; mod watchdog; diff --git a/src/markup.rs b/src/markup.rs new file mode 100644 index 0000000..9175a21 --- /dev/null +++ b/src/markup.rs @@ -0,0 +1,530 @@ +use bitflags::bitflags; +use cxx::let_cxx_string; +use std::collections::HashMap; + +use crate::{ + df, encodings, font, + global::{get_key_display, ENABLER, GPS}, + screen, +}; + +#[static_init::dynamic] +pub static mut MARKUP: Markup = Default::default(); + +// TODO: remove this? as it's parsed by original code already +#[allow(dead_code)] +#[derive(Debug)] +struct MarkupLink { + typ: df::enums::LinkType, + id: i32, + subid: i32, +} + +impl MarkupLink { + fn new(typ: df::enums::LinkType, id: i32, subid: i32) -> Self { + Self { typ, id, subid } + } +} + +bitflags! { + #[derive(Debug, Default)] + pub struct MarkupWordFlag: u32 { + const NEWLINE = 0b0001; + const BLANK_LINE = 0b0010; + const INDENT = 0b0100; + } +} + +#[derive(Debug, Default)] +struct MarkupWord { + str: String, + color: df::common::Color, + link_index: i32, + x: i32, + y: i32, // TODO: change this to pixels + flags: MarkupWordFlag, +} + +#[derive(Debug, Default)] +struct MarkupTextBox { + word: Vec, + link: Vec, + current_width: i32, + max_y: i32, + environment: usize, // pointer, not implemented +} + +impl MarkupTextBox { + // See DFHack: library/modules/Gui.cpp - void Gui::MTB_parse(df::markup_text_boxst *mtb, string parse_text) + pub fn parse(content: &String) -> Self { + let mut text: MarkupTextBox = Default::default(); + + let chars = content.chars().collect::>(); + + let mut str = String::new(); + let mut link_index: i32 = -1; + let mut color = df::enums::CursesColor::White; + let mut use_char; + let mut no_split_space; + + let i_max = chars.len(); + let mut i = 0; + while i < i_max { + let mut char_token = '\0'; + use_char = true; + no_split_space = false; + + if chars[i] == ']' { + // Skip over ']' + i += 1; + if i >= i_max { + break; + } + + if chars[i] != ']' { + // Check this char again from top + i -= 1; + continue; + } + // else "]]", append ']' to str since use_char == true + } else if chars[i] == '[' { + // Skip over '[' + i += 1; + if i >= i_max { + break; + } + + if chars[i] == '.' || chars[i] == ':' || chars[i] == '?' || chars[i] == ' ' || chars[i] == '!' { + // Immediately after '[' + + // Completely pointless for everything but ' '? + no_split_space = true; + } else if chars[i] != '[' { + use_char = false; + let token_buffer = Self::grab_token_string_pos(&chars, i, ':'); + i += token_buffer.chars().count(); + + match token_buffer.as_str() { + "CHAR" => { + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff = Self::grab_token_string_pos(&chars, i, ':'); + let buff_chars = buff.chars().collect::>(); + i += buff_chars.len(); + + char_token = if buff_chars.len() > 1 && buff_chars[0] == '~' { + buff_chars[1] + } else { + char::from_u32(buff.parse::().unwrap_or(0)).unwrap_or('\0') + }; + no_split_space = true; + use_char = true; + } + "LPAGE" => { + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff_type = Self::grab_token_string_pos(&chars, i, ':'); + i += buff_type.len(); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff_id = Self::grab_token_string_pos(&chars, i, ':'); + i += buff_id.len(); + + let link_type = match buff_type.as_str() { + "HF" => df::enums::LinkType::HIST_FIG, + "SITE" => df::enums::LinkType::SITE, + "ARTIFACT" => df::enums::LinkType::ARTIFACT, + "BOOK" => df::enums::LinkType::BOOK, + "SR" => df::enums::LinkType::SUBREGION, + "LF" => df::enums::LinkType::FEATURE_LAYER, + "ENT" => df::enums::LinkType::ENTITY, + "AB" => df::enums::LinkType::ABSTRACT_BUILDING, + "EPOP" => df::enums::LinkType::ENTITY_POPULATION, + "ART_IMAGE" => df::enums::LinkType::ART_IMAGE, + "ERA" => df::enums::LinkType::ERA, + "HEC" => df::enums::LinkType::HEC, + _ => df::enums::LinkType::NONE, + }; + + let id = buff_id.parse::().unwrap_or(0); + let mut subid = -1; + + match link_type { + df::enums::LinkType::ABSTRACT_BUILDING | df::enums::LinkType::ART_IMAGE => { + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff_subid = Self::grab_token_string_pos(&chars, i, ':'); + i += buff_subid.len(); + + subid = buff_subid.parse::().unwrap_or(0); + } + _ => {} + } + + match link_type { + df::enums::LinkType::NONE => {} + _ => { + let link = MarkupLink::new(link_type, id, subid); + text.link.push(link); + link_index = text.link.len() as i32 - 1; + } + } + } + "/LPAGE" => { + text.insert(&mut str, link_index, color); + link_index = -1; + } + "C" => { + text.insert(&mut str, link_index, color); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff1 = Self::grab_token_string_pos(&chars, i, ':'); + i += buff1.len(); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff2 = Self::grab_token_string_pos(&chars, i, ':'); + i += buff2.len(); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff3 = Self::grab_token_string_pos(&chars, i, ':'); + i += buff3.len(); + + let mut local_screenf = 7; + let mut local_screenbright = true; + + if buff1 == "VAR" { + let environment = if text.environment != 0 { "Active" } else { "NULL" }; + log::debug!("MTB_parse received:\n[C:VAR:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}", buff2, buff3, environment); + //MTB_set_color_on_var(mtb, buff2, buff3); + } else { + // skip setting colors in GPS, use local variables for colors + local_screenf = buff1.parse::().unwrap_or(7); + local_screenbright = buff3.parse::().unwrap_or(1) != 0; + } + + color = local_screenf.into(); + color = color.with_bright(local_screenbright); + } + "KEY" => { + text.insert(&mut str, link_index, color); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff = Self::grab_token_string_pos(&chars, i, ':'); + i += buff.len(); + + let mut ptr: MarkupWord = Default::default(); + let binding = buff.parse::().unwrap_or(0); + + unsafe { + let_cxx_string!(key = ""); + let key_ptr: usize = core::mem::transmute(key); + get_key_display(key_ptr, ENABLER.to_owned(), binding); + ptr.str = df::utils::deref_string(key_ptr); + }; + ptr.color = df::graphic::get_uccolor(GPS.to_owned(), df::enums::CursesColor::LightGreen); + + text.word.push(ptr); + } + "VAR" => { + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff_format = Self::grab_token_string_pos(&chars, i, ':'); + i += buff_format.len(); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff_var_type = Self::grab_token_string_pos(&chars, i, ':'); + i += buff_var_type.len(); + + // Skip over ':' + i += 1; + if i >= i_max { + break; + } + + let buff_var_name = Self::grab_token_string_pos(&chars, i, ':'); + i += buff_var_name.len(); + + let environment = if text.environment != 0 { "Active" } else { "NULL" }; + log::debug!("MTB_parse received:\n[VAR:{}:{}:{}]\nwhich is for dipscripts and is unimplemented.\nThe dipscript environment itself is: {}\n", buff_format, buff_var_type, buff_var_name, environment); + } + "R" | "B" | "P" => { + text.insert(&mut str, link_index, color); + + let mut ptr: MarkupWord = Default::default(); + + ptr.flags |= match token_buffer.as_str() { + "R" => MarkupWordFlag::NEWLINE, + "B" => MarkupWordFlag::BLANK_LINE, + _ => MarkupWordFlag::INDENT, + }; + + text.word.push(ptr); + } + _ => {} + } + } + } + + if use_char { + let ch = if char_token == '\0' { chars[i] } else { char_token }; + + // flush if the next character is CJK character + if encodings::cjk::is_cjk(ch) && !encodings::cjk::is_cjk_punctuation(ch) { + text.insert(&mut str, link_index, color); + } + + if ch != ' ' || no_split_space { + // flush the previous string if last character is CJK character + if str.len() > 0 { + let last_ch = str.chars().last().unwrap(); + if encodings::cjk::is_cjk(last_ch) && !encodings::cjk::is_cjk_punctuation(ch) { + text.insert(&mut str, link_index, color); + } + } + + str.push(ch); + } else { + text.insert(&mut str, link_index, color); + } + } + + i += 1; + } + + text.insert(&mut str, link_index, color); + + let mut i = text.word.len(); + while i > 1 { + i -= 1; + let (left, right) = text.word.split_at_mut(i); + + let cur_entry = &right[0]; + if cur_entry.link_index != -1 || cur_entry.str.is_empty() { + continue; + } + + let prev_entry = &mut left[i - 1]; + if prev_entry.link_index == -1 || prev_entry.str.is_empty() { + continue; + } + + match cur_entry.str.chars().next().unwrap() { + '.' | ',' | '?' | '!' | '。' | ',' | '?' | '!' => { + prev_entry.str.push_str(&cur_entry.str); + text.word.remove(i); + } + _ => {} + } + } + + text + } + + // See DFHack: library/modules/Gui.cpp - void Gui::MTB_set_width(df::markup_text_boxst *mtb, int32_t width) + pub fn set_width(&mut self, width: i32) { + if self.current_width == width { + return; + } + + self.max_y = 0; + self.current_width = width; + + let width_in_pixels = width * screen::CANVAS_FONT_WIDTH; + let mut remain_width = width_in_pixels; + let mut x_val = 0; + let mut y_val = 0; + + let mut iter = self.word.iter_mut().peekable(); + while let Some(cur_word) = iter.next() { + if cur_word.flags.contains(MarkupWordFlag::NEWLINE) { + remain_width = 0; + continue; + } + + if cur_word.flags.contains(MarkupWordFlag::BLANK_LINE) { + remain_width = 0; + x_val = 0; + y_val += screen::CANVAS_FONT_HEIGHT; + continue; + } + + if cur_word.flags.contains(MarkupWordFlag::INDENT) { + remain_width = width_in_pixels; + x_val = 4 * screen::CANVAS_FONT_WIDTH; + y_val += screen::CANVAS_FONT_HEIGHT; + continue; + } + + let word_width = cur_word.str.chars().map(|ch| font::get_width(ch) as i32).sum(); + if remain_width < word_width { + remain_width = width_in_pixels; + x_val = 0; + y_val += screen::CANVAS_FONT_HEIGHT; + } + + if let Some(next_word) = iter.peek() { + if next_word.str.chars().count() == 1 { + let next_char = next_word.str.chars().next().unwrap(); + if x_val > 0 && remain_width <= (font::get_width(next_char) as i32 + screen::CANVAS_FONT_WIDTH) { + match next_char { + '.' | ',' | '?' | '!' => { + remain_width = width_in_pixels; + x_val = 0; + y_val += screen::CANVAS_FONT_HEIGHT; + } + _ => {} + } + } + } + } + + if cur_word.str.chars().count() == 1 && x_val > 0 { + let cur_char = cur_word.str.chars().next().unwrap(); + match cur_char { + '.' | ',' | '?' | '!' => { + cur_word.x = x_val - screen::CANVAS_FONT_WIDTH; + cur_word.y = y_val; + + if self.max_y < y_val { + self.max_y = y_val; + } + + remain_width -= screen::CANVAS_FONT_WIDTH; + x_val += screen::CANVAS_FONT_WIDTH; + continue; + } + _ => {} + } + } + + cur_word.x = x_val; + cur_word.y = y_val; + + if self.max_y < y_val { + self.max_y = y_val; + } + + remain_width -= word_width + screen::CANVAS_FONT_WIDTH; + x_val += word_width + screen::CANVAS_FONT_WIDTH; + + if let Some(next_word) = iter.peek() { + if cur_word.str.chars().count() > 0 && next_word.str.chars().count() > 0 { + let cur_last_char = cur_word.str.chars().last().unwrap(); + let next_first_char = next_word.str.chars().next().unwrap(); + if encodings::cjk::is_cjk(cur_last_char) && encodings::cjk::is_cjk(next_first_char) { + remain_width += screen::CANVAS_FONT_WIDTH; + x_val -= screen::CANVAS_FONT_WIDTH; + } + } + } + } + } + + fn grab_token_string_pos(source: &Vec, pos: usize, compc: char) -> String { + let mut out = String::new(); + + for i in pos..source.len() { + if source[i] == compc || source[i] == ']' { + break; + } + out.push(source[i]); + } + + out + } + + fn insert(&mut self, str: &mut String, link_index: i32, color: df::enums::CursesColor) -> bool { + if str.is_empty() { + return false; + } + + let mut ptr: MarkupWord = Default::default(); + ptr.str = str.clone(); + ptr.link_index = link_index; + + ptr.color = df::graphic::get_uccolor(GPS.to_owned(), color); + + self.word.push(ptr); + str.clear(); + + return true; + } +} + +#[derive(Default)] +pub struct Markup { + items: HashMap, +} + +impl Markup { + pub fn add(&mut self, address: usize, content: &String) { + let text = MarkupTextBox::parse(content); + + self.items.insert(address, text); + } + + pub fn layout(&mut self, address: usize, current_width: i32) -> i32 { + if let Some(text) = self.items.get_mut(&address) { + text.set_width(current_width); + + return (text.max_y as f32 / screen::CANVAS_FONT_HEIGHT as f32).ceil() as i32; + } + + -1 + } + + pub fn render(&self, gps: usize, address: usize) { + if let Some(text) = self.items.get(&address) { + for word in &text.word { + let text = screen::Text::new(word.str.clone()).by_graphic(gps); + screen::SCREEN_TOP.write().add_text(text.with_offset(word.x, word.y).with_color(word.color.clone())); + } + } + } +} diff --git a/src/raw.rs b/src/raw.rs deleted file mode 100644 index a646582..0000000 --- a/src/raw.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::{cp437::CP437_TO_UTF8_BYTES, cxxstring::CxxString}; - -pub fn deref(addr: usize) -> T { - unsafe { (addr as *const T).read() } -} - -pub fn deref_string(addr: usize) -> String { - let bytes = unsafe { CxxString::from_ptr(addr as *const u8).to_bytes_without_nul() }; - let result: Vec = bytes.into_iter().flat_map(|&byte| CP437_TO_UTF8_BYTES[byte as usize].to_owned()).collect(); - String::from_utf8_lossy(&result).into_owned() -} diff --git a/src/screen.rs b/src/screen.rs deleted file mode 100644 index fbc769b..0000000 --- a/src/screen.rs +++ /dev/null @@ -1,203 +0,0 @@ -use std::{collections::HashMap, mem, ptr}; - -use sdl2::{pixels::PixelFormatEnum, rect::Rect, surface::Surface, sys as sdl}; - -use crate::{ - config::CONFIG, - enums::ScreenTexPosFlag, - font::{CJK_FONT_SIZE, FONT}, - raw, -}; - -const CANVAS_FONT_WIDTH: i32 = 8 * 2; -const CANVAS_FONT_HEIGHT: i32 = 12 * 2; - -#[static_init::dynamic] -pub static mut SCREEN: Screen = Screen::new(); - -#[static_init::dynamic] -pub static mut SCREEN_TOP: Screen = Screen::new(); - -#[repr(C)] -struct ScreenInfo { - pub dispx_z: i32, - pub dispy_z: i32, - pub origin_x: i32, - pub origin_y: i32, -} - -#[derive(Debug)] -#[repr(C)] -struct ColorInfo { - pub screenf: u8, - pub screenb: u8, - pub screenbright: bool, - pub use_old_16_colors: bool, - pub screen_color_r: u8, - pub screen_color_g: u8, - pub screen_color_b: u8, - pub screen_color_br: u8, - pub screen_color_bg: u8, - pub screen_color_bb: u8, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct ColorTuple { - pub r: u8, - pub g: u8, - pub b: u8, - pub br: u8, - pub bg: u8, - pub bb: u8, -} - -#[derive(Default)] -pub struct Screen { - dimension: (u32, u32), - canvas_ptr: usize, - // cache: (content, color) => surface_ptr - prev: HashMap<(String, ColorTuple), (usize, u32)>, - next: HashMap<(String, ColorTuple), (usize, u32)>, -} - -impl Screen { - pub fn new() -> Self { - Self { - dimension: Default::default(), - canvas_ptr: Default::default(), - prev: Default::default(), - next: Default::default(), - } - } - - pub fn resize(&mut self, w: u32, h: u32) { - self.dimension.0 = w; - self.dimension.1 = h; - - if self.canvas_ptr != 0 { - let canvas = unsafe { Surface::from_ll(self.canvas_ptr as *mut sdl::SDL_Surface) }; - mem::drop(canvas); - } - - let canvas = Surface::new( - w * CANVAS_FONT_WIDTH as u32, - h * CANVAS_FONT_HEIGHT as u32, - PixelFormatEnum::RGBA32, - ) - .unwrap(); - self.canvas_ptr = canvas.raw() as usize; - mem::forget(canvas); - } - - pub fn add(&mut self, gps: usize, x: i32, y: i32, content: String, sflag: u32) -> usize { - let color_base = gps + 0x8c; // TODO: check Windows offset - let color = raw::deref::(color_base); - let color = if color.use_old_16_colors { - let fg = (color.screenf + if color.screenbright { 8 } else { 0 }) as usize; - let bg = color.screenb as usize; - let uccolor_base = color_base + 0xcc; - ColorTuple { - r: raw::deref::(uccolor_base + fg * 3 + 0), - g: raw::deref::(uccolor_base + fg * 3 + 1), - b: raw::deref::(uccolor_base + fg * 3 + 2), - br: raw::deref::(uccolor_base + bg * 3 + 0), - bg: raw::deref::(uccolor_base + bg * 3 + 1), - bb: raw::deref::(uccolor_base + bg * 3 + 2), - } - } else { - ColorTuple { - r: color.screen_color_r, - g: color.screen_color_g, - b: color.screen_color_b, - br: color.screen_color_br, - bg: color.screen_color_bg, - bb: color.screen_color_bb, - } - }; - - // early return: we only renders the bottom half by shift up by half font height - if ScreenTexPosFlag::from_bits_retain(sflag).contains(ScreenTexPosFlag::TOP_OF_TEXT) { - // TODO: may need to return actual width? - return 0; - } - - // render text or get from cache - let key = (content.clone(), color); - let (surface_ptr, width) = match self.prev.get(&key) { - Some((ptr, width)) => (ptr.to_owned() as *mut sdl::SDL_Surface, width.to_owned()), - None => { - let mut font = FONT.write(); - let (ptr, width) = font.render(content); - let ptr = ptr as *mut sdl::SDL_Surface; - mem::drop(font); - - unsafe { sdl::SDL_SetSurfaceColorMod(ptr, color.r, color.g, color.b) }; - - (ptr, width) - } - }; - self.next.insert(key, (surface_ptr as usize, width)); - - // calculate render offset - let x = CANVAS_FONT_WIDTH * x; - let mut y: i32 = CANVAS_FONT_HEIGHT as i32 * y; - if ScreenTexPosFlag::from_bits_retain(sflag).contains(ScreenTexPosFlag::BOTTOM_OF_TEXT) { - // shift up by half font height for bottom half - y -= CANVAS_FONT_HEIGHT / 2; - } - - // render on canvas - unsafe { - let mut rect = Rect::new(x, y, width, CJK_FONT_SIZE); - let canvas = self.canvas_ptr as *mut sdl::SDL_Surface; - sdl::SDL_UpperBlit(surface_ptr, ptr::null(), canvas, rect.raw_mut()); - }; - - (width as f32 / CANVAS_FONT_WIDTH as f32).ceil() as usize - } - - pub fn clear(&mut self) { - unsafe { - let canvas = self.canvas_ptr as *mut sdl::SDL_Surface; - sdl::SDL_FillRect(canvas, ptr::null(), 0); - } - - for (k, (ptr, _)) in self.prev.iter() { - if !self.next.contains_key(k) { - unsafe { sdl::SDL_FreeSurface(ptr.to_owned() as *mut sdl::SDL_Surface) }; - } - } - - self.prev = mem::take(&mut self.next); - } - - pub fn render(&mut self, renderer: usize) { - if self.canvas_ptr == 0 { - return; - } - - let canvas = self.canvas_ptr as *mut sdl::SDL_Surface; - let screen = - raw::deref::(renderer + CONFIG.offset.as_ref().unwrap().renderer_offset_screen_info.unwrap()); - - unsafe { - let sdl_renderer = raw::deref(renderer + 0x108); - let texture = sdl::SDL_CreateTextureFromSurface(sdl_renderer, canvas); - sdl::SDL_SetTextureScaleMode(texture, sdl::SDL_ScaleMode::SDL_ScaleModeLinear); - let srcrect = Rect::new( - 0, - 0, - self.dimension.0 * CANVAS_FONT_WIDTH as u32, - self.dimension.1 * CANVAS_FONT_HEIGHT as u32, - ); - let dstrect = Rect::new( - screen.origin_x as i32, - screen.origin_y as i32, - self.dimension.0 * screen.dispx_z as u32, - self.dimension.1 * screen.dispy_z as u32, - ); - sdl::SDL_RenderCopy(sdl_renderer, texture, srcrect.raw(), dstrect.raw()); - sdl::SDL_DestroyTexture(texture); - } - } -} diff --git a/src/screen/constants.rs b/src/screen/constants.rs new file mode 100644 index 0000000..235c966 --- /dev/null +++ b/src/screen/constants.rs @@ -0,0 +1,2 @@ +pub const CANVAS_FONT_WIDTH: i32 = 8 * 2; +pub const CANVAS_FONT_HEIGHT: i32 = 12 * 2; diff --git a/src/screen/data.rs b/src/screen/data.rs new file mode 100644 index 0000000..6ede199 --- /dev/null +++ b/src/screen/data.rs @@ -0,0 +1,29 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use crate::df; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Data { + pub str: String, + pub color: df::common::Color, +} + +impl Data { + pub fn new(str: String) -> Self { + Self { + str, + color: df::common::Color::rgb(255, 255, 255), + } + } + + pub fn with_color(mut self, color: df::common::Color) -> Self { + self.color = color; + self + } + + pub fn key(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } +} diff --git a/src/screen/mod.rs b/src/screen/mod.rs new file mode 100644 index 0000000..eb84eb4 --- /dev/null +++ b/src/screen/mod.rs @@ -0,0 +1,153 @@ +pub mod constants; +pub mod data; +pub mod text; + +pub use constants::CANVAS_FONT_HEIGHT; +pub use constants::CANVAS_FONT_WIDTH; +use data::Data; +pub use text::Text; + +use crate::{ + df, + font::{CJK_FONT_SIZE, FONT}, +}; +use sdl2::{pixels::PixelFormatEnum, rect::Rect, surface::Surface, sys as sdl}; +use std::{collections::HashMap, mem, ptr}; + +#[static_init::dynamic] +pub static mut SCREEN: Screen = Screen::new(); + +#[static_init::dynamic] +pub static mut SCREEN_TOP: Screen = Screen::new(); + +#[derive(Default)] +pub struct Screen { + dimension: df::common::Dimension, + canvas_ptr: usize, + // cache: hash(data) => (surface_ptr, width) + prev: HashMap, + next: HashMap, +} + +impl Screen { + pub fn new() -> Self { + Self { + dimension: Default::default(), + canvas_ptr: Default::default(), + prev: Default::default(), + next: Default::default(), + } + } + + pub fn resize(&mut self, x: i32, y: i32) { + self.dimension = df::common::Dimension { x, y }; + + if self.canvas_ptr != 0 { + let canvas = unsafe { Surface::from_ll(self.canvas_ptr as *mut sdl::SDL_Surface) }; + mem::drop(canvas); + } + + let canvas = Surface::new( + (x * CANVAS_FONT_WIDTH) as u32, + (y * CANVAS_FONT_HEIGHT) as u32, + PixelFormatEnum::RGBA32, + ) + .unwrap(); + self.canvas_ptr = canvas.raw() as usize; + mem::forget(canvas); + } + + pub fn add_text(&mut self, text: Text) -> usize { + let Text { + data, + coord: df::common::Coord { x, y }, + render, + } = text; + + if !render { + return 0; + } + + // render text or get from cache + let key = data.key(); + let (surface_ptr, width) = match self.prev.get(&key) { + Some((ptr, width)) => (ptr.to_owned() as *mut sdl::SDL_Surface, width.to_owned()), + None => { + let Data { + str, + color: df::common::Color { r, g, b }, + } = data; + + let mut font = FONT.write(); + let (ptr, width) = font.render(str); + let ptr = ptr as *mut sdl::SDL_Surface; + mem::drop(font); + + unsafe { sdl::SDL_SetSurfaceColorMod(ptr, r, g, b) }; + + (ptr, width) + } + }; + self.next.insert(key, (surface_ptr as usize, width)); + + // render on canvas + unsafe { + let mut rect = Rect::new(x, y, width, CJK_FONT_SIZE); + let canvas = self.canvas_ptr as *mut sdl::SDL_Surface; + sdl::SDL_UpperBlit(surface_ptr, ptr::null(), canvas, rect.raw_mut()); + }; + + (width as f32 / CANVAS_FONT_WIDTH as f32).ceil() as usize + } + + pub fn clear(&mut self) { + unsafe { + let canvas = self.canvas_ptr as *mut sdl::SDL_Surface; + sdl::SDL_FillRect(canvas, ptr::null(), 0); + } + + for (k, (ptr, _)) in self.prev.iter() { + if !self.next.contains_key(k) { + unsafe { sdl::SDL_FreeSurface(ptr.to_owned() as *mut sdl::SDL_Surface) }; + } + } + + self.prev = mem::take(&mut self.next); + } + + pub fn render(&mut self, renderer: usize) { + if self.canvas_ptr == 0 { + return; + } + + let canvas = self.canvas_ptr as *mut sdl::SDL_Surface; + let sdl_renderer = df::renderer::deref_sdl_renderer(renderer); + + let srcrect = Rect::new( + 0, + 0, + (self.dimension.x * CANVAS_FONT_WIDTH) as u32, + (self.dimension.y * CANVAS_FONT_HEIGHT) as u32, + ); + + let df::renderer::ScreenInfo { + dispx_z, + dispy_z, + origin_x, + origin_y, + } = df::renderer::deref_screen_info(renderer); + let dstrect = Rect::new( + origin_x as i32, + origin_y as i32, + (self.dimension.x * dispx_z) as u32, + (self.dimension.y * dispy_z) as u32, + ); + + unsafe { + let texture = sdl::SDL_CreateTextureFromSurface(sdl_renderer, canvas); + sdl::SDL_SetTextureScaleMode(texture, sdl::SDL_ScaleMode::SDL_ScaleModeLinear); + sdl::SDL_RenderCopy(sdl_renderer, texture, srcrect.raw(), dstrect.raw()); + sdl::SDL_DestroyTexture(texture); + } + } +} diff --git a/src/screen/text.rs b/src/screen/text.rs new file mode 100644 index 0000000..95f2d90 --- /dev/null +++ b/src/screen/text.rs @@ -0,0 +1,66 @@ +use crate::df; + +use super::{constants, data}; + +pub struct Text { + pub coord: df::common::Coord, + pub data: data::Data, + pub render: bool, +} + +impl Text { + pub fn new(content: String) -> Self { + Self { + coord: Default::default(), + data: data::Data::new(content), + render: true, + } + } + + pub fn by_coord(mut self, coord: df::common::Coord) -> Self { + self.coord = coord; + self + } + + pub fn by_graphic(self, gps: usize) -> Self { + self.color_by_graphic(gps).coord_by_graphic(gps) + } + + pub fn color_by_graphic(mut self, gps: usize) -> Self { + let color = df::graphic::deref_color(gps); + self.data = self.data.with_color(color); + self + } + + pub fn coord_by_graphic(self, gps: usize) -> Self { + let mut coord = df::graphic::deref_coord(gps); + coord.x *= constants::CANVAS_FONT_WIDTH; + coord.y *= constants::CANVAS_FONT_HEIGHT; + self.by_coord(coord) + } + + pub fn with_offset(mut self, offset_x: i32, offset_y: i32) -> Self { + self.coord.x += offset_x; + self.coord.y += offset_y; + self + } + + pub fn with_sflag(mut self, sflag: u32) -> Self { + let flag = df::flags::ScreenTexPosFlag::from_bits_retain(sflag); + + if flag.contains(df::flags::ScreenTexPosFlag::TOP_OF_TEXT) { + self.render = false; + } + + if flag.contains(df::flags::ScreenTexPosFlag::BOTTOM_OF_TEXT) { + self.coord.y -= constants::CANVAS_FONT_HEIGHT / 2 + } + + self + } + + pub fn with_color(mut self, color: df::common::Color) -> Self { + self.data = self.data.with_color(color); + self + } +} diff --git a/src/watchdog.rs b/src/watchdog.rs index 9e88153..e500c1f 100644 --- a/src/watchdog.rs +++ b/src/watchdog.rs @@ -11,6 +11,7 @@ pub fn install() { let state = DeviceState::new(); let mut hook_enabled: bool = true; + // TODO: only disable hook after render loop while !*KILL.read() { let keys = state.query_keymap(); if keys.contains(&Keycode::F2) && keys.contains(&Keycode::LControl) {