Add auth operations; update web interface.

This commit is contained in:
yukirij 2024-08-10 09:25:23 -07:00
parent 7bae892c2e
commit d5c0d74016
35 changed files with 2082 additions and 747 deletions

View File

@ -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(),
}
}

View File

@ -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;

View File

@ -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 }

View File

@ -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,
}
}

View File

@ -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(())
}
}

View File

@ -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,
}

View File

@ -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 }
}
}

View File

@ -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

View File

@ -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;

View File

@ -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)]

View File

@ -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 {

View 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()
}
}

View 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()
}
}

View 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()
}
}

View 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
}
}

View File

@ -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
View 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;
}
}
}

View File

@ -2,3 +2,4 @@
pub mod color;
pub mod string;
pub mod pack;
mod chain; pub use chain::Chain;

View File

@ -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
View File

@ -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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
span.text-system{color:#909090;}

40
www/js/const.js Normal file
View 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
View 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
View File

71
www/js/interface.js Normal file
View 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
View File

@ -0,0 +1,4 @@
document.addEventListener("DOMContentLoaded", () => {
SCENE = SCENES.Offline;
LOAD(SCENES.Init);
});

View File

@ -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
View 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
View 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
View 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
},
};