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

richter

18 | 23 |
24 | 25 | 26 |
27 |

richter - an open-source Quake engine

28 |

about

29 |

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

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

status

43 |

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

45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /site/sass/_base.scss: -------------------------------------------------------------------------------- 1 | @import "reset"; 2 | 3 | @font-face { 4 | font-family: "Renner"; 5 | src: local('Renner*'), local('Renner-Book'), 6 | url("fonts/Renner-Book.woff2") format('woff2'), 7 | url("fonts/Renner-Book.woff") format('woff'); 8 | } 9 | 10 | @font-face { 11 | font-family: "Roboto"; 12 | font-style: normal; 13 | font-weight: 400; 14 | src: local("Roboto"), local("Roboto-Regular"), 15 | url("fonts/roboto-v18-latin-regular.woff2") format("woff2"), 16 | url("fonts/roboto-v18-latin-regular.woff") format("woff"); 17 | } 18 | 19 | @font-face { 20 | font-family: "DejaVu Sans Mono"; 21 | font-style: normal; 22 | src: local("DejaVu Sans Mono"), 23 | url("fonts/DejaVuSansMono.woff2") format("woff2"), 24 | url("fonts/DejaVuSansMono.woff") format("woff"); 25 | } 26 | 27 | $display-font-stack: Renner, Futura, Arial, sans-serif; 28 | $text-font-stack: Roboto, Arial, sans-serif; 29 | $code-font-stack: "DejaVu Sans Mono", "Consolas", monospace; 30 | 31 | $bg-color: #1F1F1F; 32 | $bg-hl-color: #0F0F0F; 33 | $text-color: #ABABAB; 34 | $link-color: #EFEFEF; 35 | 36 | $main-content-width: 40rem; 37 | 38 | html { 39 | height: 100%; 40 | } 41 | 42 | body { 43 | font: 100% $text-font-stack; 44 | color: $text-color; 45 | background-color: $bg-color; 46 | 47 | display: grid; 48 | grid-template-areas: 49 | "header" 50 | "main" 51 | "footer"; 52 | grid-template-rows: 0px 1fr auto; 53 | 54 | margin: 100px auto; 55 | max-width: $main-content-width; 56 | justify-items: center; 57 | 58 | min-height: 100%; 59 | } 60 | 61 | p { 62 | margin: 1rem auto; 63 | line-height: 1.5; 64 | text-align: justify; 65 | } 66 | 67 | code { 68 | font: 100% $code-font-stack; 69 | border: 1px solid $text-color; 70 | border-radius: 4px; 71 | padding: 0 0.25rem 0; 72 | margin: 0 0.25rem 0; 73 | } 74 | 75 | a { 76 | color: $link-color; 77 | text-decoration: none; 78 | } 79 | 80 | hr { 81 | margin: 1rem auto; 82 | border: 0; 83 | border-top: 1px solid $text-color; 84 | border-bottom: 1px solid $bg-hl-color; 85 | } 86 | 87 | img { 88 | max-width: 100%; 89 | } 90 | 91 | footer { 92 | font: 0.75rem $text-font-stack; 93 | text-align: center; 94 | 95 | p { 96 | text-align: center; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /site/sass/_reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /site/sass/blog-post.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | body { 4 | margin: 0 auto; 5 | display: grid; 6 | 7 | max-width: $main-content-width; 8 | 9 | .date { 10 | font-family: $text-font-stack; 11 | } 12 | 13 | .title { 14 | font-size: 2rem; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /site/sass/blog.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | body { 4 | // title of the page 5 | h1 { 6 | margin: 1rem auto; 7 | text-align: center; 8 | font: 3rem $display-font-stack; 9 | } 10 | 11 | // subtitle of the page 12 | h2 { 13 | margin: 1rem auto; 14 | text-align: center; 15 | font: 1.5rem $display-font-stack; 16 | } 17 | 18 | // post name 19 | h3 { 20 | font-weight: bold; 21 | font-family: $text-font-stack; 22 | } 23 | 24 | // post date 25 | h4 { 26 | font-style: italic; 27 | font-family: $text-font-stack; 28 | } 29 | 30 | .post { 31 | margin-bottom: 2rem; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /site/sass/style.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | $body-margin-top: 150px; 4 | 5 | body { 6 | margin: $body-margin-top auto; 7 | 8 | padding: $body-margin-top / 2 0; 9 | 10 | background-image: url(richter-insignia.svg); 11 | background-repeat: no-repeat; 12 | background-position: 50% $body-margin-top; 13 | background-size: 400px; 14 | 15 | max-width: $main-content-width; 16 | justify-items: center; 17 | 18 | h1 { 19 | $heading-font-size: 6rem; 20 | 21 | margin: ($heading-font-size / 3) auto ($heading-font-size / 3); 22 | 23 | font-family: $display-font-stack; 24 | font-size: $heading-font-size; 25 | text-align: center; 26 | letter-spacing: 1rem; 27 | } 28 | 29 | h2 { 30 | $subheading-font-size: 3rem; 31 | 32 | margin: ($subheading-font-size / 3) auto ($subheading-font-size / 3); 33 | 34 | font-family: $display-font-stack; 35 | font-size: $subheading-font-size; 36 | text-align: center; 37 | } 38 | 39 | .links { 40 | margin: 40px 0; 41 | 42 | font-family: $text-font-stack; 43 | font-size: 1.5rem; 44 | text-align: center; 45 | 46 | li { 47 | display: inline; 48 | list-style: none; 49 | 50 | // put dots between items 51 | &:not(:first-child):before { 52 | content: " · "; 53 | } 54 | } 55 | } 56 | 57 | .intro { 58 | margin-top: 40px; 59 | 60 | font-family: $text-font-stack; 61 | font-size: 1rem; 62 | text-align: justify; 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /site/static/fonts/DejaVuSansMono.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cormac-obrien/richter/506504d5f9f93dab807e61ba3cad1a27d6d5a707/site/static/fonts/DejaVuSansMono.woff -------------------------------------------------------------------------------- /site/static/fonts/DejaVuSansMono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cormac-obrien/richter/506504d5f9f93dab807e61ba3cad1a27d6d5a707/site/static/fonts/DejaVuSansMono.woff2 -------------------------------------------------------------------------------- /site/static/fonts/Renner-Book.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cormac-obrien/richter/506504d5f9f93dab807e61ba3cad1a27d6d5a707/site/static/fonts/Renner-Book.woff -------------------------------------------------------------------------------- /site/static/fonts/Renner-Book.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cormac-obrien/richter/506504d5f9f93dab807e61ba3cad1a27d6d5a707/site/static/fonts/Renner-Book.woff2 -------------------------------------------------------------------------------- /site/static/fonts/roboto-v18-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cormac-obrien/richter/506504d5f9f93dab807e61ba3cad1a27d6d5a707/site/static/fonts/roboto-v18-latin-regular.woff -------------------------------------------------------------------------------- /site/static/fonts/roboto-v18-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cormac-obrien/richter/506504d5f9f93dab807e61ba3cad1a27d6d5a707/site/static/fonts/roboto-v18-latin-regular.woff2 -------------------------------------------------------------------------------- /site/static/richter-insignia.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /site/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}{% endblock title %} 4 | 5 | 6 | -------------------------------------------------------------------------------- /site/themes/richter/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% block head %} 5 | 6 | 7 | 8 | {% block style %} 9 | 10 | {% endblock style %} 11 | {% block title %}{% endblock title %} – Richter 12 | {% endblock head %} 13 | 14 | 15 |
{% block header %}{% endblock header %}
16 |
{% block main %}{% endblock main %}
17 | 18 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /site/themes/richter/templates/blog-post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block style %} 3 | 4 | {% endblock style %} 5 | {% block title %}{{ page.title }}{% endblock title%} 6 | 7 | {% block main %} 8 |

{{ page.date }}

9 |

{{ page.title }}

10 | {{ page.content | safe }} 11 | {% endblock main %} 12 | -------------------------------------------------------------------------------- /site/themes/richter/templates/blog.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block style %} 4 | 5 | {% endblock style %} 6 | {% block title %}{{ section.title }}{% endblock title %} 7 | {% block main %} 8 |

{{ section.title }}

9 | {{ section.content | safe }} 10 | {% for page in section.pages %} 11 |
12 |

{{ page.title }}

13 |

{{ page.date }}

14 | {{ page.content | safe }} 15 |
16 | {% endfor %} 17 | {% endblock main %} 18 | -------------------------------------------------------------------------------- /site/themes/richter/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Home{% endblock title %} 4 | {% block style %} 5 | 6 | {% endblock style %} 7 | {% block main %}{{ section.content | safe }}{% endblock main %} 8 | -------------------------------------------------------------------------------- /site/themes/richter/theme.toml: -------------------------------------------------------------------------------- 1 | name = "richter" 2 | description = "theme for the Richter webpage" 3 | license = "MIT" 4 | min_version = "0.2.2" 5 | 6 | [author] 7 | name = "Mac O'Brien" 8 | homepage = "http://c-obrien.org" -------------------------------------------------------------------------------- /specifications.md: -------------------------------------------------------------------------------- 1 | # Specifications for the Original Quake (idTech 2) Engine 2 | 3 | ### Coordinate Systems 4 | 5 | Quake's coordinate system specifies its axes as follows: 6 | 7 | - The x-axis specifies depth. 8 | - The y-axis specifies width. 9 | - The z-axis specifies height. 10 | 11 | This contrasts with the OpenGL coordinate system, in which: 12 | 13 | - The x-axis specifies width. 14 | - The y-axis specifies height. 15 | - The z-axis specifies depth (inverted). 16 | 17 | Thus, to convert between the coordinate systems: 18 | 19 | x <-> -z 20 | Quake y <-> x OpenGL 21 | z <-> y 22 | 23 | x <-> y 24 | OpenGL y <-> z Quake 25 | z <-> -x 26 | -------------------------------------------------------------------------------- /src/bin/quake-client/capture.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::RefCell, 3 | fs::File, 4 | io::BufWriter, 5 | num::NonZeroU32, 6 | path::{Path, PathBuf}, 7 | rc::Rc, 8 | }; 9 | 10 | use richter::client::render::Extent2d; 11 | 12 | use chrono::Utc; 13 | 14 | const BYTES_PER_PIXEL: u32 = 4; 15 | 16 | /// Implements the "screenshot" command. 17 | /// 18 | /// This function returns a boxed closure which sets the `screenshot_path` 19 | /// argument to `Some` when called. 20 | pub fn cmd_screenshot( 21 | screenshot_path: Rc>>, 22 | ) -> Box String> { 23 | Box::new(move |args| { 24 | let path = match args.len() { 25 | // TODO: make default path configurable 26 | 0 => PathBuf::from(format!("richter-{}.png", Utc::now().format("%FT%H-%M-%S"))), 27 | 1 => PathBuf::from(args[0]), 28 | _ => { 29 | log::error!("Usage: screenshot [PATH]"); 30 | return "Usage: screenshot [PATH]".to_owned(); 31 | } 32 | }; 33 | 34 | screenshot_path.replace(Some(path)); 35 | String::new() 36 | }) 37 | } 38 | 39 | pub struct Capture { 40 | // size of the capture image 41 | capture_size: Extent2d, 42 | 43 | // width of a row in the buffer, must be a multiple of 256 for mapped reads 44 | row_width: u32, 45 | 46 | // mappable buffer 47 | buffer: wgpu::Buffer, 48 | } 49 | 50 | impl Capture { 51 | pub fn new(device: &wgpu::Device, capture_size: Extent2d) -> Capture { 52 | // bytes_per_row must be a multiple of 256 53 | // 4 bytes per pixel, so width must be multiple of 64 54 | let row_width = (capture_size.width + 63) / 64 * 64; 55 | 56 | let buffer = device.create_buffer(&wgpu::BufferDescriptor { 57 | label: Some("capture buffer"), 58 | size: (row_width * capture_size.height * BYTES_PER_PIXEL) as u64, 59 | usage: wgpu::BufferUsage::COPY_DST | wgpu::BufferUsage::MAP_READ, 60 | mapped_at_creation: false, 61 | }); 62 | 63 | Capture { 64 | capture_size, 65 | row_width, 66 | buffer, 67 | } 68 | } 69 | 70 | pub fn copy_from_texture( 71 | &self, 72 | encoder: &mut wgpu::CommandEncoder, 73 | texture: wgpu::ImageCopyTexture, 74 | ) { 75 | encoder.copy_texture_to_buffer( 76 | texture, 77 | wgpu::ImageCopyBuffer { 78 | buffer: &self.buffer, 79 | layout: wgpu::ImageDataLayout { 80 | offset: 0, 81 | bytes_per_row: Some(NonZeroU32::new(self.row_width * BYTES_PER_PIXEL).unwrap()), 82 | rows_per_image: None, 83 | }, 84 | }, 85 | self.capture_size.into(), 86 | ); 87 | } 88 | 89 | pub fn write_to_file

