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
data/lib/quake/mdl/reader.rb
CHANGED
|
@@ -8,6 +8,9 @@ module Quake
|
|
|
8
8
|
class Reader
|
|
9
9
|
IDPOLYHEADER = 0x4F504449 # "IDPO" in little-endian
|
|
10
10
|
MDL_VERSION = 6
|
|
11
|
+
MAX_LBM_HEIGHT = 480
|
|
12
|
+
MAXALIASVERTS = 2000
|
|
13
|
+
ALIAS_BASE_SIZE_RATIO = 1.0 / 11.0
|
|
11
14
|
|
|
12
15
|
def initialize(data)
|
|
13
16
|
@data = data
|
|
@@ -16,7 +19,8 @@ module Quake
|
|
|
16
19
|
|
|
17
20
|
def parse
|
|
18
21
|
header = read_header
|
|
19
|
-
|
|
22
|
+
validate_header!(header)
|
|
23
|
+
skins, skin_intervals = read_skins(header)
|
|
20
24
|
stverts = read_stverts(header[:numverts])
|
|
21
25
|
triangles = read_triangles(header[:numtris])
|
|
22
26
|
frames = read_frames(header[:numframes], header[:numverts])
|
|
@@ -29,11 +33,15 @@ module Quake
|
|
|
29
33
|
skin_width: header[:skinwidth],
|
|
30
34
|
skin_height: header[:skinheight],
|
|
31
35
|
skins: skins,
|
|
36
|
+
skin_intervals: skin_intervals,
|
|
32
37
|
stverts: stverts,
|
|
33
38
|
triangles: triangles,
|
|
34
39
|
frames: frames,
|
|
35
40
|
flags: header[:flags],
|
|
36
|
-
sync_type: header[:synctype]
|
|
41
|
+
sync_type: header[:synctype],
|
|
42
|
+
size: header[:size] * ALIAS_BASE_SIZE_RATIO,
|
|
43
|
+
mins: Math::Vec3.new(-16.0, -16.0, -16.0),
|
|
44
|
+
maxs: Math::Vec3.new(16.0, 16.0, 16.0)
|
|
37
45
|
)
|
|
38
46
|
end
|
|
39
47
|
|
|
@@ -42,7 +50,9 @@ module Quake
|
|
|
42
50
|
def read_header
|
|
43
51
|
ident, version = read_fmt("VV")
|
|
44
52
|
raise "Not an MDL file (ident=0x#{ident.to_s(16)})" unless ident == IDPOLYHEADER
|
|
45
|
-
|
|
53
|
+
unless version == MDL_VERSION
|
|
54
|
+
raise "Mod_LoadAliasModel: wrong version number (#{version} should be #{MDL_VERSION})"
|
|
55
|
+
end
|
|
46
56
|
|
|
47
57
|
sx, sy, sz = read_fmt("eee")
|
|
48
58
|
ox, oy, oz = read_fmt("eee")
|
|
@@ -70,8 +80,35 @@ module Quake
|
|
|
70
80
|
}
|
|
71
81
|
end
|
|
72
82
|
|
|
83
|
+
def validate_header!(header)
|
|
84
|
+
if header[:skinheight] > MAX_LBM_HEIGHT
|
|
85
|
+
raise "model has a skin taller than #{MAX_LBM_HEIGHT}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
raise "model has no vertices" if header[:numverts] <= 0
|
|
89
|
+
|
|
90
|
+
if header[:numverts] > MAXALIASVERTS
|
|
91
|
+
raise "model has too many vertices"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
raise "model has no triangles" if header[:numtris] <= 0
|
|
95
|
+
|
|
96
|
+
if (header[:skinwidth] & 0x03) != 0
|
|
97
|
+
raise "Mod_LoadAliasModel: skinwidth not multiple of 4"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if header[:numskins] < 1
|
|
101
|
+
raise "Mod_LoadAliasModel: Invalid # of skins: #{header[:numskins]}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if header[:numframes] < 1
|
|
105
|
+
raise "Mod_LoadAliasModel: Invalid # of frames: #{header[:numframes]}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
73
109
|
def read_skins(header)
|
|
74
110
|
skins = []
|
|
111
|
+
skin_intervals = []
|
|
75
112
|
skin_size = header[:skinwidth] * header[:skinheight]
|
|
76
113
|
|
|
77
114
|
header[:numskins].times do
|
|
@@ -80,21 +117,62 @@ module Quake
|
|
|
80
117
|
if skin_type == 0 # ALIAS_SKIN_SINGLE
|
|
81
118
|
pixels = @data[@pos, skin_size]
|
|
82
119
|
@pos += skin_size
|
|
83
|
-
skins << [pixels]
|
|
120
|
+
skins << [flood_fill_skin(pixels, header[:skinwidth], header[:skinheight])]
|
|
121
|
+
skin_intervals << nil
|
|
84
122
|
else # ALIAS_SKIN_GROUP
|
|
85
123
|
num_skins = read_fmt("V").first
|
|
86
|
-
|
|
124
|
+
intervals = read_fmt("e" * num_skins)
|
|
125
|
+
if intervals.any? { |interval| interval <= 0.0 }
|
|
126
|
+
raise "Mod_LoadAliasSkinGroup: interval<=0"
|
|
127
|
+
end
|
|
87
128
|
group = []
|
|
88
129
|
num_skins.times do
|
|
89
130
|
pixels = @data[@pos, skin_size]
|
|
90
131
|
@pos += skin_size
|
|
91
|
-
group << pixels
|
|
132
|
+
group << flood_fill_skin(pixels, header[:skinwidth], header[:skinheight])
|
|
92
133
|
end
|
|
93
134
|
skins << group
|
|
135
|
+
skin_intervals << intervals
|
|
94
136
|
end
|
|
95
137
|
end
|
|
96
138
|
|
|
97
|
-
skins
|
|
139
|
+
[skins, skin_intervals]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def flood_fill_skin(pixels, skinwidth, skinheight)
|
|
143
|
+
fillcolor = pixels.getbyte(0)
|
|
144
|
+
filledcolor = 0
|
|
145
|
+
return pixels if fillcolor == filledcolor || fillcolor == 255
|
|
146
|
+
|
|
147
|
+
skin = pixels.dup
|
|
148
|
+
fifo = [[0, 0]]
|
|
149
|
+
outpt = 0
|
|
150
|
+
|
|
151
|
+
while outpt < fifo.size
|
|
152
|
+
x, y = fifo[outpt]
|
|
153
|
+
outpt += 1
|
|
154
|
+
fdc = filledcolor
|
|
155
|
+
offset = x + skinwidth * y
|
|
156
|
+
|
|
157
|
+
[[-1, 0], [1, 0], [0, -1], [0, 1]].each do |dx, dy|
|
|
158
|
+
nx = x + dx
|
|
159
|
+
ny = y + dy
|
|
160
|
+
next if nx.negative? || nx >= skinwidth || ny.negative? || ny >= skinheight
|
|
161
|
+
|
|
162
|
+
neighbor = nx + skinwidth * ny
|
|
163
|
+
color = skin.getbyte(neighbor)
|
|
164
|
+
if color == fillcolor
|
|
165
|
+
skin.setbyte(neighbor, 255)
|
|
166
|
+
fifo << [nx, ny]
|
|
167
|
+
elsif color != 255
|
|
168
|
+
fdc = color
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
skin.setbyte(offset, fdc)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
skin
|
|
98
176
|
end
|
|
99
177
|
|
|
100
178
|
def read_stverts(count)
|
|
@@ -141,6 +219,9 @@ module Quake
|
|
|
141
219
|
_bbox_max = read_trivertx
|
|
142
220
|
|
|
143
221
|
intervals = read_fmt("e" * num_frames)
|
|
222
|
+
if intervals.any? { |interval| interval <= 0.0 }
|
|
223
|
+
raise "Mod_LoadAliasGroup: interval<=0"
|
|
224
|
+
end
|
|
144
225
|
|
|
145
226
|
frames = num_frames.times.map do
|
|
146
227
|
read_single_frame(numverts)
|
data/lib/quake/mdl/types.rb
CHANGED
|
@@ -20,8 +20,8 @@ module Quake
|
|
|
20
20
|
# Complete MDL model
|
|
21
21
|
Model = Data.define(
|
|
22
22
|
:scale, :scale_origin, :bounding_radius, :eye_position,
|
|
23
|
-
:skin_width, :skin_height, :skins,
|
|
24
|
-
:stverts, :triangles, :frames, :flags, :sync_type
|
|
23
|
+
:skin_width, :skin_height, :skins, :skin_intervals,
|
|
24
|
+
:stverts, :triangles, :frames, :flags, :sync_type, :size, :mins, :maxs
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
ALIAS_ONSEAM = 0x0020
|
data/lib/quake/pak/reader.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Quake
|
|
|
8
8
|
MAGIC = "PACK"
|
|
9
9
|
HEADER_SIZE = 12
|
|
10
10
|
DIR_ENTRY_SIZE = 64
|
|
11
|
+
MAX_FILES_IN_PACK = 2048
|
|
11
12
|
|
|
12
13
|
attr_reader :entries
|
|
13
14
|
|
|
@@ -23,7 +24,7 @@ module Quake
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def read(name)
|
|
26
|
-
entry = @entries[name]
|
|
27
|
+
entry = @entries[name]
|
|
27
28
|
raise "File not found in PAK: #{name}" unless entry
|
|
28
29
|
|
|
29
30
|
@io.seek(entry.offset)
|
|
@@ -39,9 +40,12 @@ module Quake
|
|
|
39
40
|
def parse_header
|
|
40
41
|
header = @io.read(HEADER_SIZE)
|
|
41
42
|
magic, dirofs, dirlen = header.unpack("a4V2")
|
|
42
|
-
raise "
|
|
43
|
+
raise "#{@path} is not a packfile" unless magic == MAGIC
|
|
43
44
|
|
|
44
45
|
num_entries = dirlen / DIR_ENTRY_SIZE
|
|
46
|
+
if num_entries > MAX_FILES_IN_PACK
|
|
47
|
+
raise "#{@path} has #{num_entries} files"
|
|
48
|
+
end
|
|
45
49
|
@io.seek(dirofs)
|
|
46
50
|
dir_data = @io.read(dirlen)
|
|
47
51
|
|
|
@@ -49,7 +53,9 @@ module Quake
|
|
|
49
53
|
offset = i * DIR_ENTRY_SIZE
|
|
50
54
|
raw = dir_data[offset, DIR_ENTRY_SIZE]
|
|
51
55
|
name, filepos, filelen = raw.unpack("Z56V2")
|
|
52
|
-
@entries
|
|
56
|
+
next if @entries.key?(name)
|
|
57
|
+
|
|
58
|
+
@entries[name] = Entry.new(name: name, offset: filepos, size: filelen)
|
|
53
59
|
end
|
|
54
60
|
end
|
|
55
61
|
end
|
data/lib/quake/palette.rb
CHANGED
|
@@ -17,13 +17,12 @@ module Quake
|
|
|
17
17
|
@rgb[index]
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# Convert an array of 8-bit indexed pixels to RGBA bytes
|
|
21
|
-
|
|
22
|
-
def indexed_to_rgba(pixels, transparent_index: 255)
|
|
20
|
+
# Convert an array of 8-bit indexed pixels to RGBA bytes.
|
|
21
|
+
def indexed_to_rgba(pixels, transparent_index: nil)
|
|
23
22
|
rgba = String.new(capacity: pixels.bytesize * 4)
|
|
24
23
|
pixels.each_byte do |idx|
|
|
25
24
|
r, g, b = @rgb[idx]
|
|
26
|
-
a = (idx == transparent_index) ? 0 : 255
|
|
25
|
+
a = (transparent_index && idx == transparent_index) ? 0 : 255
|
|
27
26
|
rgba << r << g << b << a
|
|
28
27
|
end
|
|
29
28
|
rgba
|
|
@@ -19,10 +19,37 @@ module Quake
|
|
|
19
19
|
CONTENTS_WATER = -3
|
|
20
20
|
CONTENTS_SLIME = -4
|
|
21
21
|
CONTENTS_LAVA = -5
|
|
22
|
+
CONTENTS_SKY = -6
|
|
22
23
|
|
|
23
24
|
DIST_EPSILON = 0.03125 # 1/32, same as Quake
|
|
25
|
+
BoxHull = Data.define(:clipnodes, :planes, :first_clipnode, :last_clipnode)
|
|
24
26
|
|
|
25
27
|
module HullTrace
|
|
28
|
+
def self.hull_for_box(mins, maxs)
|
|
29
|
+
clipnodes = 6.times.map do |i|
|
|
30
|
+
side = i & 1
|
|
31
|
+
children = Array.new(2)
|
|
32
|
+
children[side] = CONTENTS_EMPTY
|
|
33
|
+
children[side ^ 1] = i == 5 ? CONTENTS_SOLID : i + 1
|
|
34
|
+
|
|
35
|
+
Quake::Bsp::ClipNode.new(plane_index: i, children: children)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
planes = [
|
|
39
|
+
box_plane(type: 0, dist: maxs.x),
|
|
40
|
+
box_plane(type: 0, dist: mins.x),
|
|
41
|
+
box_plane(type: 1, dist: maxs.y),
|
|
42
|
+
box_plane(type: 1, dist: mins.y),
|
|
43
|
+
box_plane(type: 2, dist: maxs.z),
|
|
44
|
+
box_plane(type: 2, dist: mins.z)
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
BoxHull.new(
|
|
48
|
+
clipnodes: clipnodes, planes: planes,
|
|
49
|
+
first_clipnode: 0, last_clipnode: 5
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
26
53
|
# Check what content type a point is in, using the clipnode tree.
|
|
27
54
|
# hull_clipnodes: array of ClipNode
|
|
28
55
|
# planes: array of Plane
|
|
@@ -30,10 +57,14 @@ module Quake
|
|
|
30
57
|
# point: Vec3
|
|
31
58
|
def self.point_contents(clipnodes, planes, node_index, point)
|
|
32
59
|
while node_index >= 0
|
|
60
|
+
if node_index >= clipnodes.size
|
|
61
|
+
raise "SV_HullPointContents: bad node number"
|
|
62
|
+
end
|
|
63
|
+
|
|
33
64
|
node = clipnodes[node_index]
|
|
34
65
|
plane = planes[node.plane_index]
|
|
35
66
|
|
|
36
|
-
dist =
|
|
67
|
+
dist = plane_distance(plane, point)
|
|
37
68
|
node_index = dist >= 0 ? node.children[0] : node.children[1]
|
|
38
69
|
end
|
|
39
70
|
node_index # negative value = content type
|
|
@@ -64,11 +95,29 @@ module Quake
|
|
|
64
95
|
TraceResult.new(**result)
|
|
65
96
|
end
|
|
66
97
|
|
|
98
|
+
# Quake's Mod_MakeHull0: duplicate the render node tree as clipnodes so
|
|
99
|
+
# point traces (hull 0) can reuse the same recursive hull check. A node
|
|
100
|
+
# child < 0 encodes leaf -(child + 1); hull 0 replaces it with the leaf
|
|
101
|
+
# contents, exactly like the C loader.
|
|
102
|
+
def self.hull0_clipnodes(level)
|
|
103
|
+
return @hull0_clipnodes if @hull0_level&.equal?(level)
|
|
104
|
+
|
|
105
|
+
@hull0_level = level
|
|
106
|
+
@hull0_clipnodes = level.nodes.map do |node|
|
|
107
|
+
children = node.children.map do |child|
|
|
108
|
+
child >= 0 ? child : level.leafs[-child - 1].contents
|
|
109
|
+
end
|
|
110
|
+
Quake::Bsp::ClipNode.new(plane_index: node.plane_index, children: children)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
67
114
|
# Trace against the world AND all solid brush entities, returning the
|
|
68
115
|
# nearest hit. Brush entity traces are done in entity-local space
|
|
69
116
|
# (subtract origin, trace sub-model hull, transform back).
|
|
117
|
+
# hull_num 0 is a point trace through the render-node hull, matching
|
|
118
|
+
# Quake's SV_TraceLine with MOVE_NOMONSTERS (world + SOLID_BSP only).
|
|
70
119
|
def self.trace_world_and_entities(level, p1, p2, brush_entities, hull_num: 1)
|
|
71
|
-
clipnodes = level.clipnodes
|
|
120
|
+
clipnodes = hull_num.zero? ? hull0_clipnodes(level) : level.clipnodes
|
|
72
121
|
planes = level.planes
|
|
73
122
|
|
|
74
123
|
# World trace
|
|
@@ -79,9 +128,11 @@ module Quake
|
|
|
79
128
|
# Trace against each brush entity's sub-model
|
|
80
129
|
brush_entities&.each do |ent|
|
|
81
130
|
next unless ent.brush_entity?
|
|
131
|
+
next if ent.removed?
|
|
82
132
|
# Triggers are SOLID_TRIGGER in Quake: they detect overlap but
|
|
83
133
|
# don't block movement. They're handled by check_triggers.
|
|
84
134
|
next if ent.classname.start_with?("trigger_")
|
|
135
|
+
next if ent.classname == "func_illusionary"
|
|
85
136
|
model = level.models[ent.model_index]
|
|
86
137
|
next unless model
|
|
87
138
|
|
|
@@ -131,11 +182,15 @@ module Quake
|
|
|
131
182
|
return
|
|
132
183
|
end
|
|
133
184
|
|
|
185
|
+
if node_index >= clipnodes.size
|
|
186
|
+
raise "SV_RecursiveHullCheck: bad node number"
|
|
187
|
+
end
|
|
188
|
+
|
|
134
189
|
node = clipnodes[node_index]
|
|
135
190
|
plane = planes[node.plane_index]
|
|
136
191
|
|
|
137
|
-
d1 =
|
|
138
|
-
d2 =
|
|
192
|
+
d1 = plane_distance(plane, p1)
|
|
193
|
+
d2 = plane_distance(plane, p2)
|
|
139
194
|
|
|
140
195
|
# Both on same side?
|
|
141
196
|
if d1 >= 0 && d2 >= 0
|
|
@@ -188,6 +243,24 @@ module Quake
|
|
|
188
243
|
end
|
|
189
244
|
end
|
|
190
245
|
end
|
|
246
|
+
|
|
247
|
+
private_class_method def self.plane_distance(plane, point)
|
|
248
|
+
case plane.type
|
|
249
|
+
when 0 then point.x - plane.dist
|
|
250
|
+
when 1 then point.y - plane.dist
|
|
251
|
+
when 2 then point.z - plane.dist
|
|
252
|
+
else point.dot(plane.normal) - plane.dist
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private_class_method def self.box_plane(type:, dist:)
|
|
257
|
+
normal = case type
|
|
258
|
+
when 0 then Math::Vec3.new(1.0, 0.0, 0.0)
|
|
259
|
+
when 1 then Math::Vec3.new(0.0, 1.0, 0.0)
|
|
260
|
+
else Math::Vec3.new(0.0, 0.0, 1.0)
|
|
261
|
+
end
|
|
262
|
+
Quake::Bsp::Plane.new(normal: normal, dist: dist, type: type)
|
|
263
|
+
end
|
|
191
264
|
end
|
|
192
265
|
end
|
|
193
266
|
end
|