├── .gitignore ├── src ├── ui │ ├── sequence.rs │ ├── sequence │ │ └── background_anim.rs │ ├── image_text.rs │ ├── panel.rs │ ├── command.rs │ └── button.rs ├── graphics │ ├── lighting.rs │ ├── color │ │ ├── color.pal.gz │ │ ├── expected_blend_table_4631.bin.gz │ │ ├── expected_rgb15_to_color_idx.bin.gz │ │ ├── palette.rs │ │ └── palette │ │ │ └── overlay.rs │ ├── lighting │ │ ├── light_map_expected.bin │ │ ├── light_grid_expected.bin.gz │ │ ├── light_grid_expected2.bin.gz │ │ ├── light_grid_input.in │ │ └── light_map.rs │ ├── geometry.rs │ ├── geometry │ │ ├── camera.rs │ │ └── sqr.rs │ ├── map.rs │ └── render.rs ├── fs │ ├── dat.rs │ ├── std.rs │ └── dat │ │ ├── util.rs │ │ ├── v2.rs │ │ ├── v1.rs │ │ └── lzss.rs ├── game │ ├── rpg │ │ ├── def.rs │ │ └── def │ │ │ ├── pc_stat.rs │ │ │ ├── stat.rs │ │ │ └── skill.rs │ ├── ui.rs │ ├── sequence │ │ ├── stand.rs │ │ ├── frame_anim.rs │ │ └── move_seq.rs │ ├── ui │ │ ├── hud.rs │ │ ├── scroll_area.rs │ │ ├── move_window.rs │ │ ├── action_menu.rs │ │ └── inventory_list.rs │ ├── sequence.rs │ ├── world │ │ └── floating_text.rs │ ├── fidget.rs │ ├── dialog.rs │ └── skilldex.rs ├── util │ ├── test.rs │ ├── random.rs │ └── array2d.rs ├── state │ └── event.rs ├── asset │ ├── script.rs │ ├── palette.rs │ ├── message.rs │ ├── frame.rs │ ├── proto │ │ └── id.rs │ ├── script │ │ └── db.rs │ ├── font.rs │ └── map │ │ └── db.rs ├── state.rs ├── macros.rs ├── vm │ ├── error.rs │ ├── instruction │ │ ├── impls.rs │ │ └── impls │ │ │ └── macros.rs │ └── stack.rs ├── sequence │ ├── event.rs │ ├── cancellable.rs │ └── chain.rs ├── fs.rs ├── game.rs ├── sequence.rs ├── util.rs └── graphics.rs ├── screenshot_20190830114533.png ├── screenshot_20190917010852.png ├── screenshot_20200707141001.png ├── sdl2_windows_binaries ├── SDL2.dll ├── SDL2.lib ├── SDL2.pdb ├── SDL2main.lib └── SDL2test.lib ├── .cargo └── config.toml ├── Cargo.toml ├── README.MD └── .github └── workflows ├── snapshot.yml └── build.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /src/ui/sequence.rs: -------------------------------------------------------------------------------- 1 | pub mod background_anim; -------------------------------------------------------------------------------- /src/graphics/lighting.rs: -------------------------------------------------------------------------------- 1 | pub mod light_grid; 2 | pub mod light_map; -------------------------------------------------------------------------------- /src/fs/dat.rs: -------------------------------------------------------------------------------- 1 | mod lzss; 2 | mod util; 3 | pub mod v1; 4 | pub mod v2; 5 | -------------------------------------------------------------------------------- /src/game/rpg/def.rs: -------------------------------------------------------------------------------- 1 | pub mod pc_stat; 2 | pub mod perk; 3 | pub mod skill; 4 | pub mod stat; -------------------------------------------------------------------------------- /screenshot_20190830114533.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/screenshot_20190830114533.png -------------------------------------------------------------------------------- /screenshot_20190917010852.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/screenshot_20190917010852.png -------------------------------------------------------------------------------- /screenshot_20200707141001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/screenshot_20200707141001.png -------------------------------------------------------------------------------- /sdl2_windows_binaries/SDL2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/sdl2_windows_binaries/SDL2.dll -------------------------------------------------------------------------------- /sdl2_windows_binaries/SDL2.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/sdl2_windows_binaries/SDL2.lib -------------------------------------------------------------------------------- /sdl2_windows_binaries/SDL2.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/sdl2_windows_binaries/SDL2.pdb -------------------------------------------------------------------------------- /src/graphics/color/color.pal.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/src/graphics/color/color.pal.gz -------------------------------------------------------------------------------- /sdl2_windows_binaries/SDL2main.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/sdl2_windows_binaries/SDL2main.lib -------------------------------------------------------------------------------- /sdl2_windows_binaries/SDL2test.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/sdl2_windows_binaries/SDL2test.lib -------------------------------------------------------------------------------- /src/graphics/lighting/light_map_expected.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/src/graphics/lighting/light_map_expected.bin -------------------------------------------------------------------------------- /src/graphics/lighting/light_grid_expected.bin.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/src/graphics/lighting/light_grid_expected.bin.gz -------------------------------------------------------------------------------- /src/game/ui.rs: -------------------------------------------------------------------------------- 1 | pub mod action_menu; 2 | pub mod hud; 3 | pub mod inventory_list; 4 | pub mod move_window; 5 | pub mod scroll_area; 6 | pub mod world; 7 | -------------------------------------------------------------------------------- /src/graphics/lighting/light_grid_expected2.bin.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/src/graphics/lighting/light_grid_expected2.bin.gz -------------------------------------------------------------------------------- /src/graphics/color/expected_blend_table_4631.bin.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/src/graphics/color/expected_blend_table_4631.bin.gz -------------------------------------------------------------------------------- /src/graphics/color/expected_rgb15_to_color_idx.bin.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pingw33n/vault13/HEAD/src/graphics/color/expected_rgb15_to_color_idx.bin.gz -------------------------------------------------------------------------------- /src/util/test.rs: -------------------------------------------------------------------------------- 1 | use flate2::bufread::GzDecoder; 2 | use std::io::Read; 3 | 4 | pub fn ungz(buf: &[u8]) -> Vec { 5 | let mut r = Vec::new(); 6 | GzDecoder::new(buf).read_to_end(&mut r).unwrap(); 7 | r 8 | } -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags =["-C", "target-feature=+crt-static"] 3 | 4 | [target.i686-pc-windows-msvc] 5 | rustflags =["-C", "target-feature=+crt-static"] 6 | 7 | [target.x86_64-apple-darwin] 8 | rustflags=["-C", "link-arg=-mmacosx-version-min=10.9"] -------------------------------------------------------------------------------- /src/state/event.rs: -------------------------------------------------------------------------------- 1 | use crate::asset::proto::TargetMap; 2 | use crate::graphics::EPoint; 3 | use crate::graphics::geometry::hex::Direction; 4 | 5 | #[derive(Clone, Eq, Debug, PartialEq)] 6 | pub enum AppEvent { 7 | MapExit { 8 | map: TargetMap, 9 | pos: EPoint, 10 | direction: Direction, 11 | }, 12 | } -------------------------------------------------------------------------------- /src/asset/script.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | 3 | use std::num::NonZeroU32; 4 | 5 | /// Program ID is the identifier of script bytecode file in `scripts.lst`. 6 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 7 | pub struct ProgramId(NonZeroU32); 8 | 9 | impl ProgramId { 10 | pub fn new(val: u32) -> Option { 11 | NonZeroU32::new(val).map(Self) 12 | } 13 | 14 | pub fn index(self) -> usize { 15 | self.val() as usize - 1 16 | } 17 | 18 | pub fn val(self) -> u32 { 19 | self.0.get() 20 | } 21 | } -------------------------------------------------------------------------------- /src/game/sequence/stand.rs: -------------------------------------------------------------------------------- 1 | use crate::game::object::Handle; 2 | use crate::sequence::*; 3 | 4 | pub struct Stand { 5 | obj: Handle, 6 | done: bool, 7 | } 8 | 9 | impl Stand { 10 | pub fn new(obj: Handle) -> Self { 11 | Self { 12 | obj, 13 | done: false, 14 | } 15 | } 16 | } 17 | 18 | impl Sequence for Stand { 19 | fn update(&mut self, ctx: &mut Update) -> Result { 20 | if self.done { 21 | Result::Done 22 | } else { 23 | ctx.world.objects_mut().make_standing(self.obj); 24 | self.done = true; 25 | Result::Running(Running::NotLagging) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | 3 | use sdl2::event::{Event as SdlEvent}; 4 | use std::time::{Duration, Instant}; 5 | 6 | use crate::ui::Ui; 7 | use crate::ui::command::UiCommand; 8 | 9 | pub use event::AppEvent; 10 | 11 | pub struct HandleAppEvent<'a> { 12 | pub event: AppEvent, 13 | pub ui: &'a mut Ui, 14 | } 15 | 16 | pub struct Update<'a> { 17 | pub time: Instant, 18 | pub delta: Duration, 19 | pub ui: &'a mut Ui, 20 | pub out: &'a mut Vec, 21 | } 22 | 23 | pub trait AppState { 24 | fn handle_app_event(&mut self, ctx: HandleAppEvent); 25 | fn handle_input(&mut self, event: &SdlEvent, ui: &mut Ui) -> bool; 26 | fn handle_ui_command(&mut self, command: UiCommand, ui: &mut Ui); 27 | fn update(&mut self, ctx: Update); 28 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vault13" 3 | version = "0.1.0" 4 | authors = ["Dmytro Lysai "] 5 | license = "GPL-3.0" 6 | edition = "2024" 7 | build ="build.rs" 8 | 9 | [profile.release] 10 | debug = true 11 | 12 | [build-dependencies] 13 | regex = "1" 14 | 15 | [dependencies] 16 | bit-vec = "0.8" 17 | bstring = "0.1" 18 | btoi = "0.5" 19 | byteorder = "1" 20 | clap = "4" 21 | downcast-rs = "2" 22 | enum-as-inner = "0.6" 23 | enumflags2 = "0.7" 24 | enum-primitive-derive = "0.3" 25 | env_logger = "0.11" 26 | flate2 = "1" 27 | linearize = { version = "0.1", features = ["derive"] } 28 | log = "0.4" 29 | matches = "0.1" 30 | measure_time = "0.9" 31 | num-traits = "0.2" 32 | rand = "0.9" 33 | sdl2 = { version = "0.38", features = ["unsafe_textures"] } 34 | sdl2-sys = "0.38" 35 | slotmap = "1" 36 | static_assertions = "1" 37 | -------------------------------------------------------------------------------- /src/asset/palette.rs: -------------------------------------------------------------------------------- 1 | use byteorder::ReadBytesExt; 2 | use std::io::{self, prelude::*}; 3 | 4 | use crate::graphics::color::Rgb; 5 | use crate::graphics::color::palette::Palette; 6 | 7 | pub fn read_palette(rd: &mut impl Read) -> io::Result { 8 | let mut color_idx_to_rgb18 = [Rgb::black(); 256]; 9 | let mut mapped_colors = [false; 256]; 10 | for i in 0..256 { 11 | let r = rd.read_u8()?; 12 | let g = rd.read_u8()?; 13 | let b = rd.read_u8()?; 14 | let valid = r < 64 && b < 64 && g < 64; 15 | if valid { 16 | color_idx_to_rgb18[i] = Rgb::new(r, g, b); 17 | } 18 | mapped_colors[i] = valid; 19 | } 20 | 21 | let mut rgb15_to_color_idx = [0; 32768]; 22 | rd.read_exact(&mut rgb15_to_color_idx[..])?; 23 | 24 | Ok(Palette::new(color_idx_to_rgb18, rgb15_to_color_idx, mapped_colors)) 25 | } -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! new_handle_type { 2 | ( $( $(#[$outer:meta])* $vis:vis struct $name:ident; )* ) => { 3 | $( 4 | 5 | $(#[$outer])* 6 | #[derive(Copy, Clone, Default, 7 | Eq, Hash, PartialEq, Ord, PartialOrd, 8 | Debug)] 9 | #[repr(transparent)] 10 | $vis struct $name($crate::util::SmKey); 11 | 12 | impl From<::slotmap::KeyData> for $name { 13 | fn from(k: ::slotmap::KeyData) -> Self { 14 | $name(k.into()) 15 | } 16 | } 17 | 18 | impl From<$name> for ::slotmap::KeyData { 19 | fn from(k: $name) -> Self { 20 | k.0.into() 21 | } 22 | } 23 | 24 | unsafe impl ::slotmap::Key for $name { 25 | fn data(&self) -> ::slotmap::KeyData { 26 | self.0.data() 27 | } 28 | } 29 | 30 | )* 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/vm/error.rs: -------------------------------------------------------------------------------- 1 | use bstring::BString; 2 | use std::borrow::Cow; 3 | use std::rc::Rc; 4 | 5 | use super::ProcedureId; 6 | use crate::vm::instruction::Opcode; 7 | 8 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 9 | pub enum BadValue { 10 | Type, 11 | Content, 12 | } 13 | 14 | #[derive(Clone, Debug, Eq, PartialEq)] 15 | pub enum Error { 16 | BadInstruction, 17 | BadMetadata(Cow<'static, str>), 18 | BadOpcode(u16), 19 | BadProcedure(Rc), 20 | BadProcedureId(ProcedureId), 21 | BadState(Cow<'static, str>), 22 | BadValue(BadValue), 23 | BadExternalVar(Rc), 24 | Halted, 25 | Misc(Cow<'static, str>), 26 | UnimplementedOpcode(Opcode), 27 | StackOverflow, 28 | StackUnderflow, 29 | UnexpectedEof, 30 | } 31 | 32 | impl Error { 33 | pub fn is_halted(&self) -> bool { 34 | matches!(self, Error::Halted) 35 | } 36 | } 37 | 38 | pub type Result = ::std::result::Result; 39 | -------------------------------------------------------------------------------- /src/vm/instruction/impls.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] mod macros; 2 | mod base; 3 | mod game; 4 | 5 | pub use self::base::*; 6 | pub use self::game::*; 7 | 8 | use super::Context; 9 | use super::value::*; 10 | use super::super::*; 11 | 12 | fn binary_op(ctx: Context, f: impl FnOnce(Value, Value, &Context) -> Result) -> Result<()> { 13 | let right = ctx.prg.data_stack.pop()?; 14 | let left = ctx.prg.data_stack.pop()?; 15 | let r = f(left.clone(), right.clone(), &ctx)?; 16 | ctx.prg.data_stack.push(r)?; 17 | log_a2r1!(ctx.prg, 18 | left.resolved(ctx.prg.strings()).unwrap(), 19 | right.resolved(ctx.prg.strings()).unwrap(), 20 | ctx.prg.data_stack.top().unwrap()); 21 | Ok(()) 22 | } 23 | 24 | fn unary_op(ctx: Context, f: impl FnOnce(Value, &Context) -> Result) -> Result<()> { 25 | let v = ctx.prg.data_stack.pop()?; 26 | let r = f(v.clone(), &ctx)?; 27 | ctx.prg.data_stack.push(r)?; 28 | log_a1r1!(ctx.prg, v, ctx.prg.data_stack.top().unwrap()); 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /src/game/rpg/def/pc_stat.rs: -------------------------------------------------------------------------------- 1 | use linearize::{static_map, StaticMap}; 2 | 3 | use crate::asset::PCStat; 4 | 5 | pub struct PCStatDef { 6 | pub min: i32, 7 | pub max: i32, 8 | pub default: i32, 9 | } 10 | 11 | impl PCStatDef { 12 | pub fn defaults() -> StaticMap { 13 | use PCStat::*; 14 | static_map! { 15 | UnspentSkillPoints => Self { 16 | min: 0, 17 | max: i32::MAX, 18 | default: 0, 19 | }, 20 | Level => Self { 21 | min: 1, 22 | max: 99, 23 | default: 1, 24 | }, 25 | Experience => Self { 26 | min: 0, 27 | max: i32::MAX, 28 | default: 0, 29 | }, 30 | Reputation => Self { 31 | min: -20, 32 | max: 20, 33 | default: 0, 34 | }, 35 | Karma => Self { 36 | min: 0, 37 | max: i32::MAX, 38 | default: 0, 39 | }, 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/fs/std.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::path::{Path, PathBuf}; 3 | use std::io::{BufRead, BufReader, Result}; 4 | 5 | use super::{Metadata, Provider}; 6 | 7 | pub fn new_provider>(path: P) -> Result> { 8 | Ok(Box::new(StdFileSystem::new(path))) 9 | } 10 | 11 | struct StdFileSystem { 12 | root: PathBuf, 13 | } 14 | 15 | impl StdFileSystem { 16 | pub fn new>(root: P) -> Self { 17 | StdFileSystem { root: root.as_ref().to_path_buf() } 18 | } 19 | 20 | fn to_fs_path(&self, path: &str) -> PathBuf { 21 | let mut r = PathBuf::new(); 22 | r.push(&self.root); 23 | for s in path.split(['/', '\\']) { 24 | r.push(s); 25 | } 26 | r 27 | } 28 | } 29 | 30 | impl Provider for StdFileSystem { 31 | fn reader(&self, path: &str) -> Result> { 32 | Ok(Box::new(BufReader::new(File::open(self.to_fs_path(path))?))) 33 | } 34 | 35 | fn metadata(&self, path: &str) -> Result { 36 | let len = self.to_fs_path(path).metadata()?.len(); 37 | Ok(Metadata { len }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/sequence/event.rs: -------------------------------------------------------------------------------- 1 | use crate::game::object; 2 | use crate::graphics::EPoint; 3 | 4 | use super::*; 5 | 6 | #[derive(Clone, Debug)] 7 | pub enum Event { 8 | ObjectMoved { 9 | obj: object::Handle, 10 | old_pos: EPoint, 11 | new_pos: EPoint, 12 | }, 13 | SetDoorState { 14 | door: object::Handle, 15 | open: bool, 16 | }, 17 | Talk { 18 | talker: object::Handle, 19 | talked: object::Handle, 20 | }, 21 | Use { 22 | user: object::Handle, 23 | used: object::Handle, 24 | }, 25 | UseSkill { 26 | skill: crate::asset::Skill, 27 | user: object::Handle, 28 | target: object::Handle, 29 | } 30 | } 31 | 32 | pub struct PushEvent { 33 | event: Option, 34 | } 35 | 36 | impl PushEvent { 37 | pub fn new(event: Event) -> Self { 38 | Self { 39 | event: Some(event), 40 | } 41 | } 42 | } 43 | 44 | impl Sequence for PushEvent { 45 | fn update(&mut self, ctx: &mut Update) -> Result { 46 | if let Some(event) = self.event.take() { 47 | ctx.out.push(event); 48 | } 49 | Result::Done 50 | } 51 | } -------------------------------------------------------------------------------- /src/graphics/lighting/light_grid_input.in: -------------------------------------------------------------------------------- 1 | // Input to LightGrid::update() 2 | // (point, radius, intensity) 3 | vec![ 4 | ((95, 56), 3, 0x10000), 5 | ((88, 57), 3, 0x10000), 6 | ((116, 61), 3, 0x10000), 7 | ((132, 62), 3, 0x10000), 8 | ((140, 65), 3, 0x10000), 9 | ((115, 69), 3, 0x10000), 10 | ((102, 73), 3, 0x10000), 11 | ((140, 81), 3, 0x10000), 12 | ((131, 85), 3, 0x10000), 13 | ((102, 85), 3, 0x10000), 14 | ((92, 85), 3, 0x10000), 15 | ((78, 96), 3, 0x10000), 16 | ((78, 108), 3, 0x10000), 17 | ((104, 109), 3, 0x10000), 18 | ((120, 111), 3, 0x10000), 19 | ((95, 126), 3, 0x10000), 20 | ((87, 126), 3, 0x10000), 21 | ((83, 131), 3, 0x10000), 22 | ((100, 132), 3, 0x10000), 23 | ((95, 137), 3, 0x10000), 24 | ((87, 137), 3, 0x10000), 25 | ((95, 140), 3, 0x10000), 26 | ((87, 140), 3, 0x10000), 27 | ((83, 145), 3, 0x10000), 28 | ((100, 146), 3, 0x10000), 29 | ((95, 151), 3, 0x10000), 30 | ((87, 151), 3, 0x10000), 31 | ((95, 154), 3, 0x10000), 32 | ((87, 154), 3, 0x10000), 33 | ((83, 159), 3, 0x10000), 34 | ((100, 160), 3, 0x10000), 35 | ((95, 165), 3, 0x10000), 36 | ((87, 165), 3, 0x10000), 37 | ((92, 143), 4, 0x10000), 38 | ] -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Vault13 2 | 3 | ![Build](https://github.com/pingw33n/vault13/workflows/Build/badge.svg) 4 | 5 | Reimplementation of Fallout 2 engine. 6 | 7 | **This is a work in progress and is not playable**. 8 | 9 | # Binaries 10 | 11 | There's automated nightly build job that uploads development snapshot build as pre-releases on 12 | [Releases page](https://github.com/pingw33n/vault13/releases). 13 | 14 | To run the downloaded binary on recent macOS versions it has to be explicitly allowed in Privacy & Security settings as 15 | described [here](https://support.apple.com/en-gb/guide/mac-help/mchleab3a043/mac). That procedure 16 | has to be done twice, once for the executable and once for the `libSDL2.dylib` library. 17 | 18 | # Running demo 19 | 20 | ``` 21 | vault13 /path/to/fallout2 artemple 22 | ``` 23 | 24 | Controls that work in demo: 25 | 26 | * Mouse 27 | * Left button to run/walk when in move mode (hex cursor). 28 | * Right button to toggle move/pick mode. 29 | * Arrows - scroll map. 30 | * Hold `SHIFT` to walk instead of run. 31 | * `[` and `]` - decrease/increase ambient light. 32 | * `r` - toggle roof drawing. 33 | * `` ` `` - toggle debug info display. 34 | * `p` - toggle pause. 35 | 36 | ![Inventory](screenshot_20200707141001.png) 37 | ![Screenshot](screenshot_20190830114533.png) 38 | ![Dialog](screenshot_20190917010852.png) -------------------------------------------------------------------------------- /src/graphics/geometry.rs: -------------------------------------------------------------------------------- 1 | pub mod camera; 2 | pub mod hex; 3 | pub mod sqr; 4 | 5 | use crate::graphics::{Point, Rect}; 6 | 7 | /// Provides mapping between tile and screen coordinates. 8 | pub trait TileGridView { 9 | /// Converts screen coordinates to tile coordinates. 10 | fn screen_to_tile(&self, p: Point) -> Point; 11 | 12 | /// Converts tile coordinates to screen coordinates. 13 | fn tile_to_screen(&self, p: Point) -> Point; 14 | 15 | /// Converts tile coordinates with origin at the tile center, to screen coordinates. 16 | fn center_to_screen(&self, p: Point) -> Point; 17 | 18 | /// Returns minimal rectangle in tile coordinates that encloses the specified screen `rect`. 19 | fn enclose(&self, rect: Rect) -> Rect { 20 | enclose(rect, |p| self.screen_to_tile(p)) 21 | } 22 | } 23 | 24 | fn enclose(rect: Rect, from_screen: impl Fn(Point) -> Point) -> Rect { 25 | let right = rect.right - 1; 26 | let bottom = rect.bottom - 1; 27 | 28 | let x = from_screen(Point::new(rect.left, bottom)).x; 29 | let y = from_screen(Point::new(rect.left, rect.top)).y; 30 | let top_left = Point::new(x, y); 31 | 32 | let x = from_screen(Point::new(right, rect.top)).x; 33 | let y = from_screen(Point::new(right, bottom)).y; 34 | let bottom_right_incl = Point::new(x, y); 35 | 36 | Rect::with_points(top_left, bottom_right_incl + Point::new(1, 1)) 37 | } 38 | -------------------------------------------------------------------------------- /src/sequence/cancellable.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::rc::Rc; 3 | 4 | use super::*; 5 | 6 | #[derive(Debug)] 7 | pub struct Cancel(Rc>); 8 | 9 | impl Cancel { 10 | fn new() -> Self { 11 | Cancel(Rc::new(Cell::new(false))) 12 | } 13 | 14 | pub fn cancel(self) { 15 | self.set_done(); 16 | } 17 | 18 | pub fn is_done(&self) -> bool { 19 | self.0.get() 20 | } 21 | 22 | pub fn is_running(&self) -> bool { 23 | !self.is_done() 24 | } 25 | 26 | fn clone(&self) -> Self { 27 | Cancel(self.0.clone()) 28 | } 29 | 30 | fn set_done(&self) { 31 | self.0.set(true) 32 | } 33 | } 34 | 35 | pub struct Cancellable { 36 | sequence: T, 37 | cancel: Cancel, 38 | } 39 | 40 | impl Cancellable { 41 | pub(in super) fn new(seq: T) -> (Self, Cancel) { 42 | let signal = Cancel::new(); 43 | (Self { 44 | sequence: seq, 45 | cancel: signal.clone(), 46 | }, signal) 47 | } 48 | } 49 | 50 | impl Sequence for Cancellable { 51 | fn update(&mut self, ctx: &mut Update) -> Result { 52 | if self.cancel.is_done() { 53 | Result::Done 54 | } else { 55 | match self.sequence.update(ctx) { 56 | r @ Result::Running(_) => r, 57 | Result::Done => { 58 | self.cancel.set_done(); 59 | Result::Done 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/ui/sequence/background_anim.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use crate::sequence::*; 4 | use super::super::Handle; 5 | 6 | #[derive(Clone, Copy)] 7 | enum State { 8 | Init, 9 | Running { 10 | last: Instant, 11 | }, 12 | } 13 | 14 | pub struct BackgroundAnim { 15 | widget: Handle, 16 | delay: Duration, 17 | state: State, 18 | } 19 | 20 | impl BackgroundAnim { 21 | pub fn new(widget: Handle, delay: Duration) -> Self { 22 | Self { 23 | widget, 24 | delay, 25 | state: State::Init, 26 | } 27 | } 28 | } 29 | 30 | impl Sequence for BackgroundAnim { 31 | fn update(&mut self, ctx: &mut Update) -> Result { 32 | match self.state { 33 | State::Init => { 34 | self.state = State::Running { 35 | last: ctx.time, 36 | }; 37 | Result::Running(Running::NotLagging) 38 | } 39 | State::Running { last } => { 40 | let elapsed = ctx.time - last; 41 | if elapsed >= self.delay { 42 | let mut base = ctx.ui.widget_base_mut(self.widget); 43 | if let Some(bkg) = base.background_mut() { 44 | bkg.direction = bkg.direction.rotate_cw(); 45 | } 46 | self.state = State::Running { 47 | last: last + self.delay, 48 | }; 49 | } 50 | Result::Running(if elapsed >= self.delay * 2 { 51 | Running::Lagging 52 | } else { 53 | Running::NotLagging 54 | }) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/ui/image_text.rs: -------------------------------------------------------------------------------- 1 | use bstring::BString; 2 | use std::collections::HashMap; 3 | 4 | use crate::asset::frame::FrameId; 5 | use crate::graphics::{Point, Rect}; 6 | 7 | use super::*; 8 | 9 | pub struct ImageText { 10 | fid: FrameId, 11 | chars: HashMap, 12 | text: BString, 13 | } 14 | 15 | impl ImageText { 16 | pub fn standard_digits(fid: FrameId, width: i32) -> Self { 17 | let mut chars = HashMap::new(); 18 | for (i, c) in (b'0'..=b'9').enumerate() { 19 | let i = i as i32; 20 | chars.insert(c, Rect::with_size(width * i, 0, width, 0xffff)); 21 | } 22 | Self { 23 | fid, 24 | chars, 25 | text: BString::new(), 26 | } 27 | } 28 | 29 | pub fn big_numbers() -> Self { 30 | Self::standard_digits(FrameId::BIG_NUMBERS, 14) 31 | } 32 | 33 | pub fn text(&self) -> &BString { 34 | &self.text 35 | } 36 | 37 | pub fn text_mut(&mut self) -> &mut BString { 38 | &mut self.text 39 | } 40 | } 41 | 42 | impl Widget for ImageText { 43 | fn render(&mut self, ctx: Render) { 44 | let frm = ctx.frm_db.get(self.fid).unwrap(); 45 | let tex = &frm.first().texture; 46 | let base_rect = ctx.base.unwrap().rect; 47 | let mut x = base_rect.left; 48 | for &c in &self.text { 49 | if let Some(&rect) = self.chars.get(&c) { 50 | ctx.canvas.set_clip_rect(Rect::with_size(x, 0, rect.width(), rect.height())); 51 | let pos = Point::new(x - rect.left, base_rect.top); 52 | ctx.canvas.draw(tex, pos, 0x10000); 53 | x += rect.width(); 54 | } 55 | } 56 | ctx.canvas.reset_clip_rect(); 57 | } 58 | } -------------------------------------------------------------------------------- /src/util/random.rs: -------------------------------------------------------------------------------- 1 | use rand::{rng, Rng}; 2 | 3 | // roll_random() 4 | pub fn random(from_inclusive: i32, to_inclusive: i32) -> i32 { 5 | rng().random_range(from_inclusive..=to_inclusive) 6 | } 7 | 8 | #[derive(Clone, Copy, Debug, PartialEq, enum_primitive_derive::Primitive)] 9 | pub enum RollCheckResult { 10 | CriticalFailure = 0, 11 | Failure = 1, 12 | Success = 2, 13 | CriticalSuccess = 3, 14 | } 15 | 16 | impl RollCheckResult { 17 | pub fn is_success(self) -> bool { 18 | match self { 19 | Self::Success | Self::CriticalSuccess => true, 20 | Self::Failure | Self::CriticalFailure => false, 21 | } 22 | } 23 | 24 | pub fn is_critical(self) -> bool { 25 | match self { 26 | Self::CriticalSuccess | Self::CriticalFailure => true, 27 | Self::Success | Self::Failure => false, 28 | } 29 | } 30 | } 31 | 32 | #[derive(Clone, Copy, Debug)] 33 | pub struct RollChecker { 34 | disable_crits: bool, 35 | } 36 | 37 | impl RollChecker { 38 | pub fn new(disable_crits: bool) -> Self { 39 | Self { 40 | disable_crits, 41 | } 42 | } 43 | 44 | pub fn roll_check(self, target: i32, crit: i32) -> (RollCheckResult, i32) { 45 | let roll = target - random(1, 100); 46 | let r = if roll < 0 { 47 | if !self.disable_crits && random(1, 100) <= -roll / 10 { 48 | RollCheckResult::CriticalFailure 49 | } else { 50 | RollCheckResult::Failure 51 | } 52 | } else if !self.disable_crits && random(1, 100) <= roll / 10 + crit { 53 | RollCheckResult::CriticalSuccess 54 | } else { 55 | RollCheckResult::Success 56 | }; 57 | (r, roll) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/util/array2d.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use crate::util::VecExt; 4 | 5 | pub struct Array2d { 6 | arr: Box<[T]>, 7 | width: usize, 8 | } 9 | 10 | impl Array2d { 11 | pub fn new(arr: Box<[T]>, width: usize) -> Self { 12 | assert!(width > 0); 13 | assert_eq!(arr.len() % width, 0); 14 | Self { 15 | arr, 16 | width, 17 | } 18 | } 19 | 20 | pub fn width(&self) -> usize { 21 | self.width 22 | } 23 | 24 | pub fn height(&self) -> usize { 25 | self.arr.len() / self.width 26 | } 27 | 28 | pub fn len(&self) -> usize { 29 | self.arr.len() 30 | } 31 | 32 | pub fn get(&self, x: usize, y: usize) -> Option<&T> { 33 | self.arr.get(self.lin(x, y)) 34 | } 35 | 36 | pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut T> { 37 | let i = self.lin(x, y); 38 | self.arr.get_mut(i) 39 | } 40 | 41 | pub fn as_slice(&self) -> &[T] { 42 | &self.arr 43 | } 44 | 45 | pub fn as_slice_mut(&mut self) -> &mut [T] { 46 | &mut self.arr 47 | } 48 | 49 | fn lin(&self, x: usize, y: usize) -> usize { 50 | y.checked_mul(self.width).expect("y overflow") 51 | .checked_add(x).expect("x overflow") 52 | } 53 | } 54 | 55 | impl Array2d { 56 | pub fn with_default(width: usize, height: usize) -> Self { 57 | Self::new(Vec::with_default(width * height).into_boxed_slice(), width) 58 | } 59 | } 60 | 61 | impl Deref for Array2d { 62 | type Target = [T]; 63 | 64 | fn deref(&self) -> &Self::Target { 65 | &self.arr 66 | } 67 | } 68 | 69 | impl DerefMut for Array2d { 70 | fn deref_mut(&mut self) -> &mut Self::Target { 71 | &mut self.arr 72 | } 73 | } -------------------------------------------------------------------------------- /src/fs.rs: -------------------------------------------------------------------------------- 1 | pub mod dat; 2 | pub mod std; 3 | 4 | use ::std::io::prelude::*; 5 | use ::std::io::{Error, ErrorKind, Result}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Metadata { 9 | len: u64, 10 | } 11 | 12 | impl Metadata { 13 | pub fn len(&self) -> u64 { 14 | self.len 15 | } 16 | } 17 | 18 | pub struct FileSystem { 19 | providers: Vec>, 20 | } 21 | 22 | impl FileSystem { 23 | pub fn new() -> Self { 24 | FileSystem { providers: Vec::new() } 25 | } 26 | 27 | pub fn register_provider(&mut self, provider: Box) { 28 | self.providers.push(provider); 29 | } 30 | 31 | pub fn reader(&self, path: &str) -> Result> { 32 | self.find_provider(path, |p| p.reader(path)) 33 | } 34 | 35 | pub fn metadata(&self, path: &str) -> Result { 36 | self.find_provider(path, |p| p.metadata(path)) 37 | } 38 | 39 | fn find_provider(&self, path: &str, f: impl Fn(&dyn Provider) -> Result) -> Result { 40 | let mut error: Option = None; 41 | for provider in &self.providers { 42 | match f(provider.as_ref()) { 43 | Ok(r) => return Ok(r), 44 | Err(e) => { 45 | if e.kind() == ErrorKind::NotFound { 46 | continue; 47 | } 48 | if error.is_none() { 49 | error = Some(e); 50 | } 51 | break; 52 | } 53 | } 54 | } 55 | Err(error.unwrap_or_else(|| Error::new(ErrorKind::NotFound, 56 | format!("file not found: {}", path)))) 57 | } 58 | 59 | pub fn exists(&self, path: &str) -> bool { 60 | self.metadata(path).is_ok() 61 | } 62 | } 63 | 64 | pub trait Provider { 65 | fn reader(&self, path: &str) -> Result>; 66 | fn metadata(&self, path: &str) -> Result; 67 | } 68 | -------------------------------------------------------------------------------- /src/ui/panel.rs: -------------------------------------------------------------------------------- 1 | use bstring::BString; 2 | 3 | use crate::graphics::color::Rgb15; 4 | use crate::graphics::font::{DrawOptions, FontKey, HorzAlign, VertAlign}; 5 | use super::*; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct Text { 9 | pub text: BString, 10 | pub font: FontKey, 11 | pub color: Rgb15, 12 | pub options: DrawOptions, 13 | } 14 | 15 | /// Simple widget that can be used to utilize base widget features (background, cursor etc) and 16 | /// to draw text. 17 | pub struct Panel { 18 | text: Option, 19 | } 20 | 21 | impl Panel { 22 | pub fn new() -> Self { 23 | Self { 24 | text: None, 25 | } 26 | } 27 | 28 | pub fn text(&self) -> Option<&Text> { 29 | self.text.as_ref() 30 | } 31 | 32 | pub fn text_mut(&mut self) -> Option<&mut Text> { 33 | self.text.as_mut() 34 | } 35 | 36 | pub fn set_text(&mut self, text: Option) { 37 | self.text = text; 38 | } 39 | } 40 | 41 | impl Widget for Panel { 42 | fn render(&mut self, ctx: Render) { 43 | if let Some(text) = self.text() { 44 | let rect = ctx.base.unwrap().rect; 45 | let x = match text.options.horz_align { 46 | HorzAlign::Left => rect.left, 47 | HorzAlign::Center => rect.center().x, 48 | HorzAlign::Right => rect.right, 49 | }; 50 | let y = match text.options.vert_align { 51 | VertAlign::Top => rect.top, 52 | VertAlign::Middle => rect.center().y, 53 | VertAlign::Bottom => rect.bottom, 54 | }; 55 | 56 | let mut options = text.options.clone(); 57 | if let Some(o) = options.horz_overflow.as_mut() 58 | && o.size == 0 59 | { 60 | o.size = ctx.base.unwrap().rect.width(); 61 | } 62 | ctx.canvas.draw_text( 63 | &text.text, 64 | Point::new(x, y), 65 | text.font, 66 | text.color, 67 | &options); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/fs/dat/util.rs: -------------------------------------------------------------------------------- 1 | pub fn normalize_path(path: &str) -> String { 2 | let mut r = String::with_capacity(path.len()); 3 | 4 | for c in path.chars() { 5 | build_normalized_path(&mut r, Some(c)); 6 | } 7 | build_normalized_path(&mut r, None); 8 | 9 | r 10 | } 11 | 12 | pub fn build_normalized_path(path: &mut String, c: Option) { 13 | if let Some(mut c) = c { 14 | c = if c == '/' { 15 | '\\' 16 | } else { 17 | c.to_ascii_lowercase() 18 | }; 19 | 20 | path.push(c); 21 | } 22 | 23 | if path == ".\\" || c.is_none() && path == "." { 24 | path.truncate(0); 25 | } else if path.ends_with("\\.\\") { 26 | let l = path.len(); 27 | path.remove(l - 1); 28 | path.remove(l - 2); 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::normalize_path; 35 | 36 | #[test] 37 | fn normalizes_path_backslash() { 38 | assert_eq!(normalize_path("."), ""); 39 | assert_eq!(normalize_path(".\\"), ""); 40 | assert_eq!(normalize_path(".\\tSt"), "tst"); 41 | assert_eq!(normalize_path(".\\."), ""); 42 | assert_eq!(normalize_path(".\\.TsT"), ".tst"); 43 | assert_eq!(normalize_path(".\\.tst\\."), ".tst\\."); 44 | assert_eq!(normalize_path(".\\.tst\\.\\tst2"), ".tst\\tst2"); 45 | } 46 | 47 | #[test] 48 | fn normalizes_path_forward_slash() { 49 | assert_eq!(normalize_path("./"), ""); 50 | assert_eq!(normalize_path("./tst"), "tst"); 51 | assert_eq!(normalize_path("./."), ""); 52 | assert_eq!(normalize_path("./.tst"), ".tst"); 53 | assert_eq!(normalize_path("./.tst/."), ".tst\\."); 54 | assert_eq!(normalize_path("./.tst/./tst2"), ".tst\\tst2"); 55 | } 56 | 57 | #[test] 58 | fn normalizes_path_mixed_slashes() { 59 | assert_eq!(normalize_path("./"), ""); 60 | assert_eq!(normalize_path("./tst"), "tst"); 61 | assert_eq!(normalize_path("./."), ""); 62 | assert_eq!(normalize_path("./.tst"), ".tst"); 63 | assert_eq!(normalize_path("./.tst\\."), ".tst\\."); 64 | assert_eq!(normalize_path("./.tst\\./tst2"), ".tst\\tst2"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ui/command.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use crate::game::object; 4 | use crate::graphics::EPoint; 5 | 6 | /// Command for signaling widget-specific events to callee. 7 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 8 | pub struct UiCommand { 9 | pub source: Handle, 10 | pub data: UiCommandData, 11 | } 12 | 13 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 14 | pub enum UiCommandData { 15 | ObjectPick { 16 | kind: ObjectPickKind, 17 | obj: object::Handle, 18 | }, 19 | HexPick { 20 | action: bool, 21 | pos: EPoint, 22 | }, 23 | Action { 24 | action: crate::game::ui::action_menu::Action, 25 | }, 26 | Pick { 27 | id: u32, 28 | }, 29 | Scroll, 30 | Skilldex(SkilldexCommand), 31 | Inventory(inventory::Command), 32 | MoveWindow(move_window::Command), 33 | } 34 | 35 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 36 | pub enum ObjectPickKind { 37 | Hover, 38 | DefaultAction, 39 | ActionMenu, 40 | Skill(crate::asset::Skill), 41 | } 42 | 43 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 44 | pub enum SkilldexCommand { 45 | Cancel, 46 | Show, 47 | Skill { 48 | skill: crate::asset::Skill, 49 | target: Option, 50 | }, 51 | } 52 | 53 | pub mod inventory { 54 | use super::*; 55 | use crate::game::ui::action_menu::Action; 56 | use crate::game::ui::inventory_list::Scroll; 57 | 58 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 59 | pub enum Command { 60 | Hide, 61 | Show, 62 | Hover { 63 | object: object::Handle, 64 | }, 65 | ActionMenu { 66 | object: object::Handle, 67 | }, 68 | Action { 69 | object: object::Handle, 70 | action: Option, 71 | }, 72 | Scroll(Scroll), 73 | ListDrop { 74 | pos: Point, 75 | object: object::Handle, 76 | }, 77 | ToggleMouseMode, 78 | } 79 | } 80 | 81 | pub mod move_window { 82 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 83 | pub enum Command { 84 | Hide { 85 | ok: bool 86 | }, 87 | Inc, 88 | Dec, 89 | Max, 90 | } 91 | } -------------------------------------------------------------------------------- /src/graphics/geometry/camera.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::{Point, Rect}; 2 | use super::hex; 3 | use super::sqr; 4 | 5 | /// Defines part of tile grid to be drawn to screen viewport rect. 6 | #[derive(Clone, Debug, Eq, PartialEq)] 7 | pub struct Camera { 8 | /// Position of the top left tile (0, 0) on screen. 9 | pub origin: Point, 10 | 11 | /// Screen rect where the tile grid is visible. 12 | pub viewport: Rect, 13 | } 14 | 15 | impl Camera { 16 | pub fn hex(&self) -> hex::View { 17 | hex::View::new(self.origin) 18 | } 19 | 20 | pub fn sqr(&self) -> sqr::View { 21 | sqr::View::new(self.origin - Point::new(16, 2)) 22 | } 23 | 24 | /// Adjusts the `origin` so the center of tile at `hex_pos` is positioned in the center of viewport. 25 | pub fn look_at(&mut self, hex_pos: Point) { 26 | self.align(hex_pos, self.viewport.center()) 27 | } 28 | 29 | /// Adjusts the `origin` so the center of tile at `hex_pos` is positioned at the `screen_pos`. 30 | pub fn align(&mut self, hex_pos: Point, screen_pos: Point) { 31 | self.origin = screen_pos - hex::center_to_screen(hex_pos); 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod test { 37 | use super::*; 38 | use super::super::TileGridView; 39 | 40 | #[test] 41 | fn look_at() { 42 | let mut c = Camera { 43 | origin: Point::new(0, 0), 44 | viewport: Rect::with_size(0, 0, 640, 380), 45 | }; 46 | let expected_hex = Point::new(c.viewport.width() / 2 - 16, c.viewport.height() / 2 - 8); 47 | let expected_sqr = [ 48 | [expected_hex - Point::new(16, 2), expected_hex - Point::new(48, 2)], 49 | [expected_hex - Point::new(32, 12 + 2), expected_hex - Point::new(64, 12 + 2)]]; 50 | 51 | for &(x, y) in &[ 52 | (0, 0), 53 | (1, 0), 54 | (0, 1), 55 | (123, 123), 56 | (124, 124), 57 | ] { 58 | let p = Point::new(x, y); 59 | c.look_at(p); 60 | assert_eq!(c.hex().tile_to_screen(p), expected_hex); 61 | let expected_sqr = expected_sqr[y as usize % 2][x as usize % 2]; 62 | assert_eq!(c.sqr().tile_to_screen(p / 2), expected_sqr); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/vm/stack.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use std::fmt; 3 | use std::marker::PhantomData; 4 | 5 | use super::{BadValue, Error, Result}; 6 | use super::value::Value; 7 | 8 | /// Static identifier for the stack. Used in logging. 9 | pub trait StackId { 10 | const VALUE: &'static str; 11 | } 12 | 13 | pub struct Stack { 14 | vec: Vec, 15 | max_len: usize, 16 | _id: PhantomData, 17 | } 18 | 19 | impl Stack { 20 | pub fn new(max_len: usize) -> Self { 21 | Self { 22 | vec: Vec::new(), 23 | max_len, 24 | _id: PhantomData, 25 | } 26 | } 27 | 28 | pub fn is_empty(&self) -> bool { 29 | self.vec.is_empty() 30 | } 31 | 32 | pub fn len(&self) -> usize { 33 | self.vec.len() 34 | } 35 | 36 | pub fn top(&self) -> Option<&Value> { 37 | self.vec.last() 38 | } 39 | 40 | pub fn push(&mut self, value: Value) -> Result<()> { 41 | trace!("{}: pushing {:?}", Id::VALUE, value); 42 | if self.len() < self.max_len { 43 | self.vec.push(value); 44 | Ok(()) 45 | } else { 46 | Err(Error::StackOverflow) 47 | } 48 | } 49 | 50 | pub fn pop(&mut self) -> Result { 51 | if self.is_empty() { 52 | Err(Error::StackUnderflow) 53 | } else { 54 | let last = self.len() - 1; 55 | let r = self.vec.remove(last); 56 | trace!("{}: popped {:?}", Id::VALUE, r); 57 | Ok(r) 58 | } 59 | } 60 | 61 | pub fn truncate(&mut self, len: usize) -> Result<()> { 62 | let old_len = self.vec.len(); 63 | if len <= old_len { 64 | self.vec.truncate(len); 65 | trace!("{} stack: truncated from {} to {}", Id::VALUE, old_len, len); 66 | Ok(()) 67 | } else { 68 | Err(Error::StackUnderflow) 69 | } 70 | } 71 | 72 | pub fn get(&self, i: usize) -> Result<&Value> { 73 | self.vec.get(i).ok_or(Error::BadValue(BadValue::Content)) 74 | } 75 | 76 | pub fn get_mut(&mut self, i: usize) -> Result<&mut Value> { 77 | self.vec.get_mut(i).ok_or(Error::BadValue(BadValue::Content)) 78 | } 79 | } 80 | 81 | impl fmt::Debug for Stack { 82 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 83 | write!(f, "Stack(id=\"{}\", max_len={}, values={:?})", Id::VALUE, self.max_len, self.vec) 84 | } 85 | } -------------------------------------------------------------------------------- /src/game/ui/hud.rs: -------------------------------------------------------------------------------- 1 | use crate::asset::frame::FrameId; 2 | use crate::graphics::Rect; 3 | use crate::graphics::color::GREEN; 4 | use crate::graphics::font::FontKey; 5 | use crate::graphics::sprite::Sprite; 6 | use crate::ui::*; 7 | use crate::ui::button::Button; 8 | use crate::ui::command::{inventory, SkilldexCommand, UiCommandData}; 9 | use crate::ui::message_panel::{MessagePanel, Anchor}; 10 | 11 | pub fn create(ui: &mut Ui) -> Handle { 12 | let main_hud = ui.new_window(Rect::with_size(0, 379, 640, 100), Some(Sprite::new(FrameId::IFACE))); 13 | 14 | // Message panel. 15 | let mut mp = MessagePanel::new(ui.fonts().clone(), FontKey::antialiased(1), GREEN); 16 | mp.set_skew(1); 17 | mp.set_capacity(Some(100)); 18 | mp.set_anchor(Anchor::Bottom); 19 | let message_panel = ui.new_widget(main_hud, Rect::with_size(23, 26, 166, 65), None, None, mp); 20 | 21 | // Inventory button. 22 | // Original location is a bit off, at y=41. 23 | ui.new_widget(main_hud, Rect::with_size(211, 40, 32, 21), None, None, 24 | Button::new(FrameId::INVENTORY_BUTTON_UP, FrameId::INVENTORY_BUTTON_DOWN, 25 | Some(UiCommandData::Inventory(inventory::Command::Show)))); 26 | 27 | // Options button. 28 | ui.new_widget(main_hud, Rect::with_size(210, 62, 34, 34), None, None, 29 | Button::new(FrameId::OPTIONS_BUTTON_UP, FrameId::OPTIONS_BUTTON_DOWN, None)); 30 | 31 | // Single/burst switch button. 32 | ui.new_widget(main_hud, Rect::with_size(218, 6, 22, 21), None, None, 33 | Button::new(FrameId::BIG_RED_BUTTON_UP, FrameId::BIG_RED_BUTTON_DOWN, None)); 34 | 35 | // Skilldex button. 36 | ui.new_widget(main_hud, Rect::with_size(523, 6, 22, 21), None, None, 37 | Button::new(FrameId::BIG_RED_BUTTON_UP, FrameId::BIG_RED_BUTTON_DOWN, 38 | Some(UiCommandData::Skilldex(SkilldexCommand::Show)))); 39 | 40 | // MAP button. 41 | ui.new_widget(main_hud, Rect::with_size(526, 40, 41, 19), None, None, 42 | Button::new(FrameId::MAP_BUTTON_UP, FrameId::MAP_BUTTON_DOWN, None)); 43 | 44 | // CHA button. 45 | ui.new_widget(main_hud, Rect::with_size(526, 59, 41, 19), None, None, 46 | Button::new(FrameId::CHARACTER_BUTTON_UP, FrameId::CHARACTER_BUTTON_DOWN, None)); 47 | 48 | // PIP button. 49 | ui.new_widget(main_hud, Rect::with_size(526, 78, 41, 19), None, None, 50 | Button::new(FrameId::PIP_BUTTON_UP, FrameId::PIP_BUTTON_DOWN, None)); 51 | 52 | // Attack button. 53 | // FIXME this should be a custom button with overlay text images. 54 | ui.new_widget(main_hud, Rect::with_size(267, 26, 188, 67), None, None, 55 | Button::new(FrameId::SINGLE_ATTACK_BUTTON_UP, FrameId::SINGLE_ATTACK_BUTTON_DOWN, None)); 56 | 57 | message_panel 58 | } -------------------------------------------------------------------------------- /src/game/ui/scroll_area.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use crate::ui::*; 4 | use crate::ui::command::UiCommandData; 5 | 6 | #[derive(Clone, Copy)] 7 | enum Tick { 8 | Initial, 9 | Repeat, 10 | } 11 | 12 | struct Repeat { 13 | initial_delay: Duration, 14 | interval: Duration, 15 | state: Option<(Instant, Tick)>, 16 | } 17 | 18 | impl Repeat { 19 | pub fn new(initial_delay: Duration, interval: Duration) -> Self { 20 | Self { 21 | initial_delay, 22 | interval, 23 | state: None, 24 | } 25 | } 26 | 27 | pub fn start(&mut self, now: Instant) { 28 | self.state = Some((now, Tick::Initial)); 29 | } 30 | 31 | pub fn stop(&mut self) { 32 | self.state = None; 33 | } 34 | 35 | #[must_use] 36 | pub fn update(&mut self, now: Instant) -> bool { 37 | if let Some((last, tick)) = self.state.as_mut() { 38 | let d = match *tick { 39 | Tick::Initial => self.initial_delay, 40 | Tick::Repeat => self.interval, 41 | }; 42 | if now >= *last + d { 43 | *last += d; 44 | *tick = Tick::Repeat; 45 | return true; 46 | } 47 | } 48 | false 49 | } 50 | } 51 | 52 | pub struct ScrollArea { 53 | cursors: [Cursor; 2], 54 | repeat: Repeat, 55 | enabled: bool, 56 | } 57 | 58 | impl ScrollArea { 59 | pub fn new( 60 | enabled_cursor: Cursor, 61 | disabled_cursor: Cursor, 62 | initial_delay: Duration, 63 | interval: Duration, 64 | ) -> Self { 65 | Self { 66 | cursors: [disabled_cursor, enabled_cursor], 67 | repeat: Repeat::new(initial_delay, interval), 68 | enabled: true, 69 | } 70 | } 71 | 72 | pub fn set_enabled(&mut self, enabled: bool) { 73 | self.enabled = enabled; 74 | } 75 | } 76 | 77 | impl Widget for ScrollArea { 78 | fn handle_event(&mut self, mut ctx: HandleEvent) { 79 | match ctx.event { 80 | Event::MouseMove { .. } => { 81 | self.repeat.start(ctx.now); 82 | } 83 | Event::MouseLeave => { 84 | self.repeat.stop(); 85 | } 86 | Event::Tick => { 87 | if self.repeat.update(ctx.now) { 88 | ctx.out(UiCommandData::Scroll); 89 | } 90 | } 91 | _ => {} 92 | } 93 | } 94 | 95 | fn sync(&mut self, ctx: Sync) { 96 | ctx.base.set_cursor(Some(self.cursors[self.enabled as usize])) 97 | } 98 | 99 | fn render(&mut self, _ctx: Render) { 100 | } 101 | } -------------------------------------------------------------------------------- /src/game/sequence.rs: -------------------------------------------------------------------------------- 1 | pub mod frame_anim; 2 | pub mod move_seq; 3 | pub mod stand; 4 | 5 | use slotmap::SecondaryMap; 6 | use std::time::Instant; 7 | 8 | use crate::game::object::Handle; 9 | use crate::sequence::chain::*; 10 | use crate::sequence::*; 11 | use std::rc::Rc; 12 | use std::cell::RefCell; 13 | 14 | struct Seq { 15 | main: Control, 16 | subs: Vec, 17 | } 18 | 19 | impl Seq { 20 | fn cancel(&mut self) { 21 | for sub in self.subs.drain(..) { 22 | sub.cancel(); 23 | } 24 | } 25 | } 26 | 27 | /// Tracks sequences bound to objects. 28 | pub struct ObjSequencer { 29 | seqr: Sequencer, 30 | seqs: SecondaryMap, 31 | done_objs: Rc>>, 32 | } 33 | 34 | impl ObjSequencer { 35 | pub fn new(now: Instant) -> Self { 36 | Self { 37 | seqr: Sequencer::new(now), 38 | seqs: Default::default(), 39 | done_objs: Default::default(), 40 | } 41 | } 42 | 43 | /// Clears all sequences including the finalizing sequences. 44 | pub fn clear(&mut self) { 45 | self.seqr.stop_all(); 46 | self.seqs.clear(); 47 | self.done_objs.borrow_mut().clear(); 48 | } 49 | 50 | pub fn is_running(&self, object: Handle) -> bool { 51 | self.seqs.contains_key(object) 52 | } 53 | 54 | /// Cancels all sequences running for `object`. 55 | pub fn cancel(&mut self, object: Handle) { 56 | if let Some(mut seq) = self.seqs.remove(object) { 57 | seq.cancel(); 58 | } 59 | } 60 | 61 | /// Cancels all sequences running for `object` and starts a new `chain` sequence. 62 | pub fn replace(&mut self, object: Handle, chain: Chain) { 63 | if !self.seqs.contains_key(object) { 64 | let main = Chain::new(); 65 | 66 | let done_objs = self.done_objs.clone(); 67 | main.control().on_done(move || done_objs.borrow_mut().push(object)); 68 | 69 | self.seqs.insert(object, Seq { 70 | main: main.control().clone(), 71 | subs: Vec::new(), 72 | }); 73 | self.seqr.start(main); 74 | } 75 | let seq = self.seqs.get_mut(object).unwrap(); 76 | seq.cancel(); 77 | 78 | seq.subs.push(chain.control().clone()); 79 | seq.main.cancellable(chain); 80 | } 81 | 82 | pub fn update(&mut self, ctx: &mut Update) { 83 | self.seqr.update(ctx); 84 | self.remove_done_objs(); 85 | } 86 | 87 | pub fn sync(&mut self, ctx: &mut Sync) { 88 | self.seqr.sync(ctx); 89 | self.remove_done_objs(); 90 | } 91 | 92 | fn remove_done_objs(&mut self) { 93 | for obj in self.done_objs.borrow_mut().drain(..) { 94 | self.seqs.remove(obj); 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/game/rpg/def/stat.rs: -------------------------------------------------------------------------------- 1 | use linearize::{static_map, StaticMap}; 2 | use crate::asset::Stat; 3 | 4 | #[derive(Clone)] 5 | pub struct StatDef { 6 | pub image_fid_id: u32, 7 | pub min: i32, 8 | pub max: i32, 9 | pub default: i32, 10 | } 11 | 12 | impl StatDef { 13 | const fn new( 14 | image_fid_id: u32, 15 | min: i32, 16 | max: i32, 17 | default: i32, 18 | ) -> Self { 19 | Self { 20 | image_fid_id, 21 | min, 22 | max, 23 | default 24 | } 25 | } 26 | 27 | pub fn defaults() -> StaticMap { 28 | use Stat::*; 29 | static_map! { 30 | Strength => Self::new(0, 1, 10, 5), 31 | Perception => Self::new(1, 1, 10, 5), 32 | Endurance => Self::new(2, 1, 10, 5), 33 | Charisma => Self::new(3, 1, 10, 5), 34 | Intelligence => Self::new(4, 1, 10, 5), 35 | Agility => Self::new(5, 1, 10, 5), 36 | Luck => Self::new(6, 1, 10, 5), 37 | HitPoints => Self::new(10, 0, 999, 0), 38 | ActionPoints => Self::new(75, 1, 99, 0), 39 | ArmorClass => Self::new(18, 0, 999, 0), 40 | UnarmedDmg => Self::new(31, 0, i32::MAX, 0), 41 | MeleeDmg => Self::new(32, 0, 500, 0), 42 | CarryWeight => Self::new(20, 0, 999, 0), 43 | Sequence => Self::new(24, 0, 60, 0), 44 | HealRate => Self::new(25, 0, 30, 0), 45 | CritChance => Self::new(26, 0, 100, 0), 46 | BetterCrit => Self::new(94, -60, 100, 0), 47 | DmgThresh => Self::new(0, 0, 100, 0), 48 | DmgThreshLaser => Self::new(0, 0, 100, 0), 49 | DmgThreshFire => Self::new(0, 0, 100, 0), 50 | DmgThreshPlasma => Self::new(0, 0, 100, 0), 51 | DmgThreshElectrical => Self::new(0, 0, 100, 0), 52 | DmgThreshEmp => Self::new(0, 0, 100, 0), 53 | DmgThreshExplosion => Self::new(0, 0, 100, 0), 54 | DmgResist => Self::new(22, 0, 90, 0), 55 | DmgResistLaser => Self::new(0, 0, 90, 0), 56 | DmgResistFire => Self::new(0, 0, 90, 0), 57 | DmgResistPlasma => Self::new(0, 0, 90, 0), 58 | DmgResistElectrical => Self::new(0, 0, 90, 0), 59 | DmgResistEmp => Self::new(0, 0, 100, 0), 60 | DmgResistExplosion => Self::new(0, 0, 90, 0), 61 | RadResist => Self::new(83, 0, 95, 0), 62 | PoisonResist => Self::new(23, 0, 95, 0), 63 | Age => Self::new(0, 16, 101, 25), 64 | Gender => Self::new(0, 0, 1, 0), 65 | CurrentHitPoints => Self::new(10, 0, 2000, 0), 66 | CurrentPoison => Self::new(11, 0, 2000, 0), 67 | CurrentRad => Self::new(12, 0, 2000, 0), 68 | } 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/graphics/map.rs: -------------------------------------------------------------------------------- 1 | use crate::graphics::geometry::{hex, TileGridView}; 2 | use crate::graphics::lighting::light_map::{VERTEX_COUNT, VERTEX_HEXES}; 3 | use crate::graphics::{Point, Rect}; 4 | use crate::graphics::render::{Canvas, TextureHandle}; 5 | 6 | const ROOF_HEIGHT: i32 = 96; 7 | 8 | pub fn render_floor(canvas: &mut dyn Canvas, stg: &impl TileGridView, rect: Rect, 9 | get_tex: impl FnMut(Point) -> Option, 10 | get_light: impl Fn(Point) -> u32) { 11 | render_square_tiles(canvas, stg, rect, 0, get_tex, get_light); 12 | } 13 | 14 | pub fn render_roof(canvas: &mut dyn Canvas, stg: &impl TileGridView, rect: Rect, 15 | get_tex: impl FnMut(Point) -> Option) { 16 | let rect = Rect::with_size(rect.left, rect.top + ROOF_HEIGHT, rect.width(), rect.height()); 17 | render_square_tiles(canvas, stg, rect, ROOF_HEIGHT, get_tex, |_| 0x10000); 18 | } 19 | 20 | fn render_square_tiles(canvas: &mut dyn Canvas, stg: &impl TileGridView, rect: Rect, 21 | y_offset: i32, 22 | mut get_tex: impl FnMut(Point) -> Option, 23 | get_light: impl Fn(Point) -> u32) { 24 | let sqr_rect = stg.enclose(rect); 25 | 26 | let mut vertex_lights = [0; VERTEX_COUNT]; 27 | for y in sqr_rect.top..sqr_rect.bottom { 28 | for x in (sqr_rect.left..sqr_rect.right).rev() { 29 | if let Some(tex) = get_tex(Point::new(x, y)) { 30 | let scr_pt = stg.tile_to_screen(Point::new(x, y)) - Point::new(0, y_offset); 31 | 32 | let hex_pos = Point::new(x * 2, y * 2); 33 | for i in 0..VERTEX_COUNT { 34 | let l = get_light(hex_pos + VERTEX_HEXES[i]); 35 | vertex_lights[i] = l; 36 | } 37 | 38 | canvas.draw_multi_light(&tex, scr_pt, &vertex_lights[..]); 39 | } 40 | } 41 | } 42 | } 43 | 44 | // Whether scroll is restricted based on horz/vert distance from `dude_pos` to the new `pos`. 45 | pub fn is_scroll_limited(pos: Point, dude_pos: Point) -> bool { 46 | let dist = hex::to_screen(dude_pos) - hex::to_screen(pos); 47 | dist.x >= 480 || dist.y >= 400 48 | 49 | // There's also: 50 | // v8 = abs(dude_tile_screen_y - g_map_win_center_y), 51 | // v4 > abs(dude_tile_screen_x - g_map_win_center_x)) 52 | // || v6 > v8) 53 | } 54 | 55 | //TODO 56 | // 57 | // || (unsigned __int8)g_tile_scroll_blocking_enabled & ((flags & TSCF_IGNORE_SCROLL_RESTRICTIONS) == 0) 58 | // && !obj_scroll_blocking_at_(tile_num, elevation) 59 | // 60 | // || (tile_x = g_map_width_tiles - 1 - tile_num % g_map_width_tiles, 61 | // tile_y = tile_num / g_map_width_tiles, 62 | // g_map_border_set) 63 | // && (tile_x <= g_map_border_tile_x_min 64 | // || tile_x >= g_map_border_tile_x_max 65 | // || tile_y <= g_map_border_tile_y_min 66 | // || tile_y >= g_map_border_tile_y_max) 67 | //} 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/game.rs: -------------------------------------------------------------------------------- 1 | pub mod dialog; 2 | pub mod fidget; 3 | pub mod inventory; 4 | pub mod object; 5 | pub mod rpg; 6 | pub mod script; 7 | pub mod sequence; 8 | pub mod skilldex; 9 | pub mod state; 10 | pub mod ui; 11 | pub mod world; 12 | 13 | use crate::util::random::RollChecker; 14 | 15 | #[derive(Clone, Copy)] 16 | pub struct GameTime(u32); 17 | 18 | impl GameTime { 19 | pub const fn from_decis(decis: u32) -> Self { 20 | Self(decis) 21 | } 22 | 23 | pub fn as_decis(self) -> u32 { 24 | self.0 25 | } 26 | 27 | pub fn as_seconds(self) -> u32 { 28 | self.0 / 10 29 | } 30 | 31 | pub fn as_minutes(self) -> u32 { 32 | self.as_seconds() / 60 33 | } 34 | 35 | pub fn as_hours(self) -> u32 { 36 | self.as_minutes() / 60 37 | } 38 | 39 | pub fn year(self) -> u16 { 40 | self.ydm().0 41 | } 42 | 43 | pub fn month(self) -> u8 { 44 | self.ydm().1 45 | } 46 | 47 | pub fn day(self) -> u8 { 48 | self.ydm().2 49 | } 50 | 51 | pub fn hour(self) -> u8 { 52 | (self.as_hours() % 24) as u8 53 | } 54 | 55 | pub fn minute(self) -> u8 { 56 | (self.as_minutes() % 60) as u8 57 | } 58 | 59 | pub fn second(self) -> u8 { 60 | (self.as_seconds() % 60) as u8 61 | } 62 | 63 | pub fn decisecond(self) -> u8 { 64 | (self.as_decis() % 10) as u8 65 | } 66 | 67 | pub fn roll_checker(self) -> RollChecker { 68 | RollChecker::new(self.as_hours() >= 1) 69 | } 70 | 71 | fn ydm(self) -> (u16, u8, u8) { 72 | const DAYS_IN_MONTH: [u8; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 73 | 74 | let days = self.0 / 864000 + 24; 75 | let mut year = days / 365 + 2241; 76 | let mut month = 6; 77 | let mut day = days % 365; 78 | loop { 79 | let days_in_month = DAYS_IN_MONTH[month as usize].into(); 80 | if day < days_in_month { 81 | break; 82 | } 83 | day -= days_in_month; 84 | month += 1; 85 | if month >= 12 { 86 | year += 1; 87 | month = 0; 88 | } 89 | } 90 | (year as u16, month as u8 + 1, day as u8 + 1) 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod test { 96 | use super::*; 97 | 98 | #[cfg(test)] 99 | mod game_time { 100 | use super::*; 101 | 102 | #[test] 103 | fn test() { 104 | let t = GameTime::from_decis(302412); 105 | assert_eq!(t.year(), 2241); 106 | assert_eq!(t.month(), 7); 107 | assert_eq!(t.day(), 25); 108 | assert_eq!(t.hour(), 8); 109 | assert_eq!(t.minute(), 24); 110 | assert_eq!(t.second(), 1); 111 | assert_eq!(t.decisecond(), 2); 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /src/asset/message.rs: -------------------------------------------------------------------------------- 1 | use bstring::BString; 2 | use byteorder::ReadBytesExt; 3 | use std::io::{self, Error, ErrorKind, prelude::*}; 4 | use std::str; 5 | use std::collections::HashMap; 6 | 7 | use crate::fs::FileSystem; 8 | 9 | /// Bullet character used in message panel. 10 | pub const BULLET: u8 = b'\x95'; 11 | pub const BULLET_STR: &[u8] = b"\x95"; 12 | 13 | pub type MessageId = i32; 14 | 15 | #[derive(Debug, Default)] 16 | pub struct Messages { 17 | map: HashMap, 18 | } 19 | 20 | impl Messages { 21 | pub fn read(rd: &mut impl Read) -> io::Result { 22 | let mut map = HashMap::new(); 23 | loop { 24 | match Message::read(rd) { 25 | Ok(Some(m)) => map.insert(m.id, m), 26 | Ok(None) => break, 27 | Err(e) => return Err(e), 28 | }; 29 | } 30 | Ok(Self { 31 | map, 32 | }) 33 | } 34 | 35 | pub fn read_file(fs: &FileSystem, language: &str, path: &str) -> io::Result { 36 | let path = format!("text/{}/{}", language, path); 37 | Self::read(&mut fs.reader(&path)?) 38 | } 39 | 40 | pub fn get(&self, id: MessageId) -> Option<&Message> { 41 | self.map.get(&id) 42 | } 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct Message { 47 | pub id: MessageId, 48 | pub audio: BString, 49 | pub text: BString, 50 | } 51 | 52 | impl Message { 53 | fn read(rd: &mut impl Read) -> io::Result> { 54 | let id = maybe_read_field(rd)?; 55 | Ok(if let Some(id) = id { 56 | let id = id.parse().map_err(|_| Error::new(ErrorKind::InvalidData, "error reading ID field"))?; 57 | let audio = read_field(rd)?; 58 | let text = read_field(rd)?; 59 | Some(Self { 60 | id, 61 | audio, 62 | text, 63 | }) 64 | } else { 65 | None 66 | }) 67 | } 68 | } 69 | 70 | 71 | fn maybe_read_field(rd: &mut impl Read) -> io::Result> { 72 | loop { 73 | match rd.read_u8() { 74 | Ok(c) => match c { 75 | b'{' => break, 76 | b'}' => return Err(Error::new(ErrorKind::InvalidData, "misplaced delimiter")), 77 | _ => {} 78 | } 79 | Err(e) => if e.kind() == ErrorKind::UnexpectedEof { 80 | return Ok(None); 81 | } else { 82 | return Err(e); 83 | } 84 | } 85 | } 86 | let mut r = BString::new(); 87 | loop { 88 | let c = rd.read_u8()?; 89 | if c == b'}' { 90 | break; 91 | } 92 | if c != b'\n' { 93 | if r.len() == 1024 { 94 | return Err(Error::new(ErrorKind::InvalidData, "field too long")); 95 | } 96 | r.push(c); 97 | } 98 | } 99 | Ok(Some(r)) 100 | } 101 | 102 | fn read_field(rd: &mut impl Read) -> io::Result { 103 | match maybe_read_field(rd) { 104 | Ok(Some(v)) => Ok(v), 105 | Ok(None) => Err(Error::new(ErrorKind::InvalidData, "unexpected eof")), 106 | Err(e) => Err(e), 107 | } 108 | } -------------------------------------------------------------------------------- /src/asset/frame.rs: -------------------------------------------------------------------------------- 1 | mod db; 2 | mod id; 3 | 4 | use byteorder::{BigEndian, ReadBytesExt}; 5 | use linearize::{static_map, StaticMap}; 6 | use std::io::{self, prelude::*}; 7 | 8 | pub use id::{FrameId, Idx}; 9 | pub use db::FrameDb; 10 | 11 | use crate::graphics::Point; 12 | use crate::graphics::geometry::hex::Direction; 13 | use crate::graphics::render::TextureFactory; 14 | use crate::graphics::sprite::*; 15 | use crate::util::EnumExt; 16 | 17 | pub fn read_frm(rd: &mut impl Read, texture_factory: &TextureFactory) -> io::Result { 18 | let _version = rd.read_u32::()?; 19 | 20 | let fps = rd.read_u16::()?; 21 | let fps = if fps == 0 { 22 | 10 23 | } else { 24 | fps 25 | }; 26 | 27 | let action_frame = rd.read_u16::()?; 28 | let frames_per_direction = rd.read_u16::()? as usize; 29 | assert!(frames_per_direction > 0); 30 | 31 | let mut centers_x = StaticMap::default(); 32 | for dir in Direction::iter() { 33 | centers_x[dir] = rd.read_i16::()? as i32; 34 | } 35 | let mut centers_y = StaticMap::default(); 36 | for dir in Direction::iter() { 37 | centers_y[dir] = rd.read_i16::()? as i32; 38 | } 39 | 40 | let mut frame_offsets = StaticMap::default(); 41 | for dir in Direction::iter() { 42 | frame_offsets[dir] = rd.read_u32::()?; 43 | } 44 | 45 | let _data_len = rd.read_u32::()?; 46 | 47 | let mut loaded_offsets: StaticMap> = StaticMap::default(); 48 | let mut frame_lists: StaticMap> = StaticMap::default(); 49 | for dir in Direction::iter() { 50 | let offset = frame_offsets[dir]; 51 | let already_loaded_dir = loaded_offsets 52 | .iter() 53 | .filter_map(|(d, o)| o.filter(|&o| o == offset).map(|_| d)) 54 | .next(); 55 | if let Some(already_loaded_dir) = already_loaded_dir { 56 | frame_lists[dir] = frame_lists[already_loaded_dir].clone(); 57 | continue; 58 | } 59 | 60 | loaded_offsets[dir] = Some(offset); 61 | 62 | let mut frames = Vec::with_capacity(frames_per_direction); 63 | for _ in 0..frames_per_direction { 64 | let width = rd.read_i16::()? as i32; 65 | let height = rd.read_i16::()? as i32; 66 | let _len = rd.read_u32::()?; 67 | let shift = Point::new( 68 | rd.read_i16::()? as i32, 69 | rd.read_i16::()? as i32, 70 | ); 71 | 72 | let len = (width * height) as usize; 73 | let mut pixels = vec![0; len].into_boxed_slice(); 74 | rd.read_exact(&mut pixels)?; 75 | 76 | let mask = Mask::new(width, &pixels); 77 | let texture = texture_factory.new_texture(width, height, pixels); 78 | 79 | frames.push(Frame { 80 | shift, 81 | width, 82 | height, 83 | texture, 84 | mask, 85 | }); 86 | } 87 | frame_lists[dir] = Some(FrameList { 88 | center: Point::new(centers_x[dir], centers_y[dir]), 89 | frames, 90 | }); 91 | } 92 | 93 | Ok(FrameSet { 94 | fps, 95 | action_frame, 96 | frame_lists: static_map! { k => frame_lists[k].take().unwrap() }, 97 | }) 98 | } -------------------------------------------------------------------------------- /src/game/world/floating_text.rs: -------------------------------------------------------------------------------- 1 | use bstring::{bstr, BString}; 2 | use std::cmp; 3 | use std::time::{Duration, Instant}; 4 | 5 | use crate::game::object; 6 | use crate::graphics::{Point, Rect}; 7 | use crate::graphics::color::*; 8 | use crate::graphics::font::{self, FontKey, Fonts}; 9 | use crate::graphics::render::{Canvas, Outline}; 10 | 11 | #[derive(Clone, Debug)] 12 | pub struct Options { 13 | pub font_key: FontKey, 14 | pub color: Rgb15, 15 | pub outline_color: Option, 16 | } 17 | 18 | pub(in super) struct FloatingText { 19 | pub obj: Option, 20 | /// (line, width) 21 | lines: Vec<(BString, i32)>, 22 | width: i32, 23 | height: i32, 24 | vert_advance: i32, 25 | options: Options, 26 | time: Instant, 27 | } 28 | 29 | impl FloatingText { 30 | pub fn new(obj: Option, 31 | text: &bstr, 32 | fonts: &Fonts, 33 | options: Options, 34 | time: Instant, 35 | ) -> Self { 36 | let font = fonts.get(options.font_key); 37 | 38 | let lines: Vec<_> = font.lines(text, Some(font::Overflow { 39 | size: 200, 40 | boundary: font::OverflowBoundary::Word, 41 | action: font::OverflowAction::Wrap, 42 | })) 43 | .map(|l| (l.to_owned(), font.line_width(l))) 44 | .collect(); 45 | let vert_advance = font.vert_advance() + 1; 46 | let mut width = lines.iter().map(|&(_, w)| w).max().unwrap(); 47 | let mut height = lines.len() as i32 * vert_advance; 48 | if options.outline_color.is_some() { 49 | width += 2; 50 | height += 2; 51 | } 52 | Self { 53 | obj, 54 | lines, 55 | width, 56 | height, 57 | vert_advance, 58 | options, 59 | time, 60 | } 61 | } 62 | 63 | pub fn render(&self, pos: Point, rect: Rect, canvas: &mut dyn Canvas) { 64 | let pos = Self::fit( 65 | Rect::with_size(pos.x - self.width / 2, pos.y - self.height, 66 | self.width, self.height), 67 | rect); 68 | 69 | let mut y = pos.y; 70 | for (line, line_width) in &self.lines { 71 | let x = pos.x + (self.width - *line_width) / 2; 72 | canvas.draw_text(line, Point::new(x, y), self.options.font_key, self.options.color, 73 | &font::DrawOptions { 74 | outline: self.options.outline_color 75 | .map(|color| Outline::Fixed { color, trans_color: None }), 76 | ..Default::default() 77 | }); 78 | y += self.vert_advance; 79 | } 80 | } 81 | 82 | pub fn expires_at(&self, initial_delay: Duration, per_line_delay: Duration) -> Instant { 83 | let d = initial_delay + per_line_delay * self.lines.len() as u32; 84 | self.time + d 85 | } 86 | 87 | fn fit(rect: Rect, bound_rect: Rect) -> Point { 88 | #[inline(always)] 89 | fn fit0(lo: i32, hi: i32, bound_lo: i32, bound_hi: i32, 90 | lo_max: i32, hi_max: i32) -> i32 91 | { 92 | if bound_lo - lo > 0 { 93 | lo + cmp::min(bound_lo - lo, lo_max) 94 | } else if hi - bound_hi > 0 { 95 | lo - cmp::min(hi - bound_hi, hi_max) 96 | } else { 97 | lo 98 | } 99 | } 100 | let x = fit0(rect.left, rect.right, bound_rect.left, bound_rect.right, 101 | rect.width() / 2, rect.width() / 2); 102 | let y = fit0(rect.top, rect.bottom, bound_rect.top, bound_rect.bottom, 103 | rect.height(), 0); 104 | Point::new(x, y) 105 | } 106 | } -------------------------------------------------------------------------------- /src/sequence.rs: -------------------------------------------------------------------------------- 1 | pub mod cancellable; 2 | pub mod chain; 3 | pub mod event; 4 | 5 | use std::time::Instant; 6 | 7 | use crate::game::world::World; 8 | 9 | pub use event::Event; 10 | 11 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 12 | pub enum Running { 13 | /// The sequence is not lagging. The caller must not call `update()` again. 14 | NotLagging, 15 | 16 | /// The sequence is lagging. The caller must repeatedly call `update()` until it returns 17 | /// `Result::Running(Running::NotLagging)` status or `Result::Done(_)`. 18 | Lagging, 19 | } 20 | 21 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 22 | pub enum Result { 23 | /// Sequence is still running after the `update()` call. 24 | Running(Running), 25 | 26 | /// Sequence is finished. 27 | /// If applicable the caller must advance to the next sequence immediately. 28 | Done, 29 | } 30 | 31 | pub struct Update<'a> { 32 | pub time: Instant, 33 | pub world: &'a mut World, 34 | pub ui: &'a mut crate::ui::Ui, 35 | pub out: &'a mut Vec, 36 | } 37 | 38 | pub trait Sequence { 39 | fn update(&mut self, ctx: &mut Update) -> Result; 40 | 41 | fn cancellable(self) -> (cancellable::Cancellable, cancellable::Cancel) 42 | where Self: Sized 43 | { 44 | cancellable::Cancellable::new(self) 45 | } 46 | } 47 | 48 | impl Sequence for Box { 49 | fn update(&mut self, ctx: &mut Update) -> Result { 50 | (**self).update(ctx) 51 | } 52 | } 53 | 54 | pub struct Sync<'a> { 55 | pub world: &'a mut World, 56 | pub ui: &'a mut crate::ui::Ui, 57 | } 58 | 59 | pub struct Sequencer { 60 | last_time: Instant, 61 | sequences: Vec>, 62 | } 63 | 64 | impl Sequencer { 65 | pub fn new(now: Instant) -> Self { 66 | Self { 67 | last_time: now, 68 | sequences: Vec::new(), 69 | } 70 | } 71 | 72 | pub fn is_running(&self) -> bool { 73 | !self.sequences.is_empty() 74 | } 75 | 76 | pub fn start(&mut self, sequence: impl 'static + Sequence) { 77 | self.sequences.push(Box::new(sequence)); 78 | } 79 | 80 | pub fn stop_all(&mut self) { 81 | self.sequences.clear(); 82 | } 83 | 84 | pub fn update(&mut self, ctx: &mut Update) { 85 | assert!(self.last_time <= ctx.time); 86 | self.last_time = ctx.time; 87 | let mut i = 0; 88 | while i < self.sequences.len() { 89 | let done = { 90 | let seq = &mut self.sequences[i]; 91 | update_while_lagging(seq, ctx) == NoLagResult::Done 92 | }; 93 | if done { 94 | self.sequences.swap_remove(i); 95 | } else { 96 | i += 1; 97 | } 98 | } 99 | } 100 | 101 | /// Executes a no-advance update so the effect of cancellation can be seen immediately. 102 | pub fn sync(&mut self, ctx: &mut Sync) { 103 | let out = &mut Vec::new(); 104 | self.update(&mut Update { 105 | time: self.last_time, 106 | world: ctx.world, 107 | ui: ctx.ui, 108 | out, 109 | }); 110 | assert!(out.is_empty()); 111 | } 112 | } 113 | 114 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 115 | enum NoLagResult { 116 | Running, 117 | Done, 118 | } 119 | 120 | fn update_while_lagging(seq: &mut dyn Sequence, ctx: &mut Update) -> NoLagResult { 121 | loop { 122 | break match seq.update(ctx) { 123 | Result::Running(Running::Lagging) => continue, 124 | Result::Running(Running::NotLagging) => NoLagResult::Running, 125 | Result::Done => NoLagResult::Done, 126 | }; 127 | } 128 | } -------------------------------------------------------------------------------- /src/graphics/render.rs: -------------------------------------------------------------------------------- 1 | pub mod software; 2 | 3 | use bstring::bstr; 4 | use std::cell::RefCell; 5 | use std::fmt; 6 | use std::rc::Rc; 7 | use std::time::Instant; 8 | 9 | use crate::graphics::color::Rgb15; 10 | use crate::graphics::font::{self, FontKey, Fonts}; 11 | use crate::graphics::{Point, Rect}; 12 | 13 | #[derive(Clone)] 14 | pub struct TextureHandle(Rc); 15 | 16 | impl fmt::Debug for TextureHandle { 17 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 18 | write!(f, "TextureHandle@{:?}", self.0.key) 19 | } 20 | } 21 | 22 | new_handle_type! { 23 | struct Key; 24 | } 25 | 26 | #[derive(Clone)] 27 | struct TextureHandleInner { 28 | key: Key, 29 | drop_list: Rc>>, 30 | } 31 | 32 | impl Drop for TextureHandleInner { 33 | fn drop(&mut self) { 34 | self.drop_list.borrow_mut().push(self.key); 35 | } 36 | } 37 | 38 | #[derive(Clone)] 39 | pub struct TextureFactory(TextureFactoryInner); 40 | 41 | #[derive(Clone)] 42 | enum TextureFactoryInner { 43 | Software(software::Textures), 44 | } 45 | 46 | impl TextureFactory { 47 | pub fn new_texture(&self, width: i32, height: i32, data: Box<[u8]>) -> TextureHandle { 48 | match self.0 { 49 | TextureFactoryInner::Software(ref i) => i.new_texture(width, height, data), 50 | } 51 | } 52 | } 53 | 54 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 55 | pub enum Outline { 56 | /// If `trans_color` is not `None`, outline will have translucency effect of that color. 57 | Fixed { color: Rgb15, trans_color: Option }, 58 | 59 | /// Cycles colors vertically in [start..start + len) range of color indices. 60 | /// The whole range is mapped to the whole texture height. 61 | Cycled { start: u8, len: u8 }, 62 | } 63 | 64 | pub trait Canvas { 65 | fn cleanup(&mut self); 66 | fn present(&mut self); 67 | fn update(&mut self, time: Instant); 68 | 69 | fn fonts(&self) -> &Rc; 70 | 71 | fn set_clip_rect(&mut self, rect: Rect); 72 | fn reset_clip_rect(&mut self); 73 | 74 | fn clear(&mut self, color: Rgb15); 75 | 76 | fn draw(&mut self, tex: &TextureHandle, pos: Point, light: u32); 77 | fn draw_multi_light(&mut self, tex: &TextureHandle, pos: Point, lights: &[u32]); 78 | 79 | /// Draws the specified `texture` masked using the specified `mask`. 80 | /// `mask` values are in range [0..128]. 0 is fully opaque, 128 is fully transparent. 81 | fn draw_masked(&mut self, texture: &TextureHandle, pos: Point, 82 | mask: &TextureHandle, mask_pos: Point, 83 | light: u32); 84 | 85 | /// Alpha-blends from `src` color to `dst` color with alpha mask specified by the `mask. 86 | /// If `dst` is `None` the current color of pixels in back buffer is used. 87 | /// `color`. `mask` values are in range [0..7]. Note the meaning here is inverted compared to 88 | /// `draw_masked()`: 0 is fully transparent `src` (and fully opaque `dst`), 89 | /// 7 is fully opaque `src` (and fully transparent `dst`). 90 | fn draw_masked_color(&mut self, src: Rgb15, dst: Option, pos: Point, 91 | mask: &TextureHandle); 92 | 93 | /// Similar to `draw_masked_color()` but the `mask` specifies combined alpha and lightening 94 | /// values. This is used for drawing screen glare effect in dialog window. 95 | fn draw_highlight(&mut self, color: Rgb15, pos: Point, mask: &TextureHandle); 96 | 97 | fn draw_translucent(&mut self, tex: &TextureHandle, pos: Point, color: Rgb15, light: u32); 98 | fn draw_translucent_dark(&mut self, tex: &TextureHandle, pos: Point, color: Rgb15, light: u32); 99 | fn draw_outline(&mut self, tex: &TextureHandle, pos: Point, outline: Outline); 100 | fn draw_text(&mut self, text: &bstr, pos: Point, font: FontKey, color: Rgb15, 101 | options: &font::DrawOptions); 102 | 103 | fn draw_scaled(&mut self, src: &TextureHandle, dst: Rect); 104 | } -------------------------------------------------------------------------------- /src/fs/dat/v2.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::io::{BufReader, Error, ErrorKind, Result, SeekFrom}; 5 | use std::io::prelude::*; 6 | 7 | use std::path::{Path, PathBuf}; 8 | 9 | use super::super::{Metadata, Provider}; 10 | use super::util::{build_normalized_path, normalize_path}; 11 | 12 | pub fn new_provider>(path: P) -> Result> { 13 | Ok(Box::new(Dat::new(path)?)) 14 | } 15 | 16 | #[derive(Debug)] 17 | struct Dat { 18 | path: PathBuf, 19 | files: HashMap, 20 | } 21 | 22 | #[derive(Debug)] 23 | struct DatFile { 24 | offset: u32, 25 | size: u32, 26 | compressed_size: u32, 27 | } 28 | 29 | impl Dat { 30 | pub fn new>(path: P) -> Result { 31 | let mut reader = BufReader::new(File::open(path.as_ref())?); 32 | 33 | reader.seek(SeekFrom::End(-8))?; 34 | let file_list_size = reader.read_u32::()?; 35 | let size = reader.read_u32::()?; 36 | 37 | if path.as_ref().metadata()?.len() != size as u64 { 38 | return Err(Error::new(ErrorKind::InvalidData, 39 | "Actual file size and in-file size differ")); 40 | } 41 | 42 | if file_list_size > size - 8 { 43 | return Err(Error::new(ErrorKind::InvalidData, "File list size is too big")); 44 | } 45 | 46 | let file_list_offset = size - file_list_size - 8; 47 | reader.seek(SeekFrom::Start(file_list_offset as u64))?; 48 | 49 | let file_count = reader.read_u32::()?; 50 | 51 | let mut files = HashMap::with_capacity(file_count as usize); 52 | 53 | for _ in 0..file_count { 54 | let path = read_path(&mut reader)?; 55 | let compressed = (reader.read_u8()? & 1) != 0; 56 | let size = reader.read_u32::()?; 57 | let compressed_size = reader.read_u32::()?; 58 | let offset = reader.read_u32::()?; 59 | 60 | files.insert(path, 61 | DatFile { 62 | offset, 63 | size, 64 | compressed_size: if compressed { 65 | compressed_size 66 | } else { 67 | 0 68 | }, 69 | }); 70 | } 71 | 72 | Ok(Dat { 73 | path: path.as_ref().to_path_buf(), 74 | files, 75 | }) 76 | } 77 | 78 | fn file(&self, path: &str) -> Result<&DatFile> { 79 | self.files.get(&normalize_path(path)) 80 | .ok_or_else(|| Error::new(ErrorKind::NotFound, "file not found")) 81 | } 82 | } 83 | 84 | impl DatFile { 85 | fn is_compressed(&self) -> bool { 86 | self.compressed_size != 0 87 | } 88 | } 89 | 90 | impl Provider for Dat { 91 | fn reader(&self, path: &str) -> Result> { 92 | let dat_file = self.file(path)?; 93 | let read_size = if dat_file.is_compressed() { 94 | dat_file.compressed_size 95 | } else { 96 | dat_file.size 97 | }; 98 | let reader = BufReader::new({ 99 | let mut f = File::open(&self.path)?; 100 | f.seek(SeekFrom::Start(dat_file.offset as u64))?; 101 | f.take(read_size as u64) 102 | }); 103 | Ok(if dat_file.is_compressed() { 104 | use flate2::bufread::ZlibDecoder; 105 | Box::new(BufReader::new(ZlibDecoder::new(reader))) 106 | } else { 107 | Box::new(reader) 108 | }) 109 | } 110 | 111 | fn metadata(&self, path: &str) -> Result { 112 | self.file(path).map(|f| Metadata { len: f.size as u64 }) 113 | } 114 | } 115 | 116 | fn read_path(r: &mut R) -> Result { 117 | let l = r.read_u32::()? as usize; 118 | let mut s = String::with_capacity(l); 119 | for _ in 0..l { 120 | let c = r.read_u8()?; 121 | assert!(c.is_ascii()); 122 | let c = c as char; 123 | build_normalized_path(&mut s, Some(c)); 124 | } 125 | build_normalized_path(&mut s, None); 126 | 127 | Ok(s) 128 | } 129 | -------------------------------------------------------------------------------- /src/game/fidget.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use std::time::{Duration, Instant}; 3 | 4 | use crate::asset::{CritterAnim, EntityKind, Flag}; 5 | use crate::game::sequence::ObjSequencer; 6 | use crate::game::sequence::frame_anim::*; 7 | use crate::game::sequence::stand::Stand; 8 | use crate::game::world::World; 9 | use crate::graphics::{EPoint, Point, Rect}; 10 | use crate::graphics::geometry::TileGridView; 11 | use crate::sequence::chain::Chain; 12 | use crate::util::random::random; 13 | 14 | pub struct Fidget { 15 | next_time: Instant, 16 | } 17 | 18 | impl Fidget { 19 | pub fn new(now: Instant) -> Self { 20 | Self { 21 | next_time: now + Self::next_delay(0), 22 | } 23 | } 24 | 25 | // dude_fidget() 26 | pub fn update(&mut self, 27 | time: Instant, 28 | world: &mut World, 29 | obj_sequencer: &mut ObjSequencer) 30 | { 31 | if time < self.next_time { 32 | return; 33 | } 34 | 35 | let elevation = world.elevation(); 36 | 37 | let hex_rect = world.camera().hex().enclose(Rect { 38 | left: world.camera().viewport.left - 320, 39 | top: world.camera().viewport.top - 190, 40 | right: world.camera().viewport.width() + 320, 41 | bottom: world.camera().viewport.height() + 190 42 | }); 43 | 44 | // TODO don't store the objects 45 | let mut objs = Vec::new(); 46 | for y in hex_rect.top..hex_rect.bottom { 47 | for x in hex_rect.left..hex_rect.right { 48 | for &objh in world.objects().at(EPoint::new(elevation, Point::new(x, y))) { 49 | let obj = world.objects().get(objh); 50 | if obj.flags.contains(Flag::TurnedOff) || 51 | obj.fid.kind() != EntityKind::Critter || 52 | obj.is_critter_dead() || 53 | !world.object_bounds(objh, true).intersects(world.camera().viewport) 54 | // FIXME 55 | // g_map_header.map_id == MAP_ID_WOODSMAN_ENCOUNTER && obj.pid == Some(Pid::ENCLAVE_PATROL) 56 | { 57 | continue; 58 | } 59 | objs.push(objh); 60 | } 61 | } 62 | } 63 | 64 | if !objs.is_empty() { 65 | let objh = objs[random(0, objs.len() as i32 - 1) as usize]; 66 | 67 | if obj_sequencer.is_running(objh) { 68 | debug!("fidget: object {:?} already has a running sequence", objh); 69 | return; 70 | } 71 | 72 | // FIXME 73 | // if ( obj == g_obj_dude 74 | // || ((art_name[0] = 0, art_get_base_name_(OBJ_TYPE_CRITTER, obj->art_fid & 0xFFF, art_name), art_name[0] == 'm') 75 | // || art_name[0] == 'M') 76 | // && (distance = 2 * stat_level_(g_obj_dude, STAT_PER), obj_dist_(obj, g_obj_dude) <= distance) ) 77 | // { 78 | // play_sfx = 1; 79 | // } 80 | // if ( play_sfx ) 81 | // { 82 | // sfx_name = gsnd_build_character_sfx_name_(obj, 0, 0); 83 | // register_object_play_sfx_((int)obj, (int)sfx_name, 0); 84 | // } 85 | 86 | let seq = Chain::new(); 87 | seq.control() 88 | .cancellable(FrameAnim::new(objh, 89 | FrameAnimOptions { anim: Some(CritterAnim::Stand), ..Default::default() })) 90 | .finalizing(Stand::new(objh)); 91 | obj_sequencer.replace(objh, seq); 92 | 93 | debug!("fidget: started fidget animation for object {:?}", objh); 94 | } else { 95 | debug!("fidget: no suitable objects"); 96 | } 97 | 98 | self.next_time = time + Self::next_delay(objs.len()); 99 | } 100 | 101 | fn next_delay(obj_count: usize) -> Duration { 102 | let factor = if obj_count == 0 { 103 | 7 104 | } else { 105 | (20 / obj_count).clamp(1, 7) 106 | }; 107 | let next_delay = random(0, 3000) + 1000 * factor as i32; 108 | Duration::from_millis(next_delay as u64) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Create snapshot release 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: # Allow manual trigger 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | actions: read 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Get latest successful workflow run 22 | id: get_run 23 | env: 24 | GH_TOKEN: ${{ github.token }} 25 | run: | 26 | run_id=$(gh run list \ 27 | --workflow=build.yml \ 28 | --branch=master \ 29 | --status=success \ 30 | --limit=1 \ 31 | --json databaseId \ 32 | --jq '.[0].databaseId') 33 | 34 | if [ -z "$run_id" ] || [ "$run_id" = "null" ]; then 35 | echo "No successful workflow runs found" 36 | exit 1 37 | fi 38 | 39 | echo "run_id=$run_id" >> $GITHUB_OUTPUT 40 | 41 | commit_sha=$(gh run view "$run_id" --json headSha --jq '.headSha') 42 | echo "commit_sha=$commit_sha" >> $GITHUB_OUTPUT 43 | echo "short_sha=${commit_sha:0:7}" >> $GITHUB_OUTPUT 44 | 45 | - name: Check if release already exists for this commit 46 | id: check_release 47 | env: 48 | GH_TOKEN: ${{ github.token }} 49 | run: | 50 | latest_release_tag=$(gh release list \ 51 | --limit=1 \ 52 | --json tagName \ 53 | --jq '.[0].tagName // empty') 54 | 55 | if [ -z "$latest_release_tag" ]; then 56 | echo "No existing releases found" 57 | echo "should_release=true" >> $GITHUB_OUTPUT 58 | exit 0 59 | fi 60 | 61 | # Get the commit SHA that the latest release tag points to 62 | latest_release_sha=$(git rev-list -n 1 "$latest_release_tag" 2>/dev/null || echo "") 63 | 64 | if [ -z "$latest_release_sha" ]; then 65 | echo "Could not find commit for tag $latest_release_tag" 66 | echo "should_release=true" >> $GITHUB_OUTPUT 67 | exit 0 68 | fi 69 | 70 | echo "Latest release tag: $latest_release_tag" 71 | echo "Latest release SHA: $latest_release_sha" 72 | echo "Current build SHA: ${{ steps.get_run.outputs.commit_sha }}" 73 | 74 | if [ "$latest_release_sha" = "${{ steps.get_run.outputs.commit_sha }}" ]; then 75 | echo "No changes since last release" 76 | echo "should_release=false" >> $GITHUB_OUTPUT 77 | else 78 | echo "New changes detected" 79 | echo "should_release=true" >> $GITHUB_OUTPUT 80 | fi 81 | 82 | - name: Download artifacts 83 | if: steps.check_release.outputs.should_release == 'true' 84 | env: 85 | GH_TOKEN: ${{ github.token }} 86 | run: | 87 | mkdir -p artifacts 88 | gh run download ${{ steps.get_run.outputs.run_id }} --dir artifacts 89 | 90 | - name: Compress artifacts 91 | if: steps.check_release.outputs.should_release == 'true' 92 | run: | 93 | mkdir -p release 94 | cd artifacts 95 | for dir in */; do 96 | name="${dir%/}" 97 | zip -r "../release/${name}.zip" "$dir" 98 | done 99 | 100 | - name: Generate release tag 101 | if: steps.check_release.outputs.should_release == 'true' 102 | id: tag 103 | run: | 104 | tag="snapshot-$(date -u +%Y%m%d-%H%M%S)-${{ steps.get_run.outputs.short_sha }}" 105 | echo "tag=$tag" >> $GITHUB_OUTPUT 106 | 107 | - name: Create Release 108 | if: steps.check_release.outputs.should_release == 'true' 109 | env: 110 | GH_TOKEN: ${{ github.token }} 111 | run: | 112 | gh release create "${{ steps.tag.outputs.tag }}" \ 113 | --title "${{ steps.tag.outputs.tag }}" \ 114 | --notes "Automated snapshot release from commit ${{ steps.get_run.outputs.commit_sha }}" \ 115 | --prerelease \ 116 | --target ${{ steps.get_run.outputs.commit_sha }} \ 117 | release/*.zip -------------------------------------------------------------------------------- /src/vm/instruction/impls/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! log_ { 2 | ($vm_state:expr) => { 3 | log::debug!("[0x{:06x}] {:?}", 4 | ($vm_state).opcode.unwrap().1, 5 | ($vm_state).opcode.unwrap().0); 6 | }; 7 | } 8 | 9 | macro_rules! log_a1 { 10 | ($vm_state:expr, $arg:expr) => { 11 | log::debug!("[0x{:06x}] {:?} ({:?})", 12 | ($vm_state).opcode.unwrap().1, 13 | ($vm_state).opcode.unwrap().0, 14 | $arg); 15 | } 16 | } 17 | 18 | macro_rules! log_a1r1 { 19 | ($vm_state:expr, $arg:expr, $res:expr) => { 20 | log::debug!("[0x{:06x}] {:?} ({:?}) -> ({:?})", 21 | ($vm_state).opcode.unwrap().1, 22 | ($vm_state).opcode.unwrap().0, 23 | $arg, $res); 24 | } 25 | } 26 | 27 | macro_rules! log_a1r2 { 28 | ($vm_state:expr, $arg:expr, $res1:expr, $res2:expr) => { 29 | log::debug!("[0x{:06x}] {:?} ({:?}) -> ({:?}, {:?})", 30 | ($vm_state).opcode.unwrap().1, 31 | ($vm_state).opcode.unwrap().0, 32 | $arg, $res1, $res2); 33 | } 34 | } 35 | 36 | macro_rules! log_a2 { 37 | ($vm_state:expr, $arg1:expr, $arg2:expr) => { 38 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?})", 39 | ($vm_state).opcode.unwrap().1, 40 | ($vm_state).opcode.unwrap().0, 41 | $arg1, $arg2); 42 | } 43 | } 44 | 45 | macro_rules! log_a2r1 { 46 | ($vm_state:expr, $arg1:expr, $arg2:expr, $res:expr) => { 47 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}) -> ({:?})", 48 | ($vm_state).opcode.unwrap().1, 49 | ($vm_state).opcode.unwrap().0, 50 | $arg1, $arg2, $res); 51 | } 52 | } 53 | 54 | macro_rules! log_a3 { 55 | ($vm_state:expr, $arg1:expr, $arg2:expr, $arg3:expr) => { 56 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}, {:?})", 57 | ($vm_state).opcode.unwrap().1, 58 | ($vm_state).opcode.unwrap().0, 59 | $arg1, $arg2, $arg3); 60 | } 61 | } 62 | 63 | macro_rules! log_a3r1 { 64 | ($vm_state:expr, $arg1:expr, $arg2:expr, $arg3:expr, $res:expr) => { 65 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}, {:?}) -> ({:?})", 66 | ($vm_state).opcode.unwrap().1, 67 | ($vm_state).opcode.unwrap().0, 68 | $arg1, $arg2, $arg3, $res); 69 | } 70 | } 71 | 72 | macro_rules! log_a4r1 { 73 | ($vm_state:expr, $arg1:expr, $arg2:expr, $arg3:expr, $arg4:expr, $res:expr) => { 74 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}, {:?}, {:?}) -> ({:?})", 75 | ($vm_state).opcode.unwrap().1, 76 | ($vm_state).opcode.unwrap().0, 77 | $arg1, $arg2, $arg3, $arg4, $res); 78 | } 79 | } 80 | 81 | macro_rules! log_a4 { 82 | ($vm_state:expr, $arg1:expr, $arg2:expr, $arg3:expr, $arg4:expr) => { 83 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}, {:?}, {:?})", 84 | ($vm_state).opcode.unwrap().1, 85 | ($vm_state).opcode.unwrap().0, 86 | $arg1, $arg2, $arg3, $arg4); 87 | } 88 | } 89 | 90 | macro_rules! log_a5 { 91 | ($vm_state:expr, $arg1:expr, $arg2:expr, $arg3:expr, $arg4:expr, $arg5:expr) => { 92 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}, {:?}, {:?}, {:?})", 93 | ($vm_state).opcode.unwrap().1, 94 | ($vm_state).opcode.unwrap().0, 95 | $arg1, $arg2, $arg3, $arg4, $arg5); 96 | } 97 | } 98 | 99 | macro_rules! log_a5r1 { 100 | ($vm_state:expr, $arg1:expr, $arg2:expr, $arg3:expr, $arg4:expr, $arg5:expr, $res:expr) => { 101 | log::debug!("[0x{:06x}] {:?} ({:?}, {:?}, {:?}, {:?}, {:?}) -> ({:?})", 102 | ($vm_state).opcode.unwrap().1, 103 | ($vm_state).opcode.unwrap().0, 104 | $arg1, $arg2, $arg3, $arg4, $arg5, $res); 105 | } 106 | } 107 | 108 | macro_rules! log_r1 { 109 | ($vm_state:expr, $res:expr) => { 110 | log::debug!("[0x{:06x}] {:?} -> ({:?})", 111 | ($vm_state).opcode.unwrap().1, 112 | ($vm_state).opcode.unwrap().0, 113 | $res); 114 | } 115 | } 116 | 117 | macro_rules! log_stub { 118 | ($vm_state:expr) => { 119 | log::warn!("called {:?} which is a noop stub!", ($vm_state).opcode.unwrap().0); 120 | } 121 | } 122 | 123 | macro_rules! log_error { 124 | ($vm_state:expr, $msg:expr) => { 125 | log::error!("[{:?}] {}", ($vm_state).opcode.unwrap().0, $msg); 126 | } 127 | } -------------------------------------------------------------------------------- /src/fs/dat/v1.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{ReadBytesExt, BigEndian}; 2 | use std::collections::HashMap; 3 | use std::fs::File; 4 | use std::io::{BufReader, Error, ErrorKind, Result, SeekFrom}; 5 | use std::io::prelude::*; 6 | use std::path::{Path, PathBuf}; 7 | 8 | use super::lzss; 9 | use super::super::{Metadata, Provider}; 10 | use super::util::{build_normalized_path, normalize_path}; 11 | 12 | pub fn new_provider>(path: P) -> Result> { 13 | Ok(Box::new(Dat::new(path)?)) 14 | } 15 | 16 | #[derive(Debug)] 17 | struct Dat { 18 | path: PathBuf, 19 | files: HashMap, 20 | } 21 | 22 | #[derive(Debug)] 23 | struct DatFile { 24 | offset: u32, 25 | size: u32, 26 | compressed_size: u32, 27 | } 28 | 29 | impl Dat { 30 | pub fn new>(path: P) -> Result { 31 | let mut reader = BufReader::new(File::open(path.as_ref())?); 32 | 33 | let dir_count = reader.read_u32::()?; 34 | 35 | reader.seek(SeekFrom::Current(4 * 3))?; 36 | 37 | let mut dirs = Vec::with_capacity(dir_count as usize); 38 | for _ in 0..dir_count { 39 | dirs.push(read_path(&mut reader)?); 40 | } 41 | 42 | let mut files = HashMap::new(); 43 | 44 | for dir in &dirs { 45 | let file_count = reader.read_u32::()?; 46 | 47 | reader.seek(SeekFrom::Current(4 * 3))?; 48 | 49 | for _ in 0..file_count { 50 | let mut path = String::with_capacity(dir.len() + 257); 51 | if !dir.is_empty() { 52 | path.push_str(dir); 53 | if !path.ends_with('\\') { 54 | path.push('\\'); 55 | } 56 | } 57 | read_path_into(&mut reader, &mut path)?; 58 | 59 | let _flags = reader.read_u32::()?; 60 | let offset = reader.read_u32::()?; 61 | let size = reader.read_u32::()?; 62 | let compressed_size = reader.read_u32::()?; 63 | 64 | files.insert(path, 65 | DatFile { 66 | offset, 67 | size, 68 | compressed_size, 69 | }); 70 | } 71 | } 72 | 73 | Ok(Dat { 74 | path: path.as_ref().to_path_buf(), 75 | files, 76 | }) 77 | } 78 | 79 | fn file(&self, path: &str) -> Result<&DatFile> { 80 | self.files.get(&normalize_path(path)) 81 | .ok_or_else(|| Error::new(ErrorKind::NotFound, "file not found")) 82 | } 83 | } 84 | 85 | impl DatFile { 86 | fn is_compressed(&self) -> bool { 87 | self.compressed_size != 0 88 | } 89 | } 90 | 91 | impl Provider for Dat { 92 | fn reader(&self, path: &str) -> Result> { 93 | let dat_file = self.file(path)?; 94 | let read_size = if dat_file.is_compressed() { 95 | dat_file.compressed_size 96 | } else { 97 | dat_file.size 98 | }; 99 | let reader = BufReader::new({ 100 | let mut f = File::open(&self.path)?; 101 | f.seek(SeekFrom::Start(dat_file.offset as u64))?; 102 | f.take(read_size as u64) 103 | }); 104 | Ok(if dat_file.is_compressed() { 105 | // TODO make LzssDecoder implement BufRead 106 | Box::new(BufReader::new(lzss::LzssDecoder::new(reader, dat_file.size as u64))) 107 | } else { 108 | Box::new(reader) 109 | }) 110 | } 111 | 112 | fn metadata(&self, path: &str) -> Result { 113 | self.file(path).map(|f| Metadata { len: f.size as u64 }) 114 | } 115 | } 116 | 117 | fn read_path(reader: &mut R) -> Result { 118 | let mut r = String::new(); 119 | read_path_into(reader, &mut r)?; 120 | Ok(r) 121 | } 122 | 123 | fn read_path_into(reader: &mut R, result: &mut String) -> Result<()> { 124 | let l = reader.read_u8()? as usize; 125 | 126 | if result.capacity() < l { 127 | let rl = result.len(); 128 | result.reserve_exact(l - rl); 129 | } 130 | 131 | for _ in 0..l { 132 | let c = reader.read_u8()?; 133 | assert!(c.is_ascii()); 134 | let c = c as char; 135 | build_normalized_path(result, Some(c)); 136 | } 137 | build_normalized_path(result, None); 138 | 139 | Ok(()) 140 | } 141 | -------------------------------------------------------------------------------- /src/game/dialog.rs: -------------------------------------------------------------------------------- 1 | use bstring::{bstr, BString}; 2 | 3 | use crate::asset::frame::FrameId; 4 | use crate::asset::message::BULLET_STR; 5 | use crate::game::object; 6 | use crate::game::script::ScriptIid; 7 | use crate::game::world::World; 8 | use crate::graphics::{Point, Rect}; 9 | use crate::graphics::color::{Rgb15, GREEN}; 10 | use crate::graphics::font::FontKey; 11 | use crate::graphics::sprite::{Sprite, Effect}; 12 | use crate::ui::*; 13 | use crate::ui::message_panel::{MessagePanel, MouseControl}; 14 | use crate::ui::panel::Panel; 15 | 16 | pub struct OptionInfo { 17 | pub proc_id: Option, 18 | } 19 | 20 | pub struct Dialog { 21 | window: Handle, 22 | reply: Handle, 23 | options_widget: Handle, 24 | options: Vec, 25 | sid: ScriptIid, 26 | saved_camera_origin: Point, 27 | pub obj: object::Handle, 28 | pub running: bool, 29 | } 30 | 31 | impl Dialog { 32 | pub fn show(ui: &mut Ui, world: &mut World, obj: object::Handle) -> Self { 33 | let window = ui.new_window(Rect::with_size(0, 0, 640, 480), 34 | Some(Sprite::new(FrameId::ALLTLK))); 35 | 36 | ui.new_widget(window, Rect::with_size(0, 480 - 190, 640, 480), None, 37 | Some(Sprite::new(FrameId::DI_TALK)), Panel::new()); 38 | 39 | let reply = MessagePanel::new(ui.fonts().clone(), FontKey::antialiased(1), GREEN); 40 | let reply = ui.new_widget(window, Rect::with_size(135, 235, 382, 47), None, None, reply); 41 | 42 | let mut options = MessagePanel::new(ui.fonts().clone(), FontKey::antialiased(1), GREEN); 43 | options.set_mouse_control(MouseControl::Pick); 44 | options.set_highlight_color(Rgb15::new(31, 31, 15)); 45 | options.set_message_spacing(2); 46 | let options_widget = ui.new_widget(window, Rect::with_size(127, 340, 397, 100), None, None, options); 47 | 48 | let mut spr = Sprite::new(FrameId::HILIGHT1); 49 | spr.effect = Some(Effect::Highlight { color: Rgb15::from_packed(0x4631) }); 50 | ui.new_widget(window, Rect::with_size(426, 15, 1, 1), None, Some(spr), 51 | Panel::new()); 52 | 53 | let mut spr = Sprite::new(FrameId::HILIGHT2); 54 | spr.effect = Some(Effect::Highlight { color: Rgb15::from_packed(0x56ab) }); 55 | ui.new_widget(window, Rect::with_size(129, 214 - 2 - 131, 1, 1), None, Some(spr), 56 | Panel::new()); 57 | 58 | let (obj_pos, sid) = { 59 | let obj = world.objects().get(obj); 60 | let (sid, _) = obj.script.unwrap(); 61 | (obj.pos().point, sid) 62 | }; 63 | 64 | let saved_camera_origin = world.camera().origin; 65 | world.camera_mut().align(obj_pos, Point::new(640 / 2, 235 / 2)); 66 | 67 | Self { 68 | window, 69 | reply, 70 | options_widget, 71 | options: Vec::new(), 72 | running: false, 73 | sid, 74 | saved_camera_origin, 75 | obj, 76 | } 77 | } 78 | 79 | pub fn hide(self, ui: &mut Ui, world: &mut World) { 80 | ui.remove(self.window); 81 | world.camera_mut().origin = self.saved_camera_origin; 82 | } 83 | 84 | pub fn is(&self, widget: Handle) -> bool { 85 | self.options_widget == widget 86 | } 87 | 88 | pub fn set_reply(&self, ui: &mut Ui, reply: impl AsRef) { 89 | let mut replyw = ui.widget_mut::(self.reply); 90 | replyw.clear_messages(); 91 | replyw.push_message(BString::concat(&[&b" "[..], reply.as_ref().as_bytes()])) 92 | } 93 | 94 | pub fn clear_options(&mut self, ui: &mut Ui) { 95 | ui.widget_mut::(self.options_widget).clear_messages(); 96 | self.options.clear(); 97 | } 98 | 99 | pub fn add_option(&mut self, ui: &mut Ui, text: impl AsRef, proc_id: Option) { 100 | let mut optionsw = ui.widget_mut::(self.options_widget); 101 | optionsw.push_message(Self::build_option(text.as_ref())); 102 | self.options.push(OptionInfo { 103 | proc_id, 104 | }); 105 | } 106 | 107 | pub fn option(&self, id: u32) -> &OptionInfo { 108 | &self.options[id as usize] 109 | } 110 | 111 | pub fn is_empty(&self) -> bool { 112 | self.options.is_empty() 113 | } 114 | 115 | pub fn sid(&self) -> ScriptIid { 116 | self.sid 117 | } 118 | 119 | fn build_option(option: &bstr) -> BString { 120 | BString::concat(&[&b" "[..], BULLET_STR, &b" "[..], option.as_bytes()]) 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /src/game/ui/move_window.rs: -------------------------------------------------------------------------------- 1 | use crate::ui::{self, Ui}; 2 | use crate::graphics::sprite::{Sprite, Effect}; 3 | use crate::asset::frame::FrameId; 4 | use crate::graphics::Rect; 5 | use crate::ui::image_text::ImageText; 6 | use crate::asset::message::Messages; 7 | use crate::ui::panel::{self, Panel}; 8 | use crate::graphics::font::{FontKey, DrawOptions, HorzAlign, VertAlign}; 9 | use crate::graphics::color::Rgb15; 10 | use crate::ui::button::{Button, Text}; 11 | use bstring::bfmt::ToBString; 12 | use crate::ui::command::move_window::Command; 13 | use crate::ui::command::{UiCommand, UiCommandData}; 14 | 15 | pub struct MoveWindow { 16 | max: u32, 17 | win: ui::Handle, 18 | count: ui::Handle, 19 | value: u32, 20 | } 21 | 22 | impl MoveWindow { 23 | pub fn show(item_fid: FrameId, max: u32, msgs: &Messages, ui: &mut Ui) -> Self { 24 | assert!(max > 0); 25 | 26 | let win = ui.new_window(Rect::with_size(140, 80, 259, 162), 27 | Some(Sprite::new(FrameId::INVENTORY_MOVE_MULTIPLE_WINDOW))); 28 | ui.widget_base_mut(win).set_modal(true); 29 | 30 | let mut header = Panel::new(); 31 | header.set_text(Some(panel::Text { 32 | text: msgs.get(21).unwrap().text.clone(), 33 | font: FontKey::antialiased(3), 34 | color: Rgb15::from_packed(0x5263), 35 | options: DrawOptions { 36 | horz_align: HorzAlign::Center, 37 | ..Default::default() 38 | }, 39 | })); 40 | ui.new_widget(win, Rect::with_size(0, 9, 259, 162), None, None, header); 41 | 42 | let mut item = Sprite::new(item_fid); 43 | item.effect = Some(Effect::Fit { 44 | width: 90, 45 | height: 61, 46 | }); 47 | ui.new_widget(win, Rect::with_size(16, 46, 1, 1), None, Some(item), Panel::new()); 48 | 49 | let count = ui.new_widget(win, Rect::with_size(125, 45, 1, 1), None, None, 50 | ImageText::big_numbers()); 51 | 52 | ui.new_widget(win, Rect::with_size(200, 46, 16, 12), None, None, 53 | Button::new(FrameId::BUTTON_PLUS_UP, FrameId::BUTTON_PLUS_DOWN, 54 | Some(UiCommandData::MoveWindow(Command::Inc)))); 55 | ui.new_widget(win, Rect::with_size(200, 46 + 12, 16, 12), None, None, 56 | Button::new(FrameId::BUTTON_MINUS_UP, FrameId::BUTTON_MINUS_DOWN, 57 | Some(UiCommandData::MoveWindow(Command::Dec)))); 58 | 59 | ui.new_widget(win, Rect::with_size(98, 128, 15, 16), None, None, 60 | Button::new(FrameId::SMALL_RED_BUTTON_UP, FrameId::SMALL_RED_BUTTON_DOWN, 61 | Some(UiCommandData::MoveWindow(Command::Hide { ok: true })))); 62 | ui.new_widget(win, Rect::with_size(148, 128, 15, 16), None, None, 63 | Button::new(FrameId::SMALL_RED_BUTTON_UP, FrameId::SMALL_RED_BUTTON_DOWN, 64 | Some(UiCommandData::MoveWindow(Command::Hide { ok: false })))); 65 | 66 | let mut text = Text::new(msgs.get(22).unwrap().text.clone(), FontKey::antialiased(3)); 67 | text.color = Rgb15::from_packed(0x5263); 68 | text.options.horz_align = HorzAlign::Center; 69 | text.options.vert_align = VertAlign::Middle; 70 | let mut all = Button::new(FrameId::BUTTON_ALL_UP, FrameId::BUTTON_ALL_DOWN, 71 | Some(UiCommandData::MoveWindow(Command::Max))); 72 | all.set_text(Some(text)); 73 | ui.new_widget(win, Rect::with_size(121, 80, 94, 33), None, None, all); 74 | 75 | let r = Self { 76 | max: std::cmp::min(max, 99999), 77 | win, 78 | count, 79 | value: 1, 80 | }; 81 | r.sync(ui); 82 | r 83 | } 84 | 85 | pub fn hide(self, ui: &mut Ui) { 86 | ui.remove(self.win); 87 | } 88 | 89 | pub fn value(&self) -> u32 { 90 | self.value 91 | } 92 | 93 | pub fn handle(&mut self, cmd: UiCommand, ui: &Ui) { 94 | if let UiCommandData::MoveWindow(cmd) = cmd.data { 95 | let new_value = match cmd { 96 | Command::Hide { .. } => { 97 | return; 98 | } 99 | Command::Inc => std::cmp::min(self.value + 1, self.max), 100 | Command::Dec => std::cmp::max(self.value - 1, 1), 101 | Command::Max => self.max, 102 | }; 103 | if new_value != self.value { 104 | self.value = new_value; 105 | self.sync(ui); 106 | } 107 | } 108 | } 109 | 110 | fn sync(&self, ui: &Ui) { 111 | *ui.widget_mut::(self.count).text_mut() = 112 | format!("{:05}", self.value).to_bstring(); 113 | } 114 | } -------------------------------------------------------------------------------- /src/asset/proto/id.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt}; 2 | use num_traits::FromPrimitive; 3 | use std::fmt; 4 | use std::io::{self, Error, ErrorKind, prelude::*}; 5 | 6 | use crate::asset::EntityKind; 7 | 8 | #[derive(Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)] 9 | pub struct ProtoId(u32); 10 | 11 | impl ProtoId { 12 | pub const DUDE: Self = unsafe { Self::from_packed_unchecked(0x1000000) }; 13 | pub const SHIV: Self = unsafe { Self::from_packed_unchecked(0x17F) }; 14 | pub const POWER_ARMOR: Self = unsafe { Self::from_packed_unchecked(3) }; 15 | pub const HARDENED_POWER_ARMOR: Self = unsafe { Self::from_packed_unchecked(0xE8) }; 16 | pub const ADVANCED_POWER_ARMOR: Self = unsafe { Self::from_packed_unchecked(0x15C) }; 17 | pub const ADVANCED_POWER_ARMOR_MK2: Self = unsafe { Self::from_packed_unchecked(0x15D) }; 18 | pub const MIRRORED_SHADES: Self = unsafe { Self::from_packed_unchecked(0x1B1) }; 19 | pub const EXIT_AREA_FIRST: Self = unsafe { Self::from_packed_unchecked(0x5000010) }; 20 | pub const EXIT_AREA_LAST: Self = unsafe { Self::from_packed_unchecked(0x5000017) }; 21 | pub const RADIOACTIVE_GOO_FIRST: Self = unsafe { Self::from_packed_unchecked(0x20003D9) }; 22 | pub const RADIOACTIVE_GOO_LAST: Self = unsafe { Self::from_packed_unchecked(0x20003DC) }; 23 | pub const ACTIVE_FLARE: Self = unsafe { Self::from_packed_unchecked(0xCD) }; 24 | pub const ACTIVE_DYNAMITE: Self = unsafe { Self::from_packed_unchecked(0xCE) }; 25 | pub const ACTIVE_PLASTIC_EXPLOSIVE: Self = unsafe { Self::from_packed_unchecked(0xD1) }; 26 | pub const SCROLL_BLOCKER: Self = unsafe { Self::from_packed_unchecked(0x0500000c) }; 27 | pub const BOTTLE_CAPS: Self = unsafe { Self::from_packed_unchecked(0x29) }; 28 | pub const SOLAR_SCORCHER: Self = unsafe { Self::from_packed_unchecked(390) }; 29 | 30 | pub fn new(kind: EntityKind, id: u32) -> Option { 31 | if id <= 0xffffff { 32 | Some(Self((kind as u32) << 24 | id)) 33 | } else { 34 | None 35 | } 36 | } 37 | 38 | const unsafe fn from_packed_unchecked(v: u32) -> Self { 39 | Self(v) 40 | } 41 | 42 | pub fn from_packed(v: u32) -> Option { 43 | let kind = EntityKind::from_u32(v >> 24)?; 44 | let id = v & 0xffffff; 45 | Self::new(kind, id) 46 | } 47 | 48 | pub fn pack(self) -> u32 { 49 | self.0 50 | } 51 | 52 | pub fn read(rd: &mut impl Read) -> io::Result { 53 | let v = rd.read_u32::()?; 54 | Self::from_packed(v) 55 | .ok_or_else(|| Error::new(ErrorKind::InvalidData, 56 | format!("malformed PID: {:x}", v))) 57 | } 58 | 59 | pub fn read_opt(rd: &mut impl Read) -> io::Result> { 60 | let v = rd.read_i32::()?; 61 | Ok(if v >= 0 { 62 | Some(Self::from_packed(v as u32) 63 | .ok_or_else(|| Error::new(ErrorKind::InvalidData, 64 | format!("malformed PID: {:x}", v)))?) 65 | } else { 66 | None 67 | }) 68 | } 69 | 70 | pub fn kind(self) -> EntityKind { 71 | EntityKind::from_u32(self.0 >> 24).unwrap() 72 | } 73 | 74 | /// Returns ID that is unique among entities of the same `EntityKind`. 75 | /// The result is in range `[0..0xffffff]`. 76 | pub fn id(self) -> u32 { 77 | self.0 & 0xffffff 78 | } 79 | 80 | pub fn is_dude(self) -> bool { 81 | self == Self::DUDE 82 | } 83 | 84 | pub fn is_exit_area(self) -> bool { 85 | self >= Self::EXIT_AREA_FIRST && self <= Self::EXIT_AREA_LAST 86 | } 87 | 88 | pub fn is_radioactive_goo(self) -> bool { 89 | self >= Self::RADIOACTIVE_GOO_FIRST && self <= Self::RADIOACTIVE_GOO_LAST 90 | } 91 | } 92 | 93 | impl fmt::Debug for ProtoId { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | write!(f, "ProtoId(0x{:08x})", self.0) 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use super::*; 102 | 103 | #[test] 104 | fn test() { 105 | let pid = ProtoId::new(EntityKind::Item, 0).unwrap(); 106 | assert_eq!(pid.id(), 0); 107 | assert_eq!(pid.pack(), 0x00_000000); 108 | assert!(!pid.is_dude()); 109 | assert_eq!(pid, ProtoId::from_packed(pid.pack()).unwrap()); 110 | 111 | let pid = ProtoId::new(EntityKind::Skilldex, 0xffffff).unwrap(); 112 | assert_eq!(pid.id(), 0xffffff); 113 | assert_eq!(pid.pack(), 0x0a_ffffff); 114 | assert!(!pid.is_dude()); 115 | assert_eq!(pid, ProtoId::from_packed(pid.pack()).unwrap()); 116 | 117 | assert!(ProtoId::new(EntityKind::Critter, 0).unwrap().is_dude()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/ui/button.rs: -------------------------------------------------------------------------------- 1 | use bstring::BString; 2 | use linearize::{static_map, StaticMap}; 3 | 4 | use crate::graphics::font::{DrawOptions, FontKey, HorzAlign, VertAlign}; 5 | use crate::graphics::color::Rgb15; 6 | use crate::graphics::sprite::Sprite; 7 | use crate::ui::command::UiCommandData; 8 | use super::*; 9 | 10 | #[derive(Clone, Copy, Debug, Linearize, Eq, PartialEq, Ord, PartialOrd)] 11 | pub enum State { 12 | Disabled, 13 | Down, 14 | Up, 15 | } 16 | 17 | pub struct Config { 18 | pub background: Option, 19 | pub text: Option, 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | pub struct Text { 24 | pub pos: Point, 25 | pub text: BString, 26 | pub font: FontKey, 27 | pub color: Rgb15, 28 | pub options: DrawOptions, 29 | } 30 | 31 | impl Text { 32 | pub fn new(text: BString, font: FontKey) -> Self { 33 | Self { 34 | text, 35 | font, 36 | pos: Default::default(), 37 | color: Default::default(), 38 | options: Default::default(), 39 | } 40 | } 41 | } 42 | 43 | pub struct Button { 44 | configs: StaticMap, 45 | command: Option, 46 | state: State, 47 | } 48 | 49 | impl Button { 50 | pub fn new(up: FrameId, down: FrameId, command: Option) -> Self { 51 | Self { 52 | configs: static_map! { 53 | State::Disabled => Config { 54 | background: None, 55 | text: None, 56 | }, 57 | State::Down => Config { 58 | background: Some(Sprite::new(down)), 59 | text: None, 60 | }, 61 | State::Up => Config { 62 | background: Some(Sprite::new(up)), 63 | text: None, 64 | }, 65 | }, 66 | command, 67 | state: State::Up, 68 | } 69 | } 70 | 71 | pub fn config(&self, state: State) -> &Config { 72 | &self.configs[state] 73 | } 74 | 75 | pub fn config_mut(&mut self, state: State) -> &mut Config { 76 | &mut self.configs[state] 77 | } 78 | 79 | pub fn set_text(&mut self, text: Option) { 80 | self.configs[State::Down].text = text.clone(); 81 | self.configs[State::Up].text = text; 82 | } 83 | 84 | pub fn set_enabled(&mut self, enabled: bool) { 85 | self.state = if enabled { 86 | State::Up 87 | } else { 88 | State::Disabled 89 | }; 90 | } 91 | } 92 | 93 | impl Widget for Button { 94 | fn handle_event(&mut self, mut ctx: HandleEvent) { 95 | match ctx.event { 96 | Event::MouseDown { button, .. } if button == MouseButton::Left && self.state != State::Disabled => { 97 | self.state = State::Down; 98 | ctx.capture(); 99 | } 100 | Event::MouseMove { pos } if ctx.is_captured() => { 101 | // FIXME should optionally hit test the frame as in original. 102 | self.state = if ctx.base.rect.contains(pos) { 103 | State::Down 104 | } else { 105 | State::Up 106 | } 107 | } 108 | Event::MouseUp { pos, button } if button == MouseButton::Left && self.state != State::Disabled => { 109 | self.state = State::Up; 110 | // FIXME should optionally hit test the frame as in original. 111 | if ctx.base.rect.contains(pos) 112 | && let Some(cmd) = self.command 113 | { 114 | ctx.out(cmd); 115 | } 116 | ctx.release(); 117 | } 118 | _ => {} 119 | } 120 | } 121 | 122 | fn render(&mut self, ctx: Render) { 123 | let config = &self.configs[self.state]; 124 | let base_rect = ctx.base.unwrap().rect; 125 | if let Some(mut background) = config.background { 126 | background.pos += base_rect.top_left(); 127 | background.render(ctx.canvas, ctx.frm_db); 128 | } 129 | if let Some(text) = config.text.as_ref() { 130 | let mut pos = base_rect.top_left() + text.pos; 131 | if text.options.horz_align == HorzAlign::Center { 132 | pos.x += base_rect.width() / 2; 133 | } 134 | if text.options.vert_align == VertAlign::Middle { 135 | pos.y += base_rect.height() / 2; 136 | } 137 | ctx.canvas.draw_text( 138 | &text.text, 139 | pos, 140 | text.font, 141 | text.color, 142 | &text.options); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/asset/script/db.rs: -------------------------------------------------------------------------------- 1 | use bstring::bstr; 2 | use std::collections::HashMap; 3 | use std::io::{self, Error, ErrorKind, prelude::*}; 4 | use std::rc::Rc; 5 | 6 | use super::ProgramId; 7 | use crate::asset::message::Messages; 8 | use crate::fs::FileSystem; 9 | 10 | #[derive(Debug, Eq, PartialEq)] 11 | pub struct ScriptInfo { 12 | pub name: String, 13 | pub local_var_count: usize, 14 | } 15 | 16 | pub struct ScriptDb { 17 | fs: Rc, 18 | infos: Vec, 19 | messages: HashMap, 20 | language: String, 21 | } 22 | 23 | impl ScriptDb { 24 | pub fn new(fs: Rc, language: &str) -> io::Result { 25 | let infos = read_lst(&mut fs.reader("scripts/scripts.lst")?)?; 26 | Ok(Self { 27 | fs, 28 | infos, 29 | messages: HashMap::new(), 30 | language: language.into(), 31 | }) 32 | } 33 | 34 | pub fn info(&self, program_id: ProgramId) -> Option<&ScriptInfo> { 35 | self.infos.get(program_id.index()) 36 | } 37 | 38 | pub fn load(&self, program_id: ProgramId) -> io::Result<(Box<[u8]>, &ScriptInfo)> { 39 | let info = self.info_ok(program_id)?; 40 | let path = format!("scripts/{}.int", info.name); 41 | let mut code = Vec::new(); 42 | self.fs.reader(&path)?.read_to_end(&mut code)?; 43 | Ok((code.into(), info)) 44 | } 45 | 46 | pub fn messages(&mut self, program_id: ProgramId) -> io::Result<&Messages> { 47 | if !self.messages.contains_key(&program_id) { 48 | let msgs = self.load_messages(program_id)?; 49 | self.messages.insert(program_id, msgs); 50 | } 51 | 52 | Ok(&self.messages[&program_id]) 53 | } 54 | 55 | fn load_messages(&self, program_id: ProgramId) -> io::Result { 56 | let info = self.info_ok(program_id)?; 57 | Messages::read_file(&self.fs, &self.language, &format!("dialog/{}.msg", info.name)) 58 | } 59 | 60 | fn info_ok(&self, program_id: ProgramId) -> io::Result<&ScriptInfo> { 61 | self.info(program_id) 62 | .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, 63 | format!("program id {} doesn't exist", program_id.val()))) 64 | } 65 | } 66 | 67 | fn read_lst(rd: &mut impl BufRead) -> io::Result> { 68 | let mut r = Vec::new(); 69 | for l in rd.lines() { 70 | let mut l = l?; 71 | l.make_ascii_lowercase(); 72 | 73 | const DOT_INT: &str = ".int"; 74 | const LOCAL_VARS: &str = "local_vars="; 75 | 76 | if let Some(i) = l.find(DOT_INT) { 77 | let name = l[..i].to_owned(); 78 | 79 | let l = &l[i + DOT_INT.len()..]; 80 | let i = l.find('#') 81 | .and_then(|i| l[i + 1..].find(LOCAL_VARS).map(|j| i + 1 + j + LOCAL_VARS.len())); 82 | let local_var_count = if let Some(i) = i { 83 | let mut l = &l.as_bytes()[i..]; 84 | while let Some(c) = l.last() { 85 | if c.is_ascii_digit() { 86 | break; 87 | } else { 88 | l = &l[..l.len() - 1]; 89 | } 90 | } 91 | let l: &bstr = l.into(); 92 | btoi::btoi::(l.as_bytes()) 93 | .map_err(|_| Error::new(ErrorKind::InvalidData, 94 | format!("error parsing local_vars in scripts lst file: {}", 95 | l.display())))? as usize 96 | } else { 97 | 0 98 | }; 99 | 100 | r.push(ScriptInfo { 101 | name, 102 | local_var_count, 103 | }); 104 | } 105 | } 106 | Ok(r) 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | 113 | #[test] 114 | fn read_lst_() { 115 | fn si(name: &str, local_var_count: usize) -> ScriptInfo { 116 | ScriptInfo { 117 | name: name.into(), 118 | local_var_count, 119 | } 120 | } 121 | let a = read_lst(&mut ::std::io::Cursor::new( 122 | "\n\ 123 | \t \t\t\n\ 124 | lines without dot int are ignored\n\ 125 | script1.int\n\ 126 | SCripT2.InT ; \tdsds\n\ 127 | scr ipt 3 .int #\n\ 128 | Test0.int ; Used to Test Scripts # local_vars=8\n\ 129 | FSBroDor.int ; Brother Hood Door # local_vars=3# non_digit\ 130 | ".as_bytes())).unwrap(); 131 | assert_eq!(a, vec![ 132 | si("script1", 0), 133 | si("script2", 0), 134 | si("scr ipt 3 ", 0), 135 | si("test0", 8), 136 | si("fsbrodor", 3), 137 | ]); 138 | } 139 | } -------------------------------------------------------------------------------- /src/graphics/color/palette.rs: -------------------------------------------------------------------------------- 1 | pub mod overlay; 2 | 3 | use super::*; 4 | 5 | #[derive(Clone)] 6 | pub struct Palette { 7 | color_idx_to_rgb18: [Rgb18; 256], 8 | rgb15_to_color_idx: [u8; 32768], 9 | mapped_colors: [bool; 256], 10 | } 11 | 12 | impl Palette { 13 | pub fn new(color_idx_to_rgb18: [Rgb18; 256], rgb15_to_color_idx: [u8; 32768], 14 | mapped_colors: [bool; 256]) -> Self { 15 | Self { 16 | color_idx_to_rgb18, 17 | rgb15_to_color_idx, 18 | mapped_colors, 19 | } 20 | } 21 | 22 | pub fn rgb(&self, color_idx: u8) -> Rgb

