diff --git a/games/rstnode/Cargo.lock b/games/rstnode/Cargo.lock index 60e7e3d404b..81e017cbd20 100644 --- a/games/rstnode/Cargo.lock +++ b/games/rstnode/Cargo.lock @@ -96,6 +96,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "approx" version = "0.3.2" @@ -515,7 +524,7 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ - "ansi_term", + "ansi_term 0.11.0", "atty", "bitflags", "strsim 0.8.0", @@ -2362,6 +2371,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.8" @@ -3548,6 +3566,16 @@ dependencies = [ "thread_local", ] +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" +dependencies = [ + "byteorder", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.22" @@ -3638,6 +3666,10 @@ dependencies = [ "librsvg", "mint", "rst-core", + "svg", + "tempfile", + "tracing", + "tracing-subscriber", ] [[package]] @@ -3846,6 +3878,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "sharded-slab" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" +dependencies = [ + "lazy_static", +] + [[package]] name = "shared_library" version = "0.1.9" @@ -4090,6 +4131,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" +[[package]] +name = "svg" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbef9cf3cf75dd7772fb1f40dd6d90278a5263454db94ee399500ee9918aaa7" + [[package]] name = "syn" version = "0.15.44" @@ -4329,6 +4376,49 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0f8c7178e13481ff6765bd169b33e8d554c5d2bbede5e32c356194be02b9b9" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1fa8f0c8f4c594e4fc9debc1990deab13238077271ba84dd853d54902ee3401" +dependencies = [ + "ansi_term 0.12.1", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + [[package]] name = "ttf-parser" version = "0.6.2" diff --git a/games/rstnode/assets/raw/frame/frame_l.png b/games/rstnode/assets/raw/frame/frame_l.png new file mode 100644 index 00000000000..f37386dca35 Binary files /dev/null and b/games/rstnode/assets/raw/frame/frame_l.png differ diff --git a/games/rstnode/assets/raw/frame_l.svg b/games/rstnode/assets/raw/frame/frame_l.svg similarity index 100% rename from games/rstnode/assets/raw/frame_l.svg rename to games/rstnode/assets/raw/frame/frame_l.svg diff --git a/games/rstnode/assets/raw/frame/frame_m.png b/games/rstnode/assets/raw/frame/frame_m.png new file mode 100644 index 00000000000..02afae96ac2 Binary files /dev/null and b/games/rstnode/assets/raw/frame/frame_m.png differ diff --git a/games/rstnode/assets/raw/frame_m.svg b/games/rstnode/assets/raw/frame/frame_m.svg similarity index 100% rename from games/rstnode/assets/raw/frame_m.svg rename to games/rstnode/assets/raw/frame/frame_m.svg diff --git a/games/rstnode/assets/raw/frame/frame_s.png b/games/rstnode/assets/raw/frame/frame_s.png new file mode 100644 index 00000000000..6fc0133acbc Binary files /dev/null and b/games/rstnode/assets/raw/frame/frame_s.png differ diff --git a/games/rstnode/assets/raw/frame_s.svg b/games/rstnode/assets/raw/frame/frame_s.svg similarity index 100% rename from games/rstnode/assets/raw/frame_s.svg rename to games/rstnode/assets/raw/frame/frame_s.svg diff --git a/games/rstnode/assets/raw/frame/frame_xl.png b/games/rstnode/assets/raw/frame/frame_xl.png new file mode 100644 index 00000000000..6ec3a513d05 Binary files /dev/null and b/games/rstnode/assets/raw/frame/frame_xl.png differ diff --git a/games/rstnode/assets/raw/frame_xl.svg b/games/rstnode/assets/raw/frame/frame_xl.svg similarity index 100% rename from games/rstnode/assets/raw/frame_xl.svg rename to games/rstnode/assets/raw/frame/frame_xl.svg diff --git a/games/rstnode/assets/raw/relay/relay1.png b/games/rstnode/assets/raw/relay/relay1.png new file mode 100644 index 00000000000..0c66135130f Binary files /dev/null and b/games/rstnode/assets/raw/relay/relay1.png differ diff --git a/games/rstnode/assets/raw/relay1.svg b/games/rstnode/assets/raw/relay/relay1.svg similarity index 100% rename from games/rstnode/assets/raw/relay1.svg rename to games/rstnode/assets/raw/relay/relay1.svg diff --git a/games/rstnode/rst-client/Cargo.toml b/games/rstnode/rst-client/Cargo.toml index 929f7550bbe..7b5155f8108 100644 --- a/games/rstnode/rst-client/Cargo.toml +++ b/games/rstnode/rst-client/Cargo.toml @@ -9,8 +9,13 @@ authors = ["Bread Machine", "Katharina Fey "] [dependencies] rst-core = { path = "../rst-core" } -clap = "2.0" +cairo-rs = { version="0.8.0", features=["v1_16", "png", "svg"] } +librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "d34f570f" } ggez = "0.6.0-rc0" mint = "0.5" # Required because ggez is trash -librsvg = { git = "https://gitlab.gnome.org/GNOME/librsvg.git", rev = "d34f570f" } -cairo-rs = { version="0.8.0", features=["v1_16", "png", "svg"] } \ No newline at end of file +svg = "0.9" + +clap = "2.0" +tracing = "0.1" +tracing-subscriber = "0.2" +tempfile = "*" \ No newline at end of file diff --git a/games/rstnode/rst-client/src/assets.rs b/games/rstnode/rst-client/src/assets.rs index 7e368b8d4c1..d1ccdaf875e 100644 --- a/games/rstnode/rst-client/src/assets.rs +++ b/games/rstnode/rst-client/src/assets.rs @@ -1,5 +1,19 @@ +use crate::{error::LoadError, GameSettings}; +use cairo::{Context, Format, ImageSurface, Rectangle}; use ggez::graphics::Image; -use std::{collections::BTreeMap, path::Path}; +use librsvg::{CairoRenderer, Loader}; +use std::{ + collections::BTreeMap, + error::Error, + ffi::OsStr, + fs::{read_dir, File}, + io::BufWriter, + io::Read, + path::{Path, PathBuf}, +}; +use tempfile::tempdir; + +pub type Result = std::result::Result; /// Construct a `node` prefixed URI pub fn node(tt: &str) -> URI { @@ -24,21 +38,137 @@ impl From for URI { } /// Asset loader +#[derive(Debug)] pub struct Assets { inner: BTreeMap, } impl Assets { - pub fn load(p: &Path) -> Self { + fn new() -> Self { Self { inner: Default::default(), } } + + pub fn find>(&self, u: U) -> Option { + self.inner.get(&u.into()).map(|i| i.clone()) + } + + /// Load an asset directory path + fn load_tree(&mut self, ctx: &mut ggez::Context, tmpdir: &Path, p: &Path) -> Result<()> { + let err: LoadError = p.to_str().unwrap().into(); + + read_dir(p) + .map_err(|_| err)? + .map(|e| { + let e = e.unwrap(); + let p = e.path(); + + let ext = OsStr::new("svg"); + + if p.is_dir() { + debug!( + "Entering directory {}", + p.file_name().unwrap().to_str().unwrap() + ); + self.load_tree(ctx, tmpdir, p.as_path())?; + } else if p.extension() == Some(ext) { + let png = load_svg(tmpdir, p.as_path())?; + + let basepath = p.with_extension(""); + + let uri_cat = p.parent().unwrap().file_name().unwrap().to_str().unwrap(); + let name = basepath.file_name().unwrap().to_str().unwrap(); + let uri = format!("{}/{}", uri_cat, name); + let path_str = png.as_path().to_str().unwrap(); + + let mut content = vec![]; + let mut f = File::open(png.as_path()).map_err(|_| { + LoadError::from(format!("No such file: {}", path_str).as_str()) + })?; + + f.read_to_end(&mut content).map_err(|e| { + LoadError::from( + format!("Read error for {}: {}", path_str, e.to_string()).as_str(), + ) + })?; + + self.inner.insert( + uri.into(), + Image::from_bytes(ctx, content.as_slice()).map_err(|e| { + LoadError::from( + format!("Read error for {}: {}", path_str, e.to_string()).as_str(), + ) + })?, + ); + } + + Ok(()) + }) + .fold(Ok(()), |acc, res| match (acc, res) { + (Ok(_), Ok(_)) => Ok(()), + (Ok(_), Err(e)) => Err(e), + (Err(e), _) => Err(e), + }) + } } +/// Load all game assets into the game +/// +/// This function performs three main steps. +/// +/// 1. Check that the provided path is a directory +/// 2. Recursively load Directories and files and call +/// [`load_svg`](self::load_svg) on each `.svg` file +/// 3. Re-load newly converted assets into [`Assets`](self::Assets) +pub fn load_tree(ctx: &mut ggez::Context, settings: &GameSettings) -> Result { + let path = match settings.assets.clone() { + Some(s) => Ok(s), + None => Err(LoadError::from("No assets path set!")), + }?; + + debug!( + "Starting assets loading harness on {}", + path.to_str().unwrap() + ); + + let tmpdir = tempdir().unwrap(); + let mut assets = Assets::new(); + assets.load_tree(ctx, tmpdir.path(), path.as_path())?; + info!("Asset loading complete!"); + Ok(assets) +} /// A utility function to take an SVG and render it to a raster image /// according to a render spec -fn load_svg(p: &Path) -> () { - +pub fn load_svg(tmpdir: &Path, p: &Path) -> Result { + let err: LoadError = p.to_str().unwrap().into(); + + let handle = Loader::new().read_path(p).map_err(|_| err.clone())?; + let renderer = CairoRenderer::new(&handle); + + let surf = ImageSurface::create(Format::ARgb32, 256, 256).map_err(|_| err.clone())?; + let cr = Context::new(&surf); + + renderer + .render_document( + &cr, + &Rectangle { + x: 0.0, + y: 0.0, + width: 256.0, + height: 256.0, + }, + ) + .map_err(|_| err.clone())?; + + let png = p.with_extension("png"); + let name = png + .file_name() + .map_or_else(|| Err(err.clone()), |name| Ok(name))?; + + let out = tmpdir.join(name.clone()); + let mut file = BufWriter::new(File::create(out.clone()).map_err(|_| err.clone())?); + surf.write_to_png(&mut file).map_err(|_| err.clone())?; + Ok(out.to_path_buf()) } diff --git a/games/rstnode/rst-client/src/cli.rs b/games/rstnode/rst-client/src/cli.rs index b8a93b237a7..b13697adb29 100644 --- a/games/rstnode/rst-client/src/cli.rs +++ b/games/rstnode/rst-client/src/cli.rs @@ -1 +1,57 @@ //! Handle user CLI inputs + +use crate::{ + constants::{NAME, VERSION}, + settings::WindowMode, + GameSettings, +}; +use clap::{App, Arg}; +use std::path::PathBuf; + +/// Run CLI parser and parse options into GameSettings structure +pub fn parse(settings: &mut GameSettings) { + let app = App::new(NAME) + .version(VERSION) + .author("Bread Machine (Katharina Fey From<&'s str> for LoadError { + fn from(s: &'s str) -> Self { + Self(s.into()) + } +} + +impl Display for LoadError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error for LoadError {} diff --git a/games/rstnode/rst-client/src/graphics/entities/mod.rs b/games/rstnode/rst-client/src/graphics/entities/mod.rs index d3536d4f5fc..17f26a8e5a7 100644 --- a/games/rstnode/rst-client/src/graphics/entities/mod.rs +++ b/games/rstnode/rst-client/src/graphics/entities/mod.rs @@ -22,20 +22,28 @@ pub struct NodeRndr { pub inner: Arc, } -impl EventHandler for NodeRndr { - fn update(&mut self, _: &mut Context) -> GameResult<()> { +impl Renderer for NodeRndr { + fn update(&mut self, _: &mut ClientState, _: &mut Context) -> GameResult<()> { Ok(()) } - fn draw(&mut self, ctx: &mut Context) -> GameResult<()> { + fn draw(&self, s: &ClientState, ctx: &mut Context) -> GameResult<()> { + let frame = s.assets().find("frame/frame_s").unwrap(); + + frame.draw( + ctx, + DrawParam::new().dest([256.0, 256.0]).color(graphics::RED), + )?; + let circ = Mesh::new_circle( ctx, DrawMode::fill(), Point2::from(&self.loc), - 128.0, + 64.0, 0.1, graphics::WHITE, - ).unwrap(); + ) + .unwrap(); circ.draw(ctx, DrawParam::new()).unwrap(); Ok(()) diff --git a/games/rstnode/rst-client/src/graphics/mod.rs b/games/rstnode/rst-client/src/graphics/mod.rs index 8118207d70a..095e66f1ad1 100644 --- a/games/rstnode/rst-client/src/graphics/mod.rs +++ b/games/rstnode/rst-client/src/graphics/mod.rs @@ -8,9 +8,25 @@ pub mod entities; pub mod ui; +use crate::state::ClientState; +use ggez::{Context, GameResult}; + /// A utility module to include everything required to implement a /// graphics entity pub(self) mod prelude { - pub use ggez::{event::EventHandler, graphics::{self, Drawable, DrawParam, Mesh, DrawMode}, Context, GameResult}; + pub use ggez::{ + event::EventHandler, + graphics::{self, DrawMode, DrawParam, Drawable, Mesh}, + Context, GameResult, + }; pub use mint::Point2; + + pub use super::Renderer; + pub use crate::state::ClientState; +} + +/// A rendering trait which is given graphics context, and game state +pub trait Renderer { + fn update(&mut self, _state: &mut ClientState, _ctx: &mut Context) -> GameResult<()>; + fn draw(&self, _state: &ClientState, _ctx: &mut Context) -> GameResult<()>; } diff --git a/games/rstnode/rst-client/src/log.rs b/games/rstnode/rst-client/src/log.rs new file mode 100644 index 00000000000..f1c346ae343 --- /dev/null +++ b/games/rstnode/rst-client/src/log.rs @@ -0,0 +1,43 @@ +//! Logging specifics + +const BANNER: &'static str = " +██████╗ ███████╗████████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗ +██╔══██╗██╔════╝╚══██╔══╝ ████╗ ██║██╔═══██╗██╔══██╗██╔════╝ +██████╔╝███████╗ ██║ ██╔██╗ ██║██║ ██║██║ ██║█████╗ +██╔══██╗╚════██║ ██║ ██║╚██╗██║██║ ██║██║ ██║██╔══╝ +██║ ██║███████║ ██║ ██║ ╚████║╚██████╔╝██████╔╝███████╗ +╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝"; + +use tracing_subscriber::{filter::LevelFilter, fmt, EnvFilter}; + +pub(crate) fn initialise() { + let filter = EnvFilter::try_from_env("RST_LOG") + .unwrap_or_default() + .add_directive(LevelFilter::DEBUG.into()) + .add_directive("async_std=error".parse().unwrap()) + .add_directive("gfx_device_gl=error".parse().unwrap()) + .add_directive("ggez=error".parse().unwrap()) + .add_directive("selectors=error".parse().unwrap()) + .add_directive("gilrs=error".parse().unwrap()) + .add_directive("mio=error".parse().unwrap()); + + // Initialise the logger + fmt().with_env_filter(filter).init(); + info!("Initialising..."); + info!("{}", BANNER); + info!("Platform: unknown"); + info!("GPU Driver: unknown"); + info!("Version: {}", crate::constants::VERSION); +} + +#[macro_export] +macro_rules! fatal { + () => { + error!("Unknown failure!"); + std::process::exit(2) + }; + ($($arg:tt)*) => ({ + error!($($arg)*); + std::process::exit(2) + }) +} diff --git a/games/rstnode/rst-client/src/main.rs b/games/rstnode/rst-client/src/main.rs index 57a686a19d7..b86ee341075 100644 --- a/games/rstnode/rst-client/src/main.rs +++ b/games/rstnode/rst-client/src/main.rs @@ -1,10 +1,15 @@ //! RST Node game client +#[macro_use] +extern crate tracing; + mod assets; mod cli; mod constants; mod ctx; +mod error; mod graphics; +mod log; mod settings; mod state; mod window; @@ -14,8 +19,22 @@ pub(crate) use settings::{GameSettings, GraphicsSettings, WindowSettings}; pub(crate) use state::*; fn main() { - let settings = settings::default(); + // Initialise logging mechanism + log::initialise(); + + // Initialise default game settings + let mut settings = settings::default(); + + // Parse commandline arguments + cli::parse(&mut settings); + + // Initialise window context + let mut window = window::create(&settings); + + // Load assets tree + let assets = + assets::load_tree(window.ctx(), &settings).unwrap_or_else(|e| fatal!("LoadError: {}!", e)); + let state = ClientState::new(settings, assets); - let state = ClientState::new(&settings); - window::run(&settings, state) + window.run(state) } diff --git a/games/rstnode/rst-client/src/settings.rs b/games/rstnode/rst-client/src/settings.rs index a339c4106da..cb44eacca3a 100644 --- a/games/rstnode/rst-client/src/settings.rs +++ b/games/rstnode/rst-client/src/settings.rs @@ -1,16 +1,18 @@ //! Configuration structures for the game client use ggez::conf::{FullscreenType, NumSamples}; +use std::path::PathBuf; pub fn default() -> GameSettings { GameSettings { + assets: None, window: WindowSettings { width: 1280, height: 720, window_mode: WindowMode::Windowed, }, graphics: GraphicsSettings { - samples: Samples(16), + samples: Samples(8), vsync: true, }, } @@ -18,6 +20,7 @@ pub fn default() -> GameSettings { /// Complete tree of basic game client settings pub struct GameSettings { + pub assets: Option, pub window: WindowSettings, pub graphics: GraphicsSettings, } @@ -45,7 +48,7 @@ impl<'s> From<&'s Samples> for NumSamples { 2 => Self::Two, 4 => Self::Four, 8 => Self::Eight, - 16 => Self::Sixteen, + // 16 => Self::Sixteen, // currently broken _ => panic!("Invalid multisampling value: {}", s.0), } } diff --git a/games/rstnode/rst-client/src/state.rs b/games/rstnode/rst-client/src/state.rs index c55dc2faa6d..6b7312f13d7 100644 --- a/games/rstnode/rst-client/src/state.rs +++ b/games/rstnode/rst-client/src/state.rs @@ -1,7 +1,11 @@ //! Game client state handling use crate::{ - graphics::entities::{Coordinates, NodeRndr}, + assets::Assets, + graphics::{ + entities::{Coordinates, NodeRndr}, + Renderer, + }, GameSettings, }; use ggez::{event::EventHandler, graphics, Context, GameResult}; @@ -9,12 +13,18 @@ use rst_core::data::{Node, Owner, Upgrade}; use std::sync::Arc; pub struct ClientState { + assets: Assets, + settings: GameSettings, + + // Game state node: NodeRndr, } impl ClientState { - pub fn new(_settings: &GameSettings) -> Self { + pub fn new(settings: GameSettings, assets: Assets) -> Self { Self { + assets, + settings, node: NodeRndr { loc: Coordinates(250.0, 250.0), inner: Arc::new(Node { @@ -30,6 +40,10 @@ impl ClientState { }, } } + + pub fn assets(&self) -> &Assets { + &self.assets + } } impl EventHandler for ClientState { @@ -41,7 +55,7 @@ impl EventHandler for ClientState { graphics::clear(ctx, graphics::Color::from_rgb(15, 15, 15)); // Render the node - self.node.draw(ctx).unwrap(); + self.node.draw(&self, ctx).unwrap(); graphics::present(ctx) } diff --git a/games/rstnode/rst-client/src/window.rs b/games/rstnode/rst-client/src/window.rs index ad58c38e0ff..3dcf375a900 100644 --- a/games/rstnode/rst-client/src/window.rs +++ b/games/rstnode/rst-client/src/window.rs @@ -1,10 +1,28 @@ //! Basic window setup code use crate::{ctx, state::ClientState, GameSettings}; -use ggez::event; +use ggez::{ + event::{self, EventLoop}, + Context, +}; + +pub struct Window { + ctx: Context, + eloop: EventLoop<()>, +} + +impl Window { + pub fn ctx(&mut self) -> &mut Context { + &mut self.ctx + } + + pub fn run(self, state: ClientState) -> ! { + event::run(self.ctx, self.eloop, state) + } +} /// Start the main event loop with game settings and state -pub fn run(settings: &GameSettings, state: ClientState) -> ! { +pub fn create(settings: &GameSettings) -> Window { let (ctx, eloop) = ctx::build(settings).build().unwrap(); - event::run(ctx, eloop, state) + Window { ctx, eloop } }