move roadmap to a GH issue

This commit is contained in:
Alula 2021-02-10 14:02:13 +01:00
commit 174910b1a3
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
85 changed files with 3710 additions and 945 deletions

View File

@ -25,10 +25,10 @@ install:
- cargo -vV
cache:
- '%USERPROFILE%\.cache\sccache -> .appveyor.yml'
- '%USERPROFILE%\.cargo -> .appveyor.yml'
- '%USERPROFILE%\.rustup -> .appveyor.yml'
- 'target -> .appveyor.yml'
- '%USERPROFILE%\.cache\sccache -> Cargo.toml'
- '%USERPROFILE%\.cargo -> Cargo.toml'
- '%USERPROFILE%\.rustup -> Cargo.toml'
- 'target -> Cargo.toml'
#test_script:
# - cargo build --verbose --all

View File

@ -4,8 +4,8 @@ edition = "2018"
name = "doukutsu-rs"
version = "0.1.0"
[lib]
crate-type = ["lib", "cdylib"]
#[lib]
#crate-type = ["lib", "cdylib"]
[package.metadata.android]
android_version = 29
@ -18,12 +18,12 @@ opengles_version = [3, 1]
fullscreen = true
orientation = "sensorLandscape"
permission = [
{name = "android.permission.READ_EXTERNAL_STORAGE"},
{name = "android.permission.WRITE_EXTERNAL_STORAGE"}
{ name = "android.permission.READ_EXTERNAL_STORAGE" },
{ name = "android.permission.WRITE_EXTERNAL_STORAGE" }
]
application_metadatas = [
{name = "android:hardwareAccelerated", value = "true"},
{name = "android:requestLegacyExternalStorage", value = "true"}
{ name = "android:hardwareAccelerated", value = "true" },
{ name = "android:requestLegacyExternalStorage", value = "true" }
]
[profile.release]
@ -40,42 +40,34 @@ opt-level = 1
opt-level = 1
[features]
default = ["scripting"]
default = ["scripting", "backend-sdl"]
backend-sdl = ["sdl2"]
backend-gfx = ["winit", "imgui-gfx-renderer", "imgui-winit-support"]
scripting = ["lua-ffi"]
editor = []
[dependencies]
#cpal = {path = "./3rdparty/cpal"}
#gfx_device_gl = {path = "./3rdparty/gfx/src/backend/gl"}
#ggez = {path = "./3rdparty/ggez"}
#glutin = {path = "./3rdparty/glutin/glutin"}
#lua-ffi = {path = "./3rdparty/luajit-rs", optional = true}
bitvec = "0.17.4"
byteorder = "1.3"
case_insensitive_hashmap = "1.0.0"
chrono = "0.4"
cpal = {git = "https://github.com/doukutsu-rs/cpal.git", branch = "android-support"}
directories = "2"
gfx = "0.18"
gfx_core = "0.9"
gfx_device_gl = {git = "https://github.com/doukutsu-rs/gfx.git", branch = "pre-ll"}
ggez = {git = "https://github.com/doukutsu-rs/ggez.git", rev = "43631b0401271d4bc8fe4a5afba8aad63976dba1"}
glutin = {git = "https://github.com/doukutsu-rs/glutin.git", branch = "master"}
imgui = {git = "https://github.com/Gekkio/imgui-rs.git", rev = "7e2293bde67f869750ab0e649fbfbd842fb0c785"}
imgui-gfx-renderer = {git = "https://github.com/Gekkio/imgui-rs.git", rev = "7e2293bde67f869750ab0e649fbfbd842fb0c785"}
imgui-winit-support = {git = "https://github.com/Gekkio/imgui-rs.git", default-features = false, features = ["winit-23"], rev = "7e2293bde67f869750ab0e649fbfbd842fb0c785"}
image = {version = "0.22", default-features = false, features = ["png_codec", "pnm", "bmp"]}
cpal = { git = "https://github.com/doukutsu-rs/cpal.git", branch = "android-support" }
directories = "3"
imgui = "0.7.0"
imgui-gfx-renderer = { version = "0.7.0", optional = true }
imgui-winit-support = { version = "0.7.0", default-features = false, features = ["winit-23"], optional = true }
image = { version = "0.22", default-features = false, features = ["png_codec", "pnm", "bmp"] }
itertools = "0.9.0"
lazy_static = "1.4.0"
log = "0.4"
lua-ffi = {git = "https://github.com/doukutsu-rs/lua-ffi.git", rev = "1ef3caf772d72068297ddf75df06fd2ef8c1daab", optional = true}
lua-ffi = { git = "https://github.com/doukutsu-rs/lua-ffi.git", rev = "1ef3caf772d72068297ddf75df06fd2ef8c1daab", optional = true }
lru = "0.6.0"
num-derive = "0.3.2"
num-traits = "0.2.12"
paste = "1.0.0"
pretty_env_logger = "0.4.0"
serde = {version = "1", features = ["derive"]}
sdl2 = { version = "0.34", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
serde = { version = "1", features = ["derive"] }
serde_derive = "1"
serde_yaml = "0.8"
strum = "0.18.0"
@ -83,7 +75,10 @@ strum_macros = "0.18.0"
# remove and replace when drain_filter is in stable
vec_mut_scan = "0.3.0"
webbrowser = "0.5.5"
winit = {version = "0.24.0", features = ["serde"]}
winit = { version = "0.24.0", features = ["serde"], optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["winuser"] }
[target.'cfg(target_os = "android")'.dependencies]
ndk = "0.2"

View File

@ -3,8 +3,9 @@ use std::io;
use byteorder::{LE, ReadBytesExt};
use ggez::GameError::ResourceLoadError;
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::GameResult;
use crate::str;
#[derive(Debug)]

View File

@ -4,10 +4,13 @@ use std::path::PathBuf;
use crate::bmfont::BMFont;
use crate::common::{FILE_TYPES, Rect};
use crate::engine_constants::EngineConstants;
use ggez::{Context, filesystem, GameResult};
use ggez::GameError::ResourceLoadError;
use crate::str;
use crate::texture_set::TextureSet;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
pub struct BMFontRenderer {
font: BMFont,
@ -25,7 +28,6 @@ impl BMFontRenderer {
let font = BMFont::load_from(filesystem::open(ctx, &full_path)?)?;
let mut pages = Vec::new();
println!("stem: {:?}", stem);
let (zeros, _, _) = FILE_TYPES
.iter()
.map(|ext| (1, ext, format!("{}_0{}", stem.to_string_lossy(), ext)))
@ -38,7 +40,6 @@ impl BMFontRenderer {
for i in 0..font.pages {
let page_path = format!("{}_{:02$}", stem.to_string_lossy(), i, zeros);
println!("x: {}", &page_path);
pages.push(page_path);
}

View File

@ -5,9 +5,9 @@ use std::io::ErrorKind;
use std::io::SeekFrom;
use std::path::{Component, Path, PathBuf};
use ggez::GameResult;
use ggez::GameError::FilesystemError;
use ggez::vfs::{OpenOptions, VFile, VFS, VMetadata};
use crate::framework::error::GameError::FilesystemError;
use crate::framework::error::GameResult;
use crate::framework::vfs::{OpenOptions, VFile, VMetadata, VFS};
#[derive(Debug)]
pub struct BuiltinFile(Cursor<&'static [u8]>);
@ -59,7 +59,7 @@ impl VMetadata for BuiltinMetadata {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
enum FSNode {
File(&'static str, &'static [u8]),
Directory(&'static str, Vec<FSNode>),
@ -232,7 +232,7 @@ impl VFS for BuiltinFS {
}
}
/*#[test]
#[test]
fn test_builtin_fs() {
let fs = BuiltinFS {
root: vec![
@ -240,7 +240,7 @@ fn test_builtin_fs() {
FSNode::Directory("memes", vec![
FSNode::File("nothing.txt", &[]),
FSNode::Directory("secret stuff", vec![
FSNode::File("passwords.txt", &[b'1', b'2', b'3', b'4', b'5', b'6',]),
FSNode::File("passwords.txt", &b"12345678"),
]),
]),
FSNode::File("test2.txt", &[]),
@ -252,4 +252,4 @@ fn test_builtin_fs() {
println!("{:?}", fs.get_node(Path::new("/memes")).unwrap());
println!("{:?}", fs.get_node(Path::new("/memes/nothing.txt")).unwrap());
println!("{:?}", fs.get_node(Path::new("/memes/secret stuff/passwords.txt")).unwrap());
}*/
}

View File

@ -234,6 +234,22 @@ impl Direction {
}
}
#[derive(Debug, Clone, Copy)]
pub struct Point<T: Num + PartialOrd + Copy = isize> {
pub x: T,
pub y: T,
}
impl<T: Num + PartialOrd + Copy> Point<T> {
#[inline(always)]
pub fn new(x: T, y: T) -> Point<T> {
Point {
x,
y,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Rect<T: Num + PartialOrd + Copy = isize> {
pub left: T,
@ -243,6 +259,7 @@ pub struct Rect<T: Num + PartialOrd + Copy = isize> {
}
impl<T: Num + PartialOrd + Copy> Rect<T> {
#[inline(always)]
pub fn new(left: T, top: T, right: T, bottom: T) -> Rect<T> {
Rect {
left,
@ -252,6 +269,7 @@ impl<T: Num + PartialOrd + Copy> Rect<T> {
}
}
#[inline(always)]
pub fn new_size(x: T, y: T, width: T, height: T) -> Rect<T> {
Rect {
left: x,
@ -261,15 +279,6 @@ impl<T: Num + PartialOrd + Copy> Rect<T> {
}
}
pub fn from(rect: ggez::graphics::Rect) -> Rect<f32> {
Rect {
left: rect.x,
top: rect.y,
right: (rect.x + rect.w),
bottom: (rect.y + rect.h),
}
}
pub fn width(&self) -> T {
if let Some(Ordering::Greater) = self.left.partial_cmp(&self.right) {
self.left.sub(self.right)
@ -287,15 +296,6 @@ impl<T: Num + PartialOrd + Copy> Rect<T> {
}
}
impl<T: Num + PartialOrd + Copy + AsPrimitive<f32>> Into<ggez::graphics::Rect> for Rect<T> {
fn into(self) -> ggez::graphics::Rect {
ggez::graphics::Rect::new(self.left.as_(),
self.top.as_(),
self.width().as_(),
self.height().as_())
}
}
impl<T: Num + PartialOrd + Copy + Serialize> Serialize for Rect<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@ -371,3 +371,151 @@ pub fn interpolate_fix9_scale(old_val: i32, val: i32, frame_delta: f64) -> f32 {
(lerp_f64(old_val as f64, val as f64, frame_delta) / 512.0) as f32
}
/// A RGBA color in the `sRGB` color space represented as `f32`'s in the range `[0.0-1.0]`
///
/// For convenience, [`WHITE`](constant.WHITE.html) and [`BLACK`](constant.BLACK.html) are provided.
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Color {
/// Red component
pub r: f32,
/// Green component
pub g: f32,
/// Blue component
pub b: f32,
/// Alpha component
pub a: f32,
}
impl Color {
/// Create a new `Color` from four `f32`'s in the range `[0.0-1.0]`
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Color { r, g, b, a }
}
/// Create a new `Color` from four `u8`'s in the range `[0-255]`
pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
Color::from((r, g, b, a))
}
/// Create a new `Color` from three u8's in the range `[0-255]`,
/// with the alpha component fixed to 255 (opaque)
pub fn from_rgb(r: u8, g: u8, b: u8) -> Color {
Color::from((r, g, b))
}
/// Return a tuple of four `u8`'s in the range `[0-255]` with the `Color`'s
/// components.
pub fn to_rgba(self) -> (u8, u8, u8, u8) {
self.into()
}
/// Return a tuple of three `u8`'s in the range `[0-255]` with the `Color`'s
/// components.
pub fn to_rgb(self) -> (u8, u8, u8) {
self.into()
}
/// Convert a packed `u32` containing `0xRRGGBBAA` into a `Color`
pub fn from_rgba_u32(c: u32) -> Color {
let c = c.to_be_bytes();
Color::from((c[0], c[1], c[2], c[3]))
}
/// Convert a packed `u32` containing `0x00RRGGBB` into a `Color`.
/// This lets you do things like `Color::from_rgb_u32(0xCD09AA)` easily if you want.
pub fn from_rgb_u32(c: u32) -> Color {
let c = c.to_be_bytes();
Color::from((c[1], c[2], c[3]))
}
/// Convert a `Color` into a packed `u32`, containing `0xRRGGBBAA` as bytes.
pub fn to_rgba_u32(self) -> u32 {
let (r, g, b, a): (u8, u8, u8, u8) = self.into();
u32::from_be_bytes([r, g, b, a])
}
/// Convert a `Color` into a packed `u32`, containing `0x00RRGGBB` as bytes.
pub fn to_rgb_u32(self) -> u32 {
let (r, g, b, _a): (u8, u8, u8, u8) = self.into();
u32::from_be_bytes([0, r, g, b])
}
}
impl From<(u8, u8, u8, u8)> for Color {
/// Convert a `(R, G, B, A)` tuple of `u8`'s in the range `[0-255]` into a `Color`
fn from(val: (u8, u8, u8, u8)) -> Self {
let (r, g, b, a) = val;
let rf = (f32::from(r)) / 255.0;
let gf = (f32::from(g)) / 255.0;
let bf = (f32::from(b)) / 255.0;
let af = (f32::from(a)) / 255.0;
Color::new(rf, gf, bf, af)
}
}
impl From<(u8, u8, u8)> for Color {
/// Convert a `(R, G, B)` tuple of `u8`'s in the range `[0-255]` into a `Color`,
/// with a value of 255 for the alpha element (i.e., no transparency.)
fn from(val: (u8, u8, u8)) -> Self {
let (r, g, b) = val;
Color::from((r, g, b, 255))
}
}
impl From<[f32; 4]> for Color {
/// Turns an `[R, G, B, A] array of `f32`'s into a `Color` with no format changes.
/// All inputs should be in the range `[0.0-1.0]`.
fn from(val: [f32; 4]) -> Self {
Color::new(val[0], val[1], val[2], val[3])
}
}
impl From<(f32, f32, f32)> for Color {
/// Convert a `(R, G, B)` tuple of `f32`'s in the range `[0.0-1.0]` into a `Color`,
/// with a value of 1.0 to for the alpha element (ie, no transparency.)
fn from(val: (f32, f32, f32)) -> Self {
let (r, g, b) = val;
Color::new(r, g, b, 1.0)
}
}
impl From<(f32, f32, f32, f32)> for Color {
/// Convert a `(R, G, B, A)` tuple of `f32`'s in the range `[0.0-1.0]` into a `Color`
fn from(val: (f32, f32, f32, f32)) -> Self {
let (r, g, b, a) = val;
Color::new(r, g, b, a)
}
}
impl From<Color> for (u8, u8, u8, u8) {
/// Convert a `Color` into a `(R, G, B, A)` tuple of `u8`'s in the range of `[0-255]`.
fn from(color: Color) -> Self {
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
let a = (color.a * 255.0) as u8;
(r, g, b, a)
}
}
impl From<Color> for (u8, u8, u8) {
/// Convert a `Color` into a `(R, G, B)` tuple of `u8`'s in the range of `[0-255]`,
/// ignoring the alpha term.
fn from(color: Color) -> Self {
let (r, g, b, _) = color.into();
(r, g, b)
}
}
impl From<Color> for [f32; 4] {
/// Convert a `Color` into an `[R, G, B, A]` array of `f32`'s in the range of `[0.0-1.0]`.
fn from(color: Color) -> Self {
[color.r, color.g, color.b, color.a]
}
}

View File

@ -1,8 +1,8 @@
use ggez::{Context, GameResult};
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::boss::BossNPC;
use crate::npc::list::NPCList;
use crate::shared_game_state::SharedGameState;
@ -52,7 +52,7 @@ impl BossLifeBar {
}
impl GameEntity<(&NPCList, &BossNPC)> for BossLifeBar {
fn tick(&mut self, _state: &mut SharedGameState, (npc_list, boss): (&NPCList, &BossNPC)) -> GameResult<> {
fn tick(&mut self, _state: &mut SharedGameState, (npc_list, boss): (&NPCList, &BossNPC)) -> GameResult<()> {
match self.target {
BossLifeTarget::NPC(npc_id) => {
if let Some(npc) = npc_list.get_npc(npc_id as usize) {

View File

@ -1,7 +1,7 @@
use ggez::{Context, GameResult};
use crate::common::Rect;
use crate::shared_game_state::SharedGameState;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
#[derive(Debug, EnumIter, PartialEq, Eq, Hash, Copy, Clone)]
pub enum Alignment {

View File

@ -1,4 +1,5 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::components::draw_common::{Alignment, draw_number};

View File

@ -1,4 +1,5 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::entity::GameEntity;

View File

@ -216,7 +216,7 @@ pub struct EngineConstants {
pub world: WorldConsts,
pub npc: NPCConsts,
pub weapon: WeaponConsts,
pub tex_sizes: CaseInsensitiveHashMap<(usize, usize)>,
pub tex_sizes: CaseInsensitiveHashMap<(u16, u16)>,
pub textscript: TextScriptConsts,
pub title: TitleConsts,
pub font_path: String,

View File

@ -1,4 +1,5 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::frame::Frame;
use crate::shared_game_state::SharedGameState;

57
src/framework/backend.rs Normal file
View File

@ -0,0 +1,57 @@
use imgui::DrawData;
use crate::common::{Color, Point, Rect};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics::BlendMode;
use crate::Game;
pub trait Backend {
fn create_event_loop(&self) -> GameResult<Box<dyn BackendEventLoop>>;
}
pub trait BackendEventLoop {
fn run(&mut self, game: &mut Game, ctx: &mut Context);
fn new_renderer(&self) -> GameResult<Box<dyn BackendRenderer>>;
}
pub trait BackendRenderer {
fn clear(&mut self, color: Color);
fn present(&mut self) -> GameResult;
fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>>;
fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>>;
fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult;
fn set_render_target(&mut self, texture: Option<&Box<dyn BackendTexture>>) -> GameResult;
fn imgui(&self) -> GameResult<&mut imgui::Context>;
fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult;
fn prepare_frame(&self, ui: &imgui::Ui) -> GameResult;
}
pub trait BackendTexture {
fn dimensions(&self) -> (u16, u16);
fn add(&mut self, command: SpriteBatchCommand);
fn clear(&mut self);
fn draw(&mut self) -> GameResult;
}
pub fn init_backend() -> GameResult<Box<dyn Backend>> {
crate::framework::backend_sdl2::SDL2Backend::new()
}
pub enum SpriteBatchCommand {
DrawRect(Rect<f32>, Rect<f32>),
DrawRectFlip(Rect<f32>, Rect<f32>, bool, bool),
DrawRectTinted(Rect<f32>, Rect<f32>, Color),
}

File diff suppressed because it is too large Load Diff

33
src/framework/context.rs Normal file
View File

@ -0,0 +1,33 @@
use crate::framework::backend::{init_backend, BackendRenderer};
use crate::framework::error::GameResult;
use crate::framework::filesystem::Filesystem;
use crate::Game;
use crate::framework::keyboard::KeyboardContext;
pub struct Context {
pub(crate) filesystem: Filesystem,
pub(crate) renderer: Option<Box<dyn BackendRenderer>>,
pub(crate) keyboard_context: KeyboardContext,
pub(crate) screen_size: (f32, f32),
}
impl Context {
pub fn new() -> Context {
Context {
filesystem: Filesystem::new(),
renderer: None,
keyboard_context: KeyboardContext::new(),
screen_size: (320.0, 240.0),
}
}
pub fn run(&mut self, game: &mut Game) -> GameResult {
let backend = init_backend()?;
let mut event_loop = backend.create_event_loop()?;
self.renderer = Some(event_loop.new_renderer()?);
event_loop.run(game, self);
Ok(())
}
}

143
src/framework/error.rs Normal file
View File

@ -0,0 +1,143 @@
//! Error types and conversion functions.
use std::error::Error;
use std::fmt;
use std::string::FromUtf8Error;
use std::sync::{Arc, PoisonError};
use std::sync::mpsc::SendError;
/// An enum containing all kinds of game framework errors.
#[derive(Debug, Clone)]
pub enum GameError {
/// An error in the filesystem layout
FilesystemError(String),
/// An error in the config file
ConfigError(String),
/// Happens when an `winit::EventsLoopProxy` attempts to
/// wake up an `winit::EventsLoop` that no longer exists.
EventLoopError(String),
/// An error trying to load a resource, such as getting an invalid image file.
ResourceLoadError(String),
/// Unable to find a resource; the `Vec` is the paths it searched for and associated errors
ResourceNotFound(String, Vec<(std::path::PathBuf, GameError)>),
/// Something went wrong in the renderer
RenderError(String),
/// Something went wrong in the audio playback
AudioError(String),
/// Something went wrong trying to set or get window properties.
WindowError(String),
/// Something went wrong trying to read from a file
IOError(Arc<std::io::Error>),
/// Something went wrong trying to load/render a font
FontError(String),
/// Something went wrong applying video settings.
VideoError(String),
/// Something went wrong compiling shaders
ShaderProgramError(String),
/// Something went wrong with the `gilrs` gamepad-input library.
GamepadError(String),
/// Something went wrong with the `lyon` shape-tesselation library.
LyonError(String),
/// Something went wrong while parsing something.
ParseError(String),
/// Something went wrong while converting a value.
InvalidValue(String),
}
impl fmt::Display for GameError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GameError::ConfigError(ref s) => write!(f, "Config error: {}", s),
GameError::ResourceLoadError(ref s) => write!(f, "Error loading resource: {}", s),
GameError::ResourceNotFound(ref s, ref paths) => write!(
f,
"Resource not found: {}, searched in paths {:?}",
s, paths
),
GameError::WindowError(ref e) => write!(f, "Window creation error: {}", e),
_ => write!(f, "GameError {:?}", self),
}
}
}
impl Error for GameError {
fn cause(&self) -> Option<&dyn Error> {
match *self {
GameError::IOError(ref e) => Some(&**e),
_ => None,
}
}
}
/// A convenient result type consisting of a return type and a `GameError`
pub type GameResult<T = ()> = Result<T, GameError>;
impl From<std::io::Error> for GameError {
fn from(e: std::io::Error) -> GameError {
GameError::IOError(Arc::new(e))
}
}
impl From<image::ImageError> for GameError {
fn from(e: image::ImageError) -> GameError {
let errstr = format!("Image load error: {}", e);
GameError::ResourceLoadError(errstr)
}
}
impl From<std::string::FromUtf8Error> for GameError {
fn from(e: FromUtf8Error) -> Self {
let errstr = format!("UTF-8 decoding error: {:?}", e);
GameError::ConfigError(errstr)
}
}
#[cfg(target_os = "android")]
impl From<jni::errors::Error> for GameError {
fn from(e: jni::errors::Error) -> GameError {
GameError::WindowError(e.to_string())
}
}
impl From<strum::ParseError> for GameError {
fn from(s: strum::ParseError) -> GameError {
let errstr = format!("Strum parse error: {}", s);
GameError::ParseError(errstr)
}
}
impl From<cpal::DefaultStreamConfigError> for GameError {
fn from(s: cpal::DefaultStreamConfigError) -> GameError {
let errstr = format!("Default stream config error: {}", s);
GameError::AudioError(errstr)
}
}
impl From<cpal::PlayStreamError> for GameError {
fn from(s: cpal::PlayStreamError) -> GameError {
let errstr = format!("Play stream error: {}", s);
GameError::AudioError(errstr)
}
}
impl From<cpal::BuildStreamError> for GameError {
fn from(s: cpal::BuildStreamError) -> GameError {
let errstr = format!("Build stream error: {}", s);
GameError::AudioError(errstr)
}
}
impl<T> From<PoisonError<T>> for GameError {
fn from(s: PoisonError<T>) -> GameError {
let errstr = format!("Poison error: {}", s);
GameError::EventLoopError(errstr)
}
}
impl<T> From<SendError<T>> for GameError {
fn from(s: SendError<T>) -> GameError {
let errstr = format!("Send error: {}", s);
GameError::EventLoopError(errstr)
}
}

367
src/framework/filesystem.rs Normal file
View File

@ -0,0 +1,367 @@
use std::env;
use std::fmt;
use std::io;
use std::io::SeekFrom;
use std::path;
use std::path::PathBuf;
use directories::ProjectDirs;
use crate::framework::vfs;
use crate::framework::vfs::{VFS, OpenOptions};
use crate::framework::error::{GameResult, GameError};
use crate::framework::context::Context;
/// A structure that contains the filesystem state and cache.
#[derive(Debug)]
pub struct Filesystem {
vfs: vfs::OverlayFS,
user_vfs: vfs::OverlayFS,
}
/// Represents a file, either in the filesystem, or in the resources zip file,
/// or whatever.
pub enum File {
/// A wrapper for a VFile trait object.
VfsFile(Box<dyn vfs::VFile>),
}
impl fmt::Debug for File {
// Make this more useful?
// But we can't seem to get a filename out of a file,
// soooooo.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
File::VfsFile(ref _file) => write!(f, "VfsFile"),
}
}
}
impl io::Read for File {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
File::VfsFile(ref mut f) => f.read(buf),
}
}
}
impl io::Write for File {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
File::VfsFile(ref mut f) => f.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
File::VfsFile(ref mut f) => f.flush(),
}
}
}
impl io::Seek for File {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match *self {
File::VfsFile(ref mut f) => f.seek(pos),
}
}
}
impl Filesystem {
pub fn new() -> Filesystem {
// Set up VFS to merge resource path, root path, and zip path.
let overlay = vfs::OverlayFS::new();
// User data VFS.
let user_overlay = vfs::OverlayFS::new();
Filesystem {
vfs: overlay,
user_vfs: user_overlay,
}
}
/// Opens the given `path` and returns the resulting `File`
/// in read-only mode.
pub(crate) fn open<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Opens the given `path` from user directory and returns the resulting `File`
/// in read-only mode.
pub(crate) fn user_open<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.user_vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Opens a file in the user directory with the given
/// [`filesystem::OpenOptions`](struct.OpenOptions.html).
/// Note that even if you open a file read-write, it can only
/// write to files in the "user" directory.
pub(crate) fn open_options<P: AsRef<path::Path>>(
&mut self,
path: P,
options: OpenOptions,
) -> GameResult<File> {
self.user_vfs
.open_options(path.as_ref(), options)
.map(|f| File::VfsFile(f))
.map_err(|e| {
GameError::ResourceLoadError(format!(
"Tried to open {:?} but got error: {:?}",
path.as_ref(),
e
))
})
}
/// Creates a new file in the user directory and opens it
/// to be written to, truncating it if it already exists.
pub(crate) fn user_create<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.user_vfs.create(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Create an empty directory in the user dir
/// with the given name. Any parents to that directory
/// that do not exist will be created.
pub(crate) fn user_create_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.user_vfs.mkdir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub(crate) fn user_delete<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.user_vfs.rm(path.as_ref())
}
/// Deletes the specified directory in the user dir,
/// and all its contents!
pub(crate) fn user_delete_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.user_vfs.rmrf(path.as_ref())
}
/// Check whether a file or directory in the user directory exists.
pub(crate) fn user_exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.user_vfs.exists(path.as_ref())
}
/// Check whether a file or directory exists.
pub(crate) fn exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs.exists(path.as_ref())
}
/// Check whether a path points at a file.
pub(crate) fn user_is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.user_vfs
.metadata(path.as_ref())
.map(|m| m.is_file())
.unwrap_or(false)
}
/// Check whether a path points at a file.
pub(crate) fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs
.metadata(path.as_ref())
.map(|m| m.is_file())
.unwrap_or(false)
}
/// Check whether a path points at a directory.
pub(crate) fn user_is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.user_vfs
.metadata(path.as_ref())
.map(|m| m.is_dir())
.unwrap_or(false)
}
/// Check whether a path points at a directory.
pub(crate) fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs
.metadata(path.as_ref())
.map(|m| m.is_dir())
.unwrap_or(false)
}
/// Returns a list of all files and directories in the user directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
&mut self,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
let itr = self.user_vfs.read_dir(path.as_ref())?.map(|fname| {
fname.expect("Could not read file in read_dir()? Should never happen, I hope!")
});
Ok(Box::new(itr))
}
/// Returns a list of all files and directories in the resource directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub(crate) fn read_dir<P: AsRef<path::Path>>(
&mut self,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
let itr = self.vfs.read_dir(path.as_ref())?.map(|fname| {
fname.expect("Could not read file in read_dir()? Should never happen, I hope!")
});
Ok(Box::new(itr))
}
fn write_to_string(&mut self) -> String {
use std::fmt::Write;
let mut s = String::new();
for vfs in self.vfs.roots() {
write!(s, "Source {:?}", vfs).expect("Could not write to string; should never happen?");
match vfs.read_dir(path::Path::new("/")) {
Ok(files) => {
for itm in files {
write!(s, " {:?}", itm)
.expect("Could not write to string; should never happen?");
}
}
Err(e) => write!(s, " Could not read source: {:?}", e)
.expect("Could not write to string; should never happen?"),
}
}
s
}
/// Adds the given (absolute) path to the list of directories
/// it will search to look for resources.
///
/// You probably shouldn't use this in the general case, since it is
/// harder than it looks to make it bulletproof across platforms.
/// But it can be very nice for debugging and dev purposes, such as
/// by pushing `$CARGO_MANIFEST_DIR/resources` to it
pub fn mount(&mut self, path: &path::Path, readonly: bool) {
let physfs = vfs::PhysicalFS::new(path, readonly);
trace!("Mounting new path: {:?}", physfs);
self.vfs.push_back(Box::new(physfs));
}
pub fn mount_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
self.vfs.push_back(vfs);
}
pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
self.user_vfs.push_back(vfs);
}
}
/// Opens the given path and returns the resulting `File`
/// in read-only mode.
pub fn open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.open(path)
}
/// Opens the given path in the user directory and returns the resulting `File`
/// in read-only mode.
pub fn user_open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.user_open(path)
}
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
pub fn open_options<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
options: OpenOptions,
) -> GameResult<File> {
ctx.filesystem.open_options(path, options)
}
/// Creates a new file in the user directory and opens it
/// to be written to, truncating it if it already exists.
pub fn user_create<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.user_create(path)
}
/// Create an empty directory in the user dir
/// with the given name. Any parents to that directory
/// that do not exist will be created.
pub fn user_create_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.user_create_dir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub fn user_delete<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.user_delete(path.as_ref())
}
/// Deletes the specified directory in the user dir,
/// and all its contents!
pub fn user_delete_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.user_delete_dir(path.as_ref())
}
/// Check whether a file or directory exists.
pub fn user_exists<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.user_exists(path.as_ref())
}
/// Check whether a path points at a file.
pub fn user_is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.user_is_file(path)
}
/// Check whether a path points at a directory.
pub fn user_is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.user_is_dir(path)
}
/// Returns a list of all files and directories in the user directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub fn user_read_dir<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
ctx.filesystem.user_read_dir(path)
}
/// Check whether a file or directory exists.
pub fn exists<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.exists(path.as_ref())
}
/// Check whether a path points at a file.
pub fn is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.is_file(path)
}
/// Check whether a path points at a directory.
pub fn is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.is_dir(path)
}
/// Returns a list of all files and directories in the resource directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub fn read_dir<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
ctx.filesystem.read_dir(path)
}
/// Adds the given (absolute) path to the list of directories
/// it will search to look for resources.
///
/// You probably shouldn't use this in the general case, since it is
/// harder than it looks to make it bulletproof across platforms.
/// But it can be very nice for debugging and dev purposes, such as
/// by pushing `$CARGO_MANIFEST_DIR/resources` to it
pub fn mount(ctx: &mut Context, path: &path::Path, readonly: bool) {
ctx.filesystem.mount(path, readonly)
}
/// Adds a VFS to the list of resource search locations.
pub fn mount_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
ctx.filesystem.mount_vfs(vfs)
}
/// Adds a VFS to the list of user data search locations.
pub fn mount_user_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
ctx.filesystem.mount_user_vfs(vfs)
}

