feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
//! Tiled (`.tmx`) map support.
|
|
|
|
|
//!
|
|
|
|
|
//! Conventions understood by the game:
|
|
|
|
|
//! - Tile layers are rendered in order. A tile is solid when its tile (in the tileset)
|
|
|
|
|
//! or its layer has the boolean custom property `solid = true`.
|
|
|
|
|
//! - Object layers spawn gameplay entities based on the object *class*:
|
|
|
|
|
//! - `spawn`: player spawn point.
|
|
|
|
|
//! - `building`: building with the custom property `proto` (a building id).
|
|
|
|
|
//! - `item`: ground item with the property `proto` (an item id) and optional `count`.
|
|
|
|
|
//!
|
2026-10-21 18:22:13 +00:00
|
|
|
//! State overrides (`state.*`, `machine.*`) and legacy integer script variables
|
|
|
|
|
//! are decoded by the server for both new worlds and imports.
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
|
|
|
|
|
use std::{
|
2026-10-21 18:22:13 +00:00
|
|
|
collections::{BTreeMap, BTreeSet},
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
path::{Path, PathBuf},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use crate::components::GridPos;
|
2026-10-21 18:22:13 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
|
2026-10-21 18:22:13 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
pub struct TilesetInfo {
|
|
|
|
|
/// Image path relative to the assets directory.
|
|
|
|
|
pub image: String,
|
|
|
|
|
pub image_width: u32,
|
|
|
|
|
pub image_height: u32,
|
|
|
|
|
pub tile_width: u32,
|
|
|
|
|
pub tile_height: u32,
|
|
|
|
|
pub columns: u32,
|
|
|
|
|
pub tile_count: u32,
|
|
|
|
|
pub spacing: u32,
|
|
|
|
|
pub margin: u32,
|
2026-10-21 18:22:13 +00:00
|
|
|
pub solid_tiles: BTreeSet<u32>,
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:22:13 +00:00
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
pub struct TileCell {
|
|
|
|
|
pub tileset: usize,
|
|
|
|
|
pub id: u32,
|
|
|
|
|
pub flip_h: bool,
|
|
|
|
|
pub flip_v: bool,
|
2026-10-21 18:22:13 +00:00
|
|
|
pub flip_d: bool,
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:22:13 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
pub struct TileLayerData {
|
|
|
|
|
pub name: String,
|
2026-10-21 18:22:13 +00:00
|
|
|
pub solid: bool,
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
/// Row-major cells, `width * height` entries.
|
|
|
|
|
pub cells: Vec<Option<TileCell>>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:22:13 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
pub struct MapObject {
|
|
|
|
|
pub id: u32,
|
|
|
|
|
pub class: String,
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub pos: GridPos,
|
|
|
|
|
pub properties: BTreeMap<String, String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-21 18:22:13 +00:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
pub struct MapLayout {
|
|
|
|
|
pub width: u32,
|
|
|
|
|
pub height: u32,
|
|
|
|
|
pub tile_width: u32,
|
|
|
|
|
pub tile_height: u32,
|
|
|
|
|
pub tilesets: Vec<TilesetInfo>,
|
|
|
|
|
pub layers: Vec<TileLayerData>,
|
|
|
|
|
pub objects: Vec<MapObject>,
|
|
|
|
|
solid: Vec<bool>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum MapError {
|
|
|
|
|
#[error("failed to load map `{0}`: {1}")]
|
|
|
|
|
Tiled(PathBuf, tiled::Error),
|
|
|
|
|
#[error("map `{0}`: {1}")]
|
|
|
|
|
Unsupported(PathBuf, String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl MapLayout {
|
2026-10-21 18:22:13 +00:00
|
|
|
/// Loads an asset-relative or absolute map path.
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
pub fn load(assets_dir: &Path, map_path: &str) -> Result<Self, MapError> {
|
|
|
|
|
let full = assets_dir.join(map_path);
|
|
|
|
|
let unsupported = |msg: String| MapError::Unsupported(full.clone(), msg);
|
|
|
|
|
let map = tiled::Loader::new()
|
|
|
|
|
.load_tmx_map(&full)
|
|
|
|
|
.map_err(|e| MapError::Tiled(full.clone(), e))?;
|
|
|
|
|
if map.orientation != tiled::Orientation::Orthogonal {
|
|
|
|
|
return Err(unsupported("only orthogonal maps are supported".into()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let assets_canon = assets_dir
|
|
|
|
|
.canonicalize()
|
|
|
|
|
.unwrap_or_else(|_| assets_dir.into());
|
|
|
|
|
let mut tilesets = Vec::new();
|
|
|
|
|
for ts in map.tilesets() {
|
|
|
|
|
let image = ts.image.as_ref().ok_or_else(|| {
|
|
|
|
|
unsupported(format!(
|
|
|
|
|
"tileset `{}` must be based on a single image (image collections are not supported)",
|
|
|
|
|
ts.name
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
let image_path = image.source.canonicalize().unwrap_or(image.source.clone());
|
|
|
|
|
let rel = image_path.strip_prefix(&assets_canon).map_err(|_| {
|
|
|
|
|
unsupported(format!(
|
|
|
|
|
"tileset image `{}` must be inside the assets directory",
|
|
|
|
|
image.source.display()
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
tilesets.push(TilesetInfo {
|
|
|
|
|
image: rel.to_string_lossy().replace('\\', "/"),
|
|
|
|
|
image_width: image.width as u32,
|
|
|
|
|
image_height: image.height as u32,
|
|
|
|
|
tile_width: ts.tile_width,
|
|
|
|
|
tile_height: ts.tile_height,
|
|
|
|
|
columns: ts.columns,
|
|
|
|
|
tile_count: ts.tilecount,
|
|
|
|
|
spacing: ts.spacing,
|
|
|
|
|
margin: ts.margin,
|
2026-10-21 18:22:13 +00:00
|
|
|
solid_tiles: ts
|
|
|
|
|
.tiles()
|
|
|
|
|
.filter(|(_, tile)| bool_prop(&tile.properties, "solid"))
|
|
|
|
|
.map(|(id, _)| id)
|
|
|
|
|
.collect(),
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (width, height) = (map.width, map.height);
|
2026-10-21 18:22:13 +00:00
|
|
|
if width == 0 || height == 0 || map.tile_width == 0 || map.tile_height == 0 {
|
|
|
|
|
return Err(unsupported(
|
|
|
|
|
"map and tile dimensions must be nonzero".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
if width > i32::MAX as u32
|
|
|
|
|
|| height > i32::MAX as u32
|
|
|
|
|
|| u64::from(width) * u64::from(height) > 1_000_000
|
|
|
|
|
{
|
|
|
|
|
return Err(unsupported(
|
|
|
|
|
"maps may contain at most 1,000,000 cells".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
let mut layers = Vec::new();
|
|
|
|
|
let mut solid = vec![false; (width * height) as usize];
|
|
|
|
|
let mut objects = Vec::new();
|
|
|
|
|
let mut stack: Vec<tiled::Layer> = map.layers().collect();
|
|
|
|
|
stack.reverse();
|
|
|
|
|
while let Some(layer) = stack.pop() {
|
|
|
|
|
let layer_solid = bool_prop(&layer.properties, "solid");
|
|
|
|
|
match layer.layer_type() {
|
|
|
|
|
tiled::LayerType::Tiles(tiles) => {
|
|
|
|
|
if matches!(tiles, tiled::TileLayer::Infinite(_)) {
|
|
|
|
|
return Err(unsupported("infinite maps are not supported".into()));
|
|
|
|
|
}
|
|
|
|
|
let mut cells = Vec::with_capacity(solid.len());
|
|
|
|
|
for y in 0..height as i32 {
|
|
|
|
|
for x in 0..width as i32 {
|
|
|
|
|
let cell = tiles.get_tile(x, y).map(|t| {
|
|
|
|
|
let tile_solid = t
|
|
|
|
|
.get_tile()
|
|
|
|
|
.is_some_and(|d| bool_prop(&d.properties, "solid"));
|
|
|
|
|
if tile_solid || layer_solid {
|
|
|
|
|
solid[(y as u32 * width + x as u32) as usize] = true;
|
|
|
|
|
}
|
|
|
|
|
TileCell {
|
|
|
|
|
tileset: t.tileset_index(),
|
|
|
|
|
id: t.id(),
|
|
|
|
|
flip_h: t.flip_h,
|
|
|
|
|
flip_v: t.flip_v,
|
2026-10-21 18:22:13 +00:00
|
|
|
flip_d: t.flip_d,
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
cells.push(cell);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
layers.push(TileLayerData {
|
|
|
|
|
name: layer.name.clone(),
|
2026-10-21 18:22:13 +00:00
|
|
|
solid: layer_solid,
|
feat: add persistent-world station game
Bevy ECS workspace with three crates:
- game_core: rsn data definitions, components, Tiled map loader, protocol
- game_server: authoritative simulation, Rune scripting, persistence,
dedicated server binary
- game_client: rendering, UI and input; embeds the server for
singleplayer and hosting
Items, recipes, buildings and skills are defined in assets/data, behavior
in Rune scripts under assets/scripts, and maps are made in Tiled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-18 17:44:13 +00:00
|
|
|
cells,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
tiled::LayerType::Objects(group) => {
|
|
|
|
|
for obj in group.objects() {
|
|
|
|
|
let tw = map.tile_width as f32;
|
|
|
|
|
let th = map.tile_height as f32;
|
|
|
|
|
// Tile objects are anchored at their bottom-left corner.
|
|
|
|
|
let y = if obj.get_tile().is_some() {
|
|
|
|
|
obj.y - 1.0
|
|
|
|
|
} else {
|
|
|
|
|
obj.y
|
|
|
|
|
};
|
|
|
|
|
objects.push(MapObject {
|
|
|
|
|
id: obj.id(),
|
|
|
|
|
class: obj.user_type.clone(),
|
|
|
|
|
name: obj.name.clone(),
|
|
|
|
|
pos: GridPos::new((obj.x / tw).floor() as i32, (y / th).floor() as i32),
|
|
|
|
|
properties: obj
|
|
|
|
|
.properties
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(k, v)| (k.clone(), prop_to_string(v)))
|
|
|
|
|
.collect(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tiled::LayerType::Group(group) => {
|
|
|
|
|
let children: Vec<_> = group.layers().collect();
|
|
|
|
|
stack.extend(children.into_iter().rev());
|
|
|
|
|
}
|
|
|
|
|
tiled::LayerType::Image(_) => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
tile_width: map.tile_width,
|
|
|
|
|
tile_height: map.tile_height,
|
|
|
|
|
tilesets,
|
|
|
|
|
layers,
|
|
|
|
|
objects,
|
|
|
|
|
solid,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn in_bounds(&self, pos: GridPos) -> bool {
|
|
|
|
|
pos.x >= 0 && pos.y >= 0 && (pos.x as u32) < self.width && (pos.y as u32) < self.height
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Tiles outside of the map are always solid.
|
|
|
|
|
pub fn is_solid(&self, pos: GridPos) -> bool {
|
|
|
|
|
!self.in_bounds(pos) || self.solid[(pos.y as u32 * self.width + pos.x as u32) as usize]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn objects_of_class<'a>(&'a self, class: &'a str) -> impl Iterator<Item = &'a MapObject> {
|
|
|
|
|
self.objects.iter().filter(move |o| o.class == class)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn bool_prop(props: &tiled::Properties, key: &str) -> bool {
|
|
|
|
|
matches!(props.get(key), Some(tiled::PropertyValue::BoolValue(true)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn prop_to_string(value: &tiled::PropertyValue) -> String {
|
|
|
|
|
use tiled::PropertyValue::*;
|
|
|
|
|
match value {
|
|
|
|
|
BoolValue(v) => v.to_string(),
|
|
|
|
|
FloatValue(v) => v.to_string(),
|
|
|
|
|
IntValue(v) => v.to_string(),
|
|
|
|
|
StringValue(v) | FileValue(v) => v.clone(),
|
|
|
|
|
ObjectValue(v) => v.to_string(),
|
|
|
|
|
other => format!("{other:?}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn loads_shipped_map() {
|
|
|
|
|
let map = MapLayout::load(&crate::assets_dir(), crate::DEFAULT_MAP).unwrap();
|
|
|
|
|
assert!(map.width > 0 && map.height > 0);
|
|
|
|
|
assert!(!map.layers.is_empty());
|
|
|
|
|
assert!(map.objects_of_class("spawn").next().is_some());
|
|
|
|
|
assert!(map.is_solid(GridPos::new(-1, 0)));
|
|
|
|
|
assert!(
|
|
|
|
|
map.is_solid(GridPos::new(0, 0)),
|
|
|
|
|
"map border should be walls"
|
|
|
|
|
);
|
|
|
|
|
let spawn = map.objects_of_class("spawn").next().unwrap().pos;
|
|
|
|
|
assert!(!map.is_solid(spawn));
|
|
|
|
|
}
|
|
|
|
|
}
|