quake-rb 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +136 -0
- data/bin/quake +18 -1
- data/lib/quake/bsp/reader.rb +241 -38
- data/lib/quake/bsp/types.rb +49 -5
- data/lib/quake/bsp/vis.rb +2 -137
- data/lib/quake/camera.rb +73 -16
- data/lib/quake/entity.rb +413 -25
- data/lib/quake/game/brush_entities.rb +1814 -65
- data/lib/quake/game/engine.rb +4376 -57
- data/lib/quake/game/item_pickups.rb +584 -33
- data/lib/quake/game/player_state.rb +518 -21
- data/lib/quake/mdl/reader.rb +88 -7
- data/lib/quake/mdl/types.rb +2 -2
- data/lib/quake/pak/reader.rb +9 -3
- data/lib/quake/palette.rb +3 -4
- data/lib/quake/physics/hull_trace.rb +77 -4
- data/lib/quake/physics/player.rb +409 -112
- data/lib/quake/renderer/anorm_dots.rb +554 -0
- data/lib/quake/renderer/gl_alias_model.rb +418 -69
- data/lib/quake/renderer/gl_brush_model.rb +129 -17
- data/lib/quake/renderer/gl_hud.rb +384 -31
- data/lib/quake/renderer/gl_lightmap.rb +224 -48
- data/lib/quake/renderer/gl_particles.rb +390 -50
- data/lib/quake/renderer/gl_sky.rb +83 -10
- data/lib/quake/renderer/gl_texture_manager.rb +38 -4
- data/lib/quake/renderer/gl_textured.rb +53 -31
- data/lib/quake/renderer/gl_view_blend.rb +130 -0
- data/lib/quake/renderer/gl_viewmodel.rb +46 -11
- data/lib/quake/renderer/gl_warp_subdivision.rb +74 -0
- data/lib/quake/renderer/gl_water.rb +4 -76
- data/lib/quake/sound/events.rb +126 -2
- data/lib/quake/sound/mixer.rb +44 -9
- data/lib/quake/version.rb +1 -1
- data/lib/quake/wad/reader.rb +18 -8
- data/lib/quake/window.rb +3 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc20d9efbfef9cb809f874ff1b00448ba06e58e0fec05d756ed98b9a1104a5e7
|
|
4
|
+
data.tar.gz: 63a4aa60adb6ec8d5ab243195afdf22de9a405c6050d7a1997d50d6def884b6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e3d20548d6671537bd2f22dd78bbbd09d6b6c6527ff0ea36a12450afbfcf02f73df3bd9b122cf4fc6aaffd893039e9a1516b21c0813fd7787fcdbdcff0f727ab
|
|
7
|
+
data.tar.gz: 7ab39626446b4998f5f8df44ae5945e497496651030d922828841bff5bdc908a325f189a4cd7a8ad19db90f47a45789a60b66401c5667ff4ff8ca09210b10df0
|
data/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# quake-rb
|
|
2
|
+
|
|
3
|
+
A port of the [Quake (1996)](https://en.wikipedia.org/wiki/Quake_(video_game)) engine to Ruby. Reads original `.pak`/`.bsp`/`.mdl` assets and renders them via OpenGL + SDL2.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Mostly as a counterpart to [doom](https://github.com/khasinski/doom) — to see how far you can take id Software's classic engines in plain Ruby, and to learn the BSP/PVS/lightmap pipeline by writing it from the spec rather than reading the C.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bundle install
|
|
15
|
+
bundle exec bin/quake
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
On first run, with no `data/id1/pak0.pak` present, you'll be asked to download the freely-redistributable shareware Quake (Episode 1, ~17 MB). It's pulled from `archive.org/quakeshareware/QUAKE_SW.zip` and extracted into `./data/id1/pak0.pak`. You can also drop your own registered `pak0.pak` (and `pak1.pak`) in there manually.
|
|
19
|
+
|
|
20
|
+
System requirements:
|
|
21
|
+
|
|
22
|
+
- Ruby 3.1+
|
|
23
|
+
- SDL2 (`brew install sdl2` / `apt install libsdl2-dev`)
|
|
24
|
+
- OpenGL 2.1
|
|
25
|
+
- `unzip` (used by the shareware fetcher)
|
|
26
|
+
|
|
27
|
+
## Controls
|
|
28
|
+
|
|
29
|
+
| Key | Action |
|
|
30
|
+
|----------------|--------|
|
|
31
|
+
| WASD | Move |
|
|
32
|
+
| Mouse | Look |
|
|
33
|
+
| Space | Jump |
|
|
34
|
+
| C | Crouch |
|
|
35
|
+
| 1–8 / scroll | Weapon select |
|
|
36
|
+
| N | Toggle noclip |
|
|
37
|
+
| L | Dump player pose + save a screenshot to `debug/shots/` (debug aid) |
|
|
38
|
+
| Esc | Quit |
|
|
39
|
+
|
|
40
|
+
## What works
|
|
41
|
+
|
|
42
|
+
- BSP loading + face extraction with texture coordinates
|
|
43
|
+
- Texture mapping with the original 256-colour palette
|
|
44
|
+
- Static lightmaps from the lighting lump
|
|
45
|
+
- PVS culling, including:
|
|
46
|
+
- leaf 0 fallback (treat solid leaf as all-visible, à la TyrQuake)
|
|
47
|
+
- FatPVS expansion when the camera leaf touches a liquid surface
|
|
48
|
+
- **vis-through-water**: liquid faces in PVS bring the leaves on the other side of their plane into view, so translucent water shows the cave bed instead of clear-color void
|
|
49
|
+
- Sky rendering: two-layer scrolling cloud + back layer, PVS-culled
|
|
50
|
+
- Water/slime/lava: turbsin-warped texture, alpha-blended translucency, backface culling for the duplicate front/back face pairs Quake's BSP compiler emits
|
|
51
|
+
- Brush entities (doors, plats, buttons, triggers) with their own transforms
|
|
52
|
+
- MDL alias models with frame interpolation
|
|
53
|
+
- Particle effects + viewmodel + HUD
|
|
54
|
+
- Player physics: walk, crouch, jump, friction, water levels, platform riding
|
|
55
|
+
- Item pickups, weapon switching, Quake-style player weapon firing, shareware audio mixer
|
|
56
|
+
- Common QuakeC trigger behavior at engine level, including target firing,
|
|
57
|
+
killtarget, counters, hurt/push/teleport triggers, and changelevel requests
|
|
58
|
+
|
|
59
|
+
## What doesn't (yet)
|
|
60
|
+
|
|
61
|
+
- Multiplayer / netcode
|
|
62
|
+
- QuakeC server program — game logic is faked at engine level
|
|
63
|
+
- Full savegame/spawn parameter preservation across episode/level transitions
|
|
64
|
+
- Mirrors, particle warp, dynamic lights
|
|
65
|
+
- Sky cube / skybox replacement
|
|
66
|
+
- Demo playback
|
|
67
|
+
|
|
68
|
+
## Maps
|
|
69
|
+
|
|
70
|
+
The shareware PAK ships:
|
|
71
|
+
|
|
72
|
+
`start`, `e1m1`, `e1m2`, `e1m3`, `e1m4`, `e1m5`, `e1m6`, `e1m7`, `e1m8`
|
|
73
|
+
|
|
74
|
+
Pass `-map maps/e1m3.bsp` to load a different one.
|
|
75
|
+
|
|
76
|
+
## Architecture
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
lib/quake/
|
|
80
|
+
bsp/ reader, types, face vertices, PVS / FatPVS / vis-through-water
|
|
81
|
+
pak/ pak0.pak reader
|
|
82
|
+
mdl/ alias model reader
|
|
83
|
+
wad/ gfx.wad (HUD/menu graphics) reader
|
|
84
|
+
palette.rb 256-colour palette → RGBA
|
|
85
|
+
math/vec3.rb plain Ruby vector
|
|
86
|
+
physics/ hull trace + Player (walk/swim/jump/noclip)
|
|
87
|
+
game/
|
|
88
|
+
engine.rb orchestrates renderers + physics + entity simulation
|
|
89
|
+
brush_entities entity-driven door/plat/button state machines
|
|
90
|
+
item_pickups touchable items
|
|
91
|
+
player_state weapons, ammo, health, armor
|
|
92
|
+
renderer/
|
|
93
|
+
gl_textured opaque world brushes (with lightmap pass)
|
|
94
|
+
gl_brush_model sub-model brushes (doors etc.) at entity transforms
|
|
95
|
+
gl_alias_model .mdl monsters + items
|
|
96
|
+
gl_water *water0/*slime0/*lava0/*teleport with turbsin warp
|
|
97
|
+
gl_sky two-layer scrolling sky
|
|
98
|
+
gl_lightmap lightmap atlas builder
|
|
99
|
+
gl_texture_manager mipmapped diffuse texture cache
|
|
100
|
+
gl_particles explosion/spark sprites
|
|
101
|
+
gl_viewmodel first-person weapon
|
|
102
|
+
gl_hud statusbar + numbers
|
|
103
|
+
sound/ SDL_mixer-backed sound effects
|
|
104
|
+
pak_downloader.rb shareware fetcher
|
|
105
|
+
version.rb VERSION = "0.1.0"
|
|
106
|
+
bin/
|
|
107
|
+
quake interactive game
|
|
108
|
+
quake-debug headless DSL runner (`teleport`, `screenshot`, `dump` etc.)
|
|
109
|
+
test/ minitest suite
|
|
110
|
+
debug/scripts/ ad-hoc investigation scripts (gitignored)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The headless runner is genuinely useful for working on the engine without a window — see `debug/scripts/*` for examples (water visibility traces, leaf inspection, BSP dumps).
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
# Run the tests
|
|
119
|
+
bundle exec ruby -Itest -Ilib -e 'Dir["test/**/*_test.rb"].each { |f| require File.expand_path(f) }'
|
|
120
|
+
|
|
121
|
+
# Run a headless debug script (renders to a hidden window)
|
|
122
|
+
bundle exec bin/quake-debug debug/scripts/water_real.rb
|
|
123
|
+
|
|
124
|
+
# Run the game with YJIT
|
|
125
|
+
bundle exec bin/quake --yjit
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Status
|
|
129
|
+
|
|
130
|
+
This is a hobby project. It can comfortably walk you through `e1m1`, fire Quake-style player weapons, pick up items, and use a growing subset of QuakeC brush/trigger behavior. The QuakeC server program is still not implemented, so game logic is ported incrementally at engine level and monsters don't fight back yet.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
GPL-2.0-only, matching the original Quake source release.
|
|
135
|
+
|
|
136
|
+
The shareware Quake data is © 1996 id Software, freely redistributable. The registered Quake data (if you supply your own `pak0.pak` + `pak1.pak`) is not redistributable; you must own a copy.
|
data/bin/quake
CHANGED
|
@@ -65,9 +65,12 @@ keys = {}
|
|
|
65
65
|
running = true
|
|
66
66
|
last_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
67
|
|
|
68
|
-
puts "Controls: WASD = move, Mouse = look, Space = jump, C = crouch, N = noclip, 1-8/scroll = weapons, ESC = quit"
|
|
68
|
+
puts "Controls: WASD = move, Mouse = look, LMB/Ctrl = fire, Space = jump, C = crouch, N = noclip, 1-8/scroll = weapons, +/- = HUD size, ESC = quit"
|
|
69
69
|
puts "Movement mode: #{noclip ? 'noclip' : 'normal'}"
|
|
70
70
|
|
|
71
|
+
mouse_attack = false
|
|
72
|
+
key_attack = false
|
|
73
|
+
|
|
71
74
|
while running
|
|
72
75
|
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
73
76
|
dt = [now - last_time, 0.05].min
|
|
@@ -106,6 +109,12 @@ while running
|
|
|
106
109
|
"shot=#{shot}"
|
|
107
110
|
end
|
|
108
111
|
|
|
112
|
+
if scancode == SDL::SCANCODE_EQUALS
|
|
113
|
+
engine.adjust_viewsize(10) # sizeup: less HUD
|
|
114
|
+
elsif scancode == SDL::SCANCODE_MINUS
|
|
115
|
+
engine.adjust_viewsize(-10) # sizedown: more HUD
|
|
116
|
+
end
|
|
117
|
+
|
|
109
118
|
weapon_slot = case scancode
|
|
110
119
|
when SDL::SCANCODE_1 then 1
|
|
111
120
|
when SDL::SCANCODE_2 then 2
|
|
@@ -132,9 +141,17 @@ while running
|
|
|
132
141
|
engine.viewmodel&.set_weapon(engine.player_state.current_weapon_model)
|
|
133
142
|
when SDL::MOUSEMOTION
|
|
134
143
|
engine.player.rotate(event[:motion][:xrel], event[:motion][:yrel])
|
|
144
|
+
when SDL::MOUSEBUTTONDOWN
|
|
145
|
+
mouse_attack = true if event[:button][:button] == SDL::BUTTON_LEFT
|
|
146
|
+
when SDL::MOUSEBUTTONUP
|
|
147
|
+
mouse_attack = false if event[:button][:button] == SDL::BUTTON_LEFT
|
|
135
148
|
end
|
|
136
149
|
end
|
|
137
150
|
|
|
151
|
+
# Ctrl is Quake's classic +attack; combine with the mouse button
|
|
152
|
+
key_attack = keys[SDL::SCANCODE_LCTRL] || keys[SDL::SCANCODE_RCTRL] || false
|
|
153
|
+
engine.set_attack(mouse_attack || key_attack)
|
|
154
|
+
|
|
138
155
|
engine.set_keys(keys)
|
|
139
156
|
engine.tick(dt)
|
|
140
157
|
engine.swap_buffers
|
data/lib/quake/bsp/reader.rb
CHANGED
|
@@ -30,23 +30,39 @@ module Quake
|
|
|
30
30
|
|
|
31
31
|
def parse
|
|
32
32
|
parse_header
|
|
33
|
+
@textures = parse_textures
|
|
34
|
+
vertices = parse_vertices
|
|
35
|
+
edges = parse_edges
|
|
36
|
+
faces = parse_faces
|
|
37
|
+
surfedges = parse_surfedges
|
|
38
|
+
planes = parse_planes
|
|
39
|
+
nodes = parse_nodes
|
|
40
|
+
leafs = parse_leafs
|
|
41
|
+
clipnodes = parse_clipnodes
|
|
42
|
+
texinfo = parse_texinfo
|
|
43
|
+
faces = build_surface_data(vertices, edges, faces, surfedges, texinfo)
|
|
44
|
+
models = parse_models
|
|
45
|
+
marksurfaces = parse_marksurfaces
|
|
46
|
+
entities = parse_entities
|
|
47
|
+
visibility = parse_visibility
|
|
48
|
+
lighting = parse_lighting
|
|
33
49
|
|
|
34
50
|
Level.new(
|
|
35
|
-
vertices:
|
|
36
|
-
edges:
|
|
37
|
-
faces:
|
|
38
|
-
surfedges:
|
|
39
|
-
planes:
|
|
40
|
-
nodes:
|
|
41
|
-
leafs:
|
|
42
|
-
clipnodes:
|
|
43
|
-
texinfo:
|
|
44
|
-
models:
|
|
45
|
-
marksurfaces:
|
|
46
|
-
entities:
|
|
47
|
-
visibility:
|
|
48
|
-
lighting:
|
|
49
|
-
textures:
|
|
51
|
+
vertices: vertices,
|
|
52
|
+
edges: edges,
|
|
53
|
+
faces: faces,
|
|
54
|
+
surfedges: surfedges,
|
|
55
|
+
planes: planes,
|
|
56
|
+
nodes: nodes,
|
|
57
|
+
leafs: leafs,
|
|
58
|
+
clipnodes: clipnodes,
|
|
59
|
+
texinfo: texinfo,
|
|
60
|
+
models: models,
|
|
61
|
+
marksurfaces: marksurfaces,
|
|
62
|
+
entities: entities,
|
|
63
|
+
visibility: visibility,
|
|
64
|
+
lighting: lighting,
|
|
65
|
+
textures: @textures
|
|
50
66
|
)
|
|
51
67
|
end
|
|
52
68
|
|
|
@@ -54,7 +70,9 @@ module Quake
|
|
|
54
70
|
|
|
55
71
|
def parse_header
|
|
56
72
|
version = @data[0, 4].unpack1("l<")
|
|
57
|
-
|
|
73
|
+
unless version == BSP_VERSION
|
|
74
|
+
raise "Mod_LoadBrushModel: wrong version number (#{version} should be #{BSP_VERSION})"
|
|
75
|
+
end
|
|
58
76
|
|
|
59
77
|
NUM_LUMPS.times do |i|
|
|
60
78
|
offset = 4 + i * 8
|
|
@@ -63,13 +81,16 @@ module Quake
|
|
|
63
81
|
end
|
|
64
82
|
end
|
|
65
83
|
|
|
66
|
-
def lump_data(index)
|
|
84
|
+
def lump_data(index, record_size: nil)
|
|
67
85
|
offset, length = @lumps[index]
|
|
86
|
+
if record_size && (length % record_size) != 0
|
|
87
|
+
raise "MOD_LoadBmodel: funny lump size"
|
|
88
|
+
end
|
|
68
89
|
@data[offset, length]
|
|
69
90
|
end
|
|
70
91
|
|
|
71
92
|
def parse_vertices
|
|
72
|
-
data = lump_data(LUMP_VERTICES)
|
|
93
|
+
data = lump_data(LUMP_VERTICES, record_size: 12)
|
|
73
94
|
count = data.bytesize / 12
|
|
74
95
|
result = Array.new(count)
|
|
75
96
|
count.times do |i|
|
|
@@ -80,7 +101,7 @@ module Quake
|
|
|
80
101
|
end
|
|
81
102
|
|
|
82
103
|
def parse_edges
|
|
83
|
-
data = lump_data(LUMP_EDGES)
|
|
104
|
+
data = lump_data(LUMP_EDGES, record_size: 4)
|
|
84
105
|
count = data.bytesize / 4
|
|
85
106
|
result = Array.new(count)
|
|
86
107
|
count.times do |i|
|
|
@@ -91,8 +112,9 @@ module Quake
|
|
|
91
112
|
end
|
|
92
113
|
|
|
93
114
|
def parse_faces
|
|
94
|
-
data = lump_data(LUMP_FACES)
|
|
115
|
+
data = lump_data(LUMP_FACES, record_size: 20)
|
|
95
116
|
count = data.bytesize / 20
|
|
117
|
+
@face_count = count
|
|
96
118
|
result = Array.new(count)
|
|
97
119
|
count.times do |i|
|
|
98
120
|
raw = data[i * 20, 20]
|
|
@@ -102,19 +124,78 @@ module Quake
|
|
|
102
124
|
plane_index: plane_idx, side: side,
|
|
103
125
|
first_edge: first_edge, num_edges: num_edges,
|
|
104
126
|
texinfo_index: texinfo_idx, styles: [s0, s1, s2, s3],
|
|
105
|
-
light_offset: light_ofs
|
|
127
|
+
light_offset: light_ofs, flags: 0, texture_mins: [0, 0],
|
|
128
|
+
extents: [0, 0]
|
|
106
129
|
)
|
|
107
130
|
end
|
|
108
131
|
result
|
|
109
132
|
end
|
|
110
133
|
|
|
134
|
+
def build_surface_data(vertices, edges, faces, surfedges, texinfo)
|
|
135
|
+
faces.map do |face|
|
|
136
|
+
info = texinfo[face.texinfo_index]
|
|
137
|
+
next face unless info
|
|
138
|
+
|
|
139
|
+
texture_mins, extents = surface_extents(
|
|
140
|
+
vertices, edges, face, surfedges, info
|
|
141
|
+
)
|
|
142
|
+
if (info.flags & 1) == 0 && extents.any? { |extent| extent > 256 }
|
|
143
|
+
raise "Bad surface extents"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
flags = face.side.zero? ? 0 : Face::SURF_PLANEBACK
|
|
147
|
+
texture = @textures[info.miptex_index] if @textures
|
|
148
|
+
name = texture&.name.to_s
|
|
149
|
+
if name.start_with?("sky")
|
|
150
|
+
flags |= Face::SURF_DRAWSKY | Face::SURF_DRAWTILED
|
|
151
|
+
elsif name.start_with?("*")
|
|
152
|
+
flags |= Face::SURF_DRAWTURB | Face::SURF_DRAWTILED
|
|
153
|
+
texture_mins = [-8192, -8192]
|
|
154
|
+
extents = [16384, 16384]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
face.with(flags: flags, texture_mins: texture_mins, extents: extents)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def surface_extents(vertices, edges, face, surfedges, texinfo)
|
|
162
|
+
mins = [999999.0, 999999.0]
|
|
163
|
+
maxs = [-99999.0, -99999.0]
|
|
164
|
+
|
|
165
|
+
face.num_edges.times do |i|
|
|
166
|
+
surfedge = surfedges[face.first_edge + i]
|
|
167
|
+
edge = edges[surfedge.abs]
|
|
168
|
+
vertex_index = surfedge >= 0 ? edge.v0 : edge.v1
|
|
169
|
+
vertex = vertices[vertex_index]
|
|
170
|
+
|
|
171
|
+
values = [
|
|
172
|
+
vertex.dot(texinfo.s_vec) + texinfo.s_offset,
|
|
173
|
+
vertex.dot(texinfo.t_vec) + texinfo.t_offset
|
|
174
|
+
]
|
|
175
|
+
2.times do |axis|
|
|
176
|
+
mins[axis] = values[axis] if values[axis] < mins[axis]
|
|
177
|
+
maxs[axis] = values[axis] if values[axis] > maxs[axis]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
texture_mins = Array.new(2)
|
|
182
|
+
extents = Array.new(2)
|
|
183
|
+
2.times do |axis|
|
|
184
|
+
bmin = (mins[axis] / 16.0).floor
|
|
185
|
+
bmax = (maxs[axis] / 16.0).ceil
|
|
186
|
+
texture_mins[axis] = bmin * 16
|
|
187
|
+
extents[axis] = (bmax - bmin) * 16
|
|
188
|
+
end
|
|
189
|
+
[texture_mins, extents]
|
|
190
|
+
end
|
|
191
|
+
|
|
111
192
|
def parse_surfedges
|
|
112
|
-
data = lump_data(LUMP_SURFEDGES)
|
|
193
|
+
data = lump_data(LUMP_SURFEDGES, record_size: 4)
|
|
113
194
|
data.unpack("l<*")
|
|
114
195
|
end
|
|
115
196
|
|
|
116
197
|
def parse_planes
|
|
117
|
-
data = lump_data(LUMP_PLANES)
|
|
198
|
+
data = lump_data(LUMP_PLANES, record_size: 20)
|
|
118
199
|
count = data.bytesize / 20
|
|
119
200
|
result = Array.new(count)
|
|
120
201
|
count.times do |i|
|
|
@@ -127,7 +208,7 @@ module Quake
|
|
|
127
208
|
end
|
|
128
209
|
|
|
129
210
|
def parse_nodes
|
|
130
|
-
data = lump_data(LUMP_NODES)
|
|
211
|
+
data = lump_data(LUMP_NODES, record_size: 24)
|
|
131
212
|
count = data.bytesize / 24
|
|
132
213
|
result = Array.new(count)
|
|
133
214
|
count.times do |i|
|
|
@@ -147,7 +228,7 @@ module Quake
|
|
|
147
228
|
end
|
|
148
229
|
|
|
149
230
|
def parse_leafs
|
|
150
|
-
data = lump_data(LUMP_LEAFS)
|
|
231
|
+
data = lump_data(LUMP_LEAFS, record_size: 28)
|
|
151
232
|
count = data.bytesize / 28
|
|
152
233
|
result = Array.new(count)
|
|
153
234
|
count.times do |i|
|
|
@@ -168,7 +249,7 @@ module Quake
|
|
|
168
249
|
end
|
|
169
250
|
|
|
170
251
|
def parse_clipnodes
|
|
171
|
-
data = lump_data(LUMP_CLIPNODES)
|
|
252
|
+
data = lump_data(LUMP_CLIPNODES, record_size: 8)
|
|
172
253
|
count = data.bytesize / 8
|
|
173
254
|
result = Array.new(count)
|
|
174
255
|
count.times do |i|
|
|
@@ -179,13 +260,19 @@ module Quake
|
|
|
179
260
|
end
|
|
180
261
|
|
|
181
262
|
def parse_texinfo
|
|
182
|
-
data = lump_data(LUMP_TEXINFO)
|
|
263
|
+
data = lump_data(LUMP_TEXINFO, record_size: 40)
|
|
183
264
|
count = data.bytesize / 40
|
|
184
265
|
result = Array.new(count)
|
|
185
266
|
count.times do |i|
|
|
186
267
|
raw = data[i * 40, 40]
|
|
187
268
|
floats = raw[0, 32].unpack("e8")
|
|
188
269
|
miptex, flags = raw[32, 8].unpack("l<2")
|
|
270
|
+
if @texture_count && miptex >= @texture_count
|
|
271
|
+
raise "miptex >= loadmodel->numtextures"
|
|
272
|
+
end
|
|
273
|
+
if @texture_count.nil? || @textures[miptex].nil?
|
|
274
|
+
flags = 0
|
|
275
|
+
end
|
|
189
276
|
result[i] = TexInfo.new(
|
|
190
277
|
s_vec: Math::Vec3.new(floats[0], floats[1], floats[2]),
|
|
191
278
|
s_offset: floats[3],
|
|
@@ -198,7 +285,7 @@ module Quake
|
|
|
198
285
|
end
|
|
199
286
|
|
|
200
287
|
def parse_models
|
|
201
|
-
data = lump_data(LUMP_MODELS)
|
|
288
|
+
data = lump_data(LUMP_MODELS, record_size: 64)
|
|
202
289
|
count = data.bytesize / 64
|
|
203
290
|
result = Array.new(count)
|
|
204
291
|
count.times do |i|
|
|
@@ -206,8 +293,12 @@ module Quake
|
|
|
206
293
|
floats = raw[0, 36].unpack("e9")
|
|
207
294
|
ints = raw[36, 28].unpack("l<7")
|
|
208
295
|
result[i] = Model.new(
|
|
209
|
-
mins: Math::Vec3.new(
|
|
210
|
-
|
|
296
|
+
mins: Math::Vec3.new(
|
|
297
|
+
floats[0] - 1.0, floats[1] - 1.0, floats[2] - 1.0
|
|
298
|
+
),
|
|
299
|
+
maxs: Math::Vec3.new(
|
|
300
|
+
floats[3] + 1.0, floats[4] + 1.0, floats[5] + 1.0
|
|
301
|
+
),
|
|
211
302
|
origin: Math::Vec3.new(floats[6], floats[7], floats[8]),
|
|
212
303
|
head_nodes: ints[0..3],
|
|
213
304
|
vis_leafs: ints[4],
|
|
@@ -218,30 +309,41 @@ module Quake
|
|
|
218
309
|
end
|
|
219
310
|
|
|
220
311
|
def parse_marksurfaces
|
|
221
|
-
data = lump_data(LUMP_MARKSURFACES)
|
|
222
|
-
data.unpack("v*")
|
|
312
|
+
data = lump_data(LUMP_MARKSURFACES, record_size: 2)
|
|
313
|
+
data.unpack("v*").each do |face_index|
|
|
314
|
+
if face_index >= @face_count
|
|
315
|
+
raise "Mod_ParseMarksurfaces: bad surface number"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
223
318
|
end
|
|
224
319
|
|
|
225
320
|
def parse_entities
|
|
226
|
-
lump_data(LUMP_ENTITIES)
|
|
321
|
+
data = lump_data(LUMP_ENTITIES)
|
|
322
|
+
data&.empty? ? nil : data&.force_encoding("ASCII")
|
|
227
323
|
end
|
|
228
324
|
|
|
229
325
|
def parse_visibility
|
|
230
|
-
lump_data(LUMP_VISIBILITY)
|
|
326
|
+
data = lump_data(LUMP_VISIBILITY)
|
|
327
|
+
data&.empty? ? nil : data
|
|
231
328
|
end
|
|
232
329
|
|
|
233
330
|
def parse_lighting
|
|
234
|
-
lump_data(LUMP_LIGHTING)
|
|
331
|
+
data = lump_data(LUMP_LIGHTING)
|
|
332
|
+
data&.empty? ? nil : data
|
|
235
333
|
end
|
|
236
334
|
|
|
237
335
|
def parse_textures
|
|
238
336
|
data = lump_data(LUMP_TEXTURES)
|
|
239
|
-
|
|
337
|
+
if data.nil? || data.bytesize < 4
|
|
338
|
+
@texture_count = nil
|
|
339
|
+
return []
|
|
340
|
+
end
|
|
240
341
|
|
|
241
342
|
num_miptex = data[0, 4].unpack1("l<")
|
|
343
|
+
@texture_count = num_miptex
|
|
242
344
|
offsets = data[4, num_miptex * 4].unpack("l<#{num_miptex}")
|
|
243
345
|
|
|
244
|
-
offsets.map do |ofs|
|
|
346
|
+
textures = offsets.map do |ofs|
|
|
245
347
|
if ofs < 0
|
|
246
348
|
nil
|
|
247
349
|
else
|
|
@@ -249,15 +351,116 @@ module Quake
|
|
|
249
351
|
next nil if raw.nil? || raw.bytesize < 40
|
|
250
352
|
name = raw[0, 16].unpack1("Z16")
|
|
251
353
|
width, height = raw[16, 8].unpack("V2")
|
|
354
|
+
if (width & 15) != 0 || (height & 15) != 0
|
|
355
|
+
raise "Texture #{name} is not 16 aligned"
|
|
356
|
+
end
|
|
252
357
|
mip_offsets = raw[24, 16].unpack("V4")
|
|
253
358
|
# Extract mip level 0 pixel data (8-bit indexed)
|
|
254
359
|
pixel_offset = ofs + mip_offsets[0]
|
|
255
360
|
pixel_count = width * height
|
|
256
361
|
pixels = data[pixel_offset, pixel_count]
|
|
257
362
|
MipTex.new(name: name, width: width, height: height,
|
|
258
|
-
offsets: mip_offsets, pixels: pixels
|
|
363
|
+
offsets: mip_offsets, pixels: pixels,
|
|
364
|
+
anim_total: 0, anim_min: 0, anim_max: 0,
|
|
365
|
+
anim_next: nil, alternate_anims: nil)
|
|
259
366
|
end
|
|
260
367
|
end
|
|
368
|
+
sequence_texture_animations(textures)
|
|
369
|
+
textures
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def sequence_texture_animations(textures)
|
|
373
|
+
sequenced = {}
|
|
374
|
+
textures.each_with_index do |texture, index|
|
|
375
|
+
next unless texture&.name&.start_with?("+")
|
|
376
|
+
|
|
377
|
+
suffix = texture.name[2..]
|
|
378
|
+
next if sequenced[suffix]
|
|
379
|
+
|
|
380
|
+
anims = Array.new(10)
|
|
381
|
+
alt_anims = Array.new(10)
|
|
382
|
+
max = add_texture_animation_frame(anims, alt_anims, texture, texture.name)
|
|
383
|
+
altmax = texture_animation_alt_frame?(texture.name[1]) ? max : 0
|
|
384
|
+
max = 0 if altmax.positive?
|
|
385
|
+
|
|
386
|
+
textures[(index + 1)..]&.each do |other|
|
|
387
|
+
next unless other&.name&.start_with?("+")
|
|
388
|
+
next unless other.name[2..] == suffix
|
|
389
|
+
|
|
390
|
+
frame = add_texture_animation_frame(anims, alt_anims, other, texture.name)
|
|
391
|
+
if texture_animation_alt_frame?(other.name[1])
|
|
392
|
+
altmax = frame if frame > altmax
|
|
393
|
+
elsif frame > max
|
|
394
|
+
max = frame
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
max.times do |frame|
|
|
399
|
+
raise "Missing frame #{frame} of #{texture.name}" unless anims[frame]
|
|
400
|
+
end
|
|
401
|
+
altmax.times do |frame|
|
|
402
|
+
raise "Missing frame #{frame} of #{texture.name}" unless alt_anims[frame]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
link_texture_animation_frames(textures, anims, max, alt_anims, altmax)
|
|
406
|
+
sequenced[suffix] = true
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
ANIM_CYCLE = 2
|
|
411
|
+
|
|
412
|
+
def link_texture_animation_frames(textures, anims, max, alt_anims, altmax)
|
|
413
|
+
anim_indices = anims.map { |texture| textures.index(texture) }
|
|
414
|
+
alt_anim_indices = alt_anims.map { |texture| textures.index(texture) }
|
|
415
|
+
|
|
416
|
+
max.times do |frame|
|
|
417
|
+
texture = anims[frame]
|
|
418
|
+
index = anim_indices[frame]
|
|
419
|
+
textures[index] = texture.with(
|
|
420
|
+
anim_total: max * ANIM_CYCLE,
|
|
421
|
+
anim_min: frame * ANIM_CYCLE,
|
|
422
|
+
anim_max: (frame + 1) * ANIM_CYCLE,
|
|
423
|
+
anim_next: anim_indices[(frame + 1) % max],
|
|
424
|
+
alternate_anims: altmax.positive? ? alt_anim_indices[0] : nil
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
altmax.times do |frame|
|
|
429
|
+
texture = alt_anims[frame]
|
|
430
|
+
index = alt_anim_indices[frame]
|
|
431
|
+
textures[index] = texture.with(
|
|
432
|
+
anim_total: altmax * ANIM_CYCLE,
|
|
433
|
+
anim_min: frame * ANIM_CYCLE,
|
|
434
|
+
anim_max: (frame + 1) * ANIM_CYCLE,
|
|
435
|
+
anim_next: alt_anim_indices[(frame + 1) % altmax],
|
|
436
|
+
alternate_anims: max.positive? ? anim_indices[0] : nil
|
|
437
|
+
)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def add_texture_animation_frame(anims, alt_anims, texture, error_name)
|
|
442
|
+
frame = texture_animation_frame(texture.name[1], error_name)
|
|
443
|
+
if texture_animation_alt_frame?(texture.name[1])
|
|
444
|
+
alt_anims[frame] = texture
|
|
445
|
+
else
|
|
446
|
+
anims[frame] = texture
|
|
447
|
+
end
|
|
448
|
+
frame + 1
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def texture_animation_frame(char, name)
|
|
452
|
+
normalized = char&.upcase
|
|
453
|
+
if normalized&.between?("0", "9")
|
|
454
|
+
normalized.ord - "0".ord
|
|
455
|
+
elsif normalized&.between?("A", "J")
|
|
456
|
+
normalized.ord - "A".ord
|
|
457
|
+
else
|
|
458
|
+
raise "Bad animating texture #{name}"
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def texture_animation_alt_frame?(char)
|
|
463
|
+
char&.upcase&.between?("A", "J")
|
|
261
464
|
end
|
|
262
465
|
end
|
|
263
466
|
end
|
data/lib/quake/bsp/types.rb
CHANGED
|
@@ -4,18 +4,44 @@ module Quake
|
|
|
4
4
|
module Bsp
|
|
5
5
|
Edge = Data.define(:v0, :v1)
|
|
6
6
|
Face = Data.define(:plane_index, :side, :first_edge, :num_edges,
|
|
7
|
-
:texinfo_index, :styles, :light_offset
|
|
8
|
-
|
|
7
|
+
:texinfo_index, :styles, :light_offset,
|
|
8
|
+
:flags, :texture_mins, :extents)
|
|
9
|
+
Face.const_set(:SURF_PLANEBACK, 2)
|
|
10
|
+
Face.const_set(:SURF_DRAWSKY, 4)
|
|
11
|
+
Face.const_set(:SURF_DRAWTURB, 0x10)
|
|
12
|
+
Face.const_set(:SURF_DRAWTILED, 0x20)
|
|
13
|
+
Plane = Data.define(:normal, :dist, :type) do
|
|
14
|
+
def signbits
|
|
15
|
+
bits = 0
|
|
16
|
+
bits |= 1 if normal.x.negative?
|
|
17
|
+
bits |= 2 if normal.y.negative?
|
|
18
|
+
bits |= 4 if normal.z.negative?
|
|
19
|
+
bits
|
|
20
|
+
end
|
|
21
|
+
end
|
|
9
22
|
Node = Data.define(:plane_index, :children, :mins, :maxs,
|
|
10
23
|
:first_face, :num_faces)
|
|
11
24
|
Leaf = Data.define(:contents, :vis_offset, :mins, :maxs,
|
|
12
25
|
:first_marksurface, :num_marksurfaces, :ambient_levels)
|
|
13
26
|
ClipNode = Data.define(:plane_index, :children)
|
|
27
|
+
Hull = Data.define(:first_clipnode, :last_clipnode,
|
|
28
|
+
:clip_mins, :clip_maxs)
|
|
14
29
|
TexInfo = Data.define(:s_vec, :s_offset, :t_vec, :t_offset,
|
|
15
|
-
:miptex_index, :flags)
|
|
30
|
+
:miptex_index, :flags) do
|
|
31
|
+
def mipadjust
|
|
32
|
+
scale = (s_vec.length + t_vec.length) / 2.0
|
|
33
|
+
return 4 if scale < 0.32
|
|
34
|
+
return 3 if scale < 0.49
|
|
35
|
+
return 2 if scale < 0.99
|
|
36
|
+
|
|
37
|
+
1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
16
40
|
Model = Data.define(:mins, :maxs, :origin, :head_nodes,
|
|
17
41
|
:vis_leafs, :first_face, :num_faces)
|
|
18
|
-
MipTex = Data.define(:name, :width, :height, :offsets, :pixels
|
|
42
|
+
MipTex = Data.define(:name, :width, :height, :offsets, :pixels,
|
|
43
|
+
:anim_total, :anim_min, :anim_max, :anim_next,
|
|
44
|
+
:alternate_anims)
|
|
19
45
|
|
|
20
46
|
# Pre-computed surface data for rendering
|
|
21
47
|
Surface = Data.define(:vertices, :texcoords, :texinfo_index)
|
|
@@ -25,6 +51,24 @@ module Quake
|
|
|
25
51
|
:planes, :nodes, :leafs, :clipnodes,
|
|
26
52
|
:texinfo, :models, :marksurfaces,
|
|
27
53
|
:entities, :visibility, :lighting, :textures
|
|
28
|
-
)
|
|
54
|
+
) do
|
|
55
|
+
def hulls
|
|
56
|
+
[
|
|
57
|
+
nil,
|
|
58
|
+
Hull.new(
|
|
59
|
+
first_clipnode: 0,
|
|
60
|
+
last_clipnode: clipnodes.size - 1,
|
|
61
|
+
clip_mins: Math::Vec3.new(-16.0, -16.0, -24.0),
|
|
62
|
+
clip_maxs: Math::Vec3.new(16.0, 16.0, 32.0)
|
|
63
|
+
),
|
|
64
|
+
Hull.new(
|
|
65
|
+
first_clipnode: 0,
|
|
66
|
+
last_clipnode: clipnodes.size - 1,
|
|
67
|
+
clip_mins: Math::Vec3.new(-32.0, -32.0, -24.0),
|
|
68
|
+
clip_maxs: Math::Vec3.new(32.0, 32.0, 64.0)
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
29
73
|
end
|
|
30
74
|
end
|