summaryrefslogtreecommitdiff
path: root/2026/games/rpc2dot0/vendor/teleia/crates
diff options
context:
space:
mode:
authorLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
committerLLLL Colonq <llll@colonq>2026-08-18 00:08:28 -0400
commit32a62cfcb925225359a5c90761b795f2f737ebd2 (patch)
tree95c2261964803b19f2d39cb35d8d514ed6e5a6f4 /2026/games/rpc2dot0/vendor/teleia/crates
parentde99c5f7636a952025ea13de02bafd25bcb6bdec (diff)
Re-populate games
Diffstat (limited to '2026/games/rpc2dot0/vendor/teleia/crates')
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/Cargo.toml53
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/build.rs15
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/default.pngbin0 -> 1042 bytes
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/small.pngbin0 -> 904 bytes
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/meshes/square.obj15
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/frag.glsl15
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/vert.glsl20
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/frag.glsl160
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/vert.glsl30
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/frag.glsl12
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/vert.glsl4
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/frag.glsl12
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/vert.glsl22
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/frag.glsl11
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/vert.glsl18
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/frag.glsl23
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/vert.glsl4
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/frag.glsl12
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/vert.glsl14
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/frag.glsl16
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/vert.glsl17
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/frag.glsl193
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/vert.glsl41
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/audio.rs341
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/context.rs302
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/font.rs194
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/framebuffer.rs158
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/helpers.js21
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/js/module.js7
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d.rs1
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d/tiled.rs301
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/lib.rs355
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/mesh.rs197
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net.rs4
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/fig.rs100
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws.rs10
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client.rs9
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/native.rs124
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/wasm.rs73
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/server.rs140
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/physics.rs0
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/renderer.rs337
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/save.rs47
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/scene.rs401
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shader.rs260
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shadow.rs127
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/state.rs735
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/texture.rs156
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/ui.rs169
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/utils.rs147
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/Cargo.toml14
-rw-r--r--2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/src/lib.rs231
52 files changed, 5668 insertions, 0 deletions
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/Cargo.toml b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/Cargo.toml
new file mode 100644
index 0000000..90a0cca
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/Cargo.toml
@@ -0,0 +1,53 @@
+[package]
+name = "teleia"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+strum = { version = "0.27.1", features = ["derive"] } # utility macros for enums
+glow = { version = "=0.13.1", features = [] } # rendering
+tobj = "=4.0.1" # loader for .obj meshes loader
+ahash = "=0.8.10" # dependency of tobj
+gltf = { git = "https://github.com/lcolonq/gltf", features = ["extras", "import", "names", "utils"] } # loader for .gltf scenes
+image = { version = "0.25.10", default-features = false, features = ["jpeg", "png"] } # texture loader
+# cosmic-text = { version = "*", default-features = false, features = ["std", "swash"] } # advanced text rendering
+glam = "0.29.2" # linear algebra
+log = "0.4.21" # logging
+rand = { version = "=0.8.5", features = ["small_rng"] } # rng
+serde = { version = "1.0.228", features = ["derive"] } # serialization
+serde_json = "1.0.128" # serialize JSON
+bincode = { version = "2.0.1", features = ["serde"] } # binary serialization
+enum-map = "2.7.3" # fast maps with enums as keys
+bimap = "0.6.3" # bijective maps
+# reqwest = "*" # http requests
+bytes = "*" # bytes for http responses
+bitflags = { version = "2.6.0", features = ["serde"] } # bitwise flags
+simple-eyre = { version = "0.3.1", default-features = false } # error reporting and formatting
+rapier3d = "0.23.1" # rigid-body physics
+parry3d = "0.18.0" # collision detection
+nalgebra = {version = "0.33.2", features = ["convert-glam029"]} # linear algebra library for rapier3d
+byteorder = "1.5.0" # read numbers in particular endianness
+base64 = "0.22.1" # encode/decode base64
+
+[target.'cfg(target_arch = "wasm32")'.dependencies]
+winit = {version = "=0.29.15", features = ["serde"]} # windowing and events
+getrandom = {version = "*", features = ["js"]} # rng in the browser
+console_log = "*" # log to browser console
+console_error_panic_hook = "*" # log to browser console on panic
+tracing-wasm = "*" # trace performance in browser
+wasm-bindgen = "=0.2.100" # interface with javascript
+wasm-bindgen-futures = "*" # interface with async javascript
+js-sys = "*" # browser APIs to interact with JS runtime (e.g. run WASM)
+web-sys = { version = "*", features = ["Document", "Window", "Element", "HtmlCanvasElement", "WebGl2RenderingContext", "Headers", "Request", "RequestInit", "RequestMode", "Response", "Performance", "PerformanceTiming", "AudioContext", "AudioNode", "GainNode", "AudioParam", "AudioDestinationNode", "AudioBuffer", "AudioBufferSourceNode", "BinaryType", "Blob", "CloseEvent", "ErrorEvent", "FileReader", "MessageEvent", "ProgressEvent", "WebSocket", "Storage"] }
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+env_logger = "0.11.5" # configurable logging to stdout
+glfw = { git = "https://github.com/lcolonq/glfw-rs", features = ["serde"] } # window management
+kira = { version = "=0.9.6", default-features = false, features = ["cpal", "ogg", "mp3", "wav"] } # audio
+directories = { git = "https://github.com/lcolonq/directories-rs" } # standard system directories
+polling = "3.11.0" # interface to epoll
+tungstenite = { version = "0.24.0", features = ["native-tls"] } # websockets \ No newline at end of file
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/build.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/build.rs
new file mode 100644
index 0000000..42743ce
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/build.rs
@@ -0,0 +1,15 @@
+fn main() {
+ /*println!("cargo::rerun-if-changed=../../flake.lock");
+ match &*std::env::var("CARGO_CFG_TARGET_ARCH").unwrap() {
+ "x86_64" => if let Ok(p) = std::env::var("LIBCOLONQ_PIT_NATIVE") {
+ println!("cargo::rustc-link-search={}", p);
+ },
+ "wasm32" => if let Ok(p) = std::env::var("LIBCOLONQ_PIT_WASM") {
+
+ println!("cargo::rustc-link-search={}", p);
+ },
+ _ => eprintln!("warning: building for unknown architecture!"),
+ }
+ println!("cargo::rustc-link-lib=static=colonq-pit");*/
+ eprint!("we are not using pit at all, this is a fork of teleia where scripting is disabled");
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/default.png b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/default.png
new file mode 100644
index 0000000..7b1d2a3
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/default.png
Binary files differ
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/small.png b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/small.png
new file mode 100644
index 0000000..1d020f7
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/fonts/small.png
Binary files differ
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/meshes/square.obj b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/meshes/square.obj
new file mode 100644
index 0000000..7acdef6
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/meshes/square.obj
@@ -0,0 +1,15 @@
+# Blender 3.6.2
+# www.blender.org
+o Cube
+v -1.000000 -1.000000 0.000000
+v 1.000000 -1.000000 0.000000
+v -1.000000 1.000000 0.000000
+v 1.000000 1.000000 0.000000
+vn -0.0000 -0.0000 -1.0000
+vt 0.0 1.0
+vt 1.0 0.0
+vt 0.0 0.0
+vt 1.0 1.0
+s 0
+f 3/1/1 2/2/1 1/3/1
+f 3/1/1 4/4/1 2/2/1
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/frag.glsl
new file mode 100644
index 0000000..fbc8d71
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/frag.glsl
@@ -0,0 +1,15 @@
+#version 300 es
+precision highp float;
+
+uniform sampler2D texture_data;
+
+in vec2 vertex_texcoord;
+in vec3 vertex_color;
+out vec4 frag_color;
+
+void main() {
+ vec4 texel = texture(texture_data, vertex_texcoord);
+ if (texel.rgb == vec3(0.0, 0.0, 0.0)) discard;
+ texel.rgb = vertex_color;
+ frag_color = texel;
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/vert.glsl
new file mode 100644
index 0000000..eee22fc
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/bitmap/vert.glsl
@@ -0,0 +1,20 @@
+#version 300 es
+precision highp float;
+
+in vec3 vertex;
+in vec2 texcoord;
+in vec3 color;
+
+uniform mat4 view;
+uniform mat4 position;
+uniform mat4 projection;
+
+out vec2 vertex_texcoord;
+out vec3 vertex_color;
+
+void main() {
+ vertex_texcoord = texcoord;
+ vertex_color = color;
+ vec3 pos = (position * vec4(vertex, 1.0)).xyz;
+ gl_Position = projection * view * vec4(pos, 1.0);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/frag.glsl
new file mode 100644
index 0000000..fee9b80
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/frag.glsl
@@ -0,0 +1,160 @@
+#version 300 es
+precision highp float;
+
+uniform vec3 camera_pos;
+uniform float time;
+
+uniform vec3 light_ambient_color;
+uniform vec3 light_dir;
+uniform vec3 light_dir_color;
+uniform int light_count;
+uniform vec3 light_pos[5];
+uniform vec3 light_color[5];
+uniform vec2 light_attenuation[5];
+uniform highp sampler2DShadow light_shadowbuffer_dir;
+uniform samplerCube light_shadowbuffer_point[5];
+
+uniform int has_point_shadows;
+
+in vec2 vertex_texcoord;
+in vec3 vertex_normal;
+in vec3 vertex_fragpos;
+in vec4 vertex_fragpos_shadow_dir;
+in vec3 vertex_view_vector;
+
+out vec4 frag_color;
+
+mat3 compute_tbn() {
+ vec3 p = -vertex_view_vector;
+ vec3 normal = normalize(vertex_normal);
+ vec3 dpx = dFdx(p);
+ vec3 dpy = dFdy(p);
+ vec2 duvx = dFdx(vertex_texcoord);
+ vec2 duvy = dFdy(vertex_texcoord);
+ vec3 dpyperp = cross(dpy, normal);
+ vec3 dpxperp = cross(normal, dpx);
+ vec3 tangent = dpyperp * duvx.x + dpxperp * duvy.x;
+ vec3 bitangent = dpyperp * duvx.y + dpxperp * duvy.y;
+ float invmax = inversesqrt(max(dot(bitangent, bitangent), dot(bitangent, bitangent)));
+ return mat3(-tangent * invmax, -bitangent * invmax, normal);
+}
+
+vec4 normal_as_color(vec3 n) {
+ float r = (128.0 + 127.0 * n.r) / 255.0;
+ float g = (128.0 + 127.0 * n.g) / 255.0;
+ float b = (128.0 + 127.0 * n.b) / 255.0;
+ return vec4(r, g, b, 1.0);
+}
+
+vec3 dir_light(vec3 normal) {
+ return max(dot(normal, -normalize(light_dir)), 0.0) * light_dir_color;
+}
+
+float dir_shadow(vec3 normal) {
+ vec3 proj = vertex_fragpos_shadow_dir.xyz / vertex_fragpos_shadow_dir.w;
+ float bias = 0.002;
+ // float current_depth = proj.z;
+ // float bias = max(0.05 * (1.0 - dot(normal, -normalize(light_dir))), 0.005);
+ proj.z -= bias;
+ proj *= 0.5; proj += 0.5;
+ if (proj.z > 1.0) return 0.0;
+ return 1.0 - texture(light_shadowbuffer_dir, proj.xyz);
+}
+
+float point_shadow(vec3 normal, vec3 shadow_vector, float closest_depth) {
+ closest_depth *= 25.0;
+ float current_depth = length(shadow_vector);
+ float bias = max(0.1 * (1.0 - dot(normal, normalize(shadow_vector))), 0.005);
+ bias = min(bias + current_depth * 0.01, 0.2);
+ float shadow = current_depth - bias > closest_depth ? 1.0 : 0.0;
+ return shadow;
+}
+
+vec3 point_light(vec3 normal, const int idx) {
+ vec3 pos = light_pos[idx];
+ vec3 color = light_color[idx];
+ float linear = light_attenuation[idx].x;
+ float quadratic = light_attenuation[idx].y;
+ vec3 light_vector = pos - vertex_fragpos;
+ float distance = length(light_vector);
+ float attenuation = 1.0 / (1.0 + distance * linear + distance * distance * quadratic);
+
+ float directional = max(dot(normal.xyz, normalize(light_vector)), 0.0);
+ vec3 directional_light = color * directional;
+
+ vec3 view_dir = normalize(camera_pos - vertex_fragpos);
+ vec3 reflect_dir = reflect(-normalize(light_vector), normalize(normal.xyz));
+ float specular = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+ vec3 specular_light = 0.5 * specular * color;
+ // return (directional_light + specular_light) * attenuation;
+ return directional_light * attenuation;
+}
+
+vec3 point_light_billboard(const int idx) {
+ vec3 pos = light_pos[idx];
+ vec3 color = light_color[idx];
+ float linear = light_attenuation[idx].x;
+ float quadratic = light_attenuation[idx].y;
+ vec3 light_vector = pos - vertex_fragpos;
+ float distance = length(light_vector);
+ float attenuation = 1.0 / (1.0 + distance * linear + distance * distance * quadratic);
+
+ return color * attenuation;
+}
+
+vec3 compute_lighting(vec3 normal) {
+ vec3 ambient_light = light_ambient_color;
+
+ vec3 from_dir = dir_light(normal) * (1.0 - dir_shadow(normal));
+
+ vec3 shadow_vector[5];
+ for (int i = 0; i < light_count; ++i) {
+ shadow_vector[i] = vertex_fragpos - light_pos[i];
+ shadow_vector[i].x *= -1.0;
+ }
+
+ // can only index array of samplers with a constant, hence the weird setup
+ #define SAMPLE_SHADOW(n) n < light_count ? texture(light_shadowbuffer_point[n], shadow_vector[n]).r : 1.0
+ float shadow_depth[5];
+ shadow_depth[0] = SAMPLE_SHADOW(0);
+ shadow_depth[1] = SAMPLE_SHADOW(1);
+ shadow_depth[2] = SAMPLE_SHADOW(2);
+ shadow_depth[3] = SAMPLE_SHADOW(3);
+ shadow_depth[4] = SAMPLE_SHADOW(4);
+
+ vec3 from_points = vec3(0.0, 0.0, 0.0);
+ for (int i = 0; i < light_count; ++i) {
+ from_points += has_point_shadows != 0
+ ? point_light(normal, i) * (1.0 - point_shadow(normal, shadow_vector[i], shadow_depth[i]))
+ : point_light(normal, i);
+ }
+
+ return (ambient_light + from_dir + from_points);
+}
+
+vec3 compute_lighting_noshadow(vec3 normal) {
+ vec3 ambient_light = light_ambient_color;
+
+ vec3 from_dir = dir_light(normal);
+
+ vec3 from_points = vec3(0.0, 0.0, 0.0);
+ for (int i = 0; i < light_count; ++i) {
+ from_points += point_light(normal, i);
+ }
+
+ return (ambient_light + from_dir + from_points);
+}
+
+vec3 compute_lighting_billboard(vec3 normal) {
+ vec3 ambient_light = light_ambient_color;
+
+ vec3 from_dir = light_dir_color / 2.0;
+
+ vec3 from_points = vec3(0.0, 0.0, 0.0);
+ for (int i = 0; i < light_count; ++i) {
+ from_points += point_light_billboard(i);
+ }
+
+ return (ambient_light + from_dir + from_points);
+}
+#line 0
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/vert.glsl
new file mode 100644
index 0000000..06f7e30
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/common/vert.glsl
@@ -0,0 +1,30 @@
+#version 300 es
+precision highp float;
+
+in vec3 vertex;
+in vec3 normal;
+in vec2 texcoord;
+
+uniform mat4 view;
+uniform mat4 position;
+uniform mat4 projection;
+uniform mat4 normal_matrix;
+uniform mat4 lightspace_matrix;
+uniform vec3 camera_pos;
+
+out vec2 vertex_texcoord;
+out vec3 vertex_normal;
+out vec3 vertex_fragpos;
+out vec4 vertex_fragpos_shadow_dir;
+out vec3 vertex_view_vector;
+
+void default_main()
+{
+ vertex_texcoord = texcoord;
+ vertex_normal = (normal_matrix * vec4(normal, 1.0)).xyz;
+ vec3 pos = (position * vec4(vertex, 1.0)).xyz;
+ vertex_fragpos = pos;
+ vertex_fragpos_shadow_dir = lightspace_matrix * vec4(pos, 1.0);
+ vertex_view_vector = camera_pos - pos;
+ gl_Position = projection * view * vec4(pos, 1.0);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/frag.glsl
new file mode 100644
index 0000000..fa305e6
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/frag.glsl
@@ -0,0 +1,12 @@
+uniform sampler2D texture_data;
+
+uniform float transparency;
+
+void main()
+{
+ float opacity = 1.0 - clamp(transparency, 0.0, 1.0);
+ vec2 tcfull = vec2(vertex_texcoord.x, 1.0 - vertex_texcoord.y);
+ vec4 texel = texture(texture_data, tcfull);
+ texel.a *= opacity;
+ frag_color = texel;
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/vert.glsl
new file mode 100644
index 0000000..6e796cc
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/flat/vert.glsl
@@ -0,0 +1,4 @@
+void main()
+{
+ default_main();
+} \ No newline at end of file
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/frag.glsl
new file mode 100644
index 0000000..257c1b7
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/frag.glsl
@@ -0,0 +1,12 @@
+#version 300 es
+precision highp float;
+
+uniform sampler2D texture_data;
+
+in vec2 vertex_texcoord;
+out vec4 frag_color;
+
+void main() {
+ vec4 texel = texture(texture_data, vertex_texcoord);
+ frag_color = texel;
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/vert.glsl
new file mode 100644
index 0000000..d48f78d
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scale/vert.glsl
@@ -0,0 +1,22 @@
+#version 300 es
+precision highp float;
+
+out vec2 vertex_texcoord;
+
+void main() {
+ const vec2 positions[4] = vec2[](
+ vec2(-1, -1),
+ vec2(+1, -1),
+ vec2(-1, +1),
+ vec2(+1, +1)
+ );
+ const vec2 coords[4] = vec2[](
+ vec2(0, 0),
+ vec2(1, 0),
+ vec2(0, 1),
+ vec2(1, 1)
+ );
+
+ vertex_texcoord = coords[gl_VertexID];
+ gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/frag.glsl
new file mode 100644
index 0000000..554c25d
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/frag.glsl
@@ -0,0 +1,11 @@
+uniform sampler2D texture_data;
+
+void main()
+{
+ vec4 texel = texture(texture_data, vertex_texcoord);
+ if (texel.a != 1.0) {
+ discard;
+ }
+
+ frag_color = vec4(texel.rgb, texel.a);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/vert.glsl
new file mode 100644
index 0000000..c66a816
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/scene/vert.glsl
@@ -0,0 +1,18 @@
+in vec4 joint;
+in vec4 weight;
+
+uniform mat4 joint_matrices[128];
+
+void main()
+{
+ vertex_texcoord = texcoord;
+ vertex_normal = (normal_matrix * vec4(normal, 1.0)).xyz;
+ mat4 skin
+ = weight.x * joint_matrices[int(joint.x)]
+ + weight.y * joint_matrices[int(joint.y)]
+ + weight.z * joint_matrices[int(joint.z)]
+ + weight.w * joint_matrices[int(joint.w)];
+ vec3 pos = (position * skin * vec4(vertex, 1.0)).xyz;
+ vertex_view_vector = camera_pos - pos;
+ gl_Position = projection * view * vec4(pos, 1.0);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/frag.glsl
new file mode 100644
index 0000000..954a9d2
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/frag.glsl
@@ -0,0 +1,23 @@
+// uniform int has_normal_map;
+// uniform sampler2D normal_map;
+
+uniform sampler2D texture_data;
+
+void main()
+{
+ vec2 inverted_texcoord = vec2(vertex_texcoord.x, 1.0 - vertex_texcoord.y);
+ vec4 texel = texture(texture_data, inverted_texcoord);
+ if (texel.a != 1.0) {
+ discard;
+ }
+
+ // mat3 tbn = compute_tbn();
+ // vec3 normal = has_normal_map != 0
+ // ? normalize(tbn * (texture(normal_map, inverted_texcoord).xyz * 2.0 - 1.0))
+ // : normalize(vertex_normal);
+ vec3 normal = normalize(vertex_normal);
+
+ vec3 lighting = compute_lighting_noshadow(normal);
+
+ frag_color = vec4(texel.rgb * lighting, texel.a);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/vert.glsl
new file mode 100644
index 0000000..6e796cc
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/test/vert.glsl
@@ -0,0 +1,4 @@
+void main()
+{
+ default_main();
+} \ No newline at end of file
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/frag.glsl
new file mode 100644
index 0000000..257c1b7
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/frag.glsl
@@ -0,0 +1,12 @@
+#version 300 es
+precision highp float;
+
+uniform sampler2D texture_data;
+
+in vec2 vertex_texcoord;
+out vec4 frag_color;
+
+void main() {
+ vec4 texel = texture(texture_data, vertex_texcoord);
+ frag_color = texel;
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/vert.glsl
new file mode 100644
index 0000000..a7567f4
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/tiled/vert.glsl
@@ -0,0 +1,14 @@
+#version 300 es
+precision highp float;
+
+in vec2 vertex;
+in vec2 texcoord;
+
+uniform mat4 transform;
+
+out vec2 vertex_texcoord;
+
+void main() {
+ vertex_texcoord = texcoord;
+ gl_Position = transform * vec4(vertex, 0.0, 1.0);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/frag.glsl
new file mode 100644
index 0000000..d472c09
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/frag.glsl
@@ -0,0 +1,16 @@
+#version 300 es
+precision highp float;
+
+uniform sampler2D texture_data;
+
+in vec2 vertex_texcoord;
+in vec3 vertex_color;
+out vec4 frag_color;
+
+void main()
+{
+ float val = texture(texture_data, vertex_texcoord).r;
+ if (val == 0.0) discard;
+ vec4 texel = vec4(vertex_color, val);
+ frag_color = texel;
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/vert.glsl
new file mode 100644
index 0000000..a501e0a
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/truetype/vert.glsl
@@ -0,0 +1,17 @@
+#version 300 es
+precision highp float;
+
+in vec2 vertex;
+in vec2 texcoord;
+in vec3 color;
+
+uniform mat4 transform;
+
+out vec2 vertex_texcoord;
+out vec3 vertex_color;
+
+void main() {
+ vertex_texcoord = texcoord;
+ vertex_color = color;
+ gl_Position = transform * vec4(vertex, 0.0, 1.0);
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/frag.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/frag.glsl
new file mode 100644
index 0000000..8e7b752
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/frag.glsl
@@ -0,0 +1,193 @@
+uniform highp int flags;
+
+uniform vec3 camera_pos;
+uniform float time;
+
+uniform vec2 sprite_offset;
+uniform vec2 sprite_dims;
+
+uniform vec4 color;
+uniform sampler2D texture_color;
+uniform sampler2D texture_normal;
+uniform vec3 light_ambient_color;
+uniform vec3 light_dir;
+uniform vec3 light_dir_color;
+uniform int light_count;
+uniform vec3 light_pos[5];
+uniform vec3 light_color[5];
+uniform vec2 light_attenuation[5];
+
+uniform int effect_flip;
+uniform float effect_flash;
+uniform float effect_hueshift;
+uniform float effect_huescale;
+
+in vec2 vertex_texcoord;
+in vec3 vertex_color;
+in vec3 vertex_normal;
+in vec3 vertex_fragpos;
+in vec4 vertex_fragpos_shadow_dir;
+in vec3 vertex_view_vector;
+
+out vec4 frag_color;
+
+bool flag(int mask) {
+ return (flags & mask) != 0;
+}
+
+mat3 compute_tbn() {
+ vec3 p = -vertex_view_vector;
+ vec3 normal = normalize(vertex_normal);
+ vec3 dpx = dFdx(p);
+ vec3 dpy = dFdy(p);
+ vec2 duvx = dFdx(vertex_texcoord);
+ vec2 duvy = dFdy(vertex_texcoord);
+ vec3 dpyperp = cross(dpy, normal);
+ vec3 dpxperp = cross(normal, dpx);
+ vec3 tangent = dpyperp * duvx.x + dpxperp * duvy.x;
+ vec3 bitangent = dpyperp * duvx.y + dpxperp * duvy.y;
+ float invmax = inversesqrt(max(dot(bitangent, bitangent), dot(bitangent, bitangent)));
+ float flip = flag(FLIP_TEXTURE) ? 1.0 : -1.0;
+ return mat3(-tangent * invmax, flip * bitangent * invmax, normal);
+
+}
+
+vec4 normal_as_color(vec3 n) {
+ float r = (128.0 + 127.0 * n.r) / 255.0;
+ float g = (128.0 + 127.0 * n.g) / 255.0;
+ float b = (128.0 + 127.0 * n.b) / 255.0;
+ return vec4(r, g, b, 1.0);
+}
+
+vec3 dir_light(vec3 normal) {
+ return max(dot(normal, -normalize(light_dir)), 0.0) * light_dir_color;
+}
+
+vec3 point_light(vec3 normal, const int idx) {
+ vec3 pos = light_pos[idx];
+ vec3 color = light_color[idx];
+ float linear = light_attenuation[idx].x;
+ float quadratic = light_attenuation[idx].y;
+ vec3 light_vector = pos - vertex_fragpos;
+ float distance = length(light_vector);
+ float attenuation = 1.0 / (1.0 + distance * linear + distance * distance * quadratic);
+
+ float directional = max(dot(normal.xyz, normalize(light_vector)), 0.0);
+ vec3 directional_light = color * directional;
+
+ vec3 view_dir = normalize(camera_pos - vertex_fragpos);
+ vec3 reflect_dir = reflect(-normalize(light_vector), normalize(normal.xyz));
+ float specular = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0);
+ vec3 specular_light = 0.5 * specular * color;
+ // return (directional_light + specular_light) * attenuation;
+ return directional_light * attenuation;
+}
+
+vec3 point_light_billboard(const int idx) {
+ vec3 pos = light_pos[idx];
+ vec3 color = light_color[idx];
+ float linear = light_attenuation[idx].x;
+ float quadratic = light_attenuation[idx].y;
+ vec3 light_vector = pos - vertex_fragpos;
+ float distance = length(light_vector);
+ float attenuation = 1.0 / (1.0 + distance * linear + distance * distance * quadratic);
+
+ return color * attenuation;
+}
+
+vec3 rgb_to_hsl(vec3 rgb) {
+ vec3 ret;
+ float min = min(min(rgb.r, rgb.g), rgb.b);
+ float max = max(max(rgb.r, rgb.g), rgb.b);
+ float lum = (max + min) / 2.0;
+ ret.z = lum;
+ if (max == min) {
+ ret.x = ret.y = 0.0;
+ } else {
+ float chroma = max - min;
+ ret.y = chroma / (1.0 - abs(2.0 * lum - 1.0));
+ if (max == rgb.r) {
+ ret.x = (rgb.g - rgb.b) / chroma + (rgb.g < rgb.b ? 6.0 : 0.0);
+ } else if (max == rgb.g) {
+ ret.x = (rgb.b - rgb.r) / chroma + 2.0;
+ } else {
+ ret.x = (rgb.r - rgb.g) / chroma + 4.0;
+ }
+ ret.x /= 6.0;
+ }
+ return ret;
+}
+
+float hue_to_rgb(float p, float q, float t) {
+ if (t < 0.0) t += 1.0;
+ if (t > 1.0) t -= 1.0;
+ if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
+ if (t < 1.0/2.0) return q;
+ if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
+ return p;
+}
+
+vec3 hsl_to_rgb(vec3 hsl) {
+ vec3 ret;
+ if (hsl.y == 0.0) {
+ ret.r = ret.g = ret.b = hsl.z;
+ } else {
+ float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y;
+ float p = 2.0 * hsl.z - q;
+ ret.r = hue_to_rgb(p, q, hsl.x + 1.0/3.0);
+ ret.g = hue_to_rgb(p, q, hsl.x);
+ ret.b = hue_to_rgb(p, q, hsl.x - 1.0/3.0);
+ }
+ return ret;
+}
+
+void main() {
+ float tcy = flag(FLIP_TEXTURE) ? vertex_texcoord.y : 1.0 - vertex_texcoord.y;
+ vec2 tc = vec2(vertex_texcoord.x, tcy);
+ mat3 tbn = compute_tbn();
+ if (flag(EFFECTS)) {
+ float fbase = float(effect_flip);
+ float fmul = 1.0 - 2.0 * fbase;
+ tc = vec2(fbase + fmul * vertex_texcoord.x, tcy);
+ }
+ if (flag(SPRITE)) {
+ tc *= sprite_dims;
+ tc += sprite_offset;
+ }
+ frag_color = color;
+ if (flag(TEXTURE_COLOR)) {
+ frag_color = texture(texture_color, tc);
+ }
+ if (flag(EFFECTS)) {
+ vec3 hsl = rgb_to_hsl(frag_color.rgb);
+ hsl.x = mod(hsl.x * effect_huescale + effect_hueshift, 1.0);
+ vec3 p = hsl_to_rgb(hsl);
+ frag_color.rgb = vec3(p.r + effect_flash, p.g + effect_flash, p.b + effect_flash);
+ }
+ vec3 normal = vertex_normal;
+ if (flag(TEXTURE_NORMAL)) {
+ normal = normalize(tbn * (texture(texture_normal, tc).xyz * 2.0 - 1.0));
+ }
+ vec3 from_ambient = vec3(1.0, 1.0, 1.0);
+ if (flag(LIGHT_AMBIENT)) {
+ from_ambient = light_ambient_color;
+ }
+ vec3 from_dir = vec3(0.0, 0.0, 0.0);
+ if (flag(LIGHT_DIR)) {
+ from_dir = dir_light(normal);
+ }
+ vec3 from_points = vec3(0.0, 0.0, 0.0);
+ if (flag(LIGHT_POINT)) {
+ for (int i = 0; i < light_count; ++i) {
+ vec3 pl = point_light(normal, i);
+ from_points += pl;
+ }
+ }
+ frag_color.rgb *= (from_ambient + from_dir + from_points);
+ if (frag_color.a == 0.0) {
+ discard;
+ }
+ if (flag(VERTEX_COLOR)) {
+ frag_color.rgb = vertex_color;
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/vert.glsl b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/vert.glsl
new file mode 100644
index 0000000..bd5ac60
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/assets/shaders/uber/vert.glsl
@@ -0,0 +1,41 @@
+uniform highp int flags;
+
+in vec3 vertex;
+in vec3 normal;
+in vec2 texcoord;
+in vec3 color;
+
+uniform mat4 view;
+uniform mat4 position;
+uniform mat4 projection;
+uniform mat4 normal_matrix;
+uniform mat4 lightspace_matrix;
+uniform vec3 camera_pos;
+uniform vec3 offset;
+uniform float yskew;
+
+out vec2 vertex_texcoord;
+out vec3 vertex_color;
+out vec3 vertex_normal;
+out vec3 vertex_fragpos;
+out vec4 vertex_fragpos_shadow_dir;
+out vec3 vertex_view_vector;
+
+bool flag(int mask) {
+ return (flags & mask) != 0;
+}
+
+void main() {
+ vertex_texcoord = texcoord;
+ vertex_color = color;
+ vertex_normal = (normal_matrix * vec4(normal, 1.0)).xyz;
+ vec3 pos = (position * vec4(vertex, 1.0)).xyz;
+ vertex_fragpos = pos;
+ vertex_fragpos_shadow_dir = lightspace_matrix * vec4(pos, 1.0);
+ vertex_view_vector = camera_pos - pos;
+ gl_Position = projection * view * vec4(pos, 1.0);
+ gl_Position.xyz += offset;
+ if (flag(YSKEW)) {
+ gl_Position.y += yskew * pos.z;
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/audio.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/audio.rs
new file mode 100644
index 0000000..431b010
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/audio.rs
@@ -0,0 +1,341 @@
+use std::collections::HashMap;
+
+use std::sync::{Arc, Mutex};
+
+#[cfg(target_arch = "wasm32")]
+pub struct Context {
+ pub audio: web_sys::AudioContext,
+}
+
+#[cfg(target_arch = "wasm32")]
+impl Context {
+ pub fn new() -> Self {
+ let audio = web_sys::AudioContext::new().expect("failed to create audio context");
+ Self { audio }
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+pub struct AudioPlayingHandle {
+ node: web_sys::AudioBufferSourceNode,
+ gain: web_sys::GainNode,
+}
+
+#[cfg(target_arch = "wasm32")]
+impl AudioPlayingHandle {
+ pub fn stop(&self, _ctx: &Context) {
+ self.node.stop().expect("failed to stop audio");
+ }
+ pub fn fade_out(&self, ctx: &Context, time: f32) {
+ self.gain
+ .gain()
+ .set_target_at_time(0.0, ctx.audio.current_time(), time as f64)
+ .expect("failed to fade out audio");
+ self.node
+ .stop_with_when(ctx.audio.current_time() + time as f64 + 1.0)
+ .expect("failed to stop audio while fading out");
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+pub struct Audio {
+ pub buffer: Arc<Mutex<Option<web_sys::AudioBuffer>>>,
+}
+
+#[cfg(target_arch = "wasm32")]
+impl Audio {
+ pub fn new(ctx: &Context, bytes: &[u8]) -> Self {
+ let sbuffer = Arc::new(Mutex::new(None));
+ let sclone = sbuffer.clone();
+ let ret = Audio { buffer: sclone };
+ let jsp = ctx
+ .audio
+ .decode_audio_data(&js_sys::Uint8Array::from(bytes).buffer())
+ .expect("failed to decode audio");
+ let promise = wasm_bindgen_futures::JsFuture::from(jsp);
+ wasm_bindgen_futures::spawn_local(async move {
+ if let Some(data) = promise.await.ok() {
+ *sbuffer.lock().unwrap() = Some(web_sys::AudioBuffer::from(data));
+ }
+ ()
+ });
+ ret
+ }
+
+ pub fn from_samples(ctx: &Context, sample_rate: f32, samples: &[f32]) -> Self {
+ let buf = ctx
+ .audio
+ .create_buffer(1, samples.len() as u32, sample_rate)
+ .expect("failed to create audio buffer");
+ buf.copy_to_channel(samples, 0)
+ .expect("failed to populate audio samples");
+ Audio {
+ buffer: Arc::new(Mutex::new(Some(buf))),
+ }
+ }
+
+ pub fn play(
+ &self,
+ ctx: &mut Context,
+ looping: Option<(Option<f64>, Option<f64>)>,
+ ) -> Option<AudioPlayingHandle> {
+ let source = ctx.audio.create_buffer_source().ok()?;
+ if let Some(ab) = &*self.buffer.lock().unwrap() {
+ source.set_buffer(Some(&ab));
+ } else {
+ return None;
+ };
+ if let Some((ms, me)) = looping {
+ source.set_loop(true);
+ if let Some(s) = ms {
+ source.set_loop_start(s)
+ }
+ if let Some(e) = me {
+ source.set_loop_end(e)
+ }
+ }
+ let gain = ctx.audio.create_gain().ok()?;
+ gain.gain()
+ .set_value_at_time(0.5, ctx.audio.current_time())
+ .ok()?;
+ gain.connect_with_audio_node(&ctx.audio.destination())
+ .ok()?;
+ source.connect_with_audio_node(&gain).ok()?;
+ source.start().ok()?;
+ Some(AudioPlayingHandle { node: source, gain })
+ }
+}
+
+#[cfg(target_arch = "wasm32")]
+pub struct Assets {
+ pub ctx: Context,
+ pub audio: HashMap<String, Audio>,
+ pub music_node: Option<AudioPlayingHandle>,
+}
+
+#[cfg(target_arch = "wasm32")]
+impl Assets {
+ pub fn new<F>(f: F) -> Self
+ where
+ F: Fn(&Context) -> HashMap<String, Audio>,
+ {
+ let ctx = Context::new();
+
+ let audio = f(&ctx);
+
+ Self {
+ ctx,
+ audio,
+ music_node: None,
+ }
+ }
+
+ pub fn play_sfx(&mut self, name: &str) {
+ if let Some(a) = self.audio.get(name) {
+ a.play(&mut self.ctx, None);
+ }
+ }
+
+ pub fn is_music_playing(&self) -> bool {
+ if let Some(ms) = &self.music_node {
+ ms.node.buffer().is_some()
+ } else {
+ false
+ }
+ }
+
+ pub fn play_music(&mut self, name: &str, start: Option<f64>, end: Option<f64>) {
+ if let Some(s) = &self.music_node {
+ let _ = s.stop(&self.ctx);
+ }
+ if let Some(a) = self.audio.get(name) {
+ self.music_node = a.play(&mut self.ctx, Some((start, end)));
+ }
+ }
+
+ pub fn stop_music(&mut self) {
+ if let Some(node) = &self.music_node {
+ let _ = node.stop(&self.ctx);
+ }
+ self.music_node = None;
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub struct Context {
+ manager: kira::manager::AudioManager,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl Context {
+ #[allow(clippy::new_without_default)]
+ pub fn new() -> Self {
+ Self {
+ manager: kira::manager::AudioManager::new(
+ kira::manager::AudioManagerSettings::default(),
+ )
+ .expect("failed to create audio manager"),
+ }
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub struct AudioPlayingHandle {
+ handle: Arc<Mutex<kira::sound::static_sound::StaticSoundHandle>>,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl AudioPlayingHandle {
+ pub fn stop(&self, _ctx: &Context) {
+ self.handle
+ .lock()
+ .unwrap()
+ .stop(kira::tween::Tween::default());
+ }
+ pub fn fade_out(&self, _ctx: &Context, time: f32) {
+ self.handle.lock().unwrap().stop(kira::tween::Tween {
+ duration: std::time::Duration::from_secs_f32(time + 0.5),
+ ..Default::default()
+ });
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub struct Audio {
+ data: kira::sound::static_sound::StaticSoundData,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl Audio {
+ pub fn new(_ctx: &Context, bytes: &[u8]) -> Self {
+ Self {
+ data: kira::sound::static_sound::StaticSoundData::from_cursor(std::io::Cursor::new(
+ bytes.to_owned(),
+ ))
+ .expect("failed to decode audio"),
+ }
+ }
+
+ pub fn from_samples(_ctx: &Context, sample_rate: f32, samples: &[f32]) -> Self {
+ let frames: Vec<kira::Frame> = samples
+ .iter()
+ .map(|f| kira::Frame {
+ left: *f,
+ right: *f,
+ })
+ .collect();
+ Self {
+ data: kira::sound::static_sound::StaticSoundData {
+ sample_rate: sample_rate as u32,
+ frames: frames.into(),
+ settings: kira::sound::static_sound::StaticSoundSettings::default(),
+ slice: None,
+ },
+ }
+ }
+
+ pub fn play(
+ &self,
+ ctx: &mut Context,
+ looping: Option<(Option<f64>, Option<f64>)>,
+ ) -> Option<AudioPlayingHandle> {
+ let sd = if let Some((ss, se)) = looping {
+ let start = ss.unwrap_or(0.0);
+ if let Some(e) = se {
+ self.data.loop_region(start..e)
+ } else {
+ self.data.loop_region(start..)
+ }
+ } else {
+ self.data.clone()
+ };
+ ctx.manager.play(sd).ok().map(|h| AudioPlayingHandle {
+ handle: Arc::new(Mutex::new(h)),
+ })
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub struct Assets {
+ pub ctx: Context,
+ pub audio: HashMap<String, Audio>,
+ pub music_handle: Option<AudioPlayingHandle>,
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl Assets {
+ pub fn new<F>(f: F) -> Self
+ where
+ F: Fn(&Context) -> HashMap<String, Audio>,
+ {
+ let ctx = Context::new();
+ let audio = f(&ctx);
+ Self {
+ ctx,
+ audio,
+ music_handle: None,
+ }
+ }
+
+ pub fn play_sfx(&mut self, name: &str) {
+ if let Some(a) = self.audio.get(name) {
+ if a.play(&mut self.ctx, None).is_none() {
+ log::warn!("failed to play sound {}", name);
+ }
+ }
+ }
+
+ pub fn is_music_playing(&self) -> bool {
+ if let Some(mh) = &self.music_handle {
+ mh.handle.lock().unwrap().state() == kira::sound::PlaybackState::Playing
+ } else {
+ false
+ }
+ }
+
+ pub fn play_music(&mut self, name: &str, start: Option<f64>, end: Option<f64>) {
+ if let Some(s) = &mut self.music_handle {
+ s.stop(&self.ctx);
+ }
+ if let Some(a) = self.audio.get(name) {
+ match a.play(&mut self.ctx, Some((start, end))) {
+ Some(h) => {
+ self.music_handle = Some(h);
+ }
+ _ => {
+ log::warn!("failed to play music {}", name);
+ }
+ }
+ }
+ }
+
+ pub fn stop_music(&mut self) {
+ if let Some(h) = &mut self.music_handle {
+ h.stop(&self.ctx);
+ }
+ self.music_handle = None;
+ }
+}
+
+pub trait AudioPlayback {
+ fn play_sfx(&mut self, name: &str);
+ fn play_music(&mut self, name: &str, start: Option<f64>, end: Option<f64>);
+ fn stop_music(&mut self);
+}
+impl AudioPlayback for Option<Assets> {
+ fn play_sfx(&mut self, name: &str) {
+ if let Some(a) = self {
+ a.play_sfx(name);
+ }
+ }
+ fn play_music(&mut self, name: &str, start: Option<f64>, end: Option<f64>) {
+ if let Some(a) = self {
+ a.play_music(name, start, end);
+ }
+ }
+ fn stop_music(&mut self) {
+ if let Some(a) = self {
+ a.stop_music();
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/context.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/context.rs
new file mode 100644
index 0000000..34e0ad3
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/context.rs
@@ -0,0 +1,302 @@
+use crate::Options;
+
+use glow::HasContext;
+
+#[cfg(not(target_arch = "wasm32"))]
+use glfw::Context as _;
+
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::prelude::*;
+
+#[cfg(target_arch = "wasm32")]
+use winit::platform::web::WindowExtWebSys;
+
+#[cfg(target_arch = "wasm32")]
+#[wasm_bindgen(module = "/src/helpers.js")]
+extern {
+ fn js_track_resized_setup();
+ fn js_poll_resized() -> bool;
+ fn js_bundt_api_server() -> String;
+ fn js_bundt_secure_api_server() -> String;
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub struct Context {
+ pub render_width: f32,
+ pub render_height: f32,
+ pub options: Options,
+ pub glfw: std::cell::RefCell<glfw::Glfw>,
+ pub window: std::cell::RefCell<glfw::PWindow>,
+ pub gl: glow::Context,
+ pub emptyvao: glow::VertexArray,
+ pub start_instant: std::time::Instant,
+}
+
+
+#[cfg(target_arch = "wasm32")]
+pub struct Context {
+ pub render_width: f32,
+ pub render_height: f32,
+ pub options: Options,
+ pub window: winit::window::Window,
+ pub gl: glow::Context,
+ pub emptyvao: glow::VertexArray,
+ pub performance: web_sys::Performance,
+}
+
+impl Context {
+ pub fn compute_upscale(&self, windoww: u32, windowh: u32) -> u32 {
+ let mut ratio = 1;
+ loop {
+ if (self.render_width as u32) * ratio > windoww
+ || (self.render_height as u32) * ratio > windowh
+ {
+ break;
+ }
+ ratio += 1;
+ }
+ (ratio - 1).max(1)
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn new(glfw: std::cell::RefCell<glfw::Glfw>, window: std::cell::RefCell<glfw::PWindow>, gl: glow::Context, render_width: f32, render_height: f32, options: Options) -> Self {
+ let emptyvao = unsafe {
+ gl.create_vertex_array().expect("failed to initialize vao")
+ };
+ let ret = Self {
+ render_width, render_height,
+ options,
+ glfw, window,
+ gl,
+ emptyvao,
+ start_instant: std::time::Instant::now(),
+ };
+ ret.init();
+ ret
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ pub fn new(window: winit::window::Window, gl: glow::Context, render_width: f32, render_height: f32, options: Options) -> Self {
+ let emptyvao = unsafe {
+ gl.create_vertex_array().expect("failed to initialize vao")
+ };
+
+ #[cfg(target_arch = "wasm32")]
+ js_track_resized_setup();
+
+ let ret = Self {
+ render_width, render_height,
+ options,
+ window,
+ gl,
+ emptyvao,
+
+ #[cfg(target_arch = "wasm32")]
+ performance: web_sys::window().expect("failed to find window")
+ .performance().expect("failed to get performance"),
+ };
+ ret.init();
+ ret
+ }
+
+ pub fn init(&self) {
+ unsafe {
+ self.gl.clear_color(0.1, 0.1, 0.1, 1.0);
+ self.gl.clear_depth_f32(1.0);
+
+ #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
+ self.gl.enable(glow::DEBUG_OUTPUT);
+
+ self.gl.enable(glow::DEPTH_TEST);
+ self.gl.depth_func(glow::LEQUAL);
+
+ self.gl.enable(glow::BLEND);
+ self.gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
+
+ self.gl.enable(glow::STENCIL_TEST);
+
+ self.gl.cull_face(glow::FRONT);
+ }
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ pub fn maximize_canvas(&self) {
+ if !self.options.contains(Options::NORESIZE) {
+ web_sys::window()
+ .and_then(|win| win.document())
+ .and_then(|doc| {
+ let inner_size = {
+ let browser_window = doc.default_view()
+ .or_else(web_sys::window)
+ .unwrap();
+ winit::dpi::PhysicalSize::new(
+ browser_window.inner_width().unwrap().as_f64().unwrap(),
+ browser_window.inner_height().unwrap().as_f64().unwrap(),
+ )
+ };
+ self.window.canvas().unwrap().set_width(inner_size.width as _);
+ self.window.canvas().unwrap().set_height(inner_size.height as _);
+ let _ = self.window.request_inner_size(inner_size);
+ Some(())
+ })
+ .expect("failed to resize canvas");
+ }
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ pub fn resize_necessary(&self) -> bool { js_poll_resized() }
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn resize_necessary(&self) -> bool { false }
+
+ #[cfg(target_arch = "wasm32")]
+ pub fn bundt_api_server(&self) -> String { js_bundt_api_server() }
+ #[cfg(all(not(target_arch = "wasm32"), debug_assertions))]
+ pub fn bundt_api_server(&self) -> String { "http://localhost:8000/api".to_owned() }
+ #[cfg(all(not(target_arch = "wasm32"), not(debug_assertions)))]
+ pub fn bundt_api_server(&self) -> String { "https://api.colonq.computer/api".to_owned() }
+
+ #[cfg(target_arch = "wasm32")]
+ pub fn bundt_secure_api_server(&self) -> String { js_bundt_secure_api_server() }
+ #[cfg(all(not(target_arch = "wasm32"), debug_assertions))]
+ pub fn bundt_secure_api_server(&self) -> String { "http://localhost:8000/api".to_owned() }
+ #[cfg(all(not(target_arch = "wasm32"), not(debug_assertions)))]
+ pub fn bundt_secure_api_server(&self) -> String { "https://secure.colonq.computer/api".to_owned() }
+
+ pub fn clear_color(&self, color: glam::Vec4) {
+ unsafe {
+ self.gl.clear_color(color.x, color.y, color.z, color.w);
+ }
+ }
+
+ pub fn clear_depth(&self) {
+ unsafe {
+ self.gl.clear(glow::DEPTH_BUFFER_BIT);
+ }
+ }
+
+ pub fn clear(&self) {
+ unsafe {
+ self.gl.stencil_mask(0xff);
+ self.gl.clear(glow::COLOR_BUFFER_BIT | glow::DEPTH_BUFFER_BIT | glow::STENCIL_BUFFER_BIT);
+ }
+ }
+
+ pub fn depth_mask(&self, m: bool) {
+ unsafe {
+ self.gl.depth_mask(m);
+ }
+ }
+
+ pub fn begin_stencil(&self) {
+ unsafe {
+ self.gl.stencil_func(glow::ALWAYS, 1, 0xff);
+ self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::REPLACE);
+ }
+ }
+
+ pub fn use_stencil(&self) {
+ unsafe {
+ self.gl.stencil_func(glow::EQUAL, 1, 0xff);
+ self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
+ }
+ }
+
+ pub fn write_and_use_stencil(&self) {
+ unsafe {
+ self.gl.stencil_func(glow::EQUAL, 1, 0xff);
+ self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::REPLACE);
+ }
+ }
+
+ pub fn use_inverse_stencil(&self) {
+ unsafe {
+ self.gl.stencil_func(glow::NOTEQUAL, 1, 0xff);
+ self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
+ }
+ }
+
+ pub fn write_and_use_inverse_stencil(&self) {
+ unsafe {
+ self.gl.stencil_func(glow::NOTEQUAL, 1, 0xff);
+ self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::REPLACE);
+ }
+ }
+
+ pub fn end_stencil(&self) {
+ unsafe {
+ self.gl.stencil_func(glow::ALWAYS, 1, 0xff);
+ self.gl.stencil_op(glow::KEEP, glow::KEEP, glow::KEEP);
+ }
+ }
+
+ pub fn render_no_geometry(&self) {
+ unsafe {
+ self.gl.bind_vertex_array(Some(self.emptyvao));
+ self.gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4);
+ }
+ }
+
+ pub fn reset_blend(&self) {
+ unsafe {
+ self.gl.blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);
+ }
+ }
+
+ pub fn inverse_blend(&self) {
+ unsafe {
+ self.gl.blend_func(
+ glow::ONE_MINUS_DST_COLOR,
+ glow::ZERO,
+ );
+ }
+ }
+
+ pub fn flip_colors_blend(&self) {
+ unsafe {
+ self.gl.blend_func_separate(
+ glow::ONE_MINUS_DST_COLOR,
+ glow::ZERO,
+ glow::ZERO,
+ glow::ONE,
+ );
+ }
+ }
+
+ pub fn enable_culling(&self) {
+ unsafe {
+ self.gl.enable(glow::CULL_FACE);
+ }
+ }
+
+ pub fn disable_culling(&self) {
+ unsafe {
+ self.gl.disable(glow::CULL_FACE);
+ }
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ pub fn lock_mouse(&self) {
+ let _ = self.window.set_cursor_grab(winit::window::CursorGrabMode::Locked);
+ self.window.set_cursor_visible(false);
+ }
+
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn lock_mouse(&self) {
+ unsafe {
+ glfw::ffi::glfwSetInputMode(
+ self.window.borrow_mut().window_ptr(),
+ glfw::ffi::CURSOR,
+ glfw::ffi::CURSOR_DISABLED,
+ );
+ }
+ }
+
+ pub fn check_error(&self) {
+ unsafe {
+ let err = self.gl.get_error();
+ if err != 0 {
+ log::warn!("gl error: {}", err);
+ }
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/font.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/font.rs
new file mode 100644
index 0000000..648bb47
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/font.rs
@@ -0,0 +1,194 @@
+use crate::{context, mesh, state, texture};
+use glow::HasContext;
+
+pub struct BitmapParams<'color> {
+ pub color: &'color [glam::Vec3],
+ pub scale: glam::Vec2,
+ pub offset: glam::Vec2,
+}
+impl<'color> Default for BitmapParams<'color> {
+ fn default() -> Self {
+ Self {
+ color: &[],
+ scale: glam::Vec2::ONE,
+ offset: glam::Vec2::ZERO,
+ }
+ }
+}
+pub struct Bitmap {
+ pub char_width: i32,
+ pub char_height: i32,
+ pub font_width: i32,
+ pub font_height: i32,
+ pub font: texture::Texture,
+ pub vao: glow::VertexArray,
+ pub vertex_buf: glow::Buffer,
+ pub texcoords_buf: glow::Buffer,
+ pub colors_buf: glow::Buffer,
+ pub index_buf: glow::Buffer,
+}
+
+impl Bitmap {
+ pub fn from_image(
+ ctx: &context::Context,
+ char_width: i32, char_height: i32,
+ font_width: i32, font_height: i32,
+ data: &[u8],
+ ) -> Self {
+ let font = texture::Texture::new(ctx, data);
+ unsafe {
+ let vao = ctx.gl.create_vertex_array().expect("failed to initialize vao");
+ ctx.gl.bind_vertex_array(Some(vao));
+ let vertex_buf = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buf));
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_VERTEX, 2, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_VERTEX);
+ let texcoords_buf = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(texcoords_buf));
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_TEXCOORD, 2, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_TEXCOORD);
+ let colors_buf = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(colors_buf));
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_COLOR, 3, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_COLOR);
+ let index_buf = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(index_buf));
+ Self {
+ char_width, char_height,
+ font_width, font_height,
+ font,
+ vao,
+ vertex_buf,
+ texcoords_buf,
+ colors_buf,
+ index_buf,
+ }
+ }
+ }
+
+ pub fn default(ctx: &context::Context) -> Self {
+ Self::from_image(ctx, 7, 9, 112, 54, include_bytes!("assets/fonts/default.png"))
+ }
+
+ pub fn small(ctx: &context::Context) -> Self {
+ Self::from_image(ctx, 6, 7, 96, 42, include_bytes!("assets/fonts/small.png"))
+ }
+
+ pub fn render_text_parameterized(&self,
+ ctx: &context::Context, _st: &state::State,
+ text: &str,
+ params: BitmapParams,
+ ) {
+ let mut cur = params.offset;
+ let mut vertices = Vec::new();
+ let mut texcoords = Vec::new();
+ let mut colors = Vec::new();
+ let mut indices = Vec::new();
+ let cwidth = self.char_width as f32 / self.font_width as f32;
+ let cheight = self.char_height as f32 / self.font_height as f32;
+ let sdims = glam::Vec2::new(2.0, 2.0) * params.scale;
+ let row_len = self.font_width as u32 / self.char_width as u32;
+ for (i, c) in text.chars().enumerate() {
+ if c == '\n' {
+ cur.x = 0.0;
+ cur.y -= sdims.y;
+ } else {
+ let idx = vertices.len() as u32;
+ vertices.push(cur);
+ vertices.push(cur + glam::Vec2::new(sdims.x, 0.0));
+ vertices.push(cur + glam::Vec2::new(sdims.x, sdims.y));
+ vertices.push(cur + glam::Vec2::new(0.0, sdims.y));
+ let cidx = c as u32 - ' ' as u32;
+ let col = cidx % row_len;
+ let row = cidx / row_len;
+ let tcbase = glam::Vec2::new(col as f32 * cwidth, row as f32 * cheight);
+ texcoords.push(tcbase + glam::Vec2::new(0.0, cheight));
+ texcoords.push(tcbase + glam::Vec2::new(cwidth, cheight));
+ texcoords.push(tcbase + glam::Vec2::new(cwidth, 0.0));
+ texcoords.push(tcbase);
+ let c = if let Some(c) = params.color.get(if params.color.is_empty() { 0 } else { i % params.color.len() }) {
+ *c
+ } else {
+ glam::Vec3::new(1.0, 1.0, 1.0)
+ };
+ colors.push(c); colors.push(c); colors.push(c); colors.push(c);
+ indices.push(idx); indices.push(idx + 1); indices.push(idx + 2);
+ indices.push(idx); indices.push(idx + 3); indices.push(idx + 2);
+ cur.x += sdims.x;
+ }
+ }
+ let index_bytes: Vec<u8> = indices.iter().flat_map(|x| x.to_ne_bytes()).collect();
+ self.font.bind(ctx);
+ unsafe {
+ ctx.gl.bind_vertex_array(Some(self.vao));
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ vertices.as_ptr() as _,
+ vertices.len() * std::mem::size_of::<f32>() * 2,
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.texcoords_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ texcoords.as_ptr() as _,
+ texcoords.len() * std::mem::size_of::<f32>() * 2,
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.colors_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ colors.as_ptr() as _,
+ colors.len() * std::mem::size_of::<f32>() * 3,
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.index_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ELEMENT_ARRAY_BUFFER,
+ &index_bytes,
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.draw_elements(glow::TRIANGLES, indices.len() as _, glow::UNSIGNED_INT, 0);
+ }
+ }
+
+ pub fn render_text_helper(&self,
+ ctx: &context::Context, st: &state::State,
+ text: &str, color: &[glam::Vec3]
+ ) {
+ self.render_text_parameterized(
+ ctx, st,
+ text,
+ BitmapParams {
+ color,
+ scale: glam::Vec2::ONE,
+ offset: glam::Vec2::ZERO,
+ }
+ )
+ }
+
+ pub fn render_text(&self,
+ ctx: &context::Context, st: &state::State,
+ text: &str
+ ) {
+ self.render_text_helper(ctx, st, text, &[]);
+ }
+
+ pub fn render_text_at(&self,
+ ctx: &context::Context, st: &mut state::State,
+ pos: glam::Vec2, text: &str,
+ params: BitmapParams,
+ ) {
+ st.bind_2d(ctx, &st.shader_text_bitmap);
+ let dims = glam::Vec2::new(self.char_width as f32, self.char_height as f32);
+ let fpos = pos + glam::Vec2::new(-dims.x / 2.0, dims.y / 2.0);
+ st.shader_text_bitmap.set_position_text_bitmap(ctx, st, self, &fpos);
+ self.render_text_parameterized(ctx, st, text, params);
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/framebuffer.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/framebuffer.rs
new file mode 100644
index 0000000..606fd6a
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/framebuffer.rs
@@ -0,0 +1,158 @@
+use glow::HasContext;
+
+use crate::context;
+
+#[cfg(target_arch = "wasm32")]
+use crate::Options;
+
+pub struct Framebuffer {
+ pub tex: Option<glow::Texture>,
+ pub fbo: Option<glow::Framebuffer>,
+ pub dims: glam::Vec2,
+ pub offsets: glam::Vec2,
+}
+
+impl Framebuffer {
+ pub fn screen(ctx: &context::Context) -> Self {
+ #[cfg(target_arch = "wasm32")]
+ let (windoww, windowh): (f32, f32) = if !ctx.options.contains(Options::NORESIZE) {
+ ctx.window.inner_size().into()
+ } else {
+ (ctx.render_width, ctx.render_height)
+ };
+ #[cfg(not(target_arch = "wasm32"))]
+ let (windoww, windowh) = {
+ let (w, h) = ctx.window.borrow().get_size();
+ (w as f32, h as f32)
+ };
+ let ratio = ctx.compute_upscale(windoww as _, windowh as _) as f32;
+ let upscalew = ctx.render_width * ratio;
+ let upscaleh = ctx.render_height * ratio;
+ let offsetx = (windoww - upscalew) / 2.0;
+ let offsety = (windowh - upscaleh) / 2.0;
+ log::info!("resize window: {:?}, upscale: {:?}, offset: {:?}", (windoww, windowh), (upscalew, upscaleh), (offsetx, offsety));
+ Self {
+ tex: None,
+ fbo: None,
+ dims: glam::Vec2::new(upscalew, upscaleh),
+ offsets: glam::Vec2::new(offsetx, offsety),
+ }
+ }
+
+ pub fn new(ctx: &context::Context, dims: &glam::Vec2, offsets: &glam::Vec2) -> Self {
+ unsafe {
+ let fbo = ctx.gl.create_framebuffer()
+ .expect("failed to create framebuffer");
+ ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
+
+ let depth_buffer = ctx.gl.create_renderbuffer()
+ .expect("failed to create depth buffer");
+ ctx.gl.bind_renderbuffer(glow::RENDERBUFFER, Some(depth_buffer));
+ ctx.gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_COMPONENT32F, dims.x as _, dims.y as _);
+ ctx.gl.framebuffer_renderbuffer(glow::FRAMEBUFFER, glow::DEPTH_ATTACHMENT, glow::RENDERBUFFER, Some(depth_buffer));
+
+ let stencil_buffer = ctx.gl.create_renderbuffer()
+ .expect("failed to create stencil buffer");
+ ctx.gl.bind_renderbuffer(glow::RENDERBUFFER, Some(stencil_buffer));
+ ctx.gl.renderbuffer_storage(glow::RENDERBUFFER, glow::DEPTH_STENCIL, dims.x as _, dims.y as _);
+ ctx.gl.framebuffer_renderbuffer(glow::FRAMEBUFFER, glow::DEPTH_STENCIL_ATTACHMENT, glow::RENDERBUFFER, Some(stencil_buffer));
+
+ let tex = ctx.gl.create_texture()
+ .expect("failed to create framebuffer texture");
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex));
+ ctx.gl.tex_image_2d(
+ glow::TEXTURE_2D,
+ 0,
+ glow::RGBA as _,
+ dims.x as _,
+ dims.y as _,
+ 0,
+ glow::RGBA,
+ glow::UNSIGNED_BYTE,
+ None,
+ );
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as _);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as _);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as _);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as _);
+ ctx.gl.framebuffer_texture_2d(glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some(tex), 0);
+ ctx.gl.draw_buffer(glow::COLOR_ATTACHMENT0);
+
+ let status = ctx.gl.check_framebuffer_status(glow::FRAMEBUFFER);
+ if status != glow::FRAMEBUFFER_COMPLETE {
+ panic!("error initializing framebuffer:\n{}", status);
+ }
+
+ Self {
+ tex: Some(tex),
+ fbo: Some(fbo),
+ dims: *dims,
+ offsets: *offsets,
+ }
+ }
+ }
+
+ pub fn bind_texture(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.active_texture(glow::TEXTURE0);
+ ctx.gl.bind_texture(glow::TEXTURE_2D, self.tex);
+ }
+ }
+
+ pub fn bind(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, self.fbo);
+ ctx.gl.viewport(
+ self.offsets.x as _,
+ self.offsets.y as _,
+ self.dims.x as _,
+ self.dims.y as _,
+ );
+ }
+ }
+
+ pub fn blit(&self, ctx: &context::Context, dest: &Self, pos: &glam::Vec2, scale: &glam::Vec2) {
+ unsafe {
+ ctx.gl.bind_framebuffer(glow::READ_FRAMEBUFFER, self.fbo);
+ ctx.gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, dest.fbo);
+ ctx.gl.blit_framebuffer(
+ 0, 0, self.dims.x as _, self.dims.y as _,
+ pos.x as _, pos.y as _, (pos.x + scale.x) as _, (pos.y + scale.y) as _,
+ glow::COLOR_BUFFER_BIT, glow::NEAREST
+ );
+ }
+ }
+
+ pub fn get_pixels(&self, ctx: &context::Context, buf: &mut [glam::Vec3]) {
+ let w = self.dims.x as usize;
+ let h = self.dims.y as usize;
+ let tmp = vec![glam::Vec3::default(); w * h];
+ unsafe {
+ ctx.gl.bind_texture(glow::TEXTURE_2D, self.tex);
+ ctx.gl.get_tex_image(
+ glow::TEXTURE_2D, 0, glow::RGB, glow::FLOAT,
+ glow::PixelPackData::Slice(
+ std::slice::from_raw_parts_mut(
+ tmp.as_ptr() as *mut u8,
+ tmp.len() * std::mem::size_of::<glam::Vec3>()
+ )
+ ),
+ );
+ }
+ for y in 0..h {
+ for x in 0..w {
+ buf[x + (h - 1 - y) * w] = tmp[x + y * w];
+ }
+ }
+ }
+
+ pub fn get_pixels_raw(&self, ctx: &context::Context, buf: &mut [u8]) {
+ unsafe {
+ ctx.gl.bind_texture(glow::TEXTURE_2D, self.tex);
+ ctx.gl.get_tex_image(
+ glow::TEXTURE_2D, 0, glow::RGBA, glow::UNSIGNED_BYTE,
+ glow::PixelPackData::Slice(buf),
+ );
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/helpers.js b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/helpers.js
new file mode 100644
index 0000000..d1b74b6
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/helpers.js
@@ -0,0 +1,21 @@
+let resized = false;
+
+export async function js_track_resized_setup() {
+ window.addEventListener("resize", () => {
+ resized = true;
+ });
+}
+
+export function js_poll_resized() {
+ let ret = resized;
+ resized = false;
+ return ret;
+}
+
+export function js_bundt_api_server() {
+ return globalThis.apiServer || "http://localhost:8000/api";
+}
+
+export function js_bundt_secure_api_server() {
+ return globalThis.secureApiServer || "http://localhost:8000/api";
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/js/module.js b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/js/module.js
new file mode 100644
index 0000000..03d0047
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/js/module.js
@@ -0,0 +1,7 @@
+export function js_build_interface() {
+ return {
+ env: {
+ log_info: window.wasmBindings.log_info,
+ },
+ };
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d.rs
new file mode 100644
index 0000000..b64213a
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d.rs
@@ -0,0 +1 @@
+pub mod tiled;
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d/tiled.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d/tiled.rs
new file mode 100644
index 0000000..2e8141e
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/level2d/tiled.rs
@@ -0,0 +1,301 @@
+#![allow(dead_code)]
+
+use std::collections::HashMap;
+use serde::Deserialize;
+use glow::HasContext;
+
+use crate::{context, erm, mesh, shader, texture, Erm};
+
+#[derive(Debug)]
+pub enum Err {
+ LayerIndexOutOfBounds,
+ LayerDataTooSmall,
+ GIDNotFound(u32),
+ AssetNotFound(String),
+ GL(String),
+}
+impl std::error::Error for Err {}
+impl std::fmt::Display for Err {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::LayerIndexOutOfBounds => write!(f, "layer index out of bounds"),
+ Self::LayerDataTooSmall => write!(f, "layer data too small for dimensions"),
+ Self::GIDNotFound(gid) => write!(f, "GID not found: {}", gid),
+ Self::AssetNotFound(ass) => write!(f, "asset not found: {}", ass),
+ Self::GL(msg) => write!(f, "GL error: {msg:}"),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub enum LayerType {
+ #[serde(rename = "tilelayer")]
+ Tile,
+ #[serde(rename = "imagelayer")]
+ Image,
+ #[serde(rename = "objectgroup")]
+ ObjectGroup,
+ #[serde(rename = "group")]
+ Group,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Layer {
+ name: String,
+ id: i32,
+ #[serde(rename = "type")] ty: LayerType,
+ width: i32, height: i32,
+ x: i32, y: i32,
+ opacity: f32,
+ visible: bool,
+ data: Vec<u32>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct LevelTileset {
+ firstgid: i32,
+ source: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Level {
+ width: i32, height: i32,
+ tilewidth: i32, tileheight: i32,
+ layers: Vec<Layer>,
+ tilesets: Vec<LevelTileset>,
+}
+impl Level {
+ pub fn new(bytes: &str) -> Erm<Self> {
+ Ok(serde_json::from_str(bytes)?)
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Tileset {
+ name: String,
+ imagewidth: i32, imageheight: i32,
+ tilewidth: i32, tileheight: i32,
+ margin: i32, spacing: i32,
+}
+impl Tileset {
+ pub fn new(bytes: &str) -> Erm<Self> {
+ Ok(serde_json::from_str(bytes)?)
+ }
+}
+
+// TODO
+pub enum Flip {
+ None,
+}
+
+pub struct Asset {
+ tileset: Tileset,
+ texture: texture::Texture,
+}
+#[derive(Default)]
+pub struct Assets {
+ entries: HashMap<String, Asset>,
+}
+impl Assets {
+ pub fn new() -> Self { Self::default() }
+ pub fn load(&mut self, ctx: &context::Context, nm: &str, ts: &str, img: &[u8]) -> Erm<()> {
+ let ass = Asset {
+ tileset: Tileset::new(ts)?,
+ texture: texture::Texture::new(ctx, img),
+ };
+ if self.entries.insert(nm.to_string(), ass).is_some() {
+ log::warn!("duplicate tileset entry named: {}", nm);
+ }
+ Ok(())
+ }
+ pub fn lookup_gid(&self, level: &Level, gid: u32) -> Erm<(i32, &Asset, Flip)> {
+ let offset = (gid & 0x0fffffff) as i32;
+ for lts in level.tilesets.iter().rev() {
+ if lts.firstgid <= offset {
+ return Ok((
+ offset - lts.firstgid,
+ self.entries.get(&lts.source).ok_or(Err::AssetNotFound(lts.source.clone()))?,
+ Flip::None
+ ))
+ }
+ }
+ erm(Err::GIDNotFound(gid))
+ }
+}
+
+pub struct LayerRenderer {
+ pub vao: glow::VertexArray,
+ pub vertex_buf: glow::Buffer,
+ pub texcoords_buf: glow::Buffer,
+ pub index_buf: glow::Buffer,
+ pub index_count: usize,
+}
+impl LayerRenderer {
+ pub fn new(ctx: &context::Context) -> Erm<Self> {
+ unsafe {
+ let vao = ctx.gl.create_vertex_array().map_err(Err::GL)?;
+ ctx.gl.bind_vertex_array(Some(vao));
+ let vertex_buf = ctx.gl.create_buffer().map_err(Err::GL)?;
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buf));
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_VERTEX, 2, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_VERTEX);
+ let texcoords_buf = ctx.gl.create_buffer().map_err(Err::GL)?;
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(texcoords_buf));
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_TEXCOORD, 2, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_TEXCOORD);
+ let index_buf = ctx.gl.create_buffer().map_err(Err::GL)?;
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(index_buf));
+ Ok(Self {
+ vao,
+ vertex_buf,
+ texcoords_buf,
+ index_buf,
+ index_count: 0,
+ })
+ }
+ }
+}
+pub struct LevelRenderer {
+ pub layers: Vec<LayerRenderer>,
+ pub shader: shader::Shader,
+}
+impl LevelRenderer {
+ pub fn new(ctx: &context::Context, level: &Level) -> Erm<Self> {
+ let mut layers = Vec::new();
+ for _ in level.layers.iter() {
+ layers.push(LayerRenderer::new(ctx)?);
+ }
+ let shader = shader::Shader::new_nolib(
+ ctx,
+ include_str!("../assets/shaders/tiled/vert.glsl"),
+ include_str!("../assets/shaders/tiled/frag.glsl"),
+ );
+ Ok(Self {
+ layers,
+ shader,
+ })
+ }
+ pub fn populate_layer(
+ &mut self,
+ ctx: &context::Context,
+ assets: &Assets,
+ level: &Level,
+ lidx: usize
+ ) -> Erm<()> {
+ let lr = self.layers.get_mut(lidx).ok_or(Err::LayerIndexOutOfBounds)?;
+ let layer = level.layers.get(lidx).ok_or(Err::LayerIndexOutOfBounds)?;
+ let mut vertices = Vec::new();
+ let mut texcoords = Vec::new();
+ let mut indices = Vec::new();
+ for y in 0..layer.height {
+ for x in 0..layer.width {
+ let idx = x as usize + (y * layer.width) as usize;
+ let gid = *layer.data.get(idx).ok_or(Err::LayerDataTooSmall)?;
+ if gid == 0 { continue; }
+ let (lid, ass, _) = assets.lookup_gid(level, gid)?;
+ let cols = ass.tileset.imagewidth / ass.tileset.tilewidth;
+ let rows = ass.tileset.imageheight / ass.tileset.tileheight;
+ let col = lid % cols;
+ let row = lid / cols;
+ let twidth = 1.0 / cols as f32;
+ let theight = 1.0 / rows as f32;
+
+ let i = vertices.len() as u32;
+ let v = glam::Vec2::new(x as _, y as _);
+ vertices.push(v);
+ vertices.push(v + glam::Vec2::new(1.0, 0.0));
+ vertices.push(v + glam::Vec2::new(1.0, 1.0));
+ vertices.push(v + glam::Vec2::new(0.0, 1.0));
+ let uvbase = glam::Vec2::new(col as f32 / cols as f32, row as f32 / rows as f32);
+ texcoords.push(uvbase + glam::Vec2::new(0.0, theight));
+ texcoords.push(uvbase + glam::Vec2::new(twidth, theight));
+ texcoords.push(uvbase + glam::Vec2::new(twidth, 0.0));
+ texcoords.push(uvbase);
+ indices.push(i); indices.push(i + 1); indices.push(i + 2);
+ indices.push(i); indices.push(i + 3); indices.push(i + 2);
+ }
+ }
+ let index_bytes: Vec<u8> = indices.iter().flat_map(|x| x.to_ne_bytes()).collect();
+ unsafe {
+ ctx.gl.bind_vertex_array(Some(lr.vao));
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(lr.vertex_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ vertices.as_ptr() as _,
+ vertices.len() * std::mem::size_of::<f32>() * 2,
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(lr.texcoords_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ texcoords.as_ptr() as _,
+ texcoords.len() * std::mem::size_of::<f32>() * 2,
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(lr.index_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ELEMENT_ARRAY_BUFFER,
+ &index_bytes,
+ glow::STATIC_DRAW,
+ );
+ lr.index_count = indices.len();
+ }
+ Ok(())
+ }
+ pub fn render_layer(
+ &self,
+ ctx: &context::Context,
+ assets: &Assets,
+ level: &Level,
+ lidx: usize,
+ ) -> Erm<()> {
+ let layer = level.layers.get(lidx).ok_or(Err::LayerIndexOutOfBounds)?;
+ let lr = self.layers.get(lidx).ok_or(Err::LayerIndexOutOfBounds)?;
+ // TODO: handle layers with multiple textures
+ let gid = *layer.data.iter().find(|g| **g > 0).unwrap();
+ let (_, ass, _) = assets.lookup_gid(level, gid)?;
+ ass.texture.bind(ctx);
+ unsafe {
+ ctx.gl.bind_vertex_array(Some(lr.vao));
+ ctx.gl.draw_elements(glow::TRIANGLES, lr.index_count as _, glow::UNSIGNED_INT, 0);
+ }
+ Ok(())
+ }
+ pub fn populate(
+ &mut self,
+ ctx: &context::Context,
+ assets: &Assets,
+ level: &Level,
+ ) -> Erm<()> {
+ for lidx in 0..level.layers.len() {
+ self.populate_layer(ctx, assets, level, lidx)?;
+ }
+ Ok(())
+ }
+ pub fn render(
+ &self,
+ ctx: &context::Context,
+ assets: &Assets,
+ level: &Level,
+ ) -> Erm<()> {
+ self.shader.bind(ctx);
+ let sx = 2.0 * level.tilewidth as f32 / ctx.render_width;
+ let sy = 2.0 * level.tileheight as f32 / ctx.render_height;
+ self.shader.set_mat4(
+ ctx, "transform",
+ &glam::Mat4::from_scale_rotation_translation(
+ glam::Vec3::new(sx, -sy, 1.0),
+ glam::Quat::IDENTITY,
+ glam::Vec3::new(0.0, 0.0, 0.0),
+ ),
+ );
+ for lidx in 0..level.layers.len() {
+ self.render_layer(ctx, assets, level, lidx)?;
+ }
+ Ok(())
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/lib.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/lib.rs
new file mode 100644
index 0000000..6b1564e
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/lib.rs
@@ -0,0 +1,355 @@
+#![feature(try_blocks)]
+
+pub mod audio;
+pub mod context;
+pub mod font;
+pub mod framebuffer;
+pub mod level2d;
+pub mod mesh;
+pub mod net;
+pub mod physics;
+pub mod renderer;
+pub mod save;
+pub mod scene;
+pub mod shader;
+pub mod shadow;
+pub mod state;
+pub mod texture;
+pub mod ui;
+pub mod utils;
+
+pub use audio::AudioPlayback;
+pub use simple_eyre::eyre::WrapErr;
+pub use state::Tick;
+pub use utils::{Erm, erm, install_error_handler};
+
+#[cfg(target_arch = "wasm32")]
+use winit::platform::web::EventLoopExtWebSys;
+
+#[cfg(target_arch = "wasm32")]
+use winit::platform::web::WindowExtWebSys;
+
+#[cfg(target_arch = "wasm32")]
+use wasm_bindgen::JsCast;
+
+#[cfg(not(target_arch = "wasm32"))]
+use glfw::Context;
+
+use bitflags::bitflags;
+bitflags! {
+ pub struct Options: u32 {
+ const OVERLAY = 0b00000001;
+ const HIDDEN = 0b00000010;
+ const NORESIZE = 0b00000100;
+ const EGL = 0b00001000;
+ const FULLSCREEN_MOUSE = 0b00010000;
+ }
+}
+
+static mut CTX: Option<*const context::Context> = None;
+static mut ST: Option<*mut state::State> = None;
+static mut G: Option<*mut std::ffi::c_void> = None;
+
+pub fn contextualize<F, G, X>(mut f: F) -> X
+where
+ G: state::Game + 'static,
+ F: FnMut(&context::Context, &mut state::State, &mut G) -> X,
+{
+ unsafe {
+ match (CTX, ST, G) {
+ (Some(c), Some(s), Some(g)) => f(&*c, &mut *s, &mut *(g as *mut G)),
+ _ => panic!("context not set"),
+ }
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn run<'a, F, G>(title: &str, w: u32, h: u32, options: Options, gnew: F) -> Erm<()>
+where
+ G: state::Game + 'static,
+ F: (FnOnce(&'a context::Context) -> G),
+{
+ env_logger::Builder::new()
+ .filter(None, log::LevelFilter::Info)
+ .init();
+ install_error_handler();
+
+ log::info!("hello computer, starting up...");
+
+ let (rglfw, rwindow, gl, events) = {
+ use glfw::fail_on_errors;
+ let mut glfw = glfw::init(glfw::fail_on_errors!()).expect("failed to initialize GLFW");
+
+ if options.contains(Options::EGL) {
+ glfw.window_hint(glfw::WindowHint::ContextCreationApi(
+ glfw::ContextCreationApi::Egl,
+ ));
+ }
+ // let gl_attr = video.gl_attr();
+ // gl_attr.set_context_profile(sdl2::video::GLProfile::Core);
+ // gl_attr.set_context_version(3, 0);
+ let (mut window, events) = glfw.with_primary_monitor(|glfw, primary| {
+ if options.contains(Options::HIDDEN) {
+ glfw.window_hint(glfw::WindowHint::Visible(false));
+ glfw.create_window(w as _, h as _, title, glfw::WindowMode::Windowed)
+ .expect("failed to create window")
+ } else if options.contains(Options::OVERLAY) {
+ let mon = primary.expect("failed to get monitor");
+ let mode = mon.get_video_mode().expect("failed to get video mode");
+ glfw.window_hint(glfw::WindowHint::RedBits(Some(mode.red_bits)));
+ glfw.window_hint(glfw::WindowHint::GreenBits(Some(mode.green_bits)));
+ glfw.window_hint(glfw::WindowHint::BlueBits(Some(mode.blue_bits)));
+ glfw.window_hint(glfw::WindowHint::RefreshRate(Some(mode.refresh_rate)));
+ glfw.window_hint(glfw::WindowHint::Resizable(false));
+ glfw.window_hint(glfw::WindowHint::Decorated(false));
+ glfw.window_hint(glfw::WindowHint::Floating(true));
+ glfw.window_hint(glfw::WindowHint::TransparentFramebuffer(true));
+ unsafe {
+ // glfw.window_hint(glfw::WindowHint::MousePassthrough(true));
+ glfw::ffi::glfwWindowHint(0x0002000D, 1); // mouse passthrough
+ }
+ glfw.create_window(
+ mode.width,
+ mode.height,
+ title,
+ glfw::WindowMode::FullScreen(mon),
+ )
+ .expect("failed to create window")
+ } else {
+ glfw.create_window(w as _, h as _, title, glfw::WindowMode::Windowed)
+ .expect("failed to create window")
+ }
+ });
+ window.make_current();
+ window.set_key_polling(true);
+ window.set_mouse_button_polling(true);
+ window.set_size_polling(true);
+ window.set_focus_polling(true);
+ window.set_cursor_pos_polling(true);
+ let gl = unsafe {
+ glow::Context::from_loader_function(|s| window.get_proc_address(s) as *const _)
+ };
+ glfw.set_swap_interval(glfw::SwapInterval::Sync(1));
+ (glfw, window, gl, events)
+ };
+ let glfw = std::cell::RefCell::new(rglfw);
+ let window = std::cell::RefCell::new(rwindow);
+
+ let ctx = Box::leak(Box::new(context::Context::new(
+ glfw, window, gl, w as f32, h as f32, options,
+ )));
+ let game = Box::leak(Box::new(gnew(ctx)));
+ let st = Box::leak(Box::new(state::State::new(ctx)));
+
+ unsafe {
+ CTX = Some(ctx as _);
+ ST = Some(st as _);
+ G = Some(game as *mut G as *mut std::ffi::c_void);
+ }
+
+ game.initialize(ctx, st)?;
+ st.initialize_audio(ctx, game)?;
+ 'running: loop {
+ if ctx.window.borrow().should_close() {
+ game.finalize(ctx, st)?;
+ log::info!("bye!");
+ break 'running;
+ }
+ ctx.glfw.borrow_mut().poll_events();
+ for (_, event) in glfw::flush_messages(&events) {
+ match event {
+ glfw::WindowEvent::Size(_, _) => st.handle_resize(ctx),
+ glfw::WindowEvent::Focus(false) => {
+ st.keys = state::Keys::new();
+ }
+ glfw::WindowEvent::CursorPos(x, y) => {
+ st.mouse_moved(ctx, x as f32, y as f32, game)?;
+ }
+ glfw::WindowEvent::MouseButton(_, glfw::Action::Press, _) => {
+ st.mouse_pressed(ctx, game)?;
+ }
+ glfw::WindowEvent::MouseButton(_, glfw::Action::Release, _) => {
+ st.mouse_released(ctx, game)?;
+ }
+ glfw::WindowEvent::Key(key, _, glfw::Action::Press, _) => {
+ st.key_pressed(ctx, game, state::Keycode::new(key))?;
+ }
+ glfw::WindowEvent::Key(key, _, glfw::Action::Release, _) => {
+ st.key_released(ctx, game, state::Keycode::new(key))?;
+ }
+ _ => {}
+ }
+ }
+ if ctx.resize_necessary() {
+ st.handle_resize(ctx);
+ }
+ // if let Some(f) = &mut st.request {
+ // match std::future::Future::poll(f.as_mut(), &mut st.waker_ctx) {
+ // std::task::Poll::Pending => {},
+ // std::task::Poll::Ready(res) => {
+ // st.request = None;
+ // match res {
+ // Ok(r) => st.request_returned(&ctx, game, r),
+ // Err(e) => log::warn!("error during HTTP request: {}", e),
+ // }
+ // },
+ // }
+ // }
+ st.run_update(ctx, game)?;
+ st.run_render(ctx, game)?;
+ ctx.window.borrow_mut().swap_buffers();
+ }
+ Ok(())
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn run<'a, F, G>(w: u32, h: u32, options: Options, gnew: F)
+where
+ G: state::Game + 'static,
+ F: (Fn(&'a context::Context) -> G),
+{
+ console_log::init_with_level(log::Level::Debug).unwrap();
+ console_error_panic_hook::set_once();
+ tracing_wasm::set_as_global_default();
+ // install_error_handler();
+
+ log::info!("hello computer, starting up...");
+
+ let event_loop = winit::event_loop::EventLoop::new().expect("failed to initialize event loop");
+
+ let resize = !options.contains(Options::NORESIZE);
+ let (window, gl) = {
+ let mut builder = winit::window::WindowBuilder::new()
+ .with_maximized(resize)
+ .with_decorations(false);
+ if options.contains(Options::OVERLAY) {
+ builder = builder.with_transparent(true);
+ }
+ let window = builder
+ .build(&event_loop)
+ .expect("failed to initialize window");
+ let gl = web_sys::window()
+ .and_then(|win| win.document())
+ .and_then(|doc| {
+ let dst = doc.get_element_by_id("teleia-parent")?;
+ let canvas =
+ web_sys::Element::from(window.canvas().expect("failed to find canvas"));
+ dst.append_child(&canvas).ok()?;
+ let c = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()?;
+ let webgl2_context = c
+ .get_context("webgl2")
+ .ok()??
+ .dyn_into::<web_sys::WebGl2RenderingContext>()
+ .ok()?;
+ Some(glow::Context::from_webgl2_context(webgl2_context))
+ })
+ .expect("couldn't add canvas to document");
+ (window, gl)
+ };
+
+ let ctx = Box::leak(Box::new(context::Context::new(
+ window, gl, w as f32, h as f32, options,
+ )));
+ ctx.maximize_canvas();
+ let game = Box::leak(Box::new(gnew(ctx)));
+ let st = Box::leak(Box::new(state::State::new(&ctx)));
+
+ unsafe {
+ CTX = Some(ctx as _);
+ ST = Some(st as _);
+ G = Some(game as *mut G as *mut std::ffi::c_void);
+ }
+
+ let _ = game.initialize(ctx, st);
+ let _ = st.initialize_audio(ctx, game);
+ let res = std::rc::Rc::new(std::cell::RefCell::new(Ok(())));
+ let result = res.clone();
+ event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
+ event_loop.spawn(move |event, elwt| {
+ let res: Erm<()> = contextualize(|ctx, st, game: &mut G| {
+ match &event {
+ winit::event::Event::WindowEvent {
+ event: wev,
+ window_id,
+ ..
+ } => match wev {
+ winit::event::WindowEvent::CloseRequested if *window_id == ctx.window.id() => {
+ elwt.exit()
+ }
+ winit::event::WindowEvent::Resized { .. } => {
+ #[cfg(target_arch = "wasm32")]
+ ctx.maximize_canvas();
+ st.handle_resize(&ctx);
+ }
+ winit::event::WindowEvent::Focused(false) => {
+ st.keys = state::Keys::new();
+ }
+ winit::event::WindowEvent::CursorMoved { position, .. } => {
+ st.mouse_moved(&ctx, position.x as f32, position.y as f32, game)?;
+ }
+ winit::event::WindowEvent::MouseInput { state, .. } => match state {
+ winit::event::ElementState::Pressed => {
+ st.mouse_pressed(&ctx, game)?;
+ }
+ winit::event::ElementState::Released => {
+ st.mouse_released(&ctx, game)?;
+ }
+ },
+ winit::event::WindowEvent::KeyboardInput {
+ event:
+ winit::event::KeyEvent {
+ physical_key: winit::keyboard::PhysicalKey::Code(key),
+ state,
+ repeat: false,
+ ..
+ },
+ ..
+ } => match state {
+ winit::event::ElementState::Pressed => {
+ st.key_pressed(&ctx, game, state::Keycode { kc: *key })?;
+ }
+ winit::event::ElementState::Released => {
+ st.key_released(&ctx, game, state::Keycode { kc: *key })?;
+ }
+ },
+ _ => {}
+ },
+
+ winit::event::Event::AboutToWait => {
+ if ctx.resize_necessary() {
+ #[cfg(target_arch = "wasm32")]
+ ctx.maximize_canvas();
+ st.handle_resize(&ctx);
+ }
+ // if let Some(f) = &mut st.request {
+ // match std::future::Future::poll(f.as_mut(), &mut st.waker_ctx) {
+ // std::task::Poll::Pending => {},
+ // std::task::Poll::Ready(res) => {
+ // st.request = None;
+ // match res {
+ // Ok(r) => st.request_returned(&ctx, game, r),
+ // Err(e) => log::warn!("error during HTTP request: {}", e),
+ // }
+ // },
+ // }
+ // // f.poll();
+ // }
+ st.run_update(&ctx, game)?;
+ st.run_render(&ctx, game)?;
+ ctx.window.request_redraw();
+ }
+
+ _ => {}
+ }
+ Ok(())
+ });
+ if let Err(e) = res {
+ *result.borrow_mut() = Err(e);
+ elwt.exit();
+ }
+ });
+ let _ = game.finalize(ctx, st);
+ if let Err(e) = res.replace(Ok(())) {
+ panic!("{}", e);
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/mesh.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/mesh.rs
new file mode 100644
index 0000000..ce34de4
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/mesh.rs
@@ -0,0 +1,197 @@
+use glow::HasContext;
+
+use crate::context;
+
+pub const ATTRIB_VERTEX: u32 = 0;
+pub const ATTRIB_NORMAL: u32 = 1;
+pub const ATTRIB_TEXCOORD: u32 = 2;
+pub const ATTRIB_JOINT: u32 = 3;
+pub const ATTRIB_WEIGHT: u32 = 4;
+pub const ATTRIB_COLOR: u32 = 5;
+
+type Buffer = <glow::Context as glow::HasContext>::Buffer;
+
+pub struct Mesh {
+ pub vao: glow::VertexArray,
+ pub vbo_vertex: Buffer,
+ pub vbo_index: Buffer,
+ pub vbo_normal: Option<Buffer>,
+ pub vbo_texcoord: Option<Buffer>,
+ pub mode: u32, // glow::TRIANGLES, etc.
+ pub index_count: usize,
+ pub index_type: u32, // glow::BYTE, glow::FLOAT, etc.
+ pub index_offset: i32,
+}
+
+impl Mesh {
+ pub fn new_empty(ctx: &context::Context, normals: bool, texcoords: bool) -> Self {
+ unsafe {
+ let vao = ctx.gl.create_vertex_array().expect("failed to initialize vao");
+ ctx.gl.bind_vertex_array(Some(vao));
+
+ let vbo_vertex = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo_vertex));
+ ctx.gl.vertex_attrib_pointer_f32(ATTRIB_VERTEX, 3, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(ATTRIB_VERTEX);
+
+ let vbo_index = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(vbo_index));
+
+ let vbo_normal = if normals {
+ let vbo_normal = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo_normal));
+ ctx.gl.vertex_attrib_pointer_f32(ATTRIB_NORMAL, 3, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(ATTRIB_NORMAL);
+ Some(vbo_normal)
+ } else { None };
+
+ let vbo_texcoord = if texcoords {
+ let vbo_texcoord = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo_texcoord));
+ ctx.gl.vertex_attrib_pointer_f32(ATTRIB_TEXCOORD, 2, glow::FLOAT, false, 0, 0);
+ ctx.gl.enable_vertex_attrib_array(ATTRIB_TEXCOORD);
+ Some(vbo_texcoord)
+ } else { None };
+
+ Self {
+ vao,
+ vbo_vertex,
+ vbo_index,
+ vbo_normal,
+ vbo_texcoord,
+ mode: glow::TRIANGLES,
+ index_count: 0,
+ index_type: glow::UNSIGNED_INT,
+ index_offset: 0,
+ }
+ }
+ }
+ pub fn upload(
+ &mut self,
+ ctx: &context::Context,
+ vertices: &[f32],
+ indices: &[u32],
+ snormals: Option<&[f32]>,
+ stexcoords: Option<&[f32]>,
+ ) {
+ unsafe {
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vbo_vertex));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ vertices.as_ptr() as _,
+ std::mem::size_of_val(vertices),
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.vbo_index));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ELEMENT_ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ indices.as_ptr() as _,
+ std::mem::size_of_val(indices),
+ ),
+ glow::STATIC_DRAW,
+ );
+
+ if let Some(vbo_normal) = self.vbo_normal && let Some(normals) = snormals {
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo_normal));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ normals.as_ptr() as _,
+ std::mem::size_of_val(normals),
+ ),
+ glow::STATIC_DRAW,
+ );
+ Some(vbo_normal)
+ } else { None };
+
+ if let Some(vbo_texcoord) = self.vbo_texcoord && let Some(texcoords) = stexcoords {
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vbo_texcoord));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ texcoords.as_ptr() as _,
+ std::mem::size_of_val(texcoords),
+ ),
+ glow::STATIC_DRAW,
+ );
+ Some(vbo_texcoord)
+ } else { None };
+ self.index_count = indices.len();
+ }
+ }
+
+ pub fn build(
+ ctx: &context::Context,
+ vertices: &[f32],
+ indices: &[u32],
+ snormals: Option<&[f32]>,
+ stexcoords: Option<&[f32]>,
+ ) -> Self {
+ let mut ret = Self::new_empty(ctx, snormals.is_some(), stexcoords.is_some());
+ ret.upload(ctx, vertices, indices, snormals, stexcoords);
+ ret
+ }
+
+ pub fn upload_obj(&mut self, ctx: &context::Context, mut bytes: &[u8]) {
+ let lopts = tobj::LoadOptions {
+ triangulate: true,
+ single_index: true,
+ ..Default::default()
+ };
+ let (meshes, _materials) = tobj::load_obj_buf(
+ &mut bytes,
+ &lopts,
+ |_| Err(tobj::LoadError::GenericFailure)
+ ).expect("failed to load mesh");
+ let mesh = meshes.into_iter().next()
+ .expect("failed to load mesh")
+ .mesh;
+ self.upload(
+ ctx,
+ &mesh.positions,
+ &mesh.indices,
+ Some(&mesh.normals),
+ Some(&mesh.texcoords),
+ )
+ }
+
+ pub fn from_obj(ctx: &context::Context, mut bytes: &[u8]) -> Self {
+ let lopts = tobj::LoadOptions {
+ triangulate: true,
+ single_index: true,
+ ..Default::default()
+ };
+ let (meshes, _materials) = tobj::load_obj_buf(
+ &mut bytes,
+ &lopts,
+ |_| Err(tobj::LoadError::GenericFailure)
+ ).expect("failed to load mesh");
+ let mesh = meshes.into_iter().next()
+ .expect("failed to load mesh")
+ .mesh;
+ Self::build(
+ ctx,
+ &mesh.positions,
+ &mesh.indices,
+ Some(&mesh.normals),
+ Some(&mesh.texcoords),
+ )
+ }
+
+ pub fn render(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.bind_vertex_array(Some(self.vao));
+ ctx.gl.draw_elements(self.mode, self.index_count as _, self.index_type, self.index_offset);
+ }
+ }
+
+ pub fn render_instanced(&self, ctx: &context::Context, count: u64) {
+ unsafe {
+ ctx.gl.bind_vertex_array(Some(self.vao));
+ ctx.gl.draw_elements_instanced(self.mode, self.index_count as _, self.index_type, self.index_offset, count as _);
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net.rs
new file mode 100644
index 0000000..8758968
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net.rs
@@ -0,0 +1,4 @@
+pub mod ws;
+
+#[cfg(not(target_arch = "wasm32"))]
+pub mod fig;
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/fig.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/fig.rs
new file mode 100644
index 0000000..916cdc1
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/fig.rs
@@ -0,0 +1,100 @@
+use std::io::{Read, Write};
+use byteorder::{LE, ReadBytesExt, WriteBytesExt};
+
+use crate::{Erm, WrapErr};
+
+const KEY: usize = 42;
+
+pub fn read_length_prefixed_utf8<R>(r: &mut R) -> Erm<String> where R: std::io::Read {
+ let len = r.read_u32::<LE>()?;
+ let mut bs = vec![0; len as usize];
+ r.read_exact(&mut bs)?;
+ Ok(String::from_utf8(bs)?)
+}
+
+#[derive(Debug, Clone)]
+pub struct BinaryMessage {
+ pub event: Vec<u8>,
+ pub data: Vec<u8>
+}
+
+pub struct BinaryClient {
+ in_buf: Vec<u8>,
+ out_buf: Vec<u8>,
+ socket: std::net::TcpStream,
+ poller: polling::Poller,
+}
+impl BinaryClient {
+ pub fn new(addr: &str, subs: &[&[u8]]) -> Erm<Self> {
+ let mut socket = std::net::TcpStream::connect(addr).wrap_err("failed to connect to message bus")?;
+ for s in subs {
+ write!(socket, "s").wrap_err("failed to send subscribe message to bus")?;
+ socket.write_u32::<byteorder::LE>(s.len() as u32).wrap_err("failed to send subscribe message length to bus")?;
+ socket.write_all(s).wrap_err("failed to send subscribe message to bus")?;
+ }
+ socket.set_nonblocking(true).wrap_err("failed to set message bus socket nonblocking")?;
+ socket.flush().wrap_err("failed to flush bus connection")?;
+ let in_buf = Vec::new();
+ let out_buf = Vec::new();
+ let poller = polling::Poller::new()?;
+ unsafe {
+ poller.add(&socket, polling::Event::all(KEY)).wrap_err("failed to add event to poll")?;
+ }
+ Ok(Self {
+ in_buf, out_buf,
+ socket,
+ poller,
+ })
+ }
+ fn write_length_prefixed(&mut self, buf: &[u8]) -> Erm<()> {
+ self.out_buf.write_u32::<byteorder::LE>(buf.len() as u32).wrap_err("failed to send message")?;
+ self.out_buf.write_all(buf).wrap_err("failed to send message")?;
+ Ok(())
+
+ }
+ pub fn publish(&mut self, ev: &[u8], data: &[u8]) -> Erm<()> {
+ write!(self.out_buf, "p").wrap_err("failed to send publish message to bus")?;
+ self.write_length_prefixed(ev)?;
+ self.write_length_prefixed(data)?;
+ Ok(())
+ }
+ fn pop_incoming_message(&mut self) -> Option<BinaryMessage> {
+ let mut reader = std::io::Cursor::new(&self.in_buf);
+ let event_len = reader.read_u32::<byteorder::LE>().ok()?;
+ let mut event = vec![0; event_len as usize];
+ reader.read_exact(&mut event).ok()?;
+ let data_len = reader.read_u32::<byteorder::LE>().ok()?;
+ let mut data = vec![0; data_len as usize];
+ reader.read_exact(&mut data).ok()?;
+ let len = reader.position() as usize;
+ self.in_buf.drain(..len);
+ Some(BinaryMessage { event, data })
+ }
+ pub fn pump(&mut self) -> Erm<Option<BinaryMessage>> {
+ let mut events = polling::Events::new();
+ events.clear();
+ self.poller.wait(&mut events, Some(std::time::Duration::from_secs(0)))
+ .wrap_err("failed to poll message bus")?;
+ for ev in events.iter() {
+ if ev.key == KEY {
+ if ev.readable {
+ let mut buf = [0; 1024];
+ match self.socket.read(&mut buf) {
+ Ok(sz) => self.in_buf.extend_from_slice(&buf[..sz]),
+ Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {},
+ e => { e.wrap_err("failed to read from bus socket")?; },
+ }
+ }
+ if ev.writable && !self.out_buf.is_empty() {
+ match self.socket.write(&self.out_buf) {
+ Ok(sz) => { self.out_buf.drain(..sz); },
+ Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {},
+ e => { e.wrap_err("failed to write to bus socket")?; },
+ }
+ }
+ self.poller.modify(&self.socket, polling::Event::all(KEY)).wrap_err("failed to update event to poll")?;
+ }
+ }
+ Ok(self.pop_incoming_message())
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws.rs
new file mode 100644
index 0000000..b11df40
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws.rs
@@ -0,0 +1,10 @@
+pub mod client;
+
+#[cfg(not(target_arch = "wasm32"))]
+pub mod server;
+
+#[derive(Debug, Clone)]
+pub enum Message {
+ Binary(Vec<u8>),
+ Text(String),
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client.rs
new file mode 100644
index 0000000..fe9f957
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client.rs
@@ -0,0 +1,9 @@
+#[cfg(target_arch = "wasm32")]
+pub mod wasm;
+#[cfg(target_arch = "wasm32")]
+pub use wasm::*;
+
+#[cfg(not(target_arch = "wasm32"))]
+pub mod native;
+#[cfg(not(target_arch = "wasm32"))]
+pub use native::*;
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/native.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/native.rs
new file mode 100644
index 0000000..eb94093
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/native.rs
@@ -0,0 +1,124 @@
+use std::sync::mpsc::{Sender, Receiver, channel};
+use std::sync::{Arc, Mutex};
+use std::thread::spawn;
+
+use crate::net::ws::Message;
+use crate::{utils, Erm, WrapErr};
+
+const KEY: usize = 42;
+
+type ClientChannels = (Sender<Message>, Receiver<Message>);
+#[derive(Default)]
+pub struct Client {
+ channels: Arc<Mutex<Option<ClientChannels>>>,
+}
+
+impl Client {
+ pub fn new() -> Self { Self::default() }
+ pub fn connect(&mut self, url: &str) {
+ let (mut socket, _resp) = if let Ok(v) = tungstenite::connect(url) { v } else {
+ log::warn!("failed to connect to websocket: {}", url);
+ return;
+ };
+ let poller = match polling::Poller::new() {
+ Ok(v) => v,
+ Err(e) => { log::warn!("failed to create poller: {}", e); return; }
+ };
+ let res: Erm<()> = try { match socket.get_ref() {
+ tungstenite::stream::MaybeTlsStream::Plain(s) => unsafe {
+ s.set_nonblocking(true).wrap_err("failed to set socket nonblocking")?;
+ poller.add(s, polling::Event::readable(KEY)).wrap_err("failed to add event to poll")?;
+ },
+ tungstenite::stream::MaybeTlsStream::NativeTls(s) => unsafe {
+ s.get_ref().set_nonblocking(true).wrap_err("failed to set socket nonblocking")?;
+ poller.add(s.get_ref(), polling::Event::readable(KEY)).wrap_err("failed to add event to poll")?;
+ },
+ _ => utils::erm_msg("unknown socket type; cannot poll!")?,
+ }};
+ if let Err(e) = res {
+ log::warn!("failed to add event to poll: {}", e);
+ let _ = socket.close(None);
+ return;
+ };
+ let (outgoing_sender, outgoing_receiver) = channel();
+ let (incoming_sender, incoming_receiver) = channel();
+ *self.channels.lock().unwrap() = Some((outgoing_sender, incoming_receiver));
+ let send_channels_ref = self.channels.clone();
+ let recv_channels_ref = self.channels.clone();
+ let send_socket_ref = Arc::new(Mutex::new(socket));
+ let recv_socket_ref = send_socket_ref.clone();
+ let mut events = polling::Events::new();
+ spawn(move || loop {
+ // write all outgoing messages
+ let res: Erm<()> = try {
+ while let Ok(msg) = outgoing_receiver.recv() {
+ match msg {
+ Message::Text(txt) => send_socket_ref.lock().unwrap()
+ .send(tungstenite::Message::Text(txt))
+ .wrap_err("failed to send websocket text")?,
+ Message::Binary(bytes) => send_socket_ref.lock().unwrap()
+ .send(tungstenite::Message::Binary(bytes))
+ .wrap_err("failed to send websocket bytes")?,
+ }
+ }
+ };
+ if let Err(e) = res {
+ log::warn!("error in websocket send thread: {}", e);
+ *send_channels_ref.lock().unwrap() = None;
+ return;
+ }
+ });
+ spawn(move || loop {
+ // wait for and receive all incoming messages
+ let res: Erm<()> = try {
+ events.clear();
+ poller.wait(&mut events, None).wrap_err("failed to wait on websocket poller")?;
+ for ev in events.iter() {
+ if ev.key == KEY {
+ if ev.readable { match recv_socket_ref.lock().unwrap().read().wrap_err("failed to recv on websocket")? {
+ tungstenite::Message::Text(txt) => incoming_sender.send(Message::Text(txt))
+ .wrap_err("failed to send incoming websocket message on channel")?,
+ tungstenite::Message::Binary(bytes) => incoming_sender.send(Message::Binary(bytes))
+ .wrap_err("failed to send incoming websocket message on channel")?,
+ tungstenite::Message::Ping(bs) => recv_socket_ref.lock().unwrap()
+ .send(tungstenite::Message::Pong(bs))
+ .wrap_err("failed to send websocket pong")?,
+ m => log::warn!("unhandled incoming websocket message: {}", m),
+ }}
+ match recv_socket_ref.lock().unwrap().get_ref() {
+ tungstenite::stream::MaybeTlsStream::Plain(s) =>
+ poller.modify(s, polling::Event::readable(KEY))
+ .wrap_err("failed to modify event to poll")?,
+ tungstenite::stream::MaybeTlsStream::NativeTls(s) =>
+ poller.modify(s.get_ref(), polling::Event::readable(KEY))
+ .wrap_err("failed to modify event to poll")?,
+ _ => log::warn!("unknown socket type; cannot modify polling!"),
+ }
+ }
+ }
+ };
+ if let Err(e) = res {
+ log::warn!("error in websocket recv thread: {}", e);
+ *recv_channels_ref.lock().unwrap() = None;
+ return;
+ }
+ });
+ }
+ pub fn is_connected(&self) -> bool {
+ self.channels.lock().unwrap().is_some()
+ }
+ pub fn poll(&mut self) -> Option<Message> {
+ if let Some((_, incoming)) = &*self.channels.lock().unwrap() {
+ incoming.try_recv().ok()
+ } else { None }
+ }
+ pub fn send(&self, msg: Message) {
+ if let Some((outgoing, _)) = &mut *self.channels.lock().unwrap() {
+ if let Err(e) = outgoing.send(msg) {
+ log::warn!("failed to send websocket message on channel: {}", e);
+ }
+ } else {
+ log::warn!("tried to send message, but websocket is not connected");
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/wasm.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/wasm.rs
new file mode 100644
index 0000000..f4faff7
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/client/wasm.rs
@@ -0,0 +1,73 @@
+use std::{collections::VecDeque, sync::{Arc, Mutex}};
+
+use wasm_bindgen::prelude::*;
+
+use crate::net::ws::Message;
+
+impl Message {
+ fn from_messageevent(e: web_sys::MessageEvent) -> Self {
+ if let Ok(abuf) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
+ let array = js_sys::Uint8Array::new(&abuf).to_vec();
+ Message::Binary(array)
+ } else if let Ok(txt) = e.data().dyn_into::<js_sys::JsString>() {
+ Message::Text(txt.into())
+ } else {
+ panic!("received weird websocket message: {:?}", e);
+ }
+ }
+}
+
+pub struct Client {
+ ws: Arc<Mutex<Option<web_sys::WebSocket>>>,
+ messages: Arc<Mutex<VecDeque<Message>>>,
+}
+
+impl Client {
+ pub fn new() -> Self {
+ Self {
+ ws: Arc::new(Mutex::new(None)),
+ messages: Arc::new(Mutex::new(VecDeque::new())),
+ }
+ }
+ pub fn connect(&mut self, url: &str) {
+ let ws_ref = self.ws.clone();
+ let messages_ref = self.messages.clone();
+ let cb: Closure<dyn Fn(web_sys::MessageEvent)> = Closure::new(move |e: web_sys::MessageEvent| {
+ let msg = Message::from_messageevent(e);
+ log::info!("incoming: {:?}", msg);
+ messages_ref.lock().unwrap().push_back(msg);
+ });
+ let close_cb: Closure<dyn Fn(web_sys::MessageEvent)> = Closure::new(move |_: web_sys::MessageEvent| {
+ log::info!("closed!");
+ *ws_ref.lock().unwrap() = None;
+ });
+ let ws = web_sys::WebSocket::new(url).expect("failed to open websocket");
+ ws.set_onmessage(Some(cb.as_ref().unchecked_ref()));
+ ws.set_onclose(Some(close_cb.as_ref().unchecked_ref()));
+ cb.forget();
+ close_cb.forget();
+ *self.ws.lock().unwrap() = Some(ws);
+ }
+ pub fn is_connected(&self) -> bool {
+ self.ws.lock().unwrap().is_some()
+ }
+ pub fn poll(&mut self) -> Option<Message> {
+ self.messages.lock().unwrap().pop_front()
+ }
+ pub fn send(&self, msg: Message) {
+ if let Some(ws) = &*self.ws.lock().unwrap() {
+ match msg {
+ Message::Text(txt) => {
+ if let Err(e) = ws.send_with_str(&txt) {
+ log::warn!("failed to send string: {:?}", e);
+ }
+ },
+ Message::Binary(bytes) => {
+ if let Err(e) = ws.send_with_u8_array(&bytes) {
+ log::warn!("failed to send bytes: {:?}", e);
+ }
+ },
+ }
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/server.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/server.rs
new file mode 100644
index 0000000..9422aec
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/net/ws/server.rs
@@ -0,0 +1,140 @@
+use std::collections::HashMap;
+use std::net::{TcpListener, TcpStream};
+use std::sync::mpsc::{Sender, Receiver, channel};
+use std::sync::{Arc, Mutex};
+use std::thread::spawn;
+
+use crate::net::ws::Message;
+use crate::{utils, Erm, WrapErr};
+
+const ACCEPT_KEY: usize = 1;
+
+type ClientId = usize;
+type ClientMessage = (ClientId, Message);
+
+type ServerChannels = (Sender<ClientMessage>, Receiver<ClientMessage>);
+#[derive(Default)]
+pub struct Server {
+ clients: Arc<Mutex<HashMap<ClientId, tungstenite::WebSocket<TcpStream>>>>,
+ channels: Arc<Mutex<Option<ServerChannels>>>,
+}
+
+impl Server {
+ pub fn new() -> Self { Self::default() }
+ pub fn start(&mut self, addr: &str) {
+ let recv_clients_ref = self.clients.clone();
+ let send_clients_ref = self.clients.clone();
+ let (outgoing_sender, outgoing_receiver) = channel();
+ let (incoming_sender, incoming_receiver) = channel();
+ *self.channels.lock().unwrap() = Some((outgoing_sender, incoming_receiver));
+ let channels_ref = self.channels.clone();
+ let listener = TcpListener::bind(addr).expect("failed to bind server socket");
+ let poller = polling::Poller::new().expect("failed to create poller for websocket server");
+ unsafe {
+ poller.add(&listener, polling::Event::readable(ACCEPT_KEY))
+ .expect("failed to add poll server socket");
+ }
+ let mut events = polling::Events::new();
+ let mut next_id = ACCEPT_KEY + 1;
+ spawn(move || loop {
+ // write all outgoing messages
+ while let Ok((cid, msg)) = outgoing_receiver.recv() {
+ let res: Erm<()> = try {
+ if let Some(sock) = send_clients_ref.lock().unwrap().get_mut(&cid) {
+ let res = match msg {
+ Message::Text(txt) => sock.send(tungstenite::Message::Text(txt)),
+ Message::Binary(bytes) => sock.send(tungstenite::Message::Binary(bytes))
+ };
+ match res {
+ Ok(_) => {},
+ Err(tungstenite::Error::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {
+ log::info!("client {} receive send block", cid);
+ },
+ Err(e) => utils::erm(e)?,
+ }
+ }
+ };
+ if let Err(e) = res {
+ log::warn!("error when sending to client {}, disconnecting: {}", cid, e);
+ send_clients_ref.lock().unwrap().remove(&cid);
+ }
+ }
+ });
+ spawn(move || loop {
+ // wait for and receive all new connections and incoming messages
+ let res: Erm<()> = try {
+ events.clear();
+ poller.wait(&mut events, None).wrap_err("failed to wait on websocket server poller")?;
+ for ev in events.iter() {
+ if ev.key == ACCEPT_KEY {
+ let (sock, client_addr) = listener.accept().wrap_err("failed to accept on socket")?;
+ let id = next_id; next_id += 1;
+ let res: Erm<()> = try {
+ unsafe { poller.add(&sock, polling::Event::readable(id))
+ .wrap_err("failed to add socket to poller")?; }
+ let conn = tungstenite::accept(sock).wrap_err("failed to upgrade to websocket")?;
+ recv_clients_ref.lock().unwrap().insert(id, conn);
+ log::info!("new connected client! {}", id);
+ };
+ if let Err(e) = res {
+ log::warn!("error during connection from {}: {}", client_addr, e);
+ };
+ poller.modify(&listener, polling::Event::readable(ACCEPT_KEY))
+ .wrap_err("failed to modify event to poll")?;
+ } else {
+ let res: Erm<()> = try {
+ if let Some(sock) = recv_clients_ref.lock().unwrap().get_mut(&ev.key) {
+ match sock.read() {
+ Ok(tungstenite::Message::Text(t)) =>
+ incoming_sender.send((ev.key, Message::Text(t)))
+ .wrap_err("failed to send incoming message on channel")?,
+ Ok(tungstenite::Message::Binary(b)) =>
+ incoming_sender.send((ev.key, Message::Binary(b)))
+ .wrap_err("failed to send incoming message on channel")?,
+ Ok(tungstenite::Message::Ping(x)) =>
+ sock.write(tungstenite::Message::Pong(x))
+ .wrap_err("failed to reply with pong")?,
+ Ok(m) => log::info!("received unhandled websocket message: {}", m),
+ Err(tungstenite::Error::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => {
+ log::info!("client {} receive would block", ev.key);
+ },
+ Err(e) => utils::erm(e)?,
+ }
+ poller.modify(sock.get_ref(), polling::Event::readable(ev.key))
+ .wrap_err("failed to modify event to poll")?;
+ } else {
+ log::warn!("poller indicated event for unknown client: {}", ev.key);
+ }
+ };
+ if let Err(e) = res {
+ log::warn!("error on client {} connection, disconnecting: {}", ev.key, e);
+ recv_clients_ref.lock().unwrap().remove(&ev.key);
+ }
+ }
+ }
+ };
+ if let Err(e) = res {
+ log::warn!("unhandled error in websocket thread, stopping server: {}", e);
+ *channels_ref.lock().unwrap() = None;
+ return;
+ }
+ });
+ }
+ pub fn clients(&self) -> Vec<ClientId> {
+ self.clients.lock().unwrap().keys().copied().collect()
+ }
+ pub fn poll(&mut self) -> Option<(ClientId, Message)> {
+ if let Some((_, incoming)) = &*self.channels.lock().unwrap() {
+ incoming.try_recv().ok()
+ } else { None }
+ }
+ pub fn send(&self, client: ClientId, msg: Message) {
+ if let Some((outgoing, _)) = &mut *self.channels.lock().unwrap() {
+ if let Err(e) = outgoing.send((client, msg)) {
+ log::warn!("failed to send websocket message to client {} on channel: {}", client, e);
+ }
+ } else {
+ log::warn!("tried to send message to client {}, but websocket is not connected", client);
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/physics.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/physics.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/physics.rs
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/renderer.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/renderer.rs
new file mode 100644
index 0000000..c22c973
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/renderer.rs
@@ -0,0 +1,337 @@
+use crate::{context, state, shader, texture, mesh};
+
+use bitflags::bitflags;
+
+bitflags! {
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
+ pub struct UberFlags: u32 {
+ const TEXTURE_COLOR = 0b0000000001;
+ const TEXTURE_NORMAL = 0b0000000010;
+ const FLIP_TEXTURE = 0b0000000100;
+ const LIGHT_AMBIENT = 0b0000001000;
+ const LIGHT_DIR = 0b0000010000;
+ const LIGHT_POINT = 0b0000100000;
+ const SPRITE = 0b0001000000;
+ const EFFECTS = 0b0010000000;
+ const YSKEW = 0b0100000000;
+ const VERTEX_COLOR = 0b1000000000;
+ }
+}
+impl UberFlags {
+ fn prelude() -> String {
+ let mut s = String::new();
+ s += "#version 300 es\nprecision highp float;";
+ for (nm, f) in Self::all().iter_names() {
+ s += &format!("const int {} = {};\n", nm, f.bits());
+ }
+ s
+ }
+ fn set_flags(self, ctx: &context::Context, shader: &shader::Shader) {
+ shader.set_i32(ctx, "flags", self.bits() as i32);
+ }
+}
+
+pub trait Assets {
+ type Shader: PartialEq + Eq + Clone + Copy;
+ fn shader(&self, i: Self::Shader) -> &shader::Shader;
+ type Texture: PartialEq + Eq + Clone + Copy;
+ fn texture(&self, i: Self::Texture) -> &texture::Texture;
+ type Mesh: PartialEq + Eq + Clone + Copy;
+ fn mesh(&self, i: Self::Mesh) -> &mesh::Mesh;
+ type Material: PartialEq + Eq + Clone + Copy;
+ fn material(&self, i: Self::Material) -> &texture::Material;
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ShaderMode { TwoDimension, ThreeDimension, ThreeDimensionOrth }
+#[derive(Debug, Clone, Copy)]
+enum BoundShader<A: Assets> { None, Uber(UberFlags, ShaderMode), Shader(A::Shader, ShaderMode) }
+impl<A: Assets> PartialEq for BoundShader<A> {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (Self::None, Self::None) => true,
+ (Self::Uber(sf, sm), Self::Uber(of, om)) => sf == of && sm == om,
+ (Self::Shader(ss, sm), Self::Shader(os, om)) => ss == os && sm == om,
+ _ => false,
+ }
+ }
+}
+#[derive(Debug, Clone, Copy)]
+enum BoundTexture<A: Assets> { None, Texture(A::Texture), Material(A::Material) }
+impl<A: Assets> PartialEq for BoundTexture<A> {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (Self::None, Self::None) => true,
+ (Self::Texture(s), Self::Texture(o)) => s == o,
+ (Self::Material(s), Self::Material(o)) => s == o,
+ _ => false,
+ }
+ }
+}
+pub struct Renderer<A: Assets> {
+ pub assets: A,
+ shader_uber: shader::Shader,
+ shader: BoundShader<A>,
+ texture: BoundTexture<A>,
+}
+impl<A: Assets> Renderer<A> {
+ pub fn new<F>(ctx: &context::Context, f: F) -> Self
+ where F: FnOnce(&context::Context) -> A {
+ let shader_uber = shader::Shader::new_nolib(ctx,
+ &format!("{}{}", UberFlags::prelude(), include_str!("assets/shaders/uber/vert.glsl")),
+ &format!("{}{}", UberFlags::prelude(), include_str!("assets/shaders/uber/frag.glsl")),
+ );
+ shader_uber.bind(ctx);
+ shader_uber.set_i32(ctx, "texture_normal", 1);
+ Self {
+ assets: f(ctx),
+ shader_uber,
+ shader: BoundShader::None,
+ texture: BoundTexture::None,
+ }
+ }
+ pub fn font_char_width(&self, st: &state::State) -> f32 { st.font_default.char_width as f32 }
+ pub fn font_char_height(&self, st: &state::State) -> f32 { st.font_default.char_height as f32 }
+ pub fn unbind_texture(&mut self, _ctx: &context::Context, _st: &mut state::State) {
+ self.texture = BoundTexture::None;
+ }
+ pub fn bind_texture(&mut self, ctx: &context::Context, _st: &mut state::State, texture: A::Texture) {
+ if self.texture != BoundTexture::Texture(texture) {
+ self.assets.texture(texture).bind(ctx);
+ self.texture = BoundTexture::Texture(texture);
+ }
+ }
+ pub fn bind_material(&mut self, ctx: &context::Context, _st: &mut state::State, mat: A::Material) {
+ if self.texture != BoundTexture::Material(mat) {
+ self.assets.material(mat).bind(ctx);
+ self.texture = BoundTexture::Material(mat);
+ }
+ }
+ fn shader(&self) -> Option<(&shader::Shader, ShaderMode)> {
+ match self.shader {
+ BoundShader::Uber(_, sm) => Some((&self.shader_uber, sm)),
+ BoundShader::Shader(s, sm) => Some((self.assets.shader(s), sm)),
+ _ => None,
+ }
+ }
+ fn bind_uber(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ flags: UberFlags, mode: ShaderMode,
+ ) {
+ if let BoundShader::Uber(f, sm) = self.shader && f == flags && sm == mode { return }
+ match mode {
+ ShaderMode::TwoDimension => st.bind_2d(ctx, &self.shader_uber),
+ ShaderMode::ThreeDimension => st.bind_3d(ctx, &self.shader_uber),
+ ShaderMode::ThreeDimensionOrth => st.bind_3d_orth(ctx, &self.shader_uber),
+ }
+ flags.set_flags(ctx, &self.shader_uber);
+ self.shader = BoundShader::Uber(flags, mode)
+ }
+ fn bind_shader(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ shader: A::Shader, mode: ShaderMode,
+ ) {
+ if let BoundShader::Shader(s, sm) = self.shader && s == shader && sm == mode { return }
+ match mode {
+ ShaderMode::TwoDimension => st.bind_2d(ctx, self.assets.shader(shader)),
+ ShaderMode::ThreeDimension => st.bind_3d(ctx, self.assets.shader(shader)),
+ ShaderMode::ThreeDimensionOrth => st.bind_3d_orth(ctx, self.assets.shader(shader)),
+ }
+ self.shader = BoundShader::Shader(shader, mode)
+ }
+ pub fn bind_uber_2d(&mut self, ctx: &context::Context, st: &mut state::State, flags: UberFlags) {
+ self.bind_uber(ctx, st, flags, ShaderMode::TwoDimension);
+ }
+ pub fn bind_uber_3d(&mut self, ctx: &context::Context, st: &mut state::State, flags: UberFlags) {
+ self.bind_uber(ctx, st, flags, ShaderMode::ThreeDimension);
+ }
+ pub fn bind_uber_3d_orth(&mut self, ctx: &context::Context, st: &mut state::State, flags: UberFlags) {
+ self.bind_uber(ctx, st, flags, ShaderMode::ThreeDimensionOrth);
+ }
+ pub fn bind_shader_2d(&mut self, ctx: &context::Context, st: &mut state::State, shader: A::Shader) {
+ self.bind_shader(ctx, st, shader, ShaderMode::TwoDimension);
+ }
+ pub fn bind_shader_3d(&mut self, ctx: &context::Context, st: &mut state::State, shader: A::Shader) {
+ self.bind_shader(ctx, st, shader, ShaderMode::ThreeDimension);
+ }
+ pub fn render(&self, ctx: &context::Context, _st: &state::State, mesh: A::Mesh) {
+ self.assets.mesh(mesh).render(ctx)
+ }
+ pub fn render_square(&self, ctx: &context::Context, st: &state::State) {
+ st.mesh_square.render(ctx)
+ }
+ pub fn set_position_2d_mat(&self, ctx: &context::Context, st: &state::State, pos: glam::Mat4) {
+ if let Some((s, sm)) = self.shader() {
+ debug_assert!(sm == ShaderMode::TwoDimension, "attempted to set_position_2d in wrong mode");
+ s.set_position_2d_mat(ctx, st, &pos);
+ }
+ }
+ pub fn set_position_2d_rotate(&self, ctx: &context::Context, st: &state::State, pos: glam::Vec2, dims: glam::Vec2, rot: glam::Quat) {
+ if let Some((s, sm)) = self.shader() {
+ debug_assert!(sm == ShaderMode::TwoDimension, "attempted to set_position_2d in wrong mode");
+ s.set_position_2d_helper(ctx, st, &pos, &dims, &rot)
+ }
+ }
+ pub fn set_position_2d(&self, ctx: &context::Context, st: &state::State, pos: glam::Vec2, dims: glam::Vec2) {
+ if let Some((s, sm)) = self.shader() {
+ debug_assert!(sm == ShaderMode::TwoDimension, "attempted to set_position_2d in wrong mode");
+ s.set_position_2d(ctx, st, &pos, &dims)
+ }
+ }
+ pub fn set_position_3d(&self, ctx: &context::Context, st: &state::State, pos: glam::Mat4) {
+ if let Some((s, sm)) = self.shader() {
+ debug_assert!(sm == ShaderMode::ThreeDimension || sm == ShaderMode::ThreeDimensionOrth,
+ "attempted to set_position_3d in wrong mode"
+ );
+ s.set_position_3d(ctx, st, &pos)
+ }
+ }
+ pub fn set_i32(&self, ctx: &context::Context, _st: &state::State, nm: &str, val: i32) {
+ if let Some((s, _)) = self.shader() { s.set_i32(ctx, nm, val) }
+ }
+ pub fn set_f32(&self, ctx: &context::Context, _st: &state::State, nm: &str, val: f32) {
+ if let Some((s, _)) = self.shader() { s.set_f32(ctx, nm, val) }
+ }
+ pub fn set_vec2(&self, ctx: &context::Context, _st: &state::State, nm: &str, val: glam::Vec2) {
+ if let Some((s, _)) = self.shader() { s.set_vec2(ctx, nm, &val) }
+ }
+ pub fn set_vec3(&self, ctx: &context::Context, _st: &state::State, nm: &str, val: glam::Vec3) {
+ if let Some((s, _)) = self.shader() { s.set_vec3(ctx, nm, &val) }
+ }
+ pub fn set_vec4(&self, ctx: &context::Context, _st: &state::State, nm: &str, val: glam::Vec4) {
+ if let Some((s, _)) = self.shader() { s.set_vec4(ctx, nm, &val) }
+ }
+ pub fn set_mat4(&self, ctx: &context::Context, _st: &state::State, nm: &str, val: glam::Mat4) {
+ if let Some((s, _)) = self.shader() { s.set_mat4(ctx, nm, &val) }
+ }
+ pub fn set_texture_offset(&self,
+ ctx: &context::Context, st: &state::State,
+ xinc: i32, yinc: i32,
+ x: i32, y: i32
+ ) {
+ let xcount = xinc as f32; let xratio = 1.0 / xcount;
+ let ycount = yinc as f32; let yratio = 1.0 / ycount;
+ self.set_vec2(
+ ctx, st, "sprite_dims",
+ glam::Vec2::new(xratio, yratio),
+ );
+ self.set_vec2(
+ ctx, st, "sprite_offset",
+ glam::Vec2::new((x % xinc) as f32 * xratio, (y % yinc) as f32 * yratio),
+ );
+ }
+
+ /// Common case: draw the given textured mesh in the world (units are world tiles)
+ pub fn textured_mesh_world(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ shader: A::Shader,
+ texture: A::Texture,
+ mesh: A::Mesh,
+ pos: glam::Mat4,
+ ) {
+ self.bind_shader_3d(ctx, st, shader);
+ self.bind_texture(ctx, st, texture);
+ self.set_position_3d(ctx, st, pos);
+ self.render(ctx, st, mesh);
+ }
+
+ /// Common case: draw the given color in a rectangle on the screen (units are pixels, pos is top left)
+ pub fn color_screen(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ color: glam::Vec4,
+ pos: glam::Vec2,
+ dims: glam::Vec2,
+ ) {
+ self.bind_uber_2d(ctx, st, UberFlags::empty());
+ self.set_vec4(ctx, st, "color", color);
+ self.set_position_2d(ctx, st, pos, dims);
+ self.render_square(ctx, st);
+ }
+
+ /// Common case: draw the given texture on the screen (units are pixels, pos is top left)
+ pub fn texture_screen(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ texture: A::Texture,
+ pos: glam::Vec2,
+ dims: glam::Vec2,
+ ) {
+ self.bind_uber_2d(ctx, st, UberFlags::TEXTURE_COLOR);
+ self.bind_texture(ctx, st, texture);
+ self.set_position_2d(ctx, st, pos, dims);
+ self.render_square(ctx, st);
+ }
+
+ pub fn texture_screen_rotate(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ texture: A::Texture,
+ pos: glam::Vec2,
+ dims: glam::Vec2,
+ rot: glam::Quat,
+ ) {
+ self.bind_uber_2d(ctx, st, UberFlags::TEXTURE_COLOR);
+ self.bind_texture(ctx, st, texture);
+ self.set_position_2d_rotate(ctx, st, pos, dims, rot);
+ self.render_square(ctx, st);
+ }
+
+ pub fn texture_screen_recolor(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ texture: A::Texture, hue: f32,
+ pos: glam::Vec2,
+ dims: glam::Vec2,
+ ) {
+ self.bind_uber_2d(ctx, st, UberFlags::TEXTURE_COLOR | UberFlags::EFFECTS);
+ self.bind_texture(ctx, st, texture);
+ self.set_f32(ctx, st, "effect_huescale", 0.0);
+ self.set_f32(ctx, st, "effect_hueshift", hue);
+ self.set_position_2d(ctx, st, pos, dims);
+ self.render_square(ctx, st);
+ self.set_f32(ctx, st, "effect_huescale", 1.0);
+ self.set_f32(ctx, st, "effect_hueshift", 0.0);
+ }
+
+ /// Common case: text in the default font (units are pixels, pos is top left)
+ pub fn text_screen(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ pos: glam::Vec2,
+ s: &str,
+ ) {
+ // drawing text might bind the texture
+ self.texture = BoundTexture::None;
+ self.bind_uber_2d(ctx, st, UberFlags::TEXTURE_COLOR | UberFlags::VERTEX_COLOR | UberFlags::FLIP_TEXTURE);
+ let dims = glam::Vec2::new(st.font_default.char_width as f32, st.font_default.char_height as f32);
+ let fpos = pos + glam::Vec2::new(-dims.x / 2.0, dims.y / 2.0);
+ self.set_position_2d(ctx, st, fpos, dims);
+ st.font_default.render_text(ctx, st, s);
+ }
+
+ /// Common case: text in the default font, with a color (units are pixels, pos is top left)
+ pub fn text_colored_screen(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ pos: glam::Vec2,
+ col: glam::Vec3,
+ s: &str,
+ ) {
+ self.texture = BoundTexture::None;
+ self.bind_uber_2d(ctx, st, UberFlags::TEXTURE_COLOR | UberFlags::VERTEX_COLOR | UberFlags::FLIP_TEXTURE);
+ let dims = glam::Vec2::new(st.font_default.char_width as f32, st.font_default.char_height as f32);
+ let fpos = pos + glam::Vec2::new(-dims.x / 2.0, st.font_default.char_height as f32 / 2.0);
+ self.set_position_2d(ctx, st, fpos, dims);
+ st.font_default.render_text_helper(ctx, st, s, &[col]);
+ }
+
+ /// Common case: text in the default font (units are pixels, pos is center)
+ pub fn text_centered_screen(&mut self,
+ ctx: &context::Context, st: &mut state::State,
+ pos: glam::Vec2,
+ s: &str,
+ ) {
+ self.texture = BoundTexture::None;
+ self.bind_uber_2d(ctx, st, UberFlags::TEXTURE_COLOR | UberFlags::VERTEX_COLOR | UberFlags::FLIP_TEXTURE);
+ let width = s.len() as f32 * st.font_default.char_width as f32;
+ let dims = glam::Vec2::new(st.font_default.char_width as f32, st.font_default.char_height as f32);
+ let fpos = pos + glam::Vec2::new(-dims.x / 2.0 - (width / 2.0).round(), st.font_default.char_height as f32 / 2.0);
+ self.set_position_2d(ctx, st, fpos, dims);
+ st.font_default.render_text(ctx, st, s);
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/save.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/save.rs
new file mode 100644
index 0000000..1297a1b
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/save.rs
@@ -0,0 +1,47 @@
+#[cfg(target_arch = "wasm32")]
+use base64::prelude::*;
+
+#[cfg(target_arch = "wasm32")]
+pub fn save<W>(id: &str, data: &W) where W: serde::Serialize {
+ let window = web_sys::window().expect("failed to get window object");
+ let storage = window.local_storage()
+ .expect("failed to get local storage")
+ .expect("local storage not present");
+ let key = format!("{}_save", id);
+ let val = bincode::serde::encode_to_vec(data, bincode::config::standard()).expect("failed to serialize save");
+ let str = BASE64_STANDARD.encode(&val);
+ storage.set_item(&key, &str).expect("failed to set save");
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn load<W>(id: &str) -> Option<W> where W: serde::de::DeserializeOwned {
+ let window = web_sys::window().expect("failed to get window object");
+ let storage = window.local_storage()
+ .expect("failed to get local storage")
+ .expect("local storage not present");
+ let key = format!("{}_save", id);
+ let s = storage.get_item(&key).expect("failed to get save").expect("save not present");
+ let bytes = BASE64_STANDARD.decode(&s).expect("failed to decode base64 for save");
+ let (ret, _) = bincode::serde::decode_from_slice(&bytes, bincode::config::standard())
+ .expect("failed to deserialize save");
+ Some(ret)
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn save<W>(id: &str, data: &W) where W: serde::Serialize {
+ let pd = directories::ProjectDirs::from("", "milkfat", id).expect("failed to get save directory");
+ let _ = std::fs::create_dir_all(pd.data_dir());
+ let path = pd.data_dir().join("teleia.save");
+ let mut file = std::fs::File::create(&path).expect("failed to open save file");
+ bincode::serde::encode_into_std_write(data, &mut file, bincode::config::standard())
+ .expect("failed to write save file");
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn load<W>(id: &str) -> Option<W> where W: serde::de::DeserializeOwned {
+ let pd = directories::ProjectDirs::from("", "milkfat", id).expect("failed to get save directory");
+ let _ = std::fs::create_dir_all(pd.data_dir());
+ let path = pd.data_dir().join("teleia.save");
+ let mut file = std::fs::File::open(&path).ok()?;
+ bincode::serde::decode_from_std_read(&mut file, bincode::config::standard()).ok()
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/scene.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/scene.rs
new file mode 100644
index 0000000..a904bf1
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/scene.rs
@@ -0,0 +1,401 @@
+use std::{collections::{HashMap, VecDeque}, mem::offset_of};
+
+use glow::HasContext;
+use image::EncodableLayout;
+
+use crate::{context, mesh, shader, texture};
+
+pub type Index = usize;
+
+pub struct Primitive {
+ pub mesh: mesh::Mesh,
+ pub material: Index,
+}
+
+pub struct Object {
+ pub primitives: Vec<Primitive>,
+}
+
+pub struct Material {
+ pub base_color_factor: glam::Vec4,
+ pub base_color_texture: Option<Index>,
+
+ pub metallic_factor: f32,
+ pub roughness_factor: f32,
+ pub metallic_roughness_texture: Option<Index>,
+
+ pub normal_texture: Option<Index>,
+
+ pub occlusion_texture: Option<Index>,
+
+ pub emissive_factor: glam::Vec3,
+ pub emissive_texture: Option<Index>,
+}
+
+pub struct Skin {
+ pub inverse_bind_matrices: Vec<glam::Mat4>,
+ pub joints: Vec<Index>,
+}
+
+pub enum ChannelValues {
+ Translation(Vec<glam::Vec3>),
+ Rotation(Vec<glam::Quat>),
+ Scale(Vec<glam::Vec3>),
+}
+
+pub enum Interpolation {
+ Linear,
+ Step,
+ CubicSpline,
+}
+
+pub struct Channel {
+ pub target: Index,
+ pub interpolation: Interpolation,
+ pub keyframes: Vec<f32>,
+ pub values: ChannelValues,
+}
+
+pub struct Animation {
+ pub channels: Vec<Channel>,
+}
+
+pub struct Node {
+ pub children: Vec<Index>,
+ pub object: Option<Index>,
+ pub skin: Option<Index>,
+ pub transform: glam::Mat4,
+}
+
+pub struct Scene {
+ pub objects: Vec<Object>,
+ pub textures: Vec<texture::Texture>,
+ pub materials: Vec<Material>,
+ pub skins: Vec<Skin>,
+ pub animations: HashMap<String, Animation>,
+ pub nodes: Vec<Node>,
+ pub nodes_by_name: HashMap<String, Index>,
+ pub scene_nodes: Vec<Index>,
+}
+
+impl Scene {
+ pub fn load_default_shader(ctx: &context::Context) -> shader::Shader {
+ shader::Shader::new(
+ ctx,
+ include_str!("assets/shaders/scene/vert.glsl"),
+ include_str!("assets/shaders/scene/frag.glsl")
+ )
+ }
+ pub fn from_gltf(ctx: &context::Context, bytes: &[u8]) -> Self {
+ let (gltf, buffers, images) = gltf::import_slice(bytes).expect("failed to parse GLTF");
+ let get_buffer_data = |b: gltf::Buffer| {
+ buffers.get(b.index()).map(|gltf::buffer::Data(bytes)| bytes.as_slice())
+ };
+ let objects = gltf.meshes().map(|m| {
+ let primitives = m.primitives().map(|p| {
+ let mode = match p.mode() {
+ gltf::mesh::Mode::Points => glow::POINTS,
+ gltf::mesh::Mode::Lines => glow::LINES,
+ gltf::mesh::Mode::LineLoop => glow::LINE_LOOP,
+ gltf::mesh::Mode::LineStrip => glow::LINE_STRIP,
+ gltf::mesh::Mode::Triangles => glow::TRIANGLES,
+ gltf::mesh::Mode::TriangleStrip => glow::TRIANGLE_STRIP,
+ gltf::mesh::Mode::TriangleFan => glow::TRIANGLE_FAN,
+ };
+ unsafe {
+ let vao = ctx.gl.create_vertex_array().expect("failed to initialize vao");
+ ctx.gl.bind_vertex_array(Some(vao));
+
+ // in the past, I've been lazy and just uploaded whole buffers to the GPU.
+ // this is certainly not the right thing to do in general.
+ // perhaps I am misunderstanding, but it feels like GLTF makes it pretty difficult to do
+ // that in general, on account of things like sparse accessors.
+ // instead, we'll use the gltf crate's handy "reader" abstraction to iterate over all of the
+ // data in the buffers, assemble it ourselves, and then upload that.
+ let reader = p.reader(get_buffer_data);
+
+ // on to the actual vertex data.
+ // this is the layout of a single vertex in the buffer we send to the GPU.
+ struct Vertex {
+ pos: glam::Vec3,
+ normal: glam::Vec3,
+ texcoord: glam::Vec2,
+ joints: glam::Vec4,
+ weights: glam::Vec4,
+ }
+
+ // vertices always have positions
+ let mut vertices = Vec::new();
+ for pos in reader.read_positions().expect("primitive has no positions") {
+ vertices.push(Vertex {
+ pos: glam::Vec3::from_array(pos),
+ normal: glam::Vec3::default(),
+ texcoord: glam::Vec2::default(),
+ joints: glam::Vec4::default(),
+ weights: glam::Vec4::default(),
+ });
+ }
+
+ // if we find indices, use those. otherwise generate indices
+ let indices: Vec<u32> = if let Some(ri) = reader.read_indices() {
+ ri.into_u32().collect()
+ } else {
+ vertices.iter().enumerate().map(|(i, _)| i as u32).collect()
+ };
+ let indices_bytes: Vec<u8> = indices.iter().flat_map(|x| x.to_ne_bytes()).collect();
+ let indices_buf = ctx.gl.create_buffer().expect("failed to create index buffer object");
+ ctx.gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(indices_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ELEMENT_ARRAY_BUFFER,
+ &indices_bytes,
+ glow::STATIC_DRAW,
+ );
+
+ // optionally, we might have some other vertex attributes too
+ if let Some(iter) = reader.read_normals() {
+ for (i, n) in iter.enumerate() {
+ vertices[i].normal = glam::Vec3::from_array(n)
+ }
+ }
+ if let Some(iter) = reader.read_tex_coords(0) {
+ for (i, uv) in iter.into_f32().enumerate() {
+ vertices[i].texcoord = glam::Vec2::from_array(uv)
+ }
+ }
+ if let Some(iter) = reader.read_joints(0) {
+ for (i, j) in iter.into_u16().enumerate() {
+ vertices[i].joints = glam::Vec4::from_slice(&j.into_iter().map(|x| x as f32).collect::<Vec<f32>>())
+ }
+ }
+ if let Some(iter) = reader.read_weights(0) {
+ for (i, w) in iter.into_f32().enumerate() {
+ vertices[i].weights = glam::Vec4::from_array(w)
+ }
+ }
+
+ let vertex_size = std::mem::size_of::<Vertex>() as i32;
+ let vertices_buf = ctx.gl.create_buffer().expect("failed to create buffer object");
+ ctx.gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertices_buf));
+ ctx.gl.buffer_data_u8_slice(
+ glow::ARRAY_BUFFER,
+ std::slice::from_raw_parts(
+ vertices.as_ptr() as _,
+ vertices.len() * (vertex_size as usize),
+ ),
+ glow::STATIC_DRAW,
+ );
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_VERTEX);
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_VERTEX, 3, glow::FLOAT, false, vertex_size, offset_of!(Vertex, pos) as _);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_NORMAL);
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_NORMAL, 3, glow::FLOAT, false, vertex_size, offset_of!(Vertex, normal) as _);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_TEXCOORD);
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_TEXCOORD, 2, glow::FLOAT, false, vertex_size, offset_of!(Vertex, texcoord) as _);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_JOINT);
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_JOINT, 4, glow::FLOAT, false, vertex_size, offset_of!(Vertex, joints) as _);
+ ctx.gl.enable_vertex_attrib_array(mesh::ATTRIB_WEIGHT);
+ ctx.gl.vertex_attrib_pointer_f32(mesh::ATTRIB_WEIGHT, 4, glow::FLOAT, false, vertex_size, offset_of!(Vertex, weights) as _);
+
+
+ Primitive {
+ mesh: mesh::Mesh {
+ vao,
+ vbo_vertex: vertices_buf,
+ vbo_index: indices_buf,
+ vbo_normal: None,
+ vbo_texcoord: None,
+ mode,
+ index_count: indices.len(),
+ index_type: glow::UNSIGNED_INT,
+ index_offset: 0,
+ },
+ material: p.material().index().unwrap(),
+ }
+ }
+ }).collect();
+ Object {
+ primitives,
+ }
+ }).collect();
+ let textures: Vec<texture::Texture> = images.into_iter().map(|bi| {
+ unsafe {
+ let i = bi.image.into_rgba8();
+ let tex = ctx.gl.create_texture().expect("failed to create texture");
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex));
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_image_2d(
+ glow::TEXTURE_2D,
+ 0,
+ glow::RGBA as i32,
+ i.width() as i32,
+ i.height() as i32,
+ 0,
+ glow::RGBA,
+ glow::UNSIGNED_BYTE,
+ Some(i.as_bytes()),
+ );
+ ctx.gl.generate_mipmap(glow::TEXTURE_2D);
+ texture::Texture { tex, width: i.width() as i32, height: i.height() as i32 }
+ }
+ }).collect();
+ let materials: Vec<Material> = gltf.materials().map(|m| {
+ let pbr = m.pbr_metallic_roughness();
+ let [bcr, bcg, bcb, bca] = pbr.base_color_factor();
+ let [emx, emy, emz] = m.emissive_factor();
+ Material {
+ base_color_factor: glam::Vec4::new(bcr, bcg, bcb, bca),
+ base_color_texture: pbr.base_color_texture().map(|tex| tex.texture().source().index()),
+ metallic_factor: pbr.metallic_factor(),
+ roughness_factor: pbr.roughness_factor(),
+ metallic_roughness_texture: pbr.metallic_roughness_texture().map(|tex| tex.texture().source().index()),
+ normal_texture: m.normal_texture().map(|tex| tex.texture().source().index()),
+ occlusion_texture: m.occlusion_texture().map(|tex| tex.texture().source().index()),
+ emissive_factor: glam::Vec3::new(emx, emy, emz),
+ emissive_texture: m.emissive_texture().map(|tex| tex.texture().source().index()),
+ }
+ }).collect();
+
+ let skins = gltf.skins().map(|s| {
+ let ibm = s.reader(get_buffer_data).read_inverse_bind_matrices()
+ .expect("missing read inverse bind matrices")
+ .map(|m| glam::Mat4::from_cols_array_2d(&m)).collect();
+ Skin {
+ inverse_bind_matrices: ibm,
+ joints: s.joints().map(|j| j.index()).collect(),
+ }
+ }).collect();
+
+ let animations = HashMap::from_iter(gltf.animations().filter_map(|a| {
+ let channels = a.channels().map(|c| {
+ let read = c.reader(get_buffer_data);
+ Channel {
+ target: c.target().node().index(),
+ interpolation: match c.sampler().interpolation() {
+ gltf::animation::Interpolation::Linear => Interpolation::Linear,
+ gltf::animation::Interpolation::Step => Interpolation::Step,
+ gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline,
+ },
+ keyframes: read.read_inputs().expect("channel has no inputs").collect(),
+ values: match read.read_outputs().expect("channel has no outputs") {
+ gltf::animation::util::ReadOutputs::Translations(ts) =>
+ ChannelValues::Translation(ts.map(glam::Vec3::from_array).collect()),
+ gltf::animation::util::ReadOutputs::Rotations(ts) =>
+ ChannelValues::Rotation(ts.into_f32().map(glam::Quat::from_array).collect()),
+ gltf::animation::util::ReadOutputs::Scales(ts) =>
+ ChannelValues::Scale(ts.map(glam::Vec3::from_array).collect()),
+ _ => panic!("unsupport channel outputs"),
+ },
+ }
+ }).collect();
+ a.name().map(|nm| (nm.to_owned(), Animation { channels }))
+ }));
+
+ let nodes = gltf.nodes().map(|n| {
+ Node {
+ children: n.children().map(|c| c.index()).collect(),
+ object: n.mesh().map(|m| m.index()),
+ skin: n.skin().map(|s| s.index()),
+ transform: glam::Mat4::from_cols_array_2d(&n.transform().matrix()),
+ }
+ }).collect();
+
+ let mut nodes_by_name = HashMap::new();
+ for n in gltf.nodes() {
+ if let Some(nm) = n.name() {
+ nodes_by_name.insert(nm.to_owned(), n.index());
+ }
+ }
+
+ let scene_nodes = gltf.default_scene().unwrap().nodes().map(|n| n.index()).collect();
+
+ Self {
+ objects,
+ textures,
+ materials,
+ skins,
+ animations,
+ nodes,
+ nodes_by_name,
+ scene_nodes,
+ }
+ }
+
+ pub fn compute_joint_matrices(&self, skin: &Skin) -> Vec<glam::Mat4> {
+ let mut q: VecDeque<(Index, glam::Mat4)> = VecDeque::new();
+ q.push_back((skin.joints[0], glam::Mat4::IDENTITY));
+ let mut transforms = vec![glam::Mat4::IDENTITY; self.nodes.len()];
+ while let Some((ni, m)) = q.pop_front() {
+ let n = &self.nodes[ni];
+ transforms[ni] = m.mul_mat4(&n.transform);
+ for ci in &n.children {
+ q.push_back((*ci, transforms[ni]));
+ }
+ }
+ let mut ret = vec![glam::Mat4::IDENTITY; skin.joints.len()];
+ for (idx, ni) in skin.joints.iter().enumerate() {
+ ret[idx] = transforms[*ni].mul_mat4(&skin.inverse_bind_matrices[idx]);
+ }
+ ret
+ }
+
+ fn render_node(&self, ctx: &context::Context, shader: &shader::Shader, n: &Node) {
+ if let Some(o) = n.object.and_then(|i| self.objects.get(i)) {
+ if let Some(s) = n.skin.and_then(|i| self.skins.get(i)) {
+ let jms = self.compute_joint_matrices(s);
+ shader.set_mat4_array(ctx, "joint_matrices[0]", &jms);
+ }
+ for p in &o.primitives {
+ if let Some(tex) = self.materials.get(p.material)
+ .and_then(|m| m.base_color_texture)
+ .and_then(|t| self.textures.get(t)) {
+ tex.bind(ctx);
+ } else {
+ texture::Texture::bind_initial(ctx);
+ }
+ p.mesh.render(ctx);
+ }
+ }
+ }
+
+ pub fn render(&self, ctx: &context::Context, shader: &shader::Shader) {
+ let mut q: VecDeque<Index> = VecDeque::new();
+ for sn in &self.scene_nodes {
+ q.push_back(*sn);
+ }
+ while let Some(ni) = q.pop_front() {
+ let n = &self.nodes[ni];
+ self.render_node(ctx, shader, n);
+ for ci in &n.children {
+ q.push_back(*ci);
+ }
+ }
+ }
+
+ pub fn reflect_animation(&mut self, nm: &str, time: f32) {
+ if let Some(anim) = self.animations.get(nm) {
+ for c in &anim.channels {
+ let (previ, nexti) = if let Some(nexti) = c.keyframes.iter().position(|x| *x > time) {
+ let previ = if nexti > 0 { nexti - 1 } else { c.keyframes.len() - 1 };
+ (previ, nexti)
+ } else {
+ (c.keyframes.len() - 1, 0)
+ };
+ if let ChannelValues::Rotation(vs) = &c.values {
+ let prevt = c.keyframes[previ];
+ let nextt = c.keyframes[nexti];
+ let prev = vs[previ];
+ let next = vs[nexti];
+ let new = prev.slerp(next, (time - prevt) / (nextt - prevt));
+ let (scale, _, trans) = self.nodes[c.target].transform.to_scale_rotation_translation();
+ self.nodes[c.target].transform = glam::Mat4::from_scale_rotation_translation(
+ scale,
+ new,
+ trans,
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shader.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shader.rs
new file mode 100644
index 0000000..d18ba27
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shader.rs
@@ -0,0 +1,260 @@
+use std::collections::HashMap;
+
+use glow::HasContext;
+
+use crate::{context, font, mesh, state};
+
+const COMMON_VERT: &str = include_str!("assets/shaders/common/vert.glsl");
+const COMMON_FRAG: &str = include_str!("assets/shaders/common/frag.glsl");
+
+#[derive(Clone)]
+pub struct Shader {
+ pub program: glow::Program,
+ pub uniforms: std::rc::Rc<HashMap<String, glow::UniformLocation>>
+}
+
+impl Shader {
+ pub fn new_helper(ctx: &context::Context, vsrc: &str, fsrc: &str) -> Result<Self, String> {
+ unsafe {
+ let program = ctx.gl.create_program()?;
+
+ let vert = ctx.gl.create_shader(glow::VERTEX_SHADER)?;
+ ctx.gl.shader_source(vert, vsrc);
+ ctx.gl.compile_shader(vert);
+ if !ctx.gl.get_shader_compile_status(vert) {
+ return Err(format!(
+ "failed to compile vertex shader:\n{}",
+ ctx.gl.get_shader_info_log(vert)
+ ));
+ }
+ ctx.gl.attach_shader(program, vert);
+
+ let frag = ctx.gl.create_shader(glow::FRAGMENT_SHADER)?;
+ ctx.gl.shader_source(frag, fsrc);
+ ctx.gl.compile_shader(frag);
+ if !ctx.gl.get_shader_compile_status(frag) {
+ return Err(format!(
+ "failed to compile fragment shader:\n{}",
+ ctx.gl.get_shader_info_log(frag)
+ ));
+ }
+ ctx.gl.attach_shader(program, frag);
+
+ ctx.gl.bind_attrib_location(program, mesh::ATTRIB_VERTEX, "vertex");
+ ctx.gl.bind_attrib_location(program, mesh::ATTRIB_NORMAL, "normal");
+ ctx.gl.bind_attrib_location(program, mesh::ATTRIB_TEXCOORD, "texcoord");
+ ctx.gl.bind_attrib_location(program, mesh::ATTRIB_JOINT, "joint");
+ ctx.gl.bind_attrib_location(program, mesh::ATTRIB_WEIGHT, "weight");
+ ctx.gl.bind_attrib_location(program, mesh::ATTRIB_COLOR, "color");
+
+ ctx.gl.link_program(program);
+ if !ctx.gl.get_program_link_status(program) {
+ return Err(format!(
+ "failed to link shader program:\n{}",
+ ctx.gl.get_program_info_log(program),
+ ));
+ }
+
+ ctx.gl.detach_shader(program, vert);
+ ctx.gl.delete_shader(vert);
+ ctx.gl.detach_shader(program, frag);
+ ctx.gl.delete_shader(frag);
+
+ ctx.gl.use_program(Some(program));
+
+ let mut uniforms = HashMap::new();
+ for index in 0..ctx.gl.get_active_uniforms(program) {
+ if let Some(active) = ctx.gl.get_active_uniform(program, index) {
+ if let Some(loc) = ctx.gl.get_uniform_location(program, &active.name) {
+ uniforms.insert(active.name, loc);
+ } else {
+ log::warn!("failed to get location for uniform: {}", active.name);
+ }
+ } else {
+ log::warn!("failed to get active uniform for index: {}", index);
+ }
+ }
+
+ Ok(Self {
+ program,
+ uniforms: std::rc::Rc::new(uniforms),
+ })
+ }
+ }
+
+ pub fn new_nolib(ctx: &context::Context, vsrc: &str, fsrc: &str) -> Self {
+ match Self::new_helper(ctx, vsrc, fsrc) {
+ Ok(x) => x,
+ Err(e) => panic!("failed to load no-library shader: {}", e)
+ }
+ }
+
+ pub fn new(ctx: &context::Context, vsrcstr: &str, fsrcstr: &str) -> Self {
+ let vsrc = format!("{}\n{}\n", COMMON_VERT, vsrcstr);
+ let fsrc = format!("{}\n{}\n", COMMON_FRAG, fsrcstr);
+ Self::new_nolib(ctx, &vsrc, &fsrc)
+ }
+
+ pub fn delete(&mut self, ctx: &context::Context) {
+ unsafe { ctx.gl.delete_program(self.program); }
+ }
+
+ pub fn replace(&mut self, ctx: &context::Context, vsrc: &str, fsrc: &str) -> Result<(), String> {
+ self.delete(ctx);
+ *self = Self::new_helper(ctx, vsrc, fsrc)?;
+ Ok(())
+ }
+
+ pub fn set_i32(&self, ctx: &context::Context, name: &str, val: i32) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe { ctx.gl.uniform_1_i32(Some(loc), val) }
+ }
+ }
+
+ pub fn set_i32_array(&self, ctx: &context::Context, name: &str, val: &[i32]) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe {
+ ctx.gl.uniform_1_i32_slice(Some(loc), val)
+ }
+ }
+ }
+
+ pub fn set_f32(&self, ctx: &context::Context, name: &str, val: f32) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe { ctx.gl.uniform_1_f32(Some(loc), val) }
+ }
+ }
+
+ pub fn set_vec2(&self, ctx: &context::Context, name: &str, val: &glam::Vec2) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe {
+ ctx.gl.uniform_2_f32(
+ Some(loc),
+ val.x,
+ val.y,
+ );
+ }
+ }
+ }
+
+ pub fn set_vec2_array(&self, ctx: &context::Context, name: &str, val: &[glam::Vec2]) {
+ if let Some(loc) = self.uniforms.get(name) {
+ let vs: Vec<f32> = val.iter().flat_map(|v| [v.x, v.y]).collect();
+ unsafe {
+ ctx.gl.uniform_2_f32_slice(
+ Some(loc),
+ &vs,
+ );
+ }
+ }
+ }
+
+ pub fn set_vec3(&self, ctx: &context::Context, name: &str, val: &glam::Vec3) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe {
+ ctx.gl.uniform_3_f32(
+ Some(loc),
+ val.x,
+ val.y,
+ val.z,
+ );
+ }
+ }
+ }
+
+ pub fn set_vec3_array(&self, ctx: &context::Context, name: &str, val: &[glam::Vec3]) {
+ if let Some(loc) = self.uniforms.get(name) {
+ let vs: Vec<f32> = val.iter().flat_map(|v| [v.x, v.y, v.z]).collect();
+ unsafe {
+ ctx.gl.uniform_3_f32_slice(
+ Some(loc),
+ &vs,
+ );
+ }
+ }
+ }
+
+ pub fn set_vec4(&self, ctx: &context::Context, name: &str, val: &glam::Vec4) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe {
+ ctx.gl.uniform_4_f32(
+ Some(loc),
+ val.x,
+ val.y,
+ val.z,
+ val.w,
+ );
+ }
+ }
+ }
+
+ pub fn set_mat4(&self, ctx: &context::Context, name: &str, val: &glam::Mat4) {
+ if let Some(loc) = self.uniforms.get(name) {
+ unsafe {
+ ctx.gl.uniform_matrix_4_f32_slice(
+ Some(loc),
+ false,
+ &val.to_cols_array(),
+ );
+ }
+ }
+ }
+
+ pub fn set_mat4_array(&self, ctx: &context::Context, name: &str, val: &[glam::Mat4]) {
+ if let Some(loc) = self.uniforms.get(name) {
+ let vs: Vec<f32> = val.iter().flat_map(|m| m.to_cols_array()).collect();
+ unsafe {
+ ctx.gl.uniform_matrix_4_f32_slice(
+ Some(loc),
+ false,
+ &vs,
+ );
+ }
+ }
+ }
+
+ pub fn set_position_3d(&self, ctx: &context::Context, _st: &state::State, position: &glam::Mat4) {
+ self.set_mat4(ctx, "position", position);
+ self.set_mat4(ctx, "normal_matrix", &position.inverse().transpose());
+ }
+
+ pub fn set_position_2d_mat(&self, ctx: &context::Context, st: &state::State, position: &glam::Mat4) {
+ self.set_mat4(ctx, "position", position);
+ }
+
+ pub fn set_position_2d_helper(&self, ctx: &context::Context, st: &state::State, pos: &glam::Vec2, dims: &glam::Vec2, rot: &glam::Quat) {
+ let halfwidth = dims.x / 2.0;
+ let halfheight = dims.y / 2.0;
+ self.set_mat4(
+ ctx, "position",
+ &glam::Mat4::from_scale_rotation_translation(
+ glam::Vec3::new(halfwidth, halfheight, 1.0),
+ *rot,
+ glam::Vec3::new(
+ -st.render_dims.x / 2.0 + pos.x + halfwidth,
+ st.render_dims.y / 2.0 - pos.y - halfheight,
+ 0.0,
+ ),
+ )
+ );
+ }
+
+ pub fn set_position_2d(&self, ctx: &context::Context, st: &state::State, pos: &glam::Vec2, dims: &glam::Vec2) {
+ self.set_position_2d_helper(ctx, st, pos, dims, &glam::Quat::IDENTITY)
+ }
+
+ pub fn set_position_text_bitmap(&self,
+ ctx: &context::Context, st: &state::State,
+ font: &font::Bitmap,
+ pos: &glam::Vec2
+ ) {
+ let dims = glam::Vec2::new(font.char_width as f32, font.char_height as f32);
+ self.set_position_2d_helper(ctx, st, pos, &dims, &glam::Quat::IDENTITY)
+ }
+
+ pub fn bind(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.use_program(Some(self.program));
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shadow.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shadow.rs
new file mode 100644
index 0000000..8482179
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/shadow.rs
@@ -0,0 +1,127 @@
+use glow::HasContext;
+
+use crate::context;
+
+pub struct ShadowBuffer {
+ pub fbo: glow::Framebuffer,
+ pub depth_tex: glow::Texture,
+ pub width: i32,
+ pub height: i32,
+}
+
+impl ShadowBuffer {
+ pub fn new(ctx: &context::Context, w: i32, h: i32) -> Self {
+ unsafe {
+ // generate and bind FBO
+ let fbo = ctx.gl.create_framebuffer().expect("failed to create framebuffer");
+ ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
+
+ // generate and attach depth buffer
+ let depth_tex = ctx.gl.create_texture().expect("failed to create texture");
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(depth_tex));
+ ctx.gl.tex_image_2d(
+ glow::TEXTURE_2D,
+ 0,
+ glow::DEPTH_COMPONENT as i32,
+ w,
+ h,
+ 0,
+ glow::DEPTH_COMPONENT,
+ glow::FLOAT,
+ None,
+ );
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_BORDER as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_BORDER as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_COMPARE_MODE, glow::COMPARE_REF_TO_TEXTURE as i32);
+ ctx.gl.tex_parameter_f32_slice(glow::TEXTURE_2D, glow::TEXTURE_BORDER_COLOR, &[1.0, 1.0, 1.0, 1.0]);
+ ctx.gl.framebuffer_texture_2d(glow::FRAMEBUFFER, glow::DEPTH_ATTACHMENT, glow::TEXTURE_2D, Some(depth_tex), 0);
+ ctx.gl.draw_buffer(glow::NONE);
+ ctx.gl.read_buffer(glow::NONE);
+
+ let status = ctx.gl.check_framebuffer_status(glow::FRAMEBUFFER);
+ if status != glow::FRAMEBUFFER_COMPLETE {
+ panic!("error initializing framebuffer: {}", status);
+ }
+ Self {
+ fbo,
+ depth_tex,
+ width: w,
+ height: h,
+ }
+ }
+ }
+
+ pub fn bind(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.fbo));
+ ctx.gl.viewport(0, 0, self.width as _, self.height as _);
+ ctx.gl.clear(glow::DEPTH_BUFFER_BIT);
+
+ }
+ }
+}
+
+pub struct ShadowBuffer3D {
+ pub fbo: glow::Framebuffer,
+ pub depth_cubemap: glow::Texture,
+ pub width: i32,
+ pub height: i32,
+}
+
+impl ShadowBuffer3D {
+ pub fn new(ctx: &context::Context, w: i32, h: i32) -> Self {
+ unsafe {
+ // generate and bind FBO
+ let fbo = ctx.gl.create_framebuffer().expect("failed to create framebuffer");
+ ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(fbo));
+
+ // generate and attach depth buffer
+ let depth_cubemap = ctx.gl.create_texture().expect("failed to create texture");
+ ctx.gl.bind_texture(glow::TEXTURE_CUBE_MAP, Some(depth_cubemap));
+ for i in 0..6 {
+ ctx.gl.tex_image_2d(
+ glow::TEXTURE_CUBE_MAP_POSITIVE_X + i,
+ 0,
+ glow::DEPTH_COMPONENT as i32,
+ w,
+ h,
+ 0,
+ glow::DEPTH_COMPONENT,
+ glow::FLOAT,
+ None,
+ );
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_CUBE_MAP, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_CUBE_MAP, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_CUBE_MAP, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_CUBE_MAP, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_CUBE_MAP, glow::TEXTURE_WRAP_R, glow::CLAMP_TO_EDGE as i32);
+ }
+
+ ctx.gl.framebuffer_texture(glow::FRAMEBUFFER, glow::DEPTH_ATTACHMENT, Some(depth_cubemap), 0);
+ ctx.gl.draw_buffer(glow::NONE);
+ ctx.gl.read_buffer(glow::NONE);
+
+ let status = ctx.gl.check_framebuffer_status(glow::FRAMEBUFFER);
+ if status != glow::FRAMEBUFFER_COMPLETE {
+ panic!("error initializing framebuffer: {}", status);
+ }
+ Self {
+ fbo,
+ depth_cubemap,
+ width: w,
+ height: h,
+ }
+ }
+ }
+
+ pub fn bind(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.bind_framebuffer(glow::FRAMEBUFFER, Some(self.fbo));
+ ctx.gl.viewport(0, 0, self.width as _, self.height as _);
+ ctx.gl.clear(glow::DEPTH_BUFFER_BIT);
+
+ }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/state.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/state.rs
new file mode 100644
index 0000000..3353f33
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/state.rs
@@ -0,0 +1,735 @@
+#![allow(dead_code, unused_variables)]
+use bimap::BiHashMap;
+use enum_map::{Enum, EnumMap, enum_map};
+use serde::{Deserialize, Serialize};
+use std::{collections::HashMap, fmt::Display};
+use strum::EnumIter;
+
+#[cfg(not(target_arch = "wasm32"))]
+use glow::HasContext;
+
+use crate::{audio, context, font, framebuffer, mesh, shader, utils};
+
+pub type Tick = u64;
+
+const DELTA_TIME: f64 = 0.016; // todo
+
+pub const ORTH_WIDTH: f32 = 7.55869;
+pub const ORTH_HEIGHT: f32 = 5.03913;
+
+pub trait Game {
+ fn initialize(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn finalize(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn initialize_audio(
+ &self,
+ ctx: &context::Context,
+ st: &State,
+ actx: &audio::Context,
+ ) -> HashMap<String, audio::Audio> {
+ HashMap::new()
+ }
+ fn keybindings_were_reset(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn mouse_move(
+ &mut self,
+ ctx: &context::Context,
+ st: &mut State,
+ x: i32,
+ y: i32,
+ ) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn mouse_press(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn mouse_released(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn update(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+ fn render(&mut self, ctx: &context::Context, st: &mut State) -> utils::Erm<()> {
+ Ok(())
+ }
+}
+
+#[derive(Debug, Enum, EnumIter, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+pub enum Key {
+ Up,
+ Down,
+ Left,
+ Right,
+ A,
+ B,
+ X,
+ Y,
+ L,
+ R,
+ Start,
+ Select,
+ Debug,
+}
+#[derive(Default)]
+pub struct Keys {
+ pub pressed: EnumMap<Key, bool>,
+ pub new: EnumMap<Key, bool>,
+}
+impl Keys {
+ pub fn new() -> Self {
+ Self::default()
+ }
+ pub fn up(&self) -> bool {
+ self.pressed[Key::Up]
+ }
+ pub fn down(&self) -> bool {
+ self.pressed[Key::Down]
+ }
+ pub fn left(&self) -> bool {
+ self.pressed[Key::Left]
+ }
+ pub fn right(&self) -> bool {
+ self.pressed[Key::Right]
+ }
+ pub fn a(&self) -> bool {
+ self.pressed[Key::A]
+ }
+ pub fn b(&self) -> bool {
+ self.pressed[Key::B]
+ }
+ pub fn x(&self) -> bool {
+ self.pressed[Key::X]
+ }
+ pub fn y(&self) -> bool {
+ self.pressed[Key::Y]
+ }
+ pub fn l(&self) -> bool {
+ self.pressed[Key::L]
+ }
+ pub fn r(&self) -> bool {
+ self.pressed[Key::R]
+ }
+ pub fn start(&self) -> bool {
+ self.pressed[Key::Start]
+ }
+ pub fn select(&self) -> bool {
+ self.pressed[Key::Select]
+ }
+ pub fn debug(&self) -> bool {
+ self.pressed[Key::Debug]
+ }
+ pub fn new_up(&mut self) -> bool {
+ let ret = self.new[Key::Up];
+ self.new[Key::Up] = false;
+ ret
+ }
+ pub fn new_down(&mut self) -> bool {
+ let ret = self.new[Key::Down];
+ self.new[Key::Down] = false;
+ ret
+ }
+ pub fn new_left(&mut self) -> bool {
+ let ret = self.new[Key::Left];
+ self.new[Key::Left] = false;
+ ret
+ }
+ pub fn new_right(&mut self) -> bool {
+ let ret = self.new[Key::Right];
+ self.new[Key::Right] = false;
+ ret
+ }
+ pub fn new_a(&mut self) -> bool {
+ let ret = self.new[Key::A];
+ self.new[Key::A] = false;
+ ret
+ }
+ pub fn new_b(&mut self) -> bool {
+ let ret = self.new[Key::B];
+ self.new[Key::B] = false;
+ ret
+ }
+ pub fn new_x(&mut self) -> bool {
+ let ret = self.new[Key::X];
+ self.new[Key::X] = false;
+ ret
+ }
+ pub fn new_y(&mut self) -> bool {
+ let ret = self.new[Key::Y];
+ self.new[Key::Y] = false;
+ ret
+ }
+ pub fn new_l(&mut self) -> bool {
+ let ret = self.new[Key::L];
+ self.new[Key::L] = false;
+ ret
+ }
+ pub fn new_r(&mut self) -> bool {
+ let ret = self.new[Key::R];
+ self.new[Key::R] = false;
+ ret
+ }
+ pub fn new_start(&mut self) -> bool {
+ let ret = self.new[Key::Start];
+ self.new[Key::Start] = false;
+ ret
+ }
+ pub fn new_select(&mut self) -> bool {
+ let ret = self.new[Key::Select];
+ self.new[Key::Select] = false;
+ ret
+ }
+ pub fn new_debug(&mut self) -> bool {
+ let ret = self.new[Key::Debug];
+ self.new[Key::Debug] = false;
+ ret
+ }
+}
+
+pub struct PointLight {
+ pub pos: glam::Vec3,
+ pub color: glam::Vec3,
+ pub attenuation: glam::Vec2,
+}
+
+type Timestamp = f64;
+
+#[cfg(target_arch = "wasm32")]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Keycode {
+ pub kc: winit::keyboard::KeyCode,
+}
+#[cfg(target_arch = "wasm32")]
+impl Keycode {
+ pub fn new(kc: winit::keyboard::KeyCode) -> Self {
+ Self { kc }
+ }
+}
+#[cfg(target_arch = "wasm32")]
+impl Display for Keycode {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self.kc)
+ }
+}
+#[cfg(target_arch = "wasm32")]
+impl Serialize for Keycode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.kc.serialize(serializer)
+ }
+}
+#[cfg(target_arch = "wasm32")]
+impl<'de> Deserialize<'de> for Keycode {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let kc = winit::keyboard::KeyCode::deserialize(deserializer)?;
+ Ok(Self { kc })
+ }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct Keycode {
+ pub kc: glfw::Key,
+}
+#[cfg(not(target_arch = "wasm32"))]
+impl Keycode {
+ pub fn new(kc: glfw::Key) -> Self {
+ Self { kc }
+ }
+}
+#[cfg(not(target_arch = "wasm32"))]
+impl Display for Keycode {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self.kc)
+ }
+}
+#[cfg(not(target_arch = "wasm32"))]
+impl Serialize for Keycode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ (self.kc as i32).serialize(serializer)
+ }
+}
+#[cfg(not(target_arch = "wasm32"))]
+impl<'de> Deserialize<'de> for Keycode {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ i32::deserialize(deserializer).map(|x| unsafe { std::mem::transmute(x) })
+ }
+}
+
+pub struct State {
+ pub tick: Tick,
+ pub nextframe: Timestamp,
+ pub fps: u32,
+ pub frames_this_second: u32,
+ pub start_this_second: Timestamp,
+
+ pub rebinding: Option<Key>,
+ pub keybindings: BiHashMap<Keycode, Key>,
+ pub keys: Keys,
+
+ pub screen: framebuffer::Framebuffer,
+ pub render_framebuffer: framebuffer::Framebuffer,
+ pub render_dims: glam::Vec2,
+ pub shader_upscale: shader::Shader,
+ pub shader_text_bitmap: shader::Shader,
+ pub mesh_square: mesh::Mesh,
+ pub font_default: font::Bitmap,
+ pub font_small: font::Bitmap,
+ pub audio: Option<audio::Assets>,
+
+ pub projection: glam::Mat4,
+ pub projection_orth: glam::Mat4,
+ pub camera: (glam::Vec3, glam::Vec3, glam::Vec3),
+ pub lighting: (glam::Vec3, glam::Vec3, glam::Vec3),
+ pub point_lights: Vec<PointLight>,
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn now(ctx: &context::Context) -> Timestamp {
+ ctx.performance.now() / 1000.0
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn now(ctx: &context::Context) -> Timestamp {
+ let elapsed = ctx.start_instant.elapsed();
+ let ms = elapsed.as_millis();
+ (ms as f64) / 1000.0
+}
+
+#[cfg(target_arch = "wasm32")]
+pub fn default_keybindings() -> BiHashMap<Keycode, Key> {
+ BiHashMap::from_iter(vec![
+ (Keycode::new(winit::keyboard::KeyCode::KeyW), Key::Up),
+ (Keycode::new(winit::keyboard::KeyCode::KeyS), Key::Down),
+ (Keycode::new(winit::keyboard::KeyCode::KeyA), Key::Left),
+ (Keycode::new(winit::keyboard::KeyCode::KeyD), Key::Right),
+ (Keycode::new(winit::keyboard::KeyCode::Digit1), Key::A),
+ (Keycode::new(winit::keyboard::KeyCode::Digit2), Key::B),
+ (Keycode::new(winit::keyboard::KeyCode::Digit3), Key::X),
+ (Keycode::new(winit::keyboard::KeyCode::Digit4), Key::Y),
+ (Keycode::new(winit::keyboard::KeyCode::KeyQ), Key::L),
+ (Keycode::new(winit::keyboard::KeyCode::KeyE), Key::R),
+ (Keycode::new(winit::keyboard::KeyCode::Tab), Key::Start),
+ (Keycode::new(winit::keyboard::KeyCode::Space), Key::Select),
+ (
+ Keycode::new(winit::keyboard::KeyCode::Backquote),
+ Key::Debug,
+ ),
+ ])
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+pub fn default_keybindings() -> BiHashMap<Keycode, Key> {
+ BiHashMap::from_iter(vec![
+ (Keycode::new(glfw::Key::W), Key::Up),
+ (Keycode::new(glfw::Key::S), Key::Down),
+ (Keycode::new(glfw::Key::A), Key::Left),
+ (Keycode::new(glfw::Key::D), Key::Right),
+ (Keycode::new(glfw::Key::Num1), Key::A),
+ (Keycode::new(glfw::Key::Num2), Key::B),
+ (Keycode::new(glfw::Key::Num3), Key::X),
+ (Keycode::new(glfw::Key::Num4), Key::Y),
+ (Keycode::new(glfw::Key::Q), Key::L),
+ (Keycode::new(glfw::Key::E), Key::R),
+ (Keycode::new(glfw::Key::Tab), Key::Start),
+ (Keycode::new(glfw::Key::Space), Key::Select),
+ (Keycode::new(glfw::Key::GraveAccent), Key::Debug),
+ ])
+}
+
+impl State {
+ pub fn new(ctx: &context::Context) -> Self {
+ let screen = framebuffer::Framebuffer::screen(ctx);
+ let render_framebuffer = framebuffer::Framebuffer::new(
+ ctx,
+ &glam::Vec2::new(ctx.render_width, ctx.render_height),
+ &glam::Vec2::new(0.0, 0.0),
+ );
+ let shader_upscale = shader::Shader::new_nolib(
+ ctx,
+ include_str!("assets/shaders/scale/vert.glsl"),
+ include_str!("assets/shaders/scale/frag.glsl"),
+ );
+ let shader_text_bitmap = shader::Shader::new_nolib(
+ ctx,
+ include_str!("assets/shaders/bitmap/vert.glsl"),
+ include_str!("assets/shaders/bitmap/frag.glsl"),
+ );
+ let mesh_square = mesh::Mesh::from_obj(ctx, include_bytes!("assets/meshes/square.obj"));
+
+ let nextframe = now(ctx);
+
+ Self {
+ // we initialize the tick to 1000, which allows us to use "0" as the default time for
+ // various animation starts on entities without having them all play at game start
+ tick: 1000,
+
+ nextframe,
+ fps: 0,
+ frames_this_second: 0,
+ start_this_second: nextframe,
+
+ rebinding: None,
+ keybindings: default_keybindings(),
+ keys: Keys::new(),
+
+ screen,
+ render_framebuffer,
+ render_dims: glam::Vec2::new(ctx.render_width, ctx.render_height),
+ shader_upscale,
+ shader_text_bitmap,
+ mesh_square,
+ font_default: font::Bitmap::default(ctx),
+ font_small: font::Bitmap::small(ctx),
+ audio: None,
+
+ projection: glam::Mat4::perspective_lh(
+ std::f32::consts::PI / 4.0,
+ ctx.render_width / ctx.render_height,
+ // 0.1,
+ 0.5,
+ 50.0,
+ ),
+ projection_orth: glam::Mat4::orthographic_lh(
+ -ORTH_WIDTH,
+ ORTH_WIDTH,
+ -ORTH_HEIGHT,
+ ORTH_HEIGHT,
+ 0.5,
+ 50.0,
+ ),
+ camera: (
+ glam::Vec3::new(0.0, 0.0, 0.0),
+ glam::Vec3::new(0.0, 0.0, 1.0),
+ glam::Vec3::new(0.0, 1.0, 0.0),
+ ),
+ lighting: (
+ glam::Vec3::new(1.0, 1.0, 1.0),
+ glam::Vec3::new(1.0, 1.0, 1.0),
+ glam::Vec3::new(1.0, -1.0, 1.0),
+ ),
+ point_lights: Vec::new(),
+ // waker_ctx,
+ // http_client: reqwest::Client::new(),
+ // request: None,
+ }
+ }
+
+ pub fn handle_resize(&mut self, ctx: &context::Context) {
+ self.screen = framebuffer::Framebuffer::screen(ctx);
+ }
+
+ pub fn move_camera(
+ &mut self,
+ _ctx: &context::Context,
+ pos: &glam::Vec3,
+ dir: &glam::Vec3,
+ up: &glam::Vec3,
+ ) {
+ self.camera = (*pos, *dir, *up);
+ }
+
+ pub fn bind_framebuffer(&mut self, ctx: &context::Context, fb: &framebuffer::Framebuffer) {
+ fb.bind(ctx);
+ self.render_dims = fb.dims;
+ }
+
+ pub fn bind_render_framebuffer(&mut self, ctx: &context::Context) {
+ self.render_framebuffer.bind(ctx);
+ self.render_dims = self.render_framebuffer.dims;
+ }
+
+ pub fn bind_screen(&mut self, ctx: &context::Context) {
+ self.screen.bind(ctx);
+ self.render_dims = self.screen.dims;
+ }
+
+ pub fn set_lighting(
+ &mut self,
+ _ctx: &context::Context,
+ ambient: &glam::Vec3,
+ color: &glam::Vec3,
+ dir: &glam::Vec3,
+ ) {
+ self.lighting = (*ambient, *color, *dir);
+ }
+
+ pub fn add_point_light(
+ &mut self,
+ _ctx: &context::Context,
+ pos: &glam::Vec3,
+ color: &glam::Vec3,
+ attenuation: &glam::Vec2,
+ ) {
+ self.point_lights.push(PointLight {
+ pos: *pos,
+ color: *color,
+ attenuation: *attenuation,
+ });
+ }
+
+ pub fn clear_point_lights(&mut self, _ctx: &context::Context) {
+ self.point_lights.clear();
+ }
+
+ pub fn view(&self) -> glam::Mat4 {
+ glam::Mat4::look_to_lh(self.camera.0, self.camera.1, self.camera.2)
+ }
+
+ pub fn bind_3d_helper(
+ &self,
+ ctx: &context::Context,
+ shader: &shader::Shader,
+ plc: usize,
+ orth: bool,
+ ) {
+ shader.bind(ctx);
+ shader.set_mat4(
+ ctx,
+ "projection",
+ if orth {
+ &self.projection_orth
+ } else {
+ &self.projection
+ },
+ );
+ shader.set_mat4(ctx, "view", &self.view());
+ shader.set_vec3(ctx, "light_ambient_color", &self.lighting.0);
+ shader.set_vec3(ctx, "light_dir_color", &self.lighting.1);
+ shader.set_vec3(ctx, "light_dir", &self.lighting.2.normalize());
+ shader.set_i32(ctx, "light_count", plc as _);
+ }
+
+ pub fn bind_3d_no_point_lights(&self, ctx: &context::Context, shader: &shader::Shader) {
+ self.bind_3d_helper(ctx, shader, 0, false);
+ }
+
+ pub fn bind_3d_with_point_lights(
+ &self,
+ ctx: &context::Context,
+ shader: &shader::Shader,
+ orth: bool,
+ ) {
+ let plc = self.point_lights.len().min(5);
+ self.bind_3d_helper(ctx, shader, plc, orth);
+ if plc > 0 {
+ let lpos: Vec<_> = self.point_lights.iter().take(plc).map(|l| l.pos).collect();
+ shader.set_vec3_array(ctx, "light_pos[0]", &lpos);
+ let lcolor: Vec<_> = self
+ .point_lights
+ .iter()
+ .take(plc)
+ .map(|l| l.color)
+ .collect();
+ shader.set_vec3_array(ctx, "light_color[0]", &lcolor);
+ let lattenuation: Vec<_> = self
+ .point_lights
+ .iter()
+ .take(plc)
+ .map(|l| l.attenuation)
+ .collect();
+ shader.set_vec2_array(ctx, "light_attenuation[0]", &lattenuation);
+ }
+ }
+
+ pub fn bind_3d(&self, ctx: &context::Context, shader: &shader::Shader) {
+ self.bind_3d_with_point_lights(ctx, shader, false)
+ }
+
+ pub fn bind_3d_orth(&self, ctx: &context::Context, shader: &shader::Shader) {
+ self.bind_3d_with_point_lights(ctx, shader, true)
+ }
+
+ pub fn bind_2d(&self, ctx: &context::Context, shader: &shader::Shader) {
+ shader.bind(ctx);
+ shader.set_mat4(ctx, "projection", &glam::Mat4::IDENTITY);
+ shader.set_mat4(
+ ctx,
+ "view",
+ &glam::Mat4::from_scale(glam::Vec3::new(
+ 2.0 / ctx.render_width,
+ 2.0 / ctx.render_height,
+ 1.0,
+ )),
+ );
+ }
+
+ pub fn initialize_audio<G>(&mut self, ctx: &context::Context, game: &mut G) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ if self.audio.is_none() {
+ self.audio = Some(audio::Assets::new(|actx| {
+ game.initialize_audio(ctx, self, actx)
+ }));
+ }
+ Ok(())
+ }
+
+ pub fn mouse_moved<G>(
+ &mut self,
+ ctx: &context::Context,
+ x: f32,
+ y: f32,
+ game: &mut G,
+ ) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ if ctx.options.contains(crate::Options::FULLSCREEN_MOUSE) {
+ game.mouse_move(
+ ctx,
+ self,
+ (x * ctx.render_width / self.screen.dims.x) as i32,
+ (y * ctx.render_height / self.screen.dims.y) as i32,
+ )?;
+ } else {
+ let rx = ((x - self.screen.offsets.x) * ctx.render_width / self.screen.dims.x) as i32;
+ let ry = ((y - self.screen.offsets.y) * ctx.render_height / self.screen.dims.y) as i32;
+ if !(rx < 0
+ || rx >= ctx.render_width as i32
+ || ry < 0
+ || ry >= ctx.render_height as i32)
+ {
+ game.mouse_move(ctx, self, rx, ry)?;
+ }
+ }
+ Ok(())
+ }
+
+ pub fn mouse_pressed<G>(&mut self, ctx: &context::Context, game: &mut G) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ self.initialize_audio(ctx, game)?;
+ game.mouse_press(ctx, self)?;
+ Ok(())
+ }
+
+ pub fn mouse_released<G>(&mut self, ctx: &context::Context, game: &mut G) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ game.mouse_released(ctx, self)?;
+ Ok(())
+ }
+
+ pub fn key_pressed<G>(
+ &mut self,
+ ctx: &context::Context,
+ game: &mut G,
+ key: Keycode,
+ ) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ #[cfg(target_arch = "wasm32")]
+ let rebind = key.kc == winit::keyboard::KeyCode::F12;
+ #[cfg(not(target_arch = "wasm32"))]
+ let rebind = key.kc == glfw::Key::F12;
+ if rebind {
+ self.keybindings = default_keybindings();
+ self.rebinding = None;
+ game.keybindings_were_reset(ctx, self)?;
+ } else if let Some(k) = self.rebinding {
+ self.keybindings.insert(key, k);
+ self.rebinding = None;
+ } else if let Some(k) = self.keybindings.get_by_left(&key) {
+ self.keys.pressed[*k] = true;
+ self.keys.new[*k] = true;
+ }
+ Ok(())
+ }
+
+ pub fn key_released<G>(
+ &mut self,
+ _ctx: &context::Context,
+ _game: &mut G,
+ key: Keycode,
+ ) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ if let Some(k) = self.keybindings.get_by_left(&key) {
+ self.keys.pressed[*k] = false;
+ }
+ Ok(())
+ }
+
+ /// Return the first keybinding for the given virtual key
+ pub fn keybinding_for(&self, k: Key) -> Option<String> {
+ self.keybindings
+ .get_by_right(&k)
+ .map(|kc| format!("{}", kc))
+ }
+
+ pub fn rebind_key(&mut self, k: Key) {
+ self.rebinding = Some(k);
+ }
+
+ pub fn run_update<G>(&mut self, ctx: &context::Context, game: &mut G) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ let now = now(ctx);
+ if now > self.nextframe {
+ while self.nextframe < now {
+ // find the next target frame that isn't in the past
+ self.nextframe += DELTA_TIME;
+ }
+ self.tick += 1;
+ self.frames_this_second += 1;
+ game.update(ctx, self)?;
+ self.keys.new = enum_map! { _ => false };
+ }
+ if now - self.start_this_second > 1.0 {
+ // track FPS
+ self.start_this_second = now;
+ self.fps = self.frames_this_second;
+ self.frames_this_second = 0;
+ }
+ Ok(())
+ }
+
+ pub fn run_render<G>(&mut self, ctx: &context::Context, game: &mut G) -> utils::Erm<()>
+ where
+ G: Game,
+ {
+ self.bind_screen(ctx);
+ ctx.clear_color(if ctx.options.contains(crate::Options::OVERLAY) {
+ glam::Vec4::new(0.0, 0.0, 0.0, 0.0)
+ } else {
+ glam::Vec4::new(0.0, 0.0, 0.0, 1.0)
+ });
+ ctx.clear();
+
+ game.render(ctx, self)?;
+
+ #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
+ {
+ let err = unsafe { ctx.gl.get_error() };
+ if err != glow::NO_ERROR {
+ log::warn!("opengl error: {}", err);
+ }
+ // let log = unsafe { ctx.gl.get_debug_message_log(5) };
+ // for m in log {
+ // log::warn!("opengl debug message: {:?}", m);
+ // }
+ }
+ Ok(())
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/texture.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/texture.rs
new file mode 100644
index 0000000..b9749a2
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/texture.rs
@@ -0,0 +1,156 @@
+use glow::HasContext;
+use image::EncodableLayout;
+
+use crate::context;
+
+pub struct Texture {
+ pub tex: glow::Texture,
+ pub width: i32,
+ pub height: i32,
+}
+
+impl Texture {
+ pub fn new_empty(ctx: &context::Context) -> Self {
+ unsafe {
+ let tex = ctx.gl.create_texture().expect("failed to create texture");
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex));
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
+ Self {
+ tex,
+ width: 0, height: 0,
+ }
+ }
+ }
+
+ pub fn new(ctx: &context::Context, bytes: &[u8]) -> Self {
+ let rgba = image::ImageReader::new(std::io::Cursor::new(bytes))
+ .with_guessed_format()
+ .expect("failed to guess image format")
+ .decode()
+ .expect("failed to decode image")
+ .into_rgba8();
+ let pixels = rgba.as_bytes();
+ unsafe {
+ let tex = ctx.gl.create_texture().expect("failed to create texture");
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(tex));
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_image_2d(
+ glow::TEXTURE_2D,
+ 0,
+ glow::RGBA as i32,
+ rgba.width() as i32,
+ rgba.height() as i32,
+ 0,
+ glow::RGBA,
+ glow::UNSIGNED_BYTE,
+ Some(pixels),
+ );
+ ctx.gl.generate_mipmap(glow::TEXTURE_2D);
+
+ Self {
+ tex,
+ width: rgba.width() as i32,
+ height: rgba.height() as i32,
+ }
+ }
+ }
+
+ pub fn upload(&mut self, ctx: &context::Context, bytes: &[u8]) {
+ let rgba = image::ImageReader::new(std::io::Cursor::new(bytes))
+ .with_guessed_format()
+ .expect("failed to guess image format")
+ .decode()
+ .expect("failed to decode image")
+ .into_rgba8();
+ let pixels = rgba.as_bytes();
+ self.upload_rgba8(ctx, rgba.width() as i32, rgba.height() as i32, pixels);
+ }
+
+ pub fn upload_rgba8(&mut self, ctx: &context::Context, width: i32, height: i32, data: &[u8]) {
+ unsafe {
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.tex));
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32);
+ ctx.gl.tex_image_2d(
+ glow::TEXTURE_2D,
+ 0,
+ glow::RGBA as i32,
+ width,
+ height,
+ 0,
+ glow::RGBA,
+ glow::UNSIGNED_BYTE,
+ Some(data),
+ );
+ ctx.gl.generate_mipmap(glow::TEXTURE_2D);
+ }
+ self.width = width;
+ self.height = height;
+ }
+
+ pub fn set_repeat(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.tex));
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
+ }
+ }
+
+ pub fn set_anisotropic_filtering(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.tex));
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::REPEAT as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::REPEAT as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR_MIPMAP_LINEAR as i32);
+ ctx.gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32);
+ ctx.gl.tex_parameter_f32(glow::TEXTURE_2D, glow::TEXTURE_MAX_ANISOTROPY_EXT, 4.0);
+ }
+ }
+
+ pub fn bind(&self, ctx: &context::Context) {
+ unsafe {
+ ctx.gl.active_texture(glow::TEXTURE0);
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.tex));
+ }
+ }
+
+ pub fn bind_initial(ctx: &context::Context) {
+ unsafe {
+ ctx.gl.active_texture(glow::TEXTURE0);
+ ctx.gl.bind_texture(glow::TEXTURE_2D, None);
+ }
+ }
+
+ pub fn bind_index(&self, ctx: &context::Context, idx: u32) {
+ unsafe {
+ ctx.gl.active_texture(glow::TEXTURE0 + idx);
+ ctx.gl.bind_texture(glow::TEXTURE_2D, Some(self.tex));
+ }
+ }
+}
+
+pub struct Material {
+ pub color: Texture,
+ pub normal: Texture,
+}
+impl Material {
+ pub fn new(ctx: &context::Context, color_bytes: &[u8], normal_bytes: &[u8]) -> Self {
+ let color = Texture::new(ctx, color_bytes);
+ let normal = Texture::new(ctx, normal_bytes);
+ color.set_anisotropic_filtering(ctx);
+ normal.set_anisotropic_filtering(ctx);
+ Self { color, normal }
+ }
+ pub fn bind(&self, ctx: &context::Context) {
+ self.color.bind(ctx);
+ self.normal.bind_index(ctx, 1);
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/ui.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/ui.rs
new file mode 100644
index 0000000..8607fb6
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/ui.rs
@@ -0,0 +1,169 @@
+use crate::{state, utils};
+use crate::state::Tick;
+
+fn compute_reverse(frames: Tick, tick: Tick, start: Tick) -> Tick {
+ let leftover = frames - (tick - start)
+ .clamp(0, frames);
+ tick - leftover
+}
+
+pub enum ModeToggle {
+ Inactive { start: Tick },
+ Active { start: Tick },
+}
+
+pub struct Mode {
+ frames: Tick,
+ toggle: ModeToggle,
+}
+
+impl Mode {
+ pub fn new(frames: Tick) -> Self {
+ Self {
+ frames,
+ toggle: ModeToggle::Inactive { start: 0 },
+ }
+ }
+
+ /// Is the current state active?
+ pub fn is_active(&self) -> bool {
+ match self.toggle {
+ ModeToggle::Inactive {..} => false,
+ ModeToggle::Active {..} => true,
+ }
+ }
+
+ /// Has the current transition finished?
+ pub fn is_ready(&self, tick: Tick) -> bool {
+ let started = match self.toggle {
+ ModeToggle::Inactive { start } => start,
+ ModeToggle::Active { start } => start,
+ };
+ tick - started > self.frames
+ }
+
+ pub fn progress(&self, tick: Tick) -> f32 {
+ match self.toggle {
+ ModeToggle::Inactive { start } => {
+ 1.0 - (((tick - start) as f32) / self.frames as f32)
+ .clamp(0.0, 1.0)
+ },
+ ModeToggle::Active { start } => {
+ (((tick - start) as f32) / self.frames as f32)
+ .clamp(0.0, 1.0)
+ }
+ }
+ }
+
+ pub fn reset(&mut self) {
+ self.toggle = ModeToggle::Inactive { start: 0 };
+ }
+
+ pub fn toggle(&mut self, tick: Tick) {
+ match self.toggle {
+ ModeToggle::Inactive { start } => {
+ self.toggle = ModeToggle::Active {
+ start: compute_reverse(self.frames, tick, start)
+ };
+ },
+ ModeToggle::Active { start } => {
+ self.toggle = ModeToggle::Inactive {
+ start: compute_reverse(self.frames, tick, start)
+ };
+ },
+ }
+ }
+}
+
+pub struct Cursor {
+ pub index: i32,
+ pub prev_index: i32,
+ pub change_started: Tick,
+ pub bound: i32,
+ pub frames: Tick,
+}
+
+impl Cursor {
+ pub fn new(bound: i32, frames: Tick) -> Self {
+ Self {
+ index: 0,
+ prev_index: 0,
+ change_started: 0,
+ bound,
+ frames,
+ }
+ }
+
+ pub fn animation_index(&self, tick: Tick) -> f32 {
+ let progress = ((tick - self.change_started) as f32)
+ / (self.frames as f32 / 2.0);
+ utils::lerp(
+ self.prev_index as f32,
+ self.index as f32,
+ progress
+ )
+ }
+
+ pub fn is_ready(&self, tick: Tick) -> bool {
+ tick - self.change_started > self.frames
+ }
+
+ pub fn set(&mut self, val: i32, tick: Tick) -> bool {
+ if self.is_ready(tick) {
+ self.change_started = tick;
+ self.prev_index = self.index;
+ self.index = val;
+ self.index %= self.bound;
+ true
+ } else { false }
+ }
+ pub fn increment(&mut self, tick: Tick) -> bool { self.set(self.index + 1, tick) }
+ pub fn decrement(&mut self, tick: Tick) -> bool { self.set(self.index + self.bound - 1, tick) }
+
+ pub fn set_unlocked(&mut self, val: i32, tick: Tick) {
+ self.change_started = tick;
+ self.prev_index = self.index;
+ self.index = val;
+ self.index %= self.bound;
+ }
+ pub fn increment_unlocked(&mut self, tick: Tick) -> bool { self.set_unlocked(self.index + 1, tick); true }
+ pub fn decrement_unlocked(&mut self, tick: Tick) -> bool { self.set_unlocked(self.index + self.bound - 1, tick); true }
+
+ /// Read keypresses to update this cursor (assuming that the left/right keys decrement/increment)
+ /// Returns true if an update was performed (e.g. to determine whether to play a sound).
+ pub fn update_horizontal(&mut self, st: &mut state::State) -> bool {
+ if st.keys.new_left() {
+ self.decrement_unlocked(st.tick)
+ } else if st.keys.new_right() {
+ self.increment_unlocked(st.tick)
+ } else if st.keys.left() {
+ self.decrement(st.tick)
+ } else if st.keys.right() {
+ self.increment(st.tick)
+ } else { false }
+ }
+
+ pub fn update_vertical(&mut self, st: &mut state::State) -> bool {
+ if st.keys.new_up() {
+ self.decrement_unlocked(st.tick)
+ } else if st.keys.new_down() {
+ self.increment_unlocked(st.tick)
+ } else if st.keys.up() {
+ self.decrement(st.tick)
+ } else if st.keys.down() {
+ self.increment(st.tick)
+ } else { false }
+ }
+
+ pub fn update_lr(&mut self, st: &mut state::State) -> bool {
+ if st.keys.new_l() {
+ self.decrement_unlocked(st.tick)
+ } else if st.keys.new_r() {
+ self.increment_unlocked(st.tick)
+ } else if st.keys.l() {
+ self.decrement(st.tick)
+ } else if st.keys.r() {
+ self.increment(st.tick)
+ } else { false }
+ }
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/utils.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/utils.rs
new file mode 100644
index 0000000..14a7fdf
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia/src/utils.rs
@@ -0,0 +1,147 @@
+use std::f32::consts::PI;
+
+use enum_map::Enum;
+use serde::{Serialize, Deserialize};
+use strum::EnumIter;
+
+pub type Erm<T> = simple_eyre::Result<T>;
+
+pub fn erm<E, T>(e: E) -> Erm<T> where E: std::error::Error + std::marker::Send + std::marker::Sync + 'static {
+ Err(e.into())
+}
+
+#[derive(Debug)]
+pub struct Error {
+ pub msg: String
+}
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "teleia error: {}", self.msg)
+ }
+}
+impl std::error::Error for Error {}
+
+pub fn erm_msg<T>(msg: &str) -> Erm<T> {
+ Err(Error {
+ msg: msg.to_owned(),
+ }.into())
+}
+
+pub struct ErrorHandler;
+impl simple_eyre::eyre::EyreHandler for ErrorHandler {
+ fn debug(
+ &self,
+ error: &(dyn std::error::Error + 'static),
+ f: &mut core::fmt::Formatter<'_>,
+ ) -> core::fmt::Result {
+ if f.alternate() {
+ return core::fmt::Debug::fmt(error, f);
+ }
+ let mut first = true;
+ if let Some(s) = error.source() {
+ let errors: Vec<_> = std::iter::successors(Some(s), |e| (*e).source()).collect();
+ for err in errors.iter().rev() {
+ writeln!(f)?; write!(f, "{}{}", if first {""} else {" - "}, err)?;
+ first = false;
+ }
+ }
+ writeln!(f)?; write!(f, "{}{}", if first {""} else {" - "}, error)?;
+ Ok(())
+ }
+}
+
+pub fn install_error_handler() {
+ simple_eyre::eyre::set_hook(Box::new(move |_| Box::new(ErrorHandler))).expect("failed to install error handler");
+}
+
+#[derive(Clone, Copy, Debug, Enum, EnumIter, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
+pub enum Cardinal {
+ North,
+ South,
+ West,
+ East,
+}
+
+impl Cardinal {
+ pub fn to_string(&self) -> &'static str {
+ match self {
+ Self::North => "north",
+ Self::South => "south",
+ Self::West => "west",
+ Self::East => "east",
+ }
+ }
+
+ pub fn turn_cw(&self) -> Self {
+ match self {
+ Self::North => Self::East,
+ Self::South => Self::West,
+ Self::West => Self::North,
+ Self::East => Self::South,
+ }
+ }
+
+ pub fn turn_ccw(&self) -> Self {
+ match self {
+ Self::North => Self::West,
+ Self::South => Self::East,
+ Self::West => Self::South,
+ Self::East => Self::North,
+ }
+ }
+
+ pub fn dir(&self) -> glam::Vec3 {
+ match self {
+ Self::North => glam::Vec3::new(0.0, 1.0, 0.0),
+ Self::South => glam::Vec3::new(0.0, -1.0, 0.0),
+ Self::West => glam::Vec3::new(-1.0, 0.0, 0.0),
+ Self::East => glam::Vec3::new(1.0, 0.0, 0.0),
+ }
+ }
+
+ pub fn offsets(&self) -> (i32, i32) {
+ match self {
+ Self::North => (0, 1),
+ Self::South => (0, -1),
+ Self::West => (-1, 0),
+ Self::East => (1, 0),
+ }
+ }
+
+ pub fn angle(&self) -> f32 {
+ match self {
+ Self::North => 0.0,
+ Self::South => PI,
+ Self::West => PI / 2.0,
+ Self::East => 3.0 * PI / 2.0,
+ }
+ }
+
+ pub fn turn_by(&self, o: Self) -> Self {
+ match o {
+ Self::North => *self,
+ Self::South => self.turn_cw().turn_cw(),
+ Self::West => self.turn_cw(),
+ Self::East => self.turn_ccw(),
+ }
+ }
+
+ pub fn angle_between(&self, o: &Self) -> f32 {
+ if o == &self.turn_cw() { -PI / 2.0 }
+ else if o == &self.turn_ccw() { PI / 2.0 }
+ else if o == &self.turn_cw().turn_cw() { PI }
+ else { 0.0 }
+ }
+}
+
+pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
+ a + t.clamp(0.0, 1.0) * (b - a)
+}
+
+pub fn dir_lerp(a: glam::Vec3, b: glam::Vec3, t: f32) -> glam::Vec3 {
+ let dirrotaxis = a.cross(b).normalize();
+ let dirrotangle = a.angle_between(b);
+ let dirrotfull = glam::Quat::from_axis_angle(dirrotaxis, dirrotangle);
+ let dirrot = glam::Quat::IDENTITY.slerp(dirrotfull, t);
+ dirrot.mul_vec3(a)
+}
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/Cargo.toml b/2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/Cargo.toml
new file mode 100644
index 0000000..3ef969e
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "teleia_macros"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+
+[lib]
+proc-macro = true
+
+[dependencies]
+env = "1.0.1"
+walkdir = "2.5.0"
+heck = "0.5.0"
+litrs = "1.0.0" \ No newline at end of file
diff --git a/2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/src/lib.rs b/2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/src/lib.rs
new file mode 100644
index 0000000..5b6c0b9
--- /dev/null
+++ b/2026/games/rpc2dot0/vendor/teleia/crates/teleia_macros/src/lib.rs
@@ -0,0 +1,231 @@
+use heck::ToUpperCamelCase;
+use proc_macro::TokenStream;
+use std::collections::HashSet;
+use std::path::Path;
+use walkdir::WalkDir;
+
+#[derive(Debug, Hash, PartialEq, Eq)]
+struct Designator {
+ path: String,
+ parts: Vec<String>,
+}
+impl Designator {
+ fn new(fbase: &str, mut path: &Path, shorten: bool) -> Self {
+ let mut parts: Vec<_> = path
+ .strip_prefix(fbase)
+ .unwrap()
+ .with_extension("")
+ .components()
+ .filter_map(|c| match c {
+ std::path::Component::Normal(o) => Some(o.to_str().unwrap().to_owned()),
+ _ => None,
+ })
+ .collect();
+ if shorten {
+ parts.pop();
+ path = path.parent().expect("path has no parent");
+ }
+ Self {
+ path: path
+ .to_str()
+ .expect("path is not a string")
+ .replace("\\", "/"),
+ parts,
+ }
+ }
+ fn enum_entry(&self, fnm: &str) -> String {
+ let singular = match fnm {
+ "meshes" => "mesh",
+ "textures" => "texture",
+ "materials" => "material",
+ "shaders" => "shader",
+ _ => fnm,
+ };
+ let mut ret = format!("\n#[doc=\"newton-{}: {}\"]\n", singular, self.path);
+ ret += &self.parts.join(" ").to_upper_camel_case();
+ ret
+ }
+ fn enum_element(&self, _fnm: &str) -> String {
+ self.parts.join(" ").to_upper_camel_case()
+ }
+ fn load_expr(&self, fnm: &str) -> Option<String> {
+ let i = format!("assets/{}/{}", fnm, self.parts.join("/"));
+ match fnm {
+ "meshes" => Some(format!(
+ "teleia::mesh::Mesh::from_obj(ctx, include_bytes!(\"{}.obj\"))",
+ i
+ )),
+ "textures" => Some(format!(
+ "teleia::texture::Texture::new(ctx, include_bytes!(\"{}.png\"))",
+ i
+ )),
+ "materials" => Some(format!(
+ "teleia::texture::Material::new(ctx, include_bytes!(\"{}/color.jpg\"), include_bytes!(\"{}/normal.jpg\"))",
+ i, i
+ )),
+ "shaders" => {
+ if self.parts.contains(&"nolib".to_owned()) {
+ Some(format!(
+ "teleia::shader::Shader::new_nolib(ctx, include_str!(\"{}/vert.glsl\"), include_str!(\"{}/frag.glsl\"))",
+ i, i
+ ))
+ } else {
+ Some(format!(
+ "teleia::shader::Shader::new(ctx, include_str!(\"{}/vert.glsl\"), include_str!(\"{}/frag.glsl\"))",
+ i, i
+ ))
+ }
+ }
+ _ => None,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Field {
+ nm: String,
+ entries: HashSet<Designator>,
+}
+impl Field {
+ fn new(base: &str, nm: &str) -> Self {
+ let mut entries = HashSet::new();
+ let fbase = format!("{}{}", base, nm);
+ for f in WalkDir::new(&fbase).into_iter().flatten() {
+ if f.file_type().is_file() {
+ entries.insert(Designator::new(
+ &fbase,
+ f.path(),
+ nm == "shaders" || nm == "materials",
+ ));
+ }
+ }
+ Self {
+ nm: nm.to_owned(),
+ entries,
+ }
+ }
+ fn generate(&self) -> Option<(String, String, String)> {
+ let mut ents = Vec::new();
+ for d in self.entries.iter() {
+ if let Some(exp) = d.load_expr(&self.nm) {
+ ents.push((d.enum_entry(&self.nm), d.enum_element(&self.nm), exp));
+ }
+ }
+ let (enm, ty) = match self.nm.as_str() {
+ "meshes" => ("Mesh", "teleia::mesh::Mesh"),
+ "textures" => ("Texture", "teleia::texture::Texture"),
+ "materials" => ("Material", "teleia::texture::Material"),
+ "shaders" => ("Shader", "teleia::shader::Shader"),
+ _ => return None,
+ };
+ let enums: Vec<_> = ents.iter().map(|(e, _, _)| e.clone()).collect();
+ let edecl = format!("#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, enum_map::Enum)]
+pub enum {} {{ {} }}", enm, enums.join(", "));
+ let decl = format!("pub {}: enum_map::EnumMap<{}, {}>", self.nm, enm, ty);
+ let inits: Vec<_> = ents
+ .into_iter()
+ .map(|(_, e, exp)| format!("{}::{} => {}", enm, e, exp))
+ .collect();
+ let init = format!("{}: enum_map::enum_map!({})", self.nm, inits.join(", "));
+ Some((edecl, decl, init))
+ }
+}
+
+#[derive(Debug)]
+struct AssetData {
+ fields: Vec<Field>,
+}
+impl AssetData {
+ fn new(base: &str) -> Self {
+ let mut fields = Vec::new();
+ let dirs = std::fs::read_dir(base)
+ .unwrap_or_else(|_| panic!("failed to read assets directory: {}", base));
+ let (mut has_meshes, mut has_textures, mut has_materials, mut has_shaders) =
+ (false, false, false, false);
+ for d in dirs.flatten() {
+ let nm = d.file_name().into_string().unwrap();
+ fields.push(Field::new(base, &nm));
+ match &*nm {
+ "meshes" => has_meshes = true,
+ "textures" => has_textures = true,
+ "materials" => has_materials = true,
+ "shaders" => has_shaders = true,
+ _ => {}
+ };
+ }
+ if !has_meshes {
+ fields.push(Field {
+ nm: "meshes".to_owned(),
+ entries: HashSet::new(),
+ });
+ }
+ if !has_textures {
+ fields.push(Field {
+ nm: "textures".to_owned(),
+ entries: HashSet::new(),
+ });
+ }
+ if !has_materials {
+ fields.push(Field {
+ nm: "materials".to_owned(),
+ entries: HashSet::new(),
+ });
+ }
+ if !has_shaders {
+ fields.push(Field {
+ nm: "shaders".to_owned(),
+ entries: HashSet::new(),
+ });
+ }
+ Self { fields }
+ }
+ fn generate(&self) -> String {
+ let mut res = String::new();
+ let fdata: Vec<_> = self.fields.iter().filter_map(|f| f.generate()).collect();
+ for (edecl, _, _) in fdata.iter() {
+ res += edecl;
+ res += "\n";
+ }
+ res += "pub struct Assets {\n";
+ for (_, decl, _) in fdata.iter() {
+ res += decl;
+ res += ",\n";
+ }
+ res += "}\nimpl Assets {\npub fn new(ctx: &teleia::context::Context) -> Self {\nSelf {\n";
+ for (_, _, init) in fdata.iter() {
+ res += init;
+ res += ",\n";
+ }
+ res += "}\n}\n}\n";
+ res += "impl teleia::renderer::Assets for Assets {\n";
+ res += "type Shader = Shader;\n";
+ res +=
+ "fn shader(&self, i: Self::Shader) -> &teleia::shader::Shader { &self.shaders[i] }\n";
+ res += "type Texture = Texture;\n";
+ res += "fn texture(&self, i: Self::Texture) -> &teleia::texture::Texture { &self.textures[i] }\n";
+ res += "type Material = Material;\n";
+ res += "fn material(&self, i: Self::Material) -> &teleia::texture::Material { &self.materials[i] }\n";
+ res += "type Mesh = Mesh;\n";
+ res += "fn mesh(&self, i: Self::Mesh) -> &teleia::mesh::Mesh { &self.meshes[i] }\n";
+ res += "}\n";
+ res
+ }
+}
+
+#[proc_macro]
+pub fn generate_assets(s: TokenStream) -> TokenStream {
+ let token = s
+ .into_iter()
+ .next()
+ .expect("must pass asset base path as a string literal");
+ let lit = litrs::StringLit::try_from(token).expect("argument was not a string literal");
+ let base = lit.value();
+ let manifest = env::var("CARGO_MANIFEST_DIR").expect("failed to get manifest path");
+ let assets = AssetData::new(&format!("{}/{}", manifest, base));
+ // println!("{}", assets.generate());
+ assets
+ .generate()
+ .to_string()
+ .parse()
+ .expect("failed to parse generate_assets result")
+}