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/entity.rb
CHANGED
|
@@ -5,37 +5,76 @@ module Quake
|
|
|
5
5
|
# All Quake entities (players, monsters, items, triggers, brush models)
|
|
6
6
|
# are defined as key-value property bags in the BSP entity string.
|
|
7
7
|
class Entity
|
|
8
|
+
FL_FLY = 1
|
|
9
|
+
FL_SWIM = 2
|
|
10
|
+
FL_CLIENT = 8
|
|
11
|
+
FL_MONSTER = 32
|
|
12
|
+
FL_GODMODE = 64
|
|
13
|
+
IT_ARMOR1 = 8192
|
|
14
|
+
IT_ARMOR2 = 16_384
|
|
15
|
+
IT_ARMOR3 = 32_768
|
|
16
|
+
IT_ARMOR_MASK = IT_ARMOR1 | IT_ARMOR2 | IT_ARMOR3
|
|
17
|
+
|
|
8
18
|
attr_accessor :properties, :position, :angle, :angles, :classname,
|
|
9
19
|
:model_index, :think_time, :state, :move_dir,
|
|
10
20
|
:target, :targetname, :speed, :wait, :lip, :health,
|
|
11
|
-
:sounds, :message
|
|
21
|
+
:height, :sounds, :message, :count, :spawnflags, :killtarget,
|
|
22
|
+
:frame, :skin, :flags, :effects, :velocity, :style, :lightstyle,
|
|
23
|
+
:armortype, :armorvalue, :invincible_finished,
|
|
24
|
+
:dmg_take, :dmg_save, :dmg_inflictor, :items,
|
|
25
|
+
:super_damage_finished
|
|
12
26
|
|
|
13
27
|
def initialize(properties = {})
|
|
14
|
-
@properties = properties
|
|
28
|
+
@properties = properties.dup
|
|
15
29
|
@classname = properties["classname"] || ""
|
|
30
|
+
@spawnflags = (properties["spawnflags"] || "0").to_i
|
|
31
|
+
apply_spawn_defaults
|
|
16
32
|
|
|
17
33
|
# Parse common fields
|
|
18
|
-
@position = parse_vec3(properties["origin"]) || Math::Vec3::ORIGIN
|
|
19
|
-
@
|
|
20
|
-
@
|
|
34
|
+
@position = parse_vec3(@properties["origin"]) || Math::Vec3::ORIGIN
|
|
35
|
+
@velocity = parse_vec3(@properties["velocity"]) || Math::Vec3::ORIGIN
|
|
36
|
+
@angle = (@properties["angle"] || "0").to_f
|
|
37
|
+
@angles = parse_vec3(@properties["angles"]) || Math::Vec3::ORIGIN
|
|
21
38
|
|
|
22
39
|
# Brush model reference: "*1", "*2", etc.
|
|
23
40
|
@model_index = nil
|
|
24
|
-
if (model_str = properties["model"])
|
|
41
|
+
if (model_str = @properties["model"])
|
|
25
42
|
@model_index = model_str[1..].to_i if model_str.start_with?("*")
|
|
26
43
|
end
|
|
27
44
|
|
|
28
45
|
# Targeting
|
|
29
|
-
@target = properties["target"]
|
|
30
|
-
@targetname = properties["targetname"]
|
|
46
|
+
@target = @properties["target"]
|
|
47
|
+
@targetname = @properties["targetname"]
|
|
48
|
+
@killtarget = @properties["killtarget"]
|
|
31
49
|
|
|
32
50
|
# Movement/behavior
|
|
33
|
-
@speed = (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@
|
|
38
|
-
|
|
51
|
+
@speed = quake_float("speed", default_speed, zero_defaults_for: [
|
|
52
|
+
"func_door", "func_plat", "func_button", "func_train",
|
|
53
|
+
"trigger_push", "trigger_monsterjump"
|
|
54
|
+
])
|
|
55
|
+
@wait = quake_float("wait", default_wait, zero_defaults_for: [
|
|
56
|
+
"func_door", "func_button", "trigger_multiple", "trap_shooter"
|
|
57
|
+
])
|
|
58
|
+
@lip = quake_float("lip", default_lip, zero_defaults_for: ["func_door", "func_button"])
|
|
59
|
+
@health = (@properties["health"] || default_health).to_f
|
|
60
|
+
@height = quake_float("height", default_height, zero_defaults_for: ["trigger_monsterjump"])
|
|
61
|
+
@sounds = (@properties["sounds"] || "0").to_i
|
|
62
|
+
@message = @properties["message"]
|
|
63
|
+
@count = quake_int("count", default_count, zero_defaults_for: ["trigger_counter"])
|
|
64
|
+
@frame = quake_int("frame", default_frame)
|
|
65
|
+
@skin = quake_int("skin", default_skin)
|
|
66
|
+
@flags = quake_int("flags", "0") | default_runtime_flags
|
|
67
|
+
@takedamage = quake_int("takedamage", default_takedamage)
|
|
68
|
+
@armortype = quake_float("armortype", "0")
|
|
69
|
+
@armorvalue = quake_float("armorvalue", "0")
|
|
70
|
+
@invincible_finished = quake_float("invincible_finished", "0")
|
|
71
|
+
@items = quake_int("items", "0")
|
|
72
|
+
@super_damage_finished = quake_float("super_damage_finished", "0")
|
|
73
|
+
@dmg_take = quake_float("dmg_take", "0")
|
|
74
|
+
@dmg_save = quake_float("dmg_save", "0")
|
|
75
|
+
@dmg_inflictor = nil
|
|
76
|
+
@effects = quake_int("effects", "0") if @properties.key?("effects")
|
|
77
|
+
@style = quake_int("style", default_style, zero_defaults_for: ["light_fluorospark"])
|
|
39
78
|
|
|
40
79
|
# Runtime state
|
|
41
80
|
@state = :idle
|
|
@@ -47,8 +86,49 @@ module Quake
|
|
|
47
86
|
|
|
48
87
|
def brush_entity? = !@model_index.nil?
|
|
49
88
|
|
|
89
|
+
def removed? = !!@removed
|
|
90
|
+
|
|
91
|
+
def damageable?
|
|
92
|
+
return false if removed?
|
|
93
|
+
|
|
94
|
+
@takedamage != 0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def take_damage(amount, game_time: nil)
|
|
98
|
+
return 0.0 unless damageable?
|
|
99
|
+
|
|
100
|
+
result = absorb_damage(amount, game_time: game_time)
|
|
101
|
+
apply_health_damage(result[:take]) if result[:take].positive?
|
|
102
|
+
result[:take].to_f
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def absorb_damage(amount, game_time: nil)
|
|
106
|
+
damage = amount.to_f
|
|
107
|
+
save = (@armortype * damage).ceil
|
|
108
|
+
if save >= @armorvalue
|
|
109
|
+
save = @armorvalue
|
|
110
|
+
@armortype = 0.0
|
|
111
|
+
@items -= @items & IT_ARMOR_MASK
|
|
112
|
+
end
|
|
113
|
+
@armorvalue -= save
|
|
114
|
+
protected = (@flags & FL_GODMODE) != 0 ||
|
|
115
|
+
(game_time && @invincible_finished.positive? && @invincible_finished >= game_time.to_f)
|
|
116
|
+
take = protected ? 0.0 : (damage - save).ceil.to_f
|
|
117
|
+
{ take: take, save: save.to_f, protected: protected }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def apply_health_damage(taken)
|
|
121
|
+
@health -= taken
|
|
122
|
+
@health = -99.0 if @health < -99.0
|
|
123
|
+
if @health <= 0
|
|
124
|
+
@removed = true
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
50
128
|
# Direction vector from the "angle" field (Quake convention)
|
|
51
129
|
def forward_vector
|
|
130
|
+
return angles_forward_vector if @angles != Math::Vec3::ORIGIN
|
|
131
|
+
|
|
52
132
|
case @angle.to_i
|
|
53
133
|
when -1 # UP
|
|
54
134
|
Math::Vec3.new(0.0, 0.0, 1.0)
|
|
@@ -62,48 +142,356 @@ module Quake
|
|
|
62
142
|
|
|
63
143
|
private
|
|
64
144
|
|
|
145
|
+
def angles_forward_vector
|
|
146
|
+
if @angles.x.zero? && @angles.y == -1.0 && @angles.z.zero?
|
|
147
|
+
return Math::Vec3.new(0.0, 0.0, 1.0)
|
|
148
|
+
end
|
|
149
|
+
if @angles.x.zero? && @angles.y == -2.0 && @angles.z.zero?
|
|
150
|
+
return Math::Vec3.new(0.0, 0.0, -1.0)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
yaw = @angles.y * ::Math::PI / 180.0
|
|
154
|
+
pitch = @angles.x * ::Math::PI / 180.0
|
|
155
|
+
Math::Vec3.new(
|
|
156
|
+
::Math.cos(pitch) * ::Math.cos(yaw),
|
|
157
|
+
::Math.cos(pitch) * ::Math.sin(yaw),
|
|
158
|
+
-::Math.sin(pitch)
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def apply_spawn_defaults
|
|
163
|
+
@properties["model"] ||= default_model
|
|
164
|
+
end
|
|
165
|
+
|
|
65
166
|
def parse_vec3(str)
|
|
66
167
|
return nil unless str
|
|
67
|
-
parts = str.split.map(&:to_f)
|
|
68
|
-
|
|
168
|
+
parts = str.split.first(3).map(&:to_f)
|
|
169
|
+
parts << 0.0 while parts.size < 3
|
|
69
170
|
Math::Vec3.new(parts[0], parts[1], parts[2])
|
|
70
171
|
end
|
|
71
172
|
|
|
173
|
+
def quake_float(key, default, zero_defaults_for: [])
|
|
174
|
+
raw = @properties[key]
|
|
175
|
+
return default.to_f unless raw
|
|
176
|
+
|
|
177
|
+
value = raw.to_f
|
|
178
|
+
value.zero? && zero_defaults_for.include?(@classname) ? default.to_f : value
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def quake_int(key, default, zero_defaults_for: [])
|
|
182
|
+
raw = @properties[key]
|
|
183
|
+
return default.to_i unless raw
|
|
184
|
+
|
|
185
|
+
value = raw.to_i
|
|
186
|
+
value.zero? && zero_defaults_for.include?(@classname) ? default.to_i : value
|
|
187
|
+
end
|
|
188
|
+
|
|
72
189
|
def default_speed
|
|
73
190
|
case @classname
|
|
74
191
|
when "func_door" then "100"
|
|
75
192
|
when "func_plat" then "150"
|
|
76
193
|
when "func_button" then "40"
|
|
77
194
|
when "func_train" then "100"
|
|
195
|
+
when "trigger_push" then "1000"
|
|
196
|
+
when "trigger_monsterjump" then "200"
|
|
78
197
|
else "100"
|
|
79
198
|
end
|
|
80
199
|
end
|
|
200
|
+
|
|
201
|
+
def default_wait
|
|
202
|
+
case @classname
|
|
203
|
+
when "func_button" then "1"
|
|
204
|
+
when "trigger_multiple" then "0.2"
|
|
205
|
+
when "trigger_once", "trigger_secret", "trigger_counter" then "-1"
|
|
206
|
+
when "path_corner" then "0"
|
|
207
|
+
when "trap_shooter" then "1"
|
|
208
|
+
else "3"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def default_lip
|
|
213
|
+
case @classname
|
|
214
|
+
when "func_button" then "4"
|
|
215
|
+
else "8"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def default_count
|
|
220
|
+
case @classname
|
|
221
|
+
when "trigger_counter" then "2"
|
|
222
|
+
else "0"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def default_style
|
|
227
|
+
case @classname
|
|
228
|
+
when "light_fluorospark" then "10"
|
|
229
|
+
else "0"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def default_height
|
|
234
|
+
case @classname
|
|
235
|
+
when "trigger_monsterjump" then "200"
|
|
236
|
+
else "0"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def default_takedamage
|
|
241
|
+
if @classname.start_with?("monster_") ||
|
|
242
|
+
@classname == "misc_explobox" ||
|
|
243
|
+
@classname == "misc_explobox2"
|
|
244
|
+
"2"
|
|
245
|
+
else
|
|
246
|
+
"0"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def default_runtime_flags
|
|
251
|
+
return 0 unless @classname.start_with?("monster_")
|
|
252
|
+
|
|
253
|
+
flags = FL_MONSTER
|
|
254
|
+
flags |= FL_FLY if @classname == "monster_wizard"
|
|
255
|
+
flags |= FL_SWIM if @classname == "monster_fish"
|
|
256
|
+
flags
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def default_health
|
|
260
|
+
case @classname
|
|
261
|
+
when "monster_army" then "30"
|
|
262
|
+
when "monster_dog" then "25"
|
|
263
|
+
when "monster_ogre" then "200"
|
|
264
|
+
when "monster_knight" then "75"
|
|
265
|
+
when "monster_wizard" then "80"
|
|
266
|
+
when "monster_demon1" then "300"
|
|
267
|
+
when "monster_zombie" then "60"
|
|
268
|
+
when "monster_fish" then "25"
|
|
269
|
+
when "misc_explobox", "misc_explobox2" then "20"
|
|
270
|
+
else "0"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def default_model
|
|
275
|
+
case @classname
|
|
276
|
+
when "monster_army" then "progs/soldier.mdl"
|
|
277
|
+
when "monster_dog" then "progs/dog.mdl"
|
|
278
|
+
when "monster_ogre" then "progs/ogre.mdl"
|
|
279
|
+
when "monster_knight" then "progs/knight.mdl"
|
|
280
|
+
when "monster_wizard" then "progs/wizard.mdl"
|
|
281
|
+
when "monster_demon1" then "progs/demon.mdl"
|
|
282
|
+
when "monster_zombie" then "progs/zombie.mdl"
|
|
283
|
+
when "weapon_supershotgun" then "progs/g_shot.mdl"
|
|
284
|
+
when "weapon_nailgun" then "progs/g_nail.mdl"
|
|
285
|
+
when "weapon_supernailgun" then "progs/g_nail2.mdl"
|
|
286
|
+
when "weapon_grenadelauncher" then "progs/g_rock.mdl"
|
|
287
|
+
when "weapon_rocketlauncher" then "progs/g_rock2.mdl"
|
|
288
|
+
when "weapon_lightning" then "progs/g_light.mdl"
|
|
289
|
+
when "item_health" then health_model
|
|
290
|
+
when "item_shells" then ammo_box_model("shell")
|
|
291
|
+
when "item_spikes" then ammo_box_model("nail")
|
|
292
|
+
when "item_rockets" then ammo_box_model("rock")
|
|
293
|
+
when "item_cells" then ammo_box_model("batt")
|
|
294
|
+
when "item_armor1", "item_armor2", "item_armorInv" then "progs/armor.mdl"
|
|
295
|
+
when "item_artifact_invulnerability" then "progs/invulner.mdl"
|
|
296
|
+
when "item_artifact_envirosuit" then "progs/suit.mdl"
|
|
297
|
+
when "item_artifact_invisibility" then "progs/invisibl.mdl"
|
|
298
|
+
when "item_artifact_super_damage" then "progs/quaddama.mdl"
|
|
299
|
+
when "item_backpack" then "progs/backpack.mdl"
|
|
300
|
+
when "item_key1" then "progs/w_s_key.mdl"
|
|
301
|
+
when "item_key2" then "progs/w_g_key.mdl"
|
|
302
|
+
when "item_sigil" then sigil_model
|
|
303
|
+
when "item_weapon" then legacy_weapon_ammo_model
|
|
304
|
+
when "misc_explobox" then "maps/b_explob.bsp"
|
|
305
|
+
when "misc_explobox2" then "maps/b_exbox2.bsp"
|
|
306
|
+
when "light_globe" then "progs/s_light.spr"
|
|
307
|
+
when "light_torch_small_walltorch" then "progs/flame.mdl"
|
|
308
|
+
when "light_flame_large_yellow", "light_flame_small_yellow", "light_flame_small_white" then "progs/flame2.mdl"
|
|
309
|
+
when "viewthing" then "progs/player.mdl"
|
|
310
|
+
when "misc_teleporttrain" then "progs/teleport.mdl"
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def default_frame
|
|
315
|
+
case @classname
|
|
316
|
+
when "light_flame_large_yellow" then "1"
|
|
317
|
+
else "0"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def default_skin
|
|
322
|
+
case @classname
|
|
323
|
+
when "item_armor2" then "1"
|
|
324
|
+
when "item_armorInv" then "2"
|
|
325
|
+
else "0"
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def health_model
|
|
330
|
+
if (@spawnflags & 1) != 0
|
|
331
|
+
"maps/b_bh10.bsp"
|
|
332
|
+
elsif (@spawnflags & 2) != 0
|
|
333
|
+
"maps/b_bh100.bsp"
|
|
334
|
+
else
|
|
335
|
+
"maps/b_bh25.bsp"
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def ammo_box_model(prefix)
|
|
340
|
+
suffix = (@spawnflags & 1) != 0 ? "1" : "0"
|
|
341
|
+
"maps/b_#{prefix}#{suffix}.bsp"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def sigil_model
|
|
345
|
+
if (@spawnflags & 8) != 0
|
|
346
|
+
"progs/end4.mdl"
|
|
347
|
+
elsif (@spawnflags & 4) != 0
|
|
348
|
+
"progs/end3.mdl"
|
|
349
|
+
elsif (@spawnflags & 2) != 0
|
|
350
|
+
"progs/end2.mdl"
|
|
351
|
+
elsif (@spawnflags & 1) != 0
|
|
352
|
+
"progs/end1.mdl"
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def legacy_weapon_ammo_model
|
|
357
|
+
big = (@spawnflags & 8) != 0
|
|
358
|
+
if (@spawnflags & 2) != 0
|
|
359
|
+
big ? "maps/b_rock1.bsp" : "maps/b_rock0.bsp"
|
|
360
|
+
elsif (@spawnflags & 4) != 0
|
|
361
|
+
big ? "maps/b_nail1.bsp" : "maps/b_nail0.bsp"
|
|
362
|
+
elsif (@spawnflags & 1) != 0
|
|
363
|
+
big ? "maps/b_shell1.bsp" : "maps/b_shell0.bsp"
|
|
364
|
+
end
|
|
365
|
+
end
|
|
81
366
|
end
|
|
82
367
|
|
|
83
368
|
# Parses the BSP entity string into an array of Entity objects.
|
|
84
369
|
module EntityParser
|
|
370
|
+
SINGLE_CHAR_TOKENS = "{})(:'"
|
|
371
|
+
FIELD_NAMES = %w[
|
|
372
|
+
absmax absmin aflag aiment air_finished ammo_cells ammo_nails
|
|
373
|
+
ammo_rockets ammo_shells angles armortype armorvalue attack_finished
|
|
374
|
+
attack_state avelocity axhitme blocked bubble_count button0 button1
|
|
375
|
+
button2 chain classname cnt colormap count currentammo deadflag
|
|
376
|
+
deathtype delay dest dest1 dest2 distance dmg dmg_inflictor dmg_save
|
|
377
|
+
dmg_take effects enemy finalangle finaldest fixangle flags fly_sound
|
|
378
|
+
frags frame goalentity gravity groundentity health height ideal_yaw
|
|
379
|
+
impulse invincible_finished invincible_sound invincible_time
|
|
380
|
+
invisible_finished invisible_sound invisible_time items jump_flag
|
|
381
|
+
killtarget lastruntime lefty light_lev lip ltime mangle map max_health
|
|
382
|
+
maxs maxspeed mdl message mins model modelindex movedir movetarget
|
|
383
|
+
movetype netname nextthink noise noise1 noise2 noise3 noise4 oldenemy
|
|
384
|
+
oldorigin origin owner pain_finished pausetime pos1 pos2 rad_time
|
|
385
|
+
radsuit_finished search_time show_hostile size skin solid sounds
|
|
386
|
+
spawnflags speed state style super_damage_finished super_sound
|
|
387
|
+
super_time swim_flag t_length t_width takedamage target targetname team
|
|
388
|
+
teleport_time think touch trigger_field use v_angle velocity view_ofs
|
|
389
|
+
voided volume wad wait waitmax waitmin walkframe waterlevel watertype
|
|
390
|
+
weapon weaponframe weaponmodel worldtype yaw_speed
|
|
391
|
+
].each_with_object({}) { |name, fields| fields[name] = true }.freeze
|
|
392
|
+
|
|
85
393
|
def self.parse(entity_string)
|
|
394
|
+
return [] if entity_string.nil?
|
|
395
|
+
|
|
86
396
|
entities = []
|
|
87
397
|
current = nil
|
|
398
|
+
tokens = tokenize(entity_string)
|
|
399
|
+
index = 0
|
|
400
|
+
|
|
401
|
+
while index < tokens.length
|
|
402
|
+
token = tokens[index]
|
|
403
|
+
if current.nil?
|
|
404
|
+
raise "ED_LoadFromFile: found #{token} when expecting {" unless token == "{"
|
|
88
405
|
|
|
89
|
-
entity_string.each_line do |line|
|
|
90
|
-
line = line.strip
|
|
91
|
-
case line
|
|
92
|
-
when "{"
|
|
93
406
|
current = {}
|
|
94
|
-
|
|
95
|
-
|
|
407
|
+
index += 1
|
|
408
|
+
next
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
if token == "}"
|
|
412
|
+
entities << Entity.new(current) if current["classname"]
|
|
96
413
|
current = nil
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
414
|
+
index += 1
|
|
415
|
+
next
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
key = normalize_key(token)
|
|
419
|
+
index += 1
|
|
420
|
+
raise "ED_ParseEntity: EOF without closing brace" if index >= tokens.length
|
|
421
|
+
|
|
422
|
+
value = tokens[index]
|
|
423
|
+
raise "ED_ParseEntity: closing brace without data" if value == "}"
|
|
424
|
+
|
|
425
|
+
value = decode_string(value)
|
|
426
|
+
|
|
427
|
+
unless key.start_with?("_") || (key != "angle" && !FIELD_NAMES[key])
|
|
428
|
+
if key == "angle"
|
|
429
|
+
current["angles"] = "0 #{value} 0"
|
|
430
|
+
else
|
|
431
|
+
current[key] = value
|
|
100
432
|
end
|
|
101
433
|
end
|
|
434
|
+
index += 1
|
|
102
435
|
end
|
|
103
436
|
|
|
437
|
+
raise "ED_ParseEntity: EOF without closing brace" if current
|
|
438
|
+
|
|
104
439
|
entities
|
|
105
440
|
end
|
|
106
441
|
|
|
442
|
+
def self.tokenize(data)
|
|
443
|
+
tokens = []
|
|
444
|
+
index = 0
|
|
445
|
+
|
|
446
|
+
while index < data.length
|
|
447
|
+
char = data[index]
|
|
448
|
+
break if char == "\0"
|
|
449
|
+
|
|
450
|
+
if char.ord <= 32
|
|
451
|
+
index += 1
|
|
452
|
+
next
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
if char == "/" && data[index + 1] == "/"
|
|
456
|
+
index += 2
|
|
457
|
+
index += 1 while index < data.length && data[index] != "\n"
|
|
458
|
+
next
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
if char == '"'
|
|
462
|
+
index += 1
|
|
463
|
+
start = index
|
|
464
|
+
index += 1 while index < data.length && data[index] != '"'
|
|
465
|
+
tokens << data[start...index]
|
|
466
|
+
index += 1
|
|
467
|
+
next
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
if SINGLE_CHAR_TOKENS.include?(char)
|
|
471
|
+
tokens << char
|
|
472
|
+
index += 1
|
|
473
|
+
next
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
start = index
|
|
477
|
+
while index < data.length && data[index].ord > 32 && !SINGLE_CHAR_TOKENS.include?(data[index])
|
|
478
|
+
index += 1
|
|
479
|
+
end
|
|
480
|
+
tokens << data[start...index]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
tokens
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def self.normalize_key(key)
|
|
487
|
+
key = "light_lev" if key == "light"
|
|
488
|
+
key.rstrip
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def self.decode_string(value)
|
|
492
|
+
value.gsub(/\\./) { |match| match == "\\n" ? "\n" : "\\" }
|
|
493
|
+
end
|
|
494
|
+
|
|
107
495
|
# Build a lookup table: targetname -> [Entity]
|
|
108
496
|
def self.build_target_map(entities)
|
|
109
497
|
map = Hash.new { |h, k| h[k] = [] }
|