rstnode: basic asset loading and prototype sprite rendering

* restructure assets directory
* implement asset loading and dynamic conversion to sprites
* reload sprites with unique URIs to load at runtime
* provide an updated renderer API to give access to client state
* use new APIs to draw a single node frame on screen
* use colour APIs to dynamically change node frame colour
wip/yesman
Katharina Fey 3 years ago
parent f5b36ff6b9
commit 79c8b9ab44
Signed by: kookie
GPG Key ID: 90734A9E619C8A6C
  1. 92
      games/rstnode/Cargo.lock
  2. BIN
      games/rstnode/assets/raw/frame/frame_l.png
  3. 0
      games/rstnode/assets/raw/frame/frame_l.svg
  4. BIN
      games/rstnode/assets/raw/frame/frame_m.png
  5. 0
      games/rstnode/assets/raw/frame/frame_m.svg
  6. BIN
      games/rstnode/assets/raw/frame/frame_s.png
  7. 0
      games/rstnode/assets/raw/frame/frame_s.svg
  8. BIN
      games/rstnode/assets/raw/frame/frame_xl.png
  9. 0
      games/rstnode/assets/raw/frame/frame_xl.svg
  10. BIN
      games/rstnode/assets/raw/relay/relay1.png
  11. 0
      games/rstnode/assets/raw/relay/relay1.svg
  12. 11
      games/rstnode/rst-client/Cargo.toml
  13. 138
      games/rstnode/rst-client/src/assets.rs
  14. 56
      games/rstnode/rst-client/src/cli.rs
  15. 24
      games/rstnode/rst-client/src/error.rs
  16. 18
      games/rstnode/rst-client/src/graphics/entities/mod.rs
  17. 18
      games/rstnode/rst-client/src/graphics/mod.rs
  18. 43
      games/rstnode/rst-client/src/log.rs
  19. 25
      games/rstnode/rst-client/src/main.rs
  20. 7
      games/rstnode/rst-client/src/settings.rs
  21. 20
      games/rstnode/rst-client/src/state.rs
  22. 24
      games/rstnode/rst-client/src/window.rs

@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

@ -9,8 +9,13 @@ authors = ["Bread Machine", "Katharina Fey <kookie@spacekookie.de>"]
[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"] }
svg = "0.9"
clap = "2.0"
tracing = "0.1"
tracing-subscriber = "0.2"
tempfile = "*"

@ -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<T> = std::result::Result<T, LoadError>;
/// Construct a `node` prefixed URI
pub fn node(tt: &str) -> URI {
@ -24,21 +38,137 @@ impl From<String> for URI {
}
/// Asset loader
#[derive(Debug)]
pub struct Assets {
inner: BTreeMap<URI, Image>,
}
impl Assets {
pub fn load(p: &Path) -> Self {
fn new() -> Self {
Self {
inner: Default::default(),
}
}
pub fn find<U: Into<URI>>(&self, u: U) -> Option<Image> {
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<Assets> {
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<PathBuf> {
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())
}

@ -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 <kookie@spacekookie.de)")
.about("Main game client - consider running the game via the launcher instead")
.arg(
Arg::with_name("assets")
.required(true)
.takes_value(true)
.help("Specify the path to load assets from"),
)
.arg(
Arg::with_name("width")
.short("w")
.takes_value(true)
.help("Set the desired game window width"),
)
.arg(
Arg::with_name("height")
.short("h")
.takes_value(true)
.help("Set the desired game window height"),
)
.arg(
Arg::with_name("fullscreen")
.short("f")
.help("Specify if the game should run full screen"),
);
let matches = app.get_matches();
if let Some(assets) = matches.value_of("assets") {
settings.assets = Some(PathBuf::new().join(assets));
}
if let Some(width) = matches.value_of("width") {
settings.window.width = str::parse(width).unwrap();
}
if let Some(height) = matches.value_of("height") {
settings.window.height = str::parse(height).unwrap();
}
if matches.is_present("fullscreen") {
settings.window.window_mode = WindowMode::Fullscreen;
}
}

@ -0,0 +1,24 @@
//! Various errors that can occur
use std::{
error::Error,
fmt::{self, Display, Formatter},
};
/// Error loading an asset
#[derive(Clone, Debug)]
pub struct LoadError(String);
impl<'s> 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 {}

@ -22,20 +22,28 @@ pub struct NodeRndr {
pub inner: Arc<Node>,
}
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(())

@ -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<()>;
}

@ -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)
})
}

@ -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)
}

@ -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<PathBuf>,
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),
}
}

@ -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)
}

@ -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 }
}

Loading…
Cancel
Save