basic buzzer working with websocket comms
This commit is contained in:
		
							parent
							
								
									0fd244f3d4
								
							
						
					
					
						commit
						3bb58b5719
					
				| @ -23,6 +23,9 @@ mist = ">= 4.0.7 and < 5.0.0" | ||||
| argv = ">= 1.0.2 and < 2.0.0" | ||||
| gleam_erlang = ">= 0.34.0 and < 1.0.0" | ||||
| gleam_http = ">= 4.0.0 and < 5.0.0" | ||||
| gleam_otp = ">= 0.16.1 and < 1.0.0" | ||||
| gleam_crypto = ">= 1.5.0 and < 2.0.0" | ||||
| gleam_json = ">= 2.3.0 and < 3.0.0" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| gleeunit = ">= 1.0.0 and < 2.0.0" | ||||
|  | ||||
| @ -34,8 +34,11 @@ packages = [ | ||||
| 
 | ||||
| [requirements] | ||||
| argv = { version = ">= 1.0.2 and < 2.0.0" } | ||||
| gleam_crypto = { version = ">= 1.5.0 and < 2.0.0" } | ||||
| gleam_erlang = { version = ">= 0.34.0 and < 1.0.0" } | ||||
| gleam_http = { version = ">= 4.0.0 and < 5.0.0" } | ||||
| gleam_json = { version = ">= 2.3.0 and < 3.0.0" } | ||||
| gleam_otp = { version = ">= 0.16.1 and < 1.0.0" } | ||||
| gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } | ||||
| gleeunit = { version = ">= 1.0.0 and < 2.0.0" } | ||||
| glint = { version = ">= 1.2.1 and < 2.0.0" } | ||||
|  | ||||
| @ -1,67 +1,47 @@ | ||||
| import app/web | ||||
| import gleam/bit_array | ||||
| import gleam/bytes_tree | ||||
| import gleam/crypto | ||||
| import gleam/dynamic/decode | ||||
| import gleam/erlang/process | ||||
| import gleam/float | ||||
| import gleam/http/request.{type Request} | ||||
| import gleam/http/response.{type Response} | ||||
| import gleam/option.{None} | ||||
| import gleam/io | ||||
| import gleam/json | ||||
| import gleam/list | ||||
| import gleam/option.{None, Some} | ||||
| import gleam/otp/actor | ||||
| import gleam/result | ||||
| import gleam/string | ||||
| import mist.{type Connection, type ResponseData} | ||||
| 
 | ||||
