Add auth operations; update web interface.
This commit is contained in:
parent
7bae892c2e
commit
d5c0d74016
@ -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(),
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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<Authentication>,
|
||||
pub sessions:Trie<Session>,
|
||||
|
||||
pub session_time:Chain<SessionToken>,
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
@ -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<u32>,
|
||||
p_dusk:Option<u32>,
|
||||
specators:Vec<u32>,
|
||||
pub struct Viewer {
|
||||
pub connection:Option<u32>,
|
||||
pub user:Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Session {
|
||||
pub key:SessionToken,
|
||||
pub secret:SessionSecret,
|
||||
|
||||
pub game:Game,
|
||||
|
||||
pub p_dawn:Viewer,
|
||||
pub p_dusk:Viewer,
|
||||
pub viewers:Vec<Viewer>,
|
||||
|
||||
pub chain_id:usize,
|
||||
}
|
||||
|
@ -21,6 +21,16 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
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<protocol::QRPacket>)
|
||||
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<protocol::QRPacket>)
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ pub async fn handle_ws(mut ws:WebSocketStream<TokioIo<Upgraded>>, 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<TokioIo<Upgraded>>, 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
|
||||
|
@ -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;
|
||||
|
@ -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)]
|
||||
|
@ -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 {
|
||||
|
61
server/src/protocol/packet/resume.rs
Normal file
61
server/src/protocol/packet/resume.rs
Normal file
@ -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<u8>, index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
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<u8>
|
||||
{
|
||||
[
|
||||
pack_u16(self.status),
|
||||
].concat()
|
||||
}
|
||||
}
|
55
server/src/protocol/packet/session_create.rs
Normal file
55
server/src/protocol/packet/session_create.rs
Normal file
@ -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<u8>, _index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
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<u8>
|
||||
{
|
||||
[
|
||||
pack_u16(self.status),
|
||||
pack_u8(self.mode),
|
||||
self.token.to_vec(),
|
||||
].concat()
|
||||
}
|
||||
}
|
67
server/src/protocol/packet/session_join.rs
Normal file
67
server/src/protocol/packet/session_join.rs
Normal file
@ -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<u8>, index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
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<u8>
|
||||
{
|
||||
[
|
||||
pack_u16(self.status),
|
||||
pack_u8(self.mode),
|
||||
self.token.to_vec(),
|
||||
].concat()
|
||||
}
|
||||
}
|
121
server/src/protocol/packet/session_list.rs
Normal file
121
server/src/protocol/packet/session_list.rs
Normal file
@ -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<u8>, index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
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<PacketSessionListResponseRecord>,
|
||||
}
|
||||
impl PacketSessionListResponse {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
records:Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Packet for PacketSessionListResponse {
|
||||
type Data = Self;
|
||||
|
||||
fn encode(&self) -> Vec<u8>
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
48
server/src/system/cache/mod.rs
vendored
48
server/src/system/cache/mod.rs
vendored
@ -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::<u8>::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") {
|
||||
|
114
server/src/util/chain.rs
Normal file
114
server/src/util/chain.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use pool::Pool;
|
||||
|
||||
struct Node<T> {
|
||||
pub data:T,
|
||||
pub prev:Option<usize>,
|
||||
pub next:Option<usize>,
|
||||
}
|
||||
|
||||
pub struct Chain<T> {
|
||||
nodes:Pool<Node<T>>,
|
||||
begin:Option<usize>,
|
||||
end:Option<usize>,
|
||||
}
|
||||
impl<T> Chain<T> {
|
||||
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<usize>
|
||||
{
|
||||
self.begin
|
||||
}
|
||||
|
||||
pub fn next(&self, id:usize) -> Option<usize>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,3 +2,4 @@
|
||||
pub mod color;
|
||||
pub mod string;
|
||||
pub mod pack;
|
||||
mod chain; pub use chain::Chain;
|
||||
|
@ -1,3 +1,18 @@
|
||||
pub fn pack_u8(value:u8) -> Vec<u8>
|
||||
{
|
||||
vec![value]
|
||||
}
|
||||
|
||||
pub fn unpack_u8(data:&Vec<u8>, 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<u8>
|
||||
{
|
||||
vec![(value >> 8) as u8, (value & 0xFF) as u8]
|
||||
|
344
www/.css
344
www/.css
@ -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;}
|
83
www/css/form.css
Normal file
83
www/css/form.css
Normal file
@ -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;
|
||||
}
|
8
www/css/game.css
Normal file
8
www/css/game.css
Normal file
@ -0,0 +1,8 @@
|
||||
main>canvas#game {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
background-color: #101010;
|
||||
}
|
162
www/css/main.css
Normal file
162
www/css/main.css
Normal file
@ -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;
|
||||
}
|
54
www/css/ui.css
Normal file
54
www/css/ui.css
Normal file
@ -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;
|
||||
}
|
1
www/css/util.css
Normal file
1
www/css/util.css
Normal file
@ -0,0 +1 @@
|
||||
span.text-system{color:#909090;}
|
40
www/js/const.js
Normal file
40
www/js/const.js
Normal file
@ -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,
|
||||
};
|
157
www/js/game.js
Normal file
157
www/js/game.js
Normal file
@ -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
|
||||
},
|
||||
};
|
0
www/js/game_asset.js
Normal file
0
www/js/game_asset.js
Normal file
71
www/js/interface.js
Normal file
71
www/js/interface.js
Normal file
@ -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;
|
||||
},
|
||||
};
|
4
www/js/main.js
Normal file
4
www/js/main.js
Normal file
@ -0,0 +1,4 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
SCENE = SCENES.Offline;
|
||||
LOAD(SCENES.Init);
|
||||
});
|
@ -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);
|
||||
});
|
255
www/js/system.js
Normal file
255
www/js/system.js
Normal file
@ -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),
|
||||
]);
|
||||
}
|
205
www/js/ui.js
Normal file
205
www/js/ui.js
Normal file
@ -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);
|
||||
},
|
||||
};
|
141
www/js/util.js
Normal file
141
www/js/util.js
Normal file
@ -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
|
||||
},
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user