101
src/framework/graphics.rs Normal file
View File

@ -0,0 +1,101 @@
use crate::common::Color;
use crate::framework::context::Context;
use crate::framework::error::{GameResult, GameError};
use crate::framework::backend::BackendTexture;
pub enum FilterMode {
Nearest,
Linear,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BlendMode {
/// When combining two fragments, add their values together, saturating
/// at 1.0
Add,
/// When combining two fragments, add the value of the source times its
/// alpha channel with the value of the destination multiplied by the inverse
/// of the source alpha channel. Has the usual transparency effect: mixes the
/// two colors using a fraction of each one specified by the alpha of the source.
Alpha,
/// When combining two fragments, multiply their values together.
Multiply,
}
pub fn clear(ctx: &mut Context, color: Color) {
if let Some(renderer) = ctx.renderer.as_mut() {
renderer.clear(color)
}
}
pub fn present(ctx: &mut Context) -> GameResult {
if let Some(renderer) = ctx.renderer.as_mut() {
renderer.present()?;
}
Ok(())
}
pub fn renderer_initialized(ctx: &mut Context) -> bool {
ctx.renderer.is_some()
}
pub fn create_texture_mutable(ctx: &mut Context, width: u16, height: u16) -> GameResult<Box<dyn BackendTexture>> {
if let Some(renderer) = ctx.renderer.as_mut() {
return renderer.create_texture_mutable(width, height);
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}
pub fn create_texture(ctx: &mut Context, width: u16, height: u16, data: &[u8]) -> GameResult<Box<dyn BackendTexture>> {
if let Some(renderer) = ctx.renderer.as_mut() {
return renderer.create_texture(width, height, data);
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}
pub fn screen_size(ctx: &mut Context) -> (f32, f32) {
ctx.screen_size
}
pub fn set_render_target(ctx: &mut Context, texture: Option<&Box<dyn BackendTexture>>) -> GameResult {
if let Some(renderer) = ctx.renderer.as_mut() {
return renderer.set_render_target(texture);
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}
pub fn set_blend_mode(ctx: &mut Context, blend: BlendMode) -> GameResult {
if let Some(renderer) = ctx.renderer.as_mut() {
return renderer.set_blend_mode(blend);
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}
pub fn imgui_context(ctx: &Context) -> GameResult<&mut imgui::Context> {
if let Some(renderer) = ctx.renderer.as_ref() {
return renderer.imgui();
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}
pub fn render_imgui(ctx: &mut Context, draw_data: &imgui::DrawData) -> GameResult {
if let Some(renderer) = ctx.renderer.as_mut() {
return renderer.render_imgui(draw_data);
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}
pub fn imgui_prepare_frame(ctx: &Context, ui: &imgui::Ui) -> GameResult {
if let Some(renderer) = ctx.renderer.as_ref() {
return renderer.prepare_frame(ui);
}
Err(GameError::RenderError("Rendering backend hasn't been initialized yet.".to_string()))
}

345
src/framework/keyboard.rs Normal file
View File

@ -0,0 +1,345 @@
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::bitfield;
use crate::framework::context::Context;
#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)]
#[derive(Serialize, Deserialize)]
#[repr(u32)]
pub enum ScanCode {
/// The '1' key over the letters.
Key1,
/// The '2' key over the letters.
Key2,
/// The '3' key over the letters.
Key3,
/// The '4' key over the letters.
Key4,
/// The '5' key over the letters.
Key5,
/// The '6' key over the letters.
Key6,
/// The '7' key over the letters.
Key7,
/// The '8' key over the letters.
Key8,
/// The '9' key over the letters.
Key9,
/// The '0' key over the 'O' and 'P' keys.
Key0,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
/// The Escape key, next to F1.
Escape,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
/// Print Screen/SysRq.
Snapshot,
/// Scroll Lock.
Scroll,
/// Pause/Break key, next to Scroll lock.
Pause,
/// `Insert`, next to Backspace.
Insert,
Home,
Delete,
End,
PageDown,
PageUp,
Left,
Up,
Right,
Down,
/// The Backspace key, right over Enter.
Backspace,
/// The Enter key.
Return,
/// The space bar.
Space,
/// The "Compose" key on Linux.
Compose,
NonUsHash,
NonUsBackslash,
Caret,
Numlock,
Numpad0,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
NumpadAdd,
NumpadDivide,
NumpadDecimal,
NumpadComma,
NumpadEnter,
NumpadEquals,
NumpadMultiply,
NumpadSubtract,
AbntC1,
AbntC2,
Apostrophe,
Apps,
Asterisk,
At,
Ax,
Backslash,
Calculator,
Capital,
Capslock,
Colon,
Comma,
Convert,
Equals,
Grave,
Kana,
Kanji,
LAlt,
LBracket,
LControl,
LShift,
LWin,
Mail,
MediaSelect,
MediaStop,
Minus,
Mute,
MyComputer,
// also called "Next"
NavigateForward,
// also called "Prior"
NavigateBackward,
NextTrack,
NoConvert,
OEM102,
Period,
PlayPause,
Plus,
Power,
PrevTrack,
RAlt,
RBracket,
RControl,
RShift,
RWin,
Scrolllock,
Semicolon,
Slash,
Sleep,
Stop,
Sysrq,
Tab,
Underline,
Unlabeled,
VolumeDown,
VolumeUp,
Wake,
WebBack,
WebFavorites,
WebForward,
WebHome,
WebRefresh,
WebSearch,
WebStop,
Yen,
Copy,
Paste,
Cut,
}
bitfield! {
/// Bitflags describing the state of keyboard modifiers, such as `Control` or `Shift`.
#[derive(Debug, Copy, Clone)]
pub struct KeyMods(u8);
/// No modifiers; equivalent to `KeyMods::default()` and
/// [`KeyMods::empty()`](struct.KeyMods.html#method.empty).
/// Left or right Shift key.
pub shift, set_shift: 0;
/// Left or right Control key.
pub ctrl, set_ctrl: 1;
/// Left or right Alt key.
pub alt, set_alt: 2;
/// Left or right Win/Cmd/equivalent key.
pub win, set_win: 3;
}
#[derive(Clone, Debug)]
pub struct KeyboardContext {
active_modifiers: KeyMods,
pressed_keys_set: HashSet<ScanCode>,
last_pressed: Option<ScanCode>,
current_pressed: Option<ScanCode>,
}
impl KeyboardContext {
pub(crate) fn new() -> Self {
Self {
active_modifiers: KeyMods(0),
pressed_keys_set: HashSet::with_capacity(256),
last_pressed: None,
current_pressed: None,
}
}
pub(crate) fn set_key(&mut self, key: ScanCode, pressed: bool) {
if pressed {
let _ = self.pressed_keys_set.insert(key);
self.last_pressed = self.current_pressed;
self.current_pressed = Some(key);
} else {
let _ = self.pressed_keys_set.remove(&key);
self.current_pressed = None;
}
self.set_key_modifier(key, pressed);
}
/// Take a modifier key code and alter our state.
///
/// Double check that this edge handling is necessary;
/// winit sounds like it should do this for us,
/// see https://docs.rs/winit/0.18.0/winit/struct.KeyboardInput.html#structfield.modifiers
///
/// ...more specifically, we should refactor all this to consistant-ify events a bit and
/// make winit do more of the work.
/// But to quote Scott Pilgrim, "This is... this is... Booooooring."
fn set_key_modifier(&mut self, key: ScanCode, pressed: bool) {
if pressed {
match key {
ScanCode::LShift | ScanCode::RShift => self.active_modifiers.set_shift(true),
ScanCode::LControl | ScanCode::RControl => self.active_modifiers.set_ctrl(true),
ScanCode::LAlt | ScanCode::RAlt => self.active_modifiers.set_alt(true),
ScanCode::LWin | ScanCode::RWin => self.active_modifiers.set_win(true),
_ => (),
}
} else {
match key {
ScanCode::LShift | ScanCode::RShift => self.active_modifiers.set_shift(false),
ScanCode::LControl | ScanCode::RControl => self.active_modifiers.set_ctrl(false),
ScanCode::LAlt | ScanCode::RAlt => self.active_modifiers.set_alt(false),
ScanCode::LWin | ScanCode::RWin => self.active_modifiers.set_win(false),
_ => (),
}
}
}
pub(crate) fn set_modifiers(&mut self, keymods: KeyMods) {
self.active_modifiers = keymods;
}
pub(crate) fn is_key_pressed(&self, key: ScanCode) -> bool {
self.pressed_keys_set.contains(&key)
}
pub(crate) fn is_key_repeated(&self) -> bool {
if self.last_pressed.is_some() {
self.last_pressed == self.current_pressed
} else {
false
}
}
pub(crate) fn pressed_keys(&self) -> &HashSet<ScanCode> {
&self.pressed_keys_set
}
pub(crate) fn active_mods(&self) -> KeyMods {
self.active_modifiers
}
}
impl Default for KeyboardContext {
fn default() -> Self {
Self::new()
}
}
/// Checks if a key is currently pressed down.
pub fn is_key_pressed(ctx: &Context, key: ScanCode) -> bool {
ctx.keyboard_context.is_key_pressed(key)
}
/// Checks if the last keystroke sent by the system is repeated,
/// like when a key is held down for a period of time.
pub fn is_key_repeated(ctx: &Context) -> bool {
ctx.keyboard_context.is_key_repeated()
}
/// Returns a reference to the set of currently pressed keys.
pub fn pressed_keys(ctx: &Context) -> &HashSet<ScanCode> {
ctx.keyboard_context.pressed_keys()
}
/// Checks if keyboard modifier (or several) is active.
pub fn is_mod_active(ctx: &Context, keymods: KeyMods) -> bool {
(ctx.keyboard_context.active_mods().0 & keymods.0) != 0
}
/// Returns currently active keyboard modifiers.
pub fn active_mods(ctx: &Context) -> KeyMods {
ctx.keyboard_context.active_mods()
}

10
src/framework/mod.rs Normal file
View File

@ -0,0 +1,10 @@
pub mod backend;
pub mod backend_sdl2;
pub mod backend_null;
pub mod context;
pub mod error;
pub mod filesystem;
pub mod graphics;
pub mod keyboard;
pub mod ui;
pub mod vfs;

136
src/framework/ui.rs Normal file
View File

@ -0,0 +1,136 @@
use std::time::Instant;
use imgui::{FontConfig, FontSource};
use imgui::sys::*;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics::{imgui_context, render_imgui};
use crate::live_debugger::LiveDebugger;
use crate::scene::Scene;
use crate::shared_game_state::SharedGameState;
pub struct UI {
pub components: Components,
last_frame: Instant,
}
pub struct Components {
pub live_debugger: LiveDebugger,
}
pub fn init_imgui() -> GameResult<imgui::Context> {
let mut imgui = imgui::Context::create();
imgui.set_ini_filename(None);
imgui.fonts().add_font(&[
FontSource::DefaultFontData {
config: Some(FontConfig::default()),
},
]);
imgui.style_mut().window_padding = [4.0, 6.0];
imgui.style_mut().frame_padding = [8.0, 6.0];
imgui.style_mut().item_spacing = [8.0, 6.0];
imgui.style_mut().item_inner_spacing = [8.0, 6.0];
imgui.style_mut().indent_spacing = 20.0;
imgui.style_mut().scrollbar_size = 20.0;
imgui.style_mut().grab_min_size = 8.0;
imgui.style_mut().window_border_size = 0.0;
imgui.style_mut().child_border_size = 0.0;
imgui.style_mut().popup_border_size = 0.0;
imgui.style_mut().frame_border_size = 0.0;
imgui.style_mut().tab_border_size = 0.0;
imgui.style_mut().window_rounding = 0.0;
imgui.style_mut().child_rounding = 0.0;
imgui.style_mut().frame_rounding = 0.0;
imgui.style_mut().popup_rounding = 0.0;
imgui.style_mut().scrollbar_rounding = 0.0;
imgui.style_mut().grab_rounding = 0.0;
imgui.style_mut().tab_rounding = 0.0;
imgui.style_mut().window_title_align = [0.50, 0.50];
imgui.style_mut().window_rounding = 0.0;
let colors = &mut imgui.style_mut().colors;
colors[ImGuiCol_Text as usize] = [0.90, 0.90, 0.90, 1.00];
colors[ImGuiCol_TextDisabled as usize] = [0.50, 0.50, 0.50, 1.00];
colors[ImGuiCol_WindowBg as usize] = [0.05, 0.05, 0.05, 0.60];
colors[ImGuiCol_ChildBg as usize] = [0.05, 0.05, 0.05, 0.60];
colors[ImGuiCol_PopupBg as usize] = [0.00, 0.00, 0.00, 0.60];
colors[ImGuiCol_Border as usize] = [0.40, 0.40, 0.40, 1.00];
colors[ImGuiCol_BorderShadow as usize] = [1.00, 1.00, 1.00, 0.00];
colors[ImGuiCol_FrameBg as usize] = [0.00, 0.00, 0.00, 0.60];
colors[ImGuiCol_FrameBgHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_FrameBgActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_TitleBg as usize] = [0.06, 0.06, 0.06, 1.00];
colors[ImGuiCol_TitleBgActive as usize] = [0.00, 0.00, 0.00, 1.00];
colors[ImGuiCol_TitleBgCollapsed as usize] = [0.06, 0.06, 0.06, 0.40];
colors[ImGuiCol_MenuBarBg as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_ScrollbarBg as usize] = [0.14, 0.14, 0.14, 0.40];
colors[ImGuiCol_ScrollbarGrab as usize] = [0.31, 0.31, 0.31, 0.30];
colors[ImGuiCol_ScrollbarGrabHovered as usize] = [1.00, 1.00, 1.00, 0.30];
colors[ImGuiCol_ScrollbarGrabActive as usize] = [1.00, 1.00, 1.00, 0.50];
colors[ImGuiCol_CheckMark as usize] = [0.90, 0.90, 0.90, 1.00];
colors[ImGuiCol_SliderGrab as usize] = [0.31, 0.31, 0.31, 1.00];
colors[ImGuiCol_SliderGrabActive as usize] = [1.00, 1.00, 1.00, 0.50];
colors[ImGuiCol_Button as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_ButtonHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_ButtonActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_Header as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_HeaderHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_HeaderActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_Separator as usize] = [0.50, 0.50, 0.43, 0.50];
colors[ImGuiCol_SeparatorHovered as usize] = [0.75, 0.45, 0.10, 0.78];
colors[ImGuiCol_SeparatorActive as usize] = [0.75, 0.45, 0.10, 1.00];
colors[ImGuiCol_ResizeGrip as usize] = [0.98, 0.65, 0.26, 0.25];
colors[ImGuiCol_ResizeGripHovered as usize] = [0.98, 0.65, 0.26, 0.67];
colors[ImGuiCol_ResizeGripActive as usize] = [0.98, 0.65, 0.26, 0.95];
colors[ImGuiCol_Tab as usize] = [0.17, 0.10, 0.04, 0.94];
colors[ImGuiCol_TabHovered as usize] = [0.84, 0.37, 0.00, 0.60];
colors[ImGuiCol_TabActive as usize] = [0.67, 0.30, 0.00, 0.68];
colors[ImGuiCol_TabUnfocused as usize] = [0.06, 0.05, 0.05, 0.69];
colors[ImGuiCol_TabUnfocusedActive as usize] = [0.36, 0.17, 0.03, 0.64];
colors[ImGuiCol_PlotLines as usize] = [0.39, 0.39, 0.39, 1.00];
colors[ImGuiCol_PlotLinesHovered as usize] = [0.35, 0.92, 1.00, 1.00];
colors[ImGuiCol_PlotHistogram as usize] = [0.00, 0.20, 0.90, 1.00];
colors[ImGuiCol_PlotHistogramHovered as usize] = [0.00, 0.40, 1.00, 1.00];
colors[ImGuiCol_TextSelectedBg as usize] = [0.98, 0.65, 0.26, 0.35];
colors[ImGuiCol_DragDropTarget as usize] = [0.00, 0.00, 1.00, 0.90];
colors[ImGuiCol_NavHighlight as usize] = [0.98, 0.65, 0.26, 1.00];
colors[ImGuiCol_NavWindowingHighlight as usize] = [0.00, 0.00, 0.00, 0.70];
colors[ImGuiCol_NavWindowingDimBg as usize] = [0.20, 0.20, 0.20, 0.20];
colors[ImGuiCol_ModalWindowDimBg as usize] = [0.20, 0.20, 0.20, 0.35];
Ok(imgui)
}
impl UI {
pub fn new(ctx: &mut Context) -> GameResult<Self> {
Ok(Self {
components: Components {
live_debugger: LiveDebugger::new(),
},
last_frame: Instant::now(),
})
}
pub fn draw(&mut self, state: &mut SharedGameState, ctx: &mut Context, scene: &mut Box<dyn Scene>) -> GameResult {
let ctx2 = unsafe { &mut *(ctx as *const Context as *mut Context)};
let imgui = imgui_context(ctx)?;
let io = imgui.io_mut();
let now = Instant::now();
io.update_delta_time(now - self.last_frame);
self.last_frame = now;
let mut ui = imgui.frame();
scene.debug_overlay_draw(&mut self.components, state, ctx2, &mut ui)?;
let draw_data = ui.render();
render_imgui(ctx2, draw_data)?;
Ok(())
}
}

678
src/framework/vfs.rs Normal file
View File

@ -0,0 +1,678 @@
//! A virtual file system layer that lets us define multiple
//! "file systems" with various backing stores, then merge them
//! together.
//!
//! Basically a re-implementation of the C library `PhysFS`. The
//! `vfs` crate does something similar but has a couple design
//! decisions that make it kind of incompatible with this use case:
//! the relevant trait for it has generic methods so we can't use it
//! as a trait object, and its path abstraction is not the most
//! convenient.
use std::collections::VecDeque;
use std::fmt::{self, Debug};
use std::fs;
use std::io::{Read, Seek, Write, BufRead};
use std::path::{self, Path, PathBuf};
use crate::framework::error::{GameResult, GameError};
fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> {
path.to_str().ok_or_else(|| {
let errmessage = format!("Invalid path format for resource: {:?}", path);
GameError::FilesystemError(errmessage)
})
}
/// Virtual file
pub trait VFile: Read + Write + Seek + Debug {}
impl<T> VFile for T where T: Read + Write + Seek + Debug {}
/// Options for opening files
///
/// We need our own version of this structure because the one in
/// `std` annoyingly doesn't let you read the read/write/create/etc
/// state out of it.
#[must_use]
#[allow(missing_docs)]
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct OpenOptions {
pub read: bool,
pub write: bool,
pub create: bool,
pub append: bool,
pub truncate: bool,
}
impl OpenOptions {
/// Create a new instance
pub fn new() -> OpenOptions {
Default::default()
}
/// Open for reading
pub fn read(mut self, read: bool) -> OpenOptions {
self.read = read;
self
}
/// Open for writing
pub fn write(mut self, write: bool) -> OpenOptions {
self.write = write;
self
}
/// Create the file if it does not exist yet
pub fn create(mut self, create: bool) -> OpenOptions {
self.create = create;
self
}
/// Append at the end of the file
pub fn append(mut self, append: bool) -> OpenOptions {
self.append = append;
self
}
/// Truncate the file to 0 bytes after opening
pub fn truncate(mut self, truncate: bool) -> OpenOptions {
self.truncate = truncate;
self
}
fn to_fs_openoptions(self) -> fs::OpenOptions {
let mut opt = fs::OpenOptions::new();
let _ = opt
.read(self.read)
.write(self.write)
.create(self.create)
.append(self.append)
.truncate(self.truncate)
.create(self.create);
opt
}
}
/// Virtual filesystem
pub trait VFS: Debug {
/// Open the file at this path with the given options
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>>;
/// Open the file at this path for reading
fn open(&self, path: &Path) -> GameResult<Box<dyn VFile>> {
self.open_options(path, OpenOptions::new().read(true))
}
/// Open the file at this path for writing, truncating it if it exists already
fn create(&self, path: &Path) -> GameResult<Box<dyn VFile>> {
self.open_options(
path,
OpenOptions::new().write(true).create(true).truncate(true),
)
}
/// Open the file at this path for appending, creating it if necessary
fn append(&self, path: &Path) -> GameResult<Box<dyn VFile>> {
self.open_options(
path,
OpenOptions::new().write(true).create(true).append(true),
)
}
/// Create a directory at the location by this path
fn mkdir(&self, path: &Path) -> GameResult;
/// Remove a file or an empty directory.
fn rm(&self, path: &Path) -> GameResult;
/// Remove a file or directory and all its contents
fn rmrf(&self, path: &Path) -> GameResult;
/// Check if the file exists
fn exists(&self, path: &Path) -> bool;
/// Get the file's metadata
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>>;
/// Retrieve all file and directory entries in the given directory.
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>>;
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf>;
}
/// VFS metadata
pub trait VMetadata {
/// Returns whether or not it is a directory.
/// Note that zip files don't actually have directories, awkwardly,
/// just files with very long names.
fn is_dir(&self) -> bool;
/// Returns whether or not it is a file.
fn is_file(&self) -> bool;
/// Returns the length of the thing. If it is a directory,
/// the result of this is undefined/platform dependent.
fn len(&self) -> u64;
}
/// A VFS that points to a directory and uses it as the root of its
/// file hierarchy.
///
/// It IS allowed to have symlinks in it! They're surprisingly
/// difficult to get rid of.
#[derive(Clone)]
pub struct PhysicalFS {
root: PathBuf,
readonly: bool,
}
#[derive(Debug, Clone)]
/// Physical FS metadata
pub struct PhysicalMetadata(fs::Metadata);
impl VMetadata for PhysicalMetadata {
fn is_dir(&self) -> bool {
self.0.is_dir()
}
fn is_file(&self) -> bool {
self.0.is_file()
}
fn len(&self) -> u64 {
self.0.len()
}
}
/// This takes an absolute path and returns either a sanitized relative
/// version of it, or None if there's something bad in it.
///
/// What we want is an absolute path with no `..`'s in it, so, something
/// like "/foo" or "/foo/bar.txt". This means a path with components
/// starting with a `RootDir`, and zero or more `Normal` components.
///
/// We gotta return a new path because there's apparently no real good way
/// to turn an absolute path into a relative path with the same
/// components (other than the first), and pushing an absolute `Path`
/// onto a `PathBuf` just completely nukes its existing contents.
fn sanitize_path(path: &path::Path) -> Option<PathBuf> {
let mut c = path.components();
match c.next() {
Some(path::Component::RootDir) => (),
_ => return None,
}
fn is_normal_component(comp: path::Component) -> Option<&str> {
match comp {
path::Component::Normal(s) => s.to_str(),
_ => None,
}
}
// This could be done more cleverly but meh
let mut accm = PathBuf::new();
for component in c {
if let Some(s) = is_normal_component(component) {
accm.push(s)
} else {
return None;
}
}
Some(accm)
}
impl PhysicalFS {
/// Creates a new PhysicalFS
pub fn new(root: &Path, readonly: bool) -> Self {
PhysicalFS {
root: root.into(),
readonly,
}
}
/// Takes a given path (&str) and returns
/// a new PathBuf containing the canonical
/// absolute path you get when appending it
/// to this filesystem's root.
fn to_absolute(&self, p: &Path) -> GameResult<PathBuf> {
if let Some(safe_path) = sanitize_path(p) {
let mut root_path = self.root.clone();
root_path.push(safe_path);
Ok(root_path)
} else {
let msg = format!(
"Path {:?} is not valid: must be an absolute path with no \
references to parent directories",
p
);
Err(GameError::FilesystemError(msg))
}
}
/// Creates the PhysicalFS's root directory if necessary.
/// Idempotent.
/// This way we can not create the directory until it's
/// actually used, though it IS a tiny bit of a performance
/// malus.
fn create_root(&self) -> GameResult {
if !self.root.exists() {
fs::create_dir_all(&self.root).map_err(GameError::from)
} else {
Ok(())
}
}
}
impl Debug for PhysicalFS {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "<PhysicalFS root: {}>", self.root.display())
}
}
impl VFS for PhysicalFS {
/// Open the file at this path with the given options
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
if self.readonly
&& (open_options.write
|| open_options.create
|| open_options.append
|| open_options.truncate)
{
let msg = format!(
"Cannot alter file {:?} in root {:?}, filesystem read-only",
path, self
);
return Err(GameError::FilesystemError(msg));
}
self.create_root()?;
let p = self.to_absolute(path)?;
open_options
.to_fs_openoptions()
.open(p)
.map(|x| Box::new(x) as Box<dyn VFile>)
.map_err(GameError::from)
}
/// Create a directory at the location by this path
fn mkdir(&self, path: &Path) -> GameResult {
if self.readonly {
return Err(GameError::FilesystemError(
"Tried to make directory {} but FS is \
read-only"
.to_string(),
));
}
self.create_root()?;
let p = self.to_absolute(path)?;
//println!("Creating {:?}", p);
fs::DirBuilder::new()
.recursive(true)
.create(p)
.map_err(GameError::from)
}
/// Remove a file
fn rm(&self, path: &Path) -> GameResult {
if self.readonly {
return Err(GameError::FilesystemError(
"Tried to remove file {} but FS is read-only".to_string(),
));
}
self.create_root()?;
let p = self.to_absolute(path)?;
if p.is_dir() {
fs::remove_dir(p).map_err(GameError::from)
} else {
fs::remove_file(p).map_err(GameError::from)
}
}
/// Remove a file or directory and all its contents
fn rmrf(&self, path: &Path) -> GameResult {
if self.readonly {
return Err(GameError::FilesystemError(
"Tried to remove file/dir {} but FS is \
read-only"
.to_string(),
));
}
self.create_root()?;
let p = self.to_absolute(path)?;
if p.is_dir() {
fs::remove_dir_all(p).map_err(GameError::from)
} else {
fs::remove_file(p).map_err(GameError::from)
}
}
/// Check if the file exists
fn exists(&self, path: &Path) -> bool {
match self.to_absolute(path) {
Ok(p) => p.exists(),
_ => false,
}
}
/// Get the file's metadata
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>> {
self.create_root()?;
let p = self.to_absolute(path)?;
p.metadata()
.map(|m| Box::new(PhysicalMetadata(m)) as Box<dyn VMetadata>)
.map_err(GameError::from)
}
/// Retrieve the path entries in this path
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
self.create_root()?;
let p = self.to_absolute(path)?;
// This is inconvenient because path() returns the full absolute
// path of the bloody file, which is NOT what we want!
// But if we use file_name() to just get the name then it is ALSO not what we want!
// what we WANT is the full absolute file path, *relative to the resources dir*.
// So that we can do read_dir("/foobar/"), and for each file, open it and query
// it and such by name.
// So we build the paths ourself.
let direntry_to_path = |entry: &fs::DirEntry| -> GameResult<PathBuf> {
let fname = entry
.file_name()
.into_string()
.expect("Non-unicode char in file path? Should never happen, I hope!");
let mut pathbuf = PathBuf::from(path);
pathbuf.push(fname);
Ok(pathbuf)
};
let itr = fs::read_dir(p)?
.map(|entry| direntry_to_path(&entry?))
.collect::<Vec<_>>()
.into_iter();
Ok(Box::new(itr))
}
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf> {
Some(self.root.clone())
}
}
/// A structure that joins several VFS's together in order.
#[derive(Debug)]
pub struct OverlayFS {
roots: VecDeque<Box<dyn VFS>>,
}
impl OverlayFS {
/// Creates a new OverlayFS
pub fn new() -> Self {
Self {
roots: VecDeque::new(),
}
}
/// Adds a new VFS to the front of the list.
/// Currently unused, I suppose, but good to
/// have at least for tests.
#[allow(dead_code)]
pub fn push_front(&mut self, fs: Box<dyn VFS>) {
self.roots.push_front(fs);
}
/// Adds a new VFS to the end of the list.
pub fn push_back(&mut self, fs: Box<dyn VFS>) {
self.roots.push_back(fs);
}
/// Returns a list of registered VFS roots.
pub fn roots(&self) -> &VecDeque<Box<dyn VFS>> {
&self.roots
}
}
impl VFS for OverlayFS {
/// Open the file at this path with the given options
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
let mut tried: Vec<(PathBuf, GameError)> = vec![];
for vfs in &self.roots {
match vfs.open_options(path, open_options) {
Err(e) => {
if let Some(vfs_path) = vfs.to_path_buf() {
tried.push((vfs_path, e));
} else {
tried.push((PathBuf::from("<invalid path>"), e));
}
}
f => return f,
}
}
let errmessage = String::from(convenient_path_to_str(path)?);
Err(GameError::ResourceNotFound(errmessage, tried))
}
/// Create a directory at the location by this path
fn mkdir(&self, path: &Path) -> GameResult {
for vfs in &self.roots {
match vfs.mkdir(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not find anywhere writeable to make dir {:?}",
path
)))
}
/// Remove a file
fn rm(&self, path: &Path) -> GameResult {
for vfs in &self.roots {
match vfs.rm(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not remove file {:?}",
path
)))
}
/// Remove a file or directory and all its contents
fn rmrf(&self, path: &Path) -> GameResult {
for vfs in &self.roots {
match vfs.rmrf(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not remove file/dir {:?}",
path
)))
}
/// Check if the file exists
fn exists(&self, path: &Path) -> bool {
for vfs in &self.roots {
if vfs.exists(path) {
return true;
}
}
false
}
/// Get the file's metadata
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>> {
for vfs in &self.roots {
match vfs.metadata(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not get metadata for file/dir {:?}",
path
)))
}
/// Retrieve the path entries in this path
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
// This is tricky 'cause we have to actually merge iterators together...
// Doing it the simple and stupid way works though.
let mut v = Vec::new();
for fs in &self.roots {
if let Ok(rddir) = fs.read_dir(path) {
v.extend(rddir)
}
}
Ok(Box::new(v.into_iter()))
}
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf> {
None
}
}
#[cfg(test)]
mod tests {
use std::io::{self, BufRead};
use super::*;
#[test]
fn headless_test_path_filtering() {
// Valid pahts
let p = path::Path::new("/foo");
assert!(sanitize_path(p).is_some());
let p = path::Path::new("/foo/");
assert!(sanitize_path(p).is_some());
let p = path::Path::new("/foo/bar.txt");
assert!(sanitize_path(p).is_some());
let p = path::Path::new("/");
assert!(sanitize_path(p).is_some());
// Invalid paths
let p = path::Path::new("../foo");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("foo");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/foo/../../");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/foo/../bop");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/../bar");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("");
assert!(sanitize_path(p).is_none());
}
#[test]
fn headless_test_read() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs = PhysicalFS::new(cargo_path, true);
let f = fs.open(Path::new("/Cargo.toml")).unwrap();
let mut bf = io::BufReader::new(f);
let mut s = String::new();
let _ = bf.read_line(&mut s).unwrap();
// Trim whitespace from string 'cause it will
// potentially be different on Windows and Unix.
let trimmed_string = s.trim();
assert_eq!(trimmed_string, "[package]");
}
#[test]
fn headless_test_read_overlay() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs1 = PhysicalFS::new(cargo_path, true);
let mut f2path = PathBuf::from(cargo_path);
f2path.push("src");
let fs2 = PhysicalFS::new(&f2path, true);
let mut ofs = OverlayFS::new();
ofs.push_back(Box::new(fs1));
ofs.push_back(Box::new(fs2));
assert!(ofs.exists(Path::new("/Cargo.toml")));
assert!(ofs.exists(Path::new("/lib.rs")));
assert!(!ofs.exists(Path::new("/foobaz.rs")));
}
#[test]
fn headless_test_physical_all() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs = PhysicalFS::new(cargo_path, false);
let testdir = Path::new("/testdir");
let f1 = Path::new("/testdir/file1.txt");
// Delete testdir if it is still lying around
if fs.exists(testdir) {
fs.rmrf(testdir).unwrap();
}
assert!(!fs.exists(testdir));
// Create and delete test dir
fs.mkdir(testdir).unwrap();
assert!(fs.exists(testdir));
fs.rm(testdir).unwrap();
assert!(!fs.exists(testdir));
let test_string = "Foo!";
fs.mkdir(testdir).unwrap();
{
let mut f = fs.append(f1).unwrap();
let _ = f.write(test_string.as_bytes()).unwrap();
}
{
let mut buf = Vec::new();
let mut f = fs.open(f1).unwrap();
let _ = f.read_to_end(&mut buf).unwrap();
assert_eq!(&buf[..], test_string.as_bytes());
}
{
// Test metadata()
let m = fs.metadata(f1).unwrap();
assert!(m.is_file());
assert!(!m.is_dir());
assert_eq!(m.len(), 4);
let m = fs.metadata(testdir).unwrap();
assert!(!m.is_file());
assert!(m.is_dir());
// Not exactly sure what the "length" of a directory is, buuuuuut...
// It appears to vary based on the platform in fact.
// On my desktop, it's 18.
// On Travis's VM, it's 4096.
// On Appveyor's VM, it's 0.
// So, it's meaningless.
//assert_eq!(m.len(), 18);
}
{
// Test read_dir()
let r = fs.read_dir(testdir).unwrap();
assert_eq!(r.count(), 1);
let r = fs.read_dir(testdir).unwrap();
for f in r {
let fname = f.unwrap();
assert!(fs.exists(&fname));
}
}
{
assert!(fs.exists(f1));
fs.rm(f1).unwrap();
assert!(!fs.exists(f1));
}
fs.rmrf(testdir).unwrap();
assert!(!fs.exists(testdir));
}
// BUGGO: TODO: Make sure all functions are tested for OverlayFS and ZipFS!!
}

