// Map, tiles, generation and path finding. // // The map size is chosen per game (64..256 tiles per side). Only one map is active at a time; // its dimensions are kept in a thread-local so the many small helpers (idx, xy, inb) stay simple. // Map::generate and loading a savegame (Map::activate) set them. use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::cell::Cell; use std::cmp::Reverse; use std::collections::BinaryHeap; pub const MIN_SIZE: usize = 64; pub const MAX_SIZE: usize = 256; thread_local! { static DIMS: Cell<(usize, usize)> = Cell::new((64, 64)); } fn set_dims(w: usize, h: usize) { DIMS.with(|d| d.set((w, h))); } #[inline] pub fn mw() -> usize { DIMS.with(|d| d.get().0) } #[inline] pub fn mh() -> usize { DIMS.with(|d| d.get().1) } #[inline] pub fn n() -> usize { mw() * mh() } #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Kind { Rock, Earth, Gold, Floor, Treasury, Lair, Farm, Training, Library, Core, Portal, Gate, Torture, Tower, // floor tile occupied by a defence tower (not walkable) Temple, Water, // underground river: not walkable, needs a bridge Lava, // lava river: not walkable, burns what falls in Bridge, // wooden bridge over water (walkable) LavaBridge, // stone bridge over lava (walkable) Shard, // floor with a shard of the soul core (campaign) Silver, // silver vein: common, little value Ruby, // ruby vein: rare, valuable Diamond, // diamond vein: very rare, very valuable Aerie, // room for flying creatures (their beds) Steel, // steel wall: hardly anyone gets through ManaSpring, // crystal spring: once claimed, more mana and a higher mana maximum RuneChest, // dwarven chest with the plans of a rune forge tower (claim it: one free build) Forge, // room: creatures manufacture traps and doors Door, // floor with an installed door (res bit 0: locked) Prison, // room: prisoners wait in cells Graveyard, // room: the fallen are buried, some rise again GuardPost, // room: creatures hold the position Arena, // room: sparring Altar, // room: sacrifices } // options for the map generator (campaign missions) #[derive(Clone, Debug)] pub struct GenOpts { pub portals: Option, // None: one per 64 x 64 tiles pub treasures: Option, // knight treasure caves; None: none (free play has dwarf caves instead) pub gate: bool, // hero gate at the far side pub core_y: Option, // row of the core (None: about the middle) } impl Default for GenOpts { fn default() -> GenOpts { GenOpts { portals: None, treasures: None, gate: true, core_y: None } } } // An underground river across the map #[derive(Clone, Debug)] pub struct River { pub lava: bool, pub vertical: bool, // runs from top to bottom (else from left to right) pub pos: f32, // position across the map (0..1) pub width: i32, pub bridges: u32, // fixed bridges of the knights } // order matters for the packed savegame codec: only append new kinds const KINDS: [Kind; 34] = [ Kind::Rock, Kind::Earth, Kind::Gold, Kind::Floor, Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Core, Kind::Portal, Kind::Gate, Kind::Torture, Kind::Tower, Kind::Temple, Kind::Water, Kind::Lava, Kind::Bridge, Kind::LavaBridge, Kind::Shard, Kind::Silver, Kind::Ruby, Kind::Diamond, Kind::Aerie, Kind::Steel, Kind::ManaSpring, Kind::RuneChest, Kind::Forge, Kind::Door, Kind::Prison, Kind::Graveyard, Kind::GuardPost, Kind::Arena, Kind::Altar, ]; // treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond pub const RES_GOLD: u8 = 0; // value taken per pick strike for each treasure pub const RES_STRIKE: [i32; 4] = [25, 20, 40, 60]; #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Owner { None, Player, Hero, Rival1, // AI dungeon masters (free play, campaign) Rival2, Rival3, // the fourth master side: a third rival, or a human master in a match } const OWNERS: [Owner; 6] = [Owner::None, Owner::Player, Owner::Hero, Owner::Rival1, Owner::Rival2, Owner::Rival3]; impl Owner { pub fn of_side(side: u8) -> Owner { match side { 0 => Owner::Player, 1 => Owner::Hero, 2 => Owner::Rival1, 3 => Owner::Rival2, 4 => Owner::Rival3, _ => Owner::None, } } // the masters of a match: the four sides a dungeon master can play (1 is the heroes' side) #[allow(dead_code)] // the sides of a match; the lobby (step C/D) uses them pub const MASTER_SIDES: [u8; 4] = [0, 2, 3, 4]; } // Short serde names (only used by version 1 savegames, newer ones use the packed codec) #[derive(Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Tile { #[serde(rename = "k")] pub kind: Kind, #[serde(rename = "o")] pub owner: Owner, #[serde(rename = "h")] pub hp: f32, // dig resistance #[serde(rename = "g")] pub gold: i32, // gold left (gold vein) #[serde(rename = "m")] pub marked: bool, // marked for digging #[serde(rename = "r")] pub reserved: u32, // entity id + 1 working on this tile #[serde(rename = "f")] pub food: f32, // mushrooms in the mushroom garden #[serde(rename = "b")] pub bed: u32, // entity id + 1 sleeping here #[serde(rename = "v")] pub var: u8, // texture variant #[serde(rename = "rf", default)] pub reinforced: bool, // wall reinforced by imps #[serde(rename = "rs", default)] pub res: u8, // treasury: kind of treasure stored here (see RES_GOLD) } impl Tile { pub fn new(kind: Kind) -> Tile { Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0, reinforced: false, res: 0 } } pub fn solid(&self) -> bool { matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel) } pub fn diggable(&self) -> bool { matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond | Kind::Steel) } // the treasure of a vein (None for everything else) pub fn vein(&self) -> Option { match self.kind { Kind::Gold => Some(0), Kind::Silver => Some(1), Kind::Ruby => Some(2), Kind::Diamond => Some(3), _ => None, } } pub fn walkable(&self) -> bool { matches!( self.kind, Kind::Floor | Kind::Treasury | Kind::Lair | Kind::Farm | Kind::Training | Kind::Library | Kind::Portal | Kind::Gate | Kind::Torture | Kind::Temple | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Aerie | Kind::ManaSpring | Kind::RuneChest | Kind::Forge | Kind::Door | Kind::Prison | Kind::Graveyard | Kind::GuardPost | Kind::Arena | Kind::Altar ) } // river tiles (open, but nobody can walk there) pub fn liquid(&self) -> bool { matches!(self.kind, Kind::Water | Kind::Lava) } pub fn bridge(&self) -> bool { matches!(self.kind, Kind::Bridge | Kind::LavaBridge) } // the river below a bridge or river tile pub fn river(&self) -> Option { match self.kind { Kind::Water | Kind::Bridge => Some(Kind::Water), Kind::Lava | Kind::LavaBridge => Some(Kind::Lava), _ => None, } } } pub fn idx(x: i32, y: i32) -> usize { y as usize * mw() + x as usize } pub fn xy(i: usize) -> (i32, i32) { let w = mw(); ((i % w) as i32, (i / w) as i32) } pub fn inb(x: i32, y: i32) -> bool { x >= 0 && y >= 0 && (x as usize) < mw() && (y as usize) < mh() } pub const DIR4: [(i32, i32); 4] = [(0, -1), (1, 0), (0, 1), (-1, 0)]; pub const DIR8: [(i32, i32); 8] = [(0, -1), (1, 0), (0, 1), (-1, 0), (1, -1), (1, 1), (-1, 1), (-1, -1)]; #[derive(Serialize, Deserialize)] pub struct Rng(pub u32); impl Rng { pub fn next(&mut self) -> u32 { let mut x = self.0; x ^= x << 13; x ^= x >> 17; x ^= x << 5; self.0 = x; x } pub fn f(&mut self) -> f32 { (self.next() >> 8) as f32 / (1u32 << 24) as f32 } pub fn range(&mut self, a: i32, b: i32) -> i32 { if b <= a { return a; } a + (self.next() % (b - a + 1) as u32) as i32 } } #[derive(Serialize, Deserialize)] pub struct Map { #[serde(default = "default_size")] pub w: usize, #[serde(default = "default_size")] pub h: usize, #[serde(serialize_with = "tiles_ser", deserialize_with = "tiles_de")] pub t: Vec, pub core: (i32, i32), // top-left corner of the core (3x3) #[serde(default)] pub portals: Vec<(i32, i32)>, // top-left corners of the portals (3x3) #[serde(default, rename = "portal", skip_serializing)] legacy_portal: Option<(i32, i32)>, // version 1 savegames had a single portal pub gate: (i32, i32), #[serde(default)] pub treasures: Vec<(i32, i32)>, // centres of the treasure caves #[serde(default, with = "crate::fog::bits")] pub explored: Vec, // fog of war: tiles the dungeon knows (empty: no fog) #[serde(skip)] pub visible: Vec, // fog of war: tiles in sight right now } fn default_size() -> usize { 64 } impl Map { // Make this map's dimensions the active ones (after loading a savegame) pub fn activate(&mut self) { set_dims(self.w, self.h); if self.portals.is_empty() { if let Some(p) = self.legacy_portal.take() { self.portals.push(p); } } } pub fn get(&self, x: i32, y: i32) -> Option<&Tile> { if inb(x, y) { Some(&self.t[idx(x, y)]) } else { None } } pub fn walk(&self, x: i32, y: i32) -> bool { inb(x, y) && self.t[idx(x, y)].walkable() } // Index of the portal containing tile (x, y) pub fn portal_at(&self, x: i32, y: i32) -> Option { self.portals.iter().position(|&(px, py)| x >= px && x < px + 3 && y >= py && y < py + 3) } pub fn portal_owned(&self, k: usize) -> bool { let (px, py) = self.portals[k]; self.t[idx(px, py)].owner == Owner::Player } pub fn generate(seed: u32, w: usize, h: usize) -> Map { Map::generate_with(seed, w, h, &GenOpts::default()) } pub fn generate_with(seed: u32, w: usize, h: usize, opts: &GenOpts) -> Map { let w = w.clamp(MIN_SIZE, MAX_SIZE); let h = h.clamp(MIN_SIZE, MAX_SIZE); set_dims(w, h); let mut s = seed; loop { let m = Map::try_generate(s, w, h, opts); // make sure heroes can reach the core if !opts.gate || m.hero_path_to_core(idx(m.gate.0 + 1, m.gate.1 + 1)).is_some() { return m; } s = s.wrapping_mul(747796405).wrapping_add(2891336453) | 1; } } fn try_generate(seed: u32, w: usize, h: usize, opts: &GenOpts) -> Map { let (wi, hi) = (w as i32, h as i32); let mut rng = Rng(seed | 1); for _ in 0..10 { rng.next(); } // content scales with the area; one portal per 64x64 tiles let area = (w * h) as f32 / 4096.0; let scale = |n: f32| (n * area).round() as i32; let n_portals = opts.portals.unwrap_or((area.round() as usize).max(1)); let n_treasures = opts.treasures.unwrap_or(0); let mut t = vec![Tile::new(Kind::Earth); w * h]; for (i, tile) in t.iter_mut().enumerate() { let (x, y) = ((i % w) as i32, (i / w) as i32); tile.var = (rng.next() % 4) as u8; tile.hp = 6.0; if x < 2 || y < 2 || x >= wi - 2 || y >= hi - 2 { tile.kind = Kind::Rock; } } let inner = |x: i32, y: i32| x >= 2 && y >= 2 && x < wi - 2 && y < hi - 2; let core = (11, opts.core_y.unwrap_or(hi / 2 + rng.range(-5, 3))); let gate = (wi - 13 + rng.range(-2, 2), rng.range(10, hi - 14)); // places features far apart: best of several random candidates let mut taken: Vec<(i32, i32)> = vec![(core.0 + 1, core.1 + 1), (gate.0 + 1, gate.1 + 1)]; let place = |rng: &mut Rng, taken: &mut Vec<(i32, i32)>, margin: i32| -> (i32, i32) { let mut best = (margin, margin); let mut bd = -1; for _ in 0..40 { let p = (rng.range(margin, wi - margin - 3), rng.range(margin, hi - margin - 3)); let d = taken.iter().map(|q| (p.0 - q.0).pow(2) + (p.1 - q.1).pow(2)).min().unwrap_or(0); if d > bd { bd = d; best = p; } } taken.push(best); best }; // the first portal is close to the start let first = (core.0 + rng.range(13, 19), (core.1 + if rng.f() < 0.5 { rng.range(-14, -10) } else { rng.range(10, 14) }).clamp(5, hi - 9)); taken.push(first); let mut portals = if n_portals > 0 { vec![first] } else { Vec::new() }; for _ in 1..n_portals { portals.push(place(&mut rng, &mut taken, 6)); } let mut treasures = Vec::new(); for _ in 0..n_treasures { let p = place(&mut rng, &mut taken, 7); treasures.push((p.0 + 1, p.1 + 1)); } let blob = |t: &mut Vec, rng: &mut Rng, cx: i32, cy: i32, r: i32, k: Kind, gold: (i32, i32)| { for y in cy - r..=cy + r { for x in cx - r..=cx + r { if !inner(x, y) { continue; } let d = ((x - cx) * (x - cx) + (y - cy) * (y - cy)) as f32; if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 { let tl = &mut t[y as usize * w + x as usize]; tl.kind = k; if tl.vein().is_some() { tl.gold = rng.range(gold.0, gold.1); } } } } }; for _ in 0..scale(22.0) { let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5)); let r = rng.range(1, 3); blob(&mut t, &mut rng, cx, cy, r, Kind::Rock, (0, 0)); } for _ in 0..scale(16.0) { let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5)); let r = rng.range(1, 2); blob(&mut t, &mut rng, cx, cy, r, Kind::Gold, (250, 550)); } // more treasures: silver is common, rubies are rare, diamonds very rare and far away for _ in 0..scale(7.0) { let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5)); let r = rng.range(1, 2); blob(&mut t, &mut rng, cx, cy, r, Kind::Silver, (200, 400)); } for _ in 0..scale(3.0) { let (cx, cy) = (rng.range(wi / 4, wi - 5), rng.range(4, hi - 5)); blob(&mut t, &mut rng, cx, cy, 1, Kind::Ruby, (500, 800)); } for _ in 0..scale(1.2).max(1) { let (cx, cy) = (rng.range(wi / 3, wi - 5), rng.range(4, hi - 5)); let r = rng.range(0, 1); blob(&mut t, &mut rng, cx, cy, r, Kind::Diamond, (1000, 1500)); } // guaranteed gold near the start for k in 0..3 { let cx = core.0 + rng.range(-3, 9); let cy = core.1 + if k % 2 == 0 { rng.range(-9, -6) } else { rng.range(7, 9) }; blob(&mut t, &mut rng, cx, cy, 1, Kind::Gold, (250, 550)); } let room = |t: &mut Vec, x0: i32, y0: i32, x1: i32, y1: i32, owner: Owner| { for y in y0..=y1 { for x in x0..=x1 { if inner(x, y) { let tl = &mut t[y as usize * w + x as usize]; tl.kind = Kind::Floor; tl.owner = owner; tl.gold = 0; } } } }; // small natural caves for _ in 0..scale(4.0) { let (cx, cy) = (rng.range(20, wi - 16), rng.range(6, hi - 6)); let (cw, ch) = (rng.range(1, 2), rng.range(1, 2)); room(&mut t, cx - cw, cy - ch, cx + cw, cy + ch, Owner::None); } // treasure caves: a cave surrounded by very rich gold veins (guarded by heroes, see Game::new) for &(cx, cy) in &treasures { blob(&mut t, &mut rng, cx, cy, 4, Kind::Gold, (1000, 1600)); // some of the riches are gems for y in cy - 4..=cy + 4 { for x in cx - 4..=cx + 4 { if inner(x, y) && t[y as usize * w + x as usize].kind == Kind::Gold { let r = rng.f(); let tl = &mut t[y as usize * w + x as usize]; if r < 0.08 { tl.kind = Kind::Diamond; tl.gold = rng.range(1200, 1800); } else if r < 0.3 { tl.kind = Kind::Ruby; tl.gold = rng.range(800, 1200); } } } } room(&mut t, cx - 2, cy - 2, cx + 2, cy + 2, Owner::None); } // starting area and core room(&mut t, core.0 - 3, core.1 - 3, core.0 + 5, core.1 + 5, Owner::Player); for y in 0..3 { for x in 0..3 { let tl = &mut t[(core.1 + y) as usize * w + (core.0 + x) as usize]; tl.kind = Kind::Core; tl.owner = Owner::Player; } } // portal caves for &(px, py) in &portals { room(&mut t, px - 2, py - 2, px + 4, py + 4, Owner::None); for y in 0..3 { for x in 0..3 { t[(py + y) as usize * w + (px + x) as usize].kind = Kind::Portal; } } } // hero gate (campaign maps place the gate of the order in a fortress instead) let gate = if opts.gate { room(&mut t, gate.0 - 2, gate.1 - 2, gate.0 + 4, gate.1 + 4, Owner::Hero); for y in 0..3 { for x in 0..3 { let tl = &mut t[(gate.1 + y) as usize * w + (gate.0 + x) as usize]; tl.kind = Kind::Gate; tl.owner = Owner::Hero; } } gate } else { (0, 0) }; for tl in t.iter_mut() { if tl.vein().is_some() { tl.hp = 6.0; } } Map { w, h, t, core, portals, legacy_portal: None, gate, treasures, explored: Vec::new(), visible: Vec::new() } } // Carves a meandering river; the player's area, the core and portals stay dry. // Returns the tiles of the knight bridges. pub fn carve_river(&mut self, r: &River, seed: u32) -> Vec { let mut rng = Rng(seed | 1); let (w, h) = (self.w as i32, self.h as i32); let len = if r.vertical { h } else { w }; let across = if r.vertical { w } else { h }; let base = (r.pos * across as f32) as i32; let (f1, f2) = (0.05 + rng.f() * 0.06, 0.13 + rng.f() * 0.08); let (p1, p2) = (rng.f() * 6.0, rng.f() * 6.0); let amp = (across as f32 * 0.06).min(6.0); let mut centre = Vec::with_capacity(len as usize); for s in 0..len { let c = base as f32 + (s as f32 * f1 + p1).sin() * amp + (s as f32 * f2 + p2).sin() * amp * 0.4; centre.push(c.round() as i32); } let at = |s: i32, o: i32| if r.vertical { (o, s) } else { (s, o) }; let kind = if r.lava { Kind::Lava } else { Kind::Water }; for s in 1..len - 1 { for k in 0..r.width { let o = centre[s as usize] - r.width / 2 + k; let (x, y) = at(s, o); if x < 1 || y < 1 || x >= w - 1 || y >= h - 1 { continue; } let t = &mut self.t[idx(x, y)]; if t.owner == Owner::Player || matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate) { continue; } t.kind = kind; t.owner = Owner::None; t.gold = 0; t.marked = false; t.reinforced = false; } } // knight bridges, evenly spread, with a little landing at both ends let mut out = Vec::new(); for b in 0..r.bridges { let s = ((b as f32 + 0.5) / r.bridges as f32 * (len - 6) as f32) as i32 + 3; for k in -1..=r.width { let o = centre[s as usize] - r.width / 2 + k; let (x, y) = at(s, o); if !inb(x, y) || x < 1 || y < 1 || x >= w - 1 || y >= h - 1 { continue; } let t = &mut self.t[idx(x, y)]; if t.liquid() { t.kind = if r.lava { Kind::LavaBridge } else { Kind::Bridge }; t.owner = Owner::Hero; out.push(idx(x, y)); } else if t.solid() && t.kind != Kind::Rock { t.kind = Kind::Floor; t.owner = Owner::None; t.gold = 0; } } } out } // Generic Dijkstra search. // cost(i) -> None = impassable, Some(c) = cost of entering. // goal(i) -> true = goal reached. pub fn search(&self, start: usize, cost: C, goal: G, diag: bool) -> Option> where C: Fn(usize) -> Option, G: Fn(usize) -> bool, { let mut dist = vec![u32::MAX; self.t.len()]; let mut prev = vec![usize::MAX; self.t.len()]; let mut heap = BinaryHeap::new(); dist[start] = 0; heap.push(Reverse((0u32, start))); while let Some(Reverse((d, i))) = heap.pop() { if d > dist[i] { continue; } if goal(i) { let mut path = Vec::new(); let mut c = i; while c != start { path.push(c); c = prev[c]; } path.reverse(); return Some(path); } let (x, y) = xy(i); let dirs: &[(i32, i32)] = if diag { &DIR8 } else { &DIR4 }; for (k, &(dx, dy)) in dirs.iter().enumerate() { let nx = x + dx; let ny = y + dy; if !inb(nx, ny) { continue; } let ni = idx(nx, ny); let Some(c) = cost(ni) else { continue }; let mut step = c * 10; if k >= 4 { // diagonal only if both neighbours are walkable (no corner cutting) if c > 1 || !self.walk(x + dx, y) || !self.walk(x, y + dy) { continue; } step = 14; } let nd = d + step; if nd < dist[ni] { dist[ni] = nd; prev[ni] = i; heap.push(Reverse((nd, ni))); } } } None } pub fn walk_cost(&self) -> impl Fn(usize) -> Option + '_ { move |i| { let t = &self.t[i]; // a locked door stops the dungeon's own creatures too if t.walkable() && !(t.kind == Kind::Door && t.res & 1 == 1) { Some(1) } else { None } } } // flying creatures also cross water and lava pub fn fly_cost(&self) -> impl Fn(usize) -> Option + '_ { move |i| if self.t[i].walkable() || self.t[i].liquid() { Some(1) } else { None } } pub fn fly_path_to(&self, start: usize, target: usize) -> Option> { self.search(start, self.fly_cost(), |i| i == target, true) } pub fn hero_cost(&self) -> impl Fn(usize) -> Option + '_ { move |i| { let tl = &self.t[i]; match tl.kind { Kind::Earth if tl.reinforced => Some(70), Kind::Steel => Some(300), Kind::Earth => Some(7), Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => Some(12), Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None, Kind::Door => Some(10), _ => Some(1), } } } // next to the player's soul core pub fn adj_core(&self, i: usize) -> bool { self.adj_core_of(i, Owner::Player) } // next to the soul core of a master (player or rival) pub fn adj_core_of(&self, i: usize, owner: Owner) -> bool { let (x, y) = xy(i); DIR4.iter().any(|&(dx, dy)| self.get(x + dx, y + dy).map_or(false, |t| t.kind == Kind::Core && t.owner == owner)) } pub fn hero_path_to_core(&self, start: usize) -> Option> { self.search(start, self.hero_cost(), |i| self.adj_core(i), true) } pub fn path_to(&self, start: usize, target: usize) -> Option> { self.search(start, self.walk_cost(), |i| i == target, true) } } // ------------------------------------------------------------------ compact tile codec // // Savegame version 2 stores the tiles as one base64 string: the 2-bit texture variants, // then run-length encoded tile records (kind, owner, marked, hp, gold, food, reserved, bed). // A 256x256 map shrinks from ~4.5 MB of JSON to a few hundred KB. const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; pub fn b64_encode(data: &[u8]) -> String { let mut out = String::with_capacity(data.len() * 4 / 3 + 4); for chunk in data.chunks(3) { let b = [chunk[0], *chunk.get(1).unwrap_or(&0), *chunk.get(2).unwrap_or(&0)]; let v = (b[0] as u32) << 16 | (b[1] as u32) << 8 | b[2] as u32; for k in 0..4 { if k <= chunk.len() { out.push(B64[(v >> (18 - 6 * k) & 63) as usize] as char); } else { out.push('='); } } } out } pub fn b64_decode(s: &str) -> Option> { let mut val = [255u8; 256]; for (i, &c) in B64.iter().enumerate() { val[c as usize] = i as u8; } let bytes: Vec = s.bytes().filter(|&c| c != b'=').collect(); let mut out = Vec::with_capacity(bytes.len() * 3 / 4); for chunk in bytes.chunks(4) { let mut v = 0u32; for (k, &c) in chunk.iter().enumerate() { let d = val[c as usize]; if d == 255 { return None; } v |= (d as u32) << (18 - 6 * k); } let n = chunk.len() * 6 / 8; for k in 0..n { out.push((v >> (16 - 8 * k)) as u8); } } Some(out) } const REC: usize = 23; fn tile_record(t: &Tile) -> [u8; REC] { let mut r = [0u8; REC]; r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8; r[1] = OWNERS.iter().position(|&o| o == t.owner).unwrap_or(0) as u8; r[2] = t.marked as u8 | (t.reinforced as u8) << 1 | (t.res & 3) << 2; r[3..7].copy_from_slice(&t.hp.to_le_bytes()); r[7..11].copy_from_slice(&t.gold.to_le_bytes()); r[11..15].copy_from_slice(&t.food.to_le_bytes()); r[15..19].copy_from_slice(&t.reserved.to_le_bytes()); r[19..23].copy_from_slice(&t.bed.to_le_bytes()); r } fn tiles_ser(tiles: &[Tile], s: S) -> Result { let mut out = Vec::new(); out.extend_from_slice(&(tiles.len() as u32).to_le_bytes()); // texture variants, 4 per byte for chunk in tiles.chunks(4) { let mut b = 0u8; for (k, t) in chunk.iter().enumerate() { b |= (t.var & 3) << (k * 2); } out.push(b); } // runs of identical records let mut i = 0; while i < tiles.len() { let rec = tile_record(&tiles[i]); let mut run = 1; while i + run < tiles.len() && run < 65535 && tile_record(&tiles[i + run]) == rec { run += 1; } out.extend_from_slice(&(run as u16).to_le_bytes()); out.extend_from_slice(&rec); i += run; } s.serialize_str(&format!("v2:{}", b64_encode(&out))) } #[derive(Deserialize)] #[serde(untagged)] enum TilesIn { Packed(String), Plain(Vec), // version 1 savegames } fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { use serde::de::Error; match TilesIn::deserialize(d)? { TilesIn::Plain(v) => Ok(v), TilesIn::Packed(s) => { let data = s.strip_prefix("v2:").and_then(b64_decode).ok_or_else(|| D::Error::custom("bad tile data"))?; let rd32 = |o: usize| -> Option<[u8; 4]> { data.get(o..o + 4).map(|s| [s[0], s[1], s[2], s[3]]) }; let count = u32::from_le_bytes(rd32(0).ok_or_else(|| D::Error::custom("bad tile data"))?) as usize; if count > MAX_SIZE * MAX_SIZE { return Err(D::Error::custom("map too large")); } let mut tiles = Vec::with_capacity(count); let mut o = 4 + (count + 3) / 4; while tiles.len() < count { let run = data.get(o..o + 2).map(|s| u16::from_le_bytes([s[0], s[1]])).ok_or_else(|| D::Error::custom("bad tile data"))? as usize; let r = data.get(o + 2..o + 2 + REC).ok_or_else(|| D::Error::custom("bad tile data"))?; let f = |a: usize| [r[a], r[a + 1], r[a + 2], r[a + 3]]; let t = Tile { kind: *KINDS.get(r[0] as usize).unwrap_or(&Kind::Rock), owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None), marked: r[2] & 1 != 0, reinforced: r[2] & 2 != 0, res: (r[2] >> 2) & 3, hp: f32::from_le_bytes(f(3)), gold: i32::from_le_bytes(f(7)), food: f32::from_le_bytes(f(11)), reserved: u32::from_le_bytes(f(15)), bed: u32::from_le_bytes(f(19)), var: 0, }; for _ in 0..run.min(count - tiles.len()) { tiles.push(t); } o += 2 + REC; } for (i, t) in tiles.iter_mut().enumerate() { t.var = (data[4 + i / 4] >> ((i % 4) * 2)) & 3; } Ok(tiles) } } }