| pub fn handle_request( | ||||
|   request: Request(Connection), | ||||
|   req: Request(Connection), | ||||
|   ctx: web.Context, | ||||
| ) -> Response(ResponseData) { | ||||
|   case request.path_segments(request) { | ||||
|     [] -> serve_frontend() | ||||
|     ["static", "frontend.wasm"] -> serve_wasm(ctx) | ||||
|   io.println("Got request: " <> req.path) | ||||
|   case request.path_segments(req) { | ||||
|     [] -> serve_wasm(ctx, "index.html", "text/html") | ||||
|     ["static", "frontend.js"] -> | ||||
|       serve_wasm(ctx, "frontend.js", "text/javascript") | ||||
|     ["static", "frontend_bg.wasm"] -> | ||||
|       serve_wasm(ctx, "frontend_bg.wasm", "application/wasm") | ||||
|     ["ws"] -> serve_websocket(ctx, req) | ||||
|     _ -> response.set_body(response.new(404), mist.Bytes(bytes_tree.new())) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const game_body = " | ||||
| <html lang=\"en\"> | ||||
| 
 | ||||
| <head> | ||||
|     <meta charset=\"utf-8\"> | ||||
|     <title>TITLE</title> | ||||
|     <style> | ||||
|         html, | ||||
|         body, | ||||
|         canvas { | ||||
|             margin: 0px; | ||||
|             padding: 0px; | ||||
|             width: 100%; | ||||
|             height: 100%; | ||||
|             overflow: hidden; | ||||
|             position: absolute; | ||||
|             background: black; | ||||
|             z-index: 0; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
|     <canvas id=\"glcanvas\" tabindex='1'></canvas> | ||||
|     <!-- Minified and statically hosted version of https://github.com/not-fl3/macroquad/blob/master/js/mq_js_bundle.js --> | ||||
|     <script src=\"https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js\"></script> | ||||
|     <script>load(\"static/frontend.wasm\");</script> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
| " | ||||
| 
 | ||||
| fn serve_frontend() -> Response(ResponseData) { | ||||
|   response.new(200) | ||||
|   |> response.set_body(mist.Bytes(bytes_tree.from_string(game_body))) | ||||
|   |> response.set_header("content-type", "text/html") | ||||
| } | ||||
| 
 | ||||
| fn serve_wasm(ctx: web.Context) -> Response(ResponseData) { | ||||
|   let wasm_path = string.join([ctx.config.static_path, "frontend.wasm"], "/") | ||||
| fn serve_wasm( | ||||
|   ctx: web.Context, | ||||
|   file: String, | ||||
|   mimetype: String, | ||||
| ) -> Response(ResponseData) { | ||||
|   let wasm_path = string.join([ctx.config.static_path, file], "/") | ||||
|   mist.send_file(wasm_path, offset: 0, limit: None) | ||||
|   |> result.map(fn(file) { | ||||
|     response.new(200) | ||||
|     |> response.prepend_header("content-type", "application/wasm") | ||||
|     |> response.prepend_header("content-type", mimetype) | ||||
|     |> response.set_body(file) | ||||
|   }) | ||||
|   |> result.lazy_unwrap(fn() { | ||||
| @ -69,3 +49,107 @@ fn serve_wasm(ctx: web.Context) -> Response(ResponseData) { | ||||
|     |> response.set_body(mist.Bytes(bytes_tree.new())) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| fn serve_websocket( | ||||
|   ctx: web.Context, | ||||
|   req: Request(Connection), | ||||
| ) -> Response(ResponseData) { | ||||
|   mist.websocket( | ||||
|     request: req, | ||||
|     on_init: fn(_conn) { #(ctx, Some(ctx.selector)) }, | ||||
|     on_close: fn(_state) { io.println("goodbye!") }, | ||||
|     handler: handle_ws_message, | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| fn decode_error_to_string(decode_err: decode.DecodeError) -> String { | ||||
|   "Path: " | ||||
|   <> string.join(decode_err.path, ".") | ||||
|   <> " Expected: " | ||||
|   <> decode_err.expected | ||||
|   <> " Got: " | ||||
|   <> decode_err.found | ||||
| } | ||||
| 
 | ||||
| fn decode_errors_to_string(decode_errs: List(decode.DecodeError)) -> String { | ||||
|   string.join(list.map(decode_errs, decode_error_to_string), "\n") | ||||
| } | ||||
| 
 | ||||
| fn handle_ws_message(state, conn, message) { | ||||
|   io.println("Got WS message") | ||||
|   case message { | ||||
|     mist.Text("ping") -> { | ||||
|       io.println("Got Ping") | ||||
|       let assert Ok(_) = mist.send_text_frame(conn, "pong") | ||||
|       actor.continue(state) | ||||
|     } | ||||
|     mist.Text(msg) -> { | ||||
|       echo msg | ||||
|       case json.parse(msg, web.client_request_decoder()) { | ||||
|         Error(err) -> { | ||||
|           let err_msg = case err { | ||||
|             json.UnableToDecode(decode_err) -> | ||||
|               "Decode Error: " <> decode_errors_to_string(decode_err) | ||||
|             json.UnexpectedByte(bad_byte) -> "Unexpeted byte: " <> bad_byte | ||||
|             json.UnexpectedEndOfInput -> "Reached EoF" | ||||
|             json.UnexpectedFormat(_) -> "Unexpected format" | ||||
|             json.UnexpectedSequence(seq_msg) -> | ||||
|               "Unexpected Sequence: " <> seq_msg | ||||
|           } | ||||
|           io.println("Could not parse client message: " <> err_msg) | ||||
|         } | ||||
|         Ok(req) -> { | ||||
|           let resp = handle_client_msg(state, req) | ||||
|           echo resp | ||||
|           let resp_str = | ||||
|             web.encode_game_response(resp) | ||||
|             |> json.to_string | ||||
| 
 | ||||
|           let assert Ok(_) = mist.send_text_frame(conn, resp_str) | ||||
|           Nil | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       actor.continue(state) | ||||
|     } | ||||
|     mist.Binary(_) -> { | ||||
|       actor.continue(state) | ||||
|     } | ||||
|     mist.Custom(_) -> { | ||||
|       actor.continue(state) | ||||
|     } | ||||
|     mist.Closed | mist.Shutdown -> actor.Stop(process.Normal) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| fn handle_client_msg( | ||||
|   _ctx: web.Context, | ||||
|   req: web.ClientRequest, | ||||
| ) -> web.GameResponse { | ||||
|   case req.msg { | ||||
|     web.BuzzIn(time) -> { | ||||
|       io.println("Got buzz in @ " <> float.to_string(time)) | ||||
|       web.AckBuzzer | ||||
|     } | ||||
|     web.Register(username, gamecode) -> { | ||||
|       let secret = crypto.strong_random_bytes(16) | ||||
|       let token = | ||||
|         crypto.new_hasher(crypto.Sha512) | ||||
|         |> crypto.hash_chunk(secret) | ||||
|         |> crypto.hash_chunk(bit_array.from_string(username)) | ||||
|         |> crypto.hash_chunk(bit_array.from_string(gamecode)) | ||||
|         |> crypto.digest | ||||
| 
 | ||||
|       let token_hash = | ||||
|         crypto.new_hasher(crypto.Sha512) | ||||
|         |> crypto.hash_chunk(token) | ||||
|         |> crypto.digest | ||||
| 
 | ||||
|       io.println("New token: " <> bit_array.base64_encode(token_hash, True)) | ||||
| 
 | ||||
|       let token = bit_array.base64_encode(token, True) | ||||
| 
 | ||||
|       web.JoinResponse(token, username) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,70 @@ | ||||
| import config | ||||
| import gleam/dynamic/decode | ||||
| import gleam/erlang/process | ||||
| import gleam/json | ||||
| import session | ||||
| import storail | ||||
| 
 | ||||
| pub type Context { | ||||
|   Context(config: config.Config, sessions: storail.Collection(session.Session)) | ||||
| pub type ClientMessage { | ||||
|   BuzzIn(time: Float) | ||||
|   Register(game_code: String, username: String) | ||||
| } | ||||
| 
 | ||||
| fn client_message_decoder() -> decode.Decoder(ClientMessage) { | ||||
|   use variant <- decode.field("type", decode.string) | ||||
|   case variant { | ||||
|     "BuzzIn" -> { | ||||
|       use time <- decode.field("time", decode.float) | ||||
|       decode.success(BuzzIn(time:)) | ||||
|     } | ||||
|     "Register" -> { | ||||
|       use game_code <- decode.field("game_code", decode.string) | ||||
|       use username <- decode.field("username", decode.string) | ||||
|       decode.success(Register(game_code:, username:)) | ||||
|     } | ||||
|     _ -> decode.failure(BuzzIn(time: 0.0), "ClientMessage") | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| pub type ClientRequest { | ||||
|   ClientRequest(token: String, msg: ClientMessage) | ||||
| } | ||||
| 
 | ||||
| pub fn client_request_decoder() -> decode.Decoder(ClientRequest) { | ||||
|   use token <- decode.field("token", decode.string) | ||||
|   use msg <- decode.field("msg", client_message_decoder()) | ||||
|   decode.success(ClientRequest(token:, msg:)) | ||||
| } | ||||
| 
 | ||||
| pub type GameResponse { | ||||
|   JoinResponse(username: String, token: String) | ||||
|   AckBuzzer | ||||
|   ResetBuzzer | ||||
|   UpdatePointTotal(score: Int) | ||||
| } | ||||
| 
 | ||||
| pub fn encode_game_response(game_response: GameResponse) -> json.Json { | ||||
|   case game_response { | ||||
|     JoinResponse(..) -> | ||||
|       json.object([ | ||||
|         #("type", json.string("JoinResponse")), | ||||
|         #("username", json.string(game_response.username)), | ||||
|         #("token", json.string(game_response.token)), | ||||
|       ]) | ||||
|     AckBuzzer -> json.object([#("type", json.string("AckBuzzer"))]) | ||||
|     ResetBuzzer -> json.object([#("type", json.string("ResetBuzzer"))]) | ||||
|     UpdatePointTotal(..) -> | ||||
|       json.object([ | ||||
|         #("type", json.string("UpdatePointTotal")), | ||||
|         #("score", json.int(game_response.score)), | ||||
|       ]) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| pub type Context { | ||||
|   Context( | ||||
|     config: config.Config, | ||||
|     sessions: storail.Collection(session.Session), | ||||
|     selector: process.Selector(ClientRequest), | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -49,7 +49,11 @@ pub fn run_server() -> glint.Command(Result(Nil, Error)) { | ||||
|   let storail_cfg = storail.Config(cfg.storage_path) | ||||
| 
 | ||||
|   let ctx = | ||||
|     web.Context(config: cfg, sessions: setup_session_collection(storail_cfg)) | ||||
|     web.Context( | ||||
|       config: cfg, | ||||
|       sessions: setup_session_collection(storail_cfg), | ||||
|       selector: process.new_selector(), | ||||
|     ) | ||||
| 
 | ||||
|   let handler = router.handle_request(_, ctx) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										1
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1 +1,2 @@ | ||||
| /target | ||||
| /dist | ||||
|  | ||||
							
								
								
									
										470
									
								
								frontend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										470
									
								
								frontend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -26,6 +26,21 @@ version = "1.3.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "block-buffer" | ||||
| version = "0.10.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" | ||||
| dependencies = [ | ||||
|  "generic-array", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "bumpalo" | ||||
| version = "3.17.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "bytemuck" | ||||
| version = "1.23.0" | ||||
| @ -38,6 +53,12 @@ version = "1.5.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "bytes" | ||||
| version = "1.10.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "cfg-if" | ||||
| version = "1.0.0" | ||||
| @ -50,6 +71,15 @@ version = "1.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "cpufeatures" | ||||
| version = "0.2.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "crc32fast" | ||||
| version = "1.4.2" | ||||
| @ -59,12 +89,62 @@ dependencies = [ | ||||
|  "cfg-if", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "crypto-common" | ||||
| version = "0.1.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" | ||||
| dependencies = [ | ||||
|  "generic-array", | ||||
|  "typenum", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "data-encoding" | ||||
| version = "2.9.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "digest" | ||||
| version = "0.10.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" | ||||
| dependencies = [ | ||||
|  "block-buffer", | ||||
|  "crypto-common", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "document-features" | ||||
| version = "0.2.11" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" | ||||
| dependencies = [ | ||||
|  "litrs", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "equivalent" | ||||
| version = "1.0.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ewebsock" | ||||
| version = "0.8.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "679247b4a005c82218a5f13b713239b0b6d484ec25347a719f5b7066152a748a" | ||||
| dependencies = [ | ||||
|  "document-features", | ||||
|  "js-sys", | ||||
|  "log", | ||||
|  "tungstenite", | ||||
|  "wasm-bindgen", | ||||
|  "wasm-bindgen-futures", | ||||
|  "web-sys", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "fdeflate" | ||||
| version = "0.3.7" | ||||
| @ -84,6 +164,12 @@ dependencies = [ | ||||
|  "miniz_oxide", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "fnv" | ||||
| version = "1.0.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "foldhash" | ||||
| version = "0.1.5" | ||||
| @ -104,7 +190,32 @@ dependencies = [ | ||||
| name = "frontend" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "ewebsock", | ||||
|  "macroquad", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "web-time", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "generic-array" | ||||
| version = "0.14.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" | ||||
| dependencies = [ | ||||
|  "typenum", | ||||
|  "version_check", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "getrandom" | ||||
| version = "0.2.16" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "libc", | ||||
|  "wasi", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| @ -124,6 +235,23 @@ dependencies = [ | ||||
|  "foldhash", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "http" | ||||
| version = "1.3.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" | ||||
| dependencies = [ | ||||
|  "bytes", | ||||
|  "fnv", | ||||
|  "itoa", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "httparse" | ||||
| version = "1.10.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "image" | ||||
| version = "0.24.9" | ||||
| @ -137,12 +265,40 @@ dependencies = [ | ||||
|  "png", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "itoa" | ||||
| version = "1.0.15" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "js-sys" | ||||
| version = "0.3.77" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" | ||||
| dependencies = [ | ||||
|  "once_cell", | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "libc" | ||||
| version = "0.2.172" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "litrs" | ||||
| version = "0.4.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "log" | ||||
| version = "0.4.27" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "macroquad" | ||||
| version = "0.4.14" | ||||
| @ -172,6 +328,12 @@ dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "memchr" | ||||
| version = "2.7.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "miniquad" | ||||
| version = "0.4.8" | ||||
| @ -218,6 +380,12 @@ dependencies = [ | ||||
|  "malloc_buf", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "once_cell" | ||||
| version = "1.21.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "png" | ||||
| version = "0.17.16" | ||||
| @ -231,24 +399,306 @@ dependencies = [ | ||||
|  "miniz_oxide", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ppv-lite86" | ||||
| version = "0.2.21" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | ||||
| dependencies = [ | ||||
|  "zerocopy", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "proc-macro2" | ||||
| version = "1.0.95" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" | ||||
| dependencies = [ | ||||
|  "unicode-ident", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "quad-rand" | ||||
| version = "0.2.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5a651516ddc9168ebd67b24afd085a718be02f8858fe406591b013d101ce2f40" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "quote" | ||||
| version = "1.0.40" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rand" | ||||
| version = "0.8.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "rand_chacha", | ||||
|  "rand_core", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rand_chacha" | ||||
| version = "0.3.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" | ||||
| dependencies = [ | ||||
|  "ppv-lite86", | ||||
|  "rand_core", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rand_core" | ||||
| version = "0.6.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" | ||||
| dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "rustversion" | ||||
| version = "1.0.20" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ryu" | ||||
| version = "1.0.20" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde" | ||||
| version = "1.0.219" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" | ||||
| dependencies = [ | ||||
|  "serde_derive", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde_derive" | ||||
| version = "1.0.219" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "serde_json" | ||||
| version = "1.0.140" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" | ||||
| dependencies = [ | ||||
|  "itoa", | ||||
|  "memchr", | ||||
|  "ryu", | ||||
|  "serde", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "sha1" | ||||
| version = "0.10.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "cpufeatures", | ||||
|  "digest", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "simd-adler32" | ||||
| version = "0.3.7" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "2.0.101" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "unicode-ident", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "thiserror" | ||||
| version = "1.0.69" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" | ||||
| dependencies = [ | ||||
|  "thiserror-impl", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "thiserror-impl" | ||||
| version = "1.0.69" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ttf-parser" | ||||
| version = "0.21.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "tungstenite" | ||||
| version = "0.24.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" | ||||
| dependencies = [ | ||||
|  "byteorder", | ||||
|  "bytes", | ||||
|  "data-encoding", | ||||
|  "http", | ||||
|  "httparse", | ||||
|  "log", | ||||
|  "rand", | ||||
|  "sha1", | ||||
|  "thiserror", | ||||
|  "utf-8", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "typenum" | ||||
| version = "1.18.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "unicode-ident" | ||||
| version = "1.0.18" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "utf-8" | ||||
| version = "0.7.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "version_check" | ||||
| version = "0.9.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasi" | ||||
| version = "0.11.0+wasi-snapshot-preview1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen" | ||||
| version = "0.2.100" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "once_cell", | ||||
|  "rustversion", | ||||
|  "wasm-bindgen-macro", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-backend" | ||||
| version = "0.2.100" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" | ||||
| dependencies = [ | ||||
|  "bumpalo", | ||||
|  "log", | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "wasm-bindgen-shared", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-futures" | ||||
| version = "0.4.50" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "js-sys", | ||||
|  "once_cell", | ||||
|  "wasm-bindgen", | ||||
|  "web-sys", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-macro" | ||||
| version = "0.2.100" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" | ||||
| dependencies = [ | ||||
|  "quote", | ||||
|  "wasm-bindgen-macro-support", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-macro-support" | ||||
| version = "0.2.100" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
|  "wasm-bindgen-backend", | ||||
|  "wasm-bindgen-shared", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wasm-bindgen-shared" | ||||
| version = "0.2.100" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" | ||||
| dependencies = [ | ||||
|  "unicode-ident", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "web-sys" | ||||
| version = "0.3.77" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" | ||||
| dependencies = [ | ||||
|  "js-sys", | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "web-time" | ||||
| version = "1.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" | ||||
| dependencies = [ | ||||
|  "js-sys", | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "winapi" | ||||
| version = "0.3.9" | ||||
| @ -270,3 +720,23 @@ name = "winapi-x86_64-pc-windows-gnu" | ||||
| version = "0.4.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "zerocopy" | ||||
| version = "0.8.25" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" | ||||
| dependencies = [ | ||||
|  "zerocopy-derive", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "zerocopy-derive" | ||||
| version = "0.8.25" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| @ -4,4 +4,8 @@ version = "0.1.0" | ||||
| edition = "2024" | ||||
| 
 | ||||
| [dependencies] | ||||
| ewebsock = "0.8.0" | ||||
| macroquad = "0.4.14" | ||||
| serde = { version = "1.0.219", features = ["derive"] } | ||||
| serde_json = "1.0.140" | ||||
| web-time = "1.1.0" | ||||
|  | ||||
							
								
								
									
										132
									
								
								frontend/build_wasm.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										132
									
								
								frontend/build_wasm.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,132 @@ | ||||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| set -e | ||||
| 
 | ||||
| HELP_STRING=$( | ||||
| 	cat <<-END | ||||
| 		usage: build_wasm.sh PROJECT_NAME [--release] | ||||
| 		Build script for combining a Macroquad project with wasm-bindgen, | ||||
| 		allowing integration with the greater wasm-ecosystem. | ||||
| 		example: ./build_wasm.sh flappy-bird | ||||
| 		  This'll go through the following steps: | ||||
| 			    1. Build as target 'wasm32-unknown-unknown'. | ||||
| 			    2. Create the directory 'dist' if it doesn't already exist. | ||||
| 			    3. Run wasm-bindgen with output into the 'dist' directory. | ||||
| 		            - If the '--release' flag is provided, the build will be optimized for release. | ||||
| 			    4. Apply patches to the output js file (detailed here: https://github.com/not-fl3/macroquad/issues/212#issuecomment-835276147). | ||||
| 			    5. Generate coresponding 'index.html' file. | ||||
| 			Author: Tom Solberg <me@sbg.dev> | ||||
| 			Edit: Nik codes <nik.code.things@gmail.com> | ||||
| 			Edit: Nobbele <realnobbele@gmail.com> | ||||
| 			Edit: profan <robinhubner@gmail.com> | ||||
| 			Edit: Nik codes <nik.code.things@gmail.com> | ||||
| 			Version: 0.4 | ||||
| 	END | ||||
| ) | ||||
| 
 | ||||
| die() { | ||||
| 	echo >&2 "$HELP_STRING" | ||||
| 	echo >&2 | ||||
| 	echo >&2 "Error: $*" | ||||
| 	exit 1 | ||||
| } | ||||
| 
 | ||||
| # Parse primary commands | ||||
| while [[ $# -gt 0 ]]; do | ||||
| 	key="$1" | ||||
| 	case $key in | ||||
| 	--release) | ||||
| 		RELEASE=yes | ||||
| 		shift | ||||
| 		;; | ||||
| 
 | ||||
| 	-h | --help) | ||||
| 		echo "$HELP_STRING" | ||||
| 		exit 0 | ||||
| 		;; | ||||
| 
 | ||||
| 	*) | ||||
| 		POSITIONAL+=("$1") | ||||
| 		shift | ||||
| 		;; | ||||
| 	esac | ||||
| done | ||||
| 
 | ||||
| # Restore positionals | ||||
| set -- "${POSITIONAL[@]}" | ||||
| [ $# -ne 1 ] && die "too many arguments provided" | ||||
| 
 | ||||
| PROJECT_NAME=$1 | ||||
| 
 | ||||
| HTML=$( | ||||
| 	cat <<-END | ||||
| 		<html lang="en"> | ||||
| 		<head> | ||||
| 		    <meta charset="utf-8"> | ||||
| 		    <title>${PROJECT_NAME}</title> | ||||
| 		    <style> | ||||
| 		        html, | ||||
| 		        body, | ||||
| 		        canvas { | ||||
| 		            margin: 0px; | ||||
| 		            padding: 0px; | ||||
| 		            width: 100%; | ||||
| 		            height: 100%; | ||||
| 		            overflow: hidden; | ||||
| 		            position: absolute; | ||||
| 		            z-index: 0; | ||||
| 		        } | ||||
| 		    </style> | ||||
| 		</head> | ||||
| 		<body style="margin: 0; padding: 0; height: 100vh; width: 100vw;"> | ||||
| 		    <canvas id="glcanvas" tabindex='1' hidden></canvas> | ||||
| 		    <script src="https://not-fl3.github.io/miniquad-samples/mq_js_bundle.js"></script> | ||||
| 		    <script type="module"> | ||||
| 		        import init, { set_wasm } from "./static/${PROJECT_NAME}.js"; | ||||
| 		        async function impl_run() { | ||||
| 		            let wbg = await init(); | ||||
| 		            miniquad_add_plugin({ | ||||
| 		                register_plugin: (a) => (a.wbg = wbg), | ||||
| 		                on_init: () => set_wasm(wasm_exports), | ||||
| 		                version: "0.0.1", | ||||
| 		                name: "wbg", | ||||
| 		            }); | ||||
| 		            load("./static/${PROJECT_NAME}_bg.wasm"); | ||||
| 		        } | ||||
| 		        window.run = function() { | ||||
| 		            document.getElementById("run-container").remove(); | ||||
| 		            document.getElementById("glcanvas").removeAttribute("hidden"); | ||||
| 		            document.getElementById("glcanvas").focus(); | ||||
| 		            impl_run(); | ||||
| 		        } | ||||
| 		    </script> | ||||
| 		    <div id="run-container" style="display: flex; justify-content: center; align-items: center; height: 100%; flex-direction: column;"> | ||||
| 		        <button onclick="run()">Run Game</button> | ||||
| 		    </div> | ||||
| 		</body> | ||||
| 		</html> | ||||
| 	END | ||||
| ) | ||||
| 
 | ||||
| TARGET_DIR="target/wasm32-unknown-unknown" | ||||
| # Build | ||||
| if [ -n "$RELEASE" ]; then | ||||
| 	cargo build --release --target wasm32-unknown-unknown | ||||
| 	TARGET_DIR="$TARGET_DIR/release" | ||||
| else | ||||
| 	cargo build --target wasm32-unknown-unknown | ||||
| 	TARGET_DIR="$TARGET_DIR/debug" | ||||
| fi | ||||
| 
 | ||||
| # Generate bindgen outputs | ||||
| mkdir -p dist | ||||
| wasm-bindgen $TARGET_DIR/"$PROJECT_NAME".wasm --out-dir dist --target web --no-typescript | ||||
| 
 | ||||
| # Shim to tie the thing together | ||||
| sed -i "s/import \* as __wbg_star0 from 'env';//" dist/"$PROJECT_NAME".js | ||||
| sed -i "s/let wasm;/let wasm; export const set_wasm = (w) => wasm = w;/" dist/"$PROJECT_NAME".js | ||||
| sed -i "s/imports\['env'\] = __wbg_star0;/return imports.wbg\;/" dist/"$PROJECT_NAME".js | ||||
| sed -i "s/const imports = __wbg_get_imports();/return __wbg_get_imports();/" dist/"$PROJECT_NAME".js | ||||
| 
 | ||||
| # Create index from the HTML variable | ||||
| echo "$HTML" >dist/index.html | ||||
							
								
								
									
										66
									
								
								frontend/src/context.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								frontend/src/context.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| use ewebsock::WsEvent; | ||||
| use macroquad::prelude::{error, info}; | ||||
| 
 | ||||
| use crate::model::{ClientMessage, ClientRequest, GameResponse}; | ||||
| 
 | ||||
| pub struct Context { | ||||
|     pub sender: ewebsock::WsSender, | ||||
|     pub reciever: ewebsock::WsReceiver, | ||||
|     pub token: String, | ||||
|     pub username: String, | ||||
| } | ||||
| 
 | ||||
| impl Context { | ||||
|     pub fn new(url: &str) -> Context { | ||||
|         let (sender, reciever) = ewebsock::connect(url, ewebsock::Options::default()).unwrap(); | ||||
| 
 | ||||
|         Self { | ||||
|             sender, | ||||
|             reciever, | ||||
|             token: String::new(), | ||||
|             username: String::new(), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn send_msg(&mut self, msg: &ClientMessage) { | ||||
|         let request = ClientRequest { | ||||
|             token: self.token.clone(), | ||||
|             msg: msg.clone(), | ||||
|         }; | ||||
|         let msg = serde_json::to_string(&request).unwrap(); | ||||
|         self.sender.send(ewebsock::WsMessage::Text(msg)); | ||||
|     } | ||||
| 
 | ||||
|     pub fn recv_msg(&mut self) -> Option<GameResponse> { | ||||
|         if let Some(msg) = self.reciever.try_recv() { | ||||
|             match msg { | ||||
|                 WsEvent::Opened => { | ||||
|                     info!("Web socket opened!"); | ||||
|                     None | ||||
|                 } | ||||
|                 WsEvent::Message(ws_message) => match ws_message { | ||||
|                     ewebsock::WsMessage::Text(msg) => { | ||||
|                         let game_resp: GameResponse = serde_json::from_str(&msg).unwrap(); | ||||
| 
 | ||||
|                         if let GameResponse::JoinResponse { username, token } = &game_resp { | ||||
|                             self.username = username.clone(); | ||||
|                             self.token = token.clone(); | ||||
|                         } | ||||
| 
 | ||||
|                         Some(game_resp) | ||||
|                     } | ||||
|                     _ => None, | ||||
|                 }, | ||||
|                 WsEvent::Error(err) => { | ||||
|                     error!("Something hecky going on: {}", err); | ||||
|                     None | ||||
|                 } | ||||
|                 WsEvent::Closed => { | ||||
|                     panic!("Socket closed, something up..."); | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             None | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										87
									
								
								frontend/src/login_screen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								frontend/src/login_screen.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| use crate::StateTransition; | ||||
| use crate::context::Context; | ||||
| use crate::model::register::Register; | ||||
| use crate::model::{ClientMessage, GameResponse}; | ||||
| use crate::screen::Screen; | ||||
| use macroquad::hash; | ||||
| use macroquad::{prelude::*, ui::root_ui}; | ||||
| 
 | ||||
| #[derive(Default)] | ||||
| pub struct LoginScreen { | ||||
|     new_username: String, | ||||
|     new_room_code: String, | ||||
|     sent_join: bool, | ||||
|     retry_count: usize, | ||||
|     error_msg: bool, | ||||
| } | ||||
| 
 | ||||
| impl Screen for LoginScreen { | ||||
|     fn handle_frame(&mut self, ctx: &mut Context) -> Option<StateTransition> { | ||||
|         clear_background(WHITE); | ||||
| 
 | ||||
|         let group_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.05); | ||||
|         let window_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.25); | ||||
| 
 | ||||
|         let mut pressed_join = false; | ||||
|         root_ui().window( | ||||
|             hash!("Window"), | ||||
|             Vec2::new( | ||||
|                 screen_width() * 0.5 - window_size.x * 0.5, | ||||
|                 screen_height() * 0.5 - window_size.x * 0.5, | ||||
|             ), | ||||
|             Vec2::new(screen_width() * 0.25, screen_height() * 0.25), | ||||
|             |ui| { | ||||
|                 ui.group(hash!("User Group"), group_size, |ui| { | ||||
|                     ui.input_text(hash!("Username In"), "Username", &mut self.new_username); | ||||
|                 }); | ||||
|                 ui.group(hash!("Room Group"), group_size, |ui| { | ||||
|                     ui.input_text(hash!("Room Code In"), "Room Code", &mut self.new_room_code); | ||||
|                 }); | ||||
|                 ui.group(hash!("Join Group"), group_size, |ui| { | ||||
|                     if ui.button(Vec2::new(group_size.x * 0.45, 0.0), "Join") { | ||||
|                         info!( | ||||
|                             "User pressed joined with name={} room_code={}", | ||||
|                             self.new_username, self.new_room_code | ||||
|                         ); | ||||
| 
 | ||||
|                         pressed_join = true; | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 if self.error_msg { | ||||
|                     ui.group(hash!("Error Group"), group_size, |ui| { | ||||
|                         ui.label(Vec2::new(0.0, 0.0), "Failed to join game, try again..."); | ||||
|                     }); | ||||
|                 } | ||||
|             }, | ||||
|         ); | ||||
| 
 | ||||
|         if pressed_join { | ||||
|             ctx.send_msg(&ClientMessage::Register(Register { | ||||
|                 game_code: self.new_room_code.clone(), | ||||
|                 username: self.new_username.clone(), | ||||
|             })); | ||||
| 
 | ||||
|             self.sent_join = true; | ||||
|         } | ||||
| 
 | ||||
|         if self.sent_join { | ||||
|             let msg = ctx.recv_msg(); | ||||
| 
 | ||||
|             if let Some(GameResponse::JoinResponse { username, token }) = msg { | ||||
|                 return Some(StateTransition::JoinAsPlayer { username, token }); | ||||
|             } | ||||
| 
 | ||||
|             self.retry_count += 1; | ||||
| 
 | ||||
|             if self.retry_count > get_fps() as usize * 5 { | ||||
|                 warn!("Join failed, try again..."); | ||||
|                 self.retry_count = 0; | ||||
|                 self.sent_join = false; | ||||
|                 self.error_msg = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         None | ||||
|     } | ||||
| } | ||||
| @ -1,41 +1,59 @@ | ||||
| use macroquad::hash; | ||||
| use macroquad::{prelude::*, ui::root_ui}; | ||||
| use context::Context; | ||||
| use login_screen::LoginScreen; | ||||
| use macroquad::prelude::*; | ||||
| use player_screen::PlayerScreen; | ||||
| use screen::Screen; | ||||
| 
 | ||||
| mod context; | ||||
| mod login_screen; | ||||
| mod model; | ||||
| mod player_screen; | ||||
| mod screen; | ||||
| 
 | ||||
| pub enum State { | ||||
|     Init, | ||||
|     Login(LoginScreen), | ||||
|     PlayerScreen(PlayerScreen), | ||||
| } | ||||
| 
 | ||||
| impl State { | ||||
|     pub fn transition_state(&self, transition: StateTransition) -> Self { | ||||
|         match transition { | ||||
|             StateTransition::Init | StateTransition::LeaveGame => { | ||||
|                 Self::Login(LoginScreen::default()) | ||||
|             } | ||||
|             StateTransition::JoinAsPlayer { username, token } => { | ||||
|                 Self::PlayerScreen(PlayerScreen::new(&username, &token)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn handle_frame(&mut self, ctx: &mut Context) -> Option<StateTransition> { | ||||
|         match self { | ||||
|             Self::Init => Some(StateTransition::Init), | ||||
|             Self::Login(login_screen) => login_screen.handle_frame(ctx), | ||||
|             Self::PlayerScreen(player_screen) => player_screen.handle_frame(ctx), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub enum StateTransition { | ||||
|     Init, | ||||
|     JoinAsPlayer { username: String, token: String }, | ||||
|     LeaveGame, | ||||
| } | ||||
| 
 | ||||
| #[macroquad::main("Play of the Game")] | ||||
| async fn main() { | ||||
|     let mut username = String::new(); | ||||
|     let mut rooom_code = String::new(); | ||||
|     let mut context = Context::new("ws://127.0.0.1:8080/ws"); | ||||
| 
 | ||||
|     let mut state = State::Init; | ||||
|     state.transition_state(StateTransition::Init); | ||||
| 
 | ||||
|     loop { | ||||
|         clear_background(WHITE); | ||||
| 
 | ||||
|         let group_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.05); | ||||
|         let window_size = Vec2::new(screen_width() * 0.25, screen_height() * 0.25); | ||||
| 
 | ||||
|         root_ui().window( | ||||
|             hash!("Window"), | ||||
|             Vec2::new( | ||||
|                 screen_width() * 0.5 - window_size.x * 0.5, | ||||
|                 screen_height() * 0.5 - window_size.x * 0.5, | ||||
|             ), | ||||
|             Vec2::new(screen_width() * 0.25, screen_height() * 0.25), | ||||
|             |ui| { | ||||
|                 ui.group(hash!("User Group"), group_size, |ui| { | ||||
|                     ui.input_text(hash!("Username In"), "Username", &mut username); | ||||
|                 }); | ||||
|                 ui.group(hash!("Room Group"), group_size, |ui| { | ||||
|                     ui.input_text(hash!("Room Code In"), "Room Code", &mut rooom_code); | ||||
|                 }); | ||||
|                 ui.group(hash!("Join Group"), group_size, |ui| { | ||||
|                     if ui.button(Vec2::new(group_size.x * 0.45, 0.0), "Join") { | ||||
|                         info!( | ||||
|                             "User pressed joined with name={} room_code={}", | ||||
|                             username, rooom_code | ||||
|                         ); | ||||
|         if let Some(transition) = state.handle_frame(&mut context) { | ||||
|             state = state.transition_state(transition); | ||||
|         } | ||||
|                 }); | ||||
|             }, | ||||
|         ); | ||||
| 
 | ||||
|         next_frame().await | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										6
									
								
								frontend/src/model/buzz_in.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/model/buzz_in.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct BuzzIn { | ||||
|     pub time: f64, | ||||
| } | ||||
							
								
								
									
										28
									
								
								frontend/src/model/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/model/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| use buzz_in::BuzzIn; | ||||
| use register::Register; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| 
 | ||||
| pub mod buzz_in; | ||||
| pub mod register; | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Clone)] | ||||
| #[serde(tag = "type")] | ||||
| pub enum ClientMessage { | ||||
|     BuzzIn(BuzzIn), | ||||
|     Register(Register), | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct ClientRequest { | ||||
|     pub token: String, | ||||
|     pub msg: ClientMessage, | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, Deserialize)] | ||||
| #[serde(tag = "type")] | ||||
| pub enum GameResponse { | ||||
|     JoinResponse { username: String, token: String }, | ||||
|     AckBuzzer, | ||||
|     ResetBuzzer, | ||||
|     UpdatePountTotal { score: i32 }, | ||||
| } | ||||
							
								
								
									
										7
									
								
								frontend/src/model/register.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/model/register.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| use serde::Serialize; | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Clone)] | ||||
| pub struct Register { | ||||
|     pub game_code: String, | ||||
|     pub username: String, | ||||
| } | ||||
							
								
								
									
										72
									
								
								frontend/src/player_screen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								frontend/src/player_screen.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | ||||
| use web_time::{SystemTime, UNIX_EPOCH}; | ||||
| 
 | ||||
| use crate::StateTransition; | ||||
| use crate::context::Context; | ||||
| use crate::model::ClientMessage; | ||||
| use crate::model::buzz_in::BuzzIn; | ||||
| use crate::screen::Screen; | ||||
| use macroquad::prelude::*; | ||||
| 
 | ||||
| pub struct PlayerScreen { | ||||
|     token: String, | ||||
|     username: String, | ||||
|     score: i32, | ||||
|     button_pressed: bool, | ||||
| } | ||||
| 
 | ||||
| impl PlayerScreen { | ||||
|     pub fn new(token: &str, username: &str) -> Self { | ||||
|         Self { | ||||
|             token: token.to_string(), | ||||
|             username: username.to_string(), | ||||
|             score: 0, | ||||
|             button_pressed: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Screen for PlayerScreen { | ||||
|     fn handle_frame(&mut self, ctx: &mut Context) -> Option<StateTransition> { | ||||
|         clear_background(BEIGE); | ||||
| 
 | ||||
|         let button_color = if self.button_pressed { | ||||
|             Color::from_hex(0xaa0000) | ||||
|         } else { | ||||
|             RED | ||||
|         }; | ||||
| 
 | ||||
|         let button_size = (screen_width() * 0.33).min(screen_height() * 0.20); | ||||
|         draw_circle( | ||||
|             screen_width() * 0.5, | ||||
|             screen_height() * 0.5, | ||||
|             button_size * 1.1, | ||||
|             DARKBROWN, | ||||
|         ); | ||||
|         draw_circle( | ||||
|             screen_width() * 0.5, | ||||
|             screen_height() * 0.5, | ||||
|             button_size, | ||||
|             button_color, | ||||
|         ); | ||||
| 
 | ||||
|         if is_mouse_button_pressed(MouseButton::Left) { | ||||
|             let loc = Vec2::from(mouse_position()); | ||||
|             let normalize = loc - Vec2::new(screen_width() * 0.5, screen_height() * 0.5); | ||||
| 
 | ||||
|             if normalize.length() < button_size { | ||||
|                 let timestamp = SystemTime::now(); | ||||
|                 let unix_timestamp = timestamp.duration_since(UNIX_EPOCH).expect("Time invalid"); | ||||
| 
 | ||||
|                 let time = unix_timestamp.as_secs_f64(); | ||||
| 
 | ||||
|                 info!("Button pressed @ {}", time); | ||||
|                 self.button_pressed = true; | ||||
|                 let buzz_in = BuzzIn { time }; | ||||
| 
 | ||||
|                 ctx.send_msg(&ClientMessage::BuzzIn(buzz_in)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         None | ||||
|     } | ||||
| } | ||||
							
								
								
									
										6
									
								
								frontend/src/screen.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/screen.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| use crate::{StateTransition, context::Context}; | ||||
| 
 | ||||
| pub trait Screen { | ||||
|     fn init_frame(&mut self) {} | ||||
|     fn handle_frame(&mut self, ctx: &mut Context) -> Option<StateTransition>; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user