View File

@ -1,6 +1,7 @@
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::input::player_controller::PlayerController;
use crate::shared_game_state::SharedGameState;
use ggez::{GameResult, Context};
pub struct CombinedMenuController {
controllers: Vec<Box<dyn PlayerController>>,

View File

@ -1,5 +1,5 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::input::player_controller::PlayerController;
use crate::shared_game_state::SharedGameState;

View File

@ -1,11 +1,11 @@
use ggez::{Context, GameResult};
use ggez::input::keyboard;
use winit::event::VirtualKeyCode;
use crate::bitfield;
use crate::input::player_controller::PlayerController;
use crate::player::TargetPlayer;
use crate::shared_game_state::SharedGameState;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::keyboard;
use crate::framework::keyboard::ScanCode;
bitfield! {
#[derive(Clone, Copy)]
@ -64,8 +64,8 @@ impl PlayerController for KeyboardController {
self.state.set_skip(keyboard::is_key_pressed(ctx, keymap.skip));
self.state.set_prev_weapon(keyboard::is_key_pressed(ctx, keymap.prev_weapon));
self.state.set_next_weapon(keyboard::is_key_pressed(ctx, keymap.next_weapon));
self.state.set_enter(keyboard::is_key_pressed(ctx, VirtualKeyCode::Return));
self.state.set_escape(keyboard::is_key_pressed(ctx, VirtualKeyCode::Escape));
self.state.set_enter(keyboard::is_key_pressed(ctx, ScanCode::Return));
self.state.set_escape(keyboard::is_key_pressed(ctx, ScanCode::Escape));
Ok(())
}

View File

@ -1,4 +1,5 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;

View File

@ -1,5 +1,5 @@
use ggez::{Context, GameResult};
use winit::event::TouchPhase;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::engine_constants::EngineConstants;
@ -39,6 +39,7 @@ impl TouchControls {
}
}
/*
pub fn process_winit_event(&mut self, scale: f32, touch: winit::event::Touch) {
match touch.phase {
TouchPhase::Started | TouchPhase::Moved => {
@ -66,7 +67,7 @@ impl TouchControls {
self.clicks.retain(|p| p.id != touch.id);
}
}
}
}*/
pub fn point_in(&self, bounds: Rect) -> Option<u64> {
for point in self.points.iter() {

View File

@ -1,4 +1,6 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::bitfield;
use crate::common::Rect;

View File

@ -7,32 +7,29 @@ extern crate strum;
#[macro_use]
extern crate strum_macros;
use std::{env, mem};
use core::mem;
use std::cell::UnsafeCell;
use std::path;
use std::env;
use std::path::PathBuf;
use std::time::Instant;
use ggez::{Context, ContextBuilder, GameError, GameResult};
use ggez::conf::{Backend, WindowMode, WindowSetup};
use ggez::event::{KeyCode, KeyMods};
use ggez::filesystem::mount_vfs;
use ggez::graphics;
use ggez::graphics::{Canvas, DrawParam, window};
use ggez::graphics::glutin_ext::WindowUpdateExt;
use ggez::input::keyboard;
use ggez::mint::ColumnMatrix4;
use ggez::nalgebra::Vector2;
use directories::ProjectDirs;
use log::*;
use pretty_env_logger::env_logger::Env;
use winit::event::{ElementState, Event, KeyboardInput, WindowEvent};
use winit::event_loop::ControlFlow;
use crate::builtin_fs::BuiltinFS;
use crate::framework::backend::init_backend;
use crate::framework::context::Context;
use crate::framework::error::{GameError, GameResult};
use crate::framework::filesystem::{mount_user_vfs, mount_vfs};
use crate::framework::graphics;
use crate::framework::keyboard::ScanCode;
use crate::framework::ui::UI;
use crate::framework::vfs::PhysicalFS;
use crate::scene::loading_scene::LoadingScene;
use crate::scene::Scene;
use crate::shared_game_state::{SharedGameState, TimingMode};
use crate::texture_set::G_MAG;
use crate::ui::UI;
use crate::texture_set::{G_MAG, I_MAG};
mod bmfont;
mod bmfont_renderer;
@ -46,6 +43,7 @@ mod encoding;
mod engine_constants;
mod entity;
mod frame;
mod framework;
mod inventory;
mod input;
mod live_debugger;
@ -61,20 +59,19 @@ mod scene;
#[cfg(feature = "scripting")]
mod scripting;
mod settings;
#[cfg(feature = "backend-gfx")]
mod shaders;
mod shared_game_state;
mod stage;
mod sound;
mod text_script;
mod texture_set;
mod ui;
mod weapon;
struct Game {
pub struct Game {
scene: Option<Box<dyn Scene>>,
state: UnsafeCell<SharedGameState>,
ui: UI,
def_matrix: ColumnMatrix4<f32>,
start_time: Instant,
last_tick: u128,
next_tick: u128,
@ -86,7 +83,6 @@ impl Game {
let s = Game {
scene: None,
ui: UI::new(ctx)?,
def_matrix: DrawParam::new().to_matrix(),
state: UnsafeCell::new(SharedGameState::new(ctx)?),
start_time: Instant::now(),
last_tick: 0,
@ -157,14 +153,16 @@ impl Game {
n1 / n2
} else { 1.0 };
}
unsafe { G_MAG = if state_ref.settings.subpixel_coords { state_ref.scale } else { 1.0 } };
unsafe {
G_MAG = if state_ref.settings.subpixel_coords { state_ref.scale } else { 1.0 };
I_MAG = state_ref.scale;
}
self.loops = 0;
graphics::clear(ctx, [0.0, 0.0, 0.0, 1.0].into());
graphics::set_transform(ctx, DrawParam::new()
.scale(Vector2::new(state_ref.scale, state_ref.scale))
.to_matrix());
graphics::apply_transformations(ctx)?;
/*graphics::set_projection(ctx, DrawParam::new()
.scale(Vec2::new(state_ref.scale, state_ref.scale))
.to_matrix());*/
if let Some(scene) = self.scene.as_mut() {
scene.draw(state_ref, ctx)?;
@ -172,43 +170,13 @@ impl Game {
state_ref.touch_controls.draw(state_ref.canvas_size, &state_ref.constants, &mut state_ref.texture_set, ctx)?;
}
graphics::set_transform(ctx, self.def_matrix);
graphics::apply_transformations(ctx)?;
//graphics::set_projection(ctx, self.def_matrix);
self.ui.draw(state_ref, ctx, scene)?;
}
graphics::present(ctx)?;
Ok(())
}
fn key_down_event(&mut self, key_code: KeyCode, _key_mod: KeyMods, repeat: bool) {
if repeat { return; }
let state = unsafe { &mut *self.state.get() };
match key_code {
KeyCode::F5 => { state.settings.subpixel_coords = !state.settings.subpixel_coords }
KeyCode::F6 => { state.settings.motion_interpolation = !state.settings.motion_interpolation }
KeyCode::F7 => { state.set_speed(1.0) }
KeyCode::F8 => {
if state.settings.speed > 0.2 {
state.set_speed(state.settings.speed - 0.1);
}
}
KeyCode::F9 => {
if state.settings.speed < 3.0 {
state.set_speed(state.settings.speed + 0.1);
}
}
KeyCode::F10 => { state.settings.debug_outlines = !state.settings.debug_outlines }
KeyCode::F11 => { state.settings.god_mode = !state.settings.god_mode }
KeyCode::F12 => { state.settings.infinite_booster = !state.settings.infinite_booster }
_ => {}
}
}
fn key_up_event(&mut self, _key_code: KeyCode, _key_mod: KeyMods) {
//
}
}
#[cfg(target_os = "android")]
@ -271,68 +239,39 @@ pub fn android_main() {
init().unwrap();
}
#[cfg(target_os = "android")]
static BACKENDS: [Backend; 2] = [
Backend::OpenGLES { major: 3, minor: 0 },
Backend::OpenGLES { major: 2, minor: 0 }
];
#[cfg(not(target_os = "android"))]
static BACKENDS: [Backend; 4] = [
Backend::OpenGL { major: 3, minor: 2 },
Backend::OpenGLES { major: 3, minor: 2 },
Backend::OpenGLES { major: 3, minor: 0 },
Backend::OpenGLES { major: 2, minor: 0 }
];
fn init_ctx<P: Into<path::PathBuf> + Clone>(event_loop: &winit::event_loop::EventLoopWindowTarget<()>, resource_dir: P) -> GameResult<Context> {
for backend in BACKENDS.iter() {
let ctx = ContextBuilder::new("doukutsu-rs")
.window_setup(WindowSetup::default().title("Cave Story ~ Doukutsu Monogatari (doukutsu-rs)"))
.window_mode(WindowMode::default()
.resizable(true)
.min_dimensions(320.0, 240.0)
.dimensions(640.0, 480.0))
.add_resource_path(resource_dir.clone())
.backend(*backend)
.build(event_loop);
match ctx {
Ok(mut ctx) => {
mount_vfs(&mut ctx, Box::new(BuiltinFS::new()));
return Ok(ctx);
}
Err(err) => {
log::warn!("Failed to create backend using config {:?}: {}", backend, err);
}
}
}
Err(GameError::EventLoopError("Failed to initialize OpenGL backend. Perhaps the driver is outdated?".to_string()))
}
pub fn init() -> GameResult {
pretty_env_logger::env_logger::from_env(Env::default().default_filter_or("info"))
.filter(Some("gfx_device_gl::factory"), LevelFilter::Warn)
.init();
let resource_dir = if let Ok(data_dir) = env::var("CAVESTORY_DATA_DIR") {
path::PathBuf::from(data_dir)
} else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let mut path = path::PathBuf::from(manifest_dir);
path.push("data");
path
PathBuf::from(data_dir)
} else {
path::PathBuf::from("data")
let mut resource_dir = env::current_exe()?;
if resource_dir.file_name().is_some() {
let _ = resource_dir.pop();
}
resource_dir.push("data");
resource_dir
};
info!("Resource directory: {:?}", resource_dir);
info!("Initializing engine...");
let event_loop = winit::event_loop::EventLoop::new();
let mut context: Option<Context>;
let mut game: Option<Game> = None;
let mut context = Context::new();
mount_vfs(&mut context, Box::new(BuiltinFS::new()));
mount_vfs(&mut context, Box::new(PhysicalFS::new(&resource_dir, true)));
#[cfg(not(target_os = "android"))]
let project_dirs = match ProjectDirs::from("", "", "doukutsu-rs") {
Some(dirs) => dirs,
None => {
return Err(GameError::FilesystemError(String::from(
"No valid home directory path could be retrieved.",
)));
}
};
mount_user_vfs(&mut context, Box::new(PhysicalFS::new(project_dirs.data_local_dir(), false)));
#[cfg(target_os = "android")]
{
@ -347,151 +286,17 @@ pub fn init() -> GameResult {
}
}
context = Some(init_ctx(&event_loop, resource_dir.clone())?);
event_loop.run(move |event, target, flow| {
#[cfg(target_os = "windows")]
{
// Windows' system clock implementation isn't monotonic when the process gets switched to another core.
// Rust has mitigations for this, but apparently aren't very effective unless Instant is called very often.
let _ = Instant::now();
}
if let Some(ctx) = &mut context {
ctx.process_event(&event);
if let Some(game) = &mut game {
game.ui.handle_events(ctx, &event);
} else {
let mut new_game = Game::new(ctx).unwrap();
let state_ref = unsafe { &mut *new_game.state.get() };
state_ref.next_scene = Some(Box::new(LoadingScene::new()));
game = Some(new_game);
#[cfg(feature = "scripting")]
{
unsafe {
let game_ref = game.as_mut().unwrap();
let state_ref = game_ref.state.get();
(&mut *state_ref).lua.update_refs(game_ref.state.get(), ctx as *mut Context);
}
}
let mut game = Game::new(&mut context)?;
let state_ref = unsafe { &mut *game.state.get() };
#[cfg(feature = "scripting")]
{
unsafe {
state_ref.lua.update_refs(game.state.get(), &mut context as *mut Context);
}
}
match event {
Event::Resumed => {
#[cfg(target_os = "android")]
if context.is_none() {
context = Some(init_ctx(target, resource_dir.clone()).unwrap());
}
let _ = target;
state_ref.next_scene = Some(Box::new(LoadingScene::new()));
context.run(&mut game)?;
if let Some(game) = &mut game {
game.loops = 0;
}
}
Event::Suspended => {
#[cfg(target_os = "android")]
{
context = None;
}
if let Some(game) = &mut game {
game.loops = 0;
}
}
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::CloseRequested => {
if let Some(game) = &mut game {
let state_ref = unsafe { &mut *game.state.get() };
state_ref.shutdown();
}
*flow = ControlFlow::Exit;
}
WindowEvent::Resized(size) => {
// Minimizing a window on Windows causes this event to get called with a 0x0 size
if size.width != 0 && size.height != 0 {
if let (Some(ctx), Some(game)) = (&mut context, &mut game) {
let state_ref = unsafe { &mut *game.state.get() };
state_ref.tmp_canvas = Canvas::with_window_size(ctx).unwrap();
state_ref.game_canvas = Canvas::with_window_size(ctx).unwrap();
state_ref.lightmap_canvas = Canvas::with_window_size(ctx).unwrap();
state_ref.handle_resize(ctx).unwrap();
graphics::window(ctx).update_gfx(&mut game.ui.main_color, &mut game.ui.main_depth);
}
}
}
WindowEvent::Touch(touch) => {
if let Some(game) = &mut game {
let state_ref = unsafe { &mut *game.state.get() };
state_ref.touch_controls.process_winit_event(state_ref.scale, touch);
}
}
WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: el_state,
virtual_keycode: Some(keycode),
modifiers,
..
},
..
} => {
if let (Some(ctx), Some(game)) = (&mut context, &mut game) {
match el_state {
ElementState::Pressed => {
let repeat = keyboard::is_key_repeated(ctx);
game.key_down_event(keycode, modifiers.into(), repeat);
}
ElementState::Released => {
game.key_up_event(keycode, modifiers.into());
}
}
}
}
_ => {}
}
}
Event::RedrawRequested(win) => {
if let (Some(ctx), Some(game)) = (&mut context, &mut game) {
if win == window(ctx).window().id() {
ctx.timer_context.tick();
game.draw(ctx).unwrap();
}
}
}
Event::MainEventsCleared => {
if let (Some(ctx), Some(game)) = (&mut context, &mut game) {
game.update(ctx).unwrap();
#[cfg(target_os = "android")]
{
ctx.timer_context.tick();
game.draw(ctx).unwrap(); // redraw request is unimplemented on shitdroid
}
window(ctx).window().request_redraw();
let state_ref = unsafe { &mut *game.state.get() };
if state_ref.shutdown {
log::info!("Shutting down...");
*flow = ControlFlow::Exit;
return;
}
if state_ref.next_scene.is_some() {
mem::swap(&mut game.scene, &mut state_ref.next_scene);
state_ref.next_scene = None;
game.scene.as_mut().unwrap().init(state_ref, ctx).unwrap();
game.loops = 0;
state_ref.frame_time = 0.0;
}
}
}
_ => {}
}
})
Ok(())
}

View File

@ -1,4 +1,6 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use imgui::{CollapsingHeader, Condition, im_str, ImStr, ImString, Slider, Window};
use itertools::Itertools;

View File

@ -1,5 +1,34 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::exit;
fn main() {
doukutsu_rs::init().unwrap();
let result = doukutsu_rs::init();
#[cfg(target_os = "windows")]
unsafe {
use winapi::_core::ptr::null_mut;
use winapi::um::winuser::MessageBoxW;
use winapi::um::winuser::MB_OK;
use winapi::shared::ntdef::LPCWSTR;
use std::ffi::OsStr;
use std::os::windows::prelude::*;
if let Err(e) = result {
let title: LPCWSTR = OsStr::new("Error!")
.encode_wide().chain(Some(0)).collect::<Vec<u16>>().as_ptr();
let message: LPCWSTR = OsStr::new(format!("Whoops, doukutsu-rs crashed: {}", e).as_str())
.encode_wide().chain(Some(0)).collect::<Vec<u16>>().as_ptr();
MessageBoxW(null_mut(),
message,
title,
MB_OK);
exit(1);
}
}
if let Err(e) = result {
println!("Initialization error: {}", e);
exit(1);
}
}

View File

@ -1,10 +1,12 @@
use std::io;
use byteorder::{LE, ReadBytesExt};
use ggez::GameError::ResourceLoadError;
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::str;
use crate::framework::error::GameError::ResourceLoadError;
static SUPPORTED_PXM_VERSIONS: [u8; 1] = [0x10];
static SUPPORTED_PXE_VERSIONS: [u8; 2] = [0, 0x10];
@ -28,7 +30,6 @@ impl Map {
let version = map_data.read_u8()?;
// It's something Booster's Lab supports but I haven't seen anywhere being used in practice
if !SUPPORTED_PXM_VERSIONS.contains(&version) {
return Err(ResourceLoadError(format!("Unsupported PXM version: {:#x}", version)));
}

View File

@ -1,12 +1,11 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::input::combined_menu_controller::CombinedMenuController;
use crate::shared_game_state::SharedGameState;
pub struct MenuSaveInfo {
}
pub struct MenuSaveInfo {}
pub enum MenuEntry {
Hidden,

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::clamp;
use crate::caret::CaretType;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Direction;
use crate::npc::NPC;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::caret::CaretType;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::{abs, clamp};
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::{abs, clamp};
use crate::caret::CaretType;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::clamp;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::abs;
use num_traits::clamp;

View File

@ -1,6 +1,6 @@
use ggez::GameResult;
use crate::common::{CDEG_RAD, Direction};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::list::NPCList;
use crate::npc::NPC;
use crate::player::Player;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::caret::CaretType;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Direction;
use crate::npc::NPC;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::clamp;
use crate::caret::CaretType;

View File

@ -1,9 +1,10 @@
use std::cmp::Ordering;
use ggez::GameResult;
use num_traits::{abs, clamp};
use crate::common::Direction;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::NPC;
use crate::player::Player;
use crate::rng::RNG;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::{abs, clamp};
use crate::caret::CaretType;
@ -1586,7 +1587,7 @@ impl NPC {
npc.y = self.y - 0x1800;
npc.tsc_direction = 10 * self.anim_num + 40;
npc_list.spawn(0, npc);
let _ = npc_list.spawn(0, npc);
self.cond.set_explode_die(true);
}
}
@ -1629,7 +1630,7 @@ impl NPC {
npc.direction = Direction::Right;
npc.parent_id = self.id;
npc_list.spawn(0x100, npc);
let _ = npc_list.spawn(0x100, npc);
}
}

View File

@ -1,7 +1,8 @@
use ggez::GameResult;
use num_traits::clamp;
use crate::common::Direction;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::list::NPCList;
use crate::npc::NPC;
use crate::rng::RNG;

View File

@ -1,9 +1,11 @@
use crate::npc::NPC;
use crate::shared_game_state::SharedGameState;
use crate::player::Player;
use ggez::GameResult;
use num_traits::abs;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::NPC;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
impl NPC {
pub(crate) fn tick_n215_sandcroc_outer_wall(&mut self, state: &mut SharedGameState, players: [&mut Player; 2]) -> GameResult {
match self.action_num {

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::clamp;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Direction;
use crate::npc::list::NPCList;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::{abs, clamp};
use crate::caret::CaretType;
@ -288,7 +289,7 @@ impl NPC {
}
pub(crate) fn tick_n049_skullhead(&mut self, state: &mut SharedGameState, players: [&mut Player; 2], npc_list: &NPCList) -> GameResult {
let mut parent = self.get_parent_ref_mut(npc_list);
let parent = self.get_parent_ref_mut(npc_list);
if self.action_num > 9 && parent.as_ref().map(|n| n.npc_type == 3).unwrap_or(false) {
self.action_num = 3;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::abs;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::clamp;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::clamp;
use crate::common::Direction;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::NPC;
use crate::shared_game_state::SharedGameState;

View File

@ -1,4 +1,5 @@
use ggez::GameResult;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::caret::CaretType;
use crate::common::{CDEG_RAD, Direction, Rect};

View File

@ -1,11 +1,11 @@
use std::mem::MaybeUninit;
use ggez::{Context, GameResult};
use crate::bullet::BulletManager;
use crate::common::{Direction, interpolate_fix9_scale};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::list::NPCList;
use crate::npc::NPC;
use crate::player::Player;

View File

@ -1,8 +1,9 @@
use ggez::GameResult;
use num_traits::{abs, clamp};
use crate::caret::CaretType;
use crate::common::{CDEG_RAD, Direction, Rect};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::boss::BossNPC;
use crate::npc::list::NPCList;
use crate::npc::NPC;

View File

@ -1,4 +1,4 @@
use ggez::GameResult;
use crate::bullet::BulletManager;
use crate::caret::CaretType;
@ -9,6 +9,8 @@ use crate::npc::NPC;
use crate::player::Player;
use crate::rng::RNG;
use crate::shared_game_state::SharedGameState;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
impl NPC {
pub(crate) fn tick_n048_omega_projectiles(&mut self, state: &mut SharedGameState) -> GameResult {

View File

@ -1,7 +1,8 @@
use std::cell::{Cell, UnsafeCell};
use std::mem::MaybeUninit;
use ggez::{GameError, GameResult};
use crate::framework::context::Context;
use crate::framework::error::{GameResult, GameError};
use crate::npc::NPC;

View File

@ -2,7 +2,8 @@ use std::io;
use std::io::Cursor;
use byteorder::{LE, ReadBytesExt};
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_traits::abs;
use crate::bitfield;
@ -283,6 +284,8 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager)> for NP
216 => self.tick_n216_debug_cat(state),
222 => self.tick_n222_prison_bars(state),
227 => self.tick_n227_bucket(state),
229 => self.tick_n229_red_flowers_sprouts(state),
230 => self.tick_n230_red_flowers_blooming(state),
234 => self.tick_n234_red_flowers_picked(state),
239 => self.tick_n239_cage_bars(state),
241 => self.tick_n241_critter_red(state, players),

View File

@ -1,6 +1,7 @@
use std::clone::Clone;
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use num_derive::FromPrimitive;
use num_traits::clamp;

View File

@ -4,13 +4,15 @@ use byteorder::{BE, LE, ReadBytesExt, WriteBytesExt};
use num_traits::{clamp, FromPrimitive};
use crate::common::{Direction, FadeState};
use ggez::{Context, GameResult};
use ggez::GameError::ResourceLoadError;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::player::ControlMode;
use crate::scene::game_scene::GameScene;
use crate::shared_game_state::SharedGameState;
use crate::str;
use crate::weapon::{WeaponLevel, WeaponType};
use crate::framework::error::GameError::ResourceLoadError;
pub struct WeaponData {
pub weapon_id: u32,

View File

@ -73,7 +73,7 @@ impl Xoroshiro32PlusPlus {
pub fn next_u16(&self) -> u16 {
let mut state = self.0.get();
let mut result = (state.0.wrapping_add(state.1)).rotate_left(9).wrapping_add(state.0);
let result = (state.0.wrapping_add(state.1)).rotate_left(9).wrapping_add(state.0);
state.1 ^= state.0;
state.0 = state.0.rotate_left(13) ^ state.1 ^ (state.1 << 5);

View File

@ -1,19 +1,21 @@
use ggez::{Context, GameResult, graphics, timer};
use ggez::graphics::{BlendMode, Color, Drawable, DrawParam, FilterMode, mint};
use ggez::graphics::spritebatch::SpriteBatch;
use ggez::nalgebra::{clamp, Vector2};
use log::info;
use num_traits::abs;
use num_traits::{abs, clamp};
use crate::bullet::BulletManager;
use crate::caret::CaretType;
use crate::common::{Direction, FadeDirection, FadeState, fix9_scale, interpolate_fix9_scale, Rect};
use crate::common::{Color, Direction, FadeDirection, FadeState, fix9_scale, interpolate_fix9_scale, Rect};
use crate::components::boss_life_bar::BossLifeBar;
use crate::components::draw_common::{Alignment, draw_number};
use crate::components::hud::HUD;
use crate::components::stage_select::StageSelect;
use crate::entity::GameEntity;
use crate::frame::{Frame, UpdateTarget};
use crate::framework::backend::SpriteBatchCommand;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::framework::graphics::{BlendMode, FilterMode};
use crate::framework::ui::Components;
use crate::input::touch_controls::TouchControlType;
use crate::inventory::{Inventory, TakeExperienceResult};
use crate::npc::boss::BossNPC;
@ -28,7 +30,6 @@ use crate::shared_game_state::{Season, SharedGameState};
use crate::stage::{BackgroundType, Stage};
use crate::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptVM};
use crate::texture_set::SizedBatch;
use crate::ui::Components;
use crate::weapon::WeaponType;
pub struct GameScene {
@ -297,18 +298,18 @@ impl GameScene {
FadeDirection::Left | FadeDirection::Right => {
let mut frame = tick;
for x in (0..(state.canvas_size.0 as i32 + 16)).step_by(16) {
if frame > 15 { frame = 15; } else { frame += 1; }
for x in 0..(state.canvas_size.0 as i32 / 16 + 1) {
if frame >= 15 { frame = 15; } else { frame += 1; }
if frame >= 0 {
rect.left = frame as u16 * 16;
rect.left = frame.abs() as u16 * 16;
rect.right = rect.left + 16;
for y in (0..(state.canvas_size.1 as i32 + 16)).step_by(16) {
for y in 0..(state.canvas_size.1 as i32 / 16 + 1) {
if direction == FadeDirection::Left {
batch.add_rect(state.canvas_size.0 - x as f32, y as f32, &rect);
batch.add_rect(state.canvas_size.0 - x as f32 * 16.0, y as f32 * 16.0, &rect);
} else {
batch.add_rect(x as f32, y as f32, &rect);
batch.add_rect(x as f32 * 16.0, y as f32 * 16.0, &rect);
}
}
}
@ -317,18 +318,18 @@ impl GameScene {
FadeDirection::Up | FadeDirection::Down => {
let mut frame = tick;
for y in (0..(state.canvas_size.1 as i32 + 16)).step_by(16) {
if frame > 15 { frame = 15; } else { frame += 1; }
for y in 0..(state.canvas_size.1 as i32 / 16 + 1) {
if frame >= 15 { frame = 15; } else { frame += 1; }
if frame >= 0 {
rect.left = frame as u16 * 16;
rect.left = frame.abs() as u16 * 16;
rect.right = rect.left + 16;
for x in (0..(state.canvas_size.0 as i32 + 16)).step_by(16) {
for x in 0..(state.canvas_size.0 as i32 / 16 + 1) {
if direction == FadeDirection::Down {
batch.add_rect(x as f32, y as f32, &rect);
batch.add_rect(x as f32 * 16.0, y as f32 * 16.0, &rect);
} else {
batch.add_rect(x as f32, state.canvas_size.1 - y as f32, &rect);
batch.add_rect(x as f32 * 16.0, state.canvas_size.1 - y as f32 * 16.0, &rect);
}
}
}
@ -339,20 +340,20 @@ impl GameScene {
let center_y = (state.canvas_size.1 / 2.0 - 8.0) as i32;
let mut start_frame = tick;
for x in (0..(center_x + 16)).step_by(16) {
for x in 0..(center_x / 16 + 2) {
let mut frame = start_frame;
for y in (0..(center_y + 16)).step_by(16) {
if frame > 15 { frame = 15; } else { frame += 1; }
for y in 0..(center_y / 16 + 2) {
if frame >= 15 { frame = 15; } else { frame += 1; }
if frame >= 0 {
rect.left = frame as u16 * 16;
rect.left = frame.abs() as u16 * 16;
rect.right = rect.left + 16;
batch.add_rect((center_x - x) as f32, (center_y + y) as f32, &rect);
batch.add_rect((center_x - x) as f32, (center_y - y) as f32, &rect);
batch.add_rect((center_x + x) as f32, (center_y + y) as f32, &rect);
batch.add_rect((center_x + x) as f32, (center_y - y) as f32, &rect);
batch.add_rect((center_x - x * 16) as f32, (center_y + y * 16) as f32, &rect);
batch.add_rect((center_x - x * 16) as f32, (center_y - y * 16) as f32, &rect);
batch.add_rect((center_x + x * 16) as f32, (center_y + y * 16) as f32, &rect);
batch.add_rect((center_x + x * 16) as f32, (center_y - y * 16) as f32, &rect);
}
}
@ -433,13 +434,14 @@ impl GameScene {
};
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex_name)?;
// switch version uses +1000 face offset to display a flipped version
// switch version uses 1xxx flag to show a flipped version of face
let flip = state.textscript_vm.face > 1000;
// x1xx flag shows a talking animation
let talking = (state.textscript_vm.face % 1000) > 100;
let face_num = state.textscript_vm.face % 100;
let (scale_x, scale_y) = batch.scale();
batch.add_rect_scaled(left_pos + 14.0 + if flip { 48.0 } else { 0.0 }, top_pos + 8.0,
scale_x * if flip { -1.0 } else { 1.0 }, scale_y,
batch.add_rect_flip(left_pos + 14.0, top_pos + 8.0,
flip, false,
&Rect::new_size(
(face_num as u16 % 6) * 48,
(face_num as u16 / 6) * 48,
@ -502,7 +504,14 @@ impl GameScene {
}
fn draw_light_map(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
graphics::set_canvas(ctx, Some(&state.lightmap_canvas));
let canvas = state.lightmap_canvas.as_mut();
if let None = canvas {
return Ok(());
}
let canvas = canvas.unwrap();
graphics::set_render_target(ctx, Some(canvas));
graphics::set_blend_mode(ctx, BlendMode::Add)?;
graphics::clear(ctx, Color::from_rgb(100, 100, 110));
@ -547,9 +556,9 @@ impl GameScene {
fix9_scale(npc.y - self.frame.y, scale),
0.4, (255, 255, 0), batch);
}
4 | 7 => self.draw_light(fix9_scale(npc.x - self.frame.x, scale),
fix9_scale(npc.y - self.frame.y, scale),
1.0, (100, 100, 100), batch),
7 => self.draw_light(fix9_scale(npc.x - self.frame.x, scale),
fix9_scale(npc.y - self.frame.y, scale),
1.0, (100, 100, 100), batch),
17 if npc.anim_num == 0 => {
self.draw_light(fix9_scale(npc.x - self.frame.x, scale),
fix9_scale(npc.y - self.frame.y, scale),
@ -584,13 +593,6 @@ impl GameScene {
fix9_scale(npc.y - self.frame.y, scale),
3.5, (130 + flicker, 40 + flicker, 0), batch);
}
66 if npc.action_num == 1 && npc.anim_counter % 2 == 0 =>
self.draw_light(fix9_scale(npc.x - self.frame.x, scale),
fix9_scale(npc.y - self.frame.y, scale),
3.0, (0, 100, 255), batch),
67 => self.draw_light(fix9_scale(npc.x - self.frame.x, scale),
fix9_scale(npc.y - self.frame.y, scale),
2.0, (0, 100, 200), batch),
70 => {
let flicker = 50 + npc.anim_num as u8 * 15;
self.draw_light(fix9_scale(npc.x - self.frame.x, scale),
@ -635,97 +637,18 @@ impl GameScene {
}
graphics::set_blend_mode(ctx, BlendMode::Multiply)?;
graphics::set_canvas(ctx, Some(&state.game_canvas));
state.lightmap_canvas.set_filter(FilterMode::Linear);
state.lightmap_canvas.draw(ctx, DrawParam::new()
.scale(Vector2::new(1.0 / state.scale, 1.0 / state.scale)))?;
graphics::set_render_target(ctx, None)?;
let rect = Rect { left: 0.0, top: 0.0, right: state.screen_size.0, bottom: state.screen_size.1 };
canvas.clear();
canvas.add(SpriteBatchCommand::DrawRect(rect, rect));
canvas.draw()?;
graphics::set_blend_mode(ctx, BlendMode::Alpha)?;
Ok(())
}
fn is_water(&self, tile: u8) -> bool {
[0x02, 0x04, 0x60, 0x61, 0x62, 0x64, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0xa0, 0xa1, 0xa2, 0xa3].contains(&tile)
}
fn draw_water(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
let (frame_x, frame_y) = self.frame.xy_interpolated(state.frame_time, state.scale);
{
state.shaders.water_shader_params.resolution = [state.canvas_size.0, state.canvas_size.1];
state.shaders.water_shader_params.frame_pos = [frame_x, frame_y];
state.shaders.water_shader_params.t = self.tick as f32;
let _lock = graphics::use_shader(ctx, &state.shaders.water_shader);
state.shaders.water_shader.send(ctx, state.shaders.water_shader_params)?;
graphics::set_canvas(ctx, Some(&state.tmp_canvas));
graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0));
state.game_canvas.draw(ctx, DrawParam::new()
.scale(mint::Vector2 { x: 1.0 / state.scale, y: -1.0 / state.scale })
.offset(mint::Point2 { x: 0.0, y: -1.0 }))?;
}
graphics::set_canvas(ctx, Some(&state.game_canvas));
// cheap, clones a reference underneath
let mut tmp_batch = SpriteBatch::new(state.tmp_canvas.image().clone());
let tile_start_x = clamp(self.frame.x / 0x200 / 16, 0, self.stage.map.width as i32) as usize;
let tile_start_y = clamp(self.frame.y / 0x200 / 16, 0, self.stage.map.height as i32) as usize;
let tile_end_x = clamp((self.frame.x / 0x200 + 8 + state.canvas_size.0 as i32) / 16 + 1, 0, self.stage.map.width as i32) as usize;
let tile_end_y = clamp((self.frame.y / 0x200 + 8 + state.canvas_size.1 as i32) / 16 + 1, 0, self.stage.map.height as i32) as usize;
let mut rect = Rect {
left: 0.0,
top: 0.0,
right: 16.0,
bottom: 16.0,
};
for y in tile_start_y..tile_end_y {
for x in tile_start_x..tile_end_x {
let tile = unsafe {
self.stage.map.attrib[*self.stage.map.tiles
.get_unchecked((y * self.stage.map.width as usize) + x) as usize]
};
let tile_above = unsafe {
self.stage.map.attrib[*self.stage.map.tiles
.get_unchecked((y.saturating_sub(1) * self.stage.map.width as usize) + x) as usize]
};
if !self.is_water(tile) {
continue;
}
rect.left = (x as f32 * 16.0 - 8.0) - frame_x;
rect.top = (y as f32 * 16.0 - 8.0) - frame_y;
rect.right = rect.left + 16.0;
rect.bottom = rect.top + 16.0;
if tile_above == 0 {
rect.top += 3.0;
}
tmp_batch.add(DrawParam::new()
.src(ggez::graphics::Rect::new(rect.left / state.canvas_size.0,
rect.top / state.canvas_size.1,
(rect.right - rect.left) / state.canvas_size.0,
(rect.bottom - rect.top) / state.canvas_size.1))
.scale(mint::Vector2 {
x: 1.0 / state.scale,
y: 1.0 / state.scale,
})
.dest(mint::Point2 {
x: rect.left,
y: rect.top,
}));
}
}
tmp_batch.draw(ctx, DrawParam::new())?;
Ok(())
}
fn draw_tiles(&self, state: &mut SharedGameState, ctx: &mut Context, layer: TileLayer) -> GameResult {
let tex = match layer {
TileLayer::Snack => "Npc/NpcSym",
@ -1291,7 +1214,7 @@ impl Scene for GameScene {
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
graphics::set_canvas(ctx, Some(&state.game_canvas));
//graphics::set_canvas(ctx, Some(&state.game_canvas));
self.draw_background(state, ctx)?;
self.draw_tiles(state, ctx, TileLayer::Background)?;
if state.settings.shader_effects
@ -1316,9 +1239,9 @@ impl Scene for GameScene {
self.draw_bullets(state, ctx)?;
self.player2.draw(state, ctx, &self.frame)?;
self.player1.draw(state, ctx, &self.frame)?;
if state.settings.shader_effects && self.water_visible {
/*if state.settings.shader_effects && self.water_visible {
self.draw_water(state, ctx)?;
}
}*/
self.draw_tiles(state, ctx, TileLayer::Foreground)?;
self.draw_tiles(state, ctx, TileLayer::Snack)?;
@ -1329,9 +1252,9 @@ impl Scene for GameScene {
self.draw_light_map(state, ctx)?;
}
graphics::set_canvas(ctx, None);
/*graphics::set_canvas(ctx, None);
state.game_canvas.draw(ctx, DrawParam::new()
.scale(Vector2::new(1.0 / state.scale, 1.0 / state.scale)))?;
.scale(Vector2::new(1.0 / state.scale, 1.0 / state.scale)))?;*/
self.draw_black_bars(state, ctx)?;
if state.control_flags.control_enabled() {
@ -1389,7 +1312,7 @@ impl Scene for GameScene {
self.draw_debug_outlines(state, ctx)?;
}
draw_number(state.canvas_size.0 - 8.0, 8.0, timer::fps(ctx) as usize, Alignment::Right, state, ctx)?;
//draw_number(state.canvas_size.0 - 8.0, 8.0, timer::fps(ctx) as usize, Alignment::Right, state, ctx)?;
Ok(())
}

View File

@ -1,5 +1,6 @@
use ggez::{Context, filesystem, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::npc::NPCTable;
use crate::scene::no_data_scene::NoDataScene;
use crate::scene::Scene;

View File

@ -1,7 +1,8 @@
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;
use crate::ui::Components;
use crate::framework::ui::Components;
pub mod game_scene;
pub mod loading_scene;

View File

@ -1,4 +1,5 @@
use ggez::{Context, GameError, GameResult};
use crate::framework::context::Context;
use crate::framework::error::{GameResult, GameError};
use crate::common::Rect;
use crate::scene::Scene;
@ -18,11 +19,11 @@ impl NoDataScene {
}
}
#[cfg(target_os = "android")]
static REL_URL: &str = "https://github.com/doukutsu-rs/game-data/releases";
impl Scene for NoDataScene {
fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
#[cfg(target_os = "android")]
{
if !self.flag {

View File

@ -1,12 +1,12 @@
use ggez::{Context, GameResult, graphics};
use ggez::graphics::Color;
use crate::common::{Rect, VERSION_BANNER};
use crate::common::{Rect, VERSION_BANNER, Color};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::input::combined_menu_controller::CombinedMenuController;
use crate::input::touch_controls::TouchControlType;
use crate::menu::{Menu, MenuEntry, MenuSelectionResult};
use crate::scene::Scene;
use crate::shared_game_state::{SharedGameState, TimingMode};
use crate::input::combined_menu_controller::CombinedMenuController;
use crate::input::touch_controls::TouchControlType;
#[derive(PartialEq, Eq, Copy, Clone)]
#[repr(u8)]

View File

@ -14,9 +14,24 @@ declare interface DoukutsuScene {
tick(): number;
/**
* Returns player at specified index.
* Returns a list of players connected to current game.
*/
player(index: number): DoukutsuPlayer | null;
onlinePlayers(): DoukutsuPlayer[];
/**
* Returns a list of players on current map.
*/
mapPlayers(): DoukutsuPlayer[];
/**
* Returns the id of local player.
*/
localPlayerId(): number;
/**
* Returns player with specified id.
*/
player(id: number): DoukutsuPlayer | null;
};
declare namespace doukutsu {

View File

@ -1,14 +1,13 @@
use lua_ffi::ffi::luaL_Reg;
use lua_ffi::{LuaObject, State, c_int};
use crate::scene::game_scene::GameScene;
use crate::scripting::LuaScriptingState;
use crate::shared_game_state::SharedGameState;
pub struct Doukutsu {
pub ptr: *mut LuaScriptingState,
}
#[allow(unused)]
impl Doukutsu {
pub fn new(ptr: *mut LuaScriptingState) -> Doukutsu {
Doukutsu {

View File

@ -1,14 +1,18 @@
use std::io::{Read, Seek};
use std::io::Read;
use std::ptr::null_mut;
use ggez::{Context, filesystem, GameError, GameResult};
use ggez::filesystem::File;
use lua_ffi::{c_int, LuaFunction, LuaObject, State, ThreadStatus};
use lua_ffi::ffi::lua_pushcfunction;
use crate::framework::context::Context;
use crate::framework::error::{GameResult, GameError};
use lua_ffi::{c_int, State, ThreadStatus};
use crate::scene::game_scene::GameScene;
use crate::scripting::doukutsu::Doukutsu;
use crate::shared_game_state::SharedGameState;
use crate::framework::filesystem::File;
use crate::framework::filesystem;
mod doukutsu;
mod player;
@ -112,7 +116,7 @@ impl LuaScriptingState {
if filesystem::exists(ctx, "/scripts/") {
let mut script_count = 0;
let mut files = filesystem::read_dir(ctx, "/scripts/")?
let files = filesystem::read_dir(ctx, "/scripts/")?
.filter(|f| f.to_string_lossy().to_lowercase().ends_with(".lua"));
for file in files {

View File

@ -12,6 +12,7 @@ pub struct LuaPlayer {
inv_ptr: *mut Inventory,
}
#[allow(unused)]
impl LuaPlayer {
fn check_ref(&self, state: &mut State) -> bool {
if !self.valid_reference {

View File

@ -1,11 +1,12 @@
use ggez::{Context, GameResult};
use serde::{Deserialize, Serialize};
use winit::event::VirtualKeyCode;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::keyboard::ScanCode;
use crate::input::keyboard_player_controller::KeyboardController;
use crate::input::player_controller::PlayerController;
use crate::player::TargetPlayer;
use crate::input::touch_player_controller::TouchPlayerController;
use crate::player::TargetPlayer;
#[derive(Serialize, Deserialize)]
pub struct Settings {
@ -66,47 +67,47 @@ impl Default for Settings {
#[derive(Serialize, Deserialize)]
pub struct PlayerKeyMap {
pub left: VirtualKeyCode,
pub up: VirtualKeyCode,
pub right: VirtualKeyCode,
pub down: VirtualKeyCode,
pub prev_weapon: VirtualKeyCode,
pub next_weapon: VirtualKeyCode,
pub jump: VirtualKeyCode,
pub shoot: VirtualKeyCode,
pub skip: VirtualKeyCode,
pub inventory: VirtualKeyCode,
pub map: VirtualKeyCode,
pub left: ScanCode,
pub up: ScanCode,
pub right: ScanCode,
pub down: ScanCode,
pub prev_weapon: ScanCode,
pub next_weapon: ScanCode,
pub jump: ScanCode,
pub shoot: ScanCode,
pub skip: ScanCode,
pub inventory: ScanCode,
pub map: ScanCode,
}
fn p1_default_keymap() -> PlayerKeyMap {
PlayerKeyMap {
left: VirtualKeyCode::Left,
up: VirtualKeyCode::Up,
right: VirtualKeyCode::Right,
down: VirtualKeyCode::Down,
prev_weapon: VirtualKeyCode::A,
next_weapon: VirtualKeyCode::S,
jump: VirtualKeyCode::Z,
shoot: VirtualKeyCode::X,
skip: VirtualKeyCode::LControl,
inventory: VirtualKeyCode::Q,
map: VirtualKeyCode::W,
left: ScanCode::Left,
up: ScanCode::Up,
right: ScanCode::Right,
down: ScanCode::Down,
prev_weapon: ScanCode::A,
next_weapon: ScanCode::S,
jump: ScanCode::Z,
shoot: ScanCode::X,
skip: ScanCode::LControl,
inventory: ScanCode::Q,
map: ScanCode::W,
}
}
fn p2_default_keymap() -> PlayerKeyMap {
PlayerKeyMap {
left: VirtualKeyCode::Comma,
up: VirtualKeyCode::L,
right: VirtualKeyCode::Slash,
down: VirtualKeyCode::Period,
prev_weapon: VirtualKeyCode::G,
next_weapon: VirtualKeyCode::H,
jump: VirtualKeyCode::B,
shoot: VirtualKeyCode::N,
skip: VirtualKeyCode::RControl,
inventory: VirtualKeyCode::T,
map: VirtualKeyCode::Y,
left: ScanCode::Comma,
up: ScanCode::L,
right: ScanCode::Slash,
down: ScanCode::Period,
prev_weapon: ScanCode::G,
next_weapon: ScanCode::H,
jump: ScanCode::B,
shoot: ScanCode::N,
skip: ScanCode::U,
inventory: ScanCode::T,
map: ScanCode::Y,
}
}

View File

@ -1,6 +1,7 @@
use gfx::{self, *};
use ggez::graphics::Shader;
use ggez::{Context, GameResult};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
gfx_defines! {
constant WaterShaderParams {
@ -11,7 +12,7 @@ gfx_defines! {
}
pub struct Shaders {
pub water_shader: Shader<WaterShaderParams>,
//pub water_shader: Shader<WaterShaderParams>,
pub water_shader_params: WaterShaderParams,
}
@ -24,14 +25,14 @@ impl Shaders {
};
Ok(Shaders {
water_shader: Shader::new(
/*water_shader: Shader::new(
ctx,
"/builtin/shaders/basic_es300.vert.glsl",
"/builtin/shaders/water_es300.frag.glsl",
water_shader_params,
"WaterShaderParams",
None,
)?,
)?,*/
water_shader_params,
})
}

View File

@ -1,17 +1,17 @@
use std::ops::Div;
use std::time::Instant;
use bitvec::vec::BitVec;
use chrono::{Datelike, Local};
use ggez::{Context, filesystem, GameResult, graphics};
use ggez::filesystem::OpenOptions;
use ggez::graphics::Canvas;
use num_traits::clamp;
use num_traits::real::Real;
use crate::bmfont_renderer::BMFontRenderer;
use crate::caret::{Caret, CaretType};
use crate::common::{ControlFlags, Direction, FadeState};
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::vfs::OpenOptions;
use crate::input::touch_controls::TouchControls;
use crate::npc::NPCTable;
use crate::profile::GameProfile;
@ -21,12 +21,15 @@ use crate::scene::Scene;
#[cfg(feature = "scripting")]
use crate::scripting::LuaScriptingState;
use crate::settings::Settings;
use crate::shaders::Shaders;
use crate::sound::SoundManager;
use crate::stage::StageData;
use crate::str;
use crate::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM};
use crate::texture_set::{TextureSet};
use crate::texture_set::TextureSet;
use crate::framework::{filesystem, graphics};
use crate::framework::backend::BackendTexture;
use crate::framework::graphics::{set_render_target, create_texture_mutable};
use crate::framework::keyboard::ScanCode;
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum TimingMode {
@ -101,15 +104,13 @@ pub struct SharedGameState {
pub npc_super_pos: (i32, i32),
pub stages: Vec<StageData>,
pub frame_time: f64,
pub debugger: bool,
pub scale: f32,
pub shaders: Shaders,
pub tmp_canvas: Canvas,
pub game_canvas: Canvas,
pub lightmap_canvas: Canvas,
pub canvas_size: (f32, f32),
pub screen_size: (f32, f32),
pub next_scene: Option<Box<dyn Scene>>,
pub textscript_vm: TextScriptVM,
pub lightmap_canvas: Option<Box<dyn BackendTexture>>,
pub season: Season,
pub constants: EngineConstants,
pub font: BMFontRenderer,
@ -118,16 +119,11 @@ pub struct SharedGameState {
pub lua: LuaScriptingState,
pub sound_manager: SoundManager,
pub settings: Settings,
pub shutdown: bool,
pub shutdown: bool
}
impl SharedGameState {
pub fn new(ctx: &mut Context) -> GameResult<SharedGameState> {
let screen_size = graphics::drawable_size(ctx);
let scale = screen_size.1.div(235.0).floor().max(1.0);
let canvas_size = (screen_size.0 / scale, screen_size.1 / scale);
let mut constants = EngineConstants::defaults();
let mut base_path = "/";
let settings = Settings::load(ctx)?;
@ -164,7 +160,7 @@ impl SharedGameState {
game_flags: bitvec::bitvec![0; 8000],
fade_state: FadeState::Hidden,
game_rng: XorShift::new(0),
effect_rng: XorShift::new(Instant::now().elapsed().as_nanos() as i32),
effect_rng: XorShift::new(123),
quake_counter: 0,
teleporter_slots: Vec::with_capacity(8),
carets: Vec::with_capacity(32),
@ -174,15 +170,13 @@ impl SharedGameState {
npc_super_pos: (0, 0),
stages: Vec::with_capacity(96),
frame_time: 0.0,
scale,
shaders: Shaders::new(ctx)?,
tmp_canvas: Canvas::with_window_size(ctx)?,
game_canvas: Canvas::with_window_size(ctx)?,
lightmap_canvas: Canvas::with_window_size(ctx)?,
screen_size,
canvas_size,
debugger: false,
scale: 2.0,
screen_size: (640.0, 480.0),
canvas_size: (320.0, 240.0),
next_scene: None,
textscript_vm: TextScriptVM::new(),
lightmap_canvas: None,
season,
constants,
font,
@ -195,6 +189,28 @@ impl SharedGameState {
})
}
pub fn process_debug_keys(&mut self, key_code: ScanCode) {
match key_code {
ScanCode::F3 => { self.settings.god_mode = !self.settings.god_mode }
ScanCode::F4 => { self.settings.infinite_booster = !self.settings.infinite_booster }
ScanCode::F5 => { self.settings.subpixel_coords = !self.settings.subpixel_coords }
ScanCode::F6 => { self.settings.motion_interpolation = !self.settings.motion_interpolation }
ScanCode::F7 => { self.set_speed(1.0) }
ScanCode::F8 => {
if self.settings.speed > 0.2 {
self.set_speed(self.settings.speed - 0.1);
}
}
ScanCode::F9 => {
if self.settings.speed < 3.0 {
self.set_speed(self.settings.speed + 0.1);
}
}
ScanCode::F10 => { self.settings.debug_outlines = !self.settings.debug_outlines }
_ => {}
}
}
pub fn reload_textures(&mut self) {
let mut texture_set = TextureSet::new(self.base_path.as_str());
@ -288,11 +304,15 @@ impl SharedGameState {
}
pub fn handle_resize(&mut self, ctx: &mut Context) -> GameResult {
self.screen_size = graphics::drawable_size(ctx);
self.scale = self.screen_size.1.div(240.0).floor().max(1.0);
self.screen_size = graphics::screen_size(ctx);
self.scale = self.screen_size.1.div(230.0).floor().max(1.0);
self.canvas_size = (self.screen_size.0 / self.scale, self.screen_size.1 / self.scale);
graphics::set_screen_coordinates(ctx, graphics::Rect::new(0.0, 0.0, self.screen_size.0, self.screen_size.1))?;
let (width, height) = (self.screen_size.0 as u16, self.screen_size.1 as u16);
// ensure no texture is bound before destroying them.
set_render_target(ctx, None)?;
self.lightmap_canvas = Some(create_texture_mutable(ctx, width, height)?);
Ok(())
}
@ -312,10 +332,6 @@ impl SharedGameState {
pub fn set_speed(&mut self, value: f64) {
self.settings.speed = clamp(value, 0.1, 3.0);
self.frame_time = 0.0;
if let Err(err) = self.sound_manager.set_speed(value as f32) {
log::error!("Error while sending a message to sound manager: {}", err);
}
}
pub fn current_tps(&self) -> f64 {

View File

@ -4,16 +4,18 @@ use std::time::Duration;
use cpal::Sample;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use ggez::{Context, filesystem, GameResult};
use ggez::GameError::{AudioError, InvalidValue, ResourceLoadError};
use num_traits::clamp;
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::{GameResult};
use crate::framework::filesystem;
use crate::sound::organya::Song;
use crate::sound::pixtone::PixTonePlayback;
use crate::sound::playback::{PlaybackEngine, SavedPlaybackState};
use crate::sound::wave_bank::SoundBank;
use crate::str;
use crate::framework::error::GameError::{AudioError, ResourceLoadError, InvalidValue};
mod wave_bank;
mod organya;
@ -126,13 +128,19 @@ impl SoundManager {
.find(|path| filesystem::exists(ctx, path))
.ok_or_else(|| ResourceLoadError(format!("BGM {:?} does not exist.", song_name)))?;
let org = organya::Song::load_from(filesystem::open(ctx, path)?)?;
log::info!("Playing BGM: {}", song_name);
match filesystem::open(ctx, path).map(|f| organya::Song::load_from(f)) {
Ok(Ok(org)) => {
log::info!("Playing BGM: {} {}", song_id, song_name);
self.prev_song_id = self.current_song_id;
self.current_song_id = song_id;
self.tx.send(PlaybackMessage::SaveState)?;
self.tx.send(PlaybackMessage::PlaySong(Box::new(org)))?;
self.prev_song_id = self.current_song_id;
self.current_song_id = song_id;
self.tx.send(PlaybackMessage::SaveState)?;
self.tx.send(PlaybackMessage::PlaySong(Box::new(org)))?;
}
Ok(Err(err)) | Err(err) => {
log::warn!("Failed to load BGM {}: {}", song_id, err);
}
}
}
Ok(())
}

View File

@ -1,12 +1,18 @@
use std::io;
use byteorder::{LE, ReadBytesExt};
use crate::framework::error::{GameError, GameResult};
#[repr(u8)]
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Version {
// Can't find any files with this signature,
// But apparently these files had no Pi flag.
Beta = b'1',
Main = b'2',
// OrgMaker 2.05 Extended Drums
Extended = b'3'
Extended = b'3',
}
#[derive(Debug, Copy, Clone)]
@ -14,19 +20,19 @@ pub struct LoopRange {
// inclusive
pub start: i32,
// exclusive
pub end: i32
pub end: i32,
}
#[derive(Debug, Copy, Clone)]
pub struct Display {
pub beats: u8,
pub steps: u8
pub steps: u8,
}
#[derive(Debug, Copy, Clone)]
pub struct Timing {
pub wait: u16,
pub loop_range: LoopRange
pub loop_range: LoopRange,
}
#[derive(Copy, Clone)]
@ -35,12 +41,12 @@ pub struct Instrument {
pub freq: u16,
pub inst: u8,
pub pipi: u8,
pub notes: u16
pub notes: u16,
}
pub struct Track {
pub inst: Instrument,
pub notes: Vec<Note>
pub notes: Vec<Note>,
}
impl Clone for Track {
@ -65,14 +71,14 @@ pub struct Note {
pub key: u8,
pub len: u8,
pub vol: u8,
pub pan: u8
pub pan: u8,
}
#[derive(Debug)]
pub struct Song {
pub version: Version,
pub time: Timing,
pub tracks: [Track; 16]
pub tracks: [Track; 16],
}
impl Clone for Song {
@ -85,85 +91,82 @@ impl Clone for Song {
}
}
use byteorder::{LE, ReadBytesExt};
use std::io;
impl Song {
pub fn empty() -> Song {
Song {
version: Version::Main,
time: Timing { wait: 8, loop_range: LoopRange { start: 0, end: 1 } },
tracks: [
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
Track { inst: Instrument {freq: 1000, inst: 0, pipi: 0, notes: 0}, notes: vec![] },
]
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
Track { inst: Instrument { freq: 1000, inst: 0, pipi: 0, notes: 0 }, notes: vec![] },
],
}
}
pub fn load_from<R: io::Read>(mut f: R) -> io::Result<Song> {
pub fn load_from<R: io::Read>(mut f: R) -> GameResult<Song> {
let mut magic = [0; 6];
f.read_exact(&mut magic)?;
let version =
let version =
match &magic {
b"Org-01" => Version::Beta,
b"Org-02" => Version::Main,
b"Org-03" => Version::Extended,
_ => return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid magic number"))
_ => return Err(GameError::ResourceLoadError("Invalid magic number".to_string()))
};
let wait = f.read_u16::<LE>()?;
let _bpm = f.read_u8()?;
let _spb = f.read_u8()?;
let wait = f.read_u16::<LE>()?;
let _bpm = f.read_u8()?;
let _spb = f.read_u8()?;
let start = f.read_i32::<LE>()?;
let end = f.read_i32::<LE>()?;
let end = f.read_i32::<LE>()?;
use std::mem::MaybeUninit as Mu;
let mut insts: [Mu<Instrument>; 16] = unsafe {
Mu::uninit().assume_init()
};
for i in insts.iter_mut() {
let freq = f.read_u16::<LE>()?;
let inst = f.read_u8()?;
let pipi = f.read_u8()?;
let freq = f.read_u16::<LE>()?;
let inst = f.read_u8()?;
let pipi = f.read_u8()?;
let notes = f.read_u16::<LE>()?;
*i = Mu::new(Instrument {
freq,
inst,
pipi,
notes
notes,
});
}
let insts: [Instrument; 16] = unsafe {
std::mem::transmute(insts)
};
let mut tracks: [Mu<Track>; 16] = unsafe {
Mu::uninit().assume_init()
};
for (i, t) in tracks.iter_mut().enumerate() {
let count = insts[i].notes as usize;
#[repr(C)]
#[derive(Copy, Clone)]
struct UninitNote {
@ -171,55 +174,55 @@ impl Song {
key: Mu<u8>,
len: Mu<u8>,
vol: Mu<u8>,
pan: Mu<u8>
pan: Mu<u8>,
}
let mut notes: Vec<UninitNote> = unsafe {
vec![Mu::uninit().assume_init(); count]
};
for note in notes.iter_mut() {
note.pos = Mu::new(f.read_i32::<LE>()?);
}
for note in notes.iter_mut() {
note.key = Mu::new(f.read_u8()?);
}
for note in notes.iter_mut() {
note.len = Mu::new(f.read_u8()?);
}
for note in notes.iter_mut() {
note.vol = Mu::new(f.read_u8()?);
}
for note in notes.iter_mut() {
note.pan = Mu::new(f.read_u8()?);
}
*t = Mu::new(Track {
inst: insts[i],
notes: unsafe { std::mem::transmute(notes) }
notes: unsafe { std::mem::transmute(notes) },
});
}
let tracks = unsafe {
std::mem::transmute(tracks)
};
let song = Song {
version,
time: Timing {
wait,
loop_range: LoopRange {
start,
end
}
end,
},
},
tracks
tracks,
};
Ok(song)
}
}

View File

@ -1,6 +1,6 @@
use std::mem::MaybeUninit;
use crate::sound::organya::Song as Organya;
use crate::sound::organya::{Song as Organya, Version};
use crate::sound::stuff::*;
use crate::sound::wav::*;
use crate::sound::wave_bank::SoundBank;
@ -144,8 +144,12 @@ impl PlaybackEngine {
}
}
for (idx, (_track, buf)) in song.tracks[8..].iter().zip(self.track_buffers[128..].iter_mut()).enumerate() {
*buf = RenderBuffer::new(samples.samples[idx].clone());
for (idx, (track, buf)) in song.tracks[8..].iter().zip(self.track_buffers[128..].iter_mut()).enumerate() {
if self.song.version == Version::Extended {
*buf = RenderBuffer::new(samples.samples[track.inst.inst as usize].clone());
} else {
*buf = RenderBuffer::new(samples.samples[idx].clone());
}
}
self.song = song;
@ -162,6 +166,7 @@ impl PlaybackEngine {
self.play_pos = position;
}
#[allow(unused)]
pub fn get_total_samples(&self) -> u32 {
let ticks_intro = self.song.time.loop_range.start;
let ticks_loop = self.song.time.loop_range.end - self.song.time.loop_range.start;

View File

@ -7,10 +7,12 @@ use log::info;
use crate::encoding::read_cur_shift_jis;
use crate::engine_constants::EngineConstants;
use ggez::{Context, filesystem, GameResult};
use ggez::GameError::ResourceLoadError;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::map::{Map, NPCData};
use crate::text_script::TextScript;
use crate::framework::error::GameError::ResourceLoadError;
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct NpcType {

View File

@ -9,8 +9,6 @@ use std::ops::Not;
use std::str::FromStr;
use byteorder::ReadBytesExt;
use ggez::{Context, GameResult};
use ggez::GameError::{InvalidValue, ParseError};
use itertools::Itertools;
use num_derive::FromPrimitive;
use num_traits::{clamp, FromPrimitive};
@ -21,6 +19,9 @@ use crate::encoding::{read_cur_shift_jis, read_cur_wtf8};
use crate::engine_constants::EngineConstants;
use crate::entity::GameEntity;
use crate::frame::UpdateTarget;
use crate::framework::context::Context;
use crate::framework::error::GameError::{InvalidValue, ParseError};
use crate::framework::error::GameResult;
use crate::npc::NPC;
use crate::player::{ControlMode, TargetPlayer};
use crate::scene::game_scene::GameScene;
@ -1750,6 +1751,7 @@ impl TextScript {
}
}
#[allow(unused)]
fn read_varint<I: Iterator<Item=u8>>(iter: &mut I) -> GameResult<i32> {
let mut result = 0u32;

View File

@ -1,27 +1,27 @@
use std::collections::HashMap;
use std::io::{BufReader, Read, Seek, SeekFrom};
use ggez;
use ggez::{Context, GameError, GameResult, graphics};
use ggez::filesystem;
use ggez::graphics::{Drawable, DrawMode, DrawParam, FilterMode, Image, Mesh, mint, Rect};
use ggez::graphics::spritebatch::SpriteBatch;
use ggez::nalgebra::{Point2, Vector2};
use image::RgbaImage;
use itertools::Itertools;
use log::info;
use crate::common;
use crate::common::FILE_TYPES;
use crate::common::{FILE_TYPES, Rect};
use crate::engine_constants::EngineConstants;
use crate::framework::backend::{BackendTexture, SpriteBatchCommand};
use crate::framework::context::Context;
use crate::framework::error::{GameError, GameResult};
use crate::framework::filesystem;
use crate::framework::graphics::{create_texture, FilterMode};
use crate::settings::Settings;
use crate::shared_game_state::Season;
use crate::str;
pub static mut I_MAG: f32 = 1.0;
pub static mut G_MAG: f32 = 1.0;
pub struct SizedBatch {
pub batch: SpriteBatch,
batch: Box<dyn BackendTexture>,
width: usize,
height: usize,
real_width: usize,
@ -66,22 +66,65 @@ impl SizedBatch {
self.batch.clear();
}
pub fn add(&mut self, x: f32, y: f32) {
let param = DrawParam::new()
.dest(Point2::new(x, y))
.scale(Vector2::new(self.scale_x, self.scale_y));
pub fn add(&mut self, mut x: f32, mut y: f32) {
unsafe {
x = (x * G_MAG).round() / G_MAG;
y = (y * G_MAG).round() / G_MAG;
}
let mag = unsafe { I_MAG };
self.batch.add(param);
self.batch.add(SpriteBatchCommand::DrawRect(
Rect {
left: 0 as f32,
top: 0 as f32,
right: self.real_width as f32,
bottom: self.real_height as f32,
},
Rect {
left: x * mag,
top: y * mag,
right: (x + self.width() as f32) * mag,
bottom: (y + self.height() as f32) * mag,
},
));
}
#[inline(always)]
pub fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect<u16>) {
self.add_rect_scaled(x, y, self.scale_x, self.scale_y, rect)
self.add_rect_scaled(x, y, 1.0, 1.0, rect)
}
pub fn add_rect_flip(&mut self, mut x: f32, mut y: f32, flip_x: bool, flip_y: bool, rect: &common::Rect<u16>) {
if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 {
return;
}
unsafe {
x = (x * G_MAG).round() / G_MAG;
y = (y * G_MAG).round() / G_MAG;
}
let mag = unsafe { I_MAG };
self.batch.add(SpriteBatchCommand::DrawRectFlip(
Rect {
left: rect.left as f32 / self.scale_x,
top: rect.top as f32 / self.scale_y,
right: rect.right as f32 / self.scale_x,
bottom: rect.bottom as f32 / self.scale_y,
},
Rect {
left: x * mag,
top: y * mag,
right: (x + rect.width() as f32) * mag,
bottom: (y + rect.height() as f32) * mag,
},
flip_x, flip_y
));
}
#[inline(always)]
pub fn add_rect_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), rect: &common::Rect<u16>) {
self.add_rect_scaled_tinted(x, y, color, self.scale_x, self.scale_y, rect)
self.add_rect_scaled_tinted(x, y, color, 1.0, 1.0, rect)
}
pub fn add_rect_scaled(&mut self, mut x: f32, mut y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect<u16>) {
@ -90,36 +133,53 @@ impl SizedBatch {
}
unsafe {
x = (x * G_MAG).floor() / G_MAG;
y = (y * G_MAG).floor() / G_MAG;
x = (x * G_MAG).round() / G_MAG;
y = (y * G_MAG).round() / G_MAG;
}
let mag = unsafe { I_MAG };
let param = DrawParam::new()
.src(Rect::new(rect.left as f32 / self.width as f32,
rect.top as f32 / self.height as f32,
(rect.right - rect.left) as f32 / self.width as f32,
(rect.bottom - rect.top) as f32 / self.height as f32))
.dest(mint::Point2 { x, y })
.scale(Vector2::new(scale_x, scale_y));
self.batch.add(param);
self.batch.add(SpriteBatchCommand::DrawRect(
Rect {
left: rect.left as f32 / self.scale_x,
top: rect.top as f32 / self.scale_y,
right: rect.right as f32 / self.scale_x,
bottom: rect.bottom as f32 / self.scale_y,
},
Rect {
left: x * mag,
top: y * mag,
right: (x + rect.width() as f32 * scale_x) * mag,
bottom: (y + rect.height() as f32 * scale_y) * mag,
},
));
}
pub fn add_rect_scaled_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), scale_x: f32, scale_y: f32, rect: &common::Rect<u16>) {
pub fn add_rect_scaled_tinted(&mut self, mut x: f32, mut y: f32, color: (u8, u8, u8, u8), scale_x: f32, scale_y: f32, rect: &common::Rect<u16>) {
if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 {
return;
}
let param = DrawParam::new()
.color(color.into())
.src(Rect::new(rect.left as f32 / self.width as f32,
rect.top as f32 / self.height as f32,
(rect.right - rect.left) as f32 / self.width as f32,
(rect.bottom - rect.top) as f32 / self.height as f32))
.dest(mint::Point2 { x, y })
.scale(Vector2::new(scale_x, scale_y));
unsafe {
x = (x * G_MAG).floor() / G_MAG;
y = (y * G_MAG).floor() / G_MAG;
}
let mag = unsafe { I_MAG };
self.batch.add(param);
self.batch.add(SpriteBatchCommand::DrawRectTinted(
Rect {
left: rect.left as f32,
top: rect.top as f32,
right: rect.right as f32,
bottom: rect.bottom as f32,
},
Rect {
left: x * mag,
top: y * mag,
right: (x + rect.width() as f32 * scale_x) * mag,
bottom: (y + rect.height() as f32 * scale_y) * mag,
},
color.into(),
));
}
#[inline(always)]
@ -128,8 +188,8 @@ impl SizedBatch {
}
pub fn draw_filtered(&mut self, filter: FilterMode, ctx: &mut Context) -> GameResult {
self.batch.set_filter(filter);
self.batch.draw(ctx, DrawParam::new())?;
//self.batch.set_filter(filter);
self.batch.draw()?;
self.batch.clear();
Ok(())
}
@ -168,7 +228,7 @@ impl TextureSet {
}
}
fn load_image(&self, ctx: &mut Context, path: &str) -> GameResult<Image> {
fn load_image(&self, ctx: &mut Context, path: &str) -> GameResult<Box<dyn BackendTexture>> {
let img = {
let mut buf = [0u8; 8];
let mut reader = filesystem::open(ctx, path)?;
@ -184,7 +244,7 @@ impl TextureSet {
};
let (width, height) = img.dimensions();
Image::from_rgba8(ctx, width as u16, height as u16, img.as_ref())
create_texture(ctx, width as u16, height as u16, &img)
}
pub fn load_texture(&self, ctx: &mut Context, constants: &EngineConstants, name: &str) -> GameResult<SizedBatch> {
@ -199,27 +259,25 @@ impl TextureSet {
info!("Loading texture: {}", path);
let image = self.load_image(ctx, &path)?;
let size = image.dimensions();
let batch = self.load_image(ctx, &path)?;
let size = batch.dimensions();
assert_ne!(size.w as isize, 0, "size.w == 0");
assert_ne!(size.h as isize, 0, "size.h == 0");
assert_ne!(size.0 as isize, 0, "size.width == 0");
assert_ne!(size.1 as isize, 0, "size.height == 0");
let dim = (size.w as usize, size.h as usize);
let orig_dimensions = constants.tex_sizes.get(name).unwrap_or_else(|| &dim);
let scale_x = orig_dimensions.0 as f32 / size.w;
let scale_y = orig_dimensions.0 as f32 / size.w;
let width = (size.w * scale_x) as usize;
let height = (size.h * scale_y) as usize;
let orig_dimensions = constants.tex_sizes.get(name).unwrap_or_else(|| &size);
let scale = orig_dimensions.0 as f32 / size.0 as f32;
let width = (size.0 as f32 * scale) as usize;
let height = (size.1 as f32 * scale) as usize;
Ok(SizedBatch {
batch: SpriteBatch::new(image),
batch,
width,
height,
scale_x,
scale_y,
real_width: size.w as usize,
real_height: size.h as usize,
scale_x: scale,
scale_y: scale,
real_width: size.0 as usize,
real_height: size.1 as usize,
})
}
@ -233,14 +291,10 @@ impl TextureSet {
}
pub fn draw_rect(&self, rect: common::Rect, color: [f32; 4], ctx: &mut Context) -> GameResult {
let rect = Mesh::new_rectangle(ctx, DrawMode::fill(), rect.into(), color.into())?;
graphics::draw(ctx, &rect, DrawParam::new())?;
Ok(())
}
pub fn draw_outline_rect(&self, rect: common::Rect, width: f32, color: [f32; 4], ctx: &mut Context) -> GameResult {
let rect = Mesh::new_rectangle(ctx, DrawMode::stroke(width), rect.into(), color.into())?;
graphics::draw(ctx, &rect, DrawParam::new())?;
Ok(())
}
}

191
src/ui.rs
View File

@ -1,191 +0,0 @@
use std::time::Instant;
use imgui::{FontConfig, FontSource};
use imgui::sys::*;
use imgui_gfx_renderer::{Renderer, Shaders};
use imgui_gfx_renderer::gfx::format::DepthStencil;
use imgui_gfx_renderer::gfx::format::Rgba8;
use imgui_gfx_renderer::gfx::handle::DepthStencilView;
use imgui_gfx_renderer::gfx::handle::RenderTargetView;
use imgui_gfx_renderer::gfx::memory::Typed;
use imgui_winit_support::{HiDpiMode, WinitPlatform};
use ggez::{Context, GameResult, graphics};
use ggez::GameError::RenderError;
use crate::live_debugger::LiveDebugger;
use crate::scene::Scene;
use crate::shared_game_state::SharedGameState;
mod types {
pub type Resources = gfx_device_gl::Resources;
}
pub struct UI {
pub imgui: imgui::Context,
pub platform: WinitPlatform,
pub renderer: Renderer<Rgba8, types::Resources>,
pub components: Components,
pub main_color: RenderTargetView<types::Resources, Rgba8>,
pub main_depth: DepthStencilView<types::Resources, DepthStencil>,
last_frame: Instant,
}
pub struct Components {
pub live_debugger: LiveDebugger,
}
impl UI {
pub fn new(ctx: &mut Context) -> GameResult<Self> {
let mut imgui = imgui::Context::create();
imgui.set_ini_filename(None);
imgui.fonts().add_font(&[
FontSource::DefaultFontData {
config: Some(FontConfig::default()),
},
]);
imgui.style_mut().window_padding = [4.0, 6.0];
imgui.style_mut().frame_padding = [8.0, 6.0];
imgui.style_mut().item_spacing = [8.0, 6.0];
imgui.style_mut().item_inner_spacing = [8.0, 6.0];
imgui.style_mut().indent_spacing = 20.0;
imgui.style_mut().scrollbar_size = 20.0;
imgui.style_mut().grab_min_size = 5.0;
imgui.style_mut().window_border_size = 0.0;
imgui.style_mut().child_border_size = 1.0;
imgui.style_mut().popup_border_size = 1.0;
imgui.style_mut().frame_border_size = 1.0;
imgui.style_mut().tab_border_size = 0.0;
imgui.style_mut().window_rounding = 0.0;
imgui.style_mut().child_rounding = 0.0;
imgui.style_mut().frame_rounding = 0.0;
imgui.style_mut().popup_rounding = 0.0;
imgui.style_mut().scrollbar_rounding = 0.0;
imgui.style_mut().grab_rounding = 0.0;
imgui.style_mut().tab_rounding = 0.0;
imgui.style_mut().window_title_align = [0.50, 0.50];
imgui.style_mut().window_rounding = 0.0;
let colors = &mut imgui.style_mut().colors;
colors[ImGuiCol_Text as usize] = [0.90, 0.90, 0.90, 1.00];
colors[ImGuiCol_TextDisabled as usize] = [0.50, 0.50, 0.50, 1.00];
colors[ImGuiCol_WindowBg as usize] = [0.05, 0.05, 0.05, 0.60];
colors[ImGuiCol_ChildBg as usize] = [0.05, 0.05, 0.05, 0.60];
colors[ImGuiCol_PopupBg as usize] = [0.00, 0.00, 0.00, 0.60];
colors[ImGuiCol_Border as usize] = [0.40, 0.40, 0.40, 1.00];
colors[ImGuiCol_BorderShadow as usize] = [1.00, 1.00, 1.00, 0.00];
colors[ImGuiCol_FrameBg as usize] = [0.00, 0.00, 0.00, 0.60];
colors[ImGuiCol_FrameBgHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_FrameBgActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_TitleBg as usize] = [0.06, 0.06, 0.06, 1.00];
colors[ImGuiCol_TitleBgActive as usize] = [0.00, 0.00, 0.00, 1.00];
colors[ImGuiCol_TitleBgCollapsed as usize] = [0.06, 0.06, 0.06, 0.40];
colors[ImGuiCol_MenuBarBg as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_ScrollbarBg as usize] = [0.14, 0.14, 0.14, 0.40];
colors[ImGuiCol_ScrollbarGrab as usize] = [0.31, 0.31, 0.31, 0.30];
colors[ImGuiCol_ScrollbarGrabHovered as usize] = [1.00, 1.00, 1.00, 0.30];
colors[ImGuiCol_ScrollbarGrabActive as usize] = [1.00, 1.00, 1.00, 0.50];
colors[ImGuiCol_CheckMark as usize] = [0.90, 0.90, 0.90, 1.00];
colors[ImGuiCol_SliderGrab as usize] = [0.31, 0.31, 0.31, 1.00];
colors[ImGuiCol_SliderGrabActive as usize] = [1.00, 1.00, 1.00, 0.50];
colors[ImGuiCol_Button as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_ButtonHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_ButtonActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_Header as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_HeaderHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_HeaderActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_Separator as usize] = [0.50, 0.50, 0.43, 0.50];
colors[ImGuiCol_SeparatorHovered as usize] = [0.75, 0.45, 0.10, 0.78];
colors[ImGuiCol_SeparatorActive as usize] = [0.75, 0.45, 0.10, 1.00];
colors[ImGuiCol_ResizeGrip as usize] = [0.98, 0.65, 0.26, 0.25];
colors[ImGuiCol_ResizeGripHovered as usize] = [0.98, 0.65, 0.26, 0.67];
colors[ImGuiCol_ResizeGripActive as usize] = [0.98, 0.65, 0.26, 0.95];
colors[ImGuiCol_Tab as usize] = [0.17, 0.10, 0.04, 0.94];
colors[ImGuiCol_TabHovered as usize] = [0.84, 0.37, 0.00, 0.60];
colors[ImGuiCol_TabActive as usize] = [0.67, 0.30, 0.00, 0.68];
colors[ImGuiCol_TabUnfocused as usize] = [0.06, 0.05, 0.05, 0.69];
colors[ImGuiCol_TabUnfocusedActive as usize] = [0.36, 0.17, 0.03, 0.64];
colors[ImGuiCol_PlotLines as usize] = [0.39, 0.39, 0.39, 1.00];
colors[ImGuiCol_PlotLinesHovered as usize] = [0.35, 0.92, 1.00, 1.00];
colors[ImGuiCol_PlotHistogram as usize] = [0.00, 0.20, 0.90, 1.00];
colors[ImGuiCol_PlotHistogramHovered as usize] = [0.00, 0.40, 1.00, 1.00];
colors[ImGuiCol_TextSelectedBg as usize] = [0.98, 0.65, 0.26, 0.35];
colors[ImGuiCol_DragDropTarget as usize] = [0.00, 0.00, 1.00, 0.90];
colors[ImGuiCol_NavHighlight as usize] = [0.98, 0.65, 0.26, 1.00];
colors[ImGuiCol_NavWindowingHighlight as usize] = [0.00, 0.00, 0.00, 0.70];
colors[ImGuiCol_NavWindowingDimBg as usize] = [0.20, 0.20, 0.20, 0.20];
colors[ImGuiCol_ModalWindowDimBg as usize] = [0.20, 0.20, 0.20, 0.35];
let mut platform = WinitPlatform::init(&mut imgui);
platform.attach_window(imgui.io_mut(), graphics::window(ctx).window(), HiDpiMode::Rounded);
let (factory, dev, _, depth, color) = graphics::gfx_objects(ctx);
let shaders = {
let version = dev.get_info().shading_language;
if version.is_embedded {
if version.major >= 3 {
Shaders::GlSlEs300
} else {
Shaders::GlSlEs100
}
} else if version.major >= 4 {
Shaders::GlSl400
} else if version.major >= 3 {
if version.minor >= 2 {
Shaders::GlSl150
} else {
Shaders::GlSl130
}
} else {
Shaders::GlSl110
}
};
let renderer = Renderer::init(&mut imgui, factory, shaders)
.map_err(|e| RenderError(e.to_string()))?;
Ok(Self {
imgui,
platform,
renderer,
components: Components {
live_debugger: LiveDebugger::new(),
},
main_color: RenderTargetView::new(color),
main_depth: DepthStencilView::new(depth),
last_frame: Instant::now(),
})
}
pub fn handle_events(&mut self, ctx: &mut Context, event: &winit::event::Event<()>) {
self.platform.handle_event(self.imgui.io_mut(), graphics::window(ctx).window(), &event);
}
pub fn draw(&mut self, state: &mut SharedGameState, ctx: &mut Context, scene: &mut Box<dyn Scene>) -> GameResult {
{
let io = self.imgui.io_mut();
self.platform.prepare_frame(io, graphics::window(ctx).window())
.map_err(|e| RenderError(e.to_string()))?;
let now = Instant::now();
io.update_delta_time(now - self.last_frame);
self.last_frame = now;
}
let mut ui = self.imgui.frame();
scene.debug_overlay_draw(&mut self.components, state, ctx, &mut ui)?;
self.platform.prepare_render(&ui, graphics::window(ctx).window());
let draw_data = ui.render();
let (factory, dev, encoder, _, _) = graphics::gfx_objects(ctx);
self.renderer
.render(factory, encoder, &mut self.main_color, draw_data)
.map_err(|e| RenderError(e.to_string()))?;
encoder.flush(dev);
Ok(())
}
}

View File

@ -384,7 +384,7 @@ impl Weapon {
fn tick_spur(&mut self, player: &mut Player, player_id: TargetPlayer, inventory: &mut Inventory, bullet_manager: &mut BulletManager, state: &mut SharedGameState) {
let mut shoot = false;
let mut btype = 0;
let mut btype;
if player.controller.shoot() {
inventory.add_xp(if player.equip.has_turbocharge() { 3 } else { 2 }, player, state);
@ -443,8 +443,8 @@ impl Weapon {
WeaponLevel::None => unreachable!(),
}
const bullets: [u16; 6] = [44, 45, 46, 47, 48, 49];
if bullet_manager.count_bullets_multi(&bullets, player_id) == 0
const BULLETS: [u16; 6] = [44, 45, 46, 47, 48, 49];
if bullet_manager.count_bullets_multi(&BULLETS, player_id) == 0
&& (player.controller.trigger_shoot() || shoot) {
if !self.consume_ammo(1) {
state.sound_manager.play_sfx(37);