diff --git a/Cargo.toml b/Cargo.toml index 696a5a9..b158cdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ opt-level = 1 [features] default = ["scripting", "backend-sdl"] -backend-sdl = ["sdl2"] +backend-sdl = ["sdl2", "sdl2-sys"] backend-gfx = ["winit", "imgui-gfx-renderer", "imgui-winit-support"] scripting = ["lua-ffi"] editor = [] @@ -66,7 +66,8 @@ num-derive = "0.3.2" num-traits = "0.2.12" paste = "1.0.0" pretty_env_logger = "0.4.0" -sdl2 = { version = "0.34.3", optional = true, features = ["unsafe_textures", "bundled"] } +sdl2 = { version = "0.34.3", optional = true, features = ["unsafe_textures", "bundled", "static-link"] } +sdl2-sys = { version = "0.34", optional = true } serde = { version = "1", features = ["derive"] } serde_derive = "1" serde_yaml = "0.8" diff --git a/src/framework/backend.rs b/src/framework/backend.rs index 1fcfda4..c224faf 100644 --- a/src/framework/backend.rs +++ b/src/framework/backend.rs @@ -2,6 +2,7 @@ use crate::common::{Color, Rect, Point}; use crate::framework::context::Context; use crate::framework::error::GameResult; use crate::Game; +use crate::framework::graphics::BlendMode; pub trait Backend { fn create_event_loop(&self) -> GameResult>; @@ -18,7 +19,13 @@ pub trait BackendRenderer { fn present(&mut self) -> GameResult; + fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult>; + fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult>; + + fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult; + + fn set_render_target(&mut self, texture: Option<&Box>) -> GameResult; } pub trait BackendTexture { diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs index a3c0023..ab954cd 100644 --- a/src/framework/backend_sdl2.rs +++ b/src/framework/backend_sdl2.rs @@ -6,13 +6,14 @@ use sdl2::{EventPump, keyboard, pixels, Sdl}; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::Scancode; use sdl2::pixels::PixelFormatEnum; -use sdl2::render::{BlendMode, Texture, TextureCreator, WindowCanvas}; +use sdl2::render::{Texture, TextureCreator, WindowCanvas}; use sdl2::video::WindowContext; use crate::common::Color; use crate::framework::backend::{Backend, BackendEventLoop, BackendRenderer, BackendTexture, SpriteBatchCommand}; use crate::framework::context::Context; use crate::framework::error::{GameError, GameResult}; +use crate::framework::graphics::BlendMode; use crate::framework::keyboard::ScanCode; use crate::Game; @@ -46,10 +47,13 @@ struct SDL2EventLoop { struct SDL2Context { canvas: WindowCanvas, texture_creator: TextureCreator, + blend_mode: sdl2::render::BlendMode, } impl SDL2EventLoop { pub fn new(sdl: &Sdl) -> GameResult> { + sdl2::hint::set("SDL_HINT_RENDER_DRIVER", "opengles2"); + let event_pump = sdl.event_pump().map_err(|e| GameError::WindowError(e))?; let video = sdl.video().map_err(|e| GameError::WindowError(e))?; let window = video.window("Cave Story (doukutsu-rs)", 640, 480) @@ -71,6 +75,7 @@ impl SDL2EventLoop { refs: Rc::new(RefCell::new(SDL2Context { canvas, texture_creator, + blend_mode: sdl2::render::BlendMode::Blend, })), }; @@ -82,6 +87,12 @@ impl BackendEventLoop for SDL2EventLoop { fn run(&mut self, game: &mut Game, ctx: &mut Context) { let state = unsafe { &mut *game.state.get() }; + { + let (width, height) = self.refs.borrow().canvas.window().size(); + ctx.screen_size = (width.max(1) as f32, height.max(1) as f32); + let _ = state.handle_resize(ctx); + } + loop { for event in self.event_pump.poll_iter() { match event { @@ -160,6 +171,14 @@ fn to_sdl(color: Color) -> pixels::Color { pixels::Color::RGBA(r, g, b, a) } +unsafe fn set_raw_target(renderer: *mut sdl2::sys::SDL_Renderer, raw_texture: *mut sdl2::sys::SDL_Texture) -> GameResult { + if sdl2::sys::SDL_SetRenderTarget(renderer, raw_texture) == 0 { + Ok(()) + } else { + Err(GameError::RenderError(sdl2::get_error())) + } +} + impl BackendRenderer for SDL2Renderer { fn clear(&mut self, color: Color) { let mut refs = self.refs.borrow_mut(); @@ -176,6 +195,22 @@ impl BackendRenderer for SDL2Renderer { Ok(()) } + fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult> { + let mut refs = self.refs.borrow_mut(); + + let mut texture = refs.texture_creator + .create_texture_target(PixelFormatEnum::RGBA32, width as u32, height as u32) + .map_err(|e| GameError::RenderError(e.to_string()))?; + + Ok(Box::new(SDL2Texture { + refs: self.refs.clone(), + texture: Some(texture), + width, + height, + commands: vec![], + })) + } + fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult> { let mut refs = self.refs.borrow_mut(); @@ -183,7 +218,7 @@ impl BackendRenderer for SDL2Renderer { .create_texture_streaming(PixelFormatEnum::RGBA32, width as u32, height as u32) .map_err(|e| GameError::RenderError(e.to_string()))?; - texture.set_blend_mode(BlendMode::Blend); + texture.set_blend_mode(sdl2::render::BlendMode::Blend); texture.with_lock(None, |buffer: &mut [u8], pitch: usize| { for y in 0..(height as usize) { for x in 0..(width as usize) { @@ -198,13 +233,47 @@ impl BackendRenderer for SDL2Renderer { } }).map_err(|e| GameError::RenderError(e.to_string()))?; - return Ok(Box::new(SDL2Texture { + Ok(Box::new(SDL2Texture { refs: self.refs.clone(), texture: Some(texture), width, height, commands: vec![], - })); + })) + } + + fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult { + let mut refs = self.refs.borrow_mut(); + + refs.blend_mode = match blend { + BlendMode::Add => sdl2::render::BlendMode::Add, + BlendMode::Alpha => sdl2::render::BlendMode::Blend, + BlendMode::Multiply => sdl2::render::BlendMode::Mod, + }; + + Ok(()) + } + + fn set_render_target(&mut self, texture: Option<&Box>) -> GameResult { + let renderer = self.refs.borrow().canvas.raw(); + + // todo: horribly unsafe + match texture { + Some(texture) => unsafe { + let sdl2_texture: &Box = std::mem::transmute(texture); + + if let Some(target) = sdl2_texture.texture.as_ref() { + set_raw_target(renderer, target.raw()); + } else { + set_raw_target(renderer, std::ptr::null_mut()); + } + } + None => unsafe { + set_raw_target(renderer, std::ptr::null_mut()); + } + } + + Ok(()) } } @@ -239,20 +308,22 @@ impl BackendTexture for SDL2Texture { SpriteBatchCommand::DrawRect(src, dest) => { texture.set_color_mod(255, 255, 255); texture.set_alpha_mod(255); + texture.set_blend_mode(refs.blend_mode); refs.canvas.copy(texture, - Some(sdl2::rect::Rect::new(src.left as i32, src.top as i32, src.width() as u32, src.height() as u32)), - Some(sdl2::rect::Rect::new(dest.left as i32, dest.top as i32, dest.width() as u32, dest.height() as u32))) + Some(sdl2::rect::Rect::new(src.left.round() as i32, src.top.round() as i32, src.width().round() as u32, src.height().round() as u32)), + Some(sdl2::rect::Rect::new(dest.left.round() as i32, dest.top.round() as i32, dest.width().round() as u32, dest.height().round() as u32))) .map_err(|e| GameError::RenderError(e.to_string()))?; } SpriteBatchCommand::DrawRectTinted(src, dest, color) => { let (r, g, b, a) = color.to_rgba(); texture.set_color_mod(r, g, b); texture.set_alpha_mod(a); + texture.set_blend_mode(refs.blend_mode); refs.canvas.copy(texture, - Some(sdl2::rect::Rect::new(src.left as i32, src.top as i32, src.width() as u32, src.height() as u32)), - Some(sdl2::rect::Rect::new(dest.left as i32, dest.top as i32, dest.width() as u32, dest.height() as u32))) + Some(sdl2::rect::Rect::new(src.left.round() as i32, src.top.round() as i32, src.width().round() as u32, src.height().round() as u32)), + Some(sdl2::rect::Rect::new(dest.left.round() as i32, dest.top.round() as i32, dest.width().round() as u32, dest.height().round() as u32))) .map_err(|e| GameError::RenderError(e.to_string()))?; } } diff --git a/src/framework/graphics.rs b/src/framework/graphics.rs index a596e91..583c9bd 100644 --- a/src/framework/graphics.rs +++ b/src/framework/graphics.rs @@ -8,10 +8,18 @@ pub enum FilterMode { Linear, } -pub struct Canvas {} - -impl Canvas { - +#[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) { @@ -32,6 +40,14 @@ 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> { + 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> { if let Some(renderer) = ctx.renderer.as_mut() { return renderer.create_texture(width, height, data); @@ -43,3 +59,19 @@ pub fn create_texture(ctx: &mut Context, width: u16, height: u16, data: &[u8]) - pub fn screen_size(ctx: &mut Context) -> (f32, f32) { ctx.screen_size } + +pub fn set_render_target(ctx: &mut Context, texture: Option<&Box>) -> 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())) +} diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 8c744af..254038a 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -3,13 +3,18 @@ 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, Color}; +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::input::touch_controls::TouchControlType; use crate::inventory::{Inventory, TakeExperienceResult}; use crate::npc::boss::BossNPC; @@ -26,10 +31,6 @@ use crate::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, use crate::texture_set::SizedBatch; use crate::ui::Components; use crate::weapon::WeaponType; -use crate::framework::context::Context; -use crate::framework::error::GameResult; -use crate::framework::graphics; -use crate::framework::graphics::FilterMode; pub struct GameScene { pub tick: u32, @@ -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) { + 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.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) { + 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.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); } } } @@ -342,7 +343,7 @@ impl GameScene { for x in 0..(center_x / 16 + 2) { let mut frame = start_frame; - for y in 0..(center_y / 16 + 1) { + for y in 0..(center_y / 16 + 2) { if frame >= 15 { frame = 15; } else { frame += 1; } if frame >= 0 { @@ -502,10 +503,17 @@ 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));*/ + graphics::clear(ctx, Color::from_rgb(100, 100, 110)); { let scale = state.scale; let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "builtin/lightmap/spot")?; @@ -547,9 +555,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 +592,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), @@ -634,13 +635,15 @@ impl GameScene { batch.draw_filtered(FilterMode::Linear, ctx)?; } - /*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_blend_mode(ctx, BlendMode::Multiply)?; + graphics::set_render_target(ctx, None); - graphics::set_blend_mode(ctx, BlendMode::Alpha)?;*/ + 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(()) } @@ -1213,13 +1216,13 @@ impl Scene for GameScene { //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 + if state.settings.shader_effects && self.stage.data.background_type != BackgroundType::Black && self.stage.data.background_type != BackgroundType::Outside && self.stage.data.background_type != BackgroundType::OutsideWind && self.stage.data.background.name() != "bkBlack" { self.draw_light_map(state, ctx)?; - }*/ + } self.boss.draw(state, ctx, &self.frame)?; for npc in self.npc_list.iter_alive() { @@ -1242,11 +1245,11 @@ impl Scene for GameScene { self.draw_tiles(state, ctx, TileLayer::Foreground)?; self.draw_tiles(state, ctx, TileLayer::Snack)?; self.draw_carets(state, ctx)?; - /*if state.settings.shader_effects + if state.settings.shader_effects && (self.stage.data.background_type == BackgroundType::Black || self.stage.data.background.name() == "bkBlack") { self.draw_light_map(state, ctx)?; - }*/ + } /*graphics::set_canvas(ctx, None); state.game_canvas.draw(ctx, DrawParam::new() diff --git a/src/scripting/doukutsu.d.ts b/src/scripting/doukutsu.d.ts index 0c024d8..6ea4d4b 100644 --- a/src/scripting/doukutsu.d.ts +++ b/src/scripting/doukutsu.d.ts @@ -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 { diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 284624f..340bb2b 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -27,6 +27,8 @@ use crate::str; use crate::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; use crate::texture_set::TextureSet; use crate::framework::{filesystem, graphics}; +use crate::framework::backend::BackendTexture; +use crate::framework::graphics::{create_texture, set_render_target, create_texture_mutable}; #[derive(PartialEq, Eq, Copy, Clone)] pub enum TimingMode { @@ -106,6 +108,7 @@ pub struct SharedGameState { pub screen_size: (f32, f32), pub next_scene: Option>, pub textscript_vm: TextScriptVM, + pub lightmap_canvas: Option>, pub season: Season, pub constants: EngineConstants, pub font: BMFontRenderer, @@ -170,6 +173,7 @@ impl SharedGameState { canvas_size: (320.0, 240.0), next_scene: None, textscript_vm: TextScriptVM::new(), + lightmap_canvas: None, season, constants, font, @@ -279,6 +283,12 @@ impl SharedGameState { 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); + 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(()) } diff --git a/src/texture_set.rs b/src/texture_set.rs index 5beb9e2..1e91128 100644 --- a/src/texture_set.rs +++ b/src/texture_set.rs @@ -68,8 +68,8 @@ impl SizedBatch { pub fn add(&mut self, mut x: f32, mut y: f32) { 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 }; @@ -105,23 +105,23 @@ 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 }; self.batch.add(SpriteBatchCommand::DrawRect( Rect { - left: rect.left as f32 / scale_x, - top: rect.top as f32 / scale_y, - right: rect.right as f32 / scale_x, - bottom: rect.bottom as f32 / scale_y, + 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) * mag, - bottom: (y + rect.height() as f32) * mag, + left: x * mag * scale_x, + top: y * mag * scale_y, + right: (x + rect.width() as f32) * mag * scale_x, + bottom: (y + rect.height() as f32) * mag * scale_y, }, )); } @@ -139,16 +139,16 @@ impl SizedBatch { self.batch.add(SpriteBatchCommand::DrawRectTinted( Rect { - left: rect.left as f32 / scale_x, - top: rect.top as f32 / scale_y, - right: rect.right as f32 / scale_x, - bottom: rect.bottom as f32 / scale_y, + 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) * mag, - bottom: (y + rect.height() as f32) * mag, + right: (x + rect.width() as f32 * scale_x) * mag, + bottom: (y + rect.height() as f32 * scale_y) * mag, }, color.into(), ));