{ 23 | self.rgb18(color_idx).scale() 24 | } 25 | 26 | pub fn rgb15(&self, color_idx: u8) -> Rgb15 { 27 | self.rgb::(color_idx) 28 | } 29 | 30 | pub fn rgb18(&self, color_idx: u8) -> Rgb18 { 31 | self.color_idx_to_rgb18[color_idx as usize] 32 | } 33 | 34 | pub fn color_idx(&self, rgb: Rgb

) -> u8 { 35 | self.rgb15_to_color_idx[rgb.scale::().pack() as usize] 36 | } 37 | 38 | pub fn quantize(&self, rgb: Rgb

) -> Rgb

{ 39 | self.rgb(self.color_idx(rgb)) 40 | } 41 | 42 | pub fn darken(&self, color_idx: u8, amount: u8) -> u8 { 43 | if self.mapped_colors[color_idx as usize] { 44 | self.color_idx(self.rgb15(color_idx).darken(amount)) 45 | } else { 46 | color_idx 47 | } 48 | } 49 | 50 | pub fn lighten(&self, color_idx: u8, amount: u8) -> u8 { 51 | if self.mapped_colors[color_idx as usize] { 52 | self.color_idx(self.rgb15(color_idx).lighten(amount)) 53 | } else { 54 | color_idx 55 | } 56 | } 57 | 58 | pub fn blend(&self, color_idx1: u8, color_idx2: u8) -> u8 { 59 | let c1 = self.rgb15(color_idx1); 60 | let c2 = self.rgb15(color_idx2); 61 | self.color_idx(c1.blend(c2, |c| self.quantize(c))) 62 | } 63 | 64 | // alpha is [0..7] 65 | pub fn alpha_blend(&self, color_idx1: u8, color_idx2: u8, alpha: u8) -> u8 { 66 | let c1 = self.rgb15(color_idx1); 67 | let c2 = self.rgb15(color_idx2); 68 | let r = c1.alpha_blend(c2, alpha); 69 | self.color_idx(r) 70 | } 71 | 72 | /// Simulates color blend table lookup as it's done in the original. 73 | /// Blend table is combined from: 74 | /// 1. Tables for alpha blending `color_idx` into `base_color_idx` when `x` goes 75 | /// from 0 (opaque `color_idx`) to 7 (opaque `base_color_idx`). 76 | /// 2. Tables for lightening/darkening the `base_color_idx`. When `x == 8` the `base_color_idx` 77 | /// is darkened with amount 127. When `x` is in [9..15] lightening effect is applied with the 78 | /// [9..14] range mapped to [18..237] and 15 mapped to 18 (wrapped). 79 | /// TODO The darkening looks like a bug, likely darkening isn't desired at all and only 80 | /// lightening should be applied for the [9..15] range. The primary usage of the full blend 81 | /// table is for screen glare effect in dialog window. 82 | pub fn blend_lookup(&self, base_color_idx: u8, color_idx: u8, x: u8) -> u8 { 83 | match x { 84 | 0 => color_idx, 85 | 1..=7 => self.alpha_blend(color_idx, base_color_idx, 7 - x), 86 | amount => { 87 | let amount = amount - 8; 88 | let amount = ((amount as u32 * 0x10000 / 7 + 0xffff) >> 9) as u8; 89 | if amount <= 128 { 90 | self.darken(base_color_idx, amount) 91 | } else { 92 | self.lighten(base_color_idx, amount - 128) 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use super::*; 102 | use crate::util::test::ungz; 103 | 104 | fn palette() -> Palette { 105 | let data = ungz(include_bytes!("color.pal.gz")); 106 | crate::asset::palette::read_palette(&mut std::io::Cursor::new(&data[..])).unwrap() 107 | } 108 | 109 | #[test] 110 | fn color_idx() { 111 | let exp = ungz(include_bytes!("expected_rgb15_to_color_idx.bin.gz")); 112 | let pal = palette(); 113 | for rgb15 in 0..0x8000 { 114 | assert_eq!(pal.color_idx(Rgb15::from_packed(rgb15)), exp[rgb15 as usize]); 115 | } 116 | } 117 | 118 | #[test] 119 | fn blend_table() { 120 | let pal = palette(); 121 | let exp = ungz(include_bytes!("expected_blend_table_4631.bin.gz")); 122 | 123 | let base_idx = pal.color_idx(Rgb15::from_packed(0x4631)); 124 | 125 | for x in 0..8 { 126 | for c in 0..=255 { 127 | assert_eq!(pal.blend_lookup(base_idx, c, x), 128 | exp[x as usize * 256 + c as usize], "{} {}", x, c); 129 | } 130 | } 131 | } 132 | 133 | 134 | } -------------------------------------------------------------------------------- /src/fs/dat/lzss.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 2 | use std::cmp; 3 | use std::io::prelude::*; 4 | use std::io::{self, Cursor, Error, ErrorKind, Result}; 5 | 6 | pub struct LzssDecoder { 7 | reader: R, 8 | buf: Cursor>, 9 | buf_avail: usize, 10 | written: u64, 11 | expected_output_size: u64, 12 | state: State, 13 | } 14 | 15 | enum State { 16 | Ok, 17 | Done, 18 | Err, 19 | } 20 | 21 | impl LzssDecoder { 22 | pub fn new(reader: R, expected_output_size: u64) -> Self { 23 | LzssDecoder { 24 | reader, 25 | buf: Cursor::new(Vec::with_capacity(16 * 1024)), 26 | buf_avail: 0, 27 | written: 0, 28 | expected_output_size, 29 | state: State::Ok, 30 | } 31 | } 32 | 33 | fn fill_buf(&mut self) -> Result { 34 | match self.state { 35 | State::Ok => {} 36 | State::Done if self.buf_avail == 0 => return Ok(0), 37 | State::Done => {} 38 | State::Err => return Err(Error::new(ErrorKind::InvalidData, "Malformed LZSS stream")), 39 | } 40 | if self.buf_avail == 0 { 41 | self.buf.set_position(0); 42 | let block_written = lzss_decode_block(&mut self.reader, &mut self.buf)?; 43 | self.buf_avail = block_written as usize; 44 | self.written += block_written; 45 | if self.written == self.expected_output_size { 46 | self.state = State::Done; 47 | } else if block_written == 0 || self.written > self.expected_output_size { 48 | self.state = State::Err; 49 | } 50 | if let State::Err = self.state { 51 | return Err(Error::new(ErrorKind::InvalidData, "Malformed LZSS stream")); 52 | } 53 | self.buf.set_position(0); 54 | } 55 | Ok(self.buf_avail) 56 | } 57 | } 58 | 59 | impl Read for LzssDecoder { 60 | fn read(&mut self, buf: &mut [u8]) -> Result { 61 | let mut buf_written = 0; 62 | while buf_written < buf.len() && self.fill_buf()? != 0 { 63 | let to_write = cmp::min(self.buf_avail, buf.len() - buf_written); 64 | buf_written += self.buf.read(&mut buf[buf_written..(buf_written + to_write)])?; 65 | self.buf_avail -= to_write; 66 | } 67 | Ok(buf_written) 68 | } 69 | } 70 | 71 | pub fn lzss_decode_block(inp: &mut impl Read, out: &mut impl Write) -> Result { 72 | let block_descr = inp.read_i16::()? as i32; 73 | if block_descr == 0 { 74 | return Ok(0); 75 | } 76 | 77 | let block_size = u64::from(block_descr.unsigned_abs()); 78 | let block_written; 79 | if block_descr < 0 { 80 | block_written = io::copy(inp, out)?; 81 | if block_written != block_size { 82 | return Err(Error::new(ErrorKind::InvalidData, "Malformed LZSS stream")); 83 | } 84 | } else { // block_descr > 0 85 | block_written = lzss_decode_block_content(inp, block_size, out)?; 86 | } 87 | 88 | Ok(block_written) 89 | } 90 | 91 | fn lzss_decode_block_content( 92 | inp: &mut impl Read, 93 | block_size: u64, 94 | out: &mut impl Write, 95 | ) -> Result { 96 | const N: usize = 4096; 97 | const F: usize = 18; 98 | const THRESHOLD: usize = 2; 99 | 100 | let mut text_buf = [0x20; N + F - 1]; 101 | let mut r = N - F; 102 | let mut flags = 0i32; 103 | 104 | let mut block_read = 0u64; 105 | let mut block_written = 0u64; 106 | 107 | loop { 108 | flags >>= 1; 109 | if flags & 0x100 == 0 { 110 | if block_read >= block_size { 111 | break; 112 | } 113 | let b = inp.read_u8()? as i32; 114 | block_read += 1; 115 | 116 | if block_read >= block_size { 117 | break; 118 | } 119 | 120 | flags = b | 0xff00; 121 | } 122 | 123 | if (flags & 1) != 0 { 124 | let b = inp.read_u8()?; 125 | block_read += 1; 126 | 127 | out.write_u8(b)?; 128 | block_written += 1; 129 | 130 | if block_read >= block_size { 131 | break; 132 | } 133 | 134 | text_buf[r] = b; 135 | r = (r + 1) & (N - 1); 136 | } else { 137 | if block_read >= block_size { 138 | break; 139 | } 140 | 141 | let mut i = inp.read_u8()? as usize; 142 | block_read += 1; 143 | 144 | if block_read >= block_size { 145 | break; 146 | } 147 | 148 | let mut j = inp.read_u8()? as usize; 149 | block_read += 1; 150 | 151 | i |= (j & 0xf0) << 4; 152 | j = (j & 0x0f) + THRESHOLD; 153 | 154 | for k in 0..=j { 155 | let b = text_buf[(i + k) & (N - 1)]; 156 | 157 | out.write_u8(b)?; 158 | block_written += 1; 159 | 160 | text_buf[r] = b; 161 | r = (r + 1) & (N - 1); 162 | } 163 | } 164 | } 165 | 166 | Ok(block_written) 167 | } 168 | -------------------------------------------------------------------------------- /src/graphics/geometry/sqr.rs: -------------------------------------------------------------------------------- 1 | use num_traits::clamp; 2 | 3 | use crate::graphics::Point; 4 | use crate::graphics::geometry::TileGridView; 5 | 6 | pub const TILE_WIDTH: i32 = 80; 7 | pub const TILE_HEIGHT: i32 = 36; 8 | pub const TILE_CENTER: Point = Point::new(TILE_WIDTH / 2, TILE_HEIGHT / 2); 9 | 10 | // square_xy() 11 | pub fn from_screen(p: Point) -> Point { 12 | let x = p.x; 13 | let y = p.y - 12; 14 | 15 | let dx = 3 * x - 4 * y; 16 | let square_x = if dx >= 0 { 17 | dx / 192 18 | } else { 19 | (dx + 1) / 192 - 1 20 | }; 21 | 22 | let dy = 4 * y + x; 23 | let square_y = if dy >= 0 { 24 | dy / 128 25 | } else { 26 | ((dy + 1) / 128) - 1 27 | }; 28 | 29 | Point::new(square_x, square_y) 30 | } 31 | 32 | // square_coord_() 33 | pub fn to_screen(p: Point) -> Point { 34 | let screen_x = 48 * p.x + 32 * p.y; 35 | let screen_y = -12 * p.x + 24 * p.y; 36 | Point::new(screen_x, screen_y) 37 | } 38 | 39 | pub fn center_to_screen(p: Point) -> Point { 40 | p + TILE_CENTER 41 | } 42 | 43 | pub struct View { 44 | pub origin: Point, 45 | } 46 | 47 | impl View { 48 | pub fn new(origin: Point) -> Self { 49 | Self { 50 | origin, 51 | } 52 | } 53 | } 54 | 55 | impl TileGridView for View { 56 | fn screen_to_tile(&self, p: Point) -> Point { 57 | from_screen(p - self.origin) 58 | } 59 | 60 | fn tile_to_screen(&self, p: Point) -> Point { 61 | to_screen(p) + self.origin 62 | } 63 | 64 | fn center_to_screen(&self, p: Point) -> Point { 65 | center_to_screen(p) + self.origin 66 | } 67 | } 68 | 69 | #[derive(Clone, Debug)] 70 | pub struct TileGrid { 71 | // Width in tiles. 72 | width: i32, 73 | 74 | // Height in tiles. 75 | height: i32, 76 | } 77 | 78 | impl TileGrid { 79 | pub fn len(&self) -> usize { 80 | (self.width * self.height) as usize 81 | } 82 | 83 | pub fn width(&self) -> i32 { 84 | self.width 85 | } 86 | 87 | pub fn height(&self) -> i32 { 88 | self.height 89 | } 90 | 91 | /// Rectangular to linear coordinates with `x` axis inverted. 92 | /// This method should be used when converting linears for use in the original assets 93 | /// (maps, scripts etc). 94 | pub fn rect_to_linear_inv(&self, p: Point) -> Option { 95 | if self.is_in_bounds(p) { 96 | let x = self.width - 1 - p.x; 97 | Some((self.width * p.y + x) as u32) 98 | } else { 99 | None 100 | } 101 | } 102 | 103 | /// Linear to rectangular coordinates with `x` axis inverted. 104 | /// This method should be used when converting linears for use in the original assets 105 | /// (maps, scripts etc). 106 | pub fn linear_to_rect_inv(&self, num: u32) -> Point { 107 | let x = self.width - 1 - num as i32 % self.width; 108 | let y = num as i32 / self.width; 109 | Point::new(x, y) 110 | } 111 | 112 | /// Verifies the tile coordinates `p` are within (0, 0, width, height) boundaries. 113 | pub fn is_in_bounds(&self, p: Point) -> bool { 114 | p.x >= 0 && p.x < self.width && p.y >= 0 && p.y < self.height 115 | } 116 | 117 | pub fn clip(&self, p: Point) -> Point { 118 | Point { 119 | x: clamp(p.x, 0, self.width - 1), 120 | y: clamp(p.y, 0, self.height - 1), 121 | } 122 | } 123 | 124 | /// Inverts `x` coordinate. 0 becomes `width - 1` and `width - 1` becomes 0. 125 | pub fn invert_x(&self, x: i32) -> i32 { 126 | self.width - 1 - x 127 | } 128 | } 129 | 130 | impl Default for TileGrid { 131 | fn default() -> Self { 132 | Self { 133 | width: 100, 134 | height: 100, 135 | } 136 | } 137 | } 138 | 139 | #[cfg(test)] 140 | mod test { 141 | use super::*; 142 | 143 | #[allow(non_snake_case)] 144 | fn P(x: i32, y: i32) -> Point { 145 | Point::new(x, y) 146 | } 147 | 148 | #[test] 149 | fn view_from_screen() { 150 | let t = View::new(P(0xf0, 0xa8)); 151 | let square_xy = |x, y| { 152 | let p = t.screen_to_tile(P(x, y)); 153 | P(100 - 1 - p.x, p.y) 154 | }; 155 | assert_eq!(square_xy(0, 0), P(99, -8)); 156 | assert_eq!(square_xy(0x27f, 0x17b), P(97, 9)); 157 | } 158 | 159 | #[test] 160 | fn from_screen_() { 161 | assert_eq!(from_screen(P(0, 0)), P(0, -1)); 162 | assert_eq!(from_screen(P(0, 12)), P(0, 0)); 163 | assert_eq!(from_screen(P(0, 13)), P(-1, 0)); 164 | assert_eq!(from_screen(P(79, 0)), P(1, 0)); 165 | assert_eq!(from_screen(P(79, 25)), P(0, 1)); 166 | } 167 | 168 | #[test] 169 | fn view_to_screen() { 170 | let t = TileGrid::default(); 171 | let v = View::new(P(0x100, 0xb4)); 172 | assert_eq!(v.tile_to_screen(t.linear_to_rect_inv(0x1091)), P(4384, 492)); 173 | } 174 | 175 | #[test] 176 | fn to_screen_() { 177 | assert_eq!(to_screen(P(0, 0)), P(0, 0)); 178 | assert_eq!(to_screen(P(1, 0)), P(48, -12)); 179 | assert_eq!(to_screen(P(0, 1)), P(32, 24)); 180 | assert_eq!(to_screen(P(0, -1)), P(-32, -24)); 181 | assert_eq!(to_screen(P(-1, 0)), P(-48, 12)); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/game/ui/action_menu.rs: -------------------------------------------------------------------------------- 1 | use num_traits::clamp; 2 | use std::cmp; 3 | 4 | use crate::asset::frame::FrameId; 5 | use crate::graphics::{Point, Rect}; 6 | use crate::graphics::sprite::Sprite; 7 | use crate::ui::*; 8 | use crate::ui::command::UiCommandData; 9 | 10 | pub fn show(actions: Vec<(Action, UiCommandData)>, win: Handle, ui: &mut Ui) -> Handle { 11 | assert!(!actions.is_empty()); 12 | 13 | let (placement, saved_cursor) = { 14 | let mut win_base = ui.widget_base_mut(win); 15 | let saved_cursor = win_base.cursor(); 16 | let cursor_pos = ui.cursor_pos() - win_base.rect().top_left(); 17 | let placement = Placement::new(actions.len() as u32, cursor_pos, win_base.rect()); 18 | win_base.set_cursor(Some(placement.cursor)); 19 | (placement, saved_cursor) 20 | }; 21 | 22 | ui.show_cursor_ghost(); 23 | ui.set_cursor_constraint(placement.rect); 24 | ui.set_cursor_pos(placement.rect.top_left()); 25 | 26 | let action_menu = ui.new_widget(win, placement.rect, Some(Cursor::Hidden), None, 27 | ActionMenu::new(actions, saved_cursor)); 28 | ui.capture(action_menu); 29 | 30 | action_menu 31 | } 32 | 33 | pub fn hide(action_menu: Handle, ui: &mut Ui) { 34 | ui.hide_cursor_ghost(); 35 | ui.clear_cursor_constraint(); 36 | ui.remove(action_menu); 37 | } 38 | 39 | pub struct Placement { 40 | pub rect: Rect, 41 | pub cursor: Cursor, 42 | } 43 | 44 | impl Placement { 45 | pub fn new(action_count: u32, cursor_pos: Point, bounds: Rect) -> Self { 46 | let flipped = bounds.right - cursor_pos.x < 47 | Action::ICON_OFFSET_X + Action::ICON_WIDTH; 48 | 49 | let height = Action::ICON_HEIGHT * action_count as i32; 50 | let offset_x = if flipped { 51 | - (Action::ICON_OFFSET_X + Action::ICON_WIDTH - 1) 52 | } else { 53 | Action::ICON_OFFSET_X 54 | }; 55 | let offset_y = cmp::min(bounds.bottom - cursor_pos.y - height, 0); 56 | let pos = cursor_pos + Point::new(offset_x, offset_y); 57 | let rect = Rect::with_size(pos.x, pos.y, Action::ICON_WIDTH, height); 58 | 59 | let cursor = if flipped { 60 | Cursor::ActionArrowFlipped 61 | } else { 62 | Cursor::ActionArrow 63 | }; 64 | 65 | Self { 66 | rect, 67 | cursor, 68 | } 69 | } 70 | } 71 | 72 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 73 | pub enum Action { 74 | Cancel, 75 | Drop, 76 | Inventory, 77 | Look, 78 | Push, 79 | Rotate, 80 | Talk, 81 | Unload, 82 | UseHand, 83 | UseSkill, 84 | } 85 | 86 | impl Action { 87 | pub const ICON_OFFSET_X: i32 = 29; 88 | pub const ICON_WIDTH: i32 = 40; 89 | pub const ICON_HEIGHT: i32 = 40; 90 | 91 | pub fn icons(self) -> (FrameId, FrameId) { 92 | use Action::*; 93 | use self::FrameId as F; 94 | match self { 95 | Cancel => (F::CANCELN, F::CANCELH), 96 | Drop => (F::DROPN, F::DROPH), 97 | Inventory => (F::INVENN, F::INVENH), 98 | Look => (F::LOOKN, F::LOOKH), 99 | Push => (F::PUSHN, F::PUSHH), 100 | Rotate => (F::ROTATEN, F::ROTATEH), 101 | Talk => (F::TALKN, F::TALKH), 102 | Unload => (F::UNLOADN, F::UNLOADH), 103 | UseHand => (F::USEGETN, F::USEGETH), 104 | UseSkill => (F::SKILLN, F::SKILLH), 105 | } 106 | } 107 | } 108 | 109 | struct ActionMenu { 110 | actions: Vec<(Action, UiCommandData)>, 111 | selection: u32, 112 | saved_cursor: Option, 113 | } 114 | 115 | impl ActionMenu { 116 | fn new(actions: Vec<(Action, UiCommandData)>, saved_cursor: Option) -> Self { 117 | Self { 118 | actions, 119 | selection: 0, 120 | saved_cursor, 121 | } 122 | } 123 | 124 | fn update_selection(&mut self, base: &Base, mouse_pos: Point) { 125 | let rel_y = mouse_pos.y - base.rect().top; 126 | // Apply speed up to mouse movement. 127 | let rel_y = (rel_y as f64 * 1.5) as i32; 128 | self.selection = clamp(rel_y / Action::ICON_HEIGHT, 0, self.actions.len() as i32 - 1) as u32; 129 | } 130 | } 131 | 132 | impl Widget for ActionMenu { 133 | fn handle_event(&mut self, mut ctx: HandleEvent) { 134 | 135 | match ctx.event { 136 | Event::MouseMove { pos } => { 137 | self.update_selection(ctx.base, pos); 138 | }, 139 | Event::MouseUp { pos, .. } => { 140 | self.update_selection(ctx.base, pos); 141 | ctx.out(self.actions[self.selection as usize].1); 142 | } 143 | _ => {} 144 | } 145 | } 146 | 147 | fn render(&mut self, ctx: Render) { 148 | let mut pos = ctx.base.unwrap().rect().top_left(); 149 | for (i, icon) in self.actions.iter().map(|&(a, _)| a).enumerate() { 150 | let (normal, highl) = icon.icons(); 151 | let fid = if i as u32 == self.selection { 152 | highl 153 | } else { 154 | normal 155 | }; 156 | Sprite::new_with_pos(fid, pos).render(ctx.canvas, ctx.frm_db); 157 | 158 | pos.y += Action::ICON_HEIGHT; 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /src/asset/font.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; 2 | use log::*; 3 | use std::cmp; 4 | use std::io::{self, Error, ErrorKind, prelude::*}; 5 | 6 | use crate::fs::FileSystem; 7 | use crate::graphics::font::{Font, Glyph, FontKey, Fonts}; 8 | use crate::graphics::render::TextureFactory; 9 | 10 | fn read_aaf(rd: &mut impl Read, texture_factory: &TextureFactory) -> io::Result { 11 | let mut magic = [0u8; 4]; 12 | rd.read_exact(&mut magic[..])?; 13 | if &magic != b"AAFF" { 14 | return Err(Error::new(ErrorKind::InvalidData, "no AAFF magic bytes found")); 15 | } 16 | 17 | let height = rd.read_i16::()? as i32; 18 | let horz_spacing = rd.read_i16::()? as i32; 19 | let space_width = rd.read_i16::()? as i32; 20 | let vert_spacing = rd.read_i16::()? as i32; 21 | 22 | let mut glyph_sizes = Vec::with_capacity(256); 23 | for _ in 0..256 { 24 | let width = rd.read_i16::()? as i32; 25 | let height = rd.read_i16::()? as i32; 26 | let _offset = rd.read_u32::()?; 27 | glyph_sizes.push((width, height)); 28 | } 29 | let mut glyphs = Vec::with_capacity(256); 30 | for (c, &(width, height)) in glyph_sizes.iter().enumerate() { 31 | let mut data = vec![0; (width * height) as usize]; 32 | rd.read_exact(&mut data)?; 33 | let texture = texture_factory.new_texture(width, height, data.into_boxed_slice()); 34 | let width = if c == b' ' as usize { 35 | space_width 36 | } else { 37 | width 38 | }; 39 | glyphs.push(Glyph { 40 | width, 41 | height, 42 | texture, 43 | }); 44 | } 45 | 46 | Ok(Font { 47 | height, 48 | horz_spacing, 49 | vert_spacing, 50 | glyphs: glyphs.into_boxed_slice(), 51 | }) 52 | } 53 | 54 | fn read_fon(rd: &mut impl Read, texture_factory: &TextureFactory) -> io::Result { 55 | let glyph_count = rd.read_i32::()?; 56 | if !(0..=256).contains(&glyph_count) { 57 | return Err(Error::new(ErrorKind::InvalidData, "invalid glyph_count in FON file")); 58 | } 59 | let glyph_count = glyph_count as usize; 60 | let height = rd.read_i32::()?; 61 | let horz_spacing = rd.read_i32::()?; 62 | let _garbage = rd.read_u32::()?; 63 | let _garbage = rd.read_u32::()?; 64 | 65 | let row_bytes = |w| (w as usize).div_ceil(8); 66 | let glyph_bytes = |w| row_bytes(w) * height as usize; 67 | 68 | let mut glyph_info = Vec::with_capacity(glyph_count); 69 | let mut data_len = 0; 70 | for _ in 0..glyph_count { 71 | let width = rd.read_i32::()?; 72 | let offset = rd.read_u32::()?; 73 | glyph_info.push((width, offset as usize)); 74 | data_len = cmp::max(data_len, offset as usize + glyph_bytes(width)); 75 | } 76 | 77 | let mut data = vec![0; data_len]; 78 | rd.read_exact(&mut data)?; 79 | 80 | let mut glyphs = Vec::with_capacity(glyph_count); 81 | for (width, offset) in glyph_info { 82 | let data = &data[offset..]; 83 | let row_len = row_bytes(width); 84 | let mut glyph_pixels = Vec::with_capacity((width * height) as usize); 85 | for y in 0..height as usize { 86 | let data = &data[y * row_len..]; 87 | for x in 0..width as usize { 88 | let b = data[x / 8] & (1 << (7 - (x % 8))) != 0; 89 | glyph_pixels.push(if b { 7 } else { 0 }); 90 | } 91 | } 92 | 93 | let texture = texture_factory.new_texture(width, height, glyph_pixels.into_boxed_slice()); 94 | glyphs.push(Glyph { 95 | width, 96 | height, 97 | texture, 98 | }); 99 | } 100 | 101 | Ok(Font { 102 | height, 103 | horz_spacing, 104 | vert_spacing: 0, 105 | glyphs: glyphs.into_boxed_slice(), 106 | }) 107 | } 108 | 109 | pub fn load_fonts(fs: &FileSystem, texture_factory: &TextureFactory) -> Fonts { 110 | let mut fonts = Fonts::new(); 111 | 112 | let load_fon = |name: &str| { 113 | let mut rd = fs.reader(name)?; 114 | read_fon(&mut rd, texture_factory) 115 | }; 116 | for id in 0..10 { 117 | let name = format!("font{}.fon", id); 118 | match load_fon(&name) { 119 | Ok(font) => { 120 | info!("loaded FON font: {}", name); 121 | fonts.insert(FontKey { id, antialiased: false }, font); 122 | } 123 | Err(e) => { 124 | debug!("couldn't load FON font `{}`: {}", name, e); 125 | } 126 | } 127 | } 128 | 129 | let load_aaf = |name: &str| { 130 | let mut rd = fs.reader(name)?; 131 | read_aaf(&mut rd, texture_factory) 132 | }; 133 | for id in 0..16 { 134 | let name = format!("font{}.aaf", id); 135 | match load_aaf(&name) { 136 | Ok(font) => { 137 | info!("loaded AAF font: {}", name); 138 | fonts.insert(FontKey { id, antialiased: true }, font); 139 | } 140 | Err(e) => { 141 | debug!("couldn't load AAF font `{}`: {}", name, e); 142 | } 143 | } 144 | } 145 | 146 | fonts 147 | } 148 | -------------------------------------------------------------------------------- /src/game/sequence/frame_anim.rs: -------------------------------------------------------------------------------- 1 | use linearize::Linearize; 2 | use std::time::{Duration, Instant}; 3 | 4 | use crate::asset::CritterAnim; 5 | use crate::game::object::{Handle, SetFrame}; 6 | use crate::game::world::World; 7 | use crate::sequence::*; 8 | 9 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 10 | enum State { 11 | Started, 12 | Running(Instant), 13 | Done, 14 | } 15 | 16 | #[derive(Clone, Copy, Debug, Linearize, Eq, PartialEq)] 17 | pub enum AnimDirection { 18 | Forward, 19 | Backward, 20 | } 21 | 22 | #[derive(Clone, Debug)] 23 | pub struct FrameAnimOptions { 24 | pub anim: Option, 25 | pub direction: AnimDirection, 26 | 27 | /// If `true` makes the animation loop forever. 28 | pub wrap: bool, 29 | 30 | /// Number of frames to skip initially. 31 | pub skip: u32, 32 | } 33 | 34 | impl Default for FrameAnimOptions { 35 | fn default() -> Self { 36 | Self { 37 | anim: None, 38 | direction: AnimDirection::Forward, 39 | wrap: false, 40 | skip: 0, 41 | } 42 | } 43 | } 44 | 45 | pub struct FrameAnim { 46 | obj: Handle, 47 | options: FrameAnimOptions, 48 | frame_len: Duration, 49 | state: State, 50 | } 51 | 52 | impl FrameAnim { 53 | pub fn new(obj: Handle, options: FrameAnimOptions) -> Self { 54 | Self { 55 | obj, 56 | options, 57 | frame_len: Duration::from_millis(1000 / 10), 58 | state: State::Started, 59 | } 60 | } 61 | 62 | fn init(&mut self, world: &mut World) { 63 | let mut obj = world.objects().get_mut(self.obj); 64 | 65 | obj.fid = if let Some(anim) = self.options.anim && 66 | let Some(fid) = obj.fid.critter() 67 | { 68 | fid.with_anim(anim).into() 69 | } else { 70 | obj.fid 71 | }; 72 | 73 | self.frame_len = Duration::from_millis(1000 / world.frm_db().get(obj.fid).unwrap().fps as u64); 74 | } 75 | } 76 | 77 | impl Sequence for FrameAnim { 78 | fn update(&mut self, ctx: &mut Update) -> Result { 79 | let set_frame = match self.state { 80 | State::Started => { 81 | self.init(ctx.world); 82 | SetFrame::Index(match self.options.direction { 83 | AnimDirection::Forward => self.options.skip as usize, 84 | AnimDirection::Backward => { 85 | let obj = ctx.world.objects().get(self.obj); 86 | let frame_set = ctx.world.frm_db().get(obj.fid).unwrap(); 87 | let frames = &frame_set.frame_lists[obj.direction].frames; 88 | frames.len().checked_sub(1 + self.options.skip as usize).unwrap() 89 | } 90 | }) 91 | }, 92 | State::Running(last_time) => { 93 | if ctx.time - last_time < self.frame_len { 94 | return Result::Running(Running::NotLagging); 95 | } 96 | 97 | let frame_index = { 98 | let mut obj = ctx.world.objects().get_mut(self.obj); 99 | 100 | let frame_set = ctx.world.frm_db().get(obj.fid).unwrap(); 101 | let frames = &frame_set.frame_lists[obj.direction].frames; 102 | 103 | let done = match self.options.direction { 104 | AnimDirection::Forward => { 105 | if obj.frame_idx + 1 < frames.len() { 106 | obj.frame_idx += 1; 107 | false 108 | } else if self.options.wrap { 109 | obj.frame_idx = 0; 110 | false 111 | } else { 112 | true 113 | } 114 | } 115 | AnimDirection::Backward => { 116 | if obj.frame_idx > 0 { 117 | obj.frame_idx -= 1; 118 | false 119 | } else if self.options.wrap { 120 | obj.frame_idx = frames.len() - 1; 121 | false 122 | } else { 123 | true 124 | } 125 | } 126 | }; 127 | if done { 128 | None 129 | } else { 130 | Some(obj.frame_idx) 131 | } 132 | }; 133 | if let Some(frame_index) = frame_index { 134 | SetFrame::Index(frame_index) 135 | } else { 136 | self.state = State::Done; 137 | return Result::Running(Running::NotLagging); 138 | } 139 | } 140 | State::Done => return Result::Done, 141 | }; 142 | 143 | ctx.world.objects_mut().set_frame(self.obj, set_frame); 144 | 145 | let new_last_time = if let State::Running(last_time) = self.state { 146 | last_time + self.frame_len 147 | } else { 148 | ctx.time 149 | }; 150 | self.state = State::Running(new_last_time); 151 | 152 | Result::Running(if ctx.time - new_last_time < self.frame_len { 153 | Running::NotLagging 154 | } else { 155 | Running::Lagging 156 | }) 157 | } 158 | } -------------------------------------------------------------------------------- /src/game/skilldex.rs: -------------------------------------------------------------------------------- 1 | use linearize::StaticMap; 2 | use std::convert::TryInto; 3 | 4 | use crate::asset::frame::FrameId; 5 | use crate::asset::message::{Messages, MessageId}; 6 | use crate::fs::FileSystem; 7 | use crate::game::object; 8 | use crate::graphics::{Rect, Point}; 9 | use crate::graphics::color::Rgb15; 10 | use crate::graphics::font::{FontKey, HorzAlign, VertAlign}; 11 | use crate::graphics::sprite::Sprite; 12 | use crate::ui::*; 13 | use crate::ui::panel::{self, Panel}; 14 | use crate::ui::button::{self, Button}; 15 | use crate::ui::command::{UiCommandData, SkilldexCommand}; 16 | use crate::util::EnumExt; 17 | use crate::ui::image_text::ImageText; 18 | 19 | const TEXT_FONT: FontKey = FontKey::antialiased(3); 20 | const TEXT_COLOR: Rgb15 = unsafe { Rgb15::rgb15_from_packed_unchecked(0x4a23) }; 21 | const TEXT_COLOR_DOWN: Rgb15 = unsafe { Rgb15::rgb15_from_packed_unchecked(0x3983) }; 22 | 23 | #[derive(Clone, Copy, Debug, linearize::Linearize, Eq, PartialEq)] 24 | pub enum Skill { 25 | // Order is important here. 26 | Sneak, 27 | Lockpick, 28 | Steal, 29 | Traps, 30 | FirstAid, 31 | Doctor, 32 | Science, 33 | Repair, 34 | } 35 | 36 | impl From for crate::asset::Skill { 37 | fn from(val: Skill) -> Self { 38 | use crate::asset::Skill::*; 39 | match val { 40 | Skill::Sneak => Sneak, 41 | Skill::Lockpick => Lockpick, 42 | Skill::Steal => Steal, 43 | Skill::Traps => Traps, 44 | Skill::FirstAid => FirstAid, 45 | Skill::Doctor => Doctor, 46 | Skill::Science => Science, 47 | Skill::Repair => Repair, 48 | } 49 | } 50 | } 51 | 52 | #[derive(Clone, Copy, Debug)] 53 | pub enum Command { 54 | Cancel, 55 | Skill(Skill), 56 | } 57 | 58 | pub struct Skilldex { 59 | msgs: Messages, 60 | window: Option, 61 | } 62 | 63 | impl Skilldex { 64 | pub fn new(fs: &FileSystem, language: &str) -> Self { 65 | let msgs = Messages::read_file(fs, language, "game/skilldex.msg").unwrap(); 66 | Self { 67 | msgs, 68 | window: None, 69 | } 70 | } 71 | 72 | pub fn is_visible(&self) -> bool { 73 | self.window.is_some() 74 | } 75 | 76 | pub fn show(&mut self, 77 | ui: &mut Ui, 78 | levels: StaticMap, 79 | target: Option, 80 | ) { 81 | assert!(self.window.is_none()); 82 | 83 | let win_size = ui.frm_db().get(FrameId::SKILLDEX_WINDOW).unwrap().first().size(); 84 | let window = ui.new_window(Rect::with_size( 85 | 640 - win_size.x - 4, 379 - win_size.y - 6, win_size.x, win_size.y), 86 | Some(Sprite::new(FrameId::SKILLDEX_WINDOW))); 87 | ui.widget_base_mut(window).set_modal(true); 88 | 89 | let mut header = Panel::new(); 90 | header.set_text(Some(panel::Text { 91 | text: self.msgs.get(100).unwrap().text.clone(), 92 | font: TEXT_FONT, 93 | color: TEXT_COLOR, 94 | options: Default::default(), 95 | })); 96 | ui.new_widget(window, Rect::with_size(55, 14, 1, 1), None, None, header); 97 | 98 | let btn_size = ui.frm_db().get(FrameId::SKILLDEX_BUTTON_UP).unwrap().first().size(); 99 | for (i, skill) in Skill::iter().enumerate() { 100 | let mut btn = Button::new(FrameId::SKILLDEX_BUTTON_UP, FrameId::SKILLDEX_BUTTON_DOWN, 101 | Some(UiCommandData::Skilldex(SkilldexCommand::Skill { 102 | skill: skill.into(), 103 | target, 104 | }))); 105 | let pos = (15, 45 + (btn_size.y + 3) * i as i32).into(); 106 | let text = self.msgs.get(102 + i as MessageId).unwrap().text.clone(); 107 | let mut text = button::Text::new(text, TEXT_FONT); 108 | text.pos = Point::new(1, 1); 109 | text.options.horz_align = HorzAlign::Center; 110 | text.options.vert_align = VertAlign::Middle; 111 | btn.set_text(Some(text)); 112 | btn.config_mut(button::State::Up).text.as_mut().unwrap().color = TEXT_COLOR; 113 | btn.config_mut(button::State::Down).text.as_mut().unwrap().color = TEXT_COLOR_DOWN; 114 | 115 | ui.new_widget(window, Rect::with_points(pos, pos + btn_size), None, None, btn); 116 | 117 | let level: u32 = levels[skill].try_into().unwrap_or(0); 118 | let mut level_wid = ImageText::big_numbers(); 119 | *level_wid.text_mut() = format!("{:03}", level).into(); 120 | let pos = pos + Point::new(96, 3); 121 | ui.new_widget(window, Rect::with_size(pos.x, pos.y, 1, 1), None, None, level_wid); 122 | } 123 | 124 | let btn_size = ui.frm_db().get(FrameId::SMALL_RED_BUTTON_UP).unwrap().first().size(); 125 | let mut cancel = Button::new(FrameId::SMALL_RED_BUTTON_UP, FrameId::SMALL_RED_BUTTON_DOWN, 126 | Some(UiCommandData::Skilldex(SkilldexCommand::Cancel))); 127 | let mut text = button::Text::new(self.msgs.get(101).unwrap().text.clone(), TEXT_FONT); 128 | text.pos = Point::new(btn_size.x + 9, 1); 129 | text.color = TEXT_COLOR; 130 | text.options.vert_align = VertAlign::Middle; 131 | cancel.set_text(Some(text)); 132 | ui.new_widget(window, Rect::with_size(48, 338, 90, btn_size.y), None, None, cancel); 133 | 134 | self.window = Some(window); 135 | } 136 | 137 | pub fn hide(&mut self, ui: &mut Ui) { 138 | let window = self.window.take().unwrap(); 139 | ui.remove(window); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/graphics/lighting/light_map.rs: -------------------------------------------------------------------------------- 1 | use num_traits::clamp; 2 | 3 | use crate::graphics::Point; 4 | 5 | pub const VERTEX_COUNT: usize = 10; 6 | pub const VERTEX_HEXES: [Point; VERTEX_COUNT] = [ 7 | Point { x: 0, y: -1}, Point { x: 1, y: -1 }, 8 | Point { x: -1, y: -1}, Point { x: 0, y: 0}, Point { x: 1, y: 0}, 9 | Point { x: -1, y: 0}, Point { x: 0, y: 1}, Point { x: 1, y: 1}, 10 | Point { x: -1, y: 1}, Point { x: 0, y: 2}, 11 | ]; 12 | 13 | pub struct LightMap { 14 | data: Box<[u32]>, 15 | } 16 | 17 | impl LightMap { 18 | pub const TRI_WIDTH: i32 = 32; 19 | pub const TRI_HEIGHT: i32 = 13; 20 | pub const WIDTH: i32 = Self::TRI_WIDTH * 3 - Self::TRI_WIDTH / 2; 21 | pub const HEIGHT: i32 = Self::TRI_HEIGHT * 3 + 2 /* as in original */; 22 | 23 | pub fn new() -> Self { 24 | Self::with_data(vec![0; (Self::WIDTH * Self::HEIGHT) as usize].into_boxed_slice()) 25 | } 26 | 27 | pub fn with_data(data: Box<[u32]>) -> Self { 28 | assert_eq!(data.len(), (Self::WIDTH * Self::HEIGHT) as usize); 29 | Self { 30 | data 31 | } 32 | } 33 | 34 | pub fn build(&mut self, lights: &[u32]) { 35 | type FillInfo = [i32; LightMap::TRI_HEIGHT as usize]; 36 | 37 | static UP_TRI_FILL: FillInfo = [2, 2, 6, 8, 10, 14, 16, 18, 20, 24, 26, 28, 32]; 38 | static DOWN_TRI_FILL: FillInfo = [32, 32, 30, 26, 24, 22, 18, 16, 14, 12, 8, 6, 4]; 39 | 40 | static TRI_VERTEX_INDEXES: [[usize; 3]; VERTEX_COUNT] = [ 41 | [0, 3, 2], 42 | [0, 1, 3], 43 | [1, 4, 3], 44 | [2, 3, 5], 45 | [3, 6, 5], 46 | [3, 4, 6], 47 | [4 ,7, 6], 48 | [5, 6, 8], 49 | [6, 9, 8], 50 | [6, 7, 9], 51 | ]; 52 | 53 | assert_eq!(lights.len(), VERTEX_COUNT); 54 | 55 | fn fill_tri(light_map: &mut [u32], lights: &[u32], 56 | tri_idx: usize, fill_info: &FillInfo, leftmost_vertex_idx: usize) { 57 | let tri_vert_idx = TRI_VERTEX_INDEXES[tri_idx]; 58 | let x_inc = (lights[tri_vert_idx[1]] as i32 - lights[tri_vert_idx[leftmost_vertex_idx]] as i32) / LightMap::TRI_WIDTH; 59 | let y_inc = (lights[tri_vert_idx[2]] as i32 - lights[tri_vert_idx[0]] as i32) / LightMap::TRI_HEIGHT; 60 | let mut row_light = lights[tri_vert_idx[0]] as i32; 61 | for y in 0..LightMap::TRI_HEIGHT { 62 | let len = fill_info[y as usize]; 63 | let half_len = len / 2; 64 | let mut light = row_light; 65 | for x in LightMap::TRI_WIDTH / 2 - half_len..LightMap::TRI_WIDTH / 2 + half_len { 66 | // Computed light value can be out of [0..0x10000] bounds. 67 | // Original engine does nothing to handle this. 68 | let clipped_light = clamp(light, 0, 0x10000) as u32; 69 | light_map[(y * LightMap::WIDTH + x) as usize] = clipped_light; 70 | light += x_inc; 71 | } 72 | row_light += y_inc; 73 | } 74 | } 75 | 76 | // Up-facing tris are filled first, then go the down-facing tris. 77 | for tri_kind in 0..2 { 78 | let mut tri_idx = 0; 79 | let mut y = 0; 80 | let mut start_x = -Self::TRI_WIDTH / 2; 81 | while y <= Self::HEIGHT - Self::TRI_HEIGHT { 82 | let mut x = start_x; 83 | for _ in 0..2 { 84 | let tri_x = x; 85 | if tri_x >= 0 { 86 | if tri_kind == 1 { 87 | // fill down-facing tri 88 | fill_tri(self.slice_mut(tri_x, y), lights, tri_idx, &DOWN_TRI_FILL, 0); 89 | } 90 | tri_idx += 1; 91 | } 92 | 93 | let tri_x = x + Self::TRI_WIDTH / 2; 94 | if tri_x < Self::WIDTH - Self::TRI_WIDTH / 2 { 95 | if tri_kind == 0 { 96 | // fill up-facing tri 97 | fill_tri(self.slice_mut(tri_x, y), lights, tri_idx, &UP_TRI_FILL, 2); 98 | } 99 | tri_idx += 1; 100 | } 101 | x += Self::TRI_WIDTH; 102 | } 103 | y += Self::TRI_HEIGHT - 1; // triangles overlap 104 | start_x += Self::TRI_WIDTH / 2; 105 | } 106 | } 107 | } 108 | 109 | pub fn data(&self) -> &[u32] { 110 | &self.data 111 | } 112 | 113 | pub fn get(&self, x: i32, y: i32) -> u32 { 114 | self.data[(y * Self::WIDTH + x) as usize] 115 | } 116 | 117 | fn slice_mut(&mut self, x: i32, y: i32) -> &mut [u32] { 118 | &mut self.data[(y * Self::WIDTH + x) as usize..] 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod test { 124 | use byteorder::{ByteOrder, LittleEndian}; 125 | use super:: *; 126 | 127 | #[test] 128 | fn test() { 129 | let expected: Vec<_> = include_bytes!("light_map_expected.bin") 130 | .chunks(4) 131 | .map(LittleEndian::read_i32) 132 | .collect(); 133 | 134 | let mut actual = LightMap::new(); 135 | actual.build(&[0x10000, 0, 0, 0x10000, 0x10000, 0, 0x10000, 0, 0x10000, 0]); 136 | 137 | for y in 0..LightMap::HEIGHT { 138 | for x in 0..LightMap::WIDTH { 139 | let i = (y * LightMap::HEIGHT + x) as usize; 140 | let expected = expected[i]; 141 | let expected = clamp(expected, 0, 0x10000) as u32; 142 | assert_eq!(actual.data[i], expected, "{} {}", x, y); 143 | } 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | pub mod array2d; 2 | pub mod random; 3 | #[cfg(test)] 4 | pub mod test; 5 | 6 | use bstring::{bstr, BString}; 7 | use linearize::{Linearize, LinearizeExt}; 8 | use std::fmt; 9 | use std::marker::PhantomData; 10 | use std::ops::RangeBounds; 11 | use slotmap::KeyData; 12 | 13 | #[derive(Clone, Copy, Debug)] 14 | pub struct RangeInclusive { 15 | pub start: T, 16 | pub end: T, 17 | } 18 | 19 | pub trait VecExt { 20 | fn with_default(len: usize) -> Vec 21 | where T: Default 22 | { 23 | Self::from_fn(len, |_| T::default()) 24 | } 25 | 26 | fn from_fn(len: usize, f: impl Fn(usize) -> T) -> Vec { 27 | let mut r = Vec::with_capacity(len); 28 | for i in 0..len { 29 | r.push(f(i)); 30 | } 31 | r 32 | } 33 | 34 | fn remove_first(&mut self, item: &T) -> Option 35 | where T: PartialEq; 36 | } 37 | 38 | impl VecExt for Vec { 39 | fn remove_first(&mut self, item: &T) -> Option 40 | where T: PartialEq 41 | { 42 | self.iter().position(|v| v == item) 43 | .map(|i| self.remove(i)) 44 | } 45 | } 46 | 47 | pub fn enum_iter>(r: R) -> EnumIter { 48 | use std::ops::Bound; 49 | let i = match r.start_bound() { 50 | Bound::Included(b) => b.linearize(), 51 | Bound::Excluded(_) => unreachable!(), 52 | Bound::Unbounded => 0, 53 | }; 54 | let end = match r.end_bound() { 55 | Bound::Included(b) => b.linearize().checked_add(1).unwrap(), 56 | Bound::Excluded(b) => b.linearize(), 57 | Bound::Unbounded => T::LENGTH, 58 | }; 59 | if i > end { 60 | panic!("slice index starts at ordinal {} but ends at ordinal {}", i, end); 61 | } else if end > T::LENGTH { 62 | panic!("ordinal {} out of range for enum of length {}", i, T::LENGTH); 63 | } 64 | EnumIter::new(i, end) 65 | } 66 | 67 | pub trait EnumExt: Linearize + Copy { 68 | fn len() -> usize { 69 | Self::LENGTH 70 | } 71 | 72 | fn iter() -> EnumIter { 73 | enum_iter(..) 74 | } 75 | 76 | fn from_ordinal(v: usize) -> Self { 77 | LinearizeExt::from_linear(v).unwrap() 78 | } 79 | 80 | fn try_from_ordinal(v: usize) -> Option { 81 | if v < Self::len() { 82 | Some(Self::from_ordinal(v)) 83 | } else { 84 | None 85 | } 86 | } 87 | 88 | fn ordinal(self) -> usize { 89 | Linearize::linearize(&self) 90 | } 91 | } 92 | 93 | impl EnumExt for T {} 94 | 95 | pub struct EnumIter { 96 | i: usize, 97 | end: usize, 98 | _t: PhantomData, 99 | } 100 | 101 | impl EnumIter { 102 | fn new(i: usize, end: usize) -> Self { 103 | Self { 104 | i, 105 | end, 106 | _t: PhantomData, 107 | } 108 | } 109 | 110 | fn empty() -> Self { 111 | Self::new(0, 0) 112 | } 113 | } 114 | 115 | impl Iterator for EnumIter { 116 | type Item = T; 117 | 118 | fn next(&mut self) -> Option { 119 | if self.i == self.end { 120 | return None; 121 | } 122 | let r = T::from_linear(self.i).unwrap(); 123 | self.i += 1; 124 | Some(r) 125 | } 126 | } 127 | 128 | #[derive(Clone, Copy, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] 129 | #[repr(transparent)] 130 | pub struct SmKey(slotmap::KeyData); 131 | 132 | impl From for SmKey { 133 | fn from(k: slotmap::KeyData) -> Self { 134 | Self(k) 135 | } 136 | } 137 | 138 | impl From for slotmap::KeyData { 139 | fn from(k: SmKey) -> Self { 140 | k.0 141 | } 142 | } 143 | 144 | unsafe impl slotmap::Key for SmKey { 145 | fn data(&self) -> KeyData { 146 | self.0 147 | } 148 | } 149 | 150 | impl fmt::Debug for SmKey { 151 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 152 | let v = self.0.as_ffi(); 153 | let ver = (v >> 32) as u32; 154 | let idx = v as u32; 155 | write!(f, "{}:{}", idx, ver) 156 | } 157 | } 158 | 159 | pub fn sprintf(fmt: &bstr, args: &[&bstr]) -> BString { 160 | let mut r = BString::with_capacity(fmt.len()); 161 | let mut args = args.iter(); 162 | let mut i = 0; 163 | while i < fmt.len() { 164 | let c = fmt[i]; 165 | match c { 166 | b'%' => { 167 | i += 1; 168 | if i >= fmt.len() { 169 | panic!("truncated format spec"); 170 | } 171 | let c = fmt[i]; 172 | match c { 173 | b's' | b'd' => r.push_str(args.next().expect("no more args")), 174 | b'%' => r.push(b'%'), 175 | _ => panic!("unsupported format spec: {}", c as char), 176 | } 177 | } 178 | _ => r.push(c), 179 | } 180 | i += 1; 181 | } 182 | assert!(args.next().is_none(), "too many args"); 183 | r 184 | } 185 | 186 | #[cfg(test)] 187 | mod test_ { 188 | use super::*; 189 | 190 | #[test] 191 | fn sprintf_() { 192 | let f = sprintf; 193 | fn bs(s: &str) -> BString { 194 | s.into() 195 | } 196 | 197 | assert_eq!(f("".into(), &[]), bs("")); 198 | assert_eq!(f("no args".into(), &[]), bs("no args")); 199 | assert_eq!(f("%s one arg".into(), &["arg1".into()]), bs("arg1 one arg")); 200 | assert_eq!(f("one arg %s".into(), &["arg1".into()]), bs("one arg arg1")); 201 | assert_eq!(f("%s two args %s".into(), &["arg1".into(), "arg2".into()]), 202 | bs("arg1 two args arg2")); 203 | assert_eq!(f("%%s escape %%".into(), &[]), bs("%s escape %")); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | branches: master 8 | 9 | env: 10 | NAME: vault13 11 | RUST_VERSION: 1.92.0 12 | RUST_BACKTRACE: full 13 | jobs: 14 | build: 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu, macos, windows] 19 | arch: [amd64, arm64] 20 | exclude: 21 | - os: windows 22 | arch: arm64 23 | include: 24 | - os: ubuntu 25 | package_name_os: linux 26 | 27 | - os: ubuntu 28 | arch: amd64 29 | workflow_label: ubuntu-24.04 30 | rust_target: x86_64-unknown-linux-gnu 31 | 32 | - os: ubuntu 33 | arch: arm64 34 | workflow_label: ubuntu-24.04-arm 35 | rust_target: aarch64-unknown-linux-gnu 36 | 37 | - os: macos 38 | package_name_os: macos 39 | 40 | - os: macos 41 | arch: amd64 42 | workflow_label: macos-15-intel 43 | rust_target: x86_64-apple-darwin 44 | 45 | - os: macos 46 | arch: arm64 47 | workflow_label: macos-15 48 | rust_target: aarch64-apple-darwin 49 | 50 | - os: windows 51 | arch: amd64 52 | package_name_os: windows 53 | workflow_label: windows-2025 54 | rust_target: x86_64-pc-windows-msvc 55 | runs-on: ${{ matrix.workflow_label }} 56 | steps: 57 | - uses: actions/checkout@v6 58 | 59 | - name: Calc hashes 60 | id: hash 61 | shell: bash 62 | run: | 63 | echo "cargo_lock=${{ hashFiles('Cargo.lock') }}" >> $GITHUB_OUTPUT 64 | echo "workflow_yml=${{ hashFiles('.github/workflows/build.yml') }}" >> $GITHUB_OUTPUT 65 | 66 | - name: Cache target/release/build 67 | uses: actions/cache@v5 68 | with: 69 | path: target/release/build 70 | key: ${{ matrix.workflow_label }}-target-build-${{ steps.hash.outputs.cargo_lock }}-${{ steps.hash.outputs.workflow_yml }} 71 | - name: Cache target/release/deps 72 | uses: actions/cache@v5 73 | with: 74 | path: target/release/deps 75 | key: ${{ matrix.workflow_label }}-target-deps-${{ steps.hash.outputs.cargo_lock }}-${{ steps.hash.outputs.workflow_yml }} 76 | 77 | - name: Show OS info (Linux) 78 | if: matrix.os == 'ubuntu' 79 | shell: bash 80 | run: | 81 | cat /etc/lsb-release 82 | uname -a 83 | echo 84 | echo "Environment variables:" 85 | echo 86 | env | sort 87 | 88 | - name: Install Rust 89 | shell: bash 90 | run: | 91 | rustup default ${{ env.RUST_VERSION }}-${{ matrix.rust_target }} 92 | rustc -vV 93 | cargo -vV 94 | 95 | - name: Install deps (macOS) 96 | if: runner.os == 'macOS' 97 | shell: bash 98 | run: | 99 | brew install sdl2 100 | echo "LIBRARY_PATH=$LIBRARY_PATH:$(brew --prefix)/lib" >> $GITHUB_ENV 101 | 102 | - name: Copy SDL2 libs (Windows) 103 | if: runner.os == 'Windows' 104 | shell: bash 105 | run: | 106 | cp sdl2_windows_binaries/* "$(rustc --print target-libdir)" 107 | cp sdl2_windows_binaries/* . 108 | 109 | - name: Install deps (Ubuntu) 110 | if: matrix.os == 'ubuntu' 111 | shell: bash 112 | run: | 113 | sudo apt-get update 114 | sudo apt-get install libsdl2-dev libsndio-dev patchelf -y 115 | 116 | - name: cargo clippy 117 | if: runner.os == 'Linux' && matrix.arch == 'amd64' 118 | shell: bash 119 | run: | 120 | rustup component add clippy 121 | cargo clippy -V 122 | cargo clippy --all-targets --all-features --release --verbose -- -D warnings 123 | 124 | - name: cargo test 125 | shell: bash 126 | run: cargo test --release --verbose 127 | 128 | - name: cargo build 129 | shell: bash 130 | run: cargo build --release --verbose 131 | 132 | - name: Check repo clean 133 | shell: bash 134 | run: git diff --exit-code 135 | 136 | - name: App version 137 | shell: bash 138 | run: target/release/${NAME} --version 139 | 140 | - name: Pre-package 141 | shell: bash 142 | run: mkdir dist 143 | 144 | - name: Package (Windows) 145 | if: runner.os == 'Windows' 146 | shell: bash 147 | working-directory: dist 148 | run: | 149 | cp ../target/release/${NAME}.exe . 150 | cp ../target/release/${NAME}.pdb . 151 | cp ../sdl2_windows_binaries/SDL2.dll . 152 | 153 | - name: Package (macOS) 154 | if: runner.os == 'macOS' 155 | shell: bash 156 | working-directory: dist 157 | run: | 158 | cp ../target/release/${NAME} . 159 | DYLIB_PATH=$(otool -L ${NAME} | grep libSDL2 | awk '{print $1}') 160 | cp ${DYLIB_PATH} libSDL2.dylib 161 | install_name_tool -change ${DYLIB_PATH} libSDL2.dylib ${NAME} 162 | 163 | - name: Package (Linux) 164 | if: runner.os == 'Linux' 165 | shell: bash 166 | working-directory: dist 167 | run: | 168 | cp ../target/release/${NAME} . 169 | SO_PATH=$(ldconfig -p | grep libSDL2 | head -n1 | awk '{print $4}') 170 | cp ${SO_PATH} libSDL2.so 171 | patchelf --replace-needed $(basename $SO_PATH) libSDL2.so ${NAME} 172 | 173 | - name: Check package 174 | shell: bash 175 | working-directory: dist 176 | run: | 177 | ls -l 178 | ./${NAME} --version 179 | 180 | - name: Make artifact name 181 | id: artifact_name 182 | shell: bash 183 | run: echo "value=${{ env.NAME }}-$(git rev-parse --short $GITHUB_SHA)-${{ matrix.package_name_os }}-${{ matrix.arch }}" >> $GITHUB_OUTPUT 184 | 185 | - name: Upload 186 | uses: actions/upload-artifact@v6 187 | with: 188 | name: ${{ steps.artifact_name.outputs.value }} 189 | path: dist/* 190 | -------------------------------------------------------------------------------- /src/game/rpg/def/skill.rs: -------------------------------------------------------------------------------- 1 | use linearize::{static_map, StaticMap}; 2 | 3 | use crate::asset::{Skill, Stat}; 4 | 5 | pub struct SkillDef { 6 | pub image_fid_id: u32, 7 | pub base: i32, 8 | pub stat_multiplier: i32, 9 | pub stat1: Stat, 10 | pub stat2: Option, 11 | pub experience: i32, 12 | pub flags: u32, 13 | } 14 | 15 | impl SkillDef { 16 | pub fn defaults() -> StaticMap { 17 | use Skill::*; 18 | use Stat::*; 19 | static_map! { 20 | SmallGuns => Self { 21 | image_fid_id: 0x1c, 22 | base: 5, 23 | stat_multiplier: 4, 24 | stat1: Agility, 25 | stat2: None, 26 | experience: 0, 27 | flags: 0, 28 | }, 29 | BigGuns => Self { 30 | image_fid_id: 0x1d, 31 | base: 0, 32 | stat_multiplier: 2, 33 | stat1: Agility, 34 | stat2: None, 35 | experience: 0, 36 | flags: 0, 37 | }, 38 | EnergyWeapons => Self { 39 | image_fid_id: 0x1e, 40 | base: 0, 41 | stat_multiplier: 2, 42 | stat1: Agility, 43 | stat2: None, 44 | experience: 0, 45 | flags: 0, 46 | }, 47 | UnarmedCombat => Self { 48 | image_fid_id: 0x1f, 49 | base: 30, 50 | stat_multiplier: 2, 51 | stat1: Agility, 52 | stat2: Some(Strength), 53 | experience: 0, 54 | flags: 0, 55 | }, 56 | Melee => Self { 57 | image_fid_id: 0x20, 58 | base: 20, 59 | stat_multiplier: 2, 60 | stat1: Agility, 61 | stat2: Some(Strength), 62 | experience: 0, 63 | flags: 0, 64 | }, 65 | Throwing => Self { 66 | image_fid_id: 0x21, 67 | base: 0, 68 | stat_multiplier: 4, 69 | stat1: Agility, 70 | stat2: None, 71 | experience: 0, 72 | flags: 0, 73 | }, 74 | FirstAid => Self { 75 | image_fid_id: 0x22, 76 | base: 0, 77 | stat_multiplier: 2, 78 | stat1: Perception, 79 | stat2: Some(Intelligence), 80 | experience: 25, 81 | flags: 0, 82 | }, 83 | Doctor => Self { 84 | image_fid_id: 0x23, 85 | base: 5, 86 | stat_multiplier: 1, 87 | stat1: Perception, 88 | stat2: Some(Intelligence), 89 | experience: 50, 90 | flags: 0, 91 | }, 92 | Sneak => Self { 93 | image_fid_id: 0x24, 94 | base: 5, 95 | stat_multiplier: 3, 96 | stat1: Agility, 97 | stat2: None, 98 | experience: 0, 99 | flags: 0, 100 | }, 101 | Lockpick => Self { 102 | image_fid_id: 0x25, 103 | base: 10, 104 | stat_multiplier: 1, 105 | stat1: Perception, 106 | stat2: Some(Intelligence), 107 | experience: 25, 108 | flags: 1, 109 | }, 110 | Steal => Self { 111 | image_fid_id: 0x26, 112 | base: 0, 113 | stat_multiplier: 3, 114 | stat1: Agility, 115 | stat2: None, 116 | experience: 25, 117 | flags: 1, 118 | }, 119 | Traps => Self { 120 | image_fid_id: 0x27, 121 | base: 10, 122 | stat_multiplier: 1, 123 | stat1: Perception, 124 | stat2: Some(Agility), 125 | experience: 25, 126 | flags: 1, 127 | }, 128 | Science => Self { 129 | image_fid_id: 0x28, 130 | base: 0, 131 | stat_multiplier: 4, 132 | stat1: Intelligence, 133 | stat2: None, 134 | experience: 0, 135 | flags: 0, 136 | }, 137 | Repair => Self { 138 | image_fid_id: 0x29, 139 | base: 0, 140 | stat_multiplier: 3, 141 | stat1: Intelligence, 142 | stat2: None, 143 | experience: 0, 144 | flags: 0, 145 | }, 146 | Conversant => Self { 147 | image_fid_id: 0x2a, 148 | base: 0, 149 | stat_multiplier: 5, 150 | stat1: Charisma, 151 | stat2: None, 152 | experience: 0, 153 | flags: 0, 154 | }, 155 | Barter => Self { 156 | image_fid_id: 0x2b, 157 | base: 0, 158 | stat_multiplier: 4, 159 | stat1: Charisma, 160 | stat2: None, 161 | experience: 0, 162 | flags: 0, 163 | }, 164 | Gambling => Self { 165 | image_fid_id: 0x2c, 166 | base: 0, 167 | stat_multiplier: 5, 168 | stat1: Luck, 169 | stat2: None, 170 | experience: 0, 171 | flags: 0, 172 | }, 173 | Outdoorsman => Self { 174 | image_fid_id: 0x2d, 175 | base: 0, 176 | stat_multiplier: 2, 177 | stat1: Endurance, 178 | stat2: Some(Intelligence), 179 | experience: 100, 180 | flags: 0, 181 | }, 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /src/graphics/color/palette/overlay.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Debug)] 4 | pub struct PaletteOverlay { 5 | ranges: Vec, 6 | } 7 | 8 | impl PaletteOverlay { 9 | pub fn new(mut ranges: Vec) -> Self { 10 | ranges.sort_by_key(|r| r.start); 11 | Self { 12 | ranges, 13 | } 14 | } 15 | 16 | pub fn standard() -> Self { 17 | fn make_alarm_colors() -> Vec { 18 | let mut colors = Vec::new(); 19 | for r in 1..16 { 20 | colors.push(Rgb::new(r * 4, 0, 0)); 21 | } 22 | for r in (0..15).rev() { 23 | colors.push(Rgb::new(r * 4, 0, 0)); 24 | } 25 | colors 26 | } 27 | 28 | fn overlay_range>(colors: C, start: u8, period_millis: u64) -> PaletteOverlayRange { 29 | let colors = colors.as_ref(); 30 | PaletteOverlayRange::new(colors.into(), start, colors.len() as u8, 31 | Duration::from_millis(period_millis)) 32 | } 33 | 34 | let ranges = vec![ 35 | overlay_range(SLIME, SLIME_PALETTE_START, SLIME_PERIOD_MILLIS), 36 | overlay_range(SHORE, SHORE_PALETTE_START, SHORE_PERIOD_MILLIS), 37 | overlay_range(SLOW_FIRE, SLOW_FIRE_PALETTE_START, SLOW_FIRE_PERIOD_MILLIS), 38 | overlay_range(FAST_FIRE, FAST_FIRE_PALETTE_START, FAST_FIRE_PERIOD_MILLIS), 39 | overlay_range(COMPUTER_SCREEN, COMPUTER_SCREEN_PALETTE_START, COMPUTER_SCREEN_PERIOD_MILLIS), 40 | PaletteOverlayRange::new(make_alarm_colors(), ALARM_PALETTE_START, 1, 41 | Duration::from_millis(ALARM_PERIOD_MILLIS)), 42 | ]; 43 | Self::new(ranges) 44 | } 45 | 46 | pub fn get(&self, color_idx: u8) -> Option { 47 | match self.ranges.binary_search_by(|r| { 48 | if color_idx < r.start { 49 | cmp::Ordering::Greater 50 | } else if color_idx < r.end() { 51 | cmp::Ordering::Equal 52 | } else { 53 | cmp::Ordering::Less 54 | } 55 | }) { 56 | Ok(i) => Some(self.ranges[i].get(color_idx)), 57 | Err(_) => None, 58 | } 59 | } 60 | 61 | pub fn rotate(&mut self, time: Instant) { 62 | for range in &mut self.ranges { 63 | range.rotate(time); 64 | } 65 | } 66 | } 67 | 68 | #[derive(Debug)] 69 | struct Rotation { 70 | pos: u8, 71 | period: Duration, 72 | last_time: Option, 73 | } 74 | 75 | impl Rotation { 76 | fn rotate(&mut self, time: Instant, len: u8) { 77 | if self.last_time.map(|lt| time - lt < self.period).unwrap_or(false) { 78 | return; 79 | } 80 | if self.pos == 0 { 81 | self.pos = len - 1; 82 | } else { 83 | self.pos -= 1; 84 | } 85 | assert!(self.last_time.is_none() || self.last_time.unwrap() <= time); 86 | self.last_time = Some(time); 87 | } 88 | } 89 | 90 | #[derive(Debug)] 91 | pub struct PaletteOverlayRange { 92 | colors: Vec, 93 | start: u8, 94 | len: u8, 95 | rotation: Rotation, 96 | } 97 | 98 | impl PaletteOverlayRange { 99 | pub fn new(colors: Vec, start: u8, len: u8, rotation_period: Duration) -> Self { 100 | assert!(!colors.is_empty()); 101 | assert!(start as u32 + len as u32 <= 256); 102 | assert!(len as usize <= colors.len()); 103 | Self { 104 | colors, 105 | start, 106 | len, 107 | rotation: Rotation { 108 | pos: 0, 109 | period: rotation_period, 110 | last_time: None, 111 | } 112 | } 113 | } 114 | 115 | fn rotate(&mut self, time: Instant) { 116 | self.rotation.rotate(time, self.colors.len() as u8); 117 | } 118 | 119 | fn get(&self, color_idx: u8) -> Rgb18 { 120 | assert!(color_idx >= self.start && color_idx < self.end()); 121 | self.colors[(color_idx - self.start + self.rotation.pos) as usize % self.colors.len()].scale() 122 | } 123 | 124 | fn end(&self) -> u8 { 125 | self.start + self.len 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod test { 131 | use super::*; 132 | 133 | #[test] 134 | fn test() { 135 | let mut t = PaletteOverlay::new(vec![ 136 | PaletteOverlayRange::new(vec![Rgb18::new(1, 1, 1), Rgb18::new(2, 2, 2)], 50, 2, Duration::from_millis(100)), 137 | PaletteOverlayRange::new(vec![Rgb18::new(5, 5, 5), Rgb18::new(6, 6, 6)], 100, 1, Duration::from_millis(200)), 138 | ]); 139 | 140 | assert_eq!(t.get(0), None); 141 | assert_eq!(t.get(255), None); 142 | 143 | assert_eq!(t.get(49), None); 144 | assert_eq!(t.get(50), Some(Rgb18::new(1, 1, 1))); 145 | assert_eq!(t.get(51), Some(Rgb18::new(2, 2, 2))); 146 | assert_eq!(t.get(52), None); 147 | 148 | assert_eq!(t.get(99), None); 149 | assert_eq!(t.get(100), Some(Rgb18::new(5, 5, 5))); 150 | assert_eq!(t.get(101), None); 151 | 152 | let tm = Instant::now(); 153 | t.rotate(tm); 154 | 155 | assert_eq!(t.get(49), None); 156 | assert_eq!(t.get(50), Some(Rgb18::new(2, 2, 2))); 157 | assert_eq!(t.get(51), Some(Rgb18::new(1, 1, 1))); 158 | assert_eq!(t.get(52), None); 159 | 160 | assert_eq!(t.get(99), None); 161 | assert_eq!(t.get(100), Some(Rgb18::new(6, 6, 6))); 162 | assert_eq!(t.get(101), None); 163 | 164 | t.rotate(tm + Duration::from_millis(199)); 165 | 166 | assert_eq!(t.get(50), Some(Rgb18::new(1, 1, 1))); 167 | assert_eq!(t.get(51), Some(Rgb18::new(2, 2, 2))); 168 | assert_eq!(t.get(100), Some(Rgb18::new(6, 6, 6))); 169 | 170 | t.rotate(tm + Duration::from_millis(200)); 171 | assert_eq!(t.get(50), Some(Rgb18::new(1, 1, 1))); 172 | assert_eq!(t.get(51), Some(Rgb18::new(2, 2, 2))); 173 | assert_eq!(t.get(100), Some(Rgb18::new(5, 5, 5))); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/game/sequence/move_seq.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use crate::asset::CritterAnim; 4 | use crate::game::object::{Handle, PathTo}; 5 | use crate::game::world::World; 6 | use crate::graphics::{EPoint, Point}; 7 | use crate::graphics::geometry::hex::{self, Direction}; 8 | use crate::sequence::*; 9 | 10 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 11 | enum State { 12 | Started, 13 | Running(Instant), 14 | Done, 15 | } 16 | 17 | pub struct Move { 18 | obj: Handle, 19 | to: PathTo, 20 | to_point: Option, 21 | anim: CritterAnim, 22 | frame_len: Duration, 23 | path: Vec, 24 | state: State, 25 | path_pos: usize, 26 | } 27 | 28 | impl Move { 29 | pub fn new(obj: Handle, to: PathTo, anim: CritterAnim) -> Self { 30 | Self { 31 | obj, 32 | to, 33 | to_point: None, 34 | anim, 35 | frame_len: Duration::from_millis(1000 / 10), 36 | path: Vec::new(), 37 | state: State::Started, 38 | path_pos: 0, 39 | } 40 | } 41 | 42 | fn init_step(&mut self, world: &mut World) { 43 | let mut obj = world.objects().get_mut(self.obj); 44 | 45 | // Path can be empty in Started state. 46 | if !self.path.is_empty() { 47 | obj.direction = self.path[self.path_pos]; 48 | } 49 | obj.fid = obj.fid 50 | .critter() 51 | .unwrap() 52 | .with_anim(self.anim) 53 | .into(); 54 | 55 | if self.state == State::Started { 56 | obj.frame_idx = 0; 57 | } 58 | 59 | self.frame_len = Duration::from_millis(1000 / world.frm_db().get(obj.fid).unwrap().fps as u64); 60 | } 61 | 62 | fn rebuild_path(&mut self, world: &mut World) { 63 | // TODO non-smooth 64 | self.path = world.objects().path(self.obj, self.to, true).unwrap_or_default(); 65 | } 66 | 67 | fn to_point(&self, world: &World) -> Point { 68 | match self.to { 69 | PathTo::Object(h) => world.objects().get(h).pos().point, 70 | PathTo::Point { point, .. } => point, 71 | } 72 | } 73 | 74 | fn done(&mut self, ctx: &mut Update) { 75 | let mut obj = ctx.world.objects().get_mut(self.obj); 76 | let pos = obj.pos().point; 77 | if pos != self.to_point.unwrap() { 78 | // Make object look into target's direction. 79 | obj.direction = hex::direction(pos, self.to_point.unwrap()); 80 | } 81 | self.state = State::Done; 82 | } 83 | } 84 | 85 | impl Sequence for Move { 86 | // object_move() 87 | fn update(&mut self, ctx: &mut Update) -> Result { 88 | match self.state { 89 | State::Started => { 90 | self.rebuild_path(ctx.world); 91 | // TODO Do we need to rebuild path if the object moves? 92 | self.to_point = Some(self.to_point(ctx.world)); 93 | 94 | self.init_step(ctx.world); 95 | ctx.world.objects_mut().reset_screen_shift(self.obj); 96 | 97 | if self.path.is_empty() { 98 | self.done(ctx); 99 | return Result::Done; 100 | } 101 | }, 102 | State::Running(last_time) => { 103 | if ctx.time - last_time < self.frame_len { 104 | return Result::Running(Running::NotLagging); 105 | } 106 | } 107 | State::Done => return Result::Done, 108 | } 109 | 110 | let new_obj_pos_and_shift = { 111 | let (shift, pos) = { 112 | let mut obj = ctx.world.objects().get_mut(self.obj); 113 | 114 | let frame_set = ctx.world.frm_db().get(obj.fid).unwrap(); 115 | let frames = &frame_set.frame_lists[obj.direction].frames; 116 | 117 | if self.state != State::Started { 118 | obj.frame_idx += 1; 119 | if obj.frame_idx >= frames.len() { 120 | obj.frame_idx = 0; 121 | } 122 | } 123 | 124 | (frames[obj.frame_idx].shift, obj.pos()) 125 | }; 126 | let shift = ctx.world.objects_mut().add_screen_shift(self.obj, shift); 127 | 128 | let dir = self.path[self.path_pos]; 129 | let next_offset = hex::screen_offset(dir); 130 | if next_offset.x > 0 && shift.x >= next_offset.x || 131 | next_offset.x < 0 && shift.x <= next_offset.x || 132 | next_offset.y > 0 && shift.y >= next_offset.y || 133 | next_offset.y < 0 && shift.y <= next_offset.y 134 | { 135 | let shift = { 136 | let obj = ctx.world.objects().get(self.obj); 137 | obj.screen_shift - next_offset 138 | }; 139 | let pos_point = ctx.world.hex_grid().go(pos.point, dir, 1).unwrap(); 140 | Some((EPoint::new(pos.elevation, pos_point), shift)) 141 | } else { 142 | None 143 | } 144 | }; 145 | if let Some((pos, shift)) = new_obj_pos_and_shift { 146 | let old_pos = ctx.world.objects().get(self.obj).pos(); 147 | ctx.world.objects_mut().set_pos(self.obj, Some(pos)); 148 | 149 | ctx.out.push(Event::ObjectMoved { 150 | obj: self.obj, 151 | old_pos, 152 | new_pos: pos, 153 | }); 154 | 155 | // TODO check for blocker and rebuild path 156 | // TODO use door 157 | 158 | self.path_pos += 1; 159 | if self.path_pos >= self.path.len() { 160 | self.done(ctx); 161 | return Result::Done; 162 | } 163 | ctx.world.objects_mut().add_screen_shift(self.obj, shift); 164 | self.init_step(ctx.world); 165 | } 166 | let new_last_time = if let State::Running(last_time) = self.state { 167 | last_time + self.frame_len 168 | } else { 169 | ctx.time 170 | }; 171 | self.state = State::Running(new_last_time); 172 | 173 | Result::Running(if ctx.time - new_last_time < self.frame_len { 174 | Running::NotLagging 175 | } else { 176 | Running::Lagging 177 | }) 178 | } 179 | } -------------------------------------------------------------------------------- /src/sequence/chain.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::VecDeque; 3 | use std::rc::Rc; 4 | 5 | use super::*; 6 | 7 | #[derive(Clone, Copy, Debug)] 8 | enum State { 9 | Cancellable, 10 | Finalizing, 11 | Done, 12 | } 13 | 14 | struct Inner { 15 | on_done: Option>, 16 | new_cancellable: Vec>, 17 | new_finalizing: Vec>, 18 | state: State, 19 | } 20 | 21 | impl Inner { 22 | fn new() -> Self { 23 | Self { 24 | on_done: None, 25 | new_cancellable: Vec::new(), 26 | new_finalizing: Vec::new(), 27 | state: State::Cancellable, 28 | } 29 | } 30 | 31 | fn flush(&mut self, 32 | cancellable: &mut VecDeque>, 33 | finalizing: &mut VecDeque>, 34 | ) { 35 | cancellable.extend(self.new_cancellable.drain(..)); 36 | finalizing.extend(self.new_finalizing.drain(..)); 37 | } 38 | } 39 | 40 | #[derive(Clone)] 41 | pub struct Control(Rc>); 42 | 43 | impl Control { 44 | /// Appends a new sequence to the cancellable sub-chain. 45 | /// Calling `cancel()` will cancel any running or pending cancellable sequence. 46 | /// Panics if the cancellable sub-chain has already finished running. 47 | pub fn cancellable(&self, seq: impl 'static + Sequence) -> &Self { 48 | let mut inner = self.0.borrow_mut(); 49 | match inner.state { 50 | State::Cancellable => {}, 51 | State::Finalizing | State::Done => panic!( 52 | "can't push cancellable sequence because the cancellable sub-chain has already finished running"), 53 | } 54 | inner.new_cancellable.push(Box::new(seq)); 55 | self 56 | } 57 | 58 | /// Appends a new sequence to the finalizing sub-chain. 59 | /// Finalizing sequences run after all cancellable sequences finished. 60 | /// Finalizing sequences can't be cancelled. 61 | /// Panics if the chain has already finished running. 62 | pub fn finalizing(&self, seq: impl 'static + Sequence) -> &Self { 63 | let mut inner = self.0.borrow_mut(); 64 | match inner.state { 65 | State::Cancellable | State::Finalizing => {} 66 | State::Done => panic!( 67 | "can't push finalizing sequence because the chain has already finished running"), 68 | } 69 | inner.new_finalizing.push(Box::new(seq)); 70 | self 71 | } 72 | 73 | /// Cancels any running or pending cancellable sequence. 74 | /// Idempotent, has no effect if already cancelled or the chain is finished running. 75 | pub fn cancel(&self) -> &Self { 76 | let mut inner = self.0.borrow_mut(); 77 | match inner.state { 78 | State::Cancellable => inner.state = State::Finalizing, 79 | State::Finalizing | State::Done => {} 80 | } 81 | self 82 | } 83 | 84 | /// Sets a callback to be called when the chain finishes. 85 | pub fn on_done(&self, f: impl 'static + FnOnce()) -> &Self { 86 | let mut inner = self.0.borrow_mut(); 87 | match inner.state { 88 | State::Cancellable | State::Finalizing => {}, 89 | State::Done => panic!("already done"), 90 | } 91 | assert!(inner.on_done.replace(Box::new(f)).is_none()); 92 | self 93 | } 94 | 95 | fn new() -> Self { 96 | Control(Rc::new(RefCell::new(Inner::new()))) 97 | } 98 | 99 | fn done(&self) { 100 | let on_done = { 101 | let mut inner = self.0.borrow_mut(); 102 | inner.state = State::Done; 103 | inner.on_done.take() 104 | }; 105 | if let Some(on_done) = on_done { 106 | on_done(); 107 | } 108 | } 109 | } 110 | 111 | /// A cancellable chain of sequences. The sequences are divided into two groups: cancellable and 112 | /// finalizing. Cancellable sequence are run first and can be cancelled. Finalizing sequences 113 | /// run after cancellable sequences finished (either normally or by cancelling) and can't be 114 | /// cancelled. 115 | pub struct Chain { 116 | cancellable: VecDeque>, 117 | finalizing: VecDeque>, 118 | control: Control, 119 | } 120 | 121 | impl Chain { 122 | pub fn new() -> Self { 123 | Self { 124 | cancellable: VecDeque::new(), 125 | finalizing: VecDeque::new(), 126 | control: Control::new(), 127 | } 128 | } 129 | 130 | pub fn control(&self) -> &Control { 131 | &self.control 132 | } 133 | } 134 | 135 | impl Sequence for Chain { 136 | fn update(&mut self, ctx: &mut Update) -> Result { 137 | self.control.0.borrow_mut().flush(&mut self.cancellable, &mut self.finalizing); 138 | loop { 139 | let state = self.control.0.borrow().state; 140 | match state { 141 | State::Cancellable => { 142 | let r = match self.cancellable.front_mut().map(|seq| seq.update(ctx)) { 143 | Some(r @ Result::Running(_)) => r, 144 | Some(Result::Done) => { 145 | self.cancellable.pop_front().unwrap(); 146 | if self.cancellable.is_empty() { 147 | Result::Done 148 | } else { 149 | continue; 150 | } 151 | }, 152 | None => Result::Done, 153 | }; 154 | match r { 155 | Result::Done => { 156 | self.control.0.borrow_mut().state = State::Finalizing; 157 | } 158 | Result::Running(_) => break r, 159 | } 160 | } 161 | State::Finalizing => { 162 | let r = match self.finalizing.front_mut().map(|seq| seq.update(ctx)) { 163 | Some(r @ Result::Running(_)) => r, 164 | Some(Result::Done) => { 165 | self.finalizing.pop_front().unwrap(); 166 | if self.finalizing.is_empty() { 167 | Result::Done 168 | } else { 169 | continue; 170 | } 171 | } 172 | None => Result::Done, 173 | }; 174 | match r { 175 | Result::Done => self.control.done(), 176 | Result::Running(_) => break r, 177 | } 178 | } 179 | State::Done => break Result::Done, 180 | } 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /src/asset/map/db.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::{self, BufRead}; 3 | 4 | use crate::fs::FileSystem; 5 | use crate::graphics::geometry::hex::TileGrid; 6 | use crate::graphics::EPoint; 7 | 8 | #[derive(Debug, Eq, PartialEq)] 9 | pub struct MapDef { 10 | pub lookup_name: String, 11 | pub name: String, 12 | pub music: Option, 13 | pub ambient_sfx: Vec<(String, u32)>, 14 | pub saved: bool, 15 | pub dead_bodies_age: bool, 16 | /// Per each elevation. 17 | pub can_rest_here: Vec, 18 | pub pipboy_active: bool, 19 | pub random_start_points: Vec, 20 | } 21 | 22 | pub struct MapDb { 23 | maps: Vec, 24 | } 25 | 26 | impl MapDb { 27 | pub fn new(fs: &FileSystem) -> io::Result { 28 | Self::read(&mut fs.reader("data/maps.txt")?) 29 | } 30 | 31 | fn read(rd: &mut impl BufRead) -> io::Result { 32 | let ini = crate::asset::read_ini(rd)?; 33 | let mut maps = Vec::new(); 34 | for i in 0..1000 { 35 | let n = format!("Map {:03}", i); 36 | let Some(section) = ini.get(&n) else { break }; 37 | let Some(lookup_name) = section.get("lookup_name").cloned() else { break }; 38 | let name = section.get("map_name").map(|v| v.to_owned()).expect("missing map_name"); 39 | let music = section.get("music").map(|v| v.to_owned()).to_owned(); 40 | 41 | let ambient_sfx = if let Some(ambient_sfx) = section.get("ambient_sfx") { 42 | ambient_sfx.split(',') 43 | .map(|s| { 44 | let mut parts = s.splitn(2, ':'); 45 | let sfx = parts.next().unwrap().trim(); 46 | let val = parts.next().unwrap().trim(); 47 | 48 | // Handle bad input: 49 | // ambient_sfx=water:40, water1:25, animal:15 animal:10, pebble:5, pebble1:5 50 | // ^ 51 | let val = val.split(' ').next().unwrap(); 52 | 53 | let val = val.parse().unwrap(); 54 | (sfx.into(), val) 55 | }) 56 | .collect() 57 | } else { 58 | vec![] 59 | }; 60 | 61 | fn parse_bool(s: &str) -> bool { 62 | match s.to_ascii_lowercase().as_str() { 63 | "no" => false, 64 | "yes" => true, 65 | _ => panic!("expected yes/no but found: {}", s), 66 | } 67 | } 68 | 69 | fn get_bool(m: &HashMap, key: &str) -> Option { 70 | let s = m.get(key)?; 71 | let s = s.trim(); 72 | Some(parse_bool(s)) 73 | } 74 | 75 | let saved = get_bool(section, "saved").unwrap_or(true); 76 | let dead_bodies_age = get_bool(section, "dead_bodies_age").unwrap_or(true); 77 | let pipboy_active = get_bool(section, "pipboy_active").unwrap_or(true); 78 | 79 | let can_rest_here = if let Some(s) = section.get("can_rest_here") { 80 | s.split(',') 81 | .map(parse_bool) 82 | .collect() 83 | } else { 84 | vec![true, true, true] 85 | }; 86 | assert_eq!(can_rest_here.len(), 3); 87 | 88 | let mut random_start_points = Vec::new(); 89 | for i in 0..15 { 90 | if let Some(s) = section.get(&format!("random_start_point_{}", i)) { 91 | let mut elev: Option = None; 92 | let mut tile_num: Option = None; 93 | for s in s.split(',') { 94 | let mut parts = s.splitn(2, ':'); 95 | let k = parts.next().unwrap().trim(); 96 | let v = parts.next().unwrap().trim(); 97 | match k { 98 | "elev" if elev.is_none() => elev = Some(v.parse().unwrap()), 99 | "tile_num" if tile_num.is_none() => tile_num = Some(v.parse().unwrap()), 100 | _ => panic!("unknown or duplicated key '{}'", k), 101 | } 102 | } 103 | let elev = elev.unwrap(); 104 | let tile_num = tile_num.unwrap(); 105 | let pos = EPoint::new(elev, TileGrid::default().linear_to_rect_inv(tile_num)); 106 | 107 | random_start_points.push(pos); 108 | } else { 109 | break; 110 | } 111 | } 112 | 113 | maps.push(MapDef { 114 | lookup_name, 115 | name, 116 | music, 117 | ambient_sfx, 118 | saved, 119 | dead_bodies_age, 120 | can_rest_here, 121 | pipboy_active, 122 | random_start_points, 123 | }) 124 | } 125 | Ok(Self { 126 | maps, 127 | }) 128 | } 129 | 130 | pub fn get(&self, id: u32) -> Option<&MapDef> { 131 | self.maps.get(id as usize) 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod test { 137 | use super::*; 138 | use crate::graphics::Point; 139 | use std::io::*; 140 | 141 | #[test] 142 | fn read() { 143 | let inp = " 144 | ; comment 145 | [Map 000] 146 | lookup_name=Desert Encounter 1 147 | map_name=desert1 148 | music=07desert 149 | ambient_sfx=gustwind:20, gustwin1:5 ignored:100, foo:42 150 | saved=No ; Random encounter maps aren't saved normally (only in savegames) 151 | dead_bodies_age=No 152 | can_rest_here=No,Yes,No ; All 3 elevations 153 | pipboy_active=no 154 | random_start_point_0=elev:0, tile_num:19086 155 | random_start_point_1=elev:1, tile_num:17302 156 | random_start_point_2=elev:2, tile_num:21315 157 | 158 | 159 | [Map 001] 160 | lookup_name=Desert Encounter 2 161 | map_name=desert2"; 162 | 163 | let exp = &[ 164 | MapDef { 165 | lookup_name: "Desert Encounter 1".into(), 166 | name: "desert1".into(), 167 | music: Some("07desert".into()), 168 | ambient_sfx: vec![ 169 | ("gustwind".into(), 20), 170 | ("gustwin1".into(), 5), 171 | ("foo".into(), 42), 172 | ], 173 | saved: false, 174 | dead_bodies_age: false, 175 | can_rest_here: vec![false, true, false], 176 | pipboy_active: false, 177 | random_start_points: vec![ 178 | (EPoint::new(0, Point::new(113, 95))), 179 | (EPoint::new(1, Point::new(97, 86))), 180 | (EPoint::new(2, Point::new(84, 106))), 181 | ], 182 | }, 183 | MapDef { 184 | lookup_name: "Desert Encounter 2".to_string(), 185 | name: "desert2".to_string(), 186 | music: None, 187 | ambient_sfx: vec![], 188 | saved: true, 189 | dead_bodies_age: true, 190 | can_rest_here: vec![true, true, true], 191 | pipboy_active: true, 192 | random_start_points: vec![], 193 | }, 194 | ]; 195 | 196 | let act = MapDb::read(&mut BufReader::new(Cursor::new(inp))).unwrap().maps; 197 | assert_eq!(act, exp); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/graphics.rs: -------------------------------------------------------------------------------- 1 | use num_traits::clamp; 2 | use std::cmp; 3 | use std::ops; 4 | use std::ops::MulAssign; 5 | 6 | pub mod color; 7 | pub mod font; 8 | pub mod geometry; 9 | pub mod lighting; 10 | pub mod map; 11 | pub mod render; 12 | pub mod sprite; 13 | 14 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] 15 | pub struct Point { 16 | pub x: i32, 17 | pub y: i32, 18 | } 19 | 20 | impl Point { 21 | pub const fn new(x: i32, y: i32) -> Self { 22 | Self { 23 | x, 24 | y, 25 | } 26 | } 27 | 28 | pub fn abs(self) -> Self { 29 | Self { 30 | x: self.x.abs(), 31 | y: self.y.abs(), 32 | } 33 | } 34 | 35 | pub fn tuple(self) -> (i32, i32) { 36 | (self.x, self.y) 37 | } 38 | 39 | pub fn elevated(self, elevation: u32) -> EPoint { 40 | EPoint { 41 | elevation, 42 | point: self, 43 | } 44 | } 45 | 46 | pub fn clamp_in_rect(self, rect: Rect) -> Self { 47 | Self::new( 48 | clamp(self.x, rect.left, rect.right - 1), 49 | clamp(self.y, rect.top, rect.bottom - 1)) 50 | } 51 | } 52 | 53 | impl ops::Add for Point { 54 | type Output = Self; 55 | 56 | fn add(self, o: Self) -> Self { 57 | Self::new(self.x + o.x, self.y + o.y) 58 | } 59 | } 60 | 61 | impl ops::AddAssign for Point { 62 | fn add_assign(&mut self, o: Self) { 63 | self.x += o.x; 64 | self.y += o.y; 65 | } 66 | } 67 | 68 | impl ops::Div for Point { 69 | type Output = Self; 70 | 71 | fn div(self, rhs: i32) -> Self::Output { 72 | Self::new(self.x / rhs, self.y / rhs) 73 | } 74 | } 75 | 76 | impl ops::DivAssign for Point { 77 | fn div_assign(&mut self, rhs: i32) { 78 | self.x /= rhs; 79 | self.y /= rhs; 80 | } 81 | } 82 | 83 | impl ops::Mul for Point { 84 | type Output = Self; 85 | 86 | fn mul(self, rhs: i32) -> Self::Output { 87 | Self::new(self.x * rhs, self.y * rhs) 88 | } 89 | } 90 | 91 | impl MulAssign for Point { 92 | fn mul_assign(&mut self, rhs: i32) { 93 | self.x *= rhs; 94 | self.y *= rhs; 95 | } 96 | } 97 | 98 | impl ops::Neg for Point { 99 | type Output = Self; 100 | 101 | fn neg(self) -> Self::Output { 102 | Self { 103 | x: -self.x, 104 | y: -self.y, 105 | } 106 | } 107 | } 108 | 109 | impl ops::Sub for Point { 110 | type Output = Self; 111 | 112 | fn sub(self, o: Self) -> Self { 113 | Self::new(self.x - o.x, self.y - o.y) 114 | } 115 | } 116 | 117 | impl ops::SubAssign for Point { 118 | fn sub_assign(&mut self, o: Self) { 119 | self.x -= o.x; 120 | self.y -= o.y; 121 | } 122 | } 123 | 124 | impl std::iter::Sum for Point { 125 | fn sum>(iter: I) -> Self { 126 | iter.fold(Point::new(0, 0), ops::Add::add) 127 | } 128 | } 129 | 130 | impl<'a> From<&'a Point> for Point { 131 | fn from(v: &'a Point) -> Self { 132 | *v 133 | } 134 | } 135 | 136 | impl From<(i32, i32)> for Point { 137 | fn from(v: (i32, i32)) -> Self { 138 | Self::new(v.0, v.1) 139 | } 140 | } 141 | 142 | #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] 143 | pub struct EPoint { 144 | pub elevation: u32, 145 | pub point: Point, 146 | } 147 | 148 | impl EPoint { 149 | pub fn new(elevation: u32, point: Point) -> Self { 150 | Self { 151 | elevation, 152 | point, 153 | } 154 | } 155 | 156 | pub fn with_point(self, point: Point) -> Self { 157 | Self::new(self.elevation, point) 158 | } 159 | } 160 | 161 | impl<'a> From<&'a EPoint> for EPoint { 162 | fn from(v: &'a EPoint) -> Self { 163 | *v 164 | } 165 | } 166 | 167 | impl From<(u32, Point)> for EPoint { 168 | fn from(v: (u32, Point)) -> Self { 169 | Self::new(v.0, v.1) 170 | } 171 | } 172 | 173 | impl From<(u32, (i32, i32))> for EPoint { 174 | fn from(v: (u32, (i32, i32))) -> Self { 175 | Self::new(v.0, Point::from(v.1)) 176 | } 177 | } 178 | 179 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 180 | pub struct Rect { 181 | pub left: i32, 182 | pub top: i32, 183 | pub right: i32, 184 | pub bottom: i32, 185 | } 186 | 187 | impl Rect { 188 | pub fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self { 189 | Self { 190 | left, 191 | top, 192 | right, 193 | bottom, 194 | } 195 | } 196 | 197 | pub fn empty() -> Self { 198 | Self { 199 | left: 0, 200 | top: 0, 201 | right: 0, 202 | bottom: 0, 203 | } 204 | } 205 | 206 | pub fn full() -> Self { 207 | Self { 208 | left: i32::MIN, 209 | top: i32::MIN, 210 | right: i32::MAX, 211 | bottom: i32::MAX, 212 | } 213 | } 214 | 215 | pub fn with_size(left: i32, top: i32, width: i32, height: i32) -> Self { 216 | Self { 217 | left, 218 | top, 219 | right: left + width, 220 | bottom: top + height, 221 | } 222 | } 223 | 224 | pub fn with_points(top_left: Point, bottom_right: Point) -> Self { 225 | Self { 226 | left: top_left.x, 227 | top: top_left.y, 228 | right: bottom_right.x, 229 | bottom: bottom_right.y, 230 | } 231 | } 232 | 233 | pub fn intersect(&self, other: Self) -> Self { 234 | let left = cmp::max(self.left, other.left); 235 | let top = cmp::max(self.top, other.top); 236 | let right = cmp::min(self.right, other.right); 237 | let bottom = cmp::min(self.bottom, other.bottom); 238 | Self { 239 | left, 240 | top, 241 | right, 242 | bottom, 243 | } 244 | } 245 | 246 | pub fn translate(&self, offset: Point) -> Self { 247 | Self { 248 | left: self.left + offset.x, 249 | top: self.top + offset.y, 250 | right: self.right + offset.x, 251 | bottom: self.bottom + offset.y, 252 | } 253 | } 254 | 255 | pub fn is_empty(&self) -> bool { 256 | self.left >= self.right && 257 | self.top >= self.bottom 258 | } 259 | 260 | pub fn contains(&self, p: Point) -> bool { 261 | p.x >= self.left && p.x < self.right && 262 | p.y >= self.top && p.y < self.bottom 263 | } 264 | 265 | pub fn contains_rect(&self, other: Self) -> bool { 266 | self.intersect(other) == other 267 | } 268 | 269 | pub fn intersects(&self, other: Self) -> bool { 270 | self.left < other.right && 271 | self.right > other.left && 272 | self.top < other.bottom && 273 | self.bottom > other.top 274 | } 275 | 276 | pub fn top_left(&self) -> Point { 277 | Point::new(self.left, self.top) 278 | } 279 | 280 | pub fn bottom_right(&self) -> Point { 281 | Point::new(self.bottom, self.right) 282 | } 283 | 284 | pub fn width(&self) -> i32 { 285 | self.right - self.left 286 | } 287 | 288 | pub fn with_width(mut self, width: i32) -> Self { 289 | self.right = self.left + width; 290 | self 291 | } 292 | 293 | pub fn height(&self) -> i32 { 294 | self.bottom - self.top 295 | } 296 | 297 | pub fn with_height(mut self, height: i32) -> Self { 298 | self.bottom = self.top + height; 299 | self 300 | } 301 | 302 | pub fn center(&self) -> Point { 303 | Point::new(self.left + self.width() / 2, self.top + self.height() / 2) 304 | } 305 | } 306 | 307 | -------------------------------------------------------------------------------- /src/game/ui/inventory_list.rs: -------------------------------------------------------------------------------- 1 | use bstring::bfmt::ToBString; 2 | use bstring::BString; 3 | use std::cmp; 4 | use std::convert::TryFrom; 5 | use std::time::{Duration, Instant}; 6 | 7 | use crate::asset::frame::FrameId; 8 | use crate::graphics::{Rect, Point}; 9 | use crate::graphics::color::WHITE; 10 | use crate::graphics::font::FontKey; 11 | use crate::graphics::sprite::{Sprite, Effect}; 12 | use crate::game::object; 13 | use crate::game::ui::action_menu::{Action, Placement}; 14 | use crate::ui::*; 15 | use crate::ui::command::UiCommandData; 16 | use crate::ui::command::inventory::Command; 17 | 18 | pub struct Item { 19 | pub object: object::Handle, 20 | pub fid: FrameId, 21 | pub count: u32, 22 | } 23 | 24 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 25 | pub enum MouseMode { 26 | Action, 27 | Drag, 28 | } 29 | 30 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 31 | pub enum Scroll { 32 | Down, 33 | Up, 34 | } 35 | 36 | pub struct InventoryList { 37 | item_height: i32, 38 | item_spacing: i32, 39 | items: Vec, 40 | scroll_idx: usize, 41 | dragging: Option, 42 | mouse_mode: MouseMode, 43 | last_hovered: Option, 44 | default_action: Option, 45 | action_menu_state: Option<(Instant, usize)>, 46 | visible_items: usize, 47 | } 48 | 49 | impl InventoryList { 50 | pub fn new(item_height: i32, item_spacing: i32) -> Self { 51 | Self { 52 | item_height, 53 | item_spacing, 54 | items: Vec::new(), 55 | scroll_idx: 0, 56 | dragging: None, 57 | mouse_mode: MouseMode::Drag, 58 | last_hovered: None, 59 | default_action: None, 60 | action_menu_state: None, 61 | visible_items: 0, 62 | } 63 | } 64 | 65 | pub fn items(&self) -> &[Item] { 66 | &self.items 67 | } 68 | 69 | pub fn clear(&mut self) { 70 | self.items.clear(); 71 | self.scroll_idx = 0; 72 | } 73 | 74 | pub fn push(&mut self, item: Item) { 75 | self.items.push(item); 76 | } 77 | 78 | pub fn can_scroll(&self, scroll: Scroll) -> bool { 79 | match scroll { 80 | Scroll::Down => self.scroll_idx < self.max_scroll_idx(), 81 | Scroll::Up => self.scroll_idx > 0, 82 | } 83 | } 84 | 85 | pub fn scroll_idx(&self) -> usize { 86 | self.scroll_idx 87 | } 88 | 89 | pub fn scroll(&mut self, scroll: Scroll) { 90 | self.set_scroll_idx(match scroll { 91 | Scroll::Down => self.scroll_idx + 1, 92 | Scroll::Up => self.scroll_idx.saturating_sub(1), 93 | }); 94 | } 95 | 96 | pub fn set_scroll_idx(&mut self, scroll_idx: usize) { 97 | self.scroll_idx = cmp::min(scroll_idx, self.max_scroll_idx()); 98 | } 99 | 100 | pub fn set_mouse_mode(&mut self, mouse_mode: MouseMode) { 101 | self.mouse_mode = mouse_mode; 102 | self.default_action = None; 103 | self.action_menu_state = None; 104 | self.last_hovered = None; 105 | self.dragging = None; 106 | } 107 | 108 | fn max_scroll_idx(&self) -> usize { 109 | self.items.len().saturating_sub(self.visible_items) 110 | } 111 | 112 | fn item_index_at(&self, rect: Rect, pos: Point) -> Option { 113 | if !rect.contains(pos) { 114 | return None; 115 | } 116 | let i = (pos.y - rect.top) / (self.item_height + self.item_spacing); 117 | let i = self.scroll_idx + usize::try_from(i).unwrap(); 118 | if i < self.items.len() { 119 | Some(i) 120 | } else { 121 | None 122 | } 123 | } 124 | } 125 | 126 | impl Widget for InventoryList { 127 | fn init(&mut self, ctx: Init) { 128 | self.visible_items = (ctx.base.rect().height() / (self.item_height + self.item_spacing)) as usize; 129 | } 130 | 131 | fn handle_event(&mut self, mut ctx: HandleEvent) { 132 | match ctx.event { 133 | Event::MouseDown { pos: _, button: MouseButton::Left} => { 134 | if let Some(idx) = self.item_index_at(ctx.base.rect(), ctx.cursor_pos) { 135 | match self.mouse_mode { 136 | MouseMode::Action => { 137 | self.action_menu_state = Some((ctx.now, idx)); 138 | } 139 | MouseMode::Drag => { 140 | ctx.base.set_cursor(Some(Cursor::Frame(self.items[idx].fid))); 141 | ctx.capture(); 142 | self.dragging = Some(idx); 143 | } 144 | } 145 | } 146 | } 147 | Event::MouseUp { pos: _, button: MouseButton::Left } => { 148 | match self.mouse_mode { 149 | MouseMode::Action => { 150 | self.action_menu_state = None; 151 | if let Some(idx) = self.item_index_at(ctx.base.rect(), ctx.cursor_pos) { 152 | ctx.out(UiCommandData::Inventory(Command::Action { 153 | action: None, 154 | object: self.items[idx].object, 155 | })); 156 | } 157 | } 158 | MouseMode::Drag => if let Some(item_index) = self.dragging.take() { 159 | ctx.base.set_cursor(None); 160 | ctx.release(); 161 | let object = self.items[item_index].object; 162 | ctx.out(UiCommandData::Inventory(Command::ListDrop { 163 | pos: ctx.cursor_pos, 164 | object, 165 | })); 166 | } 167 | } 168 | } 169 | Event::MouseMove { pos: _ } if self.mouse_mode == MouseMode::Action => { 170 | if let Some(idx) = self.item_index_at(ctx.base.rect(), ctx.cursor_pos) { 171 | self.default_action = Some(Action::Look); 172 | let object = self.items[idx].object; 173 | if Some(object) != self.last_hovered { 174 | self.last_hovered = Some(object); 175 | ctx.out(UiCommandData::Inventory(Command::Hover { 176 | object, 177 | })); 178 | } 179 | } else { 180 | self.default_action = None; 181 | } 182 | } 183 | Event::MouseLeave => { 184 | self.default_action = None; 185 | } 186 | Event::Tick => { 187 | if let Some((start, item)) = self.action_menu_state 188 | && ctx.now - start >= Duration::from_millis(500) 189 | { 190 | self.default_action = None; 191 | self.action_menu_state = None; 192 | 193 | ctx.out(UiCommandData::Inventory(Command::ActionMenu { 194 | object: self.items[item].object, 195 | })); 196 | } 197 | } 198 | _ => {} 199 | } 200 | } 201 | 202 | fn render(&mut self, ctx: Render) { 203 | let rect = ctx.base.unwrap().rect(); 204 | 205 | let mut item_rect = rect.with_height(self.item_height); 206 | 207 | for item in self.items.iter().skip(self.scroll_idx) { 208 | if !rect.contains_rect(item_rect) { 209 | break; 210 | } 211 | let mut sprite = Sprite::new(item.fid); 212 | sprite.pos = item_rect.top_left(); 213 | sprite.effect = Some(Effect::Fit { 214 | width: item_rect.width(), 215 | height: item_rect.height(), 216 | }); 217 | sprite.render(ctx.canvas, ctx.frm_db); 218 | 219 | if item.count > 1 { 220 | let s = BString::concat(&[&b"x"[..], item.count.to_bstring().as_bytes()]); 221 | ctx.canvas.draw_text(&s, item_rect.top_left(), 222 | FontKey::antialiased(1), WHITE, &Default::default()) 223 | } 224 | 225 | item_rect = item_rect.translate(Point::new(0, self.item_height + self.item_spacing)); 226 | } 227 | 228 | if let Some(default_action) = self.default_action { 229 | let fid = default_action.icons().0; 230 | let mut sprite = Sprite::new(fid); 231 | sprite.pos = Placement::new(1, ctx.cursor_pos, Rect::full()).rect.top_left(); 232 | sprite.render(ctx.canvas, ctx.frm_db); 233 | } 234 | } 235 | } 236 | --------------------------------------------------------------------------------