scroll.server/crates/logic/src/battle/logic_tilemap.rs
WiseDev 774e7c60cf build and send the battle sector state
npc missions used to get a ServerErrorMessage back. now StartMission builds a
LogicGameMode snapshot off the arena tilemap and answers 21903.

towers come from assets/locations/*.csv the way initDefaultSector does it: tile
coordinates times 500, leader index decided by which half of the map the tower
sits in. the two king towers must be there, the client dereferences them without
a null check. they live in buildings.csv, not characters.csv.

LogicCharacter puts the base object fields fourth, not first. the buff component
writes a fixed array sized by the character_buffs row count even with no buffs.

training_arena parses to 2 kings, 4 princess towers, 36x64 subtiles. snapshot is
602 bytes over 6 objects. the real client has not seen it yet.
2026-08-23 10:24:20 +03:00

110 lines
3.7 KiB
Rust

use std::collections::HashMap;
use std::path::Path;
pub const SUBTILE_UNITS: i32 = 500;
pub const SECTION_OBJECTS: &str = "Objects";
pub const SECTION_MAP: &str = "Map";
pub const OBJECT_KING_TOWER: &str = "KingTower";
pub const OBJECT_PRINCESS_TOWER: &str = "PrincessTower";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LogicTilemap {
pub objects: Vec<(String, Vec<(i32, i32)>)>,
pub tiles: Vec<Vec<i32>>,
}
fn split_row(line: &str) -> Vec<String> {
let mut cells = Vec::new();
let mut current = String::new();
let mut quoted = false;
for character in line.chars() {
match character {
'"' => quoted = !quoted,
',' if !quoted => cells.push(std::mem::take(&mut current)),
'\r' => {}
_ => current.push(character),
}
}
cells.push(current);
cells
}
fn cell(row: &[String], index: usize) -> &str {
row.get(index).map(String::as_str).unwrap_or("").trim()
}
fn number(value: &str) -> Option<i32> {
value.parse::<i32>().ok()
}
impl LogicTilemap {
pub fn parse(source: &str) -> Self {
let mut tilemap = Self::default();
let mut section = String::new();
let mut object: Option<String> = None;
for line in source.lines() {
let row = split_row(line);
let head = cell(&row, 0);
if !head.is_empty() {
section = head.to_owned();
object = None;
continue;
}
match section.as_str() {
SECTION_OBJECTS => {
let name = cell(&row, 1);
if !name.is_empty() && name != "string" && name != "int" {
object = Some(name.to_owned());
tilemap.objects.push((name.to_owned(), Vec::new()));
continue;
}
let (Some(x), Some(y)) = (number(cell(&row, 2)), number(cell(&row, 3))) else {
continue;
};
if object.is_some() {
if let Some(entry) = tilemap.objects.last_mut() {
entry.1.push((x, y));
}
}
}
SECTION_MAP => {
let values: Vec<i32> = row[1..]
.iter()
.map(|value| value.trim())
.take_while(|value| !value.is_empty())
.filter_map(number)
.collect();
if !values.is_empty() {
tilemap.tiles.push(values);
}
}
_ => {}
}
}
tilemap
}
pub fn load(root: &Path, file_name: &str) -> std::io::Result<Self> {
let path = root.join(file_name);
let source = std::fs::read_to_string(&path)?;
Ok(Self::parse(&source))
}
pub fn height(&self) -> i32 {
self.tiles.len() as i32
}
pub fn width(&self) -> i32 {
self.tiles.iter().map(Vec::len).max().unwrap_or(0) as i32
}
pub fn placements(&self, name: &str) -> &[(i32, i32)] {
self.objects
.iter()
.find(|(object, _)| object == name)
.map(|(_, positions)| positions.as_slice())
.unwrap_or(&[])
}
pub fn king_towers(&self) -> &[(i32, i32)] {
self.placements(OBJECT_KING_TOWER)
}
pub fn princess_towers(&self) -> &[(i32, i32)] {
self.placements(OBJECT_PRINCESS_TOWER)
}
pub fn counts(&self) -> HashMap<&str, usize> {
self.objects
.iter()
.map(|(name, positions)| (name.as_str(), positions.len()))
.collect()
}
}