601 lines
20 KiB
Rust
601 lines
20 KiB
Rust
// 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,
|
|
}
|
|
|
|
// order matters for the packed savegame codec: only append new kinds
|
|
const KINDS: [Kind; 15] = [
|
|
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,
|
|
];
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
|
pub enum Owner {
|
|
None,
|
|
Player,
|
|
Hero,
|
|
}
|
|
|
|
const OWNERS: [Owner; 3] = [Owner::None, Owner::Player, Owner::Hero];
|
|
|
|
// 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
|
|
}
|
|
|
|
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 }
|
|
}
|
|
pub fn solid(&self) -> bool {
|
|
matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold)
|
|
}
|
|
pub fn diggable(&self) -> bool {
|
|
matches!(self.kind, Kind::Earth | Kind::Gold)
|
|
}
|
|
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
|
|
)
|
|
}
|
|
}
|
|
|
|
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<Tile>,
|
|
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
|
|
}
|
|
|
|
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<usize> {
|
|
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 {
|
|
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);
|
|
// make sure heroes can reach the core
|
|
if 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) -> 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 = (area.round() as usize).max(1);
|
|
let n_treasures = n_portals - 1;
|
|
|
|
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, 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 = vec![first];
|
|
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<Tile>, 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 k == Kind::Gold {
|
|
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));
|
|
}
|
|
// 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<Tile>, 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));
|
|
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
|
|
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;
|
|
}
|
|
}
|
|
for tl in t.iter_mut() {
|
|
if tl.kind == Kind::Gold {
|
|
tl.hp = 6.0;
|
|
}
|
|
}
|
|
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures }
|
|
}
|
|
|
|
// Generic Dijkstra search.
|
|
// cost(i) -> None = impassable, Some(c) = cost of entering.
|
|
// goal(i) -> true = goal reached.
|
|
pub fn search<C, G>(&self, start: usize, cost: C, goal: G, diag: bool) -> Option<Vec<usize>>
|
|
where
|
|
C: Fn(usize) -> Option<u32>,
|
|
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<u32> + '_ {
|
|
move |i| if self.t[i].walkable() { Some(1) } else { None }
|
|
}
|
|
|
|
pub fn hero_cost(&self) -> impl Fn(usize) -> Option<u32> + '_ {
|
|
move |i| {
|
|
let tl = &self.t[i];
|
|
match tl.kind {
|
|
Kind::Earth if tl.reinforced => Some(70),
|
|
Kind::Earth => Some(7),
|
|
Kind::Gold => Some(12),
|
|
Kind::Rock | Kind::Core | Kind::Tower => None,
|
|
_ => Some(1),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn adj_core(&self, i: usize) -> 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))
|
|
}
|
|
|
|
pub fn hero_path_to_core(&self, start: usize) -> Option<Vec<usize>> {
|
|
self.search(start, self.hero_cost(), |i| self.adj_core(i), true)
|
|
}
|
|
|
|
pub fn path_to(&self, start: usize, target: usize) -> Option<Vec<usize>> {
|
|
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+/";
|
|
|
|
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
|
|
}
|
|
|
|
fn b64_decode(s: &str) -> Option<Vec<u8>> {
|
|
let mut val = [255u8; 256];
|
|
for (i, &c) in B64.iter().enumerate() {
|
|
val[c as usize] = i as u8;
|
|
}
|
|
let bytes: Vec<u8> = 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;
|
|
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<S: Serializer>(tiles: &[Tile], s: S) -> Result<S::Ok, S::Error> {
|
|
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<Tile>), // version 1 savegames
|
|
}
|
|
|
|
fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Tile>, 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,
|
|
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)
|
|
}
|
|
}
|
|
}
|