station/crates/game_server/src/world.rs

199 lines
6.2 KiB
Rust
Raw Normal View History

//! World setup: Tiled map, spawning buildings and items, and tile occupancy.
use std::{collections::BTreeMap, sync::Arc};
use bevy::prelude::*;
use bevy_replicon::prelude::*;
use game_core::{
GameData,
components::{
Building, BuildingState, Character, GridPos, GroundItem, ItemStack, Machine, WorldMap,
},
map::MapLayout,
};
use crate::{ServerSettings, persistence};
/// The loaded map, used for collision and spawn points.
#[derive(Resource, Clone)]
pub struct WorldLayout(pub Arc<MapLayout>);
#[derive(Resource, Clone, Copy)]
pub struct SpawnPoint(pub GridPos);
pub fn setup_world(mut commands: Commands, settings: Res<ServerSettings>, data: Res<GameData>) {
let assets = game_core::assets_dir();
let layout = match MapLayout::load(&assets, &settings.map) {
Ok(layout) => layout,
Err(e) => return crate::fail(&mut commands, e.to_string()),
};
let spawn = layout
.objects_of_class("spawn")
.next()
.map(|o| o.pos)
.unwrap_or_else(|| {
warn!("map has no `spawn` object, using the map center");
GridPos::new(layout.width as i32 / 2, layout.height as i32 / 2)
});
commands.insert_resource(SpawnPoint(spawn));
commands.spawn((
Replicated,
WorldMap {
path: settings.map.clone(),
},
));
let mut save = None;
if let Some(storage) = &settings.storage {
let (storage, loaded) = match persistence::Storage::open(storage, &settings.map) {
Ok(opened) => opened,
Err(e) => {
return crate::fail(
&mut commands,
format!("failed to open the world database: {e}"),
);
}
};
info!("world `{}` ({})", storage.world.name, storage.world.id);
if loaded.is_some() && storage.world.map != settings.map {
warn!(
"world was created on map `{}`, now running `{}`",
storage.world.map, settings.map
);
}
save = loaded;
commands.insert_resource(storage);
}
match save {
Some(save) => {
info!(
"restoring persistent world: {} buildings, {} items, {} characters",
save.buildings.len(),
save.items.len(),
save.characters.len()
);
persistence::restore(&mut commands, &data, save);
}
None => {
info!("creating a new world from `{}`", settings.map);
spawn_map_objects(&mut commands, &data, &layout);
}
}
commands.insert_resource(WorldLayout(Arc::new(layout)));
}
fn spawn_map_objects(commands: &mut Commands, data: &GameData, layout: &MapLayout) {
for obj in &layout.objects {
let proto = obj.properties.get("proto");
match (obj.class.as_str(), proto) {
("spawn", _) => {}
("building", Some(proto)) => {
// Integer custom properties override the building's script variables.
let vars = obj
.properties
.iter()
.filter_map(|(k, v)| Some((k.clone(), v.parse::<i64>().ok()?)))
.collect();
if spawn_building(commands, data, proto, obj.pos, vars).is_none() {
warn!("map object {}: unknown building `{proto}`", obj.id);
}
}
("item", Some(proto)) => {
let count = obj
.properties
.get("count")
.and_then(|c| c.parse().ok())
.unwrap_or(1);
if data.items.contains_key(proto) {
spawn_ground_item(commands, ItemStack::new(proto, count), obj.pos);
} else {
warn!("map object {}: unknown item `{proto}`", obj.id);
}
}
(class, _) => warn!(
"map object {} (`{}`): unsupported class `{class}` or missing `proto` property",
obj.id, obj.name
),
}
}
}
/// Spawns a building with its default state. `vars` override the prototype's variables.
pub fn spawn_building(
commands: &mut Commands,
data: &GameData,
proto: &str,
pos: GridPos,
vars: BTreeMap<String, i64>,
) -> Option<Entity> {
let def = data.buildings.get(proto)?;
let mut state = BuildingState {
solid: def.solid,
variant: None,
health: def.max_health,
vars: def.vars.clone(),
};
state.vars.extend(vars);
let machine = data.machines.get(proto).map(Machine::new);
Some(spawn_building_with_state(
commands, proto, pos, state, machine,
))
}
pub fn spawn_building_with_state(
commands: &mut Commands,
proto: &str,
pos: GridPos,
state: BuildingState,
machine: Option<Machine>,
) -> Entity {
let mut entity = commands.spawn((
Replicated,
Building {
proto: proto.to_owned(),
},
pos,
state,
));
if let Some(machine) = machine {
entity.insert(machine);
}
entity.id()
}
pub fn spawn_ground_item(commands: &mut Commands, stack: ItemStack, pos: GridPos) -> Entity {
commands.spawn((Replicated, GroundItem(stack), pos)).id()
}
/// Whether a character can not enter `pos`.
pub fn is_blocked<'a>(
pos: GridPos,
layout: &MapLayout,
buildings: impl IntoIterator<Item = (&'a GridPos, &'a BuildingState)>,
) -> bool {
layout.is_solid(pos) || buildings.into_iter().any(|(p, s)| *p == pos && s.solid)
}
/// Finds the closest free tile around `start` (spiral search), for spawning characters.
pub fn find_free_tile(
start: GridPos,
layout: &MapLayout,
buildings: &Query<(&GridPos, &BuildingState), (With<Building>, Without<Character>)>,
) -> GridPos {
for radius in 0..16i32 {
for dy in -radius..=radius {
for dx in -radius..=radius {
if dx.abs() != radius && dy.abs() != radius {
continue;
}
let pos = GridPos::new(start.x + dx, start.y + dy);
if !is_blocked(pos, layout, buildings.iter()) {
return pos;
}
}
}
}
start
}