diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/cli/cmd/edit.rs (renamed from src/cli/cmd/edit.rs) | 4 | ||||
| -rw-r--r-- | src/client/cli/cmd/mod.rs (renamed from src/cli/cmd/mod.rs) | 2 | ||||
| -rw-r--r-- | src/client/cli/cmd/read.rs (renamed from src/cli/cmd/read.rs) | 2 | ||||
| -rw-r--r-- | src/client/cli/cmd/write.rs (renamed from src/cli/cmd/write.rs) | 3 | ||||
| -rw-r--r-- | src/client/cli/mod.rs (renamed from src/cli/mod.rs) | 6 | ||||
| -rw-r--r-- | src/client/colours.rs (renamed from src/colours.rs) | 0 | ||||
| -rw-r--r-- | src/client/config.rs (renamed from src/config.rs) | 2 | ||||
| -rw-r--r-- | src/client/editor.rs (renamed from src/editor.rs) | 41 | ||||
| -rw-r--r-- | src/client/grid.rs (renamed from src/grid.rs) | 4 | ||||
| -rw-r--r-- | src/client/gui/decimal_num_box.rs (renamed from src/gui/decimal_num_box.rs) | 0 | ||||
| -rw-r--r-- | src/client/gui/dimension_indicator.rs (renamed from src/gui/dimension_indicator.rs) | 32 | ||||
| -rw-r--r-- | src/client/gui/mod.rs (renamed from src/gui/mod.rs) | 0 | ||||
| -rw-r--r-- | src/client/gui/position_indicator.rs (renamed from src/gui/position_indicator.rs) | 6 | ||||
| -rw-r--r-- | src/client/gui/tool_sidebar.rs (renamed from src/gui/tool_sidebar.rs) | 6 | ||||
| -rw-r--r-- | src/client/input/binding.rs (renamed from src/input/binding.rs) | 0 | ||||
| -rw-r--r-- | src/client/input/button.rs (renamed from src/input/button.rs) | 0 | ||||
| -rw-r--r-- | src/client/input/mod.rs (renamed from src/input/mod.rs) | 0 | ||||
| -rw-r--r-- | src/client/map/icon_mark.rs | 95 | ||||
| -rw-r--r-- | src/client/map/icon_texture_manager.rs (renamed from src/map/icon_renderer.rs) | 14 | ||||
| -rw-r--r-- | src/client/map/mappable.rs (renamed from src/map/mappable.rs) | 20 | ||||
| -rw-r--r-- | src/client/map/mod.rs | 206 | ||||
| -rw-r--r-- | src/client/map/room_mark.rs (renamed from src/map/room.rs) | 61 | ||||
| -rw-r--r-- | src/client/map/wall_mark.rs (renamed from src/map/wall.rs) | 63 | ||||
| -rw-r--r-- | src/client/mod.rs | 137 | ||||
| -rw-r--r-- | src/client/snapping.rs (renamed from src/snapping.rs) | 2 | ||||
| -rw-r--r-- | src/client/svg/mod.rs (renamed from src/svg/mod.rs) | 0 | ||||
| -rw-r--r-- | src/client/svg/style.rs (renamed from src/svg/style.rs) | 0 | ||||
| -rw-r--r-- | src/client/tool/deletion_tool.rs (renamed from src/tool/deletion_tool.rs) | 25 | ||||
| -rw-r--r-- | src/client/tool/icon_tool.rs (renamed from src/tool/icon_tool.rs) | 44 | ||||
| -rw-r--r-- | src/client/tool/mod.rs (renamed from src/tool/mod.rs) | 28 | ||||
| -rw-r--r-- | src/client/tool/polygon_room_tool.rs (renamed from src/tool/polygon_room_tool.rs) | 27 | ||||
| -rw-r--r-- | src/client/tool/rect_room_tool.rs (renamed from src/tool/rect_room_tool.rs) | 25 | ||||
| -rw-r--r-- | src/client/tool/selection_tool.rs (renamed from src/tool/selection_tool.rs) | 18 | ||||
| -rw-r--r-- | src/client/tool/wall_tool.rs (renamed from src/tool/wall_tool.rs) | 15 | ||||
| -rw-r--r-- | src/client/transform.rs (renamed from src/transform.rs) | 0 | ||||
| -rw-r--r-- | src/client_main.rs | 178 | ||||
| -rw-r--r-- | src/map/data.rs | 87 | ||||
| -rw-r--r-- | src/map/icon.rs | 102 | ||||
| -rw-r--r-- | src/map/mod.rs | 171 | ||||
| -rw-r--r-- | src/net/cargo.rs | 33 | ||||
| -rw-r--r-- | src/net/client/connection.rs | 119 | ||||
| -rw-r--r-- | src/net/client/mod.rs | 4 | ||||
| -rw-r--r-- | src/net/mod.rs | 11 | ||||
| -rw-r--r-- | src/net/packet.rs | 73 | ||||
| -rw-r--r-- | src/net/server/connection.rs | 84 | ||||
| -rw-r--r-- | src/net/server/connection_manager.rs | 109 | ||||
| -rw-r--r-- | src/net/server/mod.rs | 6 | ||||
| -rw-r--r-- | src/server/mod.rs | 48 | ||||
| -rw-r--r-- | src/server_main.rs | 65 | ||||
| -rw-r--r-- | src/stable_vec.rs | 80 | ||||
| -rw-r--r-- | src/world/component.rs | 23 | ||||
| -rw-r--r-- | src/world/icon.rs | 23 | ||||
| -rw-r--r-- | src/world/mod.rs | 113 | ||||
| -rw-r--r-- | src/world/room.rs | 52 | ||||
| -rw-r--r-- | src/world/wall.rs | 52 |
55 files changed, 1636 insertions, 685 deletions
diff --git a/src/cli/cmd/edit.rs b/src/client/cli/cmd/edit.rs index b164332..7a02959 100644 --- a/src/cli/cmd/edit.rs +++ b/src/client/cli/cmd/edit.rs @@ -2,8 +2,8 @@ use super::Command; use super::{CmdParseError, FromArgs}; -use crate::map::MapData; -use crate::Editor; +use crate::client::map::MapData; +use crate::client::Editor; use std::path::PathBuf; /// Command to load a file from the disk and replace the current editor contents with it's info. diff --git a/src/cli/cmd/mod.rs b/src/client/cli/cmd/mod.rs index e00b895..b403772 100644 --- a/src/cli/cmd/mod.rs +++ b/src/client/cli/cmd/mod.rs @@ -8,7 +8,7 @@ pub use edit::*; pub use read::*; pub use write::*; -use crate::Editor; +use crate::client::Editor; use std::ops::RangeInclusive; /// Errors that can occur when parsing a command. This is for syntax checking, the diff --git a/src/cli/cmd/read.rs b/src/client/cli/cmd/read.rs index 45cdf99..313530a 100644 --- a/src/cli/cmd/read.rs +++ b/src/client/cli/cmd/read.rs @@ -2,8 +2,8 @@ use super::Command; use super::{CmdParseError, FromArgs}; +use crate::client::Editor; use crate::map::MapData; -use crate::Editor; use std::path::PathBuf; /// Command to read a file from the system diff --git a/src/cli/cmd/write.rs b/src/client/cli/cmd/write.rs index 399045c..3114f63 100644 --- a/src/cli/cmd/write.rs +++ b/src/client/cli/cmd/write.rs @@ -2,8 +2,7 @@ use super::Command; use super::{CmdParseError, FromArgs}; -use crate::map::MapData; -use crate::Editor; +use crate::client::Editor; use std::path::PathBuf; /// The save command can take any destination in the filesystem the user can write to. Processing diff --git a/src/cli/mod.rs b/src/client/cli/mod.rs index 370a30b..ed2a1bf 100644 --- a/src/cli/mod.rs +++ b/src/client/cli/mod.rs @@ -11,10 +11,10 @@ pub mod cmd; pub use self::cmd::*; -use crate::colours::DEFAULT_COLOURS; -use crate::input::{Button, Input}; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::input::{Button, Input}; +use crate::client::Editor; use crate::math::Vec2; -use crate::Editor; use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; use std::sync::mpsc::Receiver; diff --git a/src/colours.rs b/src/client/colours.rs index d7c728c..d7c728c 100644 --- a/src/colours.rs +++ b/src/client/colours.rs diff --git a/src/config.rs b/src/client/config.rs index 6d0680c..96ff3f5 100644 --- a/src/config.rs +++ b/src/client/config.rs @@ -1,6 +1,6 @@ //! Home of the user configuratable content of graf karto, like keybindings and (TODO) colours etc. -use crate::input::{Binding, Button, Input, MouseButton, Scancode}; +use crate::client::input::{Binding, Button, Input, MouseButton, Scancode}; use ron::de::from_reader; use ron::ser::{to_string_pretty, PrettyConfig}; use serde::{Deserialize, Serialize}; diff --git a/src/editor.rs b/src/client/editor.rs index 7bf8f5e..0fb5794 100644 --- a/src/editor.rs +++ b/src/client/editor.rs @@ -5,12 +5,13 @@ //! currently a difference between things that are being created (inside the editor) and things that //! are part of the environment (the map). -use crate::config::Config; -use crate::input::{Binding, Input}; -use crate::map::Map; -use crate::snapping::Snapper; -use crate::tool::*; -use crate::transform::Transform; +use crate::client::config::Config; +use crate::client::input::{Binding, Input}; +use crate::client::map::Map; +use crate::client::snapping::Snapper; +use crate::client::tool::*; +use crate::client::transform::Transform; +use crate::net::{Cargo, Connection}; use raylib::core::drawing::RaylibDrawHandle; use raylib::{RaylibHandle, RaylibThread}; use std::collections::HashMap; @@ -23,12 +24,18 @@ pub struct Editor { tools: HashMap<ToolType, (Box<dyn Tool>, Binding)>, active: ToolType, config: Config, + server: Connection<Cargo>, } impl Editor { /// Create a new editor with all tools necessary. There should be only one editor per program /// instance. - pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread, config: Config) -> Self { + pub fn new( + rl: &mut RaylibHandle, + rlt: &RaylibThread, + config: Config, + server: Connection<Cargo>, + ) -> Self { let map = Map::new(rl, rlt); let mut tools: HashMap<ToolType, (Box<dyn Tool>, Binding)> = @@ -87,6 +94,7 @@ impl Editor { tools, active: ToolType::RectRoomTool, config, + server, } } @@ -142,17 +150,17 @@ impl Editor { // Handle common keybindings many of the tools have. if input.poll_global(&self.config.tool_general_binds.place_single) { - active_tool.place_single(&mut self.map, &snapped_mouse_pos); + active_tool.place_single(&mut self.map, &self.server, &snapped_mouse_pos); } if input.poll_global(&self.config.tool_general_binds.finish) { - active_tool.finish(&mut self.map); + active_tool.finish(&mut self.map, &self.server); } if input.poll_global(&self.config.tool_general_binds.abort) { active_tool.abort(); } // Handle custom keybindings in case the tool has any. - active_tool.handle_custom_bindings(&mut self.map, input); + active_tool.handle_custom_bindings(&mut self.map, &self.server, input); } /// Draw all tools and in case of the active tool also what is currently being edited by it, if @@ -167,8 +175,15 @@ impl Editor { pub fn map(&self) -> &Map { &self.map } - /// Get the world containing all finished elements mutably. - pub fn map_mut(&mut self) -> &mut Map { - &mut self.map + + /// Get the server this editor is connected to. Even if the program is executed locally, this will + /// return a server, since one must have been started locally. + pub fn server(&self) -> &Connection<Cargo> { + &self.server + } + + /// Get the server this editor is connected to mutably. + pub fn server_mut(&mut self) -> &mut Connection<Cargo> { + &mut self.server } } diff --git a/src/grid.rs b/src/client/grid.rs index 9134a49..17d537d 100644 --- a/src/grid.rs +++ b/src/client/grid.rs @@ -1,8 +1,8 @@ //! The grid used to divide the map into evenly sized chunks. -use crate::colours::DEFAULT_COLOURS; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; use crate::math; -use crate::transform::Transform; use raylib::drawing::RaylibDraw; /// Draw an infinite grid that can be moved around on the screen and zoomed in and out of. diff --git a/src/gui/decimal_num_box.rs b/src/client/gui/decimal_num_box.rs index e9395f7..e9395f7 100644 --- a/src/gui/decimal_num_box.rs +++ b/src/client/gui/decimal_num_box.rs diff --git a/src/gui/dimension_indicator.rs b/src/client/gui/dimension_indicator.rs index 57f5bcc..fb6394a 100644 --- a/src/gui/dimension_indicator.rs +++ b/src/client/gui/dimension_indicator.rs @@ -1,10 +1,12 @@ //! An interface element that shows the size of the selected map items and provides a means to //! manually change the size of them in a precise manner should need be. -use crate::colours::DEFAULT_COLOURS; -use crate::map::Map; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Editor; use crate::math::{self, Rect, Vec2}; -use crate::transform::Transform; +use crate::net::Cargo; use nalgebra::{Matrix3, Vector2}; use raylib::drawing::RaylibDraw; use raylib::ffi::KeyboardKey; @@ -59,10 +61,10 @@ impl DimensionIndicator { } /// Update whatever is selected on the map according to the dimension indicator rules and rulers. - pub fn update(&mut self, map: &mut Map, rl: &mut RaylibHandle) { + pub fn update(&mut self, editor: &Editor, rl: &mut RaylibHandle) { match self.state { - State::Watching => self.update_watching(map, rl), - State::Ruling { .. } => self.update_ruling(map, rl), + State::Watching => self.update_watching(editor.map(), rl), + State::Ruling { .. } => self.update_ruling(editor, rl), }; } @@ -74,9 +76,9 @@ impl DimensionIndicator { * default, otherwise it is adjusted to the size of the combined selection. */ let mut selection_exists = false; - for e in map.elements() { + for (_id, e) in map.elements() { if e.selected() { - let element_bounds = e.bounding_rect(); + let element_bounds = e.as_component().bounding_rect(); if selection_exists { // Adjust the currently detected selection size. min.x = math::partial_min(min.x, element_bounds.x); @@ -112,7 +114,7 @@ impl DimensionIndicator { } } - fn update_ruling(&mut self, map: &mut Map, rl: &mut RaylibHandle) { + fn update_ruling(&mut self, editor: &Editor, rl: &mut RaylibHandle) { // Get the currently edited dimension for processing. let (edited_dim, editing_x) = match &mut self.state { State::Watching => panic!("Called ruler update when in watching state"), @@ -183,7 +185,7 @@ impl DimensionIndicator { Rect::new(self.bounds.x, self.bounds.y, self.bounds.h, dim) }; - self.set_bounds(map, new_bounds); + self.set_bounds(editor, new_bounds); } } } @@ -194,7 +196,7 @@ impl DimensionIndicator { /// # Panics /// If the `bounds` have a negative value for width or height, the dimensions /// cannot be set and the function will panic. - pub fn set_bounds(&mut self, map: &mut Map, bounds: Rect<f64>) { + pub fn set_bounds(&mut self, editor: &Editor, bounds: Rect<f64>) { if bounds.w <= 0. || bounds.h <= 0. { panic!("Cannot set dimensions of elements to zero."); } @@ -216,10 +218,12 @@ impl DimensionIndicator { .append_nonuniform_scaling(&scale) .append_translation(&Vector2::new(bounds.x, bounds.y)); - for element in map.elements_mut() { + for (id, element) in editor.map().elements() { if element.selected() { - if let Some(transformable) = element.as_non_rigid_mut() { - transformable.apply_matrix(&transform); + if element.as_component().as_non_rigid().is_some() { + editor + .server() + .send(Cargo::ApplyMatrix((id, transform.clone()))); } } } diff --git a/src/gui/mod.rs b/src/client/gui/mod.rs index 62173ec..62173ec 100644 --- a/src/gui/mod.rs +++ b/src/client/gui/mod.rs diff --git a/src/gui/position_indicator.rs b/src/client/gui/position_indicator.rs index 737a427..4d68b86 100644 --- a/src/gui/position_indicator.rs +++ b/src/client/gui/position_indicator.rs @@ -6,10 +6,10 @@ //! is a simple HUD so it doesn't interact with anything in the world, but that may change in the //! future. -use crate::colours::DEFAULT_COLOURS; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::snapping::Snapper; +use crate::client::transform::Transform; use crate::math::Vec2; -use crate::snapping::Snapper; -use crate::transform::Transform; use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; /// Function to draw a dot at the mouse position and the coordinates associated with it. diff --git a/src/gui/tool_sidebar.rs b/src/client/gui/tool_sidebar.rs index af6af74..3147bf8 100644 --- a/src/gui/tool_sidebar.rs +++ b/src/client/gui/tool_sidebar.rs @@ -3,10 +3,10 @@ // TODO: Currently, the keyboard shortcuts for tools are handled by the editor, but a lot speaks for // them being handled by the ToolSidebar instead. -use crate::input::Input; +use crate::client::input::Input; +use crate::client::tool::ToolType; +use crate::client::Editor; use crate::math::Rect; -use crate::tool::ToolType; -use crate::Editor; use raylib::core::texture::Texture2D; use raylib::rgui::RaylibDrawGui; use raylib::{RaylibHandle, RaylibThread}; diff --git a/src/input/binding.rs b/src/client/input/binding.rs index 386fb66..386fb66 100644 --- a/src/input/binding.rs +++ b/src/client/input/binding.rs diff --git a/src/input/button.rs b/src/client/input/button.rs index e9ef45e..e9ef45e 100644 --- a/src/input/button.rs +++ b/src/client/input/button.rs diff --git a/src/input/mod.rs b/src/client/input/mod.rs index e8b1821..e8b1821 100644 --- a/src/input/mod.rs +++ b/src/client/input/mod.rs diff --git a/src/client/map/icon_mark.rs b/src/client/map/icon_mark.rs new file mode 100644 index 0000000..39fd554 --- /dev/null +++ b/src/client/map/icon_mark.rs @@ -0,0 +1,95 @@ +//! Icon marker on the map. For information about icons see [Icon](crate::) + +use super::icon_texture_manager::IconTextureManager; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Mappable; +use crate::client::transform::Transform; +use crate::math::Vec2; +use crate::world::{Component, Icon}; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; + +/// Describes an icon in the world and can be drawn. +#[derive(Clone)] +pub struct IconMark { + icon: Icon, + selected: bool, + textures: Rc<IconTextureManager>, +} + +impl IconMark { + /// Create a new icon marker. Requires the icon textures that are used to render this icon, as well as all + /// the information necessary to describe the icon itself. + pub fn new( + id: usize, + position: Vec2<f64>, + rotation: f64, + renderer: Rc<IconTextureManager>, + ) -> Self { + Self::from_icon( + Icon { + id, + position, + rotation, + }, + renderer, + ) + } + + /// Like `new()`, but with the icon data bundled into the icon type. + pub fn from_icon(icon: Icon, textures: Rc<IconTextureManager>) -> Self { + Self { + icon, + selected: false, + textures, + } + } +} + +impl Mappable for IconMark { + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + let (texture, info) = self.textures.get(self.id); + // Round the position to whole pixels to fix rotation problems. + let mut position_px = + transform.point_m_to_px(&(self.position - (info.anchor / info.pixels_per_m))); + position_px.x = position_px.x.floor(); + position_px.y = position_px.y.floor(); + rld.draw_texture_ex( + texture, + position_px, + self.rotation as f32, + (transform.pixels_per_m() / info.pixels_per_m) as f32, + if self.selected() { + DEFAULT_COLOURS.icon_selected + } else { + DEFAULT_COLOURS.icon_normal + }, + ); + } + + fn set_selected(&mut self, selected: bool) { + self.selected = selected; + } + + fn selected(&self) -> bool { + self.selected + } + + fn as_component(&self) -> &dyn Component { + self.deref() + } +} + +impl Deref for IconMark { + type Target = Icon; + + fn deref(&self) -> &Self::Target { + &self.icon + } +} +impl DerefMut for IconMark { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.icon + } +} diff --git a/src/map/icon_renderer.rs b/src/client/map/icon_texture_manager.rs index eef053d..c6b7fea 100644 --- a/src/map/icon_renderer.rs +++ b/src/client/map/icon_texture_manager.rs @@ -21,14 +21,14 @@ pub(super) struct IconFileInfo { } /// Manager for all icon texture or rendering data. -pub struct IconRenderer { +pub struct IconTextureManager { texture_data: Vec<(Texture2D, IconFileInfo)>, } -impl IconRenderer { - /// Create a new icon renderer. This loads all textures and information for icons that is needed - /// to draw them to the screen. Usually, there should be only one IconRenderer, or at least one - /// renderer per directory and program instance. +impl IconTextureManager { + /// Create a new icon manager. This loads all textures and information for icons that is needed + /// to draw them to the screen. Usually, there should be only one IconTextureManager, or at least one + /// manager per directory and program instance. pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { /* Read all available icons from the icon directory. SVGs do not need any special scale * file, but pixel-based file formats require a RON-file declaring what the scale of the @@ -75,7 +75,7 @@ impl IconRenderer { Self { texture_data } } - /// Get the data needed to render an icon of type `icon_id`. + /// Get the textures needed to render an icon of type `icon_id`. /// /// # Panics /// If the `icon_id` does not describe a valid icon (is out of bounds), there is no data to be @@ -84,7 +84,7 @@ impl IconRenderer { &self.texture_data[icon_id] } - /// The number of icons registered in this icon-renderer. + /// The number of icons registered in this texture manager. pub fn num_icons(&self) -> usize { self.texture_data.len() } diff --git a/src/map/mappable.rs b/src/client/map/mappable.rs index 7978f50..39e774b 100644 --- a/src/map/mappable.rs +++ b/src/client/map/mappable.rs @@ -1,9 +1,8 @@ //! Something that's mappable can be placed on the map and drawn at a specific position. It has a //! dimension on the map and may be transformable in various ways. -use crate::math::Rect; -use crate::transform::Transform; -use crate::transformable::NonRigidTransformable; +use crate::client::transform::Transform; +use crate::world::Component; use raylib::drawing::RaylibDrawHandle; /// Anything that can be added to the map or world must implement this trait. @@ -19,17 +18,6 @@ pub trait Mappable { /// Get if this item is currently selected. fn selected(&self) -> bool; - /// Get the rectangle that contains the mappable object in its entirety without excess. - fn bounding_rect(&self) -> Rect<f64>; - - /// If this mappable item can be transformed in a non-rigid way, a dynamic reference is returned, - /// otherwise none. - fn as_non_rigid(&self) -> Option<&dyn NonRigidTransformable> { - None - } - - /// The same as `as_non_rigid`, but mutably. - fn as_non_rigid_mut(&mut self) -> Option<&mut dyn NonRigidTransformable> { - None - } + /// Get this mappable as a world component. + fn as_component(&self) -> &dyn Component; } diff --git a/src/client/map/mod.rs b/src/client/map/mod.rs new file mode 100644 index 0000000..eaab72f --- /dev/null +++ b/src/client/map/mod.rs @@ -0,0 +1,206 @@ +//! The map is a visual interpretation of all the items that make up the world. +//! +//! It's the main structure that the client uses to interact with the world, since +//! the world contains all the + +pub mod icon_mark; +pub mod icon_texture_manager; +pub mod mappable; +pub mod room_mark; +pub mod wall_mark; + +pub use icon_mark::*; +pub use mappable::Mappable; +pub use room_mark::*; +pub use wall_mark::*; + +use crate::client::Transform; +use crate::stable_vec::StableVec; +use crate::world::{Room, Wall, World}; +use icon_texture_manager::IconTextureManager; +use raylib::drawing::RaylibDrawHandle; +use raylib::{RaylibHandle, RaylibThread}; +use std::rc::Rc; + +/// The map containing all map elements that are seen on the screen. +pub struct Map { + rooms: StableVec<RoomMark>, + walls: StableVec<WallMark>, + icons: StableVec<IconMark>, + used_ids: StableVec<()>, + icon_renderer: Rc<IconTextureManager>, +} + +impl Map { + /// Create a new, empty map/world. + pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { + Self { + rooms: StableVec::new(), + walls: StableVec::new(), + icons: StableVec::new(), + used_ids: StableVec::new(), + icon_renderer: Rc::new(IconTextureManager::new(rl, rlt)), + } + } + + pub fn add_room(&mut self, id: usize, room: Room) -> bool { + if self.used_ids.try_insert(id, ()).is_ok() { + self.rooms + .try_insert(id, RoomMark::from_room(room)) + .unwrap(); + true + } else { + error!("Unable to add room. Id already in use."); + false + } + } + + /// Add a wall with a specific id. May fail if there already is an entity with that id. + pub fn add_wall(&mut self, id: usize, wall: Wall) -> bool { + if self.used_ids.try_insert(id, ()).is_ok() { + /* Check for intersections with any wall that was arleady created so the wall ends can be + * rendered properly. + */ + let mut start_intersects = false; + let mut end_intersects = false; + for (_, wall) in self.walls.iter() { + if wall.shape().contains_collinear(wall.shape().start) { + start_intersects = true; + } + if wall.shape().contains_collinear(wall.shape().end) { + end_intersects = true; + } + + // Currently, additional intersections can be ignored, since it is just a yes-no-question + if start_intersects && end_intersects { + break; + } + } + + self.walls + .try_insert( + id, + WallMark::from_wall(wall, start_intersects, end_intersects), + ) + .unwrap(); + true + } else { + error!("Unable to add wall. Id already in use."); + false + } + } + + /// Add an icon with a specific id. May fail if there already is an entity with that id. + pub fn add_icon(&mut self, id: usize, icon: IconMark) -> bool { + if self.used_ids.try_insert(id, ()).is_ok() { + self.icons.try_insert(id, icon).unwrap(); + true + } else { + error!("Unable to add icon. Id already in use."); + false + } + } + + /// Draw all elements of the map to the screen. This should be called after the background of the + /// map has already been drawn. + pub fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + for (_, element) in self.elements() { + element.draw(rld, transform); + } + } + + /// Get the icon-renderer that is currently used to render the icons. + pub fn icon_renderer(&self) -> Rc<IconTextureManager> { + self.icon_renderer.clone() + } + + /// Remove item with the id, if it exists. Returns `true` if an item existed, `false` otherwise. + pub fn remove(&mut self, id: usize) -> bool { + if self.used_ids.remove(id).is_none() { + return false; + } + + if self.rooms.remove(id).is_some() + || self.walls.remove(id).is_some() + || self.icons.remove(id).is_some() + { + true + } else { + panic!( + "Id {} was still registered, eventhough there was no such entity.", + id + ); + } + } + + /// Iterator over all elements as objects when an operation needs to go over all elements of the + /// map. + pub fn elements(&self) -> impl Iterator<Item = (usize, &dyn Mappable)> { + self.rooms + .iter() + .map(|(id, p)| (*id, p as &dyn Mappable)) + .chain(self.walls.iter().map(|(id, w)| (*id, w as &dyn Mappable))) + .chain(self.icons.iter().map(|(id, i)| (*id, i as &dyn Mappable))) + } + + /// Iterator over all elements, but the individual elements can be mutated. It is however + /// impossible to add or remove elements in this way. For that, use the dedicated functions. + pub fn elements_mut(&mut self) -> impl Iterator<Item = (usize, &mut dyn Mappable)> { + self.rooms + .id_iter_mut() + .map(|(id, p)| (id, p as &mut dyn Mappable)) + .chain( + self.walls + .id_iter_mut() + .map(|(id, w)| (id, w as &mut dyn Mappable)), + ) + .chain( + self.icons + .id_iter_mut() + .map(|(id, i)| (id, i as &mut dyn Mappable)), + ) + } + + /// Get the rooms of this map. + pub fn rooms(&self) -> &StableVec<RoomMark> { + &self.rooms + } + + /// Get the walls of this map. + pub fn walls(&self) -> &StableVec<WallMark> { + &self.walls + } + + /// Get the icons of this map. + pub fn icons(&self) -> &StableVec<IconMark> { + &self.icons + } + + /// Replace the internal map data with the data of the world provided. + /// (Load and replace) + pub fn set_data(&mut self, world: World) { + // Remove all data. + self.icons.clear(); + self.rooms.clear(); + self.walls.clear(); + + // Add all data from the map data. + self.add_data(world); + } + + /// Add the data provided to the current data on the map. All elements will + /// remain, with the additional elements being pushed also. This must be + /// used with caution, since the ids of the items will remain unchanged, and + /// items with the same id will therefore be ignored and not added. + pub fn add_data(&mut self, world: World) { + for (id, i) in world.icons().iter() { + self.add_icon(*id, IconMark::from_icon(*i, self.icon_renderer.clone())); + } + for (id, r) in world.rooms().iter() { + self.add_room(*id, r.clone()); + } + for (id, w) in world.walls().iter() { + self.add_wall(*id, w.clone()); + } + } +} diff --git a/src/map/room.rs b/src/client/map/room_mark.rs index 3050763..a9777fb 100644 --- a/src/map/room.rs +++ b/src/client/map/room_mark.rs @@ -2,32 +2,29 @@ //! [Polygon](crate::math::Polygon) can have. use super::Mappable; -use crate::colours::DEFAULT_COLOURS; -use crate::math::{self, Polygon, Rect, Triangle}; -use crate::transform::Transform; -use crate::transformable::NonRigidTransformable; -use crate::FLOAT_MARGIN; -use nalgebra::{Matrix3, Point2}; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; +use crate::client::FLOAT_MARGIN; +use crate::math::{self, Triangle}; +use crate::world::{Component, Room}; use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; use std::ops::Deref; -/// Data type for the Polygon room. -pub type RoomData = Polygon<f64>; - /// A polygon room, which can be placed and modified in the world. -pub struct Room { - data: RoomData, +pub struct RoomMark { + room: Room, // The polygon shape, but in triangles, so the polygon does not have to be triangulated every frame. triangulated: Vec<Triangle<f64>>, selected: bool, } -impl Room { +impl RoomMark { /// Create a room from the given polygon data. - pub fn from_data(data: RoomData) -> Self { + pub fn from_room(room: Room) -> Self { + let shape = room.shape().clone(); Self { - data: data.clone(), - triangulated: math::triangulate(data, FLOAT_MARGIN), + room, + triangulated: math::triangulate(shape, FLOAT_MARGIN), selected: false, } } @@ -36,11 +33,11 @@ impl Room { * properly, so this function must be called any time that happens. */ fn retriangulate(&mut self) { - self.triangulated = math::triangulate(self.data.clone(), FLOAT_MARGIN); + self.triangulated = math::triangulate(self.room.shape().clone(), FLOAT_MARGIN); } } -impl Mappable for Room { +impl Mappable for RoomMark { fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { for triangle in &self.triangulated { rld.draw_triangle( @@ -64,35 +61,15 @@ impl Mappable for Room { self.selected } - fn bounding_rect(&self) -> Rect<f64> { - Rect::bounding_rect_n(&self.data.corners()) - } - - fn as_non_rigid(&self) -> Option<&dyn NonRigidTransformable> { - Some(self as &dyn NonRigidTransformable) - } - - fn as_non_rigid_mut(&mut self) -> Option<&mut dyn NonRigidTransformable> { - Some(self as &mut dyn NonRigidTransformable) - } -} - -impl NonRigidTransformable for Room { - fn apply_matrix(&mut self, matrix: &Matrix3<f64>) { - for corner in self.data.corners_mut() { - *corner = matrix - .transform_point(&Point2::new(corner.x, corner.y)) - .into(); - } - - self.retriangulate(); + fn as_component(&self) -> &dyn Component { + self.deref() } } -impl Deref for Room { - type Target = RoomData; +impl Deref for RoomMark { + type Target = Room; fn deref(&self) -> &Self::Target { - &self.data + &self.room } } diff --git a/src/map/wall.rs b/src/client/map/wall_mark.rs index f1748bb..c51af9d 100644 --- a/src/map/wall.rs +++ b/src/client/map/wall_mark.rs @@ -1,4 +1,4 @@ -//! Walls, solid barriers that are generally unscaleable. +//! Walls, solid barriers that are generally unclimbable. //! //! This interpretation is generally up to the GM to decide, but generally speaking, a wall cannot be //! crossed by a player. If special conditions apply (for instance, when the player wants to scale the @@ -6,30 +6,26 @@ //! a wall. use super::Mappable; -use crate::colours::DEFAULT_COLOURS; -use crate::math::{LineSegment, Rect, Vec2}; -use crate::transform::Transform; -use crate::transformable::NonRigidTransformable; -use nalgebra::Matrix3; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; +use crate::math::Vec2; +use crate::world::{Component, Wall}; use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; use std::ops::{Deref, DerefMut}; -/// The data which defines a wall segment completely for serialisation. -pub type WallData = LineSegment<f64>; - /// A solid wall a player cannot go through, except if special conditions apply. -pub struct Wall { - data: WallData, +pub struct WallMark { + wall: Wall, selected: bool, round_start: bool, round_end: bool, } -impl Wall { +impl WallMark { /// Create a new wall from the deserialised data and information known from internal sources. - pub fn from_data(data: WallData, round_start: bool, round_end: bool) -> Self { + pub fn from_wall(wall: Wall, round_start: bool, round_end: bool) -> Self { Self { - data, + wall, selected: false, round_start, round_end, @@ -37,8 +33,8 @@ impl Wall { } /// Get the internal data for serialisation - pub fn data(&self) -> &WallData { - &self.data + pub fn data(&self) -> &Wall { + &self.wall } } @@ -59,10 +55,10 @@ fn draw_round_corner( ); } -impl Mappable for Wall { +impl Mappable for WallMark { fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { - let start_px = transform.point_m_to_px(&self.data.start); - let end_px = transform.point_m_to_px(&self.data.end); + let start_px = transform.point_m_to_px(&self.wall.shape().start); + let end_px = transform.point_m_to_px(&self.wall.shape().end); rld.draw_line_ex( start_px, end_px, @@ -90,36 +86,21 @@ impl Mappable for Wall { self.selected } - fn bounding_rect(&self) -> Rect<f64> { - Rect::bounding_rect(self.data.start, self.data.end) - } - - fn as_non_rigid(&self) -> Option<&dyn NonRigidTransformable> { - Some(self as &dyn NonRigidTransformable) - } - - fn as_non_rigid_mut(&mut self) -> Option<&mut dyn NonRigidTransformable> { - Some(self as &mut dyn NonRigidTransformable) - } -} - -impl NonRigidTransformable for Wall { - fn apply_matrix(&mut self, matrix: &Matrix3<f64>) { - self.data.start = matrix.transform_point(&self.data.start.into()).into(); - self.data.end = matrix.transform_point(&self.data.end.into()).into(); + fn as_component(&self) -> &dyn Component { + self.deref() } } -impl Deref for Wall { - type Target = WallData; +impl Deref for WallMark { + type Target = Wall; fn deref(&self) -> &Self::Target { - &self.data + &self.wall } } -impl DerefMut for Wall { +impl DerefMut for WallMark { fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.data + &mut self.wall } } diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..9151d82 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,137 @@ +pub mod cli; +pub mod colours; +pub mod config; +pub mod editor; +pub mod grid; +pub mod gui; +pub mod input; +pub mod map; +pub mod snapping; +pub mod svg; +pub mod tool; +pub mod transform; + +use crate::net::Connection; +use cli::CLI; +use config::Config; +use editor::Editor; +use float_cmp::F64Margin; +use gui::{DimensionIndicator, ToolSidebar}; +use input::Input; +use raylib::prelude::*; +use snapping::Snapper; +use std::ffi::CString; +use std::io; +use std::net::{SocketAddr, TcpStream}; +use transform::Transform; + +/// Location of the file containing the style used for the raylib user interface. +pub const GUI_STYLE: &str = "assets/style/cyber.rgs"; +/// Location of the graf karto configuration options file. +pub const CONFIG_FILE: &str = "config.ron"; + +/// The acceptable error that is used throughout the project for two floats to be considered equal. +/// If it is set too low, the editor might not work properly, if set too high, the granularity may +/// become too low for certain purposes. +pub const FLOAT_MARGIN: F64Margin = F64Margin { + epsilon: 10000. * f64::EPSILON, + ulps: 0, +}; + +pub fn run(server_address: SocketAddr) { + let (mut rl, thread) = raylib::init().resizable().title("Hello there!").build(); + rl.set_target_fps(120); + rl.set_exit_key(None); + + // Load the configuration file, if available. + let config = match Config::from_file(CONFIG_FILE) { + Ok(config) => config, + Err(err) => { + /* Create a default config file if it doesn't exist, otherwise leave the incorrectly + * formatted/corrupted or otherwise unreadable file alone. + */ + let config = Config::default(); + if err.kind() == io::ErrorKind::NotFound { + warn!("Could not find a configuration file. Creating default."); + config + .write_file(CONFIG_FILE) + .expect("Could not write config file."); + } else { + error!( + "Could not read configuration file: {}\nUsing defaults for this run.", + err + ); + } + + config + } + }; + + // Load the preferred gui style + rl.gui_load_style(Some( + &CString::new(GUI_STYLE).expect("Could not create C string from style file name"), + )); + + // Connect to the server at the given address. + let server = TcpStream::connect(server_address).expect("Unable to connect to the server."); + info!( + "Connection to server on `{:?}` established", + server.peer_addr() + ); + let server = Connection::new(server); + + let mut input = Input::new(&rl); + config::register_bindings(&config, &mut input); + let mut editor = Editor::new(&mut rl, &thread, config, server); + let mut dimension_indicator = DimensionIndicator::new(); + let mut tool_sidebar = ToolSidebar::new(&mut rl, &thread, &mut input); + let mut snapper = Snapper::default(); + let mut cli = CLI::new(&mut input); + + let mut transform = Transform::new(); + let mut last_mouse_pos = rl.get_mouse_position(); + while !rl.window_should_close() { + let screen_width = rl.get_screen_width(); + let screen_height = rl.get_screen_height(); + + input.update(&mut rl); + + // Move the canvas together with the mouse + if rl.is_mouse_button_down(MouseButton::MOUSE_MIDDLE_BUTTON) { + transform.move_by_px(&(rl.get_mouse_position() - last_mouse_pos).into()); + } + // Update the last mouse position + last_mouse_pos = rl.get_mouse_position(); + + let mouse_wheel_move = rl.get_mouse_wheel_move(); + if mouse_wheel_move != 0. { + // Zoom in for positive and zoom out for negative mouse wheel rotations. + let scale_factor = if mouse_wheel_move > 0. { 1.2 } else { 1. / 1.2 }; + transform.try_zoom( + &rl.get_mouse_position().into(), + mouse_wheel_move.abs() as f64 * scale_factor, + ); + } + + cli.update(&mut editor, &mut input); + dimension_indicator.update(&mut editor, &mut rl); + snapper.update(&mut rl, cli.active()); + editor.update(&mut rl, &transform, &snapper, &mut input); + tool_sidebar.update(screen_height as u16, &mut input); + + // Drawing section + { + let mut d = rl.begin_drawing(&thread); + d.clear_background(Color::BLACK); + grid::draw_grid(&mut d, screen_width, screen_height, &transform); + editor.map().draw(&mut d, &transform); + + editor.draw_tools(&mut d, &transform); + tool_sidebar.draw(&mut d, &mut editor); + snapper.draw(&mut d); + gui::position_indicator_draw(&mut d, last_mouse_pos.into(), &transform, &snapper); + dimension_indicator.draw(&mut d, &transform); + cli.draw(&mut d); + } + } +} diff --git a/src/snapping.rs b/src/client/snapping.rs index ceabf69..b8f9706 100644 --- a/src/snapping.rs +++ b/src/client/snapping.rs @@ -5,7 +5,7 @@ //! I thought it should be changeable. This module is responsible for snapping and managing the user //! instructions telling the program what granularity should currently be used, if any. -use crate::gui::DecimalNumBox; +use crate::client::gui::DecimalNumBox; use crate::math::{self, Vec2}; use raylib::drawing::RaylibDrawHandle; use raylib::ffi::KeyboardKey; diff --git a/src/svg/mod.rs b/src/client/svg/mod.rs index af066f1..af066f1 100644 --- a/src/svg/mod.rs +++ b/src/client/svg/mod.rs diff --git a/src/svg/style.rs b/src/client/svg/style.rs index 7a0110e..7a0110e 100644 --- a/src/svg/style.rs +++ b/src/client/svg/style.rs diff --git a/src/tool/deletion_tool.rs b/src/client/tool/deletion_tool.rs index da2090b..3095ff5 100644 --- a/src/tool/deletion_tool.rs +++ b/src/client/tool/deletion_tool.rs @@ -7,10 +7,12 @@ //! and nothing is deleted. use super::Tool; -use crate::colours::DEFAULT_COLOURS; -use crate::map::Map; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Connection; use crate::math::{ExactSurface, Rect, Vec2}; -use crate::transform::Transform; +use crate::net::Cargo; use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; /// The deletion tool itself. @@ -29,9 +31,14 @@ impl DeletionTool { } } -fn delete_rect((pos1, pos2): (&Vec2<f64>, &Vec2<f64>), map: &mut Map) { +fn delete_rect((pos1, pos2): (&Vec2<f64>, &Vec2<f64>), map: &Map, server: &Connection<Cargo>) { let bounds = Rect::bounding_rect(*pos1, *pos2); - map.retain(|e| !bounds.contains_rect(&e.bounding_rect())); + + for (id, e) in map.elements() { + if bounds.contains_rect(&e.as_component().bounding_rect()) { + server.send(Cargo::Remove(id)); + } + } } impl Tool for DeletionTool { @@ -53,18 +60,18 @@ impl Tool for DeletionTool { } } - fn place_single(&mut self, map: &mut Map, mouse_pos_m: &Vec2<f64>) { + fn place_single(&mut self, map: &mut Map, server: &Connection<Cargo>, mouse_pos_m: &Vec2<f64>) { if let Some((pos1, pos2)) = self.deletion_rect { - delete_rect((&pos1, &pos2), map); + delete_rect((&pos1, &pos2), &map, server); self.deletion_rect = None; } else { self.deletion_rect = Some((*mouse_pos_m, *mouse_pos_m)); } } - fn finish(&mut self, map: &mut Map) { + fn finish(&mut self, map: &mut Map, server: &Connection<Cargo>) { if let Some((pos1, pos2)) = self.deletion_rect { - delete_rect((&pos1, &pos2), map); + delete_rect((&pos1, &pos2), &map, server); self.deletion_rect = None; } } diff --git a/src/tool/icon_tool.rs b/src/client/tool/icon_tool.rs index 8b4afc0..caf9d60 100644 --- a/src/tool/icon_tool.rs +++ b/src/client/tool/icon_tool.rs @@ -1,14 +1,16 @@ //! Tool for creating icons. For explanation of icons, please see //! [the icon module](crate::map::icon). -use crate::config::IconToolBinds; -use crate::input::Input; -use crate::map::icon_renderer::IconRenderer; -use crate::map::{Icon, Map, Mappable}; +use crate::client::config::IconToolBinds; +use crate::client::input::Input; +use crate::client::map::{icon_texture_manager::IconTextureManager, IconMark, Map, Mappable}; +use crate::client::tool::Tool; +use crate::client::transform::Transform; use crate::math::Vec2; -use crate::tool::Tool; -use crate::transform::Transform; +use crate::net::Cargo; +use crate::net::Connection; use raylib::core::drawing::RaylibDrawHandle; +use std::ops::Deref; use std::rc::Rc; /// The icon tool itself. @@ -18,19 +20,19 @@ pub struct IconTool { active: bool, /// The information of the icon that should be placed / is currently being placed, if it /// exists. - current_icon: Icon, - renderer: Rc<IconRenderer>, + current_icon: IconMark, + textures: Rc<IconTextureManager>, } impl IconTool { /// Create a new icon tool that renders icons with the provided icon renderer. There should only /// be one instance of the tool for the program, which should be created in the editor. - pub fn new(keybindings: IconToolBinds, renderer: Rc<IconRenderer>) -> Self { + pub fn new(keybindings: IconToolBinds, textures: Rc<IconTextureManager>) -> Self { Self { keybindings, active: false, - current_icon: Icon::new(0, Vec2::default(), 0., renderer.clone()), - renderer, + current_icon: IconMark::new(0, Vec2::default(), 0., textures.clone()), + textures, } } } @@ -54,17 +56,27 @@ impl Tool for IconTool { } } - fn place_single(&mut self, map: &mut Map, _mouse_pos_m: &Vec2<f64>) { - map.push_icon(self.current_icon.clone()); + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + _mouse_pos_m: &Vec2<f64>, + ) { + server.send(Cargo::AddIcon(self.current_icon.deref().clone())); } - fn handle_custom_bindings(&mut self, _map: &mut Map, input: &mut Input) { + fn handle_custom_bindings( + &mut self, + _map: &mut Map, + _server: &Connection<Cargo>, + input: &mut Input, + ) { if input.poll_global(&self.keybindings.next) { - self.current_icon.id = (self.current_icon.id + 1) % self.renderer.num_icons(); + self.current_icon.id = (self.current_icon.id + 1) % self.textures.num_icons(); } if input.poll_global(&self.keybindings.previous) { self.current_icon.id = - (self.current_icon.id + self.renderer.num_icons() - 1) % self.renderer.num_icons(); + (self.current_icon.id + self.textures.num_icons() - 1) % self.textures.num_icons(); } if input.poll_global(&self.keybindings.rotate_clockwise) { self.current_icon.rotation += 45.; diff --git a/src/tool/mod.rs b/src/client/tool/mod.rs index b3fae80..08e1380 100644 --- a/src/tool/mod.rs +++ b/src/client/tool/mod.rs @@ -20,10 +20,12 @@ pub use rect_room_tool::RectRoomTool; pub use selection_tool::SelectionTool; pub use wall_tool::WallTool; -use crate::input::Input; -use crate::map::Map; +use crate::client::input::Input; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Connection; use crate::math::Vec2; -use crate::transform::Transform; +use crate::net::Cargo; use raylib::core::drawing::RaylibDrawHandle; #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] @@ -63,18 +65,22 @@ pub trait Tool { fn update(&mut self, map: &Map, mouse_pos_m: &Vec2<f64>); /// Draw the contents of this tool. - // TODO: Maybe make this tool mappable? This might make it easier to make things resizable while - // it's still being drawn. fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform); /// Generic keybinding. /// Code to place a single node for this tool. - fn place_single(&mut self, _map: &mut Map, _mouse_pos_m: &Vec2<f64>) {} + fn place_single( + &mut self, + _map: &mut Map, + _server: &Connection<Cargo>, + _mouse_pos_m: &Vec2<f64>, + ) { + } /// Generic keybinding. /// Code to finish whatever one is doing with this tool currently and trying to apply the /// changes to the map data. - fn finish(&mut self, _map: &mut Map) {} + fn finish(&mut self, _map: &mut Map, _server: &Connection<Cargo>) {} /// Generic keybinding. /// Stop whatever one is doing with this tool and do not apply any changes to the map data. @@ -82,5 +88,11 @@ pub trait Tool { /// If there are any additional keybindings that need to be handled by this tool, these can be /// handled here. - fn handle_custom_bindings(&mut self, _map: &mut Map, _input: &mut Input) {} + fn handle_custom_bindings( + &mut self, + _map: &mut Map, + _server: &Connection<Cargo>, + _input: &mut Input, + ) { + } } diff --git a/src/tool/polygon_room_tool.rs b/src/client/tool/polygon_room_tool.rs index d7c188f..63456cc 100644 --- a/src/tool/polygon_room_tool.rs +++ b/src/client/tool/polygon_room_tool.rs @@ -1,11 +1,13 @@ //! Tool to create rooms in the shape of generic polygons. use super::Tool; -use crate::colours::DEFAULT_COLOURS; -use crate::map::Map; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::FLOAT_MARGIN; use crate::math::{self, PolygonGraph, Vec2}; -use crate::transform::Transform; -use crate::FLOAT_MARGIN; +use crate::net::{Cargo, Connection}; +use crate::world::Room; use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; /// The tool itself. @@ -29,7 +31,7 @@ impl PolygonRoomTool { * to the map, clear the currently drawn polygon and return bool. Otherwise it will leave the * unfinished polygon as is and return false without pushing anything. */ - fn try_push(&mut self, map: &mut Map) -> bool { + fn try_push(&mut self, server: &Connection<Cargo>) -> bool { if self.unfinished_room.is_none() { return false; } @@ -43,7 +45,7 @@ impl PolygonRoomTool { .bounding_polygon(FLOAT_MARGIN) { Some(polygon) => { - map.push_room(polygon); + server.send(Cargo::AddRoom(Room::new(polygon))); self.unfinished_room = None; true } @@ -104,7 +106,12 @@ impl Tool for PolygonRoomTool { } } - fn place_single(&mut self, map: &mut Map, mouse_pos_m: &Vec2<f64>) { + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { if let Some((ref mut graph, ref mut last_placed)) = &mut self.unfinished_room { // If the corner already exists in the polygon, try to finish and push it after adding the // next edge. @@ -116,7 +123,7 @@ impl Tool for PolygonRoomTool { } if try_finish { - self.try_push(map); + self.try_push(server); } } else { // Start a new unfinished polygon @@ -124,8 +131,8 @@ impl Tool for PolygonRoomTool { } } - fn finish(&mut self, map: &mut Map) { - self.try_push(map); + fn finish(&mut self, _map: &mut Map, server: &Connection<Cargo>) { + self.try_push(server); } fn abort(&mut self) { diff --git a/src/tool/rect_room_tool.rs b/src/client/tool/rect_room_tool.rs index ec0f0ec..41f2a91 100644 --- a/src/tool/rect_room_tool.rs +++ b/src/client/tool/rect_room_tool.rs @@ -3,10 +3,12 @@ //! is necessary and the shape of the room does not have to be very special. use super::Tool; -use crate::colours::DEFAULT_COLOURS; -use crate::map::Map; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; use crate::math::{Rect, Vec2}; -use crate::transform::Transform; +use crate::net::{Cargo, Connection}; +use crate::world::Room; use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; /// The tool to create simple, rectangular rooms. @@ -47,7 +49,12 @@ impl Tool for RectRoomTool { } } - fn place_single(&mut self, map: &mut Map, mouse_pos_m: &Vec2<f64>) { + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { // Try to finish the rectangle if it has been started. if let Some((pos1, pos2)) = self.unfinished_rect { if pos1 == pos2 { @@ -55,21 +62,25 @@ impl Tool for RectRoomTool { return; } - map.push_room(Rect::bounding_rect(pos1, pos2).into()); + server.send(Cargo::AddRoom(Room::new( + Rect::bounding_rect(pos1, pos2).into(), + ))); self.unfinished_rect = None; } else { self.unfinished_rect = Some((*mouse_pos_m, *mouse_pos_m)); } } - fn finish(&mut self, map: &mut Map) { + fn finish(&mut self, _map: &mut Map, server: &Connection<Cargo>) { if let Some((pos1, pos2)) = self.unfinished_rect { if pos1 == pos2 { warn!("Cannot place rectangle with start and endpoint being the same"); return; } - map.push_room(Rect::bounding_rect(pos1, pos2).into()); + server.send(Cargo::AddRoom(Room::new( + Rect::bounding_rect(pos1, pos2).into(), + ))); self.unfinished_rect = None; } } diff --git a/src/tool/selection_tool.rs b/src/client/tool/selection_tool.rs index 4850a28..52c2155 100644 --- a/src/tool/selection_tool.rs +++ b/src/client/tool/selection_tool.rs @@ -7,10 +7,11 @@ //! For this reason, the selection tool can be thought of as a kind of meta tool over tools. use super::Tool; -use crate::colours::DEFAULT_COLOURS; -use crate::map::Map; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; use crate::math::{ExactSurface, Rect, Vec2}; -use crate::transform::Transform; +use crate::net::{Cargo, Connection}; use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; /// The selection tool makes it possible to select any item on the map when activated. @@ -48,13 +49,18 @@ impl Tool for SelectionTool { } } - fn place_single(&mut self, map: &mut Map, mouse_pos_m: &Vec2<f64>) { + fn place_single( + &mut self, + map: &mut Map, + _server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { if let Some((pos1, pos2)) = self.selection_rect { // Select all items on the map that are inside of the selection rectangle let bounds = Rect::bounding_rect(pos1, pos2); - for element in map.elements_mut() { + for (_id, element) in map.elements_mut() { // TODO: Make it possible to do this additively by custom keybinding. - element.set_selected(bounds.contains_rect(&element.bounding_rect())); + element.set_selected(bounds.contains_rect(&element.as_component().bounding_rect())); } self.selection_rect = None; } else { diff --git a/src/tool/wall_tool.rs b/src/client/tool/wall_tool.rs index e79d815..857beea 100644 --- a/src/tool/wall_tool.rs +++ b/src/client/tool/wall_tool.rs @@ -2,9 +2,11 @@ //! [the wall module](crate::map::wall). use super::Tool; -use crate::map::Map; +use crate::client::map::Map; +use crate::client::transform::Transform; use crate::math::{LineSegment, Vec2}; -use crate::transform::Transform; +use crate::net::{Cargo, Connection}; +use crate::world::Wall; use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; use raylib::ffi::{Color, Vector2}; @@ -53,11 +55,16 @@ impl Tool for WallTool { } } - fn place_single(&mut self, map: &mut Map, mouse_pos_m: &Vec2<f64>) { + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { if let Some(wall) = self.unfinished_wall.take() { // Continue with the next wall straight away. self.unfinished_wall = Some(LineSegment::new(wall.end, wall.end)); - map.push_wall(wall); + server.send(Cargo::AddWall(Wall::new(wall))); } else { self.unfinished_wall = Some(LineSegment::new(*mouse_pos_m, *mouse_pos_m)); } diff --git a/src/transform.rs b/src/client/transform.rs index 147956c..147956c 100644 --- a/src/transform.rs +++ b/src/client/transform.rs diff --git a/src/client_main.rs b/src/client_main.rs index 22251c3..e7d1a2d 100644 --- a/src/client_main.rs +++ b/src/client_main.rs @@ -21,135 +21,87 @@ #[macro_use] extern crate log; -pub mod cli; -pub mod colours; -pub mod config; -pub mod editor; -pub mod grid; -pub mod gui; -pub mod input; -pub mod map; +pub mod client; pub mod math; -pub mod snapping; +pub mod net; +pub mod server; pub mod stable_vec; -pub mod svg; -pub mod tool; -pub mod transform; pub mod transformable; +pub mod world; -use cli::CLI; -use config::Config; -use editor::Editor; -use float_cmp::F64Margin; -use gui::{DimensionIndicator, ToolSidebar}; -use input::Input; -use raylib::prelude::*; -use snapping::Snapper; -use std::ffi::CString; -use std::io; -use transform::Transform; - -/// Location of the file containing the style used for the raylib user interface. -pub const GUI_STYLE: &str = "assets/style/cyber.rgs"; -/// Location of the graf karto configuration options file. -pub const CONFIG_FILE: &str = "config.ron"; - -/// The acceptable error that is used throughout the project for two floats to be considered equal. -/// If it is set too low, the editor might not work properly, if set too high, the granularity may -/// become too low for certain purposes. -pub const FLOAT_MARGIN: F64Margin = F64Margin { - epsilon: 10000. * f64::EPSILON, - ulps: 0, -}; +use clap::{App, Arg}; +use server::DEFAULT_PORT; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; fn main() { pretty_env_logger::init(); - let (mut rl, thread) = raylib::init().resizable().title("Hello there!").build(); - rl.set_target_fps(120); - rl.set_exit_key(None); - - // Load the configuration file, if available. - let config = match Config::from_file(CONFIG_FILE) { - Ok(config) => config, - Err(err) => { - /* Create a default config file if it doesn't exist, otherwise leave the incorrectly - * formatted/corrupted or otherwise unreadable file alone. - */ - let config = Config::default(); - if err.kind() == io::ErrorKind::NotFound { - warn!("Could not find a configuration file. Creating default."); - config - .write_file(CONFIG_FILE) - .expect("Could not write config file."); - } else { - error!( - "Could not read configuration file: {}\nUsing defaults for this run.", - err - ); - } + println!("Graf Karto version: {}", clap::crate_version!()); + let default_port = DEFAULT_PORT.to_string(); + let matches = App::new("Graf Karto") + .version(clap::crate_version!()) + .author(clap::crate_authors!()) + .about(clap::crate_description!()) + .arg(Arg::with_name("connect") + .short("c") + .value_name("SERVER_ADDRESS") + .help("Specify an IP in case an external server should be used (starts a local server if not set).")) + .arg(Arg::with_name("port") + .short("p") + .value_name("SERVER_PORT") + .help("Set the port the server listens on or should listen on. When starting a local server, others may be tried if it cannot be bound.") + .default_value(&default_port)) + .arg(Arg::with_name("ipv4") + .short("v4") + .help("Use virgin IPv4 instead of chad IPv6.. you monster")) + .get_matches(); - config + let use_ipv4 = matches.is_present("ipv4"); + let mut server_port = match matches + .value_of("port") + .expect("No port found, eventhough it should have a default value") + .parse::<u16>() + { + Ok(port) => port, + Err(e) => { + error!("Not a valid server port: {:?}", e); + warn!("Using default port {}", DEFAULT_PORT); + DEFAULT_PORT } }; - // Load the preferred gui style - rl.gui_load_style(Some( - &CString::new(GUI_STYLE).expect("Could not create C string from style file name"), - )); - - let mut input = Input::new(&rl); - config::register_bindings(&config, &mut input); - let mut editor = Editor::new(&mut rl, &thread, config); - let mut dimension_indicator = DimensionIndicator::new(); - let mut tool_sidebar = ToolSidebar::new(&mut rl, &thread, &mut input); - let mut snapper = Snapper::default(); - let mut cli = CLI::new(&mut input); - - let mut transform = Transform::new(); - let mut last_mouse_pos = rl.get_mouse_position(); - while !rl.window_should_close() { - let screen_width = rl.get_screen_width(); - let screen_height = rl.get_screen_height(); - - input.update(&mut rl); - - // Move the canvas together with the mouse - if rl.is_mouse_button_down(MouseButton::MOUSE_MIDDLE_BUTTON) { - transform.move_by_px(&(rl.get_mouse_position() - last_mouse_pos).into()); + let server_address = match matches.value_of("connect") { + None => { + // Local server will be started. + if use_ipv4 { + IpAddr::V4(Ipv4Addr::LOCALHOST) + } else { + IpAddr::V6(Ipv6Addr::LOCALHOST) + } } - // Update the last mouse position - last_mouse_pos = rl.get_mouse_position(); - - let mouse_wheel_move = rl.get_mouse_wheel_move(); - if mouse_wheel_move != 0. { - // Zoom in for positive and zoom out for negative mouse wheel rotations. - let scale_factor = if mouse_wheel_move > 0. { 1.2 } else { 1. / 1.2 }; - transform.try_zoom( - &rl.get_mouse_position().into(), - mouse_wheel_move.abs() as f64 * scale_factor, - ); + Some(addr) => { + if use_ipv4 { + IpAddr::V4(dbg!(addr).parse().expect("Not a valid IPv4 address")) + } else { + IpAddr::V6(dbg!(addr).parse().expect("Not a valid IPv6 address")) + } } + }; - cli.update(&mut editor, &mut input); - dimension_indicator.update(editor.map_mut(), &mut rl); - snapper.update(&mut rl, cli.active()); - editor.update(&mut rl, &transform, &snapper, &mut input); - tool_sidebar.update(screen_height as u16, &mut input); + let server_handle = if !matches.is_present("connect") { + Some({ + let (server_handle, port) = + server::start_any_port(use_ipv4).expect("Unable to start local server"); + server_port = port; + server_handle + }) + } else { + None + }; - // Drawing section - { - let mut d = rl.begin_drawing(&thread); - d.clear_background(Color::BLACK); - grid::draw_grid(&mut d, screen_width, screen_height, &transform); - editor.map().draw(&mut d, &transform); + client::run(SocketAddr::new(server_address, server_port)); - editor.draw_tools(&mut d, &transform); - tool_sidebar.draw(&mut d, &mut editor); - snapper.draw(&mut d); - gui::position_indicator_draw(&mut d, last_mouse_pos.into(), &transform, &snapper); - dimension_indicator.draw(&mut d, &transform); - cli.draw(&mut d); - } + if let Some(handle) = server_handle { + handle.join().expect("Server thread closed unexpectedly."); } } diff --git a/src/map/data.rs b/src/map/data.rs deleted file mode 100644 index 20f193d..0000000 --- a/src/map/data.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Module containing the raw map data version of the map. - -use super::{IconData, Map, RoomData, WallData}; -use ron::de::from_reader; -use ron::ser::{to_string_pretty, PrettyConfig}; -use serde::{Deserialize, Serialize}; -use std::fs::File; -use std::io::{self, Write}; -use std::path::Path; - -/// The serialisable and deserialisable parts of the map. This can be created to get a version of the -/// map which is persistifiable or sendable/receivable without data overhead or data that might make -/// it easily corruptable. -#[derive(Serialize, Deserialize)] -pub struct MapData { - pub(super) rooms: Vec<RoomData>, - pub(super) walls: Vec<WallData>, - pub(super) icons: Vec<IconData>, -} - -impl MapData { - /// Create a serialisable map data type from the data elements contained in a map. - pub fn new(rooms: Vec<RoomData>, walls: Vec<WallData>, icons: Vec<IconData>) -> Self { - Self { - rooms, - walls, - icons, - } - } - - /// Creates a data struct from the Map. It is important to note, that this data element is not - /// bound to the Map in any way after this, so changing anything won't change anything in the map. - /// It is useful however to for instance serialize this map without extra rendering information - /// included. - pub fn extract_data(map: &Map) -> Self { - Self { - rooms: map - .rooms() - .iter() - .map(|p| (p as &RoomData).clone()) - .collect(), - walls: map - .walls() - .iter() - .map(|w| (w as &WallData).clone()) - .collect(), - icons: map - .icons() - .iter() - .map(|i| (i as &IconData).clone()) - .collect(), - } - } - - /// Load the map data from a file. Fails if the file does not exist or cannot be correctly parsed. - pub fn load_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> { - let file = File::open(&path)?; - let data: Self = match from_reader(file) { - Ok(data) => data, - Err(err) => { - return Err(io::Error::new(io::ErrorKind::InvalidData, err)); - } - }; - - Ok(data) - } - - /// Write the map data to the file located at `path`. If the file already exists, it will be - /// overwritten. If the write fails, an IO-Error is returned. - pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> { - let mut file = File::create(&path)?; - - let pretty_conf = PrettyConfig::new() - .with_depth_limit(4) - .with_decimal_floats(true) - .with_separate_tuple_members(true) - .with_indentor("\t".to_owned()); - let string = match to_string_pretty(&self, pretty_conf) { - Ok(string) => string, - Err(err) => { - return Err(io::Error::new(io::ErrorKind::InvalidInput, err)); - } - }; - - file.write_all(&string.as_bytes()) - } -} diff --git a/src/map/icon.rs b/src/map/icon.rs deleted file mode 100644 index 2e45486..0000000 --- a/src/map/icon.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Icons are map elements that have a specific size and cannot be stretched. They are usually used -//! as markers for specific places in the world. - -use super::icon_renderer::IconRenderer; -use crate::colours::DEFAULT_COLOURS; -use crate::map::Mappable; -use crate::math::{Rect, Vec2}; -use crate::transform::Transform; -use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; -use serde::{Deserialize, Serialize}; -use std::ops::{Deref, DerefMut}; -use std::rc::Rc; - -/// The icon data necessary to create an Icon again. -#[derive(Clone, Serialize, Deserialize)] -pub struct IconData { - /// The id of the icon is the icons position in the currently loaded icon_data vector. - pub id: usize, - /// The position of the icon on the map, given by the vector in meters. - pub position: Vec2<f64>, - /// Rotation of the icon texture in degrees. - pub rotation: f64, -} - -/// Describes an icon in the world and can be drawn. -#[derive(Clone)] -pub struct Icon { - data: IconData, - selected: bool, - renderer: Rc<IconRenderer>, -} - -impl Icon { - /// Create a new icon. Requires the icon renderer that is used to render this icon, as well as all - /// the information necessary to describe the icon itself. - pub fn new(id: usize, position: Vec2<f64>, rotation: f64, renderer: Rc<IconRenderer>) -> Self { - Self::from_data( - IconData { - id, - position, - rotation, - }, - renderer, - ) - } - - /// Like `new()`, but with the icon data bundled into the icon data type. - pub fn from_data(data: IconData, renderer: Rc<IconRenderer>) -> Self { - Self { - data, - selected: false, - renderer, - } - } -} - -impl Mappable for Icon { - fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { - let (texture, info) = self.renderer.get(self.id); - // Round the position to whole pixels to fix rotation problems. - let mut position_px = - transform.point_m_to_px(&(self.position - (info.anchor / info.pixels_per_m))); - position_px.x = position_px.x.floor(); - position_px.y = position_px.y.floor(); - rld.draw_texture_ex( - texture, - position_px, - self.rotation as f32, - (transform.pixels_per_m() / info.pixels_per_m) as f32, - if self.selected() { - DEFAULT_COLOURS.icon_selected - } else { - DEFAULT_COLOURS.icon_normal - }, - ); - } - - fn set_selected(&mut self, selected: bool) { - self.selected = selected; - } - - fn selected(&self) -> bool { - self.selected - } - - fn bounding_rect(&self) -> Rect<f64> { - Rect::new(self.data.position.x, self.data.position.y, 0., 0.) - } -} - -impl Deref for Icon { - type Target = IconData; - - fn deref(&self) -> &Self::Target { - &self.data - } -} -impl DerefMut for Icon { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.data - } -} diff --git a/src/map/mod.rs b/src/map/mod.rs deleted file mode 100644 index e1def09..0000000 --- a/src/map/mod.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! The map contains all the items that make up the world. -//! -//! There are two main structs to look out for, the first being [Map]. This is the interaction point -//! for most parts of the program. It contains the actual elements that are drawn on the screen. and -//! can be changed by the user. -//! The second is [MapData] and it contains the data that can be saved/loaded and distributed. Every -//! map item has an internal item that it can be dereferenced to and can be used to construct this -//! exact item in the same world elsewhere or at a different time. This is often different from the -//! item that is being drawn. An example would be the [PolygonRoom], which contains a triangulated -//! version of itself, so it can be drawn without always having to compute the triangles every frame. -//! It's data type however [PolygonRoomData] contains only the raw polygon data, not the triangulated -//! version, since that is enough to create the same [PolygonRoom] again. - -pub mod data; -pub mod icon; -pub mod icon_renderer; -pub mod mappable; -pub mod room; -pub mod wall; - -pub use data::MapData; -pub use icon::*; -pub use mappable::Mappable; -pub use room::*; -pub use wall::*; - -use crate::transform::Transform; -use icon_renderer::IconRenderer; -use raylib::drawing::RaylibDrawHandle; -use raylib::{RaylibHandle, RaylibThread}; -use std::rc::Rc; - -/// The map containing all map elements that are seen on the screen. -pub struct Map { - rooms: Vec<Room>, - walls: Vec<Wall>, - icons: Vec<Icon>, - icon_renderer: Rc<IconRenderer>, -} - -impl Map { - /// Create a new, empty map/world. - pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { - Self { - rooms: Vec::new(), - walls: Vec::new(), - icons: Vec::new(), - icon_renderer: Rc::new(IconRenderer::new(rl, rlt)), - } - } - - /// Add a room to the map. Currently, holes are not supported in the polygon, but this might - /// change later. - pub fn push_room(&mut self, room_data: RoomData) { - self.rooms.push(Room::from_data(room_data)); - } - - /// Add a wall to the world. - pub fn push_wall(&mut self, wall_data: WallData) { - /* Check for intersections with any wall that was arleady created so the wall ends can be - * rendered properly. - */ - let mut start_intersects = false; - let mut end_intersects = false; - for wall in &self.walls { - if wall.data().contains_collinear(wall_data.start) { - start_intersects = true; - } - if wall.data().contains_collinear(wall_data.end) { - end_intersects = true; - } - - // Currently, additional intersections can be ignored, since it is just a yes-no-question - if start_intersects && end_intersects { - break; - } - } - - self.walls - .push(Wall::from_data(wall_data, start_intersects, end_intersects)); - } - - /// Add an icon to the world. - pub fn push_icon(&mut self, icon: Icon) { - self.icons.push(icon); - } - - /// Draw all elements of the map to the screen. This should be called after the background of the - /// map has already been drawn. - pub fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { - for element in self.elements() { - element.draw(rld, transform); - } - } - - /// Get the icon-renderer that is currently used to render the icons. - pub fn icon_renderer(&self) -> Rc<IconRenderer> { - self.icon_renderer.clone() - } - - /// Retain all map elements that fulfill the given predicate, removing everything else. - pub fn retain<F>(&mut self, mut f: F) - where - F: FnMut(&dyn Mappable) -> bool, - { - // Call retain on all vectors containing the maps actual types. - self.rooms.retain(|p| f(p as &dyn Mappable)); - self.walls.retain(|w| f(w as &dyn Mappable)); - self.icons.retain(|i| f(i as &dyn Mappable)); - } - - /// Iterator over all elements as objects when an operation needs to go over all elements of the - /// map. - pub fn elements(&self) -> impl Iterator<Item = &dyn Mappable> { - self.rooms - .iter() - .map(|p| p as &dyn Mappable) - .chain(self.walls.iter().map(|w| w as &dyn Mappable)) - .chain(self.icons.iter().map(|i| i as &dyn Mappable)) - } - - /// Iterator over all elements, but the individual elements can be mutated. It is however - /// impossible to add or remove elements in this way. For that, use the dedicated functions. - pub fn elements_mut(&mut self) -> impl Iterator<Item = &mut dyn Mappable> { - self.rooms - .iter_mut() - .map(|p| p as &mut dyn Mappable) - .chain(self.walls.iter_mut().map(|w| w as &mut dyn Mappable)) - .chain(self.icons.iter_mut().map(|i| i as &mut dyn Mappable)) - } - - /// Get the rooms of this map. - pub fn rooms(&self) -> &Vec<Room> { - &self.rooms - } - - /// Get the walls of this map. - pub fn walls(&self) -> &Vec<Wall> { - &self.walls - } - - /// Get the icons of this map. - pub fn icons(&self) -> &Vec<Icon> { - &self.icons - } - - /// Replace the internal map data with the data provided. (Load and replace) - pub fn set_data(&mut self, data: MapData) { - // Remove all data. - self.icons.clear(); - self.rooms.clear(); - self.walls.clear(); - - // Add all data from the map data. - self.add_data(data); - } - - /// Add the data provided to the current data on the map. All elements will remain, with the - /// additional elements being pushed also. - pub fn add_data(&mut self, data: MapData) { - for i in data.icons { - self.push_icon(Icon::from_data(i, self.icon_renderer.clone())) - } - for p in data.rooms { - self.push_room(p); - } - for w in data.walls { - self.push_wall(w); - } - } -} diff --git a/src/net/cargo.rs b/src/net/cargo.rs new file mode 100644 index 0000000..b05944c --- /dev/null +++ b/src/net/cargo.rs @@ -0,0 +1,33 @@ +//! The cargo is information actually concerning the inner workings of graf karto, +//! as opposed to the inner workings of the network library. + +use crate::world::{Icon, Room, Wall, World}; +use nalgebra::Matrix3; +use serde::{Deserialize, Serialize}; + +/// Packets sent oven the network will carry this cargo to inform on what the client needs or the +/// server wants. +#[derive(Debug, Deserialize, Serialize)] +pub enum Cargo { + /// Client -> Server: Request to add an icon to the map + AddIcon(Icon), + /// Client -> Server: Request to add a room to the map + AddRoom(Room), + /// Client -> Server: Request to add a wall to the map + AddWall(Wall), + /// Client <-> Server: Update the info of the icon with the given id. + UpdateIcon((usize, Icon)), + /// Client <-> Server: Update the info of the room with the given id. + UpdateRoom((usize, Room)), + /// Client <-> Server: Update the info of the wall with the given id. + UpdateWall((usize, Wall)), + /// Client -> Server: Request to apply the given matrix to the item with the provided id. + /// If the matrix cannot be applied to an item with the given id, it will do nothing. + ApplyMatrix((usize, Matrix3<f64>)), + /// Client <-> Remove the item with the given id. + Remove(usize), + /// Server -> Client: Add all of the data additively to the map + AddMapData(World), + /// Server -> Client: Clear the current map data of the client and replace it with this. + UpdateMapData(World), +} diff --git a/src/net/client/connection.rs b/src/net/client/connection.rs new file mode 100644 index 0000000..2941a0a --- /dev/null +++ b/src/net/client/connection.rs @@ -0,0 +1,119 @@ +//! A connection to a server using the TCP-Protocol. + +use super::super::{Packet, PacketRwError}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fmt::Debug; +use std::io; +use std::net::TcpStream; +use std::ops::DerefMut; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{self, Receiver}; +use std::sync::Arc; +use std::thread::{self, JoinHandle}; +use std::time::Duration; +use std::{cell::RefCell, net::Shutdown}; + +/// Represents a connection to the server. +pub struct Connection<P: 'static + Debug + Send + DeserializeOwned + Serialize> { + stream: RefCell<TcpStream>, + packet_rx: Receiver<Packet<P>>, + running: Arc<AtomicBool>, + rcv_thread_handle: Option<JoinHandle<()>>, +} + +impl<P: 'static + Debug + Send + DeserializeOwned + Serialize> Connection<P> { + /// Create a new connection based on the given TCP-Stream. Will start up an + /// extra thread on which it will receive packets, while the thread with the + /// connection can send packets and query for packets that may have been + /// received. + pub fn new(stream: TcpStream) -> Self { + let running = Arc::new(AtomicBool::new(true)); + + let (tx, rx) = mpsc::channel(); + let mut stream_cl = stream + .try_clone() + .expect("Unable to create tcp stream for receiving packets."); + let running_cl = running.clone(); + let rcv_thread_handle = thread::spawn(move || { + // Set the read timeout, so that this thread can be closed gracefully. + stream_cl + .set_read_timeout(Some(Duration::from_millis(500))) + .expect("Unable to set socket read timeout."); + + while running_cl.load(Ordering::Relaxed) { + let packet = match Packet::read_from_stream(&mut stream_cl) { + Ok(p) => p, + Err(PacketRwError::Closed) => Packet::Disconnect, + Err(PacketRwError::IOError(err)) => { + if err.kind() == io::ErrorKind::WouldBlock { + // This error is thrown when the remote reaches the + // timeout duration and can thusly be ignored. + continue; + } + + error!("Unable to read packet: {:?}", err); + continue; + } + Err(err) => { + error!("Unable to read packet. {:?}", err); + continue; + } + }; + + // Send the packet through the mspc channel. + tx.send(packet).unwrap(); + } + }); + + Self { + stream: RefCell::new(stream), + packet_rx: rx, + running, + rcv_thread_handle: Some(rcv_thread_handle), + } + } + + /// Get the next packet in case one has been received. If no packet has been + /// received since the latest packet has been checked last time, this will + /// return none. If the client has been disconnected, this will also return + /// `None`. + pub fn next_packet(&self) -> Option<P> { + match self.packet_rx.try_recv() { + Ok(Packet::Disconnect) => { + self.running.store(false, Ordering::Relaxed); + info!("Server disconnected client."); + None + } + Ok(Packet::Cargo(packet)) => Some(packet), + Err(err) => { + warn!("unable to receive packet: {}", err); + None + } + } + } + + /// Send a packet to the server. + pub fn send(&self, packet: P) -> Result<(), PacketRwError> { + Packet::Cargo(packet).write_to_stream(self.stream.borrow_mut().deref_mut()) + } + + /// Stop the client. No further packets will be received or be sent. + pub fn stop(&mut self) -> Result<(), io::Error> { + self.stream.borrow_mut().shutdown(Shutdown::Both)?; + self.running.store(false, Ordering::Relaxed); + if let Some(handle) = self.rcv_thread_handle.take() { + handle + .join() + .expect("Packet receive thread closed unexpectedly."); + } + + Ok(()) + } +} + +impl<P: 'static + Debug + Send + DeserializeOwned + Serialize> Drop for Connection<P> { + fn drop(&mut self) { + self.stop().expect("Failed to stop the client properly"); + } +} diff --git a/src/net/client/mod.rs b/src/net/client/mod.rs new file mode 100644 index 0000000..d5db3e8 --- /dev/null +++ b/src/net/client/mod.rs @@ -0,0 +1,4 @@ +//! Networking items used solely by the client. + +pub mod connection; +pub use connection::*; diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..eb68d1d --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,11 @@ +//! Network module containing a simple client/server library. + +pub mod cargo; +pub mod client; +pub(self) mod packet; +pub mod server; + +pub use cargo::*; +pub use client::*; +pub(self) use packet::*; +pub use server::*; diff --git a/src/net/packet.rs b/src/net/packet.rs new file mode 100644 index 0000000..2d97504 --- /dev/null +++ b/src/net/packet.rs @@ -0,0 +1,73 @@ +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::io; +use std::mem; + +#[derive(Debug, Deserialize, Serialize)] +pub(super) enum Packet<P: 'static + Send + Debug> { + Cargo(P), + Disconnect, +} + +#[derive(Debug, thiserror::Error)] +pub enum PacketRwError { + #[error("packet could not be properly deserialised: {0}")] + DeserialiseError(bincode::Error), + #[error("packet could not be properly serialised: {0}")] + SerialiseError(bincode::Error), + #[error("unable to read packet from stream: {0}")] + IOError(io::Error), + #[error("connection was closed from the remote end")] + Closed, +} + +impl<P: 'static + Send + Debug + DeserializeOwned + Serialize> Packet<P> { + pub fn write_to_stream(&self, stream: &mut impl io::Write) -> Result<(), PacketRwError> { + let data: Vec<u8> = + bincode::serialize(&self).map_err(|err| PacketRwError::SerialiseError(err))?; + + // Write head with packet length + assert!(data.len() as u64 <= u32::MAX as u64); + let len = data.len() as u32; + let len = bincode::serialize(&len).map_err(|err| PacketRwError::SerialiseError(err))?; + stream + .write_all(&len) + .map_err(|err| PacketRwError::IOError(err))?; + + // Write the data of the packet and pray all errors are caught. + Ok(stream + .write_all(&data) + .map_err(|err| PacketRwError::IOError(err))?) + } + + pub fn read_from_stream(stream: &mut impl io::Read) -> Result<Self, PacketRwError> { + // Read packet head which informs us of the length. + let mut len = vec![0; mem::size_of::<u32>()]; + stream.read_exact(&mut len).map_err(|err| { + if err.kind() == io::ErrorKind::UnexpectedEof { + PacketRwError::Closed + } else { + PacketRwError::IOError(err) + } + })?; + let len: u32 = bincode::deserialize(&len) + .expect("Unable to deserialise length of packet. Stream is corrupted."); + + // Read all data from the packet according to the length. + let mut data = vec![0; len as usize]; + match stream.read_exact(&mut data) { + Ok(()) => { + let res: Result<Self, bincode::Error> = bincode::deserialize(&data); + Ok(res.map_err(|err| PacketRwError::DeserialiseError(err))?) + } + Err(err) => { + if err.kind() == io::ErrorKind::UnexpectedEof { + Err(PacketRwError::Closed) + } else { + Err(PacketRwError::IOError(err)) + } + } + } + } +} diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs new file mode 100644 index 0000000..801eb4b --- /dev/null +++ b/src/net/server/connection.rs @@ -0,0 +1,84 @@ +//! A TCP-connection from a client to the server. + +use super::super::packet::{Packet, PacketRwError}; +use super::connection_manager::ConnId; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fmt::Debug; +use std::net::TcpStream; +use std::sync::mpsc::Sender; +use std::thread::{self, JoinHandle}; + +/// Holds a stream to the client and manages receiving packets from said client +/// in an extra thread. +pub struct Connection { + stream: TcpStream, + rcv_thread_handle: JoinHandle<()>, +} + +impl Connection { + /// Start up the receiving thread after a client connection has been detected. + /// Will create a ghost disconnect packet in case the client disconnects. + pub(super) fn start_rcv<P>( + conn_id: ConnId, + stream: TcpStream, + packet_tx: Sender<(ConnId, Packet<P>)>, + ) -> Self + where + P: 'static + Debug + Send + DeserializeOwned + Serialize, + { + let mut stream_cl = stream + .try_clone() + .expect("Unable to clone TcpStream handle"); + let rcv_thread_handle = thread::spawn(move || { + let mut running = true; + while running { + // Read the newest packet from the stream. + let packet = match Packet::read_from_stream(&mut stream_cl) { + Ok(packet) => dbg!(packet), + Err(PacketRwError::Closed) => { + // Stop the thread after this packet. + running = false; + + /* Generate an internal disconnection packet, so the connection + * manager can call cleanup code if necessary. + */ + Packet::Disconnect + } + Err(err) => { + error!( + "Receiving packet failed. Connection `{}`. {:?}", + conn_id, err + ); + + // Ignore the received data. + continue; + } + }; + + /* Try sending the packet to the Connection manager. If it has already + * stopped and hung up on the channel, stop this receive thread as well. + */ + if packet_tx.send((conn_id, packet)).is_err() { + info!("Shutting down connection `{}`", conn_id); + running = false; + } + } + + info!("Packet receive thread has stopped running."); + }); + + Self { + stream, + rcv_thread_handle, + } + } + + /// Send a packet to the client via TCP. + pub(super) fn send<P>(&mut self, packet: &Packet<P>) -> Result<(), PacketRwError> + where + P: 'static + Debug + Send + DeserializeOwned + Serialize, + { + packet.write_to_stream(&mut self.stream) + } +} diff --git a/src/net/server/connection_manager.rs b/src/net/server/connection_manager.rs new file mode 100644 index 0000000..c47aa32 --- /dev/null +++ b/src/net/server/connection_manager.rs @@ -0,0 +1,109 @@ +//! The main server module, managing connections from clients. + +use super::super::packet::Packet; +use super::connection::Connection; +use crate::stable_vec::StableVec; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fmt::Debug; +use std::io; +use std::net::{SocketAddr, TcpListener}; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::sync::{Arc, RwLock}; +use std::thread; + +/// Type of id for the connections inside of the connection manager. +pub type ConnId = usize; + +/// Manages incoming connections to the servers and packets received from them. +pub struct ConnectionManager<P: 'static + Send + Debug + DeserializeOwned> { + connections: Arc<RwLock<StableVec<Connection>>>, + local_port: u16, + _tx: Sender<(ConnId, Packet<P>)>, + rx: Receiver<(ConnId, Packet<P>)>, +} + +impl<'de, P: 'static + Send + Debug + DeserializeOwned + Serialize> ConnectionManager<P> { + fn listen( + listener: TcpListener, + connections: Arc<RwLock<StableVec<Connection>>>, + packet_tx: Sender<(ConnId, Packet<P>)>, + ) { + for stream in listener.incoming() { + info!("Incoming connection."); + let stream = match stream { + Ok(stream) => stream, + Err(err) => { + error!("Unable to accept client. {}", err); + continue; + } + }; + + let mut connections = connections.write().unwrap(); + let id = connections.next_free(); + connections + .try_insert(id, Connection::start_rcv(id, stream, packet_tx.clone())) + .expect("Unable to insert client at supposedly valid id"); + info!("Client `{}` connected.", id); + } + + error!("Closing listener. This should never happen"); + } + + /// Start listening for connections. Returns the manager for connections, + /// which then can be asked about the connectins status and to send packets to + /// any client connected. + pub fn start(addr: SocketAddr) -> Result<Self, io::Error> { + let listener = TcpListener::bind(addr)?; + let local_port = listener.local_addr()?.port(); + let connections = Arc::new(RwLock::new(StableVec::new())); + + let (tx, rx) = mpsc::channel(); + let tx_cl = tx.clone(); + let connections_cl = connections.clone(); + thread::spawn(move || Self::listen(listener, connections_cl, tx_cl)); + + Ok(Self { + connections, + local_port, + _tx: tx, + rx, + }) + } + + /// Try to receive the next packet. If no packet was received, this returns `None`. If the client + /// was disconnected, this also returns `None`. + pub fn next_packet(&self) -> Option<(ConnId, P)> { + match self.rx.try_recv() { + Ok((conn_id, Packet::Disconnect)) => { + self.connections.write().unwrap().remove(conn_id); + self.next_packet() + } + Ok((conn_id, Packet::Cargo(packet))) => Some((conn_id, packet)), + Err(_err) => None, + } + } + + /// Send a packet to all clients currently connected + pub fn broadcast(&self, packet: P) -> bool { + let mut one_failed = false; + let mut conns = self.connections.write().unwrap(); + let packet = Packet::Cargo(packet); + for (ref id, ref mut conn) in conns.id_iter_mut() { + if let Err(err) = conn.send(&packet) { + error!( + "Broadcasting {:?} failed for client `{}`: {:?}", + packet, id, err + ); + one_failed = true; + } + } + + !one_failed + } + + /// Get the local port this connection manager's listener is bound to. + pub fn port(&self) -> u16 { + self.local_port + } +} diff --git a/src/net/server/mod.rs b/src/net/server/mod.rs new file mode 100644 index 0000000..1f52ad5 --- /dev/null +++ b/src/net/server/mod.rs @@ -0,0 +1,6 @@ +//! Module containing network library server parts. + +pub mod connection; +pub mod connection_manager; + +pub use connection_manager::*; diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..9b55502 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,48 @@ +//! Contains the necessary ingredients to start a graf karto world server. + +use crate::net::server::ConnectionManager; +use crate::net::Cargo; +use std::io; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::thread::{self, JoinHandle}; + +/// The default port the dedicated graf karto server runs on. +pub const DEFAULT_PORT: u16 = 44309; + +fn localhost(port: u16, ipv4: bool) -> SocketAddr { + if ipv4 { + SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port) + } else { + SocketAddr::new(Ipv6Addr::LOCALHOST.into(), port) + } +} + +/// Starts a thread for the server and tries to bind to the specified port. If the +/// port cannot be bound to it fails returning an error, otherwise the join handle +/// for the server thread is returned. +pub fn start_with_port(port: u16, ipv4: bool) -> Result<JoinHandle<()>, io::Error> { + let addr = localhost(port, ipv4); + let conn_man = ConnectionManager::start(addr)?; + + Ok(start(conn_man)) +} + +/// Starts a thread on any free system port. Returns an error in case that's not +/// possible. +pub fn start_any_port(ipv4: bool) -> Result<(JoinHandle<()>, u16), io::Error> { + let addr = localhost(0, ipv4); + + let conn_man = ConnectionManager::start(addr)?; + let port = conn_man.port(); + + Ok((start(conn_man), port)) +} + +fn start(conn_man: ConnectionManager<Cargo>) -> JoinHandle<()> { + info!("Server started on port {}", conn_man.port()); + thread::spawn(move || loop { + if let Some(cargo) = conn_man.next_packet() { + println!("Received cargo: {:?}", cargo); + } + }) +} diff --git a/src/server_main.rs b/src/server_main.rs index e69de29..d9832fd 100644 --- a/src/server_main.rs +++ b/src/server_main.rs @@ -0,0 +1,65 @@ +//! Dedicated server for graf_karto. Starts only the backend that the client relies on and allows +//! connections from the outside if possible. + +#![allow(dead_code)] +#![warn(missing_docs)] + +#[macro_use] +extern crate log; + +pub mod math; +pub mod net; +pub mod server; +pub mod stable_vec; +pub mod transformable; +pub mod world; + +use clap::{App, Arg}; +pub use server::*; + +fn main() { + println!( + "Graf Karto dedicated server version: {}", + clap::crate_version!() + ); + + let default_port = DEFAULT_PORT.to_string(); + let matches = App::new("Graf Karto") + .version(clap::crate_version!()) + .author(clap::crate_authors!()) + .about(clap::crate_description!()) + .arg( + Arg::with_name("port") + .short("p") + .value_name("SERVER_PORT") + .help("Set the port the should listen on.") + .default_value(&default_port), + ) + .arg( + Arg::with_name("ipv4") + .short("v4") + .help("Use virgin IPv4 instead of chad IPv6.. you monster"), + ) + .get_matches(); + + let use_ipv4 = matches.is_present("ipv4"); + let port = match matches + .value_of("port") + .expect("No port found, eventhough it should have a default value") + .parse::<u16>() + { + Ok(port) => port, + Err(e) => { + error!("Not a valid server port: {:?}", e); + warn!("Using default port {}", DEFAULT_PORT); + DEFAULT_PORT + } + }; + + let server_handle = server::start_with_port(port, use_ipv4) + .expect("Unable to start server. Make sure the port you want to bind it on is free."); + + server_handle + .join() + .expect("Server did not shut down correctly."); +} diff --git a/src/stable_vec.rs b/src/stable_vec.rs index 38eb162..72a1995 100644 --- a/src/stable_vec.rs +++ b/src/stable_vec.rs @@ -1,11 +1,13 @@ //! Stable vec is a vector that guarantees its content access position does not change. +use serde::{Deserialize, Serialize}; use std::ops::Deref; use std::slice::IterMut; /// Works like Vec, but with an additional field, where the position information is saved. Does not /// support inserting elements in arbitrary positions, since that may shift the data. Removing data /// in the middle is fine, however. +#[derive(Debug, Deserialize, Serialize)] pub struct StableVec<T> { data: Vec<(usize, T)>, } @@ -22,6 +24,12 @@ impl<T> StableVec<T> { Self { data: Vec::new() } } + /// Create a stable vec from a sorted (by id, T.0) vector. Don't use if you're + /// not absolutely sure the data is valid. + pub fn from_raw_unchecked(data: Vec<(usize, T)>) -> Self { + Self { data } + } + /// Add an item to the end of the vector. Returns its stable id. (`len()-1 != last_element_id`) pub fn push(&mut self, item: T) -> usize { if self.data.is_empty() { @@ -34,6 +42,72 @@ impl<T> StableVec<T> { } } + /// Remove all data from this vec, leaving it like a StableVec created with `new` datawise. + pub fn clear(&mut self) { + self.data.clear() + } + + /// Find the next free space in the vec. If there is space at the end, this will be preferred, + /// otherwise low ids are. After checking this, `try_insert` will not fail and the returned + /// id can be used to insert an item at that position. + pub fn next_free(&self) -> usize { + // Check if the item can be pushed at the end. + if self.data.is_empty() { + 0 + } else if self.data.last().unwrap().0 < usize::MAX { + self.data.last().unwrap().0 + } else { + // Try to find a position in the vector that is still free, starting at the bottom. + let mut prev_id = self.data.first().unwrap().0; + for (_, (id, _)) in self.data.iter().enumerate().skip(1) { + if *id > prev_id + 1 { + return *id - 1; + } + + prev_id = *id; + } + + panic!("There is no free space in the vector. This should never happen."); + } + } + + /// Attempts to insert the item at the given position. If there is no free space, an Err value + /// will be returned. + pub fn try_insert(&mut self, id: usize, item: T) -> Result<(), ()> { + match self.find_pos(id) { + // The item already exists, this is an error. + Ok(_) => Err(()), + Err(pos) => { + self.data.insert(pos, (id, item)); + Ok(()) + } + } + } + + /// Tries to push the item into the vector, but if it doesn't work, it searches for an opening in + /// the vector where no item currently is and places it there, favouring low ids (low ids contain + /// potentially older changes). + pub fn insert_anywhere(&mut self, item: T) -> usize { + // Check if the item can be pushed at the end and do so if possible. + if self.data.is_empty() || self.data.last().unwrap().0 < usize::MAX { + self.push(item) + } else { + // Try to find a position in the vector that is still free, starting at the bottom. + let mut prev_id = self.data.first().unwrap().0; + for (place, (id, _)) in self.data.iter().enumerate().skip(1) { + if *id > prev_id + 1 { + let id = *id - 1; + self.data.insert(place, (id, item)); + return id; + } + + prev_id = *id; + } + + panic!("Unable to insert element. No space left in the vector."); + } + } + // Find the internal position of the given id in `O(log n)` fn find_pos(&self, id: usize) -> Result<usize, usize> { self.data.binary_search_by(|x| x.0.cmp(&id)) @@ -84,6 +158,12 @@ impl<T> Deref for StableVec<T> { } } +impl<T> Into<Vec<(usize, T)>> for StableVec<T> { + fn into(self) -> Vec<(usize, T)> { + self.data + } +} + impl<'a, T> IdIterMut<'a, T> { pub(super) fn new(id_vec: &'a mut [(usize, T)]) -> Self { Self { diff --git a/src/world/component.rs b/src/world/component.rs new file mode 100644 index 0000000..e8a8df9 --- /dev/null +++ b/src/world/component.rs @@ -0,0 +1,23 @@ +//! The world is made up of components. Every component must implement this +//! general trait that makes it possible to argue about certain characteristics +//! and may make it possible to transform some items. + +use crate::math::Rect; +use crate::transformable::NonRigidTransformable; + +/// Anything that can be added to the map or world must implement this trait. +pub trait Component { + /// Get the rectangle that contains the component in its entirety without excess. + fn bounding_rect(&self) -> Rect<f64>; + + /// If this component can be transformed in a non-rigid way, a dynamic + /// reference is returned, otherwise none. + fn as_non_rigid(&self) -> Option<&dyn NonRigidTransformable> { + None + } + + /// The same as `as_non_rigid`, but mutably. + fn as_non_rigid_mut(&mut self) -> Option<&mut dyn NonRigidTransformable> { + None + } +} diff --git a/src/world/icon.rs b/src/world/icon.rs new file mode 100644 index 0000000..c8945fb --- /dev/null +++ b/src/world/icon.rs @@ -0,0 +1,23 @@ +//! Icons are world elements that have a specific size and cannot be stretched. They are usually used +//! as markers for specific places in the world. + +use super::Component; +use crate::math::{Rect, Vec2}; +use serde::{Deserialize, Serialize}; + +/// The icon datatype. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Icon { + /// The id of the icon is the icons position in the currently loaded icon_data vector. + pub id: usize, + /// The position of the icon on the map, given by the vector in meters. + pub position: Vec2<f64>, + /// Rotation of the icon texture in degrees. + pub rotation: f64, +} + +impl Component for Icon { + fn bounding_rect(&self) -> Rect<f64> { + Rect::new(self.position.x, self.position.y, 0., 0.) + } +} diff --git a/src/world/mod.rs b/src/world/mod.rs new file mode 100644 index 0000000..047ca5c --- /dev/null +++ b/src/world/mod.rs @@ -0,0 +1,113 @@ +//! The world contains all items of a session and the ways to manipulate them. +//! Generally speaking, manipulation will be done on the server side and then +//! the changes are sent to the client. + +pub mod component; +pub mod icon; +pub mod room; +pub mod wall; + +pub use component::*; +pub use icon::*; +pub use room::*; +pub use wall::*; + +use crate::stable_vec::StableVec; +use ron::de::from_reader; +use ron::ser::{to_string_pretty, PrettyConfig}; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{self, Write}; +use std::path::Path; + +/// Main structure that contains all information about a world necessary to argue +/// about structures in it. It can be persistified and loaded from disk, as well +/// as sent over the network. Everything in it is assigned an id to make it easy +/// to argue about a specific item. IDs may only be assigned by the server, not +/// the client, since otherwise multiple clients may try to create the same item. +#[derive(Debug, Serialize, Deserialize)] +pub struct World { + rooms: StableVec<Room>, + walls: StableVec<Wall>, + icons: StableVec<Icon>, + used_ids: StableVec<()>, +} + +impl World { + /// Load the world data from a file. Fails if the file does not exist or + /// cannot be correctly parsed. + pub fn load_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> { + let file = File::open(&path)?; + let data: Self = match from_reader(file) { + Ok(data) => data, + Err(err) => { + return Err(io::Error::new(io::ErrorKind::InvalidData, err)); + } + }; + + Ok(data) + } + + /// Write the world data to the file located at `path`. If the file already + /// exists, it will be overwritten. If the write fails, an IO-Error is returned. + pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> { + let mut file = File::create(&path)?; + + let pretty_conf = PrettyConfig::new() + .with_depth_limit(4) + .with_decimal_floats(true) + .with_separate_tuple_members(true) + .with_indentor("\t".to_owned()); + let string = match to_string_pretty(&self, pretty_conf) { + Ok(string) => string, + Err(err) => { + return Err(io::Error::new(io::ErrorKind::InvalidInput, err)); + } + }; + + file.write_all(&string.as_bytes()) + } + + /// Add a room to the world. Currently, holes are not supported in the polygon, but this might + /// change later. + pub fn push_room(&mut self, room: Room) -> usize { + let id = self.used_ids.insert_anywhere(()); + self.rooms + .try_insert(id, room) + .expect("Id vecs out of sync"); + id + } + + /// Add a wall to the world. + pub fn push_wall(&mut self, wall: Wall) -> usize { + let id = self.used_ids.insert_anywhere(()); + self.walls + .try_insert(id, wall) + .expect("Id vecs out of sync"); + id + } + + /// Add an icon to the world. + pub fn push_icon(&mut self, icon: Icon) -> usize { + let id = self.used_ids.insert_anywhere(()); + self.icons + .try_insert(id, icon) + .expect("Id vecs out of sync"); + id + } + + /// Get all icons of the world. + pub fn icons(&self) -> &StableVec<Icon> { + &self.icons + } + + /// Get all rooms of the world. + pub fn rooms(&self) -> &StableVec<Room> { + &self.rooms + } + + /// Get all walls of the world. + pub fn walls(&self) -> &StableVec<Wall> { + &self.walls + } +} diff --git a/src/world/room.rs b/src/world/room.rs new file mode 100644 index 0000000..fed8890 --- /dev/null +++ b/src/world/room.rs @@ -0,0 +1,52 @@ +//! Polygon rooms are the standard rooms in graf karto. They can take the form of anything that a +//! [Polygon](crate::math::Polygon) can have. + +use super::Component; +use crate::math::{Polygon, Rect}; +use crate::transformable::NonRigidTransformable; +use nalgebra::{Matrix3, Point2}; +use serde::{Deserialize, Serialize}; + +/// A polygon shaped room, which can be placed and modified in the world. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Room { + shape: Polygon<f64>, +} + +impl Room { + /// Create a new room with the given shape. + pub fn new(shape: Polygon<f64>) -> Self { + Self { shape } + } + + /// Get the polygon this room is based on. + pub fn shape(&self) -> &Polygon<f64> { + &self.shape + } +} + +impl Component for Room { + fn bounding_rect(&self) -> Rect<f64> { + Rect::bounding_rect_n(&self.shape.corners()) + } + + fn as_non_rigid(&self) -> Option<&dyn NonRigidTransformable> { + Some(self as &dyn NonRigidTransformable) + } + + fn as_non_rigid_mut(&mut self) -> Option<&mut dyn NonRigidTransformable> { + Some(self as &mut dyn NonRigidTransformable) + } +} + +impl NonRigidTransformable for Room { + // XXX: This produces undefined behaviour when using a mirroring matrix, since + // the polygon corners must be sorted in counterclockwise direction. + fn apply_matrix(&mut self, matrix: &Matrix3<f64>) { + for corner in self.shape.corners_mut() { + *corner = matrix + .transform_point(&Point2::new(corner.x, corner.y)) + .into(); + } + } +} diff --git a/src/world/wall.rs b/src/world/wall.rs new file mode 100644 index 0000000..45b0b1e --- /dev/null +++ b/src/world/wall.rs @@ -0,0 +1,52 @@ +//! Walls, solid barriers that are generally unclimbable. +//! +//! This interpretation is generally up to the GM to decide, but generally +//! speaking, a wall cannot be crossed by a player. If special conditions apply +//! (for instance, when the player wants to scale the wall), a check is necessary. +//! If a check is not necessary, then maybe you were not thinking about a wall. + +use super::Component; +use crate::math::{LineSegment, Rect}; +use crate::transformable::NonRigidTransformable; +use nalgebra::Matrix3; +use serde::{Deserialize, Serialize}; + +/// Representation of a solid wall a player cannot go through, except if special +/// conditions apply. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Wall { + shape: LineSegment<f64>, +} + +impl Wall { + /// Create a new wall with the line segment defining it's start and end. + pub fn new(shape: LineSegment<f64>) -> Self { + Self { shape } + } + + /// Get the line shape this wall is based on. + pub fn shape(&self) -> &LineSegment<f64> { + &self.shape + } +} + +impl Component for Wall { + fn bounding_rect(&self) -> Rect<f64> { + Rect::bounding_rect(self.shape.start, self.shape.end) + } + + fn as_non_rigid(&self) -> Option<&dyn NonRigidTransformable> { + Some(self as &dyn NonRigidTransformable) + } + + fn as_non_rigid_mut(&mut self) -> Option<&mut dyn NonRigidTransformable> { + Some(self as &mut dyn NonRigidTransformable) + } +} + +impl NonRigidTransformable for Wall { + fn apply_matrix(&mut self, matrix: &Matrix3<f64>) { + self.shape.start = matrix.transform_point(&self.shape.start.into()).into(); + self.shape.end = matrix.transform_point(&self.shape.end.into()).into(); + } +} |
