From 68cf299e9687d387586a260573c5d0200d4684af Mon Sep 17 00:00:00 2001 From: Alula <6276139+alula@users.noreply.github.com> Date: Thu, 14 Oct 2021 06:54:11 +0200 Subject: [PATCH] netplay shit, visual tweaks --- Cargo.toml | 11 +- app/app/build.gradle | 3 +- drsandroid/src/lib.rs | 4 +- src/common.rs | 2 + src/components/hud.rs | 12 ++- src/engine_constants/mod.rs | 2 +- src/framework/backend.rs | 6 +- src/framework/backend_null.rs | 4 +- src/framework/context.rs | 4 +- src/framework/error.rs | 6 +- src/lib.rs | 17 +++- src/main.rs | 15 ++- src/netplay/mod.rs | 3 + src/netplay/packets.rs | 8 ++ src/netplay/server.rs | 34 +++++++ src/netplay/server_config.rs | 10 ++ src/player/mod.rs | 74 ++++++++------ src/player/player_list.rs | 13 +++ src/scene/game_scene.rs | 17 ++-- src/scene/loading_scene.rs | 7 +- src/settings.rs | 30 ++++-- src/sound/mod.rs | 47 ++++++++- src/text_script.rs | 2 - src/texture_set.rs | 187 +++++++++++++++++++++++++++------- src/weapon/mod.rs | 6 ++ 25 files changed, 418 insertions(+), 106 deletions(-) create mode 100644 src/netplay/packets.rs create mode 100644 src/netplay/server.rs create mode 100644 src/netplay/server_config.rs create mode 100644 src/player/player_list.rs diff --git a/Cargo.toml b/Cargo.toml index 614420a..cbe1c93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ bench = false required-features = ["exe"] [profile.release] -lto = 'thin' -panic = 'abort' +lto = "off" +panic = "abort" [profile.dev.package."*"] opt-level = 3 @@ -33,13 +33,13 @@ category = "Game" osx_minimum_system_version = "10.12" [features] -default = ["scripting", "backend-sdl", "render-opengl", "ogg-playback", "exe"] +default = ["scripting", "backend-sdl", "render-opengl", "ogg-playback", "exe", "netplay"] ogg-playback = ["lewton"] backend-sdl = ["sdl2", "sdl2-sys"] backend-glutin = ["winit", "glutin", "render-opengl"] render-opengl = [] scripting = ["lua-ffi"] -netplay = [] +netplay = ["tokio", "serde_cbor"] editor = [] hooks = ["libc"] exe = [] @@ -73,11 +73,12 @@ sdl2 = { version = "=0.34.2", optional = true, features = ["unsafe_textures", "b sdl2-sys = { version = "=0.34.2", optional = true, features = ["bundled", "static-link"] } serde = { version = "1", features = ["derive"] } serde_derive = "1" +serde_cbor = { version = "0.11.2", optional = true } serde_json = "1.0" -serde_yaml = "0.8" simple_logger = { version = "1.13" } strum = "0.20" strum_macros = "0.20" +tokio = { version = "1.12.0", features = ["net"], optional = true } # remove and replace when drain_filter is in stable vec_mut_scan = "0.4" webbrowser = "0.5.5" diff --git a/app/app/build.gradle b/app/app/build.gradle index 7d00ea2..d0dc0a3 100644 --- a/app/app/build.gradle +++ b/app/app/build.gradle @@ -6,7 +6,7 @@ plugins { android { compileSdkVersion 30 buildToolsVersion "30.0.3" - //ndkVersion "21.3.6528147" + ndkVersion "22.1.7171670" defaultConfig { applicationId "io.github.doukutsu_rs" @@ -65,6 +65,7 @@ dependencies { } println("cargo target: ${project.buildDir.getAbsolutePath()}/rust-target") +println("ndk dir: ${android.ndkDirectory}") cargoNdk { targets = [ diff --git a/drsandroid/src/lib.rs b/drsandroid/src/lib.rs index f1a2832..3639b38 100644 --- a/drsandroid/src/lib.rs +++ b/drsandroid/src/lib.rs @@ -1,5 +1,7 @@ #[cfg(target_os = "android")] #[cfg_attr(target_os = "android", ndk_glue::main())] pub fn android_main() { - doukutsu_rs::init().unwrap(); + let options = doukutsu_rs::LaunchOptions { server_mode: false }; + + doukutsu_rs::init(options).unwrap(); } diff --git a/src/common.rs b/src/common.rs index 6057b31..eca5f70 100644 --- a/src/common.rs +++ b/src/common.rs @@ -260,10 +260,12 @@ pub struct Rect { } impl Rect { + #[inline(always)] pub fn new(left: T, top: T, right: T, bottom: T) -> Rect { Rect { left, top, right, bottom } } + #[inline(always)] pub fn new_size(x: T, y: T, width: T, height: T) -> Rect { Rect { left: x, top: y, right: x.add(width), bottom: y.add(height) } } diff --git a/src/components/hud.rs b/src/components/hud.rs index 05430a3..0bebff6 100644 --- a/src/components/hud.rs +++ b/src/components/hud.rs @@ -18,6 +18,7 @@ pub struct HUD { max_ammo: u16, xp: u16, max_xp: u16, + xp_bar_counter: u8, max_level: bool, life: u16, max_life: u16, @@ -42,6 +43,7 @@ impl HUD { max_ammo: 0, xp: 0, max_xp: 0, + xp_bar_counter: 0, max_level: false, life: 0, max_life: 0, @@ -66,6 +68,7 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD { self.max_ammo = max_ammo; self.xp = xp; self.max_xp = max_xp; + self.xp_bar_counter = if player.xp_counter != 0 { self.xp_bar_counter.wrapping_add(1) } else { 0 }; self.max_level = max_level; self.life = player.life; @@ -138,7 +141,7 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD { pos_x -= 48; } - let wtype = unsafe { *self.weapon_types.get_unchecked(a) }; + let wtype = self.weapon_types[a]; if wtype != 0 { rect = Rect::new_size(pos_x + weapon_offset - 4, 16 - 4, 24, 24); @@ -203,6 +206,10 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD { batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(0, 80, bar_width, 8)); } + if (self.xp_bar_counter & 0x02) != 0 { + batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(40, 80, 40, 8)); + } + if self.max_life != 0 { let yellow_bar_width = (self.life_bar as f32 / self.max_life as f32 * 39.0) as u16; let bar_width = (self.life as f32 / self.max_life as f32 * 39.0) as u16; @@ -252,8 +259,7 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD { pos_x -= 96.0 + self.weapon_count as f32 * 16.0; } - let wtype = unsafe { *self.weapon_types.get_unchecked(a) }; - + let wtype = self.weapon_types[a]; if wtype != 0 { rect.left = wtype as u16 * 16; rect.right = rect.left + 16; diff --git a/src/engine_constants/mod.rs b/src/engine_constants/mod.rs index da0e609..66f0deb 100644 --- a/src/engine_constants/mod.rs +++ b/src/engine_constants/mod.rs @@ -480,7 +480,7 @@ impl EngineConstants { question_right_rect: Rect { left: 48, top: 64, right: 64, bottom: 80 }, }, world: WorldConsts { snack_rect: Rect { left: 256, top: 48, right: 272, bottom: 64 } }, - npc: serde_yaml::from_str("dummy: \"lol\"").unwrap(), + npc: serde_json::from_str("{}").unwrap(), weapon: WeaponConsts { bullet_table: vec![ // Null diff --git a/src/framework/backend.rs b/src/framework/backend.rs index b9e6aa1..486315f 100644 --- a/src/framework/backend.rs +++ b/src/framework/backend.rs @@ -75,7 +75,11 @@ pub trait BackendTexture { } #[allow(unreachable_code)] -pub fn init_backend() -> GameResult> { +pub fn init_backend(headless: bool) -> GameResult> { + if headless { + return crate::framework::backend_null::NullBackend::new(); + } + #[cfg(all(feature = "backend-glutin"))] { return crate::framework::backend_glutin::GlutinBackend::new(); diff --git a/src/framework/backend_null.rs b/src/framework/backend_null.rs index 64d57bb..df8fdfe 100644 --- a/src/framework/backend_null.rs +++ b/src/framework/backend_null.rs @@ -50,9 +50,9 @@ impl BackendEventLoop for NullEventLoop { game.loops = 0; state_ref.frame_time = 0.0; } - std::thread::sleep(std::time::Duration::from_millis(5)); + std::thread::sleep(std::time::Duration::from_millis(10)); - //game.draw(ctx).unwrap(); + game.draw(ctx).unwrap(); } } diff --git a/src/framework/context.rs b/src/framework/context.rs index 158d4fd..4ca2e03 100644 --- a/src/framework/context.rs +++ b/src/framework/context.rs @@ -5,6 +5,7 @@ use crate::framework::keyboard::KeyboardContext; use crate::Game; pub struct Context { + pub headless: bool, pub(crate) filesystem: Filesystem, pub(crate) renderer: Option>, pub(crate) keyboard_context: KeyboardContext, @@ -15,6 +16,7 @@ pub struct Context { impl Context { pub fn new() -> Context { Context { + headless: false, filesystem: Filesystem::new(), renderer: None, keyboard_context: KeyboardContext::new(), @@ -24,7 +26,7 @@ impl Context { } pub fn run(&mut self, game: &mut Game) -> GameResult { - let backend = init_backend()?; + let backend = init_backend(self.headless)?; let mut event_loop = backend.create_event_loop()?; self.renderer = Some(event_loop.new_renderer()?); diff --git a/src/framework/error.rs b/src/framework/error.rs index dc25a4e..74c5fd8 100644 --- a/src/framework/error.rs +++ b/src/framework/error.rs @@ -93,9 +93,9 @@ impl From for GameError { } } -impl From for GameError { - fn from(e: serde_yaml::Error) -> Self { - let errstr = format!("Yaml error: {:?}", e); +impl From for GameError { + fn from(e: serde_json::Error) -> Self { + let errstr = format!("JSON error: {:?}", e); GameError::ParseError(errstr) } } diff --git a/src/lib.rs b/src/lib.rs index d25996c..cbf458a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,10 @@ mod text_script; mod texture_set; mod weapon; +pub struct LaunchOptions { + pub server_mode: bool, +} + lazy_static! { pub static ref GAME_SUSPENDED: Mutex = Mutex::new(false); } @@ -142,6 +146,12 @@ impl Game { fn draw(&mut self, ctx: &mut Context) -> GameResult { let state_ref = unsafe { &mut *self.state.get() }; + if ctx.headless { + self.loops = 0; + state_ref.frame_time = 1.0; + return Ok(()); + } + if state_ref.timing_mode != TimingMode::FrameSynchronized { let mut elapsed = self.start_time.elapsed().as_nanos(); @@ -185,7 +195,7 @@ impl Game { } } -pub fn init() -> GameResult { +pub fn init(options: LaunchOptions) -> GameResult { let _ = simple_logger::init_with_level(log::Level::Info); #[cfg(not(target_os = "android"))] @@ -264,6 +274,11 @@ pub fn init() -> GameResult { } } + if options.server_mode { + log::info!("Running in server mode..."); + context.headless = true; + } + let game = UnsafeCell::new(Game::new(&mut context)?); let state_ref = unsafe { &mut *((&mut *game.get()).state.get()) }; #[cfg(feature = "scripting")] diff --git a/src/main.rs b/src/main.rs index b548c45..f8178e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,18 @@ use std::process::exit; fn main() { - let result = doukutsu_rs::init(); + let args = std::env::args(); + let mut options = doukutsu_rs::LaunchOptions { + server_mode: false + }; + + for arg in args { + if arg == "--server-mode" { + options.server_mode = true; + } + } + + let result = doukutsu_rs::init(options); #[cfg(target_os = "windows")] unsafe { @@ -28,7 +39,7 @@ fn main() { } if let Err(e) = result { - println!("Initialization error: {}", e); + eprintln!("Initialization error: {}", e); exit(1); } } diff --git a/src/netplay/mod.rs b/src/netplay/mod.rs index e69de29..d2d360b 100644 --- a/src/netplay/mod.rs +++ b/src/netplay/mod.rs @@ -0,0 +1,3 @@ +pub mod packets; +pub mod server; +pub mod server_config; diff --git a/src/netplay/packets.rs b/src/netplay/packets.rs new file mode 100644 index 0000000..763b7cb --- /dev/null +++ b/src/netplay/packets.rs @@ -0,0 +1,8 @@ +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(tag = "i")] +pub enum DRSPacket { + #[serde(rename = "\x01")] + Ping(u16), + #[serde(rename = "\x02")] + Pong(u16), +} diff --git a/src/netplay/server.rs b/src/netplay/server.rs new file mode 100644 index 0000000..2381947 --- /dev/null +++ b/src/netplay/server.rs @@ -0,0 +1,34 @@ +use tokio::net::UdpSocket; +use crate::framework::error::GameResult; +use crate::netplay::server_config::ServerConfiguration; + +pub struct Server { +} + +impl Server { + pub fn start(config: ServerConfiguration) -> GameResult { + let context = ServerContext::new(config); + + + Ok(Server { + + }) + } +} + +struct ServerContext { + config: ServerConfiguration, +} + +impl ServerContext { + pub fn new(config: ServerConfiguration) -> ServerContext { + ServerContext { + config + } + } + + pub async fn run(self) { + let socket = UdpSocket::bind(&self.config.bind_to).await.unwrap(); + + } +} diff --git a/src/netplay/server_config.rs b/src/netplay/server_config.rs new file mode 100644 index 0000000..e7aee16 --- /dev/null +++ b/src/netplay/server_config.rs @@ -0,0 +1,10 @@ +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ServerConfiguration { + #[serde(default = "default_bind")] + pub bind_to: String, +} + +// 'RS' = 0x5253 = 21075 +fn default_bind() -> String { + "0.0.0.0:21075".to_string() +} diff --git a/src/player/mod.rs b/src/player/mod.rs index 48fc414..48257c7 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -4,7 +4,8 @@ use num_derive::FromPrimitive; use num_traits::clamp; use crate::caret::CaretType; -use crate::common::{Condition, Direction, Equipment, Flag, interpolate_fix9_scale, Rect}; +use crate::common::{interpolate_fix9_scale, Condition, Direction, Equipment, Flag, Rect}; +use crate::components::number_popup::NumberPopup; use crate::entity::GameEntity; use crate::frame::Frame; use crate::framework::context::Context; @@ -13,13 +14,13 @@ use crate::input::dummy_player_controller::DummyPlayerController; use crate::input::player_controller::PlayerController; use crate::npc::list::NPCList; use crate::npc::NPC; -use crate::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin}; use crate::player::skin::basic::BasicPlayerSkin; +use crate::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin}; use crate::rng::RNG; use crate::shared_game_state::SharedGameState; -use crate::components::number_popup::NumberPopup; mod player_hit; +pub mod player_list; pub mod skin; #[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive)] @@ -42,6 +43,14 @@ impl TargetPlayer { } } +#[derive(PartialEq, Eq, Copy, Clone)] +enum BoosterSwitch { + None, + Up, + Sides, + Down, +} + #[derive(Clone)] pub struct Player { pub x: i32, @@ -68,6 +77,7 @@ pub struct Player { pub up: bool, pub down: bool, pub shock_counter: u8, + pub xp_counter: u8, pub current_weapon: u8, pub stars: u8, pub damage: u16, @@ -79,7 +89,7 @@ pub struct Player { weapon_offset_y: i8, splash: bool, tick: u8, - booster_switch: u8, + booster_switch: BoosterSwitch, damage_counter: u16, damage_taken: i16, pub anim_num: u16, @@ -121,8 +131,9 @@ impl Player { current_weapon: 0, weapon_offset_y: 0, shock_counter: 0, + xp_counter: 0, tick: 0, - booster_switch: 0, + booster_switch: BoosterSwitch::None, stars: 0, damage: 0, air_counter: 0, @@ -185,12 +196,12 @@ impl Player { self.question = false; if !state.control_flags.control_enabled() { - self.booster_switch = 0; + self.booster_switch = BoosterSwitch::None; } // ground movement if self.flags.hit_bottom_wall() || self.flags.hit_right_slope() || self.flags.hit_left_slope() { - self.booster_switch = 0; + self.booster_switch = BoosterSwitch::None; if state.settings.infinite_booster { self.booster_fuel = u32::MAX; @@ -258,31 +269,30 @@ impl Player { if state.control_flags.control_enabled() { if self.controller.trigger_jump() && self.booster_fuel != 0 { if self.equip.has_booster_0_8() { - self.booster_switch = 1; + self.booster_switch = BoosterSwitch::Sides; if self.vel_y > 0x100 { - // 0.5fix9 self.vel_y /= 2; } } else if state.settings.infinite_booster || self.equip.has_booster_2_0() { if self.controller.move_up() { - self.booster_switch = 2; + self.booster_switch = BoosterSwitch::Up; self.vel_x = 0; self.vel_y = state.constants.booster.b2_0_up; } else if self.controller.move_left() { - self.booster_switch = 1; + self.booster_switch = BoosterSwitch::Sides; self.vel_x = state.constants.booster.b2_0_left; self.vel_y = 0; } else if self.controller.move_right() { - self.booster_switch = 1; + self.booster_switch = BoosterSwitch::Sides; self.vel_x = state.constants.booster.b2_0_right; self.vel_y = 0; } else if self.controller.move_down() { - self.booster_switch = 3; + self.booster_switch = BoosterSwitch::Down; self.vel_x = 0; self.vel_y = state.constants.booster.b2_0_down; } else { - self.booster_switch = 2; + self.booster_switch = BoosterSwitch::Up; self.vel_x = 0; self.vel_y = state.constants.booster.b2_0_up_nokey; } @@ -307,18 +317,18 @@ impl Player { } if (state.settings.infinite_booster || self.equip.has_booster_2_0()) - && self.booster_switch != 0 + && self.booster_switch != BoosterSwitch::None && (!self.controller.jump() || self.booster_fuel == 0) { match self.booster_switch { - 1 => self.vel_x /= 2, - 2 => self.vel_y /= 2, - _ => {} + BoosterSwitch::Sides => self.vel_x /= 2, + BoosterSwitch::Up => self.vel_y /= 2, + _ => (), } } if self.booster_fuel == 0 || !self.controller.jump() { - self.booster_switch = 0; + self.booster_switch = BoosterSwitch::None; } } @@ -348,7 +358,7 @@ impl Player { } // booster losing fuel - if self.booster_switch != 0 && self.booster_fuel != 0 { + if self.booster_switch != BoosterSwitch::None && self.booster_fuel != 0 { self.booster_fuel -= 1; } @@ -367,18 +377,18 @@ impl Player { self.vel_y += 0x55; } - if (state.settings.infinite_booster || self.equip.has_booster_2_0()) && self.booster_switch != 0 { + if (state.settings.infinite_booster || self.equip.has_booster_2_0()) && self.booster_switch != BoosterSwitch::None { match self.booster_switch { - 1 => { + BoosterSwitch::Sides => { if self.flags.hit_left_wall() || self.flags.hit_right_wall() { - self.vel_y = -0x100; // -0.5fix9 + self.vel_y = -0x100; } if self.direction == Direction::Left { - self.vel_x -= 0x20; // 0.1fix9 + self.vel_x -= 0x20; } if self.direction == Direction::Right { - self.vel_x += 0x20; // 0.1fix9 + self.vel_x += 0x20; } if self.controller.trigger_jump() || self.booster_fuel % 3 == 1 { @@ -393,7 +403,7 @@ impl Player { state.sound_manager.play_sfx(113); } } - 2 => { + BoosterSwitch::Up => { self.vel_y -= 0x20; if self.controller.trigger_jump() || self.booster_fuel % 3 == 1 { @@ -401,7 +411,7 @@ impl Player { state.sound_manager.play_sfx(113); } } - 3 if self.controller.trigger_jump() || self.booster_fuel % 3 == 1 => { + BoosterSwitch::Down if self.controller.trigger_jump() || self.booster_fuel % 3 == 1 => { state.create_caret(self.x, self.y + 0xc00, CaretType::Exhaust, Direction::Up); state.sound_manager.play_sfx(113); } @@ -409,7 +419,7 @@ impl Player { } } else if self.flags.force_up() { self.vel_y += physics.gravity_air; - } else if self.equip.has_booster_0_8() && self.booster_switch != 0 && self.vel_y > -0x400 { + } else if self.equip.has_booster_0_8() && self.booster_switch != BoosterSwitch::None && self.vel_y > -0x400 { self.vel_y -= 0x20; if self.booster_fuel % 3 == 0 { @@ -725,13 +735,16 @@ impl GameEntity<&NPCList> for Player { self.damage_counter -= 1; } + if self.xp_counter != 0 { + self.xp_counter -= 1; + } + if self.shock_counter != 0 { self.shock_counter -= 1; } else if self.damage_taken != 0 { self.damage_taken = 0; } - // todo: add additional control modes like NXEngine has such as noclip? match self.control_mode { ControlMode::Normal => self.tick_normal(state, npc_list)?, ControlMode::IronHead => self.tick_ironhead(state)?, @@ -813,7 +826,8 @@ impl GameEntity<&NPCList> for Player { self.prev_y - self.display_bounds.top as i32, self.y - self.display_bounds.top as i32, state.frame_time, - ) + self.weapon_offset_y as f32 + gun_off_y as f32 + ) + self.weapon_offset_y as f32 + + gun_off_y as f32 - frame_y, &self.weapon_rect, ); diff --git a/src/player/player_list.rs b/src/player/player_list.rs new file mode 100644 index 0000000..e1689c8 --- /dev/null +++ b/src/player/player_list.rs @@ -0,0 +1,13 @@ +use crate::player::Player; + +pub struct RemotePlayerList { + +} + +impl RemotePlayerList { + pub fn new() -> RemotePlayerList { + RemotePlayerList { + + } + } +} diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 30da5dc..1e39c73 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -33,7 +33,7 @@ use crate::scene::Scene; use crate::shared_game_state::{SharedGameState, TileSize}; use crate::stage::{BackgroundType, Stage}; use crate::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptLine, TextScriptVM}; -use crate::texture_set::SizedBatch; +use crate::texture_set::SpriteBatch; use crate::weapon::bullet::BulletManager; use crate::weapon::{Weapon, WeaponType}; @@ -661,7 +661,7 @@ impl GameScene { Ok(()) } - fn draw_light(&self, x: f32, y: f32, size: f32, color: (u8, u8, u8), batch: &mut SizedBatch) { + fn draw_light(&self, x: f32, y: f32, size: f32, color: (u8, u8, u8), batch: &mut Box) { batch.add_rect_scaled_tinted( x - size * 32.0, y - size * 32.0, @@ -680,7 +680,7 @@ impl GameScene { (br, bg, bb): (u8, u8, u8), att: f32, angle: Range, - batch: &mut SizedBatch, + batch: &mut Box, ) { let px = world_point_x as f32 / 512.0; let py = world_point_y as f32 / 512.0; @@ -2001,11 +2001,12 @@ impl Scene for GameScene { } } - self.inventory_dim += 0.1 * if state.textscript_vm.mode == ScriptMode::Inventory { - state.frame_time as f32 - } else { - -(state.frame_time as f32) - }; + self.inventory_dim += 0.1 + * if state.textscript_vm.mode == ScriptMode::Inventory { + state.frame_time as f32 + } else { + -(state.frame_time as f32) + }; self.inventory_dim = self.inventory_dim.clamp(0.0, 1.0); diff --git a/src/scene/loading_scene.rs b/src/scene/loading_scene.rs index fa95491..8450fd0 100644 --- a/src/scene/loading_scene.rs +++ b/src/scene/loading_scene.rs @@ -37,7 +37,12 @@ impl LoadingScene { let stage_select_script = TextScript::load_from(stage_select_tsc, &state.constants)?; state.textscript_vm.set_stage_select_script(stage_select_script); - state.start_intro(ctx)?; + if ctx.headless { + log::info!("Headless mode detected, skipping intro and loading last saved game."); + state.load_or_start_game(ctx)?; + } else { + state.start_intro(ctx)?; + } Ok(()) } diff --git a/src/settings.rs b/src/settings.rs index 7034051..d4417b4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -43,7 +43,7 @@ pub struct Settings { fn default_true() -> bool { true } #[inline(always)] -fn current_version() -> u32 { 2 } +fn current_version() -> u32 { 3 } #[inline(always)] fn default_interpolation() -> InterpolationMode { InterpolationMode::Linear } @@ -55,9 +55,9 @@ fn default_speed() -> f64 { impl Settings { pub fn load(ctx: &Context) -> GameResult { - if let Ok(file) = user_open(ctx, "/settings.yml") { - match serde_yaml::from_reader::<_, Settings>(file) { - Ok(settings) => return Ok(settings), + if let Ok(file) = user_open(ctx, "/settings.json") { + match serde_json::from_reader::<_, Settings>(file) { + Ok(settings) => return Ok(settings.upgrade()), Err(err) => log::warn!("Failed to deserialize settings: {}", err), } } @@ -65,9 +65,25 @@ impl Settings { Ok(Settings::default()) } + fn upgrade(mut self) -> Self { + let initial_version = self.version; + + if self.version == 2 { + self.version = 3; + self.light_cone = true; + } + + if self.version != initial_version { + log::info!("Upgraded configuration file from version {} to {}.", initial_version, self.version); + } + + self + } + pub fn save(&self, ctx: &Context) -> GameResult { - let file = user_create(ctx, "/settings.yml")?; - serde_yaml::to_writer(file, self)?; + let file = user_create(ctx, "/settings.json")?; + serde_json::to_writer(file, self)?; + Ok(()) } @@ -87,7 +103,7 @@ impl Settings { impl Default for Settings { fn default() -> Self { Settings { - version: 2, + version: current_version(), seasonal_textures: true, original_textures: false, shader_effects: true, diff --git a/src/sound/mod.rs b/src/sound/mod.rs index 6e5ec68..c1107d0 100644 --- a/src/sound/mod.rs +++ b/src/sound/mod.rs @@ -41,6 +41,7 @@ pub struct SoundManager { tx: Sender, prev_song_id: usize, current_song_id: usize, + no_audio: bool, } enum SongFormat { @@ -64,6 +65,12 @@ impl SoundManager { pub fn new(ctx: &mut Context) -> GameResult { let (tx, rx): (Sender, Receiver) = mpsc::channel(); + if ctx.headless { + log::info!("Running in headless mode, skipping initialization."); + + return Ok(SoundManager { tx: tx.clone(), prev_song_id: 0, current_song_id: 0, no_audio: true }); + } + let host = cpal::default_host(); let device = host.default_output_device().ok_or_else(|| AudioError(str!("Error initializing audio device.")))?; @@ -81,22 +88,34 @@ impl SoundManager { } }); - Ok(SoundManager { tx: tx.clone(), prev_song_id: 0, current_song_id: 0 }) + Ok(SoundManager { tx: tx.clone(), prev_song_id: 0, current_song_id: 0, no_audio: false }) } pub fn play_sfx(&self, id: u8) { + if self.no_audio { + return; + } let _ = self.tx.send(PlaybackMessage::PlaySample(id)); } pub fn loop_sfx(&self, id: u8) { + if self.no_audio { + return; + } let _ = self.tx.send(PlaybackMessage::LoopSample(id)); } pub fn stop_sfx(&self, id: u8) { + if self.no_audio { + return; + } let _ = self.tx.send(PlaybackMessage::StopSample(id)); } pub fn set_org_interpolation(&self, interpolation: InterpolationMode) { + if self.no_audio { + return; + } let _ = self.tx.send(PlaybackMessage::SetOrgInterpolation(interpolation)); } @@ -107,7 +126,7 @@ impl SoundManager { settings: &Settings, ctx: &mut Context, ) -> GameResult { - if self.current_song_id == song_id { + if self.current_song_id == song_id || self.no_audio { return Ok(()); } @@ -157,7 +176,8 @@ impl SoundManager { self.prev_song_id = self.current_song_id; self.current_song_id = song_id; - self.tx.send(PlaybackMessage::SetOrgInterpolation(settings.organya_interpolation))?; + self.tx + .send(PlaybackMessage::SetOrgInterpolation(settings.organya_interpolation))?; self.tx.send(PlaybackMessage::SaveState)?; self.tx.send(PlaybackMessage::PlayOrganyaSong(Box::new(org)))?; @@ -237,6 +257,10 @@ impl SoundManager { } pub fn save_state(&mut self) -> GameResult { + if self.no_audio { + return Ok(()); + } + self.tx.send(PlaybackMessage::SaveState)?; self.prev_song_id = self.current_song_id; @@ -244,6 +268,10 @@ impl SoundManager { } pub fn restore_state(&mut self) -> GameResult { + if self.no_audio { + return Ok(()); + } + self.tx.send(PlaybackMessage::RestoreState)?; self.current_song_id = self.prev_song_id; @@ -251,9 +279,14 @@ impl SoundManager { } pub fn set_speed(&self, speed: f32) -> GameResult { + if self.no_audio { + return Ok(()); + } + if speed <= 0.0 { return Err(InvalidValue(str!("Speed must be bigger than 0.0!"))); } + self.tx.send(PlaybackMessage::SetSpeed(speed))?; Ok(()) @@ -264,6 +297,10 @@ impl SoundManager { } pub fn set_sample_params_from_file(&self, id: u8, data: R) -> GameResult { + if self.no_audio { + return Ok(()); + } + let mut reader = BufReader::new(data).lines(); let mut params = PixToneParameters::empty(); @@ -325,6 +362,10 @@ impl SoundManager { } pub fn set_sample_params(&self, id: u8, params: PixToneParameters) -> GameResult { + if self.no_audio { + return Ok(()); + } + self.tx.send(PlaybackMessage::SetSampleParams(id, params))?; Ok(()) diff --git a/src/text_script.rs b/src/text_script.rs index e4fadb3..48f0f9b 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -1845,8 +1845,6 @@ impl TextScript { /// Compiles a decrypted text script data into internal bytecode. pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult { - log::info!("data: {}", String::from_utf8_lossy(data)); - let mut event_map = HashMap::new(); let mut iter = data.iter().copied().peekable(); let mut last_event = 0; diff --git a/src/texture_set.rs b/src/texture_set.rs index 99816ef..11ad06b 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -20,6 +20,117 @@ use crate::str; pub static mut I_MAG: f32 = 1.0; pub static mut G_MAG: f32 = 1.0; +pub trait SpriteBatch { + fn width(&self) -> usize; + + fn height(&self) -> usize; + + fn dimensions(&self) -> (usize, usize); + + fn real_dimensions(&self) -> (usize, usize); + + fn scale(&self) -> (f32, f32); + + fn has_glow_layer(&self) -> bool; + + fn has_normal_layer(&self) -> bool; + + fn to_rect(&self) -> common::Rect; + + fn clear(&mut self); + + fn add(&mut self, x: f32, y: f32); + + fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect); + + fn add_rect_flip(&mut self, x: f32, y: f32, flip_x: bool, flip_y: bool, rect: &common::Rect); + + fn add_rect_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), rect: &common::Rect); + + fn add_rect_scaled(&mut self, x: f32, y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect); + + 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, + ); + + fn draw(&mut self, ctx: &mut Context) -> GameResult; + + fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult; +} + +pub struct DummyBatch; + +impl SpriteBatch for DummyBatch { + fn width(&self) -> usize { + 1 + } + + fn height(&self) -> usize { + 1 + } + + fn dimensions(&self) -> (usize, usize) { + (1, 1) + } + + fn real_dimensions(&self) -> (usize, usize) { + (1, 1) + } + + fn scale(&self) -> (f32, f32) { + (1.0, 1.0) + } + + fn has_glow_layer(&self) -> bool { + false + } + + fn has_normal_layer(&self) -> bool { + false + } + + fn to_rect(&self) -> Rect { + Rect::new(0, 0, 1, 1) + } + + fn clear(&mut self) {} + + fn add(&mut self, _x: f32, _y: f32) {} + + fn add_rect(&mut self, _x: f32, _y: f32, _rect: &Rect) {} + + fn add_rect_flip(&mut self, _x: f32, _y: f32, _flip_x: bool, _flip_y: bool, _rect: &Rect) {} + + fn add_rect_tinted(&mut self, _x: f32, _y: f32, _color: (u8, u8, u8, u8), _rect: &Rect) {} + + fn add_rect_scaled(&mut self, _x: f32, _y: f32, _scale_x: f32, _scale_y: f32, _rect: &Rect) {} + + fn add_rect_scaled_tinted( + &mut self, + _x: f32, + _y: f32, + _color: (u8, u8, u8, u8), + _scale_x: f32, + _scale_y: f32, + _rect: &Rect, + ) { + } + + fn draw(&mut self, _ctx: &mut Context) -> GameResult { + Ok(()) + } + + fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult { + Ok(()) + } +} + pub struct SizedBatch { batch: Box, width: usize, @@ -32,53 +143,53 @@ pub struct SizedBatch { has_normal_layer: bool, } -impl SizedBatch { +impl SpriteBatch for SizedBatch { #[inline(always)] - pub fn width(&self) -> usize { + fn width(&self) -> usize { self.width } #[inline(always)] - pub fn height(&self) -> usize { + fn height(&self) -> usize { self.height } #[inline(always)] - pub fn dimensions(&self) -> (usize, usize) { + fn dimensions(&self) -> (usize, usize) { (self.width, self.height) } #[inline(always)] - pub fn real_dimensions(&self) -> (usize, usize) { + fn real_dimensions(&self) -> (usize, usize) { (self.real_width, self.real_height) } #[inline(always)] - pub fn scale(&self) -> (f32, f32) { + fn scale(&self) -> (f32, f32) { (self.scale_x, self.scale_y) } #[inline(always)] - pub fn has_glow_layer(&self) -> bool { + fn has_glow_layer(&self) -> bool { self.has_glow_layer } #[inline(always)] - pub fn has_normal_layer(&self) -> bool { + fn has_normal_layer(&self) -> bool { self.has_normal_layer } #[inline(always)] - pub fn to_rect(&self) -> common::Rect { + fn to_rect(&self) -> common::Rect { common::Rect::::new(0, 0, self.width, self.height) } #[inline(always)] - pub fn clear(&mut self) { + fn clear(&mut self) { self.batch.clear(); } - pub fn add(&mut self, x: f32, y: f32) { + fn add(&mut self, x: f32, y: f32) { let mag = unsafe { I_MAG }; self.batch.add(SpriteBatchCommand::DrawRect( @@ -93,11 +204,11 @@ impl SizedBatch { } #[inline(always)] - pub fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect) { + fn add_rect(&mut self, x: f32, y: f32, rect: &common::Rect) { self.add_rect_scaled(x, y, 1.0, 1.0, rect) } - pub fn add_rect_flip(&mut self, x: f32, y: f32, flip_x: bool, flip_y: bool, rect: &common::Rect) { + fn add_rect_flip(&mut self, x: f32, y: f32, flip_x: bool, flip_y: bool, rect: &common::Rect) { if (rect.right - rect.left) == 0 || (rect.bottom - rect.top) == 0 { return; } @@ -123,11 +234,11 @@ impl SizedBatch { } #[inline(always)] - pub fn add_rect_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), rect: &common::Rect) { + fn add_rect_tinted(&mut self, x: f32, y: f32, color: (u8, u8, u8, u8), rect: &common::Rect) { self.add_rect_scaled_tinted(x, y, color, 1.0, 1.0, rect) } - pub fn add_rect_scaled(&mut self, x: f32, y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect) { + fn add_rect_scaled(&mut self, x: f32, y: f32, scale_x: f32, scale_y: f32, rect: &common::Rect) { if (rect.right.saturating_sub(rect.left)) == 0 || (rect.bottom.saturating_sub(rect.top)) == 0 { return; } @@ -150,7 +261,7 @@ impl SizedBatch { )); } - pub fn add_rect_scaled_tinted( + fn add_rect_scaled_tinted( &mut self, x: f32, y: f32, @@ -178,11 +289,11 @@ impl SizedBatch { } #[inline(always)] - pub fn draw(&mut self, ctx: &mut Context) -> GameResult { + fn draw(&mut self, ctx: &mut Context) -> GameResult { self.draw_filtered(FilterMode::Nearest, ctx) } - pub fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult { + fn draw_filtered(&mut self, _filter: FilterMode, _ctx: &mut Context) -> GameResult { //self.batch.set_filter(filter); self.batch.draw()?; self.batch.clear(); @@ -191,13 +302,18 @@ impl SizedBatch { } pub struct TextureSet { - pub tex_map: HashMap, + pub tex_map: HashMap>, pub paths: Vec, + dummy_batch: Box, } impl TextureSet { pub fn new(base_path: &str) -> TextureSet { - TextureSet { tex_map: HashMap::new(), paths: vec![base_path.to_string(), "".to_string()] } + TextureSet { + tex_map: HashMap::new(), + paths: vec![base_path.to_string(), "".to_string()], + dummy_batch: Box::new(DummyBatch), + } } pub fn apply_seasonal_content(&mut self, season: Season, settings: &Settings) { @@ -239,14 +355,17 @@ impl TextureSet { create_texture(ctx, width as u16, height as u16, &img) } - pub fn load_texture(&self, ctx: &mut Context, constants: &EngineConstants, name: &str) -> GameResult { + pub fn load_texture( + &self, + ctx: &mut Context, + constants: &EngineConstants, + name: &str, + ) -> GameResult> { let path = self .paths .iter() .find_map(|s| { - FILE_TYPES.iter().map(|ext| [s, name, ext].join("")).find(|path| { - filesystem::exists(ctx, path) - }) + FILE_TYPES.iter().map(|ext| [s, name, ext].join("")).find(|path| filesystem::exists(ctx, path)) }) .ok_or_else(|| GameError::ResourceLoadError(format!("Texture {} does not exist.", name)))?; @@ -254,25 +373,21 @@ impl TextureSet { .paths .iter() .find_map(|s| { - FILE_TYPES.iter().map(|ext| [s, name, ".glow", ext].join("")).find(|path| { - filesystem::exists(ctx, path) - }) - }).is_some(); + FILE_TYPES.iter().map(|ext| [s, name, ".glow", ext].join("")).find(|path| filesystem::exists(ctx, path)) + }) + .is_some(); info!("Loading texture: {}", path); let batch = self.load_image(ctx, &path)?; let size = batch.dimensions(); - assert_ne!(size.0 as isize, 0, "size.width == 0"); - assert_ne!(size.1 as isize, 0, "size.height == 0"); - 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 { + Ok(Box::new(SizedBatch { batch, width, height, @@ -282,7 +397,7 @@ impl TextureSet { real_height: size.1 as usize, has_glow_layer, has_normal_layer: false, - }) + })) } pub fn get_or_load_batch( @@ -290,7 +405,11 @@ impl TextureSet { ctx: &mut Context, constants: &EngineConstants, name: &str, - ) -> GameResult<&mut SizedBatch> { + ) -> GameResult<&mut Box> { + if ctx.headless { + return Ok(&mut self.dummy_batch); + } + if !self.tex_map.contains_key(name) { let batch = self.load_texture(ctx, constants, name)?; self.tex_map.insert(str!(name), batch); diff --git a/src/weapon/mod.rs b/src/weapon/mod.rs index 2114bc6..b899b62 100644 --- a/src/weapon/mod.rs +++ b/src/weapon/mod.rs @@ -136,6 +136,12 @@ impl Weapon { state.create_caret(player.x, player.y, CaretType::LevelUp, Direction::Left); } } + + player.xp_counter = if self.wtype != WeaponType::Spur { + 30 + } else { + 10 + }; } pub fn reset_xp(&mut self) {