Add registration and authentication.
This commit is contained in:
parent
e47558d6d4
commit
7bae892c2e
@ -1,9 +1,20 @@
|
||||
pub struct Game {
|
||||
use crate::board::Board;
|
||||
|
||||
pub enum GameState {
|
||||
Active,
|
||||
Complete,
|
||||
}
|
||||
|
||||
pub struct Game {
|
||||
pub state:GameState,
|
||||
pub board:Board,
|
||||
}
|
||||
impl Game {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self { }
|
||||
Self {
|
||||
state:GameState::Active,
|
||||
board:Board::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,5 +4,4 @@ pub mod util;
|
||||
pub mod piece;
|
||||
pub mod board;
|
||||
pub mod history;
|
||||
pub mod protocol;
|
||||
mod game; pub use game::Game;
|
||||
|
@ -1,18 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub const STATUS_OK :u16 = 0x0000;
|
||||
pub const STATUS_BAD :u16 = 0x0001;
|
||||
pub const STATUS_NOT_IMPL :u16 = 0x0002;
|
||||
|
||||
pub const CODE_AUTH :u16 = 0x0000;
|
||||
pub const CODE_REGISTER :u16 = 0x0001;
|
||||
pub const CODE_RESUME :u16 = 0x0002;
|
||||
pub const CODE_EXIT :u16 = 0x0003;
|
||||
pub const CODE_LIST_GAME :u16 = 0x0010;
|
||||
pub const CODE_LIST_OPEN :u16 = 0x0011;
|
||||
pub const CODE_LIST_LIVE :u16 = 0x0012;
|
||||
pub const CODE_JOIN :u16 = 0x0020;
|
||||
pub const CODE_SPECTATE :u16 = 0x0021;
|
||||
pub const CODE_LEAVE :u16 = 0x0022;
|
||||
pub const CODE_RETIRE :u16 = 0x0023;
|
||||
pub const CODE_PLAY :u16 = 0x0030;
|
@ -1,2 +0,0 @@
|
||||
pub mod code;
|
||||
pub mod packet;
|
@ -1,10 +0,0 @@
|
||||
mod authenticate; pub use authenticate::*;
|
||||
|
||||
mod prelude {
|
||||
pub trait Packet {
|
||||
type Data;
|
||||
|
||||
fn encode(&self) -> Vec<u8>;
|
||||
fn decode(data:&Vec<u8>, index:&mut usize) -> Result<Self::Data, ()>;
|
||||
}
|
||||
} pub use prelude::*;
|
@ -1,2 +1 @@
|
||||
mod binary; pub use binary::*;
|
||||
mod pack; pub use pack::*;
|
||||
|
@ -5,7 +5,6 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
tokio-stream = "0.1.15"
|
||||
tokio-tungstenite = "0.23.1"
|
||||
tokio-rustls = "0.26.0"
|
||||
tokio-util = { version = "0.7.11", features = ["compat"] }
|
||||
@ -17,9 +16,13 @@ opaque-ke = "2.0.0"
|
||||
hyper = { version = "1.4.1", features = ["full"] }
|
||||
hyper-util = { version = "0.1.7", features = ["tokio"] }
|
||||
http-body-util = "0.1.2"
|
||||
futures = "0.3.30"
|
||||
rust-argon2 = "2.1.0"
|
||||
ring = "0.17.8"
|
||||
|
||||
game = { path = "../game" }
|
||||
|
||||
bus = { git = "https://git.tsukiyo.org/Utility/bus" }
|
||||
trie = { git = "https://git.tsukiyo.org/Utility/trie" }
|
||||
pool = { git = "https://git.tsukiyo.org/Utility/pool" }
|
||||
sparse = { git = "https://git.tsukiyo.org/Utility/sparse" }
|
||||
trie = { git = "https://git.tsukiyo.org/Utility/trie" }
|
||||
pool = { git = "https://git.tsukiyo.org/Utility/pool" }
|
||||
|
36
server/docs/protocol/requests.md
Normal file
36
server/docs/protocol/requests.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Table
|
||||
|
||||
## User Authentication
|
||||
|
||||
- Register—create a new account.
|
||||
- Handle
|
||||
- Secret
|
||||
- Invite_Code
|
||||
- Authenticate—log into an account and create a new client session.
|
||||
- Handle
|
||||
- Secret
|
||||
- Deauthenticate—revoke the current client session.
|
||||
|
||||
## Sessions
|
||||
|
||||
- List_Sessions—
|
||||
- Page
|
||||
- Ongoing or Complete
|
||||
- Joinable
|
||||
- Live
|
||||
- Update_Sessions—
|
||||
|
||||
## Game
|
||||
|
||||
- Join—
|
||||
- Session_Id
|
||||
- Spectate—
|
||||
- Session_Id
|
||||
- Leave—
|
||||
- Retire—
|
||||
- Play—
|
||||
- Move_Source [ Board, Pool ]
|
||||
- Move_From
|
||||
- Piece_From
|
||||
- Move_To
|
||||
- Piece_To
|
18
server/src/app/authentication.rs
Normal file
18
server/src/app/authentication.rs
Normal file
@ -0,0 +1,18 @@
|
||||
pub type AuthToken = [u8; 8];
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Authentication {
|
||||
pub key:AuthToken,
|
||||
pub secret:[u8; 16],
|
||||
pub user:u32,
|
||||
}
|
||||
impl Authentication {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
key:[0; 8],
|
||||
secret:[0; 16],
|
||||
user:0,
|
||||
}
|
||||
}
|
||||
}
|
15
server/src/app/connection.rs
Normal file
15
server/src/app/connection.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use crate::app::authentication::AuthToken;
|
||||
|
||||
pub struct Connection {
|
||||
pub bus:u32,
|
||||
pub auth:Option<AuthToken>,
|
||||
}
|
||||
impl Connection {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
bus:0,
|
||||
auth:None,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
use game::Game;
|
||||
|
||||
pub struct Contest {
|
||||
game:Game,
|
||||
|
||||
p_dawn:u64,
|
||||
p_dusk:u64,
|
||||
specators:Vec<u64>,
|
||||
}
|
@ -1,28 +1,42 @@
|
||||
use game::util::pack_u32;
|
||||
#![allow(dead_code)]
|
||||
|
||||
use sparse::Sparse;
|
||||
use pool::Pool;
|
||||
use trie::Trie;
|
||||
use crate::util::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 contest; use contest::Contest;
|
||||
pub mod context;
|
||||
|
||||
pub struct App {
|
||||
users:Pool<User>,
|
||||
handles:Trie<u32>,
|
||||
salts:Vec<[u8; 16]>,
|
||||
sessions:Pool<Session>,
|
||||
contests:Pool<Contest>,
|
||||
pub connections:Pool<Connection>,
|
||||
|
||||
pub users:Pool<User>,
|
||||
pub user_next:u32,
|
||||
pub user_id:Sparse<usize>,
|
||||
pub user_handle:Trie<u32>,
|
||||
pub salts:Vec<[u8; 16]>,
|
||||
|
||||
pub auths:Trie<Authentication>,
|
||||
pub sessions:Trie<Session>,
|
||||
}
|
||||
impl App {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
connections:Pool::new(),
|
||||
|
||||
users:Pool::new(),
|
||||
handles:Trie::new(),
|
||||
user_next:0,
|
||||
user_id:Sparse::new(),
|
||||
user_handle:Trie::new(),
|
||||
salts:Vec::new(),
|
||||
sessions:Pool::new(),
|
||||
contests:Pool::new(),
|
||||
|
||||
auths:Trie::new(),
|
||||
sessions:Trie::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,12 @@
|
||||
use crate::app::context::Context;
|
||||
use game::Game;
|
||||
|
||||
pub struct Session {
|
||||
key:u64,
|
||||
secret:[u8; 16],
|
||||
user:Option<usize>,
|
||||
context:Context,
|
||||
}
|
||||
impl Session {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
key:0,
|
||||
secret:[0; 16],
|
||||
user:None,
|
||||
context:Context::None,
|
||||
}
|
||||
}
|
||||
key:[u8; 8],
|
||||
secret:[u8; 8],
|
||||
|
||||
game:Game,
|
||||
|
||||
p_dawn:Option<u32>,
|
||||
p_dusk:Option<u32>,
|
||||
specators:Vec<u32>,
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub struct User {
|
||||
handle:String,
|
||||
secret:String,
|
||||
na_key:usize,
|
||||
pub id:u32,
|
||||
pub handle:String,
|
||||
pub secret:Vec<u8>,
|
||||
pub na_key:usize,
|
||||
}
|
||||
|
@ -6,44 +6,12 @@ mod util;
|
||||
mod app;
|
||||
mod system;
|
||||
mod protocol;
|
||||
mod manager;
|
||||
|
||||
use app::App;
|
||||
use hyper::{body::Bytes, upgrade::Upgraded};
|
||||
use system::{cache::WebCache, net::Stream};
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use hyper::body::Bytes;
|
||||
use hyper_util::rt::TokioIo;
|
||||
//use tokio_rustls::server::TlsStream;
|
||||
//use tokio::net::TcpStream;
|
||||
|
||||
async fn thread_datasystem(mut _app:App, bus:Bus<protocol::QRPacket>)
|
||||
{
|
||||
use protocol::QRPacket;
|
||||
use game::protocol::packet::*;
|
||||
|
||||
while match bus.receive_wait() {
|
||||
Some(packet) => {
|
||||
match packet.data {
|
||||
QRPacket::QAuth(_request) => {
|
||||
let response = PacketAuthResponse::new();
|
||||
|
||||
// get user id from handle
|
||||
|
||||
// get user salt
|
||||
|
||||
// hash request secret
|
||||
|
||||
// compare hash to user secret
|
||||
|
||||
// return response
|
||||
bus.send(packet.from, QRPacket::RAuth(response)).is_ok()
|
||||
}
|
||||
_ => { true }
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
} { }
|
||||
}
|
||||
use system::{cache::WebCache, net::Stream};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HttpServiceArgs {
|
||||
@ -51,66 +19,12 @@ struct HttpServiceArgs {
|
||||
cache:WebCache,
|
||||
}
|
||||
|
||||
async fn handle_ws(mut ws_stream:WebSocketStream<TokioIo<Upgraded>>, args:HttpServiceArgs)
|
||||
{
|
||||
use tokio_tungstenite::tungstenite::protocol::Message;
|
||||
use game::util::unpack_u16;
|
||||
use protocol::QRPacket;
|
||||
use game::protocol::{
|
||||
code::*,
|
||||
packet::*,
|
||||
};
|
||||
|
||||
println!("ws ready");
|
||||
|
||||
let bus_ds = args.bus.mailbox(1).unwrap_or(1);
|
||||
|
||||
while match ws_stream.try_next().await {
|
||||
Ok(msg) => match msg {
|
||||
Some(Message::Binary(data)) => {
|
||||
|
||||
let mut index :usize = 0;
|
||||
let code: u16 = unpack_u16(&data, &mut index);
|
||||
|
||||
match code {
|
||||
CODE_AUTH => {
|
||||
match PacketAuth::decode(&data, &mut index) {
|
||||
Ok(packet) => {
|
||||
if args.bus.send(bus_ds, QRPacket::QAuth(packet)).is_ok() {
|
||||
match args.bus.receive_wait() {
|
||||
Some(resp) => match resp.data {
|
||||
QRPacket::RAuth(_resp) => {
|
||||
|
||||
}
|
||||
_ => { }
|
||||
}
|
||||
None => { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => { }
|
||||
}
|
||||
}
|
||||
_ => { }
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
Some(_) => true,
|
||||
None => false
|
||||
}
|
||||
Err(_) => false,
|
||||
} { }
|
||||
|
||||
ws_stream.close(None).await.ok();
|
||||
}
|
||||
|
||||
async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:HttpServiceArgs) -> Result<hyper::Response<http_body_util::Full<Bytes>>, std::convert::Infallible>
|
||||
// Serve cached files and upgrade websocket connections.
|
||||
//
|
||||
{
|
||||
use hyper::{Response, body::Bytes, header::{CONTENT_TYPE, CACHE_CONTROL}}; //SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_KEY, UPGRADE
|
||||
use hyper::{Response, body::Bytes, header::{CONTENT_TYPE, CACHE_CONTROL}};
|
||||
use http_body_util::Full;
|
||||
//use tokio_tungstenite::accept_async;
|
||||
//use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
|
||||
|
||||
println!("Serving: {}", request.uri().path());
|
||||
|
||||
@ -131,37 +45,12 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:Ht
|
||||
|
||||
_ => {
|
||||
if hyper_tungstenite::is_upgrade_request(&request) {
|
||||
//if request.headers().get(UPGRADE).map(|h| h == "websocket").unwrap_or(false) {
|
||||
//let response = create_response_with_body(&request, || Full::new(Bytes::new())).unwrap();
|
||||
|
||||
//let key = request.headers().get(SEC_WEBSOCKET_KEY)
|
||||
// .and_then(|v| v.to_str().ok())
|
||||
// .map(|k| derive_accept_key(k.as_bytes()))
|
||||
// .unwrap_or_default();
|
||||
|
||||
if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) {
|
||||
tokio::task::spawn(async move {
|
||||
match websocket.await {
|
||||
Ok(websocket) => handle_ws(websocket, args).await,
|
||||
Err(_) => { }
|
||||
}
|
||||
//match hyper::upgrade::on(request).await {
|
||||
/*Ok(upgraded) => {
|
||||
match upgraded.downcast::<TokioIo<TlsStream<TcpStream>>>() {
|
||||
Ok(parts) => {
|
||||
match accept_async(parts.io.into_inner()).await {
|
||||
Ok(ws_stream) => {
|
||||
println!("here");
|
||||
handle_ws(ws_stream, args).await
|
||||
}
|
||||
Err(e) => { println!("ws not accepted: {}", e.to_string()); }
|
||||
}
|
||||
}
|
||||
Err(_) => { println!("transfer error"); }
|
||||
}
|
||||
}
|
||||
Er(e) => { println!("upgrade error: {}", e.to_string()); }*/
|
||||
//}
|
||||
Ok(websocket) => manager::handle_ws(websocket, args).await,
|
||||
Err(_) => Err(()),
|
||||
}.ok()
|
||||
});
|
||||
|
||||
Ok(response)
|
||||
@ -171,14 +60,6 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:Ht
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
//Ok(Response::builder()
|
||||
// .status(101)
|
||||
// .header(CONNECTION, "Upgrade")
|
||||
// .header(UPGRADE, "websocket")
|
||||
// .header(SEC_WEBSOCKET_ACCEPT, key)
|
||||
// .body(Full::new(Bytes::new()))
|
||||
// .unwrap())
|
||||
} else {
|
||||
Ok(Response::builder()
|
||||
.header(CONTENT_TYPE, "text/html")
|
||||
@ -190,6 +71,8 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:Ht
|
||||
}
|
||||
|
||||
async fn handle_http(stream:system::net::tls::TlsStream, addr:SocketAddr, args:HttpServiceArgs) -> Result<(),()>
|
||||
// Hand off socket connection to Hyper server.
|
||||
//
|
||||
{
|
||||
use hyper::server::conn::http1;
|
||||
use hyper::service::service_fn;
|
||||
@ -229,7 +112,7 @@ async fn main()
|
||||
match b_main.connect() {
|
||||
Ok(bus) => {
|
||||
tokio::spawn(async move {
|
||||
thread_datasystem(app, bus).await;
|
||||
manager::thread_system(app, bus).await;
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
|
199
server/src/manager/data.rs
Normal file
199
server/src/manager/data.rs
Normal file
@ -0,0 +1,199 @@
|
||||
use bus::Bus;
|
||||
use crate::{
|
||||
app::{
|
||||
App,
|
||||
authentication::Authentication,
|
||||
user::User,
|
||||
connection::Connection,
|
||||
},
|
||||
protocol,
|
||||
};
|
||||
|
||||
pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
{
|
||||
use protocol::*;
|
||||
use protocol::QRPacketData::*;
|
||||
use ring::rand::{SecureRandom, SystemRandom};
|
||||
|
||||
let rng = SystemRandom::new();
|
||||
let argon_config = argon2::Config::default();
|
||||
|
||||
while match bus.receive_wait() {
|
||||
Some(packet) => {
|
||||
let qr = &packet.data;
|
||||
match &qr.data {
|
||||
QConn(request) => {
|
||||
let id = app.connections.add(Connection {
|
||||
bus: request.bus_id,
|
||||
auth: None,
|
||||
});
|
||||
|
||||
println!("Connect: {}", id);
|
||||
|
||||
bus.send(packet.from, QRPacket::new(id as u32, RConn)).is_ok()
|
||||
}
|
||||
|
||||
QDisconn => {
|
||||
app.connections.remove(qr.id as usize).ok();
|
||||
|
||||
println!("Disconnect: {}", qr.id);
|
||||
true
|
||||
}
|
||||
|
||||
QRegister(request) => {
|
||||
let mut response = PacketRegisterResponse::new();
|
||||
response.status = STATUS_ERROR;
|
||||
|
||||
println!("Request: Register");
|
||||
|
||||
let mut is_valid = true;
|
||||
if request.code != "abc".as_bytes() { response.status = STATUS_BAD_CODE; is_valid = false; }
|
||||
if is_valid && request.handle.len() == 0 { response.status = STATUS_BAD_HANDLE; is_valid = false; }
|
||||
if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; }
|
||||
|
||||
if is_valid {
|
||||
match app.user_handle.get(request.handle.as_bytes()) {
|
||||
None => {
|
||||
let mut salt = [0u8; 16];
|
||||
match rng.fill(&mut salt) {
|
||||
Ok(_) => {
|
||||
let salt_id = app.salts.len();
|
||||
app.salts.push(salt);
|
||||
|
||||
if let Ok(hash) = argon2::hash_raw(&request.secret, &salt, &argon_config) {
|
||||
let user_id = app.user_next;
|
||||
app.user_next += 1;
|
||||
|
||||
// Create user entry
|
||||
let user_pos = app.users.add(User {
|
||||
id:user_id,
|
||||
handle:request.handle.clone(),
|
||||
secret:hash,
|
||||
na_key:salt_id,
|
||||
});
|
||||
|
||||
// Register user pool id and handle
|
||||
app.user_id.set(user_id as isize, user_pos);
|
||||
app.user_handle.set(request.handle.as_bytes(), user_id);
|
||||
|
||||
println!("Registered user '{}' @ {} with id {}", request.handle, user_pos, user_id);
|
||||
|
||||
// Generate authentication token and secret
|
||||
response.status = STATUS_OK;
|
||||
rng.fill(&mut response.secret).ok();
|
||||
loop {
|
||||
rng.fill(&mut response.token).ok();
|
||||
|
||||
if app.auths.get(&response.token).is_none() {
|
||||
app.auths.set(&response.token, Authentication {
|
||||
key:response.token,
|
||||
secret:response.secret,
|
||||
user:user_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Attach authentication to connection
|
||||
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
|
||||
conn.auth = Some(response.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => { println!("error: failed to generate salt.") }
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
response.status = STATUS_BAD_HANDLE;
|
||||
println!("notice: attempt to register existing handle: '{}'", request.handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bus.send(packet.from, QRPacket::new(qr.id, RRegister(response))).is_ok()
|
||||
}
|
||||
|
||||
QAuth(request) => {
|
||||
let mut response = PacketAuthResponse::new();
|
||||
response.status = STATUS_ERROR;
|
||||
|
||||
println!("Request: Auth");
|
||||
|
||||
let mut is_valid = true;
|
||||
if is_valid && request.handle.len() == 0 { response.status = STATUS_BAD_HANDLE; is_valid = false; }
|
||||
if is_valid && request.secret.len() == 0 { response.status = STATUS_BAD_SECRET; is_valid = false; }
|
||||
|
||||
if is_valid {
|
||||
|
||||
// Get user data from handle
|
||||
match app.user_handle.get(request.handle.as_bytes()) {
|
||||
Some(uid) => {
|
||||
if let Some(tuid) = app.user_id.get(*uid as isize) {
|
||||
if let Some(user) = app.users.get(*tuid) {
|
||||
// Get user salt
|
||||
if let Some(salt) = app.salts.get(user.na_key) {
|
||||
|
||||
// Verify salted secret against user data
|
||||
if argon2::verify_raw(&request.secret.as_bytes(), salt, &user.secret, &argon_config).unwrap_or(false) {
|
||||
println!("Authenticated user '{}' id {}", request.handle, *uid);
|
||||
|
||||
// Generate authentication token and secret
|
||||
response.status = STATUS_OK;
|
||||
rng.fill(&mut response.secret).ok();
|
||||
loop {
|
||||
rng.fill(&mut response.token).ok();
|
||||
|
||||
if app.auths.get(&response.token).is_none() {
|
||||
app.auths.set(&response.token, Authentication {
|
||||
key:response.token,
|
||||
secret:response.secret,
|
||||
user:*uid,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Attach authentication to connection
|
||||
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
|
||||
conn.auth = Some(response.token);
|
||||
}
|
||||
} else {
|
||||
println!("notice: password verification failed.");
|
||||
}
|
||||
} else {
|
||||
println!("error: user salt id '{}' not found.", user.na_key);
|
||||
}
|
||||
} else {
|
||||
println!("error: user with id '{}' not found.", uid);
|
||||
}
|
||||
} else {
|
||||
println!("error: user with id '{}' not found.", uid);
|
||||
}
|
||||
}
|
||||
None => { }
|
||||
}
|
||||
}
|
||||
|
||||
bus.send(packet.from, QRPacket::new(qr.id, RAuth(response))).is_ok()
|
||||
}
|
||||
|
||||
QDeauth => {
|
||||
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
|
||||
match conn.auth {
|
||||
Some(auth) => {
|
||||
println!("Deauthenticated connection: {}", qr.id);
|
||||
app.auths.unset(&auth);
|
||||
}
|
||||
None => { }
|
||||
}
|
||||
conn.auth = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
_ => { true }
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
} { }
|
||||
}
|
2
server/src/manager/mod.rs
Normal file
2
server/src/manager/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod data; pub use data::*;
|
||||
mod ws; pub use ws::*;
|
127
server/src/manager/ws.rs
Normal file
127
server/src/manager/ws.rs
Normal file
@ -0,0 +1,127 @@
|
||||
use hyper::upgrade::Upgraded;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
|
||||
use crate::{
|
||||
protocol,
|
||||
util::pack::{pack_u16, unpack_u16},
|
||||
HttpServiceArgs,
|
||||
};
|
||||
|
||||
fn encode_response(code:u16, data:Vec<u8>) -> Vec<u8>
|
||||
{
|
||||
[
|
||||
pack_u16(code),
|
||||
data,
|
||||
].concat()
|
||||
}
|
||||
|
||||
pub async fn handle_ws(mut ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServiceArgs) -> Result<(),()>
|
||||
// Handle websocket connection.
|
||||
//
|
||||
{
|
||||
use tokio_tungstenite::tungstenite::protocol::Message;
|
||||
use protocol::{QRPacket, QRPacketData::*, code::*, packet::*};
|
||||
|
||||
let conn_id :u32;
|
||||
|
||||
let bus_ds = args.bus.mailbox(1).unwrap_or(1);
|
||||
|
||||
// Perform connection handshake with data system.
|
||||
// - Provide system with connection/bus pairing.
|
||||
// - Acquire connection id.
|
||||
//
|
||||
args.bus.send(bus_ds, QRPacket::new(0, QConn(LocalPacketConnect {
|
||||
bus_id:args.bus.id(),
|
||||
})))?;
|
||||
match args.bus.receive_wait() {
|
||||
Some(resp) => {
|
||||
let qr = &resp.data;
|
||||
match qr.data {
|
||||
RConn => { conn_id = qr.id; }
|
||||
_ => { return Err(()); }
|
||||
}
|
||||
}
|
||||
None => { return Err(()); }
|
||||
}
|
||||
|
||||
// Decode client requests from websocket,
|
||||
// pass requests to data system,
|
||||
// and return responses to client.
|
||||
while match ws.next().await {
|
||||
Some(msg) => match msg {
|
||||
Ok(msg) => match msg {
|
||||
Message::Binary(data) => {
|
||||
let mut index :usize = 0;
|
||||
let code: u16 = unpack_u16(&data, &mut index);
|
||||
match code {
|
||||
|
||||
CODE_REGISTER => match PacketRegister::decode(&data, &mut index) {
|
||||
Ok(packet) => {
|
||||
if args.bus.send(bus_ds, QRPacket::new(conn_id, QRegister(packet))).is_ok() {
|
||||
while match args.bus.receive_wait() {
|
||||
Some(resp) => {
|
||||
let qr = &resp.data;
|
||||
match &qr.data {
|
||||
RRegister(resp) => {
|
||||
ws.send(Message::Binary(
|
||||
encode_response(code, resp.encode())
|
||||
)).await.ok();
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
None => true,
|
||||
} { }
|
||||
}
|
||||
}
|
||||
Err(_) => { }
|
||||
}
|
||||
|
||||
CODE_AUTH => match PacketAuth::decode(&data, &mut index) {
|
||||
Ok(packet) => {
|
||||
if args.bus.send(bus_ds, QRPacket::new(conn_id, QAuth(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();
|
||||
}
|
||||
|
||||
_ => { }
|
||||
}
|
||||
true
|
||||
}
|
||||
_ => {
|
||||
println!("notice: received unexpected websocket data.");
|
||||
true
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
None => false,
|
||||
} { }
|
||||
|
||||
args.bus.send(bus_ds, QRPacket::new(conn_id, QDisconn)).ok();
|
||||
ws.close(None).await.ok();
|
||||
Ok(())
|
||||
}
|
24
server/src/protocol/code.rs
Normal file
24
server/src/protocol/code.rs
Normal file
@ -0,0 +1,24 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub const STATUS_OK :u16 = 0x0000;
|
||||
pub const STATUS_ERROR :u16 = 0x0001;
|
||||
|
||||
pub const STATUS_BAD_HANDLE :u16 = 0x0001;
|
||||
pub const STATUS_BAD_SECRET :u16 = 0x0002;
|
||||
pub const STATUS_BAD_CODE :u16 = 0x0003;
|
||||
|
||||
pub const STATUS_NOT_IMPL :u16 = 0x00FF;
|
||||
|
||||
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_GAME_PLAY :u16 = 0x0030;
|
@ -1,7 +1,37 @@
|
||||
use game::protocol::packet::*;
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod code; pub use code::*;
|
||||
pub mod packet; pub use packet::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum QRPacket {
|
||||
pub enum QRPacketData {
|
||||
ROk,
|
||||
|
||||
QConn(LocalPacketConnect),
|
||||
RConn,
|
||||
|
||||
QDisconn,
|
||||
|
||||
QRegister(PacketRegister),
|
||||
RRegister(PacketRegisterResponse),
|
||||
|
||||
QAuth(PacketAuth),
|
||||
RAuth(PacketAuthResponse),
|
||||
|
||||
QDeauth,
|
||||
|
||||
QAuthResume,
|
||||
RAuthResume,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct QRPacket {
|
||||
pub id:u32,
|
||||
pub data:QRPacketData,
|
||||
}
|
||||
impl QRPacket {
|
||||
pub fn new(id:u32, data:QRPacketData) -> Self
|
||||
{
|
||||
Self { id, data }
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::util::{pack_u16, unpack_u16};
|
||||
use crate::util::pack::{pack_u16, unpack_u16};
|
||||
|
||||
use super::Packet;
|
||||
|
||||
@ -19,16 +19,6 @@ impl PacketAuth {
|
||||
impl Packet for PacketAuth {
|
||||
type Data = Self;
|
||||
|
||||
fn encode(&self) -> Vec<u8>
|
||||
{
|
||||
[
|
||||
pack_u16(self.handle.len() as u16),
|
||||
self.handle.as_bytes().to_vec(),
|
||||
pack_u16(self.secret.len() as u16),
|
||||
self.secret.as_bytes().to_vec(),
|
||||
].concat()
|
||||
}
|
||||
|
||||
fn decode(data:&Vec<u8>, index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
let mut result = Self::new();
|
||||
@ -65,7 +55,7 @@ impl Packet for PacketAuth {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PacketAuthResponse {
|
||||
pub status:bool,
|
||||
pub status:u16,
|
||||
pub token:[u8; 8],
|
||||
pub secret:[u8; 16],
|
||||
}
|
||||
@ -73,7 +63,7 @@ impl PacketAuthResponse {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
status:false,
|
||||
status:0,
|
||||
token:[0; 8],
|
||||
secret:[0; 16],
|
||||
}
|
||||
@ -84,11 +74,10 @@ impl Packet for PacketAuthResponse {
|
||||
|
||||
fn encode(&self) -> Vec<u8>
|
||||
{
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn decode(_data:&Vec<u8>, _index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
Err(())
|
||||
[
|
||||
pack_u16(self.status),
|
||||
self.token.to_vec(),
|
||||
self.secret.to_vec(),
|
||||
].concat()
|
||||
}
|
||||
}
|
4
server/src/protocol/packet/connect.rs
Normal file
4
server/src/protocol/packet/connect.rs
Normal file
@ -0,0 +1,4 @@
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct LocalPacketConnect {
|
||||
pub bus_id:u32,
|
||||
}
|
12
server/src/protocol/packet/mod.rs
Normal file
12
server/src/protocol/packet/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
mod connect; pub use connect::*;
|
||||
mod register; pub use register::*;
|
||||
mod authenticate; pub use authenticate::*;
|
||||
|
||||
mod prelude {
|
||||
pub trait Packet {
|
||||
type Data;
|
||||
|
||||
fn encode(&self) -> Vec<u8> { Vec::new() }
|
||||
fn decode(_data:&Vec<u8>, _index:&mut usize) -> Result<Self::Data, ()> { Err(()) }
|
||||
}
|
||||
} pub use prelude::*;
|
83
server/src/protocol/packet/register.rs
Normal file
83
server/src/protocol/packet/register.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use crate::util::pack::{pack_u16, unpack_u16};
|
||||
|
||||
use super::Packet;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PacketRegister {
|
||||
pub handle:String,
|
||||
pub secret:Vec<u8>,
|
||||
pub code:Vec<u8>,
|
||||
}
|
||||
impl PacketRegister {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
handle:String::new(),
|
||||
secret:Vec::new(),
|
||||
code:Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Packet for PacketRegister {
|
||||
type Data = Self;
|
||||
|
||||
fn decode(data:&Vec<u8>, index:&mut usize) -> Result<Self::Data, ()>
|
||||
{
|
||||
let mut result = Self::new();
|
||||
|
||||
let mut length = unpack_u16(data, index) as usize;
|
||||
if length > 0 && data.len() >= *index + length {
|
||||
match String::from_utf8(data[*index..*index+length].to_vec()) {
|
||||
Ok(text) => {
|
||||
result.handle = text;
|
||||
*index += length;
|
||||
}
|
||||
Err(_) => { return Err(()) }
|
||||
}
|
||||
} else { return Err(()); }
|
||||
|
||||
length = unpack_u16(data, index) as usize;
|
||||
if length > 0 && data.len() >= *index + length {
|
||||
result.secret = data[*index..*index+length].to_vec();
|
||||
*index += length;
|
||||
} else { return Err(()); }
|
||||
|
||||
length = unpack_u16(data, index) as usize;
|
||||
if length > 0 && data.len() >= *index + length {
|
||||
result.code = data[*index..*index+length].to_vec();
|
||||
*index += length;
|
||||
} else { return Err(()); }
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PacketRegisterResponse {
|
||||
pub status:u16,
|
||||
pub token:[u8; 8],
|
||||
pub secret:[u8; 16],
|
||||
}
|
||||
impl PacketRegisterResponse {
|
||||
pub fn new() -> Self
|
||||
{
|
||||
Self {
|
||||
status:0,
|
||||
token:[0; 8],
|
||||
secret:[0; 16],
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Packet for PacketRegisterResponse {
|
||||
type Data = Self;
|
||||
|
||||
fn encode(&self) -> Vec<u8>
|
||||
{
|
||||
[
|
||||
pack_u16(self.status),
|
||||
self.token.to_vec(),
|
||||
self.secret.to_vec(),
|
||||
].concat()
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ impl CertificateStore {
|
||||
};
|
||||
|
||||
if certs.is_ok() && key.is_ok() {
|
||||
self.certs.set(domain, Arc::new(Certificate {
|
||||
self.certs.set(domain.as_bytes(), Arc::new(Certificate {
|
||||
certs:certs.unwrap(),
|
||||
key:key.unwrap(),
|
||||
}));
|
||||
@ -70,7 +70,7 @@ impl CertificateStore {
|
||||
|
||||
pub fn remove(&mut self, domain:&str) -> Result<(),()>
|
||||
{
|
||||
if self.certs.unset(domain) {
|
||||
if self.certs.unset(domain.as_bytes()) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(())
|
||||
@ -79,8 +79,8 @@ impl CertificateStore {
|
||||
|
||||
pub fn get(&self, domain:&str) -> Result<Arc<Certificate>, ()>
|
||||
{
|
||||
match self.certs.get(domain) {
|
||||
Some(certs) => Ok(certs),
|
||||
match self.certs.get(domain.as_bytes()) {
|
||||
Some(certs) => Ok(certs.clone()),
|
||||
None => Err(())
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
#![allow(dead_code)]
|
||||
pub mod color;
|
||||
pub mod string;
|
||||
pub mod pack;
|
||||
|
120
www/.css
120
www/.css
@ -47,8 +47,9 @@ body>nav>button{
|
||||
cursor:pointer;
|
||||
|
||||
background-color:#282828;
|
||||
border:0;
|
||||
color:#c0c0c0;
|
||||
border:0;
|
||||
outline:0;
|
||||
} body>nav>button:hover{
|
||||
background-color:#383838;
|
||||
color:#e0e0e0;
|
||||
@ -71,8 +72,11 @@ body>nav>header{
|
||||
}
|
||||
|
||||
main{
|
||||
display:block;
|
||||
display:flex;
|
||||
position:relative;
|
||||
flex-flow:column nowrap;
|
||||
align-items:flex-start;
|
||||
justify-content:flex-start;
|
||||
width:100%;
|
||||
height:100%;
|
||||
|
||||
@ -124,8 +128,9 @@ main>nav>section>button{
|
||||
cursor:pointer;
|
||||
|
||||
background-color:#282828;
|
||||
border:0;
|
||||
color:#c0c0c0;
|
||||
border:0;
|
||||
outline:0;
|
||||
}
|
||||
main>nav>section>button:hover{
|
||||
background-color:#383838;
|
||||
@ -146,14 +151,14 @@ main>nav>section>div{
|
||||
color:#e0e0e0;
|
||||
}
|
||||
|
||||
main table{
|
||||
main.list table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
}
|
||||
main table tr{
|
||||
main.list table tr{
|
||||
height:2.5rem;
|
||||
}
|
||||
main table th{
|
||||
main.list table th{
|
||||
padding:0 1rem 0 1rem;
|
||||
|
||||
text-align:center;
|
||||
@ -164,7 +169,7 @@ main table th{
|
||||
color:#f0f0f0;
|
||||
border-bottom:1px solid #404040;
|
||||
}
|
||||
main table td{
|
||||
main.list table td{
|
||||
height:100%;
|
||||
padding:0 1rem 0 1rem;
|
||||
|
||||
@ -173,7 +178,7 @@ main table td{
|
||||
background-color:#303030;
|
||||
color:#f0f0f0;
|
||||
}
|
||||
main table td:last-child{
|
||||
main.list table td:last-child{
|
||||
display:flex;
|
||||
flex-flow:row nowrap;
|
||||
align-items:flex-start;
|
||||
@ -181,12 +186,7 @@ main table td:last-child{
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
main table td>img{
|
||||
height:2rem;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
main table td:last-child>button{
|
||||
main.list table td:last-child>button{
|
||||
display:block;
|
||||
position:relative;
|
||||
width:auto;
|
||||
@ -198,14 +198,15 @@ main table td:last-child>button{
|
||||
cursor:pointer;
|
||||
|
||||
background-color:#303030;
|
||||
border:0;
|
||||
color:#e0e0e0;
|
||||
border:0;
|
||||
outline:0;
|
||||
}
|
||||
main table td:last-child>button:hover{
|
||||
main.list table td:last-child>button:hover{
|
||||
background-color:#343434;
|
||||
}
|
||||
|
||||
main>canvas{
|
||||
main.game>canvas{
|
||||
display:block;
|
||||
position:relative;
|
||||
width:100%;
|
||||
@ -242,6 +243,7 @@ main.game>div.sidemenu>button{
|
||||
background-color:#202020;
|
||||
color:#F0F0F0;
|
||||
border:0;
|
||||
outline:0;
|
||||
|
||||
font-family:sans-serif;
|
||||
font-size:1rem;
|
||||
@ -255,4 +257,88 @@ 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;}
|
||||
|
778
www/.js
778
www/.js
@ -4,28 +4,94 @@ let SCENE = null;
|
||||
|
||||
let CONNECTED = false;
|
||||
let SOCKET = null;
|
||||
let USER = null;
|
||||
let CONTEXT = null;
|
||||
let CONTEXT = {
|
||||
Scene: null,
|
||||
Auth: null,
|
||||
Data: null,
|
||||
};
|
||||
|
||||
let UI = {
|
||||
text:function(value) {
|
||||
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:function(text, callback) {
|
||||
let b = document.createElement("button");
|
||||
b.innerText = text;
|
||||
if(callback !== null) { b.addEventListener("click", callback); }
|
||||
return b;
|
||||
button:(text, callback) => {
|
||||
let button = document.createElement("button");
|
||||
button.innerText = text;
|
||||
if(callback !== null) { button.addEventListener("click", callback); }
|
||||
return button;
|
||||
},
|
||||
|
||||
div:function(children) {
|
||||
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:function(header, rows) {
|
||||
table:(header, rows) => {
|
||||
let table = document.createElement("table");
|
||||
let tbody = document.createElement("tbody");
|
||||
|
||||
@ -54,9 +120,15 @@ let UI = {
|
||||
return table;
|
||||
},
|
||||
|
||||
mainnav:function(left_children, right_children) {
|
||||
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");
|
||||
@ -64,27 +136,423 @@ let UI = {
|
||||
|
||||
header.appendChild(left);
|
||||
header.appendChild(right);
|
||||
return header;
|
||||
|
||||
MAIN.appendChild(header);
|
||||
},
|
||||
|
||||
mainmenu:function() {
|
||||
mainmenu:() => {
|
||||
if(SOCKET !== null) {
|
||||
MENU.appendChild(UI.button("Online", function() { LOAD(SCENE.Online); }));
|
||||
MENU.appendChild(UI.button("Continue", function() { LOAD(SCENE.Continue); }));
|
||||
MENU.appendChild(UI.button("Join", function() { LOAD(SCENE.Continue); }));
|
||||
MENU.appendChild(UI.button("Live", function() { LOAD(SCENE.Continue); }));
|
||||
MENU.appendChild(UI.button("History", function() { LOAD(SCENE.Continue); }));
|
||||
MENU.appendChild(UI.button("Guide", function() { LOAD(SCENE.Continue); }));
|
||||
MENU.appendChild(UI.button("About", function() { LOAD(SCENE.Continue); }));
|
||||
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_OFFLINE();
|
||||
CONTEXT.Scene = SCENES.Online;
|
||||
RECONNECT();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
Offline:{
|
||||
load:() => {
|
||||
MENU.appendChild(UI.button("Reconnect", () => { RECONNECT(); }))
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
Register:{
|
||||
load:() => {
|
||||
if(CONTEXT.Auth !== null) return false;
|
||||
UI.mainmenu();
|
||||
UI.mainnav([], []);
|
||||
|
||||
let container = document.createElement("section");
|
||||
let form = document.createElement("div");
|
||||
form.appendChild(UI.table(null, [
|
||||
[ UI.label("Handle", "handle"), UI.textbox("handle", "") ],
|
||||
[ UI.label("Secret", "secret"), UI.password("secret") ],
|
||||
[ UI.label("Code", "code"), UI.password("code") ],
|
||||
]));
|
||||
|
||||
let button = UI.button("Register", (event) => {
|
||||
let handle = document.getElementById("handle");
|
||||
let secret = document.getElementById("secret");
|
||||
let code = document.getElementById("code");
|
||||
|
||||
handle.removeAttribute("class");
|
||||
secret.removeAttribute("class");
|
||||
code.removeAttribute("class");
|
||||
event.target.removeAttribute("class");
|
||||
|
||||
if(handle.value.length > 0 && secret.value.length > 0 && code.value.length > 0) {
|
||||
event.target.setAttribute("disabled", "");
|
||||
|
||||
let enc = new TextEncoder();
|
||||
let enc_handle = enc.encode(handle.value);
|
||||
let enc_secret = enc.encode(secret.value);
|
||||
let enc_code = enc.encode(code.value);
|
||||
|
||||
MESSAGE_COMPOSE([
|
||||
PACK.u16(OpCode.Register),
|
||||
PACK.u16(enc_handle.length),
|
||||
enc_handle,
|
||||
PACK.u16(enc_secret.length),
|
||||
enc_secret,
|
||||
PACK.u16(enc_code.length),
|
||||
enc_code,
|
||||
]);
|
||||
} else {
|
||||
if(handle.value.length == 0) { handle.setAttribute("class", "error"); }
|
||||
if(secret.value.length == 0) { secret.setAttribute("class", "error"); }
|
||||
if(code.value.length == 0) { code.setAttribute("class", "error"); }
|
||||
}
|
||||
});
|
||||
button.setAttribute("id", "submit");
|
||||
form.appendChild(button);
|
||||
|
||||
container.appendChild(form);
|
||||
MAIN.appendChild(container);
|
||||
MAIN.setAttribute("class", "form");
|
||||
|
||||
return true;
|
||||
},
|
||||
message:(code, data) => {
|
||||
if(code == OpCode.Register && data !== null) {
|
||||
let submit = document.getElementById("submit");
|
||||
switch(data.status) {
|
||||
case Status.Ok: {
|
||||
CONTEXT.Auth = data;
|
||||
LOAD(SCENES.Online);
|
||||
} break;
|
||||
default: {
|
||||
submit.removeAttribute("disabled");
|
||||
switch(data.status) {
|
||||
case Status.BadHandle: {
|
||||
document.getElementById("handle").setAttribute("class", "error");
|
||||
submit.setAttribute("class", "error");
|
||||
} break;
|
||||
case Status.BadSecret: {
|
||||
document.getElementById("secret").setAttribute("class", "error");
|
||||
submit.setAttribute("class", "error");
|
||||
} break;
|
||||
case Status.BadCode: {
|
||||
document.getElementById("code").setAttribute("class", "error");
|
||||
submit.setAttribute("class", "error");
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Authenticate:{
|
||||
load:() => {
|
||||
if(CONTEXT.Auth !== null) return false;
|
||||
UI.mainmenu();
|
||||
UI.mainnav([], []);
|
||||
|
||||
let container = document.createElement("section");
|
||||
let form = document.createElement("div");
|
||||
form.appendChild(UI.table(null, [
|
||||
[ UI.label("Handle", "handle"), UI.textbox("handle", "") ],
|
||||
[ UI.label("Secret", "secret"), UI.password("secret") ],
|
||||
]));
|
||||
|
||||
let button = UI.button("Register", (event) => {
|
||||
let handle = document.getElementById("handle");
|
||||
let secret = document.getElementById("secret");
|
||||
|
||||
handle.removeAttribute("class");
|
||||
secret.removeAttribute("class");
|
||||
event.target.removeAttribute("class");
|
||||
|
||||
if(handle.value.length > 0 && secret.value.length > 0) {
|
||||
event.target.setAttribute("disabled", "");
|
||||
|
||||
let enc = new TextEncoder();
|
||||
let enc_handle = enc.encode(handle.value);
|
||||
let enc_secret = enc.encode(secret.value);
|
||||
|
||||
MESSAGE_COMPOSE([
|
||||
PACK.u16(OpCode.Authenticate),
|
||||
PACK.u16(enc_handle.length),
|
||||
enc_handle,
|
||||
PACK.u16(enc_secret.length),
|
||||
enc_secret,
|
||||
]);
|
||||
} else {
|
||||
if(handle.value.length == 0) { handle.setAttribute("class", "error"); }
|
||||
if(secret.value.length == 0) { secret.setAttribute("class", "error"); }
|
||||
}
|
||||
});
|
||||
button.setAttribute("id", "submit");
|
||||
form.appendChild(button);
|
||||
|
||||
container.appendChild(form);
|
||||
MAIN.appendChild(container);
|
||||
MAIN.setAttribute("class", "form");
|
||||
|
||||
return true;
|
||||
},
|
||||
message:(code, data) => {
|
||||
if(code == OpCode.Authenticate && data !== null) {
|
||||
let submit = document.getElementById("submit");
|
||||
switch(data.status) {
|
||||
case Status.Ok: {
|
||||
CONTEXT.Auth = data;
|
||||
LOAD(SCENES.Online);
|
||||
} break;
|
||||
case Status.Error: {
|
||||
submit.removeAttribute("disabled");
|
||||
|
||||
document.getElementById("handle").setAttribute("class", "error");
|
||||
document.getElementById("secret").setAttribute("class", "error");
|
||||
submit.setAttribute("class", "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Online:{
|
||||
load:() => {
|
||||
UI.mainmenu();
|
||||
|
||||
let left_buttons = [ ];
|
||||
if(CONTEXT.Auth !== null) {
|
||||
left_buttons.push(UI.button("Start", null));
|
||||
}
|
||||
|
||||
UI.mainnav(
|
||||
left_buttons,
|
||||
[
|
||||
UI.div([UI.text("0 - 0 of 0")]),
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]
|
||||
);
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("id", "content");
|
||||
MAIN.appendChild(table);
|
||||
|
||||
MAIN.setAttribute("class", "list");
|
||||
|
||||
SCENE.refresh();
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
let request = new Uint8Array();
|
||||
|
||||
//SERVER.send()
|
||||
|
||||
SCENE.message(0, 0, null);
|
||||
},
|
||||
message:(code, data) => {
|
||||
let table = document.getElementById("content");
|
||||
MAIN.removeChild(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,
|
||||
));
|
||||
}
|
||||
},
|
||||
|
||||
Continue:{
|
||||
load:() => {
|
||||
if(CONTEXT.Auth === null) return false;
|
||||
UI.mainmenu();
|
||||
|
||||
let left_buttons = [ ];
|
||||
if(CONTEXT.Auth !== null) {
|
||||
left_buttons.push(UI.button("Start", null));
|
||||
}
|
||||
|
||||
UI.mainnav(
|
||||
left_buttons,
|
||||
[
|
||||
UI.div([UI.text("0 - 0 of 0")]),
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]
|
||||
);
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("id", "content");
|
||||
MAIN.appendChild(table);
|
||||
|
||||
MAIN.setAttribute("class", "list");
|
||||
|
||||
SCENE.refresh();
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Join:{
|
||||
load:() => {
|
||||
if(CONTEXT.Auth === null) return false;
|
||||
UI.mainmenu();
|
||||
|
||||
let left_buttons = [ ];
|
||||
if(CONTEXT.Auth !== null) {
|
||||
left_buttons.push(UI.button("Start", null));
|
||||
}
|
||||
|
||||
UI.mainnav(
|
||||
left_buttons,
|
||||
[
|
||||
UI.div([UI.text("0 - 0 of 0")]),
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]
|
||||
);
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("id", "content");
|
||||
MAIN.appendChild(table);
|
||||
|
||||
MAIN.setAttribute("class", "list");
|
||||
|
||||
SCENE.refresh();
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Live:{
|
||||
load:() => {
|
||||
UI.mainmenu();
|
||||
|
||||
let left_buttons = [ ];
|
||||
if(CONTEXT.Auth !== null) {
|
||||
left_buttons.push(UI.button("Start", null));
|
||||
}
|
||||
|
||||
UI.mainnav(
|
||||
left_buttons,
|
||||
[
|
||||
UI.div([UI.text("0 - 0 of 0")]),
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]
|
||||
);
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("id", "content");
|
||||
MAIN.appendChild(table);
|
||||
|
||||
MAIN.setAttribute("class", "list");
|
||||
|
||||
SCENE.refresh();
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
History:{
|
||||
load:() => {
|
||||
UI.mainmenu();
|
||||
|
||||
UI.mainnav(
|
||||
[ ],
|
||||
[
|
||||
UI.div([UI.text("0 - 0 of 0")]),
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]
|
||||
);
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("id", "content");
|
||||
MAIN.appendChild(table);
|
||||
|
||||
MAIN.setAttribute("class", "list");
|
||||
|
||||
SCENE.refresh();
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Guide:{
|
||||
load:() => {
|
||||
UI.mainmenu();
|
||||
UI.mainnav([], []);
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
About:{
|
||||
load:() => {
|
||||
UI.mainmenu();
|
||||
UI.mainnav([], []);
|
||||
return true;
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Game:{
|
||||
load:() => {
|
||||
MENU.appendChild(UI.button("Back", () => { LOAD(SCENES.Online) }));
|
||||
MENU.appendChild(UI.button("Retire", () => { }));
|
||||
return true;
|
||||
},
|
||||
unload:() => {
|
||||
|
||||
},
|
||||
refresh:() => {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function RECONNECT() {
|
||||
if(SOCKET === null) {
|
||||
console.log("Websocket connecting..");
|
||||
SOCKET = new WebSocket("wss://omen.kirisame.com:38612");
|
||||
SOCKET.binaryType = 'blob';
|
||||
SOCKET.binaryType = "arraybuffer";
|
||||
SOCKET.addEventListener("error", (event) => {
|
||||
SOCKET = null;
|
||||
LOAD(SCENES.Offline)
|
||||
@ -93,9 +561,7 @@ function RECONNECT() {
|
||||
if(SOCKET.readyState === WebSocket.OPEN) {
|
||||
console.log("Websocket connected.");
|
||||
|
||||
SOCKET.addEventListener("message", (event) => {
|
||||
MESSAGE(event.data);
|
||||
});
|
||||
SOCKET.addEventListener("message", MESSAGE);
|
||||
|
||||
SOCKET.addEventListener("close", (event) => {
|
||||
console.log("Websocket closed.");
|
||||
@ -104,171 +570,15 @@ function RECONNECT() {
|
||||
});
|
||||
|
||||
RESUME();
|
||||
SOCKET.send(new Uint8Array([ 0 ]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function RESUME() {
|
||||
LOAD(SCENES.Online);
|
||||
LOAD(CONTEXT.Scene);
|
||||
}
|
||||
|
||||
const SCENES = {
|
||||
Init:{
|
||||
load:function() {
|
||||
LOAD(SCENES.Offline);
|
||||
RECONNECT();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
Offline:{
|
||||
load:function() {
|
||||
MENU.appendChild(UI.button("Reconnect", function() { RECONNECT(); }))
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
Online:{
|
||||
load:function() {
|
||||
UI.mainmenu();
|
||||
MAIN.appendChild(UI.mainnav([
|
||||
UI.button("Start", null),
|
||||
], [
|
||||
UI.div([UI.text("0 - 0 of 0")]),
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]));
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("id", "content");
|
||||
MAIN.appendChild(table);
|
||||
|
||||
this.refresh();
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
let request = new Uint8Array();
|
||||
|
||||
//SERVER.send()
|
||||
|
||||
let cb = function() {
|
||||
let table = document.getElementById("content");
|
||||
MAIN.removeChild(table);
|
||||
|
||||
let data = [
|
||||
[ 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", "" ],
|
||||
data,
|
||||
));
|
||||
};
|
||||
cb();
|
||||
},
|
||||
},
|
||||
|
||||
Continue:{
|
||||
load:function() {
|
||||
if(USER === null) return false;
|
||||
UI.mainmenu();
|
||||
MAIN.appendChild(UI.mainnav([
|
||||
UI.button("Start", null),
|
||||
], [
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]));
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Join:{
|
||||
load:function() {
|
||||
if(USER === null) return false;
|
||||
UI.mainmenu();
|
||||
MAIN.appendChild(UI.mainnav([
|
||||
UI.button("Start", null),
|
||||
], [
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]));
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Live:{
|
||||
load:function() {
|
||||
UI.mainmenu();
|
||||
MAIN.appendChild(UI.mainnav([
|
||||
UI.button("Start", null),
|
||||
], [
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]));
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
History:{
|
||||
load:function() {
|
||||
UI.mainmenu();
|
||||
MAIN.appendChild(UI.mainnav([ ], [
|
||||
UI.button("◀", null),
|
||||
UI.button("▶", null),
|
||||
]));
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Guide:{
|
||||
load:function() {
|
||||
UI.mainmenu();
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
About:{
|
||||
load:function() {
|
||||
UI.mainmenu();
|
||||
return true;
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
Game:{
|
||||
load:function() {
|
||||
MENU.appendChild(UI.button("Back", function() { LOAD(SCENES.Online) }));
|
||||
MENU.appendChild(UI.button("Retire", function() { }));
|
||||
return true;
|
||||
},
|
||||
unload:function() {
|
||||
|
||||
},
|
||||
refresh:function() {
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function REBUILD() {
|
||||
MENU = document.createElement("nav");
|
||||
let title = document.createElement("header");
|
||||
@ -281,8 +591,99 @@ function REBUILD() {
|
||||
document.body.appendChild(MAIN);
|
||||
}
|
||||
|
||||
function MESSAGE() {
|
||||
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) {
|
||||
@ -290,10 +691,19 @@ function LOAD(scene) {
|
||||
while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); }
|
||||
REBUILD();
|
||||
SCENE = scene;
|
||||
CONTEXT.Scene = SCENE;
|
||||
if(!SCENE.load()) { LOAD(SCENES.Online); }
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
function LOAD_OFFLINE() {
|
||||
if(SCENE.unload !== undefined) { SCENE.unload(); }
|
||||
while(document.body.lastChild !== null) { document.body.removeChild(document.body.lastChild); }
|
||||
REBUILD();
|
||||
SCENE = SCENES.Offline;
|
||||
if(!SCENE.load()) { LOAD(SCENES.Online); }
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
SCENE = SCENES.Offline;
|
||||
LOAD(SCENES.Init);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user