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.
110 lines
3.7 KiB
Rust
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()
|
|
}
|
|
}
|