Implement tiled multiplayer city simulation and scaling stages

This commit is contained in:
mrleo1nid committed 2026-10-30 22:06:56 +03:00
1 parent 3d4b29fedf
commit 3b1fcd996a
53 files changed
+7354 -13

No files matched your search

+333
View File
@@ -0,0 +1,333 @@
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{
collections::{BTreeMap, VecDeque},
fs,
io::Read,
path::Path,
};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bounds {
pub min_x: f64,
pub min_y: f64,
pub max_x: f64,
pub max_y: f64,
}
#[derive(Clone, Deserialize)]
pub struct Metadata {
pub id: String,
pub name: String,
pub bounds: Bounds,
}
#[derive(Clone, Deserialize)]
pub struct TileRef {
pub id: String,
pub url: String,
pub bounds: Bounds,
pub sha256: Option<String>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Manifest {
pub format_version: u32,
pub city: Metadata,
pub tiles: Vec<TileRef>,
pub navigation: Option<NavigationRef>,
}
#[derive(Clone, Deserialize)]
pub struct NavigationRef {
pub url: String,
pub sha256: String,
}
#[derive(Clone, Default)]
pub struct Graph {
pub points: Vec<[i32; 2]>,
pub edges: Vec<Vec<usize>>,
}
#[derive(Clone)]
pub struct City {
pub id: String,
pub manifest: Manifest,
pub graph: Graph,
pub revision: String,
homes: BTreeMap<String, [i32; 2]>,
workplaces: BTreeMap<String, [i32; 2]>,
}
pub struct Commute {
pub home: String,
pub workplace: String,
pub points: Vec<[i32; 2]>,
}
pub fn safe_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= 80
&& id
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
}
fn point(v: &serde_json::Value) -> Result<[i32; 2], String> {
let x = v.get(0).and_then(|v| v.as_f64()).ok_or("Invalid point")? * 100.0;
let y = v.get(1).and_then(|v| v.as_f64()).ok_or("Invalid point")? * 100.0;
if !x.is_finite() || !y.is_finite() || x.abs() > i32::MAX as f64 || y.abs() > i32::MAX as f64 {
return Err("Point out of range".into());
}
Ok([x.round() as i32, y.round() as i32])
}
impl City {
pub fn load(root: &Path, id: &str) -> Result<Self, String> {
if !safe_id(id) {
return Err("Invalid city ID".into());
}
let folder = root.join(id);
let manifest_bytes = fs::read(folder.join("manifest.json")).map_err(|e| e.to_string())?;
let revision = format!("{:x}", Sha256::digest(&manifest_bytes));
let manifest: Manifest =
serde_json::from_slice(&manifest_bytes).map_err(|e| e.to_string())?;
if manifest.format_version != 1 {
return Err("Unsupported city format".into());
}
let mut graph = Graph::default();
let mut nodes = BTreeMap::<[i32; 2], usize>::new();
let mut roads = std::collections::HashSet::new();
let mut homes = BTreeMap::new();
let mut workplaces = BTreeMap::new();
let inputs = if let Some(nav) = &manifest.navigation {
if nav.url != "navigation.json.gz" {
return Err("Invalid navigation URL".into());
}
vec![TileRef {
id: "navigation".into(),
url: nav.url.clone(),
bounds: manifest.city.bounds.clone(),
sha256: Some(nav.sha256.clone()),
}]
} else {
manifest.tiles.clone()
};
for tile in &inputs {
let expected = if tile.id == "navigation" {
"navigation.json.gz".to_string()
} else {
format!("tiles/{}.json.gz", tile.id)
};
if tile.url != expected
|| (tile.id != "navigation"
&& tile
.id
.bytes()
.any(|c| !(c.is_ascii_digit() || c == b'-' || c == b'_')))
{
return Err("Invalid tile URL".into());
}
let bytes = fs::read(folder.join(&tile.url)).map_err(|e| e.to_string())?;
let mut text = String::new();
flate2::read::GzDecoder::new(&bytes[..])
.read_to_string(&mut text)
.map_err(|e| e.to_string())?;
if let Some(expected) = &tile.sha256 {
if &format!("{:x}", Sha256::digest(text.as_bytes())) != expected {
return Err("City tile checksum mismatch".into());
}
}
let value: serde_json::Value =
serde_json::from_str(&text).map_err(|e| e.to_string())?;
if value["formatVersion"] != 1
|| value["cityId"] != manifest.city.id
|| value["id"] != tile.id
{
return Err("Tile does not belong to manifest".into());
}
if let Some(buildings) = value["buildings"].as_array() {
for building in buildings {
let Some(id) = building["id"].as_str() else {
continue;
};
let mut entrance = None;
if let Some(floors) = building["floors"].as_array() {
for floor in floors {
if floor["level"] == 0 {
if let Some(doors) = floor["doors"].as_array() {
for door in doors {
if door["rooms"].as_array().is_some_and(|rooms| {
rooms.iter().any(|r| r == "outside")
}) {
let a = point(&door["a"])?;
let b = point(&door["b"])?;
entrance = Some([
((a[0] as i64 + b[0] as i64) / 2) as i32,
((a[1] as i64 + b[1] as i64) / 2) as i32,
]);
break;
}
}
}
}
}
}
if let Some(p) = entrance {
match building["kind"].as_str().unwrap_or("") {
"house" | "apartments" | "mixed" => {
homes.insert(id.to_string(), p);
}
"office" | "retail" | "industrial" | "school" | "hospital"
| "restaurant" | "warehouse" => {
workplaces.insert(id.to_string(), p);
}
_ => {}
}
}
}
}
for road in value["roads"].as_array().ok_or("Missing roads")? {
let id = road["id"].as_str().ok_or("Missing road ID")?;
if !roads.insert(id.to_string()) {
continue;
}
if [
"river", "stream", "canal", "ditch", "rail", "tram", "motorway", "trunk",
]
.contains(&road["kind"].as_str().unwrap_or(""))
{
continue;
}
let mut previous: Option<usize> = None;
for v in road["pts"].as_array().ok_or("Missing road points")? {
let p = point(v)?;
let next_id = graph.points.len();
let index = *nodes.entry(p).or_insert_with(|| {
graph.points.push(p);
graph.edges.push(vec![]);
next_id
});
if let Some(prev) = previous {
if prev != index {
graph.edges[prev].push(index);
graph.edges[index].push(prev);
}
}
previous = Some(index);
}
}
}
if graph.points.len() < 2 {
return Err("City has no walkable road graph".into());
}
Ok(Self {
id: id.into(),
manifest,
graph,
revision,
homes,
workplaces,
})
}
pub fn commutes(&self, count: usize, seed: u64) -> Vec<Commute> {
let homes: Vec<_> = self.homes.iter().collect();
let works: Vec<_> = self.workplaces.iter().collect();
let nearest = |p: &[i32; 2]| {
self.graph
.points
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
let distance = |q: &&[i32; 2]| {
(q[0] as f64 - p[0] as f64).powi(2) + (q[1] as f64 - p[1] as f64).powi(2)
};
distance(a).total_cmp(&distance(b))
})
.map(|(i, _)| i)
.unwrap_or(0)
};
let mut result = vec![];
if !homes.is_empty() && !works.is_empty() {
for i in 0..count {
let (home, a) =
homes[(i.wrapping_mul(7919).wrapping_add(seed as usize)) % homes.len()];
let (work, b) =
works[(i.wrapping_mul(31).wrapping_add(seed as usize)) % works.len()];
if let Some(points) = self.graph.between(nearest(a), nearest(b)) {
if points.len() > 1 {
result.push(Commute {
home: home.clone(),
workplace: work.clone(),
points,
});
}
}
}
}
if result.is_empty() {
return self
.graph
.routes(count, seed)
.into_iter()
.map(|points| Commute {
home: String::new(),
workplace: String::new(),
points,
})
.collect();
}
result
}
}
impl Graph {
fn between(&self, start: usize, end: usize) -> Option<Vec<[i32; 2]>> {
let mut parent = vec![usize::MAX; self.points.len()];
parent[start] = start;
let mut queue = VecDeque::from([start]);
while let Some(at) = queue.pop_front() {
if at == end {
let mut p = vec![self.points[at]];
let mut at = at;
while at != start {
at = parent[at];
p.push(self.points[at]);
}
p.reverse();
return Some(p);
}
for &next in &self.edges[at] {
if parent[next] == usize::MAX {
parent[next] = at;
queue.push_back(next);
}
}
}
None
}
/// Stable breadth-first paths along actual road segments, shared by many residents.
pub fn routes(&self, count: usize, seed: u64) -> Vec<Vec<[i32; 2]>> {
let mut routes = Vec::new();
for i in 0..count {
let start = (seed as usize).wrapping_add(i.wrapping_mul(7919)) % self.points.len();
let mut parent = vec![usize::MAX; self.points.len()];
parent[start] = start;
let mut queue = VecDeque::from([start]);
let mut last = start;
while let Some(node) = queue.pop_front() {
last = node;
for &next in &self.edges[node] {
if parent[next] == usize::MAX {
parent[next] = node;
queue.push_back(next);
}
}
}
if last == start {
continue;
}
let mut route = vec![self.points[last]];
while last != start {
last = parent[last];
route.push(self.points[last]);
}
route.reverse();
routes.push(route);
}
routes
}
}