Repository: cormac-obrien/richter Branch: devel Commit: 506504d5f9f9 Files: 122 Total size: 1.0 MB Directory structure: gitextract_j_etf39x/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── shaders/ │ ├── alias.frag │ ├── alias.vert │ ├── blit.frag │ ├── blit.vert │ ├── brush.frag │ ├── brush.vert │ ├── deferred.frag │ ├── deferred.vert │ ├── glyph.frag │ ├── glyph.vert │ ├── particle.frag │ ├── particle.vert │ ├── postprocess.frag │ ├── postprocess.vert │ ├── quad.frag │ ├── quad.vert │ ├── sprite.frag │ └── sprite.vert ├── site/ │ ├── config.toml │ ├── content/ │ │ ├── _index.md │ │ ├── blog/ │ │ │ ├── 2018-04-24.md │ │ │ ├── 2018-04-26/ │ │ │ │ └── index.md │ │ │ ├── 2018-05-12/ │ │ │ │ └── index.md │ │ │ ├── 2018-07-20/ │ │ │ │ └── index.md │ │ │ └── _index.md │ │ └── index.html │ ├── sass/ │ │ ├── _base.scss │ │ ├── _reset.scss │ │ ├── blog-post.scss │ │ ├── blog.scss │ │ └── style.scss │ ├── templates/ │ │ └── home.html │ └── themes/ │ └── richter/ │ ├── templates/ │ │ ├── base.html │ │ ├── blog-post.html │ │ ├── blog.html │ │ └── index.html │ └── theme.toml ├── specifications.md └── src/ ├── bin/ │ ├── quake-client/ │ │ ├── capture.rs │ │ ├── game.rs │ │ ├── main.rs │ │ ├── menu.rs │ │ └── trace.rs │ └── unpak.rs ├── client/ │ ├── cvars.rs │ ├── demo.rs │ ├── entity/ │ │ ├── mod.rs │ │ └── particle.rs │ ├── input/ │ │ ├── console.rs │ │ ├── game.rs │ │ ├── menu.rs │ │ └── mod.rs │ ├── menu/ │ │ ├── item.rs │ │ └── mod.rs │ ├── mod.rs │ ├── render/ │ │ ├── atlas.rs │ │ ├── blit.rs │ │ ├── cvars.rs │ │ ├── error.rs │ │ ├── mod.rs │ │ ├── palette.rs │ │ ├── pipeline.rs │ │ ├── target.rs │ │ ├── ui/ │ │ │ ├── console.rs │ │ │ ├── glyph.rs │ │ │ ├── hud.rs │ │ │ ├── layout.rs │ │ │ ├── menu.rs │ │ │ ├── mod.rs │ │ │ └── quad.rs │ │ ├── uniform.rs │ │ ├── warp.rs │ │ └── world/ │ │ ├── alias.rs │ │ ├── brush.rs │ │ ├── deferred.rs │ │ ├── mod.rs │ │ ├── particle.rs │ │ ├── postprocess.rs │ │ └── sprite.rs │ ├── sound/ │ │ ├── mod.rs │ │ └── music.rs │ ├── state.rs │ ├── trace.rs │ └── view.rs ├── common/ │ ├── alloc.rs │ ├── bitset.rs │ ├── bsp/ │ │ ├── load.rs │ │ └── mod.rs │ ├── console/ │ │ └── mod.rs │ ├── engine.rs │ ├── host.rs │ ├── math.rs │ ├── mdl.rs │ ├── mod.rs │ ├── model.rs │ ├── net/ │ │ ├── connect.rs │ │ └── mod.rs │ ├── pak.rs │ ├── parse/ │ │ ├── console.rs │ │ ├── map.rs │ │ └── mod.rs │ ├── sprite.rs │ ├── util.rs │ ├── vfs.rs │ └── wad.rs ├── lib.rs └── server/ ├── mod.rs ├── precache.rs ├── progs/ │ ├── functions.rs │ ├── globals.rs │ ├── mod.rs │ ├── ops.rs │ └── string_table.rs └── world/ ├── entity.rs ├── mod.rs └── phys.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: Rust on: push: branches: [ devel ] pull_request: branches: [ devel ] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install build deps run: sudo apt-get install libasound2-dev - name: Install latest nightly uses: actions-rs/toolchain@v1 with: toolchain: nightly components: rustfmt, clippy - name: Build with nightly uses: actions-rs/cargo@v1.0.1 with: command: build toolchain: nightly args: --all-targets - name: Test with nightly uses: actions-rs/cargo@v1.0.1 with: command: test toolchain: nightly args: --workspace ================================================ FILE: .gitignore ================================================ Cargo.lock site/public target *.bak *.bk *.pak *.pak.d .#* ================================================ FILE: .rustfmt.toml ================================================ unstable_features = true imports_granularity = "Crate" ================================================ FILE: Cargo.toml ================================================ [package] name = "richter" version = "0.1.0" authors = ["Cormac O'Brien "] edition = "2018" [dependencies] arrayvec = "0.7" bitflags = "1.0.1" bumpalo = "3.4" byteorder = "1.3" cgmath = "0.17.0" chrono = "0.4.0" env_logger = "0.5.3" failure = "0.1.8" futures = "0.3.5" lazy_static = "1.0.0" log = "0.4.1" nom = "5.1" num = "0.1.42" num-derive = "0.1.42" png = "0.16" rand = { version = "0.7", features = ["small_rng"] } regex = "0.2.6" # rodio = "0.12" rodio = { git = "https://github.com/RustAudio/rodio", rev = "82b4952" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" shaderc = "0.6.2" slab = "0.4" structopt = "0.3.12" strum = "0.18.0" strum_macros = "0.18.0" thiserror = "1.0" uluru = "2" wgpu = "0.8" # "winit" = "0.22.2" # necessary until winit/#1524 is merged winit = { git = "https://github.com/chemicstry/winit", branch = "optional_drag_and_drop" } ================================================ FILE: LICENSE.txt ================================================ Copyright © 2017 Cormac O'Brien Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Richter [![Build Status](https://travis-ci.org/cormac-obrien/richter.svg?branch=devel)](https://travis-ci.org/cormac-obrien/richter) A modern implementation of the Quake engine in Rust. ![alt tag](https://i.imgur.com/25nOENn.png) ## Status Richter is in pre-alpha development, so it's still under heavy construction. However, the client is nearly alpha-ready -- check out the Client section below to see progress. ### Client The client is capable of connecting to and playing on original Quake servers using `sv_protocol 15`. To connect to a Quake server, run ``` $ cargo run --release --bin quake-client -- --connect : ``` Quake servers run on port 26000 by default. I can guarantee compatibility with FitzQuake and its derived engines, as I use the QuakeSpasm server for development (just remember `sv_protocol 15`). The client also supports demo playback using the `--demo` option: ``` $ cargo run --release --bin quake-client -- --demo ``` This works for demos in the PAK archives (e.g. `demo1.dem`) or any demos you happen to have placed in the `id1` directory. #### Feature checklist - Networking - [x] NetQuake network protocol implementation (`sv_protocol 15`) - [x] Connection protocol implemented - [x] All in-game server commands handled - [x] Carryover between levels - [ ] FitzQuake extended protocol support (`sv_protocol 666`) - Rendering - [x] Deferred dynamic lighting - [x] Particle effects - Brush model (`.bsp`) rendering - Textures - [x] Static textures - [x] Animated textures - [x] Alternate animated textures - [x] Liquid texture warping - [ ] Sky texture scrolling (currently partial support) - [x] Lightmaps - [x] Occlusion culling - Alias model (`.mdl`) rendering - [x] Keyframe animation - [x] Static keyframes - [x] Animated keyframes - [ ] Keyframe interpolation - [ ] Ambient lighting - [ ] Viewmodel rendering - UI - [x] Console - [x] HUD - [x] Level intermissions - [ ] On-screen messages - [ ] Menus - Sound - [x] Loading and playback - [x] Entity sound - [ ] Ambient sound - [x] Spatial attenuation - [ ] Stereo spatialization - [x] Music - Console - [x] Line editing - [x] History browsing - [x] Cvar modification - [x] Command execution - [x] Quake script file execution - Demos - [x] Demo playback - [ ] Demo recording - File formats - [x] BSP loader - [x] MDL loader - [x] SPR loader - [x] PAK archive extraction - [x] WAD archive extraction ### Server The Richter server is still in its early stages, so there's no checklist here yet. However, you can still check out the QuakeC bytecode VM in the [`progs` module](https://github.com/cormac-obrien/richter/blob/devel/src/server/progs/mod.rs). ## Building Richter makes use of feature gates and compiler plugins, which means you'll need a nightly build of `rustc`. The simplest way to do this is to download [rustup](https://www.rustup.rs/) and follow the directions. Because a Quake distribution contains multiple binaries, this software is packaged as a Cargo library project. The source files for binaries are located in the `src/bin` directory and can be run with $ cargo run --bin where `` is the name of the source file without the `.rs` extension. ## Legal This software is released under the terms of the MIT License (see LICENSE.txt). This project is in no way affiliated with id Software LLC, Bethesda Softworks LLC, or ZeniMax Media Inc. Information regarding the Quake trademark can be found at Bethesda's [legal information page](https://bethesda.net/en/document/legal-information). Due to licensing restrictions, the data files necessary to run Quake cannot be distributed with this package. `pak0.pak`, which contains the files for the first episode ("shareware Quake"), can be retrieved from id's FTP server at `ftp://ftp.idsoftware.com/idstuff/quake`. The full game can be purchased from a number of retailers including Steam and GOG. ================================================ FILE: shaders/alias.frag ================================================ #version 450 layout(location = 0) in vec3 f_normal; layout(location = 1) in vec2 f_diffuse; // set 1: per-entity layout(set = 1, binding = 1) uniform sampler u_diffuse_sampler; // set 2: per-texture chain layout(set = 2, binding = 0) uniform texture2D u_diffuse_texture; layout(location = 0) out vec4 diffuse_attachment; layout(location = 1) out vec4 normal_attachment; layout(location = 2) out vec4 light_attachment; void main() { diffuse_attachment = texture( sampler2D(u_diffuse_texture, u_diffuse_sampler), f_diffuse ); // TODO: get ambient light from uniform light_attachment = vec4(0.25); // rescale normal to [0, 1] normal_attachment = vec4(f_normal / 2.0 + 0.5, 1.0); } ================================================ FILE: shaders/alias.vert ================================================ #version 450 layout(location = 0) in vec3 a_position1; // layout(location = 1) in vec3 a_position2; layout(location = 2) in vec3 a_normal; layout(location = 3) in vec2 a_diffuse; layout(push_constant) uniform PushConstants { mat4 transform; mat4 model_view; } push_constants; layout(location = 0) out vec3 f_normal; layout(location = 1) out vec2 f_diffuse; // convert from Quake coordinates vec3 convert(vec3 from) { return vec3(-from.y, from.z, -from.x); } void main() { f_normal = mat3(transpose(inverse(push_constants.model_view))) * convert(a_normal); f_diffuse = a_diffuse; gl_Position = push_constants.transform * vec4(convert(a_position1), 1.0); } ================================================ FILE: shaders/blit.frag ================================================ #version 450 layout(location = 0) in vec2 f_texcoord; layout(location = 0) out vec4 color_attachment; layout(set = 0, binding = 0) uniform sampler u_sampler; layout(set = 0, binding = 1) uniform texture2D u_color; void main() { color_attachment = texture(sampler2D(u_color, u_sampler), f_texcoord); } ================================================ FILE: shaders/blit.vert ================================================ #version 450 layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; layout(location = 0) out vec2 f_texcoord; void main() { f_texcoord = a_texcoord; gl_Position = vec4(a_position * 2.0 - 1.0, 0.0, 1.0); } ================================================ FILE: shaders/brush.frag ================================================ #version 450 #define LIGHTMAP_ANIM_END (255) const uint TEXTURE_KIND_REGULAR = 0; const uint TEXTURE_KIND_WARP = 1; const uint TEXTURE_KIND_SKY = 2; const float WARP_AMPLITUDE = 0.15; const float WARP_FREQUENCY = 0.25; const float WARP_SCALE = 1.0; layout(location = 0) in vec3 f_normal; layout(location = 1) in vec2 f_diffuse; // also used for fullbright layout(location = 2) in vec2 f_lightmap; flat layout(location = 3) in uvec4 f_lightmap_anim; layout(push_constant) uniform PushConstants { layout(offset = 128) uint texture_kind; } push_constants; // set 0: per-frame layout(set = 0, binding = 0) uniform FrameUniforms { float light_anim_frames[64]; vec4 camera_pos; float time; bool r_lightmap; } frame_uniforms; // set 1: per-entity layout(set = 1, binding = 1) uniform sampler u_diffuse_sampler; // also used for fullbright layout(set = 1, binding = 2) uniform sampler u_lightmap_sampler; // set 2: per-texture layout(set = 2, binding = 0) uniform texture2D u_diffuse_texture; layout(set = 2, binding = 1) uniform texture2D u_fullbright_texture; layout(set = 2, binding = 2) uniform TextureUniforms { uint kind; } texture_uniforms; // set 3: per-face layout(set = 3, binding = 0) uniform texture2D u_lightmap_texture[4]; layout(location = 0) out vec4 diffuse_attachment; layout(location = 1) out vec4 normal_attachment; layout(location = 2) out vec4 light_attachment; vec4 calc_light() { vec4 light = vec4(0.0, 0.0, 0.0, 0.0); for (int i = 0; i < 4 && f_lightmap_anim[i] != LIGHTMAP_ANIM_END; i++) { float map = texture( sampler2D(u_lightmap_texture[i], u_lightmap_sampler), f_lightmap ).r; // range [0, 4] float style = frame_uniforms.light_anim_frames[f_lightmap_anim[i]]; light[i] = map * style; } return light; } void main() { switch (push_constants.texture_kind) { case TEXTURE_KIND_REGULAR: diffuse_attachment = texture( sampler2D(u_diffuse_texture, u_diffuse_sampler), f_diffuse ); float fullbright = texture( sampler2D(u_fullbright_texture, u_diffuse_sampler), f_diffuse ).r; if (fullbright != 0.0) { light_attachment = vec4(0.25); } else { light_attachment = calc_light(); } break; case TEXTURE_KIND_WARP: // note the texcoord transpose here vec2 wave1 = 3.14159265359 * (WARP_SCALE * f_diffuse.ts + WARP_FREQUENCY * frame_uniforms.time); vec2 warp_texcoord = f_diffuse.st + WARP_AMPLITUDE * vec2(sin(wave1.s), sin(wave1.t)); diffuse_attachment = texture( sampler2D(u_diffuse_texture, u_diffuse_sampler), warp_texcoord ); light_attachment = vec4(0.25); break; case TEXTURE_KIND_SKY: vec2 base = mod(f_diffuse + frame_uniforms.time, 1.0); vec2 cloud_texcoord = vec2(base.s * 0.5, base.t); vec2 sky_texcoord = vec2(base.s * 0.5 + 0.5, base.t); vec4 sky_color = texture( sampler2D(u_diffuse_texture, u_diffuse_sampler), sky_texcoord ); vec4 cloud_color = texture( sampler2D(u_diffuse_texture, u_diffuse_sampler), cloud_texcoord ); // 0.0 if black, 1.0 otherwise float cloud_factor; if (cloud_color.r + cloud_color.g + cloud_color.b == 0.0) { cloud_factor = 0.0; } else { cloud_factor = 1.0; } diffuse_attachment = mix(sky_color, cloud_color, cloud_factor); light_attachment = vec4(0.25); break; // not possible default: break; } // rescale normal to [0, 1] normal_attachment = vec4(f_normal / 2.0 + 0.5, 1.0); } ================================================ FILE: shaders/brush.vert ================================================ #version 450 const uint TEXTURE_KIND_NORMAL = 0; const uint TEXTURE_KIND_WARP = 1; const uint TEXTURE_KIND_SKY = 2; layout(location = 0) in vec3 a_position; layout(location = 1) in vec3 a_normal; layout(location = 2) in vec2 a_diffuse; layout(location = 3) in vec2 a_lightmap; layout(location = 4) in uvec4 a_lightmap_anim; layout(push_constant) uniform PushConstants { mat4 transform; mat4 model_view; uint texture_kind; } push_constants; layout(location = 0) out vec3 f_normal; layout(location = 1) out vec2 f_diffuse; layout(location = 2) out vec2 f_lightmap; layout(location = 3) out uvec4 f_lightmap_anim; layout(set = 0, binding = 0) uniform FrameUniforms { float light_anim_frames[64]; vec4 camera_pos; float time; } frame_uniforms; // convert from Quake coordinates vec3 convert(vec3 from) { return vec3(-from.y, from.z, -from.x); } void main() { if (push_constants.texture_kind == TEXTURE_KIND_SKY) { vec3 dir = a_position - frame_uniforms.camera_pos.xyz; dir.z *= 3.0; // the coefficients here are magic taken from the Quake source float len = 6.0 * 63.0 / length(dir); dir = vec3(dir.xy * len, dir.z); f_diffuse = (mod(8.0 * frame_uniforms.time, 128.0) + dir.xy) / 128.0; } else { f_diffuse = a_diffuse; } f_normal = mat3(transpose(inverse(push_constants.model_view))) * convert(a_normal); f_lightmap = a_lightmap; f_lightmap_anim = a_lightmap_anim; gl_Position = push_constants.transform * vec4(convert(a_position), 1.0); } ================================================ FILE: shaders/deferred.frag ================================================ #version 450 // if this is changed, it must also be changed in client::entity const uint MAX_LIGHTS = 32; layout(location = 0) in vec2 a_texcoord; layout(set = 0, binding = 0) uniform sampler u_sampler; layout(set = 0, binding = 1) uniform texture2DMS u_diffuse; layout(set = 0, binding = 2) uniform texture2DMS u_normal; layout(set = 0, binding = 3) uniform texture2DMS u_light; layout(set = 0, binding = 4) uniform texture2DMS u_depth; layout(set = 0, binding = 5) uniform DeferredUniforms { mat4 inv_projection; uint light_count; uint _pad1; uvec2 _pad2; vec4 lights[MAX_LIGHTS]; } u_deferred; layout(location = 0) out vec4 color_attachment; vec3 dlight_origin(vec4 dlight) { return dlight.xyz; } float dlight_radius(vec4 dlight) { return dlight.w; } vec3 reconstruct_position(float depth) { float x = a_texcoord.s * 2.0 - 1.0; float y = (1.0 - a_texcoord.t) * 2.0 - 1.0; vec4 ndc = vec4(x, y, depth, 1.0); vec4 view = u_deferred.inv_projection * ndc; return view.xyz / view.w; } void main() { ivec2 dims = textureSize(sampler2DMS(u_diffuse, u_sampler)); ivec2 texcoord = ivec2(vec2(dims) * a_texcoord); vec4 in_color = texelFetch(sampler2DMS(u_diffuse, u_sampler), texcoord, gl_SampleID); // scale from [0, 1] to [-1, 1] vec3 in_normal = 2.0 * texelFetch(sampler2DMS(u_normal, u_sampler), texcoord, gl_SampleID).xyz - 1.0; // Double to restore overbright values. vec4 in_light = 2.0 * texelFetch(sampler2DMS(u_light, u_sampler), texcoord, gl_SampleID); float in_depth = texelFetch(sampler2DMS(u_depth, u_sampler), texcoord, gl_SampleID).x; vec3 position = reconstruct_position(in_depth); vec4 out_color = in_color; float light = in_light.x + in_light.y + in_light.z + in_light.w; for (uint i = 0; i < u_deferred.light_count && i < MAX_LIGHTS; i++) { vec4 dlight = u_deferred.lights[i]; vec3 dir = normalize(position - dlight_origin(dlight)); float dist = abs(distance(dlight_origin(dlight), position)); float radius = dlight_radius(dlight); if (dist < radius && dot(dir, in_normal) < 0.0) { // linear attenuation light += (radius - dist) / radius; } } color_attachment = vec4(light * out_color.rgb, 1.0); } ================================================ FILE: shaders/deferred.vert ================================================ #version 450 layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; layout(location = 0) out vec2 f_texcoord; void main() { f_texcoord = a_texcoord; gl_Position = vec4(a_position * 2.0 - 1.0, 0.0, 1.0); } ================================================ FILE: shaders/glyph.frag ================================================ #version 450 #extension GL_EXT_nonuniform_qualifier : require layout(location = 0) in vec2 f_texcoord; layout(location = 1) flat in uint f_layer; layout(location = 0) out vec4 output_attachment; layout(set = 0, binding = 0) uniform sampler u_sampler; layout(set = 0, binding = 1) uniform texture2D u_texture[256]; void main() { vec4 color = texture(sampler2D(u_texture[f_layer], u_sampler), f_texcoord); if (color.a == 0) { discard; } else { output_attachment = color; } } ================================================ FILE: shaders/glyph.vert ================================================ #version 450 // vertex rate layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; // instance rate layout(location = 2) in vec2 a_instance_position; layout(location = 3) in vec2 a_instance_scale; layout(location = 4) in uint a_instance_layer; layout(location = 0) out vec2 f_texcoord; layout(location = 1) out uint f_layer; void main() { f_texcoord = a_texcoord; f_layer = a_instance_layer; gl_Position = vec4(a_instance_scale * a_position + a_instance_position, 0.0, 1.0); } ================================================ FILE: shaders/particle.frag ================================================ #version 450 layout(location = 0) in vec2 f_texcoord; layout(push_constant) uniform PushConstants { layout(offset = 64) uint color; } push_constants; layout(set = 0, binding = 0) uniform sampler u_sampler; layout(set = 0, binding = 1) uniform texture2D u_texture[256]; layout(location = 0) out vec4 diffuse_attachment; // layout(location = 1) out vec4 normal_attachment; layout(location = 2) out vec4 light_attachment; void main() { vec4 tex_color = texture( sampler2D(u_texture[push_constants.color], u_sampler), f_texcoord ); if (tex_color.a == 0.0) { discard; } diffuse_attachment = tex_color; light_attachment = vec4(0.25); } ================================================ FILE: shaders/particle.vert ================================================ #version 450 layout(location = 0) in vec3 a_position; layout(location = 1) in vec2 a_texcoord; layout(push_constant) uniform PushConstants { mat4 transform; } push_constants; layout(location = 0) out vec2 f_texcoord; void main() { f_texcoord = a_texcoord; gl_Position = push_constants.transform * vec4(a_position, 1.0); } ================================================ FILE: shaders/postprocess.frag ================================================ #version 450 layout(location = 0) in vec2 a_texcoord; layout(location = 0) out vec4 color_attachment; layout(set = 0, binding = 0) uniform sampler u_sampler; layout(set = 0, binding = 1) uniform texture2DMS u_color; layout(set = 0, binding = 2) uniform PostProcessUniforms { vec4 color_shift; } postprocess_uniforms; void main() { ivec2 dims = textureSize(sampler2DMS(u_color, u_sampler)); ivec2 texcoord = ivec2(vec2(dims) * a_texcoord); vec4 in_color = texelFetch(sampler2DMS(u_color, u_sampler), texcoord, gl_SampleID); float src_factor = postprocess_uniforms.color_shift.a; float dst_factor = 1.0 - src_factor; vec4 color_shifted = src_factor * postprocess_uniforms.color_shift + dst_factor * in_color; color_attachment = color_shifted; } ================================================ FILE: shaders/postprocess.vert ================================================ #version 450 layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; layout(location = 0) out vec2 f_texcoord; void main() { f_texcoord = a_texcoord; gl_Position = vec4(a_position * 2.0 - 1.0, 0.0, 1.0); } ================================================ FILE: shaders/quad.frag ================================================ #version 450 layout(location = 0) in vec2 f_texcoord; layout(location = 0) out vec4 color_attachment; layout(set = 0, binding = 0) uniform sampler quad_sampler; layout(set = 1, binding = 0) uniform texture2D quad_texture; void main() { vec4 color = texture(sampler2D(quad_texture, quad_sampler), f_texcoord); if (color.a == 0) { discard; } else { color_attachment = color; } } ================================================ FILE: shaders/quad.vert ================================================ #version 450 layout(location = 0) in vec2 a_position; layout(location = 1) in vec2 a_texcoord; layout(location = 0) out vec2 f_texcoord; layout(set = 2, binding = 0) uniform QuadUniforms { mat4 transform; } quad_uniforms; void main() { f_texcoord = a_texcoord; gl_Position = quad_uniforms.transform * vec4(a_position, 0.0, 1.0); } ================================================ FILE: shaders/sprite.frag ================================================ #version 450 layout(location = 0) in vec3 f_normal; layout(location = 1) in vec2 f_diffuse; // set 1: per-entity layout(set = 1, binding = 1) uniform sampler u_diffuse_sampler; // set 2: per-texture chain layout(set = 2, binding = 0) uniform texture2D u_diffuse_texture; layout(location = 0) out vec4 diffuse_attachment; layout(location = 1) out vec4 normal_attachment; layout(location = 2) out vec4 light_attachment; void main() { diffuse_attachment = texture(sampler2D(u_diffuse_texture, u_diffuse_sampler), f_diffuse); // rescale normal to [0, 1] normal_attachment = vec4(f_normal / 2.0 + 0.5, 1.0); light_attachment = vec4(1.0, 1.0, 1.0, 1.0); } ================================================ FILE: shaders/sprite.vert ================================================ #version 450 layout(location = 0) in vec3 a_position; layout(location = 1) in vec3 a_normal; layout(location = 2) in vec2 a_diffuse; layout(location = 0) out vec3 f_normal; layout(location = 1) out vec2 f_diffuse; layout(set = 0, binding = 0) uniform FrameUniforms { float light_anim_frames[64]; vec4 camera_pos; float time; } frame_uniforms; layout(set = 1, binding = 0) uniform EntityUniforms { mat4 u_transform; mat4 u_model; } entity_uniforms; // convert from Quake coordinates vec3 convert(vec3 from) { return vec3(-from.y, from.z, -from.x); } void main() { f_normal = mat3(transpose(inverse(entity_uniforms.u_model))) * convert(a_normal); f_diffuse = a_diffuse; gl_Position = entity_uniforms.u_transform * vec4(convert(a_position), 1.0); } ================================================ FILE: site/config.toml ================================================ # The URL the site will be built for base_url = "http://c-obrien.org/richter" # Whether to automatically compile all Sass files in the sass directory compile_sass = true # Whether to do syntax highlighting # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Gutenberg highlight_code = true # Whether to build a search index to be used later on by a JavaScript library build_search_index = true theme = "richter" [extra] # Put all your custom variables here ================================================ FILE: site/content/_index.md ================================================ +++ title = "Richter" template = "index.html" description = "An open-source Quake engine written in Rust" date = 2018-04-22 +++ # RICHTER ## A modern Quake engine
Richter is a brand-new Quake engine, built from the ground up in [Rust](https://rust-lang.org). Currently under active development, Richter aims to accurately reproduce the original Quake feel while removing some of the cruft that might prevent new players from enjoying a landmark experience.
================================================ FILE: site/content/blog/2018-04-24.md ================================================ +++ title = "The New Site and the Way Forward" template = "blog-post.html" date = 2018-04-24 +++ I've started rebuilding the site with [Gutenberg](https://www.getgutenberg.io/) now that I actually have something to show for the past couple years (!) of on-and-off work. I figure a dev blog will be good to have when I look back on this project, even if I don't update it that often (the blog, not the project). It'll probably take me a while to get the site in order since my HTML/CSS skills are rusty, but the old site was impossible to maintain so this ought to make things easier. As for the project itself, the client is coming along nicely -- I'm hoping to reach a playable state by the end of the year, even if there are still some graphical bugs. Now that the infrastructure is there for networking, input, rendering, and sound, I can start work on the little things. The devil is in the details, etc. Defining the ultimate scope of the first alpha release is probably going to be one of the biggest challenges. There are so many features I could add to the engine, and I suspect many of them are far more complicated than they seem on the surface. Failed past projects have taught me to be wary of feature creep, so the alpha will most likely just be a working client and server -- no tools, no installers, no plugin support or anything like that. With any luck I'll be there soon. ================================================ FILE: site/content/blog/2018-04-26/index.md ================================================ +++ title = "HUD Updates and Timing Bugs" template = "blog-post.html" date = 2018-04-26 +++ ![HUD Screenshot][1] The HUD now renders armor, health and current ammo counts in addition to the per-ammo type display at the top. The latter uses [conchars][2], which, as the name suggests, are used for rendering text to the in-game console. Now that I can load and display these I can start working on the console, which ought to make debugging a great deal easier. Unfortunately, the client is still plagued by a bug with position lerping that causes the geometry to jitter back and forth. This is most likely caused by bad time delta calculations in `Client::update_time()` ([Github][3]), but I haven't been able to pinpoint the exact problem -- only that the lerp factor seems to go out of the expected range of `[0, 1)` once per server frame. I'll keep an eye on it. [1]: http://c-obrien.org/richter/blog/2018-04-26/hud-screenshot.png [2]: https://quakewiki.org/wiki/Quake_font [3]: https://github.com/cormac-obrien/richter/blob/12b1d9448cf9c3cfed013108fe0866cb78755902/src/client/mod.rs#L1499-L1552 ================================================ FILE: site/content/blog/2018-05-12/index.md ================================================ +++ title = "Shared Ownership of Rendering Resources" template = "blog-post.html" date = 2018-05-12 +++ Among the most challenging design decisions in writing the rendering code has been the issue of ownership. In order to avoid linking the rendering logic too closely with the data, most of the rendering is done by separate `Renderer` objects (i.e., to render an `AliasModel`, one must first create an `AliasRenderer`). The process of converting on-disk model data to renderable format is fairly complex. Brush models are stored in a format designed for the Quake software renderer (which Michael Abrash explained [quite nicely][1]), while alias models have texture oddities that make it difficult to render them from a vertex buffer. In addition, all textures are composed of 8-bit indices into `gfx/palette.lmp` and must be converted to RGB in order to upload them to the GPU. Richter interleaves the position and texture coordinate data before upload. The real challenge is in determining where to store the objects for resource creation (e.g. `gfx::Factory`) and the resource handles (e.g. `gfx::handle::ShaderResourceView`). Some of these objects are model-specific -- a particular texture might belong to one model, and thus can be stored in that model's `Renderer` -- but others need to be more widely available. The most obvious example of this is the vertex buffer used for rendering quads. This is conceptually straightforward, but there are several layers of a renderer that might need this functionality. The `ConsoleRenderer` needs it in order to render the console background, but also needs a `GlyphRenderer` to render console output -- and the `GlyphRenderer` needs to be able to render textured quads. The `ConsoleRenderer` could own the `GlyphRenderer`, but the `HudRenderer` also needs access to render ammo counts. This leads to a rather complex network of `Rc`s, where many different objects own the basic building blocks that make up the rendering system. It isn't bad design *per se*, but it's a little difficult to follow, and I'm hoping that once I have the renderer fully completed I can refine the architecture to something more elegant. [1]: https://www.bluesnews.com/abrash/ ================================================ FILE: site/content/blog/2018-07-20/index.md ================================================ +++ title = "Complications with Cross-Platform Input Handling" template = "blog-post.html" date = 2018-07-20 +++ It was bound to happen eventually, but the input handling module is the first part of the project to display different behavior across platforms. [winit][1] provides a fairly solid basis for input handling, but Windows and Linux differ in terms of what sort of event is delivered to the program. Initially, I used `WindowEvent`s for everything. This works perfectly well for keystrokes and mouse clicks, but mouse movement may still have acceleration applied, which is undesirable for camera control. `winit` also offers `DeviceEvent`s for this purpose. I tried just handling mouse movement with raw input, keeping all other inputs in `WindowEvent`s, but it seems that handling `DeviceEvent`s on Linux causes the `WindowEvent`s to be eaten. The next obvious solution is to simply handle everything with `DeviceEvent`s, but this presents additional problems. First, Windows doesn't seem to even deliver keyboard input as a `DeviceEvent` -- keyboard input still needs to be polled as a `WindowEvent`. It also means that window focus has to be handled manually, since `DeviceEvent`s are delivered regardless of whether the window is focused or not. To add to the complexity of this problem, apparently not all window managers are well-behaved when it comes to determining focus. I run [i3wm][2] on my Linux install, and it doesn't deliver `WindowEvent::Focused` events when toggling focus or switching workspaces. This will have to remain an unsolved problem for the time being. [1]: https://github.com/tomaka/winit/ [2]: https://i3wm.org/ ================================================ FILE: site/content/blog/_index.md ================================================ +++ title = "Blog" template = "blog.html" description = "Richter project development log" sort_by = "date" +++ ## Musings about the project and my experience with it. --- ================================================ FILE: site/content/index.html ================================================ richter

richter - an open-source Quake engine

about

Richter is an open-source reimplementation of the original Quake engine. The aims of the project are as follows:

  • produce a high-performance server which accurately implements the original game behavior and the QuakeWorld network protocol
  • produce a client which recreates the original Quake experience
  • maintain a clean, legible code base to serve as an example for other Rust software, particularly games
  • create comprehensive technical documentation for the original Quake engine, building upon the work of Fabien Sanglard, the Unofficial Quake Specs team and others

status

Richter is currently in pre-alpha development. You can keep up with the project at its github repository.

================================================ FILE: site/sass/_base.scss ================================================ @import "reset"; @font-face { font-family: "Renner"; src: local('Renner*'), local('Renner-Book'), url("fonts/Renner-Book.woff2") format('woff2'), url("fonts/Renner-Book.woff") format('woff'); } @font-face { font-family: "Roboto"; font-style: normal; font-weight: 400; src: local("Roboto"), local("Roboto-Regular"), url("fonts/roboto-v18-latin-regular.woff2") format("woff2"), url("fonts/roboto-v18-latin-regular.woff") format("woff"); } @font-face { font-family: "DejaVu Sans Mono"; font-style: normal; src: local("DejaVu Sans Mono"), url("fonts/DejaVuSansMono.woff2") format("woff2"), url("fonts/DejaVuSansMono.woff") format("woff"); } $display-font-stack: Renner, Futura, Arial, sans-serif; $text-font-stack: Roboto, Arial, sans-serif; $code-font-stack: "DejaVu Sans Mono", "Consolas", monospace; $bg-color: #1F1F1F; $bg-hl-color: #0F0F0F; $text-color: #ABABAB; $link-color: #EFEFEF; $main-content-width: 40rem; html { height: 100%; } body { font: 100% $text-font-stack; color: $text-color; background-color: $bg-color; display: grid; grid-template-areas: "header" "main" "footer"; grid-template-rows: 0px 1fr auto; margin: 100px auto; max-width: $main-content-width; justify-items: center; min-height: 100%; } p { margin: 1rem auto; line-height: 1.5; text-align: justify; } code { font: 100% $code-font-stack; border: 1px solid $text-color; border-radius: 4px; padding: 0 0.25rem 0; margin: 0 0.25rem 0; } a { color: $link-color; text-decoration: none; } hr { margin: 1rem auto; border: 0; border-top: 1px solid $text-color; border-bottom: 1px solid $bg-hl-color; } img { max-width: 100%; } footer { font: 0.75rem $text-font-stack; text-align: center; p { text-align: center; } } ================================================ FILE: site/sass/_reset.scss ================================================ /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; } ================================================ FILE: site/sass/blog-post.scss ================================================ @import "base"; body { margin: 0 auto; display: grid; max-width: $main-content-width; .date { font-family: $text-font-stack; } .title { font-size: 2rem; } } ================================================ FILE: site/sass/blog.scss ================================================ @import "base"; body { // title of the page h1 { margin: 1rem auto; text-align: center; font: 3rem $display-font-stack; } // subtitle of the page h2 { margin: 1rem auto; text-align: center; font: 1.5rem $display-font-stack; } // post name h3 { font-weight: bold; font-family: $text-font-stack; } // post date h4 { font-style: italic; font-family: $text-font-stack; } .post { margin-bottom: 2rem; } } ================================================ FILE: site/sass/style.scss ================================================ @import "base"; $body-margin-top: 150px; body { margin: $body-margin-top auto; padding: $body-margin-top / 2 0; background-image: url(richter-insignia.svg); background-repeat: no-repeat; background-position: 50% $body-margin-top; background-size: 400px; max-width: $main-content-width; justify-items: center; h1 { $heading-font-size: 6rem; margin: ($heading-font-size / 3) auto ($heading-font-size / 3); font-family: $display-font-stack; font-size: $heading-font-size; text-align: center; letter-spacing: 1rem; } h2 { $subheading-font-size: 3rem; margin: ($subheading-font-size / 3) auto ($subheading-font-size / 3); font-family: $display-font-stack; font-size: $subheading-font-size; text-align: center; } .links { margin: 40px 0; font-family: $text-font-stack; font-size: 1.5rem; text-align: center; li { display: inline; list-style: none; // put dots between items &:not(:first-child):before { content: " · "; } } } .intro { margin-top: 40px; font-family: $text-font-stack; font-size: 1rem; text-align: justify; } } ================================================ FILE: site/templates/home.html ================================================ {% block title %}{% endblock title %} ================================================ FILE: site/themes/richter/templates/base.html ================================================ {% block head %} {% block style %} {% endblock style %} {% block title %}{% endblock title %} – Richter {% endblock head %}
{% block header %}{% endblock header %}
{% block main %}{% endblock main %}
================================================ FILE: site/themes/richter/templates/blog-post.html ================================================ {% extends "base.html" %} {% block style %} {% endblock style %} {% block title %}{{ page.title }}{% endblock title%} {% block main %}

{{ page.date }}

{{ page.title }}

{{ page.content | safe }} {% endblock main %} ================================================ FILE: site/themes/richter/templates/blog.html ================================================ {% extends "base.html" %} {% block style %} {% endblock style %} {% block title %}{{ section.title }}{% endblock title %} {% block main %}

{{ section.title }}

{{ section.content | safe }} {% for page in section.pages %}

{{ page.title }}

{{ page.date }}

{{ page.content | safe }}
{% endfor %} {% endblock main %} ================================================ FILE: site/themes/richter/templates/index.html ================================================ {% extends "base.html" %} {% block title %}Home{% endblock title %} {% block style %} {% endblock style %} {% block main %}{{ section.content | safe }}{% endblock main %} ================================================ FILE: site/themes/richter/theme.toml ================================================ name = "richter" description = "theme for the Richter webpage" license = "MIT" min_version = "0.2.2" [author] name = "Mac O'Brien" homepage = "http://c-obrien.org" ================================================ FILE: specifications.md ================================================ # Specifications for the Original Quake (idTech 2) Engine ### Coordinate Systems Quake's coordinate system specifies its axes as follows: - The x-axis specifies depth. - The y-axis specifies width. - The z-axis specifies height. This contrasts with the OpenGL coordinate system, in which: - The x-axis specifies width. - The y-axis specifies height. - The z-axis specifies depth (inverted). Thus, to convert between the coordinate systems: x <-> -z Quake y <-> x OpenGL z <-> y x <-> y OpenGL y <-> z Quake z <-> -x ================================================ FILE: src/bin/quake-client/capture.rs ================================================ use std::{ cell::RefCell, fs::File, io::BufWriter, num::NonZeroU32, path::{Path, PathBuf}, rc::Rc, }; use richter::client::render::Extent2d; use chrono::Utc; const BYTES_PER_PIXEL: u32 = 4; /// Implements the "screenshot" command. /// /// This function returns a boxed closure which sets the `screenshot_path` /// argument to `Some` when called. pub fn cmd_screenshot( screenshot_path: Rc>>, ) -> Box String> { Box::new(move |args| { let path = match args.len() { // TODO: make default path configurable 0 => PathBuf::from(format!("richter-{}.png", Utc::now().format("%FT%H-%M-%S"))), 1 => PathBuf::from(args[0]), _ => { log::error!("Usage: screenshot [PATH]"); return "Usage: screenshot [PATH]".to_owned(); } }; screenshot_path.replace(Some(path)); String::new() }) } pub struct Capture { // size of the capture image capture_size: Extent2d, // width of a row in the buffer, must be a multiple of 256 for mapped reads row_width: u32, // mappable buffer buffer: wgpu::Buffer, } impl Capture { pub fn new(device: &wgpu::Device, capture_size: Extent2d) -> Capture { // bytes_per_row must be a multiple of 256 // 4 bytes per pixel, so width must be multiple of 64 let row_width = (capture_size.width + 63) / 64 * 64; let buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("capture buffer"), size: (row_width * capture_size.height * BYTES_PER_PIXEL) as u64, usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, mapped_at_creation: false, }); Capture { capture_size, row_width, buffer, } } pub fn copy_from_texture( &self, encoder: &mut wgpu::CommandEncoder, texture: wgpu::ImageCopyTexture, ) { encoder.copy_texture_to_buffer( texture, wgpu::ImageCopyBuffer { buffer: &self.buffer, layout: wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(NonZeroU32::new(self.row_width * BYTES_PER_PIXEL).unwrap()), rows_per_image: None, }, }, self.capture_size.into(), ); } pub fn write_to_file

(&self, device: &wgpu::Device, path: P) where P: AsRef, { let mut data = Vec::new(); { // map the buffer // TODO: maybe make this async so we don't force the whole program to block let slice = self.buffer.slice(..); let map_future = slice.map_async(wgpu::MapMode::Read); device.poll(wgpu::Maintain::Wait); futures::executor::block_on(map_future).unwrap(); // copy pixel data let mapped = slice.get_mapped_range(); for row in mapped.chunks(self.row_width as usize * BYTES_PER_PIXEL as usize) { // don't copy padding for pixel in (&row[..self.capture_size.width as usize * BYTES_PER_PIXEL as usize]).chunks(4) { // swap BGRA->RGBA data.extend_from_slice(&[pixel[2], pixel[1], pixel[0], pixel[3]]); } } } self.buffer.unmap(); let f = File::create(path).unwrap(); let mut png_encoder = png::Encoder::new( BufWriter::new(f), self.capture_size.width, self.capture_size.height, ); png_encoder.set_color(png::ColorType::RGBA); png_encoder.set_depth(png::BitDepth::Eight); let mut writer = png_encoder.write_header().unwrap(); writer.write_image_data(&data).unwrap(); } } ================================================ FILE: src/bin/quake-client/game.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::{cell::RefCell, path::PathBuf, rc::Rc}; use crate::{ capture::{cmd_screenshot, Capture}, trace::{cmd_trace_begin, cmd_trace_end}, }; use richter::{ client::{ input::Input, menu::Menu, render::{ Extent2d, GraphicsState, RenderTarget as _, RenderTargetResolve as _, SwapChainTarget, }, trace::TraceFrame, Client, ClientError, }, common::console::{CmdRegistry, Console, CvarRegistry}, }; use chrono::Duration; use failure::Error; use log::info; pub struct Game { cvars: Rc>, cmds: Rc>, input: Rc>, pub client: Client, // if Some(v), trace is in progress trace: Rc>>>, // if Some(path), take a screenshot and save it to path screenshot_path: Rc>>, } impl Game { pub fn new( cvars: Rc>, cmds: Rc>, input: Rc>, client: Client, ) -> Result { // set up input commands input.borrow().register_cmds(&mut cmds.borrow_mut()); // set up screenshots let screenshot_path = Rc::new(RefCell::new(None)); cmds.borrow_mut() .insert("screenshot", cmd_screenshot(screenshot_path.clone())) .unwrap(); // set up frame tracing let trace = Rc::new(RefCell::new(None)); cmds.borrow_mut() .insert("trace_begin", cmd_trace_begin(trace.clone())) .unwrap(); cmds.borrow_mut() .insert("trace_end", cmd_trace_end(cvars.clone(), trace.clone())) .unwrap(); Ok(Game { cvars, cmds, input, client, trace, screenshot_path, }) } // advance the simulation pub fn frame(&mut self, gfx_state: &GraphicsState, frame_duration: Duration) { use ClientError::*; match self.client.frame(frame_duration, gfx_state) { Ok(()) => (), Err(e) => match e { Cvar(_) | UnrecognizedProtocol(_) | NoSuchClient(_) | NoSuchPlayer(_) | NoSuchEntity(_) | NullEntity | EntityExists(_) | InvalidViewEntity(_) | TooManyStaticEntities | NoSuchLightmapAnimation(_) | Model(_) | Network(_) | Sound(_) | Vfs(_) => { log::error!("{}", e); self.client.disconnect(); } _ => panic!("{}", e), }, }; if let Some(ref mut game_input) = self.input.borrow_mut().game_input_mut() { self.client .handle_input(game_input, frame_duration) .unwrap(); } // if there's an active trace, record this frame if let Some(ref mut trace_frames) = *self.trace.borrow_mut() { trace_frames.push( self.client .trace(&[self.client.view_entity_id().unwrap()]) .unwrap(), ); } } pub fn render( &mut self, gfx_state: &GraphicsState, color_attachment_view: &wgpu::TextureView, width: u32, height: u32, console: &Console, menu: &Menu, ) { info!("Beginning render pass"); let mut encoder = gfx_state .device() .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); // render world, hud, console, menus self.client .render( gfx_state, &mut encoder, width, height, menu, self.input.borrow().focus(), ) .unwrap(); // screenshot setup let capture = self.screenshot_path.borrow().as_ref().map(|_| { let cap = Capture::new(gfx_state.device(), Extent2d { width, height }); cap.copy_from_texture( &mut encoder, wgpu::ImageCopyTexture { texture: gfx_state.final_pass_target().resolve_attachment(), mip_level: 0, origin: wgpu::Origin3d::ZERO, }, ); cap }); // blit to swap chain { let swap_chain_target = SwapChainTarget::with_swap_chain_view(color_attachment_view); let blit_pass_builder = swap_chain_target.render_pass_builder(); let mut blit_pass = encoder.begin_render_pass(&blit_pass_builder.descriptor()); gfx_state.blit_pipeline().blit(gfx_state, &mut blit_pass); } let command_buffer = encoder.finish(); { gfx_state.queue().submit(vec![command_buffer]); gfx_state.device().poll(wgpu::Maintain::Wait); } // write screenshot if requested and clear screenshot path self.screenshot_path.replace(None).map(|path| { capture .as_ref() .unwrap() .write_to_file(gfx_state.device(), path) }); } } impl std::ops::Drop for Game { fn drop(&mut self) { let _ = self.cmds.borrow_mut().remove("trace_begin"); let _ = self.cmds.borrow_mut().remove("trace_end"); } } ================================================ FILE: src/bin/quake-client/main.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. mod capture; mod game; mod menu; mod trace; use std::{ cell::{Ref, RefCell, RefMut}, fs::File, io::{Cursor, Read, Write}, net::SocketAddr, path::{Path, PathBuf}, process::exit, rc::Rc, }; use game::Game; use chrono::Duration; use common::net::ServerCmd; use richter::{ client::{ self, demo::DemoServer, input::{Input, InputFocus}, menu::Menu, render::{self, Extent2d, GraphicsState, UiRenderer, DIFFUSE_ATTACHMENT_FORMAT}, Client, }, common::{ self, console::{CmdRegistry, Console, CvarRegistry}, host::{Host, Program}, vfs::Vfs, }, }; use structopt::StructOpt; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, window::Window, }; struct ClientProgram { vfs: Rc, cvars: Rc>, cmds: Rc>, console: Rc>, menu: Rc>, window: Window, window_dimensions_changed: bool, surface: wgpu::Surface, swap_chain: RefCell, gfx_state: RefCell, ui_renderer: Rc, game: Game, input: Rc>, } impl ClientProgram { pub async fn new(window: Window, base_dir: Option, trace: bool) -> ClientProgram { let vfs = Vfs::with_base_dir(base_dir.unwrap_or(common::default_base_dir())); let con_names = Rc::new(RefCell::new(Vec::new())); let cvars = Rc::new(RefCell::new(CvarRegistry::new(con_names.clone()))); client::register_cvars(&cvars.borrow()).unwrap(); render::register_cvars(&cvars.borrow()); let cmds = Rc::new(RefCell::new(CmdRegistry::new(con_names))); // TODO: register commands as other subsystems come online let console = Rc::new(RefCell::new(Console::new(cmds.clone(), cvars.clone()))); let menu = Rc::new(RefCell::new(menu::build_main_menu().unwrap())); let input = Rc::new(RefCell::new(Input::new( InputFocus::Console, console.clone(), menu.clone(), ))); input.borrow_mut().bind_defaults(); let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); let surface = unsafe { instance.create_surface(&window) }; let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), }) .await .unwrap(); let (device, queue) = adapter .request_device( &wgpu::DeviceDescriptor { label: None, features: wgpu::Features::PUSH_CONSTANTS | wgpu::Features::SAMPLED_TEXTURE_BINDING_ARRAY | wgpu::Features::SAMPLED_TEXTURE_ARRAY_DYNAMIC_INDEXING | wgpu::Features::SAMPLED_TEXTURE_ARRAY_NON_UNIFORM_INDEXING, limits: wgpu::Limits { max_sampled_textures_per_shader_stage: 256, max_uniform_buffer_binding_size: 65536, max_push_constant_size: 256, ..Default::default() }, }, if trace { Some(Path::new("./trace/")) } else { None }, ) .await .unwrap(); let size: Extent2d = window.inner_size().into(); let swap_chain = RefCell::new(device.create_swap_chain( &surface, &wgpu::SwapChainDescriptor { usage: wgpu::TextureUsage::RENDER_ATTACHMENT, format: DIFFUSE_ATTACHMENT_FORMAT, width: size.width, height: size.height, present_mode: wgpu::PresentMode::Immediate, }, )); let vfs = Rc::new(vfs); // TODO: warn user if r_msaa_samples is invalid let mut sample_count = cvars.borrow().get_value("r_msaa_samples").unwrap_or(2.0) as u32; if !&[2, 4].contains(&sample_count) { sample_count = 2; } let gfx_state = GraphicsState::new(device, queue, size, sample_count, vfs.clone()).unwrap(); let ui_renderer = Rc::new(UiRenderer::new(&gfx_state, &menu.borrow())); // TODO: factor this out // implements "exec" command let exec_vfs = vfs.clone(); let exec_console = console.clone(); cmds.borrow_mut().insert_or_replace( "exec", Box::new(move |args| { match args.len() { // exec (filename): execute a script file 1 => { let mut script_file = match exec_vfs.open(args[0]) { Ok(s) => s, Err(e) => { return format!("Couldn't exec {}: {:?}", args[0], e); } }; let mut script = String::new(); script_file.read_to_string(&mut script).unwrap(); exec_console.borrow().stuff_text(script); String::new() } _ => format!("exec (filename): execute a script file"), } }), ).unwrap(); // this will also execute config.cfg and autoexec.cfg (assuming an unmodified quake.rc) console.borrow().stuff_text("exec quake.rc\n"); let client = Client::new( vfs.clone(), cvars.clone(), cmds.clone(), console.clone(), input.clone(), &gfx_state, &menu.borrow(), ); let game = Game::new(cvars.clone(), cmds.clone(), input.clone(), client).unwrap(); ClientProgram { vfs, cvars, cmds, console, menu, window, window_dimensions_changed: false, surface, swap_chain, gfx_state: RefCell::new(gfx_state), ui_renderer, game, input, } } /// Builds a new swap chain with the specified present mode and the window's current dimensions. fn recreate_swap_chain(&self, present_mode: wgpu::PresentMode) { let winit::dpi::PhysicalSize { width, height } = self.window.inner_size(); let swap_chain = self.gfx_state.borrow().device().create_swap_chain( &self.surface, &wgpu::SwapChainDescriptor { usage: wgpu::TextureUsage::RENDER_ATTACHMENT, format: DIFFUSE_ATTACHMENT_FORMAT, width, height, present_mode, }, ); let _ = self.swap_chain.replace(swap_chain); } fn render(&mut self) { let swap_chain_output = self.swap_chain.borrow_mut().get_current_frame().unwrap(); let winit::dpi::PhysicalSize { width, height } = self.window.inner_size(); self.game.render( &self.gfx_state.borrow(), &swap_chain_output.output.view, width, height, &self.console.borrow(), &self.menu.borrow(), ); } } impl Program for ClientProgram { fn handle_event( &mut self, event: Event, _target: &EventLoopWindowTarget, _control_flow: &mut ControlFlow, ) { match event { Event::WindowEvent { event: WindowEvent::Resized(_), .. } => { self.window_dimensions_changed = true; } e => self.input.borrow_mut().handle_event(e).unwrap(), } } fn frame(&mut self, frame_duration: Duration) { // recreate swapchain if needed if self.window_dimensions_changed { self.window_dimensions_changed = false; self.recreate_swap_chain(wgpu::PresentMode::Immediate); } let size: Extent2d = self.window.inner_size().into(); // TODO: warn user if r_msaa_samples is invalid let mut sample_count = self .cvars .borrow() .get_value("r_msaa_samples") .unwrap_or(2.0) as u32; if !&[2, 4].contains(&sample_count) { sample_count = 2; } // recreate attachments and rebuild pipelines if necessary self.gfx_state.borrow_mut().update(size, sample_count); self.game.frame(&self.gfx_state.borrow(), frame_duration); match self.input.borrow().focus() { InputFocus::Game => { if let Err(e) = self.window.set_cursor_grab(true) { // This can happen if the window is running in another // workspace. It shouldn't be considered an error. log::debug!("Couldn't grab cursor: {}", e); } self.window.set_cursor_visible(false); } _ => { if let Err(e) = self.window.set_cursor_grab(false) { log::debug!("Couldn't release cursor: {}", e); }; self.window.set_cursor_visible(true); } } // run console commands self.console.borrow().execute(); self.render(); } fn shutdown(&mut self) { // TODO: do cleanup things here } fn cvars(&self) -> Ref { self.cvars.borrow() } fn cvars_mut(&self) -> RefMut { self.cvars.borrow_mut() } } #[derive(StructOpt, Debug)] struct Opt { #[structopt(long)] trace: bool, #[structopt(long)] connect: Option, #[structopt(long)] dump_demo: Option, #[structopt(long)] demo: Option, #[structopt(long)] base_dir: Option, } fn main() { env_logger::init(); let opt = Opt::from_args(); let event_loop = EventLoop::new(); let window = { #[cfg(target_os = "windows")] { use winit::platform::windows::WindowBuilderExtWindows as _; winit::window::WindowBuilder::new() // disable file drag-and-drop so cpal and winit play nice .with_drag_and_drop(false) .with_title("Richter client") .with_inner_size(winit::dpi::PhysicalSize::::from((1366u32, 768))) .build(&event_loop) .unwrap() } #[cfg(not(target_os = "windows"))] { winit::window::WindowBuilder::new() .with_title("Richter client") .with_inner_size(winit::dpi::PhysicalSize::::from((1366u32, 768))) .build(&event_loop) .unwrap() } }; let client_program = futures::executor::block_on(ClientProgram::new(window, opt.base_dir, opt.trace)); // TODO: make dump_demo part of top-level binary and allow choosing file name if let Some(ref demo) = opt.dump_demo { let mut demfile = match client_program.vfs.open(demo) { Ok(d) => d, Err(e) => { eprintln!("error opening demofile: {}", e); std::process::exit(1); } }; let mut demserv = match DemoServer::new(&mut demfile) { Ok(d) => d, Err(e) => { eprintln!("error starting demo server: {}", e); std::process::exit(1); } }; let mut outfile = File::create("demodump.txt").unwrap(); loop { match demserv.next() { Some(msg) => { let mut curs = Cursor::new(msg.message()); loop { match ServerCmd::deserialize(&mut curs) { Ok(Some(cmd)) => write!(&mut outfile, "{:#?}\n", cmd).unwrap(), Ok(None) => break, Err(e) => { eprintln!("error processing demo: {}", e); std::process::exit(1); } } } } None => break, } } std::process::exit(0); } if let Some(ref server) = opt.connect { client_program .console .borrow_mut() .stuff_text(format!("connect {}", server)); } else if let Some(ref demo) = opt.demo { client_program .console .borrow_mut() .stuff_text(format!("playdemo {}", demo)); } let mut host = Host::new(client_program); event_loop.run(move |event, _target, control_flow| { host.handle_event(event, _target, control_flow); }); } ================================================ FILE: src/bin/quake-client/menu.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use richter::client::menu::{Menu, MenuBodyView, MenuBuilder, MenuView}; use failure::Error; pub fn build_main_menu() -> Result { Ok(MenuBuilder::new() .add_submenu("Single Player", build_menu_sp()?) .add_submenu("Multiplayer", build_menu_mp()?) .add_submenu("Options", build_menu_options()?) .add_action("Help/Ordering", Box::new(|| ())) .add_action("Quit", Box::new(|| ())) .build(MenuView { draw_plaque: true, title_path: "gfx/ttl_main.lmp".to_string(), body: MenuBodyView::Predefined { path: "gfx/mainmenu.lmp".to_string(), }, })) } fn build_menu_sp() -> Result { Ok(MenuBuilder::new() .add_action("New Game", Box::new(|| ())) // .add_submenu("Load", unimplemented!()) // .add_submenu("Save", unimplemented!()) .build(MenuView { draw_plaque: true, title_path: "gfx/ttl_sgl.lmp".to_string(), body: MenuBodyView::Predefined { path: "gfx/sp_menu.lmp".to_string(), }, })) } fn build_menu_mp() -> Result { Ok(MenuBuilder::new() .add_submenu("Join a Game", build_menu_mp_join()?) // .add_submenu("New Game", unimplemented!()) // .add_submenu("Setup", unimplemented!()) .build(MenuView { draw_plaque: true, title_path: "gfx/p_multi.lmp".to_string(), body: MenuBodyView::Predefined { path: "gfx/mp_menu.lmp".to_string(), }, })) } fn build_menu_mp_join() -> Result { Ok(MenuBuilder::new() .add_submenu("TCP", build_menu_mp_join_tcp()?) // .add_textbox // description .build(MenuView { draw_plaque: true, title_path: "gfx/p_multi.lmp".to_string(), body: MenuBodyView::Predefined { path: "gfx/mp_menu.lmp".to_string(), }, })) } fn build_menu_mp_join_tcp() -> Result { // Join Game - TCP/IP // title // // Address: 127.0.0.1 // label // // Port [26000] // text field // // Search for local games... // menu // // Join game at: // label // [ ] // text field Ok(MenuBuilder::new() // .add .add_toggle("placeholder", false, Box::new(|_| ())) .build(MenuView { draw_plaque: true, title_path: "gfx/p_multi.lmp".to_string(), body: MenuBodyView::Dynamic, })) } fn build_menu_options() -> Result { Ok(MenuBuilder::new() // .add_submenu("Customize controls", unimplemented!()) .add_action("Go to console", Box::new(|| ())) .add_action("Reset to defaults", Box::new(|| ())) .add_slider("Render scale", 0.25, 1.0, 2, 0, Box::new(|_| ()))? .add_slider("Screen Size", 0.0, 1.0, 10, 9, Box::new(|_| ()))? .add_slider("Brightness", 0.0, 1.0, 10, 9, Box::new(|_| ()))? .add_slider("Mouse Speed", 0.0, 1.0, 10, 9, Box::new(|_| ()))? .add_slider("CD music volume", 0.0, 1.0, 10, 9, Box::new(|_| ()))? .add_slider("Sound volume", 0.0, 1.0, 10, 9, Box::new(|_| ()))? .add_toggle("Always run", true, Box::new(|_| ())) .add_toggle("Invert mouse", false, Box::new(|_| ())) .add_toggle("Lookspring", false, Box::new(|_| ())) .add_toggle("Lookstrafe", false, Box::new(|_| ())) // .add_submenu("Video options", unimplemented!()) .build(MenuView { draw_plaque: true, title_path: "gfx/p_option.lmp".to_string(), body: MenuBodyView::Dynamic, })) } ================================================ FILE: src/bin/quake-client/trace.rs ================================================ use std::{cell::RefCell, io::BufWriter, rc::Rc, fs::File}; use richter::{client::trace::TraceFrame, common::console::CvarRegistry}; const DEFAULT_TRACE_PATH: &'static str = "richter-trace.json"; /// Implements the `trace_begin` command. pub fn cmd_trace_begin(trace: Rc>>>) -> Box String> { Box::new(move |_| { if trace.borrow().is_some() { log::error!("trace already in progress"); "trace already in progress".to_owned() } else { // start a new trace trace.replace(Some(Vec::new())); String::new() } }) } /// Implements the `trace_end` command. pub fn cmd_trace_end( cvars: Rc>, trace: Rc>>>, ) -> Box String> { Box::new(move |_| { if let Some(trace_frames) = trace.replace(None) { let trace_path = cvars .borrow() .get("trace_path") .unwrap_or(DEFAULT_TRACE_PATH.to_string()); let trace_file = match File::create(&trace_path) { Ok(f) => f, Err(e) => { log::error!("Couldn't open trace file for write: {}", e); return format!("Couldn't open trace file for write: {}", e); } }; let mut writer = BufWriter::new(trace_file); match serde_json::to_writer(&mut writer, &trace_frames) { Ok(()) => (), Err(e) => { log::error!("Couldn't serialize trace: {}", e); return format!("Couldn't serialize trace: {}", e); } }; log::debug!("wrote {} frames to {}", trace_frames.len(), &trace_path); format!("wrote {} frames to {}", trace_frames.len(), &trace_path) } else { log::error!("no trace in progress"); "no trace in progress".to_owned() } }) } ================================================ FILE: src/bin/unpak.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. extern crate richter; use std::{ fs, fs::File, io::{BufWriter, Write}, path::PathBuf, process::exit, }; use richter::common::pak::Pak; use structopt::StructOpt; #[derive(Debug, StructOpt)] struct Opt { #[structopt(short, long)] verbose: bool, #[structopt(long)] version: bool, #[structopt(name = "INPUT_PAK", parse(from_os_str))] input_pak: PathBuf, #[structopt(name = "OUTPUT_DIR", parse(from_os_str))] output_dir: Option, } const VERSION: &'static str = " unpak 0.1 Copyright © 2020 Cormac O'Brien Released under the terms of the MIT License "; fn main() { let opt = Opt::from_args(); if opt.version { println!("{}", VERSION); exit(0); } let pak = match Pak::new(&opt.input_pak) { Ok(p) => p, Err(why) => { println!("Couldn't open {:#?}: {}", &opt.input_pak, why); exit(1); } }; for (k, v) in pak.iter() { let mut path = PathBuf::new(); if let Some(ref d) = opt.output_dir { path.push(d); } path.push(k); if let Some(p) = path.parent() { if !p.exists() { if let Err(why) = fs::create_dir_all(p) { println!("Couldn't create parent directories: {}", why); exit(1); } } } let file = match File::create(&path) { Ok(f) => f, Err(why) => { println!("Couldn't open {}: {}", path.to_str().unwrap(), why); exit(1); } }; let mut writer = BufWriter::new(file); match writer.write_all(v.as_ref()) { Ok(_) => (), Err(why) => { println!("Couldn't write to {}: {}", path.to_str().unwrap(), why); exit(1); } } } } ================================================ FILE: src/client/cvars.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use crate::common::console::{CvarRegistry, ConsoleError}; pub fn register_cvars(cvars: &CvarRegistry) -> Result<(), ConsoleError> { cvars.register("cl_anglespeedkey", "1.5")?; cvars.register_archive("cl_backspeed", "200")?; cvars.register("cl_bob", "0.02")?; cvars.register("cl_bobcycle", "0.6")?; cvars.register("cl_bobup", "0.5")?; cvars.register_archive("_cl_color", "0")?; cvars.register("cl_crossx", "0")?; cvars.register("cl_crossy", "0")?; cvars.register_archive("cl_forwardspeed", "400")?; cvars.register("cl_movespeedkey", "2.0")?; cvars.register_archive("_cl_name", "player")?; cvars.register("cl_nolerp", "0")?; cvars.register("cl_pitchspeed", "150")?; cvars.register("cl_rollangle", "2.0")?; cvars.register("cl_rollspeed", "200")?; cvars.register("cl_shownet", "0")?; cvars.register("cl_sidespeed", "350")?; cvars.register("cl_upspeed", "200")?; cvars.register("cl_yawspeed", "140")?; cvars.register("fov", "90")?; cvars.register_archive("m_pitch", "0.022")?; cvars.register_archive("m_yaw", "0.022")?; cvars.register_archive("sensitivity", "3")?; cvars.register("v_idlescale", "0")?; cvars.register("v_ipitch_cycle", "1")?; cvars.register("v_ipitch_level", "0.3")?; cvars.register("v_iroll_cycle", "0.5")?; cvars.register("v_iroll_level", "0.1")?; cvars.register("v_iyaw_cycle", "2")?; cvars.register("v_iyaw_level", "0.3")?; cvars.register("v_kickpitch", "0.6")?; cvars.register("v_kickroll", "0.6")?; cvars.register("v_kicktime", "0.5")?; // some server cvars are needed by the client, but if the server is running // in the same process they will have been set already, so we can ignore // the duplicate cvar error let _ = cvars.register("sv_gravity", "800"); Ok(()) } ================================================ FILE: src/client/demo.rs ================================================ use std::{io, ops::Range}; use crate::common::{ net::{self, NetError}, util::read_f32_3, vfs::VirtualFile, }; use arrayvec::ArrayVec; use byteorder::{LittleEndian, ReadBytesExt}; use cgmath::{Deg, Vector3}; use io::BufReader; use thiserror::Error; /// An error returned by a demo server. #[derive(Error, Debug)] pub enum DemoServerError { #[error("Invalid CD track number")] InvalidCdTrack, #[error("No such CD track: {0}")] NoSuchCdTrack(i32), #[error("Message size ({0}) exceeds maximum allowed size {}", net::MAX_MESSAGE)] MessageTooLong(u32), #[error("I/O error: {0}")] Io(#[from] io::Error), #[error("Network error: {0}")] Net(#[from] NetError), } struct DemoMessage { view_angles: Vector3>, msg_range: Range, } /// A view of a server message from a demo. pub struct DemoMessageView<'a> { view_angles: Vector3>, message: &'a [u8], } impl<'a> DemoMessageView<'a> { /// Returns the view angles recorded for this demo message. pub fn view_angles(&self) -> Vector3> { self.view_angles } /// Returns the server message for this demo message as a slice of bytes. pub fn message(&self) -> &[u8] { self.message } } /// A server that yields commands from a demo file. pub struct DemoServer { track_override: Option, // id of next message to "send" message_id: usize, messages: Vec, // all message data message_data: Vec, } impl DemoServer { /// Construct a new `DemoServer` from the specified demo file. pub fn new(file: &mut VirtualFile) -> Result { let mut dem_reader = BufReader::new(file); let mut buf = ArrayVec::::new(); // copy CD track number (terminated by newline) into buffer for i in 0..buf.capacity() { match dem_reader.read_u8()? { b'\n' => break, // cannot panic because we won't exceed capacity with a loop this small b => buf.push(b), } if i >= buf.capacity() - 1 { // CD track would be more than 2 digits long, which is impossible Err(DemoServerError::InvalidCdTrack)?; } } let track_override = { let track_str = match std::str::from_utf8(&buf) { Ok(s) => s, Err(_) => Err(DemoServerError::InvalidCdTrack)?, }; match track_str { // if track is empty, default to track 0 "" => Some(0), s => match s.parse::() { Ok(track) => match track { // if track is -1, allow demo to specify tracks in messages -1 => None, t if t < -1 => Err(DemoServerError::InvalidCdTrack)?, _ => Some(track as u32), }, Err(_) => Err(DemoServerError::InvalidCdTrack)?, }, } }; let mut message_data = Vec::new(); let mut messages = Vec::new(); // read all messages while let Ok(msg_len) = dem_reader.read_u32::() { // get view angles let view_angles_f32 = read_f32_3(&mut dem_reader)?; let view_angles = Vector3::new( Deg(view_angles_f32[0]), Deg(view_angles_f32[1]), Deg(view_angles_f32[2]), ); // read next message let msg_start = message_data.len(); for _ in 0..msg_len { message_data.push(dem_reader.read_u8()?); } let msg_end = message_data.len(); messages.push(DemoMessage { view_angles, msg_range: msg_start..msg_end, }); } Ok(DemoServer { track_override, message_id: 0, messages, message_data, }) } /// Retrieve the next server message from the currently playing demo. /// /// If this returns `None`, the demo is complete. pub fn next(&mut self) -> Option { if self.message_id >= self.messages.len() { return None; } let msg = &self.messages[self.message_id]; self.message_id += 1; Some(DemoMessageView { view_angles: msg.view_angles, message: &self.message_data[msg.msg_range.clone()], }) } /// Returns the currently playing demo's music track override, if any. /// /// If this is `Some`, any `CdTrack` commands from the demo server should /// cause the client to play this track instead of the one specified by the /// command. pub fn track_override(&self) -> Option { self.track_override } } ================================================ FILE: src/client/entity/mod.rs ================================================ // Copyright © 2020 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. pub mod particle; use crate::common::{ alloc::LinkedSlab, engine, net::{EntityEffects, EntityState, EntityUpdate}, }; use cgmath::{Deg, Vector3}; use chrono::Duration; // if this is changed, it must also be changed in deferred.frag pub const MAX_LIGHTS: usize = 32; pub const MAX_BEAMS: usize = 24; pub const MAX_TEMP_ENTITIES: usize = 64; pub const MAX_STATIC_ENTITIES: usize = 128; #[derive(Debug)] pub struct ClientEntity { pub force_link: bool, pub baseline: EntityState, pub msg_time: Duration, pub msg_origins: [Vector3; 2], pub origin: Vector3, pub msg_angles: [Vector3>; 2], pub angles: Vector3>, pub model_id: usize, model_changed: bool, pub frame_id: usize, pub skin_id: usize, colormap: Option, pub sync_base: Duration, pub effects: EntityEffects, pub light_id: Option, // vis_frame: usize, } impl ClientEntity { pub fn from_baseline(baseline: EntityState) -> ClientEntity { ClientEntity { force_link: false, baseline: baseline.clone(), msg_time: Duration::zero(), msg_origins: [Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 0.0)], origin: baseline.origin, msg_angles: [ Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)), Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)), ], angles: baseline.angles, model_id: baseline.model_id, model_changed: false, frame_id: baseline.frame_id, skin_id: baseline.skin_id, colormap: None, sync_base: Duration::zero(), effects: baseline.effects, light_id: None, } } pub fn uninitialized() -> ClientEntity { ClientEntity { force_link: false, baseline: EntityState::uninitialized(), msg_time: Duration::zero(), msg_origins: [Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, 0.0)], origin: Vector3::new(0.0, 0.0, 0.0), msg_angles: [ Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)), Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)), ], angles: Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)), model_id: 0, model_changed: false, frame_id: 0, skin_id: 0, colormap: None, sync_base: Duration::zero(), effects: EntityEffects::empty(), light_id: None, } } /// Update the entity with values from the server. /// /// `msg_times` specifies the last two message times from the server, where /// `msg_times[0]` is more recent. pub fn update(&mut self, msg_times: [Duration; 2], update: EntityUpdate) { // enable lerping self.force_link = false; if update.no_lerp || self.msg_time != msg_times[1] { self.force_link = true; } self.msg_time = msg_times[0]; // fill in missing values from baseline let new_state = update.to_entity_state(&self.baseline); self.msg_origins[1] = self.msg_origins[0]; self.msg_origins[0] = new_state.origin; self.msg_angles[1] = self.msg_angles[0]; self.msg_angles[0] = new_state.angles; if self.model_id != new_state.model_id { self.model_changed = true; self.force_link = true; self.model_id = new_state.model_id; } self.frame_id = new_state.frame_id; self.skin_id = new_state.skin_id; self.effects = new_state.effects; self.colormap = update.colormap; if self.force_link { self.msg_origins[1] = self.msg_origins[0]; self.origin = self.msg_origins[0]; self.msg_angles[1] = self.msg_angles[0]; self.angles = self.msg_angles[0]; } } /// Sets the entity's most recent message angles to the specified value. /// /// This is primarily useful for allowing interpolated view angles in demos. pub fn update_angles(&mut self, angles: Vector3>) { self.msg_angles[0] = angles; } /// Sets the entity's angles to the specified value, overwriting the message /// history. /// /// This causes the entity to "snap" to the correct angle rather than /// interpolating to it. pub fn set_angles(&mut self, angles: Vector3>) { self.msg_angles[0] = angles; self.msg_angles[1] = angles; self.angles = angles; } /// Returns the timestamp of the last message that updated this entity. pub fn msg_time(&self) -> Duration { self.msg_time } /// Returns true if the last update to this entity changed its model. pub fn model_changed(&self) -> bool { self.model_changed } pub fn colormap(&self) -> Option { self.colormap } pub fn get_origin(&self) -> Vector3 { self.origin } pub fn get_angles(&self) -> Vector3> { self.angles } pub fn model_id(&self) -> usize { self.model_id } pub fn frame_id(&self) -> usize { self.frame_id } pub fn skin_id(&self) -> usize { self.skin_id } } /// A descriptor used to spawn dynamic lights. #[derive(Clone, Debug)] pub struct LightDesc { /// The origin of the light. pub origin: Vector3, /// The initial radius of the light. pub init_radius: f32, /// The rate of radius decay in units/second. pub decay_rate: f32, /// If the radius decays to this value, the light is ignored. pub min_radius: Option, /// Time-to-live of the light. pub ttl: Duration, } /// A dynamic point light. #[derive(Clone, Debug)] pub struct Light { origin: Vector3, init_radius: f32, decay_rate: f32, min_radius: Option, spawned: Duration, ttl: Duration, } impl Light { /// Create a light from a `LightDesc` at the specified time. pub fn from_desc(time: Duration, desc: LightDesc) -> Light { Light { origin: desc.origin, init_radius: desc.init_radius, decay_rate: desc.decay_rate, min_radius: desc.min_radius, spawned: time, ttl: desc.ttl, } } /// Return the origin of the light. pub fn origin(&self) -> Vector3 { self.origin } /// Return the radius of the light for the given time. /// /// If the radius would decay to a negative value, returns 0. pub fn radius(&self, time: Duration) -> f32 { let lived = time - self.spawned; let decay = self.decay_rate * engine::duration_to_f32(lived); let radius = (self.init_radius - decay).max(0.0); if let Some(min) = self.min_radius { if radius < min { return 0.0; } } radius } /// Returns `true` if the light should be retained at the specified time. pub fn retain(&mut self, time: Duration) -> bool { self.spawned + self.ttl > time } } /// A set of active dynamic lights. pub struct Lights { slab: LinkedSlab, } impl Lights { /// Create an empty set of lights with the given capacity. pub fn with_capacity(capacity: usize) -> Lights { Lights { slab: LinkedSlab::with_capacity(capacity), } } /// Return a reference to the light with the given key, or `None` if no /// such light exists. pub fn get(&self, key: usize) -> Option<&Light> { self.slab.get(key) } /// Return a mutable reference to the light with the given key, or `None` /// if no such light exists. pub fn get_mut(&mut self, key: usize) -> Option<&mut Light> { self.slab.get_mut(key) } /// Insert a new light into the set of lights. /// /// Returns a key corresponding to the newly inserted light. /// /// If `key` is `Some` and there is an existing light with that key, then /// the light will be overwritten with the new value. pub fn insert(&mut self, time: Duration, desc: LightDesc, key: Option) -> usize { if let Some(k) = key { if let Some(key_light) = self.slab.get_mut(k) { *key_light = Light::from_desc(time, desc); return k; } } self.slab.insert(Light::from_desc(time, desc)) } /// Return an iterator over the active lights. pub fn iter(&self) -> impl Iterator { self.slab.iter() } /// Updates the set of dynamic lights for the specified time. /// /// This will deallocate any lights which have outlived their time-to-live. pub fn update(&mut self, time: Duration) { self.slab.retain(|_, light| light.retain(time)); } } #[derive(Copy, Clone, Debug)] pub struct Beam { pub entity_id: usize, pub model_id: usize, pub expire: Duration, pub start: Vector3, pub end: Vector3, } ================================================ FILE: src/client/entity/particle.rs ================================================ // Copyright © 2020 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::ops::RangeInclusive; use crate::{ client::ClientEntity, common::{ alloc::LinkedSlab, engine, math::{self, VERTEX_NORMAL_COUNT}, }, }; use cgmath::{InnerSpace as _, Vector3, Zero as _}; use chrono::Duration; use rand::{ distributions::{Distribution as _, Uniform}, rngs::SmallRng, SeedableRng, }; lazy_static! { static ref COLOR_RAMP_EXPLOSION_FAST: ColorRamp = ColorRamp { ramp: vec![0x6F, 0x6D, 0x6B, 0x69, 0x67, 0x65, 0x63, 0x61], fps: 10.0, }; static ref COLOR_RAMP_EXPLOSION_SLOW: ColorRamp = ColorRamp { ramp: vec![0x6F, 0x6E, 0x6D, 0x6C, 0x6B, 0x6A, 0x68, 0x66], fps: 5.0, }; static ref COLOR_RAMP_FIRE: ColorRamp = ColorRamp { ramp: vec![0x6D, 0x6B, 0x06, 0x05, 0x04, 0x03], fps: 15.0, }; static ref EXPLOSION_SCATTER_DISTRIBUTION: Uniform = Uniform::new(-16.0, 16.0); static ref EXPLOSION_VELOCITY_DISTRIBUTION: Uniform = Uniform::new(-256.0, 256.0); } // TODO: make max configurable pub const MIN_PARTICLES: usize = 512; // should be possible to get the whole particle list in cache at once pub const MAX_PARTICLES: usize = 16384; /// An animated color ramp. /// /// Colors are specified using 8-bit indexed values, which should be translated /// using the palette. #[derive(Debug)] pub struct ColorRamp { // TODO: arrayvec, tinyvec, or array once const generics are stable ramp: Vec, // frames per second of the animation fps: f32, } impl ColorRamp { /// Returns the frame corresponding to the given time. /// /// If the animation has already completed by `elapsed`, returns `None`. pub fn color(&self, elapsed: Duration, frame_skip: usize) -> Option { let frame = (engine::duration_to_f32(elapsed) * self.fps) as usize + frame_skip; self.ramp.get(frame).map(|c| *c) } } /// Dictates the behavior of a particular particle. /// /// Particles which are animated with a color ramp are despawned automatically /// when the animation is complete. #[derive(Copy, Clone, Debug)] pub enum ParticleKind { /// Normal particle, unaffected by gravity. Static, /// Normal particle, affected by gravity. Grav, /// Fire and smoke particles. Animated using `COLOR_RAMP_FIRE`. Inversely /// affected by gravity, rising instead of falling. Fire { /// Specifies the number of frames to skip. frame_skip: usize, }, /// Explosion particles. May have `COLOR_RAMP_EXPLOSION_FAST` or /// `COLOR_RAMP_EXPLOSION_SLOW`. Affected by gravity. Explosion { /// Specifies the color ramp to use. ramp: &'static ColorRamp, /// Specifies the number of frames to skip. frame_skip: usize, }, /// Spawn (enemy) death explosion particle. Accelerates at /// `v(t2) = v(t1) + 4 * (t2 - t1)`. May or may not have an intrinsic /// z-velocity. Blob { /// If false, particle only moves in the XY plane and is unaffected by /// gravity. has_z_velocity: bool, }, } /// Factor at which particles are affected by gravity. pub const PARTICLE_GRAVITY_FACTOR: f32 = 0.05; /// A live particle. #[derive(Copy, Clone, Debug)] pub struct Particle { kind: ParticleKind, origin: Vector3, velocity: Vector3, color: u8, spawned: Duration, expire: Duration, } impl Particle { /// Particle update function. /// /// The return value indicates whether the particle should be retained after this /// frame. /// /// For details on how individual particles behave, see the documentation for /// [`ParticleKind`](ParticleKind). pub fn update(&mut self, time: Duration, frame_time: Duration, sv_gravity: f32) -> bool { use ParticleKind::*; let velocity_factor = engine::duration_to_f32(frame_time); let gravity = velocity_factor * sv_gravity * PARTICLE_GRAVITY_FACTOR; // don't bother updating expired particles if time >= self.expire { return false; } match self.kind { Static => true, Grav => { self.origin += self.velocity * velocity_factor; self.velocity.z -= gravity; true } Fire { frame_skip } => match COLOR_RAMP_FIRE.color(time - self.spawned, frame_skip) { Some(c) => { self.origin += self.velocity * velocity_factor; // rises instead of falling self.velocity.z += gravity; self.color = c; true } None => false, }, Explosion { ramp, frame_skip } => match ramp.color(time - self.spawned, frame_skip) { Some(c) => { self.origin += self.velocity * velocity_factor; self.velocity.z -= gravity; self.color = c; true } None => false, }, Blob { has_z_velocity } => { if !has_z_velocity { let xy_velocity = Vector3::new(self.velocity.x, self.velocity.y, 0.0); self.origin += xy_velocity * velocity_factor; } else { self.origin += self.velocity * velocity_factor; self.velocity.z -= gravity; } true } } } pub fn origin(&self) -> Vector3 { self.origin } pub fn color(&self) -> u8 { self.color } } pub enum TrailKind { Rocket = 0, Smoke = 1, Blood = 2, TracerGreen = 3, BloodSlight = 4, TracerRed = 5, Vore = 6, } /// A list of particles. /// /// Space for new particles is allocated from an internal [`Slab`](slab::Slab) of fixed /// size. pub struct Particles { // allocation pool slab: LinkedSlab, // random number generator rng: SmallRng, angle_velocities: [Vector3; VERTEX_NORMAL_COUNT], } impl Particles { /// Create a new particle list with the given capacity. /// /// This determines the capacity of both the underlying `Slab` and the set of /// live particles. pub fn with_capacity(capacity: usize) -> Particles { lazy_static! { // avelocities initialized with (rand() & 255) * 0.01; static ref VELOCITY_DISTRIBUTION: Uniform = Uniform::new(0.0, 2.56); } let slab = LinkedSlab::with_capacity(capacity.min(MAX_PARTICLES)); let rng = SmallRng::from_entropy(); let angle_velocities = [Vector3::zero(); VERTEX_NORMAL_COUNT]; let mut particles = Particles { slab, rng, angle_velocities, }; for i in 0..angle_velocities.len() { particles.angle_velocities[i] = particles.random_vector3(&VELOCITY_DISTRIBUTION); } particles } /// Insert a particle into the live list. // TODO: come up with a better eviction policy // the original engine ignores new particles if at capacity, but it's not ideal pub fn insert(&mut self, particle: Particle) -> bool { // check capacity if self.slab.len() == self.slab.capacity() { return false; } // insert it self.slab.insert(particle); true } /// Clears all particles. pub fn clear(&mut self) { self.slab.clear(); } pub fn iter(&self) -> impl Iterator { self.slab.iter() } /// Update all live particles, deleting any that are expired. /// /// Particles are updated with [Particle::update]. That /// function's return value indicates whether the particle should be retained /// or not. pub fn update(&mut self, time: Duration, frame_time: Duration, sv_gravity: f32) { self.slab .retain(|_, particle| particle.update(time, frame_time, sv_gravity)); } fn scatter(&mut self, origin: Vector3, scatter_distr: &Uniform) -> Vector3 { origin + Vector3::new( scatter_distr.sample(&mut self.rng), scatter_distr.sample(&mut self.rng), scatter_distr.sample(&mut self.rng), ) } fn random_vector3(&mut self, velocity_distr: &Uniform) -> Vector3 { Vector3::new( velocity_distr.sample(&mut self.rng), velocity_distr.sample(&mut self.rng), velocity_distr.sample(&mut self.rng), ) } /// Creates a spherical cloud of particles around an entity. pub fn create_entity_field(&mut self, time: Duration, entity: &ClientEntity) { let beam_length = 16.0; let dist = 64.0; for i in 0..VERTEX_NORMAL_COUNT { let float_time = engine::duration_to_f32(time); let angles = float_time * self.angle_velocities[i]; let sin_yaw = angles[0].sin(); let cos_yaw = angles[0].cos(); let sin_pitch = angles[1].sin(); let cos_pitch = angles[1].cos(); let forward = Vector3::new(cos_pitch * cos_yaw, cos_pitch * sin_yaw, -sin_pitch); let ttl = Duration::milliseconds(10); let origin = entity.origin + dist * math::VERTEX_NORMALS[i] + beam_length * forward; self.insert(Particle { kind: ParticleKind::Explosion { ramp: &COLOR_RAMP_EXPLOSION_FAST, frame_skip: 0, }, origin, velocity: Vector3::zero(), color: COLOR_RAMP_EXPLOSION_FAST.ramp[0], spawned: time, expire: time + ttl, }); } } /// Spawns a cloud of particles at a point. /// /// Each particle's origin is offset by a vector with components sampled /// from `scatter_distr`, and each particle's velocity is assigned a /// vector with components sampled from `velocity_distr`. /// /// Each particle's color is taken from `colors`, which is an inclusive /// range of palette indices. The spawned particles have evenly distributed /// colors throughout the range. pub fn create_random_cloud( &mut self, count: usize, colors: RangeInclusive, kind: ParticleKind, time: Duration, ttl: Duration, origin: Vector3, scatter_distr: &Uniform, velocity_distr: &Uniform, ) { let color_start = *colors.start() as usize; let color_end = *colors.end() as usize; for i in 0..count { let origin = self.scatter(origin, scatter_distr); let velocity = self.random_vector3(velocity_distr); let color = (color_start + i % (color_end - color_start + 1)) as u8; if !self.insert(Particle { kind, origin, velocity, color, spawned: time, expire: time + ttl, }) { // can't fit any more particles return; }; } } /// Creates a rocket explosion. pub fn create_explosion(&mut self, time: Duration, origin: Vector3) { lazy_static! { static ref FRAME_SKIP_DISTRIBUTION: Uniform = Uniform::new(0, 4); } // spawn 512 particles each for both color ramps for ramp in [&*COLOR_RAMP_EXPLOSION_FAST, &*COLOR_RAMP_EXPLOSION_SLOW].iter() { let frame_skip = FRAME_SKIP_DISTRIBUTION.sample(&mut self.rng); self.create_random_cloud( 512, ramp.ramp[frame_skip]..=ramp.ramp[frame_skip], ParticleKind::Explosion { ramp, frame_skip }, time, Duration::seconds(5), origin, &EXPLOSION_SCATTER_DISTRIBUTION, &EXPLOSION_VELOCITY_DISTRIBUTION, ); } } /// Creates an explosion using the given range of colors. pub fn create_color_explosion( &mut self, time: Duration, origin: Vector3, colors: RangeInclusive, ) { self.create_random_cloud( 512, colors, ParticleKind::Blob { has_z_velocity: true, }, time, Duration::milliseconds(300), origin, &EXPLOSION_SCATTER_DISTRIBUTION, &EXPLOSION_VELOCITY_DISTRIBUTION, ); } /// Creates a death explosion for the Spawn. pub fn create_spawn_explosion(&mut self, time: Duration, origin: Vector3) { // R_BlobExplosion picks a random ttl with 1 + (rand() & 8) * 0.05 // which gives a value of either 1 or 1.4 seconds. // (it's possible it was supposed to be 1 + (rand() & 7) * 0.05, which // would yield between 1 and 1.35 seconds in increments of 50ms.) let ttls = [Duration::seconds(1), Duration::milliseconds(1400)]; for ttl in ttls.iter().cloned() { self.create_random_cloud( 256, 66..=71, ParticleKind::Blob { has_z_velocity: true, }, time, ttl, origin, &EXPLOSION_SCATTER_DISTRIBUTION, &EXPLOSION_VELOCITY_DISTRIBUTION, ); self.create_random_cloud( 256, 150..=155, ParticleKind::Blob { has_z_velocity: false, }, time, ttl, origin, &EXPLOSION_SCATTER_DISTRIBUTION, &EXPLOSION_VELOCITY_DISTRIBUTION, ); } } /// Creates a projectile impact. pub fn create_projectile_impact( &mut self, time: Duration, origin: Vector3, direction: Vector3, color: u8, count: usize, ) { lazy_static! { static ref SCATTER_DISTRIBUTION: Uniform = Uniform::new(-8.0, 8.0); // any color in block of 8 (see below) static ref COLOR_DISTRIBUTION: Uniform = Uniform::new(0, 8); // ttl between 0.1 and 0.5 seconds static ref TTL_DISTRIBUTION: Uniform = Uniform::new(100, 500); } for _ in 0..count { let scatter = self.random_vector3(&SCATTER_DISTRIBUTION); // picks any color in the block of 8 the original color belongs to. // e.g., if the color argument is 17, picks randomly in [16, 23] let color = (color & !7) + COLOR_DISTRIBUTION.sample(&mut self.rng); let ttl = Duration::milliseconds(TTL_DISTRIBUTION.sample(&mut self.rng)); self.insert(Particle { kind: ParticleKind::Grav, origin: origin + scatter, velocity: 15.0 * direction, color, spawned: time, expire: time + ttl, }); } } /// Creates a lava splash effect. pub fn create_lava_splash(&mut self, time: Duration, origin: Vector3) { lazy_static! { // ttl between 2 and 2.64 seconds static ref TTL_DISTRIBUTION: Uniform = Uniform::new(2000, 2640); // any color on row 14 static ref COLOR_DISTRIBUTION: Uniform = Uniform::new(224, 232); static ref DIR_OFFSET_DISTRIBUTION: Uniform = Uniform::new(0.0, 8.0); static ref SCATTER_Z_DISTRIBUTION: Uniform = Uniform::new(0.0, 64.0); static ref VELOCITY_DISTRIBUTION: Uniform = Uniform::new(50.0, 114.0); } for i in -16..16 { for j in -16..16 { let direction = Vector3::new( 8.0 * i as f32 + DIR_OFFSET_DISTRIBUTION.sample(&mut self.rng), 8.0 * j as f32 + DIR_OFFSET_DISTRIBUTION.sample(&mut self.rng), 256.0, ); let scatter = Vector3::new( direction.x, direction.y, SCATTER_Z_DISTRIBUTION.sample(&mut self.rng), ); let velocity = VELOCITY_DISTRIBUTION.sample(&mut self.rng); let color = COLOR_DISTRIBUTION.sample(&mut self.rng); let ttl = Duration::milliseconds(TTL_DISTRIBUTION.sample(&mut self.rng)); self.insert(Particle { kind: ParticleKind::Grav, origin: origin + scatter, velocity: direction.normalize() * velocity, color, spawned: time, expire: time + ttl, }); } } } /// Creates a teleporter warp effect. pub fn create_teleporter_warp(&mut self, time: Duration, origin: Vector3) { lazy_static! { // ttl between 0.2 and 0.34 seconds static ref TTL_DISTRIBUTION: Uniform = Uniform::new(200, 340); // random grey particles static ref COLOR_DISTRIBUTION: Uniform = Uniform::new(7, 14); static ref SCATTER_DISTRIBUTION: Uniform = Uniform::new(0.0, 4.0); static ref VELOCITY_DISTRIBUTION: Uniform = Uniform::new(50.0, 114.0); } for i in (-16..16).step_by(4) { for j in (-16..16).step_by(4) { for k in (-24..32).step_by(4) { let direction = Vector3::new(j as f32, i as f32, k as f32) * 8.0; let scatter = Vector3::new(i as f32, j as f32, k as f32) + self.random_vector3(&SCATTER_DISTRIBUTION); let velocity = VELOCITY_DISTRIBUTION.sample(&mut self.rng); let color = COLOR_DISTRIBUTION.sample(&mut self.rng); let ttl = Duration::milliseconds(TTL_DISTRIBUTION.sample(&mut self.rng)); self.insert(Particle { kind: ParticleKind::Grav, origin: origin + scatter, velocity: direction.normalize() * velocity, color, spawned: time, expire: time + ttl, }); } } } } /// Create a particle trail between two points. /// /// Used for rocket fire/smoke trails, blood spatter, and projectile tracers. /// If `sparse` is true, the interval between particles is increased by 3 units. pub fn create_trail( &mut self, time: Duration, start: Vector3, end: Vector3, kind: TrailKind, sparse: bool, ) { use TrailKind::*; lazy_static! { static ref SCATTER_DISTRIBUTION: Uniform = Uniform::new(-3.0, 3.0); static ref FRAME_SKIP_DISTRIBUTION: Uniform = Uniform::new(0, 4); static ref BLOOD_COLOR_DISTRIBUTION: Uniform = Uniform::new(67, 71); static ref VORE_COLOR_DISTRIBUTION: Uniform = Uniform::new(152, 156); } let distance = (end - start).magnitude(); let direction = (end - start).normalize(); // particle interval in units let interval = if sparse { 3.0 } else { 1.0 } + match kind { BloodSlight => 3.0, _ => 0.0, }; let ttl = Duration::seconds(2); for step in 0..(distance / interval) as i32 { let frame_skip = FRAME_SKIP_DISTRIBUTION.sample(&mut self.rng); let particle_kind = match kind { Rocket => ParticleKind::Fire { frame_skip }, Smoke => ParticleKind::Fire { frame_skip: frame_skip + 2, }, Blood | BloodSlight => ParticleKind::Grav, TracerGreen | TracerRed | Vore => ParticleKind::Static, }; let scatter = self.random_vector3(&SCATTER_DISTRIBUTION); let origin = start + direction * interval + match kind { // vore scatter is [-16, 15] in original // this gives range of ~[-16, 16] Vore => scatter * 5.33, _ => scatter, }; let velocity = match kind { TracerGreen | TracerRed => { 30.0 * if step & 1 == 1 { Vector3::new(direction.y, -direction.x, 0.0) } else { Vector3::new(-direction.y, direction.x, 0.0) } } _ => Vector3::zero(), }; let color = match kind { Rocket => COLOR_RAMP_FIRE.ramp[frame_skip], Smoke => COLOR_RAMP_FIRE.ramp[frame_skip + 2], Blood | BloodSlight => BLOOD_COLOR_DISTRIBUTION.sample(&mut self.rng), TracerGreen => 52 + 2 * (step & 4) as u8, TracerRed => 230 + 2 * (step & 4) as u8, Vore => VORE_COLOR_DISTRIBUTION.sample(&mut self.rng), }; self.insert(Particle { kind: particle_kind, origin, velocity, color, spawned: time, expire: time + ttl, }); } } } #[cfg(test)] mod tests { use super::*; use cgmath::Zero; fn particles_eq(p1: &Particle, p2: &Particle) -> bool { p1.color == p2.color && p1.velocity == p2.velocity && p1.origin == p2.origin } #[test] fn test_particle_list_update() { let mut list = Particles::with_capacity(10); let exp_times = vec![10, 5, 2, 7, 3]; for exp in exp_times.iter() { list.insert(Particle { kind: ParticleKind::Static, origin: Vector3::zero(), velocity: Vector3::zero(), color: 0, spawned: Duration::zero(), expire: Duration::seconds(*exp), }); } let expected: Vec<_> = exp_times .iter() .filter(|t| **t > 5) .map(|t| Particle { kind: ParticleKind::Static, origin: Vector3::zero(), velocity: Vector3::zero(), color: 0, spawned: Duration::zero(), expire: Duration::seconds(*t), }) .collect(); let mut after_update: Vec = Vec::new(); list.update(Duration::seconds(5), Duration::milliseconds(17), 10.0); after_update .iter() .zip(expected.iter()) .for_each(|(p1, p2)| assert!(particles_eq(p1, p2))); } } ================================================ FILE: src/client/input/console.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{cell::RefCell, rc::Rc}; use crate::common::console::Console; use failure::Error; use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode as Key, WindowEvent}; pub struct ConsoleInput { console: Rc>, } impl ConsoleInput { pub fn new(console: Rc>) -> ConsoleInput { ConsoleInput { console } } pub fn handle_event(&self, event: Event) -> Result<(), Error> { match event { Event::WindowEvent { event, .. } => match event { WindowEvent::ReceivedCharacter(c) => self.console.borrow_mut().send_char(c), WindowEvent::KeyboardInput { input: KeyboardInput { virtual_keycode: Some(key), state: ElementState::Pressed, .. }, .. } => match key { Key::Up => self.console.borrow_mut().history_up(), Key::Down => self.console.borrow_mut().history_down(), Key::Left => self.console.borrow_mut().cursor_left(), Key::Right => self.console.borrow_mut().cursor_right(), Key::Grave => self.console.borrow_mut().stuff_text("toggleconsole\n"), _ => (), }, _ => (), }, _ => (), } Ok(()) } } ================================================ FILE: src/client/input/game.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{ cell::{Cell, RefCell}, collections::HashMap, rc::Rc, str::FromStr, string::ToString, }; use crate::common::{ console::{CmdRegistry, Console}, parse, }; use failure::Error; use strum::IntoEnumIterator; use strum_macros::EnumIter; use winit::{ dpi::LogicalPosition, event::{ DeviceEvent, ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta, VirtualKeyCode as Key, WindowEvent, }, }; const ACTION_COUNT: usize = 19; static INPUT_NAMES: [&'static str; 79] = [ ",", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "ALT", "B", "BACKSPACE", "C", "CTRL", "D", "DEL", "DOWNARROW", "E", "END", "ENTER", "ESCAPE", "F", "F1", "F10", "F11", "F12", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "G", "H", "HOME", "I", "INS", "J", "K", "L", "LEFTARROW", "M", "MOUSE1", "MOUSE2", "MOUSE3", "MWHEELDOWN", "MWHEELUP", "N", "O", "P", "PGDN", "PGUP", "Q", "R", "RIGHTARROW", "S", "SEMICOLON", "SHIFT", "SPACE", "T", "TAB", "U", "UPARROW", "V", "W", "X", "Y", "Z", "[", "\\", "]", "`", ]; static INPUT_VALUES: [BindInput; 79] = [ BindInput::Key(Key::Comma), BindInput::Key(Key::Period), BindInput::Key(Key::Slash), BindInput::Key(Key::Key0), BindInput::Key(Key::Key1), BindInput::Key(Key::Key2), BindInput::Key(Key::Key3), BindInput::Key(Key::Key4), BindInput::Key(Key::Key5), BindInput::Key(Key::Key6), BindInput::Key(Key::Key7), BindInput::Key(Key::Key8), BindInput::Key(Key::Key9), BindInput::Key(Key::A), BindInput::Key(Key::LAlt), BindInput::Key(Key::B), BindInput::Key(Key::Back), BindInput::Key(Key::C), BindInput::Key(Key::LControl), BindInput::Key(Key::D), BindInput::Key(Key::Delete), BindInput::Key(Key::Down), BindInput::Key(Key::E), BindInput::Key(Key::End), BindInput::Key(Key::Return), BindInput::Key(Key::Escape), BindInput::Key(Key::F), BindInput::Key(Key::F1), BindInput::Key(Key::F10), BindInput::Key(Key::F11), BindInput::Key(Key::F12), BindInput::Key(Key::F2), BindInput::Key(Key::F3), BindInput::Key(Key::F4), BindInput::Key(Key::F5), BindInput::Key(Key::F6), BindInput::Key(Key::F7), BindInput::Key(Key::F8), BindInput::Key(Key::F9), BindInput::Key(Key::G), BindInput::Key(Key::H), BindInput::Key(Key::Home), BindInput::Key(Key::I), BindInput::Key(Key::Insert), BindInput::Key(Key::J), BindInput::Key(Key::K), BindInput::Key(Key::L), BindInput::Key(Key::Left), BindInput::Key(Key::M), BindInput::MouseButton(MouseButton::Left), BindInput::MouseButton(MouseButton::Right), BindInput::MouseButton(MouseButton::Middle), BindInput::MouseWheel(MouseWheel::Down), BindInput::MouseWheel(MouseWheel::Up), BindInput::Key(Key::N), BindInput::Key(Key::O), BindInput::Key(Key::P), BindInput::Key(Key::PageDown), BindInput::Key(Key::PageUp), BindInput::Key(Key::Q), BindInput::Key(Key::R), BindInput::Key(Key::Right), BindInput::Key(Key::S), BindInput::Key(Key::Semicolon), BindInput::Key(Key::LShift), BindInput::Key(Key::Space), BindInput::Key(Key::T), BindInput::Key(Key::Tab), BindInput::Key(Key::U), BindInput::Key(Key::Up), BindInput::Key(Key::V), BindInput::Key(Key::W), BindInput::Key(Key::X), BindInput::Key(Key::Y), BindInput::Key(Key::Z), BindInput::Key(Key::LBracket), BindInput::Key(Key::Backslash), BindInput::Key(Key::RBracket), BindInput::Key(Key::Grave), ]; /// A unique identifier for an in-game action. #[derive(Clone, Copy, Debug, Eq, PartialEq, EnumIter)] pub enum Action { /// Move forward. Forward = 0, /// Move backward. Back = 1, /// Strafe left. MoveLeft = 2, /// Strafe right. MoveRight = 3, /// Move up (when swimming). MoveUp = 4, /// Move down (when swimming). MoveDown = 5, /// Look up. LookUp = 6, /// Look down. LookDown = 7, /// Look left. Left = 8, /// Look right. Right = 9, /// Change move speed (walk/run). Speed = 10, /// Jump. Jump = 11, /// Interpret `Left`/`Right` like `MoveLeft`/`MoveRight`. Strafe = 12, /// Attack with the current weapon. Attack = 13, /// Interact with an object (not used). Use = 14, /// Interpret `Forward`/`Back` like `LookUp`/`LookDown`. KLook = 15, /// Interpret upward/downward vertical mouse movements like `LookUp`/`LookDown`. MLook = 16, /// If in single-player, show the current level stats. If in multiplayer, show the scoreboard. ShowScores = 17, /// Show the team scoreboard. ShowTeamScores = 18, } impl FromStr for Action { type Err = Error; fn from_str(s: &str) -> Result { let action = match s.to_lowercase().as_str() { "forward" => Action::Forward, "back" => Action::Back, "moveleft" => Action::MoveLeft, "moveright" => Action::MoveRight, "moveup" => Action::MoveUp, "movedown" => Action::MoveDown, "lookup" => Action::LookUp, "lookdown" => Action::LookDown, "left" => Action::Left, "right" => Action::Right, "speed" => Action::Speed, "jump" => Action::Jump, "strafe" => Action::Strafe, "attack" => Action::Attack, "use" => Action::Use, "klook" => Action::KLook, "mlook" => Action::MLook, "showscores" => Action::ShowScores, "showteamscores" => Action::ShowTeamScores, _ => bail!("Invalid action name: {}", s), }; Ok(action) } } impl ToString for Action { fn to_string(&self) -> String { String::from(match *self { Action::Forward => "forward", Action::Back => "back", Action::MoveLeft => "moveleft", Action::MoveRight => "moveright", Action::MoveUp => "moveup", Action::MoveDown => "movedown", Action::LookUp => "lookup", Action::LookDown => "lookdown", Action::Left => "left", Action::Right => "right", Action::Speed => "speed", Action::Jump => "jump", Action::Strafe => "strafe", Action::Attack => "attack", Action::Use => "use", Action::KLook => "klook", Action::MLook => "mlook", Action::ShowScores => "showscores", Action::ShowTeamScores => "showteamscores", }) } } // for game input, we only care about the direction the mouse wheel moved, not how far it went in // one event /// A movement of the mouse wheel up or down. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum MouseWheel { Up, Down, } // TODO: this currently doesn't handle NaN and treats 0.0 as negative which is probably not optimal impl ::std::convert::From for MouseWheel { fn from(src: MouseScrollDelta) -> MouseWheel { match src { MouseScrollDelta::LineDelta(_, y) => { if y > 0.0 { MouseWheel::Up } else { MouseWheel::Down } } MouseScrollDelta::PixelDelta(LogicalPosition { y, .. }) => { if y > 0.0 { MouseWheel::Up } else { MouseWheel::Down } } } } } /// A physical input that can be bound to a command. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum BindInput { /// A key pressed on the keyboard. Key(Key), /// A button pressed on the mouse. MouseButton(MouseButton), /// A direction scrolled on the mouse wheel. MouseWheel(MouseWheel), } impl ::std::convert::From for BindInput { fn from(src: Key) -> BindInput { BindInput::Key(src) } } impl ::std::convert::From for BindInput { fn from(src: MouseButton) -> BindInput { BindInput::MouseButton(src) } } impl ::std::convert::From for BindInput { fn from(src: MouseWheel) -> BindInput { BindInput::MouseWheel(src) } } impl ::std::convert::From for BindInput { fn from(src: MouseScrollDelta) -> BindInput { BindInput::MouseWheel(MouseWheel::from(src)) } } impl FromStr for BindInput { type Err = Error; fn from_str(src: &str) -> Result { let upper = src.to_uppercase(); for (i, name) in INPUT_NAMES.iter().enumerate() { if upper == *name { return Ok(INPUT_VALUES[i].clone()); } } bail!("\"{}\" isn't a valid key", src); } } impl ToString for BindInput { fn to_string(&self) -> String { // this could be a binary search but it's unlikely to affect performance much for (i, input) in INPUT_VALUES.iter().enumerate() { if self == input { return INPUT_NAMES[i].to_owned(); } } String::new() } } /// An operation to perform when a `BindInput` is received. #[derive(Clone, Debug)] pub enum BindTarget { /// An action to set/unset. Action { // + is true, - is false // so "+forward" maps to trigger: true, action: Action::Forward trigger: ElementState, action: Action, }, /// Text to push to the console execution buffer. ConsoleInput { text: String }, } impl FromStr for BindTarget { type Err = Error; fn from_str(s: &str) -> Result { match parse::action(s) { // first, check if this is an action Ok((_, (trigger, action_str))) => { let action = match Action::from_str(&action_str) { Ok(a) => a, _ => return Ok(BindTarget::ConsoleInput { text: s.to_owned() }), }; Ok(BindTarget::Action { trigger, action }) } // if the parse fails, assume it's a cvar/cmd and return the text _ => Ok(BindTarget::ConsoleInput { text: s.to_owned() }), } } } impl ToString for BindTarget { fn to_string(&self) -> String { match *self { BindTarget::Action { trigger, action } => { String::new() + match trigger { ElementState::Pressed => "+", ElementState::Released => "-", } + &action.to_string() } BindTarget::ConsoleInput { ref text } => format!("\"{}\"", text.to_owned()), } } } #[derive(Clone)] pub struct GameInput { console: Rc>, bindings: Rc>>, action_states: Rc>, mouse_delta: (f64, f64), impulse: Rc>, } impl GameInput { pub fn new(console: Rc>) -> GameInput { GameInput { console, bindings: Rc::new(RefCell::new(HashMap::new())), action_states: Rc::new(RefCell::new([false; ACTION_COUNT])), mouse_delta: (0.0, 0.0), impulse: Rc::new(Cell::new(0)), } } pub fn mouse_delta(&self) -> (f64, f64) { self.mouse_delta } pub fn impulse(&self) -> u8 { self.impulse.get() } /// Bind the default controls. pub fn bind_defaults(&mut self) { self.bind(Key::W, BindTarget::from_str("+forward").unwrap()); self.bind(Key::A, BindTarget::from_str("+moveleft").unwrap()); self.bind(Key::S, BindTarget::from_str("+back").unwrap()); self.bind(Key::D, BindTarget::from_str("+moveright").unwrap()); self.bind(Key::Space, BindTarget::from_str("+jump").unwrap()); self.bind(Key::Up, BindTarget::from_str("+lookup").unwrap()); self.bind(Key::Left, BindTarget::from_str("+left").unwrap()); self.bind(Key::Down, BindTarget::from_str("+lookdown").unwrap()); self.bind(Key::Right, BindTarget::from_str("+right").unwrap()); self.bind(Key::LControl, BindTarget::from_str("+attack").unwrap()); self.bind(Key::E, BindTarget::from_str("+use").unwrap()); self.bind(Key::Grave, BindTarget::from_str("toggleconsole").unwrap()); self.bind(Key::Key1, BindTarget::from_str("impulse 1").unwrap()); self.bind(Key::Key2, BindTarget::from_str("impulse 2").unwrap()); self.bind(Key::Key3, BindTarget::from_str("impulse 3").unwrap()); self.bind(Key::Key4, BindTarget::from_str("impulse 4").unwrap()); self.bind(Key::Key5, BindTarget::from_str("impulse 5").unwrap()); self.bind(Key::Key6, BindTarget::from_str("impulse 6").unwrap()); self.bind(Key::Key7, BindTarget::from_str("impulse 7").unwrap()); self.bind(Key::Key8, BindTarget::from_str("impulse 8").unwrap()); self.bind(Key::Key9, BindTarget::from_str("impulse 9").unwrap()); } /// Bind a `BindInput` to a `BindTarget`. pub fn bind(&mut self, input: I, target: T) -> Option where I: Into, T: Into, { self.bindings .borrow_mut() .insert(input.into(), target.into()) } /// Return the `BindTarget` that `input` is bound to, or `None` if `input` is not present. pub fn binding(&self, input: I) -> Option where I: Into, { self.bindings.borrow().get(&input.into()).map(|t| t.clone()) } pub fn handle_event(&mut self, outer_event: Event) { let (input, state): (BindInput, _) = match outer_event { Event::WindowEvent { event, .. } => match event { WindowEvent::KeyboardInput { input: KeyboardInput { state, virtual_keycode: Some(key), .. }, .. } => (key.into(), state), WindowEvent::MouseInput { state, button, .. } => (button.into(), state), WindowEvent::MouseWheel { delta, .. } => (delta.into(), ElementState::Pressed), _ => return, }, Event::DeviceEvent { event, .. } => match event { DeviceEvent::MouseMotion { delta } => { self.mouse_delta.0 += delta.0; self.mouse_delta.1 += delta.1; return; } _ => return, }, _ => return, }; self.handle_input(input, state); } pub fn handle_input(&mut self, input: I, state: ElementState) where I: Into, { let bind_input = input.into(); // debug!("handle input {:?}: {:?}", &bind_input, state); if let Some(target) = self.bindings.borrow().get(&bind_input) { match *target { BindTarget::Action { trigger, action } => { self.action_states.borrow_mut()[action as usize] = state == trigger; debug!( "{}{}", if state == trigger { '+' } else { '-' }, action.to_string() ); } BindTarget::ConsoleInput { ref text } => { if state == ElementState::Pressed { self.console.borrow_mut().stuff_text(text); } } } } } pub fn action_state(&self, action: Action) -> bool { self.action_states.borrow()[action as usize] } // TODO: roll actions into a loop pub fn register_cmds(&self, cmds: &mut CmdRegistry) { let states = [("+", true), ("-", false)]; for action in Action::iter() { for (state_str, state_bool) in states.iter().cloned() { let action_states = self.action_states.clone(); let cmd_name = format!("{}{}", state_str, action.to_string()); cmds.insert_or_replace( &cmd_name, Box::new(move |_| { action_states.borrow_mut()[action as usize] = state_bool; String::new() }), ) .unwrap(); } } // "bind" let bindings = self.bindings.clone(); cmds.insert_or_replace( "bind", Box::new(move |args| { match args.len() { // bind (key) // queries what (key) is bound to, if anything 1 => match BindInput::from_str(args[0]) { Ok(i) => match bindings.borrow().get(&i) { Some(t) => format!("\"{}\" = \"{}\"", i.to_string(), t.to_string()), None => format!("\"{}\" is not bound", i.to_string()), }, Err(_) => format!("\"{}\" isn't a valid key", args[0]), }, // bind (key) [command] 2 => match BindInput::from_str(args[0]) { Ok(input) => match BindTarget::from_str(args[1]) { Ok(target) => { bindings.borrow_mut().insert(input, target); debug!("Bound {:?} to {:?}", input, args[1]); String::new() } Err(_) => { format!("\"{}\" isn't a valid bind target", args[1]) } }, Err(_) => format!("\"{}\" isn't a valid key", args[0]), }, _ => "bind [key] (command): attach a command to a key".to_owned(), } }), ) .unwrap(); // "unbindall" let bindings = self.bindings.clone(); cmds.insert_or_replace( "unbindall", Box::new(move |args| match args.len() { 0 => { let _ = bindings.replace(HashMap::new()); String::new() } _ => "unbindall: delete all keybindings".to_owned(), }), ) .unwrap(); // "impulse" let impulse = self.impulse.clone(); cmds.insert_or_replace( "impulse", Box::new(move |args| { println!("args: {}", args.len()); match args.len() { 1 => match u8::from_str(args[0]) { Ok(i) => { impulse.set(i); String::new() } Err(_) => "Impulse must be a number between 0 and 255".to_owned(), }, _ => "usage: impulse [number]".to_owned(), } }), ) .unwrap(); } // must be called every frame! pub fn refresh(&mut self) { self.clear_mouse(); self.clear_impulse(); } fn clear_mouse(&mut self) { self.handle_input(MouseWheel::Up, ElementState::Released); self.handle_input(MouseWheel::Down, ElementState::Released); self.mouse_delta = (0.0, 0.0); } fn clear_impulse(&mut self) { self.impulse.set(0); } } #[cfg(test)] mod test { use super::*; #[test] fn test_action_to_string() { let act = Action::Forward; assert_eq!(act.to_string(), "forward"); } #[test] fn test_bind_target_action_to_string() { let target = BindTarget::Action { trigger: ElementState::Pressed, action: Action::Forward, }; assert_eq!(target.to_string(), "+forward"); } } ================================================ FILE: src/client/input/menu.rs ================================================ // Copyright © 2019 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{cell::RefCell, rc::Rc}; use crate::{client::menu::Menu, common::console::Console}; use failure::Error; use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode as Key, WindowEvent}; pub struct MenuInput { menu: Rc>, console: Rc>, } impl MenuInput { pub fn new(menu: Rc>, console: Rc>) -> MenuInput { MenuInput { menu, console } } pub fn handle_event(&self, event: Event) -> Result<(), Error> { match event { Event::WindowEvent { event, .. } => match event { WindowEvent::ReceivedCharacter(_) => (), WindowEvent::KeyboardInput { input: KeyboardInput { virtual_keycode: Some(key), state: ElementState::Pressed, .. }, .. } => match key { Key::Escape => { if self.menu.borrow().at_root() { self.console.borrow().stuff_text("togglemenu\n"); } else { self.menu.borrow().back()?; } } Key::Up => self.menu.borrow().prev()?, Key::Down => self.menu.borrow().next()?, Key::Return => self.menu.borrow().activate()?, Key::Left => self.menu.borrow().left()?, Key::Right => self.menu.borrow().right()?, _ => (), }, _ => (), }, _ => (), } Ok(()) } } ================================================ FILE: src/client/input/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pub mod console; pub mod game; pub mod menu; use std::{cell::RefCell, rc::Rc}; use crate::{ client::menu::Menu, common::console::{CmdRegistry, Console}, }; use failure::Error; use winit::event::{Event, WindowEvent}; use self::{ console::ConsoleInput, game::{BindInput, BindTarget, GameInput}, menu::MenuInput, }; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum InputFocus { Game, Console, Menu, } pub struct Input { window_focused: bool, focus: InputFocus, game_input: GameInput, console_input: ConsoleInput, menu_input: MenuInput, } impl Input { pub fn new( init_focus: InputFocus, console: Rc>, menu: Rc>, ) -> Input { Input { window_focused: true, focus: init_focus, game_input: GameInput::new(console.clone()), console_input: ConsoleInput::new(console.clone()), menu_input: MenuInput::new(menu.clone(), console.clone()), } } pub fn handle_event(&mut self, event: Event) -> Result<(), Error> { match event { // we're polling for hardware events, so we have to check window focus ourselves Event::WindowEvent { event: WindowEvent::Focused(focused), .. } => self.window_focused = focused, _ => { if self.window_focused { match self.focus { InputFocus::Game => self.game_input.handle_event(event), InputFocus::Console => self.console_input.handle_event(event)?, InputFocus::Menu => self.menu_input.handle_event(event)?, } } } } Ok(()) } pub fn focus(&self) -> InputFocus { self.focus } pub fn set_focus(&mut self, new_focus: InputFocus) { self.focus = new_focus; } /// Bind a `BindInput` to a `BindTarget`. pub fn bind(&mut self, input: I, target: T) -> Option where I: Into, T: Into, { self.game_input.bind(input, target) } pub fn bind_defaults(&mut self) { self.game_input.bind_defaults(); } pub fn game_input(&self) -> Option<&GameInput> { if let InputFocus::Game = self.focus { Some(&self.game_input) } else { None } } pub fn game_input_mut(&mut self) -> Option<&mut GameInput> { if let InputFocus::Game = self.focus { Some(&mut self.game_input) } else { None } } pub fn register_cmds(&self, cmds: &mut CmdRegistry) { self.game_input.register_cmds(cmds); } } ================================================ FILE: src/client/menu/item.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::cell::{Cell, RefCell}; use crate::client::menu::Menu; use failure::Error; pub enum Item { Submenu(Menu), Action(Box), Toggle(Toggle), Enum(Enum), Slider(Slider), TextField(TextField), } pub struct Toggle { state: Cell, on_toggle: Box, } impl Toggle { pub fn new(init: bool, on_toggle: Box) -> Toggle { let t = Toggle { state: Cell::new(init), on_toggle, }; // initialize with default (t.on_toggle)(init); t } pub fn set_false(&self) { self.state.set(false); (self.on_toggle)(self.state.get()); } pub fn set_true(&self) { self.state.set(true); (self.on_toggle)(self.state.get()); } pub fn toggle(&self) { self.state.set(!self.state.get()); (self.on_toggle)(self.state.get()); } pub fn get(&self) -> bool { self.state.get() } } // TODO: add wrapping configuration to enums // e.g. resolution enum wraps, texture filtering does not pub struct Enum { selected: Cell, items: Vec, } impl Enum { pub fn new(init: usize, items: Vec) -> Result { ensure!(items.len() > 0, "Enum element must have at least one item"); ensure!(init < items.len(), "Invalid initial item ID"); let e = Enum { selected: Cell::new(init), items, }; // initialize with the default choice (e.items[e.selected.get()].on_select)(); Ok(e) } pub fn selected_name(&self) -> &str { self.items[self.selected.get()].name.as_str() } pub fn select_next(&self) { let selected = match self.selected.get() + 1 { s if s >= self.items.len() => 0, s => s, }; self.selected.set(selected); (self.items[selected].on_select)(); } pub fn select_prev(&self) { let selected = match self.selected.get() { 0 => self.items.len() - 1, s => s - 1, }; self.selected.set(selected); (self.items[selected].on_select)(); } } pub struct EnumItem { name: String, on_select: Box, } impl EnumItem { pub fn new(name: S, on_select: Box) -> Result where S: AsRef, { Ok(EnumItem { name: name.as_ref().to_string(), on_select, }) } } pub struct Slider { min: f32, _max: f32, increment: f32, steps: usize, selected: Cell, on_select: Box, } impl Slider { pub fn new( min: f32, max: f32, steps: usize, init: usize, on_select: Box, ) -> Result { ensure!(steps > 1, "Slider must have at least 2 steps"); ensure!(init < steps, "Invalid initial setting"); ensure!( min < max, "Minimum setting must be less than maximum setting" ); Ok(Slider { min, _max: max, increment: (max - min) / (steps - 1) as f32, steps, selected: Cell::new(init), on_select, }) } pub fn increase(&self) { let old = self.selected.get(); if old != self.steps - 1 { self.selected.set(old + 1); } (self.on_select)(self.min + self.selected.get() as f32 * self.increment); } pub fn decrease(&self) { let old = self.selected.get(); if old != 0 { self.selected.set(old - 1); } (self.on_select)(self.min + self.selected.get() as f32 * self.increment); } pub fn position(&self) -> f32 { self.selected.get() as f32 / self.steps as f32 } } pub struct TextField { chars: RefCell>, max_len: Option, on_update: Box, cursor: Cell, } impl TextField { pub fn new( default: Option, max_len: Option, on_update: Box, ) -> Result where S: AsRef, { let chars = RefCell::new(match default { Some(d) => d.as_ref().chars().collect(), None => Vec::new(), }); let cursor = Cell::new(chars.borrow().len()); Ok(TextField { chars, max_len, on_update, cursor, }) } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn text(&self) -> String { self.chars.borrow().iter().collect() } pub fn len(&self) -> usize { self.chars.borrow().len() } pub fn set_cursor(&self, cursor: usize) -> Result<(), Error> { ensure!(cursor <= self.len(), "Index out of range"); self.cursor.set(cursor); Ok(()) } pub fn home(&self) { self.cursor.set(0); } pub fn end(&self) { self.cursor.set(self.len()); } pub fn cursor_right(&self) { let curs = self.cursor.get(); if curs < self.len() { self.cursor.set(curs + 1); } } pub fn cursor_left(&self) { let curs = self.cursor.get(); if curs > 1 { self.cursor.set(curs - 1); } } pub fn insert(&self, c: char) { if let Some(l) = self.max_len { if self.len() == l { return; } } self.chars.borrow_mut().insert(self.cursor.get(), c); (self.on_update)(&self.text()); } pub fn backspace(&self) { if self.cursor.get() > 1 { self.chars.borrow_mut().remove(self.cursor.get() - 1); (self.on_update)(&self.text()); } } pub fn delete(&self) { if self.cursor.get() < self.len() { self.chars.borrow_mut().remove(self.cursor.get()); (self.on_update)(&self.text()); } } } #[cfg(test)] mod test { use super::*; use std::{cell::RefCell, rc::Rc}; #[test] fn test_toggle() { let s = Rc::new(RefCell::new("false".to_string())); let s2 = s.clone(); let item = Toggle::new( false, Box::new(move |state| { s2.replace(format!("{}", state)); }), ); item.toggle(); assert_eq!(*s.borrow(), "true"); } #[test] fn test_enum() { let target = Rc::new(RefCell::new("null".to_string())); let enum_items = (0..3i32) .into_iter() .map(|i: i32| { let target_handle = target.clone(); EnumItem::new( format!("option_{}", i), Box::new(move || { target_handle.replace(format!("option_{}", i)); }), ) .unwrap() }) .collect(); let e = Enum::new(0, enum_items).unwrap(); assert_eq!(*target.borrow(), "option_0"); // wrap under e.select_prev(); assert_eq!(*target.borrow(), "option_2"); e.select_next(); e.select_next(); e.select_next(); assert_eq!(*target.borrow(), "option_2"); // wrap over e.select_next(); assert_eq!(*target.borrow(), "option_0"); } #[test] fn test_slider() { let f = Rc::new(Cell::new(0.0f32)); let f2 = f.clone(); let item = Slider::new( 0.0, 10.0, 11, 0, Box::new(move |f| { f2.set(f); }), ) .unwrap(); // don't underflow item.decrease(); assert_eq!(f.get(), 0.0); for i in 0..10 { item.increase(); assert_eq!(f.get(), i as f32 + 1.0); } // don't overflow item.increase(); assert_eq!(f.get(), 10.0); } #[test] fn test_textfield() { let MAX_LEN = 10; let s = Rc::new(RefCell::new("before".to_owned())); let s2 = s.clone(); let mut tf = TextField::new( Some("default"), Some(MAX_LEN), Box::new(move |x| { s2.replace(x.to_string()); }), ) .unwrap(); tf.cursor_left(); tf.backspace(); tf.backspace(); tf.home(); tf.delete(); tf.delete(); tf.delete(); tf.cursor_right(); tf.insert('f'); tf.end(); tf.insert('e'); tf.insert('r'); assert_eq!(tf.text(), *s.borrow()); for _ in 0..2 * MAX_LEN { tf.insert('x'); } assert_eq!(tf.len(), MAX_LEN); } } ================================================ FILE: src/client/menu/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. mod item; use std::cell::Cell; use failure::Error; pub use self::item::{Enum, EnumItem, Item, Slider, TextField, Toggle}; #[derive(Clone, Copy, Debug)] pub enum MenuState { /// Menu is inactive. Inactive, /// Menu is active. `index` indicates the currently selected element. Active { index: usize }, /// A submenu of this menu is active. `index` indicates the active submenu. InSubMenu { index: usize }, } /// Specifies how the menu body should be rendered. pub enum MenuBodyView { /// The menu body is rendered using a predefined bitmap. Predefined { /// The path to the bitmap. path: String, }, /// The menu body is rendered dynamically based on its contents. Dynamic, } pub struct MenuView { pub draw_plaque: bool, pub title_path: String, pub body: MenuBodyView, } impl MenuView { /// Returns true if the Quake plaque should be drawn to the left of the menu. pub fn draw_plaque(&self) -> bool { self.draw_plaque } /// Returns the path to the menu title bitmap. pub fn title_path(&self) -> &str { &self.title_path } /// Returns a MenuBodyView which specifies how to render the menu body. pub fn body(&self) -> &MenuBodyView { &self.body } } pub struct Menu { items: Vec, state: Cell, view: MenuView, } impl Menu { /// Returns a reference to the active submenu of this menu and its parent. fn active_submenu_and_parent(&self) -> Result<(&Menu, Option<&Menu>), Error> { let mut m = self; let mut m_parent = None; while let MenuState::InSubMenu { index } = m.state.get() { match m.items[index].item { Item::Submenu(ref s) => { m_parent = Some(m); m = s; } _ => bail!("Menu state points to invalid submenu"), } } Ok((m, m_parent)) } /// Return a reference to the active submenu of this menu pub fn active_submenu(&self) -> Result<&Menu, Error> { let (m, _) = self.active_submenu_and_parent()?; Ok(m) } /// Return a reference to the parent of the active submenu of this menu. /// /// If this is the root menu, returns None. fn active_submenu_parent(&self) -> Result, Error> { let (_, m_parent) = self.active_submenu_and_parent()?; Ok(m_parent) } /// Select the next element of this Menu. pub fn next(&self) -> Result<(), Error> { let m = self.active_submenu()?; let s = m.state.get().clone(); if let MenuState::Active { index } = s { m.state.replace(MenuState::Active { index: (index + 1) % m.items.len(), }); } else { bail!("Selected menu is inactive (invariant violation)"); } Ok(()) } /// Select the previous element of this Menu. pub fn prev(&self) -> Result<(), Error> { let m = self.active_submenu()?; let s = m.state.get().clone(); if let MenuState::Active { index } = s { m.state.replace(MenuState::Active { index: (index - 1) % m.items.len(), }); } else { bail!("Selected menu is inactive (invariant violation)"); } Ok(()) } /// Return a reference to the currently selected menu item. pub fn selected(&self) -> Result<&Item, Error> { let m = self.active_submenu()?; if let MenuState::Active { index } = m.state.get() { return Ok(&m.items[index].item); } else { bail!("Active menu in invalid state (invariant violation)") } } /// Activate the currently selected menu item. /// /// If this item is a `Menu`, sets the active (sub)menu's state to /// `MenuState::InSubMenu` and the selected submenu's state to /// `MenuState::Active`. /// /// If this item is an `Action`, executes the function contained in the /// `Action`. /// /// Otherwise, this has no effect. pub fn activate(&self) -> Result<(), Error> { let m = self.active_submenu()?; if let MenuState::Active { index } = m.state.get() { match m.items[index].item { Item::Submenu(ref submenu) => { m.state.replace(MenuState::InSubMenu { index }); submenu.state.replace(MenuState::Active { index: 0 }); } Item::Action(ref action) => (action)(), _ => (), } } Ok(()) } pub fn left(&self) -> Result<(), Error> { let m = self.active_submenu()?; if let MenuState::Active { index } = m.state.get() { match m.items[index].item { Item::Enum(ref e) => e.select_prev(), Item::Slider(ref slider) => slider.decrease(), Item::TextField(ref text) => text.cursor_left(), Item::Toggle(ref toggle) => toggle.set_false(), _ => (), } } Ok(()) } pub fn right(&self) -> Result<(), Error> { let m = self.active_submenu()?; if let MenuState::Active { index } = m.state.get() { match m.items[index].item { Item::Enum(ref e) => e.select_next(), Item::Slider(ref slider) => slider.increase(), Item::TextField(ref text) => text.cursor_right(), Item::Toggle(ref toggle) => toggle.set_true(), _ => (), } } Ok(()) } /// Return `true` if the root menu is active, `false` otherwise. pub fn at_root(&self) -> bool { match self.state.get() { MenuState::Active { .. } => true, _ => false, } } /// Deactivate the active menu and activate its parent pub fn back(&self) -> Result<(), Error> { if self.at_root() { bail!("Cannot back out of root menu!"); } let (m, m_parent) = self.active_submenu_and_parent()?; m.state.replace(MenuState::Inactive); match m_parent { Some(mp) => { let s = mp.state.get().clone(); match s { MenuState::InSubMenu { index } => mp.state.replace(MenuState::Active { index }), _ => unreachable!(), }; } None => unreachable!(), } Ok(()) } pub fn items(&self) -> &[NamedMenuItem] { &self.items } pub fn state(&self) -> MenuState { self.state.get() } pub fn view(&self) -> &MenuView { &self.view } } pub struct MenuBuilder { gfx_name: Option, items: Vec, } impl MenuBuilder { pub fn new() -> MenuBuilder { MenuBuilder { gfx_name: None, items: Vec::new(), } } pub fn build(self, view: MenuView) -> Menu { // deactivate all child menus for item in self.items.iter() { if let Item::Submenu(ref m) = item.item { m.state.replace(MenuState::Inactive); } } Menu { items: self.items, state: Cell::new(MenuState::Active { index: 0 }), view, } } pub fn add_submenu(mut self, name: S, submenu: Menu) -> MenuBuilder where S: AsRef, { self.items .push(NamedMenuItem::new(name, Item::Submenu(submenu))); self } pub fn add_action(mut self, name: S, action: Box) -> MenuBuilder where S: AsRef, { self.items .push(NamedMenuItem::new(name, Item::Action(action))); self } pub fn add_toggle(mut self, name: S, init: bool, on_toggle: Box) -> MenuBuilder where S: AsRef, { self.items.push(NamedMenuItem::new( name, Item::Toggle(Toggle::new(init, on_toggle)), )); self } pub fn add_enum(mut self, name: S, items: E, init: usize) -> Result where S: AsRef, E: Into>, { self.items.push(NamedMenuItem::new( name, Item::Enum(Enum::new(init, items.into())?), )); Ok(self) } pub fn add_slider( mut self, name: S, min: f32, max: f32, steps: usize, init: usize, on_select: Box, ) -> Result where S: AsRef, { self.items.push(NamedMenuItem::new( name, Item::Slider(Slider::new(min, max, steps, init, on_select)?), )); Ok(self) } pub fn add_text_field( mut self, name: S, default: Option, max_len: Option, on_update: Box, ) -> Result where S: AsRef, { self.items.push(NamedMenuItem::new( name, Item::TextField(TextField::new(default, max_len, on_update)?), )); Ok(self) } } pub struct NamedMenuItem { name: String, item: Item, } impl NamedMenuItem { fn new(name: S, item: Item) -> NamedMenuItem where S: AsRef, { NamedMenuItem { name: name.as_ref().to_string(), item, } } pub fn name(&self) -> &str { &self.name } pub fn item(&self) -> &Item { &self.item } } #[cfg(test)] mod test { use super::*; use std::{cell::Cell, rc::Rc}; fn view() -> MenuView { MenuView { draw_plaque: false, title_path: "path".to_string(), body: MenuBodyView::Dynamic, } } fn is_inactive(state: &MenuState) -> bool { match state { MenuState::Inactive => true, _ => false, } } fn is_active(state: &MenuState) -> bool { match state { MenuState::Active { .. } => true, _ => false, } } fn is_insubmenu(state: &MenuState) -> bool { match state { MenuState::InSubMenu { .. } => true, _ => false, } } #[test] fn test_menu_builder() { let action_target = Rc::new(Cell::new(false)); let action_target_handle = action_target.clone(); let _m = MenuBuilder::new() .add_action("action", Box::new(move || action_target_handle.set(true))) .build(view()); // TODO } #[test] fn test_menu_active_submenu() { let menu = MenuBuilder::new() .add_submenu( "menu_1", MenuBuilder::new() .add_action("action_1", Box::new(|| ())) .build(view()), ) .add_submenu( "menu_2", MenuBuilder::new() .add_action("action_2", Box::new(|| ())) .build(view()), ) .build(view()); let m = &menu; let m1 = match m.items[0].item { Item::Submenu(ref m1i) => m1i, _ => unreachable!(), }; let m2 = match m.items[1].item { Item::Submenu(ref m2i) => m2i, _ => unreachable!(), }; assert!(is_active(&m.state.get())); assert!(is_inactive(&m1.state.get())); assert!(is_inactive(&m2.state.get())); // enter m1 m.activate().unwrap(); assert!(is_insubmenu(&m.state.get())); assert!(is_active(&m1.state.get())); assert!(is_inactive(&m2.state.get())); // exit m1 m.back().unwrap(); assert!(is_active(&m.state.get())); assert!(is_inactive(&m1.state.get())); assert!(is_inactive(&m2.state.get())); // enter m2 m.next().unwrap(); m.activate().unwrap(); assert!(is_insubmenu(&m.state.get())); assert!(is_inactive(&m1.state.get())); assert!(is_active(&m2.state.get())); } } ================================================ FILE: src/client/mod.rs ================================================ // Copyright © 2020 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. mod cvars; pub mod demo; pub mod entity; pub mod input; pub mod menu; pub mod render; pub mod sound; pub mod state; pub mod trace; pub mod view; pub use self::cvars::register_cvars; use std::{ cell::RefCell, collections::{HashMap, VecDeque}, io::BufReader, net::ToSocketAddrs, rc::Rc, }; use crate::{ client::{ demo::{DemoServer, DemoServerError}, entity::{ClientEntity, MAX_STATIC_ENTITIES}, input::{game::GameInput, Input}, sound::{MusicPlayer, StaticSound}, state::{ClientState, PlayerInfo}, trace::{TraceEntity, TraceFrame}, view::{IdleVars, KickVars, MouseVars, RollVars}, }, common::{ console::{CmdRegistry, Console, ConsoleError, CvarRegistry}, engine, model::ModelError, net::{ self, connect::{ConnectSocket, Request, Response, CONNECT_PROTOCOL_VERSION}, BlockingMode, ClientCmd, ClientStat, ColorShift, EntityEffects, EntityState, GameType, NetError, PlayerColor, QSocket, ServerCmd, SignOnStage, }, vfs::{Vfs, VfsError}, }, }; use cgmath::Deg; use chrono::Duration; use input::InputFocus; use menu::Menu; use render::{ClientRenderer, GraphicsState, WorldRenderer}; use rodio::{OutputStream, OutputStreamHandle}; use sound::SoundError; use thiserror::Error; use view::BobVars; // connections are tried 3 times, see // https://github.com/id-Software/Quake/blob/master/WinQuake/net_dgrm.c#L1248 const MAX_CONNECT_ATTEMPTS: usize = 3; const MAX_STATS: usize = 32; const DEFAULT_SOUND_PACKET_VOLUME: u8 = 255; const DEFAULT_SOUND_PACKET_ATTENUATION: f32 = 1.0; const CONSOLE_DIVIDER: &'static str = "\ \n\n\ \x1D\x1E\x1E\x1E\x1E\x1E\x1E\x1E\ \x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\ \x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1E\ \x1E\x1E\x1E\x1E\x1E\x1E\x1E\x1F\ \n\n"; #[derive(Error, Debug)] pub enum ClientError { #[error("Connection rejected: {0}")] ConnectionRejected(String), #[error("Couldn't read cvar value: {0}")] Cvar(ConsoleError), #[error("Server sent an invalid port number ({0})")] InvalidConnectPort(i32), #[error("Server sent an invalid connect response")] InvalidConnectResponse, #[error("Invalid server address")] InvalidServerAddress, #[error("No response from server")] NoResponse, #[error("Unrecognized protocol: {0}")] UnrecognizedProtocol(i32), #[error("Client is not connected")] NotConnected, #[error("Client has already signed on")] AlreadySignedOn, #[error("No client with ID {0}")] NoSuchClient(usize), #[error("No player with ID {0}")] NoSuchPlayer(usize), #[error("No entity with ID {0}")] NoSuchEntity(usize), #[error("Null entity access")] NullEntity, #[error("Entity already exists: {0}")] EntityExists(usize), #[error("Invalid view entity: {0}")] InvalidViewEntity(usize), #[error("Too many static entities")] TooManyStaticEntities, #[error("No such lightmap animation: {0}")] NoSuchLightmapAnimation(usize), // TODO: wrap PlayError #[error("Failed to open audio output stream")] OutputStream, #[error("Demo server error: {0}")] DemoServer(#[from] DemoServerError), #[error("Model error: {0}")] Model(#[from] ModelError), #[error("Network error: {0}")] Network(#[from] NetError), #[error("Failed to load sound: {0}")] Sound(#[from] SoundError), #[error("Virtual filesystem error: {0}")] Vfs(#[from] VfsError), } pub struct MoveVars { cl_anglespeedkey: f32, cl_pitchspeed: f32, cl_yawspeed: f32, cl_sidespeed: f32, cl_upspeed: f32, cl_forwardspeed: f32, cl_backspeed: f32, cl_movespeedkey: f32, } #[derive(Debug, FromPrimitive)] enum ColorShiftCode { Contents = 0, Damage = 1, Bonus = 2, Powerup = 3, } struct ServerInfo { _max_clients: u8, _game_type: GameType, } #[derive(Clone, Debug)] pub enum IntermissionKind { Intermission, Finale { text: String }, Cutscene { text: String }, } /// Indicates to the client what should be done with the current connection. #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum ConnectionStatus { /// Maintain the connection. Maintain, /// Disconnect from the server or demo server. Disconnect, /// Play the next demo in the demo queue. NextDemo, } /// Indicates the state of an active connection. enum ConnectionState { /// The client is in the sign-on process. SignOn(SignOnStage), /// The client is fully connected. Connected(WorldRenderer), } /// Possible targets that a client can be connected to. enum ConnectionKind { /// A regular Quake server. Server { /// The [`QSocket`](crate::net::QSocket) used to communicate with the server. qsock: QSocket, /// The client's packet composition buffer. compose: Vec, }, /// A demo server. Demo(DemoServer), } /// A connection to a game server of some kind. /// /// The exact nature of the connected server is specified by [`ConnectionKind`]. pub struct Connection { state: ClientState, conn_state: ConnectionState, kind: ConnectionKind, } impl Connection { fn handle_signon( &mut self, new_stage: SignOnStage, gfx_state: &GraphicsState, ) -> Result<(), ClientError> { use SignOnStage::*; let new_conn_state = match self.conn_state { // TODO: validate stage transition ConnectionState::SignOn(ref mut _stage) => { if let ConnectionKind::Server { ref mut compose, .. } = self.kind { match new_stage { Not => (), // TODO this is an error (invalid value) Prespawn => { ClientCmd::StringCmd { cmd: String::from("prespawn"), } .serialize(compose)?; } ClientInfo => { // TODO: fill in client info here ClientCmd::StringCmd { cmd: format!("name \"{}\"\n", "UNNAMED"), } .serialize(compose)?; ClientCmd::StringCmd { cmd: format!("color {} {}", 0, 0), } .serialize(compose)?; // TODO: need default spawn parameters? ClientCmd::StringCmd { cmd: format!("spawn {}", ""), } .serialize(compose)?; } SignOnStage::Begin => { ClientCmd::StringCmd { cmd: String::from("begin"), } .serialize(compose)?; } SignOnStage::Done => { debug!("SignOn complete"); // TODO: end load screen self.state.start_time = self.state.time; } } } match new_stage { // TODO proper error Not => panic!("SignOnStage::Not in handle_signon"), // still signing on, advance to the new stage Prespawn | ClientInfo | Begin => ConnectionState::SignOn(new_stage), // finished signing on, build world renderer Done => ConnectionState::Connected(WorldRenderer::new( gfx_state, self.state.models(), 1, )), } } // ignore spurious sign-on messages ConnectionState::Connected { .. } => return Ok(()), }; self.conn_state = new_conn_state; Ok(()) } fn parse_server_msg( &mut self, vfs: &Vfs, gfx_state: &GraphicsState, cmds: &mut CmdRegistry, console: &mut Console, music_player: &mut MusicPlayer, kick_vars: KickVars, ) -> Result { use ConnectionStatus::*; let (msg, demo_view_angles, track_override) = match self.kind { ConnectionKind::Server { ref mut qsock, .. } => { let msg = qsock.recv_msg(match self.conn_state { // if we're in the game, don't block waiting for messages ConnectionState::Connected(_) => BlockingMode::NonBlocking, // otherwise, give the server some time to respond // TODO: might make sense to make this a future or something ConnectionState::SignOn(_) => BlockingMode::Timeout(Duration::seconds(5)), })?; (msg, None, None) } ConnectionKind::Demo(ref mut demo_srv) => { // only get the next update once we've made it all the way to // the previous one if self.state.time >= self.state.msg_times[0] { let msg_view = match demo_srv.next() { Some(v) => v, None => { // if there are no commands left in the demo, play // the next demo if there is one return Ok(NextDemo); } }; let mut view_angles = msg_view.view_angles(); // invert entity angles to get the camera direction right. // yaw is already inverted. view_angles.z = -view_angles.z; // TODO: we shouldn't have to copy the message here ( msg_view.message().to_owned(), Some(view_angles), demo_srv.track_override(), ) } else { (Vec::new(), None, demo_srv.track_override()) } } }; // no data available at this time if msg.is_empty() { return Ok(Maintain); } let mut reader = BufReader::new(msg.as_slice()); while let Some(cmd) = ServerCmd::deserialize(&mut reader)? { match cmd { // TODO: have an error for this instead of panicking // once all other commands have placeholder handlers, just error // in the wildcard branch ServerCmd::Bad => panic!("Invalid command from server"), ServerCmd::NoOp => (), ServerCmd::CdTrack { track, .. } => { music_player.play_track(match track_override { Some(t) => t as usize, None => track as usize, })?; } ServerCmd::CenterPrint { text } => { // TODO: print to center of screen warn!("Center print not yet implemented!"); println!("{}", text); } ServerCmd::PlayerData(player_data) => self.state.update_player(player_data), ServerCmd::Cutscene { text } => { self.state.intermission = Some(IntermissionKind::Cutscene { text }); self.state.completion_time = Some(self.state.time); } ServerCmd::Damage { armor, blood, source, } => self.state.handle_damage(armor, blood, source, kick_vars), ServerCmd::Disconnect => { return Ok(match self.kind { ConnectionKind::Demo(_) => NextDemo, ConnectionKind::Server { .. } => Disconnect, }) } ServerCmd::FastUpdate(ent_update) => { // first update signals the last sign-on stage self.handle_signon(SignOnStage::Done, gfx_state)?; let ent_id = ent_update.ent_id as usize; self.state.update_entity(ent_id, ent_update)?; // patch view angles in demos if let Some(angles) = demo_view_angles { if ent_id == self.state.view_entity_id() { self.state.update_view_angles(angles); } } } ServerCmd::Finale { text } => { self.state.intermission = Some(IntermissionKind::Finale { text }); self.state.completion_time = Some(self.state.time); } ServerCmd::FoundSecret => self.state.stats[ClientStat::FoundSecrets as usize] += 1, ServerCmd::Intermission => { self.state.intermission = Some(IntermissionKind::Intermission); self.state.completion_time = Some(self.state.time); } ServerCmd::KilledMonster => { self.state.stats[ClientStat::KilledMonsters as usize] += 1 } ServerCmd::LightStyle { id, value } => { trace!("Inserting light style {} with value {}", id, &value); let _ = self.state.light_styles.insert(id, value); } ServerCmd::Particle { origin, direction, count, color, } => { match count { // if count is 255, this is an explosion 255 => self .state .particles .create_explosion(self.state.time, origin), // otherwise it's an impact _ => self.state.particles.create_projectile_impact( self.state.time, origin, direction, color, count as usize, ), } } ServerCmd::Print { text } => console.print_alert(&text), ServerCmd::ServerInfo { protocol_version, max_clients, game_type, message, model_precache, sound_precache, } => { // check protocol version if protocol_version != net::PROTOCOL_VERSION as i32 { Err(ClientError::UnrecognizedProtocol(protocol_version))?; } console.println(CONSOLE_DIVIDER); console.println(message); console.println(CONSOLE_DIVIDER); let _server_info = ServerInfo { _max_clients: max_clients, _game_type: game_type, }; self.state = ClientState::from_server_info( vfs, self.state.mixer.stream(), max_clients, model_precache, sound_precache, )?; let bonus_cshift = self.state.color_shifts[ColorShiftCode::Bonus as usize].clone(); cmds.insert_or_replace( "bf", Box::new(move |_| { bonus_cshift.replace(ColorShift { dest_color: [215, 186, 69], percent: 50, }); String::new() }), ) .unwrap(); } ServerCmd::SetAngle { angles } => self.state.set_view_angles(angles), ServerCmd::SetView { ent_id } => { if ent_id <= 0 { Err(ClientError::InvalidViewEntity(ent_id as usize))?; } self.state.set_view_entity(ent_id as usize)?; } ServerCmd::SignOnStage { stage } => self.handle_signon(stage, gfx_state)?, ServerCmd::Sound { volume, attenuation, entity_id, channel, sound_id, position, } => { trace!( "starting sound with id {} on entity {} channel {}", sound_id, entity_id, channel ); if entity_id as usize >= self.state.entities.len() { warn!( "server tried to start sound on nonexistent entity {}", entity_id ); break; } let volume = volume.unwrap_or(DEFAULT_SOUND_PACKET_VOLUME); let attenuation = attenuation.unwrap_or(DEFAULT_SOUND_PACKET_ATTENUATION); // TODO: apply volume, attenuation, spatialization self.state.mixer.start_sound( self.state.sounds[sound_id as usize].clone(), self.state.msg_times[0], Some(entity_id as usize), channel, volume as f32 / 255.0, attenuation, position, &self.state.listener, ); } ServerCmd::SpawnBaseline { ent_id, model_id, frame_id, colormap, skin_id, origin, angles, } => { self.state.spawn_entities( ent_id as usize, EntityState { model_id: model_id as usize, frame_id: frame_id as usize, colormap, skin_id: skin_id as usize, origin, angles, effects: EntityEffects::empty(), }, )?; } ServerCmd::SpawnStatic { model_id, frame_id, colormap, skin_id, origin, angles, } => { if self.state.static_entities.len() >= MAX_STATIC_ENTITIES { Err(ClientError::TooManyStaticEntities)?; } self.state .static_entities .push(ClientEntity::from_baseline(EntityState { origin, angles, model_id: model_id as usize, frame_id: frame_id as usize, colormap, skin_id: skin_id as usize, effects: EntityEffects::empty(), })); } ServerCmd::SpawnStaticSound { origin, sound_id, volume, attenuation, } => { self.state.static_sounds.push(StaticSound::new( &self.state.mixer.stream(), origin, self.state.sounds[sound_id as usize].clone(), volume as f32 / 255.0, attenuation as f32 / 64.0, &self.state.listener, )); } ServerCmd::TempEntity { temp_entity } => self.state.spawn_temp_entity(&temp_entity), ServerCmd::StuffText { text } => console.stuff_text(text), ServerCmd::Time { time } => { self.state.msg_times[1] = self.state.msg_times[0]; self.state.msg_times[0] = engine::duration_from_f32(time); } ServerCmd::UpdateColors { player_id, new_colors, } => { let player_id = player_id as usize; self.state.check_player_id(player_id)?; match self.state.player_info[player_id] { Some(ref mut info) => { trace!( "Player {} (ID {}) colors: {:?} -> {:?}", info.name, player_id, info.colors, new_colors, ); info.colors = new_colors; } None => { error!( "Attempted to set colors on nonexistent player with ID {}", player_id ); } } } ServerCmd::UpdateFrags { player_id, new_frags, } => { let player_id = player_id as usize; self.state.check_player_id(player_id)?; match self.state.player_info[player_id] { Some(ref mut info) => { trace!( "Player {} (ID {}) frags: {} -> {}", &info.name, player_id, info.frags, new_frags ); info.frags = new_frags as i32; } None => { error!( "Attempted to set frags on nonexistent player with ID {}", player_id ); } } } ServerCmd::UpdateName { player_id, new_name, } => { let player_id = player_id as usize; self.state.check_player_id(player_id)?; if let Some(ref mut info) = self.state.player_info[player_id] { // if this player is already connected, it's a name change debug!("Player {} has changed name to {}", &info.name, &new_name); info.name = new_name.to_owned(); } else { // if this player is not connected, it's a join debug!("Player {} with ID {} has joined", &new_name, player_id); self.state.player_info[player_id] = Some(PlayerInfo { name: new_name.to_owned(), colors: PlayerColor::new(0, 0), frags: 0, }); } } ServerCmd::UpdateStat { stat, value } => { debug!( "{:?}: {} -> {}", stat, self.state.stats[stat as usize], value ); self.state.stats[stat as usize] = value; } ServerCmd::Version { version } => { if version != net::PROTOCOL_VERSION as i32 { // TODO: handle with an error error!( "Incompatible server version: server's is {}, client's is {}", version, net::PROTOCOL_VERSION, ); panic!("bad version number"); } } x => { debug!("{:?}", x); unimplemented!(); } } } Ok(Maintain) } fn frame( &mut self, frame_time: Duration, vfs: &Vfs, gfx_state: &GraphicsState, cmds: &mut CmdRegistry, console: &mut Console, music_player: &mut MusicPlayer, idle_vars: IdleVars, kick_vars: KickVars, roll_vars: RollVars, bob_vars: BobVars, cl_nolerp: f32, sv_gravity: f32, ) -> Result { debug!("frame time: {}ms", frame_time.num_milliseconds()); // do this _before_ parsing server messages so that we know when to // request the next message from the demo server. self.state.advance_time(frame_time); match self.parse_server_msg(vfs, gfx_state, cmds, console, music_player, kick_vars)? { ConnectionStatus::Maintain => (), // if Disconnect or NextDemo, delegate up the chain s => return Ok(s), }; self.state.update_interp_ratio(cl_nolerp); // interpolate entity data and spawn particle effects, lights self.state.update_entities()?; // update temp entities (lightning, etc.) self.state.update_temp_entities()?; // remove expired lights self.state.lights.update(self.state.time); // apply particle physics and remove expired particles self.state .particles .update(self.state.time, frame_time, sv_gravity); if let ConnectionKind::Server { ref mut qsock, ref mut compose, } = self.kind { // respond to the server if qsock.can_send() && !compose.is_empty() { qsock.begin_send_msg(&compose)?; compose.clear(); } } // these all require the player entity to have spawned if let ConnectionState::Connected(_) = self.conn_state { // update view self.state .calc_final_view(idle_vars, kick_vars, roll_vars, bob_vars); // update ear positions self.state.update_listener(); // spatialize sounds for new ear positions self.state.update_sound_spatialization(); // update camera color shifts for new position/effects self.state.update_color_shifts(frame_time)?; } Ok(ConnectionStatus::Maintain) } } pub struct Client { vfs: Rc, cvars: Rc>, cmds: Rc>, console: Rc>, input: Rc>, _output_stream: OutputStream, output_stream_handle: OutputStreamHandle, music_player: Rc>, conn: Rc>>, renderer: ClientRenderer, demo_queue: Rc>>, } impl Client { pub fn new( vfs: Rc, cvars: Rc>, cmds: Rc>, console: Rc>, input: Rc>, gfx_state: &GraphicsState, menu: &Menu, ) -> Client { let conn = Rc::new(RefCell::new(None)); let (stream, handle) = match OutputStream::try_default() { Ok(o) => o, // TODO: proceed without sound and allow configuration in menu Err(_) => Err(ClientError::OutputStream).unwrap(), }; // set up overlay/ui toggles cmds.borrow_mut() .insert_or_replace( "toggleconsole", cmd_toggleconsole(conn.clone(), input.clone()), ) .unwrap(); cmds.borrow_mut() .insert_or_replace("togglemenu", cmd_togglemenu(conn.clone(), input.clone())) .unwrap(); // set up connection console commands cmds.borrow_mut() .insert_or_replace( "connect", cmd_connect(conn.clone(), input.clone(), handle.clone()), ) .unwrap(); cmds.borrow_mut() .insert_or_replace("reconnect", cmd_reconnect(conn.clone(), input.clone())) .unwrap(); cmds.borrow_mut() .insert_or_replace("disconnect", cmd_disconnect(conn.clone(), input.clone())) .unwrap(); // set up demo playback cmds.borrow_mut() .insert_or_replace( "playdemo", cmd_playdemo(conn.clone(), vfs.clone(), input.clone(), handle.clone()), ) .unwrap(); let demo_queue = Rc::new(RefCell::new(VecDeque::new())); cmds.borrow_mut() .insert_or_replace( "startdemos", cmd_startdemos( conn.clone(), vfs.clone(), input.clone(), handle.clone(), demo_queue.clone(), ), ) .unwrap(); let music_player = Rc::new(RefCell::new(MusicPlayer::new(vfs.clone(), handle.clone()))); cmds.borrow_mut() .insert_or_replace("music", cmd_music(music_player.clone())) .unwrap(); cmds.borrow_mut() .insert_or_replace("music_stop", cmd_music_stop(music_player.clone())) .unwrap(); cmds.borrow_mut() .insert_or_replace("music_pause", cmd_music_pause(music_player.clone())) .unwrap(); cmds.borrow_mut() .insert_or_replace("music_resume", cmd_music_resume(music_player.clone())) .unwrap(); Client { vfs, cvars, cmds, console, input, _output_stream: stream, output_stream_handle: handle, music_player, conn, renderer: ClientRenderer::new(gfx_state, menu), demo_queue, } } pub fn disconnect(&mut self) { self.conn.replace(None); self.input.borrow_mut().set_focus(InputFocus::Console); } pub fn frame( &mut self, frame_time: Duration, gfx_state: &GraphicsState, ) -> Result<(), ClientError> { let cl_nolerp = self.cvar_value("cl_nolerp")?; let sv_gravity = self.cvar_value("sv_gravity")?; let idle_vars = self.idle_vars()?; let kick_vars = self.kick_vars()?; let roll_vars = self.roll_vars()?; let bob_vars = self.bob_vars()?; let status = match *self.conn.borrow_mut() { Some(ref mut conn) => conn.frame( frame_time, &self.vfs, gfx_state, &mut self.cmds.borrow_mut(), &mut self.console.borrow_mut(), &mut self.music_player.borrow_mut(), idle_vars, kick_vars, roll_vars, bob_vars, cl_nolerp, sv_gravity, )?, None => ConnectionStatus::Disconnect, }; use ConnectionStatus::*; match status { Maintain => (), _ => { let conn = match status { // if client is already disconnected, this is a no-op Disconnect => None, // get the next demo from the queue NextDemo => match self.demo_queue.borrow_mut().pop_front() { Some(demo) => { let mut demo_file = match self.vfs.open(format!("{}.dem", demo)) { Ok(f) => Some(f), Err(e) => { // log the error, dump the demo queue and disconnect self.console.borrow_mut().println(format!("{}", e)); self.demo_queue.borrow_mut().clear(); None } }; demo_file.as_mut().and_then(|df| match DemoServer::new(df) { Ok(d) => Some(Connection { kind: ConnectionKind::Demo(d), state: ClientState::new(self.output_stream_handle.clone()), conn_state: ConnectionState::SignOn(SignOnStage::Prespawn), }), Err(e) => { self.console.borrow_mut().println(format!("{}", e)); self.demo_queue.borrow_mut().clear(); None } }) } // if there are no more demos in the queue, disconnect None => None, }, // covered in first match Maintain => unreachable!(), }; match conn { Some(_) => self.input.borrow_mut().set_focus(InputFocus::Game), // don't allow game focus when disconnected None => self.input.borrow_mut().set_focus(InputFocus::Console), } self.conn.replace(conn); } } Ok(()) } pub fn render( &mut self, gfx_state: &GraphicsState, encoder: &mut wgpu::CommandEncoder, width: u32, height: u32, menu: &Menu, focus: InputFocus, ) -> Result<(), ClientError> { let fov = Deg(self.cvar_value("fov")?); let cvars = self.cvars.borrow(); let console = self.console.borrow(); self.renderer.render( gfx_state, encoder, self.conn.borrow().as_ref(), width, height, fov, &cvars, &console, menu, focus, ); Ok(()) } pub fn cvar_value(&self, name: S) -> Result where S: AsRef, { self.cvars .borrow() .get_value(name.as_ref()) .map_err(ClientError::Cvar) } pub fn handle_input( &mut self, game_input: &mut GameInput, frame_time: Duration, ) -> Result<(), ClientError> { let move_vars = self.move_vars()?; let mouse_vars = self.mouse_vars()?; match *self.conn.borrow_mut() { Some(Connection { ref mut state, kind: ConnectionKind::Server { ref mut qsock, .. }, .. }) => { let move_cmd = state.handle_input(game_input, frame_time, move_vars, mouse_vars); // TODO: arrayvec here let mut msg = Vec::new(); move_cmd.serialize(&mut msg)?; qsock.send_msg_unreliable(&msg)?; // clear mouse and impulse game_input.refresh(); } _ => (), } Ok(()) } fn move_vars(&self) -> Result { Ok(MoveVars { cl_anglespeedkey: self.cvar_value("cl_anglespeedkey")?, cl_pitchspeed: self.cvar_value("cl_pitchspeed")?, cl_yawspeed: self.cvar_value("cl_yawspeed")?, cl_sidespeed: self.cvar_value("cl_sidespeed")?, cl_upspeed: self.cvar_value("cl_upspeed")?, cl_forwardspeed: self.cvar_value("cl_forwardspeed")?, cl_backspeed: self.cvar_value("cl_backspeed")?, cl_movespeedkey: self.cvar_value("cl_movespeedkey")?, }) } fn idle_vars(&self) -> Result { Ok(IdleVars { v_idlescale: self.cvar_value("v_idlescale")?, v_ipitch_cycle: self.cvar_value("v_ipitch_cycle")?, v_ipitch_level: self.cvar_value("v_ipitch_level")?, v_iroll_cycle: self.cvar_value("v_iroll_cycle")?, v_iroll_level: self.cvar_value("v_iroll_level")?, v_iyaw_cycle: self.cvar_value("v_iyaw_cycle")?, v_iyaw_level: self.cvar_value("v_iyaw_level")?, }) } fn kick_vars(&self) -> Result { Ok(KickVars { v_kickpitch: self.cvar_value("v_kickpitch")?, v_kickroll: self.cvar_value("v_kickroll")?, v_kicktime: self.cvar_value("v_kicktime")?, }) } fn mouse_vars(&self) -> Result { Ok(MouseVars { m_pitch: self.cvar_value("m_pitch")?, m_yaw: self.cvar_value("m_yaw")?, sensitivity: self.cvar_value("sensitivity")?, }) } fn roll_vars(&self) -> Result { Ok(RollVars { cl_rollangle: self.cvar_value("cl_rollangle")?, cl_rollspeed: self.cvar_value("cl_rollspeed")?, }) } fn bob_vars(&self) -> Result { Ok(BobVars { cl_bob: self.cvar_value("cl_bob")?, cl_bobcycle: self.cvar_value("cl_bobcycle")?, cl_bobup: self.cvar_value("cl_bobup")?, }) } pub fn view_entity_id(&self) -> Option { match *self.conn.borrow() { Some(Connection { ref state, .. }) => Some(state.view_entity_id()), None => None, } } pub fn trace<'a, I>(&self, entity_ids: I) -> Result where I: IntoIterator, { match *self.conn.borrow() { Some(Connection { ref state, .. }) => { let mut trace = TraceFrame { msg_times_ms: [ state.msg_times[0].num_milliseconds(), state.msg_times[1].num_milliseconds(), ], time_ms: state.time.num_milliseconds(), lerp_factor: state.lerp_factor, entities: HashMap::new(), }; for id in entity_ids.into_iter() { let ent = &state.entities[*id]; let msg_origins = [ent.msg_origins[0].into(), ent.msg_origins[1].into()]; let msg_angles_deg = [ [ ent.msg_angles[0][0].0, ent.msg_angles[0][1].0, ent.msg_angles[0][2].0, ], [ ent.msg_angles[1][0].0, ent.msg_angles[1][1].0, ent.msg_angles[1][2].0, ], ]; trace.entities.insert( *id as u32, TraceEntity { msg_origins, msg_angles_deg, origin: ent.origin.into(), }, ); } Ok(trace) } None => Err(ClientError::NotConnected), } } } impl std::ops::Drop for Client { fn drop(&mut self) { // if this errors, it was already removed so we don't care let _ = self.cmds.borrow_mut().remove("reconnect"); } } // implements the "toggleconsole" command fn cmd_toggleconsole( conn: Rc>>, input: Rc>, ) -> Box String> { Box::new(move |_| { let focus = input.borrow().focus(); match *conn.borrow() { Some(_) => match focus { InputFocus::Game => input.borrow_mut().set_focus(InputFocus::Console), InputFocus::Console => input.borrow_mut().set_focus(InputFocus::Game), InputFocus::Menu => input.borrow_mut().set_focus(InputFocus::Console), }, None => match focus { InputFocus::Console => input.borrow_mut().set_focus(InputFocus::Menu), InputFocus::Game => unreachable!(), InputFocus::Menu => input.borrow_mut().set_focus(InputFocus::Console), }, } String::new() }) } // implements the "togglemenu" command fn cmd_togglemenu( conn: Rc>>, input: Rc>, ) -> Box String> { Box::new(move |_| { let focus = input.borrow().focus(); match *conn.borrow() { Some(_) => match focus { InputFocus::Game => input.borrow_mut().set_focus(InputFocus::Menu), InputFocus::Console => input.borrow_mut().set_focus(InputFocus::Menu), InputFocus::Menu => input.borrow_mut().set_focus(InputFocus::Game), }, None => match focus { InputFocus::Console => input.borrow_mut().set_focus(InputFocus::Menu), InputFocus::Game => unreachable!(), InputFocus::Menu => input.borrow_mut().set_focus(InputFocus::Console), }, } String::new() }) } fn connect(server_addrs: A, stream: OutputStreamHandle) -> Result where A: ToSocketAddrs, { let mut con_sock = ConnectSocket::bind("0.0.0.0:0")?; let server_addr = match server_addrs.to_socket_addrs() { Ok(ref mut a) => a.next().ok_or(ClientError::InvalidServerAddress), Err(_) => Err(ClientError::InvalidServerAddress), }?; let mut response = None; for attempt in 0..MAX_CONNECT_ATTEMPTS { println!( "Connecting...(attempt {} of {})", attempt + 1, MAX_CONNECT_ATTEMPTS ); con_sock.send_request( Request::connect(net::GAME_NAME, CONNECT_PROTOCOL_VERSION), server_addr, )?; // TODO: get rid of magic constant (2.5 seconds wait time for response) match con_sock.recv_response(Some(Duration::milliseconds(2500))) { Err(err) => { match err { // if the message is invalid, log it but don't quit // TODO: this should probably disconnect NetError::InvalidData(msg) => error!("{}", msg), // other errors are fatal e => return Err(e.into()), } } Ok(opt) => { if let Some((resp, remote)) = opt { // if this response came from the right server, we're done if remote == server_addr { response = Some(resp); break; } } } } } let port = match response.ok_or(ClientError::NoResponse)? { Response::Accept(accept) => { // validate port number if accept.port < 0 || accept.port >= std::u16::MAX as i32 { Err(ClientError::InvalidConnectPort(accept.port))?; } debug!("Connection accepted on port {}", accept.port); accept.port as u16 } // our request was rejected. Response::Reject(reject) => Err(ClientError::ConnectionRejected(reject.message))?, // the server sent back a response that doesn't make sense here (i.e. something other // than an Accept or Reject). _ => Err(ClientError::InvalidConnectResponse)?, }; let mut new_addr = server_addr; new_addr.set_port(port); // we're done with the connection socket, so turn it into a QSocket with the new address let qsock = con_sock.into_qsocket(new_addr); Ok(Connection { state: ClientState::new(stream), kind: ConnectionKind::Server { qsock, compose: Vec::new(), }, conn_state: ConnectionState::SignOn(SignOnStage::Prespawn), }) } // TODO: when an audio device goes down, every command with an // OutputStreamHandle needs to be reconstructed so it doesn't pass out // references to a dead output stream // TODO: this will hang while connecting. ideally, input should be handled in a // separate thread so the OS doesn't think the client has gone unresponsive. fn cmd_connect( conn: Rc>>, input: Rc>, stream: OutputStreamHandle, ) -> Box String> { Box::new(move |args| { if args.len() < 1 { // TODO: print to console return "usage: connect :".to_owned(); } match connect(args[0], stream.clone()) { Ok(new_conn) => { conn.replace(Some(new_conn)); input.borrow_mut().set_focus(InputFocus::Game); String::new() } Err(e) => format!("{}", e), } }) } fn cmd_reconnect( conn: Rc>>, input: Rc>, ) -> Box String> { Box::new(move |_| { match *conn.borrow_mut() { Some(ref mut conn) => { // TODO: clear client state conn.conn_state = ConnectionState::SignOn(SignOnStage::Prespawn); input.borrow_mut().set_focus(InputFocus::Game); String::new() } // TODO: log message, e.g. "can't reconnect while disconnected" None => "not connected".to_string(), } }) } fn cmd_disconnect( conn: Rc>>, input: Rc>, ) -> Box String> { Box::new(move |_| { let connected = conn.borrow().is_some(); if connected { conn.replace(None); input.borrow_mut().set_focus(InputFocus::Console); String::new() } else { "not connected".to_string() } }) } fn cmd_playdemo( conn: Rc>>, vfs: Rc, input: Rc>, stream: OutputStreamHandle, ) -> Box String> { Box::new(move |args| { if args.len() != 1 { return "usage: playdemo [DEMOFILE]".to_owned(); } let mut demo_file = match vfs.open(format!("{}.dem", args[0])) { Ok(f) => f, Err(e) => return format!("{}", e), }; let demo_server = match DemoServer::new(&mut demo_file) { Ok(d) => d, Err(e) => return format!("{}", e), }; conn.replace(Some(Connection { state: ClientState::new(stream.clone()), kind: ConnectionKind::Demo(demo_server), conn_state: ConnectionState::SignOn(SignOnStage::Prespawn), })); input.borrow_mut().set_focus(InputFocus::Game); String::new() }) } fn cmd_startdemos( conn: Rc>>, vfs: Rc, input: Rc>, stream: OutputStreamHandle, demo_queue: Rc>>, ) -> Box String> { Box::new(move |args| { if args.len() == 0 { return "usage: startdemos [DEMOS]".to_owned(); } for arg in args { demo_queue.borrow_mut().push_back(arg.to_string()); } let mut demo_file = match vfs.open(format!( "{}.dem", demo_queue.borrow_mut().pop_front().unwrap() )) { Ok(f) => f, Err(e) => return format!("{}", e), }; let demo_server = match DemoServer::new(&mut demo_file) { Ok(d) => d, Err(e) => return format!("{}", e), }; conn.replace(Some(Connection { state: ClientState::new(stream.clone()), kind: ConnectionKind::Demo(demo_server), conn_state: ConnectionState::SignOn(SignOnStage::Prespawn), })); input.borrow_mut().set_focus(InputFocus::Game); String::new() }) } fn cmd_music(music_player: Rc>) -> Box String> { Box::new(move |args| { if args.len() != 1 { return "usage: music [TRACKNAME]".to_owned(); } let res = music_player.borrow_mut().play_named(args[0]); match res { Ok(()) => String::new(), Err(e) => { music_player.borrow_mut().stop(); format!("{}", e) } } }) } fn cmd_music_stop(music_player: Rc>) -> Box String> { Box::new(move |_| { music_player.borrow_mut().stop(); String::new() }) } fn cmd_music_pause(music_player: Rc>) -> Box String> { Box::new(move |_| { music_player.borrow_mut().pause(); String::new() }) } fn cmd_music_resume(music_player: Rc>) -> Box String> { Box::new(move |_| { music_player.borrow_mut().resume(); String::new() }) } ================================================ FILE: src/client/render/atlas.rs ================================================ use std::{cmp::Ordering, mem::size_of}; use crate::client::render::Palette; use failure::Error; const DEFAULT_ATLAS_DIM: u32 = 1024; struct Rect { x: u32, y: u32, width: u32, height: u32, } fn area_order(t1: &TextureData, t2: &TextureData) -> Ordering { (t1.width * t1.height).cmp(&(t2.width * t2.height)) } #[derive(Clone, Debug)] pub struct TextureData { width: u32, height: u32, indexed: Vec, } impl TextureData { fn empty(width: u32, height: u32) -> TextureData { let len = (width * height) as usize; let mut indexed = Vec::with_capacity(len); indexed.resize(len, 0); TextureData { width, height, indexed, } } fn subtexture(&mut self, other: &TextureData, xy: [u32; 2]) -> Result<(), Error> { let [x, y] = xy; ensure!(x + other.width <= self.width); ensure!(y + other.height <= self.height); for r in 0..other.height { for c in 0..other.width { self.indexed[(self.width * (y + r) + x + c) as usize] = other.indexed[(other.width * r + c) as usize]; } } Ok(()) } } pub struct TextureAtlasBuilder { textures: Vec, } impl TextureAtlasBuilder { pub fn new() -> TextureAtlasBuilder { TextureAtlasBuilder { textures: Vec::new(), } } pub fn add(&mut self, texture: TextureData) -> Result { self.textures.push(texture); Ok(self.textures.len() - 1) } /// Constructs a TextureAtlas by efficiently packing multiple textures together. /// /// - Enumerate and sort the textures by total area, returning the sorted /// list of textures and a corresponding list of each texture's original index /// - Create a list of available rectangular spaces in the atlas, starting with /// the entire space. /// - For each texture, find a large enough space. Remove the space from the list, /// splitting off any unnecessary space and returning that to the list. Add the /// coordinates to a list of texture locations. /// - Sort the list of texture locations back into the original texture order. pub fn build( self, label: Option<&str>, device: &wgpu::Device, queue: &wgpu::Queue, palette: &Palette, ) -> Result { let TextureAtlasBuilder { textures } = self; let mut enumerated_textures = textures .into_iter() .enumerate() .collect::>(); enumerated_textures.sort_unstable_by(|e1, e2| area_order(&e1.1, &e2.1)); let (indices, textures): (Vec, Vec) = enumerated_textures.into_iter().unzip(); let mut atlas = TextureData::empty(DEFAULT_ATLAS_DIM, DEFAULT_ATLAS_DIM); let mut spaces = vec![Rect { x: 0, y: 0, width: atlas.width, height: atlas.height, }]; let mut subtextures: Vec = Vec::with_capacity(textures.len()); // iterate in reverse: largest textures first for tex in textures.iter().rev() { let mut coords: Option<(u32, u32)> = None; // find a large enough space for i in (0..spaces.len()).rev() { use std::cmp::Ordering::*; // - find a large enough space to fit the current texture // - copy the texture into the space // - remove the space from the list of candidates // - split off any unused space and return it to the list let subtex = match ( spaces[i].width.cmp(&tex.width), spaces[i].height.cmp(&tex.height), ) { // if either dimension is too small, keep looking (Less, _) | (_, Less) => continue, // perfect fit! (Equal, Equal) => { let Rect { x, y, .. } = spaces.remove(i); (x, y) } // split off the right side (Greater, Equal) => { let space = spaces.remove(i); spaces.push(Rect { x: space.x + tex.width, y: space.y, width: space.width - tex.width, height: space.height, }); (space.x, space.y) } // split off the bottom (Equal, Greater) => { let space = spaces.remove(i); spaces.push(Rect { x: space.x, y: space.y + tex.height, width: space.width, height: space.height - tex.height, }); (space.x, space.y) } // split off two spaces, maximizing the size of the large one (Greater, Greater) => { let space = spaces.remove(i); let w_diff = space.width - tex.width; let h_diff = space.height - tex.height; let (space_a, space_b) = if w_diff > h_diff { // ============= // | | | // | tex | | // | | A | // |-----| | // | B | | // ============= ( Rect { // A x: space.x + tex.width, y: space.y, width: space.width - tex.width, height: space.height, }, Rect { // B x: space.x, y: space.y + tex.height, width: tex.width, height: space.height - tex.height, }, ) } else { // ============= // | tex | B | // |-----------| // | | // | A | // | | // ============= ( Rect { // A x: space.x, y: space.y + tex.height, width: space.width, height: space.height - tex.height, }, Rect { // B x: space.x + tex.width, y: space.y, width: space.width - tex.width, height: tex.height, }, ) }; // put the smaller space closer to the end spaces.push(space_a); spaces.push(space_b); (space.x, space.y) } }; coords = Some(subtex); } match coords { Some((x, y)) => { let base_s = x as f32 / atlas.width as f32; let base_t = y as f32 / atlas.height as f32; let subtex_w = tex.width as f32 / atlas.width as f32; let subtex_h = tex.height as f32 / atlas.height as f32; subtextures.push(TextureAtlasSubtexture { base_xy: [x, y], base_st: [base_s, base_t], width: subtex_w, height: subtex_h, }); } None => bail!("Can't pack all textures in an atlas this size!"), } } // copy the textures into the atlas for (subtex, tex) in subtextures.iter().rev().zip(textures.iter()) { atlas.subtexture(tex, subtex.base_xy)?; } let mut enumerated_subtextures: Vec<(usize, TextureAtlasSubtexture)> = indices .into_iter() .zip(subtextures.into_iter().rev()) .collect(); // sort back into the original order enumerated_subtextures.sort_unstable_by(|e1, e2| e1.0.cmp(&e2.0)); let (_, subtextures): (Vec, Vec) = enumerated_subtextures.into_iter().unzip(); let (rgba, fullbright) = palette.translate(&atlas.indexed); let diffuse_buffer = device.create_buffer_with_data(&rgba, wgpu::BufferUsage::COPY_SRC); let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor { label: None, size: wgpu::Extent3d { width: atlas.width, height: atlas.height, depth_or_array_layers: 1, }, array_layer_count: 1, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8UnormSrgb, usage: wgpu::TextureUsage::NONE, }); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); encoder.copy_buffer_to_texture( wgpu::BufferCopyView { buffer: &diffuse_buffer, offset: 0, bytes_per_row: atlas.width * atlas.height * size_of::<[u8; 4]> as u32, rows_per_image: 1, }, wgpu::ImageCopyTexture { texture: &diffuse_texture, mip_level: 1, array_layer: 0, origin: wgpu::Origin3d::ZERO, }, wgpu::Extent3d { width: atlas.width, height: atlas.height, depth_or_array_layers: 1, }, ); let cmd_buffer = encoder.finish(); queue.submit(&[cmd_buffer]); Ok(TextureAtlas { atlas: diffuse_texture, width: atlas.width, height: atlas.height, subtextures, }) } } struct TextureAtlasSubtexture { // base subtexture coordinates in the atlas pixel space base_xy: [u32; 2], // base subtexture coordinates in the atlas texel space base_st: [f32; 2], // dimensions of the subtexture in atlas texel space width: f32, height: f32, } impl TextureAtlasSubtexture { fn convert_texcoords(&self, st: [f32; 2]) -> [f32; 2] { [ self.base_st[0] + st[0] * self.width, self.base_st[1] + st[1] * self.height, ] } } pub struct TextureAtlas { /// A handle to the atlas data on the GPU. atlas: wgpu::Texture, /// The width in texels of the atlas. width: u32, /// The height in texels of the atlas. height: u32, subtextures: Vec, } impl TextureAtlas { pub fn convert_texcoords(&self, id: usize, st: [f32; 2]) -> [f32; 2] { self.subtextures[id].convert_texcoords(st) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_texture_data_subtexture() { let src = TextureData { width: 2, height: 2, #[rustfmt::skip] indexed: vec![ 1, 2, 3, 4, ], }; let dst = TextureData { width: 4, height: 4, #[rustfmt::skip] indexed: vec![ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], }; let mut dst_copy = dst.clone(); dst_copy.subtexture(&src, [1, 1]).unwrap(); assert_eq!( dst_copy.indexed, vec![0, 0, 0, 0, 0, 1, 2, 0, 0, 3, 4, 0, 0, 0, 0, 0,] ); } } ================================================ FILE: src/client/render/blit.rs ================================================ use crate::client::render::{pipeline::Pipeline, ui::quad::QuadPipeline, GraphicsState}; pub struct BlitPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, bind_group: wgpu::BindGroup, sampler: wgpu::Sampler, } impl BlitPipeline { pub fn create_bind_group( device: &wgpu::Device, layouts: &[wgpu::BindGroupLayout], sampler: &wgpu::Sampler, input: &wgpu::TextureView, ) -> wgpu::BindGroup { device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("blit bind group"), layout: &layouts[0], entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(&sampler), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(input), }, ], }) } pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, input: &wgpu::TextureView, ) -> BlitPipeline { let (pipeline, bind_group_layouts) = BlitPipeline::create(device, compiler, &[], 1); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: None, address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, lod_min_clamp: -1000.0, lod_max_clamp: 1000.0, compare: None, anisotropy_clamp: None, ..Default::default() }); let bind_group = Self::create_bind_group(device, &bind_group_layouts, &sampler, input); BlitPipeline { pipeline, bind_group_layouts, bind_group, sampler, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, input: &wgpu::TextureView, ) { let layout_refs: Vec<_> = self.bind_group_layouts.iter().collect(); let pipeline = BlitPipeline::recreate(device, compiler, &layout_refs, 1); self.pipeline = pipeline; self.bind_group = Self::create_bind_group(device, self.bind_group_layouts(), &self.sampler, input); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn blit<'a>(&'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>) { pass.set_pipeline(&self.pipeline()); pass.set_bind_group(0, &self.bind_group, &[]); pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); pass.draw(0..6, 0..1); } } impl Pipeline for BlitPipeline { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "blit" } fn bind_group_layout_descriptors() -> Vec> { vec![wgpu::BindGroupLayoutDescriptor { label: Some("blit bind group"), entries: &[ // sampler wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { filtering: true, comparison: false, }, count: None, }, // blit texture wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: None, }, ], }] } fn vertex_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/blit.vert")) } fn fragment_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/blit.frag")) } fn primitive_state() -> wgpu::PrimitiveState { QuadPipeline::primitive_state() } fn color_target_states() -> Vec { QuadPipeline::color_target_states() } fn depth_stencil_state() -> Option { None } fn vertex_buffer_layouts() -> Vec> { QuadPipeline::vertex_buffer_layouts() } } ================================================ FILE: src/client/render/cvars.rs ================================================ // Copyright © 2020 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use crate::common::console::CvarRegistry; pub fn register_cvars(cvars: &CvarRegistry) { cvars.register("r_lightmap", "0").unwrap(); cvars.register("r_msaa_samples", "4").unwrap(); } ================================================ FILE: src/client/render/error.rs ================================================ use crate::common::{ vfs::VfsError, wad::WadError, }; use failure::{Backtrace, Context, Fail}; use std::{ convert::From, fmt::{self, Display}, }; #[derive(Debug)] pub struct RenderError { inner: Context, } impl RenderError { pub fn kind(&self) -> RenderErrorKind { *self.inner.get_context() } } impl From for RenderError { fn from(kind: RenderErrorKind) -> Self { RenderError { inner: Context::new(kind), } } } impl From for RenderError { fn from(vfs_error: VfsError) -> Self { match vfs_error { VfsError::NoSuchFile(_) => { vfs_error.context(RenderErrorKind::ResourceNotLoaded).into() } _ => vfs_error.context(RenderErrorKind::Other).into(), } } } impl From for RenderError { fn from(wad_error: WadError) -> Self { wad_error.context(RenderErrorKind::ResourceNotLoaded).into() } } impl From> for RenderError { fn from(inner: Context) -> Self { RenderError { inner } } } impl Fail for RenderError { fn cause(&self) -> Option<&dyn Fail> { self.inner.cause() } fn backtrace(&self) -> Option<&Backtrace> { self.inner.backtrace() } } impl Display for RenderError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt(&self.inner, f) } } #[derive(Clone, Copy, Eq, PartialEq, Debug, Fail)] pub enum RenderErrorKind { #[fail(display = "Failed to load resource")] ResourceNotLoaded, #[fail(display = "Unspecified render error")] Other, } ================================================ FILE: src/client/render/mod.rs ================================================ // Copyright © 2020 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. /// Rendering functionality. /// /// # Pipeline stages /// /// The current rendering implementation consists of the following stages: /// - Initial geometry pass /// - Inputs: /// - `AliasPipeline` /// - `BrushPipeline` /// - `SpritePipeline` /// - Output: `InitialPassTarget` /// - Deferred lighting pass /// - Inputs: /// - `DeferredPipeline` /// - Output: `DeferredPassTarget` /// - Final pass /// - Inputs: /// - `PostProcessPipeline` /// - `QuadPipeline` /// - `GlyphPipeline` /// - Output: `FinalPassTarget` /// - Blit to swap chain /// - Inputs: /// - `BlitPipeline` /// - Output: `SwapChainTarget` // mod atlas; mod blit; mod cvars; mod error; mod palette; mod pipeline; mod target; mod ui; mod uniform; mod warp; mod world; pub use cvars::register_cvars; pub use error::{RenderError, RenderErrorKind}; pub use palette::Palette; pub use pipeline::Pipeline; pub use postprocess::PostProcessRenderer; pub use target::{RenderTarget, RenderTargetResolve, SwapChainTarget}; pub use ui::{hud::HudState, UiOverlay, UiRenderer, UiState}; pub use world::{ deferred::{DeferredRenderer, DeferredUniforms, PointLight}, Camera, WorldRenderer, }; use std::{ borrow::Cow, cell::{Cell, Ref, RefCell, RefMut}, mem::size_of, num::{NonZeroU32, NonZeroU64, NonZeroU8}, rc::Rc, }; use crate::{ client::{ entity::MAX_LIGHTS, input::InputFocus, menu::Menu, render::{ blit::BlitPipeline, target::{DeferredPassTarget, FinalPassTarget, InitialPassTarget}, ui::{glyph::GlyphPipeline, quad::QuadPipeline}, uniform::DynamicUniformBuffer, world::{ alias::AliasPipeline, brush::BrushPipeline, deferred::DeferredPipeline, particle::ParticlePipeline, postprocess::{self, PostProcessPipeline}, sprite::SpritePipeline, EntityUniforms, }, }, Connection, ConnectionKind, }, common::{ console::{Console, CvarRegistry}, model::Model, net::SignOnStage, vfs::Vfs, wad::Wad, }, }; use super::ConnectionState; use bumpalo::Bump; use cgmath::{Deg, InnerSpace, Vector3, Zero}; use chrono::{DateTime, Duration, Utc}; use failure::Error; const DEPTH_ATTACHMENT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float; pub const DIFFUSE_ATTACHMENT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm; const NORMAL_ATTACHMENT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; const LIGHT_ATTACHMENT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; const DIFFUSE_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; const FULLBRIGHT_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm; const LIGHTMAP_TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm; /// Create a `wgpu::TextureDescriptor` appropriate for the provided texture data. pub fn texture_descriptor<'a>( label: Option<&'a str>, width: u32, height: u32, format: wgpu::TextureFormat, ) -> wgpu::TextureDescriptor { wgpu::TextureDescriptor { label, size: wgpu::Extent3d { width, height, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::SAMPLED, } } pub fn create_texture<'a>( device: &wgpu::Device, queue: &wgpu::Queue, label: Option<&'a str>, width: u32, height: u32, data: &TextureData, ) -> wgpu::Texture { trace!( "Creating texture ({:?}: {}x{})", data.format(), width, height ); let texture = device.create_texture(&texture_descriptor(label, width, height, data.format())); queue.write_texture( wgpu::ImageCopyTexture { texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, }, data.data(), wgpu::ImageDataLayout { offset: 0, bytes_per_row: NonZeroU32::new(width * data.stride()), rows_per_image: None, }, wgpu::Extent3d { width, height, depth_or_array_layers: 1, }, ); texture } pub struct DiffuseData<'a> { pub rgba: Cow<'a, [u8]>, } pub struct FullbrightData<'a> { pub fullbright: Cow<'a, [u8]>, } pub struct LightmapData<'a> { pub lightmap: Cow<'a, [u8]>, } pub enum TextureData<'a> { Diffuse(DiffuseData<'a>), Fullbright(FullbrightData<'a>), Lightmap(LightmapData<'a>), } impl<'a> TextureData<'a> { pub fn format(&self) -> wgpu::TextureFormat { match self { TextureData::Diffuse(_) => DIFFUSE_TEXTURE_FORMAT, TextureData::Fullbright(_) => FULLBRIGHT_TEXTURE_FORMAT, TextureData::Lightmap(_) => LIGHTMAP_TEXTURE_FORMAT, } } pub fn data(&self) -> &[u8] { match self { TextureData::Diffuse(d) => &d.rgba, TextureData::Fullbright(d) => &d.fullbright, TextureData::Lightmap(d) => &d.lightmap, } } pub fn stride(&self) -> u32 { (match self { TextureData::Diffuse(_) => size_of::<[u8; 4]>(), TextureData::Fullbright(_) => size_of::(), TextureData::Lightmap(_) => size_of::(), }) as u32 } pub fn size(&self) -> wgpu::BufferAddress { self.data().len() as wgpu::BufferAddress } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Extent2d { pub width: u32, pub height: u32, } impl std::convert::Into for Extent2d { fn into(self) -> wgpu::Extent3d { wgpu::Extent3d { width: self.width, height: self.height, depth_or_array_layers: 1, } } } impl std::convert::From> for Extent2d { fn from(other: winit::dpi::PhysicalSize) -> Extent2d { let winit::dpi::PhysicalSize { width, height } = other; Extent2d { width, height } } } pub struct GraphicsState { device: wgpu::Device, queue: wgpu::Queue, initial_pass_target: InitialPassTarget, deferred_pass_target: DeferredPassTarget, final_pass_target: FinalPassTarget, world_bind_group_layouts: Vec, world_bind_groups: Vec, frame_uniform_buffer: wgpu::Buffer, entity_uniform_buffer: RefCell>, diffuse_sampler: wgpu::Sampler, lightmap_sampler: wgpu::Sampler, sample_count: Cell, alias_pipeline: AliasPipeline, brush_pipeline: BrushPipeline, sprite_pipeline: SpritePipeline, deferred_pipeline: DeferredPipeline, particle_pipeline: ParticlePipeline, postprocess_pipeline: PostProcessPipeline, glyph_pipeline: GlyphPipeline, quad_pipeline: QuadPipeline, blit_pipeline: BlitPipeline, default_lightmap: wgpu::Texture, default_lightmap_view: wgpu::TextureView, vfs: Rc, palette: Palette, gfx_wad: Wad, compiler: RefCell, } impl GraphicsState { pub fn new( device: wgpu::Device, queue: wgpu::Queue, size: Extent2d, sample_count: u32, vfs: Rc, ) -> Result { let palette = Palette::load(&vfs, "gfx/palette.lmp"); let gfx_wad = Wad::load(vfs.open("gfx.wad")?).unwrap(); let mut compiler = shaderc::Compiler::new().unwrap(); let initial_pass_target = InitialPassTarget::new(&device, size, sample_count); let deferred_pass_target = DeferredPassTarget::new(&device, size, sample_count); let final_pass_target = FinalPassTarget::new(&device, size, sample_count); let frame_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("frame uniform buffer"), size: size_of::() as wgpu::BufferAddress, usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, mapped_at_creation: false, }); let entity_uniform_buffer = RefCell::new(DynamicUniformBuffer::new(&device)); let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: None, address_mode_u: wgpu::AddressMode::Repeat, address_mode_v: wgpu::AddressMode::Repeat, address_mode_w: wgpu::AddressMode::Repeat, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest, // TODO: these are the OpenGL defaults; see if there's a better choice for us lod_min_clamp: -1000.0, lod_max_clamp: 1000.0, compare: None, anisotropy_clamp: NonZeroU8::new(16), ..Default::default() }); let lightmap_sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: None, address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest, // TODO: these are the OpenGL defaults; see if there's a better choice for us lod_min_clamp: -1000.0, lod_max_clamp: 1000.0, compare: None, anisotropy_clamp: NonZeroU8::new(16), ..Default::default() }); let world_bind_group_layouts: Vec = world::BIND_GROUP_LAYOUT_DESCRIPTORS .iter() .map(|desc| device.create_bind_group_layout(desc)) .collect(); let world_bind_groups = vec![ device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("per-frame bind group"), layout: &world_bind_group_layouts[world::BindGroupLayoutId::PerFrame as usize], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &frame_uniform_buffer, offset: 0, size: None, }), }], }), device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("brush per-entity bind group"), layout: &world_bind_group_layouts[world::BindGroupLayoutId::PerEntity as usize], entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: &entity_uniform_buffer.borrow().buffer(), offset: 0, size: Some( NonZeroU64::new(size_of::() as u64).unwrap(), ), }), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&diffuse_sampler), }, wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(&lightmap_sampler), }, ], }), ]; let alias_pipeline = AliasPipeline::new( &device, &mut compiler, &world_bind_group_layouts, sample_count, ); let brush_pipeline = BrushPipeline::new( &device, &queue, &mut compiler, &world_bind_group_layouts, sample_count, ); let sprite_pipeline = SpritePipeline::new( &device, &mut compiler, &world_bind_group_layouts, sample_count, ); let deferred_pipeline = DeferredPipeline::new(&device, &mut compiler, sample_count); let particle_pipeline = ParticlePipeline::new(&device, &queue, &mut compiler, sample_count, &palette); let postprocess_pipeline = PostProcessPipeline::new(&device, &mut compiler, sample_count); let quad_pipeline = QuadPipeline::new(&device, &mut compiler, sample_count); let glyph_pipeline = GlyphPipeline::new(&device, &mut compiler, sample_count); let blit_pipeline = BlitPipeline::new(&device, &mut compiler, final_pass_target.resolve_view()); let default_lightmap = create_texture( &device, &queue, None, 1, 1, &TextureData::Lightmap(LightmapData { lightmap: (&[0xFF][..]).into(), }), ); let default_lightmap_view = default_lightmap.create_view(&Default::default()); Ok(GraphicsState { device, queue, initial_pass_target, deferred_pass_target, final_pass_target, frame_uniform_buffer, entity_uniform_buffer, world_bind_group_layouts, world_bind_groups, sample_count: Cell::new(sample_count), alias_pipeline, brush_pipeline, sprite_pipeline, deferred_pipeline, particle_pipeline, postprocess_pipeline, glyph_pipeline, quad_pipeline, blit_pipeline, diffuse_sampler, lightmap_sampler, default_lightmap, default_lightmap_view, vfs, palette, gfx_wad, compiler: RefCell::new(compiler), }) } pub fn create_texture<'a>( &self, label: Option<&'a str>, width: u32, height: u32, data: &TextureData, ) -> wgpu::Texture { create_texture(&self.device, &self.queue, label, width, height, data) } /// Update graphics state with the new framebuffer size and sample count. /// /// If the framebuffer size has changed, this recreates all render targets with the new size. /// /// If the framebuffer sample count has changed, this recreates all render targets with the /// new sample count and rebuilds the render pipelines to output that number of samples. pub fn update(&mut self, size: Extent2d, sample_count: u32) { if self.sample_count.get() != sample_count { self.sample_count.set(sample_count); self.recreate_pipelines(sample_count); } if self.initial_pass_target.size() != size || self.initial_pass_target.sample_count() != sample_count { self.initial_pass_target = InitialPassTarget::new(self.device(), size, sample_count); } if self.deferred_pass_target.size() != size || self.deferred_pass_target.sample_count() != sample_count { self.deferred_pass_target = DeferredPassTarget::new(self.device(), size, sample_count); } if self.final_pass_target.size() != size || self.final_pass_target.sample_count() != sample_count { self.final_pass_target = FinalPassTarget::new(self.device(), size, sample_count); self.blit_pipeline.rebuild( &self.device, &mut *self.compiler.borrow_mut(), self.final_pass_target.resolve_view(), ) } } /// Rebuild all render pipelines using the new sample count. /// /// This must be called when the sample count of the render target(s) changes or the program /// will panic. fn recreate_pipelines(&mut self, sample_count: u32) { self.alias_pipeline.rebuild( &self.device, &mut self.compiler.borrow_mut(), &self.world_bind_group_layouts, sample_count, ); self.brush_pipeline.rebuild( &self.device, &mut self.compiler.borrow_mut(), &self.world_bind_group_layouts, sample_count, ); self.sprite_pipeline.rebuild( &self.device, &mut self.compiler.borrow_mut(), &self.world_bind_group_layouts, sample_count, ); self.deferred_pipeline .rebuild(&self.device, &mut self.compiler.borrow_mut(), sample_count); self.postprocess_pipeline.rebuild( &self.device, &mut self.compiler.borrow_mut(), sample_count, ); self.glyph_pipeline .rebuild(&self.device, &mut self.compiler.borrow_mut(), sample_count); self.quad_pipeline .rebuild(&self.device, &mut self.compiler.borrow_mut(), sample_count); self.blit_pipeline.rebuild( &self.device, &mut self.compiler.borrow_mut(), self.final_pass_target.resolve_view(), ); } pub fn device(&self) -> &wgpu::Device { &self.device } pub fn queue(&self) -> &wgpu::Queue { &self.queue } pub fn initial_pass_target(&self) -> &InitialPassTarget { &self.initial_pass_target } pub fn deferred_pass_target(&self) -> &DeferredPassTarget { &self.deferred_pass_target } pub fn final_pass_target(&self) -> &FinalPassTarget { &self.final_pass_target } pub fn frame_uniform_buffer(&self) -> &wgpu::Buffer { &self.frame_uniform_buffer } pub fn entity_uniform_buffer(&self) -> Ref> { self.entity_uniform_buffer.borrow() } pub fn entity_uniform_buffer_mut(&self) -> RefMut> { self.entity_uniform_buffer.borrow_mut() } pub fn diffuse_sampler(&self) -> &wgpu::Sampler { &self.diffuse_sampler } pub fn default_lightmap(&self) -> &wgpu::Texture { &self.default_lightmap } pub fn default_lightmap_view(&self) -> &wgpu::TextureView { &self.default_lightmap_view } pub fn lightmap_sampler(&self) -> &wgpu::Sampler { &self.lightmap_sampler } pub fn world_bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.world_bind_group_layouts } pub fn world_bind_groups(&self) -> &[wgpu::BindGroup] { &self.world_bind_groups } // pipelines pub fn alias_pipeline(&self) -> &AliasPipeline { &self.alias_pipeline } pub fn brush_pipeline(&self) -> &BrushPipeline { &self.brush_pipeline } pub fn sprite_pipeline(&self) -> &SpritePipeline { &self.sprite_pipeline } pub fn deferred_pipeline(&self) -> &DeferredPipeline { &self.deferred_pipeline } pub fn particle_pipeline(&self) -> &ParticlePipeline { &self.particle_pipeline } pub fn postprocess_pipeline(&self) -> &PostProcessPipeline { &self.postprocess_pipeline } pub fn glyph_pipeline(&self) -> &GlyphPipeline { &self.glyph_pipeline } pub fn quad_pipeline(&self) -> &QuadPipeline { &self.quad_pipeline } pub fn blit_pipeline(&self) -> &BlitPipeline { &self.blit_pipeline } pub fn vfs(&self) -> &Vfs { &self.vfs } pub fn palette(&self) -> &Palette { &self.palette } pub fn gfx_wad(&self) -> &Wad { &self.gfx_wad } } pub struct ClientRenderer { deferred_renderer: DeferredRenderer, postprocess_renderer: PostProcessRenderer, ui_renderer: UiRenderer, bump: Bump, start_time: DateTime, } impl ClientRenderer { pub fn new(state: &GraphicsState, menu: &Menu) -> ClientRenderer { ClientRenderer { deferred_renderer: DeferredRenderer::new( state, state.initial_pass_target.diffuse_view(), state.initial_pass_target.normal_view(), state.initial_pass_target.light_view(), state.initial_pass_target.depth_view(), ), postprocess_renderer: PostProcessRenderer::new( state, state.deferred_pass_target.color_view(), ), ui_renderer: UiRenderer::new(state, menu), bump: Bump::new(), start_time: Utc::now(), } } pub fn render( &mut self, gfx_state: &GraphicsState, encoder: &mut wgpu::CommandEncoder, conn: Option<&Connection>, width: u32, height: u32, fov: Deg, cvars: &CvarRegistry, console: &Console, menu: &Menu, focus: InputFocus, ) { self.bump.reset(); if let Some(Connection { state: ref cl_state, ref conn_state, ref kind, }) = conn { match conn_state { ConnectionState::Connected(ref world) => { // if client is fully connected, draw world let camera = match kind { ConnectionKind::Demo(_) => { cl_state.demo_camera(width as f32 / height as f32, fov) } ConnectionKind::Server { .. } => { cl_state.camera(width as f32 / height as f32, fov) } }; // initial render pass { let init_pass_builder = gfx_state.initial_pass_target().render_pass_builder(); let mut init_pass = encoder.begin_render_pass(&init_pass_builder.descriptor()); world.render_pass( gfx_state, &mut init_pass, &self.bump, &camera, cl_state.time(), cl_state.iter_visible_entities(), cl_state.iter_particles(), cl_state.lightstyle_values().unwrap().as_slice(), cl_state.viewmodel_id(), cvars, ); } // deferred lighting pass { let deferred_pass_builder = gfx_state.deferred_pass_target().render_pass_builder(); let mut deferred_pass = encoder.begin_render_pass(&deferred_pass_builder.descriptor()); let mut lights = [PointLight { origin: Vector3::zero(), radius: 0.0, }; MAX_LIGHTS]; let mut light_count = 0; for (light_id, light) in cl_state.iter_lights().enumerate() { light_count += 1; let light_origin = light.origin(); let converted_origin = Vector3::new(-light_origin.y, light_origin.z, -light_origin.x); lights[light_id].origin = (camera.view() * converted_origin.extend(1.0)).truncate(); lights[light_id].radius = light.radius(cl_state.time()); } let uniforms = DeferredUniforms { inv_projection: camera.inverse_projection().into(), light_count, _pad: [0; 3], lights, }; self.deferred_renderer.rebuild( gfx_state, gfx_state.initial_pass_target().diffuse_view(), gfx_state.initial_pass_target().normal_view(), gfx_state.initial_pass_target().light_view(), gfx_state.initial_pass_target().depth_view(), ); self.deferred_renderer .record_draw(gfx_state, &mut deferred_pass, uniforms); } } // if client is still signing on, draw the loading screen ConnectionState::SignOn(_) => { // TODO: loading screen } } } let ui_state = match conn { Some(Connection { state: ref cl_state, .. }) => UiState::InGame { hud: match cl_state.intermission() { Some(kind) => HudState::Intermission { kind, completion_duration: cl_state.completion_time().unwrap() - cl_state.start_time(), stats: cl_state.stats(), console, }, None => HudState::InGame { items: cl_state.items(), item_pickup_time: cl_state.item_pickup_times(), stats: cl_state.stats(), face_anim_time: cl_state.face_anim_time(), console, }, }, overlay: match focus { InputFocus::Game => None, InputFocus::Console => Some(UiOverlay::Console(console)), InputFocus::Menu => Some(UiOverlay::Menu(menu)), }, }, None => UiState::Title { overlay: match focus { InputFocus::Console => UiOverlay::Console(console), InputFocus::Menu => UiOverlay::Menu(menu), InputFocus::Game => unreachable!(), }, }, }; // final render pass: postprocess the world and draw the UI { // quad_commands must outlive final pass let mut quad_commands = Vec::new(); let mut glyph_commands = Vec::new(); let final_pass_builder = gfx_state.final_pass_target().render_pass_builder(); let mut final_pass = encoder.begin_render_pass(&final_pass_builder.descriptor()); if let Some(Connection { state: ref cl_state, ref conn_state, .. }) = conn { // only postprocess if client is in the game if let ConnectionState::Connected(_) = conn_state { self.postprocess_renderer .rebuild(gfx_state, gfx_state.deferred_pass_target.color_view()); self.postprocess_renderer.record_draw( gfx_state, &mut final_pass, cl_state.color_shift(), ); } } self.ui_renderer.render_pass( &gfx_state, &mut final_pass, Extent2d { width, height }, // use client time when in game, renderer time otherwise match conn { Some(Connection { ref state, .. }) => state.time, None => Utc::now().signed_duration_since(self.start_time), }, &ui_state, &mut quad_commands, &mut glyph_commands, ); } } } ================================================ FILE: src/client/render/palette.rs ================================================ use std::{borrow::Cow, io::BufReader}; use crate::{ client::render::{DiffuseData, FullbrightData}, common::vfs::Vfs, }; use byteorder::ReadBytesExt; pub struct Palette { rgb: [[u8; 3]; 256], } impl Palette { pub fn new(data: &[u8]) -> Palette { if data.len() != 768 { panic!("Bad len for rgb data"); } let mut rgb = [[0; 3]; 256]; for color in 0..256 { for component in 0..3 { rgb[color][component] = data[color * 3 + component]; } } Palette { rgb } } pub fn load(vfs: &Vfs, path: S) -> Palette where S: AsRef, { let mut data = BufReader::new(vfs.open(path).unwrap()); let mut rgb = [[0u8; 3]; 256]; for color in 0..256 { for component in 0..3 { rgb[color][component] = data.read_u8().unwrap(); } } Palette { rgb } } // TODO: this will not render console characters correctly, as they use index 0 (black) to // indicate transparency. /// Translates a set of indices into a list of RGBA values and a list of fullbright values. pub fn translate(&self, indices: &[u8]) -> (DiffuseData, FullbrightData) { let mut rgba = Vec::with_capacity(indices.len() * 4); let mut fullbright = Vec::with_capacity(indices.len()); for index in indices { match *index { 0xFF => { for _ in 0..4 { rgba.push(0); fullbright.push(0); } } i => { for component in 0..3 { rgba.push(self.rgb[*index as usize][component]); } rgba.push(0xFF); fullbright.push(if i > 223 { 0xFF } else { 0 }); } } } ( DiffuseData { rgba: Cow::Owned(rgba), }, FullbrightData { fullbright: Cow::Owned(fullbright), }, ) } } ================================================ FILE: src/client/render/pipeline.rs ================================================ // Copyright © 2020 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::mem::size_of; use crate::common::util::{any_as_bytes, Pod}; /// The `Pipeline` trait, which allows render pipelines to be defined more-or-less declaratively. fn create_shader( device: &wgpu::Device, compiler: &mut shaderc::Compiler, name: S, kind: shaderc::ShaderKind, source: S, ) -> wgpu::ShaderModule where S: AsRef, { log::debug!("creating shader {}", name.as_ref()); let spirv = compiler .compile_into_spirv(source.as_ref(), kind, name.as_ref(), "main", None) .unwrap(); device.create_shader_module(&wgpu::ShaderModuleDescriptor { label: Some(name.as_ref()), source: wgpu::ShaderSource::SpirV(spirv.as_binary().into()), flags: wgpu::ShaderFlags::empty(), }) } pub enum PushConstantUpdate { /// Update the push constant to a new value. Update(T), /// Retain the current value of the push constant. Retain, /// Clear the push constant to no value. Clear, } /// A trait describing the behavior of a render pipeline. /// /// This trait's methods are used to define the pipeline's behavior in a more-or-less declarative /// style, leaving the actual creation to the default implementation of `Pipeline::create()`. pub trait Pipeline { /// Push constants used for the vertex stage of the pipeline. type VertexPushConstants: Pod; /// Push constants shared between the vertex and fragment stages of the pipeline. type SharedPushConstants: Pod; /// Push constants used for the fragment stage of the pipeline. type FragmentPushConstants: Pod; /// The name of this pipeline. fn name() -> &'static str; /// The `BindGroupLayoutDescriptor`s describing the bindings used in the pipeline. fn bind_group_layout_descriptors() -> Vec>; /// The GLSL source of the pipeline's vertex shader. fn vertex_shader() -> &'static str; /// The GLSL source of the pipeline's fragment shader. fn fragment_shader() -> &'static str; /// The primitive state used for rasterization in this pipeline. fn primitive_state() -> wgpu::PrimitiveState; /// The color state used for the pipeline. fn color_target_states() -> Vec; /// The depth-stencil state used for the pipeline, if any. fn depth_stencil_state() -> Option; /// Descriptors for the vertex buffers used by the pipeline. fn vertex_buffer_layouts() -> Vec>; fn vertex_push_constant_range() -> wgpu::PushConstantRange { let range = wgpu::PushConstantRange { stages: wgpu::ShaderStage::VERTEX, range: 0..size_of::() as u32 + size_of::() as u32, }; debug!("vertex push constant range: {:#?}", &range); range } fn fragment_push_constant_range() -> wgpu::PushConstantRange { let range = wgpu::PushConstantRange { stages: wgpu::ShaderStage::FRAGMENT, range: size_of::() as u32 ..size_of::() as u32 + size_of::() as u32 + size_of::() as u32, }; debug!("fragment push constant range: {:#?}", &range); range } fn push_constant_ranges() -> Vec { let vpc_size = size_of::(); let spc_size = size_of::(); let fpc_size = size_of::(); match (vpc_size, spc_size, fpc_size) { (0, 0, 0) => Vec::new(), (_, 0, 0) => vec![Self::vertex_push_constant_range()], (0, 0, _) => vec![Self::fragment_push_constant_range()], _ => vec![ Self::vertex_push_constant_range(), Self::fragment_push_constant_range(), ], } } /// Ensures that the associated push constant types have the proper size and /// alignment. fn validate_push_constant_types(limits: wgpu::Limits) { let pc_alignment = wgpu::PUSH_CONSTANT_ALIGNMENT as usize; let max_pc_size = limits.max_push_constant_size as usize; let vpc_size = size_of::(); let spc_size = size_of::(); let fpc_size = size_of::(); assert_eq!( vpc_size % pc_alignment, 0, "Vertex push constant size must be a multiple of {} bytes", wgpu::PUSH_CONSTANT_ALIGNMENT, ); assert_eq!( spc_size % pc_alignment, 0, "Shared push constant size must be a multiple of {} bytes", wgpu::PUSH_CONSTANT_ALIGNMENT, ); assert_eq!( fpc_size % pc_alignment, 0, "Fragment push constant size must be a multiple of {} bytes", wgpu::PUSH_CONSTANT_ALIGNMENT, ); assert!( vpc_size + spc_size + fpc_size < max_pc_size, "Combined size of push constants must be less than push constant size limit of {}", max_pc_size ); } /// Constructs a `RenderPipeline` and a list of `BindGroupLayout`s from the associated methods. /// /// `bind_group_layout_prefix` specifies a list of `BindGroupLayout`s to be prefixed onto those /// created from this pipeline's `bind_group_layout_descriptors()` method when creating the /// `RenderPipeline`. This permits the reuse of `BindGroupLayout`s between pipelines. fn create( device: &wgpu::Device, compiler: &mut shaderc::Compiler, bind_group_layout_prefix: &[wgpu::BindGroupLayout], sample_count: u32, ) -> (wgpu::RenderPipeline, Vec) { Self::validate_push_constant_types(device.limits()); info!("Creating {} pipeline", Self::name()); let bind_group_layouts = Self::bind_group_layout_descriptors() .iter() .map(|desc| device.create_bind_group_layout(desc)) .collect::>(); info!( "{} layouts in prefix | {} specific to pipeline", bind_group_layout_prefix.len(), bind_group_layouts.len(), ); let pipeline_layout = { // add bind group layout prefix let layouts: Vec<&wgpu::BindGroupLayout> = bind_group_layout_prefix .iter() .chain(bind_group_layouts.iter()) .collect(); info!("{} layouts total", layouts.len()); let ranges = Self::push_constant_ranges(); let label = format!("{} pipeline layout", Self::name()); let desc = wgpu::PipelineLayoutDescriptor { label: Some(&label), bind_group_layouts: &layouts, push_constant_ranges: &ranges, }; device.create_pipeline_layout(&desc) }; let vertex_shader = create_shader( device, compiler, format!("{}.vert", Self::name()).as_str(), shaderc::ShaderKind::Vertex, Self::vertex_shader(), ); let fragment_shader = create_shader( device, compiler, format!("{}.frag", Self::name()).as_str(), shaderc::ShaderKind::Fragment, Self::fragment_shader(), ); info!("create_render_pipeline"); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(&format!("{} pipeline", Self::name())), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &vertex_shader, entry_point: "main", buffers: &Self::vertex_buffer_layouts(), }, primitive: Self::primitive_state(), fragment: Some(wgpu::FragmentState { module: &fragment_shader, entry_point: "main", targets: &Self::color_target_states(), }), multisample: wgpu::MultisampleState { count: sample_count, mask: !0, alpha_to_coverage_enabled: false, }, depth_stencil: Self::depth_stencil_state(), }); (pipeline, bind_group_layouts) } /// Reconstructs the pipeline using its original bind group layouts and a new sample count. /// /// Pipelines must be reconstructed when the MSAA sample count is changed. fn recreate( device: &wgpu::Device, compiler: &mut shaderc::Compiler, bind_group_layouts: &[&wgpu::BindGroupLayout], sample_count: u32, ) -> wgpu::RenderPipeline { Self::validate_push_constant_types(device.limits()); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(&format!("{} pipeline layout", Self::name())), bind_group_layouts, push_constant_ranges: &[ Self::vertex_push_constant_range(), Self::fragment_push_constant_range(), ], }); let vertex_shader = create_shader( device, compiler, format!("{}.vert", Self::name()).as_str(), shaderc::ShaderKind::Vertex, Self::vertex_shader(), ); let fragment_shader = create_shader( device, compiler, format!("{}.frag", Self::name()).as_str(), shaderc::ShaderKind::Fragment, Self::fragment_shader(), ); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(&format!("{} pipeline", Self::name())), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &vertex_shader, entry_point: "main", buffers: &Self::vertex_buffer_layouts(), }, primitive: Self::primitive_state(), fragment: Some(wgpu::FragmentState { module: &fragment_shader, entry_point: "main", targets: &Self::color_target_states(), }), multisample: wgpu::MultisampleState { count: sample_count, mask: !0, alpha_to_coverage_enabled: false, }, depth_stencil: Self::depth_stencil_state(), }); pipeline } /// Set the push constant data for a render pass. /// /// For each argument, if the value is `Some`, then the corresponding push /// constant range is updated. If the value is `None`, the corresponding push /// constant range is cleared. fn set_push_constants<'a>( pass: &mut wgpu::RenderPass<'a>, vpc: PushConstantUpdate<&'a Self::VertexPushConstants>, spc: PushConstantUpdate<&'a Self::SharedPushConstants>, fpc: PushConstantUpdate<&'a Self::FragmentPushConstants>, ) { use PushConstantUpdate::*; let vpc_offset = 0; let spc_offset = vpc_offset + size_of::() as u32; let fpc_offset = spc_offset + size_of::() as u32; // these push constant size checks are known statically and will be // compiled out if size_of::() > 0 { let data = match vpc { Update(v) => Some(unsafe { any_as_bytes(v) }), Retain => None, Clear => Some(&[][..]), }; if let Some(d) = data { trace!( "Update vertex push constants at offset {} with data {:?}", vpc_offset, data ); pass.set_push_constants(wgpu::ShaderStage::VERTEX, vpc_offset, d); } } if size_of::() > 0 { let data = match spc { Update(s) => Some(unsafe { any_as_bytes(s) }), Retain => None, Clear => Some(&[][..]), }; if let Some(d) = data { trace!( "Update shared push constants at offset {} with data {:?}", spc_offset, data ); pass.set_push_constants( wgpu::ShaderStage::VERTEX | wgpu::ShaderStage::FRAGMENT, spc_offset, d, ); } } if size_of::() > 0 { let data = match fpc { Update(f) => Some(unsafe { any_as_bytes(f) }), Retain => None, Clear => Some(&[][..]), }; if let Some(d) = data { trace!( "Update fragment push constants at offset {} with data {:?}", fpc_offset, data ); pass.set_push_constants(wgpu::ShaderStage::FRAGMENT, fpc_offset, d); } } } } ================================================ FILE: src/client/render/target.rs ================================================ // Copyright © 2020 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use crate::client::render::{ Extent2d, DEPTH_ATTACHMENT_FORMAT, DIFFUSE_ATTACHMENT_FORMAT, LIGHT_ATTACHMENT_FORMAT, NORMAL_ATTACHMENT_FORMAT, }; // TODO: collapse these into a single definition /// Create a texture suitable for use as a color attachment. /// /// The resulting texture will have the RENDER_ATTACHMENT flag as well as /// any flags specified by `usage`. pub fn create_color_attachment( device: &wgpu::Device, size: Extent2d, sample_count: u32, usage: wgpu::TextureUsage, ) -> wgpu::Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("color attachment"), size: size.into(), mip_level_count: 1, sample_count, dimension: wgpu::TextureDimension::D2, format: DIFFUSE_ATTACHMENT_FORMAT, usage: wgpu::TextureUsage::RENDER_ATTACHMENT | usage, }) } /// Create a texture suitable for use as a normal attachment. /// /// The resulting texture will have the RENDER_ATTACHMENT flag as well as /// any flags specified by `usage`. pub fn create_normal_attachment( device: &wgpu::Device, size: Extent2d, sample_count: u32, usage: wgpu::TextureUsage, ) -> wgpu::Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("normal attachment"), size: size.into(), mip_level_count: 1, sample_count, dimension: wgpu::TextureDimension::D2, format: NORMAL_ATTACHMENT_FORMAT, usage: wgpu::TextureUsage::RENDER_ATTACHMENT | usage, }) } /// Create a texture suitable for use as a light attachment. /// /// The resulting texture will have the RENDER_ATTACHMENT flag as well as /// any flags specified by `usage`. pub fn create_light_attachment( device: &wgpu::Device, size: Extent2d, sample_count: u32, usage: wgpu::TextureUsage, ) -> wgpu::Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("light attachment"), size: size.into(), mip_level_count: 1, sample_count, dimension: wgpu::TextureDimension::D2, format: LIGHT_ATTACHMENT_FORMAT, usage: wgpu::TextureUsage::RENDER_ATTACHMENT | usage, }) } /// Create a texture suitable for use as a depth attachment. /// /// The underlying texture will have the RENDER_ATTACHMENT flag as well as /// any flags specified by `usage`. pub fn create_depth_attachment( device: &wgpu::Device, size: Extent2d, sample_count: u32, usage: wgpu::TextureUsage, ) -> wgpu::Texture { device.create_texture(&wgpu::TextureDescriptor { label: Some("depth attachment"), size: size.into(), mip_level_count: 1, sample_count, dimension: wgpu::TextureDimension::D2, format: DEPTH_ATTACHMENT_FORMAT, usage: wgpu::TextureUsage::RENDER_ATTACHMENT | usage, }) } /// Intermediate object that can generate `RenderPassDescriptor`s. pub struct RenderPassBuilder<'a> { color_attachments: Vec>, depth_attachment: Option>, } impl<'a> RenderPassBuilder<'a> { pub fn descriptor(&self) -> wgpu::RenderPassDescriptor { wgpu::RenderPassDescriptor { label: None, color_attachments: &self.color_attachments, depth_stencil_attachment: self.depth_attachment.clone(), } } } /// A trait describing a render target. /// /// A render target consists of a series of color attachments and an optional depth-stencil /// attachment. pub trait RenderTarget { fn render_pass_builder<'a>(&'a self) -> RenderPassBuilder<'a>; } /// A trait describing a render target with a built-in resolve attachment. pub trait RenderTargetResolve: RenderTarget { fn resolve_attachment(&self) -> &wgpu::Texture; fn resolve_view(&self) -> &wgpu::TextureView; } // TODO: use ArrayVec in concrete types so it can be passed // as Cow::Borrowed in RenderPassDescriptor /// Render target for the initial world pass. pub struct InitialPassTarget { size: Extent2d, sample_count: u32, diffuse_attachment: wgpu::Texture, diffuse_view: wgpu::TextureView, normal_attachment: wgpu::Texture, normal_view: wgpu::TextureView, light_attachment: wgpu::Texture, light_view: wgpu::TextureView, depth_attachment: wgpu::Texture, depth_view: wgpu::TextureView, } impl InitialPassTarget { pub fn new(device: &wgpu::Device, size: Extent2d, sample_count: u32) -> InitialPassTarget { let diffuse_attachment = create_color_attachment(device, size, sample_count, wgpu::TextureUsage::SAMPLED); let normal_attachment = create_normal_attachment(device, size, sample_count, wgpu::TextureUsage::SAMPLED); let light_attachment = create_light_attachment(device, size, sample_count, wgpu::TextureUsage::SAMPLED); let depth_attachment = create_depth_attachment(device, size, sample_count, wgpu::TextureUsage::SAMPLED); let diffuse_view = diffuse_attachment.create_view(&Default::default()); let normal_view = normal_attachment.create_view(&Default::default()); let light_view = light_attachment.create_view(&Default::default()); let depth_view = depth_attachment.create_view(&Default::default()); InitialPassTarget { size, sample_count, diffuse_attachment, diffuse_view, normal_attachment, normal_view, light_attachment, light_view, depth_attachment, depth_view, } } pub fn size(&self) -> Extent2d { self.size } pub fn sample_count(&self) -> u32 { self.sample_count } pub fn diffuse_attachment(&self) -> &wgpu::Texture { &self.diffuse_attachment } pub fn diffuse_view(&self) -> &wgpu::TextureView { &self.diffuse_view } pub fn normal_attachment(&self) -> &wgpu::Texture { &self.normal_attachment } pub fn normal_view(&self) -> &wgpu::TextureView { &self.normal_view } pub fn light_attachment(&self) -> &wgpu::Texture { &self.light_attachment } pub fn light_view(&self) -> &wgpu::TextureView { &self.light_view } pub fn depth_attachment(&self) -> &wgpu::Texture { &self.depth_attachment } pub fn depth_view(&self) -> &wgpu::TextureView { &self.depth_view } } impl RenderTarget for InitialPassTarget { fn render_pass_builder<'a>(&'a self) -> RenderPassBuilder { RenderPassBuilder { color_attachments: vec![ wgpu::RenderPassColorAttachment { view: self.diffuse_view(), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, }, wgpu::RenderPassColorAttachment { view: self.normal_view(), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, }, wgpu::RenderPassColorAttachment { view: self.light_view(), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, }, ], depth_attachment: Some(wgpu::RenderPassDepthStencilAttachment { view: self.depth_view(), depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: true, }), stencil_ops: None, }), } } } pub struct DeferredPassTarget { size: Extent2d, sample_count: u32, color_attachment: wgpu::Texture, color_view: wgpu::TextureView, } impl DeferredPassTarget { pub fn new(device: &wgpu::Device, size: Extent2d, sample_count: u32) -> DeferredPassTarget { let color_attachment = create_color_attachment(device, size, sample_count, wgpu::TextureUsage::SAMPLED); let color_view = color_attachment.create_view(&Default::default()); DeferredPassTarget { size, sample_count, color_attachment, color_view, } } pub fn size(&self) -> Extent2d { self.size } pub fn sample_count(&self) -> u32 { self.sample_count } pub fn color_attachment(&self) -> &wgpu::Texture { &self.color_attachment } pub fn color_view(&self) -> &wgpu::TextureView { &self.color_view } } impl RenderTarget for DeferredPassTarget { fn render_pass_builder<'a>(&'a self) -> RenderPassBuilder { RenderPassBuilder { color_attachments: vec![wgpu::RenderPassColorAttachment { view: self.color_view(), resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, }], depth_attachment: None, } } } pub struct FinalPassTarget { size: Extent2d, sample_count: u32, color_attachment: wgpu::Texture, color_view: wgpu::TextureView, resolve_attachment: wgpu::Texture, resolve_view: wgpu::TextureView, } impl FinalPassTarget { pub fn new(device: &wgpu::Device, size: Extent2d, sample_count: u32) -> FinalPassTarget { let color_attachment = create_color_attachment(device, size, sample_count, wgpu::TextureUsage::empty()); let color_view = color_attachment.create_view(&Default::default()); // add COPY_SRC so we can copy to a buffer for capture and SAMPLED so we // can blit to the swap chain let resolve_attachment = create_color_attachment( device, size, 1, wgpu::TextureUsage::COPY_SRC | wgpu::TextureUsage::SAMPLED, ); let resolve_view = resolve_attachment.create_view(&Default::default()); FinalPassTarget { size, sample_count, color_attachment, color_view, resolve_attachment, resolve_view, } } pub fn size(&self) -> Extent2d { self.size } pub fn sample_count(&self) -> u32 { self.sample_count } } impl RenderTarget for FinalPassTarget { fn render_pass_builder<'a>(&'a self) -> RenderPassBuilder { RenderPassBuilder { color_attachments: vec![wgpu::RenderPassColorAttachment { view: &self.color_view, resolve_target: Some(self.resolve_view()), ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, }], depth_attachment: None, } } } impl RenderTargetResolve for FinalPassTarget { fn resolve_attachment(&self) -> &wgpu::Texture { &self.resolve_attachment } fn resolve_view(&self) -> &wgpu::TextureView { &self.resolve_view } } pub struct SwapChainTarget<'a> { swap_chain_view: &'a wgpu::TextureView, } impl<'a> SwapChainTarget<'a> { pub fn with_swap_chain_view(swap_chain_view: &'a wgpu::TextureView) -> SwapChainTarget<'a> { SwapChainTarget { swap_chain_view } } } impl<'a> RenderTarget for SwapChainTarget<'a> { fn render_pass_builder(&self) -> RenderPassBuilder { RenderPassBuilder { color_attachments: vec![wgpu::RenderPassColorAttachment { view: self.swap_chain_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: true, }, }], depth_attachment: None, } } } ================================================ FILE: src/client/render/ui/console.rs ================================================ use crate::{ client::render::{ ui::{ glyph::{GlyphRendererCommand, GLYPH_HEIGHT, GLYPH_WIDTH}, layout::{Anchor, AnchorCoord, Layout, ScreenPosition, Size}, quad::{QuadRendererCommand, QuadTexture}, }, GraphicsState, }, common::{console::Console, engine, wad::QPic}, }; use chrono::Duration; const PAD_LEFT: i32 = GLYPH_WIDTH as i32; pub struct ConsoleRenderer { conback: QuadTexture, } impl ConsoleRenderer { pub fn new(state: &GraphicsState) -> ConsoleRenderer { let conback = QuadTexture::from_qpic( state, &QPic::load(state.vfs().open("gfx/conback.lmp").unwrap()).unwrap(), ); ConsoleRenderer { conback } } pub fn generate_commands<'a>( &'a self, console: &Console, time: Duration, quad_cmds: &mut Vec>, glyph_cmds: &mut Vec, proportion: f32, ) { // TODO: take scale as cvar let scale = 2.0; let console_anchor = Anchor { x: AnchorCoord::Zero, y: AnchorCoord::Proportion(1.0 - proportion), }; // draw console background quad_cmds.push(QuadRendererCommand { texture: &self.conback, layout: Layout { position: ScreenPosition::Absolute(console_anchor), anchor: Anchor::BOTTOM_LEFT, size: Size::DisplayScale { ratio: 1.0 }, }, }); // draw version string let version_string = format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); glyph_cmds.push(GlyphRendererCommand::Text { text: version_string, position: ScreenPosition::Absolute(console_anchor), anchor: Anchor::BOTTOM_RIGHT, scale, }); // draw input line glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: ']' as u8, position: ScreenPosition::Relative { anchor: console_anchor, x_ofs: PAD_LEFT, y_ofs: 0, }, anchor: Anchor::BOTTOM_LEFT, scale, }); let input_text = console.get_string(); glyph_cmds.push(GlyphRendererCommand::Text { text: input_text, position: ScreenPosition::Relative { anchor: console_anchor, x_ofs: PAD_LEFT + GLYPH_WIDTH as i32, y_ofs: 0, }, anchor: Anchor::BOTTOM_LEFT, scale, }); // blink cursor in half-second intervals if engine::duration_to_f32(time).fract() > 0.5 { glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: 11, position: ScreenPosition::Relative { anchor: console_anchor, x_ofs: PAD_LEFT + (GLYPH_WIDTH * (console.cursor() + 1)) as i32, y_ofs: 0, }, anchor: Anchor::BOTTOM_LEFT, scale, }); } // draw previous output for (line_id, line) in console.output().lines().enumerate() { // TODO: implement scrolling if line_id > 100 { break; } for (chr_id, chr) in line.iter().enumerate() { let position = ScreenPosition::Relative { anchor: console_anchor, x_ofs: PAD_LEFT + (1 + chr_id * GLYPH_WIDTH) as i32, y_ofs: ((line_id + 1) * GLYPH_HEIGHT) as i32, }; let c = if *chr as u32 > std::u8::MAX as u32 { warn!( "char \"{}\" (U+{:4}) cannot be displayed in the console", *chr, *chr as u32 ); '?' } else { *chr }; glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: c as u8, position, anchor: Anchor::BOTTOM_LEFT, scale, }); } } } } ================================================ FILE: src/client/render/ui/glyph.rs ================================================ use std::{mem::size_of, num::NonZeroU32}; use crate::{ client::render::{ ui::{ layout::{Anchor, ScreenPosition}, quad::{QuadPipeline, QuadVertex}, screen_space_vertex_scale, screen_space_vertex_translate, }, Extent2d, GraphicsState, Pipeline, TextureData, }, common::util::any_slice_as_bytes, }; use cgmath::Vector2; pub const GLYPH_WIDTH: usize = 8; pub const GLYPH_HEIGHT: usize = 8; const GLYPH_COLS: usize = 16; const GLYPH_ROWS: usize = 16; const GLYPH_COUNT: usize = GLYPH_ROWS * GLYPH_COLS; const GLYPH_TEXTURE_WIDTH: usize = GLYPH_WIDTH * GLYPH_COLS; /// The maximum number of glyphs that can be rendered at once. pub const MAX_INSTANCES: usize = 65536; lazy_static! { static ref VERTEX_BUFFER_ATTRIBUTES: [Vec; 2] = [ wgpu::vertex_attr_array![ 0 => Float32x2, // a_position 1 => Float32x2 // a_texcoord ].to_vec(), wgpu::vertex_attr_array![ 2 => Float32x2, // a_instance_position 3 => Float32x2, // a_instance_scale 4 => Uint32 // a_instance_layer ].to_vec(), ]; } pub struct GlyphPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, instance_buffer: wgpu::Buffer, } impl GlyphPipeline { pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) -> GlyphPipeline { let (pipeline, bind_group_layouts) = GlyphPipeline::create(device, compiler, &[], sample_count); let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("quad instance buffer"), size: (MAX_INSTANCES * size_of::()) as u64, usage: wgpu::BufferUsage::VERTEX | wgpu::BufferUsage::COPY_DST, mapped_at_creation: false, }); GlyphPipeline { pipeline, bind_group_layouts, instance_buffer, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) { let layout_refs = self.bind_group_layouts.iter().collect::>(); self.pipeline = GlyphPipeline::recreate(device, compiler, &layout_refs, sample_count); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn instance_buffer(&self) -> &wgpu::Buffer { &self.instance_buffer } } const BIND_GROUP_LAYOUT_ENTRIES: &[wgpu::BindGroupLayoutEntry] = &[ // sampler wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { filtering: true, comparison: false, }, count: None, }, // glyph texture array wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: NonZeroU32::new(GLYPH_COUNT as u32), }, ]; impl Pipeline for GlyphPipeline { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "glyph" } fn vertex_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/glyph.vert")) } fn fragment_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/glyph.frag")) } fn primitive_state() -> wgpu::PrimitiveState { QuadPipeline::primitive_state() } fn bind_group_layout_descriptors() -> Vec> { vec![wgpu::BindGroupLayoutDescriptor { label: Some("glyph constant bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES, }] } fn color_target_states() -> Vec { QuadPipeline::color_target_states() } fn depth_stencil_state() -> Option { QuadPipeline::depth_stencil_state() } fn vertex_buffer_layouts() -> Vec> { vec![ wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Vertex, attributes: &VERTEX_BUFFER_ATTRIBUTES[0], }, wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Instance, attributes: &VERTEX_BUFFER_ATTRIBUTES[1], }, ] } } #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct GlyphInstance { pub position: Vector2, pub scale: Vector2, pub layer: u32, } pub enum GlyphRendererCommand { Glyph { glyph_id: u8, position: ScreenPosition, anchor: Anchor, scale: f32, }, Text { text: String, position: ScreenPosition, anchor: Anchor, scale: f32, }, } pub struct GlyphRenderer { #[allow(dead_code)] textures: Vec, #[allow(dead_code)] texture_views: Vec, const_bind_group: wgpu::BindGroup, } impl GlyphRenderer { pub fn new(state: &GraphicsState) -> GlyphRenderer { let conchars = state.gfx_wad().open_conchars().unwrap(); // TODO: validate conchars dimensions let indices = conchars .indices() .iter() .map(|i| if *i == 0 { 0xFF } else { *i }) .collect::>(); // reorder indices from atlas order to array order let mut array_order = Vec::new(); for glyph_id in 0..GLYPH_COUNT { for glyph_r in 0..GLYPH_HEIGHT { for glyph_c in 0..GLYPH_WIDTH { let atlas_r = GLYPH_HEIGHT * (glyph_id / GLYPH_COLS) + glyph_r; let atlas_c = GLYPH_WIDTH * (glyph_id % GLYPH_COLS) + glyph_c; array_order.push(indices[atlas_r * GLYPH_TEXTURE_WIDTH + atlas_c]); } } } let textures = array_order .chunks_exact(GLYPH_WIDTH * GLYPH_HEIGHT) .enumerate() .map(|(id, indices)| { let (diffuse_data, _) = state.palette().translate(&indices); state.create_texture( Some(&format!("conchars[{}]", id)), GLYPH_WIDTH as u32, GLYPH_HEIGHT as u32, &TextureData::Diffuse(diffuse_data), ) }) .collect::>(); let texture_views = textures .iter() .map(|tex| tex.create_view(&Default::default())) .collect::>(); let texture_view_refs = texture_views.iter().collect::>(); let const_bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("glyph constant bind group"), layout: &state.glyph_pipeline().bind_group_layouts()[0], entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(state.diffuse_sampler()), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureViewArray(&texture_view_refs[..]), }, ], }); GlyphRenderer { textures, texture_views, const_bind_group, } } pub fn generate_instances( &self, commands: &[GlyphRendererCommand], target_size: Extent2d, ) -> Vec { let mut instances = Vec::new(); let Extent2d { width: display_width, height: display_height, } = target_size; for cmd in commands { match cmd { GlyphRendererCommand::Glyph { glyph_id, position, anchor, scale, } => { let (screen_x, screen_y) = position.to_xy(display_width, display_height, *scale); let (glyph_x, glyph_y) = anchor.to_xy( (GLYPH_WIDTH as f32 * scale) as u32, (GLYPH_HEIGHT as f32 * scale) as u32, ); let x = screen_x - glyph_x; let y = screen_y - glyph_y; instances.push(GlyphInstance { position: screen_space_vertex_translate( display_width, display_height, x, y, ), scale: screen_space_vertex_scale( display_width, display_height, (GLYPH_WIDTH as f32 * scale) as u32, (GLYPH_HEIGHT as f32 * scale) as u32, ), layer: *glyph_id as u32, }); } GlyphRendererCommand::Text { text, position, anchor, scale, } => { let (screen_x, screen_y) = position.to_xy(display_width, display_height, *scale); let (glyph_x, glyph_y) = anchor.to_xy( ((text.chars().count() * GLYPH_WIDTH) as f32 * scale) as u32, (GLYPH_HEIGHT as f32 * scale) as u32, ); let x = screen_x - glyph_x; let y = screen_y - glyph_y; for (chr_id, chr) in text.as_str().chars().enumerate() { let abs_x = x + ((GLYPH_WIDTH * chr_id) as f32 * scale) as i32; if abs_x >= display_width as i32 { // don't render past the edge of the screen break; } instances.push(GlyphInstance { position: screen_space_vertex_translate( display_width, display_height, abs_x, y, ), scale: screen_space_vertex_scale( display_width, display_height, (GLYPH_WIDTH as f32 * scale) as u32, (GLYPH_HEIGHT as f32 * scale) as u32, ), layer: chr as u32, }); } } } } instances } pub fn record_draw<'a>( &'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>, target_size: Extent2d, commands: &[GlyphRendererCommand], ) { let instances = self.generate_instances(commands, target_size); state .queue() .write_buffer(state.glyph_pipeline().instance_buffer(), 0, unsafe { any_slice_as_bytes(&instances) }); pass.set_pipeline(state.glyph_pipeline().pipeline()); pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); pass.set_vertex_buffer(1, state.glyph_pipeline().instance_buffer().slice(..)); pass.set_bind_group(0, &self.const_bind_group, &[]); pass.draw(0..6, 0..commands.len() as u32); } } ================================================ FILE: src/client/render/ui/hud.rs ================================================ use std::{collections::HashMap, iter::FromIterator}; use crate::{ client::{ render::{ ui::{ glyph::GlyphRendererCommand, layout::{Anchor, Layout, ScreenPosition, Size}, quad::{QuadRendererCommand, QuadTexture}, }, GraphicsState, }, IntermissionKind, }, common::{ console::Console, net::{ClientStat, ItemFlags}, wad::QPic, }, }; use arrayvec::ArrayVec; use chrono::Duration; use num::FromPrimitive as _; use strum::IntoEnumIterator as _; use strum_macros::EnumIter; // intermission overlay size const OVERLAY_WIDTH: i32 = 320; const OVERLAY_HEIGHT: i32 = 200; const OVERLAY_X_OFS: i32 = -OVERLAY_WIDTH / 2; const OVERLAY_Y_OFS: i32 = -OVERLAY_HEIGHT / 2; const OVERLAY_ANCHOR: Anchor = Anchor::CENTER; pub enum HudState<'a> { InGame { items: ItemFlags, item_pickup_time: &'a [Duration], stats: &'a [i32], face_anim_time: Duration, console: &'a Console, }, Intermission { kind: &'a IntermissionKind, completion_duration: Duration, stats: &'a [i32], console: &'a Console, }, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum HudTextureId { Digit { alt: bool, value: usize }, Minus { alt: bool }, Colon, Slash, Weapon { id: WeaponId, frame: WeaponFrame }, Ammo { id: AmmoId }, Armor { id: usize }, Item { id: ItemId }, Sigil { id: usize }, Face { id: FaceId }, StatusBar, InvBar, ScoreBar, // these are not in gfx.wad Complete, Intermission, } impl std::fmt::Display for HudTextureId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use HudTextureId::*; match *self { Digit { alt, value } => write!(f, "{}NUM_{}", if alt { "A" } else { "" }, value), Minus { alt } => write!(f, "{}NUM_MINUS", if alt { "A" } else { "" }), Colon => write!(f, "NUM_COLON"), Slash => write!(f, "NUM_SLASH"), Weapon { id, frame } => write!(f, "INV{}_{}", frame, id), Ammo { id } => write!(f, "SB_{}", id), Armor { id } => write!(f, "SB_ARMOR{}", id + 1), Item { id } => write!(f, "SB_{}", id), Sigil { id } => write!(f, "SB_SIGIL{}", id + 1), Face { id } => write!(f, "{}", id), StatusBar => write!(f, "SBAR"), InvBar => write!(f, "IBAR"), ScoreBar => write!(f, "SCOREBAR"), // these are not in gfx.wad Complete => write!(f, "gfx/complete.lmp"), Intermission => write!(f, "gfx/inter.lmp"), } } } const WEAPON_ID_NAMES: [&'static str; 7] = [ "SHOTGUN", "SSHOTGUN", "NAILGUN", "SNAILGUN", "RLAUNCH", "SRLAUNCH", "LIGHTNG", ]; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive, EnumIter)] enum WeaponId { Shotgun = 0, SuperShotgun = 1, Nailgun = 2, SuperNailgun = 3, RocketLauncher = 4, GrenadeLauncher = 5, LightningGun = 6, } impl std::fmt::Display for WeaponId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", WEAPON_ID_NAMES[*self as usize]) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum WeaponFrame { Inactive, Active, Pickup { frame: usize }, } impl std::fmt::Display for WeaponFrame { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { WeaponFrame::Inactive => write!(f, ""), WeaponFrame::Active => write!(f, "2"), WeaponFrame::Pickup { frame } => write!(f, "A{}", frame + 1), } } } const AMMO_ID_NAMES: [&'static str; 4] = ["SHELLS", "NAILS", "ROCKET", "CELLS"]; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive, EnumIter)] enum AmmoId { Shells = 0, Nails = 1, Rockets = 2, Cells = 3, } impl std::fmt::Display for AmmoId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", AMMO_ID_NAMES[*self as usize]) } } const ITEM_ID_NAMES: [&'static str; 6] = ["KEY1", "KEY2", "INVIS", "INVULN", "SUIT", "QUAD"]; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, FromPrimitive, EnumIter)] enum ItemId { Key1 = 0, Key2 = 1, Invisibility = 2, Invulnerability = 3, BioSuit = 4, QuadDamage = 5, } impl std::fmt::Display for ItemId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", ITEM_ID_NAMES[*self as usize]) } } #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] enum FaceId { Normal { pain: bool, frame: usize }, Invisible, Invulnerable, InvisibleInvulnerable, QuadDamage, } impl std::fmt::Display for FaceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use FaceId::*; match *self { Normal { pain, frame } => { write!(f, "FACE{}{}", if pain { "_P" } else { "" }, frame + 1) } Invisible => write!(f, "FACE_INVIS"), Invulnerable => write!(f, "FACE_INVUL2"), InvisibleInvulnerable => write!(f, "FACE_INV2"), QuadDamage => write!(f, "FACE_QUAD"), } } } pub struct HudRenderer { textures: HashMap, } impl HudRenderer { /// Construct a new `HudRenderer`. pub fn new(state: &GraphicsState) -> HudRenderer { use HudTextureId::*; let mut ids = Vec::new(); // digits and minus ids.extend((&[false, true]).iter().flat_map(|b| { (0..10) .map(move |i| Digit { alt: *b, value: i }) .chain(std::iter::once(Minus { alt: *b })) })); // weapons ids.extend(WeaponId::iter().flat_map(|id| { (0..5) .map(|frame| WeaponFrame::Pickup { frame }) .chain(std::iter::once(WeaponFrame::Inactive)) .chain(std::iter::once(WeaponFrame::Active)) .map(move |frame| Weapon { id, frame }) })); // ammo ids.extend(AmmoId::iter().map(|id| Ammo { id })); // armor ids.extend((0..3).map(|id| Armor { id })); // items ids.extend(ItemId::iter().map(|id| Item { id })); // sigils ids.extend((0..4).map(|id| Sigil { id })); // faces ids.extend( (&[false, true]) .iter() .flat_map(|b| (0..5).map(move |i| FaceId::Normal { pain: *b, frame: i })) .chain( vec![ FaceId::Invisible, FaceId::Invulnerable, FaceId::InvisibleInvulnerable, FaceId::QuadDamage, ] .into_iter(), ) .map(move |id| Face { id }), ); // unit variants ids.extend(vec![Colon, Slash, StatusBar, InvBar, ScoreBar].into_iter()); let mut textures = HashMap::new(); for id in ids.into_iter() { debug!("Opening {}", id); let qpic = state.gfx_wad().open_qpic(id.to_string()).unwrap(); let texture = QuadTexture::from_qpic(state, &qpic); textures.insert(id, texture); } // new id list for textures not in gfx.wad let ids = vec![Complete, Intermission]; for id in ids.into_iter() { debug!("Opening {}", id); let qpic = QPic::load(state.vfs().open(&format!("{}", id)).unwrap()).unwrap(); textures.insert(id, QuadTexture::from_qpic(state, &qpic)); } HudRenderer { textures } } fn cmd_number<'a>( &'a self, number: i32, alt_color: bool, max_digits: usize, screen_anchor: Anchor, screen_x_ofs: i32, screen_y_ofs: i32, quad_anchor: Anchor, scale: f32, quad_cmds: &mut Vec>, ) { use HudTextureId::*; let number_str = format!("{}", number); let number_chars = number_str.chars().collect::>(); let mut skip = 0; let mut place_ofs = 0; if number_chars.len() > max_digits { skip = number_chars.len() - max_digits; } else if max_digits > number_chars.len() { place_ofs = (max_digits - number_chars.len()) as i32 * 24; } for (chr_id, chr) in number_chars.into_iter().skip(skip).enumerate() { let tex_id = match chr { '-' => Minus { alt: alt_color }, '0'..='9' => Digit { alt: alt_color, value: chr as usize - '0' as usize, }, _ => unreachable!(), }; quad_cmds.push(QuadRendererCommand { texture: self.textures.get(&tex_id).unwrap(), layout: Layout { position: ScreenPosition::Relative { anchor: screen_anchor, x_ofs: screen_x_ofs + place_ofs + 24 * chr_id as i32, y_ofs: screen_y_ofs, }, anchor: quad_anchor, size: Size::Scale { factor: scale }, }, }); } } // Draw a quad on the status bar. // // `x_ofs` and `y_ofs` are specified relative to the bottom-left corner of // the status bar. fn cmd_sbar_quad<'a>( &'a self, texture_id: HudTextureId, x_ofs: i32, y_ofs: i32, scale: f32, quad_cmds: &mut Vec>, ) { quad_cmds.push(QuadRendererCommand { texture: self.textures.get(&texture_id).unwrap(), layout: Layout { position: ScreenPosition::Relative { anchor: Anchor::BOTTOM_CENTER, x_ofs: OVERLAY_X_OFS + x_ofs, y_ofs, }, anchor: Anchor::BOTTOM_LEFT, size: Size::Scale { factor: scale }, }, }); } // Draw a quad on the status bar. // // `x_ofs` and `y_ofs` are specified relative to the bottom-left corner of // the status bar. fn cmd_sbar_number<'a>( &'a self, number: i32, alt_color: bool, max_digits: usize, x_ofs: i32, y_ofs: i32, scale: f32, quad_cmds: &mut Vec>, ) { self.cmd_number( number, alt_color, max_digits, Anchor::BOTTOM_CENTER, OVERLAY_X_OFS + x_ofs, y_ofs, Anchor::BOTTOM_LEFT, scale, quad_cmds, ); } // Draw the status bar. fn cmd_sbar<'a>( &'a self, time: Duration, items: ItemFlags, item_pickup_time: &'a [Duration], stats: &'a [i32], face_anim_time: Duration, scale: f32, quad_cmds: &mut Vec>, glyph_cmds: &mut Vec, ) { use HudTextureId::*; let sbar = self.textures.get(&StatusBar).unwrap(); let sbar_x_ofs = -(sbar.width() as i32) / 2; // status bar background self.cmd_sbar_quad(StatusBar, 0, 0, scale, quad_cmds); // inventory bar background self.cmd_sbar_quad(InvBar, 0, sbar.height() as i32, scale, quad_cmds); // weapon slots for i in 0..7 { if items.contains(ItemFlags::from_bits(ItemFlags::SHOTGUN.bits() << i).unwrap()) { let id = WeaponId::from_usize(i).unwrap(); let pickup_time = item_pickup_time[i]; let delta = time - pickup_time; let frame = if delta >= Duration::milliseconds(100) { if stats[ClientStat::ActiveWeapon as usize] as u32 == ItemFlags::SHOTGUN.bits() << i { WeaponFrame::Active } else { WeaponFrame::Inactive } } else { WeaponFrame::Pickup { frame: (delta.num_milliseconds() * 100) as usize % 5, } }; self.cmd_sbar_quad( Weapon { id, frame }, 24 * i as i32, sbar.height() as i32, scale, quad_cmds, ); } } // ammo counters for i in 0..4 { let ammo_str = format!("{: >3}", stats[ClientStat::Shells as usize + i]); for (chr_id, chr) in ammo_str.chars().enumerate() { if chr != ' ' { glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: 18 + chr as u8 - '0' as u8, position: ScreenPosition::Relative { anchor: Anchor::BOTTOM_CENTER, x_ofs: sbar_x_ofs + 8 * (6 * i + chr_id) as i32 + 10, y_ofs: sbar.height() as i32 + 16, }, anchor: Anchor::BOTTOM_LEFT, scale, }); } } } // items (keys and powerups) for i in 0..6 { if items.contains(ItemFlags::from_bits(ItemFlags::KEY_1.bits() << i).unwrap()) { quad_cmds.push(QuadRendererCommand { texture: self .textures .get(&Item { id: ItemId::from_usize(i).unwrap(), }) .unwrap(), layout: Layout { position: ScreenPosition::Relative { anchor: Anchor::BOTTOM_CENTER, x_ofs: sbar_x_ofs + 16 * i as i32 + 192, y_ofs: sbar.height() as i32, }, anchor: Anchor::BOTTOM_LEFT, size: Size::Scale { factor: scale }, }, }) } } // sigils for i in 0..4 { if items.contains(ItemFlags::from_bits(ItemFlags::SIGIL_1.bits() << i).unwrap()) { quad_cmds.push(QuadRendererCommand { texture: self.textures.get(&Sigil { id: i }).unwrap(), layout: Layout { position: ScreenPosition::Relative { anchor: Anchor::BOTTOM_CENTER, x_ofs: sbar_x_ofs + 8 * i as i32 + 288, y_ofs: sbar.height() as i32, }, anchor: Anchor::BOTTOM_LEFT, size: Size::Scale { factor: scale }, }, }); } } // armor let armor_width = self.textures.get(&Armor { id: 0 }).unwrap().width() as i32; if items.contains(ItemFlags::INVULNERABILITY) { self.cmd_sbar_number(666, true, 3, armor_width, 0, scale, quad_cmds); // TODO draw_disc } else { let armor = stats[ClientStat::Armor as usize]; self.cmd_sbar_number(armor, armor <= 25, 3, armor_width, 0, scale, quad_cmds); let mut armor_id = None; for i in (0..3).rev() { if items.contains(ItemFlags::from_bits(ItemFlags::ARMOR_1.bits() << i).unwrap()) { armor_id = Some(Armor { id: i }); break; } } if let Some(a) = armor_id { self.cmd_sbar_quad(a, 0, 0, scale, quad_cmds); } } // health let health = stats[ClientStat::Health as usize]; self.cmd_sbar_number(health, health <= 25, 3, 136, 0, scale, quad_cmds); let ammo = stats[ClientStat::Ammo as usize]; self.cmd_sbar_number(ammo, ammo <= 10, 3, 248, 0, scale, quad_cmds); let face = if items.contains(ItemFlags::INVISIBILITY | ItemFlags::INVULNERABILITY) { FaceId::InvisibleInvulnerable } else if items.contains(ItemFlags::QUAD) { FaceId::QuadDamage } else if items.contains(ItemFlags::INVISIBILITY) { FaceId::Invisible } else if items.contains(ItemFlags::INVULNERABILITY) { FaceId::Invulnerable } else { let health = stats[ClientStat::Health as usize]; let frame = 4 - if health >= 100 { 4 } else { health.max(0) as usize / 20 }; FaceId::Normal { pain: face_anim_time > time, frame, } }; self.cmd_sbar_quad(Face { id: face }, 112, 0, scale, quad_cmds); // crosshair glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: '+' as u8, position: ScreenPosition::Absolute(Anchor::CENTER), anchor: Anchor::TOP_LEFT, scale, }); } // Draw a quad on the intermission overlay. // // `x_ofs` and `y_ofs` are specified relative to the top-left corner of the // overlay. fn cmd_intermission_quad<'a>( &'a self, texture_id: HudTextureId, x_ofs: i32, y_ofs: i32, scale: f32, quad_cmds: &mut Vec>, ) { quad_cmds.push(QuadRendererCommand { texture: self.textures.get(&texture_id).unwrap(), layout: Layout { position: ScreenPosition::Relative { anchor: Anchor::CENTER, x_ofs: OVERLAY_X_OFS + x_ofs, y_ofs: OVERLAY_Y_OFS + y_ofs, }, anchor: Anchor::TOP_LEFT, size: Size::Scale { factor: scale }, }, }); } // Draw a number on the intermission overlay. // // `x_ofs` and `y_ofs` are specified relative to the top-left corner of the // overlay. fn cmd_intermission_number<'a>( &'a self, number: i32, max_digits: usize, x_ofs: i32, y_ofs: i32, scale: f32, quad_cmds: &mut Vec>, ) { self.cmd_number( number, false, max_digits, OVERLAY_ANCHOR, OVERLAY_X_OFS + x_ofs, OVERLAY_Y_OFS + y_ofs, Anchor::TOP_LEFT, scale, quad_cmds, ); } // Draw the intermission overlay. fn cmd_intermission_overlay<'a>( &'a self, _kind: &'a IntermissionKind, completion_duration: Duration, stats: &'a [i32], scale: f32, quad_cmds: &mut Vec>, ) { use HudTextureId::*; // TODO: check gametype self.cmd_intermission_quad(Complete, 64, OVERLAY_HEIGHT - 24, scale, quad_cmds); self.cmd_intermission_quad(Intermission, 0, OVERLAY_HEIGHT - 56, scale, quad_cmds); // TODO: zero-pad number of seconds let time_y_ofs = OVERLAY_HEIGHT - 64; let minutes = completion_duration.num_minutes() as i32; let seconds = completion_duration.num_seconds() as i32 - 60 * minutes; self.cmd_intermission_number(minutes, 3, 160, time_y_ofs, scale, quad_cmds); self.cmd_intermission_quad(Colon, 234, time_y_ofs, scale, quad_cmds); self.cmd_intermission_number(seconds, 2, 246, time_y_ofs, scale, quad_cmds); // secrets let secrets_y_ofs = OVERLAY_HEIGHT - 104; let secrets_found = stats[ClientStat::FoundSecrets as usize]; let secrets_total = stats[ClientStat::TotalSecrets as usize]; self.cmd_intermission_number(secrets_found, 3, 160, secrets_y_ofs, scale, quad_cmds); self.cmd_intermission_quad(Slash, 232, secrets_y_ofs, scale, quad_cmds); self.cmd_intermission_number(secrets_total, 3, 240, secrets_y_ofs, scale, quad_cmds); // monsters let monsters_y_ofs = OVERLAY_HEIGHT - 144; let monsters_killed = stats[ClientStat::KilledMonsters as usize]; let monsters_total = stats[ClientStat::TotalMonsters as usize]; self.cmd_intermission_number(monsters_killed, 3, 160, monsters_y_ofs, scale, quad_cmds); self.cmd_intermission_quad(Slash, 232, monsters_y_ofs, scale, quad_cmds); self.cmd_intermission_number(monsters_total, 3, 240, monsters_y_ofs, scale, quad_cmds); } /// Generate render commands to draw the HUD in the specified state. pub fn generate_commands<'state, 'a>( &'a self, hud_state: &HudState<'a>, time: Duration, quad_cmds: &mut Vec>, glyph_cmds: &mut Vec, ) { // TODO: get from cvar let scale = 2.0; let console_timeout = Duration::seconds(3); match hud_state { HudState::InGame { items, item_pickup_time, stats, face_anim_time, console, } => { self.cmd_sbar( time, *items, item_pickup_time, stats, *face_anim_time, scale, quad_cmds, glyph_cmds, ); let output = console.output(); for (id, line) in output.recent_lines(console_timeout, 100, 10).enumerate() { for (chr_id, chr) in line.into_iter().enumerate() { glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: *chr as u8, position: ScreenPosition::Relative { anchor: Anchor::TOP_LEFT, x_ofs: 8 * chr_id as i32, y_ofs: -8 * id as i32, }, anchor: Anchor::TOP_LEFT, scale, }); } } } HudState::Intermission { kind, completion_duration, stats, console, } => { self.cmd_intermission_overlay(kind, *completion_duration, stats, scale, quad_cmds); // TODO: dedup this code let output = console.output(); for (id, line) in output.recent_lines(console_timeout, 100, 10).enumerate() { for (chr_id, chr) in line.into_iter().enumerate() { glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id: *chr as u8, position: ScreenPosition::Relative { anchor: Anchor::TOP_LEFT, x_ofs: 8 * chr_id as i32, y_ofs: -8 * id as i32, }, anchor: Anchor::TOP_LEFT, scale, }); } } } } } } ================================================ FILE: src/client/render/ui/layout.rs ================================================ #[derive(Clone, Copy, Debug)] pub struct Layout { /// The position of the quad on the screen. pub position: ScreenPosition, /// Which part of the quad to position at `position`. pub anchor: Anchor, /// The size at which to render the quad. pub size: Size, } /// An anchor coordinate. #[derive(Clone, Copy, Debug)] pub enum AnchorCoord { /// A value of zero in this dimension. Zero, /// The center of the quad in this dimension. Center, /// The maximum extent of the quad in this dimension. Max, /// An absolute anchor coordinate, in pixels. Absolute(i32), /// A proportion of the maximum extent of the quad in this dimension. Proportion(f32), } impl AnchorCoord { pub fn to_value(&self, max: u32) -> i32 { match *self { AnchorCoord::Zero => 0, AnchorCoord::Center => max as i32 / 2, AnchorCoord::Max => max as i32, AnchorCoord::Absolute(v) => v, AnchorCoord::Proportion(p) => (p * max as f32) as i32, } } } /// An anchor position on a quad. /// /// The anchor specifies which part of the quad should be considered the origin /// when positioning the quad, or when positioning quads relative to one another. #[derive(Clone, Copy, Debug)] pub struct Anchor { /// The x-coordinate of the anchor. pub x: AnchorCoord, /// The y-coordinate of the anchor. pub y: AnchorCoord, } impl Anchor { pub const BOTTOM_LEFT: Anchor = Anchor { x: AnchorCoord::Zero, y: AnchorCoord::Zero, }; pub const CENTER_LEFT: Anchor = Anchor { x: AnchorCoord::Zero, y: AnchorCoord::Center, }; pub const TOP_LEFT: Anchor = Anchor { x: AnchorCoord::Zero, y: AnchorCoord::Max, }; pub const BOTTOM_CENTER: Anchor = Anchor { x: AnchorCoord::Center, y: AnchorCoord::Zero, }; pub const CENTER: Anchor = Anchor { x: AnchorCoord::Center, y: AnchorCoord::Center, }; pub const TOP_CENTER: Anchor = Anchor { x: AnchorCoord::Center, y: AnchorCoord::Max, }; pub const BOTTOM_RIGHT: Anchor = Anchor { x: AnchorCoord::Max, y: AnchorCoord::Zero, }; pub const CENTER_RIGHT: Anchor = Anchor { x: AnchorCoord::Max, y: AnchorCoord::Center, }; pub const TOP_RIGHT: Anchor = Anchor { x: AnchorCoord::Max, y: AnchorCoord::Max, }; pub fn absolute_xy(x: i32, y: i32) -> Anchor { Anchor { x: AnchorCoord::Absolute(x), y: AnchorCoord::Absolute(y), } } pub fn to_xy(&self, width: u32, height: u32) -> (i32, i32) { (self.x.to_value(width), self.y.to_value(height)) } } /// The position of a quad rendered on the screen. #[derive(Clone, Copy, Debug)] pub enum ScreenPosition { /// The quad is positioned at the exact coordinates provided. Absolute(Anchor), /// The quad is positioned relative to a reference point. Relative { anchor: Anchor, /// The offset along the x-axis from `reference_x`. x_ofs: i32, /// The offset along the y-axis from `reference_y`. y_ofs: i32, }, } impl ScreenPosition { pub fn to_xy(&self, display_width: u32, display_height: u32, scale: f32) -> (i32, i32) { match *self { ScreenPosition::Absolute(Anchor { x: anchor_x, y: anchor_y, }) => ( anchor_x.to_value(display_width), anchor_y.to_value(display_height), ), ScreenPosition::Relative { anchor: Anchor { x: anchor_x, y: anchor_y, }, x_ofs, y_ofs, } => ( anchor_x.to_value(display_width) + (x_ofs as f32 * scale) as i32, anchor_y.to_value(display_height) + (y_ofs as f32 * scale) as i32, ), } } } /// Specifies what size a quad should be when rendered on the screen. #[derive(Clone, Copy, Debug)] pub enum Size { /// Render the quad at an exact size in pixels. Absolute { /// The width of the quad in pixels. width: u32, /// The height of the quad in pixels. height: u32, }, /// Render the quad at a size specified relative to the dimensions of its texture. Scale { /// The factor to multiply by the quad's texture dimensions to determine its size. factor: f32, }, /// Render the quad at a size specified relative to the size of the display. DisplayScale { /// The ratio of the display size at which to render the quad. ratio: f32, }, } impl Size { pub fn to_wh( &self, texture_width: u32, texture_height: u32, display_width: u32, display_height: u32, ) -> (u32, u32) { match *self { Size::Absolute { width, height } => (width, height), Size::Scale { factor } => ( (texture_width as f32 * factor) as u32, (texture_height as f32 * factor) as u32, ), Size::DisplayScale { ratio } => ( (display_width as f32 * ratio) as u32, (display_height as f32 * ratio) as u32, ), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_anchor_to_xy() { let width = 1366; let height = 768; assert_eq!(Anchor::BOTTOM_LEFT.to_xy(width, height), (0, 0)); assert_eq!(Anchor::CENTER_LEFT.to_xy(width, height), (0, 384)); assert_eq!(Anchor::TOP_LEFT.to_xy(width, height), (0, 768)); assert_eq!(Anchor::BOTTOM_CENTER.to_xy(width, height), (683, 0)); assert_eq!(Anchor::CENTER.to_xy(width, height), (683, 384)); assert_eq!(Anchor::TOP_CENTER.to_xy(width, height), (683, 768)); assert_eq!(Anchor::BOTTOM_RIGHT.to_xy(width, height), (1366, 0)); assert_eq!(Anchor::CENTER_RIGHT.to_xy(width, height), (1366, 384)); assert_eq!(Anchor::TOP_RIGHT.to_xy(width, height), (1366, 768)); } } ================================================ FILE: src/client/render/ui/menu.rs ================================================ use std::collections::HashMap; use crate::{ client::{ menu::{Item, Menu, MenuBodyView, MenuState, NamedMenuItem}, render::{ ui::{ glyph::{GlyphRendererCommand, GLYPH_HEIGHT, GLYPH_WIDTH}, layout::{Anchor, Layout, ScreenPosition, Size}, quad::{QuadRendererCommand, QuadTexture}, }, GraphicsState, }, }, common::wad::QPic, }; use chrono::Duration; // original minimum Quake resolution const MENU_WIDTH: i32 = 320; const MENU_HEIGHT: i32 = 200; const SLIDER_LEFT: u8 = 128; const SLIDER_MIDDLE: u8 = 129; const SLIDER_RIGHT: u8 = 130; const SLIDER_HANDLE: u8 = 131; const SLIDER_WIDTH: i32 = 10; #[derive(Clone, Copy, Debug)] enum Align { Left, Center, } impl Align { pub fn x_ofs(&self) -> i32 { match *self { Align::Left => -MENU_WIDTH / 2, Align::Center => 0, } } pub fn anchor(&self) -> Anchor { match *self { Align::Left => Anchor::TOP_LEFT, Align::Center => Anchor::TOP_CENTER, } } } pub struct MenuRenderer { textures: HashMap, } impl MenuRenderer { pub fn new(state: &GraphicsState, menu: &Menu) -> MenuRenderer { let mut tex_names = std::collections::HashSet::new(); tex_names.insert("gfx/qplaque.lmp".to_string()); tex_names.extend((1..=6).into_iter().map(|i| format!("gfx/menudot{}.lmp", i))); let mut menus = vec![menu]; // walk menu and collect necessary textures while let Some(m) = menus.pop() { tex_names.insert(m.view().title_path().to_string()); if let MenuBodyView::Predefined { ref path, .. } = m.view().body() { tex_names.insert(path.to_string()); } for item in m.items() { if let Item::Submenu(ref sub) = item.item() { menus.push(sub); } } } MenuRenderer { textures: tex_names .into_iter() .map(|name| { ( name.clone(), QuadTexture::from_qpic( state, &QPic::load(state.vfs().open(&name).unwrap()).unwrap(), ), ) }) .collect(), } } fn texture(&self, name: S) -> &QuadTexture where S: AsRef, { debug!("Fetch texture {}", name.as_ref()); self.textures.get(name.as_ref()).unwrap() } fn cmd_draw_quad<'state>( &self, texture: &'state QuadTexture, align: Align, x_ofs: i32, y_ofs: i32, scale: f32, quad_cmds: &mut Vec>, ) { quad_cmds.push(QuadRendererCommand { texture, layout: Layout { position: ScreenPosition::Relative { anchor: Anchor::CENTER, x_ofs: align.x_ofs() + x_ofs, y_ofs: MENU_HEIGHT / 2 + y_ofs, }, anchor: align.anchor(), size: Size::Scale { factor: scale }, }, }); } fn cmd_draw_glyph( &self, glyph_id: u8, x_ofs: i32, y_ofs: i32, scale: f32, glyph_cmds: &mut Vec, ) { glyph_cmds.push(GlyphRendererCommand::Glyph { glyph_id, position: ScreenPosition::Relative { anchor: Anchor::CENTER, x_ofs: -MENU_WIDTH / 2 + x_ofs, y_ofs: -MENU_HEIGHT / 2 + y_ofs, }, anchor: Anchor::TOP_LEFT, scale, }); } fn cmd_draw_plaque<'a>(&'a self, scale: f32, quad_cmds: &mut Vec>) { let plaque = self.texture("gfx/qplaque.lmp"); self.cmd_draw_quad(plaque, Align::Left, 16, 4, scale, quad_cmds); } fn cmd_draw_title<'a, S>( &'a self, name: S, scale: f32, quad_cmds: &mut Vec>, ) where S: AsRef, { let title = self.texture(name.as_ref()); self.cmd_draw_quad(title, Align::Center, 0, 4, scale, quad_cmds); } fn cmd_draw_body_predef<'a, S>( &'a self, name: S, cursor_pos: usize, time: Duration, scale: f32, quad_cmds: &mut Vec>, ) where S: AsRef, { let predef = self.texture(name.as_ref()); self.cmd_draw_quad(predef, Align::Left, 72, -32, scale, quad_cmds); let curs_frame = (time.num_milliseconds() / 100) % 6; let curs = self.texture(&format!("gfx/menudot{}.lmp", curs_frame + 1)); self.cmd_draw_quad( curs, Align::Left, 72 - curs.width() as i32, -32 - cursor_pos as i32 * 20, scale, quad_cmds, ); } fn cmd_draw_item_name( &self, x: i32, y: i32, name: S, scale: f32, glyph_cmds: &mut Vec, ) where S: AsRef, { glyph_cmds.push(GlyphRendererCommand::Text { text: name.as_ref().to_string(), position: ScreenPosition::Relative { anchor: Anchor::CENTER, x_ofs: -MENU_WIDTH / 2 + x - GLYPH_WIDTH as i32, y_ofs: -MENU_HEIGHT / 2 + y, }, anchor: Anchor::TOP_RIGHT, scale, }); } fn cmd_draw_item_text( &self, x: i32, y: i32, text: S, scale: f32, glyph_cmds: &mut Vec, ) where S: AsRef, { glyph_cmds.push(GlyphRendererCommand::Text { text: text.as_ref().to_string(), position: ScreenPosition::Relative { anchor: Anchor::CENTER, x_ofs: -MENU_WIDTH / 2 + x + GLYPH_WIDTH as i32, y_ofs: -MENU_HEIGHT / 2 + y, }, anchor: Anchor::TOP_LEFT, scale, }); } fn cmd_draw_slider( &self, x: i32, y: i32, pos: f32, scale: f32, glyph_cmds: &mut Vec, ) { self.cmd_draw_glyph(SLIDER_LEFT, x, y, scale, glyph_cmds); for i in 0..SLIDER_WIDTH { self.cmd_draw_glyph(SLIDER_MIDDLE, x + 8 * (i + 1), y, scale, glyph_cmds); } self.cmd_draw_glyph(SLIDER_RIGHT, x + 8 * SLIDER_WIDTH, y, scale, glyph_cmds); let handle_x = x + ((8 * (SLIDER_WIDTH - 1)) as f32 * pos) as i32; self.cmd_draw_glyph(SLIDER_HANDLE, handle_x, y, scale, glyph_cmds); } fn cmd_draw_body_dynamic( &self, items: &[NamedMenuItem], cursor_pos: usize, time: Duration, scale: f32, glyph_cmds: &mut Vec, ) { for (item_id, item) in items.iter().enumerate() { let y = MENU_HEIGHT - 32 - (GLYPH_HEIGHT * item_id) as i32; let x = 16 + 24 * GLYPH_WIDTH as i32; self.cmd_draw_item_name(x, y, item.name(), scale, glyph_cmds); match item.item() { Item::Toggle(toggle) => self.cmd_draw_item_text( x, y, if toggle.get() { "yes" } else { "no" }, scale, glyph_cmds, ), Item::Enum(e) => { self.cmd_draw_item_text(x, y, e.selected_name(), scale, glyph_cmds) } Item::Slider(slider) => { self.cmd_draw_slider(x, y, slider.position(), scale, glyph_cmds) } Item::TextField(_) => (), _ => (), } } if time.num_milliseconds() / 250 % 2 == 0 { self.cmd_draw_glyph( 141, 200, MENU_HEIGHT - 32 - 8 * cursor_pos as i32, scale, glyph_cmds, ); } } pub fn generate_commands<'a>( &'a self, menu: &Menu, time: Duration, quad_cmds: &mut Vec>, glyph_cmds: &mut Vec, ) { let active_menu = menu.active_submenu().unwrap(); let view = active_menu.view(); // TODO: use cvar let scale = 2.0; if view.draw_plaque() { self.cmd_draw_plaque(scale, quad_cmds); } self.cmd_draw_title(view.title_path(), scale, quad_cmds); let cursor_pos = match active_menu.state() { MenuState::Active { index } => index, _ => unreachable!(), }; match *view.body() { MenuBodyView::Predefined { ref path } => { self.cmd_draw_body_predef(path, cursor_pos, time, scale, quad_cmds); } MenuBodyView::Dynamic => { self.cmd_draw_body_dynamic( &active_menu.items(), cursor_pos, time, scale, glyph_cmds, ); } } } } ================================================ FILE: src/client/render/ui/mod.rs ================================================ pub mod console; pub mod glyph; pub mod hud; pub mod layout; pub mod menu; pub mod quad; use std::cell::RefCell; use crate::{ client::{ menu::Menu, render::{ ui::{ console::ConsoleRenderer, glyph::{GlyphRenderer, GlyphRendererCommand}, hud::{HudRenderer, HudState}, menu::MenuRenderer, quad::{QuadRenderer, QuadRendererCommand, QuadUniforms}, }, uniform::{self, DynamicUniformBufferBlock}, Extent2d, GraphicsState, }, }, common::{console::Console, util::any_slice_as_bytes}, }; use cgmath::{Matrix4, Vector2}; use chrono::Duration; pub fn screen_space_vertex_translate( display_w: u32, display_h: u32, pos_x: i32, pos_y: i32, ) -> Vector2 { // rescale from [0, DISPLAY_*] to [-1, 1] (NDC) Vector2::new( (pos_x * 2 - display_w as i32) as f32 / display_w as f32, (pos_y * 2 - display_h as i32) as f32 / display_h as f32, ) } pub fn screen_space_vertex_scale( display_w: u32, display_h: u32, quad_w: u32, quad_h: u32, ) -> Vector2 { Vector2::new( (quad_w * 2) as f32 / display_w as f32, (quad_h * 2) as f32 / display_h as f32, ) } pub fn screen_space_vertex_transform( display_w: u32, display_h: u32, quad_w: u32, quad_h: u32, pos_x: i32, pos_y: i32, ) -> Matrix4 { let Vector2 { x: ndc_x, y: ndc_y } = screen_space_vertex_translate(display_w, display_h, pos_x, pos_y); let Vector2 { x: scale_x, y: scale_y, } = screen_space_vertex_scale(display_w, display_h, quad_w, quad_h); Matrix4::from_translation([ndc_x, ndc_y, 0.0].into()) * Matrix4::from_nonuniform_scale(scale_x, scale_y, 1.0) } pub enum UiOverlay<'a> { Menu(&'a Menu), Console(&'a Console), } pub enum UiState<'a> { Title { overlay: UiOverlay<'a>, }, InGame { hud: HudState<'a>, overlay: Option>, }, } pub struct UiRenderer { console_renderer: ConsoleRenderer, menu_renderer: MenuRenderer, hud_renderer: HudRenderer, glyph_renderer: GlyphRenderer, quad_renderer: QuadRenderer, } impl UiRenderer { pub fn new(state: &GraphicsState, menu: &Menu) -> UiRenderer { UiRenderer { console_renderer: ConsoleRenderer::new(state), menu_renderer: MenuRenderer::new(state, menu), hud_renderer: HudRenderer::new(state), glyph_renderer: GlyphRenderer::new(state), quad_renderer: QuadRenderer::new(state), } } pub fn render_pass<'pass>( &'pass self, state: &'pass GraphicsState, pass: &mut wgpu::RenderPass<'pass>, target_size: Extent2d, time: Duration, ui_state: &UiState<'pass>, quad_commands: &'pass mut Vec>, glyph_commands: &'pass mut Vec, ) { let (hud_state, overlay) = match ui_state { UiState::Title { overlay } => (None, Some(overlay)), UiState::InGame { hud, overlay } => (Some(hud), overlay.as_ref()), }; if let Some(hstate) = hud_state { self.hud_renderer .generate_commands(hstate, time, quad_commands, glyph_commands); } if let Some(o) = overlay { match o { UiOverlay::Menu(menu) => { self.menu_renderer .generate_commands(menu, time, quad_commands, glyph_commands); } UiOverlay::Console(console) => { // TODO: take in-game console proportion as cvar let proportion = match hud_state { Some(_) => 0.33, None => 1.0, }; self.console_renderer.generate_commands( console, time, quad_commands, glyph_commands, proportion, ); } } } self.quad_renderer .record_draw(state, pass, target_size, quad_commands); self.glyph_renderer .record_draw(state, pass, target_size, glyph_commands); } } ================================================ FILE: src/client/render/ui/quad.rs ================================================ use std::{ cell::{Ref, RefCell, RefMut}, mem::size_of, num::NonZeroU64, }; use crate::{ client::render::{ ui::{ layout::{Layout, Size}, screen_space_vertex_transform, }, uniform::{self, DynamicUniformBuffer, DynamicUniformBufferBlock}, Extent2d, GraphicsState, Pipeline, TextureData, DIFFUSE_ATTACHMENT_FORMAT, }, common::{util::any_slice_as_bytes, wad::QPic}, }; use cgmath::Matrix4; pub const VERTICES: [QuadVertex; 6] = [ QuadVertex { position: [0.0, 0.0], texcoord: [0.0, 1.0], }, QuadVertex { position: [0.0, 1.0], texcoord: [0.0, 0.0], }, QuadVertex { position: [1.0, 1.0], texcoord: [1.0, 0.0], }, QuadVertex { position: [0.0, 0.0], texcoord: [0.0, 1.0], }, QuadVertex { position: [1.0, 1.0], texcoord: [1.0, 0.0], }, QuadVertex { position: [1.0, 0.0], texcoord: [1.0, 1.0], }, ]; // these type aliases are here to aid readability of e.g. size_of::() pub type Position = [f32; 2]; pub type Texcoord = [f32; 2]; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct QuadVertex { position: Position, texcoord: Texcoord, } lazy_static! { static ref VERTEX_BUFFER_ATTRIBUTES: Vec = vec![ // position wgpu::VertexAttribute { offset: 0, format: wgpu::VertexFormat::Float32x2, shader_location: 0, }, // diffuse texcoord wgpu::VertexAttribute { offset: size_of::() as u64, format: wgpu::VertexFormat::Float32x2, shader_location: 1, }, ]; } pub struct QuadPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, vertex_buffer: wgpu::Buffer, uniform_buffer: RefCell>, uniform_buffer_blocks: RefCell>>, } impl QuadPipeline { pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) -> QuadPipeline { let (pipeline, bind_group_layouts) = QuadPipeline::create(device, compiler, &[], sample_count); use wgpu::util::DeviceExt as _; let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_slice_as_bytes(&VERTICES) }, usage: wgpu::BufferUsage::VERTEX, }); let uniform_buffer = RefCell::new(DynamicUniformBuffer::new(device)); let uniform_buffer_blocks = RefCell::new(Vec::new()); QuadPipeline { pipeline, bind_group_layouts, vertex_buffer, uniform_buffer, uniform_buffer_blocks, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) { let layout_refs = self.bind_group_layouts.iter().collect::>(); self.pipeline = QuadPipeline::recreate(device, compiler, &layout_refs, sample_count); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn vertex_buffer(&self) -> &wgpu::Buffer { &self.vertex_buffer } pub fn uniform_buffer(&self) -> Ref> { self.uniform_buffer.borrow() } pub fn uniform_buffer_mut(&self) -> RefMut> { self.uniform_buffer.borrow_mut() } pub fn uniform_buffer_blocks(&self) -> Ref>> { self.uniform_buffer_blocks.borrow() } pub fn uniform_buffer_blocks_mut( &self, ) -> RefMut>> { self.uniform_buffer_blocks.borrow_mut() } } const BIND_GROUP_LAYOUT_ENTRIES: &[&[wgpu::BindGroupLayoutEntry]] = &[ &[ // sampler wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { filtering: true, comparison: false, }, count: None, }, ], &[ // texture wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: None, }, ], &[ // transform matrix // TODO: move to push constants once they're exposed in wgpu wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::all(), ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, min_binding_size: NonZeroU64::new(size_of::() as u64), }, count: None, }, ], ]; impl Pipeline for QuadPipeline { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "quad" } fn bind_group_layout_descriptors() -> Vec> { vec![ // group 0: per-frame wgpu::BindGroupLayoutDescriptor { label: Some("per-frame quad bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES[0], }, // group 1: per-texture wgpu::BindGroupLayoutDescriptor { label: Some("per-texture quad bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES[1], }, // group 2: per-quad wgpu::BindGroupLayoutDescriptor { label: Some("per-texture quad bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES[2], }, ] } fn vertex_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/quad.vert")) } fn fragment_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/quad.frag")) } fn primitive_state() -> wgpu::PrimitiveState { wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Cw, cull_mode: Some(wgpu::Face::Back), clamp_depth: false, polygon_mode: wgpu::PolygonMode::Fill, conservative: false, } } fn color_target_states() -> Vec { vec![wgpu::ColorTargetState { format: DIFFUSE_ATTACHMENT_FORMAT, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrite::ALL, }] } fn depth_stencil_state() -> Option { None } // NOTE: if the vertex format is changed, this descriptor must also be changed accordingly. fn vertex_buffer_layouts() -> Vec> { vec![wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Vertex, attributes: &VERTEX_BUFFER_ATTRIBUTES[..], }] } } #[repr(C, align(256))] #[derive(Clone, Copy, Debug)] pub struct QuadUniforms { transform: Matrix4, } pub struct QuadTexture { #[allow(dead_code)] texture: wgpu::Texture, #[allow(dead_code)] texture_view: wgpu::TextureView, bind_group: wgpu::BindGroup, width: u32, height: u32, } impl QuadTexture { pub fn from_qpic(state: &GraphicsState, qpic: &QPic) -> QuadTexture { let (diffuse_data, _) = state.palette().translate(qpic.indices()); let texture = state.create_texture( None, qpic.width(), qpic.height(), &TextureData::Diffuse(diffuse_data), ); let texture_view = texture.create_view(&Default::default()); let bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &state.quad_pipeline().bind_group_layouts()[1], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&texture_view), }], }); QuadTexture { texture, texture_view, bind_group, width: qpic.width(), height: qpic.height(), } } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } pub fn scale_width(&self, scale: f32) -> u32 { (self.width as f32 * scale) as u32 } pub fn scale_height(&self, scale: f32) -> u32 { (self.height as f32 * scale) as u32 } } /// A command which specifies how a quad should be rendered. pub struct QuadRendererCommand<'a> { /// The texture to be mapped to the quad. pub texture: &'a QuadTexture, /// The layout specifying the size and position of the quad on the screen. pub layout: Layout, } pub struct QuadRenderer { sampler_bind_group: wgpu::BindGroup, transform_bind_group: wgpu::BindGroup, } impl QuadRenderer { pub fn new(state: &GraphicsState) -> QuadRenderer { let sampler_bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("quad sampler bind group"), layout: &state.quad_pipeline().bind_group_layouts()[0], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(state.diffuse_sampler()), }], }); let transform_bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("quad transform bind group"), layout: &state.quad_pipeline().bind_group_layouts()[2], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: state.quad_pipeline().uniform_buffer().buffer(), offset: 0, size: Some(NonZeroU64::new(size_of::() as u64).unwrap()), }), }], }); QuadRenderer { sampler_bind_group, transform_bind_group, } } fn generate_uniforms<'cmds>( &self, commands: &[QuadRendererCommand<'cmds>], target_size: Extent2d, ) -> Vec { let mut uniforms = Vec::new(); for cmd in commands { let QuadRendererCommand { texture, layout: Layout { position, anchor, size, }, } = *cmd; let scale = match size { Size::Scale { factor } => factor, _ => 1.0, }; let Extent2d { width: display_width, height: display_height, } = target_size; let (screen_x, screen_y) = position.to_xy(display_width, display_height, scale); let (quad_x, quad_y) = anchor.to_xy(texture.width, texture.height); let x = screen_x - (quad_x as f32 * scale) as i32; let y = screen_y - (quad_y as f32 * scale) as i32; let (quad_width, quad_height) = size.to_wh(texture.width, texture.height, display_width, display_height); uniforms.push(QuadUniforms { transform: screen_space_vertex_transform( display_width, display_height, quad_width, quad_height, x, y, ), }); } uniforms } pub fn record_draw<'pass, 'cmds>( &'pass self, state: &'pass GraphicsState, pass: &mut wgpu::RenderPass<'pass>, target_size: Extent2d, commands: &'pass [QuadRendererCommand<'pass>], ) { // update uniform buffer let uniforms = self.generate_uniforms(commands, target_size); uniform::clear_and_rewrite( state.queue(), &mut state.quad_pipeline().uniform_buffer_mut(), &mut state.quad_pipeline().uniform_buffer_blocks_mut(), &uniforms, ); pass.set_pipeline(state.quad_pipeline().pipeline()); pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); pass.set_bind_group(0, &self.sampler_bind_group, &[]); for (cmd, block) in commands .iter() .zip(state.quad_pipeline().uniform_buffer_blocks().iter()) { pass.set_bind_group(1, &cmd.texture.bind_group, &[]); pass.set_bind_group(2, &self.transform_bind_group, &[block.offset()]); pass.draw(0..6, 0..1); } } } ================================================ FILE: src/client/render/uniform.rs ================================================ use std::{ cell::{Cell, RefCell}, marker::PhantomData, mem::{align_of, size_of}, rc::Rc, }; use crate::common::util::{any_as_bytes, Pod}; use failure::Error; // minimum limit is 16384: // https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#limits-maxUniformBufferRange // but https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxUniformBufferRange&platform=windows // indicates that a limit of 65536 or higher is more common const DYNAMIC_UNIFORM_BUFFER_SIZE: wgpu::BufferAddress = 65536; // https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#limits-minUniformBufferOffsetAlignment pub const DYNAMIC_UNIFORM_BUFFER_ALIGNMENT: usize = 256; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct UniformBool { value: u32, } impl UniformBool { pub fn new(value: bool) -> UniformBool { UniformBool { value: value as u32, } } } // uniform float array elements are aligned as if they were vec4s #[repr(C, align(16))] #[derive(Clone, Copy, Debug)] pub struct UniformArrayFloat { value: f32, } impl UniformArrayFloat { pub fn new(value: f32) -> UniformArrayFloat { UniformArrayFloat { value } } } /// A handle to a dynamic uniform buffer on the GPU. /// /// Allows allocation and updating of individual blocks of memory. pub struct DynamicUniformBuffer where T: Pod, { // keeps track of how many blocks are allocated so we know whether we can // clear the buffer or not _rc: RefCell>, // represents the data in the buffer, which we don't actually own _phantom: PhantomData, inner: wgpu::Buffer, allocated: Cell, update_buf: Vec, } impl DynamicUniformBuffer where T: Pod, { pub fn new<'b>(device: &'b wgpu::Device) -> DynamicUniformBuffer { // TODO: is this something we can enforce at compile time? assert!(align_of::() % DYNAMIC_UNIFORM_BUFFER_ALIGNMENT == 0); let inner = device.create_buffer(&wgpu::BufferDescriptor { label: Some("dynamic uniform buffer"), size: DYNAMIC_UNIFORM_BUFFER_SIZE, usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, mapped_at_creation: false, }); let mut update_buf = Vec::with_capacity(DYNAMIC_UNIFORM_BUFFER_SIZE as usize); update_buf.resize(DYNAMIC_UNIFORM_BUFFER_SIZE as usize, 0); DynamicUniformBuffer { _rc: RefCell::new(Rc::new(())), _phantom: PhantomData, inner, allocated: Cell::new(0), update_buf, } } pub fn block_size(&self) -> wgpu::BufferSize { std::num::NonZeroU64::new( ((DYNAMIC_UNIFORM_BUFFER_ALIGNMENT / 8).max(size_of::())) as u64, ) .unwrap() } /// Allocates a block of memory in this dynamic uniform buffer with the /// specified initial value. #[must_use] pub fn allocate(&mut self, val: T) -> DynamicUniformBufferBlock { let allocated = self.allocated.get(); let size = self.block_size().get(); trace!( "Allocating dynamic uniform block (allocated: {})", allocated ); if allocated + size > DYNAMIC_UNIFORM_BUFFER_SIZE { panic!( "Not enough space to allocate {} bytes in dynamic uniform buffer", size ); } let addr = allocated; self.allocated.set(allocated + size); let block = DynamicUniformBufferBlock { _rc: self._rc.borrow().clone(), _phantom: PhantomData, addr, }; self.write_block(&block, val); block } pub fn write_block(&mut self, block: &DynamicUniformBufferBlock, val: T) { let start = block.addr as usize; let end = start + self.block_size().get() as usize; let slice = &mut self.update_buf[start..end]; slice.copy_from_slice(unsafe { any_as_bytes(&val) }); } /// Removes all allocations from the underlying buffer. /// /// Returns an error if the buffer is currently mapped or there are /// outstanding allocated blocks. pub fn clear(&self) -> Result<(), Error> { let out = self._rc.replace(Rc::new(())); match Rc::try_unwrap(out) { // no outstanding blocks Ok(()) => { self.allocated.set(0); Ok(()) } Err(rc) => { let _ = self._rc.replace(rc); bail!("Can't clear uniform buffer: there are outstanding references to allocated blocks."); } } } pub fn flush(&self, queue: &wgpu::Queue) { queue.write_buffer(&self.inner, 0, &self.update_buf); } pub fn buffer(&self) -> &wgpu::Buffer { &self.inner } } /// An address into a dynamic uniform buffer. #[derive(Debug)] pub struct DynamicUniformBufferBlock { _rc: Rc<()>, _phantom: PhantomData, addr: wgpu::BufferAddress, } impl DynamicUniformBufferBlock { pub fn offset(&self) -> wgpu::DynamicOffset { self.addr as wgpu::DynamicOffset } } pub fn clear_and_rewrite( queue: &wgpu::Queue, buffer: &mut DynamicUniformBuffer, blocks: &mut Vec>, uniforms: &[T], ) where T: Pod, { blocks.clear(); buffer.clear().unwrap(); for (uni_id, uni) in uniforms.iter().enumerate() { if uni_id >= blocks.len() { let block = buffer.allocate(*uni); blocks.push(block); } else { buffer.write_block(&blocks[uni_id], *uni); } } buffer.flush(queue); } ================================================ FILE: src/client/render/warp.rs ================================================ use std::cmp::Ordering; use crate::common::math; use cgmath::{InnerSpace, Vector2, Vector3}; // TODO: make this a cvar const SUBDIVIDE_SIZE: f32 = 32.0; /// Subdivide the given polygon on a grid. /// /// The algorithm is described as follows: /// Given a polygon *P*, /// 1. Calculate the extents *P*min, *P*max and the midpoint *P*mid of *P*. /// 1. Calculate the distance vector *D*i for each *P*i. /// 1. For each axis *A* = [X, Y, Z]: /// 1. If the distance between either *P*minA or /// *P*maxA and *P*midA is less than 8, continue to /// the next axis. /// 1. For each vertex *v*... /// TODO... pub fn subdivide(verts: Vec>) -> Vec> { let mut out = Vec::new(); subdivide_impl(verts, &mut out); out } fn subdivide_impl(mut verts: Vec>, output: &mut Vec>) { let (min, max) = math::bounds(&verts); let mut front = Vec::new(); let mut back = Vec::new(); // subdivide polygon along each axis in order for ax in 0..3 { // find the midpoint of the polygon bounds let mid = { let m = (min[ax] + max[ax]) / 2.0; SUBDIVIDE_SIZE * (m / SUBDIVIDE_SIZE).round() }; if max[ax] - mid < 8.0 || mid - min[ax] < 8.0 { // this component doesn't need to be subdivided further. // if no components need to be subdivided further, this breaks the loop. continue; } // collect the distances of each vertex from the midpoint let mut dist: Vec = verts.iter().map(|v| (*v)[ax] - mid).collect(); dist.push(dist[0]); // duplicate first vertex verts.push(verts[0]); for (vi, v) in (&verts[..verts.len() - 1]).iter().enumerate() { // sort vertices to front and back of axis let cmp = dist[vi].partial_cmp(&0.0).unwrap(); match cmp { Ordering::Less => { back.push(*v); } Ordering::Equal => { // if this vertex is on the axis, split it in two front.push(*v); back.push(*v); continue; } Ordering::Greater => { front.push(*v); } } if dist[vi + 1] != 0.0 && cmp != dist[vi + 1].partial_cmp(&0.0).unwrap() { // segment crosses the axis, add a vertex at the intercept let ratio = dist[vi] / (dist[vi] - dist[vi + 1]); let intercept = v + ratio * (verts[vi + 1] - v); front.push(intercept); back.push(intercept); } } subdivide_impl(front, output); subdivide_impl(back, output); return; } // polygon is smaller than SUBDIVIDE_SIZE along all three axes assert!(verts.len() >= 3); let v1 = verts[0]; let mut v2 = verts[1]; for v3 in &verts[2..] { output.push(v1); output.push(v2); output.push(*v3); v2 = *v3; } } ================================================ FILE: src/client/render/world/alias.rs ================================================ use std::{mem::size_of, ops::Range}; use crate::{ client::render::{ world::{BindGroupLayoutId, WorldPipelineBase}, GraphicsState, Pipeline, TextureData, }, common::{ mdl::{self, AliasModel}, util::any_slice_as_bytes, }, }; use cgmath::{InnerSpace as _, Matrix4, Vector3, Zero as _}; use chrono::Duration; use failure::Error; pub struct AliasPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, } impl AliasPipeline { pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, world_bind_group_layouts: &[wgpu::BindGroupLayout], sample_count: u32, ) -> AliasPipeline { let (pipeline, bind_group_layouts) = AliasPipeline::create(device, compiler, world_bind_group_layouts, sample_count); AliasPipeline { pipeline, bind_group_layouts, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, world_bind_group_layouts: &[wgpu::BindGroupLayout], sample_count: u32, ) { let layout_refs: Vec<_> = world_bind_group_layouts .iter() .chain(self.bind_group_layouts.iter()) .collect(); self.pipeline = AliasPipeline::recreate(device, compiler, &layout_refs, sample_count); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } } #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct VertexPushConstants { pub transform: Matrix4, pub model_view: Matrix4, } lazy_static! { static ref VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![ // frame 0 position 0 => Float32x3, // frame 1 position // 1 => Float32x3, // normal 2 => Float32x3, // texcoord 3 => Float32x2, ]; } impl Pipeline for AliasPipeline { type VertexPushConstants = VertexPushConstants; type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "alias" } fn vertex_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/alias.vert")) } fn fragment_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/alias.frag")) } fn bind_group_layout_descriptors() -> Vec> { vec![ // group 2: updated per-texture wgpu::BindGroupLayoutDescriptor { label: Some("brush per-texture chain bind group"), entries: &[ // diffuse texture, updated once per face wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: None, }, ], }, ] } fn primitive_state() -> wgpu::PrimitiveState { WorldPipelineBase::primitive_state() } fn color_target_states() -> Vec { WorldPipelineBase::color_target_states() } fn depth_stencil_state() -> Option { WorldPipelineBase::depth_stencil_state() } // NOTE: if the vertex format is changed, this descriptor must also be changed accordingly. fn vertex_buffer_layouts() -> Vec> { vec![wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Vertex, attributes: &VERTEX_ATTRIBUTES[..], }] } } // these type aliases are here to aid readability of e.g. size_of::() type Position = [f32; 3]; type Normal = [f32; 3]; type DiffuseTexcoord = [f32; 2]; #[repr(C)] #[derive(Clone, Copy, Debug)] struct AliasVertex { position: Position, normal: Normal, diffuse_texcoord: DiffuseTexcoord, } enum Keyframe { Static { vertex_range: Range, }, Animated { vertex_ranges: Vec>, total_duration: Duration, durations: Vec, }, } impl Keyframe { fn animate(&self, time: Duration) -> Range { match self { Keyframe::Static { vertex_range } => vertex_range.clone(), Keyframe::Animated { vertex_ranges, total_duration, durations, } => { let mut time_ms = time.num_milliseconds() % total_duration.num_milliseconds(); for (frame_id, frame_duration) in durations.iter().enumerate() { time_ms -= frame_duration.num_milliseconds(); if time_ms <= 0 { return vertex_ranges[frame_id].clone(); } } unreachable!() } } } } enum Texture { Static { diffuse_texture: wgpu::Texture, diffuse_view: wgpu::TextureView, bind_group: wgpu::BindGroup, }, Animated { diffuse_textures: Vec, diffuse_views: Vec, bind_groups: Vec, total_duration: Duration, durations: Vec, }, } impl Texture { fn animate(&self, time: Duration) -> &wgpu::BindGroup { match self { Texture::Static { ref bind_group, .. } => bind_group, Texture::Animated { diffuse_textures, diffuse_views, bind_groups, total_duration, durations, } => { let mut time_ms = time.num_milliseconds() % total_duration.num_milliseconds(); for (frame_id, frame_duration) in durations.iter().enumerate() { time_ms -= frame_duration.num_milliseconds(); if time_ms <= 0 { return &bind_groups[frame_id]; } } unreachable!() } } } } pub struct AliasRenderer { keyframes: Vec, textures: Vec, vertex_buffer: wgpu::Buffer, } impl AliasRenderer { pub fn new(state: &GraphicsState, alias_model: &AliasModel) -> Result { let mut vertices = Vec::new(); let mut keyframes = Vec::new(); let w = alias_model.texture_width(); let h = alias_model.texture_height(); for keyframe in alias_model.keyframes() { match *keyframe { mdl::Keyframe::Static(ref static_keyframe) => { let vertex_start = vertices.len() as u32; for polygon in alias_model.polygons() { let mut tri = [Vector3::zero(); 3]; let mut texcoords = [[0.0; 2]; 3]; for (i, index) in polygon.indices().iter().enumerate() { tri[i] = static_keyframe.vertices()[*index as usize].into(); let texcoord = &alias_model.texcoords()[*index as usize]; let s = if !polygon.faces_front() && texcoord.is_on_seam() { (texcoord.s() + w / 2) as f32 + 0.5 } else { texcoord.s() as f32 + 0.5 } / w as f32; let t = (texcoord.t() as f32 + 0.5) / h as f32; texcoords[i] = [s, t]; } let normal = (tri[0] - tri[1]).cross(tri[2] - tri[1]).normalize(); for i in 0..3 { vertices.push(AliasVertex { position: tri[i].into(), normal: normal.into(), diffuse_texcoord: texcoords[i], }); } } let vertex_end = vertices.len() as u32; keyframes.push(Keyframe::Static { vertex_range: vertex_start..vertex_end, }); } mdl::Keyframe::Animated(ref kf) => { let mut durations = Vec::new(); let mut vertex_ranges = Vec::new(); for frame in kf.frames() { durations.push(frame.duration()); let vertex_start = vertices.len() as u32; for polygon in alias_model.polygons() { let mut tri = [Vector3::zero(); 3]; let mut texcoords = [[0.0; 2]; 3]; for (i, index) in polygon.indices().iter().enumerate() { tri[i] = frame.vertices()[*index as usize].into(); let texcoord = &alias_model.texcoords()[*index as usize]; let s = if !polygon.faces_front() && texcoord.is_on_seam() { (texcoord.s() + w / 2) as f32 + 0.5 } else { texcoord.s() as f32 + 0.5 } / w as f32; let t = (texcoord.t() as f32 + 0.5) / h as f32; texcoords[i] = [s, t]; } let normal = (tri[0] - tri[1]).cross(tri[2] - tri[1]).normalize(); for i in 0..3 { vertices.push(AliasVertex { position: tri[i].into(), normal: normal.into(), diffuse_texcoord: texcoords[i], }); } } let vertex_end = vertices.len() as u32; vertex_ranges.push(vertex_start..vertex_end); } let total_duration = durations.iter().fold(Duration::zero(), |s, d| s + *d); keyframes.push(Keyframe::Animated { vertex_ranges, durations, total_duration, }); } } } use wgpu::util::DeviceExt as _; let vertex_buffer = state .device() .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_slice_as_bytes(vertices.as_slice()) }, usage: wgpu::BufferUsage::VERTEX, }); let mut textures = Vec::new(); for texture in alias_model.textures() { match *texture { mdl::Texture::Static(ref tex) => { let (diffuse_data, _fullbright_data) = state.palette.translate(tex.indices()); let diffuse_texture = state.create_texture(None, w, h, &TextureData::Diffuse(diffuse_data)); let diffuse_view = diffuse_texture.create_view(&Default::default()); let bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: None, // TODO: per-pipeline bind group layout ids layout: &state.alias_pipeline().bind_group_layouts() [BindGroupLayoutId::PerTexture as usize - 2], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&diffuse_view), }], }); textures.push(Texture::Static { diffuse_texture, diffuse_view, bind_group, }); } mdl::Texture::Animated(ref tex) => { let mut total_duration = Duration::zero(); let mut durations = Vec::new(); let mut diffuse_textures = Vec::new(); let mut diffuse_views = Vec::new(); let mut bind_groups = Vec::new(); for frame in tex.frames() { total_duration = total_duration + frame.duration(); durations.push(frame.duration()); let (diffuse_data, _fullbright_data) = state.palette.translate(frame.indices()); let diffuse_texture = state.create_texture(None, w, h, &TextureData::Diffuse(diffuse_data)); let diffuse_view = diffuse_texture.create_view(&Default::default()); let bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &state.alias_pipeline().bind_group_layouts() [BindGroupLayoutId::PerTexture as usize - 2], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&diffuse_view), }], }); diffuse_textures.push(diffuse_texture); diffuse_views.push(diffuse_view); bind_groups.push(bind_group); } textures.push(Texture::Animated { diffuse_textures, diffuse_views, bind_groups, total_duration, durations, }); } } } Ok(AliasRenderer { keyframes, textures, vertex_buffer, }) } pub fn record_draw<'a>( &'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>, time: Duration, keyframe_id: usize, texture_id: usize, ) { pass.set_pipeline(state.alias_pipeline().pipeline()); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); pass.set_bind_group( BindGroupLayoutId::PerTexture as u32, self.textures[texture_id].animate(time), &[], ); pass.draw(self.keyframes[keyframe_id].animate(time), 0..1) } } ================================================ FILE: src/client/render/world/brush.rs ================================================ // Copyright © 2020 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::{ borrow::Cow, cell::{Cell, RefCell}, collections::HashMap, mem::size_of, num::NonZeroU32, ops::Range, rc::Rc, }; use crate::{ client::render::{ pipeline::PushConstantUpdate, warp, world::{BindGroupLayoutId, WorldPipelineBase}, Camera, GraphicsState, LightmapData, Pipeline, TextureData, }, common::{ bsp::{ self, BspData, BspFace, BspLeaf, BspModel, BspTexInfo, BspTexture, BspTextureKind, BspTextureMipmap, }, math, util::any_slice_as_bytes, }, }; use bumpalo::Bump; use cgmath::{InnerSpace as _, Matrix4, Vector3}; use chrono::Duration; use failure::Error; pub struct BrushPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, } impl BrushPipeline { pub fn new( device: &wgpu::Device, queue: &wgpu::Queue, compiler: &mut shaderc::Compiler, world_bind_group_layouts: &[wgpu::BindGroupLayout], sample_count: u32, ) -> BrushPipeline { let (pipeline, bind_group_layouts) = BrushPipeline::create(device, compiler, world_bind_group_layouts, sample_count); BrushPipeline { pipeline, // TODO: pick a starting capacity bind_group_layouts, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, world_bind_group_layouts: &[wgpu::BindGroupLayout], sample_count: u32, ) { let layout_refs: Vec<_> = world_bind_group_layouts .iter() .chain(self.bind_group_layouts.iter()) .collect(); self.pipeline = BrushPipeline::recreate(device, compiler, &layout_refs, sample_count); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn bind_group_layout(&self, id: BindGroupLayoutId) -> &wgpu::BindGroupLayout { assert!(id as usize >= BindGroupLayoutId::PerTexture as usize); &self.bind_group_layouts[id as usize - BindGroupLayoutId::PerTexture as usize] } } #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct VertexPushConstants { pub transform: Matrix4, pub model_view: Matrix4, } #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct SharedPushConstants { pub texture_kind: u32, } const BIND_GROUP_LAYOUT_ENTRIES: &[&[wgpu::BindGroupLayoutEntry]] = &[ &[ // diffuse texture, updated once per face wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: None, }, // fullbright texture wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: None, }, ], &[ // lightmap texture array wgpu::BindGroupLayoutEntry { count: NonZeroU32::new(4), binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, }, ], ]; lazy_static! { static ref VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 5] = wgpu::vertex_attr_array![ // position 0 => Float32x3, // normal 1 => Float32x3, // diffuse texcoord 2 => Float32x2, // lightmap texcoord 3 => Float32x2, // lightmap animation ids 4 => Uint8x4, ]; } impl Pipeline for BrushPipeline { type VertexPushConstants = VertexPushConstants; type SharedPushConstants = SharedPushConstants; type FragmentPushConstants = (); fn name() -> &'static str { "brush" } fn vertex_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/brush.vert")) } fn fragment_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/brush.frag")) } // NOTE: if any of the binding indices are changed, they must also be changed in // the corresponding shaders and the BindGroupLayout generation functions. fn bind_group_layout_descriptors() -> Vec> { vec![ // group 2: updated per-texture wgpu::BindGroupLayoutDescriptor { label: Some("brush per-texture bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES[0], }, // group 3: updated per-face wgpu::BindGroupLayoutDescriptor { label: Some("brush per-face bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES[1], }, ] } fn primitive_state() -> wgpu::PrimitiveState { WorldPipelineBase::primitive_state() } fn color_target_states() -> Vec { WorldPipelineBase::color_target_states() } fn depth_stencil_state() -> Option { WorldPipelineBase::depth_stencil_state() } // NOTE: if the vertex format is changed, this descriptor must also be changed accordingly. fn vertex_buffer_layouts() -> Vec> { vec![wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Vertex, attributes: &VERTEX_ATTRIBUTES[..], }] } } fn calculate_lightmap_texcoords( position: Vector3, face: &BspFace, texinfo: &BspTexInfo, ) -> [f32; 2] { let mut s = texinfo.s_vector.dot(position) + texinfo.s_offset; s -= (face.texture_mins[0] as f32 / 16.0).floor() * 16.0; s += 0.5; s /= face.extents[0] as f32; let mut t = texinfo.t_vector.dot(position) + texinfo.t_offset; t -= (face.texture_mins[1] as f32 / 16.0).floor() * 16.0; t += 0.5; t /= face.extents[1] as f32; [s, t] } type Position = [f32; 3]; type Normal = [f32; 3]; type DiffuseTexcoord = [f32; 2]; type LightmapTexcoord = [f32; 2]; type LightmapAnim = [u8; 4]; #[repr(C)] #[derive(Clone, Copy, Debug)] struct BrushVertex { position: Position, normal: Normal, diffuse_texcoord: DiffuseTexcoord, lightmap_texcoord: LightmapTexcoord, lightmap_anim: LightmapAnim, } #[repr(u32)] #[derive(Clone, Copy, Debug)] pub enum TextureKind { Normal = 0, Warp = 1, Sky = 2, } /// A single frame of a brush texture. pub struct BrushTextureFrame { bind_group_id: usize, diffuse: wgpu::Texture, fullbright: wgpu::Texture, diffuse_view: wgpu::TextureView, fullbright_view: wgpu::TextureView, kind: TextureKind, } /// A brush texture. pub enum BrushTexture { /// A brush texture with a single frame. Static(BrushTextureFrame), /// A brush texture with multiple frames. /// /// Animated brush textures advance one frame every 200 milliseconds, i.e., /// they have a framerate of 5 fps. Animated { primary: Vec, alternate: Option>, }, } impl BrushTexture { fn kind(&self) -> TextureKind { match self { BrushTexture::Static(ref frame) => frame.kind, BrushTexture::Animated { ref primary, .. } => primary[0].kind, } } } #[derive(Debug)] struct BrushFace { vertices: Range, min: Vector3, max: Vector3, texture_id: usize, lightmap_ids: Vec, light_styles: [u8; 4], /// Indicates whether the face should be drawn this frame. /// /// This is set to false by default, and will be set to true if the model is /// a worldmodel and the containing leaf is in the PVS. If the model is not /// a worldmodel, this flag is ignored. draw_flag: Cell, } struct BrushLeaf { facelist_ids: Range, } impl std::convert::From for BrushLeaf where B: std::borrow::Borrow, { fn from(bsp_leaf: B) -> Self { let bsp_leaf = bsp_leaf.borrow(); BrushLeaf { facelist_ids: bsp_leaf.facelist_id..bsp_leaf.facelist_id + bsp_leaf.facelist_count, } } } pub struct BrushRendererBuilder { bsp_data: Rc, face_range: Range, leaves: Option>, per_texture_bind_groups: RefCell>, per_face_bind_groups: Vec, vertices: Vec, faces: Vec, texture_chains: HashMap>, textures: Vec, lightmaps: Vec, //lightmap_views: Vec, } impl BrushRendererBuilder { pub fn new(bsp_model: &BspModel, worldmodel: bool) -> BrushRendererBuilder { BrushRendererBuilder { bsp_data: bsp_model.bsp_data().clone(), face_range: bsp_model.face_id..bsp_model.face_id + bsp_model.face_count, leaves: if worldmodel { Some( bsp_model .iter_leaves() .map(|leaf| BrushLeaf::from(leaf)) .collect(), ) } else { None }, per_texture_bind_groups: RefCell::new(Vec::new()), per_face_bind_groups: Vec::new(), vertices: Vec::new(), faces: Vec::new(), texture_chains: HashMap::new(), textures: Vec::new(), lightmaps: Vec::new(), //lightmap_views: Vec::new(), } } fn create_face(&mut self, state: &GraphicsState, face_id: usize) -> BrushFace { let face = &self.bsp_data.faces()[face_id]; let face_vert_id = self.vertices.len(); let texinfo = &self.bsp_data.texinfo()[face.texinfo_id]; let tex = &self.bsp_data.textures()[texinfo.tex_id]; let mut min = Vector3::new(f32::INFINITY, f32::INFINITY, f32::INFINITY); let mut max = Vector3::new(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY); let no_collinear = math::remove_collinear(self.bsp_data.face_iter_vertices(face_id).collect()); for vert in no_collinear.iter() { for component in 0..3 { min[component] = min[component].min(vert[component]); max[component] = max[component].max(vert[component]); } } if tex.name().starts_with("*") { // tessellate the surface so we can do texcoord warping let verts = warp::subdivide(no_collinear); let normal = (verts[0] - verts[1]).cross(verts[2] - verts[1]).normalize(); for vert in verts.into_iter() { self.vertices.push(BrushVertex { position: vert.into(), normal: normal.into(), diffuse_texcoord: [ ((vert.dot(texinfo.s_vector) + texinfo.s_offset) / tex.width() as f32), ((vert.dot(texinfo.t_vector) + texinfo.t_offset) / tex.height() as f32), ], lightmap_texcoord: calculate_lightmap_texcoords(vert.into(), face, texinfo), lightmap_anim: face.light_styles, }) } } else { // expand the vertices into a triangle list. // the vertices are guaranteed to be in valid triangle fan order (that's // how GLQuake renders them) so we expand from triangle fan to triangle // list order. // // v1 is the base vertex, so it remains constant. // v2 takes the previous value of v3. // v3 is the newest vertex. let verts = no_collinear; let normal = (verts[0] - verts[1]).cross(verts[2] - verts[1]).normalize(); let mut vert_iter = verts.into_iter(); let v1 = vert_iter.next().unwrap(); let mut v2 = vert_iter.next().unwrap(); for v3 in vert_iter { let tri = &[v1, v2, v3]; // skip collinear points for vert in tri.iter() { self.vertices.push(BrushVertex { position: (*vert).into(), normal: normal.into(), diffuse_texcoord: [ ((vert.dot(texinfo.s_vector) + texinfo.s_offset) / tex.width() as f32), ((vert.dot(texinfo.t_vector) + texinfo.t_offset) / tex.height() as f32), ], lightmap_texcoord: calculate_lightmap_texcoords( (*vert).into(), face, texinfo, ), lightmap_anim: face.light_styles, }); } v2 = v3; } } // build the lightmaps let lightmaps = if !texinfo.special { self.bsp_data.face_lightmaps(face_id) } else { Vec::new() }; let mut lightmap_ids = Vec::new(); for lightmap in lightmaps { let lightmap_data = TextureData::Lightmap(LightmapData { lightmap: Cow::Borrowed(lightmap.data()), }); let texture = state.create_texture(None, lightmap.width(), lightmap.height(), &lightmap_data); let id = self.lightmaps.len(); self.lightmaps.push(texture); //self.lightmap_views //.push(self.lightmaps[id].create_view(&Default::default())); lightmap_ids.push(id); } BrushFace { vertices: face_vert_id as u32..self.vertices.len() as u32, min, max, texture_id: texinfo.tex_id as usize, lightmap_ids, light_styles: face.light_styles, draw_flag: Cell::new(true), } } fn create_per_texture_bind_group( &self, state: &GraphicsState, tex: &BrushTextureFrame, ) -> wgpu::BindGroup { let layout = &state .brush_pipeline() .bind_group_layout(BindGroupLayoutId::PerTexture); let desc = wgpu::BindGroupDescriptor { label: Some("per-texture bind group"), layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&tex.diffuse_view), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&tex.fullbright_view), }, ], }; state.device().create_bind_group(&desc) } fn create_per_face_bind_group(&self, state: &GraphicsState, face_id: usize) -> wgpu::BindGroup { let mut lightmap_views: Vec<_> = self.faces[face_id] .lightmap_ids .iter() .map(|id| self.lightmaps[*id].create_view(&Default::default())) .collect(); lightmap_views.resize_with(4, || { state.default_lightmap().create_view(&Default::default()) }); let lightmap_view_refs = lightmap_views.iter().collect::>(); let layout = &state .brush_pipeline() .bind_group_layout(BindGroupLayoutId::PerFace); let desc = wgpu::BindGroupDescriptor { label: Some("per-face bind group"), layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureViewArray(&lightmap_view_refs[..]), }], }; state.device().create_bind_group(&desc) } fn create_brush_texture_frame( &self, state: &GraphicsState, mipmap: &[u8], width: u32, height: u32, name: S, ) -> BrushTextureFrame where S: AsRef, { let name = name.as_ref(); let (diffuse_data, fullbright_data) = state.palette().translate(mipmap); let diffuse = state.create_texture(None, width, height, &TextureData::Diffuse(diffuse_data)); let fullbright = state.create_texture( None, width, height, &TextureData::Fullbright(fullbright_data), ); let diffuse_view = diffuse.create_view(&Default::default()); let fullbright_view = fullbright.create_view(&Default::default()); let kind = if name.starts_with("sky") { TextureKind::Sky } else if name.starts_with("*") { TextureKind::Warp } else { TextureKind::Normal }; let mut frame = BrushTextureFrame { bind_group_id: 0, diffuse, fullbright, diffuse_view, fullbright_view, kind, }; // generate texture bind group let per_texture_bind_group = self.create_per_texture_bind_group(state, &frame); let bind_group_id = self.per_texture_bind_groups.borrow().len(); self.per_texture_bind_groups .borrow_mut() .push(per_texture_bind_group); frame.bind_group_id = bind_group_id; frame } pub fn create_brush_texture(&self, state: &GraphicsState, tex: &BspTexture) -> BrushTexture { // TODO: upload mipmaps let (width, height) = tex.dimensions(); match tex.kind() { // sequence animated textures BspTextureKind::Animated { primary, alternate } => { let primary_frames: Vec<_> = primary .iter() .map(|f| { self.create_brush_texture_frame( state, f.mipmap(BspTextureMipmap::Full), width, height, tex.name(), ) }) .collect(); let alternate_frames: Option> = alternate.as_ref().map(|a| { a.iter() .map(|f| { self.create_brush_texture_frame( state, f.mipmap(BspTextureMipmap::Full), width, height, tex.name(), ) }) .collect() }); BrushTexture::Animated { primary: primary_frames, alternate: alternate_frames, } } BspTextureKind::Static(bsp_tex) => { BrushTexture::Static(self.create_brush_texture_frame( state, bsp_tex.mipmap(BspTextureMipmap::Full), tex.width(), tex.height(), tex.name(), )) } } } pub fn build(mut self, state: &GraphicsState) -> Result { // create the diffuse and fullbright textures for tex in self.bsp_data.textures().iter() { self.textures.push(self.create_brush_texture(state, tex)); } // generate faces, vertices and lightmaps // bsp_face_id is the id of the face in the bsp data // face_id is the new id of the face in the renderer for bsp_face_id in self.face_range.start..self.face_range.end { let face_id = self.faces.len(); let face = self.create_face(state, bsp_face_id); self.faces.push(face); let face_tex_id = self.faces[face_id].texture_id; // update the corresponding texture chain self.texture_chains .entry(face_tex_id) .or_insert(Vec::new()) .push(face_id); // generate face bind group let per_face_bind_group = self.create_per_face_bind_group(state, face_id); self.per_face_bind_groups.push(per_face_bind_group); } use wgpu::util::DeviceExt as _; let vertex_buffer = state .device() .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_slice_as_bytes(self.vertices.as_slice()) }, usage: wgpu::BufferUsage::VERTEX, }); Ok(BrushRenderer { bsp_data: self.bsp_data, vertex_buffer, leaves: self.leaves, per_texture_bind_groups: self.per_texture_bind_groups.into_inner(), per_face_bind_groups: self.per_face_bind_groups, texture_chains: self.texture_chains, faces: self.faces, textures: self.textures, lightmaps: self.lightmaps, //lightmap_views: self.lightmap_views, }) } } pub struct BrushRenderer { bsp_data: Rc, leaves: Option>, vertex_buffer: wgpu::Buffer, per_texture_bind_groups: Vec, per_face_bind_groups: Vec, // faces are grouped by texture to reduce the number of texture rebinds // texture_chains maps texture ids to face ids texture_chains: HashMap>, faces: Vec, textures: Vec, lightmaps: Vec, //lightmap_views: Vec, } impl BrushRenderer { /// Record the draw commands for this brush model to the given `wgpu::RenderPass`. pub fn record_draw<'a>( &'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>, bump: &'a Bump, time: Duration, camera: &Camera, frame_id: usize, ) { pass.set_pipeline(state.brush_pipeline().pipeline()); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); // if this is a worldmodel, mark faces to be drawn if let Some(ref leaves) = self.leaves { let pvs = self .bsp_data .get_pvs(self.bsp_data.find_leaf(camera.origin), leaves.len()); // only draw faces in pvs for leaf_id in pvs { for facelist_id in leaves[leaf_id].facelist_ids.clone() { let face = &self.faces[self.bsp_data.facelist()[facelist_id]]; // TODO: frustum culling face.draw_flag.set(true); } } } for (tex_id, face_ids) in self.texture_chains.iter() { use PushConstantUpdate::*; BrushPipeline::set_push_constants( pass, Retain, Update(bump.alloc(SharedPushConstants { texture_kind: self.textures[*tex_id].kind() as u32, })), Retain, ); let bind_group_id = match &self.textures[*tex_id] { BrushTexture::Static(ref frame) => frame.bind_group_id, BrushTexture::Animated { primary, alternate } => { // if frame is not zero and this texture has an alternate // animation, use it let anim = if frame_id == 0 { primary } else if let Some(a) = alternate { a } else { primary }; let time_ms = time.num_milliseconds(); let total_ms = (bsp::frame_duration() * anim.len() as i32).num_milliseconds(); let anim_ms = if total_ms == 0 { 0 } else { time_ms % total_ms }; anim[(anim_ms / bsp::frame_duration().num_milliseconds()) as usize] .bind_group_id } }; pass.set_bind_group( BindGroupLayoutId::PerTexture as u32, &self.per_texture_bind_groups[bind_group_id], &[], ); for face_id in face_ids.iter() { let face = &self.faces[*face_id]; // only skip the face if we have visibility data but it's not marked if self.leaves.is_some() && !face.draw_flag.replace(false) { continue; } pass.set_bind_group( BindGroupLayoutId::PerFace as u32, &self.per_face_bind_groups[*face_id], &[], ); pass.draw(face.vertices.clone(), 0..1); } } } } ================================================ FILE: src/client/render/world/deferred.rs ================================================ use std::{mem::size_of, num::NonZeroU64}; use cgmath::{Matrix4, SquareMatrix as _, Vector3, Zero as _}; use crate::{ client::{ entity::MAX_LIGHTS, render::{pipeline::Pipeline, ui::quad::QuadPipeline, GraphicsState}, }, common::util::any_as_bytes, }; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct PointLight { pub origin: Vector3, pub radius: f32, } #[repr(C, align(256))] #[derive(Clone, Copy, Debug)] pub struct DeferredUniforms { pub inv_projection: [[f32; 4]; 4], pub light_count: u32, pub _pad: [u32; 3], pub lights: [PointLight; MAX_LIGHTS], } pub struct DeferredPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, uniform_buffer: wgpu::Buffer, } impl DeferredPipeline { pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) -> DeferredPipeline { let (pipeline, bind_group_layouts) = DeferredPipeline::create(device, compiler, &[], sample_count); use wgpu::util::DeviceExt as _; let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_as_bytes(&DeferredUniforms { inv_projection: Matrix4::identity().into(), light_count: 0, _pad: [0; 3], lights: [PointLight { origin: Vector3::zero(), radius: 0.0, }; MAX_LIGHTS], }) }, usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, }); DeferredPipeline { pipeline, bind_group_layouts, uniform_buffer, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) { let layout_refs: Vec<_> = self.bind_group_layouts.iter().collect(); let pipeline = DeferredPipeline::recreate(device, compiler, &layout_refs, sample_count); self.pipeline = pipeline; } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn uniform_buffer(&self) -> &wgpu::Buffer { &self.uniform_buffer } } const BIND_GROUP_LAYOUT_ENTRIES: &[wgpu::BindGroupLayoutEntry] = &[ // sampler wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { filtering: true, comparison: false, }, count: None, }, // color buffer wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: true, }, count: None, }, // normal buffer wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: true, }, count: None, }, // light buffer wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: true, }, count: None, }, // depth buffer wgpu::BindGroupLayoutEntry { binding: 4, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: true, }, count: None, }, // uniform buffer wgpu::BindGroupLayoutEntry { binding: 5, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: NonZeroU64::new(size_of::() as u64), }, count: None, }, ]; impl Pipeline for DeferredPipeline { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "deferred" } fn bind_group_layout_descriptors() -> Vec> { vec![wgpu::BindGroupLayoutDescriptor { label: Some("deferred bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES, }] } fn vertex_shader() -> &'static str { include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/deferred.vert" )) } fn fragment_shader() -> &'static str { include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/deferred.frag" )) } fn primitive_state() -> wgpu::PrimitiveState { QuadPipeline::primitive_state() } fn color_target_states() -> Vec { QuadPipeline::color_target_states() } fn depth_stencil_state() -> Option { None } fn vertex_buffer_layouts() -> Vec> { QuadPipeline::vertex_buffer_layouts() } } pub struct DeferredRenderer { bind_group: wgpu::BindGroup, } impl DeferredRenderer { fn create_bind_group( state: &GraphicsState, diffuse_buffer: &wgpu::TextureView, normal_buffer: &wgpu::TextureView, light_buffer: &wgpu::TextureView, depth_buffer: &wgpu::TextureView, ) -> wgpu::BindGroup { state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("deferred bind group"), layout: &state.deferred_pipeline().bind_group_layouts()[0], entries: &[ // sampler wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(state.diffuse_sampler()), }, // diffuse buffer wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(diffuse_buffer), }, // normal buffer wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(normal_buffer), }, // light buffer wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(light_buffer), }, // depth buffer wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(depth_buffer), }, // uniform buffer wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: state.deferred_pipeline().uniform_buffer(), offset: 0, size: None, }), }, ], }) } pub fn new( state: &GraphicsState, diffuse_buffer: &wgpu::TextureView, normal_buffer: &wgpu::TextureView, light_buffer: &wgpu::TextureView, depth_buffer: &wgpu::TextureView, ) -> DeferredRenderer { let bind_group = Self::create_bind_group( state, diffuse_buffer, normal_buffer, light_buffer, depth_buffer, ); DeferredRenderer { bind_group } } pub fn rebuild( &mut self, state: &GraphicsState, diffuse_buffer: &wgpu::TextureView, normal_buffer: &wgpu::TextureView, light_buffer: &wgpu::TextureView, depth_buffer: &wgpu::TextureView, ) { self.bind_group = Self::create_bind_group( state, diffuse_buffer, normal_buffer, light_buffer, depth_buffer, ); } pub fn update_uniform_buffers(&self, state: &GraphicsState, uniforms: DeferredUniforms) { // update color shift state .queue() .write_buffer(state.deferred_pipeline().uniform_buffer(), 0, unsafe { any_as_bytes(&uniforms) }); } pub fn record_draw<'pass>( &'pass self, state: &'pass GraphicsState, pass: &mut wgpu::RenderPass<'pass>, uniforms: DeferredUniforms, ) { self.update_uniform_buffers(state, uniforms); pass.set_pipeline(state.deferred_pipeline().pipeline()); pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); pass.set_bind_group(0, &self.bind_group, &[]); pass.draw(0..6, 0..1); } } ================================================ FILE: src/client/render/world/mod.rs ================================================ pub mod alias; pub mod brush; pub mod deferred; pub mod particle; pub mod postprocess; pub mod sprite; use std::{cell::RefCell, mem::size_of}; use crate::{ client::{ entity::particle::Particle, render::{ pipeline::{Pipeline, PushConstantUpdate}, uniform::{DynamicUniformBufferBlock, UniformArrayFloat, UniformBool}, world::{ alias::{AliasPipeline, AliasRenderer}, brush::{BrushPipeline, BrushRenderer, BrushRendererBuilder}, sprite::{SpritePipeline, SpriteRenderer}, }, GraphicsState, DEPTH_ATTACHMENT_FORMAT, DIFFUSE_ATTACHMENT_FORMAT, LIGHT_ATTACHMENT_FORMAT, NORMAL_ATTACHMENT_FORMAT, }, ClientEntity, }, common::{ console::CvarRegistry, engine, math::Angles, model::{Model, ModelKind}, sprite::SpriteKind, util::any_as_bytes, }, }; use bumpalo::Bump; use cgmath::{Euler, InnerSpace, Matrix4, SquareMatrix as _, Vector3, Vector4}; use chrono::Duration; lazy_static! { static ref BIND_GROUP_LAYOUT_DESCRIPTOR_BINDINGS: [Vec; 2] = [ vec![ wgpu::BindGroupLayoutEntry { binding:0, visibility:wgpu::ShaderStage::all(), ty:wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: std::num::NonZeroU64::new(size_of::() as u64) }, count:None, }, ], vec![ // transform matrix // TODO: move this to push constants once they're exposed in wgpu wgpu::BindGroupLayoutEntry { binding:0, visibility:wgpu::ShaderStage::VERTEX, ty:wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, min_binding_size: std::num::NonZeroU64::new(size_of::() as u64) }, count:None, }, // diffuse and fullbright sampler wgpu::BindGroupLayoutEntry { binding:1, visibility:wgpu::ShaderStage::FRAGMENT, ty:wgpu::BindingType::Sampler { filtering: true, comparison: false }, count:None, }, // lightmap sampler wgpu::BindGroupLayoutEntry { binding:2, visibility:wgpu::ShaderStage::FRAGMENT, ty:wgpu::BindingType::Sampler { filtering: true, comparison: false }, count:None, }, ], ]; pub static ref BIND_GROUP_LAYOUT_DESCRIPTORS: [wgpu::BindGroupLayoutDescriptor<'static>; 2] = [ // group 0: updated per-frame wgpu::BindGroupLayoutDescriptor { label: Some("per-frame bind group"), entries: &BIND_GROUP_LAYOUT_DESCRIPTOR_BINDINGS[0], }, // group 1: updated per-entity wgpu::BindGroupLayoutDescriptor { label: Some("brush per-entity bind group"), entries: &BIND_GROUP_LAYOUT_DESCRIPTOR_BINDINGS[1], }, ]; } struct WorldPipelineBase; impl Pipeline for WorldPipelineBase { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "world" } fn vertex_shader() -> &'static str { "" } fn fragment_shader() -> &'static str { "" } fn bind_group_layout_descriptors() -> Vec> { // TODO vec![] } fn primitive_state() -> wgpu::PrimitiveState { wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Cw, cull_mode: None, clamp_depth: false, polygon_mode: wgpu::PolygonMode::Fill, conservative: false, } } fn color_target_states() -> Vec { vec![ // diffuse attachment wgpu::ColorTargetState { format: DIFFUSE_ATTACHMENT_FORMAT, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrite::ALL, }, // normal attachment wgpu::ColorTargetState { format: NORMAL_ATTACHMENT_FORMAT, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrite::ALL, }, // light attachment wgpu::ColorTargetState { format: LIGHT_ATTACHMENT_FORMAT, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrite::ALL, }, ] } fn depth_stencil_state() -> Option { Some(wgpu::DepthStencilState { format: DEPTH_ATTACHMENT_FORMAT, depth_write_enabled: true, depth_compare: wgpu::CompareFunction::LessEqual, stencil: wgpu::StencilState { front: wgpu::StencilFaceState::IGNORE, back: wgpu::StencilFaceState::IGNORE, read_mask: 0, write_mask: 0, }, bias: wgpu::DepthBiasState { constant: 0, slope_scale: 0.0, clamp: 0.0, }, }) } fn vertex_buffer_layouts() -> Vec> { Vec::new() } } #[derive(Clone, Copy, Debug)] pub enum BindGroupLayoutId { PerFrame = 0, PerEntity = 1, PerTexture = 2, PerFace = 3, } pub struct Camera { origin: Vector3, angles: Angles, view: Matrix4, view_projection: Matrix4, projection: Matrix4, inverse_projection: Matrix4, clipping_planes: [Vector4; 6], } impl Camera { pub fn new(origin: Vector3, angles: Angles, projection: Matrix4) -> Camera { // convert coordinates let converted_origin = Vector3::new(-origin.y, origin.z, -origin.x); // translate the world by inverse of camera position let translation = Matrix4::from_translation(-converted_origin); let rotation = angles.mat4_wgpu(); let view = rotation * translation; let view_projection = projection * view; // see https://www.gamedevs.org/uploads/fast-extraction-viewing-frustum-planes-from-world-view-projection-matrix.pdf let clipping_planes = [ // left view_projection.w + view_projection.x, // right view_projection.w - view_projection.x, // bottom view_projection.w + view_projection.y, // top view_projection.w - view_projection.y, // near view_projection.w + view_projection.z, // far view_projection.w - view_projection.z, ]; Camera { origin, angles, view, view_projection, projection, inverse_projection: projection.invert().unwrap(), clipping_planes, } } pub fn origin(&self) -> Vector3 { self.origin } pub fn angles(&self) -> Angles { self.angles } pub fn view(&self) -> Matrix4 { self.view } pub fn view_projection(&self) -> Matrix4 { self.view_projection } pub fn projection(&self) -> Matrix4 { self.projection } pub fn inverse_projection(&self) -> Matrix4 { self.inverse_projection } // TODO: this seems to be too lenient /// Determines whether a point falls outside the viewing frustum. pub fn cull_point(&self, p: Vector3) -> bool { for plane in self.clipping_planes.iter() { if (self.view_projection() * p.extend(1.0)).dot(*plane) < 0.0 { return true; } } false } } #[repr(C, align(256))] #[derive(Copy, Clone)] // TODO: derive Debug once const generics are stable pub struct FrameUniforms { // TODO: pack frame values into a [Vector4; 16], lightmap_anim_frames: [UniformArrayFloat; 64], camera_pos: Vector4, time: f32, // TODO: pack flags into a bit string r_lightmap: UniformBool, } #[repr(C, align(256))] #[derive(Clone, Copy, Debug)] pub struct EntityUniforms { /// Model-view-projection transform matrix transform: Matrix4, /// Model-only transform matrix model: Matrix4, } enum EntityRenderer { Alias(AliasRenderer), Brush(BrushRenderer), Sprite(SpriteRenderer), None, } /// Top-level renderer. pub struct WorldRenderer { worldmodel_renderer: BrushRenderer, entity_renderers: Vec, world_uniform_block: DynamicUniformBufferBlock, entity_uniform_blocks: RefCell>>, } impl WorldRenderer { pub fn new(state: &GraphicsState, models: &[Model], worldmodel_id: usize) -> WorldRenderer { let mut worldmodel_renderer = None; let mut entity_renderers = Vec::new(); let world_uniform_block = state.entity_uniform_buffer_mut().allocate(EntityUniforms { transform: Matrix4::identity(), model: Matrix4::identity(), }); for (i, model) in models.iter().enumerate() { if i == worldmodel_id { match *model.kind() { ModelKind::Brush(ref bmodel) => { worldmodel_renderer = Some( BrushRendererBuilder::new(bmodel, true) .build(state) .unwrap(), ); } _ => panic!("Invalid worldmodel"), } } else { match *model.kind() { ModelKind::Alias(ref amodel) => entity_renderers.push(EntityRenderer::Alias( AliasRenderer::new(state, amodel).unwrap(), )), ModelKind::Brush(ref bmodel) => { entity_renderers.push(EntityRenderer::Brush( BrushRendererBuilder::new(bmodel, false) .build(state) .unwrap(), )); } ModelKind::Sprite(ref smodel) => { entity_renderers .push(EntityRenderer::Sprite(SpriteRenderer::new(&state, smodel))); } _ => { warn!("Non-brush renderers not implemented!"); entity_renderers.push(EntityRenderer::None); } } } } WorldRenderer { worldmodel_renderer: worldmodel_renderer.unwrap(), entity_renderers, world_uniform_block, entity_uniform_blocks: RefCell::new(Vec::new()), } } pub fn update_uniform_buffers<'a, I>( &self, state: &GraphicsState, camera: &Camera, time: Duration, entities: I, lightstyle_values: &[f32], cvars: &CvarRegistry, ) where I: Iterator, { trace!("Updating frame uniform buffer"); state .queue() .write_buffer(state.frame_uniform_buffer(), 0, unsafe { any_as_bytes(&FrameUniforms { lightmap_anim_frames: { let mut frames = [UniformArrayFloat::new(0.0); 64]; for i in 0..64 { frames[i] = UniformArrayFloat::new(lightstyle_values[i]); } frames }, camera_pos: camera.origin.extend(1.0), time: engine::duration_to_f32(time), r_lightmap: UniformBool::new(cvars.get_value("r_lightmap").unwrap() != 0.0), }) }); trace!("Updating entity uniform buffer"); let world_uniforms = EntityUniforms { transform: camera.view_projection(), model: Matrix4::identity(), }; state .entity_uniform_buffer_mut() .write_block(&self.world_uniform_block, world_uniforms); for (ent_pos, ent) in entities.into_iter().enumerate() { let ent_uniforms = EntityUniforms { transform: self.calculate_mvp_transform(camera, ent), model: self.calculate_model_transform(camera, ent), }; if ent_pos >= self.entity_uniform_blocks.borrow().len() { // if we don't have enough blocks, get a new one let block = state.entity_uniform_buffer_mut().allocate(ent_uniforms); self.entity_uniform_blocks.borrow_mut().push(block); } else { state .entity_uniform_buffer_mut() .write_block(&self.entity_uniform_blocks.borrow()[ent_pos], ent_uniforms); } } state.entity_uniform_buffer().flush(state.queue()); } pub fn render_pass<'a, E, P>( &'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>, bump: &'a Bump, camera: &Camera, time: Duration, entities: E, particles: P, lightstyle_values: &[f32], viewmodel_id: usize, cvars: &CvarRegistry, ) where E: Iterator + Clone, P: Iterator, { use PushConstantUpdate::*; info!("Updating uniform buffers"); self.update_uniform_buffers( state, camera, time, entities.clone(), lightstyle_values, cvars, ); pass.set_bind_group( BindGroupLayoutId::PerFrame as u32, &state.world_bind_groups()[BindGroupLayoutId::PerFrame as usize], &[], ); // draw world info!("Drawing world"); pass.set_pipeline(state.brush_pipeline().pipeline()); BrushPipeline::set_push_constants( pass, Update(bump.alloc(brush::VertexPushConstants { transform: camera.view_projection(), model_view: camera.view(), })), Clear, Clear, ); pass.set_bind_group( BindGroupLayoutId::PerEntity as u32, &state.world_bind_groups()[BindGroupLayoutId::PerEntity as usize], &[self.world_uniform_block.offset()], ); self.worldmodel_renderer .record_draw(state, pass, &bump, time, camera, 0); // draw entities info!("Drawing entities"); for (ent_pos, ent) in entities.enumerate() { pass.set_bind_group( BindGroupLayoutId::PerEntity as u32, &state.world_bind_groups()[BindGroupLayoutId::PerEntity as usize], &[self.entity_uniform_blocks.borrow()[ent_pos].offset()], ); match self.renderer_for_entity(&ent) { EntityRenderer::Brush(ref bmodel) => { pass.set_pipeline(state.brush_pipeline().pipeline()); BrushPipeline::set_push_constants( pass, Update(bump.alloc(brush::VertexPushConstants { transform: self.calculate_mvp_transform(camera, ent), model_view: self.calculate_mv_transform(camera, ent), })), Clear, Clear, ); bmodel.record_draw(state, pass, &bump, time, camera, ent.frame_id); } EntityRenderer::Alias(ref alias) => { pass.set_pipeline(state.alias_pipeline().pipeline()); AliasPipeline::set_push_constants( pass, Update(bump.alloc(alias::VertexPushConstants { transform: self.calculate_mvp_transform(camera, ent), model_view: self.calculate_mv_transform(camera, ent), })), Clear, Clear, ); alias.record_draw(state, pass, time, ent.frame_id(), ent.skin_id()); } EntityRenderer::Sprite(ref sprite) => { pass.set_pipeline(state.sprite_pipeline().pipeline()); SpritePipeline::set_push_constants(pass, Clear, Clear, Clear); sprite.record_draw(state, pass, ent.frame_id(), time); } _ => warn!("non-brush renderers not implemented!"), // _ => unimplemented!(), } } let viewmodel_orig = camera.origin(); let cam_angles = camera.angles(); let viewmodel_mat = Matrix4::from_translation(Vector3::new( -viewmodel_orig.y, viewmodel_orig.z, -viewmodel_orig.x, )) * Matrix4::from_angle_y(cam_angles.yaw) * Matrix4::from_angle_x(-cam_angles.pitch) * Matrix4::from_angle_z(cam_angles.roll); match self.entity_renderers[viewmodel_id] { EntityRenderer::Alias(ref alias) => { pass.set_pipeline(state.alias_pipeline().pipeline()); AliasPipeline::set_push_constants( pass, Update(bump.alloc(alias::VertexPushConstants { transform: camera.view_projection() * viewmodel_mat, model_view: camera.view() * viewmodel_mat, })), Clear, Clear, ); alias.record_draw(state, pass, time, 0, 0); } _ => unreachable!("non-alias viewmodel"), } log::debug!("Drawing particles"); state .particle_pipeline() .record_draw(pass, &bump, camera, particles); } fn renderer_for_entity(&self, ent: &ClientEntity) -> &EntityRenderer { // subtract 1 from index because world entity isn't counted &self.entity_renderers[ent.model_id() - 1] } fn calculate_mvp_transform(&self, camera: &Camera, entity: &ClientEntity) -> Matrix4 { let model_transform = self.calculate_model_transform(camera, entity); camera.view_projection() * model_transform } fn calculate_mv_transform(&self, camera: &Camera, entity: &ClientEntity) -> Matrix4 { let model_transform = self.calculate_model_transform(camera, entity); camera.view() * model_transform } fn calculate_model_transform(&self, camera: &Camera, entity: &ClientEntity) -> Matrix4 { let origin = entity.get_origin(); let angles = entity.get_angles(); let rotation = match self.renderer_for_entity(entity) { EntityRenderer::Sprite(ref sprite) => match sprite.kind() { // used for decals SpriteKind::Oriented => Matrix4::from(Euler::new(angles.z, -angles.x, angles.y)), _ => { // keep sprite facing player, but preserve roll let cam_angles = camera.angles(); Angles { pitch: -cam_angles.pitch, roll: angles.x, yaw: -cam_angles.yaw, } .mat4_quake() } }, _ => Matrix4::from(Euler::new(angles.x, angles.y, angles.z)), }; Matrix4::from_translation(Vector3::new(-origin.y, origin.z, -origin.x)) * rotation } } ================================================ FILE: src/client/render/world/particle.rs ================================================ use std::{ mem::size_of, num::{NonZeroU32, NonZeroU8}, }; use crate::{ client::{ entity::particle::Particle, render::{ create_texture, pipeline::{Pipeline, PushConstantUpdate}, world::{Camera, WorldPipelineBase}, Palette, TextureData, }, }, common::{math::Angles, util::any_slice_as_bytes}, }; use bumpalo::Bump; use cgmath::Matrix4; lazy_static! { static ref VERTEX_BUFFER_ATTRIBUTES: [Vec; 1] = [ wgpu::vertex_attr_array![ // position 0 => Float32x3, // texcoord 1 => Float32x2, ].to_vec(), ]; } #[rustfmt::skip] const PARTICLE_TEXTURE_PIXELS: [u8; 64] = [ 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, ]; pub struct ParticlePipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, vertex_buffer: wgpu::Buffer, sampler: wgpu::Sampler, textures: Vec, texture_views: Vec, bind_group: wgpu::BindGroup, } impl ParticlePipeline { pub fn new( device: &wgpu::Device, queue: &wgpu::Queue, compiler: &mut shaderc::Compiler, sample_count: u32, palette: &Palette, ) -> ParticlePipeline { let (pipeline, bind_group_layouts) = ParticlePipeline::create(device, compiler, &[], sample_count); use wgpu::util::DeviceExt as _; let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_slice_as_bytes(&VERTICES) }, usage: wgpu::BufferUsage::VERTEX, }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { label: Some("particle sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Linear, lod_min_clamp: -1000.0, lod_max_clamp: 1000.0, compare: None, anisotropy_clamp: NonZeroU8::new(16), border_color: None, }); let textures: Vec = (0..256) .map(|i| { let mut pixels = PARTICLE_TEXTURE_PIXELS; // set up palette translation for pix in pixels.iter_mut() { if *pix == 0 { *pix = 0xFF; } else { *pix *= i as u8; } } let (diffuse_data, _) = palette.translate(&pixels); create_texture( device, queue, Some(&format!("particle texture {}", i)), 8, 8, &TextureData::Diffuse(diffuse_data), ) }) .collect(); let texture_views: Vec = textures .iter() .map(|t| t.create_view(&Default::default())) .collect(); let texture_view_refs = texture_views.iter().collect::>(); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("particle bind group"), layout: &bind_group_layouts[0], entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::Sampler(&sampler), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureViewArray(&texture_view_refs[..]), }, ], }); ParticlePipeline { pipeline, bind_group_layouts, sampler, textures, texture_views, bind_group, vertex_buffer, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) { let layout_refs: Vec<_> = self.bind_group_layouts.iter().collect(); self.pipeline = ParticlePipeline::recreate(device, compiler, &layout_refs, sample_count); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn vertex_buffer(&self) -> &wgpu::Buffer { &self.vertex_buffer } pub fn record_draw<'a, 'b, P>( &'a self, pass: &mut wgpu::RenderPass<'a>, bump: &'a Bump, camera: &Camera, particles: P, ) where P: Iterator, { use PushConstantUpdate::*; pass.set_pipeline(self.pipeline()); pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); pass.set_bind_group(0, &self.bind_group, &[]); // face toward camera let Angles { pitch, yaw, roll } = camera.angles(); let rotation = Angles { pitch: -pitch, yaw: -yaw, roll: -roll, } .mat4_wgpu(); for particle in particles { let q_origin = particle.origin(); let translation = Matrix4::from_translation([-q_origin.y, q_origin.z, -q_origin.x].into()); Self::set_push_constants( pass, Update(bump.alloc(VertexPushConstants { transform: camera.view_projection() * translation * rotation, })), Retain, Update(bump.alloc(FragmentPushConstants { color: particle.color() as u32, })), ); pass.draw(0..6, 0..1); } } } #[derive(Copy, Clone, Debug)] pub struct VertexPushConstants { pub transform: Matrix4, } #[derive(Copy, Clone, Debug)] pub struct FragmentPushConstants { pub color: u32, } const BIND_GROUP_LAYOUT_ENTRIES: &[wgpu::BindGroupLayoutEntry] = &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { filtering: true, comparison: false, }, count: None, }, // per-index texture array wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: NonZeroU32::new(256), }, ]; lazy_static! { static ref VERTEX_ATTRIBUTES: [[wgpu::VertexAttribute; 2]; 2] = [ wgpu::vertex_attr_array![ // position 0 => Float32x3, // texcoord 1 => Float32x2, ], wgpu::vertex_attr_array![ // instance position 2 => Float32x3, // color index 3 => Uint32, ] ]; } impl Pipeline for ParticlePipeline { type VertexPushConstants = VertexPushConstants; type SharedPushConstants = (); type FragmentPushConstants = FragmentPushConstants; fn name() -> &'static str { "particle" } fn vertex_shader() -> &'static str { include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/particle.vert" )) } fn fragment_shader() -> &'static str { include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/particle.frag" )) } // NOTE: if any of the binding indices are changed, they must also be changed in // the corresponding shaders and the BindGroupLayout generation functions. fn bind_group_layout_descriptors() -> Vec> { vec![ // group 0 wgpu::BindGroupLayoutDescriptor { label: Some("particle bind group layout"), entries: BIND_GROUP_LAYOUT_ENTRIES, }, ] } fn primitive_state() -> wgpu::PrimitiveState { WorldPipelineBase::primitive_state() } fn color_target_states() -> Vec { WorldPipelineBase::color_target_states() } fn depth_stencil_state() -> Option { let mut desc = WorldPipelineBase::depth_stencil_state().unwrap(); desc.depth_write_enabled = false; Some(desc) } // NOTE: if the vertex format is changed, this descriptor must also be changed accordingly. fn vertex_buffer_layouts() -> Vec> { vec![wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Vertex, attributes: &VERTEX_ATTRIBUTES[0], }] } } #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct ParticleVertex { position: [f32; 3], texcoord: [f32; 2], } pub const VERTICES: [ParticleVertex; 6] = [ ParticleVertex { position: [-1.0, -1.0, 0.0], texcoord: [0.0, 1.0], }, ParticleVertex { position: [-1.0, 1.0, 0.0], texcoord: [0.0, 0.0], }, ParticleVertex { position: [1.0, 1.0, 0.0], texcoord: [1.0, 0.0], }, ParticleVertex { position: [-1.0, -1.0, 0.0], texcoord: [0.0, 1.0], }, ParticleVertex { position: [1.0, 1.0, 0.0], texcoord: [1.0, 0.0], }, ParticleVertex { position: [1.0, -1.0, 0.0], texcoord: [1.0, 1.0], }, ]; #[repr(C)] pub struct ParticleInstance { color: u32, } ================================================ FILE: src/client/render/world/postprocess.rs ================================================ use std::{mem::size_of, num::NonZeroU64}; use crate::{ client::render::{pipeline::Pipeline, ui::quad::QuadPipeline, GraphicsState}, common::util::any_as_bytes, }; #[repr(C, align(256))] #[derive(Clone, Copy, Debug)] pub struct PostProcessUniforms { pub color_shift: [f32; 4], } pub struct PostProcessPipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, uniform_buffer: wgpu::Buffer, } impl PostProcessPipeline { pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) -> PostProcessPipeline { let (pipeline, bind_group_layouts) = PostProcessPipeline::create(device, compiler, &[], sample_count); use wgpu::util::DeviceExt as _; let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_as_bytes(&PostProcessUniforms { color_shift: [0.0; 4], }) }, usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, }); PostProcessPipeline { pipeline, bind_group_layouts, uniform_buffer, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, sample_count: u32, ) { let layout_refs: Vec<_> = self.bind_group_layouts.iter().collect(); let pipeline = PostProcessPipeline::recreate(device, compiler, &layout_refs, sample_count); self.pipeline = pipeline; } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn uniform_buffer(&self) -> &wgpu::Buffer { &self.uniform_buffer } } const BIND_GROUP_LAYOUT_ENTRIES: &[wgpu::BindGroupLayoutEntry] = &[ // sampler wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Sampler { filtering: true, comparison: false, }, count: None, }, // color buffer wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: true, }, count: None, }, // PostProcessUniforms wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: NonZeroU64::new(size_of::() as u64), }, count: None, }, ]; impl Pipeline for PostProcessPipeline { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "postprocess" } fn bind_group_layout_descriptors() -> Vec> { vec![wgpu::BindGroupLayoutDescriptor { label: Some("postprocess bind group"), entries: BIND_GROUP_LAYOUT_ENTRIES, }] } fn vertex_shader() -> &'static str { include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/postprocess.vert" )) } fn fragment_shader() -> &'static str { include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/shaders/postprocess.frag" )) } fn primitive_state() -> wgpu::PrimitiveState { QuadPipeline::primitive_state() } fn color_target_states() -> Vec { QuadPipeline::color_target_states() } fn depth_stencil_state() -> Option { None } fn vertex_buffer_layouts() -> Vec> { QuadPipeline::vertex_buffer_layouts() } } pub struct PostProcessRenderer { bind_group: wgpu::BindGroup, } impl PostProcessRenderer { pub fn create_bind_group( state: &GraphicsState, color_buffer: &wgpu::TextureView, ) -> wgpu::BindGroup { state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: Some("postprocess bind group"), layout: &state.postprocess_pipeline().bind_group_layouts()[0], entries: &[ // sampler wgpu::BindGroupEntry { binding: 0, // TODO: might need a dedicated sampler if downsampling resource: wgpu::BindingResource::Sampler(state.diffuse_sampler()), }, // color buffer wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(color_buffer), }, // uniform buffer wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { buffer: state.postprocess_pipeline().uniform_buffer(), offset: 0, size: None, }), }, ], }) } pub fn new(state: &GraphicsState, color_buffer: &wgpu::TextureView) -> PostProcessRenderer { let bind_group = Self::create_bind_group(state, color_buffer); PostProcessRenderer { bind_group } } pub fn rebuild(&mut self, state: &GraphicsState, color_buffer: &wgpu::TextureView) { self.bind_group = Self::create_bind_group(state, color_buffer); } pub fn update_uniform_buffers(&self, state: &GraphicsState, color_shift: [f32; 4]) { // update color shift state .queue() .write_buffer(state.postprocess_pipeline().uniform_buffer(), 0, unsafe { any_as_bytes(&PostProcessUniforms { color_shift }) }); } pub fn record_draw<'pass>( &'pass self, state: &'pass GraphicsState, pass: &mut wgpu::RenderPass<'pass>, color_shift: [f32; 4], ) { self.update_uniform_buffers(state, color_shift); pass.set_pipeline(state.postprocess_pipeline().pipeline()); pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); pass.set_bind_group(0, &self.bind_group, &[]); pass.draw(0..6, 0..1); } } ================================================ FILE: src/client/render/world/sprite.rs ================================================ use std::mem::size_of; use crate::{ client::render::{ world::{BindGroupLayoutId, WorldPipelineBase}, GraphicsState, Pipeline, TextureData, }, common::{ sprite::{SpriteFrame, SpriteKind, SpriteModel, SpriteSubframe}, util::any_slice_as_bytes, }, }; use chrono::Duration; pub struct SpritePipeline { pipeline: wgpu::RenderPipeline, bind_group_layouts: Vec, vertex_buffer: wgpu::Buffer, } impl SpritePipeline { pub fn new( device: &wgpu::Device, compiler: &mut shaderc::Compiler, world_bind_group_layouts: &[wgpu::BindGroupLayout], sample_count: u32, ) -> SpritePipeline { let (pipeline, bind_group_layouts) = SpritePipeline::create(device, compiler, world_bind_group_layouts, sample_count); use wgpu::util::DeviceExt as _; let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: None, contents: unsafe { any_slice_as_bytes(&VERTICES) }, usage: wgpu::BufferUsage::VERTEX, }); SpritePipeline { pipeline, bind_group_layouts, vertex_buffer, } } pub fn rebuild( &mut self, device: &wgpu::Device, compiler: &mut shaderc::Compiler, world_bind_group_layouts: &[wgpu::BindGroupLayout], sample_count: u32, ) { let layout_refs: Vec<_> = world_bind_group_layouts .iter() .chain(self.bind_group_layouts.iter()) .collect(); self.pipeline = SpritePipeline::recreate(device, compiler, &layout_refs, sample_count); } pub fn pipeline(&self) -> &wgpu::RenderPipeline { &self.pipeline } pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { &self.bind_group_layouts } pub fn vertex_buffer(&self) -> &wgpu::Buffer { &self.vertex_buffer } } lazy_static! { static ref VERTEX_BUFFER_ATTRIBUTES: [wgpu::VertexAttribute; 3] = wgpu::vertex_attr_array![ // position 0 => Float32x3, // normal 1 => Float32x3, // texcoord 2 => Float32x2, ]; } impl Pipeline for SpritePipeline { type VertexPushConstants = (); type SharedPushConstants = (); type FragmentPushConstants = (); fn name() -> &'static str { "sprite" } fn vertex_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/sprite.vert")) } fn fragment_shader() -> &'static str { include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/sprite.frag")) } // NOTE: if any of the binding indices are changed, they must also be changed in // the corresponding shaders and the BindGroupLayout generation functions. fn bind_group_layout_descriptors() -> Vec> { vec![ // group 2: updated per-texture wgpu::BindGroupLayoutDescriptor { label: Some("sprite per-texture chain bind group"), entries: &[ // diffuse texture, updated once per face wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStage::FRAGMENT, ty: wgpu::BindingType::Texture { view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, multisampled: false, }, count: None, }, ], }, ] } fn primitive_state() -> wgpu::PrimitiveState { WorldPipelineBase::primitive_state() } fn color_target_states() -> Vec { WorldPipelineBase::color_target_states() } fn depth_stencil_state() -> Option { WorldPipelineBase::depth_stencil_state() } // NOTE: if the vertex format is changed, this descriptor must also be changed accordingly. fn vertex_buffer_layouts() -> Vec> { vec![wgpu::VertexBufferLayout { array_stride: size_of::() as u64, step_mode: wgpu::InputStepMode::Vertex, attributes: &VERTEX_BUFFER_ATTRIBUTES[..], }] } } // these type aliases are here to aid readability of e.g. size_of::() type Position = [f32; 3]; type Normal = [f32; 3]; type DiffuseTexcoord = [f32; 2]; #[repr(C)] #[derive(Clone, Copy, Debug)] pub struct SpriteVertex { position: Position, normal: Normal, diffuse_texcoord: DiffuseTexcoord, } pub const VERTICES: [SpriteVertex; 6] = [ SpriteVertex { position: [0.0, 0.0, 0.0], normal: [0.0, 0.0, 1.0], diffuse_texcoord: [0.0, 1.0], }, SpriteVertex { position: [0.0, 1.0, 0.0], normal: [0.0, 0.0, 1.0], diffuse_texcoord: [0.0, 0.0], }, SpriteVertex { position: [1.0, 1.0, 0.0], normal: [0.0, 0.0, 1.0], diffuse_texcoord: [1.0, 0.0], }, SpriteVertex { position: [0.0, 0.0, 0.0], normal: [0.0, 0.0, 1.0], diffuse_texcoord: [0.0, 1.0], }, SpriteVertex { position: [1.0, 1.0, 0.0], normal: [0.0, 0.0, 1.0], diffuse_texcoord: [1.0, 0.0], }, SpriteVertex { position: [1.0, 0.0, 0.0], normal: [0.0, 0.0, 1.0], diffuse_texcoord: [1.0, 1.0], }, ]; enum Frame { Static { diffuse: wgpu::Texture, diffuse_view: wgpu::TextureView, bind_group: wgpu::BindGroup, }, Animated { diffuses: Vec, diffuse_views: Vec, bind_groups: Vec, total_duration: Duration, durations: Vec, }, } impl Frame { fn new(state: &GraphicsState, sframe: &SpriteFrame) -> Frame { fn convert_subframe( state: &GraphicsState, subframe: &SpriteSubframe, ) -> (wgpu::Texture, wgpu::TextureView, wgpu::BindGroup) { let (diffuse_data, _fullbright_data) = state.palette.translate(subframe.indexed()); let diffuse = state.create_texture( None, subframe.width(), subframe.height(), &TextureData::Diffuse(diffuse_data), ); let diffuse_view = diffuse.create_view(&Default::default()); let bind_group = state .device() .create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &state.sprite_pipeline().bind_group_layouts() [BindGroupLayoutId::PerTexture as usize - 2], entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&diffuse_view), }], }); (diffuse, diffuse_view, bind_group) } match sframe { SpriteFrame::Static { frame } => { let (diffuse, diffuse_view, bind_group) = convert_subframe(state, frame); Frame::Static { diffuse, diffuse_view, bind_group, } } SpriteFrame::Animated { subframes, durations, } => { let mut diffuses = Vec::new(); let mut diffuse_views = Vec::new(); let mut bind_groups = Vec::new(); let _ = subframes .iter() .map(|subframe| { let (diffuse, diffuse_view, bind_group) = convert_subframe(state, subframe); diffuses.push(diffuse); diffuse_views.push(diffuse_view); bind_groups.push(bind_group); }) .count(); // count to consume the iterator let total_duration = durations.iter().fold(Duration::zero(), |init, d| init + *d); Frame::Animated { diffuses, diffuse_views, bind_groups, total_duration, durations: durations.clone(), } } } } fn animate(&self, time: Duration) -> &wgpu::BindGroup { match self { Frame::Static { bind_group, .. } => &bind_group, Frame::Animated { bind_groups, total_duration, durations, .. } => { let mut time_ms = time.num_milliseconds() % total_duration.num_milliseconds(); for (i, d) in durations.iter().enumerate() { time_ms -= d.num_milliseconds(); if time_ms <= 0 { return &bind_groups[i]; } } unreachable!() } } } } pub struct SpriteRenderer { kind: SpriteKind, frames: Vec, } impl SpriteRenderer { pub fn new(state: &GraphicsState, sprite: &SpriteModel) -> SpriteRenderer { let frames = sprite .frames() .iter() .map(|f| Frame::new(state, f)) .collect(); SpriteRenderer { kind: sprite.kind(), frames, } } pub fn record_draw<'a>( &'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>, frame_id: usize, time: Duration, ) { pass.set_pipeline(state.sprite_pipeline().pipeline()); pass.set_vertex_buffer(0, state.sprite_pipeline().vertex_buffer().slice(..)); pass.set_bind_group( BindGroupLayoutId::PerTexture as u32, self.frames[frame_id].animate(time), &[], ); pass.draw(0..VERTICES.len() as u32, 0..1); } pub fn kind(&self) -> SpriteKind { self.kind } } ================================================ FILE: src/client/sound/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. mod music; pub use music::MusicPlayer; use std::{ cell::{Cell, RefCell}, io::{self, BufReader, Cursor, Read}, }; use crate::common::vfs::{Vfs, VfsError}; use cgmath::{InnerSpace, Vector3}; use rodio::{ source::{Buffered, SamplesConverter}, Decoder, OutputStreamHandle, Sink, Source, }; use thiserror::Error; use chrono::Duration; pub const DISTANCE_ATTENUATION_FACTOR: f32 = 0.001; const MAX_ENTITY_CHANNELS: usize = 128; #[derive(Error, Debug)] pub enum SoundError { #[error("No such music track: {0}")] NoSuchTrack(String), #[error("I/O error: {0}")] Io(#[from] io::Error), #[error("Virtual filesystem error: {0}")] Vfs(#[from] VfsError), #[error("WAV decoder error: {0}")] Decoder(#[from] rodio::decoder::DecoderError), } /// Data needed for sound spatialization. /// /// This struct is updated every frame. #[derive(Debug)] pub struct Listener { origin: Cell>, left_ear: Cell>, right_ear: Cell>, } impl Listener { pub fn new() -> Listener { Listener { origin: Cell::new(Vector3::new(0.0, 0.0, 0.0)), left_ear: Cell::new(Vector3::new(0.0, 0.0, 0.0)), right_ear: Cell::new(Vector3::new(0.0, 0.0, 0.0)), } } pub fn origin(&self) -> Vector3 { self.origin.get() } pub fn left_ear(&self) -> Vector3 { self.left_ear.get() } pub fn right_ear(&self) -> Vector3 { self.right_ear.get() } pub fn set_origin(&self, new_origin: Vector3) { self.origin.set(new_origin); } pub fn set_left_ear(&self, new_origin: Vector3) { self.left_ear.set(new_origin); } pub fn set_right_ear(&self, new_origin: Vector3) { self.right_ear.set(new_origin); } pub fn attenuate( &self, emitter_origin: Vector3, base_volume: f32, attenuation: f32, ) -> f32 { let decay = (emitter_origin - self.origin.get()).magnitude() * attenuation * DISTANCE_ATTENUATION_FACTOR; let volume = ((1.0 - decay) * base_volume).max(0.0); volume } } #[derive(Clone)] pub struct AudioSource(Buffered>>, f32>>); impl AudioSource { pub fn load(vfs: &Vfs, name: S) -> Result where S: AsRef, { let name = name.as_ref(); let full_path = "sound/".to_owned() + name; let mut file = vfs.open(&full_path)?; let mut data = Vec::new(); file.read_to_end(&mut data)?; let src = Decoder::new(Cursor::new(data))? .convert_samples() .buffered(); Ok(AudioSource(src)) } } pub struct StaticSound { origin: Vector3, sink: RefCell, volume: f32, attenuation: f32, } impl StaticSound { pub fn new( stream: &OutputStreamHandle, origin: Vector3, src: AudioSource, volume: f32, attenuation: f32, listener: &Listener, ) -> StaticSound { // TODO: handle PlayError once PR accepted let sink = Sink::try_new(&stream).unwrap(); let infinite = src.0.clone().repeat_infinite(); sink.append(infinite); sink.set_volume(listener.attenuate(origin, volume, attenuation)); StaticSound { origin, sink: RefCell::new(sink), volume, attenuation, } } pub fn update(&self, listener: &Listener) { let sink = self.sink.borrow_mut(); sink.set_volume(listener.attenuate(self.origin, self.volume, self.attenuation)); } } /// Represents a single audio channel, capable of playing one sound at a time. pub struct Channel { stream: OutputStreamHandle, sink: RefCell>, master_vol: Cell, attenuation: Cell, } impl Channel { /// Create a new `Channel` backed by the given `Device`. pub fn new(stream: OutputStreamHandle) -> Channel { Channel { stream, sink: RefCell::new(None), master_vol: Cell::new(0.0), attenuation: Cell::new(0.0), } } /// Play a new sound on this channel, cutting off any sound that was previously playing. pub fn play( &self, src: AudioSource, ent_pos: Vector3, listener: &Listener, volume: f32, attenuation: f32, ) { self.master_vol.set(volume); self.attenuation.set(attenuation); // stop the old sound self.sink.replace(None); // start the new sound let new_sink = Sink::try_new(&self.stream).unwrap(); new_sink.append(src.0); new_sink.set_volume(listener.attenuate( ent_pos, self.master_vol.get(), self.attenuation.get(), )); self.sink.replace(Some(new_sink)); } pub fn update(&self, ent_pos: Vector3, listener: &Listener) { if let Some(ref sink) = *self.sink.borrow_mut() { // attenuate using quake coordinates since distance is the same either way sink.set_volume(listener.attenuate( ent_pos, self.master_vol.get(), self.attenuation.get(), )); }; } /// Stop the sound currently playing on this channel, if there is one. pub fn stop(&self) { self.sink.replace(None); } /// Returns whether or not this `Channel` is currently in use. pub fn in_use(&self) -> bool { let replace_sink; match *self.sink.borrow() { Some(ref sink) => replace_sink = sink.empty(), None => return false, } // if the sink isn't in use, free it if replace_sink { self.sink.replace(None); false } else { true } } } pub struct EntityChannel { start_time: Duration, // if None, sound is associated with a temp entity ent_id: Option, ent_channel: i8, channel: Channel, } impl EntityChannel { pub fn channel(&self) -> &Channel { &self.channel } pub fn entity_id(&self) -> Option { self.ent_id } } pub struct EntityMixer { stream: OutputStreamHandle, // TODO: replace with an array once const type parameters are implemented channels: Box<[Option]>, } impl EntityMixer { pub fn new(stream: OutputStreamHandle) -> EntityMixer { let mut channel_vec = Vec::new(); for _ in 0..MAX_ENTITY_CHANNELS { channel_vec.push(None); } EntityMixer { stream, channels: channel_vec.into_boxed_slice(), } } fn find_free_channel(&self, ent_id: Option, ent_channel: i8) -> usize { let mut oldest = 0; for (i, channel) in self.channels.iter().enumerate() { match *channel { Some(ref chan) => { // if this channel is free, return it if !chan.channel.in_use() { return i; } // replace sounds on the same entity channel if ent_channel != 0 && chan.ent_id == ent_id && (chan.ent_channel == ent_channel || ent_channel == -1) { return i; } // TODO: don't clobber player sounds with monster sounds // keep track of which sound started the earliest match self.channels[oldest] { Some(ref o) => { if chan.start_time < o.start_time { oldest = i; } } None => oldest = i, } } None => return i, } } // if there are no good channels, just replace the one that's been running the longest oldest } pub fn start_sound( &mut self, src: AudioSource, time: Duration, ent_id: Option, ent_channel: i8, volume: f32, attenuation: f32, origin: Vector3, listener: &Listener, ) { let chan_id = self.find_free_channel(ent_id, ent_channel); let new_channel = Channel::new(self.stream.clone()); new_channel.play( src.clone(), origin, listener, volume, attenuation, ); self.channels[chan_id] = Some(EntityChannel { start_time: time, ent_id, ent_channel, channel: new_channel, }) } pub fn iter_entity_channels(&self) -> impl Iterator { self.channels.iter().filter_map(|e| e.as_ref()) } pub fn stream(&self) -> OutputStreamHandle { self.stream.clone() } } ================================================ FILE: src/client/sound/music.rs ================================================ use std::{ io::{Cursor, Read}, rc::Rc, }; use crate::{client::sound::SoundError, common::vfs::Vfs}; use rodio::{Decoder, OutputStreamHandle, Sink, Source}; /// Plays music tracks. pub struct MusicPlayer { vfs: Rc, stream: OutputStreamHandle, playing: Option, sink: Option, } impl MusicPlayer { pub fn new(vfs: Rc, stream: OutputStreamHandle) -> MusicPlayer { MusicPlayer { vfs, stream, playing: None, sink: None, } } /// Start playing the track with the given name. /// /// Music tracks are expected to be in the "music/" directory of the virtual /// filesystem, so they can be placed either in an actual directory /// `"id1/music/"` or packaged in a PAK archive with a path beginning with /// `"music/"`. /// /// If the specified track is already playing, this has no effect. pub fn play_named(&mut self, name: S) -> Result<(), SoundError> where S: AsRef, { let name = name.as_ref(); // don't replay the same track if let Some(ref playing) = self.playing { if playing == name { return Ok(()); } } // TODO: there's probably a better way to do this extension check let mut file = if !name.contains('.') { // try all supported formats self.vfs .open(format!("music/{}.flac", name)) .or_else(|_| self.vfs.open(format!("music/{}.wav", name))) .or_else(|_| self.vfs.open(format!("music/{}.mp3", name))) .or_else(|_| self.vfs.open(format!("music/{}.ogg", name))) .or(Err(SoundError::NoSuchTrack(name.to_owned())))? } else { self.vfs.open(name)? }; let mut data = Vec::new(); file.read_to_end(&mut data)?; let source = Decoder::new(Cursor::new(data))? .convert_samples::() .buffered() .repeat_infinite(); // stop the old track before starting the new one so there's no overlap self.sink = None; // TODO handle PlayError let new_sink = Sink::try_new(&self.stream).unwrap(); new_sink.append(source); self.sink = Some(new_sink); Ok(()) } /// Start playing the track with the given number. /// /// Note that the first actual music track is track 2; track 1 on the /// original Quake CD-ROM held the game data. pub fn play_track(&mut self, track_id: usize) -> Result<(), SoundError> { self.play_named(format!("track{:02}", track_id)) } /// Stop the current music track. /// /// This ceases playback entirely. To pause the track, allowing it to be /// resumed later, use `MusicPlayer::pause()`. /// /// If no music track is currently playing, this has no effect. pub fn stop(&mut self) { self.sink = None; self.playing = None; } /// Pause the current music track. /// /// If no music track is currently playing, or if the current track is /// already paused, this has no effect. pub fn pause(&mut self) { if let Some(ref mut sink) = self.sink { sink.pause(); } } /// Resume playback of the current music track. /// /// If no music track is currently playing, or if the current track is not /// paused, this has no effect. pub fn resume(&mut self) { if let Some(ref mut sink) = self.sink { sink.play(); } } } ================================================ FILE: src/client/state.rs ================================================ use std::{cell::RefCell, collections::HashMap, rc::Rc}; use super::view::BobVars; use crate::{ client::{ entity::{ particle::{Particle, Particles, TrailKind, MAX_PARTICLES}, Beam, ClientEntity, Light, LightDesc, Lights, MAX_BEAMS, MAX_LIGHTS, MAX_TEMP_ENTITIES, }, input::game::{Action, GameInput}, render::Camera, sound::{AudioSource, EntityMixer, Listener, StaticSound}, view::{IdleVars, KickVars, MouseVars, RollVars, View}, ClientError, ColorShiftCode, IntermissionKind, MoveVars, MAX_STATS, }, common::{ bsp, engine, math::{self, Angles}, model::{Model, ModelFlags, ModelKind, SyncType}, net::{ self, BeamEntityKind, ButtonFlags, ColorShift, EntityEffects, ItemFlags, PlayerData, PointEntityKind, TempEntity, }, vfs::Vfs, }, }; use arrayvec::ArrayVec; use cgmath::{Angle as _, Deg, InnerSpace as _, Matrix4, Vector3, Zero as _}; use chrono::Duration; use net::{ClientCmd, ClientStat, EntityState, EntityUpdate, PlayerColor}; use rand::{ distributions::{Distribution as _, Uniform}, rngs::SmallRng, SeedableRng, }; use rodio::OutputStreamHandle; const CACHED_SOUND_NAMES: &[&'static str] = &[ "hknight/hit.wav", "weapons/r_exp3.wav", "weapons/ric1.wav", "weapons/ric2.wav", "weapons/ric3.wav", "weapons/tink1.wav", "wizard/hit.wav", ]; pub struct PlayerInfo { pub name: String, pub frags: i32, pub colors: PlayerColor, // translations: [u8; VID_GRADES], } // client information regarding the current level pub struct ClientState { // local rng rng: SmallRng, // model precache pub models: Vec, // name-to-id map pub model_names: HashMap, // audio source precache pub sounds: Vec, // sounds that are always needed even if not in precache cached_sounds: HashMap, // ambient sounds (infinite looping, static position) pub static_sounds: Vec, // entities and entity-like things pub entities: Vec, pub static_entities: Vec, pub temp_entities: Vec, // dynamic point lights pub lights: Lights, // lightning bolts and grappling hook cable pub beams: [Option; MAX_BEAMS], // particle effects pub particles: Particles, // visible entities, rebuilt per-frame pub visible_entity_ids: Vec, pub light_styles: HashMap, // various values relevant to the player and level (see common::net::ClientStat) pub stats: [i32; MAX_STATS], pub max_players: usize, pub player_info: [Option; net::MAX_CLIENTS], // the last two timestamps sent by the server (for lerping) pub msg_times: [Duration; 2], pub time: Duration, pub lerp_factor: f32, pub items: ItemFlags, pub item_get_time: [Duration; net::MAX_ITEMS], pub face_anim_time: Duration, pub color_shifts: [Rc>; 4], pub view: View, pub msg_velocity: [Vector3; 2], pub velocity: Vector3, // paused: bool, pub on_ground: bool, pub in_water: bool, pub intermission: Option, pub start_time: Duration, pub completion_time: Option, pub mixer: EntityMixer, pub listener: Listener, } impl ClientState { // TODO: add parameter for number of player slots and reserve them in entity list pub fn new(stream: OutputStreamHandle) -> ClientState { ClientState { rng: SmallRng::from_entropy(), models: vec![Model::none()], model_names: HashMap::new(), sounds: Vec::new(), cached_sounds: HashMap::new(), static_sounds: Vec::new(), entities: Vec::new(), static_entities: Vec::new(), temp_entities: Vec::new(), lights: Lights::with_capacity(MAX_LIGHTS), beams: [None; MAX_BEAMS], particles: Particles::with_capacity(MAX_PARTICLES), visible_entity_ids: Vec::new(), light_styles: HashMap::new(), stats: [0; MAX_STATS], max_players: 0, player_info: Default::default(), msg_times: [Duration::zero(), Duration::zero()], time: Duration::zero(), lerp_factor: 0.0, items: ItemFlags::empty(), item_get_time: [Duration::zero(); net::MAX_ITEMS], color_shifts: [ Rc::new(RefCell::new(ColorShift { dest_color: [0; 3], percent: 0, })), Rc::new(RefCell::new(ColorShift { dest_color: [0; 3], percent: 0, })), Rc::new(RefCell::new(ColorShift { dest_color: [0; 3], percent: 0, })), Rc::new(RefCell::new(ColorShift { dest_color: [0; 3], percent: 0, })), ], view: View::new(), face_anim_time: Duration::zero(), msg_velocity: [Vector3::zero(), Vector3::zero()], velocity: Vector3::zero(), on_ground: false, in_water: false, intermission: None, start_time: Duration::zero(), completion_time: None, mixer: EntityMixer::new(stream), listener: Listener::new(), } } pub fn from_server_info( vfs: &Vfs, stream: OutputStreamHandle, max_clients: u8, model_precache: Vec, sound_precache: Vec, ) -> Result { // TODO: validate submodel names let mut models = Vec::with_capacity(model_precache.len()); models.push(Model::none()); let mut model_names = HashMap::new(); for mod_name in model_precache { // BSPs can have more than one model if mod_name.ends_with(".bsp") { let bsp_data = vfs.open(&mod_name)?; let (mut brush_models, _) = bsp::load(bsp_data).unwrap(); for bmodel in brush_models.drain(..) { let id = models.len(); let name = bmodel.name().to_owned(); models.push(bmodel); model_names.insert(name, id); } } else if !mod_name.starts_with("*") { // model names starting with * are loaded from the world BSP debug!("Loading model {}", mod_name); let id = models.len(); models.push(Model::load(vfs, &mod_name)?); model_names.insert(mod_name, id); } // TODO: send keepalive message? } let mut sounds = vec![AudioSource::load(&vfs, "misc/null.wav")?]; for ref snd_name in sound_precache { debug!("Loading sound {}: {}", sounds.len(), snd_name); sounds.push(AudioSource::load(vfs, snd_name)?); // TODO: send keepalive message? } let mut cached_sounds = HashMap::new(); for name in CACHED_SOUND_NAMES { cached_sounds.insert(name.to_string(), AudioSource::load(vfs, name)?); } Ok(ClientState { models, model_names, sounds, cached_sounds, max_players: max_clients as usize, ..ClientState::new(stream) }) } /// Advance the simulation time by the specified amount. /// /// This method does not change the state of the world to match the new time value. pub fn advance_time(&mut self, frame_time: Duration) { self.time = self.time + frame_time; } /// Update the client state interpolation ratio. /// /// This calculates the ratio used to interpolate entities between the last /// two updates from the server. pub fn update_interp_ratio(&mut self, cl_nolerp: f32) { if cl_nolerp != 0.0 { self.time = self.msg_times[0]; self.lerp_factor = 1.0; return; } let server_delta = engine::duration_to_f32(match self.msg_times[0] - self.msg_times[1] { // if no time has passed between updates, don't lerp anything d if d == Duration::zero() => { self.time = self.msg_times[0]; self.lerp_factor = 1.0; return; } d if d > Duration::milliseconds(100) => { self.msg_times[1] = self.msg_times[0] - Duration::milliseconds(100); Duration::milliseconds(100) } d if d < Duration::zero() => { warn!( "Negative time delta from server!: ({})s", engine::duration_to_f32(d) ); d } d => d, }); let frame_delta = engine::duration_to_f32(self.time - self.msg_times[1]); self.lerp_factor = match frame_delta / server_delta { f if f < 0.0 => { if f < -0.01 { self.time = self.msg_times[1]; } 0.0 } f if f > 1.0 => { if f > 1.01 { self.time = self.msg_times[0]; } 1.0 } f => f, } } /// Update all entities in the game world. /// /// This method is responsible for the following: /// - Updating entity position /// - Despawning entities which did not receive an update in the last server /// message /// - Spawning particles on entities with particle effects /// - Spawning dynamic lights on entities with lighting effects pub fn update_entities(&mut self) -> Result<(), ClientError> { lazy_static! { static ref MFLASH_DIMLIGHT_DISTRIBUTION: Uniform = Uniform::new(200.0, 232.0); static ref BRIGHTLIGHT_DISTRIBUTION: Uniform = Uniform::new(400.0, 432.0); } let lerp_factor = self.lerp_factor; self.velocity = self.msg_velocity[1] + lerp_factor * (self.msg_velocity[0] - self.msg_velocity[1]); // TODO: if we're in demo playback, interpolate the view angles let obj_rotate = Deg(100.0 * engine::duration_to_f32(self.time)).normalize(); // rebuild the list of visible entities self.visible_entity_ids.clear(); // in the extremely unlikely event that there's only a world entity and nothing else, just // return if self.entities.len() <= 1 { return Ok(()); } // NOTE that we start at entity 1 since we don't need to link the world entity for (ent_id, ent) in self.entities.iter_mut().enumerate().skip(1) { if ent.model_id == 0 { // nothing in this entity slot continue; } // if we didn't get an update this frame, remove the entity if ent.msg_time != self.msg_times[0] { ent.model_id = 0; continue; } let prev_origin = ent.origin; if ent.force_link { trace!("force link on entity {}", ent_id); ent.origin = ent.msg_origins[0]; ent.angles = ent.msg_angles[0]; } else { let origin_delta = ent.msg_origins[0] - ent.msg_origins[1]; let ent_lerp_factor = if origin_delta.magnitude2() > 10_000.0 { // if the entity moved more than 100 units in one frame, // assume it was teleported and don't lerp anything 1.0 } else { lerp_factor }; ent.origin = ent.msg_origins[1] + ent_lerp_factor * origin_delta; // assume that entities will not whip around 180+ degrees in one // frame and adjust the delta accordingly. this avoids a bug // where small turns between 0 <-> 359 cause the demo camera to // face backwards for one frame. for i in 0..3 { let mut angle_delta = ent.msg_angles[0][i] - ent.msg_angles[1][i]; if angle_delta > Deg(180.0) { angle_delta = Deg(360.0) - angle_delta; } else if angle_delta < Deg(-180.0) { angle_delta = Deg(360.0) + angle_delta; } ent.angles[i] = (ent.msg_angles[1][i] + angle_delta * ent_lerp_factor).normalize(); } } let model = &self.models[ent.model_id]; if model.has_flag(ModelFlags::ROTATE) { ent.angles[1] = obj_rotate; } if ent.effects.contains(EntityEffects::BRIGHT_FIELD) { self.particles.create_entity_field(self.time, ent); } // TODO: factor out EntityEffects->LightDesc mapping if ent.effects.contains(EntityEffects::MUZZLE_FLASH) { // TODO: angle and move origin to muzzle ent.light_id = Some(self.lights.insert( self.time, LightDesc { origin: ent.origin + Vector3::new(0.0, 0.0, 16.0), init_radius: MFLASH_DIMLIGHT_DISTRIBUTION.sample(&mut self.rng), decay_rate: 0.0, min_radius: Some(32.0), ttl: Duration::milliseconds(100), }, ent.light_id, )); } if ent.effects.contains(EntityEffects::BRIGHT_LIGHT) { ent.light_id = Some(self.lights.insert( self.time, LightDesc { origin: ent.origin, init_radius: BRIGHTLIGHT_DISTRIBUTION.sample(&mut self.rng), decay_rate: 0.0, min_radius: None, ttl: Duration::milliseconds(1), }, ent.light_id, )); } if ent.effects.contains(EntityEffects::DIM_LIGHT) { ent.light_id = Some(self.lights.insert( self.time, LightDesc { origin: ent.origin, init_radius: MFLASH_DIMLIGHT_DISTRIBUTION.sample(&mut self.rng), decay_rate: 0.0, min_radius: None, ttl: Duration::milliseconds(1), }, ent.light_id, )); } // check if this entity leaves a trail let trail_kind = if model.has_flag(ModelFlags::GIB) { Some(TrailKind::Blood) } else if model.has_flag(ModelFlags::ZOMGIB) { Some(TrailKind::BloodSlight) } else if model.has_flag(ModelFlags::TRACER) { Some(TrailKind::TracerGreen) } else if model.has_flag(ModelFlags::TRACER2) { Some(TrailKind::TracerRed) } else if model.has_flag(ModelFlags::ROCKET) { ent.light_id = Some(self.lights.insert( self.time, LightDesc { origin: ent.origin, init_radius: 200.0, decay_rate: 0.0, min_radius: None, ttl: Duration::milliseconds(10), }, ent.light_id, )); Some(TrailKind::Rocket) } else if model.has_flag(ModelFlags::GRENADE) { Some(TrailKind::Smoke) } else if model.has_flag(ModelFlags::TRACER3) { Some(TrailKind::Vore) } else { None }; // if the entity leaves a trail, generate it if let Some(kind) = trail_kind { self.particles .create_trail(self.time, prev_origin, ent.origin, kind, false); } // don't render the player model if self.view.entity_id() != ent_id { // mark entity for rendering self.visible_entity_ids.push(ent_id); } // enable lerp for next frame ent.force_link = false; } // apply effects to static entities as well for ent in self.static_entities.iter_mut() { if ent.effects.contains(EntityEffects::BRIGHT_LIGHT) { debug!("spawn bright light on static entity"); ent.light_id = Some(self.lights.insert( self.time, LightDesc { origin: ent.origin, init_radius: BRIGHTLIGHT_DISTRIBUTION.sample(&mut self.rng), decay_rate: 0.0, min_radius: None, ttl: Duration::milliseconds(1), }, ent.light_id, )); } if ent.effects.contains(EntityEffects::DIM_LIGHT) { debug!("spawn dim light on static entity"); ent.light_id = Some(self.lights.insert( self.time, LightDesc { origin: ent.origin, init_radius: MFLASH_DIMLIGHT_DISTRIBUTION.sample(&mut self.rng), decay_rate: 0.0, min_radius: None, ttl: Duration::milliseconds(1), }, ent.light_id, )); } } Ok(()) } pub fn update_temp_entities(&mut self) -> Result<(), ClientError> { lazy_static! { static ref ANGLE_DISTRIBUTION: Uniform = Uniform::new(0.0, 360.0); } self.temp_entities.clear(); for id in 0..self.beams.len() { // remove beam if expired if self.beams[id].map_or(false, |b| b.expire < self.time) { self.beams[id] = None; continue; } let view_ent = self.view_entity_id(); if let Some(ref mut beam) = self.beams[id] { // keep lightning gun bolts fixed to player if beam.entity_id == view_ent { beam.start = self.entities[view_ent].origin; } let vec = beam.end - beam.start; let yaw = Deg::from(cgmath::Rad(vec.y.atan2(vec.x))).normalize(); let forward = (vec.x.powf(2.0) + vec.y.powf(2.0)).sqrt(); let pitch = Deg::from(cgmath::Rad(vec.z.atan2(forward))).normalize(); let len = vec.magnitude(); let direction = vec.normalize(); for interval in 0..(len / 30.0) as i32 { let mut ent = ClientEntity::uninitialized(); ent.origin = beam.start + 30.0 * interval as f32 * direction; ent.angles = Vector3::new(pitch, yaw, Deg(ANGLE_DISTRIBUTION.sample(&mut self.rng))); if self.temp_entities.len() < MAX_TEMP_ENTITIES { self.temp_entities.push(ent); } else { warn!("too many temp entities!"); } } } } Ok(()) } pub fn update_player(&mut self, update: PlayerData) { self.view .set_view_height(update.view_height.unwrap_or(net::DEFAULT_VIEWHEIGHT)); self.view .set_ideal_pitch(update.ideal_pitch.unwrap_or(Deg(0.0))); self.view.set_punch_angles(Angles { pitch: update.punch_pitch.unwrap_or(Deg(0.0)), roll: update.punch_roll.unwrap_or(Deg(0.0)), yaw: update.punch_yaw.unwrap_or(Deg(0.0)), }); // store old velocity self.msg_velocity[1] = self.msg_velocity[0]; self.msg_velocity[0].x = update.velocity_x.unwrap_or(0.0); self.msg_velocity[0].y = update.velocity_y.unwrap_or(0.0); self.msg_velocity[0].z = update.velocity_z.unwrap_or(0.0); let item_diff = update.items - self.items; if !item_diff.is_empty() { // item flags have changed, something got picked up let bits = item_diff.bits(); for i in 0..net::MAX_ITEMS { if bits & 1 << i != 0 { // item with flag value `i` was picked up self.item_get_time[i] = self.time; } } } self.items = update.items; self.on_ground = update.on_ground; self.in_water = update.in_water; self.stats[ClientStat::WeaponFrame as usize] = update.weapon_frame.unwrap_or(0) as i32; self.stats[ClientStat::Armor as usize] = update.armor.unwrap_or(0) as i32; self.stats[ClientStat::Weapon as usize] = update.weapon.unwrap_or(0) as i32; self.stats[ClientStat::Health as usize] = update.health as i32; self.stats[ClientStat::Ammo as usize] = update.ammo as i32; self.stats[ClientStat::Shells as usize] = update.ammo_shells as i32; self.stats[ClientStat::Nails as usize] = update.ammo_nails as i32; self.stats[ClientStat::Rockets as usize] = update.ammo_rockets as i32; self.stats[ClientStat::Cells as usize] = update.ammo_cells as i32; // TODO: this behavior assumes the `standard_quake` behavior and will likely // break with the mission packs self.stats[ClientStat::ActiveWeapon as usize] = update.active_weapon as i32; } pub fn handle_input( &mut self, game_input: &mut GameInput, frame_time: Duration, move_vars: MoveVars, mouse_vars: MouseVars, ) -> ClientCmd { use Action::*; let mlook = game_input.action_state(MLook); self.view.handle_input( frame_time, game_input, self.intermission.as_ref(), mlook, move_vars.cl_anglespeedkey, move_vars.cl_pitchspeed, move_vars.cl_yawspeed, mouse_vars, ); let mut move_left = game_input.action_state(MoveLeft); let mut move_right = game_input.action_state(MoveRight); if game_input.action_state(Strafe) { move_left |= game_input.action_state(Left); move_right |= game_input.action_state(Right); } let mut sidemove = move_vars.cl_sidespeed * (move_right as i32 - move_left as i32) as f32; let mut upmove = move_vars.cl_upspeed * (game_input.action_state(MoveUp) as i32 - game_input.action_state(MoveDown) as i32) as f32; let mut forwardmove = 0.0; if !game_input.action_state(KLook) { forwardmove += move_vars.cl_forwardspeed * game_input.action_state(Forward) as i32 as f32; forwardmove -= move_vars.cl_backspeed * game_input.action_state(Back) as i32 as f32; } if game_input.action_state(Speed) { sidemove *= move_vars.cl_movespeedkey; upmove *= move_vars.cl_movespeedkey; forwardmove *= move_vars.cl_movespeedkey; } let mut button_flags = ButtonFlags::empty(); if game_input.action_state(Attack) { button_flags |= ButtonFlags::ATTACK; } if game_input.action_state(Jump) { button_flags |= ButtonFlags::JUMP; } if !mlook { // TODO: IN_Move (mouse / joystick / gamepad) } let send_time = self.msg_times[0]; // send "raw" angles without any pitch/roll from movement or damage let angles = self.view.input_angles(); ClientCmd::Move { send_time, angles: Vector3::new(angles.pitch, angles.yaw, angles.roll), fwd_move: forwardmove as i16, side_move: sidemove as i16, up_move: upmove as i16, button_flags, impulse: game_input.impulse(), } } pub fn handle_damage( &mut self, armor: u8, health: u8, source: Vector3, kick_vars: KickVars, ) { self.face_anim_time = self.time + Duration::milliseconds(200); let dmg_factor = (armor + health).min(20) as f32 / 2.0; let mut cshift = self.color_shifts[ColorShiftCode::Damage as usize].borrow_mut(); cshift.percent += 3 * dmg_factor as i32; cshift.percent = cshift.percent.clamp(0, 150); if armor > health { cshift.dest_color = [200, 100, 100]; } else if armor > 0 { cshift.dest_color = [220, 50, 50]; } else { cshift.dest_color = [255, 0, 0]; } let v_ent = &self.entities[self.view.entity_id()]; let v_angles = Angles { pitch: v_ent.angles.x, roll: v_ent.angles.z, yaw: v_ent.angles.y, }; self.view.handle_damage( self.time, armor as f32, health as f32, v_ent.origin, v_angles, source, kick_vars, ); } pub fn calc_final_view( &mut self, idle_vars: IdleVars, kick_vars: KickVars, roll_vars: RollVars, bob_vars: BobVars, ) { self.view.calc_final_angles( self.time, self.intermission.as_ref(), self.velocity, idle_vars, kick_vars, roll_vars, ); self.view.calc_final_origin( self.time, self.entities[self.view.entity_id()].origin, self.velocity, bob_vars, ); } /// Spawn an entity with the given ID, also spawning any uninitialized /// entities between the former last entity and the new one. // TODO: skipping entities indicates that the entities have been freed by // the server. it may make more sense to use a HashMap to store entities by // ID since the lookup table is relatively sparse. pub fn spawn_entities(&mut self, id: usize, baseline: EntityState) -> Result<(), ClientError> { // don't clobber existing entities if id < self.entities.len() { Err(ClientError::EntityExists(id))?; } // spawn intermediate entities (uninitialized) for i in self.entities.len()..id { debug!("Spawning uninitialized entity with ID {}", i); self.entities.push(ClientEntity::uninitialized()); } debug!( "Spawning entity with id {} from baseline {:?}", id, baseline ); self.entities.push(ClientEntity::from_baseline(baseline)); Ok(()) } pub fn update_entity(&mut self, id: usize, update: EntityUpdate) -> Result<(), ClientError> { if id >= self.entities.len() { let baseline = EntityState { origin: Vector3::new( update.origin_x.unwrap_or(0.0), update.origin_y.unwrap_or(0.0), update.origin_z.unwrap_or(0.0), ), angles: Vector3::new( update.pitch.unwrap_or(Deg(0.0)), update.yaw.unwrap_or(Deg(0.0)), update.roll.unwrap_or(Deg(0.0)), ), model_id: update.model_id.unwrap_or(0) as usize, frame_id: update.frame_id.unwrap_or(0) as usize, colormap: update.colormap.unwrap_or(0), skin_id: update.skin_id.unwrap_or(0) as usize, effects: EntityEffects::empty(), }; self.spawn_entities(id, baseline)?; } let entity = &mut self.entities[id]; entity.update(self.msg_times, update); if entity.model_changed() { match self.models[entity.model_id].kind() { ModelKind::None => (), _ => { entity.sync_base = match self.models[entity.model_id].sync_type() { SyncType::Sync => Duration::zero(), SyncType::Rand => unimplemented!(), // TODO } } } } if let Some(_c) = entity.colormap() { // only players may have custom colormaps if id > self.max_players { warn!( "Server attempted to set colormap on entity {}, which is not a player", id ); } // TODO: set player custom colormaps } Ok(()) } pub fn spawn_temp_entity(&mut self, temp_entity: &TempEntity) { lazy_static! { static ref ZERO_ONE_DISTRIBUTION: Uniform = Uniform::new(0.0, 1.0); } let mut spike_sound = || match ZERO_ONE_DISTRIBUTION.sample(&mut self.rng) { x if x < 0.2 => "weapons/tink1.wav", x if x < 0.4667 => "weapons/ric1.wav", x if x < 0.7333 => "weapons/ric2.wav", _ => "weapons/ric3.wav", }; match temp_entity { TempEntity::Point { kind, origin } => { use PointEntityKind::*; match kind { // projectile impacts WizSpike | KnightSpike | Spike | SuperSpike | Gunshot => { let (color, count, sound) = match kind { // TODO: start wizard/hit.wav WizSpike => (20, 30, Some("wizard/hit.wav")), KnightSpike => (226, 20, Some("hknight/hit.wav")), // TODO: for Spike and SuperSpike, start one of: // - 26.67%: weapons/tink1.wav // - 20.0%: weapons/ric1.wav // - 20.0%: weapons/ric2.wav // - 20.0%: weapons/ric3.wav Spike => (0, 10, Some(spike_sound())), SuperSpike => (0, 20, Some(spike_sound())), // no impact sound Gunshot => (0, 20, None), _ => unreachable!(), }; self.particles.create_projectile_impact( self.time, *origin, Vector3::zero(), color, count, ); if let Some(snd) = sound { self.mixer.start_sound( self.cached_sounds.get(snd).unwrap().clone(), self.time, None, 0, 1.0, 1.0, *origin, &self.listener, ); } } Explosion => { self.particles.create_explosion(self.time, *origin); self.lights.insert( self.time, LightDesc { origin: *origin, init_radius: 350.0, decay_rate: 300.0, min_radius: None, ttl: Duration::milliseconds(500), }, None, ); self.mixer.start_sound( self.cached_sounds .get("weapons/r_exp3.wav") .unwrap() .clone(), self.time, None, 0, 1.0, 1.0, *origin, &self.listener, ); } ColorExplosion { color_start, color_len, } => { self.particles.create_color_explosion( self.time, *origin, (*color_start)..=(*color_start + *color_len - 1), ); self.lights.insert( self.time, LightDesc { origin: *origin, init_radius: 350.0, decay_rate: 300.0, min_radius: None, ttl: Duration::milliseconds(500), }, None, ); self.mixer.start_sound( self.cached_sounds .get("weapons/r_exp3.wav") .unwrap() .clone(), self.time, None, 0, 1.0, 1.0, *origin, &self.listener, ); } TarExplosion => { self.particles.create_spawn_explosion(self.time, *origin); self.mixer.start_sound( self.cached_sounds .get("weapons/r_exp3.wav") .unwrap() .clone(), self.time, None, 0, 1.0, 1.0, *origin, &self.listener, ); } LavaSplash => self.particles.create_lava_splash(self.time, *origin), Teleport => self.particles.create_teleporter_warp(self.time, *origin), } } TempEntity::Beam { kind, entity_id, start, end, } => { use BeamEntityKind::*; let model_name = match kind { Lightning { model_id } => format!( "progs/bolt{}.mdl", match model_id { 1 => "", 2 => "2", 3 => "3", x => panic!("invalid lightning model id: {}", x), } ), Grapple => "progs/beam.mdl".to_string(), }; self.spawn_beam( self.time, *entity_id as usize, *self.model_names.get(&model_name).unwrap(), *start, *end, ); } } } pub fn spawn_beam( &mut self, time: Duration, entity_id: usize, model_id: usize, start: Vector3, end: Vector3, ) { // always override beam with same entity_id if it exists // otherwise use the first free slot let mut free = None; for i in 0..self.beams.len() { if let Some(ref mut beam) = self.beams[i] { if beam.entity_id == entity_id { beam.model_id = model_id; beam.expire = time + Duration::milliseconds(200); beam.start = start; beam.end = end; } } else if free.is_none() { free = Some(i); } } if let Some(i) = free { self.beams[i] = Some(Beam { entity_id, model_id, expire: time + Duration::milliseconds(200), start, end, }); } else { warn!("No free beam slots!"); } } pub fn update_listener(&self) { // TODO: update to self.view_origin() let view_origin = self.entities[self.view.entity_id()].origin; let world_translate = Matrix4::from_translation(view_origin); let left_base = Vector3::new(0.0, 4.0, self.view.view_height()); let right_base = Vector3::new(0.0, -4.0, self.view.view_height()); let rotate = self.view.input_angles().mat4_quake(); let left = (world_translate * rotate * left_base.extend(1.0)).truncate(); let right = (world_translate * rotate * right_base.extend(1.0)).truncate(); self.listener.set_origin(view_origin); self.listener.set_left_ear(left); self.listener.set_right_ear(right); } pub fn update_sound_spatialization(&self) { self.update_listener(); // update entity sounds for e_channel in self.mixer.iter_entity_channels() { if let Some(ent_id) = e_channel.entity_id() { if e_channel.channel().in_use() { e_channel .channel() .update(self.entities[ent_id].origin, &self.listener); } } } // update static sounds for ss in self.static_sounds.iter() { ss.update(&self.listener); } } fn view_leaf_contents(&self) -> Result { match self.models[1].kind() { ModelKind::Brush(ref bmodel) => { let bsp_data = bmodel.bsp_data(); let leaf_id = bsp_data.find_leaf(self.entities[self.view.entity_id()].origin); let leaf = &bsp_data.leaves()[leaf_id]; Ok(leaf.contents) } _ => panic!("non-brush worldmodel"), } } pub fn update_color_shifts(&mut self, frame_time: Duration) -> Result<(), ClientError> { let float_time = engine::duration_to_f32(frame_time); // set color for leaf contents self.color_shifts[ColorShiftCode::Contents as usize].replace( match self.view_leaf_contents()? { bsp::BspLeafContents::Empty => ColorShift { dest_color: [0, 0, 0], percent: 0, }, bsp::BspLeafContents::Lava => ColorShift { dest_color: [255, 80, 0], percent: 150, }, bsp::BspLeafContents::Slime => ColorShift { dest_color: [0, 25, 5], percent: 150, }, _ => ColorShift { dest_color: [130, 80, 50], percent: 128, }, }, ); // decay damage and item pickup shifts // always decay at least 1 "percent" (actually 1/255) // TODO: make percent an actual percent ([0.0, 1.0]) let mut dmg_shift = self.color_shifts[ColorShiftCode::Damage as usize].borrow_mut(); dmg_shift.percent -= ((float_time * 150.0) as i32).max(1); dmg_shift.percent = dmg_shift.percent.max(0); let mut bonus_shift = self.color_shifts[ColorShiftCode::Bonus as usize].borrow_mut(); bonus_shift.percent -= ((float_time * 100.0) as i32).max(1); bonus_shift.percent = bonus_shift.percent.max(0); // set power-up overlay self.color_shifts[ColorShiftCode::Powerup as usize].replace( if self.items.contains(ItemFlags::QUAD) { ColorShift { dest_color: [0, 0, 255], percent: 30, } } else if self.items.contains(ItemFlags::SUIT) { ColorShift { dest_color: [0, 255, 0], percent: 20, } } else if self.items.contains(ItemFlags::INVISIBILITY) { ColorShift { dest_color: [100, 100, 100], percent: 100, } } else if self.items.contains(ItemFlags::INVULNERABILITY) { ColorShift { dest_color: [255, 255, 0], percent: 30, } } else { ColorShift { dest_color: [0, 0, 0], percent: 0, } }, ); Ok(()) } /// Update the view angles to the specified value, disabling interpolation. pub fn set_view_angles(&mut self, angles: Vector3>) { self.view.update_input_angles(Angles { pitch: angles.x, roll: angles.z, yaw: angles.y, }); let final_angles = self.view.final_angles(); self.entities[self.view.entity_id()].set_angles(Vector3::new( final_angles.pitch, final_angles.yaw, final_angles.roll, )); } /// Update the view angles to the specified value, enabling interpolation. pub fn update_view_angles(&mut self, angles: Vector3>) { self.view.update_input_angles(Angles { pitch: angles.x, roll: angles.z, yaw: angles.y, }); let final_angles = self.view.final_angles(); self.entities[self.view.entity_id()].update_angles(Vector3::new( final_angles.pitch, final_angles.yaw, final_angles.roll, )); } pub fn set_view_entity(&mut self, entity_id: usize) -> Result<(), ClientError> { // view entity may not have been spawned yet, so check // against both max_players and the current number of // entities if entity_id > self.max_players && entity_id >= self.entities.len() { Err(ClientError::InvalidViewEntity(entity_id))?; } self.view.set_entity_id(entity_id); Ok(()) } pub fn models(&self) -> &[Model] { &self.models } pub fn viewmodel_id(&self) -> usize { match self.stats[ClientStat::Weapon as usize] as usize { 0 => 0, x => x - 1, } } pub fn iter_visible_entities(&self) -> impl Iterator + Clone { self.visible_entity_ids .iter() .map(move |i| &self.entities[*i]) .chain(self.temp_entities.iter()) .chain(self.static_entities.iter()) } pub fn iter_particles(&self) -> impl Iterator { self.particles.iter() } pub fn iter_lights(&self) -> impl Iterator { self.lights.iter() } pub fn time(&self) -> Duration { self.time } pub fn view_entity_id(&self) -> usize { self.view.entity_id() } pub fn camera(&self, aspect: f32, fov: Deg) -> Camera { let fov_y = math::fov_x_to_fov_y(fov, aspect).unwrap(); Camera::new( self.view.final_origin(), self.view.final_angles(), cgmath::perspective(fov_y, aspect, 4.0, 4096.0), ) } pub fn demo_camera(&self, aspect: f32, fov: Deg) -> Camera { let fov_y = math::fov_x_to_fov_y(fov, aspect).unwrap(); let angles = self.entities[self.view.entity_id()].angles; Camera::new( self.view.final_origin(), Angles { pitch: angles.x, roll: angles.z, yaw: angles.y, }, cgmath::perspective(fov_y, aspect, 4.0, 4096.0), ) } pub fn lightstyle_values(&self) -> Result, ClientError> { let mut values = ArrayVec::new(); for lightstyle_id in 0..64 { match self.light_styles.get(&lightstyle_id) { Some(ls) => { let float_time = engine::duration_to_f32(self.time); let frame = if ls.len() == 0 { None } else { Some((float_time * 10.0) as usize % ls.len()) }; values.push(match frame { // 'z' - 'a' = 25, so divide by 12.5 to get range [0, 2] Some(f) => (ls.as_bytes()[f] - 'a' as u8) as f32 / 12.5, None => 1.0, }) } None => Err(ClientError::NoSuchLightmapAnimation(lightstyle_id as usize))?, } } Ok(values) } pub fn intermission(&self) -> Option<&IntermissionKind> { self.intermission.as_ref() } pub fn start_time(&self) -> Duration { self.start_time } pub fn completion_time(&self) -> Option { self.completion_time } pub fn stats(&self) -> &[i32] { &self.stats } pub fn items(&self) -> ItemFlags { self.items } pub fn item_pickup_times(&self) -> &[Duration] { &self.item_get_time } pub fn face_anim_time(&self) -> Duration { self.face_anim_time } pub fn color_shift(&self) -> [f32; 4] { self.color_shifts.iter().fold([0.0; 4], |accum, elem| { let elem_a = elem.borrow().percent as f32 / 255.0 / 2.0; if elem_a == 0.0 { return accum; } let in_a = accum[3]; let out_a = in_a + elem_a * (1.0 - in_a); let color_factor = elem_a / out_a; let mut out = [0.0; 4]; for i in 0..3 { out[i] = accum[i] * (1.0 - color_factor) + elem.borrow().dest_color[i] as f32 / 255.0 * color_factor; } out[3] = out_a.min(1.0).max(0.0); out }) } pub fn check_entity_id(&self, id: usize) -> Result<(), ClientError> { match id { 0 => Err(ClientError::NullEntity), e if e >= self.entities.len() => Err(ClientError::NoSuchEntity(id)), _ => Ok(()), } } pub fn check_player_id(&self, id: usize) -> Result<(), ClientError> { if id >= net::MAX_CLIENTS { Err(ClientError::NoSuchClient(id)) } else if id > self.max_players { Err(ClientError::NoSuchPlayer(id)) } else { Ok(()) } } } ================================================ FILE: src/client/trace.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::collections::HashMap; use serde::Serialize; /// Client-side debug tracing. #[derive(Serialize)] pub struct TraceEntity { pub msg_origins: [[f32; 3]; 2], pub msg_angles_deg: [[f32; 3]; 2], pub origin: [f32; 3], } #[derive(Serialize)] pub struct TraceFrame { pub msg_times_ms: [i64; 2], pub time_ms: i64, pub lerp_factor: f32, pub entities: HashMap, } ================================================ FILE: src/client/view.rs ================================================ use std::f32::consts::PI; use crate::{ client::input::game::{Action, GameInput}, common::{ engine::{duration_from_f32, duration_to_f32}, math::{self, Angles}, }, }; use super::IntermissionKind; use cgmath::{Angle as _, Deg, InnerSpace as _, Vector3, Zero as _}; use chrono::Duration; pub struct View { // entity "holding" the camera entity_id: usize, // how high the entity is "holding" the camera view_height: f32, // TODO ideal_pitch: Deg, // view angles from the server msg_angles: [Angles; 2], // view angles from client input input_angles: Angles, // pitch and roll from damage damage_angles: Angles, // time at which damage punch decays to zero damage_time: Duration, // punch angles from server punch_angles: Angles, // final angles combining all sources final_angles: Angles, // final origin accounting for view bob final_origin: Vector3, } impl View { pub fn new() -> View { View { entity_id: 0, view_height: 0.0, ideal_pitch: Deg(0.0), msg_angles: [Angles::zero(); 2], input_angles: Angles::zero(), damage_angles: Angles::zero(), damage_time: Duration::zero(), punch_angles: Angles::zero(), final_angles: Angles::zero(), final_origin: Vector3::zero(), } } pub fn entity_id(&self) -> usize { self.entity_id } pub fn set_entity_id(&mut self, id: usize) { self.entity_id = id; } pub fn view_height(&self) -> f32 { self.view_height } pub fn set_view_height(&mut self, view_height: f32) { self.view_height = view_height; } pub fn ideal_pitch(&self) -> Deg { self.ideal_pitch } pub fn set_ideal_pitch(&mut self, ideal_pitch: Deg) { self.ideal_pitch = ideal_pitch; } pub fn punch_angles(&self) -> Angles { self.punch_angles } pub fn set_punch_angles(&mut self, punch_angles: Angles) { self.punch_angles = punch_angles; } pub fn input_angles(&self) -> Angles { self.input_angles } /// Update the current input angles with a new value. pub fn update_input_angles(&mut self, input_angles: Angles) { self.input_angles = input_angles; } pub fn handle_input( &mut self, frame_time: Duration, game_input: &GameInput, intermission: Option<&IntermissionKind>, mlook: bool, cl_anglespeedkey: f32, cl_pitchspeed: f32, cl_yawspeed: f32, mouse_vars: MouseVars, ) { let frame_time_f32 = duration_to_f32(frame_time); let speed = if game_input.action_state(Action::Speed) { frame_time_f32 * cl_anglespeedkey } else { frame_time_f32 }; // ignore camera controls during intermission if intermission.is_some() { return; } if !game_input.action_state(Action::Strafe) { let right_factor = game_input.action_state(Action::Right) as i32 as f32; let left_factor = game_input.action_state(Action::Left) as i32 as f32; self.input_angles.yaw += Deg(speed * cl_yawspeed * (left_factor - right_factor)); self.input_angles.yaw = self.input_angles.yaw.normalize(); } let lookup_factor = game_input.action_state(Action::LookUp) as i32 as f32; let lookdown_factor = game_input.action_state(Action::LookDown) as i32 as f32; self.input_angles.pitch += Deg(speed * cl_pitchspeed * (lookdown_factor - lookup_factor)); if mlook { let pitch_factor = mouse_vars.m_pitch * mouse_vars.sensitivity; let yaw_factor = mouse_vars.m_yaw * mouse_vars.sensitivity; self.input_angles.pitch += Deg(game_input.mouse_delta().1 as f32 * pitch_factor); self.input_angles.yaw -= Deg(game_input.mouse_delta().0 as f32 * yaw_factor); } if lookup_factor != 0.0 || lookdown_factor != 0.0 { // TODO: V_StopPitchDrift } // clamp pitch to [-70, 80] and roll to [-50, 50] self.input_angles.pitch = math::clamp_deg(self.input_angles.pitch, Deg(-70.0), Deg(80.0)); self.input_angles.roll = math::clamp_deg(self.input_angles.roll, Deg(-50.0), Deg(50.0)); } pub fn handle_damage( &mut self, time: Duration, armor_dmg: f32, health_dmg: f32, view_ent_origin: Vector3, view_ent_angles: Angles, src_origin: Vector3, vars: KickVars, ) { self.damage_time = time + duration_from_f32(vars.v_kicktime); // dmg_factor is at most 10.0 let dmg_factor = (armor_dmg + health_dmg).min(20.0) / 2.0; let dmg_vector = (view_ent_origin - src_origin).normalize(); let rot = view_ent_angles.mat3_quake(); let roll_factor = dmg_vector.dot(-rot.x); self.damage_angles.roll = Deg(dmg_factor * roll_factor * vars.v_kickroll); let pitch_factor = dmg_vector.dot(rot.y); self.damage_angles.pitch = Deg(dmg_factor * pitch_factor * vars.v_kickpitch); } pub fn calc_final_angles( &mut self, time: Duration, intermission: Option<&IntermissionKind>, velocity: Vector3, mut idle_vars: IdleVars, kick_vars: KickVars, roll_vars: RollVars, ) { let move_angles = Angles { pitch: Deg(0.0), roll: roll(self.input_angles, velocity, roll_vars), yaw: Deg(0.0), }; let kick_factor = duration_to_f32(self.damage_time - time).max(0.0) / kick_vars.v_kicktime; let damage_angles = self.damage_angles * kick_factor; // always idle during intermission if intermission.is_some() { idle_vars.v_idlescale = 1.0; } let idle_angles = idle(time, idle_vars); self.final_angles = self.input_angles + move_angles + damage_angles + self.punch_angles + idle_angles; } pub fn final_angles(&self) -> Angles { self.final_angles } pub fn calc_final_origin( &mut self, time: Duration, origin: Vector3, velocity: Vector3, bob_vars: BobVars, ) { // offset the view by 1/32 unit to keep it from intersecting liquid planes let plane_offset = Vector3::new(1.0 / 32.0, 1.0 / 32.0, 1.0 / 32.0); let height_offset = Vector3::new(0.0, 0.0, self.view_height); let bob_offset = Vector3::new(0.0, 0.0, bob(time, velocity, bob_vars)); self.final_origin = origin + plane_offset + height_offset + bob_offset; } pub fn final_origin(&self) -> Vector3 { self.final_origin } pub fn viewmodel_angle(&self) -> Angles { // TODO self.final_angles() } } #[derive(Copy, Clone, Debug)] pub struct MouseVars { pub m_pitch: f32, pub m_yaw: f32, pub sensitivity: f32, } #[derive(Clone, Copy, Debug)] pub struct KickVars { pub v_kickpitch: f32, pub v_kickroll: f32, pub v_kicktime: f32, } #[derive(Clone, Copy, Debug)] pub struct BobVars { pub cl_bob: f32, pub cl_bobcycle: f32, pub cl_bobup: f32, } pub fn bob(time: Duration, velocity: Vector3, vars: BobVars) -> f32 { let time = duration_to_f32(time); let ratio = (time % vars.cl_bobcycle) / vars.cl_bobcycle; let cycle = if ratio < vars.cl_bobup { PI * ratio / vars.cl_bobup } else { PI + PI * (ratio - vars.cl_bobup) / (1.0 - vars.cl_bobup) }; // drop z coordinate let vel_mag = velocity.truncate().magnitude(); let bob = vars.cl_bob * (vel_mag * 0.3 + vel_mag * 0.7 * cycle.sin()); bob.max(-7.0).min(4.0) } #[derive(Clone, Copy, Debug)] pub struct RollVars { pub cl_rollangle: f32, pub cl_rollspeed: f32, } pub fn roll(angles: Angles, velocity: Vector3, vars: RollVars) -> Deg { let rot = angles.mat3_quake(); let side = velocity.dot(rot.y); let sign = side.signum(); let side_abs = side.abs(); let roll_abs = if side < vars.cl_rollspeed { side_abs * vars.cl_rollangle / vars.cl_rollspeed } else { vars.cl_rollangle }; Deg(roll_abs * sign) } #[derive(Clone, Copy, Debug)] pub struct IdleVars { pub v_idlescale: f32, pub v_ipitch_cycle: f32, pub v_ipitch_level: f32, pub v_iroll_cycle: f32, pub v_iroll_level: f32, pub v_iyaw_cycle: f32, pub v_iyaw_level: f32, } pub fn idle(time: Duration, vars: IdleVars) -> Angles { let time = duration_to_f32(time); let pitch = Deg(vars.v_idlescale * (time * vars.v_ipitch_cycle).sin() * vars.v_ipitch_level); let roll = Deg(vars.v_idlescale * (time * vars.v_iroll_cycle).sin() * vars.v_iroll_level); let yaw = Deg(vars.v_idlescale * (time * vars.v_iyaw_cycle).sin() * vars.v_iyaw_level); Angles { pitch, roll, yaw } } ================================================ FILE: src/common/alloc.rs ================================================ use std::{collections::LinkedList, mem}; use slab::Slab; /// A slab allocator with a linked list of allocations. /// /// This allocator trades O(1) random access by key, a property of /// [`Slab`](slab::Slab), for the ability to iterate only those entries that are /// actually allocated. This significantly reduces the cost of `retain()`: where /// `Slab::retain` is O(capacity) regardless of how many values are allocated, /// [`LinkedSlab::retain`](LinkedSlab::retain) is O(n) in the number of values. pub struct LinkedSlab { slab: Slab, allocated: LinkedList, } impl LinkedSlab { /// Construct a new, empty `LinkedSlab` with the specified capacity. /// /// The returned allocator will be able to store exactly `capacity` without /// reallocating. If `capacity` is 0, the slab will not allocate. pub fn with_capacity(capacity: usize) -> LinkedSlab { LinkedSlab { slab: Slab::with_capacity(capacity), allocated: LinkedList::new(), } } /// Return the number of values the allocator can store without reallocating. pub fn capacity(&self) -> usize { self.slab.capacity() } /// Clear the allocator of all values. pub fn clear(&mut self) { self.allocated.clear(); self.slab.clear(); } /// Return the number of stored values. pub fn len(&self) -> usize { self.slab.len() } /// Return `true` if there are no values allocated. pub fn is_empty(&self) -> bool { self.slab.is_empty() } /// Return an iterator over the allocated values. pub fn iter(&self) -> impl Iterator { self.allocated .iter() .map(move |key| self.slab.get(*key).unwrap()) } /// Return a reference to the value associated with the given key. /// /// If the given key is not associated with a value, then None is returned. pub fn get(&self, key: usize) -> Option<&T> { self.slab.get(key) } /// Return a mutable reference to the value associated with the given key. /// /// If the given key is not associated with a value, then None is returned. pub fn get_mut(&mut self, key: usize) -> Option<&mut T> { self.slab.get_mut(key) } /// Allocate a value, returning the key assigned to the value. /// /// This operation is O(1). pub fn insert(&mut self, val: T) -> usize { let key = self.slab.insert(val); self.allocated.push_front(key); key } /// Remove and return the value associated with the given key. /// /// The key is then released and may be associated with future stored values. /// /// Note that this operation is O(n) in the number of allocated values. pub fn remove(&mut self, key: usize) -> T { self.allocated.drain_filter(|k| *k == key); self.slab.remove(key) } /// Return `true` if a value is associated with the given key. pub fn contains(&self, key: usize) -> bool { self.slab.contains(key) } /// Retain only the elements specified by the predicate. /// /// The predicate is permitted to modify allocated values in-place. /// /// This operation is O(n) in the number of allocated values. pub fn retain(&mut self, mut f: F) where F: FnMut(usize, &mut T) -> bool, { // move contents out to avoid double mutable borrow of self. // neither LinkedList::new() nor Slab::new() allocates any memory, so // this is free. let mut allocated = mem::replace(&mut self.allocated, LinkedList::new()); let mut slab = mem::replace(&mut self.slab, Slab::new()); allocated.drain_filter(|k| { let retain = match slab.get_mut(*k) { Some(ref mut v) => f(*k, v), None => true, }; if !retain { slab.remove(*k); } !retain }); // put them back self.slab = slab; self.allocated = allocated; } } #[cfg(test)] mod tests { use super::*; use std::{collections::HashSet, iter::FromIterator as _}; #[test] fn test_iter() { let values: Vec = vec![1, 3, 5, 7, 11, 13, 17, 19]; let mut linked_slab = LinkedSlab::with_capacity(values.len()); let mut expected = HashSet::new(); for value in values.iter() { linked_slab.insert(*value); expected.insert(*value); } let mut actual = HashSet::new(); for value in linked_slab.iter() { actual.insert(*value); } assert_eq!(expected, actual); } #[test] fn test_retain() { let mut values: Vec = vec![0, 9, 1, 8, 2, 7, 3, 6, 4, 5]; let mut linked_slab = LinkedSlab::with_capacity(values.len()); for value in values.iter() { linked_slab.insert(*value); } values.retain(|v| v % 2 == 0); let mut expected: HashSet = HashSet::from_iter(values.into_iter()); linked_slab.retain(|_, v| *v % 2 == 0); let mut actual = HashSet::from_iter(linked_slab.iter().map(|v| *v)); assert_eq!(expected, actual); } } ================================================ FILE: src/common/bitset.rs ================================================ pub struct BitSet { blocks: [u64; N_64], } impl BitSet { pub fn new() -> Self { BitSet { blocks: [0; N_64] } } pub fn all_set() -> Self { BitSet { blocks: [u64::MAX; N_64], } } #[inline] fn bit_location(bit: u64) -> (u64, u64) { ( bit >> 6, // divide by 64 1 << (bit & 63), // modulo 64 ) } #[inline] pub fn count(&self) -> usize { let mut count = 0; for block in self.blocks { count += block.count_ones() as usize; } count } #[inline] pub fn contains(&self, bit: u64) -> bool { let (index, mask) = Self::bit_location(bit); self.blocks[index as usize] & mask != 0 } #[inline] pub fn set(&mut self, bit: u64) { let (index, mask) = Self::bit_location(bit); self.blocks[index as usize] |= mask; } #[inline] pub fn clear(&mut self, bit: u64) { let (index, mask) = Self::bit_location(bit); self.blocks[index as usize] &= !mask; } #[inline] pub fn toggle(&mut self, bit: u64) { let (index, mask) = Self::bit_location(bit); self.blocks[index as usize] ^= mask; } #[inline] pub fn iter(&self) -> BitSetIter<'_, N_64> { BitSetIter::new(&self.blocks) } } pub struct BitSetIter<'a, const N_64: usize> { block_index: usize, block_val: u64, blocks: &'a [u64; N_64], } impl<'a, const N_64: usize> BitSetIter<'a, N_64> { fn new(blocks: &'a [u64; N_64]) -> BitSetIter<'_, N_64> { BitSetIter { block_index: 0, block_val: blocks[0], blocks, } } } impl<'a, const N_64: usize> Iterator for BitSetIter<'a, N_64> { type Item = u64; fn next(&mut self) -> Option { while self.block_index < N_64 { println!( "block_index = {} | block_val = {:b}", self.block_index, self.block_val ); if self.block_val != 0 { // Locate the next set bit in the block. let next_bit = self.block_val.trailing_zeros(); // Clear the bit. self.block_val &= !(1 << next_bit); // Return it. return Some((u64::BITS * self.block_index as u32 + next_bit) as u64); } else { // No set bits, move to the next block. self.block_index += 1; self.block_val = *self.blocks.get(self.block_index)?; } } None } } #[cfg(test)] mod tests { use super::*; #[test] fn test_set_bit() { let mut bits: BitSet<2> = BitSet::new(); let cases = &[0, 1, 63, 64]; for case in cases.iter().copied() { bits.set(case); assert!(bits.contains(case)); } } #[test] fn test_clear_bit() { let mut bits: BitSet<2> = BitSet::all_set(); let cases = &[0, 1, 63, 64]; for case in cases.iter().copied() { bits.clear(case); assert!(!bits.contains(case)); } } #[test] fn test_iter() { let mut bits: BitSet<8> = BitSet::new(); let cases = &[1, 2, 3, 5, 8, 13, 21, 34, 55, 89]; for case in cases.iter().cloned() { bits.set(case); } let back = bits.iter().collect::>(); assert_eq!(&cases[..], &back); } } ================================================ FILE: src/common/bsp/load.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{ collections::HashMap, io::{BufRead, BufReader, Read, Seek, SeekFrom}, mem::size_of, rc::Rc, }; use crate::common::{ bsp::{ BspCollisionHull, BspCollisionNode, BspCollisionNodeChild, BspData, BspEdge, BspEdgeDirection, BspEdgeIndex, BspFace, BspFaceSide, BspLeaf, BspLeafContents, BspModel, BspRenderNode, BspRenderNodeChild, BspTexInfo, BspTexture, MAX_HULLS, MAX_LIGHTSTYLES, MIPLEVELS, }, math::{Axis, Hyperplane}, model::Model, util::read_f32_3, }; use super::{BspTextureFrame, BspTextureKind}; use byteorder::{LittleEndian, ReadBytesExt}; use cgmath::{InnerSpace, Vector3}; use chrono::Duration; use failure::ResultExt as _; use num::FromPrimitive; use thiserror::Error; const VERSION: i32 = 29; pub const MAX_MODELS: usize = 256; const MAX_LEAVES: usize = 32767; const MAX_ENTSTRING: usize = 65536; const MAX_PLANES: usize = 8192; const MAX_RENDER_NODES: usize = 32767; const MAX_COLLISION_NODES: usize = 32767; const MAX_VERTICES: usize = 65535; const MAX_FACES: usize = 65535; const _MAX_MARKTEXINFO: usize = 65535; const _MAX_TEXINFO: usize = 4096; const MAX_EDGES: usize = 256000; const MAX_EDGELIST: usize = 512000; const MAX_TEXTURES: usize = 0x200000; const _MAX_LIGHTMAP: usize = 0x100000; const MAX_VISLIST: usize = 0x100000; const TEX_NAME_MAX: usize = 16; const NUM_AMBIENTS: usize = 4; const MAX_TEXTURE_FRAMES: usize = 10; const TEXTURE_FRAME_LEN_MS: i64 = 200; const ASCII_0: usize = '0' as usize; const ASCII_9: usize = '9' as usize; const ASCII_CAPITAL_A: usize = 'A' as usize; const ASCII_CAPITAL_J: usize = 'J' as usize; const ASCII_SMALL_A: usize = 'a' as usize; const ASCII_SMALL_J: usize = 'j' as usize; #[derive(Error, Debug)] pub enum BspFileError { #[error("I/O error")] Io(#[from] std::io::Error), #[error("unsupported BSP format version (expected {}, found {0})", VERSION)] UnsupportedVersion(i32), #[error("negative BSP file section offset: {0}")] NegativeSectionOffset(i32), #[error("negative BSP file section size: {0}")] NegativeSectionSize(i32), #[error( "invalid BSP file section size: section {section:?} size is {size}, must be multiple of {}", section.element_size(), )] InvalidSectionSize { section: BspFileSectionId, size: usize, }, #[error("invalid BSP texture frame specifier: {0}")] InvalidTextureFrameSpecifier(String), #[error("texture has primary animation with 0 frames: {0}")] EmptyPrimaryAnimation(String), } #[derive(Copy, Clone, Debug)] struct BspFileSection { offset: u64, size: usize, } impl BspFileSection { fn read_from(reader: &mut R) -> Result where R: ReadBytesExt, { let offset = match reader.read_i32::()? { ofs if ofs < 0 => Err(BspFileError::NegativeSectionOffset(ofs)), ofs => Ok(ofs as u64), }?; let size = match reader.read_i32::()? { sz if sz < 0 => Err(BspFileError::NegativeSectionSize(sz)), sz => Ok(sz as usize), }?; Ok(BspFileSection { offset, size }) } } const SECTION_COUNT: usize = 15; #[derive(Debug, FromPrimitive)] pub enum BspFileSectionId { Entities = 0, Planes = 1, Textures = 2, Vertices = 3, Visibility = 4, RenderNodes = 5, TextureInfo = 6, Faces = 7, Lightmaps = 8, CollisionNodes = 9, Leaves = 10, FaceList = 11, Edges = 12, EdgeList = 13, Models = 14, } const PLANE_SIZE: usize = 20; const RENDER_NODE_SIZE: usize = 24; const LEAF_SIZE: usize = 28; const TEXTURE_INFO_SIZE: usize = 40; const FACE_SIZE: usize = 20; const COLLISION_NODE_SIZE: usize = 8; const FACELIST_SIZE: usize = 2; const EDGE_SIZE: usize = 4; const EDGELIST_SIZE: usize = 4; const MODEL_SIZE: usize = 64; const VERTEX_SIZE: usize = 12; impl BspFileSectionId { // the size on disk of one element of a BSP file section. fn element_size(&self) -> usize { use BspFileSectionId::*; match self { Entities => size_of::(), Planes => PLANE_SIZE, Textures => size_of::(), Vertices => VERTEX_SIZE, Visibility => size_of::(), RenderNodes => RENDER_NODE_SIZE, TextureInfo => TEXTURE_INFO_SIZE, Faces => FACE_SIZE, Lightmaps => size_of::(), CollisionNodes => COLLISION_NODE_SIZE, Leaves => LEAF_SIZE, FaceList => FACELIST_SIZE, Edges => EDGE_SIZE, EdgeList => EDGELIST_SIZE, Models => MODEL_SIZE, } } } struct BspFileTable { sections: [BspFileSection; SECTION_COUNT], } impl BspFileTable { fn read_from(reader: &mut R) -> Result where R: ReadBytesExt, { let mut sections = [BspFileSection { offset: 0, size: 0 }; SECTION_COUNT]; for (id, section) in sections.iter_mut().enumerate() { *section = BspFileSection::read_from(reader)?; let section_id = BspFileSectionId::from_usize(id).unwrap(); if section.size % section_id.element_size() != 0 { Err(BspFileError::InvalidSectionSize { section: section_id, size: section.size, })? } } Ok(BspFileTable { sections }) } fn section(&self, section_id: BspFileSectionId) -> BspFileSection { self.sections[section_id as usize] } fn check_end_position( &self, seeker: &mut S, section_id: BspFileSectionId, ) -> Result<(), failure::Error> where S: Seek, { let section = self.section(section_id); ensure!( seeker.seek(SeekFrom::Current(0))? == seeker.seek(SeekFrom::Start(section.offset + section.size as u64))?, "BSP read misaligned" ); Ok(()) } } fn read_hyperplane(reader: &mut R) -> Result where R: ReadBytesExt, { let normal: Vector3 = read_f32_3(reader)?.into(); let dist = reader.read_f32::()?; let plane = match Axis::from_i32(reader.read_i32::()?) { Some(ax) => match ax { Axis::X => Hyperplane::axis_x(dist), Axis::Y => Hyperplane::axis_y(dist), Axis::Z => Hyperplane::axis_z(dist), }, None => Hyperplane::new(normal, dist), }; Ok(plane) } #[derive(Debug)] struct BspFileTexture { name: String, width: u32, height: u32, mipmaps: [Vec; MIPLEVELS], } // load a textures from the BSP file. // // converts the texture's name to all lowercase, including its frame specifier // if it has one. fn load_texture( mut reader: &mut R, tex_section_ofs: u64, tex_ofs: u64, ) -> Result where R: ReadBytesExt + Seek, { // convert texture name from NUL-terminated to str let mut tex_name_bytes = [0u8; TEX_NAME_MAX]; reader.read(&mut tex_name_bytes)?; let len = tex_name_bytes .iter() .enumerate() .find(|&item| item.1 == &0) .unwrap_or((TEX_NAME_MAX, &0)) .0; let tex_name = String::from_utf8(tex_name_bytes[..len].to_vec())?.to_lowercase(); let width = reader.read_u32::()?; let height = reader.read_u32::()?; let mut mip_offsets = [0usize; MIPLEVELS]; for m in 0..MIPLEVELS { mip_offsets[m] = reader.read_u32::()? as usize; } let mut mipmaps = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; for m in 0..MIPLEVELS { let factor = 2usize.pow(m as u32); let mipmap_size = (width as usize / factor) * (height as usize / factor); let offset = tex_section_ofs + tex_ofs + mip_offsets[m] as u64; reader.seek(SeekFrom::Start(offset))?; (&mut reader) .take(mipmap_size as u64) .read_to_end(&mut mipmaps[m])?; } Ok(BspFileTexture { name: tex_name, width, height, mipmaps, }) } fn load_render_node(reader: &mut R) -> Result where R: ReadBytesExt, { let plane_id = reader.read_i32::()?; if plane_id < 0 { bail!("Invalid plane id"); } // If the child ID is positive, it points to another internal node. If it is negative, its // bitwise negation points to a leaf node. let front = match reader.read_i16::()? { f if f < 0 => BspRenderNodeChild::Leaf((!f) as usize), f => BspRenderNodeChild::Node(f as usize), }; let back = match reader.read_i16::()? { b if b < 0 => BspRenderNodeChild::Leaf((!b) as usize), b => BspRenderNodeChild::Node(b as usize), }; let min = read_i16_3(reader)?; let max = read_i16_3(reader)?; let face_id = reader.read_i16::()?; if face_id < 0 { bail!("Invalid face id"); } let face_count = reader.read_u16::()?; if face_count as usize > MAX_FACES { bail!("Invalid face count"); } Ok(BspRenderNode { plane_id: plane_id as usize, children: [front, back], min, max, face_id: face_id as usize, face_count: face_count as usize, }) } fn load_texinfo(reader: &mut R, texture_count: usize) -> Result where R: ReadBytesExt, { let s_vector = read_f32_3(reader)?.into(); let s_offset = reader.read_f32::()?; let t_vector = read_f32_3(reader)?.into(); let t_offset = reader.read_f32::()?; let tex_id = match reader.read_i32::()? { t if t < 0 || t as usize > texture_count => bail!("Invalid texture ID"), t => t as usize, }; let special = match reader.read_i32::()? { 0 => false, 1 => true, _ => bail!("Invalid texture flags"), }; Ok(BspTexInfo { s_vector, s_offset, t_vector, t_offset, tex_id, special, }) } /// Load a BSP file, returning the models it contains and a `String` describing the entities /// it contains. pub fn load(data: R) -> Result<(Vec, String), failure::Error> where R: Read + Seek, { let mut reader = BufReader::new(data); let _version = match reader.read_i32::()? { VERSION => Ok(VERSION), other => Err(BspFileError::UnsupportedVersion(other)), }?; let table = BspFileTable::read_from(&mut reader)?; let ent_section = table.section(BspFileSectionId::Entities); let plane_section = table.section(BspFileSectionId::Planes); let tex_section = table.section(BspFileSectionId::Textures); let vert_section = table.section(BspFileSectionId::Vertices); let vis_section = table.section(BspFileSectionId::Visibility); let texinfo_section = table.section(BspFileSectionId::TextureInfo); let face_section = table.section(BspFileSectionId::Faces); let lightmap_section = table.section(BspFileSectionId::Lightmaps); let collision_node_section = table.section(BspFileSectionId::CollisionNodes); let leaf_section = table.section(BspFileSectionId::Leaves); let facelist_section = table.section(BspFileSectionId::FaceList); let edge_section = table.section(BspFileSectionId::Edges); let edgelist_section = table.section(BspFileSectionId::EdgeList); let model_section = table.section(BspFileSectionId::Models); let render_node_section = table.section(BspFileSectionId::RenderNodes); let plane_count = plane_section.size / PLANE_SIZE; let vert_count = vert_section.size / VERTEX_SIZE; let render_node_count = render_node_section.size / RENDER_NODE_SIZE; let texinfo_count = texinfo_section.size / TEXTURE_INFO_SIZE; let face_count = face_section.size / FACE_SIZE; let collision_node_count = collision_node_section.size / COLLISION_NODE_SIZE; let leaf_count = leaf_section.size / LEAF_SIZE; let facelist_count = facelist_section.size / FACELIST_SIZE; let edge_count = edge_section.size / EDGE_SIZE; let edgelist_count = edgelist_section.size / EDGELIST_SIZE; let model_count = model_section.size / MODEL_SIZE; // check limits ensure!(plane_count <= MAX_PLANES, "Plane count exceeds MAX_PLANES"); ensure!( vert_count <= MAX_VERTICES, "Vertex count exceeds MAX_VERTICES" ); ensure!( vis_section.size <= MAX_VISLIST, "Visibility data size exceeds MAX_VISLIST" ); ensure!( render_node_count <= MAX_RENDER_NODES, "Render node count exceeds MAX_RENDER_NODES" ); ensure!( collision_node_count <= MAX_COLLISION_NODES, "Collision node count exceeds MAX_COLLISION_NODES" ); ensure!(leaf_count <= MAX_LEAVES, "Leaf count exceeds MAX_LEAVES"); ensure!(edge_count <= MAX_EDGES, "Edge count exceeds MAX_EDGES"); ensure!( edgelist_count <= MAX_EDGELIST, "Edge list count exceeds MAX_EDGELIST" ); ensure!( model_count > 0, "No brush models (need at least 1 for worldmodel)" ); ensure!(model_count <= MAX_MODELS, "Model count exceeds MAX_MODELS"); reader.seek(SeekFrom::Start(ent_section.offset))?; let mut ent_data = Vec::with_capacity(MAX_ENTSTRING); reader.read_until(0x00, &mut ent_data)?; ensure!( ent_data.len() <= MAX_ENTSTRING, "Entity data exceeds MAX_ENTSTRING" ); let ent_string = String::from_utf8(ent_data).context("Failed to create string from entity data")?; table.check_end_position(&mut reader, BspFileSectionId::Entities)?; // load planes reader.seek(SeekFrom::Start(plane_section.offset))?; let mut planes = Vec::with_capacity(plane_count); for _ in 0..plane_count { planes.push(read_hyperplane(&mut reader)?); } let planes_rc = Rc::new(planes.into_boxed_slice()); table.check_end_position(&mut reader, BspFileSectionId::Planes)?; // load textures reader.seek(SeekFrom::Start(tex_section.offset))?; let tex_count = reader.read_i32::()?; ensure!( tex_count >= 0 && tex_count as usize <= MAX_TEXTURES, "Invalid texture count" ); let tex_count = tex_count as usize; let mut tex_offsets = Vec::with_capacity(tex_count); for _ in 0..tex_count { let ofs = reader.read_i32::()?; tex_offsets.push(match ofs { o if o < -1 => bail!("negative texture offset ({})", ofs), -1 => None, o => Some(o as usize), }); } let mut file_textures = Vec::with_capacity(tex_count); for (id, tex_ofs) in tex_offsets.into_iter().enumerate() { match tex_ofs { Some(ofs) => { reader.seek(SeekFrom::Start(tex_section.offset + ofs as u64))?; let texture = load_texture(&mut reader, tex_section.offset as u64, ofs as u64)?; debug!( "Texture {id:>width$}: {name}", id = id, width = (tex_count as f32).log(10.0) as usize, name = texture.name, ); file_textures.push(texture); } None => { file_textures.push(BspFileTexture { name: String::new(), width: 0, height: 0, mipmaps: [Vec::new(), Vec::new(), Vec::new(), Vec::new()], }); } } } table.check_end_position(&mut reader, BspFileSectionId::Textures)?; struct BspFileTextureAnimations { primary: Vec<(usize, BspFileTexture)>, alternate: Vec<(usize, BspFileTexture)>, } // maps animated texture names to primary and alternate animations // e.g., for textures of the form +#slip, maps "slip" to the ids of // [+0slip, +1slip, ...] and [+aslip, +bslip, ...] let mut anim_file_textures: HashMap = HashMap::new(); // final texture array let mut textures = Vec::new(); // mapping from texture ids on disk to texture ids in memory let mut texture_ids = Vec::new(); // map file texture ids to actual texture ids let mut static_texture_ids = HashMap::new(); let mut animated_texture_ids = HashMap::new(); debug!("Sequencing textures"); for (file_texture_id, file_texture) in file_textures.into_iter().enumerate() { // recognize textures of the form +[frame][stem], where: // - frame is in [0-9A-Za-z] // - stem is the remainder of the string match file_texture.name.strip_prefix("+") { Some(rest) => { let (frame, stem) = rest.split_at(1); debug!( "Sequencing texture {}: {}", file_texture_id, &file_texture.name ); let anims = anim_file_textures .entry(stem.to_owned()) .or_insert(BspFileTextureAnimations { primary: Vec::new(), alternate: Vec::new(), }); match frame.chars().nth(0).unwrap() { '0'..='9' => anims.primary.push((file_texture_id, file_texture)), // guaranteed to be lowercase by load_texture 'a'..='j' => anims.alternate.push((file_texture_id, file_texture)), _ => Err(BspFileError::InvalidTextureFrameSpecifier( file_texture.name.clone(), ))?, }; } // if the string doesn't match, it's not animated, so add it as a static texture None => { let BspFileTexture { name, width, height, mipmaps, } = file_texture; let texture_id = textures.len(); static_texture_ids.insert(file_texture_id, texture_id); textures.push(BspTexture { name, width, height, kind: BspTextureKind::Static(BspTextureFrame { mipmaps }), }); } }; } // sequence animated textures with the same stem for ( name, BspFileTextureAnimations { primary: mut pri, alternate: mut alt, }, ) in anim_file_textures.into_iter() { if pri.len() == 0 { Err(BspFileError::EmptyPrimaryAnimation(name.to_owned()))?; } // TODO: ensure one-to-one frame specifiers // sort names in ascending order to get the frames ordered correctly pri.sort_unstable_by(|(_, tex), (_, other)| tex.name.cmp(&other.name)); // TODO: verify width and height? let width = pri[0].1.width; let height = pri[0].1.height; // texture id of each frame in the file let mut corresponding_file_ids = Vec::new(); let mut primary = Vec::new(); for (file_id, file_texture) in pri { debug!( "primary frame: id = {}, name = {}", file_id, file_texture.name ); corresponding_file_ids.push(file_id); primary.push(BspTextureFrame { mipmaps: file_texture.mipmaps, }); } let mut alt_corresp_file_ids = Vec::new(); let alternate = match alt.len() { 0 => None, _ => { alt.sort_unstable_by(|(_, tex), (_, other)| tex.name.cmp(&other.name)); let mut alternate = Vec::new(); for (file_id, file_texture) in alt { alt_corresp_file_ids.push(file_id); alternate.push(BspTextureFrame { mipmaps: file_texture.mipmaps, }); } Some(alternate) } }; // actual id of the animated texture let texture_id = textures.len(); // update map to point other data to the right texture for id in corresponding_file_ids { debug!("map disk texture id {} to texture id {}", id, texture_id); animated_texture_ids.insert(id, texture_id); } for id in alt_corresp_file_ids { debug!("map disk texture id {} to texture id {}", id, texture_id); animated_texture_ids.insert(id, texture_id); } // push the sequenced texture textures.push(BspTexture { name: name.to_owned(), width, height, kind: BspTextureKind::Animated { primary, alternate }, }); } // build disk-to-memory texture id map for file_texture_id in 0..tex_count { texture_ids.push(if let Some(id) = static_texture_ids.get(&file_texture_id) { *id } else if let Some(id) = animated_texture_ids.get(&file_texture_id) { *id } else { panic!( "Texture sequencing failed: texture with id {} unaccounted for", file_texture_id ); }); } reader.seek(SeekFrom::Start(vert_section.offset))?; let mut vertices = Vec::with_capacity(vert_count); for _ in 0..vert_count { vertices.push(read_f32_3(&mut reader)?.into()); } table.check_end_position(&mut reader, BspFileSectionId::Vertices)?; reader.seek(SeekFrom::Start(vis_section.offset))?; // visibility data let mut vis_data = Vec::with_capacity(vis_section.size); (&mut reader) .take(vis_section.size as u64) .read_to_end(&mut vis_data)?; table.check_end_position(&mut reader, BspFileSectionId::Visibility)?; // render nodes reader.seek(SeekFrom::Start(render_node_section.offset))?; debug!("Render node count = {}", render_node_count); let mut render_nodes = Vec::with_capacity(render_node_count); for _ in 0..render_node_count { render_nodes.push(load_render_node(&mut reader)?); } table.check_end_position(&mut reader, BspFileSectionId::RenderNodes)?; // texinfo reader.seek(SeekFrom::Start(texinfo_section.offset))?; let mut texinfo = Vec::with_capacity(texinfo_count); for _ in 0..texinfo_count { let mut txi = load_texinfo(&mut reader, tex_count)?; // !!! IMPORTANT !!! // remap texture ids from the on-disk ids to our ids txi.tex_id = texture_ids[txi.tex_id]; texinfo.push(txi); } table.check_end_position(&mut reader, BspFileSectionId::TextureInfo)?; reader.seek(SeekFrom::Start(face_section.offset))?; let mut faces = Vec::with_capacity(face_count); for _ in 0..face_count { let plane_id = reader.read_i16::()?; if plane_id < 0 || plane_id as usize > plane_count { bail!("Invalid plane count"); } let side = match reader.read_i16::()? { 0 => BspFaceSide::Front, 1 => BspFaceSide::Back, _ => bail!("Invalid face side"), }; let edge_id = reader.read_i32::()?; if edge_id < 0 { bail!("Invalid edge ID"); } let edge_count = reader.read_i16::()?; if edge_count < 3 { bail!("Invalid edge count"); } let texinfo_id = reader.read_i16::()?; if texinfo_id < 0 || texinfo_id as usize > texinfo_count { bail!("Invalid texinfo ID"); } let mut light_styles = [0; MAX_LIGHTSTYLES]; for i in 0..light_styles.len() { light_styles[i] = reader.read_u8()?; } let lightmap_id = match reader.read_i32::()? { o if o < -1 => bail!("Invalid lightmap offset"), -1 => None, o => Some(o as usize), }; faces.push(BspFace { plane_id: plane_id as usize, side, edge_id: edge_id as usize, edge_count: edge_count as usize, texinfo_id: texinfo_id as usize, light_styles, lightmap_id, texture_mins: [0, 0], extents: [0, 0], }); } table.check_end_position(&mut reader, BspFileSectionId::Faces)?; reader.seek(SeekFrom::Start(lightmap_section.offset))?; let mut lightmaps = Vec::with_capacity(lightmap_section.size); (&mut reader) .take(lightmap_section.size as u64) .read_to_end(&mut lightmaps)?; table.check_end_position(&mut reader, BspFileSectionId::Lightmaps)?; reader.seek(SeekFrom::Start(collision_node_section.offset))?; let mut collision_nodes = Vec::with_capacity(collision_node_count); for _ in 0..collision_node_count { let plane_id = match reader.read_i32::()? { x if x < 0 => bail!("Invalid plane id"), x => x as usize, }; let front = match reader.read_i16::()? { x if x < 0 => match BspLeafContents::from_i16(-x) { Some(c) => BspCollisionNodeChild::Contents(c), None => bail!("Invalid leaf contents ({})", -x), }, x => BspCollisionNodeChild::Node(x as usize), }; let back = match reader.read_i16::()? { x if x < 0 => match BspLeafContents::from_i16(-x) { Some(c) => BspCollisionNodeChild::Contents(c), None => bail!("Invalid leaf contents ({})", -x), }, x => BspCollisionNodeChild::Node(x as usize), }; collision_nodes.push(BspCollisionNode { plane_id, children: [front, back], }); } let collision_nodes_rc = Rc::new(collision_nodes.into_boxed_slice()); let hull_1 = BspCollisionHull { planes: planes_rc.clone(), nodes: collision_nodes_rc.clone(), node_id: 0, node_count: collision_node_count, mins: Vector3::new(-16.0, -16.0, -24.0), maxs: Vector3::new(16.0, 16.0, 32.0), }; let hull_2 = BspCollisionHull { planes: planes_rc.clone(), nodes: collision_nodes_rc.clone(), node_id: 0, node_count: collision_node_count, mins: Vector3::new(-32.0, -32.0, -24.0), maxs: Vector3::new(32.0, 32.0, 64.0), }; if reader.seek(SeekFrom::Current(0))? != reader.seek(SeekFrom::Start( collision_node_section.offset + collision_node_section.size as u64, ))? { bail!("BSP read data misaligned"); } reader.seek(SeekFrom::Start(leaf_section.offset))?; let mut leaves = Vec::with_capacity(leaf_count); // leaves.push(BspLeaf { // contents: BspLeafContents::Solid, // vis_offset: None, // min: [-32768, -32768, -32768], // max: [32767, 32767, 32767], // facelist_id: 0, // facelist_count: 0, // sounds: [0u8; NUM_AMBIENTS], // }); for _ in 0..leaf_count { // note the negation here (the constants are negative in the original engine to differentiate // them from plane IDs) let contents_id = -reader.read_i32::()?; let contents = match BspLeafContents::from_i32(contents_id) { Some(c) => c, None => bail!("Invalid leaf contents ({})", contents_id), }; let vis_offset = match reader.read_i32::()? { x if x < -1 => bail!("Invalid visibility data offset"), -1 => None, x => Some(x as usize), }; let min = read_i16_3(&mut reader)?; let max = read_i16_3(&mut reader)?; let facelist_id = reader.read_u16::()? as usize; let facelist_count = reader.read_u16::()? as usize; let mut sounds = [0u8; NUM_AMBIENTS]; reader.read(&mut sounds)?; leaves.push(BspLeaf { contents, vis_offset, min, max, facelist_id, facelist_count, sounds, }); } table.check_end_position(&mut reader, BspFileSectionId::Leaves)?; reader.seek(SeekFrom::Start(facelist_section.offset))?; let mut facelist = Vec::with_capacity(facelist_count); for _ in 0..facelist_count { facelist.push(reader.read_u16::()? as usize); } if reader.seek(SeekFrom::Current(0))? != reader.seek(SeekFrom::Start( facelist_section.offset + facelist_section.size as u64, ))? { bail!("BSP read data misaligned"); } reader.seek(SeekFrom::Start(edge_section.offset))?; let mut edges = Vec::with_capacity(edge_count); for _ in 0..edge_count { edges.push(BspEdge { vertex_ids: [ reader.read_u16::()?, reader.read_u16::()?, ], }); } table.check_end_position(&mut reader, BspFileSectionId::Edges)?; reader.seek(SeekFrom::Start(edgelist_section.offset))?; let mut edgelist = Vec::with_capacity(edgelist_count); for _ in 0..edgelist_count { edgelist.push(match reader.read_i32::()? { x if x >= 0 => BspEdgeIndex { direction: BspEdgeDirection::Forward, index: x as usize, }, x if x < 0 => BspEdgeIndex { direction: BspEdgeDirection::Backward, index: -x as usize, }, x => bail!(format!("Invalid edge index {}", x)), }); } if reader.seek(SeekFrom::Current(0))? != reader.seek(SeekFrom::Start( edgelist_section.offset + edgelist_section.size as u64, ))? { bail!("BSP read data misaligned"); } // see Calc_SurfaceExtents, // https://github.com/id-Software/Quake/blob/master/WinQuake/gl_model.c#L705-L749 for (face_id, face) in faces.iter_mut().enumerate() { let texinfo = &texinfo[face.texinfo_id]; let mut s_min = ::std::f32::INFINITY; let mut t_min = ::std::f32::INFINITY; let mut s_max = ::std::f32::NEG_INFINITY; let mut t_max = ::std::f32::NEG_INFINITY; for edge_idx in &edgelist[face.edge_id..face.edge_id + face.edge_count] { let vertex_id = edges[edge_idx.index].vertex_ids[edge_idx.direction as usize] as usize; let vertex = vertices[vertex_id]; let s = texinfo.s_vector.dot(vertex) + texinfo.s_offset; let t = texinfo.t_vector.dot(vertex) + texinfo.t_offset; s_min = s_min.min(s); s_max = s_max.max(s); t_min = t_min.min(t); t_max = t_max.max(t); } let b_mins = [(s_min / 16.0).floor(), (t_min / 16.0).floor()]; let b_maxs = [(s_max / 16.0).ceil(), (t_max / 16.0).ceil()]; for i in 0..2 { face.texture_mins[i] = b_mins[i] as i16 * 16; face.extents[i] = (b_maxs[i] - b_mins[i]) as i16 * 16; if !texinfo.special && face.extents[i] > 2000 { bail!( "Bad face extents: face {}, texture {}: {:?}", face_id, textures[texinfo.tex_id].name, face.extents ); } } } // see Mod_MakeHull0, // https://github.com/id-Software/Quake/blob/master/WinQuake/gl_model.c#L1001-L1031 // // This essentially duplicates the render nodes into a tree of collision nodes. let mut render_as_collision_nodes = Vec::with_capacity(render_nodes.len()); for i in 0..render_nodes.len() { render_as_collision_nodes.push(BspCollisionNode { plane_id: render_nodes[i].plane_id, children: [ match render_nodes[i].children[0] { BspRenderNodeChild::Node(n) => BspCollisionNodeChild::Node(n), BspRenderNodeChild::Leaf(l) => { BspCollisionNodeChild::Contents(leaves[l].contents) } }, match render_nodes[i].children[1] { BspRenderNodeChild::Node(n) => BspCollisionNodeChild::Node(n), BspRenderNodeChild::Leaf(l) => { BspCollisionNodeChild::Contents(leaves[l].contents) } }, ], }) } let render_as_collision_nodes_rc = Rc::new(render_as_collision_nodes.into_boxed_slice()); let hull_0 = BspCollisionHull { planes: planes_rc.clone(), nodes: render_as_collision_nodes_rc.clone(), node_id: 0, node_count: render_as_collision_nodes_rc.len(), mins: Vector3::new(0.0, 0.0, 0.0), maxs: Vector3::new(0.0, 0.0, 0.0), }; let bsp_data = Rc::new(BspData { planes: planes_rc.clone(), textures: textures.into_boxed_slice(), vertices: vertices.into_boxed_slice(), visibility: vis_data.into_boxed_slice(), render_nodes: render_nodes.into_boxed_slice(), texinfo: texinfo.into_boxed_slice(), faces: faces.into_boxed_slice(), lightmaps: lightmaps.into_boxed_slice(), hulls: [hull_0, hull_1, hull_2], leaves: leaves.into_boxed_slice(), facelist: facelist.into_boxed_slice(), edges: edges.into_boxed_slice(), edgelist: edgelist.into_boxed_slice(), }); reader.seek(SeekFrom::Start(model_section.offset))?; let mut total_leaf_count = 0; let mut brush_models = Vec::with_capacity(model_count); for i in 0..model_count { // pad the bounding box by one unit in all directions let min = Vector3::from(read_f32_3(&mut reader)?) - Vector3::new(1.0, 1.0, 1.0); let max = Vector3::from(read_f32_3(&mut reader)?) + Vector3::new(1.0, 1.0, 1.0); let origin = read_f32_3(&mut reader)?.into(); debug!("model[{}].min = {:?}", i, min); debug!("model[{}].max = {:?}", i, max); debug!("model[{}].origin = {:?}", i, max); let mut collision_node_ids = [0; MAX_HULLS]; for i in 0..collision_node_ids.len() { collision_node_ids[i] = match reader.read_i32::()? { r if r < 0 => bail!("Invalid collision tree root node"), r => r as usize, }; } // throw away the last collision node ID -- BSP files make room for 4 collision hulls but // only 3 are ever used. reader.read_i32::()?; debug!("model[{}].headnodes = {:?}", i, collision_node_ids); let leaf_id = total_leaf_count; debug!("model[{}].leaf_id = {:?}", i, leaf_id); let leaf_count = match reader.read_i32::()? { x if x < 0 => bail!("Invalid leaf count"), x => x as usize, }; total_leaf_count += leaf_count; debug!("model[{}].leaf_count = {:?}", i, leaf_count); let face_id = match reader.read_i32::()? { x if x < 0 => bail!("Invalid face id"), x => x as usize, }; let face_count = match reader.read_i32::()? { x if x < 0 => bail!("Invalid face count"), x => x as usize, }; let mut collision_node_counts = [0; MAX_HULLS]; for i in 0..collision_node_counts.len() { collision_node_counts[i] = collision_node_count - collision_node_ids[i]; } brush_models.push(BspModel { bsp_data: bsp_data.clone(), min, max, origin, collision_node_ids, collision_node_counts, leaf_id, leaf_count, face_id, face_count, }); } table.check_end_position(&mut reader, BspFileSectionId::Models)?; let models = brush_models .into_iter() .enumerate() .map(|(i, bmodel)| Model::from_brush_model(format!("*{}", i), bmodel)) .collect(); Ok((models, ent_string)) } fn read_i16_3(reader: &mut R) -> Result<[i16; 3], std::io::Error> where R: ReadBytesExt, { let mut ar = [0i16; 3]; reader.read_i16_into::(&mut ar)?; Ok(ar) } ================================================ FILE: src/common/bsp/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // TODO: // - Replace index fields with direct references where possible //! Quake BSP file and data structure handling. //! //! # Data Structure //! //! The binary space partitioning tree, or BSP, is the central data structure used by the Quake //! engine for collision detection and rendering level geometry. At its core, the BSP tree is a //! binary search tree with each node representing a subspace of the map. The tree is navigated //! using the planes stored in each node; each child represents one side of the plane. //! //! # File Format //! //! The BSP file header consists only of the file format version number, stored as an `i32`. //! //! This is followed by a series of "lumps" (as they are called in the Quake source code), //! which act as a directory into the BSP file data. There are 15 of these lumps, each //! consisting of a 32-bit offset (into the file data) and a 32-bit size (in bytes). //! //! ## Entities //! Lump 0 points to the level entity data, which is stored in a JSON-like dictionary //! format. Entities are anonymous; they do not have names, only attributes. They are stored //! as follows: //! //! ```text //! { //! "attribute0" "value0" //! "attribute1" "value1" //! "attribute2" "value2" //! } //! { //! "attribute0" "value0" //! "attribute1" "value1" //! "attribute2" "value2" //! } //! ``` //! //! The newline character is `0x0A` (line feed). The entity data is stored as a null-terminated //! string (it ends when byte `0x00` is reached). //! //! ## Planes //! //! Lump 1 points to the planes used to partition the map, stored in point-normal form as 4 IEEE 754 //! single-precision floats. The first 3 floats form the normal vector for the plane, and the last //! float specifies the distance from the map origin along the line defined by the normal vector. //! //! ## Textures //! //! The textures are preceded by a 32-bit integer count and a list of 32-bit integer offsets. The //! offsets are given in bytes from the beginning of the texture section (the offset given by the //! texture lump at the start of the file). //! //! The textures themselves consist of a 16-byte name field, a 32-bit integer width, a 32-bit //! integer height, and 4 32-bit mipmap offsets. These offsets are given in bytes from the beginning //! of the texture. Each mipmap has its dimensions halved (i.e. its area quartered) from the //! previous mipmap: the first is full size, the second 1/4, the third 1/16, and the last 1/64. Each //! byte represents one pixel and contains an index into `gfx/palette.lmp`. //! //! ### Texture sequencing //! //! Animated textures are stored as individual frames with no guarantee of being in the correct //! order. This means that animated textures must be sequenced when the map is loaded. Frames of //! animated textures have names beginning with `U+002B PLUS SIGN` (`+`). //! //! Each texture can have two animations of up to MAX_TEXTURE_FRAMES frames each. The character //! following the plus sign determines whether the frame belongs to the first or second animation. //! //! If it is between `U+0030 DIGIT ZERO` (`0`) and `U+0039 DIGIT NINE` (`9`), then the character //! represents that texture's frame index in the first animation sequence. //! //! If it is between `U+0041 LATIN CAPITAL LETTER A` (`A`) and `U+004A LATIN CAPITAL LETTER J`, or //! between `U+0061 LATIN SMALL LETTER A` (`a`) and `U+006A LATIN SMALL LETTER J`, then the //! character represents that texture's frame index in the second animation sequence as that //! letter's position in the English alphabet (that is, `A`/`a` correspond to 0 and `J`/`j` //! correspond to 9). //! //! ## Vertex positions //! //! The vertex positions are stored as 3-component vectors of `float`. The Quake coordinate system //! defines X as the longitudinal axis, Y as the lateral axis, and Z as the vertical axis. //! //! # Visibility lists //! //! The visibility lists are simply stored as a series of run-length encoded bit strings. The total //! size of the visibility data is given by the lump size. //! //! ## Nodes //! //! Nodes are stored with a 32-bit integer plane ID denoting which plane splits the node. This is //! followed by two 16-bit integers which point to the children in front and back of the plane. If //! the high bit is set, the ID points to a leaf node; if not, it points to another internal node. //! //! After the node IDs are a 16-bit integer face ID, which denotes the index of the first face in //! the face list that belongs to this node, and a 16-bit integer face count, which denotes the //! number of faces to draw starting with the face ID. //! //! ## Edges //! //! The edges are stored as a pair of 16-bit integer vertex IDs. mod load; use std::{collections::HashSet, error::Error, fmt, iter::Iterator, rc::Rc}; use crate::common::math::{Hyperplane, HyperplaneSide, LinePlaneIntersect}; // TODO: Either Trace should be moved into common or the functions requiring it should be moved into server use crate::server::world::{Trace, TraceEnd, TraceStart}; use cgmath::Vector3; use chrono::Duration; pub use self::load::{load, BspFileError}; // this is 4 in the original source, but the 4th hull is never used. const MAX_HULLS: usize = 3; pub const MAX_LIGHTMAPS: usize = 64; pub const MAX_LIGHTSTYLES: usize = 4; pub const MAX_SOUNDS: usize = 4; pub const MIPLEVELS: usize = 4; const DIST_EPSILON: f32 = 0.03125; pub fn frame_duration() -> Duration { Duration::milliseconds(200) } #[derive(Debug)] pub enum BspError { Io(::std::io::Error), Other(String), } impl BspError { fn with_msg(msg: S) -> Self where S: AsRef, { BspError::Other(msg.as_ref().to_owned()) } } impl fmt::Display for BspError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { BspError::Io(ref err) => err.fmt(f), BspError::Other(ref msg) => write!(f, "{}", msg), } } } impl Error for BspError { fn description(&self) -> &str { match *self { BspError::Io(ref err) => err.description(), BspError::Other(ref msg) => &msg, } } } impl From<::std::io::Error> for BspError { fn from(error: ::std::io::Error) -> Self { BspError::Io(error) } } #[derive(Copy, Clone, Debug, FromPrimitive)] pub enum BspTextureMipmap { Full = 0, Half = 1, Quarter = 2, Eighth = 3, } #[derive(Debug)] pub struct BspTextureFrame { mipmaps: [Vec; MIPLEVELS], } impl BspTextureFrame { pub fn mipmap(&self, level: BspTextureMipmap) -> &[u8] { &self.mipmaps[level as usize] } } #[derive(Debug)] pub enum BspTextureKind { Static(BspTextureFrame), Animated { primary: Vec, alternate: Option>, }, } #[derive(Debug)] pub struct BspTexture { name: String, width: u32, height: u32, kind: BspTextureKind, } impl BspTexture { /// Returns the name of the texture. pub fn name(&self) -> &str { self.name.as_ref() } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } /// Returns a tuple containing the width and height of the texture. pub fn dimensions(&self) -> (u32, u32) { (self.width, self.height) } /// Returns this texture's animation data, if any. pub fn kind(&self) -> &BspTextureKind { &self.kind } } #[derive(Debug)] pub enum BspRenderNodeChild { Node(usize), Leaf(usize), } #[derive(Debug)] pub struct BspRenderNode { pub plane_id: usize, pub children: [BspRenderNodeChild; 2], pub min: [i16; 3], pub max: [i16; 3], pub face_id: usize, pub face_count: usize, } #[derive(Debug)] pub struct BspTexInfo { pub s_vector: Vector3, pub s_offset: f32, pub t_vector: Vector3, pub t_offset: f32, pub tex_id: usize, pub special: bool, } #[derive(Copy, Clone, Debug)] pub enum BspFaceSide { Front, Back, } #[derive(Debug)] pub struct BspFace { pub plane_id: usize, pub side: BspFaceSide, pub edge_id: usize, pub edge_count: usize, pub texinfo_id: usize, pub light_styles: [u8; MAX_LIGHTSTYLES], pub lightmap_id: Option, pub texture_mins: [i16; 2], pub extents: [i16; 2], } /// The contents of a leaf in the BSP tree, specifying how it should look and behave. #[derive(Copy, Clone, Debug, Eq, FromPrimitive, PartialEq)] pub enum BspLeafContents { /// The leaf has nothing in it. Vision is unobstructed and movement is unimpeded. Empty = 1, /// The leaf is solid. Physics objects will collide with its surface and may not move inside it. Solid = 2, /// The leaf is full of water. Vision is warped to simulate refraction and movement is done by /// swimming instead of walking. Water = 3, /// The leaf is full of acidic slime. Vision is tinted green, movement is done by swimming and /// entities take periodic minor damage. Slime = 4, /// The leaf is full of lava. Vision is tinted red, movement is done by swimming and entities /// take periodic severe damage. Lava = 5, // This doesn't appear to ever be used // Sky = 6, // This is removed during map compilation // Origin = 7, // This is converted to `BspLeafContents::Solid` // Collide = 8, /// Same as `BspLeafContents::Water`, but the player is constantly pushed in the positive /// x-direction (east). Current0 = 9, /// Same as `BspLeafContents::Water`, but the player is constantly pushed in the positive /// y-direction (north). Current90 = 10, /// Same as `BspLeafContents::Water`, but the player is constantly pushed in the negative /// x-direction (west). Current180 = 11, /// Same as `BspLeafContents::Water`, but the player is constantly pushed in the negative /// y-direction (south). Current270 = 12, /// Same as `BspLeafContents::Water`, but the player is constantly pushed in the positive /// z-direction (up). CurrentUp = 13, /// Same as `BspLeafContents::Water`, but the player is constantly pushed in the negative /// z-direction (down). CurrentDown = 14, } #[derive(Debug)] pub enum BspCollisionNodeChild { Node(usize), Contents(BspLeafContents), } #[derive(Debug)] pub struct BspCollisionNode { plane_id: usize, children: [BspCollisionNodeChild; 2], } #[derive(Debug)] pub struct BspCollisionHull { planes: Rc>, nodes: Rc>, node_id: usize, node_count: usize, mins: Vector3, maxs: Vector3, } impl BspCollisionHull { // TODO: see if we can't make this a little less baffling /// Constructs a collision hull with the given minimum and maximum bounds. /// /// This generates six planes which intersect to form a rectangular prism. The interior of the /// prism is `BspLeafContents::Solid`; the exterior is `BspLeafContents::Empty`. pub fn for_bounds( mins: Vector3, maxs: Vector3, ) -> Result { debug!( "Generating collision hull for min = {:?} max = {:?}", mins, maxs ); if mins.x >= maxs.x || mins.y >= maxs.y || mins.z >= maxs.z { return Err(BspError::with_msg("min bound exceeds max bound")); } let mut nodes = Vec::new(); let mut planes = Vec::new(); // front plane (positive x) planes.push(Hyperplane::axis_x(maxs.x)); nodes.push(BspCollisionNode { plane_id: 0, children: [ BspCollisionNodeChild::Contents(BspLeafContents::Empty), BspCollisionNodeChild::Node(1), ], }); // back plane (negative x) planes.push(Hyperplane::axis_x(mins.x)); nodes.push(BspCollisionNode { plane_id: 1, children: [ BspCollisionNodeChild::Node(2), BspCollisionNodeChild::Contents(BspLeafContents::Empty), ], }); // left plane (positive y) planes.push(Hyperplane::axis_y(maxs.y)); nodes.push(BspCollisionNode { plane_id: 2, children: [ BspCollisionNodeChild::Contents(BspLeafContents::Empty), BspCollisionNodeChild::Node(3), ], }); // right plane (negative y) planes.push(Hyperplane::axis_y(mins.y)); nodes.push(BspCollisionNode { plane_id: 3, children: [ BspCollisionNodeChild::Node(4), BspCollisionNodeChild::Contents(BspLeafContents::Empty), ], }); // top plane (positive z) planes.push(Hyperplane::axis_z(maxs.z)); nodes.push(BspCollisionNode { plane_id: 4, children: [ BspCollisionNodeChild::Contents(BspLeafContents::Empty), BspCollisionNodeChild::Node(5), ], }); // bottom plane (negative z) planes.push(Hyperplane::axis_z(mins.z)); nodes.push(BspCollisionNode { plane_id: 5, children: [ BspCollisionNodeChild::Contents(BspLeafContents::Solid), BspCollisionNodeChild::Contents(BspLeafContents::Empty), ], }); Ok(BspCollisionHull { planes: Rc::new(planes.into_boxed_slice()), nodes: Rc::new(nodes.into_boxed_slice()), node_id: 0, node_count: 6, mins, maxs, }) } pub fn min(&self) -> Vector3 { self.mins } pub fn max(&self) -> Vector3 { self.maxs } /// Returns the leaf contents at the given point in this hull. pub fn contents_at_point(&self, point: Vector3) -> Result { self.contents_at_point_node(self.node_id, point) } fn contents_at_point_node( &self, node: usize, point: Vector3, ) -> Result { let mut current_node = &self.nodes[node]; loop { let ref plane = self.planes[current_node.plane_id]; match current_node.children[plane.point_side(point) as usize] { BspCollisionNodeChild::Contents(c) => return Ok(c), BspCollisionNodeChild::Node(n) => current_node = &self.nodes[n], } } } pub fn trace(&self, start: Vector3, end: Vector3) -> Result { self.recursive_trace(self.node_id, start, end) } fn recursive_trace( &self, node: usize, start: Vector3, end: Vector3, ) -> Result { debug!("start={:?} end={:?}", start, end); let ref node = self.nodes[node]; let ref plane = self.planes[node.plane_id]; match plane.line_segment_intersection(start, end) { // start -> end falls entirely on one side of the plane LinePlaneIntersect::NoIntersection(side) => { debug!("No intersection"); match node.children[side as usize] { // this is an internal node, keep searching for a leaf BspCollisionNodeChild::Node(n) => { debug!("Descending to {:?} node with ID {}", side, n); self.recursive_trace(n, start, end) } // start -> end falls entirely inside a leaf BspCollisionNodeChild::Contents(c) => { debug!("Found leaf with contents {:?}", c); Ok(Trace::new( TraceStart::new(start, 0.0), TraceEnd::terminal(end), c, )) } } } // start -> end crosses the plane at one point LinePlaneIntersect::PointIntersection(point_intersect) => { let near_side = plane.point_side(start); let far_side = plane.point_side(end); let mid = point_intersect.point(); let ratio = point_intersect.ratio(); debug!("Intersection at {:?} (ratio={})", mid, ratio); // calculate the near subtrace let near = match node.children[near_side as usize] { BspCollisionNodeChild::Node(near_n) => { debug!( "Descending to near ({:?}) node with ID {}", near_side, near_n ); self.recursive_trace(near_n, start, mid)? } BspCollisionNodeChild::Contents(near_c) => { debug!("Found near leaf with contents {:?}", near_c); Trace::new( TraceStart::new(start, 0.0), TraceEnd::boundary( mid, ratio, match near_side { HyperplaneSide::Positive => plane.to_owned(), HyperplaneSide::Negative => -plane.to_owned(), }, ), near_c, ) } }; // check for an early collision if near.is_terminal() || near.end_point() != point_intersect.point() { return Ok(near); } // if we haven't collided yet, calculate the far subtrace let far = match node.children[far_side as usize] { BspCollisionNodeChild::Node(far_n) => { debug!("Descending to far ({:?}) node with ID {}", far_side, far_n); self.recursive_trace(far_n, mid, end)? } BspCollisionNodeChild::Contents(far_c) => { debug!("Found far leaf with contents {:?}", far_c); Trace::new(TraceStart::new(mid, ratio), TraceEnd::terminal(end), far_c) } }; // check for collision and join traces accordingly Ok(near.join(far)) } } } pub fn gen_dot_graph(&self) -> String { let mut dot = String::new(); dot += "digraph hull {\n"; dot += " rankdir=LR\n"; let mut rank_lists = Vec::new(); let mut leaf_names = Vec::new(); dot += &self.gen_dot_graph_recursive(0, &mut rank_lists, &mut leaf_names, self.node_id); for rank in rank_lists { dot += " {rank=same;"; for node_id in rank { dot += &format!("n{},", node_id); } // discard trailing comma dot.pop().unwrap(); dot += "}\n" } dot += " {rank=same;"; for leaf in leaf_names { dot += &format!("{},", leaf); } // discard trailing comma dot.pop().unwrap(); dot.pop().unwrap(); dot += "}\n"; dot += "}"; dot } fn gen_dot_graph_recursive( &self, rank: usize, rank_lists: &mut Vec>, leaf_names: &mut Vec, node_id: usize, ) -> String { let mut result = String::new(); if rank >= rank_lists.len() { rank_lists.push(HashSet::new()); } rank_lists[rank].insert(node_id); for child in self.nodes[node_id].children.iter() { match child { &BspCollisionNodeChild::Node(n) => { result += &format!(" n{} -> n{}\n", node_id, n); result += &self.gen_dot_graph_recursive(rank + 1, rank_lists, leaf_names, n); } &BspCollisionNodeChild::Contents(_) => { let leaf_count = leaf_names.len(); let leaf_name = format!("l{}", leaf_count); result += &format!(" n{} -> {}\n", node_id, leaf_name); leaf_names.push(leaf_name); } } } result } } #[derive(Debug)] pub struct BspLeaf { pub contents: BspLeafContents, pub vis_offset: Option, pub min: [i16; 3], pub max: [i16; 3], pub facelist_id: usize, pub facelist_count: usize, pub sounds: [u8; MAX_SOUNDS], } #[derive(Debug)] pub struct BspEdge { pub vertex_ids: [u16; 2], } #[derive(Copy, Clone, Debug)] pub enum BspEdgeDirection { Forward = 0, Backward = 1, } #[derive(Debug)] pub struct BspEdgeIndex { pub direction: BspEdgeDirection, pub index: usize, } #[derive(Debug)] pub struct BspLightmap<'a> { width: u32, height: u32, data: &'a [u8], } impl<'a> BspLightmap<'a> { pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } pub fn data(&self) -> &[u8] { self.data } } #[derive(Debug)] pub struct BspData { pub(crate) planes: Rc>, pub(crate) textures: Box<[BspTexture]>, pub(crate) vertices: Box<[Vector3]>, pub(crate) visibility: Box<[u8]>, pub(crate) render_nodes: Box<[BspRenderNode]>, pub(crate) texinfo: Box<[BspTexInfo]>, pub(crate) faces: Box<[BspFace]>, pub(crate) lightmaps: Box<[u8]>, pub(crate) leaves: Box<[BspLeaf]>, pub(crate) facelist: Box<[usize]>, pub(crate) edges: Box<[BspEdge]>, pub(crate) edgelist: Box<[BspEdgeIndex]>, pub(crate) hulls: [BspCollisionHull; MAX_HULLS], } impl BspData { pub fn planes(&self) -> &[Hyperplane] { &self.planes } pub fn textures(&self) -> &[BspTexture] { &self.textures } pub fn vertices(&self) -> &[Vector3] { &self.vertices } pub fn render_nodes(&self) -> &[BspRenderNode] { &self.render_nodes } pub fn texinfo(&self) -> &[BspTexInfo] { &self.texinfo } pub fn face(&self, face_id: usize) -> &BspFace { &self.faces[face_id] } pub fn face_iter_vertices(&self, face_id: usize) -> impl Iterator> + '_ { let face = &self.faces[face_id]; self.edgelist[face.edge_id..face.edge_id + face.edge_count] .iter() .map(move |id| { self.vertices[self.edges[id.index].vertex_ids[id.direction as usize] as usize] }) } pub fn face_texinfo(&self, face_id: usize) -> &BspTexInfo { &self.texinfo[self.faces[face_id].texinfo_id] } pub fn face_lightmaps(&self, face_id: usize) -> Vec { let face = &self.faces[face_id]; match face.lightmap_id { Some(lightmap_id) => { let lightmap_w = face.extents[0] as u32 / 16 + 1; let lightmap_h = face.extents[1] as u32 / 16 + 1; let lightmap_size = (lightmap_w * lightmap_h) as usize; face.light_styles .iter() .take_while(|style| **style != 255) .enumerate() .map(|(i, _)| { let start = lightmap_id + lightmap_size * i as usize; let end = start + lightmap_size; BspLightmap { width: lightmap_w, height: lightmap_h, data: &self.lightmaps[start..end], } }) .collect() } None => Vec::new(), } } pub fn faces(&self) -> &[BspFace] { &self.faces } pub fn lightmaps(&self) -> &[u8] { &self.lightmaps } pub fn leaves(&self) -> &[BspLeaf] { &self.leaves } pub fn facelist(&self) -> &[usize] { &self.facelist } pub fn edges(&self) -> &[BspEdge] { &self.edges } pub fn edgelist(&self) -> &[BspEdgeIndex] { &self.edgelist } pub fn hulls(&self) -> &[BspCollisionHull] { &self.hulls } /// Locates the leaf containing the given position vector and returns its index. pub fn find_leaf(&self, pos: V) -> usize where V: Into>, { let pos_vec = pos.into(); let mut node = &self.render_nodes[0]; loop { let plane = &self.planes[node.plane_id]; match node.children[plane.point_side(pos_vec) as usize] { BspRenderNodeChild::Node(node_id) => { node = &self.render_nodes[node_id]; } BspRenderNodeChild::Leaf(leaf_id) => return leaf_id, } } } pub fn get_pvs(&self, leaf_id: usize, leaf_count: usize) -> Vec { // leaf 0 is outside the map, everything is visible if leaf_id == 0 { return Vec::new(); } match self.leaves[leaf_id].vis_offset { Some(o) => { let mut visleaf = 1; let mut visleaf_list = Vec::new(); let mut it = (&self.visibility[o..]).iter(); while visleaf < leaf_count { let byte = it.next().unwrap(); match *byte { // a zero byte signals the start of an RLE sequence 0 => visleaf += 8 * *it.next().unwrap() as usize, bits => { for shift in 0..8 { if bits & 1 << shift != 0 { visleaf_list.push(visleaf); } visleaf += 1; } } } } visleaf_list } None => Vec::new(), } } pub fn gen_dot_graph(&self) -> String { let mut dot = String::new(); dot += "digraph render {\n"; dot += " rankdir=LR\n"; let mut rank_lists = Vec::new(); let mut leaf_names = Vec::new(); dot += &self.gen_dot_graph_recursive(0, &mut rank_lists, &mut leaf_names, 0); for rank in rank_lists { dot += " {rank=same;"; for node_id in rank { dot += &format!("n{},", node_id); } // discard trailing comma dot.pop().unwrap(); dot += "}\n" } dot += " {rank=same;"; for leaf_id in 1..self.leaves().len() { dot += &format!("l{},", leaf_id); } // discard trailing comma dot.pop().unwrap(); dot += "}\n"; dot += "}"; dot } fn gen_dot_graph_recursive( &self, rank: usize, rank_lists: &mut Vec>, leaf_names: &mut Vec, node_id: usize, ) -> String { let mut result = String::new(); if rank >= rank_lists.len() { rank_lists.push(HashSet::new()); } rank_lists[rank].insert(node_id); for child in self.render_nodes[node_id].children.iter() { match *child { BspRenderNodeChild::Node(n) => { result += &format!(" n{} -> n{}\n", node_id, n); result += &self.gen_dot_graph_recursive(rank + 1, rank_lists, leaf_names, n); } BspRenderNodeChild::Leaf(leaf_id) => match leaf_id { 0 => { result += &format!( " l0_{0} [shape=point label=\"\"]\n n{0} -> l0_{0}\n", node_id ); } _ => result += &format!(" n{} -> l{}\n", node_id, leaf_id), }, } } result } } #[derive(Debug)] pub struct BspModel { pub bsp_data: Rc, pub min: Vector3, pub max: Vector3, pub origin: Vector3, pub collision_node_ids: [usize; MAX_HULLS], pub collision_node_counts: [usize; MAX_HULLS], pub leaf_id: usize, pub leaf_count: usize, pub face_id: usize, pub face_count: usize, } impl BspModel { pub fn bsp_data(&self) -> Rc { self.bsp_data.clone() } /// Returns the minimum extent of this BSP model. pub fn min(&self) -> Vector3 { self.min } /// Returns the maximum extent of this BSP model. pub fn max(&self) -> Vector3 { self.max } /// Returns the size of this BSP model. pub fn size(&self) -> Vector3 { self.max - self.min } /// Returns the origin of this BSP model. pub fn origin(&self) -> Vector3 { self.origin } pub fn iter_leaves(&self) -> impl Iterator { // add 1 to leaf_count because...??? TODO: figure out if this is documented anywhere self.bsp_data.leaves[self.leaf_id..self.leaf_id + self.leaf_count + 1].iter() } pub fn iter_faces(&self) -> impl Iterator { self.bsp_data.facelist[self.face_id..self.face_id + self.face_count] .iter() .map(move |face_id| &self.bsp_data.faces[*face_id]) } pub fn face_list(&self) -> &[usize] { &self.bsp_data.facelist[self.face_id..self.face_id + self.face_count] } pub fn hull(&self, index: usize) -> Result { if index > MAX_HULLS { return Err(BspError::with_msg(format!( "Invalid hull index ({})", index ))); } let main_hull = &self.bsp_data.hulls[index]; Ok(BspCollisionHull { planes: main_hull.planes.clone(), nodes: main_hull.nodes.clone(), node_id: self.collision_node_ids[index], node_count: self.collision_node_counts[index], mins: main_hull.mins, maxs: main_hull.maxs, }) } } impl BspData {} #[cfg(test)] mod test { use super::*; use cgmath::Zero; #[test] fn test_hull_for_bounds() { let hull = BspCollisionHull::for_bounds(Vector3::zero(), Vector3::new(1.0, 1.0, 1.0)).unwrap(); let empty_points = vec![ // points strictly less than hull min should be empty Vector3::new(-1.0, -1.0, -1.0), // points strictly greater than hull max should be empty Vector3::new(2.0, 2.0, 2.0), // points in front of hull should be empty Vector3::new(2.0, 0.5, 0.5), // points behind hull should be empty Vector3::new(-1.0, 0.5, 0.5), // points left of hull should be empty Vector3::new(0.5, 2.0, 0.5), // points right of hull should be empty Vector3::new(0.5, -1.0, 0.5), // points above hull should be empty Vector3::new(0.5, 0.5, 2.0), // points below hull should be empty Vector3::new(0.5, 0.5, -1.0), ]; for point in empty_points { assert_eq!( hull.contents_at_point(point).unwrap(), BspLeafContents::Empty ); } let solid_points = vec![ // center of the hull should be solid Vector3::new(0.5, 0.5, 0.5), // various interior corners should be solid Vector3::new(0.01, 0.01, 0.01), Vector3::new(0.99, 0.01, 0.01), Vector3::new(0.01, 0.99, 0.01), Vector3::new(0.01, 0.01, 0.99), Vector3::new(0.99, 0.99, 0.01), Vector3::new(0.99, 0.01, 0.99), Vector3::new(0.01, 0.99, 0.99), Vector3::new(0.99, 0.99, 0.99), ]; for point in solid_points { assert_eq!( hull.contents_at_point(point).unwrap(), BspLeafContents::Solid ); } } } ================================================ FILE: src/common/console/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::{ cell::{Ref, RefCell}, collections::{HashMap, VecDeque}, fmt::Write, iter::FromIterator, rc::Rc, }; use crate::common::parse; use chrono::{Duration, Utc}; use thiserror::Error; #[derive(Error, Debug)] pub enum ConsoleError { #[error("{0}")] CmdError(String), #[error("Could not parse cvar as a number: {name} = \"{value}\"")] CvarParseFailed { name: String, value: String }, #[error("A command named \"{0}\" already exists")] DuplicateCommand(String), #[error("A cvar named \"{0}\" already exists")] DuplicateCvar(String), #[error("No such command: {0}")] NoSuchCommand(String), #[error("No such cvar: {0}")] NoSuchCvar(String), } type Cmd = Box String>; fn insert_name(names: &mut Vec, name: S) -> Result where S: AsRef, { let name = name.as_ref(); match names.binary_search_by(|item| item.as_str().cmp(name)) { Ok(i) => Err(i), Err(i) => { names.insert(i, name.to_owned()); Ok(i) } } } /// Stores console commands. pub struct CmdRegistry { cmds: HashMap, names: Rc>>, } impl CmdRegistry { pub fn new(names: Rc>>) -> CmdRegistry { CmdRegistry { cmds: HashMap::new(), names, } } /// Registers a new command with the given name. /// /// Returns an error if a command with the specified name already exists. pub fn insert(&mut self, name: S, cmd: Cmd) -> Result<(), ConsoleError> where S: AsRef, { let name = name.as_ref(); match self.cmds.get(name) { Some(_) => Err(ConsoleError::DuplicateCommand(name.to_owned()))?, None => { if insert_name(&mut self.names.borrow_mut(), name).is_err() { return Err(ConsoleError::DuplicateCvar(name.into())); } self.cmds.insert(name.to_owned(), cmd); } } Ok(()) } /// Registers a new command with the given name, or replaces one if the name is in use. pub fn insert_or_replace(&mut self, name: S, cmd: Cmd) -> Result<(), ConsoleError> where S: AsRef, { let name = name.as_ref(); // If the name isn't registered as a command and it exists in the name // table, it's a cvar. if !self.cmds.contains_key(name) && insert_name(&mut self.names.borrow_mut(), name).is_err() { return Err(ConsoleError::DuplicateCvar(name.into())); } self.cmds.insert(name.into(), cmd); Ok(()) } /// Removes the command with the given name. /// /// Returns an error if there was no command with that name. pub fn remove(&mut self, name: S) -> Result<(), ConsoleError> where S: AsRef, { if self.cmds.remove(name.as_ref()).is_none() { return Err(ConsoleError::NoSuchCommand(name.as_ref().to_string()))?; } let mut names = self.names.borrow_mut(); match names.binary_search_by(|item| item.as_str().cmp(name.as_ref())) { Ok(i) => drop(names.remove(i)), Err(_) => unreachable!("name in map but not in list: {}", name.as_ref()), } Ok(()) } /// Executes a command. /// /// Returns an error if no command with the specified name exists. pub fn exec(&mut self, name: S, args: &[&str]) -> Result where S: AsRef, { let cmd = self .cmds .get(name.as_ref()) .ok_or(ConsoleError::NoSuchCommand(name.as_ref().to_string()))?; Ok(cmd(args)) } pub fn contains(&self, name: S) -> bool where S: AsRef, { self.cmds.contains_key(name.as_ref()) } pub fn names(&self) -> Rc>> { self.names.clone() } } /// A configuration variable. /// /// Cvars are the primary method of configuring the game. #[derive(Debug)] struct Cvar { // Value of this variable val: String, // If true, this variable should be archived in vars.rc archive: bool, // If true: // - If a server cvar, broadcast updates to clients // - If a client cvar, update userinfo notify: bool, // The default value of this variable default: String, } #[derive(Debug)] pub struct CvarRegistry { cvars: RefCell>, names: Rc>>, } impl CvarRegistry { /// Construct a new empty `CvarRegistry`. pub fn new(names: Rc>>) -> CvarRegistry { CvarRegistry { cvars: RefCell::new(HashMap::new()), names, } } fn register_impl( &self, name: S, default: S, archive: bool, notify: bool, ) -> Result<(), ConsoleError> where S: AsRef, { let name = name.as_ref(); let default = default.as_ref(); let mut cvars = self.cvars.borrow_mut(); match cvars.get(name) { Some(_) => Err(ConsoleError::DuplicateCvar(name.into()))?, None => { if insert_name(&mut self.names.borrow_mut(), name).is_err() { return Err(ConsoleError::DuplicateCommand(name.into())); } cvars.insert( name.to_owned(), Cvar { val: default.to_owned(), archive, notify, default: default.to_owned(), }, ); } } Ok(()) } /// Register a new `Cvar` with the given name. pub fn register(&self, name: S, default: S) -> Result<(), ConsoleError> where S: AsRef, { self.register_impl(name, default, false, false) } /// Register a new archived `Cvar` with the given name. /// /// The value of this `Cvar` should be written to `vars.rc` whenever the game is closed or /// `host_writeconfig` is issued. pub fn register_archive(&self, name: S, default: S) -> Result<(), ConsoleError> where S: AsRef, { self.register_impl(name, default, true, false) } /// Register a new notify `Cvar` with the given name. /// /// When this `Cvar` is set: /// - If the host is a server, broadcast that the variable has been changed to all clients. /// - If the host is a client, update the clientinfo string. pub fn register_notify(&self, name: S, default: S) -> Result<(), ConsoleError> where S: AsRef, { self.register_impl(name, default, false, true) } /// Register a new notify + archived `Cvar` with the given name. /// /// The value of this `Cvar` should be written to `vars.rc` whenever the game is closed or /// `host_writeconfig` is issued. /// /// Additionally, when this `Cvar` is set: /// - If the host is a server, broadcast that the variable has been changed to all clients. /// - If the host is a client, update the clientinfo string. pub fn register_archive_notify(&mut self, name: S, default: S) -> Result<(), ConsoleError> where S: AsRef, { self.register_impl(name, default, true, true) } pub fn get(&self, name: S) -> Result where S: AsRef, { Ok(self .cvars .borrow() .get(name.as_ref()) .ok_or(ConsoleError::NoSuchCvar(name.as_ref().to_owned()))? .val .clone()) } pub fn get_value(&self, name: S) -> Result where S: AsRef, { let name = name.as_ref(); let mut cvars = self.cvars.borrow_mut(); let cvar = cvars .get_mut(name) .ok_or(ConsoleError::NoSuchCvar(name.to_owned()))?; // try parsing as f32 let val_string = cvar.val.clone(); let val = match val_string.parse::() { Ok(v) => Ok(v), // if parse fails, reset to default value and try again Err(_) => { cvar.val = cvar.default.clone(); cvar.val.parse::() } } .or(Err(ConsoleError::CvarParseFailed { name: name.to_owned(), value: val_string.clone(), }))?; Ok(val) } pub fn set(&self, name: S, value: S) -> Result<(), ConsoleError> where S: AsRef, { trace!("cvar assignment: {} {}", name.as_ref(), value.as_ref()); let mut cvars = self.cvars.borrow_mut(); let mut cvar = cvars .get_mut(name.as_ref()) .ok_or(ConsoleError::NoSuchCvar(name.as_ref().to_owned()))?; cvar.val = value.as_ref().to_owned(); if cvar.notify { // TODO: update userinfo/serverinfo unimplemented!(); } Ok(()) } pub fn contains(&self, name: S) -> bool where S: AsRef, { self.cvars.borrow().contains_key(name.as_ref()) } } /// The line of text currently being edited in the console. pub struct ConsoleInput { text: Vec, curs: usize, } impl ConsoleInput { /// Constructs a new `ConsoleInput`. /// /// Initializes the text content to be empty and places the cursor at position 0. pub fn new() -> ConsoleInput { ConsoleInput { text: Vec::new(), curs: 0, } } /// Returns the current content of the `ConsoleInput`. pub fn get_text(&self) -> Vec { self.text.to_owned() } /// Sets the content of the `ConsoleInput` to `Text`. /// /// This also moves the cursor to the end of the line. pub fn set_text(&mut self, text: &Vec) { self.text = text.clone(); self.curs = self.text.len(); } /// Inserts the specified character at the position of the cursor. /// /// The cursor is moved one character to the right. pub fn insert(&mut self, c: char) { self.text.insert(self.curs, c); self.cursor_right(); } /// Moves the cursor to the right. /// /// If the cursor is at the end of the current text, no change is made. pub fn cursor_right(&mut self) { if self.curs < self.text.len() { self.curs += 1; } } /// Moves the cursor to the left. /// /// If the cursor is at the beginning of the current text, no change is made. pub fn cursor_left(&mut self) { if self.curs > 0 { self.curs -= 1; } } /// Deletes the character to the right of the cursor. /// /// If the cursor is at the end of the current text, no character is deleted. pub fn delete(&mut self) { if self.curs < self.text.len() { self.text.remove(self.curs); } } /// Deletes the character to the left of the cursor. /// /// If the cursor is at the beginning of the current text, no character is deleted. pub fn backspace(&mut self) { if self.curs > 0 { self.text.remove(self.curs - 1); self.curs -= 1; } } /// Clears the contents of the `ConsoleInput`. /// /// Also moves the cursor to position 0. pub fn clear(&mut self) { self.text.clear(); self.curs = 0; } } pub struct History { lines: VecDeque>, curs: usize, } impl History { pub fn new() -> History { History { lines: VecDeque::new(), curs: 0, } } pub fn add_line(&mut self, line: Vec) { self.lines.push_front(line); self.curs = 0; } // TODO: handle case where history is empty pub fn line_up(&mut self) -> Option> { if self.lines.len() == 0 || self.curs >= self.lines.len() { None } else { self.curs += 1; Some(self.lines[self.curs - 1].clone()) } } pub fn line_down(&mut self) -> Option> { if self.curs > 0 { self.curs -= 1; } if self.curs > 0 { Some(self.lines[self.curs - 1].clone()) } else { Some(Vec::new()) } } } pub struct ConsoleOutput { // A ring buffer of lines of text. Each line has an optional timestamp used // to determine whether it should be displayed on screen. If the timestamp // is `None`, the message will not be displayed. // // The timestamp is specified in seconds since the Unix epoch (so it is // decoupled from client/server time). lines: VecDeque<(Vec, Option)>, } impl ConsoleOutput { pub fn new() -> ConsoleOutput { ConsoleOutput { lines: VecDeque::new(), } } fn push(&mut self, chars: C, timestamp: Option) where C: IntoIterator, { self.lines .push_front((chars.into_iter().collect(), timestamp)) // TODO: set maximum capacity and pop_back when we reach it } pub fn lines(&self) -> impl Iterator { self.lines.iter().map(|(v, _)| v.as_slice()) } /// Return an iterator over lines that have been printed in the last /// `interval` of time. /// /// The iterator yields the oldest results first. /// /// `max_candidates` specifies the maximum number of lines to consider, /// while `max_results` specifies the maximum number of lines that should /// be returned. pub fn recent_lines( &self, interval: Duration, max_candidates: usize, max_results: usize, ) -> impl Iterator { let timestamp = (Utc::now() - interval).timestamp(); self.lines .iter() // search only the most recent `max_candidates` lines .take(max_candidates) // yield oldest to newest .rev() // eliminate non-timestamped lines and lines older than `timestamp` .filter_map(move |(l, t)| if (*t)? > timestamp { Some(l) } else { None }) // return at most `max_results` lines .take(max_results) .map(Vec::as_slice) } } pub struct Console { cmds: Rc>, cvars: Rc>, aliases: Rc>>, input: ConsoleInput, hist: History, buffer: RefCell, out_buffer: RefCell>, output: RefCell, } impl Console { pub fn new(cmds: Rc>, cvars: Rc>) -> Console { let output = RefCell::new(ConsoleOutput::new()); cmds.borrow_mut() .insert( "echo", Box::new(move |args| { let msg = match args.len() { 0 => "", _ => args[0], }; msg.to_owned() }), ) .unwrap(); let aliases: Rc>> = Rc::new(RefCell::new(HashMap::new())); let cmd_aliases = aliases.clone(); cmds.borrow_mut() .insert( "alias", Box::new(move |args| { match args.len() { 0 => { for (name, script) in cmd_aliases.borrow().iter() { println!(" {}: {}", name, script); } println!("{} alias command(s)", cmd_aliases.borrow().len()); } 2 => { let name = args[0].to_string(); let script = args[1].to_string(); let _ = cmd_aliases.borrow_mut().insert(name, script); } _ => (), } String::new() }), ) .unwrap(); let find_names = cmds.borrow().names(); cmds.borrow_mut() .insert( "find", Box::new(move |args| match args.len() { 1 => { let names = find_names.borrow_mut(); // Find the index of the first item >= the target. let start = match names.binary_search_by(|item| item.as_str().cmp(&args[0])) { Ok(i) => i, Err(i) => i, }; // Take every item starting with the target. let it = (&names[start..]) .iter() .take_while(move |item| item.starts_with(&args[0])) .map(|s| s.as_str()); let mut output = String::new(); for name in it { write!(&mut output, "{}\n", name).unwrap(); } output } _ => "usage: find ".into(), }), ) .unwrap(); Console { cmds, cvars, aliases: aliases.clone(), input: ConsoleInput::new(), hist: History::new(), buffer: RefCell::new(String::new()), out_buffer: RefCell::new(Vec::new()), output, } } // The timestamp is applied to any line flushed during this call. fn print_impl(&self, s: S, timestamp: Option) where S: AsRef, { let mut buf = self.out_buffer.borrow_mut(); let mut it = s.as_ref().chars(); while let Some(c) = it.next() { if c == '\n' { // Flush and clear the line buffer. self.output .borrow_mut() .push(buf.iter().copied(), timestamp); buf.clear(); } else { buf.push(c); } } } pub fn print(&self, s: S) where S: AsRef, { self.print_impl(s, None); } pub fn print_alert(&self, s: S) where S: AsRef, { self.print_impl(s, Some(Utc::now().timestamp())); } pub fn println(&self, s: S) where S: AsRef, { self.print_impl(s, None); self.print_impl("\n", None); } pub fn println_alert(&self, s: S) where S: AsRef, { let ts = Some(Utc::now().timestamp()); self.print_impl(s, ts); self.print_impl("\n", ts); } pub fn send_char(&mut self, c: char) { match c { // ignore grave and escape keys '`' | '\x1b' => (), '\r' => { // cap with a newline and push to the execution buffer let mut entered = self.get_string(); entered.push('\n'); self.buffer.borrow_mut().push_str(&entered); // add the current input to the history self.hist.add_line(self.input.get_text()); // echo the input to console output let mut input_echo: Vec = vec![']']; input_echo.append(&mut self.input.get_text()); self.output.borrow_mut().push(input_echo, None); // clear the input line self.input.clear(); } '\x08' => self.input.backspace(), '\x7f' => self.input.delete(), '\t' => warn!("Tab completion not implemented"), // TODO: tab completion // TODO: we should probably restrict what characters are allowed c => self.input.insert(c), } } pub fn cursor(&self) -> usize { self.input.curs } pub fn cursor_right(&mut self) { self.input.cursor_right() } pub fn cursor_left(&mut self) { self.input.cursor_left() } pub fn history_up(&mut self) { if let Some(line) = self.hist.line_up() { self.input.set_text(&line); } } pub fn history_down(&mut self) { if let Some(line) = self.hist.line_down() { self.input.set_text(&line); } } /// Interprets the contents of the execution buffer. pub fn execute(&self) { let text = self.buffer.replace(String::new()); let (_remaining, commands) = parse::commands(text.as_str()).unwrap(); for command in commands.iter() { debug!("{:?}", command); } for args in commands { if let Some(arg_0) = args.get(0) { let maybe_alias = self.aliases.borrow().get(*arg_0).map(|a| a.to_owned()); match maybe_alias { Some(a) => { self.stuff_text(a); self.execute(); } None => { let tail_args: Vec<&str> = args.iter().map(|s| s.as_ref()).skip(1).collect(); if self.cmds.borrow().contains(arg_0) { match self.cmds.borrow_mut().exec(arg_0, &tail_args) { Ok(o) => { if !o.is_empty() { self.println(o) } } Err(e) => self.println(format!("{}", e)), } } else if self.cvars.borrow().contains(arg_0) { // TODO error handling on cvar set match args.get(1) { Some(arg_1) => self.cvars.borrow_mut().set(arg_0, arg_1).unwrap(), None => { let msg = format!( "\"{}\" is \"{}\"", arg_0, self.cvars.borrow().get(arg_0).unwrap() ); self.println(msg); } } } else { // TODO: try sending to server first self.println(format!("Unrecognized command \"{}\"", arg_0)); } } } } } } pub fn get_string(&self) -> String { String::from_iter(self.input.text.clone().into_iter()) } pub fn stuff_text(&self, text: S) where S: AsRef, { debug!("stuff_text:\n{:?}", text.as_ref()); self.buffer.borrow_mut().push_str(text.as_ref()); // in case the last line doesn't end with a newline self.buffer.borrow_mut().push_str("\n"); } pub fn output(&self) -> Ref { self.output.borrow() } } ================================================ FILE: src/common/engine.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{fs::File, io::Read}; use cgmath::{Deg, Vector3}; use chrono::Duration; // TODO: the palette should be host-specific and loaded alongside pak0.pak (or the latest PAK with a // palette.lmp) lazy_static! { static ref PALETTE: [u8; 768] = { let mut _palette = [0; 768]; let mut f = File::open("pak0.pak.d/gfx/palette.lmp").unwrap(); match f.read(&mut _palette) { Err(why) => panic!("{}", why), Ok(768) => _palette, _ => panic!("Bad read on pak0/gfx/palette.lmp"), } }; } pub fn indexed_to_rgba(indices: &[u8]) -> Vec { let mut rgba = Vec::with_capacity(4 * indices.len()); for i in 0..indices.len() { if indices[i] != 0xFF { for c in 0..3 { rgba.push(PALETTE[(3 * (indices[i] as usize) + c) as usize]); } rgba.push(0xFF); } else { for _ in 0..4 { rgba.push(0x00); } } } rgba } // TODO: handle this unwrap? i64 can handle ~200,000 years in microseconds #[inline] pub fn duration_to_f32(d: Duration) -> f32 { d.num_microseconds().unwrap() as f32 / 1_000_000.0 } #[inline] pub fn duration_from_f32(f: f32) -> Duration { Duration::microseconds((f * 1_000_000.0) as i64) } #[inline] pub fn deg_vector_to_f32_vector(av: Vector3>) -> Vector3 { Vector3::new(av[0].0, av[1].0, av[2].0) } #[inline] pub fn deg_vector_from_f32_vector(v: Vector3) -> Vector3> { Vector3::new(Deg(v[0]), Deg(v[1]), Deg(v[2])) } ================================================ FILE: src/common/host.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::cell::{Ref, RefMut}; use crate::common::{console::CvarRegistry, engine}; use chrono::{DateTime, Duration, Utc}; use winit::{ event::{Event, WindowEvent}, event_loop::{ControlFlow, EventLoopWindowTarget}, }; pub trait Program: Sized { fn handle_event( &mut self, event: Event, _target: &EventLoopWindowTarget, control_flow: &mut ControlFlow, ); fn frame(&mut self, frame_duration: Duration); fn shutdown(&mut self); fn cvars(&self) -> Ref; fn cvars_mut(&self) -> RefMut; } pub struct Host

where P: Program, { program: P, init_time: DateTime, prev_frame_time: DateTime, prev_frame_duration: Duration, } impl

Host

where P: Program, { pub fn new(program: P) -> Host

{ let init_time = Utc::now(); program .cvars_mut() .register_archive("host_maxfps", "72") .unwrap(); Host { program, init_time, prev_frame_time: init_time, prev_frame_duration: Duration::zero(), } } pub fn handle_event( &mut self, event: Event, _target: &EventLoopWindowTarget, control_flow: &mut ControlFlow, ) { match event { Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { self.program.shutdown(); *control_flow = ControlFlow::Exit; } Event::MainEventsCleared => self.frame(), Event::Suspended | Event::Resumed => unimplemented!(), Event::LoopDestroyed => { // TODO: // - host_writeconfig // - others... } e => self.program.handle_event(e, _target, control_flow), } } pub fn frame(&mut self) { // TODO: make sure this doesn't cause weirdness with e.g. leap seconds let new_frame_time = Utc::now(); self.prev_frame_duration = new_frame_time.signed_duration_since(self.prev_frame_time); // if the time elapsed since the last frame is too low, don't run this one yet let prev_frame_duration = self.prev_frame_duration; if !self.check_frame_duration(prev_frame_duration) { // avoid busy waiting if we're running at a really high framerate std::thread::sleep(std::time::Duration::from_millis(1)); return; } // we're running this frame, so update the frame time self.prev_frame_time = new_frame_time; self.program.frame(self.prev_frame_duration); } // Returns whether enough time has elapsed to run the next frame. fn check_frame_duration(&mut self, frame_duration: Duration) -> bool { let host_maxfps = self .program .cvars() .get_value("host_maxfps") .unwrap_or(72.0); let min_frame_duration = engine::duration_from_f32(1.0 / host_maxfps); frame_duration >= min_frame_duration } pub fn uptime(&self) -> Duration { self.prev_frame_time.signed_duration_since(self.init_time) } } ================================================ FILE: src/common/math.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::{cmp::Ordering, convert::Into, ops::Neg}; use cgmath::{Angle, Deg, InnerSpace, Matrix3, Matrix4, Vector2, Vector3, Zero}; trait CoordSys {} struct Quake; impl CoordSys for Quake {} struct Wgpu; impl CoordSys for Wgpu {} pub const VERTEX_NORMAL_COUNT: usize = 162; lazy_static! { /// Precomputed vertex normals used for alias models and particle effects pub static ref VERTEX_NORMALS: [Vector3; VERTEX_NORMAL_COUNT] = [ [-0.525731, 0.000000, 0.850651].into(), [-0.442863, 0.238856, 0.864188].into(), [-0.295242, 0.000000, 0.955423].into(), [-0.309017, 0.500000, 0.809017].into(), [-0.162460, 0.262866, 0.951056].into(), [0.000000, 0.000000, 1.000000].into(), [0.000000, 0.850651, 0.525731].into(), [-0.147621, 0.716567, 0.681718].into(), [0.147621, 0.716567, 0.681718].into(), [0.000000, 0.525731, 0.850651].into(), [0.309017, 0.500000, 0.809017].into(), [0.525731, 0.000000, 0.850651].into(), [0.295242, 0.000000, 0.955423].into(), [0.442863, 0.238856, 0.864188].into(), [0.162460, 0.262866, 0.951056].into(), [-0.681718, 0.147621, 0.716567].into(), [-0.809017, 0.309017, 0.500000].into(), [-0.587785, 0.425325, 0.688191].into(), [-0.850651, 0.525731, 0.000000].into(), [-0.864188, 0.442863, 0.238856].into(), [-0.716567, 0.681718, 0.147621].into(), [-0.688191, 0.587785, 0.425325].into(), [-0.500000, 0.809017, 0.309017].into(), [-0.238856, 0.864188, 0.442863].into(), [-0.425325, 0.688191, 0.587785].into(), [-0.716567, 0.681718, -0.147621].into(), [-0.500000, 0.809017, -0.309017].into(), [-0.525731, 0.850651, 0.000000].into(), [0.000000, 0.850651, -0.525731].into(), [-0.238856, 0.864188, -0.442863].into(), [0.000000, 0.955423, -0.295242].into(), [-0.262866, 0.951056, -0.162460].into(), [0.000000, 1.000000, 0.000000].into(), [0.000000, 0.955423, 0.295242].into(), [-0.262866, 0.951056, 0.162460].into(), [0.238856, 0.864188, 0.442863].into(), [0.262866, 0.951056, 0.162460].into(), [0.500000, 0.809017, 0.309017].into(), [0.238856, 0.864188, -0.442863].into(), [0.262866, 0.951056, -0.162460].into(), [0.500000, 0.809017, -0.309017].into(), [0.850651, 0.525731, 0.000000].into(), [0.716567, 0.681718, 0.147621].into(), [0.716567, 0.681718, -0.147621].into(), [0.525731, 0.850651, 0.000000].into(), [0.425325, 0.688191, 0.587785].into(), [0.864188, 0.442863, 0.238856].into(), [0.688191, 0.587785, 0.425325].into(), [0.809017, 0.309017, 0.500000].into(), [0.681718, 0.147621, 0.716567].into(), [0.587785, 0.425325, 0.688191].into(), [0.955423, 0.295242, 0.000000].into(), [1.000000, 0.000000, 0.000000].into(), [0.951056, 0.162460, 0.262866].into(), [0.850651, -0.525731, 0.000000].into(), [0.955423, -0.295242, 0.000000].into(), [0.864188, -0.442863, 0.238856].into(), [0.951056, -0.162460, 0.262866].into(), [0.809017, -0.309017, 0.500000].into(), [0.681718, -0.147621, 0.716567].into(), [0.850651, 0.000000, 0.525731].into(), [0.864188, 0.442863, -0.238856].into(), [0.809017, 0.309017, -0.500000].into(), [0.951056, 0.162460, -0.262866].into(), [0.525731, 0.000000, -0.850651].into(), [0.681718, 0.147621, -0.716567].into(), [0.681718, -0.147621, -0.716567].into(), [0.850651, 0.000000, -0.525731].into(), [0.809017, -0.309017, -0.500000].into(), [0.864188, -0.442863, -0.238856].into(), [0.951056, -0.162460, -0.262866].into(), [0.147621, 0.716567, -0.681718].into(), [0.309017, 0.500000, -0.809017].into(), [0.425325, 0.688191, -0.587785].into(), [0.442863, 0.238856, -0.864188].into(), [0.587785, 0.425325, -0.688191].into(), [0.688191, 0.587785, -0.425325].into(), [-0.147621, 0.716567, -0.681718].into(), [-0.309017, 0.500000, -0.809017].into(), [0.000000, 0.525731, -0.850651].into(), [-0.525731, 0.000000, -0.850651].into(), [-0.442863, 0.238856, -0.864188].into(), [-0.295242, 0.000000, -0.955423].into(), [-0.162460, 0.262866, -0.951056].into(), [0.000000, 0.000000, -1.000000].into(), [0.295242, 0.000000, -0.955423].into(), [0.162460, 0.262866, -0.951056].into(), [-0.442863, -0.238856, -0.864188].into(), [-0.309017, -0.500000, -0.809017].into(), [-0.162460, -0.262866, -0.951056].into(), [0.000000, -0.850651, -0.525731].into(), [-0.147621, -0.716567, -0.681718].into(), [0.147621, -0.716567, -0.681718].into(), [0.000000, -0.525731, -0.850651].into(), [0.309017, -0.500000, -0.809017].into(), [0.442863, -0.238856, -0.864188].into(), [0.162460, -0.262866, -0.951056].into(), [0.238856, -0.864188, -0.442863].into(), [0.500000, -0.809017, -0.309017].into(), [0.425325, -0.688191, -0.587785].into(), [0.716567, -0.681718, -0.147621].into(), [0.688191, -0.587785, -0.425325].into(), [0.587785, -0.425325, -0.688191].into(), [0.000000, -0.955423, -0.295242].into(), [0.000000, -1.000000, 0.000000].into(), [0.262866, -0.951056, -0.162460].into(), [0.000000, -0.850651, 0.525731].into(), [0.000000, -0.955423, 0.295242].into(), [0.238856, -0.864188, 0.442863].into(), [0.262866, -0.951056, 0.162460].into(), [0.500000, -0.809017, 0.309017].into(), [0.716567, -0.681718, 0.147621].into(), [0.525731, -0.850651, 0.000000].into(), [-0.238856, -0.864188, -0.442863].into(), [-0.500000, -0.809017, -0.309017].into(), [-0.262866, -0.951056, -0.162460].into(), [-0.850651, -0.525731, 0.000000].into(), [-0.716567, -0.681718, -0.147621].into(), [-0.716567, -0.681718, 0.147621].into(), [-0.525731, -0.850651, 0.000000].into(), [-0.500000, -0.809017, 0.309017].into(), [-0.238856, -0.864188, 0.442863].into(), [-0.262866, -0.951056, 0.162460].into(), [-0.864188, -0.442863, 0.238856].into(), [-0.809017, -0.309017, 0.500000].into(), [-0.688191, -0.587785, 0.425325].into(), [-0.681718, -0.147621, 0.716567].into(), [-0.442863, -0.238856, 0.864188].into(), [-0.587785, -0.425325, 0.688191].into(), [-0.309017, -0.500000, 0.809017].into(), [-0.147621, -0.716567, 0.681718].into(), [-0.425325, -0.688191, 0.587785].into(), [-0.162460, -0.262866, 0.951056].into(), [0.442863, -0.238856, 0.864188].into(), [0.162460, -0.262866, 0.951056].into(), [0.309017, -0.500000, 0.809017].into(), [0.147621, -0.716567, 0.681718].into(), [0.000000, -0.525731, 0.850651].into(), [0.425325, -0.688191, 0.587785].into(), [0.587785, -0.425325, 0.688191].into(), [0.688191, -0.587785, 0.425325].into(), [-0.955423, 0.295242, 0.000000].into(), [-0.951056, 0.162460, 0.262866].into(), [-1.000000, 0.000000, 0.000000].into(), [-0.850651, 0.000000, 0.525731].into(), [-0.955423, -0.295242, 0.000000].into(), [-0.951056, -0.162460, 0.262866].into(), [-0.864188, 0.442863, -0.238856].into(), [-0.951056, 0.162460, -0.262866].into(), [-0.809017, 0.309017, -0.500000].into(), [-0.864188, -0.442863, -0.238856].into(), [-0.951056, -0.162460, -0.262866].into(), [-0.809017, -0.309017, -0.500000].into(), [-0.681718, 0.147621, -0.716567].into(), [-0.681718, -0.147621, -0.716567].into(), [-0.850651, 0.000000, -0.525731].into(), [-0.688191, 0.587785, -0.425325].into(), [-0.587785, 0.425325, -0.688191].into(), [-0.425325, 0.688191, -0.587785].into(), [-0.425325, -0.688191, -0.587785].into(), [-0.587785, -0.425325, -0.688191].into(), [-0.688191, -0.587785, -0.425325].into(), ]; } #[derive(Clone, Copy, Debug)] pub struct Angles { pub pitch: Deg, pub roll: Deg, pub yaw: Deg, } impl Angles { pub fn zero() -> Angles { Angles { pitch: Deg(0.0), roll: Deg(0.0), yaw: Deg(0.0), } } pub fn mat3_quake(&self) -> Matrix3 { Matrix3::from_angle_x(-self.roll) * Matrix3::from_angle_y(-self.pitch) * Matrix3::from_angle_z(self.yaw) } pub fn mat4_quake(&self) -> Matrix4 { Matrix4::from_angle_x(-self.roll) * Matrix4::from_angle_y(-self.pitch) * Matrix4::from_angle_z(self.yaw) } pub fn mat3_wgpu(&self) -> Matrix3 { Matrix3::from_angle_z(-self.roll) * Matrix3::from_angle_x(self.pitch) * Matrix3::from_angle_y(-self.yaw) } pub fn mat4_wgpu(&self) -> Matrix4 { Matrix4::from_angle_z(-self.roll) * Matrix4::from_angle_x(self.pitch) * Matrix4::from_angle_y(-self.yaw) } } impl std::ops::Add for Angles { type Output = Self; fn add(self, other: Self) -> Self { Self { pitch: self.pitch + other.pitch, roll: self.roll + other.roll, yaw: self.yaw + other.yaw, } } } impl std::ops::Mul for Angles { type Output = Self; fn mul(self, other: f32) -> Self { Self { pitch: self.pitch * other, roll: self.roll * other, yaw: self.yaw * other, } } } pub fn clamp_deg(val: Deg, min: Deg, max: Deg) -> Deg { assert!(min <= max); return if val < min { min } else if val > max { max } else { val }; } #[derive(Copy, Clone, Debug, PartialEq)] pub enum HyperplaneSide { Positive = 0, Negative = 1, } impl Neg for HyperplaneSide { type Output = HyperplaneSide; fn neg(self) -> Self::Output { match self { HyperplaneSide::Positive => HyperplaneSide::Negative, HyperplaneSide::Negative => HyperplaneSide::Positive, } } } impl HyperplaneSide { // TODO: check this against the original game logic. pub fn from_dist(dist: f32) -> HyperplaneSide { if dist >= 0.0 { HyperplaneSide::Positive } else { HyperplaneSide::Negative } } } #[derive(Debug)] /// The intersection of a line or segment and a plane at a point. pub struct PointIntersection { // percentage of distance between start and end where crossover occurred ratio: f32, // crossover point point: Vector3, // plane crossed over plane: Hyperplane, } impl PointIntersection { pub fn ratio(&self) -> f32 { self.ratio } pub fn point(&self) -> Vector3 { self.point } pub fn plane(&self) -> &Hyperplane { &self.plane } } #[derive(Debug)] /// The intersection of a line or line segment with a plane. /// /// A true mathematical representation would account for lines or segments contained entirely within /// the plane, but here a distance of 0.0 is considered Positive. Thus, lines or segments contained /// by the plane are considered to be `NoIntersection(Positive)`. pub enum LinePlaneIntersect { /// The line or line segment never intersects with the plane. NoIntersection(HyperplaneSide), /// The line or line segment intersects with the plane at precisely one point. PointIntersection(PointIntersection), } #[derive(Copy, Clone, Debug, FromPrimitive)] pub enum Axis { X = 0, Y = 1, Z = 2, } #[derive(Clone, Debug)] enum Alignment { Axis(Axis), Normal(Vector3), } #[derive(Clone, Debug)] pub struct Hyperplane { alignment: Alignment, dist: f32, } impl Neg for Hyperplane { type Output = Self; fn neg(self) -> Self::Output { let normal = match self.alignment { Alignment::Axis(a) => { let mut n = Vector3::zero(); n[a as usize] = -1.0; n } Alignment::Normal(n) => -n, }; Hyperplane::new(normal, -self.dist) } } impl Hyperplane { /// Creates a new hyperplane aligned along the given normal, `dist` units away from the origin. /// /// If the given normal is equivalent to one of the axis normals, the hyperplane will be optimized /// to only consider that axis when performing point comparisons. pub fn new(normal: Vector3, dist: f32) -> Hyperplane { match normal { n if n == Vector3::unit_x() => Self::axis_x(dist), n if n == Vector3::unit_y() => Self::axis_y(dist), n if n == Vector3::unit_z() => Self::axis_z(dist), _ => Self::from_normal(normal.normalize(), dist), } } /// Creates a new hyperplane aligned along the x-axis, `dist` units away from the origin. /// /// This hyperplane will only consider the x-axis when performing point comparisons. pub fn axis_x(dist: f32) -> Hyperplane { Hyperplane { alignment: Alignment::Axis(Axis::X), dist, } } /// Creates a new hyperplane aligned along the y-axis, `dist` units away from the origin. /// /// This hyperplane will only consider the y-axis when performing point comparisons. pub fn axis_y(dist: f32) -> Hyperplane { Hyperplane { alignment: Alignment::Axis(Axis::Y), dist, } } /// Creates a new hyperplane aligned along the z-axis, `dist` units away from the origin. /// /// This hyperplane will only consider the z-axis when performing point comparisons. pub fn axis_z(dist: f32) -> Hyperplane { Hyperplane { alignment: Alignment::Axis(Axis::Z), dist, } } /// Creates a new hyperplane aligned along the given normal, `dist` units away from the origin. /// /// This function will force the hyperplane alignment to be represented as a normal even if it /// is aligned along an axis. pub fn from_normal(normal: Vector3, dist: f32) -> Hyperplane { Hyperplane { alignment: Alignment::Normal(normal.normalize()), dist, } } /// Returns the surface normal of this plane. pub fn normal(&self) -> Vector3 { match self.alignment { Alignment::Axis(ax) => match ax { Axis::X => Vector3::unit_x(), Axis::Y => Vector3::unit_y(), Axis::Z => Vector3::unit_z(), }, Alignment::Normal(normal) => normal, } } /// Calculates the shortest distance between this hyperplane and the given point. pub fn point_dist(&self, point: Vector3) -> f32 { match self.alignment { Alignment::Axis(a) => point[a as usize] - self.dist, Alignment::Normal(n) => point.dot(n) - self.dist, } } /// Calculates which side of this hyperplane the given point belongs to. /// /// Points with a distance of 0.0 are considered to be on the positive side. pub fn point_side(&self, point: Vector3) -> HyperplaneSide { let point_dist_greater = match self.alignment { Alignment::Axis(a) => point[a as usize] >= self.dist, Alignment::Normal(n) => point.dot(n) - self.dist >= 0.0, }; match point_dist_greater { true => HyperplaneSide::Positive, false => HyperplaneSide::Negative, } } /// Calculates the intersection of a line segment with this hyperplane. pub fn line_segment_intersection( &self, start: Vector3, end: Vector3, ) -> LinePlaneIntersect { let start_dist = self.point_dist(start); let end_dist = self.point_dist(end); debug!( "line_segment_intersection: alignment={:?} plane_dist={} start_dist={} end_dist={}", self.alignment, self.dist, start_dist, end_dist ); let start_side = HyperplaneSide::from_dist(start_dist); let end_side = HyperplaneSide::from_dist(end_dist); // if both points fall on the same side of the hyperplane, there is no intersection if start_side == end_side { return LinePlaneIntersect::NoIntersection(start_side); } // calculate how far along the segment the intersection occurred let ratio = start_dist / (start_dist - end_dist); let point = start + ratio * (end - start); let plane = match start_side { HyperplaneSide::Positive => self.to_owned(), HyperplaneSide::Negative => -self.to_owned(), }; LinePlaneIntersect::PointIntersection(PointIntersection { ratio, point, plane, }) } } pub fn fov_x_to_fov_y(fov_x: Deg, aspect: f32) -> Option> { // aspect = tan(fov_x / 2) / tan(fov_y / 2) // tan(fov_y / 2) = tan(fov_x / 2) / aspect // fov_y / 2 = atan(tan(fov_x / 2) / aspect) // fov_y = 2 * atan(tan(fov_x / 2) / aspect) match fov_x { // TODO: genericize over cgmath::Angle f if f < Deg(0.0) => None, f if f > Deg(360.0) => None, f => Some(Deg::atan((f / 2.0).tan() / aspect) * 2.0), } } // see https://github.com/id-Software/Quake/blob/master/WinQuake/gl_rsurf.c#L1544 const COLLINEAR_EPSILON: f32 = 0.001; /// Determines if the given points are collinear. /// /// A set of points V is considered collinear if /// norm(V1 − V0) = /// norm(V2 − V1) = /// . . . = /// norm(Vk − 1 − Vk). /// /// Special cases: /// - If `vs.len() < 2`, always returns `false`. /// - If `vs.len() == 2`, always returns `true`. pub fn collinear(vs: &[Vector3]) -> bool { match vs.len() { l if l < 2 => false, 2 => true, _ => { let init = (vs[1] - vs[0]).normalize(); for i in 2..vs.len() { let norm = (vs[i] - vs[i - 1]).normalize(); if (norm[0] - init[0]).abs() > COLLINEAR_EPSILON || (norm[1] - init[1]).abs() > COLLINEAR_EPSILON || (norm[2] - init[2]).abs() > COLLINEAR_EPSILON { return false; } } true } } } pub fn remove_collinear(vs: Vec>) -> Vec> { assert!(vs.len() >= 3); let mut out = Vec::new(); let mut vs_iter = vs.into_iter().cycle(); let v_init = vs_iter.next().unwrap(); let mut v1 = v_init; let mut v2 = vs_iter.next().unwrap(); out.push(v1); for v3 in vs_iter { let tri = &[v1, v2, v3]; if !collinear(tri) { out.push(v2); } if v3 == v_init { break; } v1 = v2; v2 = v3; } out } pub fn bounds<'a, I>(points: I) -> (Vector3, Vector3) where I: IntoIterator>, { let mut min = Vector3::new(32767.0, 32767.0, 32767.0); let mut max = Vector3::new(-32768.0, -32768.0, -32768.0); for p in points.into_iter() { for c in 0..3 { min[c] = p[c].min(min[c]); max[c] = p[c].max(max[c]); } } (min, max) } pub fn vec2_extend_n(v: Vector2, n: usize, val: f32) -> Vector3 { let mut ar = [0.0; 3]; for i in 0..3 { match i.cmp(&n) { Ordering::Less => ar[i] = v[i], Ordering::Equal => ar[i] = val, Ordering::Greater => ar[i] = v[i - 1], } } ar.into() } pub fn vec3_truncate_n(v: Vector3, n: usize) -> Vector2 { let mut ar = [0.0; 2]; for i in 0..3 { match i.cmp(&n) { Ordering::Less => ar[i] = v[i], Ordering::Equal => (), Ordering::Greater => ar[i - 1] = v[i], } } ar.into() } #[cfg(test)] mod test { use super::*; #[test] fn test_hyperplane_side_x() { let plane = Hyperplane::axis_x(1.0); assert_eq!( plane.point_side(Vector3::unit_x() * 2.0), HyperplaneSide::Positive ); assert_eq!( plane.point_side(Vector3::unit_x() * -2.0), HyperplaneSide::Negative ); } #[test] fn test_hyperplane_side_y() { let plane = Hyperplane::axis_y(1.0); assert_eq!( plane.point_side(Vector3::unit_y() * 2.0), HyperplaneSide::Positive ); assert_eq!( plane.point_side(Vector3::unit_y() * -2.0), HyperplaneSide::Negative ); } #[test] fn test_hyperplane_side_z() { let plane = Hyperplane::axis_z(1.0); assert_eq!( plane.point_side(Vector3::unit_z() * 2.0), HyperplaneSide::Positive ); assert_eq!( plane.point_side(Vector3::unit_z() * -2.0), HyperplaneSide::Negative ); } #[test] fn test_hyperplane_side_arbitrary() { // test 16 hyperplanes around the origin for x_comp in [1.0, -1.0].into_iter() { for y_comp in [1.0, -1.0].into_iter() { for z_comp in [1.0, -1.0].into_iter() { for dist in [1, -1].into_iter() { let base_vector = Vector3::new(*x_comp, *y_comp, *z_comp); let plane = Hyperplane::new(base_vector, *dist as f32); assert_eq!( plane.point_side(Vector3::zero()), match *dist { 1 => HyperplaneSide::Negative, -1 => HyperplaneSide::Positive, _ => unreachable!(), } ); assert_eq!( plane.point_side(base_vector * 2.0 * *dist as f32), match *dist { 1 => HyperplaneSide::Positive, -1 => HyperplaneSide::Negative, _ => unreachable!(), } ); } } } } } #[test] fn test_hyperplane_point_dist_x() { let plane = Hyperplane::axis_x(1.0); assert_eq!(plane.point_dist(Vector3::unit_x() * 2.0), 1.0); assert_eq!(plane.point_dist(Vector3::zero()), -1.0); } #[test] fn test_hyperplane_point_dist_y() { let plane = Hyperplane::axis_y(1.0); assert_eq!(plane.point_dist(Vector3::unit_y() * 2.0), 1.0); assert_eq!(plane.point_dist(Vector3::zero()), -1.0); } #[test] fn test_hyperplane_point_dist_z() { let plane = Hyperplane::axis_z(1.0); assert_eq!(plane.point_dist(Vector3::unit_z() * 2.0), 1.0); assert_eq!(plane.point_dist(Vector3::zero()), -1.0); } #[test] fn test_hyperplane_point_dist_x_no_axis() { let plane = Hyperplane::from_normal(Vector3::unit_x(), 1.0); assert_eq!(plane.point_dist(Vector3::unit_x() * 2.0), 1.0); assert_eq!(plane.point_dist(Vector3::zero()), -1.0); } #[test] fn test_hyperplane_point_dist_y_no_axis() { let plane = Hyperplane::from_normal(Vector3::unit_y(), 1.0); assert_eq!(plane.point_dist(Vector3::unit_y() * 2.0), 1.0); assert_eq!(plane.point_dist(Vector3::zero()), -1.0); } #[test] fn test_hyperplane_point_dist_z_no_axis() { let plane = Hyperplane::from_normal(Vector3::unit_z(), 1.0); assert_eq!(plane.point_dist(Vector3::unit_z() * 2.0), 1.0); assert_eq!(plane.point_dist(Vector3::zero()), -1.0); } #[test] fn test_hyperplane_line_segment_intersection_x() { let plane = Hyperplane::axis_x(1.0); let start = Vector3::new(0.0, 0.5, 0.5); let end = Vector3::new(2.0, 0.5, 0.5); match plane.line_segment_intersection(start, end) { LinePlaneIntersect::PointIntersection(p_i) => { assert_eq!(p_i.ratio(), 0.5); assert_eq!(p_i.point(), Vector3::new(1.0, 0.5, 0.5)); } _ => panic!(), } } #[test] fn test_hyperplane_line_segment_intersection_y() { let plane = Hyperplane::axis_y(1.0); let start = Vector3::new(0.5, 0.0, 0.5); let end = Vector3::new(0.5, 2.0, 0.5); match plane.line_segment_intersection(start, end) { LinePlaneIntersect::PointIntersection(p_i) => { assert_eq!(p_i.ratio(), 0.5); assert_eq!(p_i.point(), Vector3::new(0.5, 1.0, 0.5)); } _ => panic!(), } } #[test] fn test_hyperplane_line_segment_intersection_z() { let plane = Hyperplane::axis_z(1.0); let start = Vector3::new(0.5, 0.5, 0.0); let end = Vector3::new(0.5, 0.5, 2.0); match plane.line_segment_intersection(start, end) { LinePlaneIntersect::PointIntersection(p_i) => { assert_eq!(p_i.ratio(), 0.5); assert_eq!(p_i.point(), Vector3::new(0.5, 0.5, 1.0)); } _ => panic!(), } } #[test] fn test_collinear() { let cases = vec![ ( vec![Vector3::unit_x(), Vector3::unit_y(), Vector3::unit_z()], false, ), ( vec![ Vector3::unit_x(), Vector3::unit_x() * 2.0, Vector3::unit_x() * 3.0, ], true, ), ( vec![ [1400.0, 848.0, -456.0].into(), [1352.0, 848.0, -456.0].into(), [1272.0, 848.0, -456.0].into(), [1256.0, 848.0, -456.0].into(), [1208.0, 848.0, -456.0].into(), [1192.0, 848.0, -456.0].into(), [1176.0, 848.0, -456.0].into(), ], true, ), ]; for (input, result) in cases.into_iter() { assert_eq!(collinear(&input), result); } } #[test] fn test_remove_collinear() { let cases = vec![ ( vec![ [1176.0, 992.0, -456.0].into(), [1176.0, 928.0, -456.0].into(), [1176.0, 880.0, -456.0].into(), [1176.0, 864.0, -456.0].into(), [1176.0, 848.0, -456.0].into(), [1120.0, 848.0, -456.0].into(), [1120.0, 992.0, -456.0].into(), ], vec![ [1176.0, 992.0, -456.0].into(), [1176.0, 848.0, -456.0].into(), [1120.0, 848.0, -456.0].into(), [1120.0, 992.0, -456.0].into(), ], ), ( vec![ [1400.0, 768.0, -456.0].into(), [1400.0, 848.0, -456.0].into(), [1352.0, 848.0, -456.0].into(), [1272.0, 848.0, -456.0].into(), [1256.0, 848.0, -456.0].into(), [1208.0, 848.0, -456.0].into(), [1192.0, 848.0, -456.0].into(), [1176.0, 848.0, -456.0].into(), [1120.0, 848.0, -456.0].into(), [1200.0, 768.0, -456.0].into(), ], vec![ [1400.0, 768.0, -456.0].into(), [1400.0, 848.0, -456.0].into(), [1120.0, 848.0, -456.0].into(), [1200.0, 768.0, -456.0].into(), ], ), ]; for (input, output) in cases.into_iter() { assert_eq!(remove_collinear(input), output); } } } ================================================ FILE: src/common/mdl.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::io::{self, BufReader, Read, Seek, SeekFrom}; use crate::common::{ engine, model::{ModelFlags, SyncType}, util::read_f32_3, }; use byteorder::{LittleEndian, ReadBytesExt}; use cgmath::{ElementWise as _, Vector3}; use chrono::Duration; use num::FromPrimitive; use thiserror::Error; pub const MAGIC: i32 = ('I' as i32) << 0 | ('D' as i32) << 8 | ('P' as i32) << 16 | ('O' as i32) << 24; pub const VERSION: i32 = 6; const HEADER_SIZE: u64 = 84; #[derive(Error, Debug)] pub enum MdlFileError { #[error("I/O error: {0}")] Io(#[from] io::Error), #[error("Invalid magic number: found {0}, expected {}", MAGIC)] InvalidMagicNumber(i32), #[error("Unrecognized version: {0}")] UnrecognizedVersion(i32), #[error("Invalid texture width: {0}")] InvalidTextureWidth(i32), #[error("Invalid texture height: {0}")] InvalidTextureHeight(i32), #[error("Invalid vertex count: {0}")] InvalidVertexCount(i32), #[error("Invalid polygon count: {0}")] InvalidPolygonCount(i32), #[error("Invalid keyframe count: {0}")] InvalidKeyframeCount(i32), #[error("Invalid model flags: {0:X?}")] InvalidFlags(i32), #[error("Invalid texture kind: {0}")] InvalidTextureKind(i32), #[error("Invalid seam flag: {0}")] InvalidSeamFlag(i32), #[error("Invalid texture coordinates: {0:?}")] InvalidTexcoord([i32; 2]), #[error("Invalid front-facing flag: {0}")] InvalidFrontFacing(i32), #[error("Keyframe name too long: {0:?}")] KeyframeNameTooLong([u8; 16]), #[error("Non-UTF-8 keyframe name: {0}")] NonUtf8KeyframeName(#[from] std::string::FromUtf8Error), } #[derive(Clone, Debug)] pub struct StaticTexture { indices: Box<[u8]>, } impl StaticTexture { /// Returns the indexed colors of this texture. pub fn indices(&self) -> &[u8] { &self.indices } } #[derive(Clone, Debug)] pub struct AnimatedTextureFrame { duration: Duration, indices: Box<[u8]>, } impl AnimatedTextureFrame { /// Returns the duration of this frame. pub fn duration(&self) -> Duration { self.duration } /// Returns the indexed colors of this texture. pub fn indices(&self) -> &[u8] { &self.indices } } #[derive(Clone, Debug)] pub struct AnimatedTexture { frames: Box<[AnimatedTextureFrame]>, } impl AnimatedTexture { pub fn frames(&self) -> &[AnimatedTextureFrame] { &self.frames } } #[derive(Clone, Debug)] pub enum Texture { Static(StaticTexture), Animated(AnimatedTexture), } #[derive(Clone, Debug)] pub struct Texcoord { is_on_seam: bool, s: u32, t: u32, } impl Texcoord { pub fn is_on_seam(&self) -> bool { self.is_on_seam } pub fn s(&self) -> u32 { self.s } pub fn t(&self) -> u32 { self.t } } #[derive(Clone, Debug)] pub struct IndexedPolygon { faces_front: bool, indices: [u32; 3], } impl IndexedPolygon { pub fn faces_front(&self) -> bool { self.faces_front } pub fn indices(&self) -> &[u32; 3] { &self.indices } } #[derive(Clone, Debug)] pub struct StaticKeyframe { name: String, min: Vector3, max: Vector3, vertices: Box<[Vector3]>, } impl StaticKeyframe { /// Returns the name of this keyframe. pub fn name(&self) -> &str { &self.name } /// Returns the minimum extent of this keyframe relative to the model origin. pub fn min(&self) -> Vector3 { self.min } /// Returns the minimum extent of this keyframe relative to the model origin. pub fn max(&self) -> Vector3 { self.max } /// Returns the vertices defining this keyframe. pub fn vertices(&self) -> &[Vector3] { &self.vertices } } #[derive(Clone, Debug)] pub struct AnimatedKeyframeFrame { name: String, min: Vector3, max: Vector3, duration: Duration, vertices: Box<[Vector3]>, } impl AnimatedKeyframeFrame { /// Returns the name of this subframe. pub fn name(&self) -> &str { &self.name } /// Returns the minimum extent of this keyframe relative to the model origin. pub fn min(&self) -> Vector3 { self.min } /// Returns the minimum extent of this keyframe relative to the model origin. pub fn max(&self) -> Vector3 { self.max } /// Returns the duration of this subframe. pub fn duration(&self) -> Duration { self.duration } /// Returns the vertices defining this subframe. pub fn vertices(&self) -> &[Vector3] { &self.vertices } } #[derive(Clone, Debug)] pub struct AnimatedKeyframe { min: Vector3, max: Vector3, frames: Box<[AnimatedKeyframeFrame]>, } impl AnimatedKeyframe { /// Returns the minimum extent of all subframes in this keyframe relative to the model origin. pub fn min(&self) -> Vector3 { self.min } /// Returns the maximum extent of all subframes in this keyframe relative to the model origin. pub fn max(&self) -> Vector3 { self.max } /// Returns the subframes of this keyframe. pub fn frames(&self) -> &[AnimatedKeyframeFrame] { &self.frames } } #[derive(Clone, Debug)] pub enum Keyframe { Static(StaticKeyframe), Animated(AnimatedKeyframe), } #[derive(Debug)] pub struct AliasModel { origin: Vector3, radius: f32, texture_width: u32, texture_height: u32, textures: Box<[Texture]>, texcoords: Box<[Texcoord]>, polygons: Box<[IndexedPolygon]>, keyframes: Box<[Keyframe]>, flags: ModelFlags, } impl AliasModel { pub fn origin(&self) -> Vector3 { self.origin } pub fn radius(&self) -> f32 { self.radius } pub fn texture_width(&self) -> u32 { self.texture_width } pub fn texture_height(&self) -> u32 { self.texture_height } pub fn textures(&self) -> &[Texture] { &self.textures } pub fn texcoords(&self) -> &[Texcoord] { &self.texcoords } pub fn polygons(&self) -> &[IndexedPolygon] { &self.polygons } pub fn keyframes(&self) -> &[Keyframe] { &self.keyframes } pub fn flags(&self) -> ModelFlags { self.flags } } pub fn load(data: R) -> Result where R: Read + Seek, { let mut reader = BufReader::new(data); // struct MdlHeader { // magic: i32 // version: i32 // scale: [f32; 3] // origin: [f32; 3] // radius: f32 // eye_position: [f32; 3] // texture_count: i32, // texture_width: i32, // texture_height: i32, // vertex_count: i32, // poly_count: i32, // keyframe_count: i32, // sync_type: i32, // flags_bits: i32, // } let magic = reader.read_i32::()?; if magic != MAGIC { Err(MdlFileError::InvalidMagicNumber(magic))?; } let version = reader.read_i32::()?; if version != VERSION { Err(MdlFileError::UnrecognizedVersion(version))?; } let scale: Vector3 = read_f32_3(&mut reader)?.into(); let origin: Vector3 = read_f32_3(&mut reader)?.into(); let radius = reader.read_f32::()?; let _eye_position: Vector3 = read_f32_3(&mut reader)?.into(); let texture_count = reader.read_i32::()?; let texture_width = reader.read_i32::()?; if texture_width <= 0 { Err(MdlFileError::InvalidTextureWidth(texture_width))?; } let texture_height = reader.read_i32::()?; if texture_height <= 0 { Err(MdlFileError::InvalidTextureHeight(texture_height))?; } let vertex_count = reader.read_i32::()?; if vertex_count <= 0 { Err(MdlFileError::InvalidVertexCount(vertex_count))?; } let poly_count = reader.read_i32::()?; if poly_count <= 0 { Err(MdlFileError::InvalidPolygonCount(poly_count))?; } let keyframe_count = reader.read_i32::()?; if keyframe_count <= 0 { Err(MdlFileError::InvalidKeyframeCount(keyframe_count))?; } let _sync_type = SyncType::from_i32(reader.read_i32::()?); let flags_bits = reader.read_i32::()?; if flags_bits < 0 || flags_bits > u8::MAX as i32 { Err(MdlFileError::InvalidFlags(flags_bits))?; } let flags = ModelFlags::from_bits(flags_bits as u8).ok_or(MdlFileError::InvalidFlags(flags_bits))?; // unused let _size = reader.read_i32::()?; assert_eq!( reader.seek(SeekFrom::Current(0))?, reader.seek(SeekFrom::Start(HEADER_SIZE))?, "Misaligned read on MDL header" ); let mut textures: Vec = Vec::with_capacity(texture_count as usize); for _ in 0..texture_count { // TODO: add a TextureKind type let texture = match reader.read_i32::()? { // Static 0 => { let mut indices: Vec = Vec::with_capacity((texture_width * texture_height) as usize); (&mut reader) .take((texture_width * texture_height) as u64) .read_to_end(&mut indices)?; Texture::Static(StaticTexture { indices: indices.into_boxed_slice(), }) } // Animated 1 => { // TODO: sanity check this value let texture_frame_count = reader.read_i32::()? as usize; let mut durations = Vec::with_capacity(texture_frame_count); for _ in 0..texture_frame_count { durations.push(engine::duration_from_f32( reader.read_f32::()?, )); } let mut frames = Vec::with_capacity(texture_frame_count); for frame_id in 0..texture_frame_count { let mut indices: Vec = Vec::with_capacity((texture_width * texture_height) as usize); (&mut reader) .take((texture_width * texture_height) as u64) .read_to_end(&mut indices)?; frames.push(AnimatedTextureFrame { duration: durations[frame_id as usize], indices: indices.into_boxed_slice(), }); } Texture::Animated(AnimatedTexture { frames: frames.into_boxed_slice(), }) } k => Err(MdlFileError::InvalidTextureKind(k))?, }; textures.push(texture); } let mut texcoords = Vec::with_capacity(vertex_count as usize); for _ in 0..vertex_count { let is_on_seam = match reader.read_i32::()? { 0 => false, 0x20 => true, x => Err(MdlFileError::InvalidSeamFlag(x))?, }; let s = reader.read_i32::()?; let t = reader.read_i32::()?; if s < 0 || t < 0 { Err(MdlFileError::InvalidTexcoord([s, t]))?; } texcoords.push(Texcoord { is_on_seam, s: s as u32, t: t as u32, }); } let mut polygons = Vec::with_capacity(poly_count as usize); for _ in 0..poly_count { let faces_front = match reader.read_i32::()? { 0 => false, 1 => true, x => Err(MdlFileError::InvalidFrontFacing(x))?, }; let mut indices = [0; 3]; for i in 0..3 { indices[i] = reader.read_i32::()? as u32; } polygons.push(IndexedPolygon { faces_front, indices, }); } let mut keyframes: Vec = Vec::with_capacity(keyframe_count as usize); for _ in 0..keyframe_count { keyframes.push(match reader.read_i32::()? { 0 => { let min = read_vertex(&mut reader, scale, origin)?; reader.read_u8()?; // discard vertex normal let max = read_vertex(&mut reader, scale, origin)?; reader.read_u8()?; // discard vertex normal let name = { let mut bytes: [u8; 16] = [0; 16]; reader.read(&mut bytes)?; let len = bytes .iter() .position(|b| *b == 0) .ok_or(MdlFileError::KeyframeNameTooLong(bytes))?; String::from_utf8(bytes[0..(len + 1)].to_vec())? }; debug!("Keyframe name: {}", name); let mut vertices: Vec> = Vec::with_capacity(vertex_count as usize); for _ in 0..vertex_count { vertices.push(read_vertex(&mut reader, scale, origin)?); reader.read_u8()?; // discard vertex normal } Keyframe::Static(StaticKeyframe { name, min, max, vertices: vertices.into_boxed_slice(), }) } 1 => { let subframe_count = match reader.read_i32::()? { s if s <= 0 => panic!("Invalid subframe count: {}", s), s => s, }; let abs_min = read_vertex(&mut reader, scale, origin)?; reader.read_u8()?; // discard vertex normal let abs_max = read_vertex(&mut reader, scale, origin)?; reader.read_u8()?; // discard vertex normal let mut durations = Vec::new(); for _ in 0..subframe_count { durations.push(engine::duration_from_f32( reader.read_f32::()?, )); } let mut subframes = Vec::new(); for subframe_id in 0..subframe_count { let min = read_vertex(&mut reader, scale, origin)?; reader.read_u8()?; // discard vertex normal let max = read_vertex(&mut reader, scale, origin)?; reader.read_u8()?; // discard vertex normal let name = { let mut bytes: [u8; 16] = [0; 16]; reader.read(&mut bytes)?; let len = bytes .iter() .position(|b| *b == 0) .ok_or(MdlFileError::KeyframeNameTooLong(bytes))?; String::from_utf8(bytes[0..(len + 1)].to_vec())? }; debug!("Frame name: {}", name); let mut vertices: Vec> = Vec::with_capacity(vertex_count as usize); for _ in 0..vertex_count { vertices.push(read_vertex(&mut reader, scale, origin)?); reader.read_u8()?; // discard vertex normal } subframes.push(AnimatedKeyframeFrame { min, max, name, duration: durations[subframe_id as usize], vertices: vertices.into_boxed_slice(), }) } Keyframe::Animated(AnimatedKeyframe { min: abs_min, max: abs_max, frames: subframes.into_boxed_slice(), }) } x => panic!("Bad frame kind value: {}", x), }); } if reader.seek(SeekFrom::Current(0))? != reader.seek(SeekFrom::End(0))? { panic!("Misaligned read on MDL file"); } Ok(AliasModel { origin, radius, texture_width: texture_width as u32, texture_height: texture_height as u32, textures: textures.into_boxed_slice(), texcoords: texcoords.into_boxed_slice(), polygons: polygons.into_boxed_slice(), keyframes: keyframes.into_boxed_slice(), flags, }) } fn read_vertex( reader: &mut R, scale: Vector3, translate: Vector3, ) -> Result, io::Error> where R: ReadBytesExt, { Ok(Vector3::new( reader.read_u8()? as f32, reader.read_u8()? as f32, reader.read_u8()? as f32, ) .mul_element_wise(scale) + translate) } ================================================ FILE: src/common/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. pub mod alloc; pub mod bitset; pub mod bsp; pub mod console; pub mod engine; pub mod host; pub mod math; pub mod mdl; pub mod model; pub mod net; pub mod pak; pub mod parse; pub mod sprite; pub mod util; pub mod vfs; pub mod wad; use std::path::PathBuf; pub fn default_base_dir() -> std::path::PathBuf { match std::env::current_dir() { Ok(cwd) => cwd, Err(e) => { log::error!("cannot access current directory: {}", e); std::process::exit(1); } } } pub const MAX_LIGHTSTYLES: usize = 64; /// The maximum number of `.pak` files that should be loaded at runtime. /// /// The original engine does not make this restriction, and this limit can be increased if need be. pub const MAX_PAKFILES: usize = 32; ================================================ FILE: src/common/model.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::common::{ bsp::{BspFileError, BspModel}, mdl::{self, AliasModel, MdlFileError}, sprite::{self, SpriteModel}, vfs::{Vfs, VfsError}, }; use cgmath::Vector3; use thiserror::Error; #[derive(Error, Debug)] pub enum ModelError { #[error("BSP file error: {0}")] BspFile(#[from] BspFileError), #[error("MDL file error: {0}")] MdlFile(#[from] MdlFileError), #[error("SPR file error")] SprFile, #[error("Virtual filesystem error: {0}")] Vfs(#[from] VfsError), } #[derive(Debug, FromPrimitive)] pub enum SyncType { Sync = 0, Rand = 1, } bitflags! { pub struct ModelFlags: u8 { const ROCKET = 0b00000001; const GRENADE = 0b00000010; const GIB = 0b00000100; const ROTATE = 0b00001000; const TRACER = 0b00010000; const ZOMGIB = 0b00100000; const TRACER2 = 0b01000000; const TRACER3 = 0b10000000; } } #[derive(Debug)] pub struct Model { pub name: String, pub kind: ModelKind, pub flags: ModelFlags, } #[derive(Debug)] pub enum ModelKind { // TODO: find a more elegant way to express the null model None, Brush(BspModel), Alias(AliasModel), Sprite(SpriteModel), } impl Model { pub fn none() -> Model { Model { name: String::new(), kind: ModelKind::None, flags: ModelFlags::empty(), } } pub fn kind(&self) -> &ModelKind { &self.kind } pub fn load(vfs: &Vfs, name: S) -> Result where S: AsRef, { let name = name.as_ref(); // TODO: original engine uses the magic numbers of each format instead of the extension. if name.ends_with(".bsp") { panic!("BSP files may contain multiple models, use bsp::load for this"); } else if name.ends_with(".mdl") { Ok(Model::from_alias_model( name.to_owned(), mdl::load(vfs.open(name)?)?, )) } else if name.ends_with(".spr") { Ok(Model::from_sprite_model( name.to_owned(), sprite::load(vfs.open(name)?), )) } else { panic!("Unrecognized model type: {}", name); } } /// Construct a new generic model from a brush model. pub fn from_brush_model(name: S, brush_model: BspModel) -> Model where S: AsRef, { Model { name: name.as_ref().to_owned(), kind: ModelKind::Brush(brush_model), flags: ModelFlags::empty(), } } /// Construct a new generic model from an alias model. pub fn from_alias_model(name: S, alias_model: AliasModel) -> Model where S: AsRef, { let flags = alias_model.flags(); Model { name: name.as_ref().to_owned(), kind: ModelKind::Alias(alias_model), flags, } } /// Construct a new generic model from a sprite model. pub fn from_sprite_model(name: S, sprite_model: SpriteModel) -> Model where S: AsRef, { Model { name: name.as_ref().to_owned(), kind: ModelKind::Sprite(sprite_model), flags: ModelFlags::empty(), } } /// Return the name of this model. pub fn name(&self) -> &str { &self.name } /// Return the minimum extent of this model. pub fn min(&self) -> Vector3 { debug!("Retrieving min of model {}", self.name); match self.kind { ModelKind::None => panic!("attempted to take min() of NULL model"), ModelKind::Brush(ref bmodel) => bmodel.min(), ModelKind::Sprite(ref smodel) => smodel.min(), // TODO: maybe change this? // https://github.com/id-Software/Quake/blob/master/WinQuake/gl_model.c#L1625 ModelKind::Alias(_) => Vector3::new(-16.0, -16.0, -16.0), } } /// Return the maximum extent of this model. pub fn max(&self) -> Vector3 { debug!("Retrieving max of model {}", self.name); match self.kind { ModelKind::None => panic!("attempted to take max() of NULL model"), ModelKind::Brush(ref bmodel) => bmodel.max(), ModelKind::Sprite(ref smodel) => smodel.max(), // TODO: maybe change this? // https://github.com/id-Software/Quake/blob/master/WinQuake/gl_model.c#L1625 ModelKind::Alias(_) => Vector3::new(16.0, 16.0, 16.0), } } pub fn sync_type(&self) -> SyncType { match self.kind { ModelKind::None => panic!("Attempted to take sync_type() of NULL model"), ModelKind::Brush(_) => SyncType::Sync, // TODO: expose sync_type in Sprite and reflect it here ModelKind::Sprite(ref _smodel) => SyncType::Sync, // TODO: expose sync_type in Mdl and reflect it here ModelKind::Alias(ref _amodel) => SyncType::Sync, } } pub fn flags(&self) -> ModelFlags { self.flags } pub fn has_flag(&self, flag: ModelFlags) -> bool { self.flags.contains(flag) } } ================================================ FILE: src/common/net/connect.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::{ io::{BufReader, Cursor, ErrorKind}, mem::size_of, net::{SocketAddr, ToSocketAddrs, UdpSocket}, }; use crate::common::{ net::{NetError, QSocket, MAX_MESSAGE}, util, }; use byteorder::{LittleEndian, NetworkEndian, ReadBytesExt, WriteBytesExt}; use chrono::Duration; use num::FromPrimitive; pub const CONNECT_PROTOCOL_VERSION: u8 = 3; const CONNECT_CONTROL: i32 = 1 << 31; const CONNECT_LENGTH_MASK: i32 = 0x0000FFFF; pub trait ConnectPacket { /// Returns the numeric value of this packet's code. fn code(&self) -> u8; /// Returns the length in bytes of this packet's content. fn content_len(&self) -> usize; /// Writes this packet's content to the given sink. fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt; /// Returns the total length in bytes of this packet, including the header. fn packet_len(&self) -> i32 { let mut len = 0; // control header len += size_of::(); // request/reply code len += size_of::(); len += self.content_len(); len as i32 } /// Generates the control header for this packet. fn control_header(&self) -> i32 { CONNECT_CONTROL | (self.packet_len() & CONNECT_LENGTH_MASK) } /// Generates the byte representation of this packet for transmission. fn to_bytes(&self) -> Result, NetError> { let mut writer = Cursor::new(Vec::new()); writer.write_i32::(self.control_header())?; writer.write_u8(self.code())?; self.write_content(&mut writer)?; let packet = writer.into_inner(); Ok(packet) } } #[derive(Debug, FromPrimitive)] pub enum RequestCode { Connect = 1, ServerInfo = 2, PlayerInfo = 3, RuleInfo = 4, } #[derive(Debug)] pub struct RequestConnect { pub game_name: String, pub proto_ver: u8, } impl ConnectPacket for RequestConnect { fn code(&self) -> u8 { RequestCode::Connect as u8 } fn content_len(&self) -> usize { let mut len = 0; // game name and terminating zero byte len += self.game_name.len() + size_of::(); // protocol version len += size_of::(); len } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write(self.game_name.as_bytes())?; writer.write_u8(0)?; writer.write_u8(self.proto_ver)?; Ok(()) } } #[derive(Debug)] pub struct RequestServerInfo { pub game_name: String, } impl ConnectPacket for RequestServerInfo { fn code(&self) -> u8 { RequestCode::ServerInfo as u8 } fn content_len(&self) -> usize { // game name and terminating zero byte self.game_name.len() + size_of::() } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write(self.game_name.as_bytes())?; writer.write_u8(0)?; Ok(()) } } #[derive(Debug)] pub struct RequestPlayerInfo { pub player_id: u8, } impl ConnectPacket for RequestPlayerInfo { fn code(&self) -> u8 { RequestCode::PlayerInfo as u8 } fn content_len(&self) -> usize { // player id size_of::() } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_u8(self.player_id)?; Ok(()) } } #[derive(Debug)] pub struct RequestRuleInfo { pub prev_cvar: String, } impl ConnectPacket for RequestRuleInfo { fn code(&self) -> u8 { RequestCode::RuleInfo as u8 } fn content_len(&self) -> usize { // previous cvar in rule chain and terminating zero byte self.prev_cvar.len() + size_of::() } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write(self.prev_cvar.as_bytes())?; writer.write_u8(0)?; Ok(()) } } /// A request from a client to retrieve information from or connect to the server. #[derive(Debug)] pub enum Request { Connect(RequestConnect), ServerInfo(RequestServerInfo), PlayerInfo(RequestPlayerInfo), RuleInfo(RequestRuleInfo), } impl Request { pub fn connect(game_name: S, proto_ver: u8) -> Request where S: AsRef, { Request::Connect(RequestConnect { game_name: game_name.as_ref().to_owned(), proto_ver, }) } pub fn server_info(game_name: S) -> Request where S: AsRef, { Request::ServerInfo(RequestServerInfo { game_name: game_name.as_ref().to_owned(), }) } pub fn player_info(player_id: u8) -> Request { Request::PlayerInfo(RequestPlayerInfo { player_id }) } pub fn rule_info(prev_cvar: S) -> Request where S: AsRef, { Request::RuleInfo(RequestRuleInfo { prev_cvar: prev_cvar.as_ref().to_string(), }) } } impl ConnectPacket for Request { fn code(&self) -> u8 { use self::Request::*; match *self { Connect(ref c) => c.code(), ServerInfo(ref s) => s.code(), PlayerInfo(ref p) => p.code(), RuleInfo(ref r) => r.code(), } } fn content_len(&self) -> usize { use self::Request::*; match *self { Connect(ref c) => c.content_len(), ServerInfo(ref s) => s.content_len(), PlayerInfo(ref p) => p.content_len(), RuleInfo(ref r) => r.content_len(), } } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { use self::Request::*; match *self { Connect(ref c) => c.write_content(writer), ServerInfo(ref s) => s.write_content(writer), PlayerInfo(ref p) => p.write_content(writer), RuleInfo(ref r) => r.write_content(writer), } } } #[derive(Debug, FromPrimitive)] pub enum ResponseCode { Accept = 0x81, Reject = 0x82, ServerInfo = 0x83, PlayerInfo = 0x84, RuleInfo = 0x85, } #[derive(Debug)] pub struct ResponseAccept { pub port: i32, } impl ConnectPacket for ResponseAccept { fn code(&self) -> u8 { ResponseCode::Accept as u8 } fn content_len(&self) -> usize { // port number size_of::() } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_i32::(self.port)?; Ok(()) } } #[derive(Debug)] pub struct ResponseReject { pub message: String, } impl ConnectPacket for ResponseReject { fn code(&self) -> u8 { ResponseCode::Reject as u8 } fn content_len(&self) -> usize { // message plus terminating zero byte self.message.len() + size_of::() } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write(self.message.as_bytes())?; writer.write_u8(0)?; Ok(()) } } #[derive(Debug)] pub struct ResponseServerInfo { pub address: String, pub hostname: String, pub levelname: String, pub client_count: u8, pub client_max: u8, pub protocol_version: u8, } impl ConnectPacket for ResponseServerInfo { fn code(&self) -> u8 { ResponseCode::ServerInfo as u8 } fn content_len(&self) -> usize { let mut len = 0; // address string and terminating zero byte len += self.address.len() + size_of::(); // hostname string and terminating zero byte len += self.hostname.len() + size_of::(); // levelname string and terminating zero byte len += self.levelname.len() + size_of::(); // current client count len += size_of::(); // maximum client count len += size_of::(); // protocol version len += size_of::(); len } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write(self.address.as_bytes())?; writer.write_u8(0)?; writer.write(self.hostname.as_bytes())?; writer.write_u8(0)?; writer.write(self.levelname.as_bytes())?; writer.write_u8(0)?; writer.write_u8(self.client_count)?; writer.write_u8(self.client_max)?; writer.write_u8(self.protocol_version)?; Ok(()) } } #[derive(Debug)] pub struct ResponsePlayerInfo { pub player_id: u8, pub player_name: String, pub colors: i32, pub frags: i32, pub connect_duration: i32, pub address: String, } impl ConnectPacket for ResponsePlayerInfo { fn code(&self) -> u8 { ResponseCode::PlayerInfo as u8 } fn content_len(&self) -> usize { let mut len = 0; // player id len += size_of::(); // player name and terminating zero byte len += self.player_name.len() + size_of::(); // colors len += size_of::(); // frags len += size_of::(); // connection duration len += size_of::(); // address and terminating zero byte len += self.address.len() + size_of::(); len } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_u8(self.player_id)?; writer.write(self.player_name.as_bytes())?; writer.write_u8(0)?; // NUL-terminate writer.write_i32::(self.colors)?; writer.write_i32::(self.frags)?; writer.write_i32::(self.connect_duration)?; writer.write(self.address.as_bytes())?; writer.write_u8(0)?; Ok(()) } } #[derive(Debug)] pub struct ResponseRuleInfo { pub cvar_name: String, pub cvar_val: String, } impl ConnectPacket for ResponseRuleInfo { fn code(&self) -> u8 { ResponseCode::RuleInfo as u8 } fn content_len(&self) -> usize { let mut len = 0; // cvar name and terminating zero byte len += self.cvar_name.len() + size_of::(); // cvar val and terminating zero byte len += self.cvar_val.len() + size_of::(); len } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write(self.cvar_name.as_bytes())?; writer.write_u8(0)?; writer.write(self.cvar_val.as_bytes())?; writer.write_u8(0)?; Ok(()) } } #[derive(Debug)] pub enum Response { Accept(ResponseAccept), Reject(ResponseReject), ServerInfo(ResponseServerInfo), PlayerInfo(ResponsePlayerInfo), RuleInfo(ResponseRuleInfo), } impl ConnectPacket for Response { fn code(&self) -> u8 { use self::Response::*; match *self { Accept(ref a) => a.code(), Reject(ref r) => r.code(), ServerInfo(ref s) => s.code(), PlayerInfo(ref p) => p.code(), RuleInfo(ref r) => r.code(), } } fn content_len(&self) -> usize { use self::Response::*; match *self { Accept(ref a) => a.content_len(), Reject(ref r) => r.content_len(), ServerInfo(ref s) => s.content_len(), PlayerInfo(ref p) => p.content_len(), RuleInfo(ref r) => r.content_len(), } } fn write_content(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { use self::Response::*; match *self { Accept(ref a) => a.write_content(writer), Reject(ref r) => r.write_content(writer), ServerInfo(ref s) => s.write_content(writer), PlayerInfo(ref p) => p.write_content(writer), RuleInfo(ref r) => r.write_content(writer), } } } /// A socket that listens for new connections or queries. pub struct ConnectListener { socket: UdpSocket, } impl ConnectListener { /// Creates a `ConnectListener` from the given address. pub fn bind(addr: A) -> Result where A: ToSocketAddrs, { let socket = UdpSocket::bind(addr)?; Ok(ConnectListener { socket }) } /// Receives a request and returns it along with its remote address. pub fn recv_request(&self) -> Result<(Request, SocketAddr), NetError> { // Original engine receives connection requests in `net_message`, // allocated at https://github.com/id-Software/Quake/blob/master/WinQuake/net_main.c#L851 let mut recv_buf = [0u8; MAX_MESSAGE]; let (len, remote) = self.socket.recv_from(&mut recv_buf)?; let mut reader = BufReader::new(&recv_buf[..len]); let control = reader.read_i32::()?; // TODO: figure out what a control value of -1 means if control == -1 { return Err(NetError::with_msg("Control value is -1")); } // high 4 bits must be 0x8000 (CONNECT_CONTROL) if control & !CONNECT_LENGTH_MASK != CONNECT_CONTROL { return Err(NetError::InvalidData(format!( "control value {:X}", control & !CONNECT_LENGTH_MASK ))); } // low 4 bits must be total length of packet let control_len = (control & CONNECT_LENGTH_MASK) as usize; if control_len != len { return Err(NetError::InvalidData(format!( "Actual packet length ({}) differs from header value ({})", len, control_len, ))); } // validate request code let request_byte = reader.read_u8()?; let request_code = match RequestCode::from_u8(request_byte) { Some(r) => r, None => { return Err(NetError::InvalidData(format!( "request code {}", request_byte ))) } }; let request = match request_code { RequestCode::Connect => { let game_name = util::read_cstring(&mut reader).unwrap(); let proto_ver = reader.read_u8()?; Request::Connect(RequestConnect { game_name, proto_ver, }) } RequestCode::ServerInfo => { let game_name = util::read_cstring(&mut reader).unwrap(); Request::ServerInfo(RequestServerInfo { game_name }) } RequestCode::PlayerInfo => { let player_id = reader.read_u8()?; Request::PlayerInfo(RequestPlayerInfo { player_id }) } RequestCode::RuleInfo => { let prev_cvar = util::read_cstring(&mut reader).unwrap(); Request::RuleInfo(RequestRuleInfo { prev_cvar }) } }; Ok((request, remote)) } pub fn send_response(&self, response: Response, remote: SocketAddr) -> Result<(), NetError> { self.socket.send_to(&response.to_bytes()?, remote)?; Ok(()) } } pub struct ConnectSocket { socket: UdpSocket, } impl ConnectSocket { pub fn bind(local: A) -> Result where A: ToSocketAddrs, { let socket = UdpSocket::bind(local)?; Ok(ConnectSocket { socket }) } pub fn into_qsocket(self, remote: SocketAddr) -> QSocket { QSocket::new(self.socket, remote) } /// Send a `Request` to the server at the specified address. pub fn send_request(&mut self, request: Request, remote: SocketAddr) -> Result<(), NetError> { self.socket.send_to(&request.to_bytes()?, remote)?; Ok(()) } /// Receive a `Response` from the server. /// /// If `timeout` is not `None`, the operation times out after the specified duration and the /// function returns `None`. pub fn recv_response( &mut self, timeout: Option, ) -> Result, NetError> { let mut recv_buf = [0u8; MAX_MESSAGE]; // if a timeout was specified, apply it for this recv self.socket .set_read_timeout(timeout.map(|d| d.to_std().unwrap()))?; let (len, remote) = match self.socket.recv_from(&mut recv_buf) { Err(e) => match e.kind() { ErrorKind::WouldBlock | ErrorKind::TimedOut => return Ok(None), _ => return Err(NetError::from(e)), }, Ok(ret) => ret, }; self.socket.set_read_timeout(None)?; let mut reader = BufReader::new(&recv_buf[..len]); let control = reader.read_i32::()?; // TODO: figure out what a control value of -1 means if control == -1 { return Err(NetError::with_msg("Control value is -1")); } // high 4 bits must be 0x8000 (CONNECT_CONTROL) if control & !CONNECT_LENGTH_MASK != CONNECT_CONTROL { return Err(NetError::InvalidData(format!( "control value {:X}", control & !CONNECT_LENGTH_MASK ))); } // low 4 bits must be total length of packet let control_len = (control & CONNECT_LENGTH_MASK) as usize; if control_len != len { return Err(NetError::with_msg(format!( "Actual packet length ({}) differs from header value ({})", len, control_len, ))); } let response_byte = reader.read_u8()?; let response_code = match ResponseCode::from_u8(response_byte) { Some(r) => r, None => { return Err(NetError::InvalidData(format!( "response code {}", response_byte ))) } }; let response = match response_code { ResponseCode::Accept => { let port = reader.read_i32::()?; Response::Accept(ResponseAccept { port }) } ResponseCode::Reject => { let message = util::read_cstring(&mut reader).unwrap(); Response::Reject(ResponseReject { message }) } ResponseCode::ServerInfo => { let address = util::read_cstring(&mut reader).unwrap(); let hostname = util::read_cstring(&mut reader).unwrap(); let levelname = util::read_cstring(&mut reader).unwrap(); let client_count = reader.read_u8()?; let client_max = reader.read_u8()?; let protocol_version = reader.read_u8()?; Response::ServerInfo(ResponseServerInfo { address, hostname, levelname, client_count, client_max, protocol_version, }) } ResponseCode::PlayerInfo => unimplemented!(), ResponseCode::RuleInfo => unimplemented!(), }; Ok(Some((response, remote))) } } #[cfg(test)] mod test { use super::*; // test_request_*_packet_len // // These tests ensure that ConnectPacket::packet_len() returns an accurate value by comparing it // with the number of bytes returned by ConnectPacket::to_bytes(). #[test] fn test_request_connect_packet_len() { let request_connect = RequestConnect { game_name: String::from("QUAKE"), proto_ver: CONNECT_PROTOCOL_VERSION, }; let packet_len = request_connect.packet_len() as usize; let packet = request_connect.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_request_server_info_packet_len() { let request_server_info = RequestServerInfo { game_name: String::from("QUAKE"), }; let packet_len = request_server_info.packet_len() as usize; let packet = request_server_info.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_request_player_info_packet_len() { let request_player_info = RequestPlayerInfo { player_id: 0 }; let packet_len = request_player_info.packet_len() as usize; let packet = request_player_info.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_request_rule_info_packet_len() { let request_rule_info = RequestRuleInfo { prev_cvar: String::from("sv_gravity"), }; let packet_len = request_rule_info.packet_len() as usize; let packet = request_rule_info.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_response_accept_packet_len() { let response_accept = ResponseAccept { port: 26000 }; let packet_len = response_accept.packet_len() as usize; let packet = response_accept.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_response_reject_packet_len() { let response_reject = ResponseReject { message: String::from("error"), }; let packet_len = response_reject.packet_len() as usize; let packet = response_reject.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_response_server_info_packet_len() { let response_server_info = ResponseServerInfo { address: String::from("127.0.0.1"), hostname: String::from("localhost"), levelname: String::from("e1m1"), client_count: 1, client_max: 16, protocol_version: 15, }; let packet_len = response_server_info.packet_len() as usize; let packet = response_server_info.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_response_player_info_packet_len() { let response_player_info = ResponsePlayerInfo { player_id: 0, player_name: String::from("player"), colors: 0, frags: 0, connect_duration: 120, address: String::from("127.0.0.1"), }; let packet_len = response_player_info.packet_len() as usize; let packet = response_player_info.to_bytes().unwrap(); assert_eq!(packet_len, packet.len()); } #[test] fn test_connect_listener_bind() { let _listener = ConnectListener::bind("127.0.0.1:26000").unwrap(); } } ================================================ FILE: src/common/net/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // TODO: need to figure out an equivalence relation for read_/write_coord and read_/write_angle pub mod connect; use std::{ collections::VecDeque, error::Error, fmt, io::{BufRead, BufReader, Cursor, Read, Write}, net::{SocketAddr, UdpSocket}, }; use crate::common::{engine, util}; use byteorder::{LittleEndian, NetworkEndian, ReadBytesExt, WriteBytesExt}; use cgmath::{Deg, Vector3, Zero}; use chrono::Duration; use num::FromPrimitive; pub const MAX_MESSAGE: usize = 8192; const MAX_DATAGRAM: usize = 1024; const HEADER_SIZE: usize = 8; const MAX_PACKET: usize = HEADER_SIZE + MAX_DATAGRAM; pub const PROTOCOL_VERSION: u8 = 15; const NAME_LEN: usize = 64; const FAST_UPDATE_FLAG: u8 = 0x80; const VELOCITY_READ_FACTOR: f32 = 16.0; const VELOCITY_WRITE_FACTOR: f32 = 1.0 / VELOCITY_READ_FACTOR; const PARTICLE_DIRECTION_READ_FACTOR: f32 = 1.0 / 16.0; const PARTICLE_DIRECTION_WRITE_FACTOR: f32 = 1.0 / PARTICLE_DIRECTION_READ_FACTOR; const SOUND_ATTENUATION_WRITE_FACTOR: u8 = 64; const SOUND_ATTENUATION_READ_FACTOR: f32 = 1.0 / SOUND_ATTENUATION_WRITE_FACTOR as f32; pub static GAME_NAME: &'static str = "QUAKE"; pub const MAX_CLIENTS: usize = 16; pub const MAX_ITEMS: usize = 32; pub const DEFAULT_VIEWHEIGHT: f32 = 22.0; #[derive(Debug)] pub enum NetError { Io(::std::io::Error), InvalidData(String), Other(String), } impl NetError { pub fn with_msg(msg: S) -> Self where S: AsRef, { NetError::Other(msg.as_ref().to_owned()) } } impl fmt::Display for NetError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { NetError::Io(ref err) => { write!(f, "I/O error: ")?; err.fmt(f) } NetError::InvalidData(ref msg) => write!(f, "Invalid data: {}", msg), NetError::Other(ref msg) => write!(f, "{}", msg), } } } impl Error for NetError { fn description(&self) -> &str { match *self { NetError::Io(ref err) => err.description(), NetError::InvalidData(_) => "Invalid data", NetError::Other(ref msg) => &msg, } } } impl From<::std::io::Error> for NetError { fn from(error: ::std::io::Error) -> Self { NetError::Io(error) } } // the original engine treats these as bitflags, but all of them are mutually exclusive except for // NETFLAG_DATA (reliable message) and NETFLAG_EOM (end of reliable message). #[derive(Debug, Eq, FromPrimitive, PartialEq)] pub enum MsgKind { Reliable = 0x0001, Ack = 0x0002, ReliableEom = 0x0009, Unreliable = 0x0010, Ctl = 0x8000, } bitflags! { pub struct UpdateFlags: u16 { const MORE_BITS = 1 << 0; const ORIGIN_X = 1 << 1; const ORIGIN_Y = 1 << 2; const ORIGIN_Z = 1 << 3; const YAW = 1 << 4; const NO_LERP = 1 << 5; const FRAME = 1 << 6; const SIGNAL = 1 << 7; const PITCH = 1 << 8; const ROLL = 1 << 9; const MODEL = 1 << 10; const COLORMAP = 1 << 11; const SKIN = 1 << 12; const EFFECTS = 1 << 13; const LONG_ENTITY = 1 << 14; } } bitflags! { pub struct ClientUpdateFlags: u16 { const VIEW_HEIGHT = 1 << 0; const IDEAL_PITCH = 1 << 1; const PUNCH_PITCH = 1 << 2; const PUNCH_YAW = 1 << 3; const PUNCH_ROLL = 1 << 4; const VELOCITY_X = 1 << 5; const VELOCITY_Y = 1 << 6; const VELOCITY_Z = 1 << 7; // const AIM_ENT = 1 << 8; // unused const ITEMS = 1 << 9; const ON_GROUND = 1 << 10; const IN_WATER = 1 << 11; const WEAPON_FRAME = 1 << 12; const ARMOR = 1 << 13; const WEAPON = 1 << 14; } } bitflags! { pub struct SoundFlags: u8 { const VOLUME = 1 << 0; const ATTENUATION = 1 << 1; const LOOPING = 1 << 2; } } bitflags! { pub struct ItemFlags: u32 { const SHOTGUN = 0x00000001; const SUPER_SHOTGUN = 0x00000002; const NAILGUN = 0x00000004; const SUPER_NAILGUN = 0x00000008; const GRENADE_LAUNCHER = 0x00000010; const ROCKET_LAUNCHER = 0x00000020; const LIGHTNING = 0x00000040; const SUPER_LIGHTNING = 0x00000080; const SHELLS = 0x00000100; const NAILS = 0x00000200; const ROCKETS = 0x00000400; const CELLS = 0x00000800; const AXE = 0x00001000; const ARMOR_1 = 0x00002000; const ARMOR_2 = 0x00004000; const ARMOR_3 = 0x00008000; const SUPER_HEALTH = 0x00010000; const KEY_1 = 0x00020000; const KEY_2 = 0x00040000; const INVISIBILITY = 0x00080000; const INVULNERABILITY = 0x00100000; const SUIT = 0x00200000; const QUAD = 0x00400000; const SIGIL_1 = 0x10000000; const SIGIL_2 = 0x20000000; const SIGIL_3 = 0x40000000; const SIGIL_4 = 0x80000000; } } bitflags! { pub struct ButtonFlags: u8 { const ATTACK = 0x01; const JUMP = 0x02; } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct PlayerColor { top: u8, bottom: u8, } impl PlayerColor { pub fn new(top: u8, bottom: u8) -> PlayerColor { if top > 15 { warn!("Top color index ({}) will be truncated", top); } if bottom > 15 { warn!("Bottom color index ({}) will be truncated", bottom); } PlayerColor { top, bottom } } pub fn from_bits(bits: u8) -> PlayerColor { let top = bits >> 4; let bottom = bits & 0x0F; PlayerColor { top, bottom } } pub fn bits(&self) -> u8 { self.top << 4 | (self.bottom & 0x0F) } } impl ::std::convert::From for PlayerColor { fn from(src: u8) -> PlayerColor { PlayerColor { top: src << 4, bottom: src & 0x0F, } } } #[derive(Clone, Copy, Debug)] pub struct ColorShift { pub dest_color: [u8; 3], pub percent: i32, } #[derive(Copy, Clone, Debug, Eq, FromPrimitive, PartialEq)] pub enum ClientStat { Health = 0, Frags = 1, Weapon = 2, Ammo = 3, Armor = 4, WeaponFrame = 5, Shells = 6, Nails = 7, Rockets = 8, Cells = 9, ActiveWeapon = 10, TotalSecrets = 11, TotalMonsters = 12, FoundSecrets = 13, KilledMonsters = 14, } /// Numeric codes used to identify the type of a temporary entity. #[derive(Debug, Eq, FromPrimitive, PartialEq)] pub enum TempEntityCode { Spike = 0, SuperSpike = 1, Gunshot = 2, Explosion = 3, TarExplosion = 4, Lightning1 = 5, Lightning2 = 6, WizSpike = 7, KnightSpike = 8, Lightning3 = 9, LavaSplash = 10, Teleport = 11, ColorExplosion = 12, Grapple = 13, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PointEntityKind { Spike, SuperSpike, Gunshot, Explosion, ColorExplosion { color_start: u8, color_len: u8 }, TarExplosion, WizSpike, KnightSpike, LavaSplash, Teleport, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum BeamEntityKind { /// Lightning bolt Lightning { /// id of the lightning model to use. must be 1, 2, or 3. model_id: u8, }, /// Grappling hook cable Grapple, } #[derive(Clone, Debug, PartialEq)] pub enum TempEntity { Point { kind: PointEntityKind, origin: Vector3, }, Beam { kind: BeamEntityKind, entity_id: i16, start: Vector3, end: Vector3, }, } impl TempEntity { pub fn read_temp_entity(reader: &mut R) -> Result where R: BufRead + ReadBytesExt, { let code_byte = reader.read_u8()?; let code = match TempEntityCode::from_u8(code_byte) { Some(c) => c, None => { return Err(NetError::InvalidData(format!( "Temp entity code {}", code_byte ))) } }; use TempEntity::*; use TempEntityCode as Code; Ok(match code { Code::Spike | Code::SuperSpike | Code::Gunshot | Code::Explosion | Code::TarExplosion | Code::WizSpike | Code::KnightSpike | Code::LavaSplash | Code::Teleport => Point { kind: match code { Code::Spike => PointEntityKind::Spike, Code::SuperSpike => PointEntityKind::SuperSpike, Code::Gunshot => PointEntityKind::Gunshot, Code::Explosion => PointEntityKind::Explosion, Code::TarExplosion => PointEntityKind::TarExplosion, Code::WizSpike => PointEntityKind::WizSpike, Code::KnightSpike => PointEntityKind::KnightSpike, Code::LavaSplash => PointEntityKind::LavaSplash, Code::Teleport => PointEntityKind::Teleport, _ => unreachable!(), }, origin: read_coord_vector3(reader)?, }, Code::ColorExplosion => { let origin = read_coord_vector3(reader)?; let color_start = reader.read_u8()?; let color_len = reader.read_u8()?; Point { origin, kind: PointEntityKind::ColorExplosion { color_start, color_len, }, } } Code::Lightning1 | Code::Lightning2 | Code::Lightning3 => Beam { kind: BeamEntityKind::Lightning { model_id: match code { Code::Lightning1 => 1, Code::Lightning2 => 2, Code::Lightning3 => 3, _ => unreachable!(), }, }, entity_id: reader.read_i16::()?, start: read_coord_vector3(reader)?, end: read_coord_vector3(reader)?, }, Code::Grapple => Beam { kind: BeamEntityKind::Grapple, entity_id: reader.read_i16::()?, start: read_coord_vector3(reader)?, end: read_coord_vector3(reader)?, }, }) } pub fn write_temp_entity(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { use TempEntityCode as Code; match *self { TempEntity::Point { kind, origin } => { use PointEntityKind as Pk; match kind { Pk::Spike | Pk::SuperSpike | Pk::Gunshot | Pk::Explosion | Pk::TarExplosion | Pk::WizSpike | Pk::KnightSpike | Pk::LavaSplash | Pk::Teleport => { let code = match kind { Pk::Spike => Code::Spike, Pk::SuperSpike => Code::SuperSpike, Pk::Gunshot => Code::Gunshot, Pk::Explosion => Code::Explosion, Pk::TarExplosion => Code::TarExplosion, Pk::WizSpike => Code::WizSpike, Pk::KnightSpike => Code::KnightSpike, Pk::LavaSplash => Code::LavaSplash, Pk::Teleport => Code::Teleport, _ => unreachable!(), }; // write code writer.write_u8(code as u8)?; } PointEntityKind::ColorExplosion { color_start, color_len, } => { // write code and colors writer.write_u8(Code::ColorExplosion as u8)?; writer.write_u8(color_start)?; writer.write_u8(color_len)?; } }; write_coord_vector3(writer, origin)?; } TempEntity::Beam { kind, entity_id, start, end, } => { let code = match kind { BeamEntityKind::Lightning { model_id } => match model_id { 1 => Code::Lightning1, 2 => Code::Lightning2, 3 => Code::Lightning3, // TODO: error _ => panic!("invalid lightning model id: {}", model_id), }, BeamEntityKind::Grapple => Code::Grapple, }; writer.write_i16::(entity_id)?; writer.write_u8(code as u8)?; write_coord_vector3(writer, start)?; write_coord_vector3(writer, end)?; } } Ok(()) } } #[derive(Copy, Clone, Ord, Debug, Eq, FromPrimitive, PartialOrd, PartialEq)] pub enum SignOnStage { Not = 0, Prespawn = 1, ClientInfo = 2, Begin = 3, Done = 4, } bitflags! { pub struct EntityEffects: u8 { const BRIGHT_FIELD = 0b0001; const MUZZLE_FLASH = 0b0010; const BRIGHT_LIGHT = 0b0100; const DIM_LIGHT = 0b1000; } } #[derive(Clone, Debug)] pub struct EntityState { pub origin: Vector3, pub angles: Vector3>, pub model_id: usize, pub frame_id: usize, // TODO: more specific types for these pub colormap: u8, pub skin_id: usize, pub effects: EntityEffects, } impl EntityState { pub fn uninitialized() -> EntityState { EntityState { origin: Vector3::new(0.0, 0.0, 0.0), angles: Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)), model_id: 0, frame_id: 0, colormap: 0, skin_id: 0, effects: EntityEffects::empty(), } } } #[derive(Clone, Debug, PartialEq)] pub struct EntityUpdate { pub ent_id: u16, pub model_id: Option, pub frame_id: Option, pub colormap: Option, pub skin_id: Option, pub effects: Option, pub origin_x: Option, pub pitch: Option>, pub origin_y: Option, pub yaw: Option>, pub origin_z: Option, pub roll: Option>, pub no_lerp: bool, } #[derive(Clone, Debug, PartialEq)] pub struct PlayerData { pub view_height: Option, pub ideal_pitch: Option>, pub punch_pitch: Option>, pub velocity_x: Option, pub punch_yaw: Option>, pub velocity_y: Option, pub punch_roll: Option>, pub velocity_z: Option, pub items: ItemFlags, pub on_ground: bool, pub in_water: bool, pub weapon_frame: Option, pub armor: Option, pub weapon: Option, pub health: i16, pub ammo: u8, pub ammo_shells: u8, pub ammo_nails: u8, pub ammo_rockets: u8, pub ammo_cells: u8, pub active_weapon: u8, } impl EntityUpdate { /// Create an `EntityState` from this update, filling in any `None` values /// from the specified baseline state. pub fn to_entity_state(&self, baseline: &EntityState) -> EntityState { EntityState { origin: Vector3::new( self.origin_x.unwrap_or(baseline.origin.x), self.origin_y.unwrap_or(baseline.origin.y), self.origin_z.unwrap_or(baseline.origin.z), ), angles: Vector3::new( self.pitch.unwrap_or(baseline.angles[0]), self.yaw.unwrap_or(baseline.angles[1]), self.roll.unwrap_or(baseline.angles[2]), ), model_id: self.model_id.map_or(baseline.model_id, |m| m as usize), frame_id: self.frame_id.map_or(baseline.frame_id, |f| f as usize), skin_id: self.skin_id.map_or(baseline.skin_id, |s| s as usize), effects: self.effects.unwrap_or(baseline.effects), colormap: self.colormap.unwrap_or(baseline.colormap), } } } /// A trait for in-game server and client network commands. pub trait Cmd: Sized { /// Returns the numeric value of this command's code. fn code(&self) -> u8; /// Reads data from the given source and constructs a command object. fn deserialize(reader: &mut R) -> Result where R: BufRead + ReadBytesExt; /// Writes this command's content to the given sink. fn serialize(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt; } // TODO: use feature(arbitrary_enum_discriminant) #[derive(Debug, FromPrimitive)] pub enum ServerCmdCode { Bad = 0, NoOp = 1, Disconnect = 2, UpdateStat = 3, Version = 4, SetView = 5, Sound = 6, Time = 7, Print = 8, StuffText = 9, SetAngle = 10, ServerInfo = 11, LightStyle = 12, UpdateName = 13, UpdateFrags = 14, PlayerData = 15, StopSound = 16, UpdateColors = 17, Particle = 18, Damage = 19, SpawnStatic = 20, // SpawnBinary = 21, // unused SpawnBaseline = 22, TempEntity = 23, SetPause = 24, SignOnStage = 25, CenterPrint = 26, KilledMonster = 27, FoundSecret = 28, SpawnStaticSound = 29, Intermission = 30, Finale = 31, CdTrack = 32, SellScreen = 33, Cutscene = 34, } #[derive(Copy, Clone, Debug, Eq, FromPrimitive, PartialEq)] pub enum GameType { CoOp = 0, Deathmatch = 1, } #[derive(Debug, PartialEq)] pub enum ServerCmd { Bad, NoOp, Disconnect, UpdateStat { stat: ClientStat, value: i32, }, Version { version: i32, }, SetView { ent_id: i16, }, Sound { volume: Option, attenuation: Option, entity_id: u16, channel: i8, sound_id: u8, position: Vector3, }, Time { time: f32, }, Print { text: String, }, StuffText { text: String, }, SetAngle { angles: Vector3>, }, ServerInfo { protocol_version: i32, max_clients: u8, game_type: GameType, message: String, model_precache: Vec, sound_precache: Vec, }, LightStyle { id: u8, value: String, }, UpdateName { player_id: u8, new_name: String, }, UpdateFrags { player_id: u8, new_frags: i16, }, PlayerData(PlayerData), StopSound { entity_id: u16, channel: u8, }, UpdateColors { player_id: u8, new_colors: PlayerColor, }, Particle { origin: Vector3, direction: Vector3, count: u8, color: u8, }, Damage { armor: u8, blood: u8, source: Vector3, }, SpawnStatic { model_id: u8, frame_id: u8, colormap: u8, skin_id: u8, origin: Vector3, angles: Vector3>, }, // SpawnBinary, // unused SpawnBaseline { ent_id: u16, model_id: u8, frame_id: u8, colormap: u8, skin_id: u8, origin: Vector3, angles: Vector3>, }, TempEntity { temp_entity: TempEntity, }, SetPause { paused: bool, }, SignOnStage { stage: SignOnStage, }, CenterPrint { text: String, }, KilledMonster, FoundSecret, SpawnStaticSound { origin: Vector3, sound_id: u8, volume: u8, attenuation: u8, }, Intermission, Finale { text: String, }, CdTrack { track: u8, loop_: u8, }, SellScreen, Cutscene { text: String, }, FastUpdate(EntityUpdate), } impl ServerCmd { pub fn code(&self) -> u8 { let code = match *self { ServerCmd::Bad => ServerCmdCode::Bad, ServerCmd::NoOp => ServerCmdCode::NoOp, ServerCmd::Disconnect => ServerCmdCode::Disconnect, ServerCmd::UpdateStat { .. } => ServerCmdCode::UpdateStat, ServerCmd::Version { .. } => ServerCmdCode::Version, ServerCmd::SetView { .. } => ServerCmdCode::SetView, ServerCmd::Sound { .. } => ServerCmdCode::Sound, ServerCmd::Time { .. } => ServerCmdCode::Time, ServerCmd::Print { .. } => ServerCmdCode::Print, ServerCmd::StuffText { .. } => ServerCmdCode::StuffText, ServerCmd::SetAngle { .. } => ServerCmdCode::SetAngle, ServerCmd::ServerInfo { .. } => ServerCmdCode::ServerInfo, ServerCmd::LightStyle { .. } => ServerCmdCode::LightStyle, ServerCmd::UpdateName { .. } => ServerCmdCode::UpdateName, ServerCmd::UpdateFrags { .. } => ServerCmdCode::UpdateFrags, ServerCmd::PlayerData(_) => ServerCmdCode::PlayerData, ServerCmd::StopSound { .. } => ServerCmdCode::StopSound, ServerCmd::UpdateColors { .. } => ServerCmdCode::UpdateColors, ServerCmd::Particle { .. } => ServerCmdCode::Particle, ServerCmd::Damage { .. } => ServerCmdCode::Damage, ServerCmd::SpawnStatic { .. } => ServerCmdCode::SpawnStatic, ServerCmd::SpawnBaseline { .. } => ServerCmdCode::SpawnBaseline, ServerCmd::TempEntity { .. } => ServerCmdCode::TempEntity, ServerCmd::SetPause { .. } => ServerCmdCode::SetPause, ServerCmd::SignOnStage { .. } => ServerCmdCode::SignOnStage, ServerCmd::CenterPrint { .. } => ServerCmdCode::CenterPrint, ServerCmd::KilledMonster => ServerCmdCode::KilledMonster, ServerCmd::FoundSecret => ServerCmdCode::FoundSecret, ServerCmd::SpawnStaticSound { .. } => ServerCmdCode::SpawnStaticSound, ServerCmd::Intermission => ServerCmdCode::Intermission, ServerCmd::Finale { .. } => ServerCmdCode::Finale, ServerCmd::CdTrack { .. } => ServerCmdCode::CdTrack, ServerCmd::SellScreen => ServerCmdCode::SellScreen, ServerCmd::Cutscene { .. } => ServerCmdCode::Cutscene, // TODO: figure out a more elegant way of doing this ServerCmd::FastUpdate(_) => panic!("FastUpdate has no code"), }; code as u8 } pub fn deserialize(reader: &mut R) -> Result, NetError> where R: BufRead + ReadBytesExt, { let code_num = match reader.read_u8() { Ok(c) => c, Err(ref e) if e.kind() == ::std::io::ErrorKind::UnexpectedEof => return Ok(None), Err(e) => return Err(NetError::from(e)), }; if code_num & FAST_UPDATE_FLAG != 0 { let all_bits; let low_bits = code_num & !FAST_UPDATE_FLAG; if low_bits & UpdateFlags::MORE_BITS.bits() as u8 != 0 { let high_bits = reader.read_u8()?; all_bits = (high_bits as u16) << 8 | low_bits as u16; } else { all_bits = low_bits as u16; } let update_flags = match UpdateFlags::from_bits(all_bits) { Some(u) => u, None => { return Err(NetError::InvalidData(format!( "UpdateFlags: {:b}", all_bits ))) } }; let ent_id; if update_flags.contains(UpdateFlags::LONG_ENTITY) { ent_id = reader.read_u16::()?; } else { ent_id = reader.read_u8()? as u16; } let model_id; if update_flags.contains(UpdateFlags::MODEL) { model_id = Some(reader.read_u8()?); } else { model_id = None; } let frame_id; if update_flags.contains(UpdateFlags::FRAME) { frame_id = Some(reader.read_u8()?); } else { frame_id = None; } let colormap; if update_flags.contains(UpdateFlags::COLORMAP) { colormap = Some(reader.read_u8()?); } else { colormap = None; } let skin_id; if update_flags.contains(UpdateFlags::SKIN) { skin_id = Some(reader.read_u8()?); } else { skin_id = None; } let effects; if update_flags.contains(UpdateFlags::EFFECTS) { let effects_bits = reader.read_u8()?; effects = match EntityEffects::from_bits(effects_bits) { Some(e) => Some(e), None => { return Err(NetError::InvalidData(format!( "EntityEffects: {:b}", effects_bits ))) } }; } else { effects = None; } let origin_x; if update_flags.contains(UpdateFlags::ORIGIN_X) { origin_x = Some(read_coord(reader)?); } else { origin_x = None; } let pitch; if update_flags.contains(UpdateFlags::PITCH) { pitch = Some(read_angle(reader)?); } else { pitch = None; } let origin_y; if update_flags.contains(UpdateFlags::ORIGIN_Y) { origin_y = Some(read_coord(reader)?); } else { origin_y = None; } let yaw; if update_flags.contains(UpdateFlags::YAW) { yaw = Some(read_angle(reader)?); } else { yaw = None; } let origin_z; if update_flags.contains(UpdateFlags::ORIGIN_Z) { origin_z = Some(read_coord(reader)?); } else { origin_z = None; } let roll; if update_flags.contains(UpdateFlags::ROLL) { roll = Some(read_angle(reader)?); } else { roll = None; } let no_lerp = update_flags.contains(UpdateFlags::NO_LERP); return Ok(Some(ServerCmd::FastUpdate(EntityUpdate { ent_id, model_id, frame_id, colormap, skin_id, effects, origin_x, pitch, origin_y, yaw, origin_z, roll, no_lerp, }))); } let code = match ServerCmdCode::from_u8(code_num) { Some(c) => c, None => { return Err(NetError::InvalidData(format!( "Invalid server command code: {}", code_num ))) } }; let cmd = match code { ServerCmdCode::Bad => ServerCmd::Bad, ServerCmdCode::NoOp => ServerCmd::NoOp, ServerCmdCode::Disconnect => ServerCmd::Disconnect, ServerCmdCode::UpdateStat => { let stat_id = reader.read_u8()?; let stat = match ClientStat::from_u8(stat_id) { Some(c) => c, None => { return Err(NetError::InvalidData(format!( "value for ClientStat: {}", stat_id, ))) } }; let value = reader.read_i32::()?; ServerCmd::UpdateStat { stat, value } } ServerCmdCode::Version => { let version = reader.read_i32::()?; ServerCmd::Version { version } } ServerCmdCode::SetView => { let ent_id = reader.read_i16::()?; ServerCmd::SetView { ent_id } } ServerCmdCode::Sound => { let flags_bits = reader.read_u8()?; let flags = match SoundFlags::from_bits(flags_bits) { Some(f) => f, None => { return Err(NetError::InvalidData(format!( "SoundFlags: {:b}", flags_bits ))) } }; let volume = match flags.contains(SoundFlags::VOLUME) { true => Some(reader.read_u8()?), false => None, }; let attenuation = match flags.contains(SoundFlags::ATTENUATION) { true => Some(reader.read_u8()? as f32 * SOUND_ATTENUATION_READ_FACTOR), false => None, }; let entity_channel = reader.read_i16::()?; let entity_id = (entity_channel >> 3) as u16; let channel = (entity_channel & 0b111) as i8; let sound_id = reader.read_u8()?; let position = Vector3::new( read_coord(reader)?, read_coord(reader)?, read_coord(reader)?, ); ServerCmd::Sound { volume, attenuation, entity_id, channel, sound_id, position, } } ServerCmdCode::Time => { let time = reader.read_f32::()?; ServerCmd::Time { time } } ServerCmdCode::Print => { let text = match util::read_cstring(reader) { Ok(t) => t, Err(e) => return Err(NetError::with_msg(format!("{}", e))), }; ServerCmd::Print { text } } ServerCmdCode::StuffText => { let text = match util::read_cstring(reader) { Ok(t) => t, Err(e) => return Err(NetError::with_msg(format!("{}", e))), }; ServerCmd::StuffText { text } } ServerCmdCode::SetAngle => { let angles = Vector3::new( read_angle(reader)?, read_angle(reader)?, read_angle(reader)?, ); ServerCmd::SetAngle { angles } } ServerCmdCode::ServerInfo => { let protocol_version = reader.read_i32::()?; let max_clients = reader.read_u8()?; let game_type_code = reader.read_u8()?; let game_type = match GameType::from_u8(game_type_code) { Some(g) => g, None => { return Err(NetError::InvalidData(format!( "Invalid game type ({})", game_type_code ))) } }; let message = util::read_cstring(reader).unwrap(); let mut model_precache = Vec::new(); loop { let model_name = util::read_cstring(reader).unwrap(); if model_name.is_empty() { break; } model_precache.push(model_name); } let mut sound_precache = Vec::new(); loop { let sound_name = util::read_cstring(reader).unwrap(); if sound_name.is_empty() { break; } sound_precache.push(sound_name); } ServerCmd::ServerInfo { protocol_version, max_clients, game_type, message, model_precache, sound_precache, } } ServerCmdCode::LightStyle => { let id = reader.read_u8()?; let value = util::read_cstring(reader).unwrap(); ServerCmd::LightStyle { id, value } } ServerCmdCode::UpdateName => { let player_id = reader.read_u8()?; let new_name = util::read_cstring(reader).unwrap(); ServerCmd::UpdateName { player_id, new_name, } } ServerCmdCode::UpdateFrags => { let player_id = reader.read_u8()?; let new_frags = reader.read_i16::()?; ServerCmd::UpdateFrags { player_id, new_frags, } } ServerCmdCode::PlayerData => { let flags_bits = reader.read_u16::()?; let flags = match ClientUpdateFlags::from_bits(flags_bits) { Some(f) => f, None => { return Err(NetError::InvalidData(format!( "client update flags: {:b}", flags_bits ))) } }; let view_height = match flags.contains(ClientUpdateFlags::VIEW_HEIGHT) { true => Some(reader.read_i8()? as f32), false => None, }; let ideal_pitch = match flags.contains(ClientUpdateFlags::IDEAL_PITCH) { true => Some(Deg(reader.read_i8()? as f32)), false => None, }; let punch_pitch = match flags.contains(ClientUpdateFlags::PUNCH_PITCH) { true => Some(Deg(reader.read_i8()? as f32)), false => None, }; let velocity_x = match flags.contains(ClientUpdateFlags::VELOCITY_X) { true => Some(reader.read_i8()? as f32 * VELOCITY_READ_FACTOR), false => None, }; let punch_yaw = match flags.contains(ClientUpdateFlags::PUNCH_YAW) { true => Some(Deg(reader.read_i8()? as f32)), false => None, }; let velocity_y = match flags.contains(ClientUpdateFlags::VELOCITY_Y) { true => Some(reader.read_i8()? as f32 * VELOCITY_READ_FACTOR), false => None, }; let punch_roll = match flags.contains(ClientUpdateFlags::PUNCH_ROLL) { true => Some(Deg(reader.read_i8()? as f32)), false => None, }; let velocity_z = match flags.contains(ClientUpdateFlags::VELOCITY_Z) { true => Some(reader.read_i8()? as f32 * VELOCITY_READ_FACTOR), false => None, }; let items_bits = reader.read_u32::()?; let items = match ItemFlags::from_bits(items_bits) { Some(i) => i, None => { return Err(NetError::InvalidData(format!( "ItemFlags: {:b}", items_bits ))) } }; let on_ground = flags.contains(ClientUpdateFlags::ON_GROUND); let in_water = flags.contains(ClientUpdateFlags::IN_WATER); let weapon_frame = match flags.contains(ClientUpdateFlags::WEAPON_FRAME) { true => Some(reader.read_u8()?), false => None, }; let armor = match flags.contains(ClientUpdateFlags::ARMOR) { true => Some(reader.read_u8()?), false => None, }; let weapon = match flags.contains(ClientUpdateFlags::WEAPON) { true => Some(reader.read_u8()?), false => None, }; let health = reader.read_i16::()?; let ammo = reader.read_u8()?; let ammo_shells = reader.read_u8()?; let ammo_nails = reader.read_u8()?; let ammo_rockets = reader.read_u8()?; let ammo_cells = reader.read_u8()?; let active_weapon = reader.read_u8()?; ServerCmd::PlayerData(PlayerData { view_height, ideal_pitch, punch_pitch, velocity_x, punch_yaw, velocity_y, punch_roll, velocity_z, items, on_ground, in_water, weapon_frame, armor, weapon, health, ammo, ammo_shells, ammo_nails, ammo_rockets, ammo_cells, active_weapon, }) } ServerCmdCode::StopSound => { let entity_channel = reader.read_u16::()?; let entity_id = entity_channel >> 3; let channel = (entity_channel & 0b111) as u8; ServerCmd::StopSound { entity_id, channel } } ServerCmdCode::UpdateColors => { let player_id = reader.read_u8()?; let new_colors_bits = reader.read_u8()?; let new_colors = PlayerColor::from_bits(new_colors_bits); ServerCmd::UpdateColors { player_id, new_colors, } } ServerCmdCode::Particle => { let origin = read_coord_vector3(reader)?; let mut direction = Vector3::zero(); for i in 0..3 { direction[i] = reader.read_i8()? as f32 * PARTICLE_DIRECTION_READ_FACTOR; } let count = reader.read_u8()?; let color = reader.read_u8()?; ServerCmd::Particle { origin, direction, count, color, } } ServerCmdCode::Damage => { let armor = reader.read_u8()?; let blood = reader.read_u8()?; let source = read_coord_vector3(reader)?; ServerCmd::Damage { armor, blood, source, } } ServerCmdCode::SpawnStatic => { let model_id = reader.read_u8()?; let frame_id = reader.read_u8()?; let colormap = reader.read_u8()?; let skin_id = reader.read_u8()?; let mut origin = Vector3::zero(); let mut angles = Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)); for i in 0..3 { origin[i] = read_coord(reader)?; angles[i] = read_angle(reader)?; } ServerCmd::SpawnStatic { model_id, frame_id, colormap, skin_id, origin, angles, } } ServerCmdCode::SpawnBaseline => { let ent_id = reader.read_u16::()?; let model_id = reader.read_u8()?; let frame_id = reader.read_u8()?; let colormap = reader.read_u8()?; let skin_id = reader.read_u8()?; let mut origin = Vector3::zero(); let mut angles = Vector3::new(Deg(0.0), Deg(0.0), Deg(0.0)); for i in 0..3 { origin[i] = read_coord(reader)?; angles[i] = read_angle(reader)?; } ServerCmd::SpawnBaseline { ent_id, model_id, frame_id, colormap, skin_id, origin, angles, } } ServerCmdCode::TempEntity => { let temp_entity = TempEntity::read_temp_entity(reader)?; ServerCmd::TempEntity { temp_entity } } ServerCmdCode::SetPause => { let paused = match reader.read_u8()? { 0 => false, 1 => true, x => return Err(NetError::InvalidData(format!("setpause: {}", x))), }; ServerCmd::SetPause { paused } } ServerCmdCode::SignOnStage => { let stage_num = reader.read_u8()?; let stage = match SignOnStage::from_u8(stage_num) { Some(s) => s, None => { return Err(NetError::InvalidData(format!( "Invalid value for sign-on stage: {}", stage_num ))) } }; ServerCmd::SignOnStage { stage } } ServerCmdCode::CenterPrint => { let text = match util::read_cstring(reader) { Ok(t) => t, Err(e) => return Err(NetError::with_msg(format!("{}", e))), }; ServerCmd::CenterPrint { text } } ServerCmdCode::KilledMonster => ServerCmd::KilledMonster, ServerCmdCode::FoundSecret => ServerCmd::FoundSecret, ServerCmdCode::SpawnStaticSound => { let origin = read_coord_vector3(reader)?; let sound_id = reader.read_u8()?; let volume = reader.read_u8()?; let attenuation = reader.read_u8()?; ServerCmd::SpawnStaticSound { origin, sound_id, volume, attenuation, } } ServerCmdCode::Intermission => ServerCmd::Intermission, ServerCmdCode::Finale => { let text = match util::read_cstring(reader) { Ok(t) => t, Err(e) => return Err(NetError::with_msg(format!("{}", e))), }; ServerCmd::Finale { text } } ServerCmdCode::CdTrack => { let track = reader.read_u8()?; let loop_ = reader.read_u8()?; ServerCmd::CdTrack { track, loop_ } } ServerCmdCode::SellScreen => ServerCmd::SellScreen, ServerCmdCode::Cutscene => { let text = match util::read_cstring(reader) { Ok(t) => t, Err(e) => return Err(NetError::with_msg(format!("{}", e))), }; ServerCmd::Cutscene { text } } }; Ok(Some(cmd)) } pub fn serialize(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_u8(self.code())?; match *self { ServerCmd::Bad | ServerCmd::NoOp | ServerCmd::Disconnect => (), ServerCmd::UpdateStat { stat, value } => { writer.write_u8(stat as u8)?; writer.write_i32::(value)?; } ServerCmd::Version { version } => { writer.write_i32::(version)?; } ServerCmd::SetView { ent_id } => { writer.write_i16::(ent_id)?; } ServerCmd::Sound { volume, attenuation, entity_id, channel, sound_id, position, } => { let mut sound_flags = SoundFlags::empty(); if volume.is_some() { sound_flags |= SoundFlags::VOLUME; } if attenuation.is_some() { sound_flags |= SoundFlags::ATTENUATION; } writer.write_u8(sound_flags.bits())?; if let Some(v) = volume { writer.write_u8(v)?; } if let Some(a) = attenuation { writer.write_u8(a as u8 * SOUND_ATTENUATION_WRITE_FACTOR)?; } // TODO: document this better. The entity and channel fields are combined in Sound commands. let ent_channel = (entity_id as i16) << 3 | channel as i16 & 0b111; writer.write_i16::(ent_channel)?; writer.write_u8(sound_id)?; for component in 0..3 { write_coord(writer, position[component])?; } } ServerCmd::Time { time } => writer.write_f32::(time)?, ServerCmd::Print { ref text } => { writer.write(text.as_bytes())?; writer.write_u8(0)?; } ServerCmd::StuffText { ref text } => { writer.write(text.as_bytes())?; writer.write_u8(0)?; } ServerCmd::SetAngle { angles } => write_angle_vector3(writer, angles)?, ServerCmd::ServerInfo { protocol_version, max_clients, game_type, ref message, ref model_precache, ref sound_precache, } => { writer.write_i32::(protocol_version)?; writer.write_u8(max_clients)?; writer.write_u8(game_type as u8)?; writer.write(message.as_bytes())?; writer.write_u8(0)?; for model_name in model_precache.iter() { writer.write(model_name.as_bytes())?; writer.write_u8(0)?; } writer.write_u8(0)?; for sound_name in sound_precache.iter() { writer.write(sound_name.as_bytes())?; writer.write_u8(0)?; } writer.write_u8(0)?; } ServerCmd::LightStyle { id, ref value } => { writer.write_u8(id)?; writer.write(value.as_bytes())?; writer.write_u8(0)?; } ServerCmd::UpdateName { player_id, ref new_name, } => { writer.write_u8(player_id)?; writer.write(new_name.as_bytes())?; writer.write_u8(0)?; } ServerCmd::UpdateFrags { player_id, new_frags, } => { writer.write_u8(player_id)?; writer.write_i16::(new_frags)?; } ServerCmd::PlayerData(PlayerData { view_height, ideal_pitch, punch_pitch, velocity_x, punch_yaw, velocity_y, punch_roll, velocity_z, items, on_ground, in_water, weapon_frame, armor, weapon, health, ammo, ammo_shells, ammo_nails, ammo_rockets, ammo_cells, active_weapon, }) => { let mut flags = ClientUpdateFlags::empty(); if view_height.is_some() { flags |= ClientUpdateFlags::VIEW_HEIGHT; } if ideal_pitch.is_some() { flags |= ClientUpdateFlags::IDEAL_PITCH; } if punch_pitch.is_some() { flags |= ClientUpdateFlags::PUNCH_PITCH; } if velocity_x.is_some() { flags |= ClientUpdateFlags::VELOCITY_X; } if punch_yaw.is_some() { flags |= ClientUpdateFlags::PUNCH_YAW; } if velocity_y.is_some() { flags |= ClientUpdateFlags::VELOCITY_Y; } if punch_roll.is_some() { flags |= ClientUpdateFlags::PUNCH_ROLL; } if velocity_z.is_some() { flags |= ClientUpdateFlags::VELOCITY_Z; } // items are always sent flags |= ClientUpdateFlags::ITEMS; if on_ground { flags |= ClientUpdateFlags::ON_GROUND; } if in_water { flags |= ClientUpdateFlags::IN_WATER; } if weapon_frame.is_some() { flags |= ClientUpdateFlags::WEAPON_FRAME; } if armor.is_some() { flags |= ClientUpdateFlags::ARMOR; } if weapon.is_some() { flags |= ClientUpdateFlags::WEAPON; } // write flags writer.write_u16::(flags.bits())?; if let Some(vh) = view_height { writer.write_u8(vh as i32 as u8)?; } if let Some(ip) = ideal_pitch { writer.write_u8(ip.0 as i32 as u8)?; } if let Some(pp) = punch_pitch { writer.write_u8(pp.0 as i32 as u8)?; } if let Some(vx) = velocity_x { writer.write_u8((vx * VELOCITY_WRITE_FACTOR) as i32 as u8)?; } if let Some(py) = punch_yaw { writer.write_u8(py.0 as i32 as u8)?; } if let Some(vy) = velocity_y { writer.write_u8((vy * VELOCITY_WRITE_FACTOR) as i32 as u8)?; } if let Some(pr) = punch_roll { writer.write_u8(pr.0 as i32 as u8)?; } if let Some(vz) = velocity_z { writer.write_u8((vz * VELOCITY_WRITE_FACTOR) as i32 as u8)?; } writer.write_u32::(items.bits())?; if let Some(wf) = weapon_frame { writer.write_u8(wf)?; } if let Some(a) = armor { writer.write_u8(a)?; } if let Some(w) = weapon { writer.write_u8(w)?; } writer.write_i16::(health)?; writer.write_u8(ammo)?; writer.write_u8(ammo_shells)?; writer.write_u8(ammo_nails)?; writer.write_u8(ammo_rockets)?; writer.write_u8(ammo_cells)?; writer.write_u8(active_weapon)?; } ServerCmd::StopSound { entity_id, channel } => { let entity_channel = entity_id << 3 | channel as u16 & 0b111; writer.write_u16::(entity_channel)?; } ServerCmd::UpdateColors { player_id, new_colors, } => { writer.write_u8(player_id)?; writer.write_u8(new_colors.bits())?; } ServerCmd::Particle { origin, direction, count, color, } => { write_coord_vector3(writer, origin)?; for i in 0..3 { writer.write_i8(match direction[i] * PARTICLE_DIRECTION_WRITE_FACTOR { d if d > ::std::i8::MAX as f32 => ::std::i8::MAX, d if d < ::std::i8::MIN as f32 => ::std::i8::MIN, d => d as i8, })?; } writer.write_u8(count)?; writer.write_u8(color)?; } ServerCmd::Damage { armor, blood, source, } => { writer.write_u8(armor)?; writer.write_u8(blood)?; write_coord_vector3(writer, source)?; } ServerCmd::SpawnStatic { model_id, frame_id, colormap, skin_id, origin, angles, } => { writer.write_u8(model_id)?; writer.write_u8(frame_id)?; writer.write_u8(colormap)?; writer.write_u8(skin_id)?; for i in 0..3 { write_coord(writer, origin[i])?; write_angle(writer, angles[i])?; } } ServerCmd::SpawnBaseline { ent_id, model_id, frame_id, colormap, skin_id, origin, angles, } => { writer.write_u16::(ent_id)?; writer.write_u8(model_id)?; writer.write_u8(frame_id)?; writer.write_u8(colormap)?; writer.write_u8(skin_id)?; for i in 0..3 { write_coord(writer, origin[i])?; write_angle(writer, angles[i])?; } } ServerCmd::TempEntity { ref temp_entity } => { temp_entity.write_temp_entity(writer)?; } ServerCmd::SetPause { paused } => { writer.write_u8(match paused { false => 0, true => 1, })?; } ServerCmd::SignOnStage { stage } => { writer.write_u8(stage as u8)?; } ServerCmd::CenterPrint { ref text } => { writer.write(text.as_bytes())?; writer.write_u8(0)?; } ServerCmd::KilledMonster | ServerCmd::FoundSecret => (), ServerCmd::SpawnStaticSound { origin, sound_id, volume, attenuation, } => { write_coord_vector3(writer, origin)?; writer.write_u8(sound_id)?; writer.write_u8(volume)?; writer.write_u8(attenuation)?; } ServerCmd::Intermission => (), ServerCmd::Finale { ref text } => { writer.write(text.as_bytes())?; writer.write_u8(0)?; } ServerCmd::CdTrack { track, loop_ } => { writer.write_u8(track)?; writer.write_u8(loop_)?; } ServerCmd::SellScreen => (), ServerCmd::Cutscene { ref text } => { writer.write(text.as_bytes())?; writer.write_u8(0)?; } // TODO ServerCmd::FastUpdate(_) => unimplemented!(), } Ok(()) } } #[derive(FromPrimitive)] pub enum ClientCmdCode { Bad = 0, NoOp = 1, Disconnect = 2, Move = 3, StringCmd = 4, } #[derive(Debug, PartialEq)] pub enum ClientCmd { Bad, NoOp, Disconnect, Move { send_time: Duration, angles: Vector3>, fwd_move: i16, side_move: i16, up_move: i16, button_flags: ButtonFlags, impulse: u8, }, StringCmd { cmd: String, }, } impl ClientCmd { pub fn code(&self) -> u8 { match *self { ClientCmd::Bad => ClientCmdCode::Bad as u8, ClientCmd::NoOp => ClientCmdCode::NoOp as u8, ClientCmd::Disconnect => ClientCmdCode::Disconnect as u8, ClientCmd::Move { .. } => ClientCmdCode::Move as u8, ClientCmd::StringCmd { .. } => ClientCmdCode::StringCmd as u8, } } pub fn deserialize(reader: &mut R) -> Result where R: ReadBytesExt + BufRead, { let code_val = reader.read_u8()?; let code = match ClientCmdCode::from_u8(code_val) { Some(c) => c, None => { return Err(NetError::InvalidData(format!( "Invalid client command code: {}", code_val ))) } }; let cmd = match code { ClientCmdCode::Bad => ClientCmd::Bad, ClientCmdCode::NoOp => ClientCmd::NoOp, ClientCmdCode::Disconnect => ClientCmd::Disconnect, ClientCmdCode::Move => { let send_time = engine::duration_from_f32(reader.read_f32::()?); let angles = Vector3::new( read_angle(reader)?, read_angle(reader)?, read_angle(reader)?, ); let fwd_move = reader.read_i16::()?; let side_move = reader.read_i16::()?; let up_move = reader.read_i16::()?; let button_flags_val = reader.read_u8()?; let button_flags = match ButtonFlags::from_bits(button_flags_val) { Some(bf) => bf, None => { return Err(NetError::InvalidData(format!( "Invalid value for button flags: {}", button_flags_val ))) } }; let impulse = reader.read_u8()?; ClientCmd::Move { send_time, angles, fwd_move, side_move, up_move, button_flags, impulse, } } ClientCmdCode::StringCmd => { let cmd = util::read_cstring(reader).unwrap(); ClientCmd::StringCmd { cmd } } }; Ok(cmd) } pub fn serialize(&self, writer: &mut W) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_u8(self.code())?; match *self { ClientCmd::Bad => (), ClientCmd::NoOp => (), ClientCmd::Disconnect => (), ClientCmd::Move { send_time, angles, fwd_move, side_move, up_move, button_flags, impulse, } => { writer.write_f32::(engine::duration_to_f32(send_time))?; write_angle_vector3(writer, angles)?; writer.write_i16::(fwd_move)?; writer.write_i16::(side_move)?; writer.write_i16::(up_move)?; writer.write_u8(button_flags.bits())?; writer.write_u8(impulse)?; } ClientCmd::StringCmd { ref cmd } => { writer.write(cmd.as_bytes())?; writer.write_u8(0)?; } } Ok(()) } } #[derive(PartialEq)] pub enum BlockingMode { Blocking, NonBlocking, Timeout(Duration), } pub struct QSocket { socket: UdpSocket, remote: SocketAddr, unreliable_send_sequence: u32, unreliable_recv_sequence: u32, ack_sequence: u32, send_sequence: u32, send_queue: VecDeque>, send_cache: Box<[u8]>, send_next: bool, send_count: usize, resend_count: usize, recv_sequence: u32, recv_buf: [u8; MAX_MESSAGE], } impl QSocket { pub fn new(socket: UdpSocket, remote: SocketAddr) -> QSocket { QSocket { socket, remote, unreliable_send_sequence: 0, unreliable_recv_sequence: 0, ack_sequence: 0, send_sequence: 0, send_queue: VecDeque::new(), send_cache: Box::new([]), send_count: 0, send_next: false, resend_count: 0, recv_sequence: 0, recv_buf: [0; MAX_MESSAGE], } } pub fn can_send(&self) -> bool { self.send_queue.is_empty() && self.send_cache.is_empty() } /// Begin sending a reliable message over this socket. pub fn begin_send_msg(&mut self, msg: &[u8]) -> Result<(), NetError> { // make sure all reliable messages have been ACKed in their entirety if !self.send_queue.is_empty() { return Err(NetError::with_msg( "begin_send_msg: previous message unacknowledged", )); } // empty messages are an error if msg.len() == 0 { return Err(NetError::with_msg( "begin_send_msg: Input data has zero length", )); } // check upper message length bound if msg.len() > MAX_MESSAGE { return Err(NetError::with_msg( "begin_send_msg: Input data exceeds MAX_MESSAGE", )); } // split the message into chunks and enqueue them for chunk in msg.chunks(MAX_DATAGRAM) { self.send_queue .push_back(chunk.to_owned().into_boxed_slice()); } // send the first chunk self.send_msg_next()?; Ok(()) } /// Resend the last reliable message packet. pub fn resend_msg(&mut self) -> Result<(), NetError> { if self.send_cache.is_empty() { Err(NetError::with_msg("Attempted resend with empty send cache")) } else { self.socket.send_to(&self.send_cache, self.remote)?; self.resend_count += 1; Ok(()) } } /// Send the next segment of a reliable message. pub fn send_msg_next(&mut self) -> Result<(), NetError> { // grab the first chunk in the queue let content = self .send_queue .pop_front() .expect("Send queue is empty (this is a bug)"); // if this was the last chunk, set the EOM flag let msg_kind = match self.send_queue.is_empty() { true => MsgKind::ReliableEom, false => MsgKind::Reliable, }; // compose the packet let mut compose = Vec::with_capacity(MAX_PACKET); compose.write_u16::(msg_kind as u16)?; compose.write_u16::((HEADER_SIZE + content.len()) as u16)?; compose.write_u32::(self.send_sequence)?; compose.write_all(&content)?; // store packet to send cache self.send_cache = compose.into_boxed_slice(); // increment send sequence self.send_sequence += 1; // send the composed packet self.socket.send_to(&self.send_cache, self.remote)?; // TODO: update send time // bump send count self.send_count += 1; // don't send the next chunk until this one gets ACKed self.send_next = false; Ok(()) } pub fn send_msg_unreliable(&mut self, content: &[u8]) -> Result<(), NetError> { if content.len() == 0 { return Err(NetError::with_msg("Unreliable message has zero length")); } if content.len() > MAX_DATAGRAM { return Err(NetError::with_msg( "Unreliable message length exceeds MAX_DATAGRAM", )); } let packet_len = HEADER_SIZE + content.len(); // compose the packet let mut packet = Vec::with_capacity(MAX_PACKET); packet.write_u16::(MsgKind::Unreliable as u16)?; packet.write_u16::(packet_len as u16)?; packet.write_u32::(self.unreliable_send_sequence)?; packet.write_all(content)?; // increment unreliable send sequence self.unreliable_send_sequence += 1; // send the message self.socket.send_to(&packet, self.remote)?; // bump send count self.send_count += 1; Ok(()) } /// Receive a message on this socket. // TODO: the flow control in this function is completely baffling, make it a little less awful pub fn recv_msg(&mut self, block: BlockingMode) -> Result, NetError> { let mut msg = Vec::new(); match block { BlockingMode::Blocking => { self.socket.set_nonblocking(false)?; self.socket.set_read_timeout(None)?; } BlockingMode::NonBlocking => { self.socket.set_nonblocking(true)?; self.socket.set_read_timeout(None)?; } BlockingMode::Timeout(d) => { self.socket.set_nonblocking(false)?; self.socket.set_read_timeout(Some(d.to_std().unwrap()))?; } } loop { let (packet_len, src_addr) = match self.socket.recv_from(&mut self.recv_buf) { Ok(x) => x, Err(e) => { use std::io::ErrorKind; match e.kind() { // these errors are expected in nonblocking mode ErrorKind::WouldBlock | ErrorKind::TimedOut => return Ok(Vec::new()), _ => return Err(NetError::from(e)), } } }; if src_addr != self.remote { // this packet didn't come from remote, drop it debug!( "forged packet (src_addr was {}, should be {})", src_addr, self.remote ); continue; } let mut reader = BufReader::new(Cursor::new(&self.recv_buf[..packet_len])); let msg_kind_code = reader.read_u16::()?; let msg_kind = match MsgKind::from_u16(msg_kind_code) { Some(f) => f, None => { return Err(NetError::InvalidData(format!( "Invalid message kind: {}", msg_kind_code ))) } }; if packet_len < HEADER_SIZE { // TODO: increment short packet count debug!("short packet"); continue; } let field_len = reader.read_u16::()?; if field_len as usize != packet_len { return Err(NetError::InvalidData(format!( "Length field and actual length differ ({} != {})", field_len, packet_len ))); } let sequence; if msg_kind != MsgKind::Ctl { sequence = reader.read_u32::()?; } else { sequence = 0; } match msg_kind { // ignore control messages MsgKind::Ctl => (), MsgKind::Unreliable => { // we've received a newer datagram, ignore if sequence < self.unreliable_recv_sequence { println!("Stale datagram with sequence # {}", sequence); break; } // we've skipped some datagrams, count them as dropped if sequence > self.unreliable_recv_sequence { let drop_count = sequence - self.unreliable_recv_sequence; println!( "Dropped {} packet(s) ({} -> {})", drop_count, sequence, self.unreliable_recv_sequence ); } self.unreliable_recv_sequence = sequence + 1; // copy the rest of the packet into the message buffer and return reader.read_to_end(&mut msg)?; return Ok(msg); } MsgKind::Ack => { if sequence != self.send_sequence - 1 { println!("Stale ACK received"); } else if sequence != self.ack_sequence { println!("Duplicate ACK received"); } else { self.ack_sequence += 1; if self.ack_sequence != self.send_sequence { return Err(NetError::with_msg("ACK sequencing error")); } // our last reliable message has been acked if self.send_queue.is_empty() { // the whole message is through, clear the send cache self.send_cache = Box::new([]); } else { // send the next chunk before returning self.send_next = true; } } } // TODO: once we start reading a reliable message, don't allow other packets until // we have the whole thing MsgKind::Reliable | MsgKind::ReliableEom => { // send ack message and increment self.recv_sequence let mut ack_buf: [u8; HEADER_SIZE] = [0; HEADER_SIZE]; let mut ack_curs = Cursor::new(&mut ack_buf[..]); ack_curs.write_u16::(MsgKind::Ack as u16)?; ack_curs.write_u16::(HEADER_SIZE as u16)?; ack_curs.write_u32::(sequence)?; self.socket.send_to(ack_curs.into_inner(), self.remote)?; // if this was a duplicate, drop it if sequence != self.recv_sequence { println!("Duplicate message received"); continue; } self.recv_sequence += 1; reader.read_to_end(&mut msg)?; // if this is the last chunk of a reliable message, break out and return if msg_kind == MsgKind::ReliableEom { break; } } } } if self.send_next { self.send_msg_next()?; } Ok(msg) } } fn read_coord(reader: &mut R) -> Result where R: BufRead + ReadBytesExt, { Ok(reader.read_i16::()? as f32 / 8.0) } fn read_coord_vector3(reader: &mut R) -> Result, NetError> where R: BufRead + ReadBytesExt, { Ok(Vector3::new( read_coord(reader)?, read_coord(reader)?, read_coord(reader)?, )) } fn write_coord(writer: &mut W, coord: f32) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_i16::((coord * 8.0) as i16)?; Ok(()) } fn write_coord_vector3(writer: &mut W, coords: Vector3) -> Result<(), NetError> where W: WriteBytesExt, { for coord in &coords[..] { write_coord(writer, *coord)?; } Ok(()) } fn read_angle(reader: &mut R) -> Result, NetError> where R: BufRead + ReadBytesExt, { Ok(Deg(reader.read_i8()? as f32 * (360.0 / 256.0))) } fn read_angle_vector3(reader: &mut R) -> Result>, NetError> where R: BufRead + ReadBytesExt, { Ok(Vector3::new( read_angle(reader)?, read_angle(reader)?, read_angle(reader)?, )) } fn write_angle(writer: &mut W, angle: Deg) -> Result<(), NetError> where W: WriteBytesExt, { writer.write_u8(((angle.0 as i32 * 256 / 360) & 0xFF) as u8)?; Ok(()) } fn write_angle_vector3(writer: &mut W, angles: Vector3>) -> Result<(), NetError> where W: WriteBytesExt, { for angle in &angles[..] { write_angle(writer, *angle)?; } Ok(()) } #[cfg(test)] mod test { use super::*; use std::io::BufReader; #[test] fn test_server_cmd_update_stat_read_write_eq() { let src = ServerCmd::UpdateStat { stat: ClientStat::Nails, value: 64, }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_version_read_write_eq() { let src = ServerCmd::Version { version: 42 }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_set_view_read_write_eq() { let src = ServerCmd::SetView { ent_id: 17 }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_time_read_write_eq() { let src = ServerCmd::Time { time: 23.07 }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_print_read_write_eq() { let src = ServerCmd::Print { text: String::from("print test"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_stuff_text_read_write_eq() { let src = ServerCmd::StuffText { text: String::from("stufftext test"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_server_info_read_write_eq() { let src = ServerCmd::ServerInfo { protocol_version: 42, max_clients: 16, game_type: GameType::Deathmatch, message: String::from("Test message"), model_precache: vec![String::from("test1.bsp"), String::from("test2.bsp")], sound_precache: vec![String::from("test1.wav"), String::from("test2.wav")], }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_light_style_read_write_eq() { let src = ServerCmd::LightStyle { id: 11, value: String::from("aaaaabcddeefgghjjjkaaaazzzzyxwaaaba"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_update_name_read_write_eq() { let src = ServerCmd::UpdateName { player_id: 7, new_name: String::from("newname"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_update_frags_read_write_eq() { let src = ServerCmd::UpdateFrags { player_id: 7, new_frags: 11, }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_stop_sound_read_write_eq() { let src = ServerCmd::StopSound { entity_id: 17, channel: 3, }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_update_colors_read_write_eq() { let src = ServerCmd::UpdateColors { player_id: 11, new_colors: PlayerColor::new(4, 13), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_set_pause_read_write_eq() { let src = ServerCmd::SetPause { paused: true }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_sign_on_stage_read_write_eq() { let src = ServerCmd::SignOnStage { stage: SignOnStage::Begin, }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_center_print_read_write_eq() { let src = ServerCmd::CenterPrint { text: String::from("Center print test"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_finale_read_write_eq() { let src = ServerCmd::Finale { text: String::from("Finale test"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_cd_track_read_write_eq() { let src = ServerCmd::CdTrack { track: 5, loop_: 1 }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_server_cmd_cutscene_read_write_eq() { let src = ServerCmd::Cutscene { text: String::from("Cutscene test"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ServerCmd::deserialize(&mut reader).unwrap().unwrap(); assert_eq!(src, dst); } #[test] fn test_client_cmd_string_cmd_read_write_eq() { let src = ClientCmd::StringCmd { cmd: String::from("StringCmd test"), }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ClientCmd::deserialize(&mut reader).unwrap(); assert_eq!(src, dst); } #[test] fn test_client_cmd_move_read_write_eq() { let src = ClientCmd::Move { send_time: Duration::milliseconds(1234), // have to use angles that won't lose precision from write_angle angles: Vector3::new(Deg(90.0), Deg(-90.0), Deg(0.0)), fwd_move: 27, side_move: 85, up_move: 76, button_flags: ButtonFlags::empty(), impulse: 121, }; let mut packet = Vec::new(); src.serialize(&mut packet).unwrap(); let mut reader = BufReader::new(packet.as_slice()); let dst = ClientCmd::deserialize(&mut reader).unwrap(); assert_eq!(src, dst); } fn gen_qsocket_pair() -> (QSocket, QSocket) { let src_udp = UdpSocket::bind("localhost:0").unwrap(); let src_addr = src_udp.local_addr().unwrap(); let dst_udp = UdpSocket::bind("localhost:0").unwrap(); let dst_addr = dst_udp.local_addr().unwrap(); ( QSocket::new(src_udp, dst_addr), QSocket::new(dst_udp, src_addr), ) } #[test] fn test_qsocket_send_msg_short() { let (mut src, mut dst) = gen_qsocket_pair(); let message = String::from("test message").into_bytes(); src.begin_send_msg(&message).unwrap(); let received = dst.recv_msg(BlockingMode::Blocking).unwrap(); assert_eq!(message, received); // TODO: assert can_send == true, send_next == false, etc } #[test] fn test_qsocket_send_msg_unreliable_recv_msg_eq() { let (mut src, mut dst) = gen_qsocket_pair(); let message = String::from("test message").into_bytes(); src.send_msg_unreliable(&message).unwrap(); let received = dst.recv_msg(BlockingMode::Blocking).unwrap(); assert_eq!(message, received); } #[test] #[should_panic] fn test_qsocket_send_msg_unreliable_zero_length_fails() { let (mut src, _) = gen_qsocket_pair(); let message = []; src.send_msg_unreliable(&message).unwrap(); } #[test] #[should_panic] fn test_qsocket_send_msg_unreliable_exceeds_max_length_fails() { let (mut src, _) = gen_qsocket_pair(); let message = [0; MAX_DATAGRAM + 1]; src.send_msg_unreliable(&message).unwrap(); } } ================================================ FILE: src/common/pak.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //! Quake PAK archive manipulation. use std::{ collections::{hash_map::Iter, HashMap}, fs, io::{self, Read, Seek, SeekFrom}, path::Path, }; use byteorder::{LittleEndian, ReadBytesExt}; use thiserror::Error; const PAK_MAGIC: [u8; 4] = [b'P', b'A', b'C', b'K']; const PAK_ENTRY_SIZE: usize = 64; #[derive(Error, Debug)] pub enum PakError { #[error("I/O error: {0}")] Io(#[from] io::Error), #[error("Invalid magic number: {0:?}")] InvalidMagicNumber([u8; 4]), #[error("Invalid file table offset: {0}")] InvalidTableOffset(i32), #[error("Invalid file table size: {0}")] InvalidTableSize(i32), #[error("Invalid file offset: {0}")] InvalidFileOffset(i32), #[error("Invalid file size: {0}")] InvalidFileSize(i32), #[error("File name too long: {0}")] FileNameTooLong(String), #[error("Non-UTF-8 file name: {0}")] NonUtf8FileName(#[from] std::string::FromUtf8Error), #[error("No such file in PAK archive: {0}")] NoSuchFile(String), } /// An open Pak archive. #[derive(Debug)] pub struct Pak(HashMap>); impl Pak { // TODO: rename to from_path or similar pub fn new

(path: P) -> Result where P: AsRef, { debug!("Opening {}", path.as_ref().to_str().unwrap()); let mut infile = fs::File::open(path)?; let mut magic = [0u8; 4]; infile.read(&mut magic)?; if magic != PAK_MAGIC { Err(PakError::InvalidMagicNumber(magic))?; } // Locate the file table let table_offset = match infile.read_i32::()? { o if o <= 0 => Err(PakError::InvalidTableOffset(o))?, o => o as u32, }; let table_size = match infile.read_i32::()? { s if s <= 0 || s as usize % PAK_ENTRY_SIZE != 0 => Err(PakError::InvalidTableSize(s))?, s => s as u32, }; let mut map = HashMap::new(); for i in 0..(table_size as usize / PAK_ENTRY_SIZE) { let entry_offset = table_offset as u64 + (i * PAK_ENTRY_SIZE) as u64; infile.seek(SeekFrom::Start(entry_offset))?; let mut path_bytes = [0u8; 56]; infile.read(&mut path_bytes)?; let file_offset = match infile.read_i32::()? { o if o <= 0 => Err(PakError::InvalidFileOffset(o))?, o => o as u32, }; let file_size = match infile.read_i32::()? { s if s <= 0 => Err(PakError::InvalidFileSize(s))?, s => s as u32, }; let last = path_bytes .iter() .position(|b| *b == 0) .ok_or(PakError::FileNameTooLong( String::from_utf8_lossy(&path_bytes).into_owned(), ))?; let path = String::from_utf8(path_bytes[0..last].to_vec())?; infile.seek(SeekFrom::Start(file_offset as u64))?; let mut data: Vec = Vec::with_capacity(file_size as usize); (&mut infile) .take(file_size as u64) .read_to_end(&mut data)?; map.insert(path, data.into_boxed_slice()); } Ok(Pak(map)) } /// Opens a file in the file tree for reading. /// /// # Examples /// ```no_run /// # extern crate richter; /// use richter::common::pak::Pak; /// /// # fn main() { /// let mut pak = Pak::new("pak0.pak").unwrap(); /// let progs_dat = pak.open("progs.dat").unwrap(); /// # } /// ``` pub fn open(&self, path: S) -> Result<&[u8], PakError> where S: AsRef, { let path = path.as_ref(); self.0 .get(path) .map(|s| s.as_ref()) .ok_or(PakError::NoSuchFile(path.to_owned())) } pub fn iter<'a>(&self) -> Iter> { self.0.iter() } } ================================================ FILE: src/common/parse/console.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use crate::common::parse::quoted; use nom::{ branch::alt, bytes::complete::tag, character::complete::{line_ending, not_line_ending, one_of, space0}, combinator::{opt, recognize}, multi::{many0, many1}, sequence::{delimited, preceded, terminated, tuple}, }; /// Match a line comment. /// /// A line comment is considered to be composed of: /// - Two forward slashes (`"//"`) /// - Zero or more characters, excluding line endings (`"\n"` or `"\r\n"`) pub fn line_comment(input: &str) -> nom::IResult<&str, &str> { recognize(preceded(tag("//"), not_line_ending))(input) } /// Match an empty line. /// /// An empty line is considered to be composed of: /// - Zero or more spaces or tabs /// - An optional line comment /// - A line ending (`"\n"` or `"\r\n"`) pub fn empty_line(input: &str) -> nom::IResult<&str, &str> { recognize(tuple((space0, opt(line_comment), line_ending)))(input) } /// Match a basic argument terminator. /// /// Basic (unquoted) arguments can be terminated by any of: /// - A non-newline whitespace character (`" "` or `"\t"`) /// - The beginning of a line comment (`"//"`) /// - A line ending (`"\r\n"` or `"\n"`) /// - A semicolon (`";"`) pub fn basic_arg_terminator(input: &str) -> nom::IResult<&str, &str> { alt((recognize(one_of(" \t;")), line_ending, tag("//")))(input) } /// Match a sequence of any non-whitespace, non-line-ending ASCII characters, /// ending with whitespace, a line comment or a line terminator. pub fn basic_arg(input: &str) -> nom::IResult<&str, &str> { // break on comment, semicolon, quote, or whitespace let patterns = ["//", ";", "\"", " ", "\t", "\r\n", "\n"]; // length in bytes of matched sequence let mut match_len = 0; // consume characters not matching any of the patterns loop { let remaining = input.split_at(match_len).1; let terminator = patterns.iter().fold(false, |found_match: bool, p| { found_match || remaining.starts_with(*p) }); let chr = match remaining.chars().nth(0) { Some(c) => c, None => break, }; if terminator || !chr.is_ascii() || chr.is_ascii_control() { break; } match_len += chr.len_utf8(); } match match_len { // TODO: more descriptive error? 0 => Err(nom::Err::Error((input, nom::error::ErrorKind::Many1))), len => { let (matched, rest) = input.split_at(len); Ok((rest, matched)) } } } /// Match a basic argument or a quoted string. pub fn arg(input: &str) -> nom::IResult<&str, &str> { alt((quoted, basic_arg))(input) } /// Match a command terminator. /// /// Commands can be terminated by either: /// - A semicolon (`";"`), or /// - An empty line (see `empty_line`) pub fn command_terminator(input: &str) -> nom::IResult<&str, &str> { alt((empty_line, tag(";")))(input) } /// Match a single command. /// /// A command is considered to be composed of: /// - Zero or more leading non-newline whitespace characters /// - One or more arguments, separated by non-newline whitespace characters /// - A command terminator (see `command_terminator`) pub fn command(input: &str) -> nom::IResult<&str, Vec<&str>> { terminated(many1(preceded(space0, arg)), command_terminator)(input) } pub fn commands(input: &str) -> nom::IResult<&str, Vec>> { delimited( many0(empty_line), many0(terminated(command, many0(empty_line))), many0(empty_line), )(input) } #[cfg(test)] mod test { use super::*; #[test] fn test_line_comment() { let result = line_comment("// a comment\nnext line"); assert_eq!(result, Ok(("\nnext line", "// a comment"))); } #[test] fn test_empty_line() { let result = empty_line(" \t \t // a comment\nnext line"); assert_eq!(result, Ok(("next line", " \t \t // a comment\n"))); } #[test] fn test_basic_arg_space_terminated() { let result = basic_arg("space_terminated "); assert_eq!(result, Ok((" ", "space_terminated"))); } #[test] fn test_basic_arg_newline_terminated() { let result = basic_arg("newline_terminated\n"); assert_eq!(result, Ok(("\n", "newline_terminated"))); } #[test] fn test_basic_arg_semicolon_terminated() { let result = basic_arg("semicolon_terminated;"); assert_eq!(result, Ok((";", "semicolon_terminated"))); } #[test] fn test_arg_basic() { let result = arg("basic_arg \t;"); assert_eq!(result, Ok((" \t;", "basic_arg"))); } #[test] fn test_quoted_arg() { let result = arg("\"quoted argument\";\n"); assert_eq!(result, Ok((";\n", "quoted argument"))); } #[test] fn test_command_basic() { let result = command("arg_0 arg_1;\n"); assert_eq!(result, Ok(("\n", vec!["arg_0", "arg_1"]))); } #[test] fn test_command_quoted() { let result = command("bind \"space\" \"+jump\";\n"); assert_eq!(result, Ok(("\n", vec!["bind", "space", "+jump"]))); } #[test] fn test_command_comment() { let result = command("bind \"space\" \"+jump\" // bind space to jump\n\n"); assert_eq!(result, Ok(("\n", vec!["bind", "space", "+jump"]))); } #[test] fn test_commands_quake_rc() { let script = " // load the base configuration exec default.cfg // load the last saved configuration exec config.cfg // run a user script file if present exec autoexec.cfg // // stuff command line statements // stuffcmds // start demos if not already running a server startdemos demo1 demo2 demo3 "; let expected = vec![ vec!["exec", "default.cfg"], vec!["exec", "config.cfg"], vec!["exec", "autoexec.cfg"], vec!["stuffcmds"], vec!["startdemos", "demo1", "demo2", "demo3"], ]; let result = commands(script); assert_eq!(result, Ok(("", expected))); } } ================================================ FILE: src/common/parse/map.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::collections::HashMap; use crate::common::parse::quoted; use nom::{ bytes::complete::tag, character::complete::newline, combinator::{all_consuming, map}, multi::many0, sequence::{delimited, separated_pair, terminated}, }; // "name" "value"\n pub fn entity_attribute(input: &str) -> nom::IResult<&str, (&str, &str)> { terminated(separated_pair(quoted, tag(" "), quoted), newline)(input) } // { // "name1" "value1" // "name2" "value2" // "name3" "value3" // } pub fn entity(input: &str) -> nom::IResult<&str, HashMap<&str, &str>> { delimited( terminated(tag("{"), newline), map(many0(entity_attribute), |attrs| attrs.into_iter().collect()), terminated(tag("}"), newline), )(input) } pub fn entities(input: &str) -> Result>, failure::Error> { let input = input.strip_suffix('\0').unwrap_or(input); match all_consuming(many0(entity))(input) { Ok(("", entities)) => Ok(entities), Ok(_) => unreachable!(), Err(e) => bail!("parse failed: {}", e), } } ================================================ FILE: src/common/parse/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pub mod console; pub mod map; use cgmath::Vector3; use nom::{ branch::alt, bytes::complete::{tag, take_while1}, character::complete::{alphanumeric1, one_of, space1}, combinator::map, sequence::{delimited, tuple}, }; use winit::event::ElementState; pub use self::{console::commands, map::entities}; pub fn non_newline_spaces(input: &str) -> nom::IResult<&str, &str> { space1(input) } fn string_contents(input: &str) -> nom::IResult<&str, &str> { take_while1(|c: char| !"\"".contains(c) && c.is_ascii() && !c.is_ascii_control())(input) } pub fn quoted(input: &str) -> nom::IResult<&str, &str> { delimited(tag("\""), string_contents, tag("\""))(input) } pub fn action(input: &str) -> nom::IResult<&str, (ElementState, &str)> { tuple(( map(one_of("+-"), |c| match c { '+' => ElementState::Pressed, '-' => ElementState::Released, _ => unreachable!(), }), alphanumeric1, ))(input) } pub fn newline(input: &str) -> nom::IResult<&str, &str> { nom::character::complete::line_ending(input) } // TODO: rename to line_terminator and move to console module pub fn line_ending(input: &str) -> nom::IResult<&str, &str> { alt((tag(";"), nom::character::complete::line_ending))(input) } pub fn vector3_components(src: S) -> Option<[f32; 3]> where S: AsRef, { let src = src.as_ref(); let components: Vec<_> = src.split(" ").collect(); if components.len() != 3 { return None; } let x: f32 = match components[0].parse().ok() { Some(p) => p, None => return None, }; let y: f32 = match components[1].parse().ok() { Some(p) => p, None => return None, }; let z: f32 = match components[2].parse().ok() { Some(p) => p, None => return None, }; Some([x, y, z]) } pub fn vector3(src: S) -> Option> where S: AsRef, { let src = src.as_ref(); let components: Vec<_> = src.split(" ").collect(); if components.len() != 3 { return None; } let x: f32 = match components[0].parse().ok() { Some(p) => p, None => return None, }; let y: f32 = match components[1].parse().ok() { Some(p) => p, None => return None, }; let z: f32 = match components[2].parse().ok() { Some(p) => p, None => return None, }; Some(Vector3::new(x, y, z)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_quoted() { let s = "\"hello\""; assert_eq!(quoted(s), Ok(("", "hello"))) } #[test] fn test_action() { let s = "+up"; assert_eq!(action(s), Ok(("", (ElementState::Pressed, "up")))) } } ================================================ FILE: src/common/sprite.rs ================================================ // Copyright © 2018 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::io::{BufReader, Read, Seek}; use crate::common::{engine, model::SyncType}; use byteorder::{LittleEndian, ReadBytesExt}; use cgmath::Vector3; use chrono::Duration; use num::FromPrimitive; const MAGIC: u32 = ('I' as u32) << 0 | ('D' as u32) << 8 | ('S' as u32) << 16 | ('P' as u32) << 24; const VERSION: u32 = 1; #[derive(Clone, Copy, Debug, Eq, FromPrimitive, PartialEq)] pub enum SpriteKind { ViewPlaneParallelUpright = 0, Upright = 1, ViewPlaneParallel = 2, Oriented = 3, ViewPlaneParallelOriented = 4, } #[derive(Debug)] pub struct SpriteModel { kind: SpriteKind, max_width: usize, max_height: usize, radius: f32, frames: Vec, } impl SpriteModel { pub fn min(&self) -> Vector3 { Vector3::new( -(self.max_width as f32) / 2.0, -(self.max_width as f32) / 2.0, -(self.max_height as f32) / 2.0, ) } pub fn max(&self) -> Vector3 { Vector3::new( self.max_width as f32 / 2.0, self.max_width as f32 / 2.0, self.max_height as f32 / 2.0, ) } pub fn radius(&self) -> f32 { self.radius } pub fn kind(&self) -> SpriteKind { self.kind } pub fn frames(&self) -> &[SpriteFrame] { &self.frames } } #[derive(Debug)] pub enum SpriteFrame { Static { frame: SpriteSubframe, }, Animated { subframes: Vec, durations: Vec, }, } #[derive(Debug)] pub struct SpriteSubframe { width: u32, height: u32, up: f32, down: f32, left: f32, right: f32, indexed: Vec, } impl SpriteSubframe { pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } pub fn indexed(&self) -> &[u8] { &self.indexed } } pub fn load(data: R) -> SpriteModel where R: Read + Seek, { let mut reader = BufReader::new(data); let magic = reader.read_u32::().unwrap(); if magic != MAGIC { panic!( "Bad magic number for sprite model (got {}, should be {})", magic, MAGIC ); } let version = reader.read_u32::().unwrap(); if version != VERSION { panic!( "Bad version number for sprite model (got {}, should be {})", version, VERSION ); } // TODO: use an enum for this let kind = SpriteKind::from_i32(reader.read_i32::().unwrap()).unwrap(); let radius = reader.read_f32::().unwrap(); let max_width = match reader.read_i32::().unwrap() { w if w < 0 => panic!("Negative max width ({})", w), w => w as usize, }; let max_height = match reader.read_i32::().unwrap() { h if h < 0 => panic!("Negative max height ({})", h), h => h as usize, }; let frame_count = match reader.read_i32::().unwrap() { c if c < 1 => panic!("Invalid frame count ({}), must be at least 1", c), c => c as usize, }; let _beam_len = match reader.read_i32::().unwrap() { l if l < 0 => panic!("Negative beam length ({})", l), l => l as usize, }; debug!( "max_width = {} max_height = {} frame_count = {}", max_width, max_height, frame_count ); let _sync_type = SyncType::from_i32(reader.read_i32::().unwrap()).unwrap(); let mut frames = Vec::with_capacity(frame_count); for i in 0..frame_count { let frame_kind_int = reader.read_i32::().unwrap(); // TODO: substitute out this magic number if frame_kind_int == 0 { let origin_x = reader.read_i32::().unwrap(); let origin_z = reader.read_i32::().unwrap(); let width = match reader.read_i32::().unwrap() { w if w < 0 => panic!("Negative frame width ({})", w), w => w, }; let height = match reader.read_i32::().unwrap() { h if h < 0 => panic!("Negative frame height ({})", h), h => h, }; debug!("Frame {}: width = {} height = {}", i, width, height); let index_count = (width * height) as usize; let mut indices = Vec::with_capacity(index_count); for _ in 0..index_count as usize { indices.push(reader.read_u8().unwrap()); } frames.push(SpriteFrame::Static { frame: SpriteSubframe { width: width as u32, height: height as u32, up: origin_z as f32, down: (origin_z - height) as f32, left: origin_x as f32, right: (width + origin_x) as f32, indexed: indices, }, }); } else { let subframe_count = match reader.read_i32::().unwrap() { c if c < 0 => panic!("Negative subframe count ({}) in frame {}", c, i), c => c as usize, }; let mut durations = Vec::with_capacity(subframe_count); for _ in 0..subframe_count { durations.push(engine::duration_from_f32( reader.read_f32::().unwrap(), )); } let mut subframes = Vec::with_capacity(subframe_count); for _ in 0..subframe_count { let origin_x = reader.read_i32::().unwrap(); let origin_z = reader.read_i32::().unwrap(); let width = match reader.read_i32::().unwrap() { w if w < 0 => panic!("Negative subframe width ({}) in frame {}", w, i), w => w, }; let height = match reader.read_i32::().unwrap() { h if h < 0 => panic!("Negative subframe height ({}) in frame {}", h, i), h => h, }; let index_count = (width * height) as usize; let mut indices = Vec::with_capacity(index_count); for _ in 0..index_count as usize { indices.push(reader.read_u8().unwrap()); } subframes.push(SpriteSubframe { width: width as u32, height: height as u32, up: origin_z as f32, down: (origin_z - height) as f32, left: origin_x as f32, right: (width + origin_x) as f32, indexed: indices, }); } frames.push(SpriteFrame::Animated { durations, subframes, }); } } SpriteModel { kind, max_width, max_height, radius, frames, } } ================================================ FILE: src/common/util.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::mem::size_of; use byteorder::{LittleEndian, ReadBytesExt}; /// A plain-old-data type. pub trait Pod: 'static + Copy + Sized + Send + Sync {} impl Pod for T {} /// Read a `[f32; 3]` in little-endian byte order. pub fn read_f32_3(reader: &mut R) -> Result<[f32; 3], std::io::Error> where R: ReadBytesExt, { let mut ar = [0.0f32; 3]; reader.read_f32_into::(&mut ar)?; Ok(ar) } /// Read a null-terminated sequence of bytes and convert it into a `String`. /// /// The zero byte is consumed. /// /// ## Panics /// - If the end of the input is reached before a zero byte is found. pub fn read_cstring(src: &mut R) -> Result where R: std::io::BufRead, { let mut bytes: Vec = Vec::new(); src.read_until(0, &mut bytes).unwrap(); bytes.pop(); String::from_utf8(bytes) } pub unsafe fn any_as_bytes(t: &T) -> &[u8] where T: Pod, { std::slice::from_raw_parts((t as *const T) as *const u8, size_of::()) } pub unsafe fn any_slice_as_bytes(t: &[T]) -> &[u8] where T: Pod, { std::slice::from_raw_parts(t.as_ptr() as *const u8, size_of::() * t.len()) } pub unsafe fn bytes_as_any(bytes: &[u8]) -> T where T: Pod, { assert_eq!(bytes.len(), size_of::()); std::ptr::read_unaligned(bytes.as_ptr() as *const T) } pub unsafe fn any_as_u32_slice(t: &T) -> &[u32] where T: Pod, { assert!(size_of::() % size_of::() == 0); std::slice::from_raw_parts( (t as *const T) as *const u32, size_of::() / size_of::(), ) } ================================================ FILE: src/common/vfs.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{ fs::File, io::{self, BufReader, Cursor, Read, Seek, SeekFrom}, path::{Path, PathBuf}, }; use crate::common::pak::{Pak, PakError}; use thiserror::Error; #[derive(Error, Debug)] pub enum VfsError { #[error("Couldn't load pakfile: {0}")] Pak(#[from] PakError), #[error("File does not exist: {0}")] NoSuchFile(String), } #[derive(Debug)] enum VfsComponent { Pak(Pak), Directory(PathBuf), } #[derive(Debug)] pub struct Vfs { components: Vec, } impl Vfs { pub fn new() -> Vfs { Vfs { components: Vec::new(), } } /// Initializes the virtual filesystem using a base directory. pub fn with_base_dir(base_dir: PathBuf) -> Vfs { let mut vfs = Vfs::new(); let mut game_dir = base_dir; game_dir.push("id1"); if !game_dir.is_dir() { log::error!(concat!( "`id1/` directory does not exist! Use the `--base-dir` option with the name of the", " directory which contains `id1/`." )); std::process::exit(1); } vfs.add_directory(&game_dir).unwrap(); // ...then add PAK archives. let mut num_paks = 0; let mut pak_path = game_dir; for vfs_id in 0..crate::common::MAX_PAKFILES { // Add the file name. pak_path.push(format!("pak{}.pak", vfs_id)); // Keep adding PAKs until we don't find one or we hit MAX_PAKFILES. if !pak_path.exists() { // If the lowercase path doesn't exist, try again with uppercase. pak_path.pop(); pak_path.push(format!("PAK{}.PAK", vfs_id)); if !pak_path.exists() { break; } } vfs.add_pakfile(&pak_path).unwrap(); num_paks += 1; // Remove the file name, leaving the game directory. pak_path.pop(); } if num_paks == 0 { log::warn!("No PAK files found."); } vfs } pub fn add_pakfile

(&mut self, path: P) -> Result<(), VfsError> where P: AsRef, { let path = path.as_ref(); self.components.push(VfsComponent::Pak(Pak::new(path)?)); Ok(()) } pub fn add_directory

(&mut self, path: P) -> Result<(), VfsError> where P: AsRef, { self.components .push(VfsComponent::Directory(path.as_ref().to_path_buf())); Ok(()) } pub fn open(&self, virtual_path: S) -> Result where S: AsRef, { let vp = virtual_path.as_ref(); // iterate in reverse so later PAKs overwrite earlier ones for c in self.components.iter().rev() { match c { VfsComponent::Pak(pak) => { if let Ok(f) = pak.open(vp) { return Ok(VirtualFile::PakBacked(Cursor::new(f))); } } VfsComponent::Directory(path) => { let mut full_path = path.to_owned(); full_path.push(vp); if let Ok(f) = File::open(full_path) { return Ok(VirtualFile::FileBacked(BufReader::new(f))); } } } } Err(VfsError::NoSuchFile(vp.to_owned())) } } pub enum VirtualFile<'a> { PakBacked(Cursor<&'a [u8]>), FileBacked(BufReader), } impl<'a> Read for VirtualFile<'a> { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { VirtualFile::PakBacked(curs) => curs.read(buf), VirtualFile::FileBacked(file) => file.read(buf), } } } impl<'a> Seek for VirtualFile<'a> { fn seek(&mut self, pos: SeekFrom) -> io::Result { match self { VirtualFile::PakBacked(curs) => curs.seek(pos), VirtualFile::FileBacked(file) => file.seek(pos), } } } ================================================ FILE: src/common/wad.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. use std::{ collections::HashMap, convert::From, fmt::{self, Display}, io::{self, BufReader, Cursor, Read, Seek, SeekFrom}, }; use crate::common::util; use byteorder::{LittleEndian, ReadBytesExt}; use failure::{Backtrace, Context, Error, Fail}; // see definition of lumpinfo_t: // https://github.com/id-Software/Quake/blob/master/WinQuake/wad.h#L54-L63 const LUMPINFO_SIZE: usize = 32; const MAGIC: u32 = 'W' as u32 | ('A' as u32) << 8 | ('D' as u32) << 16 | ('2' as u32) << 24; #[derive(Debug)] pub struct WadError { inner: Context, } impl WadError { pub fn kind(&self) -> WadErrorKind { *self.inner.get_context() } } impl From for WadError { fn from(kind: WadErrorKind) -> Self { WadError { inner: Context::new(kind), } } } impl From> for WadError { fn from(inner: Context) -> Self { WadError { inner } } } impl From for WadError { fn from(io_error: io::Error) -> Self { let kind = io_error.kind(); match kind { io::ErrorKind::UnexpectedEof => io_error.context(WadErrorKind::UnexpectedEof).into(), _ => io_error.context(WadErrorKind::Io).into(), } } } impl Fail for WadError { fn cause(&self) -> Option<&dyn Fail> { self.inner.cause() } fn backtrace(&self) -> Option<&Backtrace> { self.inner.backtrace() } } impl Display for WadError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Display::fmt(&self.inner, f) } } #[derive(Clone, Copy, Eq, PartialEq, Debug, Fail)] pub enum WadErrorKind { #[fail(display = "CONCHARS must be loaded with the dedicated function")] ConcharsUseDedicatedFunction, #[fail(display = "Invalid magic number")] InvalidMagicNumber, #[fail(display = "I/O error")] Io, #[fail(display = "No such file in WAD")] NoSuchFile, #[fail(display = "Failed to load QPic")] QPicNotLoaded, #[fail(display = "Unexpected end of data")] UnexpectedEof, } pub struct QPic { width: u32, height: u32, indices: Box<[u8]>, } impl QPic { pub fn load(data: R) -> Result where R: Read + Seek, { let mut reader = BufReader::new(data); let width = reader.read_u32::()?; let height = reader.read_u32::()?; let mut indices = Vec::new(); (&mut reader) .take((width * height) as u64) .read_to_end(&mut indices)?; Ok(QPic { width, height, indices: indices.into_boxed_slice(), }) } pub fn width(&self) -> u32 { self.width } pub fn height(&self) -> u32 { self.height } pub fn indices(&self) -> &[u8] { &self.indices } } struct LumpInfo { offset: u32, size: u32, name: String, } pub struct Wad { files: HashMap>, } impl Wad { pub fn load(data: R) -> Result where R: Read + Seek, { let mut reader = BufReader::new(data); let magic = reader.read_u32::()?; if magic != MAGIC { return Err(WadErrorKind::InvalidMagicNumber.into()); } let lump_count = reader.read_u32::()?; let lumpinfo_ofs = reader.read_u32::()?; reader.seek(SeekFrom::Start(lumpinfo_ofs as u64))?; let mut lump_infos = Vec::new(); for _ in 0..lump_count { // TODO sanity check these values let offset = reader.read_u32::()?; let _size_on_disk = reader.read_u32::()?; let size = reader.read_u32::()?; let _type = reader.read_u8()?; let _compression = reader.read_u8()?; let _pad = reader.read_u16::()?; let mut name_bytes = [0u8; 16]; reader.read_exact(&mut name_bytes)?; let name_lossy = String::from_utf8_lossy(&name_bytes); debug!("name: {}", name_lossy); let name = util::read_cstring(&mut BufReader::new(Cursor::new(name_bytes)))?; lump_infos.push(LumpInfo { offset, size, name }); } let mut files = HashMap::new(); for lump_info in lump_infos { let mut data = Vec::with_capacity(lump_info.size as usize); reader.seek(SeekFrom::Start(lump_info.offset as u64))?; (&mut reader) .take(lump_info.size as u64) .read_to_end(&mut data)?; files.insert(lump_info.name.to_owned(), data.into_boxed_slice()); } Ok(Wad { files }) } pub fn open_conchars(&self) -> Result { match self.files.get("CONCHARS") { Some(ref data) => { let width = 128; let height = 128; let indices = Vec::from(&data[..(width * height) as usize]); Ok(QPic { width, height, indices: indices.into_boxed_slice(), }) } None => bail!("conchars not found in WAD"), } } pub fn open_qpic(&self, name: S) -> Result where S: AsRef, { if name.as_ref() == "CONCHARS" { Err(WadErrorKind::ConcharsUseDedicatedFunction)? } match self.files.get(name.as_ref()) { Some(ref data) => QPic::load(Cursor::new(data)), None => Err(WadErrorKind::NoSuchFile.into()), } } } ================================================ FILE: src/lib.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #![deny(unused_must_use)] #![feature(drain_filter)] #[macro_use] extern crate bitflags; extern crate byteorder; extern crate cgmath; extern crate chrono; extern crate env_logger; #[macro_use] extern crate failure; #[macro_use] extern crate lazy_static; #[macro_use] extern crate log; extern crate num; #[macro_use] extern crate num_derive; extern crate rand; extern crate regex; extern crate rodio; extern crate winit; pub mod client; pub mod common; pub mod server; ================================================ FILE: src/server/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pub mod precache; pub mod progs; pub mod world; use std::{ cell::{Ref, RefCell}, collections::HashMap, rc::Rc, }; use crate::{ common::{ console::CvarRegistry, engine::{duration_from_f32, duration_to_f32}, math::Hyperplane, model::Model, parse, vfs::Vfs, }, server::{ progs::{functions::FunctionKind, GlobalAddrFunction}, world::{FieldAddrEntityId, FieldAddrVector, MoveKind}, }, }; use self::{ precache::Precache, progs::{ globals::{ GLOBAL_ADDR_ARG_0, GLOBAL_ADDR_ARG_1, GLOBAL_ADDR_ARG_2, GLOBAL_ADDR_ARG_3, GLOBAL_ADDR_RETURN, }, EntityFieldAddr, EntityId, ExecutionContext, FunctionId, GlobalAddrEntity, GlobalAddrFloat, Globals, LoadProgs, Opcode, ProgsError, StringId, StringTable, }, world::{ phys::{self, CollideKind, CollisionFlags, Trace, TraceEndKind}, EntityFlags, EntitySolid, FieldAddrFloat, FieldAddrFunctionId, FieldAddrStringId, World, }, }; use arrayvec::ArrayVec; use cgmath::{InnerSpace, Vector3, Zero}; use chrono::Duration; const MAX_DATAGRAM: usize = 1024; const MAX_LIGHTSTYLES: usize = 64; /// The state of a client's connection to the server. pub enum ClientState { /// The client is still connecting. Connecting, /// The client is active. Active(ClientActive), } pub struct ClientActive { /// If true, client may execute any command. privileged: bool, /// ID of the entity controlled by this client. entity_id: EntityId, } bitflags! { pub struct SessionFlags: i32 { const EPISODE_1 = 0x0001; const EPISODE_2 = 0x0002; const EPISODE_3 = 0x0004; const EPISODE_4 = 0x0008; const NEW_UNIT = 0x0010; const NEW_EPISODE = 0x0020; const CROSS_TRIGGERS = 0xFF00; } } /// A fixed-size pool of client connections. pub struct ClientSlots { /// Occupied slots are `Some`. slots: Vec>, } impl ClientSlots { /// Creates a new pool which supports at most `limit` clients. pub fn new(limit: usize) -> ClientSlots { let mut slots = Vec::with_capacity(limit); slots.resize_with(limit, || None); ClientSlots { slots } } /// Returns a reference to the client in a slot. /// /// If the slot is unoccupied, or if `id` is greater than `self.limit()`, /// returns `None`. pub fn get(&self, id: usize) -> Option<&ClientState> { self.slots.get(id)?.as_ref() } /// Returns the maximum number of simultaneous clients. pub fn limit(&self) -> usize { self.slots.len() } /// Finds an available connection slot for a new client. pub fn find_available(&mut self) -> Option<&mut ClientState> { let slot = self.slots.iter_mut().find(|s| s.is_none())?; Some(slot.insert(ClientState::Connecting)) } } /// Server state that persists between levels. pub struct SessionPersistent { client_slots: ClientSlots, flags: SessionFlags, } impl SessionPersistent { pub fn new(max_clients: usize) -> SessionPersistent { SessionPersistent { client_slots: ClientSlots::new(max_clients), flags: SessionFlags::empty(), } } pub fn client(&self, slot: usize) -> Option<&ClientState> { self.client_slots.get(slot) } } /// The state of a server. pub enum SessionState { /// The server is loading. /// /// Certain operations, such as precaching, are only permitted while the /// server is loading a level. Loading(SessionLoading), /// The server is active (in-game). Active(SessionActive), } /// Contains the state of the server during level load. pub struct SessionLoading { level: LevelState, } impl SessionLoading { pub fn new( vfs: Rc, cvars: Rc>, progs: LoadProgs, models: Vec, entmap: String, ) -> SessionLoading { SessionLoading { level: LevelState::new(vfs, cvars, progs, models, entmap), } } /// Adds a name to the sound precache. /// /// If the sound already exists in the precache, this has no effect. #[inline] pub fn precache_sound(&mut self, name_id: StringId) { self.level.precache_sound(name_id) } /// Adds a name to the model precache. /// /// If the model already exists in the precache, this has no effect. #[inline] pub fn precache_model(&mut self, name_id: StringId) { self.level.precache_model(name_id) } /// Completes the loading process. /// /// This consumes the `ServerLoading` and returns a `ServerActive`. pub fn finish(self) -> SessionActive { SessionActive { level: self.level } } } /// State specific to an active (in-game) server. pub struct SessionActive { level: LevelState, } /// A server instance. pub struct Session { persist: SessionPersistent, state: SessionState, } impl Session { pub fn new( max_clients: usize, vfs: Rc, cvars: Rc>, progs: LoadProgs, models: Vec, entmap: String, ) -> Session { Session { persist: SessionPersistent::new(max_clients), state: SessionState::Loading(SessionLoading { level: LevelState::new(vfs, cvars, progs, models, entmap), }), } } /// Returns the maximum number of clients allowed on the server. pub fn max_clients(&self) -> usize { self.persist.client_slots.limit() } #[inline] pub fn client(&self, slot: usize) -> Option<&ClientState> { self.persist.client(slot) } pub fn precache_sound(&mut self, name_id: StringId) { if let SessionState::Loading(ref mut loading) = self.state { loading.precache_sound(name_id); } else { panic!("Sounds cannot be precached after loading"); } } pub fn precache_model(&mut self, name_id: StringId) { if let SessionState::Loading(ref mut loading) = self.state { loading.precache_model(name_id); } else { panic!("Models cannot be precached after loading"); } } #[inline] fn level(&self) -> &LevelState { match self.state { SessionState::Loading(ref loading) => &loading.level, SessionState::Active(ref active) => &active.level, } } #[inline] fn level_mut(&mut self) -> &mut LevelState { match self.state { SessionState::Loading(ref mut loading) => &mut loading.level, SessionState::Active(ref mut active) => &mut active.level, } } #[inline] pub fn sound_id(&self, name_id: StringId) -> Option { self.level().sound_id(name_id) } #[inline] pub fn model_id(&self, name_id: StringId) -> Option { self.level().model_id(name_id) } #[inline] pub fn set_lightstyle(&mut self, index: usize, val: StringId) { self.level_mut().set_lightstyle(index, val); } /// Returns the amount of time the current level has been active. #[inline] pub fn time(&self) -> Option { match self.state { SessionState::Loading(_) => None, SessionState::Active(ref active) => Some(active.level.time), } } } /// Server-side level state. #[derive(Debug)] pub struct LevelState { vfs: Rc, cvars: Rc>, string_table: Rc>, sound_precache: Precache, model_precache: Precache, lightstyles: [StringId; MAX_LIGHTSTYLES], /// Amount of time the current level has been active. time: Duration, /// QuakeC bytecode execution context. /// /// This includes the program counter, call stack, and local variables. cx: ExecutionContext, /// Global values for QuakeC bytecode. globals: Globals, /// The state of the game world. /// /// This contains the entities and world geometry. world: World, datagram: ArrayVec, } impl LevelState { pub fn new( vfs: Rc, cvars: Rc>, progs: LoadProgs, models: Vec, entmap: String, ) -> LevelState { let LoadProgs { cx, globals, entity_def, string_table, } = progs; let mut sound_precache = Precache::new(); sound_precache.precache(""); let mut model_precache = Precache::new(); model_precache.precache(""); for model in models.iter() { let model_name = (*string_table).borrow_mut().find_or_insert(model.name()); model_precache.precache(string_table.borrow().get(model_name).unwrap()); } let world = World::create(models, entity_def.clone(), string_table.clone()).unwrap(); let entity_list = parse::entities(&entmap).unwrap(); let mut level = LevelState { vfs, cvars, string_table, sound_precache, model_precache, lightstyles: [StringId(0); MAX_LIGHTSTYLES], time: Duration::zero(), cx, globals, world, datagram: ArrayVec::new(), }; for entity in entity_list { level.spawn_entity_from_map(entity).unwrap(); } level } #[inline] pub fn precache_sound(&mut self, name_id: StringId) { let name = Ref::map(self.string_table.borrow(), |this| { this.get(name_id).unwrap() }); self.sound_precache.precache(&*name); } #[inline] pub fn precache_model(&mut self, name_id: StringId) { let name = Ref::map(self.string_table.borrow(), |this| { this.get(name_id).unwrap() }); self.model_precache.precache(&*name) } #[inline] pub fn sound_id(&self, name_id: StringId) -> Option { let name = Ref::map(self.string_table.borrow(), |this| { this.get(name_id).unwrap() }); self.sound_precache.find(&*name) } #[inline] pub fn model_id(&self, name_id: StringId) -> Option { let name = Ref::map(self.string_table.borrow(), |this| { this.get(name_id).unwrap() }); self.model_precache.find(&*name) } #[inline] pub fn set_lightstyle(&mut self, index: usize, val: StringId) { self.lightstyles[index] = val; } /// Execute a QuakeC function in the VM. pub fn execute_program(&mut self, f: FunctionId) -> Result<(), ProgsError> { let mut runaway = 100000; let exit_depth = self.cx.call_stack_depth(); self.cx.enter_function(&mut self.globals, f)?; while self.cx.call_stack_depth() != exit_depth { runaway -= 1; if runaway == 0 { panic!("runaway program"); } let statement = self.cx.load_statement(); let op = statement.opcode; let a = statement.arg1; let b = statement.arg2; let c = statement.arg3; debug!( " {:<9} {:>5} {:>5} {:>5}", format!("{:?}", op), a, b, c ); use Opcode::*; // Y'all like jump tables? match op { // Control flow ================================================ If => { let cond = self.globals.get_float(a)? != 0.0; log::debug!("If: cond == {}", cond); if cond { self.cx.jump_relative(b); continue; } } IfNot => { let cond = self.globals.get_float(a)? != 0.0; log::debug!("IfNot: cond == {}", cond); if !cond { self.cx.jump_relative(b); continue; } } Goto => { self.cx.jump_relative(a); continue; } Call0 | Call1 | Call2 | Call3 | Call4 | Call5 | Call6 | Call7 | Call8 => { // TODO: pass to equivalent of PF_VarString let _arg_count = op as usize - Opcode::Call0 as usize; let f_to_call = self.globals.function_id(a)?; if f_to_call.0 == 0 { panic!("NULL function"); } let name_id = self.cx.function_def(f_to_call)?.name_id; let name = self.string_table.borrow().get(name_id).unwrap().to_owned(); if let FunctionKind::BuiltIn(b) = self.cx.function_def(f_to_call)?.kind { debug!("Calling built-in function {}", name); use progs::functions::BuiltinFunctionId::*; match b { MakeVectors => self.globals.make_vectors()?, SetOrigin => self.builtin_set_origin()?, SetModel => self.builtin_set_model()?, SetSize => self.builtin_set_size()?, Break => unimplemented!(), Random => self.globals.builtin_random()?, Sound => unimplemented!(), Normalize => unimplemented!(), Error => unimplemented!(), ObjError => unimplemented!(), VLen => self.globals.builtin_v_len()?, VecToYaw => self.globals.builtin_vec_to_yaw()?, Spawn => self.builtin_spawn()?, Remove => self.builtin_remove()?, TraceLine => unimplemented!(), CheckClient => unimplemented!(), Find => unimplemented!(), PrecacheSound => self.builtin_precache_sound()?, PrecacheModel => self.builtin_precache_model()?, StuffCmd => unimplemented!(), FindRadius => unimplemented!(), BPrint => unimplemented!(), SPrint => unimplemented!(), DPrint => self.builtin_dprint()?, FToS => unimplemented!(), VToS => unimplemented!(), CoreDump => unimplemented!(), TraceOn => unimplemented!(), TraceOff => unimplemented!(), EPrint => unimplemented!(), WalkMove => unimplemented!(), DropToFloor => self.builtin_drop_to_floor()?, LightStyle => self.builtin_light_style()?, RInt => self.globals.builtin_r_int()?, Floor => self.globals.builtin_floor()?, Ceil => self.globals.builtin_ceil()?, CheckBottom => unimplemented!(), PointContents => unimplemented!(), FAbs => self.globals.builtin_f_abs()?, Aim => unimplemented!(), Cvar => self.builtin_cvar()?, LocalCmd => unimplemented!(), NextEnt => unimplemented!(), Particle => unimplemented!(), ChangeYaw => unimplemented!(), VecToAngles => unimplemented!(), WriteByte => unimplemented!(), WriteChar => unimplemented!(), WriteShort => unimplemented!(), WriteLong => unimplemented!(), WriteCoord => unimplemented!(), WriteAngle => unimplemented!(), WriteString => unimplemented!(), WriteEntity => unimplemented!(), MoveToGoal => unimplemented!(), PrecacheFile => unimplemented!(), MakeStatic => unimplemented!(), ChangeLevel => unimplemented!(), CvarSet => self.builtin_cvar_set()?, CenterPrint => unimplemented!(), AmbientSound => self.builtin_ambient_sound()?, PrecacheModel2 => unimplemented!(), PrecacheSound2 => unimplemented!(), PrecacheFile2 => unimplemented!(), SetSpawnArgs => unimplemented!(), } debug!("Returning from built-in function {}", name); } else { self.cx.enter_function(&mut self.globals, f_to_call)?; continue; } } Done | Return => self.op_return(a, b, c)?, MulF => self.globals.op_mul_f(a, b, c)?, MulV => self.globals.op_mul_v(a, b, c)?, MulFV => self.globals.op_mul_fv(a, b, c)?, MulVF => self.globals.op_mul_vf(a, b, c)?, Div => self.globals.op_div(a, b, c)?, AddF => self.globals.op_add_f(a, b, c)?, AddV => self.globals.op_add_v(a, b, c)?, SubF => self.globals.op_sub_f(a, b, c)?, SubV => self.globals.op_sub_v(a, b, c)?, EqF => self.globals.op_eq_f(a, b, c)?, EqV => self.globals.op_eq_v(a, b, c)?, EqS => self.globals.op_eq_s(a, b, c)?, EqEnt => self.globals.op_eq_ent(a, b, c)?, EqFnc => self.globals.op_eq_fnc(a, b, c)?, NeF => self.globals.op_ne_f(a, b, c)?, NeV => self.globals.op_ne_v(a, b, c)?, NeS => self.globals.op_ne_s(a, b, c)?, NeEnt => self.globals.op_ne_ent(a, b, c)?, NeFnc => self.globals.op_ne_fnc(a, b, c)?, Le => self.globals.op_le(a, b, c)?, Ge => self.globals.op_ge(a, b, c)?, Lt => self.globals.op_lt(a, b, c)?, Gt => self.globals.op_gt(a, b, c)?, LoadF => self.op_load_f(a, b, c)?, LoadV => self.op_load_v(a, b, c)?, LoadS => self.op_load_s(a, b, c)?, LoadEnt => self.op_load_ent(a, b, c)?, LoadFld => panic!("load_fld not implemented"), LoadFnc => self.op_load_fnc(a, b, c)?, Address => self.op_address(a, b, c)?, StoreF => self.globals.op_store_f(a, b, c)?, StoreV => self.globals.op_store_v(a, b, c)?, StoreS => self.globals.op_store_s(a, b, c)?, StoreEnt => self.globals.op_store_ent(a, b, c)?, StoreFld => self.globals.op_store_fld(a, b, c)?, StoreFnc => self.globals.op_store_fnc(a, b, c)?, StorePF => self.op_storep_f(a, b, c)?, StorePV => self.op_storep_v(a, b, c)?, StorePS => self.op_storep_s(a, b, c)?, StorePEnt => self.op_storep_ent(a, b, c)?, StorePFld => panic!("storep_fld not implemented"), StorePFnc => self.op_storep_fnc(a, b, c)?, NotF => self.globals.op_not_f(a, b, c)?, NotV => self.globals.op_not_v(a, b, c)?, NotS => self.globals.op_not_s(a, b, c)?, NotEnt => self.globals.op_not_ent(a, b, c)?, NotFnc => self.globals.op_not_fnc(a, b, c)?, And => self.globals.op_and(a, b, c)?, Or => self.globals.op_or(a, b, c)?, BitAnd => self.globals.op_bit_and(a, b, c)?, BitOr => self.globals.op_bit_or(a, b, c)?, State => self.op_state(a, b, c)?, } // Increment program counter. self.cx.jump_relative(1); } Ok(()) } pub fn execute_program_by_name(&mut self, name: S) -> Result<(), ProgsError> where S: AsRef, { let func_id = self.cx.find_function_by_name(name)?; self.execute_program(func_id)?; Ok(()) } /// Link an entity into the `World`. /// /// If `touch_triggers` is `true`, this will invoke the touch function of /// any trigger the entity is touching. pub fn link_entity( &mut self, ent_id: EntityId, touch_triggers: bool, ) -> Result<(), ProgsError> { self.world.link_entity(ent_id)?; if touch_triggers { self.touch_triggers(ent_id)?; } Ok(()) } pub fn spawn_entity(&mut self) -> Result { let ent_id = self.world.alloc_uninitialized()?; self.link_entity(ent_id, false)?; Ok(ent_id) } pub fn spawn_entity_from_map( &mut self, map: HashMap<&str, &str>, ) -> Result { let classname = match map.get("classname") { Some(c) => c.to_owned(), None => return Err(ProgsError::with_msg("No classname for entity")), }; let ent_id = self.world.alloc_from_map(map)?; // TODO: set origin, mins and maxs here if needed // set `self` before calling spawn function self.globals .put_entity_id(ent_id, GlobalAddrEntity::Self_ as i16)?; self.execute_program_by_name(classname)?; self.link_entity(ent_id, true)?; Ok(ent_id) } pub fn set_entity_origin( &mut self, ent_id: EntityId, origin: Vector3, ) -> Result<(), ProgsError> { self.world .entity_mut(ent_id)? .store(FieldAddrVector::Origin, origin.into())?; self.link_entity(ent_id, false)?; Ok(()) } pub fn set_entity_model( &mut self, ent_id: EntityId, model_name_id: StringId, ) -> Result<(), ProgsError> { let model_id = { let ent = self.world.entity_mut(ent_id)?; ent.put_string_id(model_name_id, FieldAddrStringId::ModelName as i16)?; let model_id = match self.string_table.borrow().get(model_name_id) { Some(name) => match self.model_precache.find(name) { Some(i) => i, None => return Err(ProgsError::with_msg("model not precached")), }, None => return Err(ProgsError::with_msg("invalid StringId")), }; ent.put_float(model_id as f32, FieldAddrFloat::ModelIndex as i16)?; model_id }; self.world.set_entity_model(ent_id, model_id)?; Ok(()) } pub fn think(&mut self, ent_id: EntityId, frame_time: Duration) -> Result<(), ProgsError> { let ent = self.world.entity_mut(ent_id)?; let think_time = duration_from_f32(ent.load(FieldAddrFloat::NextThink)?); if think_time <= Duration::zero() || think_time > self.time + frame_time { // Think either already happened or isn't due yet. return Ok(()); } // Deschedule next think. ent.store(FieldAddrFloat::NextThink, 0.0)?; // Call entity's think function. let think = ent.load(FieldAddrFunctionId::Think)?; self.globals .store(GlobalAddrFloat::Time, duration_to_f32(think_time))?; self.globals.store(GlobalAddrEntity::Self_, ent_id)?; self.globals.store(GlobalAddrEntity::Other, EntityId(0))?; self.execute_program(think)?; Ok(()) } pub fn physics( &mut self, clients: &ClientSlots, frame_time: Duration, ) -> Result<(), ProgsError> { self.globals.store(GlobalAddrEntity::Self_, EntityId(0))?; self.globals.store(GlobalAddrEntity::Other, EntityId(0))?; self.globals .store(GlobalAddrFloat::Time, duration_to_f32(self.time))?; let start_frame = self .globals .function_id(GlobalAddrFunction::StartFrame as i16)?; self.execute_program(start_frame)?; // TODO: don't alloc let mut ent_ids = Vec::new(); self.world.list_entities(&mut ent_ids); for ent_id in ent_ids { if self.globals.load(GlobalAddrFloat::ForceRetouch)? != 0.0 { // Force all entities to touch triggers, even if they didn't // move. This is required when e.g. creating new triggers, as // stationary entities typically do not get relinked, and so // will ignore new triggers even when touching them. // // TODO: this may have a subtle ordering bug. If entity 2 has // physics run, sets ForceRetouch and spawns entity 1, then // entity 1 will not have a chance to touch triggers this frame. // Quake solves this by using a linked list and always spawning // at the end so that newly spawned entities always have physics // run this frame. self.link_entity(ent_id, true)?; } let max_clients = clients.limit(); if ent_id.0 != 0 && ent_id.0 < max_clients { self.physics_player(clients, ent_id)?; } else { match self.world.entity(ent_id).move_kind()? { MoveKind::Walk => { todo!("MoveKind::Walk"); } MoveKind::Push => self.physics_push(ent_id, frame_time)?, // No actual physics for this entity, but still let it think. MoveKind::None => self.think(ent_id, frame_time)?, MoveKind::NoClip => self.physics_noclip(ent_id, frame_time)?, MoveKind::Step => self.physics_step(ent_id, frame_time)?, // all airborne entities have the same physics _ => unimplemented!(), } } match self.globals.load(GlobalAddrFloat::ForceRetouch)? { f if f > 0.0 => self.globals.store(GlobalAddrFloat::ForceRetouch, f - 1.0)?, _ => (), } } // TODO: increase sv.time by host_frametime unimplemented!(); } // TODO: rename arguments when implementing pub fn physics_player( &mut self, clients: &ClientSlots, ent_id: EntityId, ) -> Result<(), ProgsError> { let client_id = ent_id.0.checked_sub(1).ok_or_else(|| { ProgsError::with_msg(format!("Invalid client entity ID: {:?}", ent_id)) })?; if clients.get(client_id).is_none() { // No client in this slot. return Ok(()); } let ent = self.world.entity_mut(ent_id)?; ent.limit_velocity(self.cvars.borrow().get_value("sv_maxvelocity").unwrap())?; unimplemented!(); } pub fn physics_push( &mut self, ent_id: EntityId, frame_time: Duration, ) -> Result<(), ProgsError> { let ent = self.world.entity_mut(ent_id)?; let local_time = duration_from_f32(ent.load(FieldAddrFloat::LocalTime)?); let next_think = duration_from_f32(ent.load(FieldAddrFloat::NextThink)?); let move_time = if local_time + frame_time > next_think { (next_think - local_time).max(Duration::zero()) } else { frame_time }; drop(ent); if !move_time.is_zero() { self.move_push(ent_id, frame_time, move_time)?; } let ent = self.world.entity_mut(ent_id)?; let old_local_time = local_time; let new_local_time = duration_from_f32(ent.load(FieldAddrFloat::LocalTime)?); // Let the entity think if it needs to. if old_local_time < next_think && next_think <= new_local_time { // Deschedule thinking. ent.store(FieldAddrFloat::NextThink, 0.0)?; self.globals .put_float(duration_to_f32(self.time), GlobalAddrFloat::Time as i16)?; self.globals .put_entity_id(ent_id, GlobalAddrEntity::Self_ as i16)?; self.globals .put_entity_id(EntityId(0), GlobalAddrEntity::Other as i16)?; let think = ent.function_id(FieldAddrFunctionId::Think as i16)?; self.execute_program(think)?; } Ok(()) } pub fn physics_noclip( &mut self, ent_id: EntityId, frame_time: Duration, ) -> Result<(), ProgsError> { // Let entity think, then move if it didn't remove itself. self.think(ent_id, frame_time)?; if let Ok(ent) = self.world.entity_mut(ent_id) { let frame_time_f = duration_to_f32(frame_time); let angles: Vector3 = ent.load(FieldAddrVector::Angles)?.into(); let angle_vel: Vector3 = ent.load(FieldAddrVector::AngularVelocity)?.into(); let new_angles = angles + frame_time_f * angle_vel; ent.store(FieldAddrVector::Angles, new_angles.into())?; let orig: Vector3 = ent.load(FieldAddrVector::Origin)?.into(); let vel: Vector3 = ent.load(FieldAddrVector::Velocity)?.into(); let new_orig = orig + frame_time_f * vel; ent.store(FieldAddrVector::Origin, new_orig.into())?; } Ok(()) } pub fn physics_step( &mut self, ent_id: EntityId, frame_time: Duration, ) -> Result<(), ProgsError> { let in_freefall = !self .world .entity(ent_id) .flags()? .intersects(EntityFlags::ON_GROUND | EntityFlags::FLY | EntityFlags::IN_WATER); if in_freefall { let sv_gravity = self.cvars.borrow().get_value("sv_gravity").unwrap(); let vel: Vector3 = self .world .entity(ent_id) .load(FieldAddrVector::Velocity)? .into(); // If true, play an impact sound when the entity hits the ground. let hit_sound = vel.z < -0.1 * sv_gravity; self.world .entity_mut(ent_id)? .apply_gravity(sv_gravity, frame_time)?; let sv_maxvelocity = self.cvars.borrow().get_value("sv_maxvelocity").unwrap(); self.world .entity_mut(ent_id)? .limit_velocity(sv_maxvelocity)?; // Move the entity and relink it. self.move_ballistic(frame_time, ent_id)?; self.link_entity(ent_id, true)?; let ent = self.world.entity_mut(ent_id)?; if ent.flags()?.contains(EntityFlags::ON_GROUND) && hit_sound { // Entity hit the ground this frame. todo!("SV_StartSound(demon/dland2.wav)"); } } self.think(ent_id, frame_time)?; todo!("SV_CheckWaterTransition"); Ok(()) } pub fn move_push( &mut self, ent_id: EntityId, frame_time: Duration, move_time: Duration, ) -> Result<(), ProgsError> { let ent = self.world.entity_mut(ent_id)?; let vel: Vector3 = ent.load(FieldAddrVector::Velocity)?.into(); if vel.is_zero() { // Entity doesn't need to move. let local_time = ent.load(FieldAddrFloat::LocalTime)?; let new_local_time = local_time + duration_to_f32(move_time); ent.store(FieldAddrFloat::LocalTime, new_local_time)?; return Ok(()); } let move_time_f = duration_to_f32(move_time); let move_vector = vel * move_time_f; // TODO let mins = todo!() } const MAX_BALLISTIC_COLLISIONS: usize = 4; /// Movement function for freefalling entities. pub fn move_ballistic( &mut self, sim_time: Duration, ent_id: EntityId, ) -> Result<(CollisionFlags, Option), ProgsError> { let mut sim_time_f = duration_to_f32(sim_time); let mut out_trace = None; let mut flags = CollisionFlags::empty(); let mut touching_planes: ArrayVec = ArrayVec::new(); let init_velocity = self.world.entity(ent_id).velocity()?; let mut trace_velocity = init_velocity; // Even when the entity collides with something along its path, it may // continue moving. This may occur when bouncing or sliding off a solid // object, or when moving between media (e.g. from air to water). for _ in 0..Self::MAX_BALLISTIC_COLLISIONS { let velocity = self.world.entity(ent_id).velocity()?; if velocity.is_zero() { // Not moving. break; } let orig = self.world.entity(ent_id).origin()?; let end = orig + sim_time_f * velocity; let min = self.world.entity(ent_id).min()?; let max = self.world.entity(ent_id).max()?; let (trace, hit_entity) = self.world .move_entity(ent_id, orig, min, max, end, CollideKind::Normal)?; if trace.all_solid() { // Entity is stuck in a wall. self.world .entity_mut(ent_id)? .store(FieldAddrVector::Velocity, Vector3::zero().into())?; return Ok((CollisionFlags::HORIZONTAL | CollisionFlags::VERTICAL, None)); } if trace.ratio() > 0.0 { // If the entity moved at all, update its position. self.world .entity_mut(ent_id)? .store(FieldAddrVector::Origin, trace.end_point().into())?; touching_planes.clear(); trace_velocity = self.world.entity(ent_id).velocity()?; } // Find the plane the entity hit, if any. let boundary = match trace.end().kind() { // Entity didn't hit anything. TraceEndKind::Terminal => break, TraceEndKind::Boundary(b) => b, }; // Sanity check to make sure the trace actually hit something. let hit_entity = match hit_entity { Some(h) => h, None => panic!("trace collided with nothing"), }; // TODO: magic constant if boundary.plane.normal().z > 0.7 { flags |= CollisionFlags::HORIZONTAL; if self.world.entity(hit_entity).solid()? == EntitySolid::Bsp { self.world .entity_mut(ent_id)? .add_flags(EntityFlags::ON_GROUND)?; self.world .entity_mut(ent_id)? .store(FieldAddrEntityId::Ground, hit_entity)?; } } else if boundary.plane.normal().z == 0.0 { flags |= CollisionFlags::VERTICAL; out_trace = Some(trace.clone()); } self.impact_entities(ent_id, hit_entity)?; if !self.world.entity_exists(ent_id) { // Entity removed by touch function. break; } sim_time_f -= trace.ratio() * sim_time_f; if touching_planes.try_push(boundary.plane.clone()).is_err() { // Touching too many planes to make much sense of, so stop. self.world .entity_mut(ent_id)? .store(FieldAddrVector::Velocity, Vector3::zero().into())?; return Ok((CollisionFlags::HORIZONTAL | CollisionFlags::VERTICAL, None)); } let end_velocity = match phys::velocity_after_multi_collision(trace_velocity, &touching_planes, 1.0) { Some(v) => v, None => { // Entity is wedged in a corner, so it simply stops. self.world .entity_mut(ent_id)? .store(FieldAddrVector::Velocity, Vector3::zero().into())?; return Ok(( CollisionFlags::HORIZONTAL | CollisionFlags::VERTICAL | CollisionFlags::STOPPED, None, )); } }; if init_velocity.dot(end_velocity) <= 0.0 { // Avoid bouncing the entity at a sharp angle. self.world .entity_mut(ent_id)? .store(FieldAddrVector::Velocity, Vector3::zero().into())?; return Ok((flags, out_trace)); } self.world .entity_mut(ent_id)? .store(FieldAddrVector::Velocity, end_velocity.into())?; } Ok((flags, out_trace)) } const DROP_TO_FLOOR_DIST: f32 = 256.0; /// Moves an entity straight down until it collides with a solid surface. /// /// Returns `true` if the entity hit the floor, `false` otherwise. /// /// ## Notes /// - The drop distance is limited to 256, so entities which are more than 256 units above a /// solid surface will not actually hit the ground. pub fn drop_entity_to_floor(&mut self, ent_id: EntityId) -> Result { debug!("Finding floor for entity with ID {}", ent_id.0); let origin = self.world.entity(ent_id).origin()?; let end = Vector3::new(origin.x, origin.y, origin.z - Self::DROP_TO_FLOOR_DIST); let min = self.world.entity(ent_id).min()?; let max = self.world.entity(ent_id).max()?; let (trace, collide_entity) = self.world .move_entity(ent_id, origin, min, max, end, CollideKind::Normal)?; debug!("End position after drop: {:?}", trace.end_point()); let drop_dist = 256.0; let actual_dist = (trace.end_point() - origin).magnitude(); if collide_entity.is_none() || actual_dist == drop_dist || trace.all_solid() { // Entity didn't hit the floor or is stuck. Ok(false) } else { // Entity hit the floor. Update origin, relink and set ON_GROUND flag. self.world .entity_mut(ent_id)? .put_vector(trace.end_point().into(), FieldAddrVector::Origin as i16)?; self.link_entity(ent_id, false)?; self.world .entity_mut(ent_id)? .add_flags(EntityFlags::ON_GROUND)?; self.world .entity_mut(ent_id)? .put_entity_id(collide_entity.unwrap(), FieldAddrEntityId::Ground as i16)?; Ok(true) } } pub fn touch_triggers(&mut self, ent_id: EntityId) -> Result<(), ProgsError> { // TODO: alloc once let mut touched = Vec::new(); self.world.list_touched_triggers(&mut touched, ent_id, 0)?; // Save state. let restore_self = self.globals.load(GlobalAddrEntity::Self_)?; let restore_other = self.globals.load(GlobalAddrEntity::Other)?; // Activate the touched triggers. for trigger_id in touched { let trigger_touch = self .world .entity(trigger_id) .load(FieldAddrFunctionId::Touch)?; self.globals.store(GlobalAddrEntity::Self_, trigger_id)?; self.globals.store(GlobalAddrEntity::Other, ent_id)?; self.execute_program(trigger_touch)?; } // Restore state. self.globals.store(GlobalAddrEntity::Self_, restore_self)?; self.globals.store(GlobalAddrEntity::Other, restore_other)?; Ok(()) } /// Runs two entities' touch functions. pub fn impact_entities(&mut self, ent_a: EntityId, ent_b: EntityId) -> Result<(), ProgsError> { let restore_self = self.globals.load(GlobalAddrEntity::Self_)?; let restore_other = self.globals.load(GlobalAddrEntity::Other)?; self.globals .store(GlobalAddrFloat::Time, duration_to_f32(self.time))?; // Set up and run Entity A's touch function. let touch_a = self.world.entity(ent_a).load(FieldAddrFunctionId::Touch)?; let solid_a = self.world.entity(ent_a).solid()?; if touch_a.0 != 0 && solid_a != EntitySolid::Not { self.globals.store(GlobalAddrEntity::Self_, ent_a)?; self.globals.store(GlobalAddrEntity::Other, ent_b)?; self.execute_program(touch_a)?; } // Set up and run Entity B's touch function. let touch_b = self.world.entity(ent_b).load(FieldAddrFunctionId::Touch)?; let solid_b = self.world.entity(ent_b).solid()?; if touch_b.0 != 0 && solid_b != EntitySolid::Not { self.globals.store(GlobalAddrEntity::Self_, ent_b)?; self.globals.store(GlobalAddrEntity::Other, ent_a)?; self.execute_program(touch_b)?; } self.globals.store(GlobalAddrEntity::Self_, restore_self)?; self.globals.store(GlobalAddrEntity::Other, restore_other)?; Ok(()) } // QuakeC instructions ==================================================== pub fn op_return(&mut self, a: i16, b: i16, c: i16) -> Result<(), ProgsError> { let val1 = self.globals.get_bytes(a)?; let val2 = self.globals.get_bytes(b)?; let val3 = self.globals.get_bytes(c)?; self.globals.put_bytes(val1, GLOBAL_ADDR_RETURN as i16)?; self.globals .put_bytes(val2, GLOBAL_ADDR_RETURN as i16 + 1)?; self.globals .put_bytes(val3, GLOBAL_ADDR_RETURN as i16 + 2)?; self.cx.leave_function(&mut self.globals)?; Ok(()) } // LOAD_F: load float field from entity pub fn op_load_f(&mut self, e_ofs: i16, e_f: i16, dest_ofs: i16) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(e_ofs)?; let fld_ofs = self.globals.get_field_addr(e_f)?; let f = self.world.entity(ent_id).get_float(fld_ofs.0 as i16)?; self.globals.put_float(f, dest_ofs)?; Ok(()) } // LOAD_V: load vector field from entity pub fn op_load_v( &mut self, ent_id_addr: i16, ent_vector_addr: i16, dest_addr: i16, ) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(ent_id_addr)?; let ent_vector = self.globals.get_field_addr(ent_vector_addr)?; let v = self.world.entity(ent_id).get_vector(ent_vector.0 as i16)?; self.globals.put_vector(v, dest_addr)?; Ok(()) } pub fn op_load_s( &mut self, ent_id_addr: i16, ent_string_id_addr: i16, dest_addr: i16, ) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(ent_id_addr)?; let ent_string_id = self.globals.get_field_addr(ent_string_id_addr)?; let s = self .world .entity(ent_id) .string_id(ent_string_id.0 as i16)?; self.globals.put_string_id(s, dest_addr)?; Ok(()) } pub fn op_load_ent( &mut self, ent_id_addr: i16, ent_entity_id_addr: i16, dest_addr: i16, ) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(ent_id_addr)?; let ent_entity_id = self.globals.get_field_addr(ent_entity_id_addr)?; let e = self .world .entity(ent_id) .entity_id(ent_entity_id.0 as i16)?; self.globals.put_entity_id(e, dest_addr)?; Ok(()) } pub fn op_load_fnc( &mut self, ent_id_addr: i16, ent_function_id_addr: i16, dest_addr: i16, ) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(ent_id_addr)?; let fnc_function_id = self.globals.get_field_addr(ent_function_id_addr)?; let f = self .world .entity(ent_id) .function_id(fnc_function_id.0 as i16)?; self.globals.put_function_id(f, dest_addr)?; Ok(()) } pub fn op_address( &mut self, ent_id_addr: i16, fld_addr_addr: i16, dest_addr: i16, ) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(ent_id_addr)?; let fld_addr = self.globals.get_field_addr(fld_addr_addr)?; self.globals.put_entity_field( self.world.ent_fld_addr_to_i32(EntityFieldAddr { entity_id: ent_id, field_addr: fld_addr, }), dest_addr, )?; Ok(()) } pub fn op_storep_f( &mut self, src_float_addr: i16, dst_ent_fld_addr: i16, unused: i16, ) -> Result<(), ProgsError> { if unused != 0 { return Err(ProgsError::with_msg("storep_f: nonzero arg3")); } let f = self.globals.get_float(src_float_addr)?; let ent_fld_addr = self .world .ent_fld_addr_from_i32(self.globals.get_entity_field(dst_ent_fld_addr)?); self.world .entity_mut(ent_fld_addr.entity_id)? .put_float(f, ent_fld_addr.field_addr.0 as i16)?; Ok(()) } pub fn op_storep_v( &mut self, src_vector_addr: i16, dst_ent_fld_addr: i16, unused: i16, ) -> Result<(), ProgsError> { if unused != 0 { return Err(ProgsError::with_msg("storep_v: nonzero arg3")); } let v = self.globals.get_vector(src_vector_addr)?; let ent_fld_addr = self .world .ent_fld_addr_from_i32(self.globals.get_entity_field(dst_ent_fld_addr)?); self.world .entity_mut(ent_fld_addr.entity_id)? .put_vector(v, ent_fld_addr.field_addr.0 as i16)?; Ok(()) } pub fn op_storep_s( &mut self, src_string_id_addr: i16, dst_ent_fld_addr: i16, unused: i16, ) -> Result<(), ProgsError> { if unused != 0 { return Err(ProgsError::with_msg("storep_s: nonzero arg3")); } let s = self.globals.string_id(src_string_id_addr)?; let ent_fld_addr = self .world .ent_fld_addr_from_i32(self.globals.get_entity_field(dst_ent_fld_addr)?); self.world .entity_mut(ent_fld_addr.entity_id)? .put_string_id(s, ent_fld_addr.field_addr.0 as i16)?; Ok(()) } pub fn op_storep_ent( &mut self, src_entity_id_addr: i16, dst_ent_fld_addr: i16, unused: i16, ) -> Result<(), ProgsError> { if unused != 0 { return Err(ProgsError::with_msg("storep_ent: nonzero arg3")); } let e = self.globals.entity_id(src_entity_id_addr)?; let ent_fld_addr = self .world .ent_fld_addr_from_i32(self.globals.get_entity_field(dst_ent_fld_addr)?); self.world .entity_mut(ent_fld_addr.entity_id)? .put_entity_id(e, ent_fld_addr.field_addr.0 as i16)?; Ok(()) } pub fn op_storep_fnc( &mut self, src_function_id_addr: i16, dst_ent_fld_addr: i16, unused: i16, ) -> Result<(), ProgsError> { if unused != 0 { return Err(ProgsError::with_msg("storep_fnc: nonzero arg3")); } let f = self.globals.function_id(src_function_id_addr)?; let ent_fld_addr = self .world .ent_fld_addr_from_i32(self.globals.get_entity_field(dst_ent_fld_addr)?); self.world .entity_mut(ent_fld_addr.entity_id)? .put_function_id(f, ent_fld_addr.field_addr.0 as i16)?; Ok(()) } pub fn op_state( &mut self, frame_id_addr: i16, unused_b: i16, unused_c: i16, ) -> Result<(), ProgsError> { if unused_b != 0 { return Err(ProgsError::with_msg("storep_fnc: nonzero arg2")); } else if unused_c != 0 { return Err(ProgsError::with_msg("storep_fnc: nonzero arg3")); } let self_id = self.globals.entity_id(GlobalAddrEntity::Self_ as i16)?; let self_ent = self.world.entity_mut(self_id)?; let next_think_time = self.globals.get_float(GlobalAddrFloat::Time as i16)? + 0.1; self_ent.put_float(next_think_time, FieldAddrFloat::NextThink as i16)?; let frame_id = self.globals.get_float(frame_id_addr)?; self_ent.put_float(frame_id, FieldAddrFloat::FrameId as i16)?; Ok(()) } // QuakeC built-in functions ============================================== pub fn builtin_set_origin(&mut self) -> Result<(), ProgsError> { let e_id = self.globals.entity_id(GLOBAL_ADDR_ARG_0 as i16)?; let origin = self.globals.get_vector(GLOBAL_ADDR_ARG_1 as i16)?; self.set_entity_origin(e_id, Vector3::from(origin))?; Ok(()) } pub fn builtin_set_model(&mut self) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(GLOBAL_ADDR_ARG_0 as i16)?; let model_name_id = self.globals.string_id(GLOBAL_ADDR_ARG_1 as i16)?; self.set_entity_model(ent_id, model_name_id)?; Ok(()) } pub fn builtin_set_size(&mut self) -> Result<(), ProgsError> { let e_id = self.globals.entity_id(GLOBAL_ADDR_ARG_0 as i16)?; let mins = self.globals.get_vector(GLOBAL_ADDR_ARG_1 as i16)?; let maxs = self.globals.get_vector(GLOBAL_ADDR_ARG_2 as i16)?; self.world.set_entity_size(e_id, mins.into(), maxs.into())?; Ok(()) } // TODO: move to Globals pub fn builtin_random(&mut self) -> Result<(), ProgsError> { self.globals .put_float(rand::random(), GLOBAL_ADDR_RETURN as i16)?; Ok(()) } pub fn builtin_spawn(&mut self) -> Result<(), ProgsError> { let ent_id = self.spawn_entity()?; self.globals .put_entity_id(ent_id, GLOBAL_ADDR_RETURN as i16)?; Ok(()) } pub fn builtin_remove(&mut self) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(GLOBAL_ADDR_ARG_0 as i16)?; self.world.remove_entity(ent_id)?; Ok(()) } pub fn builtin_precache_sound(&mut self) -> Result<(), ProgsError> { // TODO: disable precaching after server is active // TODO: precaching doesn't actually load yet let s_id = self.globals.string_id(GLOBAL_ADDR_ARG_0 as i16)?; self.precache_sound(s_id); self.globals .put_string_id(s_id, GLOBAL_ADDR_RETURN as i16)?; Ok(()) } pub fn builtin_precache_model(&mut self) -> Result<(), ProgsError> { // TODO: disable precaching after server is active // TODO: precaching doesn't actually load yet let s_id = self.globals.string_id(GLOBAL_ADDR_ARG_0 as i16)?; if self.model_id(s_id).is_none() { self.precache_model(s_id); self.world.add_model(&self.vfs, s_id)?; } self.globals .put_string_id(s_id, GLOBAL_ADDR_RETURN as i16)?; Ok(()) } pub fn builtin_dprint(&mut self) -> Result<(), ProgsError> { let strs = self.string_table.borrow(); let s_id = self.globals.string_id(GLOBAL_ADDR_ARG_0 as i16)?; let string = strs.get(s_id).unwrap(); debug!("DPRINT: {}", string); Ok(()) } pub fn builtin_drop_to_floor(&mut self) -> Result<(), ProgsError> { let ent_id = self.globals.entity_id(GlobalAddrEntity::Self_ as i16)?; let hit_floor = self.drop_entity_to_floor(ent_id)?; self.globals .put_float(hit_floor as u32 as f32, GLOBAL_ADDR_RETURN as i16)?; Ok(()) } pub fn builtin_light_style(&mut self) -> Result<(), ProgsError> { let index = match self.globals.get_float(GLOBAL_ADDR_ARG_0 as i16)? as i32 { i if i < 0 => return Err(ProgsError::with_msg("negative lightstyle ID")), i => i as usize, }; let val = self.globals.string_id(GLOBAL_ADDR_ARG_1 as i16)?; self.set_lightstyle(index, val); Ok(()) } pub fn builtin_cvar(&mut self) -> Result<(), ProgsError> { let s_id = self.globals.string_id(GLOBAL_ADDR_ARG_0 as i16)?; let strs = self.string_table.borrow(); let s = strs.get(s_id).unwrap(); let f = self.cvars.borrow().get_value(s).unwrap(); self.globals.put_float(f, GLOBAL_ADDR_RETURN as i16)?; Ok(()) } pub fn builtin_cvar_set(&mut self) -> Result<(), ProgsError> { let strs = self.string_table.borrow(); let var_id = self.globals.string_id(GLOBAL_ADDR_ARG_0 as i16)?; let var = strs.get(var_id).unwrap(); let val_id = self.globals.string_id(GLOBAL_ADDR_ARG_1 as i16)?; let val = strs.get(val_id).unwrap(); self.cvars.borrow_mut().set(var, val).unwrap(); Ok(()) } pub fn builtin_ambient_sound(&mut self) -> Result<(), ProgsError> { let _pos = self.globals.get_vector(GLOBAL_ADDR_ARG_0 as i16)?; let name = self.globals.string_id(GLOBAL_ADDR_ARG_1 as i16)?; let _volume = self.globals.get_float(GLOBAL_ADDR_ARG_2 as i16)?; let _attenuation = self.globals.get_float(GLOBAL_ADDR_ARG_3 as i16)?; let _sound_index = match self.sound_id(name) { Some(i) => i, None => return Err(ProgsError::with_msg("sound not precached")), }; // TODO: write to server signon packet Ok(()) } } ================================================ FILE: src/server/precache.rs ================================================ use std::ops::Range; use arrayvec::{ArrayString, ArrayVec}; /// Maximum permitted length of a precache path. const MAX_PRECACHE_PATH: usize = 64; const MAX_PRECACHE_ENTRIES: usize = 256; /// A list of resources to be loaded before entering the game. /// /// This is used by the server to inform clients which resources (sounds and /// models) they should load before joining. It also serves as the canonical /// mapping of resource IDs for a given level. // TODO: ideally, this is parameterized by the maximum number of entries, but // it's not currently possible to do { MAX_PRECACHE_PATH * N } where N is a // const generic parameter. In practice both models and sounds have a maximum // value of 256. #[derive(Debug)] pub struct Precache { str_data: ArrayString<{ MAX_PRECACHE_PATH * MAX_PRECACHE_ENTRIES }>, items: ArrayVec, MAX_PRECACHE_ENTRIES>, } impl Precache { /// Creates a new empty `Precache`. pub fn new() -> Precache { Precache { str_data: ArrayString::new(), items: ArrayVec::new(), } } /// Retrieves an item from the precache if the item exists. pub fn get(&self, index: usize) -> Option<&str> { if index > self.items.len() { return None; } let range = self.items[index].clone(); Some(&self.str_data[range]) } /// Returns the index of the target value if it exists. pub fn find(&self, target: S) -> Option where S: AsRef, { let (idx, _) = self .iter() .enumerate() .find(|&(_, item)| item == target.as_ref())?; Some(idx) } /// Adds an item to the precache. /// /// If the item already exists in the precache, this has no effect. pub fn precache(&mut self, item: S) where S: AsRef, { let item = item.as_ref(); if item.len() > MAX_PRECACHE_PATH { panic!( "precache name (\"{}\") too long: max length is {}", item, MAX_PRECACHE_PATH ); } if self.find(item).is_some() { // Already precached. return; } let start = self.str_data.len(); self.str_data.push_str(item); let end = self.str_data.len(); self.items.push(start..end); } /// Returns an iterator over the values in the precache. pub fn iter(&self) -> impl Iterator { self.items .iter() .cloned() .map(move |range| &self.str_data[range]) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_precache_one() { let mut p = Precache::new(); p.precache("hello"); assert_eq!(Some("hello"), p.get(0)); } #[test] fn test_precache_several() { let mut p = Precache::new(); let items = &["Quake", "is", "a", "1996", "first-person", "shooter"]; for item in items { p.precache(item); } // Pick an element in the middle assert_eq!(Some("first-person"), p.get(4)); // Check all the elements for (precached, &original) in p.iter().zip(items.iter()) { assert_eq!(precached, original); } } } ================================================ FILE: src/server/progs/functions.rs ================================================ // Copyright © 2018 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{cell::RefCell, convert::TryInto, rc::Rc}; use num::FromPrimitive; use crate::server::progs::{ops::Opcode, ProgsError, StringId, StringTable}; pub const MAX_ARGS: usize = 8; #[derive(Clone, Debug)] #[repr(C)] pub struct Statement { pub opcode: Opcode, pub arg1: i16, pub arg2: i16, pub arg3: i16, } impl Statement { pub fn new(op: i16, arg1: i16, arg2: i16, arg3: i16) -> Result { let opcode = match Opcode::from_i16(op) { Some(o) => o, None => return Err(ProgsError::with_msg(format!("Bad opcode 0x{:x}", op))), }; Ok(Statement { opcode, arg1, arg2, arg3, }) } } #[derive(Copy, Clone, Debug, Default, PartialEq)] #[repr(C)] pub struct FunctionId(pub usize); impl TryInto for FunctionId { type Error = ProgsError; fn try_into(self) -> Result { if self.0 > ::std::i32::MAX as usize { Err(ProgsError::with_msg("function ID out of range")) } else { Ok(self.0 as i32) } } } #[derive(Debug)] pub enum FunctionKind { BuiltIn(BuiltinFunctionId), QuakeC(usize), } #[derive(Copy, Clone, Debug, FromPrimitive)] pub enum BuiltinFunctionId { // pr_builtin[0] is the null function MakeVectors = 1, SetOrigin = 2, SetModel = 3, SetSize = 4, // pr_builtin[5] (PF_setabssize) was never implemented Break = 6, Random = 7, Sound = 8, Normalize = 9, Error = 10, ObjError = 11, VLen = 12, VecToYaw = 13, Spawn = 14, Remove = 15, TraceLine = 16, CheckClient = 17, Find = 18, PrecacheSound = 19, PrecacheModel = 20, StuffCmd = 21, FindRadius = 22, BPrint = 23, SPrint = 24, DPrint = 25, FToS = 26, VToS = 27, CoreDump = 28, TraceOn = 29, TraceOff = 30, EPrint = 31, WalkMove = 32, // pr_builtin[33] is not implemented DropToFloor = 34, LightStyle = 35, RInt = 36, Floor = 37, Ceil = 38, // pr_builtin[39] is not implemented CheckBottom = 40, PointContents = 41, // pr_builtin[42] is not implemented FAbs = 43, Aim = 44, Cvar = 45, LocalCmd = 46, NextEnt = 47, Particle = 48, ChangeYaw = 49, // pr_builtin[50] is not implemented VecToAngles = 51, WriteByte = 52, WriteChar = 53, WriteShort = 54, WriteLong = 55, WriteCoord = 56, WriteAngle = 57, WriteString = 58, WriteEntity = 59, // pr_builtin[60] through pr_builtin[66] are only defined for Quake 2 MoveToGoal = 67, PrecacheFile = 68, MakeStatic = 69, ChangeLevel = 70, // pr_builtin[71] is not implemented CvarSet = 72, CenterPrint = 73, AmbientSound = 74, PrecacheModel2 = 75, PrecacheSound2 = 76, PrecacheFile2 = 77, SetSpawnArgs = 78, } #[derive(Debug)] pub struct FunctionDef { pub kind: FunctionKind, pub arg_start: usize, pub locals: usize, pub name_id: StringId, pub srcfile_id: StringId, pub argc: usize, pub argsz: [u8; MAX_ARGS], } #[derive(Debug)] pub struct Functions { pub string_table: Rc>, pub defs: Box<[FunctionDef]>, pub statements: Box<[Statement]>, } impl Functions { pub fn id_from_i32(&self, value: i32) -> Result { if value < 0 { return Err(ProgsError::with_msg("id < 0")); } if (value as usize) < self.defs.len() { Ok(FunctionId(value as usize)) } else { Err(ProgsError::with_msg(format!( "no function with ID {}", value ))) } } pub fn get_def(&self, id: FunctionId) -> Result<&FunctionDef, ProgsError> { if id.0 >= self.defs.len() { Err(ProgsError::with_msg(format!( "No function with ID {}", id.0 ))) } else { Ok(&self.defs[id.0]) } } pub fn find_function_by_name(&self, name: S) -> Result where S: AsRef, { for (i, def) in self.defs.iter().enumerate() { let strs = self.string_table.borrow(); let f_name = strs.get(def.name_id).ok_or_else(|| { ProgsError::with_msg(format!("No string with ID {:?}", def.name_id)) })?; if f_name == name.as_ref() { return Ok(FunctionId(i)); } } Err(ProgsError::with_msg(format!( "No function named {}", name.as_ref() ))) } } ================================================ FILE: src/server/progs/globals.rs ================================================ // Copyright © 2018 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{cell::RefCell, convert::TryInto, error::Error, fmt, rc::Rc}; use crate::server::progs::{ EntityId, FieldAddr, FunctionId, GlobalDef, StringId, StringTable, Type, }; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use cgmath::{Deg, Euler, InnerSpace, Matrix3, Vector3}; pub const GLOBAL_STATIC_START: usize = 28; pub const GLOBAL_DYNAMIC_START: usize = 64; pub const GLOBAL_STATIC_COUNT: usize = GLOBAL_DYNAMIC_START - GLOBAL_STATIC_START; #[allow(dead_code)] pub const GLOBAL_ADDR_NULL: usize = 0; pub const GLOBAL_ADDR_RETURN: usize = 1; pub const GLOBAL_ADDR_ARG_0: usize = 4; pub const GLOBAL_ADDR_ARG_1: usize = 7; pub const GLOBAL_ADDR_ARG_2: usize = 10; pub const GLOBAL_ADDR_ARG_3: usize = 13; #[allow(dead_code)] pub const GLOBAL_ADDR_ARG_4: usize = 16; #[allow(dead_code)] pub const GLOBAL_ADDR_ARG_5: usize = 19; #[allow(dead_code)] pub const GLOBAL_ADDR_ARG_6: usize = 22; #[allow(dead_code)] pub const GLOBAL_ADDR_ARG_7: usize = 25; #[derive(Debug)] pub enum GlobalsError { Io(::std::io::Error), Address(isize), Other(String), } impl GlobalsError { pub fn with_msg(msg: S) -> Self where S: AsRef, { GlobalsError::Other(msg.as_ref().to_owned()) } } impl fmt::Display for GlobalsError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { GlobalsError::Io(ref err) => { write!(f, "I/O error: ")?; err.fmt(f) } GlobalsError::Address(val) => write!(f, "Invalid address ({})", val), GlobalsError::Other(ref msg) => write!(f, "{}", msg), } } } impl Error for GlobalsError {} impl From<::std::io::Error> for GlobalsError { fn from(error: ::std::io::Error) -> Self { GlobalsError::Io(error) } } pub trait GlobalAddr { /// The type of value referenced by this address. type Value; /// Loads the value at this address. fn load(&self, globals: &Globals) -> Result; /// Stores a value at this address. fn store(&self, globals: &mut Globals, value: Self::Value) -> Result<(), GlobalsError>; } #[derive(Copy, Clone, FromPrimitive)] pub enum GlobalAddrFloat { Time = 31, FrameTime = 32, ForceRetouch = 33, Deathmatch = 35, Coop = 36, TeamPlay = 37, ServerFlags = 38, TotalSecrets = 39, TotalMonsters = 40, FoundSecrets = 41, KilledMonsters = 42, Arg0 = 43, Arg1 = 44, Arg2 = 45, Arg3 = 46, Arg4 = 47, Arg5 = 48, Arg6 = 49, Arg7 = 50, Arg8 = 51, Arg9 = 52, Arg10 = 53, Arg11 = 54, Arg12 = 55, Arg13 = 56, Arg14 = 57, Arg15 = 58, VForwardX = 59, VForwardY = 60, VForwardZ = 61, VUpX = 62, VUpY = 63, VUpZ = 64, VRightX = 65, VRightY = 66, VRightZ = 67, TraceAllSolid = 68, TraceStartSolid = 69, TraceFraction = 70, TraceEndPosX = 71, TraceEndPosY = 72, TraceEndPosZ = 73, TracePlaneNormalX = 74, TracePlaneNormalY = 75, TracePlaneNormalZ = 76, TracePlaneDist = 77, TraceInOpen = 79, TraceInWater = 80, } impl GlobalAddr for GlobalAddrFloat { type Value = f32; #[inline] fn load(&self, globals: &Globals) -> Result { globals.get_float(*self as i16) } #[inline] fn store(&self, globals: &mut Globals, value: Self::Value) -> Result<(), GlobalsError> { globals.put_float(value, *self as i16) } } #[derive(Copy, Clone, FromPrimitive)] pub enum GlobalAddrVector { VForward = 59, VUp = 62, VRight = 65, TraceEndPos = 71, TracePlaneNormal = 74, } impl GlobalAddr for GlobalAddrVector { type Value = [f32; 3]; #[inline] fn load(&self, globals: &Globals) -> Result { globals.get_vector(*self as i16) } #[inline] fn store(&self, globals: &mut Globals, value: Self::Value) -> Result<(), GlobalsError> { globals.put_vector(value, *self as i16) } } #[derive(FromPrimitive)] pub enum GlobalAddrString { MapName = 34, } #[derive(Copy, Clone, FromPrimitive)] pub enum GlobalAddrEntity { Self_ = 28, Other = 29, World = 30, TraceEntity = 78, MsgEntity = 81, } impl GlobalAddr for GlobalAddrEntity { type Value = EntityId; #[inline] fn load(&self, globals: &Globals) -> Result { globals.entity_id(*self as i16) } #[inline] fn store(&self, globals: &mut Globals, value: Self::Value) -> Result<(), GlobalsError> { globals.put_entity_id(value, *self as i16) } } #[derive(FromPrimitive)] pub enum GlobalAddrField {} #[derive(FromPrimitive)] pub enum GlobalAddrFunction { Main = 82, StartFrame = 83, PlayerPreThink = 84, PlayerPostThink = 85, ClientKill = 86, ClientConnect = 87, PutClientInServer = 88, ClientDisconnect = 89, SetNewArgs = 90, SetChangeArgs = 91, } #[derive(Debug)] pub struct Globals { string_table: Rc>, defs: Box<[GlobalDef]>, addrs: Box<[[u8; 4]]>, } impl Globals { /// Constructs a new `Globals` object. pub fn new( string_table: Rc>, defs: Box<[GlobalDef]>, addrs: Box<[[u8; 4]]>, ) -> Globals { Globals { string_table, defs, addrs, } } /// Performs a type check at `addr` with type `type_`. /// /// The type check allows checking `QFloat` against `QVector` and vice-versa, since vectors have /// overlapping definitions with their x-components (e.g. a vector `origin` and its x-component /// `origin_X` will have the same address). pub fn type_check(&self, addr: usize, type_: Type) -> Result<(), GlobalsError> { match self.defs.iter().find(|def| def.offset as usize == addr) { Some(d) => { if type_ == d.type_ { return Ok(()); } else if type_ == Type::QFloat && d.type_ == Type::QVector { return Ok(()); } else if type_ == Type::QVector && d.type_ == Type::QFloat { return Ok(()); } else { return Err(GlobalsError::with_msg("type check failed")); } } None => return Ok(()), } } /// Returns a reference to the memory at the given address. pub fn get_addr(&self, addr: i16) -> Result<&[u8], GlobalsError> { if addr < 0 { return Err(GlobalsError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(GlobalsError::Address(addr as isize)); } Ok(&self.addrs[addr]) } /// Returns a mutable reference to the memory at the given address. pub fn get_addr_mut(&mut self, addr: i16) -> Result<&mut [u8], GlobalsError> { if addr < 0 { return Err(GlobalsError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(GlobalsError::Address(addr as isize)); } Ok(&mut self.addrs[addr]) } /// Returns a copy of the memory at the given address. pub fn get_bytes(&self, addr: i16) -> Result<[u8; 4], GlobalsError> { if addr < 0 { return Err(GlobalsError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(GlobalsError::Address(addr as isize)); } Ok(self.addrs[addr]) } /// Writes the provided data to the memory at the given address. /// /// This can be used to circumvent the type checker in cases where an operation is not dependent /// of the type of the data. pub fn put_bytes(&mut self, val: [u8; 4], addr: i16) -> Result<(), GlobalsError> { if addr < 0 { return Err(GlobalsError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(GlobalsError::Address(addr as isize)); } self.addrs[addr] = val; Ok(()) } /// Loads an `i32` from the given virtual address. pub fn get_int(&self, addr: i16) -> Result { Ok(self.get_addr(addr)?.read_i32::()?) } /// Loads an `i32` from the given virtual address. pub fn put_int(&mut self, val: i32, addr: i16) -> Result<(), GlobalsError> { self.get_addr_mut(addr)?.write_i32::(val)?; Ok(()) } /// Loads an `f32` from the given virtual address. pub fn get_float(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QFloat)?; Ok(self.get_addr(addr)?.read_f32::()?) } /// Stores an `f32` at the given virtual address. pub fn put_float(&mut self, val: f32, addr: i16) -> Result<(), GlobalsError> { self.type_check(addr as usize, Type::QFloat)?; self.get_addr_mut(addr)?.write_f32::(val)?; Ok(()) } /// Loads an `[f32; 3]` from the given virtual address. pub fn get_vector(&self, addr: i16) -> Result<[f32; 3], GlobalsError> { self.type_check(addr as usize, Type::QVector)?; let mut v = [0.0; 3]; for i in 0..3 { v[i] = self.get_float(addr + i as i16)?; } Ok(v) } /// Stores an `[f32; 3]` at the given virtual address. pub fn put_vector(&mut self, val: [f32; 3], addr: i16) -> Result<(), GlobalsError> { self.type_check(addr as usize, Type::QVector)?; for i in 0..3 { self.put_float(val[i], addr + i as i16)?; } Ok(()) } /// Loads a `StringId` from the given virtual address. pub fn string_id(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QString)?; Ok(StringId( self.get_addr(addr)?.read_i32::()? as usize )) } /// Stores a `StringId` at the given virtual address. pub fn put_string_id(&mut self, val: StringId, addr: i16) -> Result<(), GlobalsError> { self.type_check(addr as usize, Type::QString)?; self.get_addr_mut(addr)? .write_i32::(val.try_into().unwrap())?; Ok(()) } /// Loads an `EntityId` from the given virtual address. pub fn entity_id(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QEntity)?; match self.get_addr(addr)?.read_i32::()? { e if e < 0 => Err(GlobalsError::with_msg(format!( "Negative entity ID ({})", e ))), e => Ok(EntityId(e as usize)), } } /// Stores an `EntityId` at the given virtual address. pub fn put_entity_id(&mut self, val: EntityId, addr: i16) -> Result<(), GlobalsError> { self.type_check(addr as usize, Type::QEntity)?; self.get_addr_mut(addr)? .write_i32::(val.0 as i32)?; Ok(()) } /// Loads a `FieldAddr` from the given virtual address. pub fn get_field_addr(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QField)?; match self.get_addr(addr)?.read_i32::()? { f if f < 0 => Err(GlobalsError::with_msg(format!( "Negative entity ID ({})", f ))), f => Ok(FieldAddr(f as usize)), } } /// Stores a `FieldAddr` at the given virtual address. pub fn put_field_addr(&mut self, val: FieldAddr, addr: i16) -> Result<(), GlobalsError> { self.type_check(addr as usize, Type::QField)?; self.get_addr_mut(addr)? .write_i32::(val.0 as i32)?; Ok(()) } /// Loads a `FunctionId` from the given virtual address. pub fn function_id(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QFunction)?; Ok(FunctionId( self.get_addr(addr)?.read_i32::()? as usize )) } /// Stores a `FunctionId` at the given virtual address. pub fn put_function_id(&mut self, val: FunctionId, addr: i16) -> Result<(), GlobalsError> { self.type_check(addr as usize, Type::QFunction)?; self.get_addr_mut(addr)? .write_i32::(val.try_into().unwrap())?; Ok(()) } // TODO: typecheck these with QPointer? pub fn get_entity_field(&self, addr: i16) -> Result { Ok(self.get_addr(addr)?.read_i32::()?) } pub fn put_entity_field(&mut self, val: i32, addr: i16) -> Result<(), GlobalsError> { self.get_addr_mut(addr)?.write_i32::(val)?; Ok(()) } pub fn load(&self, addr: A) -> Result { addr.load(self) } pub fn store(&mut self, addr: A, value: A::Value) -> Result<(), GlobalsError> { addr.store(self, value) } /// Copies the data at `src_addr` to `dst_addr` without type checking. pub fn untyped_copy(&mut self, src_addr: i16, dst_addr: i16) -> Result<(), GlobalsError> { let src = self.get_addr(src_addr)?.to_owned(); let dst = self.get_addr_mut(dst_addr)?; for i in 0..4 { dst[i] = src[i] } Ok(()) } // QuakeC instructions ===================================================== pub fn op_mul_f(&mut self, f1_id: i16, f2_id: i16, prod_id: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_id)?; let f2 = self.get_float(f2_id)?; self.put_float(f1 * f2, prod_id)?; Ok(()) } // MUL_V: Vector dot-product pub fn op_mul_v(&mut self, v1_id: i16, v2_id: i16, dot_id: i16) -> Result<(), GlobalsError> { let v1 = self.get_vector(v1_id)?; let v2 = self.get_vector(v2_id)?; let mut dot = 0.0; for c in 0..3 { dot += v1[c] * v2[c]; } self.put_float(dot, dot_id)?; Ok(()) } // MUL_FV: Component-wise multiplication of vector by scalar pub fn op_mul_fv(&mut self, f_id: i16, v_id: i16, prod_id: i16) -> Result<(), GlobalsError> { let f = self.get_float(f_id)?; let v = self.get_vector(v_id)?; let mut prod = [0.0; 3]; for c in 0..prod.len() { prod[c] = v[c] * f; } self.put_vector(prod, prod_id)?; Ok(()) } // MUL_VF: Component-wise multiplication of vector by scalar pub fn op_mul_vf(&mut self, v_id: i16, f_id: i16, prod_id: i16) -> Result<(), GlobalsError> { let v = self.get_vector(v_id)?; let f = self.get_float(f_id)?; let mut prod = [0.0; 3]; for c in 0..prod.len() { prod[c] = v[c] * f; } self.put_vector(prod, prod_id)?; Ok(()) } // DIV: Float division pub fn op_div(&mut self, f1_id: i16, f2_id: i16, quot_id: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_id)?; let f2 = self.get_float(f2_id)?; self.put_float(f1 / f2, quot_id)?; Ok(()) } // ADD_F: Float addition pub fn op_add_f(&mut self, f1_ofs: i16, f2_ofs: i16, sum_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float(f1 + f2, sum_ofs)?; Ok(()) } // ADD_V: Vector addition pub fn op_add_v(&mut self, v1_id: i16, v2_id: i16, sum_id: i16) -> Result<(), GlobalsError> { let v1 = self.get_vector(v1_id)?; let v2 = self.get_vector(v2_id)?; let mut sum = [0.0; 3]; for c in 0..sum.len() { sum[c] = v1[c] + v2[c]; } self.put_vector(sum, sum_id)?; Ok(()) } // SUB_F: Float subtraction pub fn op_sub_f(&mut self, f1_id: i16, f2_id: i16, diff_id: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_id)?; let f2 = self.get_float(f2_id)?; self.put_float(f1 - f2, diff_id)?; Ok(()) } // SUB_V: Vector subtraction pub fn op_sub_v(&mut self, v1_id: i16, v2_id: i16, diff_id: i16) -> Result<(), GlobalsError> { let v1 = self.get_vector(v1_id)?; let v2 = self.get_vector(v2_id)?; let mut diff = [0.0; 3]; for c in 0..diff.len() { diff[c] = v1[c] - v2[c]; } self.put_vector(diff, diff_id)?; Ok(()) } // EQ_F: Test equality of two floats pub fn op_eq_f(&mut self, f1_id: i16, f2_id: i16, eq_id: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_id)?; let f2 = self.get_float(f2_id)?; self.put_float( match f1 == f2 { true => 1.0, false => 0.0, }, eq_id, )?; Ok(()) } // EQ_V: Test equality of two vectors pub fn op_eq_v(&mut self, v1_id: i16, v2_id: i16, eq_id: i16) -> Result<(), GlobalsError> { let v1 = self.get_vector(v1_id)?; let v2 = self.get_vector(v2_id)?; self.put_float( match v1 == v2 { true => 1.0, false => 0.0, }, eq_id, )?; Ok(()) } // EQ_S: Test equality of two strings pub fn op_eq_s(&mut self, s1_ofs: i16, s2_ofs: i16, eq_ofs: i16) -> Result<(), GlobalsError> { if s1_ofs < 0 || s2_ofs < 0 { return Err(GlobalsError::with_msg("eq_s: negative string offset")); } if s1_ofs == s2_ofs || self.string_id(s1_ofs)? == self.string_id(s2_ofs)? { self.put_float(1.0, eq_ofs)?; } else { self.put_float(0.0, eq_ofs)?; } Ok(()) } // EQ_ENT: Test equality of two entities (by identity) pub fn op_eq_ent(&mut self, e1_ofs: i16, e2_ofs: i16, eq_ofs: i16) -> Result<(), GlobalsError> { let e1 = self.entity_id(e1_ofs)?; let e2 = self.entity_id(e2_ofs)?; self.put_float( match e1 == e2 { true => 1.0, false => 0.0, }, eq_ofs, )?; Ok(()) } // EQ_FNC: Test equality of two functions (by identity) pub fn op_eq_fnc(&mut self, f1_ofs: i16, f2_ofs: i16, eq_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.function_id(f1_ofs)?; let f2 = self.function_id(f2_ofs)?; self.put_float( match f1 == f2 { true => 1.0, false => 0.0, }, eq_ofs, )?; Ok(()) } // NE_F: Test inequality of two floats pub fn op_ne_f(&mut self, f1_ofs: i16, f2_ofs: i16, ne_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float( match f1 != f2 { true => 1.0, false => 0.0, }, ne_ofs, )?; Ok(()) } // NE_V: Test inequality of two vectors pub fn op_ne_v(&mut self, v1_ofs: i16, v2_ofs: i16, ne_ofs: i16) -> Result<(), GlobalsError> { let v1 = self.get_vector(v1_ofs)?; let v2 = self.get_vector(v2_ofs)?; self.put_float( match v1 != v2 { true => 1.0, false => 0.0, }, ne_ofs, )?; Ok(()) } // NE_S: Test inequality of two strings pub fn op_ne_s(&mut self, s1_ofs: i16, s2_ofs: i16, ne_ofs: i16) -> Result<(), GlobalsError> { if s1_ofs < 0 || s2_ofs < 0 { return Err(GlobalsError::with_msg("eq_s: negative string offset")); } if s1_ofs != s2_ofs && self.string_id(s1_ofs)? != self.string_id(s2_ofs)? { self.put_float(1.0, ne_ofs)?; } else { self.put_float(0.0, ne_ofs)?; } Ok(()) } pub fn op_ne_ent(&mut self, e1_ofs: i16, e2_ofs: i16, ne_ofs: i16) -> Result<(), GlobalsError> { let e1 = self.entity_id(e1_ofs)?; let e2 = self.entity_id(e2_ofs)?; self.put_float( match e1 != e2 { true => 1.0, false => 0.0, }, ne_ofs, )?; Ok(()) } pub fn op_ne_fnc(&mut self, f1_ofs: i16, f2_ofs: i16, ne_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.function_id(f1_ofs)?; let f2 = self.function_id(f2_ofs)?; self.put_float( match f1 != f2 { true => 1.0, false => 0.0, }, ne_ofs, )?; Ok(()) } // LE: Less than or equal to comparison pub fn op_le(&mut self, f1_ofs: i16, f2_ofs: i16, le_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float( match f1 <= f2 { true => 1.0, false => 0.0, }, le_ofs, )?; Ok(()) } // GE: Greater than or equal to comparison pub fn op_ge(&mut self, f1_ofs: i16, f2_ofs: i16, ge_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float( match f1 >= f2 { true => 1.0, false => 0.0, }, ge_ofs, )?; Ok(()) } // LT: Less than comparison pub fn op_lt(&mut self, f1_ofs: i16, f2_ofs: i16, lt_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float( match f1 < f2 { true => 1.0, false => 0.0, }, lt_ofs, )?; Ok(()) } // GT: Greater than comparison pub fn op_gt(&mut self, f1_ofs: i16, f2_ofs: i16, gt_ofs: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float( match f1 > f2 { true => 1.0, false => 0.0, }, gt_ofs, )?; Ok(()) } // STORE_F pub fn op_store_f( &mut self, src_ofs: i16, dest_ofs: i16, unused: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg3 to STORE_F")); } let f = self.get_float(src_ofs)?; self.put_float(f, dest_ofs)?; Ok(()) } // STORE_V pub fn op_store_v( &mut self, src_ofs: i16, dest_ofs: i16, unused: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg3 to STORE_V")); } if dest_ofs > 0 && dest_ofs < GLOBAL_STATIC_START as i16 { // Untyped copy is required because STORE_V is used to copy function arguments into the global // argument slots. // // See https://github.com/id-Software/Quake-Tools/blob/master/qcc/pr_comp.c#L362 for c in 0..3 { self.untyped_copy(src_ofs + c as i16, dest_ofs + c as i16)?; } } else { for c in 0..3 { let f = self.get_float(src_ofs + c)?; self.put_float(f, dest_ofs + c)?; } } Ok(()) } pub fn op_store_s( &mut self, src_ofs: i16, dest_ofs: i16, unused: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg3 to STORE_S")); } let s = self.string_id(src_ofs)?; self.put_string_id(s, dest_ofs)?; Ok(()) } pub fn op_store_ent( &mut self, src_ofs: i16, dest_ofs: i16, unused: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg3 to STORE_ENT")); } let ent = self.entity_id(src_ofs)?; self.put_entity_id(ent, dest_ofs)?; Ok(()) } pub fn op_store_fld( &mut self, src_ofs: i16, dest_ofs: i16, unused: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg3 to STORE_FLD")); } let fld = self.get_field_addr(src_ofs)?; self.put_field_addr(fld, dest_ofs)?; Ok(()) } pub fn op_store_fnc( &mut self, src_ofs: i16, dest_ofs: i16, unused: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg3 to STORE_FNC")); } let fnc = self.function_id(src_ofs)?; self.put_function_id(fnc, dest_ofs)?; Ok(()) } // NOT_F: Compare float to 0.0 pub fn op_not_f(&mut self, f_id: i16, unused: i16, not_id: i16) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg2 to NOT_F")); } let f = self.get_float(f_id)?; self.put_float( match f == 0.0 { true => 1.0, false => 0.0, }, not_id, )?; Ok(()) } // NOT_V: Compare vec to { 0.0, 0.0, 0.0 } pub fn op_not_v(&mut self, v_id: i16, unused: i16, not_id: i16) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg2 to NOT_V")); } let v = self.get_vector(v_id)?; let zero_vec = [0.0; 3]; self.put_vector( match v == zero_vec { true => [1.0; 3], false => zero_vec, }, not_id, )?; Ok(()) } // NOT_S: Compare string to null string pub fn op_not_s(&mut self, s_ofs: i16, unused: i16, not_ofs: i16) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg2 to NOT_S")); } if s_ofs < 0 { return Err(GlobalsError::with_msg("not_s: negative string offset")); } let s = self.string_id(s_ofs)?; if s_ofs == 0 || s.0 == 0 { self.put_float(1.0, not_ofs)?; } else { self.put_float(0.0, not_ofs)?; } Ok(()) } // NOT_FNC: Compare function to null function (0) pub fn op_not_fnc( &mut self, fnc_id_ofs: i16, unused: i16, not_ofs: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg2 to NOT_FNC")); } let fnc_id = self.function_id(fnc_id_ofs)?; self.put_float( match fnc_id { FunctionId(0) => 1.0, _ => 0.0, }, not_ofs, )?; Ok(()) } // NOT_ENT: Compare entity to null entity (0) pub fn op_not_ent( &mut self, ent_ofs: i16, unused: i16, not_ofs: i16, ) -> Result<(), GlobalsError> { if unused != 0 { return Err(GlobalsError::with_msg("Nonzero arg2 to NOT_ENT")); } let ent = self.entity_id(ent_ofs)?; self.put_float( match ent { EntityId(0) => 1.0, _ => 0.0, }, not_ofs, )?; Ok(()) } // AND: Logical AND pub fn op_and(&mut self, f1_id: i16, f2_id: i16, and_id: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_id)?; let f2 = self.get_float(f2_id)?; self.put_float( match f1 != 0.0 && f2 != 0.0 { true => 1.0, false => 0.0, }, and_id, )?; Ok(()) } // OR: Logical OR pub fn op_or(&mut self, f1_id: i16, f2_id: i16, or_id: i16) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_id)?; let f2 = self.get_float(f2_id)?; self.put_float( match f1 != 0.0 || f2 != 0.0 { true => 1.0, false => 0.0, }, or_id, )?; Ok(()) } // BIT_AND: Bitwise AND pub fn op_bit_and( &mut self, f1_ofs: i16, f2_ofs: i16, bit_and_ofs: i16, ) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float((f1 as i32 & f2 as i32) as f32, bit_and_ofs)?; Ok(()) } // BIT_OR: Bitwise OR pub fn op_bit_or( &mut self, f1_ofs: i16, f2_ofs: i16, bit_or_ofs: i16, ) -> Result<(), GlobalsError> { let f1 = self.get_float(f1_ofs)?; let f2 = self.get_float(f2_ofs)?; self.put_float((f1 as i32 | f2 as i32) as f32, bit_or_ofs)?; Ok(()) } // QuakeC built-in functions =============================================== #[inline] pub fn builtin_random(&mut self) -> Result<(), GlobalsError> { self.put_float(rand::random(), GLOBAL_ADDR_RETURN as i16) } /// Calculate `v_forward`, `v_right` and `v_up` from `angles`. /// /// This requires some careful coordinate system transformations. Angle vectors are stored /// as `[pitch, yaw, roll]` -- that is, rotations about the lateral (right), vertical (up), and /// longitudinal (forward) axes respectively. However, Quake's coordinate system maps `x` to the /// longitudinal (forward) axis, `y` to the *negative* lateral (leftward) axis, and `z` to the /// vertical (up) axis. As a result, the rotation matrix has to be calculated from `[roll, /// -pitch, yaw]` instead. pub fn make_vectors(&mut self) -> Result<(), GlobalsError> { let angles = self.get_vector(GLOBAL_ADDR_ARG_0 as i16)?; let rotation_matrix = make_vectors(angles); self.put_vector(rotation_matrix.x.into(), GlobalAddrVector::VForward as i16)?; self.put_vector(rotation_matrix.y.into(), GlobalAddrVector::VRight as i16)?; self.put_vector(rotation_matrix.z.into(), GlobalAddrVector::VUp as i16)?; Ok(()) } /// Calculate the magnitude of a vector. /// /// Loads the vector from `GLOBAL_ADDR_ARG_0` and stores its magnitude at /// `GLOBAL_ADDR_RETURN`. pub fn builtin_v_len(&mut self) -> Result<(), GlobalsError> { let v = Vector3::from(self.get_vector(GLOBAL_ADDR_ARG_0 as i16)?); self.put_float(v.magnitude(), GLOBAL_ADDR_RETURN as i16)?; Ok(()) } /// Calculate a yaw angle from a direction vector. /// /// Loads the direction vector from `GLOBAL_ADDR_ARG_0` and stores the yaw value at /// `GLOBAL_ADDR_RETURN`. pub fn builtin_vec_to_yaw(&mut self) -> Result<(), GlobalsError> { let v = self.get_vector(GLOBAL_ADDR_ARG_0 as i16)?; let mut yaw; if v[0] == 0.0 || v[1] == 0.0 { yaw = 0.0; } else { yaw = v[1].atan2(v[0]).to_degrees(); if yaw < 0.0 { yaw += 360.0; } } self.put_float(yaw, GLOBAL_ADDR_RETURN as i16)?; Ok(()) } /// Round a float to the nearest integer. /// /// Loads the float from `GLOBAL_ADDR_ARG_0` and stores the rounded value at /// `GLOBAL_ADDR_RETURN`. pub fn builtin_r_int(&mut self) -> Result<(), GlobalsError> { let f = self.get_float(GLOBAL_ADDR_ARG_0 as i16)?; self.put_float(f.round(), GLOBAL_ADDR_RETURN as i16)?; Ok(()) } /// Round a float to the nearest integer less than or equal to it. /// /// Loads the float from `GLOBAL_ADDR_ARG_0` and stores the rounded value at /// `GLOBAL_ADDR_RETURN`. pub fn builtin_floor(&mut self) -> Result<(), GlobalsError> { let f = self.get_float(GLOBAL_ADDR_ARG_0 as i16)?; self.put_float(f.floor(), GLOBAL_ADDR_RETURN as i16)?; Ok(()) } /// Round a float to the nearest integer greater than or equal to it. /// /// Loads the float from `GLOBAL_ADDR_ARG_0` and stores the rounded value at /// `GLOBAL_ADDR_RETURN`. pub fn builtin_ceil(&mut self) -> Result<(), GlobalsError> { let f = self.get_float(GLOBAL_ADDR_ARG_0 as i16)?; self.put_float(f.ceil(), GLOBAL_ADDR_RETURN as i16)?; Ok(()) } /// Calculate the absolute value of a float. /// /// Loads the float from `GLOBAL_ADDR_ARG_0` and stores its absolute value at /// `GLOBAL_ADDR_RETURN`. pub fn builtin_f_abs(&mut self) -> Result<(), GlobalsError> { let f = self.get_float(GLOBAL_ADDR_ARG_0 as i16)?; self.put_float(f.abs(), GLOBAL_ADDR_RETURN as i16)?; Ok(()) } } pub fn make_vectors(angles: [f32; 3]) -> Matrix3 { let pitch = Deg(-angles[0]); let yaw = Deg(angles[1]); let roll = Deg(angles[2]); Matrix3::from(Euler::new(roll, pitch, yaw)) } #[cfg(test)] mod test { use super::*; use cgmath::SquareMatrix; #[test] fn test_make_vectors_no_rotation() { let angles_zero = [0.0; 3]; let result = make_vectors(angles_zero); assert_eq!(Matrix3::identity(), result); } #[test] fn test_make_vectors_pitch() { let pitch_90 = [90.0, 0.0, 0.0]; let result = make_vectors(pitch_90); assert_eq!(Matrix3::from_angle_y(Deg(-90.0)), result); } #[test] fn test_make_vectors_yaw() { let yaw_90 = [0.0, 90.0, 0.0]; let result = make_vectors(yaw_90); assert_eq!(Matrix3::from_angle_z(Deg(90.0)), result); } #[test] fn test_make_vectors_roll() { let roll_90 = [0.0, 0.0, 90.0]; let result = make_vectors(roll_90); assert_eq!(Matrix3::from_angle_x(Deg(90.0)), result); } } ================================================ FILE: src/server/progs/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //! QuakeC bytecode interpreter //! //! # Loading //! //! QuakeC bytecode is typically loaded from `progs.dat` or `qwprogs.dat`. Bytecode files begin with //! a brief header with an `i32` format version number (which must equal VERSION) and an `i32` CRC //! checksum to ensure the correct bytecode is being loaded. //! //! ```text //! version: i32, //! crc: i32, //! ``` //! //! This is followed by a series of six lumps acting as a directory into the file data. Each lump //! consists of an `i32` byte offset into the file data and an `i32` element count. //! //! ```text //! statement_offset: i32, //! statement_count: i32, //! //! globaldef_offset: i32, //! globaldef_count: i32, //! //! fielddef_offset: i32, //! fielddef_count: i32, //! //! function_offset: i32, //! function_count: i32, //! //! string_offset: i32, //! string_count: i32, //! //! global_offset: i32, //! global_count: i32, //! ``` //! //! These offsets are not guaranteed to be in order, and in fact `progs.dat` usually has the string //! section first. Offsets are in bytes from the beginning of the file. //! //! ## String data //! //! The string data block is located at the offset given by `string_offset` and consists of a series //! of null-terminated ASCII strings laid end-to-end. The first string is always the empty string, //! i.e. the first byte is always the null byte. The total size in bytes of the string data is given //! by `string_count`. //! //! ## Statements //! //! The statement table is located at the offset given by `statement_offset` and consists of //! `statement_count` 8-byte instructions of the form //! //! ```text //! opcode: u16, //! arg1: i16, //! arg2: i16, //! arg3: i16, //! ``` //! //! Not every opcode uses three arguments, but all statements have space for three arguments anyway, //! probably for simplicity. The semantics of these arguments differ depending on the opcode. //! //! ## Function Definitions //! //! Function definitions contain both high-level information about the function (name and source //! file) and low-level information necessary to execute it (entry point, argument count, etc). //! Functions are stored on disk as follows: //! //! ```text //! statement_id: i32, // index of first statement; negatives are built-in functions //! arg_start: i32, // address to store/load first argument //! local_count: i32, // number of local variables on the stack //! profile: i32, // incremented every time function called //! fnc_name_ofs: i32, // offset of function name in string table //! srcfile_name_ofs: i32, // offset of source file name in string table //! arg_count: i32, // number of arguments (max. 8) //! arg_sizes: [u8; 8], // sizes of each argument //! ``` pub mod functions; pub mod globals; mod ops; mod string_table; use std::{ cell::RefCell, convert::TryInto, error::Error, fmt, io::{Read, Seek, SeekFrom}, rc::Rc, }; use crate::server::world::{EntityError, EntityTypeDef}; use byteorder::{LittleEndian, ReadBytesExt}; use num::FromPrimitive; use self::{ functions::{BuiltinFunctionId, FunctionDef, FunctionKind, Statement, MAX_ARGS}, globals::{GLOBAL_ADDR_ARG_0, GLOBAL_STATIC_COUNT}, }; pub use self::{ functions::{FunctionId, Functions}, globals::{ GlobalAddrEntity, GlobalAddrFloat, GlobalAddrFunction, GlobalAddrVector, Globals, GlobalsError, }, ops::Opcode, string_table::StringTable, }; const VERSION: i32 = 6; const CRC: i32 = 5927; const MAX_CALL_STACK_DEPTH: usize = 32; const MAX_LOCAL_STACK_DEPTH: usize = 2048; const LUMP_COUNT: usize = 6; const SAVE_GLOBAL: u16 = 1 << 15; // the on-disk size of a bytecode statement const STATEMENT_SIZE: usize = 8; // the on-disk size of a function declaration const FUNCTION_SIZE: usize = 36; // the on-disk size of a global or field definition const DEF_SIZE: usize = 8; #[derive(Debug)] pub enum ProgsError { Io(::std::io::Error), Globals(GlobalsError), Entity(EntityError), CallStackOverflow, LocalStackOverflow, Other(String), } impl ProgsError { pub fn with_msg(msg: S) -> Self where S: AsRef, { ProgsError::Other(msg.as_ref().to_owned()) } } impl fmt::Display for ProgsError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::ProgsError::*; match *self { Io(ref err) => { write!(f, "I/O error: ")?; err.fmt(f) } Globals(ref err) => { write!(f, "Globals error: ")?; err.fmt(f) } Entity(ref err) => { write!(f, "Entity error: ")?; err.fmt(f) } CallStackOverflow => write!(f, "Call stack overflow"), LocalStackOverflow => write!(f, "Local stack overflow"), Other(ref msg) => write!(f, "{}", msg), } } } impl Error for ProgsError {} impl From<::std::io::Error> for ProgsError { fn from(error: ::std::io::Error) -> Self { ProgsError::Io(error) } } impl From for ProgsError { fn from(error: GlobalsError) -> Self { ProgsError::Globals(error) } } impl From for ProgsError { fn from(error: EntityError) -> Self { ProgsError::Entity(error) } } #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] #[repr(C)] pub struct StringId(pub usize); impl TryInto for StringId { type Error = ProgsError; fn try_into(self) -> Result { if self.0 > ::std::i32::MAX as usize { Err(ProgsError::with_msg("string id out of i32 range")) } else { Ok(self.0 as i32) } } } impl StringId { pub fn new() -> StringId { StringId(0) } } #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)] #[repr(C)] pub struct EntityId(pub usize); #[derive(Copy, Clone, Debug, Default, PartialEq)] #[repr(C)] pub struct FieldAddr(pub usize); #[derive(Copy, Clone, Debug, Default, PartialEq)] #[repr(C)] pub struct EntityFieldAddr { pub entity_id: EntityId, pub field_addr: FieldAddr, } enum LumpId { Statements = 0, GlobalDefs = 1, Fielddefs = 2, Functions = 3, Strings = 4, Globals = 5, } #[derive(Copy, Clone, Debug, FromPrimitive, PartialEq)] #[repr(u16)] pub enum Type { QVoid = 0, QString = 1, QFloat = 2, QVector = 3, QEntity = 4, QField = 5, QFunction = 6, QPointer = 7, } #[derive(Copy, Clone, Debug)] struct Lump { offset: usize, count: usize, } #[derive(Debug)] pub struct GlobalDef { save: bool, type_: Type, offset: u16, name_id: StringId, } /// An entity field definition. /// /// These definitions can be used to look up entity fields by name. This is /// required for custom fields defined in QuakeC code; their offsets are not /// known at compile time. #[derive(Debug)] pub struct FieldDef { pub type_: Type, pub offset: u16, pub name_id: StringId, } /// The values returned by loading a `progs.dat` file. pub struct LoadProgs { pub cx: ExecutionContext, pub globals: Globals, pub entity_def: Rc, pub string_table: Rc>, } /// Loads all data from a `progs.dat` file. /// /// This returns objects representing the necessary context to execute QuakeC bytecode. pub fn load(mut src: R) -> Result where R: Read + Seek, { assert!(src.read_i32::()? == VERSION); assert!(src.read_i32::()? == CRC); let mut lumps = [Lump { offset: 0, count: 0, }; LUMP_COUNT]; for l in 0..lumps.len() as usize { lumps[l] = Lump { offset: src.read_i32::()? as usize, count: src.read_i32::()? as usize, }; debug!("{:?}: {:?}", l, lumps[l]); } let ent_addr_count = src.read_i32::()? as usize; debug!("Field count: {}", ent_addr_count); // Read string data and construct StringTable let string_lump = &lumps[LumpId::Strings as usize]; src.seek(SeekFrom::Start(string_lump.offset as u64))?; let mut strings = Vec::new(); (&mut src) .take(string_lump.count as u64) .read_to_end(&mut strings)?; let string_table = Rc::new(RefCell::new(StringTable::new(strings))); assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (string_lump.offset + string_lump.count) as u64, ))? ); // Read function definitions and statements and construct Functions let function_lump = &lumps[LumpId::Functions as usize]; src.seek(SeekFrom::Start(function_lump.offset as u64))?; let mut function_defs = Vec::with_capacity(function_lump.count); for i in 0..function_lump.count { assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (function_lump.offset + i * FUNCTION_SIZE) as u64, ))? ); let kind = match src.read_i32::()? { x if x < 0 => match BuiltinFunctionId::from_i32(-x) { Some(f) => FunctionKind::BuiltIn(f), None => { return Err(ProgsError::with_msg(format!( "Invalid built-in function ID {}", -x ))) } }, x => FunctionKind::QuakeC(x as usize), }; let arg_start = src.read_i32::()?; let locals = src.read_i32::()?; // throw away profile variable let _ = src.read_i32::()?; let name_id = string_table .borrow() .id_from_i32(src.read_i32::()?)?; let srcfile_id = string_table .borrow() .id_from_i32(src.read_i32::()?)?; let argc = src.read_i32::()?; let mut argsz = [0; MAX_ARGS]; src.read(&mut argsz)?; function_defs.push(FunctionDef { kind, arg_start: arg_start as usize, locals: locals as usize, name_id, srcfile_id, argc: argc as usize, argsz, }); } assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (function_lump.offset + function_lump.count * FUNCTION_SIZE) as u64, ))? ); let statement_lump = &lumps[LumpId::Statements as usize]; src.seek(SeekFrom::Start(statement_lump.offset as u64))?; let mut statements = Vec::with_capacity(statement_lump.count); for _ in 0..statement_lump.count { statements.push(Statement::new( src.read_i16::()?, src.read_i16::()?, src.read_i16::()?, src.read_i16::()?, )?); } assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (statement_lump.offset + statement_lump.count * STATEMENT_SIZE) as u64, ))? ); let functions = Functions { string_table: string_table.clone(), defs: function_defs.into_boxed_slice(), statements: statements.into_boxed_slice(), }; let globaldef_lump = &lumps[LumpId::GlobalDefs as usize]; src.seek(SeekFrom::Start(globaldef_lump.offset as u64))?; let mut globaldefs = Vec::new(); for _ in 0..globaldef_lump.count { let type_ = src.read_u16::()?; let offset = src.read_u16::()?; let name_id = string_table .borrow() .id_from_i32(src.read_i32::()?)?; globaldefs.push(GlobalDef { save: type_ & SAVE_GLOBAL != 0, type_: Type::from_u16(type_ & !SAVE_GLOBAL).unwrap(), offset, name_id, }); } assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (globaldef_lump.offset + globaldef_lump.count * DEF_SIZE) as u64, ))? ); let fielddef_lump = &lumps[LumpId::Fielddefs as usize]; src.seek(SeekFrom::Start(fielddef_lump.offset as u64))?; let mut field_defs = Vec::new(); for _ in 0..fielddef_lump.count { let type_ = src.read_u16::()?; let offset = src.read_u16::()?; let name_id = string_table .borrow() .id_from_i32(src.read_i32::()?)?; if type_ & SAVE_GLOBAL != 0 { return Err(ProgsError::with_msg( "Save flag not allowed in field definitions", )); } field_defs.push(FieldDef { type_: Type::from_u16(type_).unwrap(), offset, name_id, }); } assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (fielddef_lump.offset + fielddef_lump.count * DEF_SIZE) as u64, ))? ); let globals_lump = &lumps[LumpId::Globals as usize]; src.seek(SeekFrom::Start(globals_lump.offset as u64))?; if globals_lump.count < GLOBAL_STATIC_COUNT { return Err(ProgsError::with_msg( "Global count lower than static global count", )); } let mut addrs = Vec::with_capacity(globals_lump.count); for _ in 0..globals_lump.count { let mut block = [0; 4]; src.read(&mut block)?; // TODO: handle endian conversions (BigEndian systems should use BigEndian internally) addrs.push(block); } assert_eq!( src.seek(SeekFrom::Current(0))?, src.seek(SeekFrom::Start( (globals_lump.offset + globals_lump.count * 4) as u64, ))? ); let functions_rc = Rc::new(functions); let cx = ExecutionContext::create(string_table.clone(), functions_rc.clone()); let globals = Globals::new( string_table.clone(), globaldefs.into_boxed_slice(), addrs.into_boxed_slice(), ); let entity_def = Rc::new(EntityTypeDef::new( string_table.clone(), ent_addr_count, field_defs.into_boxed_slice(), )?); Ok(LoadProgs { cx, globals, entity_def, string_table, }) } #[derive(Debug)] struct StackFrame { instr_id: usize, func_id: FunctionId, } /// A QuakeC VM context. #[derive(Debug)] pub struct ExecutionContext { string_table: Rc>, functions: Rc, pc: usize, current_function: FunctionId, call_stack: Vec, local_stack: Vec<[u8; 4]>, } impl ExecutionContext { pub fn create( string_table: Rc>, functions: Rc, ) -> ExecutionContext { ExecutionContext { string_table, functions, pc: 0, current_function: FunctionId(0), call_stack: Vec::with_capacity(MAX_CALL_STACK_DEPTH), local_stack: Vec::with_capacity(MAX_LOCAL_STACK_DEPTH), } } pub fn call_stack_depth(&self) -> usize { self.call_stack.len() } pub fn find_function_by_name>( &mut self, name: S, ) -> Result { self.functions.find_function_by_name(name) } pub fn function_def(&self, id: FunctionId) -> Result<&FunctionDef, ProgsError> { self.functions.get_def(id) } pub fn enter_function( &mut self, globals: &mut Globals, f: FunctionId, ) -> Result<(), ProgsError> { let def = self.functions.get_def(f)?; debug!( "Calling QuakeC function {}", self.string_table.borrow().get(def.name_id).unwrap() ); // save stack frame self.call_stack.push(StackFrame { instr_id: self.pc, func_id: self.current_function, }); // check call stack overflow if self.call_stack.len() >= MAX_CALL_STACK_DEPTH { return Err(ProgsError::CallStackOverflow); } // preemptively check local stack overflow if self.local_stack.len() + def.locals > MAX_LOCAL_STACK_DEPTH { return Err(ProgsError::LocalStackOverflow); } // save locals to stack for i in 0..def.locals { self.local_stack .push(globals.get_bytes((def.arg_start + i) as i16)?); } for arg in 0..def.argc { for component in 0..def.argsz[arg] as usize { let val = globals.get_bytes((GLOBAL_ADDR_ARG_0 + arg * 3 + component) as i16)?; globals.put_bytes(val, def.arg_start as i16)?; } } self.current_function = f; match def.kind { FunctionKind::BuiltIn(_) => { panic!("built-in functions should not be called with enter_function()") } FunctionKind::QuakeC(pc) => self.pc = pc, } Ok(()) } pub fn leave_function(&mut self, globals: &mut Globals) -> Result<(), ProgsError> { let def = self.functions.get_def(self.current_function)?; debug!( "Returning from QuakeC function {}", self.string_table.borrow().get(def.name_id).unwrap() ); for i in (0..def.locals).rev() { globals.put_bytes(self.local_stack.pop().unwrap(), (def.arg_start + i) as i16)?; } let frame = match self.call_stack.pop() { Some(f) => f, None => return Err(ProgsError::with_msg("call stack underflow")), }; self.current_function = frame.func_id; self.pc = frame.instr_id; Ok(()) } pub fn load_statement(&self) -> Statement { self.functions.statements[self.pc].clone() } /// Performs an unconditional relative jump. pub fn jump_relative(&mut self, rel: i16) { self.pc = (self.pc as isize + rel as isize) as usize; } } ================================================ FILE: src/server/progs/ops.rs ================================================ // Copyright © 2018 Cormac O'Brien. // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #[derive(Copy, Clone, Debug, FromPrimitive, PartialEq)] #[repr(i16)] pub enum Opcode { Done = 0, MulF = 1, MulV = 2, MulFV = 3, MulVF = 4, Div = 5, AddF = 6, AddV = 7, SubF = 8, SubV = 9, EqF = 10, EqV = 11, EqS = 12, EqEnt = 13, EqFnc = 14, NeF = 15, NeV = 16, NeS = 17, NeEnt = 18, NeFnc = 19, Le = 20, Ge = 21, Lt = 22, Gt = 23, LoadF = 24, LoadV = 25, LoadS = 26, LoadEnt = 27, LoadFld = 28, LoadFnc = 29, Address = 30, StoreF = 31, StoreV = 32, StoreS = 33, StoreEnt = 34, StoreFld = 35, StoreFnc = 36, StorePF = 37, StorePV = 38, StorePS = 39, StorePEnt = 40, StorePFld = 41, StorePFnc = 42, Return = 43, NotF = 44, NotV = 45, NotS = 46, NotEnt = 47, NotFnc = 48, If = 49, IfNot = 50, Call0 = 51, Call1 = 52, Call2 = 53, Call3 = 54, Call4 = 55, Call5 = 56, Call6 = 57, Call7 = 58, Call8 = 59, State = 60, Goto = 61, And = 62, Or = 63, BitAnd = 64, BitOr = 65, } ================================================ FILE: src/server/progs/string_table.rs ================================================ use std::{cell::RefCell, collections::HashMap}; use crate::server::progs::{ProgsError, StringId}; #[derive(Debug)] pub struct StringTable { /// Interned string data. data: String, /// Caches string lengths for faster lookup. lengths: RefCell>, } impl StringTable { pub fn new(data: Vec) -> StringTable { StringTable { data: String::from_utf8(data).unwrap(), lengths: RefCell::new(HashMap::new()), } } pub fn id_from_i32(&self, value: i32) -> Result { if value < 0 { return Err(ProgsError::with_msg("id < 0")); } let id = StringId(value as usize); if id.0 < self.data.len() { Ok(id) } else { Err(ProgsError::with_msg(format!("no string with ID {}", value))) } } pub fn find(&self, target: S) -> Option where S: AsRef, { let target = target.as_ref(); for (ofs, _) in target.char_indices() { let sub = &self.data[ofs..]; if !sub.starts_with(target) { continue; } // Make sure the string is NUL-terminated. Otherwise, this could // erroneously return the StringId of a String whose first // `target.len()` bytes were equal to `target`, but which had // additional bytes. if sub.as_bytes().get(target.len()) != Some(&0) { continue; } return Some(StringId(ofs)); } None } pub fn get(&self, id: StringId) -> Option<&str> { let start = id.0; if start >= self.data.len() { return None; } if let Some(len) = self.lengths.borrow().get(&id) { let end = start + len; return Some(&self.data[start..end]); } match (&self.data[start..]) .chars() .take(1024 * 1024) .enumerate() .find(|&(_i, c)| c == '\0') { Some((len, _)) => { self.lengths.borrow_mut().insert(id, len); let end = start + len; Some(&self.data[start..end]) } None => panic!("string data not NUL-terminated!"), } } pub fn insert(&mut self, s: S) -> StringId where S: AsRef, { let s = s.as_ref(); assert!(!s.contains('\0')); let id = StringId(self.data.len()); self.data.push_str(s); self.lengths.borrow_mut().insert(id, s.len()); id } pub fn find_or_insert(&mut self, target: S) -> StringId where S: AsRef, { match self.find(target.as_ref()) { Some(id) => id, None => self.insert(target), } } pub fn iter(&self) -> impl Iterator { self.data.split('\0') } } ================================================ FILE: src/server/world/entity.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. use std::{cell::RefCell, convert::TryInto, error::Error, fmt, rc::Rc}; use crate::{ common::{engine::duration_to_f32, net::EntityState}, server::{ progs::{EntityId, FieldDef, FunctionId, ProgsError, StringId, StringTable, Type}, world::phys::MoveKind, }, }; use arrayvec::ArrayString; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use cgmath::Vector3; use chrono::Duration; use num::FromPrimitive; use uluru::LRUCache; pub const MAX_ENT_LEAVES: usize = 16; pub const STATIC_ADDRESS_COUNT: usize = 105; #[derive(Debug)] pub enum EntityError { Io(::std::io::Error), Address(isize), Other(String), } impl EntityError { pub fn with_msg(msg: S) -> Self where S: AsRef, { EntityError::Other(msg.as_ref().to_owned()) } } impl fmt::Display for EntityError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { EntityError::Io(ref err) => { write!(f, "I/O error: ")?; err.fmt(f) } EntityError::Address(val) => write!(f, "Invalid address ({})", val), EntityError::Other(ref msg) => write!(f, "{}", msg), } } } impl Error for EntityError {} impl From<::std::io::Error> for EntityError { fn from(error: ::std::io::Error) -> Self { EntityError::Io(error) } } /// A trait which covers addresses of typed values. pub trait FieldAddr { /// The type of value referenced by this address. type Value; /// Loads the value at this address. fn load(&self, ent: &Entity) -> Result; /// Stores a value at this address. fn store(&self, ent: &mut Entity, value: Self::Value) -> Result<(), EntityError>; } #[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] pub enum FieldAddrFloat { ModelIndex = 0, AbsMinX = 1, AbsMinY = 2, AbsMinZ = 3, AbsMaxX = 4, AbsMaxY = 5, AbsMaxZ = 6, /// Used by mobile level geometry such as moving platforms. LocalTime = 7, /// Determines the movement behavior of an entity. The value must be a variant of `MoveKind`. MoveKind = 8, Solid = 9, OriginX = 10, OriginY = 11, OriginZ = 12, OldOriginX = 13, OldOriginY = 14, OldOriginZ = 15, VelocityX = 16, VelocityY = 17, VelocityZ = 18, AnglesX = 19, AnglesY = 20, AnglesZ = 21, AngularVelocityX = 22, AngularVelocityY = 23, AngularVelocityZ = 24, PunchAngleX = 25, PunchAngleY = 26, PunchAngleZ = 27, /// The index of the entity's animation frame. FrameId = 30, /// The index of the entity's skin. SkinId = 31, /// Effects flags applied to the entity. See `EntityEffects`. Effects = 32, /// Minimum extent in local coordinates, X-coordinate. MinsX = 33, /// Minimum extent in local coordinates, Y-coordinate. MinsY = 34, /// Minimum extent in local coordinates, Z-coordinate. MinsZ = 35, /// Maximum extent in local coordinates, X-coordinate. MaxsX = 36, /// Maximum extent in local coordinates, Y-coordinate. MaxsY = 37, /// Maximum extent in local coordinates, Z-coordinate. MaxsZ = 38, SizeX = 39, SizeY = 40, SizeZ = 41, /// The next server time at which the entity should run its think function. NextThink = 46, /// The entity's remaining health. Health = 48, /// The number of kills scored by the entity. Frags = 49, Weapon = 50, WeaponFrame = 52, /// The entity's remaining ammunition for its selected weapon. CurrentAmmo = 53, /// The entity's remaining shotgun shells. AmmoShells = 54, /// The entity's remaining shotgun shells. AmmoNails = 55, /// The entity's remaining rockets/grenades. AmmoRockets = 56, AmmoCells = 57, Items = 58, TakeDamage = 59, DeadFlag = 61, ViewOffsetX = 62, ViewOffsetY = 63, ViewOffsetZ = 64, Button0 = 65, Button1 = 66, Button2 = 67, Impulse = 68, FixAngle = 69, ViewAngleX = 70, ViewAngleY = 71, ViewAngleZ = 72, IdealPitch = 73, Flags = 76, Colormap = 77, Team = 78, MaxHealth = 79, TeleportTime = 80, ArmorStrength = 81, ArmorValue = 82, WaterLevel = 83, Contents = 84, IdealYaw = 85, YawSpeed = 86, SpawnFlags = 89, DmgTake = 92, DmgSave = 93, MoveDirectionX = 96, MoveDirectionY = 97, MoveDirectionZ = 98, Sounds = 100, } impl FieldAddr for FieldAddrFloat { type Value = f32; #[inline] fn load(&self, ent: &Entity) -> Result { ent.get_float(*self as i16) } #[inline] fn store(&self, ent: &mut Entity, value: Self::Value) -> Result<(), EntityError> { ent.put_float(value, *self as i16) } } #[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)] pub enum FieldAddrVector { AbsMin = 1, AbsMax = 4, Origin = 10, OldOrigin = 13, Velocity = 16, Angles = 19, AngularVelocity = 22, PunchAngle = 25, Mins = 33, Maxs = 36, Size = 39, ViewOffset = 62, ViewAngle = 70, MoveDirection = 96, } impl FieldAddr for FieldAddrVector { type Value = [f32; 3]; #[inline] fn load(&self, ent: &Entity) -> Result { ent.get_vector(*self as i16) } #[inline] fn store(&self, ent: &mut Entity, value: Self::Value) -> Result<(), EntityError> { ent.put_vector(value, *self as i16) } } #[derive(Copy, Clone, Debug, FromPrimitive)] pub enum FieldAddrStringId { ClassName = 28, ModelName = 29, WeaponModelName = 51, NetName = 74, Target = 90, TargetName = 91, Message = 99, Noise0Name = 101, Noise1Name = 102, Noise2Name = 103, Noise3Name = 104, } impl FieldAddr for FieldAddrStringId { type Value = StringId; fn load(&self, ent: &Entity) -> Result { ent.get_int(*self as i16) .map(|val| StringId(val.try_into().unwrap())) } fn store(&self, ent: &mut Entity, value: Self::Value) -> Result<(), EntityError> { ent.put_int(value.0.try_into().unwrap(), *self as i16) } } #[derive(Copy, Clone, Debug, FromPrimitive)] pub enum FieldAddrEntityId { /// The entity this entity is standing on. Ground = 47, Chain = 60, Enemy = 75, Aim = 87, Goal = 88, DmgInflictor = 94, Owner = 95, } impl FieldAddr for FieldAddrEntityId { type Value = EntityId; fn load(&self, ent: &Entity) -> Result { ent.entity_id(*self as i16) } fn store(&self, ent: &mut Entity, value: Self::Value) -> Result<(), EntityError> { ent.put_entity_id(value, *self as i16) } } #[derive(Copy, Clone, Debug, FromPrimitive)] pub enum FieldAddrFunctionId { Touch = 42, Use = 43, Think = 44, Blocked = 45, } impl FieldAddr for FieldAddrFunctionId { type Value = FunctionId; #[inline] fn load(&self, ent: &Entity) -> Result { ent.function_id(*self as i16) } #[inline] fn store(&self, ent: &mut Entity, value: Self::Value) -> Result<(), EntityError> { ent.put_function_id(value, *self as i16) } } bitflags! { pub struct EntityFlags: u16 { const FLY = 0b0000000000001; const SWIM = 0b0000000000010; const CONVEYOR = 0b0000000000100; const CLIENT = 0b0000000001000; const IN_WATER = 0b0000000010000; const MONSTER = 0b0000000100000; const GOD_MODE = 0b0000001000000; const NO_TARGET = 0b0000010000000; const ITEM = 0b0000100000000; const ON_GROUND = 0b0001000000000; const PARTIAL_GROUND = 0b0010000000000; const WATER_JUMP = 0b0100000000000; const JUMP_RELEASED = 0b1000000000000; } } // TODO: if this never gets used, remove it #[allow(dead_code)] fn float_addr(addr: usize) -> Result { match FieldAddrFloat::from_usize(addr) { Some(f) => Ok(f), None => Err(ProgsError::with_msg(format!( "float_addr: invalid address ({})", addr ))), } } // TODO: if this never gets used, remove it #[allow(dead_code)] fn vector_addr(addr: usize) -> Result { match FieldAddrVector::from_usize(addr) { Some(v) => Ok(v), None => Err(ProgsError::with_msg(format!( "vector_addr: invalid address ({})", addr ))), } } #[derive(Debug)] struct FieldDefCacheEntry { name: ArrayString<64>, index: usize, } #[derive(Debug)] pub struct EntityTypeDef { string_table: Rc>, addr_count: usize, field_defs: Box<[FieldDef]>, name_cache: RefCell>, } impl EntityTypeDef { pub fn new( string_table: Rc>, addr_count: usize, field_defs: Box<[FieldDef]>, ) -> Result { if addr_count < STATIC_ADDRESS_COUNT { return Err(EntityError::with_msg(format!( "addr_count ({}) < STATIC_ADDRESS_COUNT ({})", addr_count, STATIC_ADDRESS_COUNT ))); } Ok(EntityTypeDef { string_table, addr_count, field_defs, name_cache: RefCell::new(LRUCache::default()), }) } pub fn addr_count(&self) -> usize { self.addr_count } pub fn field_defs(&self) -> &[FieldDef] { self.field_defs.as_ref() } /// Locate a field definition given its name. pub fn find(&self, name: S) -> Option<&FieldDef> where S: AsRef, { let name = name.as_ref(); if let Some(cached) = self .name_cache .borrow_mut() .find(|entry| &entry.name == name) { return Some(&self.field_defs[cached.index]); } let name_id = self.string_table.borrow().find(name)?; let (index, def) = self .field_defs .iter() .enumerate() .find(|(_, def)| def.name_id == name_id)?; self.name_cache.borrow_mut().insert(FieldDefCacheEntry { name: ArrayString::from(name).unwrap(), index, }); Some(def) } } #[derive(Debug, FromPrimitive, PartialEq)] pub enum EntitySolid { Not = 0, Trigger = 1, BBox = 2, SlideBox = 3, Bsp = 4, } #[derive(Debug)] pub struct Entity { string_table: Rc>, type_def: Rc, addrs: Box<[[u8; 4]]>, pub leaf_count: usize, pub leaf_ids: [usize; MAX_ENT_LEAVES], pub baseline: EntityState, } impl Entity { pub fn new(string_table: Rc>, type_def: Rc) -> Entity { let mut addrs = Vec::with_capacity(type_def.addr_count); for _ in 0..type_def.addr_count { addrs.push([0; 4]); } Entity { string_table, type_def, addrs: addrs.into_boxed_slice(), leaf_count: 0, leaf_ids: [0; MAX_ENT_LEAVES], baseline: EntityState::uninitialized(), } } pub fn type_check(&self, addr: usize, type_: Type) -> Result<(), EntityError> { match self .type_def .field_defs .iter() .find(|def| def.type_ != Type::QVoid && def.offset as usize == addr) { Some(d) => { if type_ == d.type_ { return Ok(()); } else if type_ == Type::QFloat && d.type_ == Type::QVector { return Ok(()); } else if type_ == Type::QVector && d.type_ == Type::QFloat { return Ok(()); } else { return Err(EntityError::with_msg(format!( "type check failed: addr={} expected={:?} actual={:?}", addr, type_, d.type_ ))); } } None => return Ok(()), } } pub fn field_def(&self, name: S) -> Option<&FieldDef> where S: AsRef, { self.type_def.find(name) } /// Returns a reference to the memory at the given address. pub fn get_addr(&self, addr: i16) -> Result<&[u8], EntityError> { if addr < 0 { return Err(EntityError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(EntityError::Address(addr as isize)); } Ok(&self.addrs[addr]) } /// Returns a mutable reference to the memory at the given address. pub fn get_addr_mut(&mut self, addr: i16) -> Result<&mut [u8], EntityError> { if addr < 0 { return Err(EntityError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(EntityError::Address(addr as isize)); } Ok(&mut self.addrs[addr]) } /// Returns a copy of the memory at the given address. pub fn get_bytes(&self, addr: i16) -> Result<[u8; 4], EntityError> { if addr < 0 { return Err(EntityError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(EntityError::Address(addr as isize)); } Ok(self.addrs[addr]) } /// Writes the provided data to the memory at the given address. /// /// This can be used to circumvent the type checker in cases where an operation is not dependent /// of the type of the data. pub fn put_bytes(&mut self, val: [u8; 4], addr: i16) -> Result<(), EntityError> { if addr < 0 { return Err(EntityError::Address(addr as isize)); } let addr = addr as usize; if addr > self.addrs.len() { return Err(EntityError::Address(addr as isize)); } self.addrs[addr] = val; Ok(()) } #[inline] pub fn load(&self, field: F) -> Result where F: FieldAddr, { field.load(self) } #[inline] pub fn store(&mut self, field: F, value: F::Value) -> Result<(), EntityError> where F: FieldAddr, { field.store(self, value) } /// Loads an `i32` from the given virtual address. pub fn get_int(&self, addr: i16) -> Result { Ok(self.get_addr(addr)?.read_i32::()?) } /// Loads an `i32` from the given virtual address. pub fn put_int(&mut self, val: i32, addr: i16) -> Result<(), EntityError> { self.get_addr_mut(addr)?.write_i32::(val)?; Ok(()) } /// Loads an `f32` from the given virtual address. pub fn get_float(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QFloat)?; Ok(self.get_addr(addr)?.read_f32::()?) } /// Stores an `f32` at the given virtual address. pub fn put_float(&mut self, val: f32, addr: i16) -> Result<(), EntityError> { self.type_check(addr as usize, Type::QFloat)?; self.get_addr_mut(addr)?.write_f32::(val)?; Ok(()) } /// Loads an `[f32; 3]` from the given virtual address. pub fn get_vector(&self, addr: i16) -> Result<[f32; 3], EntityError> { self.type_check(addr as usize, Type::QVector)?; let mut v = [0.0; 3]; for i in 0..3 { v[i] = self.get_float(addr + i as i16)?; } Ok(v) } /// Stores an `[f32; 3]` at the given virtual address. pub fn put_vector(&mut self, val: [f32; 3], addr: i16) -> Result<(), EntityError> { self.type_check(addr as usize, Type::QVector)?; for i in 0..3 { self.put_float(val[i], addr + i as i16)?; } Ok(()) } /// Loads a `StringId` from the given virtual address. pub fn string_id(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QString)?; Ok(StringId( self.get_addr(addr)?.read_i32::()? as usize )) } /// Stores a `StringId` at the given virtual address. pub fn put_string_id(&mut self, val: StringId, addr: i16) -> Result<(), EntityError> { self.type_check(addr as usize, Type::QString)?; self.get_addr_mut(addr)? .write_i32::(val.try_into().unwrap())?; Ok(()) } /// Loads an `EntityId` from the given virtual address. pub fn entity_id(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QEntity)?; match self.get_addr(addr)?.read_i32::()? { e if e < 0 => Err(EntityError::with_msg(format!("Negative entity ID ({})", e))), e => Ok(EntityId(e as usize)), } } /// Stores an `EntityId` at the given virtual address. pub fn put_entity_id(&mut self, val: EntityId, addr: i16) -> Result<(), EntityError> { self.type_check(addr as usize, Type::QEntity)?; self.get_addr_mut(addr)? .write_i32::(val.0 as i32)?; Ok(()) } /// Loads a `FunctionId` from the given virtual address. pub fn function_id(&self, addr: i16) -> Result { self.type_check(addr as usize, Type::QFunction)?; Ok(FunctionId( self.get_addr(addr)?.read_i32::()? as usize )) } /// Stores a `FunctionId` at the given virtual address. pub fn put_function_id(&mut self, val: FunctionId, addr: i16) -> Result<(), EntityError> { self.type_check(addr as usize, Type::QFunction)?; self.get_addr_mut(addr)? .write_i32::(val.try_into().unwrap())?; Ok(()) } /// Set this entity's minimum and maximum bounds and calculate its size. pub fn set_min_max_size(&mut self, min: V, max: V) -> Result<(), EntityError> where V: Into>, { let min = min.into(); let max = max.into(); let size = max - min; debug!("Setting entity min: {:?}", min); self.put_vector(min.into(), FieldAddrVector::Mins as i16)?; debug!("Setting entity max: {:?}", max); self.put_vector(max.into(), FieldAddrVector::Maxs as i16)?; debug!("Setting entity size: {:?}", size); self.put_vector(size.into(), FieldAddrVector::Size as i16)?; Ok(()) } pub fn model_index(&self) -> Result { let model_index = self.get_float(FieldAddrFloat::ModelIndex as i16)?; if model_index < 0.0 || model_index > ::std::usize::MAX as f32 { Err(EntityError::with_msg(format!( "Invalid value for entity.model_index ({})", model_index, ))) } else { Ok(model_index as usize) } } pub fn abs_min(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::AbsMin as i16)?.into()) } pub fn abs_max(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::AbsMax as i16)?.into()) } pub fn solid(&self) -> Result { let solid_i = self.get_float(FieldAddrFloat::Solid as i16)? as i32; match EntitySolid::from_i32(solid_i) { Some(s) => Ok(s), None => Err(EntityError::with_msg(format!( "Invalid value for entity.solid ({})", solid_i, ))), } } pub fn origin(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::Origin as i16)?.into()) } pub fn min(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::Mins as i16)?.into()) } pub fn max(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::Maxs as i16)?.into()) } pub fn size(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::Size as i16)?.into()) } pub fn velocity(&self) -> Result, EntityError> { Ok(self.get_vector(FieldAddrVector::Velocity as i16)?.into()) } /// Applies gravity to the entity. /// /// The effect depends on the provided value of the `sv_gravity` cvar, the /// amount of time being simulated, and the entity's own `gravity` field /// value. pub fn apply_gravity( &mut self, sv_gravity: f32, frame_time: Duration, ) -> Result<(), EntityError> { let ent_gravity = match self.field_def("gravity") { Some(def) => self.get_float(def.offset as i16)?, None => 1.0, }; let mut vel = self.velocity()?; vel.z -= ent_gravity * sv_gravity * duration_to_f32(frame_time); self.store(FieldAddrVector::Velocity, vel.into())?; Ok(()) } /// Limits the entity's velocity by clamping each component (not the /// magnitude!) to an absolute value of `sv_maxvelocity`. pub fn limit_velocity(&mut self, sv_maxvelocity: f32) -> Result<(), EntityError> { let mut vel = self.velocity()?; for c in &mut vel[..] { *c = c.clamp(-sv_maxvelocity, sv_maxvelocity); } self.put_vector(vel.into(), FieldAddrVector::Velocity as i16)?; Ok(()) } pub fn move_kind(&self) -> Result { let move_kind_f = self.get_float(FieldAddrFloat::MoveKind as i16)?; let move_kind_i = move_kind_f as i32; match MoveKind::from_i32(move_kind_i) { Some(m) => Ok(m), None => Err(EntityError::with_msg(format!( "Invalid value for entity.move_kind ({})", move_kind_f, ))), } } pub fn flags(&self) -> Result { let flags_i = self.get_float(FieldAddrFloat::Flags as i16)? as u16; match EntityFlags::from_bits(flags_i) { Some(f) => Ok(f), None => Err(EntityError::with_msg(format!( "Invalid internal flags value ({})", flags_i ))), } } pub fn add_flags(&mut self, flags: EntityFlags) -> Result<(), EntityError> { let result = self.flags()? | flags; self.put_float(result.bits() as f32, FieldAddrFloat::Flags as i16)?; Ok(()) } pub fn owner(&self) -> Result { Ok(self.entity_id(FieldAddrEntityId::Owner as i16)?) } } ================================================ FILE: src/server/world/mod.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mod entity; pub mod phys; use std::{ cell::RefCell, collections::{HashMap, HashSet}, rc::Rc, }; use self::{ entity::Entity, phys::{Collide, CollideKind}, }; pub use self::{ entity::{ EntityError, EntityFlags, EntitySolid, EntityTypeDef, FieldAddrEntityId, FieldAddrFloat, FieldAddrFunctionId, FieldAddrStringId, FieldAddrVector, }, phys::{MoveKind, Trace, TraceEnd, TraceEndKind, TraceStart}, }; use crate::{ common::{ bsp, bsp::{BspCollisionHull, BspLeafContents}, mdl, model::{Model, ModelKind}, parse, sprite, vfs::Vfs, }, server::progs::{ EntityFieldAddr, EntityId, FieldAddr, FieldDef, FunctionId, ProgsError, StringId, StringTable, Type, }, }; use arrayvec::ArrayVec; use cgmath::{InnerSpace, Vector3, Zero}; const AREA_DEPTH: usize = 4; const NUM_AREA_NODES: usize = 2usize.pow(AREA_DEPTH as u32 + 1) - 1; const MAX_ENTITIES: usize = 600; #[derive(Debug)] enum AreaNodeKind { Branch(AreaBranch), Leaf, } #[derive(Debug)] struct AreaNode { kind: AreaNodeKind, triggers: HashSet, solids: HashSet, } // The areas form a quadtree-like BSP tree which alternates splitting on the X // and Y axes. // // 00 X // 01 02 Y // 03 04 05 06 X // 07 08 09 10 11 12 13 14 Y // 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Leaves // // [21] [19] [17] [15] // || || || || // || || || || // 12===05===11 08===03===07 // || || || || || || // || || || || || || // [22] || [20] [18] || [16] // || || // 02========00========01 // || || // [29] || [27] [25] || [23] // || || || || || || // || || || || || || // 14===06===13 10===04===09 // || || || || // || || || || // [30] [28] [26] [24] // // The tree won't necessarily look like this, this just assumes a rectangular area with width // between 1-2x its length. impl AreaNode { /// Generate a breadth-first 2-D binary space partitioning tree with the given extents. pub fn generate(mins: Vector3, maxs: Vector3) -> ArrayVec { let mut nodes: ArrayVec = ArrayVec::new(); // we generate the skeleton of the tree iteratively -- the nodes are linked but have no // geometric data. // place internal nodes for i in 0..AREA_DEPTH { for _ in 0..2usize.pow(i as u32) { let len = nodes.len(); nodes.push(AreaNode { kind: AreaNodeKind::Branch(AreaBranch { axis: AreaBranchAxis::X, dist: 0.0, front: 2 * len + 1, back: 2 * len + 2, }), triggers: HashSet::new(), solids: HashSet::new(), }); } } // place leaves for _ in 0..2usize.pow(AREA_DEPTH as u32) { nodes.push(AreaNode { kind: AreaNodeKind::Leaf, triggers: HashSet::new(), solids: HashSet::new(), }); } // recursively assign geometric data to the nodes AreaNode::setup(&mut nodes, 0, mins, maxs); nodes } fn setup( nodes: &mut ArrayVec, index: usize, mins: Vector3, maxs: Vector3, ) { let size = maxs - mins; let axis; if size.x > size.y { axis = AreaBranchAxis::X; } else { axis = AreaBranchAxis::Y; } let dist = 0.5 * (maxs[axis as usize] + mins[axis as usize]); let mut front_mins = mins; front_mins[axis as usize] = dist; let mut back_maxs = maxs; back_maxs[axis as usize] = dist; let front; let back; match nodes[index].kind { AreaNodeKind::Branch(ref mut b) => { b.axis = axis; b.dist = dist; front = b.front; back = b.back; } AreaNodeKind::Leaf => return, } AreaNode::setup(nodes, front, front_mins, maxs); AreaNode::setup(nodes, back, mins, back_maxs); } } #[derive(Copy, Clone, Debug)] enum AreaBranchAxis { X = 0, Y = 1, } #[derive(Debug)] struct AreaBranch { axis: AreaBranchAxis, dist: f32, front: usize, back: usize, } #[derive(Debug)] struct AreaEntity { entity: Entity, area_id: Option, } #[derive(Debug)] enum AreaEntitySlot { Vacant, Occupied(AreaEntity), } /// A representation of the current state of the game world. #[derive(Debug)] pub struct World { string_table: Rc>, type_def: Rc, area_nodes: ArrayVec, slots: Box<[AreaEntitySlot]>, models: Vec, } impl World { pub fn create( mut brush_models: Vec, type_def: Rc, string_table: Rc>, ) -> Result { // generate area tree for world model let area_nodes = AreaNode::generate(brush_models[0].min(), brush_models[0].max()); let mut models = Vec::with_capacity(brush_models.len() + 1); // put null model at index 0 models.push(Model::none()); // take ownership of all brush models models.append(&mut brush_models); // generate world entity let mut world_entity = Entity::new(string_table.clone(), type_def.clone()); world_entity.put_string_id( string_table.borrow_mut().find_or_insert(models[1].name()), FieldAddrStringId::ModelName as i16, )?; world_entity.put_float(1.0, FieldAddrFloat::ModelIndex as i16)?; world_entity.put_float(EntitySolid::Bsp as u32 as f32, FieldAddrFloat::Solid as i16)?; world_entity.put_float( MoveKind::Push as u32 as f32, FieldAddrFloat::MoveKind as i16, )?; let mut slots = Vec::with_capacity(MAX_ENTITIES); slots.push(AreaEntitySlot::Occupied(AreaEntity { entity: world_entity, area_id: None, })); for _ in 0..MAX_ENTITIES - 1 { slots.push(AreaEntitySlot::Vacant); } Ok(World { string_table, area_nodes, type_def, slots: slots.into_boxed_slice(), models, }) } pub fn add_model(&mut self, vfs: &Vfs, name_id: StringId) -> Result<(), ProgsError> { let strs = self.string_table.borrow(); let name = strs.get(name_id).unwrap(); if name.ends_with(".bsp") { let data = vfs.open(name).unwrap(); let (mut brush_models, _) = bsp::load(data).unwrap(); if brush_models.len() > 1 { return Err(ProgsError::with_msg( "Complex brush models must be loaded before world creation", )); } self.models.append(&mut brush_models); } else if name.ends_with(".mdl") { let data = vfs.open(&name).unwrap(); let alias_model = mdl::load(data).unwrap(); self.models .push(Model::from_alias_model(&name, alias_model)); } else if name.ends_with(".spr") { let data = vfs.open(&name).unwrap(); let sprite_model = sprite::load(data); self.models .push(Model::from_sprite_model(&name, sprite_model)); } else { return Err(ProgsError::with_msg(format!( "Unrecognized model type: {}", name ))); } Ok(()) } fn find_def(&self, name: S) -> Result<&FieldDef, ProgsError> where S: AsRef, { let name = name.as_ref(); match self .type_def .field_defs() .iter() .find(|def| self.string_table.borrow().get(def.name_id).unwrap() == name) { Some(d) => Ok(d), None => Err(ProgsError::with_msg(format!("no field with name {}", name))), } } /// Convert an entity ID and field address to an internal representation used by the VM. /// /// This representation should be compatible with the one used by the original Quake. pub fn ent_fld_addr_to_i32(&self, ent_fld_addr: EntityFieldAddr) -> i32 { let total_addr = (ent_fld_addr.entity_id.0 * self.type_def.addr_count() + ent_fld_addr.field_addr.0) * 4; if total_addr > ::std::i32::MAX as usize { panic!("ent_fld_addr_to_i32: total_addr overflow"); } total_addr as i32 } /// Convert the internal representation of a field offset back to struct form. pub fn ent_fld_addr_from_i32(&self, val: i32) -> EntityFieldAddr { if val < 0 { panic!("ent_fld_addr_from_i32: negative value ({})", val); } if val % 4 != 0 { panic!("ent_fld_addr_from_i32: value % 4 != 0 ({})", val); } let total_addr = val as usize / 4; EntityFieldAddr { entity_id: EntityId(total_addr / self.type_def.addr_count()), field_addr: FieldAddr(total_addr % self.type_def.addr_count()), } } fn find_vacant_slot(&self) -> Result { for (i, slot) in self.slots.iter().enumerate() { if let &AreaEntitySlot::Vacant = slot { return Ok(i); } } panic!("no vacant slots"); } pub fn alloc_uninitialized(&mut self) -> Result { let slot_id = self.find_vacant_slot().unwrap(); self.slots[slot_id] = AreaEntitySlot::Occupied(AreaEntity { entity: Entity::new(self.string_table.clone(), self.type_def.clone()), area_id: None, }); Ok(EntityId(slot_id)) } /// Allocate a new entity and initialize it with the data in the given map. /// /// For each entry in `map`, this will locate a field definition for the entry key, parse the /// entry value to the correct type, and store it at that field. It will then locate the spawn /// method for the entity's `classname` and execute it. /// /// ## Special cases /// /// There are two cases where the keys do not directly correspond to entity fields: /// /// - `angle`: This allows QuakeEd to write a single value instead of a set of Euler angles. /// The value should be interpreted as the second component of the `angles` field. /// - `light`: This is simply an alias for `light_lev`. pub fn alloc_from_map(&mut self, map: HashMap<&str, &str>) -> Result { let mut ent = Entity::new(self.string_table.clone(), self.type_def.clone()); for (key, val) in map.iter() { debug!(".{} = {}", key, val); match *key { // ignore keys starting with an underscore k if k.starts_with("_") => (), "angle" => { // this is referred to in the original source as "anglehack" -- essentially, // only the yaw (Y) value is given. see // https://github.com/id-Software/Quake/blob/master/WinQuake/pr_edict.c#L826-L834 let def = self.find_def("angles")?.clone(); ent.put_vector([0.0, val.parse().unwrap(), 0.0], def.offset as i16)?; } "light" => { // more fun hacks brought to you by Carmack & Friends let def = self.find_def("light_lev")?.clone(); ent.put_float(val.parse().unwrap(), def.offset as i16)?; } k => { let def = self.find_def(k)?.clone(); match def.type_ { // void has no value, skip it Type::QVoid => (), // TODO: figure out if this ever happens Type::QPointer => unimplemented!(), Type::QString => { let s_id = self.string_table.borrow_mut().insert(val); ent.put_string_id(s_id, def.offset as i16)?; } Type::QFloat => ent.put_float(val.parse().unwrap(), def.offset as i16)?, Type::QVector => ent.put_vector( parse::vector3_components(val).unwrap(), def.offset as i16, )?, Type::QEntity => { let id: usize = val.parse().unwrap(); if id > MAX_ENTITIES { panic!("out-of-bounds entity access"); } match self.slots[id] { AreaEntitySlot::Vacant => panic!("no entity with id {}", id), AreaEntitySlot::Occupied(_) => (), } ent.put_entity_id(EntityId(id), def.offset as i16)? } Type::QField => panic!("attempted to store field of type Field in entity"), Type::QFunction => { // TODO: need to validate this against function table } } } } } let entry_id = self.find_vacant_slot().unwrap(); self.slots[entry_id] = AreaEntitySlot::Occupied(AreaEntity { entity: ent, area_id: None, }); Ok(EntityId(entry_id)) } pub fn free(&mut self, entity_id: EntityId) -> Result<(), ProgsError> { // TODO: unlink entity from world if entity_id.0 as usize > self.slots.len() { return Err(ProgsError::with_msg(format!( "Invalid entity ID ({:?})", entity_id ))); } if let AreaEntitySlot::Vacant = self.slots[entity_id.0 as usize] { return Ok(()); } self.slots[entity_id.0 as usize] = AreaEntitySlot::Vacant; Ok(()) } /// Returns a reference to an entity. /// /// # Panics /// /// This method panics if `entity_id` does not refer to a valid slot or if /// the slot is vacant. #[inline] pub fn entity(&self, entity_id: EntityId) -> &Entity { match self.slots[entity_id.0 as usize] { AreaEntitySlot::Vacant => panic!("no such entity: {:?}", entity_id), AreaEntitySlot::Occupied(ref e) => &e.entity, } } pub fn try_entity(&self, entity_id: EntityId) -> Result<&Entity, ProgsError> { if entity_id.0 as usize > self.slots.len() { return Err(ProgsError::with_msg(format!( "Invalid entity ID ({})", entity_id.0 as usize ))); } match self.slots[entity_id.0 as usize] { AreaEntitySlot::Vacant => Err(ProgsError::with_msg(format!( "No entity at list entry {}", entity_id.0 as usize ))), AreaEntitySlot::Occupied(ref e) => Ok(&e.entity), } } pub fn entity_mut(&mut self, entity_id: EntityId) -> Result<&mut Entity, ProgsError> { if entity_id.0 as usize > self.slots.len() { return Err(ProgsError::with_msg(format!( "Invalid entity ID ({})", entity_id.0 as usize ))); } match self.slots[entity_id.0 as usize] { AreaEntitySlot::Vacant => Err(ProgsError::with_msg(format!( "No entity at list entry {}", entity_id.0 as usize ))), AreaEntitySlot::Occupied(ref mut e) => Ok(&mut e.entity), } } pub fn entity_exists(&mut self, entity_id: EntityId) -> bool { matches!( self.slots[entity_id.0 as usize], AreaEntitySlot::Occupied(_) ) } pub fn list_entities(&self, list: &mut Vec) { for (id, slot) in self.slots.iter().enumerate() { if let &AreaEntitySlot::Occupied(_) = slot { list.push(EntityId(id)); } } } fn area_entity(&self, entity_id: EntityId) -> Result<&AreaEntity, ProgsError> { if entity_id.0 as usize > self.slots.len() { return Err(ProgsError::with_msg(format!( "Invalid entity ID ({})", entity_id.0 as usize ))); } match self.slots[entity_id.0 as usize] { AreaEntitySlot::Vacant => Err(ProgsError::with_msg(format!( "No entity at list entry {}", entity_id.0 as usize ))), AreaEntitySlot::Occupied(ref e) => Ok(e), } } fn area_entity_mut(&mut self, entity_id: EntityId) -> Result<&mut AreaEntity, ProgsError> { if entity_id.0 as usize > self.slots.len() { return Err(ProgsError::with_msg(format!( "Invalid entity ID ({})", entity_id.0 as usize ))); } match self.slots[entity_id.0 as usize] { AreaEntitySlot::Vacant => Err(ProgsError::with_msg(format!( "No entity at list entry {}", entity_id.0 as usize ))), AreaEntitySlot::Occupied(ref mut e) => Ok(e), } } /// Lists the triggers touched by an entity. /// /// The triggers' IDs are stored in `touched`. pub fn list_touched_triggers( &mut self, touched: &mut Vec, ent_id: EntityId, area_id: usize, ) -> Result<(), ProgsError> { 'next_trigger: for trigger_id in self.area_nodes[area_id].triggers.iter().copied() { if trigger_id == ent_id { // Don't trigger self. continue; } let ent = self.entity(ent_id); let trigger = self.entity(trigger_id); let trigger_touch = trigger.load(FieldAddrFunctionId::Touch)?; if trigger_touch == FunctionId(0) || trigger.solid()? == EntitySolid::Not { continue; } for i in 0..3 { if ent.abs_min()?[i] > trigger.abs_max()?[i] || ent.abs_max()?[i] < trigger.abs_min()?[i] { // Entities are not touching. continue 'next_trigger; } } touched.push(trigger_id); } // Touch all triggers in sub-areas. if let AreaNodeKind::Branch(AreaBranch { front, back, .. }) = self.area_nodes[area_id].kind { self.list_touched_triggers(touched, ent_id, front)?; self.list_touched_triggers(touched, ent_id, back)?; } Ok(()) } pub fn unlink_entity(&mut self, e_id: EntityId) -> Result<(), ProgsError> { // if this entity has been removed or freed, do nothing if let AreaEntitySlot::Vacant = self.slots[e_id.0 as usize] { return Ok(()); } let area_id = match self.area_entity(e_id)?.area_id { Some(i) => i, // entity not linked None => return Ok(()), }; if self.area_nodes[area_id].triggers.remove(&e_id) { debug!("Unlinking entity {} from area triggers", e_id.0); } else if self.area_nodes[area_id].solids.remove(&e_id) { debug!("Unlinking entity {} from area solids", e_id.0); } self.area_entity_mut(e_id)?.area_id = None; Ok(()) } pub fn link_entity(&mut self, e_id: EntityId) -> Result<(), ProgsError> { // don't link the world entity if e_id.0 == 0 { return Ok(()); } // if this entity has been removed or freed, do nothing if let AreaEntitySlot::Vacant = self.slots[e_id.0 as usize] { return Ok(()); } self.unlink_entity(e_id)?; let mut abs_min; let mut abs_max; let solid; { let ent = self.entity_mut(e_id)?; let origin = Vector3::from(ent.get_vector(FieldAddrVector::Origin as i16)?); let mins = Vector3::from(ent.get_vector(FieldAddrVector::Mins as i16)?); let maxs = Vector3::from(ent.get_vector(FieldAddrVector::Maxs as i16)?); debug!("origin = {:?} mins = {:?} maxs = {:?}", origin, mins, maxs); abs_min = origin + mins; abs_max = origin + maxs; let flags_f = ent.get_float(FieldAddrFloat::Flags as i16)?; let flags = EntityFlags::from_bits(flags_f as u16).unwrap(); if flags.contains(EntityFlags::ITEM) { abs_min.x -= 15.0; abs_min.y -= 15.0; abs_max.x += 15.0; abs_max.y += 15.0; } else { abs_min.x -= 1.0; abs_min.y -= 1.0; abs_min.z -= 1.0; abs_max.x += 1.0; abs_max.y += 1.0; abs_max.z += 1.0; } ent.put_vector(abs_min.into(), FieldAddrVector::AbsMin as i16)?; ent.put_vector(abs_max.into(), FieldAddrVector::AbsMax as i16)?; // Mark leaves containing entity for PVS. ent.leaf_count = 0; let model_index = ent.get_float(FieldAddrFloat::ModelIndex as i16)?; if model_index != 0.0 { // TODO: SV_FindTouchedLeafs todo!("SV_FindTouchedLeafs"); } solid = ent.solid()?; if solid == EntitySolid::Not { // this entity has no touch interaction, we're done return Ok(()); } } let mut node_id = 0; loop { match self.area_nodes[node_id].kind { AreaNodeKind::Branch(ref b) => { debug!( "abs_min = {:?} | abs_max = {:?} | dist = {}", abs_min, abs_max, b.dist ); if abs_min[b.axis as usize] > b.dist { node_id = b.front; } else if abs_max[b.axis as usize] < b.dist { node_id = b.back; } else { // entity spans both sides of the plane break; } } AreaNodeKind::Leaf => break, } } if solid == EntitySolid::Trigger { debug!("Linking entity {} into area {} triggers", e_id.0, node_id); self.area_nodes[node_id].triggers.insert(e_id); self.area_entity_mut(e_id)?.area_id = Some(node_id); } else { debug!("Linking entity {} into area {} solids", e_id.0, node_id); self.area_nodes[node_id].solids.insert(e_id); self.area_entity_mut(e_id)?.area_id = Some(node_id); } Ok(()) } pub fn set_entity_model(&mut self, e_id: EntityId, model_id: usize) -> Result<(), ProgsError> { if model_id == 0 { self.set_entity_size(e_id, Vector3::zero(), Vector3::zero())?; } else { let min = self.models[model_id].min(); let max = self.models[model_id].max(); self.set_entity_size(e_id, min, max)?; } Ok(()) } pub fn set_entity_size( &mut self, e_id: EntityId, min: Vector3, max: Vector3, ) -> Result<(), ProgsError> { let ent = self.entity_mut(e_id)?; ent.set_min_max_size(min, max)?; Ok(()) } /// Unlink an entity from the world and remove it. pub fn remove_entity(&mut self, e_id: EntityId) -> Result<(), ProgsError> { self.unlink_entity(e_id)?; self.free(e_id)?; Ok(()) } // TODO: handle the offset return value internally pub fn hull_for_entity( &self, e_id: EntityId, min: Vector3, max: Vector3, ) -> Result<(BspCollisionHull, Vector3), ProgsError> { let solid = self.entity(e_id).solid()?; debug!("Entity solid type: {:?}", solid); match solid { EntitySolid::Bsp => { if self.entity(e_id).move_kind()? != MoveKind::Push { return Err(ProgsError::with_msg(format!( "Brush entities must have MoveKind::Push (has {:?})", self.entity(e_id).move_kind() ))); } let size = max - min; match self.models[self.entity(e_id).model_index()?].kind() { &ModelKind::Brush(ref bmodel) => { let hull_index; // TODO: replace these magic constants if size[0] < 3.0 { debug!("Using hull 0"); hull_index = 0; } else if size[0] <= 32.0 { debug!("Using hull 1"); hull_index = 1; } else { debug!("Using hull 2"); hull_index = 2; } let hull = bmodel.hull(hull_index).unwrap(); let offset = hull.min() - min + self.entity(e_id).origin()?; Ok((hull, offset)) } _ => Err(ProgsError::with_msg(format!( "Non-brush entities may not have MoveKind::Push" ))), } } _ => { let hull = BspCollisionHull::for_bounds( self.entity(e_id).min()?, self.entity(e_id).max()?, ) .unwrap(); let offset = self.entity(e_id).origin()?; Ok((hull, offset)) } } } // TODO: come up with a better name. This doesn't actually move the entity! pub fn move_entity( &mut self, e_id: EntityId, start: Vector3, min: Vector3, max: Vector3, end: Vector3, kind: CollideKind, ) -> Result<(Trace, Option), ProgsError> { debug!( "start={:?} min={:?} max={:?} end={:?}", start, min, max, end ); debug!("Collision test: Entity {} with world entity", e_id.0); let world_trace = self.collide_move_with_entity(EntityId(0), start, min, max, end)?; debug!( "End position after collision test with world hull: {:?}", world_trace.end_point() ); // if this is a rocket or a grenade, expand the monster collision box let (monster_min, monster_max) = match kind { CollideKind::Missile => ( min - Vector3::new(15.0, 15.0, 15.0), max + Vector3::new(15.0, 15.0, 15.0), ), _ => (min, max), }; let (move_min, move_max) = self::phys::bounds_for_move(start, monster_min, monster_max, end); let collide = Collide { e_id: Some(e_id), move_min, move_max, min, max, monster_min, monster_max, start, end, kind, }; let (collide_trace, collide_ent) = self.collide(&collide)?; if collide_trace.all_solid() || collide_trace.start_solid() || collide_trace.ratio() < world_trace.ratio() { Ok((collide_trace, collide_ent)) } else { Ok((world_trace, Some(EntityId(0)))) } } pub fn collide(&self, collide: &Collide) -> Result<(Trace, Option), ProgsError> { self.collide_area(0, collide) } fn collide_area( &self, area_id: usize, collide: &Collide, ) -> Result<(Trace, Option), ProgsError> { let mut trace = Trace::new( TraceStart::new(Vector3::zero(), 0.0), TraceEnd::terminal(Vector3::zero()), BspLeafContents::Empty, ); let mut collide_entity = None; let area = &self.area_nodes[area_id]; for touch in area.solids.iter() { // don't collide an entity with itself if let Some(e) = collide.e_id { if e == *touch { continue; } } match self.entity(*touch).solid()? { // if the other entity has no collision, skip it EntitySolid::Not => continue, // triggers should not appear in the solids list EntitySolid::Trigger => { return Err(ProgsError::with_msg(format!( "Trigger in solids list with ID ({})", touch.0 ))) } // don't collide with monsters if the collide specifies not to do so s => { if s != EntitySolid::Bsp && collide.kind == CollideKind::NoMonsters { continue; } } } // if bounding boxes never intersect, skip this entity for i in 0..3 { if collide.move_min[i] > self.entity(*touch).abs_max()?[i] || collide.move_max[i] < self.entity(*touch).abs_min()?[i] { continue; } } if let Some(e) = collide.e_id { if self.entity(e).size()?[0] != 0.0 && self.entity(*touch).size()?[0] == 0.0 { continue; } } if trace.all_solid() { return Ok((trace, collide_entity)); } if let Some(e) = collide.e_id { // don't collide against owner or owned entities if self.entity(*touch).owner()? == e || self.entity(e).owner()? == *touch { continue; } } // select bounding boxes based on whether or not candidate is a monster let tmp_trace; if self.entity(*touch).flags()?.contains(EntityFlags::MONSTER) { tmp_trace = self.collide_move_with_entity( *touch, collide.start, collide.monster_min, collide.monster_max, collide.end, )?; } else { tmp_trace = self.collide_move_with_entity( *touch, collide.start, collide.min, collide.max, collide.end, )?; } let old_dist = (trace.end_point() - collide.start).magnitude(); let new_dist = (tmp_trace.end_point() - collide.start).magnitude(); // check to see if this candidate is the closest yet and update trace if so if tmp_trace.all_solid() || tmp_trace.start_solid() || new_dist < old_dist { collide_entity = Some(*touch); trace = tmp_trace; } } match area.kind { AreaNodeKind::Leaf => (), AreaNodeKind::Branch(ref b) => { if collide.move_max[b.axis as usize] > b.dist { self.collide_area(b.front, collide)?; } if collide.move_min[b.axis as usize] < b.dist { self.collide_area(b.back, collide)?; } } } Ok((trace, collide_entity)) } pub fn collide_move_with_entity( &self, e_id: EntityId, start: Vector3, min: Vector3, max: Vector3, end: Vector3, ) -> Result { let (hull, offset) = self.hull_for_entity(e_id, min, max)?; debug!("hull offset: {:?}", offset); debug!( "hull contents at start: {:?}", hull.contents_at_point(start).unwrap() ); Ok(hull .trace(start - offset, end - offset) .unwrap() .adjust(offset)) } } ================================================ FILE: src/server/world/phys.rs ================================================ // Copyright © 2018 Cormac O'Brien // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software // and associated documentation files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all copies or // substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. //! Physics and collision detection. use crate::{ common::{bsp::BspLeafContents, math::Hyperplane}, server::progs::EntityId, }; use bitflags::bitflags; use cgmath::{InnerSpace, Vector3, Zero}; /// Velocity in units/second under which a *component* (not the entire /// velocity!) is instantly reduced to zero. /// /// This prevents objects from sliding indefinitely at low velocity. const STOP_THRESHOLD: f32 = 0.1; #[derive(Copy, Clone, Debug, Eq, FromPrimitive, PartialEq)] pub enum MoveKind { /// Does not move. None = 0, AngleNoClip = 1, AngleClip = 2, /// Player-controlled. Walk = 3, /// Moves in discrete steps (monsters). Step = 4, Fly = 5, Toss = 6, Push = 7, NoClip = 8, FlyMissile = 9, Bounce = 10, } #[derive(Copy, Clone, Debug, Eq, FromPrimitive, PartialEq)] pub enum CollideKind { Normal = 0, NoMonsters = 1, Missile = 2, } #[derive(Debug)] pub struct Collide { /// The ID of the entity being moved. pub e_id: Option, /// The minimum extent of the entire move. pub move_min: Vector3, /// The maximum extent of the entire move. pub move_max: Vector3, /// The minimum extent of the moving object. pub min: Vector3, /// The maximum extent of the moving object. pub max: Vector3, /// The minimum extent of the moving object when colliding with a monster. pub monster_min: Vector3, /// The maximum extent of the moving object when colliding with a monster. pub monster_max: Vector3, /// The start point of the move. pub start: Vector3, /// The end point of the move. pub end: Vector3, /// How this move collides with other entities. pub kind: CollideKind, } /// Calculates a new velocity after collision with a surface. /// /// `overbounce` approximates the elasticity of the collision. A value of `1` /// reduces the component of `initial` antiparallel to `surface_normal` to zero, /// while a value of `2` reflects that component to be parallel to /// `surface_normal`. pub fn velocity_after_collision( initial: Vector3, surface_normal: Vector3, overbounce: f32, ) -> (Vector3, CollisionFlags) { let mut flags = CollisionFlags::empty(); if surface_normal.z > 0.0 { flags |= CollisionFlags::HORIZONTAL; } else if surface_normal.z == 0.0 { flags |= CollisionFlags::VERTICAL; } let change = (overbounce * initial.dot(surface_normal)) * surface_normal; let mut out = initial - change; for i in 0..3 { if out[i].abs() < STOP_THRESHOLD { out[i] = 0.0; } } (out, flags) } /// Calculates a new velocity after collision with multiple surfaces. pub fn velocity_after_multi_collision( initial: Vector3, planes: &[Hyperplane], overbounce: f32, ) -> Option> { // Try to find a plane which produces a post-collision velocity that will // not cause a subsequent collision with any of the other planes. for (a, plane_a) in planes.iter().enumerate() { let (velocity_a, _flags) = velocity_after_collision(initial, plane_a.normal(), overbounce); for (b, plane_b) in planes.iter().enumerate() { if a == b { // Don't test a plane against itself. continue; } if velocity_a.dot(plane_b.normal()) < 0.0 { // New velocity would be directed into another plane. break; } } // This velocity is not expected to cause immediate collisions with // other planes, so return it. return Some(velocity_a); } if planes.len() > 2 { // Quake simply gives up in this case. This is distinct from returning // the zero vector, as it indicates that the trajectory has really // wedged something in a corner. None } else { // Redirect velocity along the intersection of the planes. let dir = planes[0].normal().cross(planes[1].normal()); let scale = initial.dot(dir); Some(scale * dir) } } /// Represents the start of a collision trace. #[derive(Clone, Debug)] pub struct TraceStart { point: Vector3, /// The ratio along the original trace length at which this (sub)trace /// begins. ratio: f32, } impl TraceStart { pub fn new(point: Vector3, ratio: f32) -> TraceStart { TraceStart { point, ratio } } } /// Represents the end of a trace which crossed between leaves. #[derive(Clone, Debug)] pub struct TraceEndBoundary { pub ratio: f32, pub plane: Hyperplane, } /// Indicates the the nature of the end of a trace. #[derive(Clone, Debug)] pub enum TraceEndKind { /// This endpoint falls within a leaf. Terminal, /// This endpoint falls on a leaf boundary (a plane). Boundary(TraceEndBoundary), } /// Represents the end of a trace. #[derive(Clone, Debug)] pub struct TraceEnd { point: Vector3, kind: TraceEndKind, } impl TraceEnd { pub fn terminal(point: Vector3) -> TraceEnd { TraceEnd { point, kind: TraceEndKind::Terminal, } } pub fn boundary(point: Vector3, ratio: f32, plane: Hyperplane) -> TraceEnd { TraceEnd { point, kind: TraceEndKind::Boundary(TraceEndBoundary { ratio, plane }), } } pub fn kind(&self) -> &TraceEndKind { &self.kind } } #[derive(Clone, Debug)] pub struct Trace { start: TraceStart, end: TraceEnd, contents: BspLeafContents, start_solid: bool, } impl Trace { pub fn new(start: TraceStart, end: TraceEnd, contents: BspLeafContents) -> Trace { let start_solid = contents == BspLeafContents::Solid; Trace { start, end, contents, start_solid, } } /// Join this trace end-to-end with another. /// /// - If `self.end_point()` does not equal `other.start_point()`, returns `self`. /// - If `self.contents` equals `other.contents`, the traces are combined (e.g. the new trace /// starts with `self.start` and ends with `other.end`). /// - If `self.contents` is `Solid` but `other.contents` is not, the trace is allowed to move /// out of the solid area. The `startsolid` flag should be set accordingly. /// - Otherwise, `self` is returned, representing a collision or transition between leaf types. /// /// ## Panics /// - If `self.end.kind` is `Terminal`. /// - If `self.end.point` does not equal `other.start.point`. pub fn join(self, other: Trace) -> Trace { debug!( "start1={:?} end1={:?} start2={:?} end2={:?}", self.start.point, self.end.point, other.start.point, other.end.point ); // don't allow chaining after terminal // TODO: impose this constraint with the type system if let TraceEndKind::Terminal = self.end.kind { panic!("Attempted to join after terminal trace"); } // don't allow joining disjoint traces if self.end.point != other.start.point { panic!("Attempted to join disjoint traces"); } // combine traces with the same contents if self.contents == other.contents { return Trace { start: self.start, end: other.end, contents: self.contents, start_solid: self.start_solid, }; } if self.contents == BspLeafContents::Solid && other.contents != BspLeafContents::Solid { return Trace { start: self.start, end: other.end, contents: other.contents, start_solid: true, }; } self } /// Adjusts the start and end points of the trace by an offset. pub fn adjust(self, offset: Vector3) -> Trace { Trace { start: TraceStart { point: self.start.point + offset, ratio: self.start.ratio, }, end: TraceEnd { point: self.end.point + offset, kind: self.end.kind, }, contents: self.contents, start_solid: self.start_solid, } } /// Returns the point at which the trace began. pub fn start_point(&self) -> Vector3 { self.start.point } /// Returns the end of this trace. pub fn end(&self) -> &TraceEnd { &self.end } /// Returns the point at which the trace ended. pub fn end_point(&self) -> Vector3 { self.end.point } /// Returns true if the entire trace is within solid leaves. pub fn all_solid(&self) -> bool { self.contents == BspLeafContents::Solid } /// Returns true if the trace began in a solid leaf but ended outside it. pub fn start_solid(&self) -> bool { self.start_solid } pub fn in_open(&self) -> bool { self.contents == BspLeafContents::Empty } pub fn in_water(&self) -> bool { self.contents != BspLeafContents::Empty && self.contents != BspLeafContents::Solid } /// Returns whether the trace ended without a collision. pub fn is_terminal(&self) -> bool { if let TraceEndKind::Terminal = self.end.kind { true } else { false } } /// Returns the ratio of travelled distance to intended distance. /// /// This indicates how far along the original trajectory the trace proceeded /// before colliding with a different medium. pub fn ratio(&self) -> f32 { match &self.end.kind { TraceEndKind::Terminal => 1.0, TraceEndKind::Boundary(boundary) => boundary.ratio, } } } bitflags! { pub struct CollisionFlags: u32 { const HORIZONTAL = 1; const VERTICAL = 2; const STOPPED = 4; } } pub fn bounds_for_move( start: Vector3, min: Vector3, max: Vector3, end: Vector3, ) -> (Vector3, Vector3) { let mut box_min = Vector3::zero(); let mut box_max = Vector3::zero(); for i in 0..3 { if end[i] > start[i] { box_min[i] = start[i] + min[i] - 1.0; box_max[i] = end[i] + max[i] + 1.0; } else { box_min[i] = end[i] + min[i] - 1.0; box_max[i] = start[i] + max[i] + 1.0; } } (box_min, box_max) }