diff --git a/game/src/game/mod.rs b/game/src/game/mod.rs index 80941bc..528d997 100644 --- a/game/src/game/mod.rs +++ b/game/src/game/mod.rs @@ -1,7 +1,10 @@ use crate::board::Board; +#[derive(Clone, Copy, PartialEq)] pub enum GameState { - Active, + None, + Joinable, + Ongoing, Complete, } @@ -13,7 +16,7 @@ impl Game { pub fn new() -> Self { Self { - state:GameState::Active, + state:GameState::Joinable, board:Board::new(), } } diff --git a/game/src/lib.rs b/game/src/lib.rs index 4eff8de..84cd7fe 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -4,4 +4,4 @@ pub mod util; pub mod piece; pub mod board; pub mod history; -mod game; pub use game::Game; +pub mod game; pub use game::Game; diff --git a/game/src/util/binary.rs b/game/src/util/binary.rs index 5dc3ea8..8bd2511 100644 --- a/game/src/util/binary.rs +++ b/game/src/util/binary.rs @@ -1 +1,2 @@ pub const fn bit(v:u32) -> u32 { 1u32 << v } +pub const fn mask(b:u32, s:u32) -> u32 { ((1u32 << b) - 1) << s } diff --git a/server/src/app/authentication.rs b/server/src/app/authentication.rs index be568ff..c0c136a 100644 --- a/server/src/app/authentication.rs +++ b/server/src/app/authentication.rs @@ -1,17 +1,18 @@ pub type AuthToken = [u8; 8]; +pub type AuthSecret = [u8; 16]; #[derive(Clone, Copy)] pub struct Authentication { pub key:AuthToken, - pub secret:[u8; 16], + pub secret:AuthSecret, pub user:u32, } impl Authentication { pub fn new() -> Self { Self { - key:[0; 8], - secret:[0; 16], + key:AuthToken::default(), + secret:AuthSecret::default(), user:0, } } diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index 03f7b95..33f39e7 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -3,12 +3,15 @@ use sparse::Sparse; use pool::Pool; use trie::Trie; -use crate::util::pack::pack_u32; +use crate::util::{ + Chain, + pack::pack_u32, +}; pub mod connection; use connection::Connection; pub mod user; use user::User; pub mod authentication; use authentication::Authentication; -pub mod session; use session::Session; +pub mod session; use session::{Session, SessionToken}; pub mod context; pub struct App { @@ -22,6 +25,8 @@ pub struct App { pub auths:Trie, pub sessions:Trie, + + pub session_time:Chain, } impl App { pub fn new() -> Self @@ -37,6 +42,8 @@ impl App { auths:Trie::new(), sessions:Trie::new(), + + session_time:Chain::new(), } } @@ -54,7 +61,7 @@ impl App { fs::write(path_data.join("s/.i"), pack_u32(0)).ok(); fs::write(path_data.join("u/.i"), pack_u32(0)).ok(); } - + Ok(()) } } diff --git a/server/src/app/session.rs b/server/src/app/session.rs index d20bdc1..f5b8cbe 100644 --- a/server/src/app/session.rs +++ b/server/src/app/session.rs @@ -1,12 +1,22 @@ use game::Game; -pub struct Session { - key:[u8; 8], - secret:[u8; 8], +pub type SessionToken = [u8; 8]; +pub type SessionSecret = [u8; 8]; - game:Game, - - p_dawn:Option, - p_dusk:Option, - specators:Vec, +pub struct Viewer { + pub connection:Option, + pub user:Option, +} + +pub struct Session { + pub key:SessionToken, + pub secret:SessionSecret, + + pub game:Game, + + pub p_dawn:Viewer, + pub p_dusk:Viewer, + pub viewers:Vec, + + pub chain_id:usize, } diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index 92ee856..8331053 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -21,6 +21,16 @@ pub async fn thread_system(mut app:App, bus:Bus) while match bus.receive_wait() { Some(packet) => { let qr = &packet.data; + + let mut user_id = None; + if let Some(conn) = app.connections.get(qr.id as usize) { + if let Some(auth_id) = conn.auth { + if let Some(auth) = app.auths.get(&auth_id) { + user_id = Some(auth.user); + } + } + } + match &qr.data { QConn(request) => { let id = app.connections.add(Connection { @@ -177,6 +187,31 @@ pub async fn thread_system(mut app:App, bus:Bus) bus.send(packet.from, QRPacket::new(qr.id, RAuth(response))).is_ok() } + QAuthResume(request) => { + let mut response = PacketAuthResumeResponse::new(); + response.status = STATUS_ERROR; + + if let Some(auth) = app.auths.get(&request.token) { + + // Compare full secret length to reduce time-based attacks. + let mut valid = true; + for i in 0..16 { + valid |= auth.secret[i] == request.secret[i]; + } + + if valid { + if let Some(conn) = app.connections.get_mut(qr.id as usize) { + conn.auth = Some(request.token); + response.status = STATUS_OK; + } else { + response.status = STATUS_SERVER_ERROR; + } + } + } + + bus.send(packet.from, QRPacket::new(qr.id, RAuthResume(response))).is_ok() + } + QDeauth => { if let Some(conn) = app.connections.get_mut(qr.id as usize) { match conn.auth { @@ -191,6 +226,173 @@ pub async fn thread_system(mut app:App, bus:Bus) true } + QSessionList(request) => { + use game::game::GameState; + + println!("Request: Session List"); + + let mut response = PacketSessionListResponse::new(); + + let mut count = 0; + + let mut next_id = app.session_time.begin(); + while let Some(id) = next_id { + let token = app.session_time.get(id).unwrap(); + if let Some(session) = app.sessions.get(token) { + + // Requirements: + // - GameState must be None or match state of session. + // - Joinable must have either player slot empty. + // - IsPlayer must have the current user in either player slot. + // - IsLive must have both users connected to the session. + + let mut valid = request.game_state == GameState::None || request.game_state == session.game.state; + valid &= request.game_state != GameState::Joinable || session.p_dawn.user.is_none() || session.p_dusk.user.is_none(); + valid &= !request.is_player || session.p_dawn.user == user_id || session.p_dusk.user == user_id; + valid &= !request.is_live || (session.p_dawn.connection.is_some() && session.p_dusk.connection.is_some()); + + if valid { + let is_player = user_id.is_some() && (session.p_dawn.user == user_id || session.p_dusk.user == user_id); + + let dawn_handle = if let Some(uid) = session.p_dawn.user { + if let Some(cuid) = app.user_id.get(uid as isize) { + if let Some(user) = app.users.get(*cuid) { + user.handle.clone() + } else { String::new() } + } else { String::new() } + } else { String::new() }; + + let dusk_handle = if let Some(uid) = session.p_dusk.user { + if let Some(cuid) = app.user_id.get(uid as isize) { + if let Some(user) = app.users.get(*cuid) { + user.handle.clone() + } else { String::new() } + } else { String::new() } + } else { String::new() }; + + response.records.push(PacketSessionListResponseRecord { + token:session.key, + handles:[ + dawn_handle, + dusk_handle, + ], + turn:0, + last_move:[0; 5], + viewers:0, + player:is_player, + }); + + count += 1; + } + } + + if count >= 60 { break; } + next_id = app.session_time.next(id); + } + + bus.send(packet.from, QRPacket::new(qr.id, RSessionList(response))).is_ok() + } + + QSessionCreate(_request) => { + use crate::app::session::*; + + println!("Request: Session Create"); + + let mut response = PacketSessionCreateResponse::new(); + response.status = STATUS_ERROR; + + if let Some(uid) = user_id { + // Generate session token + let mut token = SessionToken::default(); + let mut secret = SessionSecret::default(); + loop { + rng.fill(&mut token).ok(); + if app.sessions.get(&token).is_none() { break; } + } + rng.fill(&mut secret).ok(); + + let chain_id = app.session_time.add(token); + app.sessions.set(&token, Session { + key:token, + secret:secret, + game:game::Game::new(), + p_dawn:Viewer { + connection:None,//Some(qr.id), + user:Some(uid), + }, + p_dusk:Viewer { + connection:None, + user:None, + }, + viewers:Vec::new(), + chain_id:chain_id, + }); + app.session_time.set(chain_id, token); + + // Set player to Dawn. + response.mode = 0; + + response.status = STATUS_OK; + response.token = token; + } + + bus.send(packet.from, QRPacket::new(qr.id, RSessionCreate(response))).is_ok() + } + + QSessionJoin(request) => { + println!("Request: Session Join"); + + let mut response = PacketSessionJoinResponse::new(); + response.status = STATUS_ERROR; + + // Verify that session exists. + if let Some(session) = app.sessions.get_mut(&request.token) { + + // Join game as player. + if request.join { + + // Verify client is authenticated. + if let Some(uid) = user_id { + + // User must not already be player. + if session.p_dawn.user != user_id && session.p_dusk.user != user_id { + + // Add user to empty player slot. + if if session.p_dawn.user.is_none() { + session.p_dawn.user = Some(uid); + response.mode = 0; + true + } else if session.p_dusk.user.is_none() { + session.p_dusk.user = Some(uid); + response.mode = 1; + true + } else { + // Session is not empty. + response.status = STATUS_ERROR; + false + } { + println!("Add user to session."); + response.status = STATUS_OK; + } + } else { + println!("User resumes session."); + response.status = STATUS_OK; + response.mode = (session.p_dusk.user == user_id) as u8; + } + } else { response.status = STATUS_NOAUTH; } + } + + // Join game as spectator. + else { + println!("User spectates session."); + response.status = STATUS_OK; + response.mode = 2; + } + } + + bus.send(packet.from, QRPacket::new(qr.id, RSessionJoin(response))).is_ok() + } + _ => { true } } } diff --git a/server/src/manager/ws.rs b/server/src/manager/ws.rs index 23f71ae..612ef4f 100644 --- a/server/src/manager/ws.rs +++ b/server/src/manager/ws.rs @@ -55,6 +55,9 @@ pub async fn handle_ws(mut ws:WebSocketStream>, args:HttpServi Message::Binary(data) => { let mut index :usize = 0; let code: u16 = unpack_u16(&data, &mut index); + + println!("MESSAGE {:x}", code); + match code { CODE_REGISTER => match PacketRegister::decode(&data, &mut index) { @@ -103,10 +106,102 @@ pub async fn handle_ws(mut ws:WebSocketStream>, args:HttpServi Err(_) => { } } + CODE_AUTH_RESUME => match PacketAuthResume::decode(&data, &mut index) { + Ok(packet) => { + if args.bus.send(bus_ds, QRPacket::new(conn_id, QAuthResume(packet))).is_ok() { + while match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match &qr.data { + RAuth(resp) => { + ws.send(Message::Binary( + encode_response(code, resp.encode()) + )).await.ok(); + false + } + _ => true, + } + } + None => true, + } { } + } + } + Err(_) => { } + } + CODE_DEAUTH => { args.bus.send(bus_ds, QRPacket::new(conn_id, QDeauth)).ok(); } + CODE_SESSION_LIST => match PacketSessionList::decode(&data, &mut index) { + Ok(packet) => { + if args.bus.send(bus_ds, QRPacket::new(conn_id, QSessionList(packet))).is_ok() { + while match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match &qr.data { + RSessionList(resp) => { + ws.send(Message::Binary( + encode_response(code, resp.encode()) + )).await.ok(); + false + } + _ => true, + } + } + None => true, + } { } + } + } + Err(_) => { println!("error: packet decode failed."); } + } + + CODE_SESSION_CREATE => match PacketSessionCreate::decode(&data, &mut index) { + Ok(packet) => { + if args.bus.send(bus_ds, QRPacket::new(conn_id, QSessionCreate(packet))).is_ok() { + while match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match &qr.data { + RSessionCreate(resp) => { + ws.send(Message::Binary( + encode_response(code, resp.encode()) + )).await.ok(); + false + } + _ => true, + } + } + None => true, + } { } + } + } + Err(_) => { println!("error: packet decode failed."); } + } + + CODE_SESSION_JOIN => match PacketSessionJoin::decode(&data, &mut index) { + Ok(packet) => { + if args.bus.send(bus_ds, QRPacket::new(conn_id, QSessionJoin(packet))).is_ok() { + while match args.bus.receive_wait() { + Some(resp) => { + let qr = &resp.data; + match &qr.data { + RSessionJoin(resp) => { + ws.send(Message::Binary( + encode_response(code, resp.encode()) + )).await.ok(); + false + } + _ => true, + } + } + None => true, + } { } + } + } + Err(_) => { println!("error: packet decode failed."); } + } + _ => { } } true diff --git a/server/src/protocol/code.rs b/server/src/protocol/code.rs index 66982b7..2225af6 100644 --- a/server/src/protocol/code.rs +++ b/server/src/protocol/code.rs @@ -1,24 +1,32 @@ -#![allow(dead_code)] +/* +** Status Codes +*/ pub const STATUS_OK :u16 = 0x0000; pub const STATUS_ERROR :u16 = 0x0001; +pub const STATUS_NOAUTH :u16 = 0x0002; -pub const STATUS_BAD_HANDLE :u16 = 0x0001; -pub const STATUS_BAD_SECRET :u16 = 0x0002; -pub const STATUS_BAD_CODE :u16 = 0x0003; +pub const STATUS_BAD_HANDLE :u16 = 0x0010; +pub const STATUS_BAD_SECRET :u16 = 0x0011; +pub const STATUS_BAD_CODE :u16 = 0x0012; +pub const STATUS_SERVER_ERROR :u16 = 0x00FE; pub const STATUS_NOT_IMPL :u16 = 0x00FF; + +/* +** Operation Codes +*/ + pub const CODE_REGISTER :u16 = 0x0010; pub const CODE_AUTH :u16 = 0x0011; pub const CODE_DEAUTH :u16 = 0x0013; pub const CODE_AUTH_RESUME :u16 = 0x0012; -pub const CODE_LIST_SESSION :u16 = 0x0010; - -pub const CODE_SESSION_JOIN :u16 = 0x0020; -pub const CODE_SESSION_SPECTATE :u16 = 0x0021; -pub const CODE_SESSION_LEAVE :u16 = 0x0022; -pub const CODE_SESSION_RETIRE :u16 = 0x0023; +pub const CODE_SESSION_LIST :u16 = 0x0020; +pub const CODE_SESSION_CREATE :u16 = 0x0021; +pub const CODE_SESSION_JOIN :u16 = 0x0022; +pub const CODE_SESSION_RETIRE :u16 = 0x002E; +pub const CODE_SESSION_LEAVE :u16 = 0x002F; pub const CODE_GAME_PLAY :u16 = 0x0030; diff --git a/server/src/protocol/mod.rs b/server/src/protocol/mod.rs index ed98afa..157d279 100644 --- a/server/src/protocol/mod.rs +++ b/server/src/protocol/mod.rs @@ -18,10 +18,19 @@ pub enum QRPacketData { QAuth(PacketAuth), RAuth(PacketAuthResponse), + QAuthResume(PacketAuthResume), + RAuthResume(PacketAuthResumeResponse), + QDeauth, - QAuthResume, - RAuthResume, + QSessionList(PacketSessionList), + RSessionList(PacketSessionListResponse), + + QSessionCreate(PacketSessionCreate), + RSessionCreate(PacketSessionCreateResponse), + + QSessionJoin(PacketSessionJoin), + RSessionJoin(PacketSessionJoinResponse), } #[derive(Clone)] diff --git a/server/src/protocol/packet/authenticate.rs b/server/src/protocol/packet/auth.rs similarity index 100% rename from server/src/protocol/packet/authenticate.rs rename to server/src/protocol/packet/auth.rs diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs index 8b3100e..4901996 100644 --- a/server/src/protocol/packet/mod.rs +++ b/server/src/protocol/packet/mod.rs @@ -1,6 +1,12 @@ mod connect; pub use connect::*; + mod register; pub use register::*; -mod authenticate; pub use authenticate::*; +mod auth; pub use auth::*; +mod resume; pub use resume::*; + +mod session_list; pub use session_list::*; +mod session_create; pub use session_create::*; +mod session_join; pub use session_join::*; mod prelude { pub trait Packet { diff --git a/server/src/protocol/packet/resume.rs b/server/src/protocol/packet/resume.rs new file mode 100644 index 0000000..b7ef156 --- /dev/null +++ b/server/src/protocol/packet/resume.rs @@ -0,0 +1,61 @@ +use crate::{ + app::authentication::{AuthToken, AuthSecret}, + util::pack::pack_u16, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketAuthResume { + pub token:AuthToken, + pub secret:AuthSecret, +} +impl PacketAuthResume { + pub fn new() -> Self + { + Self { + token:AuthToken::default(), + secret:AuthSecret::default(), + } + } +} +impl Packet for PacketAuthResume { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + if data.len() - *index == 24 { + for i in 0..8 { result.token[i] = data[*index]; *index += 1; } + for i in 0..16 { result.secret[i] = data[*index]; *index += 1; } + Ok(result) + } else { + Err(()) + } + } +} + + +#[derive(Clone)] +pub struct PacketAuthResumeResponse { + pub status:u16, +} +impl PacketAuthResumeResponse { + pub fn new() -> Self + { + Self { + status:0, + } + } +} +impl Packet for PacketAuthResumeResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + ].concat() + } +} diff --git a/server/src/protocol/packet/session_create.rs b/server/src/protocol/packet/session_create.rs new file mode 100644 index 0000000..87678da --- /dev/null +++ b/server/src/protocol/packet/session_create.rs @@ -0,0 +1,55 @@ +use crate::{ + app::session::SessionToken, + util::pack::{pack_u8, pack_u16}, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketSessionCreate { + +} +impl PacketSessionCreate { + pub fn new() -> Self + { + Self { } + } +} +impl Packet for PacketSessionCreate { + type Data = Self; + + fn decode(_data:&Vec, _index:&mut usize) -> Result + { + Ok(Self::new()) + } +} + + +#[derive(Clone)] +pub struct PacketSessionCreateResponse { + pub status:u16, + pub token:SessionToken, + pub mode:u8, +} +impl PacketSessionCreateResponse { + pub fn new() -> Self + { + Self { + status:0, + token:SessionToken::default(), + mode:0, + } + } +} +impl Packet for PacketSessionCreateResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + pack_u8(self.mode), + self.token.to_vec(), + ].concat() + } +} diff --git a/server/src/protocol/packet/session_join.rs b/server/src/protocol/packet/session_join.rs new file mode 100644 index 0000000..824ddf1 --- /dev/null +++ b/server/src/protocol/packet/session_join.rs @@ -0,0 +1,67 @@ +use crate::{ + app::session::SessionToken, + util::pack::{pack_u8, pack_u16}, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketSessionJoin { + pub token:SessionToken, + pub join:bool, +} +impl PacketSessionJoin { + pub fn new() -> Self + { + Self { + token:SessionToken::default(), + join:false, + } + } +} +impl Packet for PacketSessionJoin { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + if data.len() - *index == 9 { + for i in 0..8 { result.token[i] = data[*index]; *index += 1; } + result.join = data[*index] != 0; + Ok(result) + } else { + Err(()) + } + } +} + + +#[derive(Clone)] +pub struct PacketSessionJoinResponse { + pub status:u16, + pub token:SessionToken, + pub mode:u8, +} +impl PacketSessionJoinResponse { + pub fn new() -> Self + { + Self { + status:0, + token:SessionToken::default(), + mode:0, + } + } +} +impl Packet for PacketSessionJoinResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + [ + pack_u16(self.status), + pack_u8(self.mode), + self.token.to_vec(), + ].concat() + } +} diff --git a/server/src/protocol/packet/session_list.rs b/server/src/protocol/packet/session_list.rs new file mode 100644 index 0000000..d66ca3c --- /dev/null +++ b/server/src/protocol/packet/session_list.rs @@ -0,0 +1,121 @@ +use crate::{ + app::session::SessionToken, + util::pack::{pack_u16, pack_u32, unpack_u16}, +}; +use game::{ + game::GameState, + util::mask, +}; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketSessionList { + pub page:u16, + pub game_state:GameState, + pub is_player:bool, + pub is_live:bool, +} +impl PacketSessionList { + pub fn new() -> Self + { + Self { + page:0, + game_state:GameState::Joinable, + is_player:false, + is_live:false, + } + } +} +impl Packet for PacketSessionList { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + /* Read flags + ** 0:[2] - Game state + ** 2:[1] - User is player of session + ** 3:[1] - Both players are online + */ + if data.len() - *index == 4 { + let flags = unpack_u16(data, index); + result.game_state = match flags & mask(2, 0) as u16 { + 1 => GameState::Joinable, + 2 => GameState::Ongoing, + 3 => GameState::Complete, + _ => GameState::None, + }; + result.is_player = (flags & mask(1, 2) as u16) != 0; + result.is_live = (flags & mask(1, 3) as u16) != 0; + + result.page = unpack_u16(data, index); + + Ok(result) + } else { + Err(()) + } + } +} + + +#[derive(Clone)] +pub struct PacketSessionListResponseRecord { + pub token:SessionToken, + pub handles:[String; 2], + pub turn:u16, + pub last_move:[u8; 5], + pub viewers:u32, + pub player:bool, +} + +#[derive(Clone)] +pub struct PacketSessionListResponse { + pub records:Vec, +} +impl PacketSessionListResponse { + pub fn new() -> Self + { + Self { + records:Vec::new(), + } + } +} +impl Packet for PacketSessionListResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + let mut result = pack_u16(self.records.len() as u16); + + for record in &self.records { + let mut chunk = record.token.to_vec(); + + // Dawn handle + let mut bytes = record.handles[0].as_bytes().to_vec(); + chunk.append(&mut pack_u16(bytes.len() as u16)); + if bytes.len() > 0 { chunk.append(&mut bytes); } + + // Dusk handle + let mut bytes = record.handles[1].as_bytes().to_vec(); + chunk.append(&mut pack_u16(bytes.len() as u16)); + if bytes.len() > 0 { chunk.append(&mut bytes); } + + // Turn number + chunk.append(&mut pack_u16(record.turn)); + + // Last move + chunk.append(&mut record.last_move.to_vec()); + + // Spectator count + chunk.append(&mut pack_u32(record.viewers)); + + // User is player + chunk.append(&mut vec![record.player as u8]); + + result.append(&mut chunk); + } + result + } +} diff --git a/server/src/system/cache/mod.rs b/server/src/system/cache/mod.rs index 29c5cd3..2622366 100644 --- a/server/src/system/cache/mod.rs +++ b/server/src/system/cache/mod.rs @@ -19,8 +19,6 @@ impl WebCache { use std::fs::File; let mut html = String::new(); - let mut css = String::new(); - let mut js = String::new(); let mut favicon = Vec::::new(); // Cache html file @@ -32,23 +30,43 @@ impl WebCache { Err(_) => { } } - // Cache css file - match File::open("www/.css") { - Ok(mut file) => { - file.read_to_string(&mut css).ok(); - css = minimize_whitespace(&css); + // Cache js file + let css_path = std::path::Path::new("www/css/"); + let css: String = [ + "main.css", + "util.css", + "ui.css", + "form.css", + "game.css", + ].map(|path| { + let mut buffer = String::new(); + match File::open(css_path.join(path)) { + Ok(mut file) => { file.read_to_string(&mut buffer).ok(); } + Err(_) => { } } - Err(_) => { } - } + buffer + }).concat(); // Cache js file - match File::open("www/.js") { - Ok(mut file) => { - file.read_to_string(&mut js).ok(); - //js = minimize_whitespace(&js); + let js_path = std::path::Path::new("www/js/"); + let js = [ + "const.js", + "util.js", + "game_asset.js", + "game.js", + "interface.js", + "ui.js", + "scene.js", + "system.js", + "main.js", + ].map(|path| { + let mut buffer = String::new(); + match File::open(js_path.join(path)) { + Ok(mut file) => { file.read_to_string(&mut buffer).ok(); } + Err(_) => { } } - Err(_) => { } - } + buffer + }).concat(); // Cache favicon file match File::open("www/favicon.png") { diff --git a/server/src/util/chain.rs b/server/src/util/chain.rs new file mode 100644 index 0000000..b81ff9b --- /dev/null +++ b/server/src/util/chain.rs @@ -0,0 +1,114 @@ +use pool::Pool; + +struct Node { + pub data:T, + pub prev:Option, + pub next:Option, +} + +pub struct Chain { + nodes:Pool>, + begin:Option, + end:Option, +} +impl Chain { + pub fn new() -> Self + { + Self { + nodes:Pool::new(), + begin:None, + end:None, + } + } + + pub fn add(&mut self, data:T) -> usize + // Add an element to the top of the chain. + // + { + let id = self.nodes.add(Node { + data:data, + prev:None, + next:self.begin, + }); + self.begin = Some(id); + id + } + + pub fn promote(&mut self, id:usize) + // Move an element to the top of the chain. + // + { + let mut prev = None; + let mut next = None; + + if let Some(node) = self.nodes.get_mut(id) { + prev = node.prev; + next = node.next; + node.prev = None; + node.next = self.begin; + } + self.begin = Some(id); + if self.begin == self.end { self.end = next; } + + if let Some(pid) = prev { + if let Some(pnode) = self.nodes.get_mut(pid) { + pnode.next = next; + } + } + if let Some(nid) = next { + if let Some(nnode) = self.nodes.get_mut(nid) { + nnode.next = prev; + } + } + } + + pub fn get(&self, id:usize) -> Option<&T> + { + if let Some(node) = self.nodes.get(id) { + Some(&node.data) + } else { + None + } + } + + pub fn set(&mut self, id:usize, data:T) + { + if let Some(node) = self.nodes.get_mut(id) { + node.data = data; + } + } + + pub fn begin(&self) -> Option + { + self.begin + } + + pub fn next(&self, id:usize) -> Option + { + if let Some(node) = self.nodes.get(id) { + node.next + } else { + None + } + } + + pub fn remove(&mut self) + // Remove the last element. + // + { + let mut prev = None; + if let Some(end) = self.end { + if let Some(node) = self.nodes.get_mut(end) { + prev = node.prev; + } + self.nodes.remove(end).ok(); + + if let Some(id) = prev { + if let Some(node) = self.nodes.get_mut(id) { + node.next = None; + } + } + self.end = prev; + } + } +} diff --git a/server/src/util/mod.rs b/server/src/util/mod.rs index bdd920d..7966c06 100644 --- a/server/src/util/mod.rs +++ b/server/src/util/mod.rs @@ -2,3 +2,4 @@ pub mod color; pub mod string; pub mod pack; +mod chain; pub use chain::Chain; diff --git a/server/src/util/pack.rs b/server/src/util/pack.rs index b3a222e..1928590 100644 --- a/server/src/util/pack.rs +++ b/server/src/util/pack.rs @@ -1,3 +1,18 @@ +pub fn pack_u8(value:u8) -> Vec +{ + vec![value] +} + +pub fn unpack_u8(data:&Vec, index:&mut usize) -> u8 +{ + let mut result :u8 = 0; + if *index < data.len() { + result = data[*index]; + *index += 1; + } + result +} + pub fn pack_u16(value:u16) -> Vec { vec![(value >> 8) as u8, (value & 0xFF) as u8] diff --git a/www/.css b/www/.css deleted file mode 100644 index 5b10137..0000000 --- a/www/.css +++ /dev/null @@ -1,344 +0,0 @@ -*{ - box-sizing:border-box; -} - -html{ - display:block; - position:relative; - width:100%; - height:100%; - padding:0; - margin:0; - overflow:hidden; -} - -body{ - display:flex; - position:relative; - flex-flow:row nowrap; - align-items:flex-start; - justify-content:flex-start; - width:100%; - height:100%; - padding:0; - margin:0; - overflow:hidden; - - font-family:sans-serif; -} - -body>nav{ - display:block; - position:relative; - width:9rem; - height:100%; - - background-color:#282828; -} -body>nav>button{ - display:block; - position:relative; - width:100%; - height:2.5rem; - padding:0 1rem 0 1rem; - - text-align:left; - font-size:1.25rem; - cursor:pointer; - - background-color:#282828; - color:#c0c0c0; - border:0; - outline:0; -} body>nav>button:hover{ - background-color:#383838; - color:#e0e0e0; -} - -body>nav>header{ - display:block; - position:relative; - width:100%; - height:3rem; - - line-height:3rem; - text-align:center; - font-size:1.8rem; - font-weight:bold; - font-variant:small-caps; - - color:#d0d0d0; - border-bottom:1px solid #404040; -} - -main{ - display:flex; - position:relative; - flex-flow:column nowrap; - align-items:flex-start; - justify-content:flex-start; - width:100%; - height:100%; - - flex-grow:1; - - background-color:#202020; -} - -main>nav{ - display:flex; - position:relative; - flex-flow:row nowrap; - align-items:flex-start; - justify-content:flex-start; - width:100%; - height:3rem; - - background-color:#282828; - border-bottom:1px solid #404040; -} - -main>nav>section:first-child{ - display:flex; - position:relative; - flex-flow:row nowrap; - align-items:flex-start; - justify-content:flex-start; - height:100%; -} - -main>nav>section:last-child{ - display:flex; - position:relative; - flex-flow:row nowrap; - align-items:flex-start; - justify-content:flex-end; - height:100%; - flex-grow:1; -} - -main>nav>section>button{ - display:block; - position:relative; - width:auto; - height:100%; - padding:0 1rem 0 1rem; - text-align:left; - font-size:1.25rem; - cursor:pointer; - - background-color:#282828; - color:#c0c0c0; - border:0; - outline:0; -} -main>nav>section>button:hover{ - background-color:#383838; - color:#e0e0e0; -} - -main>nav>section>div{ - display:block; - position:relative; - width:auto; - height:100%; - padding:0 1rem 0 1rem; - - line-height:3rem; - text-align:left; - font-size:1.25rem; - - color:#e0e0e0; -} - -main.list table{ - width:100%; - border-collapse:collapse; -} -main.list table tr{ - height:2.5rem; -} -main.list table th{ - padding:0 1rem 0 1rem; - - text-align:center; - font-size:1.2rem; - font-weight:bold; - - background-color:#383838; - color:#f0f0f0; - border-bottom:1px solid #404040; -} -main.list table td{ - height:100%; - padding:0 1rem 0 1rem; - - text-align:center; - - background-color:#303030; - color:#f0f0f0; -} -main.list table td:last-child{ - display:flex; - flex-flow:row nowrap; - align-items:flex-start; - justify-content:flex-end; - flex-grow:1; -} - -main.list table td:last-child>button{ - display:block; - position:relative; - width:auto; - height:100%; - padding:0 1rem 0 1rem; - - text-align:left; - font-size:1.25rem; - cursor:pointer; - - background-color:#303030; - color:#e0e0e0; - border:0; - outline:0; -} -main.list table td:last-child>button:hover{ - background-color:#343434; -} - -main.game>canvas{ - display:block; - position:relative; - width:100%; - height:100%; - padding:0; - margin:0; - - background-color:#202020; -} - -main.game>div.sidemenu{ - display:flex; - position:absolute; - bottom:0px; - right:1px; - z-index:10; - - flex-flow:column nowrap; - - width:auto; - height:auto; - - border:0; - border-top-left-radius:1rem; - - overflow:hidden; -} - -main.game>div.sidemenu>button{ - display:block; - position:relative; - padding:1rem; - margin:0 0 1px 0; - background-color:#202020; - color:#F0F0F0; - border:0; - outline:0; - - font-family:sans-serif; - font-size:1rem; - - cursor:pointer; -} -main.game>div.sidemenu>button:hover{ - background-color:#404040; -} -main.game>div.sidemenu>button.warn:hover{ - background-color:#602020; -} - -main.form>section{ - display:flex; - position:relative; - flex-flow:column nowrap; - align-items:center; - justify-content:center; - width:100%; - height:auto; - flex-grow:1; -} - -main.form>section>div{ - display:block; - position:relative; - width:100%; - max-width:30rem; - padding:0.5rem; - - background-color:#303030; -} - -main.form>section>div>table{ - width:100%; -} - -main.form>section>div>table label{ - display:block; - position:relative; - width:auto; - height:auto; - padding:0 0.5rem 0 0.5rem; - - text-align:right; - font-size:1.2rem; - - color:#e0e0e0; -} - -main.form>section>div>table input{ - display:block; - position:relative; - width:100%; - height:2.5rem; - padding:0 1rem 0 1rem; - - text-align:left; - font-size:1.15rem; - - background-color:#202020; - color:#e0e0e0; - border:1px solid #303030; - outline:0; -} -main.form>section>div>table input.error{ - border:1px solid #603030; -} - -main.form>section>div>button{ - display:block; - position:relative; - width:100%; - height:2.5rem; - padding:0.5rem 1rem 0.5rem 1rem; - - font-size:1.2rem; - cursor:pointer; - - background-color:#303030; - color:#e0e0e0; - border:0; - outline:0; -} -main.form>section>div>button.error{ - border:1px solid #603030; -} -main.form>section>div>button:hover{ - background-color:#383838; -} -main.form>section>div>button:disabled{ - background-color:#2c2c2c; - color:#606060; - cursor:pointer; -} - -span.text-system{color:#909090;} diff --git a/www/css/form.css b/www/css/form.css new file mode 100644 index 0000000..00b9005 --- /dev/null +++ b/www/css/form.css @@ -0,0 +1,83 @@ +main.form>section{ + display:flex; + position:relative; + flex-flow:column nowrap; + align-items:center; + justify-content:center; + width:100%; + height:auto; + flex-grow:1; +} + +main.form>section>div{ + display:block; + position:relative; + width:100%; + max-width:30rem; + padding:0.5rem; + + background-color:#303030; +} + +main.form>section>div>table{ + width:100%; +} + +main.form>section>div>table label{ + display:block; + position:relative; + width:auto; + height:auto; + padding:0 0.5rem 0 0.5rem; + + text-align:right; + font-size:1.2rem; + + color:#e0e0e0; +} + +main.form>section>div>table input{ + display:block; + position:relative; + width:100%; + height:2.5rem; + padding:0 1rem 0 1rem; + + text-align:left; + font-size:1.15rem; + + background-color:#202020; + color:#e0e0e0; + border:1px solid #303030; + outline:0; +} +main.form>section>div>table input.error{ + border:1px solid #603030; +} + +main.form>section>div>button{ + display:block; + position:relative; + width:100%; + height:2.5rem; + padding:0.5rem 1rem 0.5rem 1rem; + + font-size:1.2rem; + cursor:pointer; + + background-color:#303030; + color:#e0e0e0; + border:0; + outline:0; +} +main.form>section>div>button.error{ + border:1px solid #603030; +} +main.form>section>div>button:hover{ + background-color:#383838; +} +main.form>section>div>button:disabled{ + background-color:#2c2c2c; + color:#606060; + cursor:pointer; +} diff --git a/www/css/game.css b/www/css/game.css new file mode 100644 index 0000000..16234d1 --- /dev/null +++ b/www/css/game.css @@ -0,0 +1,8 @@ +main>canvas#game { + display: block; + position: relative; + width: 100%; + flex-grow: 1; + + background-color: #101010; +} diff --git a/www/css/main.css b/www/css/main.css new file mode 100644 index 0000000..b3f9394 --- /dev/null +++ b/www/css/main.css @@ -0,0 +1,162 @@ +*{ + box-sizing:border-box; +} + +html{ + display:block; + position:relative; + width:100%; + height:100%; + padding:0; + margin:0; + overflow:hidden; +} + +body{ + display:flex; + position:relative; + flex-flow:row nowrap; + align-items:flex-start; + justify-content:flex-start; + width:100%; + height:100%; + padding:0; + margin:0; + overflow:hidden; + + font-family:sans-serif; +} + +body>nav{ + display:flex; + position:relative; + flex-flow:column nowrap; + width:9rem; + height:100%; + + background-color:#282828; +} +body>nav>section{ + display:block; + position:relative; + width:100%; + height:auto; +} +body>nav>section:first-of-type{ + flex-grow:1; +} +body>nav>section>button{ + display:block; + position:relative; + width:100%; + height:2.5rem; + padding:0 1rem 0 1rem; + + text-align:left; + font-size:1.25rem; + cursor:pointer; + + background-color:#282828; + color:#c0c0c0; + border:0; + outline:0; +} body>nav>section>button:hover{ + background-color:#383838; + color:#e0e0e0; +} + +body>nav>header{ + display:block; + position:relative; + width:100%; + height:3rem; + + line-height:3rem; + text-align:center; + font-size:1.8rem; + font-weight:bold; + font-variant:small-caps; + + color:#d0d0d0; + border-bottom:1px solid #404040; +} + +main{ + display:flex; + position:relative; + flex-flow:column nowrap; + align-items:flex-start; + justify-content:flex-start; + width:100%; + height:100%; + + flex-grow:1; + + background-color:#202020; +} + +main>nav{ + display:flex; + position:relative; + flex-flow:row nowrap; + align-items:flex-start; + justify-content:flex-start; + width:100%; + height:3rem; + + background-color:#282828; + border-bottom:1px solid #404040; +} + +main>nav>section:first-child{ + display:flex; + position:relative; + flex-flow:row nowrap; + align-items:flex-start; + justify-content:flex-start; + height:100%; +} + +main>nav>section:last-child{ + display:flex; + position:relative; + flex-flow:row nowrap; + align-items:flex-start; + justify-content:flex-end; + height:100%; + flex-grow:1; +} + +main>nav>section>button{ + display:block; + position:relative; + width:auto; + height:100%; + padding:0 1rem 0 1rem; + text-align:left; + font-size:1.25rem; + cursor:pointer; + + background-color:#282828; + color:#c0c0c0; + border:0; + outline:0; +} +main>nav>section>button:hover{ + background-color:#383838; + color:#e0e0e0; +} + +main>nav>section>div{ + display:block; + position:relative; + width:auto; + height:100%; + padding:0 1rem 0 1rem; + + line-height:3rem; + text-align:left; + font-size:1.25rem; + + color:#e0e0e0; +} diff --git a/www/css/ui.css b/www/css/ui.css new file mode 100644 index 0000000..0c1e5c6 --- /dev/null +++ b/www/css/ui.css @@ -0,0 +1,54 @@ +main>table.list{ + width:100%; + border-collapse:collapse; +} +main>table.list tr{ + height:2.5rem; +} +main>table.list th{ + padding:0 1rem 0 1rem; + + text-align:center; + font-size:1.2rem; + font-weight:bold; + + background-color:#383838; + color:#f0f0f0; + border-bottom:1px solid #404040; +} +main>table.list td{ + height:100%; + padding:0 1rem 0 1rem; + + text-align:center; + + background-color:#303030; + color:#f0f0f0; +} +main>table.list td:last-child{ + display:flex; + flex-flow:row nowrap; + align-items:flex-start; + justify-content:flex-end; + flex-grow:1; +} + +main>table.list td:last-child>button{ + display:block; + position:relative; + width:auto; + height:100%; + padding:0 1rem 0 1rem; + + text-align:left; + font-size:1.25rem; + cursor:pointer; + + background-color:#303030; + color:#e0e0e0; + border:0; + outline:0; +} +main>table.list td:last-child>button:hover{ + background-color:#343434; +} diff --git a/www/css/util.css b/www/css/util.css new file mode 100644 index 0000000..2bafc97 --- /dev/null +++ b/www/css/util.css @@ -0,0 +1 @@ +span.text-system{color:#909090;} diff --git a/www/js/const.js b/www/js/const.js new file mode 100644 index 0000000..aebf944 --- /dev/null +++ b/www/js/const.js @@ -0,0 +1,40 @@ +let MAIN = null; +let MENU = null; +let SCENE = null; + +let CONNECTED = false; +let SOCKET = null; +let CONTEXT = { + Scene: null, + Auth: null, + Data: null, +}; + +const Status = { + Ok: 0, + Error: 1, + NotImplement: 2, + + BadHandle: 1, + BadSecret: 2, + BadCode: 3, +}; + +const OpCode = { + Register :0x0010, + Authenticate :0x0011, + Resume :0x0012, + Deauthenticate :0x0013, + + SessionList :0x0020, + SessionCreate :0x0021, + SessionJoin :0x0022, + + GameState :0x0030, +}; + +const GameState = { + Joinable :0x00, + Ongoing :0x01, + Complete :0x02, +}; diff --git a/www/js/game.js b/www/js/game.js new file mode 100644 index 0000000..f71a493 --- /dev/null +++ b/www/js/game.js @@ -0,0 +1,157 @@ +class GamePieceMove { + +} + +class GamePiece { + constructor(name, assets, moves, promote_moves) { + this.name = name; + this.assets = assets; + this.moves = moves; + this.pmoves = promote_moves; + } +} + +let GAME_DATA = { + board: { + tiles: [ ], + }, + + pieces: [ + new PieceDef("Militia「兵」", "♟︎", // ♟︎士 + ["asset/militia_dusk.svg", "asset/militia_dawn.svg"], + new Move() + .add(0) + .add(1) + .add(5), + + new Move() + .add(0) + .add(1) + .add(2) + .add(4) + .add(5) + ), + new PieceDef("Knight「騎」", "♞", // ♞馬 + ["asset/knight_dusk.svg", "asset/knight_dawn.svg"], + new Move() + .add(3) + .add(6) + .add(11) + .add(13) + .add(17), + + new Move() + .add(3) + .add(6) + .add(7) + .add(10) + .add(11) + .add(13) + .add(14) + .add(16) + .add(17) + ), + new PieceDef("Lance「槍」", "♛", // ♛槍 + ["asset/lance_dusk.svg", "asset/lance_dawn.svg"], + new Move() + .add(0, true) + .add(1) + .add(5), + + new Move() + .add(0, true) + .add(1, true) + .add(2, true) + .add(3, true) + .add(4, true) + .add(5, true) + ), + new PieceDef("Tower「楼」", "♖", // ♖高 + ["asset/tower_dusk.svg", "asset/tower_dawn.svg"], + new Move() + .add(0) + .add(1) + .add(3) + .add(5) + .add(6) + .add(11), + + new Move() + .add(0) + .add(1) + .add(2) + .add(3) + .add(4) + .add(5) + .add(6) + .add(8) + .add(9) + .add(11) + ), + new PieceDef("Castle「城」", "♜", // ♜城 + ["asset/castle_dusk.svg", "asset/castle_dawn.svg"], + new Move() + .add(0) + .add(1) + .add(2) + .add(4) + .add(5) + .add(7) + .add(10), + + new Move() + .add(0) + .add(1) + .add(2) + .add(3) + .add(4) + .add(5) + .add(7, true) + .add(10, true) + ), + new PieceDef("Dragon「竜」", "♝", // ♝竜 + ["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"], + new Move() + .add(6, true) + .add(7, true) + .add(8, true) + .add(9, true) + .add(10, true) + .add(11, true), + + new Move() + .add(0, true) + .add(1, true) + .add(2, true) + .add(3, true) + .add(4, true) + .add(5, true) + .add(6, true) + .add(7, true) + .add(8, true) + .add(9, true) + .add(10, true) + .add(11, true) + ), + new PieceDef("King「王」", "♚", // ♚王 + ["asset/king_dusk.svg", "asset/king_dawn.svg"], + new Move() + .add(0) + .add(1) + .add(2) + .add(3) + .add(4) + .add(5) + .add(7) + .add(10) + ), + ], +}; + + + +const GAME = { + init() { + GAME_DATA.board.tiles + }, +}; diff --git a/www/js/game_asset.js b/www/js/game_asset.js new file mode 100644 index 0000000..e69de29 diff --git a/www/js/interface.js b/www/js/interface.js new file mode 100644 index 0000000..627d98f --- /dev/null +++ b/www/js/interface.js @@ -0,0 +1,71 @@ +let INTERFACE_DATA = { + canvas:null, + context:null, + + scale:1, +}; + +const INTERFACE = { + hover(event) { + + }, + + click(event) { + + }, + + resize() { + INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth; + INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight; + }, + + draw() { + this.resize(); + + // Determine + let width = INTERFACE_DATA.canvas.width; + let height = INTERFACE_DATA.canvas.height; + let min_dimension = Math.min(width, height); + + let scale = 1; + //let margin = INTERFACE_DATA.canvas. + + + // Draw indicator gradient if player's turn. + + + + // Draw tiles + for(let i = 0; i < GAME.board.tiles.length; ++i) { + // Draw background + + // Draw piece + + // Draw + } + + + // Draw player pool + + + // Draw opponent pool + + }, + + init() { + INTERFACE_DATA.canvas = document.getElementById("game"); + if(canvas !== undefined) { + INTERFACE_DATA.context = canvas.getContext("2d"); + + canvas.addEventListener("mousemove", INTERFACE.hover); + canvas.addEventListener("mousedown", INTERFACE.click); + canvas.addEventListener("resize", INTERFACE.draw); + this.draw(); + } + }, + + uninit() { + INTERFACE_DATA.canvas = null; + INTERFACE_DATA.context = null; + }, +}; diff --git a/www/js/main.js b/www/js/main.js new file mode 100644 index 0000000..f7009bb --- /dev/null +++ b/www/js/main.js @@ -0,0 +1,4 @@ +document.addEventListener("DOMContentLoaded", () => { + SCENE = SCENES.Offline; + LOAD(SCENES.Init); +}); diff --git a/www/.js b/www/js/scene.js similarity index 51% rename from www/.js rename to www/js/scene.js index 8626fea..ff05f9a 100644 --- a/www/.js +++ b/www/js/scene.js @@ -1,173 +1,6 @@ -let MAIN = null; -let MENU = null; -let SCENE = null; - -let CONNECTED = false; -let SOCKET = null; -let CONTEXT = { - Scene: null, - Auth: null, - Data: null, -}; - -const Status = { - Ok: 0, - Error: 1, - NotImplement: 2, - - BadHandle: 1, - BadSecret: 2, - BadCode: 3, -}; - -const OpCode = { - Register :0x0010, - Authenticate :0x0011, - Resume :0x0012, - Deauthenticate :0x0013, -}; - -class Message { - constructor(code, data) { - this.code = code; - this.data = data; - } -} - -const PACK = { - u8:(value) => { - return new Uint8Array([ value & 0xFF ]); - }, - u16:(value) => { - return new Uint8Array([ (value >> 8) & 0xFF, value & 0xFF ]); - }, - u32:(value) => { - return new Uint8Array([ - (value >> 24) & 0xFF, - (value >> 16) & 0xFF, - (value >> 8) & 0xFF, - value & 0xFF - ]); - }, -}; - -const UI = { - text:(value) => { - return document.createTextNode(value); - }, - - button:(text, callback) => { - let button = document.createElement("button"); - button.innerText = text; - if(callback !== null) { button.addEventListener("click", callback); } - return button; - }, - - textbox:(id, placeholder) => { - let input = document.createElement("input"); - input.setAttribute("type", "text"); - if(id !== null) { input.setAttribute("id", id); } - input.setAttribute("placeholder", placeholder); - return input; - }, - - password:(id) => { - let input = document.createElement("input"); - input.setAttribute("type", "password"); - if(id !== null) { input.setAttribute("id", id); } - return input; - }, - - label:(name, id) => { - let label = document.createElement("label"); - label.setAttribute("for", id); - label.innerText = name; - return label; - }, - - div:(children) => { - let div = document.createElement("div"); - for(child of children) { div.appendChild(child); } - return div; - }, - - table:(header, rows) => { - let table = document.createElement("table"); - let tbody = document.createElement("tbody"); - - if(header !== null) { - let row = document.createElement("tr"); - for(head of header) { - let cell = document.createElement("th"); - cell.innerText = head; - row.appendChild(cell); - } - tbody.appendChild(row); - } - - for(row of rows) { - let tr = document.createElement("tr"); - for(node of row) { - let cell = document.createElement("td"); - if(Array.isArray(node)) { for(item of node) { cell.appendChild(item); } } - else { cell.appendChild(node); } - tr.appendChild(cell); - } - tbody.appendChild(tr); - } - - table.appendChild(tbody); - return table; - }, - - mainnav:(left_children, right_children) => { - let header = document.createElement("nav"); - let left = document.createElement("section"); - - if(CONTEXT.Auth === null) { - left.appendChild(UI.button("Register", () => { LOAD(SCENES.Register) })); - left.appendChild(UI.button("Log In", () => { LOAD(SCENES.Authenticate) })); - } - - for(child of left_children) { left.appendChild(child); } - - let right = document.createElement("section"); - for(child of right_children) { right.appendChild(child); } - - header.appendChild(left); - header.appendChild(right); - - MAIN.appendChild(header); - }, - - mainmenu:() => { - if(SOCKET !== null) { - MENU.appendChild(UI.button("Online", () => { LOAD(SCENES.Online); })); - if(CONTEXT.Auth !== null) { - MENU.appendChild(UI.button("Continue", () => { LOAD(SCENES.Continue); })); - MENU.appendChild(UI.button("Join", () => { LOAD(SCENES.Join); })); - } - MENU.appendChild(UI.button("Live", () => { LOAD(SCENES.Live); })); - MENU.appendChild(UI.button("History", () => { LOAD(SCENES.History); })); - MENU.appendChild(UI.button("Guide", () => { LOAD(SCENES.Guide); })); - MENU.appendChild(UI.button("About", () => { LOAD(SCENES.About); })); - - if(CONTEXT.Auth !== null) { - MENU.appendChild(UI.button("Logout", () => { - MESSAGE_COMPOSE([ - PACK.u16(OpCode.Deauthenticate), - ]); - CONTEXT.Auth = null; - LOAD(SCENE); - })); - } - } - }, -}; - const SCENES = { Init:{ - load:() => { + load() { LOAD_OFFLINE(); CONTEXT.Scene = SCENES.Online; RECONNECT(); @@ -176,14 +9,16 @@ const SCENES = { }, Offline:{ - load:() => { - MENU.appendChild(UI.button("Reconnect", () => { RECONNECT(); })) + load() { + UI.nav([ + UI.button("Reconnect", () => { RECONNECT(); }) + ], []); return true; }, }, Register:{ - load:() => { + load() { if(CONTEXT.Auth !== null) return false; UI.mainmenu(); UI.mainnav([], []); @@ -238,7 +73,7 @@ const SCENES = { return true; }, - message:(code, data) => { + message(code, data) { if(code == OpCode.Register && data !== null) { let submit = document.getElementById("submit"); switch(data.status) { @@ -269,7 +104,7 @@ const SCENES = { }, Authenticate:{ - load:() => { + load() { if(CONTEXT.Auth !== null) return false; UI.mainmenu(); UI.mainnav([], []); @@ -281,7 +116,7 @@ const SCENES = { [ UI.label("Secret", "secret"), UI.password("secret") ], ])); - let button = UI.button("Register", (event) => { + let button = UI.button("Login", (event) => { let handle = document.getElementById("handle"); let secret = document.getElementById("secret"); @@ -317,7 +152,7 @@ const SCENES = { return true; }, - message:(code, data) => { + message(code, data) { if(code == OpCode.Authenticate && data !== null) { let submit = document.getElementById("submit"); switch(data.status) { @@ -338,12 +173,19 @@ const SCENES = { }, Online:{ - load:() => { + load() { UI.mainmenu(); + CONTEXT.data = { + page:0, + records:[], + }; + let left_buttons = [ ]; if(CONTEXT.Auth !== null) { - left_buttons.push(UI.button("Start", null)); + left_buttons.push(UI.button("Start", () => { + MESSAGE_SESSION_START(); + })); } UI.mainnav( @@ -352,42 +194,33 @@ const SCENES = { UI.div([UI.text("0 - 0 of 0")]), UI.button("◀", null), UI.button("▶", null), + UI.button("Refresh", null), ] ); let table = document.createElement("table"); table.setAttribute("id", "content"); + table.setAttribute("class", "list"); MAIN.appendChild(table); - MAIN.setAttribute("class", "list"); - SCENE.refresh(); return true; }, - refresh:() => { - let request = new Uint8Array(); - - //SERVER.send() - - SCENE.message(0, 0, null); + refresh() { + MESSAGE_SESSION_LIST(0, 0, false, false); }, - message:(code, data) => { + message(code, data) { let table = document.getElementById("content"); - MAIN.removeChild(table); + UI.clear(table); - let rows = [ - [ UI.text("Player1"), UI.text("Player2"), UI.text("0"), UI.text("Ha1-D ◈ Ba1-M"), UI.text("0"), [ UI.button("Join", null), UI.button("Spectate", null) ] ], - ]; - - MAIN.appendChild(UI.table( - [ "Dawn", "Dusk", "Turn", "Move", "Spectators", "" ], - rows, - )); + if(data !== null) { + table.appendChild(UI.session_table(data.records)); + } } }, Continue:{ - load:() => { + load() { if(CONTEXT.Auth === null) return false; UI.mainmenu(); @@ -414,13 +247,13 @@ const SCENES = { SCENE.refresh(); return true; }, - refresh:() => { + refresh() { }, }, Join:{ - load:() => { + load() { if(CONTEXT.Auth === null) return false; UI.mainmenu(); @@ -447,13 +280,13 @@ const SCENES = { SCENE.refresh(); return true; }, - refresh:() => { + refresh() { }, }, Live:{ - load:() => { + load() { UI.mainmenu(); let left_buttons = [ ]; @@ -479,13 +312,13 @@ const SCENES = { SCENE.refresh(); return true; }, - refresh:() => { + refresh() { }, }, History:{ - load:() => { + load() { UI.mainmenu(); UI.mainnav( @@ -506,190 +339,66 @@ const SCENES = { SCENE.refresh(); return true; }, - refresh:() => { + refresh() { }, }, Guide:{ - load:() => { + load() { UI.mainmenu(); UI.mainnav([], []); return true; }, - refresh:() => { + refresh() { }, }, About:{ - load:() => { + load() { UI.mainmenu(); UI.mainnav([], []); return true; }, - refresh:() => { + refresh() { }, }, Game:{ - load:() => { - MENU.appendChild(UI.button("Back", () => { LOAD(SCENES.Online) })); - MENU.appendChild(UI.button("Retire", () => { })); + load() { + UI.nav([ + UI.button("Rotate", () => { }), + ], [ + UI.button("Back", () => { LOAD(SCENES.Online) }), + UI.button("Retire", () => { }), + ]); + + let canvas = document.createElement("canvas"); + canvas.setAttribute("id", "game"); + + MAIN.appendChild(canvas); + + INTERFACE.init(); + return true; }, - unload:() => { + unload() { }, - refresh:() => { + refresh() { + }, + message() { + }, }, }; -function RECONNECT() { - if(SOCKET === null) { - console.log("Websocket connecting.."); - SOCKET = new WebSocket("wss://omen.kirisame.com:38612"); - SOCKET.binaryType = "arraybuffer"; - SOCKET.addEventListener("error", (event) => { - SOCKET = null; - LOAD(SCENES.Offline) - }); - SOCKET.addEventListener("open", (event) => { - if(SOCKET.readyState === WebSocket.OPEN) { - console.log("Websocket connected."); - - SOCKET.addEventListener("message", MESSAGE); - - SOCKET.addEventListener("close", (event) => { - console.log("Websocket closed."); - SOCKET = null; - RECONNECT(); - }); - - RESUME(); - } - }); - } -} - -function RESUME() { - LOAD(CONTEXT.Scene); -} - -function REBUILD() { - MENU = document.createElement("nav"); - let title = document.createElement("header"); - title.innerText = "Omen"; - MENU.appendChild(title); - - MAIN = document.createElement("main"); - - document.body.appendChild(MENU); - document.body.appendChild(MAIN); -} - -function MESSAGE(event) { - console.log("Message received."); - - if(SCENE.message !== undefined) { - let bytes = new Uint8Array(event.data); - let code = 0; - let index = 2; - let data = null; - - if(bytes.length >= 2) { - code = (bytes[0] << 8) + bytes[1]; - } - - switch(code) { - case OpCode.Register: { - console.log("Register response."); - - if(bytes.length == 28) { - console.log("Good size"); - data = { - status:(bytes[2] << 8) + bytes[3], - token:new Uint8Array(), - secret:new Uint8Array(), - }; - index += 2; - - for(let i = 0; i < 8; ++i) { - data.token += bytes[index++]; - } - for(let i = 0; i < 16; ++i) { - data.secret += bytes[index++]; - } - } else { - console.log("Register bad length:" + bytes.length); - return; - } - } break; - - case OpCode.Authenticate: { - console.log("Authenticate response."); - - if(bytes.length == 28) { - console.log("Good size"); - data = { - status:(bytes[2] << 8) + bytes[3], - token:new Uint8Array(), - secret:new Uint8Array(), - }; - index += 2; - - for(let i = 0; i < 8; ++i) { - data.token += bytes[index++]; - } - for(let i = 0; i < 16; ++i) { - data.secret += bytes[index++]; - } - } else { - console.log("Authenticate bad length:" + bytes.length); - return; - } - } break; - - case OpCode.Resume: { - - } break; - - case OpCode.Deauthenticate: { - - } break; - default: - return; - } - - SCENE.message(code, data); - } -} - -function MESSAGE_COMPOSE(data) { - if(SOCKET !== null) { - let length = 0; - for(let i = 0; i < data.length; ++i) { - length += data[i].length; - } - - let raw = new Uint8Array(length); - length = 0; - for(let i = 0; i < data.length; ++i) { - raw.set(data[i], length); - length += data[i].length; - } - - SOCKET.send(raw); - } -} - function LOAD(scene) { if(SCENE.unload !== undefined) { SCENE.unload(); } - while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); } - REBUILD(); + UI.rebuild(); SCENE = scene; CONTEXT.Scene = SCENE; if(!SCENE.load()) { LOAD(SCENES.Online); } @@ -698,12 +407,7 @@ function LOAD(scene) { function LOAD_OFFLINE() { if(SCENE.unload !== undefined) { SCENE.unload(); } while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); } - REBUILD(); + UI.rebuild(); SCENE = SCENES.Offline; if(!SCENE.load()) { LOAD(SCENES.Online); } } - -document.addEventListener("DOMContentLoaded", () => { - SCENE = SCENES.Offline; - LOAD(SCENES.Init); -}); diff --git a/www/js/system.js b/www/js/system.js new file mode 100644 index 0000000..f0e0f86 --- /dev/null +++ b/www/js/system.js @@ -0,0 +1,255 @@ +function RECONNECT() { + if(SOCKET === null) { + console.log("Websocket connecting.."); + SOCKET = new WebSocket("wss://omen.kirisame.com:38612"); + SOCKET.binaryType = "arraybuffer"; + SOCKET.addEventListener("error", (event) => { + SOCKET = null; + LOAD_OFFLINE() + }); + SOCKET.addEventListener("open", (event) => { + if(SOCKET.readyState === WebSocket.OPEN) { + console.log("Websocket connected."); + + SOCKET.addEventListener("message", MESSAGE); + + SOCKET.addEventListener("close", (event) => { + console.log("Websocket closed."); + SOCKET = null; + RECONNECT(); + }); + + RESUME(); + } + }); + } else { + RESUME(); + } +} + +function RESUME() { + if(CONTEXT.Auth !== null) { + MESSAGE_COMPOSE([ + CONTEXT.Auth.token, + CONTEXT.Auth.secret, + ]); + } else { + LOAD(CONTEXT.Scene); + } +} + +function MESSAGE(event) { + console.log("Message received."); + + if(SCENE.message !== undefined) { + let bytes = new Uint8Array(event.data); + let code = 0; + let index = 2; + let data = null; + let result = null; + + if(bytes.length >= 2) { + code = (bytes[0] << 8) + bytes[1]; + } + + switch(code) { + case OpCode.Register: { + console.log("RECV Register"); + + if(bytes.length - index == 26) { + data = { + status:(bytes[2] << 8) + bytes[3], + token:new Uint8Array(8), + secret:new Uint8Array(16), + }; + index += 2; + + for(let i = 0; i < 8; ++i) { + data.token[i] = bytes[index++]; + } + for(let i = 0; i < 16; ++i) { + data.secret[i] = bytes[index++]; + } + } else { + console.error("Register packet bad length:" + bytes.length); + return; + } + } break; + + case OpCode.Authenticate: { + console.log("RECV Authenticate"); + + if(bytes.length - index == 26) { + data = { + status:(bytes[2] << 8) + bytes[3], + token:new Uint8Array(), + secret:new Uint8Array(), + }; + index += 2; + + for(let i = 0; i < 8; ++i) { + data.token += bytes[index++]; + } + for(let i = 0; i < 16; ++i) { + data.secret += bytes[index++]; + } + } else { + console.error("Authenticate packet bad length:" + bytes.length); + return; + } + } break; + + case OpCode.Resume: { + console.log("RECV Resume"); + + result = UNPACK.u16(bytes, index); + index = result.index; + if(result.data != Status.Ok) { + CONTEXT.Auth = null; + } + + LOAD(CONTEXT.Scene); + } break; + + case OpCode.Deauthenticate: { + console.log("RECV Deauthenticate"); + } break; + + case OpCode.SessionList: { + console.log("RECV Session list"); + + if(bytes.length - index >= 2) { + data = { + records: [], + }; + + result = UNPACK.u16(bytes, index); + index = result.index; + let count = result.data; + + for(let i = 0; i < count; ++i) { + let record = { + token: new Uint8Array(8), + dawn: "", + dusk: "", + turn: 0, + move: "", + viewers: 0, + player: false, + }; + + if(index <= bytes.length + 8) { + for(let i = 0; i < 8; ++i) { + record.token[i] = bytes[index]; + index += 1; + } + } + + result = UNPACK.string(bytes, index); + index = result.index; + record.dawn = result.data; + + result = UNPACK.string(bytes, index); + index = result.index; + record.dusk = result.data; + + result = UNPACK.u16(bytes, index); + index = result.index; + record.turn = result.data; + + if(index <= bytes.length + 5) { + let move = new Uint8Array(5); + for(let i = 0; i < 5; ++i) { + move[i] = bytes[index]; + index += 1; + } + record.move = UNPACK.move(move); + } + + result = UNPACK.u32(bytes, index); + index = result.index; + record.viewers = result.data; + + record.player = bytes[index++] != 0; + + data.records.push(record); + } + } + } break; + + case OpCode.SessionCreate: + case OpCode.SessionJoin: { + if(bytes.length - index == 11) { + data = { + token:new Uint8Array(8), + mode:2, + }; + + let result = UNPACK.u16(data, index); + index = result.index; + let status = result.data; + + result = UNPACK.u8(data, index); + index = result.index; + data.mode = result.data; + + for(let i = 0; i < 8; ++i) { data.token[i] = data[index++]; } + + if(status == Status.Ok) { + LOAD(SCENES.Game); + } + } + } break; + + default: + return; + } + + if(SCENE.message !== undefined) { SCENE.message(code, data) }; + } +} + +function MESSAGE_COMPOSE(data) { + if(SOCKET !== null) { + let length = 0; + for(let i = 0; i < data.length; ++i) { + length += data[i].length; + } + + let raw = new Uint8Array(length); + length = 0; + for(let i = 0; i < data.length; ++i) { + raw.set(data[i], length); + length += data[i].length; + } + + SOCKET.send(raw); + } +} + +function MESSAGE_SESSION_LIST(page, game_state, is_player, is_live) { + let flags = 0; + flags |= game_state; + flags |= +is_player << 2; + flags |= +is_live << 3; + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.SessionList), + PACK.u16(flags), + PACK.u16(page), + ]); +} + +function MESSAGE_SESSION_START() { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.SessionCreate), + ]); +} + +function MESSAGE_SESSION_JOIN(token, player) { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.SessionJoin), + token, + PACK.u8(player), + ]); +} diff --git a/www/js/ui.js b/www/js/ui.js new file mode 100644 index 0000000..9905bde --- /dev/null +++ b/www/js/ui.js @@ -0,0 +1,205 @@ +const UI = { + text(value) { + return document.createTextNode(value); + }, + + button(text, callback) { + let button = document.createElement("button"); + button.innerText = text; + if(callback !== null) { button.addEventListener("click", callback); } + return button; + }, + + textbox(id, placeholder) { + let input = document.createElement("input"); + input.setAttribute("type", "text"); + if(id !== null) { input.setAttribute("id", id); } + input.setAttribute("placeholder", placeholder); + return input; + }, + + password(id) { + let input = document.createElement("input"); + input.setAttribute("type", "password"); + if(id !== null) { input.setAttribute("id", id); } + return input; + }, + + label(name, id) { + let label = document.createElement("label"); + label.setAttribute("for", id); + label.innerText = name; + return label; + }, + + span(children, attr_class) { + let span = document.createElement("span"); + if(attr_class !== undefined) { span.setAttribute("class", attr_class); } + for(child of children) { span.appendChild(child); } + return span; + }, + + div(children, attr_class) { + let div = document.createElement("div"); + if(attr_class !== undefined) { div.setAttribute("class", attr_class); } + for(child of children) { div.appendChild(child); } + return div; + }, + + table_content(header, rows) { + let tbody = document.createElement("tbody"); + + if(header !== null) { + let row = document.createElement("tr"); + for(head of header) { + let cell = document.createElement("th"); + cell.innerText = head; + row.appendChild(cell); + } + tbody.appendChild(row); + } + + for(row of rows) { + let tr = document.createElement("tr"); + for(node of row) { + let cell = document.createElement("td"); + if(Array.isArray(node)) { for(item of node) { cell.appendChild(item); } } + else { cell.appendChild(node); } + tr.appendChild(cell); + } + tbody.appendChild(tr); + } + + return tbody; + }, + + table(header, rows) { + let table = document.createElement("table"); + table.appendChild(this.table_content(header, rows)); + return table; + }, + + mainnav(left_children, right_children) { + let header = document.createElement("nav"); + let left = document.createElement("section"); + + if(CONTEXT.Auth === null) { + left.appendChild(UI.button("Register", () => { LOAD(SCENES.Register) })); + left.appendChild(UI.button("Log In", () => { LOAD(SCENES.Authenticate) })); + } + + for(child of left_children) { left.appendChild(child); } + + let right = document.createElement("section"); + for(child of right_children) { right.appendChild(child); } + + header.appendChild(left); + header.appendChild(right); + + MAIN.appendChild(header); + }, + + nav(top, bottom) { + let section = document.createElement("section"); + for(node of top) { section.appendChild(node); } + MENU.appendChild(section); + + section = document.createElement("section"); + for(node of bottom) { section.appendChild(node); } + MENU.appendChild(section); + }, + + mainmenu() { + if(SOCKET !== null) { + let top = [ ]; + let bottom = [ ]; + + top.push(UI.button("Online", () => { LOAD(SCENES.Online); })); + if(CONTEXT.Auth !== null) { + top.push(UI.button("Continue", () => { LOAD(SCENES.Continue); })); + top.push(UI.button("Join", () => { LOAD(SCENES.Join); })); + } + top.push(UI.button("Live", () => { LOAD(SCENES.Live); })); + top.push(UI.button("History", () => { LOAD(SCENES.History); })); + top.push(UI.button("Guide", () => { LOAD(SCENES.Guide); })); + top.push(UI.button("About", () => { LOAD(SCENES.About); })); + + if(CONTEXT.Auth !== null) { + bottom.push(UI.button("Logout", () => { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.Deauthenticate), + ]); + CONTEXT.Auth = null; + LOAD(SCENE); + })); + } + + UI.nav(top, bottom); + } + }, + + session_table(records) { + let rows = [ ]; + + for(let r = 0; r < records.length; ++r) { + let buttons = [ ]; + let join_callback = function() { + MESSAGE_SESSION_JOIN(this.token, true); + }; + join_callback = join_callback.bind({token: records[r].token}); + + let spectate_callback = function() { + MESSAGE_SESSION_JOIN(this.token, false); + }; + spectate_callback = spectate_callback.bind({token: records[r].token}); + + if(records[r].player) { + buttons.push(UI.button("Resume", join_callback)); + } else { + if(CONTEXT.Auth !== null && (records[r].dawn == "" || records[r].dusk == "")) { + buttons.push(UI.button("Join", join_callback)); + } + buttons.push(UI.button("Spectate", spectate_callback)); + } + + let dawn = UI.text(records[r].dawn); + if(records[r].dawn == "") { dawn = UI.span([UI.text("Vacant")], "text-system"); } + + let dusk = UI.text(records[r].dusk); + if(records[r].dusk == "") { dusk = UI.span([UI.text("Vacant")], "text-system"); } + + rows.push([ + dawn, + dusk, + UI.text(records[r].turn), + UI.text(records[r].viewers), + buttons, + ]); + } + + let tbody = UI.table_content( + [ "Dawn", "Dusk", "Turn", "Spectators", "" ], + rows, + ); + + return tbody; + }, + + clear(dom) { + while(dom.lastChild !== null) { dom.removeChild(document.body.lastChild); } + }, + + rebuild() { + this.clear(document.body); + + MENU = document.createElement("nav"); + let title = document.createElement("header"); + title.innerText = "Omen"; + MENU.appendChild(title); + + MAIN = document.createElement("main"); + + document.body.appendChild(MENU); + document.body.appendChild(MAIN); + }, +}; diff --git a/www/js/util.js b/www/js/util.js new file mode 100644 index 0000000..7e560bc --- /dev/null +++ b/www/js/util.js @@ -0,0 +1,141 @@ +const PACK = { + u8(value) { + return new Uint8Array([ value & 0xFF ]); + }, + u16(value) { + return new Uint8Array([ (value >> 8) & 0xFF, value & 0xFF ]); + }, + u32(value) { + return new Uint8Array([ + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF + ]); + }, +}; + +const UNPACK = { + u8(data, index) { + let result = 0; + if(index + 1 <= data.length) { + result = data[index]; + index += 1; + } + return { data: result, index: index }; + }, + u16(data, index) { + let result = 0; + if(index + 2 <= data.length) { + result = (data[index] << 8) + data[index + 1]; + index += 2; + } + return { data: result, index: index }; + }, + u32(data, index) { + let result = 0; + if(index + 4 <= data.length) { + result = (data[index] << 24) + + (data[index + 1] << 16) + + (data[index + 1] << 8) + + data[index + 1]; + index += 4; + } + return { data: result, index: index }; + }, + string(data, index) { + let result = UNPACK.u16(data, index); + index = result.index; + let length = result.data; + + let dec = new TextDecoder(); + + let result_str = ""; + if(index + length <= data.length) { + let bytes = new Uint8Array(length); + for(let i = 0; i < length; ++i) { + bytes[i] = data[index + i]; + } + index += length; + + result_str = dec.decode(bytes); + } + + return { data: result_str, index: index }; + }, + move(bytes) { + function piece_by_id(id) { + switch(id) { + case 0: return ""; + case 1: return "M"; + case 2: return "N"; + case 3: return "L"; + case 4: return "T"; + case 5: return "C"; + case 6: return "D"; + case 7: return "K"; + } + } + + /* + ** From [6] + ** To [6] + ** Piece [3] + ** Take [3] + ** + */ + + let from = (bytes[0] & 0xFC) >> 2; + let to = ((bytes[0] & 0x03) << 4) | ((bytes[1] & 0xC0) >> 6); + let piece = piece_by_id((bytes[1] & 0x38) >> 3); + let take = piece_by_id(bytes[1] & 0x07); + let source = (bytes[2] & 0x80) >> 7; + switch((bytes[2] & 0x60) >> 5) { + case 0: state = ""; + case 1: state = "◇"; break; + case 2: state = "◈"; break; + case 3: state = "◆"; break; + } + + let str = ""; + if(state.length > 0) { + if(source == 1) { + str = "" + piece + " " + state + " " + to; + } else { + str = "" + from + " " + piece + " " + state + " " + to + " " + take; + } + if(take != "") { str += " " + take; } + } + return str; + } +}; + +const BITWISE = { + lsb(x) { + return x & -x; + }, + + ffs(x) { + return 31 - Math.clz32(x & -x); + }, + + count(mask) { + // source: https://graphics.stanford.edu/~seander/bithacks.html + mask = mask|0; + mask = mask - ((mask >> 1) & 0x55555555); + mask = (mask & 0x33333333) + ((mask >> 2) & 0x33333333); + return ((mask + (mask >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; + } +}; + +const MATH = { + sign(a) + { + return 1 - ((a < 0) << 1); + }, + + mod(a, b) + { + return ((a % b) + b) % b + }, +};