diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..3b60027 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +# Web builds: miniquad's JS imports (console_log, init_webgl, ...) are +# resolved at runtime by web/mq_js_bundle.js — tell the linker to allow them. +[target.wasm32-unknown-unknown] +rustflags = ["-C", "link-arg=--allow-undefined"] diff --git a/.gitignore b/.gitignore index 5b9a3e6..83bb33b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ 001.jpeg 002.png 003.png +web/dist/ diff --git a/Cargo.lock b/Cargo.lock index 028e19d..a27a13d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,25 @@ dependencies = [ "png", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "itoa" version = "1.0.18" @@ -389,8 +408,10 @@ dependencies = [ name = "mrpgi" version = "0.1.0" dependencies = [ + "include_dir", "macroquad", "mrpgi-core", + "serde_json", ] [[package]] diff --git a/mrpgi-core/Cargo.toml b/mrpgi-core/Cargo.toml index f0caa61..92dd0da 100644 --- a/mrpgi-core/Cargo.toml +++ b/mrpgi-core/Cargo.toml @@ -8,6 +8,10 @@ description = "MRPGI headless engine core — the sim, parser, renderer and cont serde = { version = "1", features = ["derive"] } serde_json = "1" image = { version = "0.24", default-features = false, features = ["png"] } # PNG-only: lean builds + +# Native-only: the AI lane and the HTTP surface don't exist in browser builds +# (wasm gets the deterministic parser and a baked-in game). +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] ureq = { version = "2", features = ["json", "tls"] } # AI parser lane: Ollama / OpenRouter HTTP tiny_http = "0.12" # the --serve control surface; ponytail: swap for axum+WS if streaming push is ever needed diff --git a/mrpgi-core/src/ai.rs b/mrpgi-core/src/ai.rs index 4d2c71d..b6054f9 100644 --- a/mrpgi-core/src/ai.rs +++ b/mrpgi-core/src/ai.rs @@ -13,9 +13,12 @@ //! OPENROUTER_API_KEY (or MRPGI_AI_KEY) for openrouter use std::sync::mpsc::{self, Receiver}; +#[cfg(not(target_arch = "wasm32"))] use std::thread; +#[cfg(not(target_arch = "wasm32"))] use std::time::Duration; +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] // browser builds only ever construct Off #[derive(Clone, PartialEq)] enum Provider { Off, @@ -23,6 +26,7 @@ enum Provider { OpenRouter, } +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] // url/key unused when the lane is compiled out #[derive(Clone)] pub struct AiConfig { provider: Provider, @@ -33,6 +37,15 @@ pub struct AiConfig { impl AiConfig { pub fn from_env() -> Self { + // No HTTP client, no worker threads in the browser build. + #[cfg(target_arch = "wasm32")] + return Self::off(); + #[cfg(not(target_arch = "wasm32"))] + Self::from_env_native() + } + + #[cfg(not(target_arch = "wasm32"))] + fn from_env_native() -> Self { let which = std::env::var("MRPGI_AI").unwrap_or_else(|_| "ollama".into()).to_lowercase(); let provider = match which.as_str() { "off" | "none" | "0" => Provider::Off, @@ -84,6 +97,7 @@ Command:" /// Fire the translation on a worker thread; poll the returned receiver with /// `try_recv()`. The result is a sanitized command (or "" = no idea). +#[cfg(not(target_arch = "wasm32"))] pub fn translate_async(cfg: AiConfig, prompt: String) -> Receiver { let (tx, rx) = mpsc::channel(); thread::spawn(move || { @@ -93,6 +107,16 @@ pub fn translate_async(cfg: AiConfig, prompt: String) -> Receiver { rx } +/// Browser stub — never reached (the wasm AiConfig is always off), but keeps +/// the call sites compiling without a cfg forest. +#[cfg(target_arch = "wasm32")] +pub fn translate_async(_cfg: AiConfig, _prompt: String) -> Receiver { + let (tx, rx) = mpsc::channel(); + let _ = tx.send(String::new()); + rx +} + +#[cfg(not(target_arch = "wasm32"))] fn sanitize(s: &str) -> String { s.lines() .map(|l| l.trim()) @@ -103,6 +127,7 @@ fn sanitize(s: &str) -> String { .to_lowercase() } +#[cfg(not(target_arch = "wasm32"))] fn call(cfg: &AiConfig, prompt: &str) -> Result> { let agent = ureq::AgentBuilder::new() .timeout_connect(Duration::from_secs(5)) diff --git a/mrpgi-core/src/assets.rs b/mrpgi-core/src/assets.rs index be0d1bd..9cef9b8 100644 --- a/mrpgi-core/src/assets.rs +++ b/mrpgi-core/src/assets.rs @@ -67,6 +67,11 @@ pub fn quantize(img: &image::RgbaImage, dither: bool) -> Cel { Cel { w, h, pixels } } +/// Quantize a PNG already in memory (the browser build's sprite path). +pub fn cel_from_bytes(bytes: &[u8], dither: bool) -> Option { + image::load_from_memory(bytes).ok().map(|img| quantize(&img.to_rgba8(), dither)) +} + /// Load a PNG and quantize it. Returns a small magenta placeholder on failure /// so missing art is obvious rather than a crash. pub fn load_cel(path: &str, dither: bool) -> Cel { diff --git a/mrpgi-core/src/world.rs b/mrpgi-core/src/world.rs index 3388b21..7e16319 100644 --- a/mrpgi-core/src/world.rs +++ b/mrpgi-core/src/world.rs @@ -278,6 +278,28 @@ impl World { w } + /// Build a World entirely from memory — no filesystem. This is how the + /// browser build boots: the game folder is baked into the binary and + /// handed over here as parsed rooms + quantized sprites. + pub fn from_memory( + manifest: GameManifest, + rooms: impl IntoIterator, + sprites: Vec<(String, Cel)>, + ) -> Self { + let mut w = World { + base: PathBuf::from("."), + current: manifest.start_room, + manifest, + doc: RoomDoc::default(), + fb: FrameBuffer::new(), + sprites, + overlay: rooms.into_iter().collect(), + }; + let start = w.current; + w.goto_room(start); + w + } + pub fn room_path(&self, n: u32) -> PathBuf { self.base.join(format!("rooms/room{}.json", n)) } diff --git a/mrpgi/Cargo.toml b/mrpgi/Cargo.toml index 6dfb4c8..bd6c638 100644 --- a/mrpgi/Cargo.toml +++ b/mrpgi/Cargo.toml @@ -7,3 +7,8 @@ description = "Monster Robot Party Game Interpreter — macroquad GUI front-end [dependencies] mrpgi-core = { path = "../mrpgi-core" } macroquad = "0.4" + +# Browser builds bake a game folder into the binary (no filesystem on the web). +[target.'cfg(target_arch = "wasm32")'.dependencies] +include_dir = "0.7" +serde_json = "1" diff --git a/mrpgi/src/main.rs b/mrpgi/src/main.rs index 77cbe7d..4ae8fb4 100644 --- a/mrpgi/src/main.rs +++ b/mrpgi/src/main.rs @@ -34,28 +34,70 @@ enum Mode { Play, } +/// Browser boot: the game folder is compiled into the binary (there is no +/// filesystem on the web) and handed to the core fully parsed. +#[cfg(target_arch = "wasm32")] +fn embedded_world() -> World { + static GAME: include_dir::Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../games/lost-fuse"); + let manifest = GAME + .get_file("game.json") + .and_then(|f| serde_json::from_slice(f.contents()).ok()) + .unwrap_or_default(); + let rooms = (0u32..64).filter_map(|n| { + let f = GAME.get_file(format!("rooms/room{}.json", n))?; + Some((n, serde_json::from_slice(f.contents()).ok()?)) + }); + let mut sprites: Vec<(String, mrpgi_core::view::Cel)> = GAME + .get_dir("sprites") + .map(|d| { + d.files() + .filter_map(|f| { + let name = f.path().file_stem()?.to_str()?.to_string(); + Some((name, mrpgi_core::assets::cel_from_bytes(f.contents(), false)?)) + }) + .collect() + }) + .unwrap_or_default(); + sprites.sort_by(|a, b| a.0.cmp(&b.0)); + World::from_memory(manifest, rooms, sprites) +} + #[macroquad::main(window_conf)] async fn main() { let game_dir = std::env::args() .skip_while(|a| a != "--game") .nth(1) .unwrap_or_else(|| ".".to_string()); - if std::env::args().any(|a| a == "--sample") { - mrpgi_core::kit::write_sample_world(&game_dir); + #[cfg(not(target_arch = "wasm32"))] + { + if std::env::args().any(|a| a == "--sample") { + mrpgi_core::kit::write_sample_world(&game_dir); + } + if std::env::args().any(|a| a == "--kit") { + mrpgi_core::kit::write_kit(&game_dir); + } + mrpgi_core::assets::ensure_sample_sprites( + &std::path::Path::new(&game_dir).join("sprites").to_string_lossy(), + ); } - if std::env::args().any(|a| a == "--kit") { - mrpgi_core::kit::write_kit(&game_dir); - } - mrpgi_core::assets::ensure_sample_sprites( - &std::path::Path::new(&game_dir).join("sprites").to_string_lossy(), - ); let ai_cfg = AiConfig::from_env(); let ai_label = ai_cfg.label(); - let mut gs = GameState::new(World::load_game(&game_dir), ai_cfg); + #[cfg(not(target_arch = "wasm32"))] + let world = World::load_game(&game_dir); + #[cfg(target_arch = "wasm32")] + let world = embedded_world(); + let mut gs = GameState::new(world, ai_cfg); let mut ed = Editor::new(&game_dir); + // Native boots into the editor; the web build boots straight into the game. + #[cfg(not(target_arch = "wasm32"))] let mut mode = Mode::Edit; + #[cfg(target_arch = "wasm32")] + let mut mode = Mode::Play; + #[cfg(target_arch = "wasm32")] + let _ = gs.apply(Command::NewGame { room: None }); + let mut play_img = Image::gen_image_color(PIC_W as u16, PIC_H as u16, BLACK); let play_tex = Texture2D::from_image(&play_img); play_tex.set_filter(FilterMode::Nearest); diff --git a/web/build.sh b/web/build.sh new file mode 100755 index 0000000..1f3277c --- /dev/null +++ b/web/build.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# Build the browser version of MRPGI into web/dist/ (self-contained, static). +# Requires the wasm target: rustup target add wasm32-unknown-unknown +set -e +cd "$(dirname "$0")/.." +cargo build -p mrpgi --release --target wasm32-unknown-unknown +mkdir -p web/dist +cp target/wasm32-unknown-unknown/release/mrpgi.wasm web/dist/ +cp web/index.html web/mq_js_bundle.js web/dist/ +echo "web build ready: $(du -sh web/dist | cut -f1) in web/dist/" diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..15260df --- /dev/null +++ b/web/index.html @@ -0,0 +1,40 @@ + + + + + + MRPGI — Monster Robot Party Game Interpreter + + + + +
MRPGIbooting the party…
+ + + + + diff --git a/web/mq_js_bundle.js b/web/mq_js_bundle.js new file mode 100644 index 0000000..7271d05 --- /dev/null +++ b/web/mq_js_bundle.js @@ -0,0 +1,3 @@ +"use strict";const version=2,canvas=document.querySelector("#glcanvas");var gl,clipboard=null,wasm_memory,animation_frame_timeout,FS,GL,Module,wasm_exports,emscripten_shaders_hack,importObject,plugins=[],high_dpi=!1,blocking_event_loop=!1;function init_webgl(e){if(e==1){gl=canvas.getContext("webgl");function t(e){var t=e.getExtension("OES_vertex_array_object");t?(e.createVertexArray=function(){return t.createVertexArrayOES()},e.deleteVertexArray=function(e){t.deleteVertexArrayOES(e)},e.bindVertexArray=function(e){t.bindVertexArrayOES(e)},e.isVertexArray=function(e){return t.isVertexArrayOES(e)}):alert("Unable to get OES_vertex_array_object extension")}function n(e){var t=e.getExtension("ANGLE_instanced_arrays");t&&(e.vertexAttribDivisor=function(e,n){t.vertexAttribDivisorANGLE(e,n)},e.drawArraysInstanced=function(e,n,s,o){t.drawArraysInstancedANGLE(e,n,s,o)},e.drawElementsInstanced=function(e,n,s,o,i){t.drawElementsInstancedANGLE(e,n,s,o,i)})}function s(e){var t=e.getExtension("EXT_disjoint_timer_query");t&&(e.createQuery=function(){return t.createQueryEXT()},e.beginQuery=function(e,n){return t.beginQueryEXT(e,n)},e.endQuery=function(e){return t.endQueryEXT(e)},e.deleteQuery=function(e){t.deleteQueryEXT(e)},e.getQueryObject=function(e,n){return t.getQueryObjectEXT(e,n)})}function o(e){var t=e.getExtension("WEBGL_draw_buffers");t&&(e.drawBuffers=function(e){return t.drawBuffersWEBGL(e)})}try{gl.getExtension("EXT_shader_texture_lod"),gl.getExtension("OES_standard_derivatives")}catch(e){console.warn(e)}t(gl),n(gl),s(gl),o(gl),gl.getExtension("WEBGL_depth_texture")==null&&alert("Cant initialize WEBGL_depth_texture extension")}else gl=canvas.getContext("webgl2");gl===null&&alert("Unable to initialize WebGL. Your browser or machine may not support it.")}canvas.focus(),canvas.requestPointerLock=canvas.requestPointerLock||canvas.mozRequestPointerLock||function(){},document.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock||function(){};function assert(e,t){e==!1&&alert(t)}function getArray(e,t,n){return new t(wasm_memory.buffer,e,n)}function UTF8ToString(e,t){let i=new Uint8Array(wasm_memory.buffer,e);for(var n,a,r,c,s=0,l=s+t,o="";!(s>=l);){if(n=i[s++],!n)return o;if(!(n&128)){o+=String.fromCharCode(n);continue}if(a=i[s++]&63,(n&224)==192){o+=String.fromCharCode((n&31)<<6|a);continue}r=i[s++]&63,(n&240)==224?n=(n&15)<<12|a<<6|r:((n&248)!=240&&console.warn("Invalid UTF-8 leading byte 0x"+n.toString(16)+" encountered when deserializing a UTF-8 string on the asm.js/wasm heap to a JS string!"),n=(n&7)<<18|a<<12|r<<6|i[s++]&63),n<65536?o+=String.fromCharCode(n):(c=n-65536,o+=String.fromCharCode(55296|c>>10,56320|c&1023))}return o}function stringToUTF8(e,t,n,s){for(var o,r,c=n,i=n+s,a=0;a=55296&&o<=57343&&(r=e.charCodeAt(++a),o=65536+((o&1023)<<10)|r&1023),o<=127){if(n>=i)break;t[n++]=o}else if(o<=2047){if(n+1>=i)break;t[n++]=192|o>>6,t[n++]=128|o&63}else if(o<=65535){if(n+2>=i)break;t[n++]=224|o>>12,t[n++]=128|o>>6&63,t[n++]=128|o&63}else{if(n+3>=i)break;o>=2097152&&console.warn("Invalid Unicode code point 0x"+o.toString(16)+" encountered when serializing a JS string to an UTF-8 string on the asm.js/wasm heap! (Valid unicode code points should be in range 0-0x1FFFFF)."),t[n++]=240|o>>18,t[n++]=128|o>>12&63,t[n++]=128|o>>6&63,t[n++]=128|o&63}return n-c}FS={loaded_files:[],unique_id:0},GL={counter:1,buffers:[],mappedBuffers:{},programs:[],framebuffers:[],renderbuffers:[],textures:[],uniforms:[],shaders:[],vaos:[],timerQueries:[],contexts:{},programInfos:{},getNewId:function(e){for(var n=GL.counter++,t=e.length;t=0&&n=GL.counter){console.error("GL_INVALID_VALUE in glGetProgramiv");return}var s,o=GL.programInfos[e];if(!o){console.error("GL_INVALID_OPERATION in glGetProgramiv(program="+e+", pname="+t+", p=0x"+n.toString(16)+"): The specified GL object name does not refer to a program object!");return}if(t==35716)s=gl.getProgramInfoLog(GL.programs[e]),assert(s!==null),getArray(n,Int32Array,1)[0]=s.length+1;else if(t==35719){console.error("unsupported operation");return}else if(t==35722){console.error("unsupported operation");return}else if(t==35381){console.error("unsupported operation");return}else getArray(n,Int32Array,1)[0]=gl.getProgramParameter(GL.programs[e],t)},glCreateShader:function(e){var t=GL.getNewId(GL.shaders);return GL.shaders[t]=gl.createShader(e),t},glStencilFuncSeparate:function(e,t,n,s){gl.stencilFuncSeparate(e,t,n,s)},glStencilMaskSeparate:function(e,t){gl.stencilMaskSeparate(e,t)},glStencilOpSeparate:function(e,t,n,s){gl.stencilOpSeparate(e,t,n,s)},glFrontFace:function(e){gl.frontFace(e)},glCullFace:function(e){gl.cullFace(e)},glCopyTexImage2D:function(e,t,n,s,o,i,a,r){gl.copyTexImage2D(e,t,n,s,o,i,a,r)},glShaderSource:function(e,t,n,s){GL.validateGLObjectID(GL.shaders,e,"glShaderSource","shader");var i,o=GL.getSource(e,t,n,s);emscripten_shaders_hack&&(o=o.replace(/#extension GL_OES_standard_derivatives : enable/g,""),o=o.replace(/#extension GL_EXT_shader_texture_lod : enable/g,""),i="",o.indexOf("gl_FragColor")!=-1&&(i+=`out mediump vec4 GL_FragColor; +`,o=o.replace(/gl_FragColor/g,"GL_FragColor")),o.indexOf("attribute")!=-1?(o=o.replace(/attribute/g,"in"),o=o.replace(/varying/g,"out")):o=o.replace(/varying/g,"in"),o=o.replace(/textureCubeLodEXT/g,"textureCubeLod"),o=o.replace(/texture2DLodEXT/g,"texture2DLod"),o=o.replace(/texture2DProjLodEXT/g,"texture2DProjLod"),o=o.replace(/texture2DGradEXT/g,"texture2DGrad"),o=o.replace(/texture2DProjGradEXT/g,"texture2DProjGrad"),o=o.replace(/textureCubeGradEXT/g,"textureCubeGrad"),o=o.replace(/textureCube/g,"texture"),o=o.replace(/texture1D/g,"texture"),o=o.replace(/texture2D/g,"texture"),o=o.replace(/texture3D/g,"texture"),o=o.replace(/#version 100/g,`#version 300 es +`+i)),gl.shaderSource(GL.shaders[e],o)},glGetProgramInfoLog:function(e,t,n,s){GL.validateGLObjectID(GL.programs,e,"glGetProgramInfoLog","program");var o,i=gl.getProgramInfoLog(GL.programs[e]);assert(i!==null);let a=getArray(s,Uint8Array,t);for(o=0;o(add_missing_functions_stabs(e),WebAssembly.instantiate(e,importObject))).then(e=>{wasm_memory=e.exports.memory,wasm_exports=e.exports;var t=wasm_exports.crate_version();version!=t&&console.error("Version mismatch: gl.js version is: "+version+", miniquad crate version is: "+t),init_plugins(plugins),e.exports.main()}).catch(e=>{console.error(e)}):t.then(function(e){return e.arrayBuffer()}).then(function(e){return WebAssembly.compile(e)}).then(function(e){return add_missing_functions_stabs(e),WebAssembly.instantiate(e,importObject)}).then(function(e){wasm_memory=e.exports.memory,wasm_exports=e.exports;var t=wasm_exports.crate_version();version!=t&&console.error("Version mismatch: gl.js version is: "+version+", rust sapp-wasm crate version is: "+t),init_plugins(plugins),e.exports.main()}).catch(e=>{console.error("WASM failed to load, probably incompatible gl.js version"),console.error(e)})}(function(){"use strict";const c=window.AudioContext||window.webkitAudioContext;let e,n=new Map,t=[],o=1,m=1;function d(){if(e==null){e=new c;let n=e.listener;{let s=window.AudioContext||window.webkitAudioContext,n=new s;var t=function(){console.log("fix"),e.resume();var i=n.createBuffer(1,1,22050),o=n.createBufferSource();o.buffer=i,o.connect(n.destination),o.start?o.start(0):o.play?o.play(0):o.noteOn&&o.noteOn(0),document.removeEventListener("touchstart",t),document.removeEventListener("touchend",t),document.removeEventListener("mousedown",t),document.removeEventListener("keydown",t)};document.addEventListener("touchstart",t),document.addEventListener("touchend",t),document.addEventListener("mousedown",t),document.addEventListener("keydown",t)}}}function r(t,s){let a=wasm_memory.buffer.slice(t,t+s),i=o;return o+=1,e.decodeAudioData(a,function(e){n.set(i,e)},function(e){console.error("Failed to decode audio buffer",e)}),i}function a(e){return n.has(e)&&n.get(e)!=void 0}function l(){let n=t.find(e=>e.sound_key===0);return n!=null?n.source=e.createBufferSource():(n={sound_key:0,playback_key:0,source:e.createBufferSource(),gain_node:e.createGain(),ended:null},t.push(n)),n}function s(e){try{e.source.removeEventListener("ended",e.ended),e.source.disconnect(),e.gain_node.disconnect(),e.sound_key=0,e.playback_key=0}catch(e){console.error("Error stopping sound",e)}}function u(t,o,i){let r=m++,a=l();a.sound_key=t,a.playback_key=r,a.source.connect(a.gain_node),a.gain_node.connect(e.destination),a.gain_node.gain.value=o,a.source.loop=i,a.ended=function(){s(a)},a.source.addEventListener("ended",a.ended);try{a.source.buffer=n.get(t),a.source.start(0)}catch(e){console.error("Error starting sound",e)}return r}function h(e,n){t.forEach(t=>{t.sound_key===e&&(t.gain_node.gain.value=n)})}function i(e){t.forEach(t=>{t.sound_key===e&&s(t)})}function f(e){i(e),n.delete(e)}function p(e){let n=t.find(t=>t.playback_key===e);n!=null&&s(n)}function g(e,n){let s=t.find(t=>t.playback_key===e);s!=null&&(s.gain_node.gain.value=n)}function v(e){e.env.audio_init=d,e.env.audio_add_buffer=r,e.env.audio_play_buffer=u,e.env.audio_source_is_loaded=a,e.env.audio_source_set_volume=h,e.env.audio_source_stop=i,e.env.audio_source_delete=f,e.env.audio_playback_stop=p,e.env.audio_playback_set_volume=g}miniquad_add_plugin({register_plugin:v,version:1,name:"macroquad_audio"})})(),function(){"use strict";var n,i=null,e={};e[-1]=null,e[-2]=void 0,n=0;function o(n){n.env.js_create_string=function(e,n){var s=UTF8ToString(e,n);return t(s)},n.env.js_create_buffer=function(e,n){var s=new Uint8Array(wasm_memory.buffer,e,n),o=new Uint8Array(new ArrayBuffer(s.byteLength));return o.set(new Uint8Array(s)),t(o)},n.env.js_create_object=function(){var e={};return t(e)},n.env.js_set_field_f32=function(t,n,s,o){var i=UTF8ToString(n,s);e[t][i]=o},n.env.js_set_field_u32=function(t,n,s,o){var i=UTF8ToString(n,s);e[t][i]=o},n.env.js_set_field_string=function(t,n,s,o,i){var a=UTF8ToString(n,s),r=UTF8ToString(o,i);e[t][a]=r},n.env.js_unwrap_to_str=function(t,n,o){for(var r=e[t],a=s(r),c=a.length,l=new Uint8Array(wasm_memory.buffer,n,o),i=0;i>6,128|t&63):t<55296||t>=57344?n.push(224|t>>12,128|t>>6&63,128|t&63):(s++,t=65536+((t&1023)<<10|e.charCodeAt(s)&1023),n.push(240|t>>18,128|t>>12&63,128|t>>6&63,128|t&63));return n}function t(t){if(t==null)return-2;if(t===null)return-1;var s=n;return e[s]=t,n+=1,s}function a(t){var n=e[t];return delete e[t],n}function r(t){return e[t]}}(),function(){function l(){}function d(e){e.env.ws_connect=a,e.env.ws_is_connected=i,e.env.ws_send=r,e.env.ws_try_recv=c,e.env.http_make_request=h,e.env.http_try_recv=u}miniquad_add_plugin({register_plugin:d,on_init:l,version:1,name:"quad_net"});var e,t,s,o=0,n=[];function i(){return o}function a(e){t=new WebSocket(consume_js_object(e)),t.binaryType="arraybuffer",t.onopen=function(){o=1},t.onmessage=function(e){if(typeof e.data=="string")n.push({text:1,data:e.data});else{var t=new Uint8Array(e.data);n.push({text:0,data:t})}}}function r(e){var n=consume_js_object(e);n.buffer!=void 0?t.send(n.buffer):t.send(n)}function c(){return n.length!=0?js_object(n.shift()):-1}s=0,e={};function u(t){if(e[t]!=void 0&&e[t]!=null){var n=e[t];return e[t]=null,js_object(n)}return-1}function h(t,n,o,i){var a,r,c,d,u,l=s;s+=1,t==0&&(r="POST"),t==1&&(r="PUT"),t==2&&(r="GET"),t==3&&(r="DELETE"),d=consume_js_object(n),u=consume_js_object(o),c=consume_js_object(i),a=new XMLHttpRequest,a.open(r,d,!0),a.responseType="arraybuffer";for(const e in c)a.setRequestHeader(e,c[e]);return a.onload=function(){if(this.status==200){var n=new Uint8Array(this.response);e[l]=n}},a.onerror=function(e){console.error("Failed to make a request"),console.error(e)},a.send(u),l}}() \ No newline at end of file