diff --git a/Cargo.lock b/Cargo.lock index ba1d0d9..08605ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "bidrum" version = "0.1.0" dependencies = [ + "bidrum-controller-lib", "bidrum-data-struct-lib", "cairo-rs", "clap", @@ -116,6 +117,14 @@ dependencies = [ "sdl2", "serde", "serde_json", +] + +[[package]] +name = "bidrum-controller-lib" +version = "0.1.0" +dependencies = [ + "bidrum-data-struct-lib", + "device_query", "serialport", ] @@ -690,9 +699,9 @@ dependencies = [ [[package]] name = "io-kit-sys" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" dependencies = [ "core-foundation-sys", "mach2", diff --git a/Cargo.toml b/Cargo.toml index 32ca1c9..0997b76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "game", "data-struct-lib", - "chart-recorder" + "chart-recorder", + "controller-lib" ] [profile.dev.package.kira] diff --git a/controller-lib/Cargo.toml b/controller-lib/Cargo.toml new file mode 100644 index 0000000..6a57dac --- /dev/null +++ b/controller-lib/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bidrum-controller-lib" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bidrum-data-struct-lib = { path = "../data-struct-lib" } +serialport = "4.3.0" +device_query = "2.0.0" \ No newline at end of file diff --git a/controller-lib/src/keyboard.rs b/controller-lib/src/keyboard.rs new file mode 100644 index 0000000..e41bb43 --- /dev/null +++ b/controller-lib/src/keyboard.rs @@ -0,0 +1,2 @@ +pub mod coin_device; +pub mod janggu_device; diff --git a/controller-lib/src/keyboard/coin_device.rs b/controller-lib/src/keyboard/coin_device.rs new file mode 100644 index 0000000..a5842c9 --- /dev/null +++ b/controller-lib/src/keyboard/coin_device.rs @@ -0,0 +1,70 @@ +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, + }, + time::Duration, +}; + +use device_query::{DeviceQuery, DeviceState, Keycode}; + +use crate::CoinInputDevice; + +/// United bidrum controller of Janggu and Coin/Bill acceptor +pub struct KeyboardCoinDevice { + unconsumed_coins: Arc, + stopping: Arc, +} + +impl Drop for KeyboardCoinDevice { + fn drop(&mut self) { + self.stopping.store(true, Ordering::Relaxed); + } +} + +impl CoinInputDevice for KeyboardCoinDevice { + fn get_unconsumed_coins(&self) -> u32 { + self.unconsumed_coins.load(Ordering::Relaxed) + } + + fn consume_coins(&mut self, coins: u32) { + self.unconsumed_coins.fetch_sub(coins, Ordering::Relaxed); + } +} + +impl KeyboardCoinDevice { + pub fn new() -> KeyboardCoinDevice { + let unconsumed_coins = Arc::new(AtomicU32::new(0)); + let stopping = Arc::new(AtomicBool::new(false)); + + { + let unconsumed_coins = unconsumed_coins.clone(); + + let _stopping = stopping.clone(); + + std::thread::spawn(move || { + let mut pressed = false; + let device_state = DeviceState::new(); + loop { + let new_pressed = device_state.get_keys().contains(&Keycode::C); + if new_pressed && !pressed { + // increase one on keydown + unconsumed_coins.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + + pressed = new_pressed; + std::thread::sleep(Duration::from_millis(10)); + } + }); + } + + KeyboardCoinDevice { + unconsumed_coins: unconsumed_coins, + stopping: stopping, + } + } +} + +fn has_coin_bit(bits: u8) -> bool { + bits & 16 != 0 +} diff --git a/controller-lib/src/keyboard/janggu_device.rs b/controller-lib/src/keyboard/janggu_device.rs new file mode 100644 index 0000000..04bc162 --- /dev/null +++ b/controller-lib/src/keyboard/janggu_device.rs @@ -0,0 +1,94 @@ +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU8, Ordering}, + Arc, RwLock, + }, + thread, +}; + +use bidrum_data_struct_lib::janggu::{JangguFace, JangguInputState}; +use device_query::{DeviceQuery, DeviceState, Keycode}; + +use crate::JangguDevice; + +pub struct KeyboardJangguDevice { + stopping: Arc, + // Using RwLock is too slow + state: Arc, +} + +impl KeyboardJangguDevice { + pub fn new() -> KeyboardJangguDevice { + let stopping = Arc::new(AtomicBool::new(false)); + let state = Arc::new(AtomicU8::new(0)); + + { + let stopping = stopping.clone(); + let state = state.clone(); + + thread::spawn(move || { + let mut device_states = DeviceState::new(); + loop { + if stopping.load(Ordering::Relaxed) { + break; + } + + state.store(keyboard_to_bits(&mut device_states), Ordering::Relaxed); + } + }); + } + + KeyboardJangguDevice { + state: state, + stopping: stopping, + } + } +} + +impl Drop for KeyboardJangguDevice { + fn drop(&mut self) { + self.stopping.store(true, Ordering::Relaxed); + } +} + +impl JangguDevice for KeyboardJangguDevice { + fn read_janggu_input_state(&self) -> JangguInputState { + bits_to_janggu_input_state(self.state.load(Ordering::Relaxed)) + } +} + +fn bits_to_janggu_input_state(bits: u8) -> JangguInputState { + JangguInputState { + 궁채: if bits & 1 != 0 { + Some(JangguFace::궁편) + } else if bits & 2 != 0 { + Some(JangguFace::열편) + } else { + None + }, + 열채: if bits & 4 != 0 { + Some(JangguFace::궁편) + } else if bits & 8 != 0 { + Some(JangguFace::열편) + } else { + None + }, + } +} + +fn keyboard_to_bits(device_states: &mut DeviceState) -> u8 { + let keys = device_states.get_keys(); + let mut bits = 0; + if keys.contains(&Keycode::D) { + bits |= 1; + } else if keys.contains(&Keycode::F) { + bits |= 2; + } + if keys.contains(&Keycode::J) { + bits |= 4; + } else if keys.contains(&Keycode::K) { + bits |= 8; + } + + return bits; +} diff --git a/controller-lib/src/lib.rs b/controller-lib/src/lib.rs new file mode 100644 index 0000000..763eab2 --- /dev/null +++ b/controller-lib/src/lib.rs @@ -0,0 +1,17 @@ +pub mod keyboard; +pub mod serial; + +use bidrum_data_struct_lib::janggu::JangguInputState; + +/// Bidrum Janggu Controller +pub trait JangguDevice { + /// Reads janggu controller input state + fn read_janggu_input_state(&self) -> JangguInputState; +} + +/// Bidrum Coin/Bill Acceptor +pub trait CoinInputDevice { + /// Reads unconsumed coin count + fn get_unconsumed_coins(&self) -> u32; + fn consume_coins(&mut self, coins: u32); +} diff --git a/controller-lib/src/serial.rs b/controller-lib/src/serial.rs new file mode 100644 index 0000000..c59d347 --- /dev/null +++ b/controller-lib/src/serial.rs @@ -0,0 +1,15 @@ +mod coin_device; +mod janggu_device; +mod serial_reader; + +use coin_device::SerialCoinDevice; +use janggu_device::SerialJangguDevice; +use serial_reader::BidrumSerialReader; + +pub fn new(serial_port: String) -> (SerialJangguDevice, SerialCoinDevice) { + let serial_reader = BidrumSerialReader::new(serial_port); + return ( + SerialJangguDevice::new(serial_reader.clone()), + SerialCoinDevice::new(serial_reader.clone()), + ); +} diff --git a/controller-lib/src/serial/coin_device.rs b/controller-lib/src/serial/coin_device.rs new file mode 100644 index 0000000..25388bf --- /dev/null +++ b/controller-lib/src/serial/coin_device.rs @@ -0,0 +1,72 @@ +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, + }, + thread::{self}, +}; + +use crate::CoinInputDevice; + +use super::serial_reader::BidrumSerialReader; + +/// United bidrum controller of Janggu and Coin/Bill acceptor +pub struct SerialCoinDevice { + serial_reader: BidrumSerialReader, + unconsumed_coins: Arc, + stopping: Arc, +} + +impl Drop for SerialCoinDevice { + fn drop(&mut self) { + self.stopping.store(true, Ordering::Relaxed); + } +} + +impl CoinInputDevice for SerialCoinDevice { + fn get_unconsumed_coins(&self) -> u32 { + self.unconsumed_coins.load(Ordering::Relaxed) + } + + fn consume_coins(&mut self, coins: u32) { + self.unconsumed_coins.fetch_sub(coins, Ordering::Relaxed); + } +} + +impl SerialCoinDevice { + pub(super) fn new(serial_reader: BidrumSerialReader) -> SerialCoinDevice { + let unconsumed_coins = Arc::new(AtomicU32::new(0)); + let stopping = Arc::new(AtomicBool::new(false)); + + { + let unconsumed_coins = unconsumed_coins.clone(); + let bits = serial_reader.bits.clone(); + let stopping = stopping.clone(); + thread::spawn(move || { + let mut prev = false; + loop { + if stopping.load(Ordering::Relaxed) { + break; + } + + let current = has_coin_bit(bits.load(Ordering::Relaxed)); + if !prev && current { + unconsumed_coins.fetch_add(1, Ordering::Relaxed); + } + + prev = current; + } + }); + } + + SerialCoinDevice { + serial_reader: serial_reader, + unconsumed_coins: unconsumed_coins, + stopping: stopping, + } + } +} + +fn has_coin_bit(bits: u8) -> bool { + bits & 16 != 0 +} diff --git a/controller-lib/src/serial/janggu_device.rs b/controller-lib/src/serial/janggu_device.rs new file mode 100644 index 0000000..8113e8e --- /dev/null +++ b/controller-lib/src/serial/janggu_device.rs @@ -0,0 +1,44 @@ +use std::sync::atomic::Ordering; + +use bidrum_data_struct_lib::janggu::{JangguFace, JangguInputState}; + +use crate::JangguDevice; + +use super::serial_reader::BidrumSerialReader; + +pub struct SerialJangguDevice { + serial_reader: BidrumSerialReader, +} + +impl SerialJangguDevice { + pub(super) fn new(serial_reader: BidrumSerialReader) -> SerialJangguDevice { + SerialJangguDevice { + serial_reader: serial_reader, + } + } +} + +impl JangguDevice for SerialJangguDevice { + fn read_janggu_input_state(&self) -> JangguInputState { + return parse_janggu_bits(self.serial_reader.bits.load(Ordering::Relaxed)); + } +} + +fn parse_janggu_bits(bits: u8) -> JangguInputState { + return JangguInputState { + 궁채: if bits & 1 != 0 { + Some(JangguFace::궁편) + } else if bits & 2 != 0 { + Some(JangguFace::열편) + } else { + None + }, + 열채: if bits & 4 != 0 { + Some(JangguFace::궁편) + } else if bits & 8 != 0 { + Some(JangguFace::열편) + } else { + None + }, + }; +} diff --git a/controller-lib/src/serial/serial_reader.rs b/controller-lib/src/serial/serial_reader.rs new file mode 100644 index 0000000..bc94923 --- /dev/null +++ b/controller-lib/src/serial/serial_reader.rs @@ -0,0 +1,92 @@ +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU32, AtomicU8, Ordering}, + Arc, + }, + thread::{self, sleep}, + time::Duration, +}; + +use serialport::SerialPort; + +/// United bidrum controller of Janggu and Coin/Bill acceptor +pub(super) struct BidrumSerialReader { + stopping: Arc, + pub(super) bits: Arc, + counter: Arc, +} + +impl Clone for BidrumSerialReader { + fn clone(&self) -> Self { + self.counter.fetch_add(1, Ordering::Relaxed); + + Self { + stopping: self.stopping.clone(), + bits: self.bits.clone(), + counter: self.counter.clone(), + } + } +} + +impl Drop for BidrumSerialReader { + fn drop(&mut self) { + self.counter.fetch_sub(1, Ordering::Relaxed); + + if self.counter.load(Ordering::Relaxed) == 0 { + self.stopping.store(true, Ordering::Relaxed); + } + } +} + +impl BidrumSerialReader { + pub(super) fn new(controller_port: String) -> BidrumSerialReader { + println!("Openning serial port {}", controller_port); + + let mut port = serialport::new(controller_port, 9600) + .timeout(Duration::from_millis(20)) + .open() + .expect("Failed to open port"); + + println!("Waiting 3 seconds (Arduino needs time for serial initialization)"); + sleep(Duration::from_millis(3000)); + println!("Waited 3 seconds!"); + + let stopping = Arc::new(AtomicBool::new(false)); + let bits = Arc::new(AtomicU8::new(0)); + let counter = Arc::new(AtomicU32::new(1)); + { + let stopping = stopping.clone(); + let bits = bits.clone(); + + thread::spawn(move || loop { + if stopping.load(Ordering::Relaxed) { + break; + } + read_serial(&mut port, &bits); + }); + } + + BidrumSerialReader { + stopping: stopping, + counter: counter, + bits: bits, + } + } +} + +/// Read serial inputs from port and emulates key inputs +fn read_serial(port: &mut Box, bits_data: &Arc) { + loop { + let available_bytes: u32 = port.bytes_to_read().expect("Failed to read buffer size"); + if available_bytes > 0 { + break; + } + sleep(Duration::from_millis(10)); + } + let mut message: [u8; 1] = [0]; + + port.read_exact(message.as_mut_slice()) + .expect("Controller reading failure!"); + + bits_data.store(message[0], std::sync::atomic::Ordering::Relaxed); +} diff --git a/game/Cargo.toml b/game/Cargo.toml index e33342f..42e309a 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -7,6 +7,10 @@ description = "Rhythm game with korean traditional drum(janggu)-like controller" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +uncommercial = [] + [dependencies] clap = { version = "4.4.13", features = ["derive"] } ffmpeg-next = "7.0.0" @@ -15,8 +19,8 @@ num-rational = "0.4.1" sdl2 = { version = "0.36.0", features = ["image"] } serde = { version = "1.0.195", features = ["derive"] } serde_json = "1.0.111" -serialport = "4.3.0" bidrum-data-struct-lib = { path = "../data-struct-lib" } +bidrum-controller-lib = { path = "../controller-lib" } device_query = "2.0.0" ezing = "0.2.1" const_format = "0.2.32" diff --git a/game/src/controller_wrapper.rs b/game/src/controller_wrapper.rs new file mode 100644 index 0000000..631056a --- /dev/null +++ b/game/src/controller_wrapper.rs @@ -0,0 +1,123 @@ +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, RwLock, + }, + thread, +}; + +use bidrum_controller_lib::{keyboard, serial, CoinInputDevice, JangguDevice}; +use bidrum_data_struct_lib::janggu::JangguInputState; + +/// Wrapper of Coin/Janggu controller +/// to avoid cumbersome ownership/borrow/lifetime problems +pub struct ControllerWrapper { + janggu_state: Arc>, + coins: Arc, + coins_to_consume: Arc, + stopping: Arc, +} + +impl Drop for ControllerWrapper { + fn drop(&mut self) { + self.stopping.store(true, Ordering::Relaxed); + } +} + +impl ControllerWrapper { + pub fn read_janggu_state(&self) -> JangguInputState { + *self.janggu_state.read().expect("Failed to get lock") + } + pub fn get_coins(&self) -> u32 { + self.coins.load(Ordering::Relaxed) + } + pub fn consume_coins(&mut self, coins: u32) { + self.coins_to_consume.fetch_add(coins, Ordering::Relaxed); + } + pub fn keyboard() -> ControllerWrapper { + let coins = Arc::new(AtomicU32::new(0)); + let coins_to_consume = Arc::new(AtomicU32::new(0)); + let stopping = Arc::new(AtomicBool::new(false)); + let janggu_state = Arc::new(RwLock::new(JangguInputState { + 궁채: None, + 열채: None, + })); + { + let coins = coins.clone(); + let coins_to_consume = coins_to_consume.clone(); + let stopping = stopping.clone(); + let janggu_state = janggu_state.clone(); + + thread::spawn(move || { + let mut coin_device = keyboard::coin_device::KeyboardCoinDevice::new(); + let janggu_device = keyboard::janggu_device::KeyboardJangguDevice::new(); + + loop { + if stopping.load(Ordering::Relaxed) { + break; + } + + let consume = coins_to_consume.load(Ordering::Relaxed); + if consume > 0 { + coin_device.consume_coins(consume); + coins_to_consume.fetch_sub(consume, Ordering::Relaxed); + } + + coins.store(coin_device.get_unconsumed_coins(), Ordering::Relaxed); + let mut janggu_state = janggu_state.write().expect("Failed to get lock"); + *janggu_state = janggu_device.read_janggu_input_state(); + } + }); + } + + ControllerWrapper { + janggu_state: janggu_state, + coins: coins, + coins_to_consume: coins_to_consume, + stopping: stopping, + } + } + + pub fn serial(controller_port: String) -> ControllerWrapper { + let coins = Arc::new(AtomicU32::new(0)); + let coins_to_consume = Arc::new(AtomicU32::new(0)); + let stopping = Arc::new(AtomicBool::new(false)); + let janggu_state = Arc::new(RwLock::new(JangguInputState { + 궁채: None, + 열채: None, + })); + { + let coins = coins.clone(); + let coins_to_consume = coins_to_consume.clone(); + let stopping = stopping.clone(); + let janggu_state = janggu_state.clone(); + + thread::spawn(move || { + let (janggu_device, mut coin_device) = serial::new(controller_port); + + loop { + if stopping.load(Ordering::Relaxed) { + break; + } + + let consume = coins_to_consume.load(Ordering::Relaxed); + if consume > 0 { + coin_device.consume_coins(consume); + coins_to_consume.fetch_sub(consume, Ordering::Relaxed); + } + + coins.store(coin_device.get_unconsumed_coins(), Ordering::Relaxed); + let mut janggu_state = janggu_state.write().expect("Failed to get lock"); + *janggu_state = janggu_device.read_janggu_input_state(); + } + }); + } + + ControllerWrapper { + janggu_state: janggu_state, + coins: coins, + coins_to_consume: coins_to_consume, + stopping: stopping, + } + } +} diff --git a/game/src/game/common.rs b/game/src/game/common.rs index 91f3230..2b0d2f6 100644 --- a/game/src/game/common.rs +++ b/game/src/game/common.rs @@ -8,19 +8,13 @@ use crate::constants::{ DEFAULT_FONT_COLOR, DEFAULT_FONT_OUTLINE_COLOR, DEFAULT_FONT_PATH as FONT_PATH, }; -pub(crate) fn event_loop_common(event: &Event, coins: &mut u32) -> bool { +pub(crate) fn event_loop_common(event: &Event) -> bool { match event { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => return true, - Event::KeyDown { - keycode: Some(Keycode::C), - .. - } => { - *coins += 1; - } _ => {} } @@ -39,13 +33,19 @@ pub(crate) fn render_common(context: &mut GameCommonContext) { .expect("Unable to load font"); // render a surface, and convert it to a texture bound to the canvas - let text = if context.price == 1 { - format!("CREDIT: {}", context.coins) + let text = if context.price == 0 { + if cfg!(feature = "uncommercial") { + "FREE PLAY (UNCOMMERCIAL)".to_string() + } else { + "FREE PLAY".to_string() + } + } else if context.price == 1 { + format!("CREDIT: {}", context.coin_and_janggu.get_coins()) } else { format!( "CREDIT: {} ({}/{})", - context.coins / context.price, - context.coins % context.price, + context.coin_and_janggu.get_coins() / context.price, + context.coin_and_janggu.get_coins() % context.price, context.price ) }; diff --git a/game/src/game/display_result.rs b/game/src/game/display_result.rs index 214dd04..45f596d 100644 --- a/game/src/game/display_result.rs +++ b/game/src/game/display_result.rs @@ -80,7 +80,7 @@ pub(crate) fn display_result( .unwrap(); loop { for event in common_context.event_pump.poll_iter() { - if event_loop_common(&event, &mut common_context.coins) { + if event_loop_common(&event) { return; } match event { diff --git a/game/src/game/game_common_context.rs b/game/src/game/game_common_context.rs index ad41437..67e460e 100644 --- a/game/src/game/game_common_context.rs +++ b/game/src/game/game_common_context.rs @@ -1,22 +1,19 @@ -use std::{ - sync::{atomic::AtomicU8, Arc}, - time::Instant, -}; +use std::time::Instant; use kira::manager::AudioManager; use sdl2::{render::Canvas, video::Window, EventPump}; -use crate::serial::parse_janggu_bits; use bidrum_data_struct_lib::janggu::JangguInputState; +use crate::controller_wrapper::ControllerWrapper; + pub(crate) struct GameCommonContext { - pub(crate) coins: u32, + pub(crate) coin_and_janggu: ControllerWrapper, pub(crate) price: u32, pub(crate) sdl_context: sdl2::Sdl, pub(crate) audio_manager: AudioManager, pub(crate) canvas: Canvas, pub(crate) event_pump: EventPump, - pub(crate) janggu_bits_ptr: Arc, /// ddpi, hdpi, vdpi pub(crate) dpi: (f32, f32, f32), pub(crate) game_initialized_at: Instant, @@ -25,9 +22,6 @@ pub(crate) struct GameCommonContext { impl GameCommonContext { pub(crate) fn read_janggu_state(&self) -> JangguInputState { - return parse_janggu_bits( - self.janggu_bits_ptr - .load(std::sync::atomic::Ordering::Relaxed), - ); + self.coin_and_janggu.read_janggu_state() } } diff --git a/game/src/game/game_player.rs b/game/src/game/game_player.rs index c1d6f9c..0998f99 100644 --- a/game/src/game/game_player.rs +++ b/game/src/game/game_player.rs @@ -71,7 +71,7 @@ pub(crate) fn play_song( let sound_data = loop { // process input events for event in common_context.event_pump.poll_iter() { - if event_loop_common(&event, &mut common_context.coins) { + if event_loop_common(&event) { return None; } } @@ -151,7 +151,7 @@ pub(crate) fn play_song( 'running: loop { let tick_now = clock.time().ticks as i128 - start_tick.ticks as i128; for event in common_context.event_pump.poll_iter() { - if event_loop_common(&event, &mut common_context.coins) { + if event_loop_common(&event) { handle.stop(Tween::default()).expect("Failed to stop song"); break 'running; } diff --git a/game/src/game/game_player/effect_sound_player.rs b/game/src/game/game_player/effect_sound_player.rs index 96ba469..27d6708 100644 --- a/game/src/game/game_player/effect_sound_player.rs +++ b/game/src/game/game_player/effect_sound_player.rs @@ -7,7 +7,7 @@ use kira::{ }; use rand::seq::SliceRandom; -use crate::{constants::DEFAULT_SOUND_PATH as SOUND_PATH}; +use crate::constants::DEFAULT_SOUND_PATH as SOUND_PATH; use super::{game_result::GameResult, janggu_state_with_tick::JangguStateWithTick}; diff --git a/game/src/game/init.rs b/game/src/game/init.rs index 8297ac4..1834ffb 100644 --- a/game/src/game/init.rs +++ b/game/src/game/init.rs @@ -1,10 +1,9 @@ -use std::{ - sync::{atomic::AtomicU8, Arc}, - time::Instant, -}; +use std::time::Instant; use kira::manager::{backend::DefaultBackend, AudioManager, AudioManagerSettings}; +use crate::controller_wrapper::ControllerWrapper; + use super::{game_common_context::GameCommonContext, start::start_game, title::render_title}; pub struct InitGameOptions { @@ -12,9 +11,10 @@ pub struct InitGameOptions { pub height: Option, pub fullscreen: bool, pub vsync: bool, + pub price: u32, } -pub(crate) fn init_game(janggu_bits: Arc, options: InitGameOptions) { +pub(crate) fn init_game(controller_wrapper: ControllerWrapper, options: InitGameOptions) { // init sdl let sdl_context = sdl2::init().expect("sdl context initialization Fail"); @@ -95,15 +95,14 @@ pub(crate) fn init_game(janggu_bits: Arc, options: InitGameOptions) { // create GameCommonContext object let mut context = GameCommonContext { - coins: 0, - price: 2, + coin_and_janggu: controller_wrapper, + price: options.price, canvas: canvas, dpi: dpi, sdl_context: sdl_context, event_pump: event_pump, audio_manager: AudioManager::::new(AudioManagerSettings::default()) .expect("AudioManager initialization failure"), - janggu_bits_ptr: janggu_bits, game_initialized_at: Instant::now(), freetype_library: freetype_library, }; diff --git a/game/src/game/select_song.rs b/game/src/game/select_song.rs index ffa1262..b484595 100644 --- a/game/src/game/select_song.rs +++ b/game/src/game/select_song.rs @@ -126,7 +126,7 @@ pub(crate) fn select_song( // waiting keyboard input for event in common_context.event_pump.poll_iter() { - if event_loop_common(&event, &mut common_context.coins) { + if event_loop_common(&event) { break 'running; } } diff --git a/game/src/game/title.rs b/game/src/game/title.rs index 1565900..79d914c 100644 --- a/game/src/game/title.rs +++ b/game/src/game/title.rs @@ -71,7 +71,7 @@ fn render_text(common_context: &mut GameCommonContext) { let mut texture = create_font_texture( &texture_creator, &mut font, - if common_context.coins >= common_context.price { + if common_context.coin_and_janggu.get_coins() >= common_context.price { "장구를 쳐서 시작하세요!" } else { "동전을 넣어주세요" @@ -138,7 +138,7 @@ pub(crate) fn render_title(common_context: &mut GameCommonContext) -> TitleResul ); loop { for event in common_context.event_pump.poll_iter() { - if event_loop_common(&event, &mut common_context.coins) { + if event_loop_common(&event) { return TitleResult::Exit; } } @@ -148,8 +148,10 @@ pub(crate) fn render_title(common_context: &mut GameCommonContext) -> TitleResul title_started_at.elapsed().as_millis() as i128, ); if janggu_state.궁채.is_keydown_now || janggu_state.열채.is_keydown_now { - if common_context.coins >= common_context.price { - common_context.coins -= common_context.price; + if common_context.coin_and_janggu.get_coins() >= common_context.price { + common_context + .coin_and_janggu + .consume_coins(common_context.price); return TitleResult::StartGame; } } diff --git a/game/src/game/tutorial.rs b/game/src/game/tutorial.rs index a1dfbef..ccb87c3 100644 --- a/game/src/game/tutorial.rs +++ b/game/src/game/tutorial.rs @@ -56,7 +56,7 @@ fn ask_for_tutorial(common_context: &mut GameCommonContext) -> bool { } for i in common_context.event_pump.poll_iter() { - if event_loop_common(&i, &mut common_context.coins) { + if event_loop_common(&i) { background_video.stop_decoding(); return false; } @@ -93,7 +93,7 @@ fn ask_for_tutorial(common_context: &mut GameCommonContext) -> bool { } for i in common_context.event_pump.poll_iter() { - if event_loop_common(&i, &mut common_context.coins) { + if event_loop_common(&i) { background_video.stop_decoding(); return false; } @@ -134,7 +134,7 @@ fn ask_for_tutorial(common_context: &mut GameCommonContext) -> bool { let tick = dialog_started_at.elapsed().as_millis(); for i in common_context.event_pump.poll_iter() { - if event_loop_common(&i, &mut common_context.coins) { + if event_loop_common(&i) { selected = Some(false); break 'running; } @@ -207,7 +207,7 @@ fn ask_for_tutorial(common_context: &mut GameCommonContext) -> bool { } for i in common_context.event_pump.poll_iter() { - if event_loop_common(&i, &mut common_context.coins) { + if event_loop_common(&i) { background_video.stop_decoding(); return false; } @@ -299,7 +299,7 @@ pub(self) fn display_tutorial_messages( loop { let tick = janggu_state_and_tutorial_start_time.1.elapsed().as_millis() as i128; for event in common_context.event_pump.poll_iter() { - event_loop_common(&event, &mut common_context.coins); + event_loop_common(&event); } janggu_state_and_tutorial_start_time diff --git a/game/src/game/tutorial/greetings.rs b/game/src/game/tutorial/greetings.rs index 804399b..e002bd5 100644 --- a/game/src/game/tutorial/greetings.rs +++ b/game/src/game/tutorial/greetings.rs @@ -48,7 +48,7 @@ pub(crate) fn do_tutorial_greetings( } let tick = janggu_state_and_tutorial_start_time.1.elapsed().as_millis() as i128; for event in common_context.event_pump.poll_iter() { - event_loop_common(&event, &mut common_context.coins); + event_loop_common(&event); } janggu_state_and_tutorial_start_time diff --git a/game/src/game/tutorial/learn_stick_note.rs b/game/src/game/tutorial/learn_stick_note.rs index f2c3707..e04c318 100644 --- a/game/src/game/tutorial/learn_stick_note.rs +++ b/game/src/game/tutorial/learn_stick_note.rs @@ -81,7 +81,7 @@ fn display_animated_example_note( loop { for event in common_context.event_pump.poll_iter() { - event_loop_common(&event, &mut common_context.coins); + event_loop_common(&event); } if voice_started_at.elapsed() >= std::cmp::max(total_note_duration, message.1.duration()) { @@ -223,7 +223,7 @@ fn display_tryitout_notes( loop { for event in common_context.event_pump.poll_iter() { - event_loop_common(&event, &mut common_context.coins); + event_loop_common(&event); } // If tutorial ends, return diff --git a/game/src/janggu_keyboard.rs b/game/src/janggu_keyboard.rs deleted file mode 100644 index d3ef090..0000000 --- a/game/src/janggu_keyboard.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::sync::{atomic::AtomicU8, Arc}; - -use device_query::{DeviceQuery, DeviceState, Keycode}; - -/// Read keyboard inputs and save janggu input bits -pub(crate) fn read_janggu_key_loop(bits_data: Arc) { - loop { - let mut bits: u8 = 0; - let device_states = DeviceState::new(); - let keys = device_states.get_keys(); - if keys.contains(&Keycode::D) { - bits |= 1; - } - if keys.contains(&Keycode::F) { - bits |= 2; - } - if keys.contains(&Keycode::J) { - bits |= 4; - } - if keys.contains(&Keycode::K) { - bits |= 8; - } - - bits_data.store(bits, std::sync::atomic::Ordering::Relaxed); - } -} diff --git a/game/src/main.rs b/game/src/main.rs index ce18440..f341641 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -1,19 +1,10 @@ mod constants; +mod controller_wrapper; mod game; -mod janggu_keyboard; -mod serial; - -use std::{ - sync::{atomic::AtomicU8, Arc}, - thread::{self, sleep}, - time::Duration, -}; use clap::Parser; +use controller_wrapper::ControllerWrapper; use game::init::{init_game, InitGameOptions}; -use janggu_keyboard::read_janggu_key_loop; - -use crate::serial::read_serial_loop; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -33,52 +24,47 @@ struct Args { /// Enables vsync or not? (Default: enabled in macos, disabled otherwise) #[arg(long)] vsync: Option, + /// Price + #[cfg(not(feature = "uncommercial"))] + #[arg(long, default_value_t = 2)] + price: u32, } -fn main() { - let args = Args::parse(); - let bits = AtomicU8::new(0); - let bits_arc = Arc::new(bits); - match args.controller_port { - Some(controller_port) => { - println!("Openning serial port {}", controller_port); - - let port = serialport::new(controller_port, 9600) - .timeout(Duration::from_millis(20)) - .open() - .expect("Failed to open port"); - - println!("Waiting 3 seconds (Arduino needs time for serial initialization)"); - sleep(Duration::from_millis(3000)); - println!("Waited 3 seconds!"); +#[cfg(feature = "uncommercial")] +macro_rules! price { + ($args: expr) => { + 0 + }; +} - let ptr = bits_arc.clone(); - thread::spawn(move || { - read_serial_loop(port, ptr); - }); - } - _ => { - println!("Controller port not provided! Reading keyboard...."); +#[cfg(not(feature = "uncommercial"))] +macro_rules! price { + ($args: expr) => { + $args.price + }; +} - let ptr = bits_arc.clone(); - thread::spawn(move || { - read_janggu_key_loop(ptr); - }); - } +fn main() { + if cfg!(feature = "uncommercial") { + println!("This is uncommercial version, only free play is available."); } - let ptr = bits_arc.clone(); - init_game( - ptr, - InitGameOptions { - fullscreen: !args.windowed, - height: args.window_height, - width: args.window_width, - vsync: args.vsync.unwrap_or(if cfg!(target_os = "macos") { - true - } else { - false - }), - }, - ); + let args = Args::parse(); + let options = InitGameOptions { + fullscreen: !args.windowed, + height: args.window_height, + width: args.window_width, + vsync: args.vsync.unwrap_or(if cfg!(target_os = "macos") { + true + } else { + false + }), + price: price!(args), + }; + + let controller_wrapper = match args.controller_port { + Some(controller_port) => ControllerWrapper::serial(controller_port), + _ => ControllerWrapper::keyboard(), + }; + init_game(controller_wrapper, options); } diff --git a/game/src/serial.rs b/game/src/serial.rs deleted file mode 100644 index cc99874..0000000 --- a/game/src/serial.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::{ - sync::{atomic::AtomicU8, Arc}, - thread::sleep, - time::Duration, -}; - -use bidrum_data_struct_lib::janggu::JangguFace; -use serialport::SerialPort; - -use bidrum_data_struct_lib::janggu::JangguInputState; - -pub(crate) fn parse_janggu_bits(bits: u8) -> JangguInputState { - JangguInputState { - 궁채: if bits & 1 != 0 { - Some(JangguFace::궁편) - } else if bits & 2 != 0 { - Some(JangguFace::열편) - } else { - None - }, - 열채: if bits & 4 != 0 { - Some(JangguFace::궁편) - } else if bits & 8 != 0 { - Some(JangguFace::열편) - } else { - None - }, - } -} - -/// Read serial inputs from port and emulates key inputs -pub(crate) fn read_serial_loop(mut port: Box, bits_data: Arc) { - loop { - loop { - let available_bytes: u32 = port.bytes_to_read().expect("Failed to read buffer size"); - if available_bytes > 0 { - break; - } - sleep(Duration::from_millis(10)); - } - let mut message: [u8; 1] = [0]; - - port.read_exact(message.as_mut_slice()) - .expect("Controller reading failure!"); - - bits_data.store(message[0], std::sync::atomic::Ordering::Relaxed); - } -}