(&self, device: &wgpu::Device, path: P) 90 | where 91 | P: AsRef, 92 | { 93 | let mut data = Vec::new(); 94 | { 95 | // map the buffer 96 | // TODO: maybe make this async so we don't force the whole program to block 97 | let slice = self.buffer.slice(..); 98 | let map_future = slice.map_async(wgpu::MapMode::Read); 99 | device.poll(wgpu::Maintain::Wait); 100 | futures::executor::block_on(map_future).unwrap(); 101 | 102 | // copy pixel data 103 | let mapped = slice.get_mapped_range(); 104 | for row in mapped.chunks(self.row_width as usize * BYTES_PER_PIXEL as usize) { 105 | // don't copy padding 106 | for pixel in 107 | (&row[..self.capture_size.width as usize * BYTES_PER_PIXEL as usize]).chunks(4) 108 | { 109 | // swap BGRA->RGBA 110 | data.extend_from_slice(&[pixel[2], pixel[1], pixel[0], pixel[3]]); 111 | } 112 | } 113 | } 114 | self.buffer.unmap(); 115 | 116 | let f = File::create(path).unwrap(); 117 | let mut png_encoder = png::Encoder::new( 118 | BufWriter::new(f), 119 | self.capture_size.width, 120 | self.capture_size.height, 121 | ); 122 | png_encoder.set_color(png::ColorType::RGBA); 123 | png_encoder.set_depth(png::BitDepth::Eight); 124 | let mut writer = png_encoder.write_header().unwrap(); 125 | writer.write_image_data(&data).unwrap(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/bin/quake-client/game.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use std::{cell::RefCell, path::PathBuf, rc::Rc}; 22 | 23 | use crate::{ 24 | capture::{cmd_screenshot, Capture}, 25 | trace::{cmd_trace_begin, cmd_trace_end}, 26 | }; 27 | 28 | use richter::{ 29 | client::{ 30 | input::Input, 31 | menu::Menu, 32 | render::{ 33 | Extent2d, GraphicsState, RenderTarget as _, RenderTargetResolve as _, SwapChainTarget, 34 | }, 35 | trace::TraceFrame, 36 | Client, ClientError, 37 | }, 38 | common::console::{CmdRegistry, Console, CvarRegistry}, 39 | }; 40 | 41 | use chrono::Duration; 42 | use failure::Error; 43 | use log::info; 44 | 45 | pub struct Game { 46 | cvars: Rc>, 47 | cmds: Rc>, 48 | input: Rc>, 49 | pub client: Client, 50 | 51 | // if Some(v), trace is in progress 52 | trace: Rc>>>, 53 | 54 | // if Some(path), take a screenshot and save it to path 55 | screenshot_path: Rc>>, 56 | } 57 | 58 | impl Game { 59 | pub fn new( 60 | cvars: Rc>, 61 | cmds: Rc>, 62 | input: Rc>, 63 | client: Client, 64 | ) -> Result { 65 | // set up input commands 66 | input.borrow().register_cmds(&mut cmds.borrow_mut()); 67 | 68 | // set up screenshots 69 | let screenshot_path = Rc::new(RefCell::new(None)); 70 | cmds.borrow_mut() 71 | .insert("screenshot", cmd_screenshot(screenshot_path.clone())) 72 | .unwrap(); 73 | 74 | // set up frame tracing 75 | let trace = Rc::new(RefCell::new(None)); 76 | cmds.borrow_mut() 77 | .insert("trace_begin", cmd_trace_begin(trace.clone())) 78 | .unwrap(); 79 | cmds.borrow_mut() 80 | .insert("trace_end", cmd_trace_end(cvars.clone(), trace.clone())) 81 | .unwrap(); 82 | 83 | Ok(Game { 84 | cvars, 85 | cmds, 86 | input, 87 | client, 88 | trace, 89 | screenshot_path, 90 | }) 91 | } 92 | 93 | // advance the simulation 94 | pub fn frame(&mut self, gfx_state: &GraphicsState, frame_duration: Duration) { 95 | use ClientError::*; 96 | 97 | match self.client.frame(frame_duration, gfx_state) { 98 | Ok(()) => (), 99 | Err(e) => match e { 100 | Cvar(_) 101 | | UnrecognizedProtocol(_) 102 | | NoSuchClient(_) 103 | | NoSuchPlayer(_) 104 | | NoSuchEntity(_) 105 | | NullEntity 106 | | EntityExists(_) 107 | | InvalidViewEntity(_) 108 | | TooManyStaticEntities 109 | | NoSuchLightmapAnimation(_) 110 | | Model(_) 111 | | Network(_) 112 | | Sound(_) 113 | | Vfs(_) => { 114 | log::error!("{}", e); 115 | self.client.disconnect(); 116 | } 117 | 118 | _ => panic!("{}", e), 119 | }, 120 | }; 121 | 122 | if let Some(ref mut game_input) = self.input.borrow_mut().game_input_mut() { 123 | self.client 124 | .handle_input(game_input, frame_duration) 125 | .unwrap(); 126 | } 127 | 128 | // if there's an active trace, record this frame 129 | if let Some(ref mut trace_frames) = *self.trace.borrow_mut() { 130 | trace_frames.push( 131 | self.client 132 | .trace(&[self.client.view_entity_id().unwrap()]) 133 | .unwrap(), 134 | ); 135 | } 136 | } 137 | 138 | pub fn render( 139 | &mut self, 140 | gfx_state: &GraphicsState, 141 | color_attachment_view: &wgpu::TextureView, 142 | width: u32, 143 | height: u32, 144 | console: &Console, 145 | menu: &Menu, 146 | ) { 147 | info!("Beginning render pass"); 148 | let mut encoder = gfx_state 149 | .device() 150 | .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); 151 | 152 | // render world, hud, console, menus 153 | self.client 154 | .render( 155 | gfx_state, 156 | &mut encoder, 157 | width, 158 | height, 159 | menu, 160 | self.input.borrow().focus(), 161 | ) 162 | .unwrap(); 163 | 164 | // screenshot setup 165 | let capture = self.screenshot_path.borrow().as_ref().map(|_| { 166 | let cap = Capture::new(gfx_state.device(), Extent2d { width, height }); 167 | cap.copy_from_texture( 168 | &mut encoder, 169 | wgpu::ImageCopyTexture { 170 | texture: gfx_state.final_pass_target().resolve_attachment(), 171 | mip_level: 0, 172 | origin: wgpu::Origin3d::ZERO, 173 | }, 174 | ); 175 | cap 176 | }); 177 | 178 | // blit to swap chain 179 | { 180 | let swap_chain_target = SwapChainTarget::with_swap_chain_view(color_attachment_view); 181 | let blit_pass_builder = swap_chain_target.render_pass_builder(); 182 | let mut blit_pass = encoder.begin_render_pass(&blit_pass_builder.descriptor()); 183 | gfx_state.blit_pipeline().blit(gfx_state, &mut blit_pass); 184 | } 185 | 186 | let command_buffer = encoder.finish(); 187 | { 188 | gfx_state.queue().submit(vec![command_buffer]); 189 | gfx_state.device().poll(wgpu::Maintain::Wait); 190 | } 191 | 192 | // write screenshot if requested and clear screenshot path 193 | self.screenshot_path.replace(None).map(|path| { 194 | capture 195 | .as_ref() 196 | .unwrap() 197 | .write_to_file(gfx_state.device(), path) 198 | }); 199 | } 200 | } 201 | 202 | impl std::ops::Drop for Game { 203 | fn drop(&mut self) { 204 | let _ = self.cmds.borrow_mut().remove("trace_begin"); 205 | let _ = self.cmds.borrow_mut().remove("trace_end"); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/bin/quake-client/menu.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use richter::client::menu::{Menu, MenuBodyView, MenuBuilder, MenuView}; 22 | 23 | use failure::Error; 24 | 25 | pub fn build_main_menu() -> Result { 26 | Ok(MenuBuilder::new() 27 | .add_submenu("Single Player", build_menu_sp()?) 28 | .add_submenu("Multiplayer", build_menu_mp()?) 29 | .add_submenu("Options", build_menu_options()?) 30 | .add_action("Help/Ordering", Box::new(|| ())) 31 | .add_action("Quit", Box::new(|| ())) 32 | .build(MenuView { 33 | draw_plaque: true, 34 | title_path: "gfx/ttl_main.lmp".to_string(), 35 | body: MenuBodyView::Predefined { 36 | path: "gfx/mainmenu.lmp".to_string(), 37 | }, 38 | })) 39 | } 40 | 41 | fn build_menu_sp() -> Result { 42 | Ok(MenuBuilder::new() 43 | .add_action("New Game", Box::new(|| ())) 44 | // .add_submenu("Load", unimplemented!()) 45 | // .add_submenu("Save", unimplemented!()) 46 | .build(MenuView { 47 | draw_plaque: true, 48 | title_path: "gfx/ttl_sgl.lmp".to_string(), 49 | body: MenuBodyView::Predefined { 50 | path: "gfx/sp_menu.lmp".to_string(), 51 | }, 52 | })) 53 | } 54 | 55 | fn build_menu_mp() -> Result { 56 | Ok(MenuBuilder::new() 57 | .add_submenu("Join a Game", build_menu_mp_join()?) 58 | // .add_submenu("New Game", unimplemented!()) 59 | // .add_submenu("Setup", unimplemented!()) 60 | .build(MenuView { 61 | draw_plaque: true, 62 | title_path: "gfx/p_multi.lmp".to_string(), 63 | body: MenuBodyView::Predefined { 64 | path: "gfx/mp_menu.lmp".to_string(), 65 | }, 66 | })) 67 | } 68 | 69 | fn build_menu_mp_join() -> Result { 70 | Ok(MenuBuilder::new() 71 | .add_submenu("TCP", build_menu_mp_join_tcp()?) 72 | // .add_textbox // description 73 | .build(MenuView { 74 | draw_plaque: true, 75 | title_path: "gfx/p_multi.lmp".to_string(), 76 | body: MenuBodyView::Predefined { 77 | path: "gfx/mp_menu.lmp".to_string(), 78 | }, 79 | })) 80 | } 81 | 82 | fn build_menu_mp_join_tcp() -> Result { 83 | // Join Game - TCP/IP // title 84 | // 85 | // Address: 127.0.0.1 // label 86 | // 87 | // Port [26000] // text field 88 | // 89 | // Search for local games... // menu 90 | // 91 | // Join game at: // label 92 | // [ ] // text field 93 | Ok(MenuBuilder::new() 94 | // .add 95 | .add_toggle("placeholder", false, Box::new(|_| ())) 96 | .build(MenuView { 97 | draw_plaque: true, 98 | title_path: "gfx/p_multi.lmp".to_string(), 99 | body: MenuBodyView::Dynamic, 100 | })) 101 | } 102 | 103 | fn build_menu_options() -> Result { 104 | Ok(MenuBuilder::new() 105 | // .add_submenu("Customize controls", unimplemented!()) 106 | .add_action("Go to console", Box::new(|| ())) 107 | .add_action("Reset to defaults", Box::new(|| ())) 108 | .add_slider("Render scale", 0.25, 1.0, 2, 0, Box::new(|_| ()))? 109 | .add_slider("Screen Size", 0.0, 1.0, 10, 9, Box::new(|_| ()))? 110 | .add_slider("Brightness", 0.0, 1.0, 10, 9, Box::new(|_| ()))? 111 | .add_slider("Mouse Speed", 0.0, 1.0, 10, 9, Box::new(|_| ()))? 112 | .add_slider("CD music volume", 0.0, 1.0, 10, 9, Box::new(|_| ()))? 113 | .add_slider("Sound volume", 0.0, 1.0, 10, 9, Box::new(|_| ()))? 114 | .add_toggle("Always run", true, Box::new(|_| ())) 115 | .add_toggle("Invert mouse", false, Box::new(|_| ())) 116 | .add_toggle("Lookspring", false, Box::new(|_| ())) 117 | .add_toggle("Lookstrafe", false, Box::new(|_| ())) 118 | // .add_submenu("Video options", unimplemented!()) 119 | .build(MenuView { 120 | draw_plaque: true, 121 | title_path: "gfx/p_option.lmp".to_string(), 122 | body: MenuBodyView::Dynamic, 123 | })) 124 | } 125 | -------------------------------------------------------------------------------- /src/bin/quake-client/trace.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, io::BufWriter, rc::Rc, fs::File}; 2 | 3 | use richter::{client::trace::TraceFrame, common::console::CvarRegistry}; 4 | 5 | const DEFAULT_TRACE_PATH: &'static str = "richter-trace.json"; 6 | 7 | /// Implements the `trace_begin` command. 8 | pub fn cmd_trace_begin(trace: Rc>>>) -> Box String> { 9 | Box::new(move |_| { 10 | if trace.borrow().is_some() { 11 | log::error!("trace already in progress"); 12 | "trace already in progress".to_owned() 13 | } else { 14 | // start a new trace 15 | trace.replace(Some(Vec::new())); 16 | String::new() 17 | } 18 | }) 19 | } 20 | 21 | /// Implements the `trace_end` command. 22 | pub fn cmd_trace_end( 23 | cvars: Rc>, 24 | trace: Rc>>>, 25 | ) -> Box String> { 26 | Box::new(move |_| { 27 | if let Some(trace_frames) = trace.replace(None) { 28 | let trace_path = cvars 29 | .borrow() 30 | .get("trace_path") 31 | .unwrap_or(DEFAULT_TRACE_PATH.to_string()); 32 | let trace_file = match File::create(&trace_path) { 33 | Ok(f) => f, 34 | Err(e) => { 35 | log::error!("Couldn't open trace file for write: {}", e); 36 | return format!("Couldn't open trace file for write: {}", e); 37 | } 38 | }; 39 | 40 | let mut writer = BufWriter::new(trace_file); 41 | 42 | match serde_json::to_writer(&mut writer, &trace_frames) { 43 | Ok(()) => (), 44 | Err(e) => { 45 | log::error!("Couldn't serialize trace: {}", e); 46 | return format!("Couldn't serialize trace: {}", e); 47 | } 48 | }; 49 | 50 | log::debug!("wrote {} frames to {}", trace_frames.len(), &trace_path); 51 | format!("wrote {} frames to {}", trace_frames.len(), &trace_path) 52 | } else { 53 | log::error!("no trace in progress"); 54 | "no trace in progress".to_owned() 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/bin/unpak.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | extern crate richter; 19 | 20 | use std::{ 21 | fs, 22 | fs::File, 23 | io::{BufWriter, Write}, 24 | path::PathBuf, 25 | process::exit, 26 | }; 27 | 28 | use richter::common::pak::Pak; 29 | 30 | use structopt::StructOpt; 31 | 32 | #[derive(Debug, StructOpt)] 33 | struct Opt { 34 | #[structopt(short, long)] 35 | verbose: bool, 36 | 37 | #[structopt(long)] 38 | version: bool, 39 | 40 | #[structopt(name = "INPUT_PAK", parse(from_os_str))] 41 | input_pak: PathBuf, 42 | 43 | #[structopt(name = "OUTPUT_DIR", parse(from_os_str))] 44 | output_dir: Option, 45 | } 46 | 47 | const VERSION: &'static str = " 48 | unpak 0.1 49 | Copyright © 2020 Cormac O'Brien 50 | Released under the terms of the MIT License 51 | "; 52 | 53 | fn main() { 54 | let opt = Opt::from_args(); 55 | 56 | if opt.version { 57 | println!("{}", VERSION); 58 | exit(0); 59 | } 60 | 61 | let pak = match Pak::new(&opt.input_pak) { 62 | Ok(p) => p, 63 | Err(why) => { 64 | println!("Couldn't open {:#?}: {}", &opt.input_pak, why); 65 | exit(1); 66 | } 67 | }; 68 | 69 | for (k, v) in pak.iter() { 70 | let mut path = PathBuf::new(); 71 | 72 | if let Some(ref d) = opt.output_dir { 73 | path.push(d); 74 | } 75 | 76 | path.push(k); 77 | 78 | if let Some(p) = path.parent() { 79 | if !p.exists() { 80 | if let Err(why) = fs::create_dir_all(p) { 81 | println!("Couldn't create parent directories: {}", why); 82 | exit(1); 83 | } 84 | } 85 | } 86 | 87 | let file = match File::create(&path) { 88 | Ok(f) => f, 89 | Err(why) => { 90 | println!("Couldn't open {}: {}", path.to_str().unwrap(), why); 91 | exit(1); 92 | } 93 | }; 94 | 95 | let mut writer = BufWriter::new(file); 96 | match writer.write_all(v.as_ref()) { 97 | Ok(_) => (), 98 | Err(why) => { 99 | println!("Couldn't write to {}: {}", path.to_str().unwrap(), why); 100 | exit(1); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/client/cvars.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use crate::common::console::{CvarRegistry, ConsoleError}; 22 | 23 | pub fn register_cvars(cvars: &CvarRegistry) -> Result<(), ConsoleError> { 24 | cvars.register("cl_anglespeedkey", "1.5")?; 25 | cvars.register_archive("cl_backspeed", "200")?; 26 | cvars.register("cl_bob", "0.02")?; 27 | cvars.register("cl_bobcycle", "0.6")?; 28 | cvars.register("cl_bobup", "0.5")?; 29 | cvars.register_archive("_cl_color", "0")?; 30 | cvars.register("cl_crossx", "0")?; 31 | cvars.register("cl_crossy", "0")?; 32 | cvars.register_archive("cl_forwardspeed", "400")?; 33 | cvars.register("cl_movespeedkey", "2.0")?; 34 | cvars.register_archive("_cl_name", "player")?; 35 | cvars.register("cl_nolerp", "0")?; 36 | cvars.register("cl_pitchspeed", "150")?; 37 | cvars.register("cl_rollangle", "2.0")?; 38 | cvars.register("cl_rollspeed", "200")?; 39 | cvars.register("cl_shownet", "0")?; 40 | cvars.register("cl_sidespeed", "350")?; 41 | cvars.register("cl_upspeed", "200")?; 42 | cvars.register("cl_yawspeed", "140")?; 43 | cvars.register("fov", "90")?; 44 | cvars.register_archive("m_pitch", "0.022")?; 45 | cvars.register_archive("m_yaw", "0.022")?; 46 | cvars.register_archive("sensitivity", "3")?; 47 | cvars.register("v_idlescale", "0")?; 48 | cvars.register("v_ipitch_cycle", "1")?; 49 | cvars.register("v_ipitch_level", "0.3")?; 50 | cvars.register("v_iroll_cycle", "0.5")?; 51 | cvars.register("v_iroll_level", "0.1")?; 52 | cvars.register("v_iyaw_cycle", "2")?; 53 | cvars.register("v_iyaw_level", "0.3")?; 54 | cvars.register("v_kickpitch", "0.6")?; 55 | cvars.register("v_kickroll", "0.6")?; 56 | cvars.register("v_kicktime", "0.5")?; 57 | 58 | // some server cvars are needed by the client, but if the server is running 59 | // in the same process they will have been set already, so we can ignore 60 | // the duplicate cvar error 61 | let _ = cvars.register("sv_gravity", "800"); 62 | 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /src/client/demo.rs: -------------------------------------------------------------------------------- 1 | use std::{io, ops::Range}; 2 | 3 | use crate::common::{ 4 | net::{self, NetError}, 5 | util::read_f32_3, 6 | vfs::VirtualFile, 7 | }; 8 | 9 | use arrayvec::ArrayVec; 10 | use byteorder::{LittleEndian, ReadBytesExt}; 11 | use cgmath::{Deg, Vector3}; 12 | use io::BufReader; 13 | use thiserror::Error; 14 | 15 | /// An error returned by a demo server. 16 | #[derive(Error, Debug)] 17 | pub enum DemoServerError { 18 | #[error("Invalid CD track number")] 19 | InvalidCdTrack, 20 | #[error("No such CD track: {0}")] 21 | NoSuchCdTrack(i32), 22 | #[error("Message size ({0}) exceeds maximum allowed size {}", net::MAX_MESSAGE)] 23 | MessageTooLong(u32), 24 | #[error("I/O error: {0}")] 25 | Io(#[from] io::Error), 26 | #[error("Network error: {0}")] 27 | Net(#[from] NetError), 28 | } 29 | 30 | struct DemoMessage { 31 | view_angles: Vector3>, 32 | msg_range: Range, 33 | } 34 | 35 | /// A view of a server message from a demo. 36 | pub struct DemoMessageView<'a> { 37 | view_angles: Vector3>, 38 | message: &'a [u8], 39 | } 40 | 41 | impl<'a> DemoMessageView<'a> { 42 | /// Returns the view angles recorded for this demo message. 43 | pub fn view_angles(&self) -> Vector3> { 44 | self.view_angles 45 | } 46 | 47 | /// Returns the server message for this demo message as a slice of bytes. 48 | pub fn message(&self) -> &[u8] { 49 | self.message 50 | } 51 | } 52 | 53 | /// A server that yields commands from a demo file. 54 | pub struct DemoServer { 55 | track_override: Option, 56 | 57 | // id of next message to "send" 58 | message_id: usize, 59 | 60 | messages: Vec, 61 | 62 | // all message data 63 | message_data: Vec, 64 | } 65 | 66 | impl DemoServer { 67 | /// Construct a new `DemoServer` from the specified demo file. 68 | pub fn new(file: &mut VirtualFile) -> Result { 69 | let mut dem_reader = BufReader::new(file); 70 | 71 | let mut buf = ArrayVec::::new(); 72 | // copy CD track number (terminated by newline) into buffer 73 | for i in 0..buf.capacity() { 74 | match dem_reader.read_u8()? { 75 | b'\n' => break, 76 | // cannot panic because we won't exceed capacity with a loop this small 77 | b => buf.push(b), 78 | } 79 | 80 | if i >= buf.capacity() - 1 { 81 | // CD track would be more than 2 digits long, which is impossible 82 | Err(DemoServerError::InvalidCdTrack)?; 83 | } 84 | } 85 | 86 | let track_override = { 87 | let track_str = match std::str::from_utf8(&buf) { 88 | Ok(s) => s, 89 | Err(_) => Err(DemoServerError::InvalidCdTrack)?, 90 | }; 91 | 92 | match track_str { 93 | // if track is empty, default to track 0 94 | "" => Some(0), 95 | s => match s.parse::() { 96 | Ok(track) => match track { 97 | // if track is -1, allow demo to specify tracks in messages 98 | -1 => None, 99 | t if t < -1 => Err(DemoServerError::InvalidCdTrack)?, 100 | _ => Some(track as u32), 101 | }, 102 | Err(_) => Err(DemoServerError::InvalidCdTrack)?, 103 | }, 104 | } 105 | }; 106 | 107 | let mut message_data = Vec::new(); 108 | let mut messages = Vec::new(); 109 | 110 | // read all messages 111 | while let Ok(msg_len) = dem_reader.read_u32::() { 112 | // get view angles 113 | let view_angles_f32 = read_f32_3(&mut dem_reader)?; 114 | let view_angles = Vector3::new( 115 | Deg(view_angles_f32[0]), 116 | Deg(view_angles_f32[1]), 117 | Deg(view_angles_f32[2]), 118 | ); 119 | 120 | // read next message 121 | let msg_start = message_data.len(); 122 | for _ in 0..msg_len { 123 | message_data.push(dem_reader.read_u8()?); 124 | } 125 | let msg_end = message_data.len(); 126 | 127 | messages.push(DemoMessage { 128 | view_angles, 129 | msg_range: msg_start..msg_end, 130 | }); 131 | } 132 | 133 | Ok(DemoServer { 134 | track_override, 135 | message_id: 0, 136 | messages, 137 | message_data, 138 | }) 139 | } 140 | 141 | /// Retrieve the next server message from the currently playing demo. 142 | /// 143 | /// If this returns `None`, the demo is complete. 144 | pub fn next(&mut self) -> Option { 145 | if self.message_id >= self.messages.len() { 146 | return None; 147 | } 148 | 149 | let msg = &self.messages[self.message_id]; 150 | self.message_id += 1; 151 | 152 | Some(DemoMessageView { 153 | view_angles: msg.view_angles, 154 | message: &self.message_data[msg.msg_range.clone()], 155 | }) 156 | } 157 | 158 | /// Returns the currently playing demo's music track override, if any. 159 | /// 160 | /// If this is `Some`, any `CdTrack` commands from the demo server should 161 | /// cause the client to play this track instead of the one specified by the 162 | /// command. 163 | pub fn track_override(&self) -> Option { 164 | self.track_override 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/client/input/console.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::{cell::RefCell, rc::Rc}; 19 | 20 | use crate::common::console::Console; 21 | 22 | use failure::Error; 23 | use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode as Key, WindowEvent}; 24 | 25 | pub struct ConsoleInput { 26 | console: Rc>, 27 | } 28 | 29 | impl ConsoleInput { 30 | pub fn new(console: Rc>) -> ConsoleInput { 31 | ConsoleInput { console } 32 | } 33 | 34 | pub fn handle_event(&self, event: Event) -> Result<(), Error> { 35 | match event { 36 | Event::WindowEvent { event, .. } => match event { 37 | WindowEvent::ReceivedCharacter(c) => self.console.borrow_mut().send_char(c), 38 | 39 | WindowEvent::KeyboardInput { 40 | input: 41 | KeyboardInput { 42 | virtual_keycode: Some(key), 43 | state: ElementState::Pressed, 44 | .. 45 | }, 46 | .. 47 | } => match key { 48 | Key::Up => self.console.borrow_mut().history_up(), 49 | Key::Down => self.console.borrow_mut().history_down(), 50 | Key::Left => self.console.borrow_mut().cursor_left(), 51 | Key::Right => self.console.borrow_mut().cursor_right(), 52 | Key::Grave => self.console.borrow_mut().stuff_text("toggleconsole\n"), 53 | _ => (), 54 | }, 55 | 56 | _ => (), 57 | }, 58 | 59 | _ => (), 60 | } 61 | 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/client/input/menu.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::{cell::RefCell, rc::Rc}; 19 | 20 | use crate::{client::menu::Menu, common::console::Console}; 21 | 22 | use failure::Error; 23 | use winit::event::{ElementState, Event, KeyboardInput, VirtualKeyCode as Key, WindowEvent}; 24 | 25 | pub struct MenuInput { 26 | menu: Rc>, 27 | console: Rc>, 28 | } 29 | 30 | impl MenuInput { 31 | pub fn new(menu: Rc>, console: Rc>) -> MenuInput { 32 | MenuInput { menu, console } 33 | } 34 | 35 | pub fn handle_event(&self, event: Event) -> Result<(), Error> { 36 | match event { 37 | Event::WindowEvent { event, .. } => match event { 38 | WindowEvent::ReceivedCharacter(_) => (), 39 | 40 | WindowEvent::KeyboardInput { 41 | input: 42 | KeyboardInput { 43 | virtual_keycode: Some(key), 44 | state: ElementState::Pressed, 45 | .. 46 | }, 47 | .. 48 | } => match key { 49 | Key::Escape => { 50 | if self.menu.borrow().at_root() { 51 | self.console.borrow().stuff_text("togglemenu\n"); 52 | } else { 53 | self.menu.borrow().back()?; 54 | } 55 | } 56 | 57 | Key::Up => self.menu.borrow().prev()?, 58 | Key::Down => self.menu.borrow().next()?, 59 | Key::Return => self.menu.borrow().activate()?, 60 | Key::Left => self.menu.borrow().left()?, 61 | Key::Right => self.menu.borrow().right()?, 62 | 63 | _ => (), 64 | }, 65 | 66 | _ => (), 67 | }, 68 | 69 | _ => (), 70 | } 71 | 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/client/input/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | pub mod console; 19 | pub mod game; 20 | pub mod menu; 21 | 22 | use std::{cell::RefCell, rc::Rc}; 23 | 24 | use crate::{ 25 | client::menu::Menu, 26 | common::console::{CmdRegistry, Console}, 27 | }; 28 | 29 | use failure::Error; 30 | use winit::event::{Event, WindowEvent}; 31 | 32 | use self::{ 33 | console::ConsoleInput, 34 | game::{BindInput, BindTarget, GameInput}, 35 | menu::MenuInput, 36 | }; 37 | 38 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 39 | pub enum InputFocus { 40 | Game, 41 | Console, 42 | Menu, 43 | } 44 | 45 | pub struct Input { 46 | window_focused: bool, 47 | focus: InputFocus, 48 | 49 | game_input: GameInput, 50 | console_input: ConsoleInput, 51 | menu_input: MenuInput, 52 | } 53 | 54 | impl Input { 55 | pub fn new( 56 | init_focus: InputFocus, 57 | console: Rc>, 58 | menu: Rc>, 59 | ) -> Input { 60 | Input { 61 | window_focused: true, 62 | focus: init_focus, 63 | 64 | game_input: GameInput::new(console.clone()), 65 | console_input: ConsoleInput::new(console.clone()), 66 | menu_input: MenuInput::new(menu.clone(), console.clone()), 67 | } 68 | } 69 | 70 | pub fn handle_event(&mut self, event: Event) -> Result<(), Error> { 71 | match event { 72 | // we're polling for hardware events, so we have to check window focus ourselves 73 | Event::WindowEvent { 74 | event: WindowEvent::Focused(focused), 75 | .. 76 | } => self.window_focused = focused, 77 | 78 | _ => { 79 | if self.window_focused { 80 | match self.focus { 81 | InputFocus::Game => self.game_input.handle_event(event), 82 | InputFocus::Console => self.console_input.handle_event(event)?, 83 | InputFocus::Menu => self.menu_input.handle_event(event)?, 84 | } 85 | } 86 | } 87 | } 88 | 89 | Ok(()) 90 | } 91 | 92 | pub fn focus(&self) -> InputFocus { 93 | self.focus 94 | } 95 | 96 | pub fn set_focus(&mut self, new_focus: InputFocus) { 97 | self.focus = new_focus; 98 | } 99 | 100 | /// Bind a `BindInput` to a `BindTarget`. 101 | pub fn bind(&mut self, input: I, target: T) -> Option 102 | where 103 | I: Into, 104 | T: Into, 105 | { 106 | self.game_input.bind(input, target) 107 | } 108 | 109 | pub fn bind_defaults(&mut self) { 110 | self.game_input.bind_defaults(); 111 | } 112 | 113 | pub fn game_input(&self) -> Option<&GameInput> { 114 | if let InputFocus::Game = self.focus { 115 | Some(&self.game_input) 116 | } else { 117 | None 118 | } 119 | } 120 | 121 | pub fn game_input_mut(&mut self) -> Option<&mut GameInput> { 122 | if let InputFocus::Game = self.focus { 123 | Some(&mut self.game_input) 124 | } else { 125 | None 126 | } 127 | } 128 | 129 | pub fn register_cmds(&self, cmds: &mut CmdRegistry) { 130 | self.game_input.register_cmds(cmds); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/client/render/blit.rs: -------------------------------------------------------------------------------- 1 | use crate::client::render::{pipeline::Pipeline, ui::quad::QuadPipeline, GraphicsState}; 2 | 3 | pub struct BlitPipeline { 4 | pipeline: wgpu::RenderPipeline, 5 | bind_group_layouts: Vec, 6 | bind_group: wgpu::BindGroup, 7 | sampler: wgpu::Sampler, 8 | } 9 | 10 | impl BlitPipeline { 11 | pub fn create_bind_group( 12 | device: &wgpu::Device, 13 | layouts: &[wgpu::BindGroupLayout], 14 | sampler: &wgpu::Sampler, 15 | input: &wgpu::TextureView, 16 | ) -> wgpu::BindGroup { 17 | device.create_bind_group(&wgpu::BindGroupDescriptor { 18 | label: Some("blit bind group"), 19 | layout: &layouts[0], 20 | entries: &[ 21 | wgpu::BindGroupEntry { 22 | binding: 0, 23 | resource: wgpu::BindingResource::Sampler(&sampler), 24 | }, 25 | wgpu::BindGroupEntry { 26 | binding: 1, 27 | resource: wgpu::BindingResource::TextureView(input), 28 | }, 29 | ], 30 | }) 31 | } 32 | 33 | pub fn new( 34 | device: &wgpu::Device, 35 | compiler: &mut shaderc::Compiler, 36 | input: &wgpu::TextureView, 37 | ) -> BlitPipeline { 38 | let (pipeline, bind_group_layouts) = BlitPipeline::create(device, compiler, &[], 1); 39 | 40 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 41 | label: None, 42 | address_mode_u: wgpu::AddressMode::ClampToEdge, 43 | address_mode_v: wgpu::AddressMode::ClampToEdge, 44 | address_mode_w: wgpu::AddressMode::ClampToEdge, 45 | mag_filter: wgpu::FilterMode::Nearest, 46 | min_filter: wgpu::FilterMode::Nearest, 47 | mipmap_filter: wgpu::FilterMode::Nearest, 48 | lod_min_clamp: -1000.0, 49 | lod_max_clamp: 1000.0, 50 | compare: None, 51 | anisotropy_clamp: None, 52 | ..Default::default() 53 | }); 54 | 55 | let bind_group = Self::create_bind_group(device, &bind_group_layouts, &sampler, input); 56 | 57 | BlitPipeline { 58 | pipeline, 59 | bind_group_layouts, 60 | bind_group, 61 | sampler, 62 | } 63 | } 64 | 65 | pub fn rebuild( 66 | &mut self, 67 | device: &wgpu::Device, 68 | compiler: &mut shaderc::Compiler, 69 | input: &wgpu::TextureView, 70 | ) { 71 | let layout_refs: Vec<_> = self.bind_group_layouts.iter().collect(); 72 | let pipeline = BlitPipeline::recreate(device, compiler, &layout_refs, 1); 73 | self.pipeline = pipeline; 74 | self.bind_group = 75 | Self::create_bind_group(device, self.bind_group_layouts(), &self.sampler, input); 76 | } 77 | 78 | pub fn pipeline(&self) -> &wgpu::RenderPipeline { 79 | &self.pipeline 80 | } 81 | 82 | pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { 83 | &self.bind_group_layouts 84 | } 85 | 86 | pub fn blit<'a>(&'a self, state: &'a GraphicsState, pass: &mut wgpu::RenderPass<'a>) { 87 | pass.set_pipeline(&self.pipeline()); 88 | pass.set_bind_group(0, &self.bind_group, &[]); 89 | pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); 90 | pass.draw(0..6, 0..1); 91 | } 92 | } 93 | 94 | impl Pipeline for BlitPipeline { 95 | type VertexPushConstants = (); 96 | type SharedPushConstants = (); 97 | type FragmentPushConstants = (); 98 | 99 | fn name() -> &'static str { 100 | "blit" 101 | } 102 | 103 | fn bind_group_layout_descriptors() -> Vec> { 104 | vec![wgpu::BindGroupLayoutDescriptor { 105 | label: Some("blit bind group"), 106 | entries: &[ 107 | // sampler 108 | wgpu::BindGroupLayoutEntry { 109 | binding: 0, 110 | visibility: wgpu::ShaderStage::FRAGMENT, 111 | ty: wgpu::BindingType::Sampler { 112 | filtering: true, 113 | comparison: false, 114 | }, 115 | count: None, 116 | }, 117 | // blit texture 118 | wgpu::BindGroupLayoutEntry { 119 | binding: 1, 120 | visibility: wgpu::ShaderStage::FRAGMENT, 121 | ty: wgpu::BindingType::Texture { 122 | view_dimension: wgpu::TextureViewDimension::D2, 123 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 124 | multisampled: false, 125 | }, 126 | count: None, 127 | }, 128 | ], 129 | }] 130 | } 131 | 132 | fn vertex_shader() -> &'static str { 133 | include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/blit.vert")) 134 | } 135 | 136 | fn fragment_shader() -> &'static str { 137 | include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/blit.frag")) 138 | } 139 | 140 | fn primitive_state() -> wgpu::PrimitiveState { 141 | QuadPipeline::primitive_state() 142 | } 143 | 144 | fn color_target_states() -> Vec { 145 | QuadPipeline::color_target_states() 146 | } 147 | 148 | fn depth_stencil_state() -> Option { 149 | None 150 | } 151 | 152 | fn vertex_buffer_layouts() -> Vec> { 153 | QuadPipeline::vertex_buffer_layouts() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/client/render/cvars.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Cormac O'Brien. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use crate::common::console::CvarRegistry; 22 | 23 | pub fn register_cvars(cvars: &CvarRegistry) { 24 | cvars.register("r_lightmap", "0").unwrap(); 25 | cvars.register("r_msaa_samples", "4").unwrap(); 26 | } 27 | -------------------------------------------------------------------------------- /src/client/render/error.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{ 2 | vfs::VfsError, 3 | wad::WadError, 4 | }; 5 | use failure::{Backtrace, Context, Fail}; 6 | use std::{ 7 | convert::From, 8 | fmt::{self, Display}, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub struct RenderError { 13 | inner: Context, 14 | } 15 | 16 | impl RenderError { 17 | pub fn kind(&self) -> RenderErrorKind { 18 | *self.inner.get_context() 19 | } 20 | } 21 | 22 | impl From for RenderError { 23 | fn from(kind: RenderErrorKind) -> Self { 24 | RenderError { 25 | inner: Context::new(kind), 26 | } 27 | } 28 | } 29 | 30 | impl From for RenderError { 31 | fn from(vfs_error: VfsError) -> Self { 32 | match vfs_error { 33 | VfsError::NoSuchFile(_) => { 34 | vfs_error.context(RenderErrorKind::ResourceNotLoaded).into() 35 | } 36 | _ => vfs_error.context(RenderErrorKind::Other).into(), 37 | } 38 | } 39 | } 40 | 41 | impl From for RenderError { 42 | fn from(wad_error: WadError) -> Self { 43 | wad_error.context(RenderErrorKind::ResourceNotLoaded).into() 44 | } 45 | } 46 | 47 | impl From> for RenderError { 48 | fn from(inner: Context) -> Self { 49 | RenderError { inner } 50 | } 51 | } 52 | 53 | impl Fail for RenderError { 54 | fn cause(&self) -> Option<&dyn Fail> { 55 | self.inner.cause() 56 | } 57 | 58 | fn backtrace(&self) -> Option<&Backtrace> { 59 | self.inner.backtrace() 60 | } 61 | } 62 | 63 | impl Display for RenderError { 64 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 65 | Display::fmt(&self.inner, f) 66 | } 67 | } 68 | 69 | #[derive(Clone, Copy, Eq, PartialEq, Debug, Fail)] 70 | pub enum RenderErrorKind { 71 | #[fail(display = "Failed to load resource")] 72 | ResourceNotLoaded, 73 | #[fail(display = "Unspecified render error")] 74 | Other, 75 | } 76 | -------------------------------------------------------------------------------- /src/client/render/palette.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, io::BufReader}; 2 | 3 | use crate::{ 4 | client::render::{DiffuseData, FullbrightData}, 5 | common::vfs::Vfs, 6 | }; 7 | 8 | use byteorder::ReadBytesExt; 9 | 10 | pub struct Palette { 11 | rgb: [[u8; 3]; 256], 12 | } 13 | 14 | impl Palette { 15 | pub fn new(data: &[u8]) -> Palette { 16 | if data.len() != 768 { 17 | panic!("Bad len for rgb data"); 18 | } 19 | 20 | let mut rgb = [[0; 3]; 256]; 21 | for color in 0..256 { 22 | for component in 0..3 { 23 | rgb[color][component] = data[color * 3 + component]; 24 | } 25 | } 26 | 27 | Palette { rgb } 28 | } 29 | 30 | pub fn load(vfs: &Vfs, path: S) -> Palette 31 | where 32 | S: AsRef, 33 | { 34 | let mut data = BufReader::new(vfs.open(path).unwrap()); 35 | 36 | let mut rgb = [[0u8; 3]; 256]; 37 | 38 | for color in 0..256 { 39 | for component in 0..3 { 40 | rgb[color][component] = data.read_u8().unwrap(); 41 | } 42 | } 43 | 44 | Palette { rgb } 45 | } 46 | 47 | // TODO: this will not render console characters correctly, as they use index 0 (black) to 48 | // indicate transparency. 49 | /// Translates a set of indices into a list of RGBA values and a list of fullbright values. 50 | pub fn translate(&self, indices: &[u8]) -> (DiffuseData, FullbrightData) { 51 | let mut rgba = Vec::with_capacity(indices.len() * 4); 52 | let mut fullbright = Vec::with_capacity(indices.len()); 53 | 54 | for index in indices { 55 | match *index { 56 | 0xFF => { 57 | for _ in 0..4 { 58 | rgba.push(0); 59 | fullbright.push(0); 60 | } 61 | } 62 | 63 | i => { 64 | for component in 0..3 { 65 | rgba.push(self.rgb[*index as usize][component]); 66 | } 67 | rgba.push(0xFF); 68 | fullbright.push(if i > 223 { 0xFF } else { 0 }); 69 | } 70 | } 71 | } 72 | 73 | ( 74 | DiffuseData { 75 | rgba: Cow::Owned(rgba), 76 | }, 77 | FullbrightData { 78 | fullbright: Cow::Owned(fullbright), 79 | }, 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/client/render/ui/console.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::render::{ 3 | ui::{ 4 | glyph::{GlyphRendererCommand, GLYPH_HEIGHT, GLYPH_WIDTH}, 5 | layout::{Anchor, AnchorCoord, Layout, ScreenPosition, Size}, 6 | quad::{QuadRendererCommand, QuadTexture}, 7 | }, 8 | GraphicsState, 9 | }, 10 | common::{console::Console, engine, wad::QPic}, 11 | }; 12 | 13 | use chrono::Duration; 14 | 15 | const PAD_LEFT: i32 = GLYPH_WIDTH as i32; 16 | 17 | pub struct ConsoleRenderer { 18 | conback: QuadTexture, 19 | } 20 | 21 | impl ConsoleRenderer { 22 | pub fn new(state: &GraphicsState) -> ConsoleRenderer { 23 | let conback = QuadTexture::from_qpic( 24 | state, 25 | &QPic::load(state.vfs().open("gfx/conback.lmp").unwrap()).unwrap(), 26 | ); 27 | 28 | ConsoleRenderer { conback } 29 | } 30 | 31 | pub fn generate_commands<'a>( 32 | &'a self, 33 | console: &Console, 34 | time: Duration, 35 | quad_cmds: &mut Vec>, 36 | glyph_cmds: &mut Vec, 37 | proportion: f32, 38 | ) { 39 | // TODO: take scale as cvar 40 | let scale = 2.0; 41 | let console_anchor = Anchor { 42 | x: AnchorCoord::Zero, 43 | y: AnchorCoord::Proportion(1.0 - proportion), 44 | }; 45 | 46 | // draw console background 47 | quad_cmds.push(QuadRendererCommand { 48 | texture: &self.conback, 49 | layout: Layout { 50 | position: ScreenPosition::Absolute(console_anchor), 51 | anchor: Anchor::BOTTOM_LEFT, 52 | size: Size::DisplayScale { ratio: 1.0 }, 53 | }, 54 | }); 55 | 56 | // draw version string 57 | let version_string = format!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 58 | glyph_cmds.push(GlyphRendererCommand::Text { 59 | text: version_string, 60 | position: ScreenPosition::Absolute(console_anchor), 61 | anchor: Anchor::BOTTOM_RIGHT, 62 | scale, 63 | }); 64 | 65 | // draw input line 66 | glyph_cmds.push(GlyphRendererCommand::Glyph { 67 | glyph_id: ']' as u8, 68 | position: ScreenPosition::Relative { 69 | anchor: console_anchor, 70 | x_ofs: PAD_LEFT, 71 | y_ofs: 0, 72 | }, 73 | anchor: Anchor::BOTTOM_LEFT, 74 | scale, 75 | }); 76 | let input_text = console.get_string(); 77 | glyph_cmds.push(GlyphRendererCommand::Text { 78 | text: input_text, 79 | position: ScreenPosition::Relative { 80 | anchor: console_anchor, 81 | x_ofs: PAD_LEFT + GLYPH_WIDTH as i32, 82 | y_ofs: 0, 83 | }, 84 | anchor: Anchor::BOTTOM_LEFT, 85 | scale, 86 | }); 87 | // blink cursor in half-second intervals 88 | if engine::duration_to_f32(time).fract() > 0.5 { 89 | glyph_cmds.push(GlyphRendererCommand::Glyph { 90 | glyph_id: 11, 91 | position: ScreenPosition::Relative { 92 | anchor: console_anchor, 93 | x_ofs: PAD_LEFT + (GLYPH_WIDTH * (console.cursor() + 1)) as i32, 94 | y_ofs: 0, 95 | }, 96 | anchor: Anchor::BOTTOM_LEFT, 97 | scale, 98 | }); 99 | } 100 | 101 | // draw previous output 102 | for (line_id, line) in console.output().lines().enumerate() { 103 | // TODO: implement scrolling 104 | if line_id > 100 { 105 | break; 106 | } 107 | 108 | for (chr_id, chr) in line.iter().enumerate() { 109 | let position = ScreenPosition::Relative { 110 | anchor: console_anchor, 111 | x_ofs: PAD_LEFT + (1 + chr_id * GLYPH_WIDTH) as i32, 112 | y_ofs: ((line_id + 1) * GLYPH_HEIGHT) as i32, 113 | }; 114 | 115 | let c = if *chr as u32 > std::u8::MAX as u32 { 116 | warn!( 117 | "char \"{}\" (U+{:4}) cannot be displayed in the console", 118 | *chr, *chr as u32 119 | ); 120 | '?' 121 | } else { 122 | *chr 123 | }; 124 | 125 | glyph_cmds.push(GlyphRendererCommand::Glyph { 126 | glyph_id: c as u8, 127 | position, 128 | anchor: Anchor::BOTTOM_LEFT, 129 | scale, 130 | }); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/client/render/ui/layout.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug)] 2 | pub struct Layout { 3 | /// The position of the quad on the screen. 4 | pub position: ScreenPosition, 5 | 6 | /// Which part of the quad to position at `position`. 7 | pub anchor: Anchor, 8 | 9 | /// The size at which to render the quad. 10 | pub size: Size, 11 | } 12 | 13 | /// An anchor coordinate. 14 | #[derive(Clone, Copy, Debug)] 15 | pub enum AnchorCoord { 16 | /// A value of zero in this dimension. 17 | Zero, 18 | 19 | /// The center of the quad in this dimension. 20 | Center, 21 | 22 | /// The maximum extent of the quad in this dimension. 23 | Max, 24 | 25 | /// An absolute anchor coordinate, in pixels. 26 | Absolute(i32), 27 | 28 | /// A proportion of the maximum extent of the quad in this dimension. 29 | Proportion(f32), 30 | } 31 | 32 | impl AnchorCoord { 33 | pub fn to_value(&self, max: u32) -> i32 { 34 | match *self { 35 | AnchorCoord::Zero => 0, 36 | AnchorCoord::Center => max as i32 / 2, 37 | AnchorCoord::Max => max as i32, 38 | AnchorCoord::Absolute(v) => v, 39 | AnchorCoord::Proportion(p) => (p * max as f32) as i32, 40 | } 41 | } 42 | } 43 | 44 | /// An anchor position on a quad. 45 | /// 46 | /// The anchor specifies which part of the quad should be considered the origin 47 | /// when positioning the quad, or when positioning quads relative to one another. 48 | #[derive(Clone, Copy, Debug)] 49 | pub struct Anchor { 50 | /// The x-coordinate of the anchor. 51 | pub x: AnchorCoord, 52 | 53 | /// The y-coordinate of the anchor. 54 | pub y: AnchorCoord, 55 | } 56 | 57 | impl Anchor { 58 | pub const BOTTOM_LEFT: Anchor = Anchor { 59 | x: AnchorCoord::Zero, 60 | y: AnchorCoord::Zero, 61 | }; 62 | pub const CENTER_LEFT: Anchor = Anchor { 63 | x: AnchorCoord::Zero, 64 | y: AnchorCoord::Center, 65 | }; 66 | pub const TOP_LEFT: Anchor = Anchor { 67 | x: AnchorCoord::Zero, 68 | y: AnchorCoord::Max, 69 | }; 70 | pub const BOTTOM_CENTER: Anchor = Anchor { 71 | x: AnchorCoord::Center, 72 | y: AnchorCoord::Zero, 73 | }; 74 | pub const CENTER: Anchor = Anchor { 75 | x: AnchorCoord::Center, 76 | y: AnchorCoord::Center, 77 | }; 78 | pub const TOP_CENTER: Anchor = Anchor { 79 | x: AnchorCoord::Center, 80 | y: AnchorCoord::Max, 81 | }; 82 | pub const BOTTOM_RIGHT: Anchor = Anchor { 83 | x: AnchorCoord::Max, 84 | y: AnchorCoord::Zero, 85 | }; 86 | pub const CENTER_RIGHT: Anchor = Anchor { 87 | x: AnchorCoord::Max, 88 | y: AnchorCoord::Center, 89 | }; 90 | pub const TOP_RIGHT: Anchor = Anchor { 91 | x: AnchorCoord::Max, 92 | y: AnchorCoord::Max, 93 | }; 94 | 95 | pub fn absolute_xy(x: i32, y: i32) -> Anchor { 96 | Anchor { 97 | x: AnchorCoord::Absolute(x), 98 | y: AnchorCoord::Absolute(y), 99 | } 100 | } 101 | 102 | pub fn to_xy(&self, width: u32, height: u32) -> (i32, i32) { 103 | (self.x.to_value(width), self.y.to_value(height)) 104 | } 105 | } 106 | 107 | /// The position of a quad rendered on the screen. 108 | #[derive(Clone, Copy, Debug)] 109 | pub enum ScreenPosition { 110 | /// The quad is positioned at the exact coordinates provided. 111 | Absolute(Anchor), 112 | 113 | /// The quad is positioned relative to a reference point. 114 | Relative { 115 | anchor: Anchor, 116 | 117 | /// The offset along the x-axis from `reference_x`. 118 | x_ofs: i32, 119 | 120 | /// The offset along the y-axis from `reference_y`. 121 | y_ofs: i32, 122 | }, 123 | } 124 | 125 | impl ScreenPosition { 126 | pub fn to_xy(&self, display_width: u32, display_height: u32, scale: f32) -> (i32, i32) { 127 | match *self { 128 | ScreenPosition::Absolute(Anchor { 129 | x: anchor_x, 130 | y: anchor_y, 131 | }) => ( 132 | anchor_x.to_value(display_width), 133 | anchor_y.to_value(display_height), 134 | ), 135 | ScreenPosition::Relative { 136 | anchor: 137 | Anchor { 138 | x: anchor_x, 139 | y: anchor_y, 140 | }, 141 | x_ofs, 142 | y_ofs, 143 | } => ( 144 | anchor_x.to_value(display_width) + (x_ofs as f32 * scale) as i32, 145 | anchor_y.to_value(display_height) + (y_ofs as f32 * scale) as i32, 146 | ), 147 | } 148 | } 149 | } 150 | 151 | /// Specifies what size a quad should be when rendered on the screen. 152 | #[derive(Clone, Copy, Debug)] 153 | pub enum Size { 154 | /// Render the quad at an exact size in pixels. 155 | Absolute { 156 | /// The width of the quad in pixels. 157 | width: u32, 158 | 159 | /// The height of the quad in pixels. 160 | height: u32, 161 | }, 162 | 163 | /// Render the quad at a size specified relative to the dimensions of its texture. 164 | Scale { 165 | /// The factor to multiply by the quad's texture dimensions to determine its size. 166 | factor: f32, 167 | }, 168 | 169 | /// Render the quad at a size specified relative to the size of the display. 170 | DisplayScale { 171 | /// The ratio of the display size at which to render the quad. 172 | ratio: f32, 173 | }, 174 | } 175 | 176 | impl Size { 177 | pub fn to_wh( 178 | &self, 179 | texture_width: u32, 180 | texture_height: u32, 181 | display_width: u32, 182 | display_height: u32, 183 | ) -> (u32, u32) { 184 | match *self { 185 | Size::Absolute { width, height } => (width, height), 186 | Size::Scale { factor } => ( 187 | (texture_width as f32 * factor) as u32, 188 | (texture_height as f32 * factor) as u32, 189 | ), 190 | Size::DisplayScale { ratio } => ( 191 | (display_width as f32 * ratio) as u32, 192 | (display_height as f32 * ratio) as u32, 193 | ), 194 | } 195 | } 196 | } 197 | 198 | #[cfg(test)] 199 | mod tests { 200 | use super::*; 201 | 202 | #[test] 203 | fn test_anchor_to_xy() { 204 | let width = 1366; 205 | let height = 768; 206 | 207 | assert_eq!(Anchor::BOTTOM_LEFT.to_xy(width, height), (0, 0)); 208 | assert_eq!(Anchor::CENTER_LEFT.to_xy(width, height), (0, 384)); 209 | assert_eq!(Anchor::TOP_LEFT.to_xy(width, height), (0, 768)); 210 | assert_eq!(Anchor::BOTTOM_CENTER.to_xy(width, height), (683, 0)); 211 | assert_eq!(Anchor::CENTER.to_xy(width, height), (683, 384)); 212 | assert_eq!(Anchor::TOP_CENTER.to_xy(width, height), (683, 768)); 213 | assert_eq!(Anchor::BOTTOM_RIGHT.to_xy(width, height), (1366, 0)); 214 | assert_eq!(Anchor::CENTER_RIGHT.to_xy(width, height), (1366, 384)); 215 | assert_eq!(Anchor::TOP_RIGHT.to_xy(width, height), (1366, 768)); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/client/render/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod console; 2 | pub mod glyph; 3 | pub mod hud; 4 | pub mod layout; 5 | pub mod menu; 6 | pub mod quad; 7 | 8 | use std::cell::RefCell; 9 | 10 | use crate::{ 11 | client::{ 12 | menu::Menu, 13 | render::{ 14 | ui::{ 15 | console::ConsoleRenderer, 16 | glyph::{GlyphRenderer, GlyphRendererCommand}, 17 | hud::{HudRenderer, HudState}, 18 | menu::MenuRenderer, 19 | quad::{QuadRenderer, QuadRendererCommand, QuadUniforms}, 20 | }, 21 | uniform::{self, DynamicUniformBufferBlock}, 22 | Extent2d, GraphicsState, 23 | }, 24 | }, 25 | common::{console::Console, util::any_slice_as_bytes}, 26 | }; 27 | 28 | use cgmath::{Matrix4, Vector2}; 29 | use chrono::Duration; 30 | 31 | pub fn screen_space_vertex_translate( 32 | display_w: u32, 33 | display_h: u32, 34 | pos_x: i32, 35 | pos_y: i32, 36 | ) -> Vector2 { 37 | // rescale from [0, DISPLAY_*] to [-1, 1] (NDC) 38 | Vector2::new( 39 | (pos_x * 2 - display_w as i32) as f32 / display_w as f32, 40 | (pos_y * 2 - display_h as i32) as f32 / display_h as f32, 41 | ) 42 | } 43 | 44 | pub fn screen_space_vertex_scale( 45 | display_w: u32, 46 | display_h: u32, 47 | quad_w: u32, 48 | quad_h: u32, 49 | ) -> Vector2 { 50 | Vector2::new( 51 | (quad_w * 2) as f32 / display_w as f32, 52 | (quad_h * 2) as f32 / display_h as f32, 53 | ) 54 | } 55 | 56 | pub fn screen_space_vertex_transform( 57 | display_w: u32, 58 | display_h: u32, 59 | quad_w: u32, 60 | quad_h: u32, 61 | pos_x: i32, 62 | pos_y: i32, 63 | ) -> Matrix4 { 64 | let Vector2 { x: ndc_x, y: ndc_y } = 65 | screen_space_vertex_translate(display_w, display_h, pos_x, pos_y); 66 | 67 | let Vector2 { 68 | x: scale_x, 69 | y: scale_y, 70 | } = screen_space_vertex_scale(display_w, display_h, quad_w, quad_h); 71 | 72 | Matrix4::from_translation([ndc_x, ndc_y, 0.0].into()) 73 | * Matrix4::from_nonuniform_scale(scale_x, scale_y, 1.0) 74 | } 75 | 76 | pub enum UiOverlay<'a> { 77 | Menu(&'a Menu), 78 | Console(&'a Console), 79 | } 80 | 81 | pub enum UiState<'a> { 82 | Title { 83 | overlay: UiOverlay<'a>, 84 | }, 85 | InGame { 86 | hud: HudState<'a>, 87 | overlay: Option>, 88 | }, 89 | } 90 | 91 | pub struct UiRenderer { 92 | console_renderer: ConsoleRenderer, 93 | menu_renderer: MenuRenderer, 94 | hud_renderer: HudRenderer, 95 | glyph_renderer: GlyphRenderer, 96 | quad_renderer: QuadRenderer, 97 | } 98 | 99 | impl UiRenderer { 100 | pub fn new(state: &GraphicsState, menu: &Menu) -> UiRenderer { 101 | UiRenderer { 102 | console_renderer: ConsoleRenderer::new(state), 103 | menu_renderer: MenuRenderer::new(state, menu), 104 | hud_renderer: HudRenderer::new(state), 105 | glyph_renderer: GlyphRenderer::new(state), 106 | quad_renderer: QuadRenderer::new(state), 107 | } 108 | } 109 | 110 | pub fn render_pass<'pass>( 111 | &'pass self, 112 | state: &'pass GraphicsState, 113 | pass: &mut wgpu::RenderPass<'pass>, 114 | target_size: Extent2d, 115 | time: Duration, 116 | ui_state: &UiState<'pass>, 117 | quad_commands: &'pass mut Vec>, 118 | glyph_commands: &'pass mut Vec, 119 | ) { 120 | let (hud_state, overlay) = match ui_state { 121 | UiState::Title { overlay } => (None, Some(overlay)), 122 | UiState::InGame { hud, overlay } => (Some(hud), overlay.as_ref()), 123 | }; 124 | 125 | if let Some(hstate) = hud_state { 126 | self.hud_renderer 127 | .generate_commands(hstate, time, quad_commands, glyph_commands); 128 | } 129 | 130 | if let Some(o) = overlay { 131 | match o { 132 | UiOverlay::Menu(menu) => { 133 | self.menu_renderer 134 | .generate_commands(menu, time, quad_commands, glyph_commands); 135 | } 136 | UiOverlay::Console(console) => { 137 | // TODO: take in-game console proportion as cvar 138 | let proportion = match hud_state { 139 | Some(_) => 0.33, 140 | None => 1.0, 141 | }; 142 | 143 | self.console_renderer.generate_commands( 144 | console, 145 | time, 146 | quad_commands, 147 | glyph_commands, 148 | proportion, 149 | ); 150 | } 151 | } 152 | } 153 | 154 | self.quad_renderer 155 | .record_draw(state, pass, target_size, quad_commands); 156 | self.glyph_renderer 157 | .record_draw(state, pass, target_size, glyph_commands); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/client/render/uniform.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{Cell, RefCell}, 3 | marker::PhantomData, 4 | mem::{align_of, size_of}, 5 | rc::Rc, 6 | }; 7 | 8 | use crate::common::util::{any_as_bytes, Pod}; 9 | 10 | use failure::Error; 11 | 12 | // minimum limit is 16384: 13 | // https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#limits-maxUniformBufferRange 14 | // but https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxUniformBufferRange&platform=windows 15 | // indicates that a limit of 65536 or higher is more common 16 | const DYNAMIC_UNIFORM_BUFFER_SIZE: wgpu::BufferAddress = 65536; 17 | 18 | // https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#limits-minUniformBufferOffsetAlignment 19 | pub const DYNAMIC_UNIFORM_BUFFER_ALIGNMENT: usize = 256; 20 | 21 | #[repr(C)] 22 | #[derive(Clone, Copy, Debug)] 23 | pub struct UniformBool { 24 | value: u32, 25 | } 26 | 27 | impl UniformBool { 28 | pub fn new(value: bool) -> UniformBool { 29 | UniformBool { 30 | value: value as u32, 31 | } 32 | } 33 | } 34 | 35 | // uniform float array elements are aligned as if they were vec4s 36 | #[repr(C, align(16))] 37 | #[derive(Clone, Copy, Debug)] 38 | pub struct UniformArrayFloat { 39 | value: f32, 40 | } 41 | 42 | impl UniformArrayFloat { 43 | pub fn new(value: f32) -> UniformArrayFloat { 44 | UniformArrayFloat { value } 45 | } 46 | } 47 | 48 | /// A handle to a dynamic uniform buffer on the GPU. 49 | /// 50 | /// Allows allocation and updating of individual blocks of memory. 51 | pub struct DynamicUniformBuffer 52 | where 53 | T: Pod, 54 | { 55 | // keeps track of how many blocks are allocated so we know whether we can 56 | // clear the buffer or not 57 | _rc: RefCell>, 58 | 59 | // represents the data in the buffer, which we don't actually own 60 | _phantom: PhantomData, 61 | 62 | inner: wgpu::Buffer, 63 | allocated: Cell, 64 | update_buf: Vec, 65 | } 66 | 67 | impl DynamicUniformBuffer 68 | where 69 | T: Pod, 70 | { 71 | pub fn new<'b>(device: &'b wgpu::Device) -> DynamicUniformBuffer { 72 | // TODO: is this something we can enforce at compile time? 73 | assert!(align_of::() % DYNAMIC_UNIFORM_BUFFER_ALIGNMENT == 0); 74 | 75 | let inner = device.create_buffer(&wgpu::BufferDescriptor { 76 | label: Some("dynamic uniform buffer"), 77 | size: DYNAMIC_UNIFORM_BUFFER_SIZE, 78 | usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, 79 | mapped_at_creation: false, 80 | }); 81 | 82 | let mut update_buf = Vec::with_capacity(DYNAMIC_UNIFORM_BUFFER_SIZE as usize); 83 | update_buf.resize(DYNAMIC_UNIFORM_BUFFER_SIZE as usize, 0); 84 | 85 | DynamicUniformBuffer { 86 | _rc: RefCell::new(Rc::new(())), 87 | _phantom: PhantomData, 88 | inner, 89 | allocated: Cell::new(0), 90 | update_buf, 91 | } 92 | } 93 | 94 | pub fn block_size(&self) -> wgpu::BufferSize { 95 | std::num::NonZeroU64::new( 96 | ((DYNAMIC_UNIFORM_BUFFER_ALIGNMENT / 8).max(size_of::())) as u64, 97 | ) 98 | .unwrap() 99 | } 100 | 101 | /// Allocates a block of memory in this dynamic uniform buffer with the 102 | /// specified initial value. 103 | #[must_use] 104 | pub fn allocate(&mut self, val: T) -> DynamicUniformBufferBlock { 105 | let allocated = self.allocated.get(); 106 | let size = self.block_size().get(); 107 | trace!( 108 | "Allocating dynamic uniform block (allocated: {})", 109 | allocated 110 | ); 111 | if allocated + size > DYNAMIC_UNIFORM_BUFFER_SIZE { 112 | panic!( 113 | "Not enough space to allocate {} bytes in dynamic uniform buffer", 114 | size 115 | ); 116 | } 117 | 118 | let addr = allocated; 119 | self.allocated.set(allocated + size); 120 | 121 | let block = DynamicUniformBufferBlock { 122 | _rc: self._rc.borrow().clone(), 123 | _phantom: PhantomData, 124 | addr, 125 | }; 126 | 127 | self.write_block(&block, val); 128 | block 129 | } 130 | 131 | pub fn write_block(&mut self, block: &DynamicUniformBufferBlock, val: T) { 132 | let start = block.addr as usize; 133 | let end = start + self.block_size().get() as usize; 134 | let slice = &mut self.update_buf[start..end]; 135 | slice.copy_from_slice(unsafe { any_as_bytes(&val) }); 136 | } 137 | 138 | /// Removes all allocations from the underlying buffer. 139 | /// 140 | /// Returns an error if the buffer is currently mapped or there are 141 | /// outstanding allocated blocks. 142 | pub fn clear(&self) -> Result<(), Error> { 143 | let out = self._rc.replace(Rc::new(())); 144 | match Rc::try_unwrap(out) { 145 | // no outstanding blocks 146 | Ok(()) => { 147 | self.allocated.set(0); 148 | Ok(()) 149 | } 150 | Err(rc) => { 151 | let _ = self._rc.replace(rc); 152 | bail!("Can't clear uniform buffer: there are outstanding references to allocated blocks."); 153 | } 154 | } 155 | } 156 | 157 | pub fn flush(&self, queue: &wgpu::Queue) { 158 | queue.write_buffer(&self.inner, 0, &self.update_buf); 159 | } 160 | 161 | pub fn buffer(&self) -> &wgpu::Buffer { 162 | &self.inner 163 | } 164 | } 165 | 166 | /// An address into a dynamic uniform buffer. 167 | #[derive(Debug)] 168 | pub struct DynamicUniformBufferBlock { 169 | _rc: Rc<()>, 170 | _phantom: PhantomData, 171 | 172 | addr: wgpu::BufferAddress, 173 | } 174 | 175 | impl DynamicUniformBufferBlock { 176 | pub fn offset(&self) -> wgpu::DynamicOffset { 177 | self.addr as wgpu::DynamicOffset 178 | } 179 | } 180 | 181 | pub fn clear_and_rewrite( 182 | queue: &wgpu::Queue, 183 | buffer: &mut DynamicUniformBuffer, 184 | blocks: &mut Vec>, 185 | uniforms: &[T], 186 | ) where 187 | T: Pod, 188 | { 189 | blocks.clear(); 190 | buffer.clear().unwrap(); 191 | for (uni_id, uni) in uniforms.iter().enumerate() { 192 | if uni_id >= blocks.len() { 193 | let block = buffer.allocate(*uni); 194 | blocks.push(block); 195 | } else { 196 | buffer.write_block(&blocks[uni_id], *uni); 197 | } 198 | } 199 | buffer.flush(queue); 200 | } 201 | -------------------------------------------------------------------------------- /src/client/render/warp.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use crate::common::math; 4 | 5 | use cgmath::{InnerSpace, Vector2, Vector3}; 6 | 7 | // TODO: make this a cvar 8 | const SUBDIVIDE_SIZE: f32 = 32.0; 9 | 10 | /// Subdivide the given polygon on a grid. 11 | /// 12 | /// The algorithm is described as follows: 13 | /// Given a polygon *P*, 14 | /// 1. Calculate the extents *P*min, *P*max and the midpoint *P*mid of *P*. 15 | /// 1. Calculate the distance vector *D*i for each *P*i. 16 | /// 1. For each axis *A* = [X, Y, Z]: 17 | /// 1. If the distance between either *P*minA or 18 | /// *P*maxA and *P*midA is less than 8, continue to 19 | /// the next axis. 20 | /// 1. For each vertex *v*... 21 | /// TODO... 22 | pub fn subdivide(verts: Vec>) -> Vec> { 23 | let mut out = Vec::new(); 24 | subdivide_impl(verts, &mut out); 25 | out 26 | } 27 | 28 | fn subdivide_impl(mut verts: Vec>, output: &mut Vec>) { 29 | let (min, max) = math::bounds(&verts); 30 | 31 | let mut front = Vec::new(); 32 | let mut back = Vec::new(); 33 | 34 | // subdivide polygon along each axis in order 35 | for ax in 0..3 { 36 | // find the midpoint of the polygon bounds 37 | let mid = { 38 | let m = (min[ax] + max[ax]) / 2.0; 39 | SUBDIVIDE_SIZE * (m / SUBDIVIDE_SIZE).round() 40 | }; 41 | 42 | if max[ax] - mid < 8.0 || mid - min[ax] < 8.0 { 43 | // this component doesn't need to be subdivided further. 44 | // if no components need to be subdivided further, this breaks the loop. 45 | continue; 46 | } 47 | 48 | // collect the distances of each vertex from the midpoint 49 | let mut dist: Vec = verts.iter().map(|v| (*v)[ax] - mid).collect(); 50 | dist.push(dist[0]); 51 | 52 | // duplicate first vertex 53 | verts.push(verts[0]); 54 | for (vi, v) in (&verts[..verts.len() - 1]).iter().enumerate() { 55 | // sort vertices to front and back of axis 56 | let cmp = dist[vi].partial_cmp(&0.0).unwrap(); 57 | match cmp { 58 | Ordering::Less => { 59 | back.push(*v); 60 | } 61 | Ordering::Equal => { 62 | // if this vertex is on the axis, split it in two 63 | front.push(*v); 64 | back.push(*v); 65 | continue; 66 | } 67 | Ordering::Greater => { 68 | front.push(*v); 69 | } 70 | } 71 | 72 | if dist[vi + 1] != 0.0 && cmp != dist[vi + 1].partial_cmp(&0.0).unwrap() { 73 | // segment crosses the axis, add a vertex at the intercept 74 | let ratio = dist[vi] / (dist[vi] - dist[vi + 1]); 75 | let intercept = v + ratio * (verts[vi + 1] - v); 76 | front.push(intercept); 77 | back.push(intercept); 78 | } 79 | } 80 | 81 | subdivide_impl(front, output); 82 | subdivide_impl(back, output); 83 | return; 84 | } 85 | 86 | // polygon is smaller than SUBDIVIDE_SIZE along all three axes 87 | assert!(verts.len() >= 3); 88 | let v1 = verts[0]; 89 | let mut v2 = verts[1]; 90 | for v3 in &verts[2..] { 91 | output.push(v1); 92 | output.push(v2); 93 | output.push(*v3); 94 | v2 = *v3; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/client/render/world/postprocess.rs: -------------------------------------------------------------------------------- 1 | use std::{mem::size_of, num::NonZeroU64}; 2 | 3 | use crate::{ 4 | client::render::{pipeline::Pipeline, ui::quad::QuadPipeline, GraphicsState}, 5 | common::util::any_as_bytes, 6 | }; 7 | 8 | #[repr(C, align(256))] 9 | #[derive(Clone, Copy, Debug)] 10 | pub struct PostProcessUniforms { 11 | pub color_shift: [f32; 4], 12 | } 13 | 14 | pub struct PostProcessPipeline { 15 | pipeline: wgpu::RenderPipeline, 16 | bind_group_layouts: Vec, 17 | uniform_buffer: wgpu::Buffer, 18 | } 19 | 20 | impl PostProcessPipeline { 21 | pub fn new( 22 | device: &wgpu::Device, 23 | compiler: &mut shaderc::Compiler, 24 | sample_count: u32, 25 | ) -> PostProcessPipeline { 26 | let (pipeline, bind_group_layouts) = 27 | PostProcessPipeline::create(device, compiler, &[], sample_count); 28 | use wgpu::util::DeviceExt as _; 29 | let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 30 | label: None, 31 | contents: unsafe { 32 | any_as_bytes(&PostProcessUniforms { 33 | color_shift: [0.0; 4], 34 | }) 35 | }, 36 | usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, 37 | }); 38 | 39 | PostProcessPipeline { 40 | pipeline, 41 | bind_group_layouts, 42 | uniform_buffer, 43 | } 44 | } 45 | 46 | pub fn rebuild( 47 | &mut self, 48 | device: &wgpu::Device, 49 | compiler: &mut shaderc::Compiler, 50 | sample_count: u32, 51 | ) { 52 | let layout_refs: Vec<_> = self.bind_group_layouts.iter().collect(); 53 | let pipeline = PostProcessPipeline::recreate(device, compiler, &layout_refs, sample_count); 54 | self.pipeline = pipeline; 55 | } 56 | 57 | pub fn pipeline(&self) -> &wgpu::RenderPipeline { 58 | &self.pipeline 59 | } 60 | 61 | pub fn bind_group_layouts(&self) -> &[wgpu::BindGroupLayout] { 62 | &self.bind_group_layouts 63 | } 64 | 65 | pub fn uniform_buffer(&self) -> &wgpu::Buffer { 66 | &self.uniform_buffer 67 | } 68 | } 69 | 70 | const BIND_GROUP_LAYOUT_ENTRIES: &[wgpu::BindGroupLayoutEntry] = &[ 71 | // sampler 72 | wgpu::BindGroupLayoutEntry { 73 | binding: 0, 74 | visibility: wgpu::ShaderStage::FRAGMENT, 75 | ty: wgpu::BindingType::Sampler { 76 | filtering: true, 77 | comparison: false, 78 | }, 79 | count: None, 80 | }, 81 | // color buffer 82 | wgpu::BindGroupLayoutEntry { 83 | binding: 1, 84 | visibility: wgpu::ShaderStage::FRAGMENT, 85 | ty: wgpu::BindingType::Texture { 86 | view_dimension: wgpu::TextureViewDimension::D2, 87 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 88 | multisampled: true, 89 | }, 90 | count: None, 91 | }, 92 | // PostProcessUniforms 93 | wgpu::BindGroupLayoutEntry { 94 | binding: 2, 95 | visibility: wgpu::ShaderStage::FRAGMENT, 96 | ty: wgpu::BindingType::Buffer { 97 | ty: wgpu::BufferBindingType::Uniform, 98 | has_dynamic_offset: false, 99 | min_binding_size: NonZeroU64::new(size_of::() as u64), 100 | }, 101 | count: None, 102 | }, 103 | ]; 104 | 105 | impl Pipeline for PostProcessPipeline { 106 | type VertexPushConstants = (); 107 | type SharedPushConstants = (); 108 | type FragmentPushConstants = (); 109 | 110 | fn name() -> &'static str { 111 | "postprocess" 112 | } 113 | 114 | fn bind_group_layout_descriptors() -> Vec> { 115 | vec![wgpu::BindGroupLayoutDescriptor { 116 | label: Some("postprocess bind group"), 117 | entries: BIND_GROUP_LAYOUT_ENTRIES, 118 | }] 119 | } 120 | 121 | fn vertex_shader() -> &'static str { 122 | include_str!(concat!( 123 | env!("CARGO_MANIFEST_DIR"), 124 | "/shaders/postprocess.vert" 125 | )) 126 | } 127 | 128 | fn fragment_shader() -> &'static str { 129 | include_str!(concat!( 130 | env!("CARGO_MANIFEST_DIR"), 131 | "/shaders/postprocess.frag" 132 | )) 133 | } 134 | 135 | fn primitive_state() -> wgpu::PrimitiveState { 136 | QuadPipeline::primitive_state() 137 | } 138 | 139 | fn color_target_states() -> Vec { 140 | QuadPipeline::color_target_states() 141 | } 142 | 143 | fn depth_stencil_state() -> Option { 144 | None 145 | } 146 | 147 | fn vertex_buffer_layouts() -> Vec> { 148 | QuadPipeline::vertex_buffer_layouts() 149 | } 150 | } 151 | 152 | pub struct PostProcessRenderer { 153 | bind_group: wgpu::BindGroup, 154 | } 155 | 156 | impl PostProcessRenderer { 157 | pub fn create_bind_group( 158 | state: &GraphicsState, 159 | color_buffer: &wgpu::TextureView, 160 | ) -> wgpu::BindGroup { 161 | state 162 | .device() 163 | .create_bind_group(&wgpu::BindGroupDescriptor { 164 | label: Some("postprocess bind group"), 165 | layout: &state.postprocess_pipeline().bind_group_layouts()[0], 166 | entries: &[ 167 | // sampler 168 | wgpu::BindGroupEntry { 169 | binding: 0, 170 | // TODO: might need a dedicated sampler if downsampling 171 | resource: wgpu::BindingResource::Sampler(state.diffuse_sampler()), 172 | }, 173 | // color buffer 174 | wgpu::BindGroupEntry { 175 | binding: 1, 176 | resource: wgpu::BindingResource::TextureView(color_buffer), 177 | }, 178 | // uniform buffer 179 | wgpu::BindGroupEntry { 180 | binding: 2, 181 | resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding { 182 | buffer: state.postprocess_pipeline().uniform_buffer(), 183 | offset: 0, 184 | size: None, 185 | }), 186 | }, 187 | ], 188 | }) 189 | } 190 | 191 | pub fn new(state: &GraphicsState, color_buffer: &wgpu::TextureView) -> PostProcessRenderer { 192 | let bind_group = Self::create_bind_group(state, color_buffer); 193 | 194 | PostProcessRenderer { bind_group } 195 | } 196 | 197 | pub fn rebuild(&mut self, state: &GraphicsState, color_buffer: &wgpu::TextureView) { 198 | self.bind_group = Self::create_bind_group(state, color_buffer); 199 | } 200 | 201 | pub fn update_uniform_buffers(&self, state: &GraphicsState, color_shift: [f32; 4]) { 202 | // update color shift 203 | state 204 | .queue() 205 | .write_buffer(state.postprocess_pipeline().uniform_buffer(), 0, unsafe { 206 | any_as_bytes(&PostProcessUniforms { color_shift }) 207 | }); 208 | } 209 | 210 | pub fn record_draw<'pass>( 211 | &'pass self, 212 | state: &'pass GraphicsState, 213 | pass: &mut wgpu::RenderPass<'pass>, 214 | color_shift: [f32; 4], 215 | ) { 216 | self.update_uniform_buffers(state, color_shift); 217 | pass.set_pipeline(state.postprocess_pipeline().pipeline()); 218 | pass.set_vertex_buffer(0, state.quad_pipeline().vertex_buffer().slice(..)); 219 | pass.set_bind_group(0, &self.bind_group, &[]); 220 | pass.draw(0..6, 0..1); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/client/sound/music.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Cursor, Read}, 3 | rc::Rc, 4 | }; 5 | 6 | use crate::{client::sound::SoundError, common::vfs::Vfs}; 7 | 8 | use rodio::{Decoder, OutputStreamHandle, Sink, Source}; 9 | 10 | /// Plays music tracks. 11 | pub struct MusicPlayer { 12 | vfs: Rc, 13 | stream: OutputStreamHandle, 14 | playing: Option, 15 | sink: Option, 16 | } 17 | 18 | impl MusicPlayer { 19 | pub fn new(vfs: Rc, stream: OutputStreamHandle) -> MusicPlayer { 20 | MusicPlayer { 21 | vfs, 22 | stream, 23 | playing: None, 24 | sink: None, 25 | } 26 | } 27 | 28 | /// Start playing the track with the given name. 29 | /// 30 | /// Music tracks are expected to be in the "music/" directory of the virtual 31 | /// filesystem, so they can be placed either in an actual directory 32 | /// `"id1/music/"` or packaged in a PAK archive with a path beginning with 33 | /// `"music/"`. 34 | /// 35 | /// If the specified track is already playing, this has no effect. 36 | pub fn play_named(&mut self, name: S) -> Result<(), SoundError> 37 | where 38 | S: AsRef, 39 | { 40 | let name = name.as_ref(); 41 | 42 | // don't replay the same track 43 | if let Some(ref playing) = self.playing { 44 | if playing == name { 45 | return Ok(()); 46 | } 47 | } 48 | 49 | // TODO: there's probably a better way to do this extension check 50 | let mut file = if !name.contains('.') { 51 | // try all supported formats 52 | self.vfs 53 | .open(format!("music/{}.flac", name)) 54 | .or_else(|_| self.vfs.open(format!("music/{}.wav", name))) 55 | .or_else(|_| self.vfs.open(format!("music/{}.mp3", name))) 56 | .or_else(|_| self.vfs.open(format!("music/{}.ogg", name))) 57 | .or(Err(SoundError::NoSuchTrack(name.to_owned())))? 58 | } else { 59 | self.vfs.open(name)? 60 | }; 61 | 62 | let mut data = Vec::new(); 63 | file.read_to_end(&mut data)?; 64 | let source = Decoder::new(Cursor::new(data))? 65 | .convert_samples::() 66 | .buffered() 67 | .repeat_infinite(); 68 | 69 | // stop the old track before starting the new one so there's no overlap 70 | self.sink = None; 71 | // TODO handle PlayError 72 | let new_sink = Sink::try_new(&self.stream).unwrap(); 73 | new_sink.append(source); 74 | self.sink = Some(new_sink); 75 | 76 | Ok(()) 77 | } 78 | 79 | /// Start playing the track with the given number. 80 | /// 81 | /// Note that the first actual music track is track 2; track 1 on the 82 | /// original Quake CD-ROM held the game data. 83 | pub fn play_track(&mut self, track_id: usize) -> Result<(), SoundError> { 84 | self.play_named(format!("track{:02}", track_id)) 85 | } 86 | 87 | /// Stop the current music track. 88 | /// 89 | /// This ceases playback entirely. To pause the track, allowing it to be 90 | /// resumed later, use `MusicPlayer::pause()`. 91 | /// 92 | /// If no music track is currently playing, this has no effect. 93 | pub fn stop(&mut self) { 94 | self.sink = None; 95 | self.playing = None; 96 | } 97 | 98 | /// Pause the current music track. 99 | /// 100 | /// If no music track is currently playing, or if the current track is 101 | /// already paused, this has no effect. 102 | pub fn pause(&mut self) { 103 | if let Some(ref mut sink) = self.sink { 104 | sink.pause(); 105 | } 106 | } 107 | 108 | /// Resume playback of the current music track. 109 | /// 110 | /// If no music track is currently playing, or if the current track is not 111 | /// paused, this has no effect. 112 | pub fn resume(&mut self) { 113 | if let Some(ref mut sink) = self.sink { 114 | sink.play(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/client/trace.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use std::collections::HashMap; 22 | 23 | use serde::Serialize; 24 | 25 | /// Client-side debug tracing. 26 | 27 | #[derive(Serialize)] 28 | pub struct TraceEntity { 29 | pub msg_origins: [[f32; 3]; 2], 30 | pub msg_angles_deg: [[f32; 3]; 2], 31 | pub origin: [f32; 3], 32 | } 33 | 34 | #[derive(Serialize)] 35 | pub struct TraceFrame { 36 | pub msg_times_ms: [i64; 2], 37 | pub time_ms: i64, 38 | pub lerp_factor: f32, 39 | pub entities: HashMap, 40 | } 41 | -------------------------------------------------------------------------------- /src/common/alloc.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::LinkedList, mem}; 2 | 3 | use slab::Slab; 4 | 5 | /// A slab allocator with a linked list of allocations. 6 | /// 7 | /// This allocator trades O(1) random access by key, a property of 8 | /// [`Slab`](slab::Slab), for the ability to iterate only those entries that are 9 | /// actually allocated. This significantly reduces the cost of `retain()`: where 10 | /// `Slab::retain` is O(capacity) regardless of how many values are allocated, 11 | /// [`LinkedSlab::retain`](LinkedSlab::retain) is O(n) in the number of values. 12 | pub struct LinkedSlab { 13 | slab: Slab, 14 | allocated: LinkedList, 15 | } 16 | 17 | impl LinkedSlab { 18 | /// Construct a new, empty `LinkedSlab` with the specified capacity. 19 | /// 20 | /// The returned allocator will be able to store exactly `capacity` without 21 | /// reallocating. If `capacity` is 0, the slab will not allocate. 22 | pub fn with_capacity(capacity: usize) -> LinkedSlab { 23 | LinkedSlab { 24 | slab: Slab::with_capacity(capacity), 25 | allocated: LinkedList::new(), 26 | } 27 | } 28 | 29 | /// Return the number of values the allocator can store without reallocating. 30 | pub fn capacity(&self) -> usize { 31 | self.slab.capacity() 32 | } 33 | 34 | /// Clear the allocator of all values. 35 | pub fn clear(&mut self) { 36 | self.allocated.clear(); 37 | self.slab.clear(); 38 | } 39 | 40 | /// Return the number of stored values. 41 | pub fn len(&self) -> usize { 42 | self.slab.len() 43 | } 44 | 45 | /// Return `true` if there are no values allocated. 46 | pub fn is_empty(&self) -> bool { 47 | self.slab.is_empty() 48 | } 49 | 50 | /// Return an iterator over the allocated values. 51 | pub fn iter(&self) -> impl Iterator { 52 | self.allocated 53 | .iter() 54 | .map(move |key| self.slab.get(*key).unwrap()) 55 | } 56 | 57 | /// Return a reference to the value associated with the given key. 58 | /// 59 | /// If the given key is not associated with a value, then None is returned. 60 | pub fn get(&self, key: usize) -> Option<&T> { 61 | self.slab.get(key) 62 | } 63 | 64 | /// Return a mutable reference to the value associated with the given key. 65 | /// 66 | /// If the given key is not associated with a value, then None is returned. 67 | pub fn get_mut(&mut self, key: usize) -> Option<&mut T> { 68 | self.slab.get_mut(key) 69 | } 70 | 71 | /// Allocate a value, returning the key assigned to the value. 72 | /// 73 | /// This operation is O(1). 74 | pub fn insert(&mut self, val: T) -> usize { 75 | let key = self.slab.insert(val); 76 | self.allocated.push_front(key); 77 | key 78 | } 79 | 80 | /// Remove and return the value associated with the given key. 81 | /// 82 | /// The key is then released and may be associated with future stored values. 83 | /// 84 | /// Note that this operation is O(n) in the number of allocated values. 85 | pub fn remove(&mut self, key: usize) -> T { 86 | self.allocated.drain_filter(|k| *k == key); 87 | self.slab.remove(key) 88 | } 89 | 90 | /// Return `true` if a value is associated with the given key. 91 | pub fn contains(&self, key: usize) -> bool { 92 | self.slab.contains(key) 93 | } 94 | 95 | /// Retain only the elements specified by the predicate. 96 | /// 97 | /// The predicate is permitted to modify allocated values in-place. 98 | /// 99 | /// This operation is O(n) in the number of allocated values. 100 | pub fn retain(&mut self, mut f: F) 101 | where 102 | F: FnMut(usize, &mut T) -> bool, 103 | { 104 | // move contents out to avoid double mutable borrow of self. 105 | // neither LinkedList::new() nor Slab::new() allocates any memory, so 106 | // this is free. 107 | let mut allocated = mem::replace(&mut self.allocated, LinkedList::new()); 108 | let mut slab = mem::replace(&mut self.slab, Slab::new()); 109 | 110 | allocated.drain_filter(|k| { 111 | let retain = match slab.get_mut(*k) { 112 | Some(ref mut v) => f(*k, v), 113 | None => true, 114 | }; 115 | 116 | if !retain { 117 | slab.remove(*k); 118 | } 119 | 120 | !retain 121 | }); 122 | 123 | // put them back 124 | self.slab = slab; 125 | self.allocated = allocated; 126 | } 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::*; 132 | 133 | use std::{collections::HashSet, iter::FromIterator as _}; 134 | 135 | #[test] 136 | fn test_iter() { 137 | let values: Vec = vec![1, 3, 5, 7, 11, 13, 17, 19]; 138 | 139 | let mut linked_slab = LinkedSlab::with_capacity(values.len()); 140 | let mut expected = HashSet::new(); 141 | 142 | for value in values.iter() { 143 | linked_slab.insert(*value); 144 | expected.insert(*value); 145 | } 146 | 147 | let mut actual = HashSet::new(); 148 | for value in linked_slab.iter() { 149 | actual.insert(*value); 150 | } 151 | 152 | assert_eq!(expected, actual); 153 | } 154 | 155 | #[test] 156 | fn test_retain() { 157 | let mut values: Vec = vec![0, 9, 1, 8, 2, 7, 3, 6, 4, 5]; 158 | 159 | let mut linked_slab = LinkedSlab::with_capacity(values.len()); 160 | 161 | for value in values.iter() { 162 | linked_slab.insert(*value); 163 | } 164 | 165 | values.retain(|v| v % 2 == 0); 166 | let mut expected: HashSet = HashSet::from_iter(values.into_iter()); 167 | 168 | linked_slab.retain(|_, v| *v % 2 == 0); 169 | 170 | let mut actual = HashSet::from_iter(linked_slab.iter().map(|v| *v)); 171 | 172 | assert_eq!(expected, actual); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/common/bitset.rs: -------------------------------------------------------------------------------- 1 | pub struct BitSet { 2 | blocks: [u64; N_64], 3 | } 4 | 5 | impl BitSet { 6 | pub fn new() -> Self { 7 | BitSet { blocks: [0; N_64] } 8 | } 9 | 10 | pub fn all_set() -> Self { 11 | BitSet { 12 | blocks: [u64::MAX; N_64], 13 | } 14 | } 15 | 16 | #[inline] 17 | fn bit_location(bit: u64) -> (u64, u64) { 18 | ( 19 | bit >> 6, // divide by 64 20 | 1 << (bit & 63), // modulo 64 21 | ) 22 | } 23 | 24 | #[inline] 25 | pub fn count(&self) -> usize { 26 | let mut count = 0; 27 | 28 | for block in self.blocks { 29 | count += block.count_ones() as usize; 30 | } 31 | 32 | count 33 | } 34 | 35 | #[inline] 36 | pub fn contains(&self, bit: u64) -> bool { 37 | let (index, mask) = Self::bit_location(bit); 38 | self.blocks[index as usize] & mask != 0 39 | } 40 | 41 | #[inline] 42 | pub fn set(&mut self, bit: u64) { 43 | let (index, mask) = Self::bit_location(bit); 44 | self.blocks[index as usize] |= mask; 45 | } 46 | 47 | #[inline] 48 | pub fn clear(&mut self, bit: u64) { 49 | let (index, mask) = Self::bit_location(bit); 50 | self.blocks[index as usize] &= !mask; 51 | } 52 | 53 | #[inline] 54 | pub fn toggle(&mut self, bit: u64) { 55 | let (index, mask) = Self::bit_location(bit); 56 | self.blocks[index as usize] ^= mask; 57 | } 58 | 59 | #[inline] 60 | pub fn iter(&self) -> BitSetIter<'_, N_64> { 61 | BitSetIter::new(&self.blocks) 62 | } 63 | } 64 | 65 | pub struct BitSetIter<'a, const N_64: usize> { 66 | block_index: usize, 67 | block_val: u64, 68 | blocks: &'a [u64; N_64], 69 | } 70 | 71 | impl<'a, const N_64: usize> BitSetIter<'a, N_64> { 72 | fn new(blocks: &'a [u64; N_64]) -> BitSetIter<'_, N_64> { 73 | BitSetIter { 74 | block_index: 0, 75 | block_val: blocks[0], 76 | blocks, 77 | } 78 | } 79 | } 80 | 81 | impl<'a, const N_64: usize> Iterator for BitSetIter<'a, N_64> { 82 | type Item = u64; 83 | 84 | fn next(&mut self) -> Option { 85 | while self.block_index < N_64 { 86 | println!( 87 | "block_index = {} | block_val = {:b}", 88 | self.block_index, self.block_val 89 | ); 90 | 91 | if self.block_val != 0 { 92 | // Locate the next set bit in the block. 93 | let next_bit = self.block_val.trailing_zeros(); 94 | 95 | // Clear the bit. 96 | self.block_val &= !(1 << next_bit); 97 | 98 | // Return it. 99 | return Some((u64::BITS * self.block_index as u32 + next_bit) as u64); 100 | } else { 101 | // No set bits, move to the next block. 102 | self.block_index += 1; 103 | self.block_val = *self.blocks.get(self.block_index)?; 104 | } 105 | } 106 | 107 | None 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::*; 114 | 115 | #[test] 116 | fn test_set_bit() { 117 | let mut bits: BitSet<2> = BitSet::new(); 118 | 119 | let cases = &[0, 1, 63, 64]; 120 | 121 | for case in cases.iter().copied() { 122 | bits.set(case); 123 | assert!(bits.contains(case)); 124 | } 125 | } 126 | 127 | #[test] 128 | fn test_clear_bit() { 129 | let mut bits: BitSet<2> = BitSet::all_set(); 130 | 131 | let cases = &[0, 1, 63, 64]; 132 | 133 | for case in cases.iter().copied() { 134 | bits.clear(case); 135 | assert!(!bits.contains(case)); 136 | } 137 | } 138 | 139 | #[test] 140 | fn test_iter() { 141 | let mut bits: BitSet<8> = BitSet::new(); 142 | 143 | let cases = &[1, 2, 3, 5, 8, 13, 21, 34, 55, 89]; 144 | 145 | for case in cases.iter().cloned() { 146 | bits.set(case); 147 | } 148 | 149 | let back = bits.iter().collect::>(); 150 | 151 | assert_eq!(&cases[..], &back); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/common/engine.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::{fs::File, io::Read}; 19 | 20 | use cgmath::{Deg, Vector3}; 21 | use chrono::Duration; 22 | 23 | // TODO: the palette should be host-specific and loaded alongside pak0.pak (or the latest PAK with a 24 | // palette.lmp) 25 | lazy_static! { 26 | static ref PALETTE: [u8; 768] = { 27 | let mut _palette = [0; 768]; 28 | let mut f = File::open("pak0.pak.d/gfx/palette.lmp").unwrap(); 29 | match f.read(&mut _palette) { 30 | Err(why) => panic!("{}", why), 31 | Ok(768) => _palette, 32 | _ => panic!("Bad read on pak0/gfx/palette.lmp"), 33 | } 34 | }; 35 | } 36 | 37 | pub fn indexed_to_rgba(indices: &[u8]) -> Vec { 38 | let mut rgba = Vec::with_capacity(4 * indices.len()); 39 | for i in 0..indices.len() { 40 | if indices[i] != 0xFF { 41 | for c in 0..3 { 42 | rgba.push(PALETTE[(3 * (indices[i] as usize) + c) as usize]); 43 | } 44 | rgba.push(0xFF); 45 | } else { 46 | for _ in 0..4 { 47 | rgba.push(0x00); 48 | } 49 | } 50 | } 51 | rgba 52 | } 53 | 54 | // TODO: handle this unwrap? i64 can handle ~200,000 years in microseconds 55 | #[inline] 56 | pub fn duration_to_f32(d: Duration) -> f32 { 57 | d.num_microseconds().unwrap() as f32 / 1_000_000.0 58 | } 59 | 60 | #[inline] 61 | pub fn duration_from_f32(f: f32) -> Duration { 62 | Duration::microseconds((f * 1_000_000.0) as i64) 63 | } 64 | 65 | #[inline] 66 | pub fn deg_vector_to_f32_vector(av: Vector3>) -> Vector3 { 67 | Vector3::new(av[0].0, av[1].0, av[2].0) 68 | } 69 | 70 | #[inline] 71 | pub fn deg_vector_from_f32_vector(v: Vector3) -> Vector3> { 72 | Vector3::new(Deg(v[0]), Deg(v[1]), Deg(v[2])) 73 | } 74 | -------------------------------------------------------------------------------- /src/common/host.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use std::cell::{Ref, RefMut}; 22 | 23 | use crate::common::{console::CvarRegistry, engine}; 24 | 25 | use chrono::{DateTime, Duration, Utc}; 26 | use winit::{ 27 | event::{Event, WindowEvent}, 28 | event_loop::{ControlFlow, EventLoopWindowTarget}, 29 | }; 30 | 31 | pub trait Program: Sized { 32 | fn handle_event( 33 | &mut self, 34 | event: Event, 35 | _target: &EventLoopWindowTarget, 36 | control_flow: &mut ControlFlow, 37 | ); 38 | 39 | fn frame(&mut self, frame_duration: Duration); 40 | fn shutdown(&mut self); 41 | fn cvars(&self) -> Ref; 42 | fn cvars_mut(&self) -> RefMut; 43 | } 44 | 45 | pub struct Host

46 | where 47 | P: Program, 48 | { 49 | program: P, 50 | 51 | init_time: DateTime, 52 | prev_frame_time: DateTime, 53 | prev_frame_duration: Duration, 54 | } 55 | 56 | impl

Host

57 | where 58 | P: Program, 59 | { 60 | pub fn new(program: P) -> Host

{ 61 | let init_time = Utc::now(); 62 | program 63 | .cvars_mut() 64 | .register_archive("host_maxfps", "72") 65 | .unwrap(); 66 | 67 | Host { 68 | program, 69 | init_time, 70 | prev_frame_time: init_time, 71 | prev_frame_duration: Duration::zero(), 72 | } 73 | } 74 | 75 | pub fn handle_event( 76 | &mut self, 77 | event: Event, 78 | _target: &EventLoopWindowTarget, 79 | control_flow: &mut ControlFlow, 80 | ) { 81 | match event { 82 | Event::WindowEvent { 83 | event: WindowEvent::CloseRequested, 84 | .. 85 | } => { 86 | self.program.shutdown(); 87 | *control_flow = ControlFlow::Exit; 88 | } 89 | 90 | Event::MainEventsCleared => self.frame(), 91 | Event::Suspended | Event::Resumed => unimplemented!(), 92 | Event::LoopDestroyed => { 93 | // TODO: 94 | // - host_writeconfig 95 | // - others... 96 | } 97 | 98 | e => self.program.handle_event(e, _target, control_flow), 99 | } 100 | } 101 | 102 | pub fn frame(&mut self) { 103 | // TODO: make sure this doesn't cause weirdness with e.g. leap seconds 104 | let new_frame_time = Utc::now(); 105 | self.prev_frame_duration = new_frame_time.signed_duration_since(self.prev_frame_time); 106 | 107 | // if the time elapsed since the last frame is too low, don't run this one yet 108 | let prev_frame_duration = self.prev_frame_duration; 109 | if !self.check_frame_duration(prev_frame_duration) { 110 | // avoid busy waiting if we're running at a really high framerate 111 | std::thread::sleep(std::time::Duration::from_millis(1)); 112 | return; 113 | } 114 | 115 | // we're running this frame, so update the frame time 116 | self.prev_frame_time = new_frame_time; 117 | 118 | self.program.frame(self.prev_frame_duration); 119 | } 120 | 121 | // Returns whether enough time has elapsed to run the next frame. 122 | fn check_frame_duration(&mut self, frame_duration: Duration) -> bool { 123 | let host_maxfps = self 124 | .program 125 | .cvars() 126 | .get_value("host_maxfps") 127 | .unwrap_or(72.0); 128 | let min_frame_duration = engine::duration_from_f32(1.0 / host_maxfps); 129 | frame_duration >= min_frame_duration 130 | } 131 | 132 | pub fn uptime(&self) -> Duration { 133 | self.prev_frame_time.signed_duration_since(self.init_time) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | pub mod alloc; 22 | pub mod bitset; 23 | pub mod bsp; 24 | pub mod console; 25 | pub mod engine; 26 | pub mod host; 27 | pub mod math; 28 | pub mod mdl; 29 | pub mod model; 30 | pub mod net; 31 | pub mod pak; 32 | pub mod parse; 33 | pub mod sprite; 34 | pub mod util; 35 | pub mod vfs; 36 | pub mod wad; 37 | 38 | use std::path::PathBuf; 39 | 40 | pub fn default_base_dir() -> std::path::PathBuf { 41 | match std::env::current_dir() { 42 | Ok(cwd) => cwd, 43 | Err(e) => { 44 | log::error!("cannot access current directory: {}", e); 45 | std::process::exit(1); 46 | } 47 | } 48 | } 49 | 50 | pub const MAX_LIGHTSTYLES: usize = 64; 51 | 52 | /// The maximum number of `.pak` files that should be loaded at runtime. 53 | /// 54 | /// The original engine does not make this restriction, and this limit can be increased if need be. 55 | pub const MAX_PAKFILES: usize = 32; 56 | -------------------------------------------------------------------------------- /src/common/model.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use crate::common::{ 19 | bsp::{BspFileError, BspModel}, 20 | mdl::{self, AliasModel, MdlFileError}, 21 | sprite::{self, SpriteModel}, 22 | vfs::{Vfs, VfsError}, 23 | }; 24 | 25 | use cgmath::Vector3; 26 | use thiserror::Error; 27 | 28 | #[derive(Error, Debug)] 29 | pub enum ModelError { 30 | #[error("BSP file error: {0}")] 31 | BspFile(#[from] BspFileError), 32 | #[error("MDL file error: {0}")] 33 | MdlFile(#[from] MdlFileError), 34 | #[error("SPR file error")] 35 | SprFile, 36 | #[error("Virtual filesystem error: {0}")] 37 | Vfs(#[from] VfsError), 38 | } 39 | 40 | #[derive(Debug, FromPrimitive)] 41 | pub enum SyncType { 42 | Sync = 0, 43 | Rand = 1, 44 | } 45 | 46 | bitflags! { 47 | pub struct ModelFlags: u8 { 48 | const ROCKET = 0b00000001; 49 | const GRENADE = 0b00000010; 50 | const GIB = 0b00000100; 51 | const ROTATE = 0b00001000; 52 | const TRACER = 0b00010000; 53 | const ZOMGIB = 0b00100000; 54 | const TRACER2 = 0b01000000; 55 | const TRACER3 = 0b10000000; 56 | } 57 | } 58 | 59 | #[derive(Debug)] 60 | pub struct Model { 61 | pub name: String, 62 | pub kind: ModelKind, 63 | pub flags: ModelFlags, 64 | } 65 | 66 | #[derive(Debug)] 67 | pub enum ModelKind { 68 | // TODO: find a more elegant way to express the null model 69 | None, 70 | Brush(BspModel), 71 | Alias(AliasModel), 72 | Sprite(SpriteModel), 73 | } 74 | 75 | impl Model { 76 | pub fn none() -> Model { 77 | Model { 78 | name: String::new(), 79 | kind: ModelKind::None, 80 | flags: ModelFlags::empty(), 81 | } 82 | } 83 | 84 | pub fn kind(&self) -> &ModelKind { 85 | &self.kind 86 | } 87 | 88 | pub fn load(vfs: &Vfs, name: S) -> Result 89 | where 90 | S: AsRef, 91 | { 92 | let name = name.as_ref(); 93 | // TODO: original engine uses the magic numbers of each format instead of the extension. 94 | if name.ends_with(".bsp") { 95 | panic!("BSP files may contain multiple models, use bsp::load for this"); 96 | } else if name.ends_with(".mdl") { 97 | Ok(Model::from_alias_model( 98 | name.to_owned(), 99 | mdl::load(vfs.open(name)?)?, 100 | )) 101 | } else if name.ends_with(".spr") { 102 | Ok(Model::from_sprite_model( 103 | name.to_owned(), 104 | sprite::load(vfs.open(name)?), 105 | )) 106 | } else { 107 | panic!("Unrecognized model type: {}", name); 108 | } 109 | } 110 | 111 | /// Construct a new generic model from a brush model. 112 | pub fn from_brush_model(name: S, brush_model: BspModel) -> Model 113 | where 114 | S: AsRef, 115 | { 116 | Model { 117 | name: name.as_ref().to_owned(), 118 | kind: ModelKind::Brush(brush_model), 119 | flags: ModelFlags::empty(), 120 | } 121 | } 122 | 123 | /// Construct a new generic model from an alias model. 124 | pub fn from_alias_model(name: S, alias_model: AliasModel) -> Model 125 | where 126 | S: AsRef, 127 | { 128 | let flags = alias_model.flags(); 129 | 130 | Model { 131 | name: name.as_ref().to_owned(), 132 | kind: ModelKind::Alias(alias_model), 133 | flags, 134 | } 135 | } 136 | 137 | /// Construct a new generic model from a sprite model. 138 | pub fn from_sprite_model(name: S, sprite_model: SpriteModel) -> Model 139 | where 140 | S: AsRef, 141 | { 142 | Model { 143 | name: name.as_ref().to_owned(), 144 | kind: ModelKind::Sprite(sprite_model), 145 | flags: ModelFlags::empty(), 146 | } 147 | } 148 | 149 | /// Return the name of this model. 150 | pub fn name(&self) -> &str { 151 | &self.name 152 | } 153 | 154 | /// Return the minimum extent of this model. 155 | pub fn min(&self) -> Vector3 { 156 | debug!("Retrieving min of model {}", self.name); 157 | match self.kind { 158 | ModelKind::None => panic!("attempted to take min() of NULL model"), 159 | ModelKind::Brush(ref bmodel) => bmodel.min(), 160 | ModelKind::Sprite(ref smodel) => smodel.min(), 161 | 162 | // TODO: maybe change this? 163 | // https://github.com/id-Software/Quake/blob/master/WinQuake/gl_model.c#L1625 164 | ModelKind::Alias(_) => Vector3::new(-16.0, -16.0, -16.0), 165 | } 166 | } 167 | 168 | /// Return the maximum extent of this model. 169 | pub fn max(&self) -> Vector3 { 170 | debug!("Retrieving max of model {}", self.name); 171 | match self.kind { 172 | ModelKind::None => panic!("attempted to take max() of NULL model"), 173 | ModelKind::Brush(ref bmodel) => bmodel.max(), 174 | ModelKind::Sprite(ref smodel) => smodel.max(), 175 | 176 | // TODO: maybe change this? 177 | // https://github.com/id-Software/Quake/blob/master/WinQuake/gl_model.c#L1625 178 | ModelKind::Alias(_) => Vector3::new(16.0, 16.0, 16.0), 179 | } 180 | } 181 | 182 | pub fn sync_type(&self) -> SyncType { 183 | match self.kind { 184 | ModelKind::None => panic!("Attempted to take sync_type() of NULL model"), 185 | ModelKind::Brush(_) => SyncType::Sync, 186 | // TODO: expose sync_type in Sprite and reflect it here 187 | ModelKind::Sprite(ref _smodel) => SyncType::Sync, 188 | // TODO: expose sync_type in Mdl and reflect it here 189 | ModelKind::Alias(ref _amodel) => SyncType::Sync, 190 | } 191 | } 192 | 193 | pub fn flags(&self) -> ModelFlags { 194 | self.flags 195 | } 196 | 197 | pub fn has_flag(&self, flag: ModelFlags) -> bool { 198 | self.flags.contains(flag) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/common/pak.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | //! Quake PAK archive manipulation. 19 | 20 | use std::{ 21 | collections::{hash_map::Iter, HashMap}, 22 | fs, 23 | io::{self, Read, Seek, SeekFrom}, 24 | path::Path, 25 | }; 26 | 27 | use byteorder::{LittleEndian, ReadBytesExt}; 28 | use thiserror::Error; 29 | 30 | const PAK_MAGIC: [u8; 4] = [b'P', b'A', b'C', b'K']; 31 | const PAK_ENTRY_SIZE: usize = 64; 32 | 33 | #[derive(Error, Debug)] 34 | pub enum PakError { 35 | #[error("I/O error: {0}")] 36 | Io(#[from] io::Error), 37 | #[error("Invalid magic number: {0:?}")] 38 | InvalidMagicNumber([u8; 4]), 39 | #[error("Invalid file table offset: {0}")] 40 | InvalidTableOffset(i32), 41 | #[error("Invalid file table size: {0}")] 42 | InvalidTableSize(i32), 43 | #[error("Invalid file offset: {0}")] 44 | InvalidFileOffset(i32), 45 | #[error("Invalid file size: {0}")] 46 | InvalidFileSize(i32), 47 | #[error("File name too long: {0}")] 48 | FileNameTooLong(String), 49 | #[error("Non-UTF-8 file name: {0}")] 50 | NonUtf8FileName(#[from] std::string::FromUtf8Error), 51 | #[error("No such file in PAK archive: {0}")] 52 | NoSuchFile(String), 53 | } 54 | 55 | /// An open Pak archive. 56 | #[derive(Debug)] 57 | pub struct Pak(HashMap>); 58 | 59 | impl Pak { 60 | // TODO: rename to from_path or similar 61 | pub fn new

(path: P) -> Result 62 | where 63 | P: AsRef, 64 | { 65 | debug!("Opening {}", path.as_ref().to_str().unwrap()); 66 | 67 | let mut infile = fs::File::open(path)?; 68 | let mut magic = [0u8; 4]; 69 | infile.read(&mut magic)?; 70 | 71 | if magic != PAK_MAGIC { 72 | Err(PakError::InvalidMagicNumber(magic))?; 73 | } 74 | 75 | // Locate the file table 76 | let table_offset = match infile.read_i32::()? { 77 | o if o <= 0 => Err(PakError::InvalidTableOffset(o))?, 78 | o => o as u32, 79 | }; 80 | 81 | let table_size = match infile.read_i32::()? { 82 | s if s <= 0 || s as usize % PAK_ENTRY_SIZE != 0 => Err(PakError::InvalidTableSize(s))?, 83 | s => s as u32, 84 | }; 85 | 86 | let mut map = HashMap::new(); 87 | 88 | for i in 0..(table_size as usize / PAK_ENTRY_SIZE) { 89 | let entry_offset = table_offset as u64 + (i * PAK_ENTRY_SIZE) as u64; 90 | infile.seek(SeekFrom::Start(entry_offset))?; 91 | 92 | let mut path_bytes = [0u8; 56]; 93 | infile.read(&mut path_bytes)?; 94 | 95 | let file_offset = match infile.read_i32::()? { 96 | o if o <= 0 => Err(PakError::InvalidFileOffset(o))?, 97 | o => o as u32, 98 | }; 99 | 100 | let file_size = match infile.read_i32::()? { 101 | s if s <= 0 => Err(PakError::InvalidFileSize(s))?, 102 | s => s as u32, 103 | }; 104 | 105 | let last = path_bytes 106 | .iter() 107 | .position(|b| *b == 0) 108 | .ok_or(PakError::FileNameTooLong( 109 | String::from_utf8_lossy(&path_bytes).into_owned(), 110 | ))?; 111 | let path = String::from_utf8(path_bytes[0..last].to_vec())?; 112 | infile.seek(SeekFrom::Start(file_offset as u64))?; 113 | 114 | let mut data: Vec = Vec::with_capacity(file_size as usize); 115 | (&mut infile) 116 | .take(file_size as u64) 117 | .read_to_end(&mut data)?; 118 | 119 | map.insert(path, data.into_boxed_slice()); 120 | } 121 | 122 | Ok(Pak(map)) 123 | } 124 | 125 | /// Opens a file in the file tree for reading. 126 | /// 127 | /// # Examples 128 | /// ```no_run 129 | /// # extern crate richter; 130 | /// use richter::common::pak::Pak; 131 | /// 132 | /// # fn main() { 133 | /// let mut pak = Pak::new("pak0.pak").unwrap(); 134 | /// let progs_dat = pak.open("progs.dat").unwrap(); 135 | /// # } 136 | /// ``` 137 | pub fn open(&self, path: S) -> Result<&[u8], PakError> 138 | where 139 | S: AsRef, 140 | { 141 | let path = path.as_ref(); 142 | self.0 143 | .get(path) 144 | .map(|s| s.as_ref()) 145 | .ok_or(PakError::NoSuchFile(path.to_owned())) 146 | } 147 | 148 | pub fn iter<'a>(&self) -> Iter> { 149 | self.0.iter() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/common/parse/map.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::collections::HashMap; 19 | 20 | use crate::common::parse::quoted; 21 | 22 | use nom::{ 23 | bytes::complete::tag, 24 | character::complete::newline, 25 | combinator::{all_consuming, map}, 26 | multi::many0, 27 | sequence::{delimited, separated_pair, terminated}, 28 | }; 29 | 30 | // "name" "value"\n 31 | pub fn entity_attribute(input: &str) -> nom::IResult<&str, (&str, &str)> { 32 | terminated(separated_pair(quoted, tag(" "), quoted), newline)(input) 33 | } 34 | 35 | // { 36 | // "name1" "value1" 37 | // "name2" "value2" 38 | // "name3" "value3" 39 | // } 40 | pub fn entity(input: &str) -> nom::IResult<&str, HashMap<&str, &str>> { 41 | delimited( 42 | terminated(tag("{"), newline), 43 | map(many0(entity_attribute), |attrs| attrs.into_iter().collect()), 44 | terminated(tag("}"), newline), 45 | )(input) 46 | } 47 | 48 | pub fn entities(input: &str) -> Result>, failure::Error> { 49 | let input = input.strip_suffix('\0').unwrap_or(input); 50 | match all_consuming(many0(entity))(input) { 51 | Ok(("", entities)) => Ok(entities), 52 | Ok(_) => unreachable!(), 53 | Err(e) => bail!("parse failed: {}", e), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/common/parse/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | pub mod console; 19 | pub mod map; 20 | 21 | use cgmath::Vector3; 22 | use nom::{ 23 | branch::alt, 24 | bytes::complete::{tag, take_while1}, 25 | character::complete::{alphanumeric1, one_of, space1}, 26 | combinator::map, 27 | sequence::{delimited, tuple}, 28 | }; 29 | use winit::event::ElementState; 30 | 31 | pub use self::{console::commands, map::entities}; 32 | 33 | pub fn non_newline_spaces(input: &str) -> nom::IResult<&str, &str> { 34 | space1(input) 35 | } 36 | 37 | fn string_contents(input: &str) -> nom::IResult<&str, &str> { 38 | take_while1(|c: char| !"\"".contains(c) && c.is_ascii() && !c.is_ascii_control())(input) 39 | } 40 | 41 | pub fn quoted(input: &str) -> nom::IResult<&str, &str> { 42 | delimited(tag("\""), string_contents, tag("\""))(input) 43 | } 44 | 45 | pub fn action(input: &str) -> nom::IResult<&str, (ElementState, &str)> { 46 | tuple(( 47 | map(one_of("+-"), |c| match c { 48 | '+' => ElementState::Pressed, 49 | '-' => ElementState::Released, 50 | _ => unreachable!(), 51 | }), 52 | alphanumeric1, 53 | ))(input) 54 | } 55 | 56 | pub fn newline(input: &str) -> nom::IResult<&str, &str> { 57 | nom::character::complete::line_ending(input) 58 | } 59 | 60 | // TODO: rename to line_terminator and move to console module 61 | pub fn line_ending(input: &str) -> nom::IResult<&str, &str> { 62 | alt((tag(";"), nom::character::complete::line_ending))(input) 63 | } 64 | 65 | pub fn vector3_components(src: S) -> Option<[f32; 3]> 66 | where 67 | S: AsRef, 68 | { 69 | let src = src.as_ref(); 70 | 71 | let components: Vec<_> = src.split(" ").collect(); 72 | if components.len() != 3 { 73 | return None; 74 | } 75 | 76 | let x: f32 = match components[0].parse().ok() { 77 | Some(p) => p, 78 | None => return None, 79 | }; 80 | 81 | let y: f32 = match components[1].parse().ok() { 82 | Some(p) => p, 83 | None => return None, 84 | }; 85 | 86 | let z: f32 = match components[2].parse().ok() { 87 | Some(p) => p, 88 | None => return None, 89 | }; 90 | 91 | Some([x, y, z]) 92 | } 93 | 94 | pub fn vector3(src: S) -> Option> 95 | where 96 | S: AsRef, 97 | { 98 | let src = src.as_ref(); 99 | 100 | let components: Vec<_> = src.split(" ").collect(); 101 | if components.len() != 3 { 102 | return None; 103 | } 104 | 105 | let x: f32 = match components[0].parse().ok() { 106 | Some(p) => p, 107 | None => return None, 108 | }; 109 | 110 | let y: f32 = match components[1].parse().ok() { 111 | Some(p) => p, 112 | None => return None, 113 | }; 114 | 115 | let z: f32 = match components[2].parse().ok() { 116 | Some(p) => p, 117 | None => return None, 118 | }; 119 | 120 | Some(Vector3::new(x, y, z)) 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use super::*; 126 | 127 | #[test] 128 | fn test_quoted() { 129 | let s = "\"hello\""; 130 | assert_eq!(quoted(s), Ok(("", "hello"))) 131 | } 132 | 133 | #[test] 134 | fn test_action() { 135 | let s = "+up"; 136 | assert_eq!(action(s), Ok(("", (ElementState::Pressed, "up")))) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/common/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::mem::size_of; 19 | 20 | use byteorder::{LittleEndian, ReadBytesExt}; 21 | 22 | /// A plain-old-data type. 23 | pub trait Pod: 'static + Copy + Sized + Send + Sync {} 24 | impl Pod for T {} 25 | 26 | /// Read a `[f32; 3]` in little-endian byte order. 27 | pub fn read_f32_3(reader: &mut R) -> Result<[f32; 3], std::io::Error> 28 | where 29 | R: ReadBytesExt, 30 | { 31 | let mut ar = [0.0f32; 3]; 32 | reader.read_f32_into::(&mut ar)?; 33 | Ok(ar) 34 | } 35 | 36 | /// Read a null-terminated sequence of bytes and convert it into a `String`. 37 | /// 38 | /// The zero byte is consumed. 39 | /// 40 | /// ## Panics 41 | /// - If the end of the input is reached before a zero byte is found. 42 | pub fn read_cstring(src: &mut R) -> Result 43 | where 44 | R: std::io::BufRead, 45 | { 46 | let mut bytes: Vec = Vec::new(); 47 | src.read_until(0, &mut bytes).unwrap(); 48 | bytes.pop(); 49 | String::from_utf8(bytes) 50 | } 51 | 52 | pub unsafe fn any_as_bytes(t: &T) -> &[u8] 53 | where 54 | T: Pod, 55 | { 56 | std::slice::from_raw_parts((t as *const T) as *const u8, size_of::()) 57 | } 58 | 59 | pub unsafe fn any_slice_as_bytes(t: &[T]) -> &[u8] 60 | where 61 | T: Pod, 62 | { 63 | std::slice::from_raw_parts(t.as_ptr() as *const u8, size_of::() * t.len()) 64 | } 65 | 66 | pub unsafe fn bytes_as_any(bytes: &[u8]) -> T 67 | where 68 | T: Pod, 69 | { 70 | assert_eq!(bytes.len(), size_of::()); 71 | std::ptr::read_unaligned(bytes.as_ptr() as *const T) 72 | } 73 | 74 | pub unsafe fn any_as_u32_slice(t: &T) -> &[u32] 75 | where 76 | T: Pod, 77 | { 78 | assert!(size_of::() % size_of::() == 0); 79 | std::slice::from_raw_parts( 80 | (t as *const T) as *const u32, 81 | size_of::() / size_of::(), 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/common/vfs.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::{ 19 | fs::File, 20 | io::{self, BufReader, Cursor, Read, Seek, SeekFrom}, 21 | path::{Path, PathBuf}, 22 | }; 23 | 24 | use crate::common::pak::{Pak, PakError}; 25 | 26 | use thiserror::Error; 27 | 28 | #[derive(Error, Debug)] 29 | pub enum VfsError { 30 | #[error("Couldn't load pakfile: {0}")] 31 | Pak(#[from] PakError), 32 | #[error("File does not exist: {0}")] 33 | NoSuchFile(String), 34 | } 35 | 36 | #[derive(Debug)] 37 | enum VfsComponent { 38 | Pak(Pak), 39 | Directory(PathBuf), 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct Vfs { 44 | components: Vec, 45 | } 46 | 47 | impl Vfs { 48 | pub fn new() -> Vfs { 49 | Vfs { 50 | components: Vec::new(), 51 | } 52 | } 53 | 54 | /// Initializes the virtual filesystem using a base directory. 55 | pub fn with_base_dir(base_dir: PathBuf) -> Vfs { 56 | let mut vfs = Vfs::new(); 57 | 58 | let mut game_dir = base_dir; 59 | game_dir.push("id1"); 60 | 61 | if !game_dir.is_dir() { 62 | log::error!(concat!( 63 | "`id1/` directory does not exist! Use the `--base-dir` option with the name of the", 64 | " directory which contains `id1/`." 65 | )); 66 | 67 | std::process::exit(1); 68 | } 69 | 70 | vfs.add_directory(&game_dir).unwrap(); 71 | 72 | // ...then add PAK archives. 73 | let mut num_paks = 0; 74 | let mut pak_path = game_dir; 75 | for vfs_id in 0..crate::common::MAX_PAKFILES { 76 | // Add the file name. 77 | pak_path.push(format!("pak{}.pak", vfs_id)); 78 | 79 | // Keep adding PAKs until we don't find one or we hit MAX_PAKFILES. 80 | if !pak_path.exists() { 81 | // If the lowercase path doesn't exist, try again with uppercase. 82 | pak_path.pop(); 83 | pak_path.push(format!("PAK{}.PAK", vfs_id)); 84 | if !pak_path.exists() { 85 | break; 86 | } 87 | } 88 | 89 | vfs.add_pakfile(&pak_path).unwrap(); 90 | num_paks += 1; 91 | 92 | // Remove the file name, leaving the game directory. 93 | pak_path.pop(); 94 | } 95 | 96 | if num_paks == 0 { 97 | log::warn!("No PAK files found."); 98 | } 99 | 100 | vfs 101 | } 102 | 103 | pub fn add_pakfile

(&mut self, path: P) -> Result<(), VfsError> 104 | where 105 | P: AsRef, 106 | { 107 | let path = path.as_ref(); 108 | self.components.push(VfsComponent::Pak(Pak::new(path)?)); 109 | Ok(()) 110 | } 111 | 112 | pub fn add_directory

(&mut self, path: P) -> Result<(), VfsError> 113 | where 114 | P: AsRef, 115 | { 116 | self.components 117 | .push(VfsComponent::Directory(path.as_ref().to_path_buf())); 118 | Ok(()) 119 | } 120 | 121 | pub fn open(&self, virtual_path: S) -> Result 122 | where 123 | S: AsRef, 124 | { 125 | let vp = virtual_path.as_ref(); 126 | 127 | // iterate in reverse so later PAKs overwrite earlier ones 128 | for c in self.components.iter().rev() { 129 | match c { 130 | VfsComponent::Pak(pak) => { 131 | if let Ok(f) = pak.open(vp) { 132 | return Ok(VirtualFile::PakBacked(Cursor::new(f))); 133 | } 134 | } 135 | 136 | VfsComponent::Directory(path) => { 137 | let mut full_path = path.to_owned(); 138 | full_path.push(vp); 139 | 140 | if let Ok(f) = File::open(full_path) { 141 | return Ok(VirtualFile::FileBacked(BufReader::new(f))); 142 | } 143 | } 144 | } 145 | } 146 | 147 | Err(VfsError::NoSuchFile(vp.to_owned())) 148 | } 149 | } 150 | 151 | pub enum VirtualFile<'a> { 152 | PakBacked(Cursor<&'a [u8]>), 153 | FileBacked(BufReader), 154 | } 155 | 156 | impl<'a> Read for VirtualFile<'a> { 157 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 158 | match self { 159 | VirtualFile::PakBacked(curs) => curs.read(buf), 160 | VirtualFile::FileBacked(file) => file.read(buf), 161 | } 162 | } 163 | } 164 | 165 | impl<'a> Seek for VirtualFile<'a> { 166 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 167 | match self { 168 | VirtualFile::PakBacked(curs) => curs.seek(pos), 169 | VirtualFile::FileBacked(file) => file.seek(pos), 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/common/wad.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | 21 | use std::{ 22 | collections::HashMap, 23 | convert::From, 24 | fmt::{self, Display}, 25 | io::{self, BufReader, Cursor, Read, Seek, SeekFrom}, 26 | }; 27 | 28 | use crate::common::util; 29 | 30 | use byteorder::{LittleEndian, ReadBytesExt}; 31 | use failure::{Backtrace, Context, Error, Fail}; 32 | 33 | // see definition of lumpinfo_t: 34 | // https://github.com/id-Software/Quake/blob/master/WinQuake/wad.h#L54-L63 35 | const LUMPINFO_SIZE: usize = 32; 36 | const MAGIC: u32 = 'W' as u32 | ('A' as u32) << 8 | ('D' as u32) << 16 | ('2' as u32) << 24; 37 | 38 | #[derive(Debug)] 39 | pub struct WadError { 40 | inner: Context, 41 | } 42 | 43 | impl WadError { 44 | pub fn kind(&self) -> WadErrorKind { 45 | *self.inner.get_context() 46 | } 47 | } 48 | 49 | impl From for WadError { 50 | fn from(kind: WadErrorKind) -> Self { 51 | WadError { 52 | inner: Context::new(kind), 53 | } 54 | } 55 | } 56 | 57 | impl From> for WadError { 58 | fn from(inner: Context) -> Self { 59 | WadError { inner } 60 | } 61 | } 62 | 63 | impl From for WadError { 64 | fn from(io_error: io::Error) -> Self { 65 | let kind = io_error.kind(); 66 | match kind { 67 | io::ErrorKind::UnexpectedEof => io_error.context(WadErrorKind::UnexpectedEof).into(), 68 | _ => io_error.context(WadErrorKind::Io).into(), 69 | } 70 | } 71 | } 72 | 73 | impl Fail for WadError { 74 | fn cause(&self) -> Option<&dyn Fail> { 75 | self.inner.cause() 76 | } 77 | 78 | fn backtrace(&self) -> Option<&Backtrace> { 79 | self.inner.backtrace() 80 | } 81 | } 82 | 83 | impl Display for WadError { 84 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 85 | Display::fmt(&self.inner, f) 86 | } 87 | } 88 | 89 | #[derive(Clone, Copy, Eq, PartialEq, Debug, Fail)] 90 | pub enum WadErrorKind { 91 | #[fail(display = "CONCHARS must be loaded with the dedicated function")] 92 | ConcharsUseDedicatedFunction, 93 | #[fail(display = "Invalid magic number")] 94 | InvalidMagicNumber, 95 | #[fail(display = "I/O error")] 96 | Io, 97 | #[fail(display = "No such file in WAD")] 98 | NoSuchFile, 99 | #[fail(display = "Failed to load QPic")] 100 | QPicNotLoaded, 101 | #[fail(display = "Unexpected end of data")] 102 | UnexpectedEof, 103 | } 104 | 105 | pub struct QPic { 106 | width: u32, 107 | height: u32, 108 | indices: Box<[u8]>, 109 | } 110 | 111 | impl QPic { 112 | pub fn load(data: R) -> Result 113 | where 114 | R: Read + Seek, 115 | { 116 | let mut reader = BufReader::new(data); 117 | 118 | let width = reader.read_u32::()?; 119 | let height = reader.read_u32::()?; 120 | 121 | let mut indices = Vec::new(); 122 | (&mut reader) 123 | .take((width * height) as u64) 124 | .read_to_end(&mut indices)?; 125 | 126 | Ok(QPic { 127 | width, 128 | height, 129 | indices: indices.into_boxed_slice(), 130 | }) 131 | } 132 | 133 | pub fn width(&self) -> u32 { 134 | self.width 135 | } 136 | 137 | pub fn height(&self) -> u32 { 138 | self.height 139 | } 140 | 141 | pub fn indices(&self) -> &[u8] { 142 | &self.indices 143 | } 144 | } 145 | 146 | struct LumpInfo { 147 | offset: u32, 148 | size: u32, 149 | name: String, 150 | } 151 | 152 | pub struct Wad { 153 | files: HashMap>, 154 | } 155 | 156 | impl Wad { 157 | pub fn load(data: R) -> Result 158 | where 159 | R: Read + Seek, 160 | { 161 | let mut reader = BufReader::new(data); 162 | 163 | let magic = reader.read_u32::()?; 164 | if magic != MAGIC { 165 | return Err(WadErrorKind::InvalidMagicNumber.into()); 166 | } 167 | 168 | let lump_count = reader.read_u32::()?; 169 | let lumpinfo_ofs = reader.read_u32::()?; 170 | 171 | reader.seek(SeekFrom::Start(lumpinfo_ofs as u64))?; 172 | 173 | let mut lump_infos = Vec::new(); 174 | 175 | for _ in 0..lump_count { 176 | // TODO sanity check these values 177 | let offset = reader.read_u32::()?; 178 | let _size_on_disk = reader.read_u32::()?; 179 | let size = reader.read_u32::()?; 180 | let _type = reader.read_u8()?; 181 | let _compression = reader.read_u8()?; 182 | let _pad = reader.read_u16::()?; 183 | let mut name_bytes = [0u8; 16]; 184 | reader.read_exact(&mut name_bytes)?; 185 | let name_lossy = String::from_utf8_lossy(&name_bytes); 186 | debug!("name: {}", name_lossy); 187 | let name = util::read_cstring(&mut BufReader::new(Cursor::new(name_bytes)))?; 188 | 189 | lump_infos.push(LumpInfo { offset, size, name }); 190 | } 191 | 192 | let mut files = HashMap::new(); 193 | 194 | for lump_info in lump_infos { 195 | let mut data = Vec::with_capacity(lump_info.size as usize); 196 | reader.seek(SeekFrom::Start(lump_info.offset as u64))?; 197 | (&mut reader) 198 | .take(lump_info.size as u64) 199 | .read_to_end(&mut data)?; 200 | files.insert(lump_info.name.to_owned(), data.into_boxed_slice()); 201 | } 202 | 203 | Ok(Wad { files }) 204 | } 205 | 206 | pub fn open_conchars(&self) -> Result { 207 | match self.files.get("CONCHARS") { 208 | Some(ref data) => { 209 | let width = 128; 210 | let height = 128; 211 | let indices = Vec::from(&data[..(width * height) as usize]); 212 | 213 | Ok(QPic { 214 | width, 215 | height, 216 | indices: indices.into_boxed_slice(), 217 | }) 218 | } 219 | 220 | None => bail!("conchars not found in WAD"), 221 | } 222 | } 223 | 224 | pub fn open_qpic(&self, name: S) -> Result 225 | where 226 | S: AsRef, 227 | { 228 | if name.as_ref() == "CONCHARS" { 229 | Err(WadErrorKind::ConcharsUseDedicatedFunction)? 230 | } 231 | 232 | match self.files.get(name.as_ref()) { 233 | Some(ref data) => QPic::load(Cursor::new(data)), 234 | None => Err(WadErrorKind::NoSuchFile.into()), 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | #![deny(unused_must_use)] 19 | #![feature(drain_filter)] 20 | 21 | #[macro_use] 22 | extern crate bitflags; 23 | extern crate byteorder; 24 | extern crate cgmath; 25 | extern crate chrono; 26 | extern crate env_logger; 27 | #[macro_use] 28 | extern crate failure; 29 | #[macro_use] 30 | extern crate lazy_static; 31 | #[macro_use] 32 | extern crate log; 33 | extern crate num; 34 | #[macro_use] 35 | extern crate num_derive; 36 | extern crate rand; 37 | extern crate regex; 38 | extern crate rodio; 39 | extern crate winit; 40 | 41 | pub mod client; 42 | pub mod common; 43 | pub mod server; 44 | -------------------------------------------------------------------------------- /src/server/precache.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use arrayvec::{ArrayString, ArrayVec}; 4 | 5 | /// Maximum permitted length of a precache path. 6 | const MAX_PRECACHE_PATH: usize = 64; 7 | 8 | const MAX_PRECACHE_ENTRIES: usize = 256; 9 | 10 | /// A list of resources to be loaded before entering the game. 11 | /// 12 | /// This is used by the server to inform clients which resources (sounds and 13 | /// models) they should load before joining. It also serves as the canonical 14 | /// mapping of resource IDs for a given level. 15 | // TODO: ideally, this is parameterized by the maximum number of entries, but 16 | // it's not currently possible to do { MAX_PRECACHE_PATH * N } where N is a 17 | // const generic parameter. In practice both models and sounds have a maximum 18 | // value of 256. 19 | #[derive(Debug)] 20 | pub struct Precache { 21 | str_data: ArrayString<{ MAX_PRECACHE_PATH * MAX_PRECACHE_ENTRIES }>, 22 | items: ArrayVec, MAX_PRECACHE_ENTRIES>, 23 | } 24 | 25 | impl Precache { 26 | /// Creates a new empty `Precache`. 27 | pub fn new() -> Precache { 28 | Precache { 29 | str_data: ArrayString::new(), 30 | items: ArrayVec::new(), 31 | } 32 | } 33 | 34 | /// Retrieves an item from the precache if the item exists. 35 | pub fn get(&self, index: usize) -> Option<&str> { 36 | if index > self.items.len() { 37 | return None; 38 | } 39 | 40 | let range = self.items[index].clone(); 41 | Some(&self.str_data[range]) 42 | } 43 | 44 | /// Returns the index of the target value if it exists. 45 | pub fn find(&self, target: S) -> Option 46 | where 47 | S: AsRef, 48 | { 49 | let (idx, _) = self 50 | .iter() 51 | .enumerate() 52 | .find(|&(_, item)| item == target.as_ref())?; 53 | Some(idx) 54 | } 55 | 56 | /// Adds an item to the precache. 57 | /// 58 | /// If the item already exists in the precache, this has no effect. 59 | pub fn precache(&mut self, item: S) 60 | where 61 | S: AsRef, 62 | { 63 | let item = item.as_ref(); 64 | 65 | if item.len() > MAX_PRECACHE_PATH { 66 | panic!( 67 | "precache name (\"{}\") too long: max length is {}", 68 | item, MAX_PRECACHE_PATH 69 | ); 70 | } 71 | 72 | if self.find(item).is_some() { 73 | // Already precached. 74 | return; 75 | } 76 | 77 | let start = self.str_data.len(); 78 | self.str_data.push_str(item); 79 | let end = self.str_data.len(); 80 | 81 | self.items.push(start..end); 82 | } 83 | 84 | /// Returns an iterator over the values in the precache. 85 | pub fn iter(&self) -> impl Iterator { 86 | self.items 87 | .iter() 88 | .cloned() 89 | .map(move |range| &self.str_data[range]) 90 | } 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | 97 | #[test] 98 | fn test_precache_one() { 99 | let mut p = Precache::new(); 100 | 101 | p.precache("hello"); 102 | assert_eq!(Some("hello"), p.get(0)); 103 | } 104 | 105 | #[test] 106 | fn test_precache_several() { 107 | let mut p = Precache::new(); 108 | 109 | let items = &["Quake", "is", "a", "1996", "first-person", "shooter"]; 110 | 111 | for item in items { 112 | p.precache(item); 113 | } 114 | 115 | // Pick an element in the middle 116 | assert_eq!(Some("first-person"), p.get(4)); 117 | 118 | // Check all the elements 119 | for (precached, &original) in p.iter().zip(items.iter()) { 120 | assert_eq!(precached, original); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/server/progs/functions.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | use std::{cell::RefCell, convert::TryInto, rc::Rc}; 19 | 20 | use num::FromPrimitive; 21 | 22 | use crate::server::progs::{ops::Opcode, ProgsError, StringId, StringTable}; 23 | 24 | pub const MAX_ARGS: usize = 8; 25 | 26 | #[derive(Clone, Debug)] 27 | #[repr(C)] 28 | pub struct Statement { 29 | pub opcode: Opcode, 30 | pub arg1: i16, 31 | pub arg2: i16, 32 | pub arg3: i16, 33 | } 34 | 35 | impl Statement { 36 | pub fn new(op: i16, arg1: i16, arg2: i16, arg3: i16) -> Result { 37 | let opcode = match Opcode::from_i16(op) { 38 | Some(o) => o, 39 | None => return Err(ProgsError::with_msg(format!("Bad opcode 0x{:x}", op))), 40 | }; 41 | 42 | Ok(Statement { 43 | opcode, 44 | arg1, 45 | arg2, 46 | arg3, 47 | }) 48 | } 49 | } 50 | 51 | #[derive(Copy, Clone, Debug, Default, PartialEq)] 52 | #[repr(C)] 53 | pub struct FunctionId(pub usize); 54 | 55 | impl TryInto for FunctionId { 56 | type Error = ProgsError; 57 | 58 | fn try_into(self) -> Result { 59 | if self.0 > ::std::i32::MAX as usize { 60 | Err(ProgsError::with_msg("function ID out of range")) 61 | } else { 62 | Ok(self.0 as i32) 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | pub enum FunctionKind { 69 | BuiltIn(BuiltinFunctionId), 70 | QuakeC(usize), 71 | } 72 | 73 | #[derive(Copy, Clone, Debug, FromPrimitive)] 74 | pub enum BuiltinFunctionId { 75 | // pr_builtin[0] is the null function 76 | MakeVectors = 1, 77 | SetOrigin = 2, 78 | SetModel = 3, 79 | SetSize = 4, 80 | // pr_builtin[5] (PF_setabssize) was never implemented 81 | Break = 6, 82 | Random = 7, 83 | Sound = 8, 84 | Normalize = 9, 85 | Error = 10, 86 | ObjError = 11, 87 | VLen = 12, 88 | VecToYaw = 13, 89 | Spawn = 14, 90 | Remove = 15, 91 | TraceLine = 16, 92 | CheckClient = 17, 93 | Find = 18, 94 | PrecacheSound = 19, 95 | PrecacheModel = 20, 96 | StuffCmd = 21, 97 | FindRadius = 22, 98 | BPrint = 23, 99 | SPrint = 24, 100 | DPrint = 25, 101 | FToS = 26, 102 | VToS = 27, 103 | CoreDump = 28, 104 | TraceOn = 29, 105 | TraceOff = 30, 106 | EPrint = 31, 107 | WalkMove = 32, 108 | // pr_builtin[33] is not implemented 109 | DropToFloor = 34, 110 | LightStyle = 35, 111 | RInt = 36, 112 | Floor = 37, 113 | Ceil = 38, 114 | // pr_builtin[39] is not implemented 115 | CheckBottom = 40, 116 | PointContents = 41, 117 | // pr_builtin[42] is not implemented 118 | FAbs = 43, 119 | Aim = 44, 120 | Cvar = 45, 121 | LocalCmd = 46, 122 | NextEnt = 47, 123 | Particle = 48, 124 | ChangeYaw = 49, 125 | // pr_builtin[50] is not implemented 126 | VecToAngles = 51, 127 | WriteByte = 52, 128 | WriteChar = 53, 129 | WriteShort = 54, 130 | WriteLong = 55, 131 | WriteCoord = 56, 132 | WriteAngle = 57, 133 | WriteString = 58, 134 | WriteEntity = 59, 135 | // pr_builtin[60] through pr_builtin[66] are only defined for Quake 2 136 | MoveToGoal = 67, 137 | PrecacheFile = 68, 138 | MakeStatic = 69, 139 | ChangeLevel = 70, 140 | // pr_builtin[71] is not implemented 141 | CvarSet = 72, 142 | CenterPrint = 73, 143 | AmbientSound = 74, 144 | PrecacheModel2 = 75, 145 | PrecacheSound2 = 76, 146 | PrecacheFile2 = 77, 147 | SetSpawnArgs = 78, 148 | } 149 | 150 | #[derive(Debug)] 151 | pub struct FunctionDef { 152 | pub kind: FunctionKind, 153 | pub arg_start: usize, 154 | pub locals: usize, 155 | pub name_id: StringId, 156 | pub srcfile_id: StringId, 157 | pub argc: usize, 158 | pub argsz: [u8; MAX_ARGS], 159 | } 160 | 161 | #[derive(Debug)] 162 | pub struct Functions { 163 | pub string_table: Rc>, 164 | pub defs: Box<[FunctionDef]>, 165 | pub statements: Box<[Statement]>, 166 | } 167 | 168 | impl Functions { 169 | pub fn id_from_i32(&self, value: i32) -> Result { 170 | if value < 0 { 171 | return Err(ProgsError::with_msg("id < 0")); 172 | } 173 | 174 | if (value as usize) < self.defs.len() { 175 | Ok(FunctionId(value as usize)) 176 | } else { 177 | Err(ProgsError::with_msg(format!( 178 | "no function with ID {}", 179 | value 180 | ))) 181 | } 182 | } 183 | 184 | pub fn get_def(&self, id: FunctionId) -> Result<&FunctionDef, ProgsError> { 185 | if id.0 >= self.defs.len() { 186 | Err(ProgsError::with_msg(format!( 187 | "No function with ID {}", 188 | id.0 189 | ))) 190 | } else { 191 | Ok(&self.defs[id.0]) 192 | } 193 | } 194 | 195 | pub fn find_function_by_name(&self, name: S) -> Result 196 | where 197 | S: AsRef, 198 | { 199 | for (i, def) in self.defs.iter().enumerate() { 200 | let strs = self.string_table.borrow(); 201 | let f_name = strs.get(def.name_id).ok_or_else(|| { 202 | ProgsError::with_msg(format!("No string with ID {:?}", def.name_id)) 203 | })?; 204 | if f_name == name.as_ref() { 205 | return Ok(FunctionId(i)); 206 | } 207 | } 208 | 209 | Err(ProgsError::with_msg(format!( 210 | "No function named {}", 211 | name.as_ref() 212 | ))) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/server/progs/ops.rs: -------------------------------------------------------------------------------- 1 | // Copyright © 2018 Cormac O'Brien. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 4 | // and associated documentation files (the "Software"), to deal in the Software without 5 | // restriction, including without limitation the rights to use, copy, modify, merge, publish, 6 | // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the 7 | // Software is furnished to do so, subject to the following conditions: 8 | // 9 | // The above copyright notice and this permission notice shall be included in all copies or 10 | // substantial portions of the Software. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | #[derive(Copy, Clone, Debug, FromPrimitive, PartialEq)] 19 | #[repr(i16)] 20 | pub enum Opcode { 21 | Done = 0, 22 | MulF = 1, 23 | MulV = 2, 24 | MulFV = 3, 25 | MulVF = 4, 26 | Div = 5, 27 | AddF = 6, 28 | AddV = 7, 29 | SubF = 8, 30 | SubV = 9, 31 | EqF = 10, 32 | EqV = 11, 33 | EqS = 12, 34 | EqEnt = 13, 35 | EqFnc = 14, 36 | NeF = 15, 37 | NeV = 16, 38 | NeS = 17, 39 | NeEnt = 18, 40 | NeFnc = 19, 41 | Le = 20, 42 | Ge = 21, 43 | Lt = 22, 44 | Gt = 23, 45 | LoadF = 24, 46 | LoadV = 25, 47 | LoadS = 26, 48 | LoadEnt = 27, 49 | LoadFld = 28, 50 | LoadFnc = 29, 51 | Address = 30, 52 | StoreF = 31, 53 | StoreV = 32, 54 | StoreS = 33, 55 | StoreEnt = 34, 56 | StoreFld = 35, 57 | StoreFnc = 36, 58 | StorePF = 37, 59 | StorePV = 38, 60 | StorePS = 39, 61 | StorePEnt = 40, 62 | StorePFld = 41, 63 | StorePFnc = 42, 64 | Return = 43, 65 | NotF = 44, 66 | NotV = 45, 67 | NotS = 46, 68 | NotEnt = 47, 69 | NotFnc = 48, 70 | If = 49, 71 | IfNot = 50, 72 | Call0 = 51, 73 | Call1 = 52, 74 | Call2 = 53, 75 | Call3 = 54, 76 | Call4 = 55, 77 | Call5 = 56, 78 | Call6 = 57, 79 | Call7 = 58, 80 | Call8 = 59, 81 | State = 60, 82 | Goto = 61, 83 | And = 62, 84 | Or = 63, 85 | BitAnd = 64, 86 | BitOr = 65, 87 | } 88 | -------------------------------------------------------------------------------- /src/server/progs/string_table.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap}; 2 | 3 | use crate::server::progs::{ProgsError, StringId}; 4 | 5 | #[derive(Debug)] 6 | pub struct StringTable { 7 | /// Interned string data. 8 | data: String, 9 | 10 | /// Caches string lengths for faster lookup. 11 | lengths: RefCell>, 12 | } 13 | 14 | impl StringTable { 15 | pub fn new(data: Vec) -> StringTable { 16 | StringTable { 17 | data: String::from_utf8(data).unwrap(), 18 | lengths: RefCell::new(HashMap::new()), 19 | } 20 | } 21 | 22 | pub fn id_from_i32(&self, value: i32) -> Result { 23 | if value < 0 { 24 | return Err(ProgsError::with_msg("id < 0")); 25 | } 26 | 27 | let id = StringId(value as usize); 28 | 29 | if id.0 < self.data.len() { 30 | Ok(id) 31 | } else { 32 | Err(ProgsError::with_msg(format!("no string with ID {}", value))) 33 | } 34 | } 35 | 36 | pub fn find(&self, target: S) -> Option 37 | where 38 | S: AsRef, 39 | { 40 | let target = target.as_ref(); 41 | for (ofs, _) in target.char_indices() { 42 | let sub = &self.data[ofs..]; 43 | if !sub.starts_with(target) { 44 | continue; 45 | } 46 | 47 | // Make sure the string is NUL-terminated. Otherwise, this could 48 | // erroneously return the StringId of a String whose first 49 | // `target.len()` bytes were equal to `target`, but which had 50 | // additional bytes. 51 | if sub.as_bytes().get(target.len()) != Some(&0) { 52 | continue; 53 | } 54 | 55 | return Some(StringId(ofs)); 56 | } 57 | 58 | None 59 | } 60 | 61 | pub fn get(&self, id: StringId) -> Option<&str> { 62 | let start = id.0; 63 | 64 | if start >= self.data.len() { 65 | return None; 66 | } 67 | 68 | if let Some(len) = self.lengths.borrow().get(&id) { 69 | let end = start + len; 70 | return Some(&self.data[start..end]); 71 | } 72 | 73 | match (&self.data[start..]) 74 | .chars() 75 | .take(1024 * 1024) 76 | .enumerate() 77 | .find(|&(_i, c)| c == '\0') 78 | { 79 | Some((len, _)) => { 80 | self.lengths.borrow_mut().insert(id, len); 81 | let end = start + len; 82 | Some(&self.data[start..end]) 83 | } 84 | None => panic!("string data not NUL-terminated!"), 85 | } 86 | } 87 | 88 | pub fn insert(&mut self, s: S) -> StringId 89 | where 90 | S: AsRef, 91 | { 92 | let s = s.as_ref(); 93 | 94 | assert!(!s.contains('\0')); 95 | 96 | let id = StringId(self.data.len()); 97 | self.data.push_str(s); 98 | self.lengths.borrow_mut().insert(id, s.len()); 99 | id 100 | } 101 | 102 | pub fn find_or_insert(&mut self, target: S) -> StringId 103 | where 104 | S: AsRef, 105 | { 106 | match self.find(target.as_ref()) { 107 | Some(id) => id, 108 | None => self.insert(target), 109 | } 110 | } 111 | 112 | pub fn iter(&self) -> impl Iterator { 113 | self.data.split('\0') 114 | } 115 | } 116 | --------------------------------------------------------------------------------