From 50ede0febf6b22cfab916c436968ce1d769f6714 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Sun, 26 Jul 2026 11:38:51 +1000 Subject: [PATCH] web: MRPCI runs in a browser, as a player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macroquad GUI now builds for wasm32 the way MRPGI's does, with two deliberate differences. It boots into Play and the F8 editor toggle is compiled out of the web build. What ships to someone playing a game should be the game, not the tool that made it. And it embeds a WorldBundle rather than a game *folder*. MRPGI reaches for include_dir! because its bundle carries only manifest+rooms; MRPCI's already carries scripts and sprites too, so one `--export-bundle` document is the whole game. That is also the format MRP3GI boots from, which means one file now feeds two engines. mrpci_alloc/mrpci_world_in let the page hand over another bundle while the engine is running — the arcade picker and drag-and-drop both arrive there, and the session restarts on the new world. Co-Authored-By: Claude Opus 5 --- .cargo/config.toml | 16 +++++++ .gitignore | 2 + Cargo.lock | 1 + mrpci/Cargo.toml | 3 ++ mrpci/src/main.rs | 108 ++++++++++++++++++++++++++++++++++++++------ web/build.sh | 45 ++++++++++++++++++ web/index.html | 76 +++++++++++++++++++++++++++++++ web/mq_js_bundle.js | 3 ++ 8 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 .cargo/config.toml create mode 100755 web/build.sh create mode 100644 web/index.html create mode 100644 web/mq_js_bundle.js diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ebe7fc9 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,16 @@ +# Which game gets baked into wasm builds (WEB_BUNDLE in mrpci/src/main.rs). +# It is a WorldBundle JSON, not a folder: the browser has no filesystem, and +# `mrpci-headless --export-bundle` already writes exactly this document. +# web/build.sh regenerates it and overrides the path per build. +[env] +MRPCI_WEB_BUNDLE = { value = "web/game.bundle.json", relative = true } + +# 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. +# The mrpci_* exports are the page's game-loading bridge into the engine. +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "link-arg=--allow-undefined", + "-C", "link-arg=--export=mrpci_alloc", + "-C", "link-arg=--export=mrpci_world_in", +] diff --git a/.gitignore b/.gitignore index 1ebe09b..1d13293 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target games/*/saves/ .DS_Store +web/dist/ +web/game.bundle.json diff --git a/Cargo.lock b/Cargo.lock index 9001122..b3ba154 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -534,6 +534,7 @@ version = "0.1.0" dependencies = [ "macroquad", "mrpci-core", + "serde_json", ] [[package]] diff --git a/mrpci/Cargo.toml b/mrpci/Cargo.toml index 80ce395..9a36761 100644 --- a/mrpci/Cargo.toml +++ b/mrpci/Cargo.toml @@ -7,3 +7,6 @@ description = "Monster Robot Party Creative Interpreter — the SCI-generation s [dependencies] mrpci-core = { path = "../mrpci-core" } macroquad = { version = "0.4", features = ["audio"] } +# The browser build has no filesystem: it boots from a WorldBundle JSON baked +# in at compile time, and the page can hand it another one at runtime. +serde_json = "1" diff --git a/mrpci/src/main.rs b/mrpci/src/main.rs index db05a8a..a3e8454 100644 --- a/mrpci/src/main.rs +++ b/mrpci/src/main.rs @@ -101,25 +101,82 @@ fn conf() -> Conf { } } +/// The browser build's default game, baked in at compile time. +/// +/// A folder of JSON is a filesystem idea and the browser hasn't got one, so +/// the web build carries a `WorldBundle` — the same single-document form +/// `mrpci-headless --export-bundle` writes and the MRP3GI bridge boots from. +/// Which bundle is `MRPCI_WEB_BUNDLE` at compile time (see .cargo/config.toml). +#[cfg(target_arch = "wasm32")] +const WEB_BUNDLE: &str = include_str!(env!("MRPCI_WEB_BUNDLE")); + +/// Rebuild a whole world from a bundle JSON. `None` if it doesn't parse — +/// a bad drag-and-drop should be a shrug, not a blank screen. +pub fn world_from_bundle(json: &str) -> Option { + let b: mrpci_core::WorldBundle = serde_json::from_str(json).ok()?; + // Sorted, like the headless `--bundle` path: both boots must bake + // identical cels or the same game would look different in two windows. + let mut sprites: Vec<_> = b.sprites.into_iter().collect(); + sprites.sort_by(|a, c| a.0.cmp(&c.0)); + Some(World::from_memory(b.manifest, b.rooms, b.scripts, sprites)) +} + +/// The web save/load bridge, mirroring MRPGI's. The page writes a bundle +/// into an `mrpci_alloc` buffer and hands it over; the main loop picks it up +/// on the next frame. No threads, no async. +#[cfg(target_arch = "wasm32")] +mod web_bridge { + use std::sync::Mutex; + + pub static PENDING_LOAD: Mutex> = Mutex::new(None); + + /// JS asks for a buffer to write into. + #[no_mangle] + pub extern "C" fn mrpci_alloc(len: usize) -> *mut u8 { + Box::into_raw(vec![0u8; len].into_boxed_slice()) as *mut u8 + } + + /// JS hands over a bundle JSON it wrote into that buffer. + #[no_mangle] + pub extern "C" fn mrpci_world_in(ptr: *mut u8, len: usize) { + let s = unsafe { + let boxed = Box::from_raw(std::slice::from_raw_parts_mut(ptr, len) as *mut [u8]); + String::from_utf8_lossy(&boxed).into_owned() + }; + *PENDING_LOAD.lock().unwrap() = Some(s); + } +} + fn main() { - let args: Vec = std::env::args().collect(); - let game_dir = args - .iter() - .position(|a| a == "--game") - .and_then(|i| args.get(i + 1).cloned()) - .unwrap_or_else(|| { - // Default to the demo if it's been generated, else the cwd. - if std::path::Path::new("games/neon-precinct/game.json").exists() { - "games/neon-precinct".into() - } else { - ".".into() - } - }); + #[cfg(not(target_arch = "wasm32"))] + let game_dir = { + let args: Vec = std::env::args().collect(); + args.iter() + .position(|a| a == "--game") + .and_then(|i| args.get(i + 1).cloned()) + .unwrap_or_else(|| { + // Default to the demo if it's been generated, else the cwd. + if std::path::Path::new("games/neon-precinct/game.json").exists() { + "games/neon-precinct".into() + } else { + ".".into() + } + }) + }; + // On the web the "game dir" is only ever an audio-lookup prefix; the + // world itself is embedded. Relative, so it works under any sub-path. + #[cfg(target_arch = "wasm32")] + let game_dir = String::from("game"); macroquad::Window::from_config(conf(), amain(game_dir)); } async fn amain(game_dir: String) { + #[cfg(not(target_arch = "wasm32"))] let world = World::load_game(&game_dir); + // A malformed baked bundle would be a build error we'd rather see as an + // empty room than a white screen. + #[cfg(target_arch = "wasm32")] + let world = world_from_bundle(WEB_BUNDLE).unwrap_or_else(|| World::load_game(".")); let mut gs = GameState::new(world); let mut audio = sound::LazyAudio::start(&game_dir); let mut ui = Ui { @@ -167,7 +224,32 @@ async fn amain(game_dir: String) { show_mouse(false); loop { + // --- a game handed over by the page ---------------------------------- + // The browser build is a *player*: the picker (and drag-and-drop) + // swap the whole world underneath it, and the session restarts. + #[cfg(target_arch = "wasm32")] + { + let incoming = web_bridge::PENDING_LOAD.lock().unwrap().take(); + if let Some(json) = incoming { + if let Some(w) = world_from_bundle(&json) { + gs = GameState::new(w); + ui.log.clear(); + ui.selected_item = None; + ui.inventory_open = false; + ui.typing = false; + ui.fade = 0.0; + let boot = gs.apply(Command::NewGame { room: None }); + handle_events(&boot, &mut ui, &mut audio); + } else { + ui.log.push("(that file isn't an MRPCI game bundle.)".into()); + } + } + } + // --- edit-mode toggle ------------------------------------------------- + // Native only. Authoring is a desktop job; what ships to a player is + // the game, not the tool that made it. + #[cfg(not(target_arch = "wasm32"))] if is_key_pressed(KeyCode::F8) { ed.active = !ed.active; if ed.active { diff --git a/web/build.sh b/web/build.sh new file mode 100755 index 0000000..b352208 --- /dev/null +++ b/web/build.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# Build the browser version of MRPCI into web/dist/ (self-contained, static). +# Requires the wasm target: rustup target add wasm32-unknown-unknown +# +# ./web/build.sh # bakes games/neon-precinct +# ./web/build.sh /path/to/game # bake any game folder instead +# +# The browser build is a PLAYER: it boots straight into the game and the F8 +# editor toggle is compiled out. Authoring stays on the desktop. +# +# Unlike MRPGI's build, which embeds a game *folder* with include_dir!, this +# bakes a single WorldBundle JSON — the same document `--export-bundle` +# writes and the MRP3GI bridge boots from. One format, three engines, and the +# page can hand the engine another one at runtime. +set -e +cd "$(dirname "$0")/.." + +GAME="${1:-games/neon-precinct}" +BUNDLE="web/game.bundle.json" + +echo "baking game: $GAME" +cargo build --release -p mrpci-core --bin mrpci-headless +./target/release/mrpci-headless --game "$GAME" --export-bundle "$BUNDLE" + +# include_str! reads the path at compile time; make sure the crate recompiles +# when only the bundle changed. +touch mrpci/src/main.rs +cargo build -p mrpci --release --target wasm32-unknown-unknown + +rm -rf web/dist +mkdir -p web/dist +cp target/wasm32-unknown-unknown/release/mrpci.wasm web/dist/ +cp web/index.html web/mq_js_bundle.js web/dist/ + +# A game's own sfx/ and music/ ride along if it has them; the engine falls +# back to its built-in synth for anything missing, so this is optional. +for kind in sfx music; do + if [ -d "$GAME/$kind" ]; then + mkdir -p "web/dist/game/$kind" + cp "$GAME/$kind"/*.wav "$GAME/$kind"/*.ogg "$GAME/$kind"/*.mp3 \ + "web/dist/game/$kind/" 2>/dev/null || true + fi +done + +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..e855c9d --- /dev/null +++ b/web/index.html @@ -0,0 +1,76 @@ + + + + + + MRPCI — Monster Robot Party Creative Interpreter + + + + + + +
starting the interpreter…
+
+ click verb at pixel · right-click cycle verb · F1–F4 walk/look/do/talk · + I inventory · Enter type a command · F5/F7 save/restore · M mute · N scanlines +
+ + + + + + + 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