doukutsu-rs/src/map.rs

598 lines
20 KiB
Rust

use std::collections::HashMap;
use std::io;
use std::io::{BufRead, BufReader, Cursor, Read};
use std::sync::Arc;
use byteorder::{ReadBytesExt, LE};
use crate::common::{Color, Rect};
use crate::encoding::read_cur_shift_jis;
use crate::framework::context::Context;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::{GameError, GameResult};
use crate::framework::filesystem;
use crate::shared_game_state::TileSize;
use crate::stage::{PxPackScroll, PxPackStageData, StageData};
static SUPPORTED_PXM_VERSIONS: [u8; 1] = [0x10];
static SUPPORTED_PXE_VERSIONS: [u8; 2] = [0, 0x10];
pub struct Map {
pub width: u16,
pub height: u16,
pub tiles: Vec<u8>,
pub attrib: [u8; 0x100],
pub tile_size: TileSize,
}
static SOLID_TILES: [u8; 8] = [0x05, 0x41, 0x43, 0x46, 0x54, 0x55, 0x56, 0x57];
static WATER_TILES: [u8; 16] =
[0x02, 0x60, 0x61, 0x62, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0xa0, 0xa1, 0xa2, 0xa3];
#[derive(Copy, Clone, PartialEq)]
pub enum WaterRegionType {
WaterLine,
WaterDepth,
}
impl Map {
pub fn load_pxm<R: io::Read>(mut map_data: R, mut attrib_data: R) -> GameResult<Map> {
let mut magic = [0; 3];
map_data.read_exact(&mut magic)?;
if &magic != b"PXM" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
let version = map_data.read_u8()?;
if !SUPPORTED_PXM_VERSIONS.contains(&version) {
return Err(ResourceLoadError(format!("Unsupported PXM version: {:#x}", version)));
}
let width = map_data.read_u16::<LE>()?;
let height = map_data.read_u16::<LE>()?;
let mut tiles = vec![0u8; (width * height) as usize];
let mut attrib = [0u8; 0x100];
log::info!("Map size: {}x{}", width, height);
map_data.read_exact(&mut tiles)?;
if attrib_data.read_exact(&mut attrib).is_err() {
log::warn!("Map attribute data is shorter than 256 bytes!");
}
Ok(Map { width, height, tiles, attrib, tile_size: TileSize::Tile16x16 })
}
pub fn load_pxpack<R: io::Read>(
mut map_data: R,
root: &str,
data: &mut StageData,
ctx: &mut Context,
) -> GameResult<Map> {
let mut magic = [0u8; 16];
map_data.read_exact(&mut magic)?;
// based on https://github.com/tilderain/pxEdit/blob/kero/pxMap.py
if &magic != b"PXPACK121127a**\0" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
fn read_string<R: io::Read>(map_data: &mut R) -> GameResult<String> {
let mut bytes = map_data.read_u8()? as u32;
let mut raw_chars = Vec::new();
raw_chars.resize(bytes as usize, 0u8);
map_data.read(&mut raw_chars)?;
let mut raw_chars = Cursor::new(raw_chars);
let mut chars = Vec::new();
chars.reserve(bytes as usize);
while bytes > 0 {
let (consumed, chr) = read_cur_shift_jis(&mut raw_chars, bytes);
chars.push(chr);
bytes -= consumed;
}
Ok(chars.iter().collect())
}
fn skip_string<R: io::Read>(map_data: &mut R) -> GameResult {
let bytes = map_data.read_u8()? as u32;
for _ in 0..bytes {
map_data.read_u8()?;
}
Ok(())
}
let map_name = read_string(&mut map_data)?;
skip_string(&mut map_data)?; // left, right, up, down
skip_string(&mut map_data)?;
skip_string(&mut map_data)?;
skip_string(&mut map_data)?;
skip_string(&mut map_data)?; // spritesheet
map_data.read_u16::<LE>()?;
map_data.read_u16::<LE>()?;
map_data.read_u8()?;
let bg_color = Color::from_rgb(map_data.read_u8()?, map_data.read_u8()?, map_data.read_u8()?);
let mut tileset_fg = read_string(&mut map_data)?;
map_data.read_u8()?; // ignored
let scroll_fg = PxPackScroll::from(map_data.read_u8()?);
let mut tileset_mg = read_string(&mut map_data)?;
map_data.read_u8()?; // ignored
let scroll_mg = PxPackScroll::from(map_data.read_u8()?);
let mut tileset_bg = read_string(&mut map_data)?;
map_data.read_u8()?; // ignored
let scroll_bg = PxPackScroll::from(map_data.read_u8()?);
if tileset_fg == "" {
tileset_fg = data.tileset.filename()
}
if tileset_mg == "" {
tileset_mg = data.tileset.filename()
}
if tileset_bg == "" {
tileset_bg = data.tileset.filename()
}
let mut tiles = Vec::new();
let mut attrib = [0u8; 0x100];
let mut magic = [0u8; 8];
map_data.read_exact(&mut magic)?;
if &magic != b"pxMAP01\0" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
let width_fg = map_data.read_u16::<LE>()?;
let height_fg = map_data.read_u16::<LE>()?;
map_data.read_u8()?;
log::info!("Foreground map size: {}x{}", width_fg, height_fg);
let size_fg = width_fg as u32 * height_fg as u32;
tiles.resize(size_fg as usize, 0u8);
map_data.read_exact(&mut tiles[0..size_fg as usize])?;
map_data.read_exact(&mut magic)?;
if &magic != b"pxMAP01\0" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
let width_mg = map_data.read_u16::<LE>()?;
let height_mg = map_data.read_u16::<LE>()?;
log::info!("Middleground map size: {}x{}", width_mg, height_mg);
let size_mg = width_mg as u32 * height_mg as u32;
if size_mg != 0 {
tiles.resize(size_fg as usize + size_mg as usize, 0u8);
map_data.read_u8()?;
map_data.read_exact(&mut tiles[size_fg as usize..(size_fg as usize + size_mg as usize)])?;
}
map_data.read_exact(&mut magic)?;
if &magic != b"pxMAP01\0" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
let width_bg = map_data.read_u16::<LE>()?;
let height_bg = map_data.read_u16::<LE>()?;
log::info!("Background map size: {}x{}", width_bg, height_bg);
let size_bg = width_bg as u32 * height_bg as u32;
if size_bg != 0 {
map_data.read_u8()?;
tiles.resize(size_fg as usize + size_mg as usize + size_bg as usize, 0u8);
map_data.read_exact(
&mut tiles
[(size_fg as usize + size_mg as usize)..(size_fg as usize + size_mg as usize + size_bg as usize)],
)?;
}
if let Ok(mut attrib_data) = filesystem::open(ctx, [root, "Stage/", &tileset_fg, ".pxa"].join("")) {
if attrib_data.read_exact(&mut attrib).is_err() {
log::warn!("Map attribute data is shorter than 256 bytes!");
}
} else if let Ok(mut attrib_data) = filesystem::open(ctx, [root, "Stage/", &tileset_fg, ".pxattr"].join("")) {
attrib_data.read_exact(&mut magic)?;
if &magic != b"pxMAP01\0" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
attrib_data.read_u16::<LE>()?;
attrib_data.read_u16::<LE>()?;
attrib_data.read_u8()?;
if attrib_data.read_exact(&mut attrib).is_err() {
log::warn!("Map attribute data is shorter than 256 bytes!");
}
for attr in attrib.iter_mut() {
*attr = match *attr {
1 | 45 => 0x41,
2 | 66 => 0x44,
3 | 67 => 0x46,
4 | 68 => 0x43,
5 => 0x42,
7 => 0x4a,
8 => 0x50,
9 => 0x51,
10 => 0x52,
11 => 0x53,
12 => 0x54,
13 => 0x55,
14 => 0x56,
15 => 0x57,
40 => 0x5a,
41 => 0x5b,
42 => 0x5c,
43 => 0x5d,
64 => 0x60,
65 | 109 => 0x61,
69 => 0x62,
72 => 0x70,
73 => 0x71,
74 => 0x72,
75 => 0x73,
76 => 0x74,
77 => 0x75,
78 => 0x76,
79 => 0x77,
104 => 0x7a,
105 => 0x7b,
106 => 0x7c,
107 => 0x7d,
_ => 0,
};
}
} else {
log::warn!(
"No tile attribute data found for foreground tileset {}, collision might be broken.",
tileset_fg
);
}
if map_name != "" {
data.name = map_name;
}
data.background_color = bg_color;
data.pxpack_data = Some(PxPackStageData {
tileset_fg,
tileset_mg,
tileset_bg,
scroll_fg,
scroll_mg,
scroll_bg,
size_fg: (width_fg, height_fg),
size_mg: (width_mg, height_mg),
size_bg: (width_bg, height_bg),
offset_mg: size_fg,
offset_bg: size_fg + size_mg,
});
Ok(Map { width: width_fg, height: height_fg, tiles, attrib, tile_size: TileSize::Tile8x8 })
}
pub fn get_attribute(&self, x: usize, y: usize) -> u8 {
if x >= self.width as usize || y >= self.height as usize {
return 0;
}
self.attrib[*self.tiles.get(self.width as usize * y + x).unwrap_or_else(|| &0u8) as usize]
}
pub fn find_water_regions(&self) -> Vec<(WaterRegionType, Rect<u16>)> {
let mut result = Vec::new();
if self.height == 0 || self.width == 0 {
return result;
}
let mut walked = vec![false; self.width as usize * self.height as usize];
let mut rects = Vec::<Rect<u16>>::new();
for x in 0..self.width {
for y in 0..self.height {
let idx = self.width as usize * y as usize + x as usize;
if walked[idx] {
continue;
}
let attr = self.get_attribute(x as usize, y as usize);
if !WATER_TILES.contains(&attr) {
continue;
}
walked[idx] = true;
let mut rect = Rect::new(x, y, x, y);
let mut queue = Vec::new();
queue.push((0b1100, x, y));
while let Some((flow_flags, fx, fy)) = queue.pop() {
let idx = self.width as usize * fy as usize + fx as usize;
walked[idx] = true;
if fx < rect.left {
rect.left = fx;
for y in rect.top..rect.bottom {
walked[self.width as usize * y as usize + rect.left as usize] = true;
}
}
if fx > rect.right {
rect.right = fx;
for y in rect.top..rect.bottom {
walked[self.width as usize * y as usize + rect.right as usize] = true;
}
}
if fy < rect.top {
rect.top = fy;
for x in rect.left..rect.right {
walked[self.width as usize * rect.top as usize + x as usize] = true;
}
}
if fy > rect.bottom {
rect.bottom = fy;
for x in rect.left..rect.right {
walked[self.width as usize * rect.bottom as usize + x as usize] = true;
}
}
let mut check = |flow_flags: u8, ex: i32, ey: i32| {
if ex < 0 || ex >= self.width as i32 || ey < 0 || ey >= self.height as i32 {
return;
}
if walked[self.width as usize * ey as usize + ex as usize] {
return;
}
let attr = self.get_attribute(ex as usize, ey as usize);
if WATER_TILES.contains(&attr) {
queue.push((flow_flags, ex as u16, ey as u16));
}
};
if flow_flags & 0b0001 != 0 {
check(0b1011, fx as i32 - 1, fy as i32);
}
if flow_flags & 0b0100 != 0 {
check(0b1110, fx as i32 + 1, fy as i32);
}
if flow_flags & 0b0010 != 0 {
check(0b0111, fx as i32, fy as i32 - 1);
}
if flow_flags & 0b1000 != 0 {
check(0b1101, fx as i32, fy as i32 + 1);
}
}
rects.push(rect);
}
}
walked.fill(false);
for mut rect in rects {
let line = rect.top;
let line_up = rect.top - 1;
let min_x = rect.left;
let max_x = rect.right;
rect.top += 1;
result.push((WaterRegionType::WaterDepth, rect));
let mut x = min_x;
let mut length = 0;
let mut make_water_line = false;
loop {
let idx = self.width as usize * line as usize + x as usize;
let attr = self.get_attribute(x as usize, line as usize);
let attr_up = if rect.top > 0 { self.get_attribute(x as usize, line_up as usize) } else { 0x41 };
if !SOLID_TILES.contains(&attr_up) && !WATER_TILES.contains(&attr_up) {
make_water_line = true;
}
if !walked[idx] && WATER_TILES.contains(&attr) {
length += 1;
} else if length != 0 {
let bounds = Rect::new(x - length, line, x, line);
result.push((
if make_water_line { WaterRegionType::WaterLine } else { WaterRegionType::WaterDepth },
bounds,
));
length = 0;
} else {
length = 0;
}
walked[idx] = true;
x += 1;
if x >= max_x {
if length != 0 {
let bounds = Rect::new(x - length, line, x, line);
result.push((
if make_water_line { WaterRegionType::WaterLine } else { WaterRegionType::WaterDepth },
bounds,
));
}
break;
}
}
}
result
}
}
#[derive(Debug)]
pub struct NPCData {
pub id: u16,
pub x: i16,
pub y: i16,
pub flag_num: u16,
pub event_num: u16,
pub npc_type: u16,
pub flags: u16,
pub layer: u8,
}
impl NPCData {
pub fn load_from<R: io::Read>(mut data: R) -> GameResult<Vec<NPCData>> {
let mut magic = [0; 3];
data.read_exact(&mut magic)?;
if &magic != b"PXE" {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
let version = data.read_u8()?;
if !SUPPORTED_PXE_VERSIONS.contains(&version) {
return Err(ResourceLoadError(format!("Unsupported PXE version: {:#x}", version)));
}
let count = data.read_u32::<LE>()? as usize;
let mut npcs = Vec::with_capacity(count);
for i in 0..count {
let x = data.read_i16::<LE>()?;
let y = data.read_i16::<LE>()?;
let flag_num = data.read_u16::<LE>()?;
let event_num = data.read_u16::<LE>()?;
let npc_type = data.read_u16::<LE>()?;
let flags = data.read_u16::<LE>()?;
// booster's lab also specifies a layer field in version 0x10, prob for multi-layered maps
let layer = if version == 0x10 { data.read_u8()? } else { 0 };
npcs.push(NPCData { id: 170 + i as u16, x, y, flag_num, event_num, npc_type, flags, layer })
}
Ok(npcs)
}
}
#[derive(Clone, Copy)]
pub struct WaterParamEntry {
pub color_top: Color,
pub color_middle: Color,
pub color_bottom: Color,
}
pub struct WaterParams {
entries: HashMap<u8, WaterParamEntry>,
}
impl WaterParams {
pub fn new() -> WaterParams {
WaterParams { entries: HashMap::new() }
}
pub fn load_from<R: io::Read>(&mut self, data: R) -> GameResult {
fn next_u8(s: &mut std::str::Split<&str>, error_msg: &str) -> GameResult<u8> {
match s.next() {
None => Err(GameError::ParseError("Out of range.".to_string())),
Some(v) => v.trim().parse::<u8>().map_err(|_| GameError::ParseError(error_msg.to_string())),
}
}
for line in BufReader::new(data).lines() {
match line {
Ok(line) => {
let mut splits = line.split(":");
if splits.clone().count() != 5 {
return Err(GameError::ParseError("Invalid count of delimiters.".to_string()));
}
let tile_min = next_u8(&mut splits, "Invalid minimum tile value.")?;
let tile_max = next_u8(&mut splits, "Invalid maximum tile value.")?;
if tile_min > tile_max {
return Err(GameError::ParseError("tile_min > tile_max".to_string()));
}
let mut read_color = || -> GameResult<Color> {
let cstr = splits.next().unwrap().trim();
if !cstr.starts_with("[") || !cstr.ends_with("]") {
return Err(GameError::ParseError("Invalid format of color value.".to_string()));
}
let mut csplits = cstr[1..cstr.len() - 1].split(",");
if csplits.clone().count() != 4 {
return Err(GameError::ParseError("Invalid count of delimiters.".to_string()));
}
let r = next_u8(&mut csplits, "Invalid red value.")?;
let g = next_u8(&mut csplits, "Invalid green value.")?;
let b = next_u8(&mut csplits, "Invalid blue value.")?;
let a = next_u8(&mut csplits, "Invalid alpha value.")?;
Ok(Color::from_rgba(r, g, b, a))
};
let color_top = read_color()?;
let color_middle = read_color()?;
let color_bottom = read_color()?;
let entry = WaterParamEntry { color_top, color_middle, color_bottom };
for i in tile_min..=tile_max {
self.entries.insert(i, entry);
}
}
Err(e) => return Err(GameError::IOError(Arc::new(e))),
}
}
Ok(())
}
#[inline]
pub fn loaded(&self) -> bool {
!self.entries.is_empty()
}
pub fn get_entry(&self, tile: u8) -> &WaterParamEntry {
static DEFAULT_ENTRY: WaterParamEntry = WaterParamEntry {
color_top: Color::new(1.0, 1.0, 1.0, 1.0),
color_middle: Color::new(1.0, 1.0, 1.0, 1.0),
color_bottom: Color::new(1.0, 1.0, 1.0, 1.0),
};
self.entries.get(&tile).unwrap_or(&DEFAULT_ENTRY)
}
}