Implement tiled multiplayer city simulation and scaling stages
This commit is contained in:
1 parent
3d4b29fedf
commit
3b1fcd996a
53 files changed
+7354
-13
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user