-
Notifications
You must be signed in to change notification settings - Fork 3
/
blam.lua
3444 lines (3154 loc) · 120 KB
/
blam.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
------------------------------------------------------------------------------
-- Blam! library for Chimera/SAPP Lua scripting
-- Sledmine, JerryBrick
-- Easier memory handle and provides standard functions for scripting
------------------------------------------------------------------------------
local cos = math.cos
local sin = math.sin
local atan = math.atan
local pi = math.pi
math.atan2 = math.atan2 or function(y, x)
return atan(y / x) + (x < 0 and pi or 0)
end
local atan2 = math.atan2
local sqrt = math.sqrt
local fmod = math.fmod
local rad = math.rad
local deg = math.deg
local blam = {_VERSION = "1.14.0"}
------------------------------------------------------------------------------
-- Useful functions for internal usage
------------------------------------------------------------------------------
-- From legacy glue library!
--- String or number to hex
local function tohex(s, upper)
if type(s) == "number" then
return (upper and "%08.8X" or "%08.8x"):format(s)
end
if upper then
return (s:sub(".", function(c)
return ("%02X"):format(c:byte())
end))
else
return (s:gsub(".", function(c)
return ("%02x"):format(c:byte())
end))
end
end
--- Hex to binary string
local function fromhex(s)
if #s % 2 == 1 then
return fromhex("0" .. s)
end
return (s:gsub("..", function(cc)
return string.char(tonumber(cc, 16))
end))
end
local function split(s, sep)
if (sep == nil or sep == "") then
return 1
end
local position, array = 0, {}
for st, sp in function()
return string.find(s, sep, position, true)
end do
table.insert(array, string.sub(s, position, st - 1))
position = sp + 1
end
table.insert(array, string.sub(s, position))
return array
end
local null = 0xFFFFFFFF
--- Get if given value equals a null value in game engine terms
---@param value any
---@return boolean
function blam.isNull(value)
if value == 0xFF or value == 0xFFFF or value == null or value == nil then
return true
end
return false
end
local isNull = blam.isNull
---Return if game instance is host
---@return boolean
function blam.isGameHost()
return server_type == "local"
end
---Return if game instance is single player
---@return boolean
function blam.isGameSinglePlayer()
return server_type == "none"
end
---Return if the game instance is running on a dedicated server or connected as a "network client"
---@return boolean
function blam.isGameDedicated()
return server_type == "dedicated"
end
---Return if the game instance is a SAPP server
---@return boolean
function blam.isGameSAPP()
return register_callback or server_type == "sapp"
end
------------------------------------------------------------------------------
-- Blam! engine data
------------------------------------------------------------------------------
---@alias tagId number
-- Engine address list
local addressList = {
tagDataHeader = 0x40440000,
cameraType = 0x00647498, -- from giraffe
gamePaused = 0x004ACA79,
gameOnMenus = 0x00622058,
joystickInput = 0x64D998, -- from aLTis
firstPerson = 0x40000EB8, -- from aLTis
objectTable = 0x400506B4,
deviceGroupsTable = 0x00816110,
widgetsInstance = 0x6B401C,
-- syncedNetworkObjects = 0x004F7FA2
syncedNetworkObjects = 0x006226F0, -- pointer, from Vulpes
screenResolution = 0x637CF0,
currentWidgetIdAddress = 0x6B401C,
cinematicGlobals = 0x0068c83c
}
-- Server side addresses adjustment
if blam.isGameSAPP() then
addressList.deviceGroupsTable = 0x006E1C50
addressList.objectTable = 0x4005062C
addressList.syncedNetworkObjects = 0x00598020 -- not pointer cause cheat engine sucks
addressList.cinematicGlobals = 0x005f506c
end
-- Tag classes values
---@enum tagClasses
local tagClasses = {
actorVariant = "actv",
actor = "actr",
antenna = "ant!",
biped = "bipd",
bitmap = "bitm",
cameraTrack = "trak",
colorTable = "colo",
continuousDamageEffect = "cdmg",
contrail = "cont",
damageEffect = "jpt!",
decal = "deca",
detailObjectCollection = "dobc",
deviceControl = "ctrl",
deviceLightFixture = "lifi",
deviceMachine = "mach",
device = "devi",
dialogue = "udlg",
effect = "effe",
equipment = "eqip",
flag = "flag",
fog = "fog ",
font = "font",
garbage = "garb",
gbxmodel = "mod2",
globals = "matg",
glow = "glw!",
grenadeHudInterface = "grhi",
hudGlobals = "hudg",
hudMessageText = "hmt ",
hudNumber = "hud#",
itemCollection = "itmc",
item = "item",
lensFlare = "lens",
lightVolume = "mgs2",
light = "ligh",
lightning = "elec",
materialEffects = "foot",
meter = "metr",
modelAnimations = "antr",
modelCollisiionGeometry = "coll",
model = "mode",
multiplayerScenarioDescription = "mply",
object = "obje",
particleSystem = "pctl",
particle = "part",
physics = "phys",
placeholder = "plac",
pointPhysics = "pphy",
preferencesNetworkGame = "ngpr",
projectile = "proj",
scenarioStructureBsp = "sbsp",
scenario = "scnr",
scenery = "scen",
shaderEnvironment = "senv",
shaderModel = "soso",
shaderTransparentChicagoExtended = "scex",
shaderTransparentChicago = "schi",
shaderTransparentGeneric = "sotr",
shaderTransparentGlass = "sgla",
shaderTransparentMeter = "smet",
shaderTransparentPlasma = "spla",
shaderTransparentWater = "swat",
shader = "shdr",
sky = "sky ",
soundEnvironment = "snde",
soundLooping = "lsnd",
soundScenery = "ssce",
sound = "snd!",
spheroid = "boom",
stringList = "str#",
tagCollection = "tagc",
uiWidgetCollection = "Soul",
uiWidgetDefinition = "DeLa",
unicodeStringList = "ustr",
unitHudInterface = "unhi",
unit = "unit",
vehicle = "vehi",
virtualKeyboard = "vcky",
weaponHudInterface = "wphi",
weapon = "weap",
weatherParticleSystem = "rain",
wind = "wind"
}
-- Blam object classes values
---@enum objectClasses
local objectClasses = {
biped = 0,
vehicle = 1,
weapon = 2,
equipment = 3,
garbage = 4,
projectile = 5,
scenery = 6,
machine = 7,
control = 8,
lightFixture = 9,
placeHolder = 10,
soundScenery = 11
}
-- Camera types
---@enum cameraTypes
local cameraTypes = {
scripted = 1, -- 22192
firstPerson = 2, -- 30400
devcam = 3, -- 30704
thirdPerson = 4, -- 31952
deadCamera = 5 -- 23776
}
-- Netgame flag classes
---@enum netgameFlagClasses
local netgameFlagClasses = {
ctfFlag = 0,
ctfVehicle = 1,
ballSpawn = 2,
raceTrack = 3,
raceVehicle = 4,
vegasBank = 5,
teleportFrom = 6,
teleportTo = 7,
hillFlag = 8
}
-- Game type classes
---@enum gameTypeClasses
local gameTypeClasses = {
none = 0,
ctf = 1,
slayer = 2,
oddball = 3,
koth = 4,
race = 5,
terminator = 6,
stub = 7,
ignored1 = 8,
ignored2 = 9,
ignored3 = 10,
ignored4 = 11,
allGames = 12,
allExceptCtf = 13,
allExceptRaceCtf = 14
}
-- Multiplayer team classes
---@enum multiplayerTeamClasses
local multiplayerTeamClasses = {red = 0, blue = 1}
-- Unit team classes
---@enum unitTeamClasses
local unitTeamClasses = {
defaultByUnit = 0,
player = 1,
human = 2,
covenant = 3,
flood = 4,
sentinel = 5,
unused6 = 6,
unused7 = 7,
unused8 = 8,
unused9 = 9
}
-- Object network role classes
---@enum objectNetworkRoleClasses
local objectNetworkRoleClasses = {
master = 0,
puppet = 1,
locallyControlledPuppet = 2,
localOnly = 3
}
-- Standard console colors
local consoleColors = {
success = {1, 0.235, 0.82, 0},
warning = {1, 0.94, 0.75, 0.098},
error = {1, 1, 0.2, 0.2},
unknown = {1, 0.66, 0.66, 0.66}
}
-- Offset input from the joystick game data
local joystickInputs = {
-- No zero values also pressed time until maxmimum byte size
button1 = 0, -- Triangle
button2 = 1, -- Circle
button3 = 2, -- Cross
button4 = 3, -- Square
leftBumper = 4,
rightBumper = 5,
leftTrigger = 6,
rightTrigger = 7,
backButton = 8,
startButton = 9,
leftStick = 10,
rightStick = 11,
rightStick2 = 12,
-- TODO Add joys axis
leftStickUp = 30,
leftStickDown = 32,
rightStickUp = 34,
rightStickDown = 36,
triggers = 38,
-- Multiple values on the same offset, check dPadValues table
dPad = 96,
-- Non zero values
dPadUp = 100,
dPadDown = 104,
dPadLeft = 106,
dPadRight = 102,
dPadUpRight = 101,
dPadDownRight = 103,
dPadUpLeft = 107,
dPadDownLeft = 105
}
-- Values for the possible dPad values from the joystick inputs
local dPadValues = {
noButton = 1020,
upRight = 766,
downRight = 768,
upLeft = 772,
downLeft = 770,
left = 771,
right = 767,
down = 769,
up = 765
}
local engineConstants = {defaultNetworkObjectsCount = 509}
-- Global variables
--- This is the current gametype that is running. If no gametype is running, this will be set to nil
---, possible values are: ctf, slayer, oddball, king, race.
---@type string | nil
gametype = gametype
---This is the index of the local player. This is a value between 0 and 15, this value does not
---match with player index in the server and is not instantly assigned after joining.
---@type number | nil
local_player_index = local_player_index
---This is the name of the current loaded map.
---@type string
map = map
---Return if the map has protected tags data.
---@type boolean
map_is_protected = map_is_protected
---This is the name of the script. If the script is a global script, it will be defined as the
---filename of the script. Otherwise, it will be the name of the map.
---@type string
script_name = script_name
---This is the script type, possible values are global or map.
---@type string
script_type = script_type
---@type '"none"' | '"local"' | '"dedicated"' | '"sapp"'
server_type = server_type
---Return whether or not the script is sandboxed. See Sandoboxed Scripts for more information.
---@deprecated
---@type boolean
sandboxed = sandboxed ---@diagnostic disable-line: deprecated
local backupFunctions = {}
backupFunctions.console_is_open = _G.console_is_open
backupFunctions.console_out = _G.console_out
backupFunctions.execute_script = _G.execute_script
backupFunctions.get_global = _G.get_global
-- backupFunctions.set_global = _G.set_global
backupFunctions.get_tag = _G.get_tag
backupFunctions.set_callback = _G.set_callback
backupFunctions.set_timer = _G.set_timer
backupFunctions.stop_timer = _G.stop_timer
backupFunctions.spawn_object = _G.spawn_object
backupFunctions.delete_object = _G.delete_object
backupFunctions.get_object = _G.get_object
backupFunctions.get_dynamic_player = _G.get_dynamic_player
backupFunctions.hud_message = _G.hud_message
backupFunctions.create_directory = _G.create_directory
backupFunctions.remove_directory = _G.remove_directory
backupFunctions.directory_exists = _G.directory_exists
backupFunctions.list_directory = _G.list_directory
backupFunctions.write_file = _G.write_file
backupFunctions.read_file = _G.read_file
backupFunctions.delete_file = _G.delete_file
backupFunctions.file_exists = _G.file_exists
------------------------------------------------------------------------------
-- Chimera API auto completion
-- EmmyLua autocompletion for some functions!
-- Functions below do not have a real implementation and are not supossed to be imported
------------------------------------------------------------------------------
---Attempt to spawn an object given tag class, tag path and coordinates.
---Given a tag id is also accepted.
---@overload fun(tagId: number, x: number, y: number, z: number):number
---@param tagClass tagClasses Type of the tag to spawn
---@param tagPath string Path of object to spawn
---@param x number
---@param y number
---@param z number
---@return number? objectId
function spawn_object(tagClass, tagPath, x, y, z)
end
---Attempt to get the address of a player unit object given player index, returning nil on failure.<br>
---If no argument is given, the address to the local player’s unit object is returned, instead.
---@param playerIndex? number
---@return number? objectAddress
function get_dynamic_player(playerIndex)
end
spawn_object = backupFunctions.spawn_object
get_dynamic_player = backupFunctions.get_dynamic_player
------------------------------------------------------------------------------
-- SAPP API bindings
------------------------------------------------------------------------------
---Write content to a text file given file path
---@param path string Path to the file to write
---@param content string Content to write into the file
---@return boolean, string? result True if successful otherwise nil, error
function write_file(path, content)
local file, error = io.open(path, "w")
if (not file) then
return false, error
end
local success, err = file:write(content)
file:close()
if (not success) then
os.remove(path)
return false, err
else
return true
end
end
---Read the contents from a file given file path.
---@param path string Path to the file to read
---@return boolean, string? content string if successful otherwise nil, error
function read_file(path)
local file, error = io.open(path, "r")
if (not file) then
return false, error
end
local content, error = file:read("*a")
if (content == nil) then
return false, error
end
file:close()
return content
end
---Attempt create a directory with the given path.
---
---An error will occur if the directory can not be created.
---@param path string Path to the directory to create
---@return boolean
function create_directory(path)
local success, error = os.execute("mkdir " .. path)
if (not success) then
return false
end
return true
end
---Attempt to remove a directory with the given path.
---
---An error will occur if the directory can not be removed.
---@param path string Path to the directory to remove
---@return boolean
function remove_directory(path)
local success, error = os.execute("rmdir -r " .. path)
if (not success) then
return false
end
return true
end
---Verify if a directory exists given directory path
---@param path string
---@return boolean
function directory_exists(path)
print("directory_exists", path)
return os.execute("dir \"" .. path .. "\" > nul") == 0
end
---List the contents from a directory given directory path
---@param path string
---@return nil | integer | table
function list_directory(path)
-- TODO This needs a way to separate folders from files
if (path) then
local command = "dir \"" .. path .. "\" /B"
local pipe = io.popen(command, "r")
if pipe then
local output = pipe:read("*a")
if (output) then
local items = split(output, "\n")
for index, item in pairs(items) do
if (item and item == "") then
items[index] = nil
end
end
return items
end
end
end
return nil
end
---Delete a file given file path
---@param path string
---@return boolean
function delete_file(path)
return os.remove(path)
end
---Return if a file exists given file path.
---@param path string
---@return boolean
function file_exists(path)
local file = io.open(path, "r")
if (file) then
file:close()
return true
end
return false
end
---Return the memory address of a tag given tagId or tagClass and tagPath
---@param tagIdOrTagType string | number
---@param tagPath? string
---@return number?
function get_tag(tagIdOrTagType, tagPath)
if (not tagPath) then
return lookup_tag(tagIdOrTagType)
else
return lookup_tag(tagIdOrTagType, tagPath)
end
end
---Execute a custom Halo script.
---
---A script can be either a standalone Halo command or a Lisp-formatted Halo scripting block.
---@param command string
function execute_script(command)
return execute_command(command)
end
---Return the address of the object memory given object id
---@param objectId number
---@return number?
function get_object(objectId)
if (objectId) then
local object_memory = get_object_memory(objectId)
if (object_memory ~= 0) then
return object_memory
end
end
return nil
end
---Despawn an object given objectId. An error will occur if the object does not exist.
---@param objectId number
function delete_object(objectId)
destroy_object(objectId)
end
---Output text to the console, optional text colors in decimal format.<br>
---Avoid sending console messages if console_is_open() is true to avoid annoying the player.
---@param message string | number
---@param red? number
---@param green? number
---@param blue? number
function console_out(message, red, green, blue)
-- TODO Add color printing to this function on SAPP
cprint(message)
end
---Output text to console as debug message.
---
---This function will only output text if the debug mode is enabled.
---@param message string
function console_debug(message)
if DebugMode then
console_out(message)
end
end
---Return true if the player has the console open, always returns true on SAPP.
---@return boolean
function console_is_open()
return true
end
---Get the value of a Halo scripting global.\
---An error will be triggered if the global is not found
---@param name string Name of the global variable to get from hsc
---@return boolean | number
function get_global(name)
error("SAPP can not retrieve global variables as Chimera does.. yet!")
end
---Print message to player HUD.\
---Messages will be printed to console if SAPP uses this function
---@param message string
function hud_message(message)
cprint(message)
end
---Set the callback for an event game from the game events available on Chimera
---@param event '"command"' | '"frame"' | '"preframe"' | '"map load"' | '"precamera"' | '"rcon message"' | '"tick"' | '"pretick"' | '"unload"'
---@param callback string Global function name to call when the event is triggered
function set_callback(event, callback)
if event == "tick" then
register_callback(cb["EVENT_TICK"], callback)
elseif event == "pretick" then
error("SAPP does not support pretick event")
elseif event == "frame" then
error("SAPP does not support frame event")
elseif event == "preframe" then
error("SAPP does not support preframe event")
elseif event == "map load" then
register_callback(cb["EVENT_GAME_START"], callback)
elseif event == "precamera" then
error("SAPP does not support precamera event")
elseif event == "rcon message" then
_G[callback .. "_rcon_message"] = function(playerIndex,
command,
environment,
password)
return _G[callback](playerIndex, command, password)
end
register_callback(cb["EVENT_COMMAND"], callback .. "_rcon_message")
elseif event == "command" then
_G[callback .. "_command"] = function(playerIndex, command, environment)
return _G[callback](playerIndex, command, environment)
end
register_callback(cb["EVENT_COMMAND"], callback .. "_command")
elseif event == "unload" then
register_callback(cb["EVENT_GAME_END"], callback)
else
error("Unknown event: " .. event)
end
end
---Register a timer to be called every intervalMilliseconds.<br>
---The callback function will be called with the arguments passed after the callbackName.<br>
---
---**WARNING:** SAPP will not return a timerId, it will return nil instead so timers can not be stopped.
---@param intervalMilliseconds number
---@param globalFunctionCallbackName string
---@vararg any
---@return number?
function set_timer(intervalMilliseconds, globalFunctionCallbackName, ...)
return timer(intervalMilliseconds, globalFunctionCallbackName, ...)
end
function stop_timer(timerId)
error("SAPP does not support stopping timers")
end
if register_callback then
-- Provide global server type variable on SAPP
server_type = "sapp"
print("Compatibility with Chimera Lua API has been loaded!")
else
console_is_open = backupFunctions.console_is_open
console_out = backupFunctions.console_out
execute_script = backupFunctions.execute_script
get_global = backupFunctions.get_global
-- set_global = -- backupFunctions.set_global
get_tag = backupFunctions.get_tag
set_callback = backupFunctions.set_callback
set_timer = backupFunctions.set_timer
stop_timer = backupFunctions.stop_timer
spawn_object = backupFunctions.spawn_object
delete_object = backupFunctions.delete_object
get_object = backupFunctions.get_object
get_dynamic_player = backupFunctions.get_dynamic_player
hud_message = backupFunctions.hud_message
create_directory = backupFunctions.create_directory
remove_directory = backupFunctions.remove_directory
directory_exists = backupFunctions.directory_exists
list_directory = backupFunctions.list_directory
write_file = backupFunctions.write_file
read_file = backupFunctions.read_file
delete_file = backupFunctions.delete_file
file_exists = backupFunctions.file_exists
end
------------------------------------------------------------------------------
-- Generic functions
------------------------------------------------------------------------------
--- Verify if the given variable is a number
---@param var any
---@return boolean
local function isNumber(var)
return (type(var) == "number")
end
--- Verify if the given variable is a string
---@param var any
---@return boolean
local function isString(var)
return (type(var) == "string")
end
--- Verify if the given variable is a boolean
---@param var any
---@return boolean
local function isBoolean(var)
return (type(var) == "boolean")
end
--- Verify if the given variable is a table
---@param var any
---@return boolean
local function isTable(var)
return (type(var) == "table")
end
--- Remove spaces and tabs from the beginning and the end of a string
---@param str string
---@return string
local function trim(str)
return str:match("^%s*(.*)"):match("(.-)%s*$")
end
--- Verify if the value is valid
---@param var any
---@return boolean
local function isValid(var)
return (var and var ~= "" and var ~= 0)
end
------------------------------------------------------------------------------
-- Utilities
------------------------------------------------------------------------------
--- Convert tag class int to string
---@param tagClassInt number
---@return string?
local function tagClassFromInt(tagClassInt)
if (tagClassInt) then
local tagClassHex = tohex(tagClassInt)
local tagClass = ""
if (tagClassHex) then
local byte = ""
for char in string.gmatch(tagClassHex, ".") do
byte = byte .. char
if (#byte % 2 == 0) then
tagClass = tagClass .. string.char(tonumber(byte, 16))
byte = ""
end
end
end
return tagClass
end
return nil
end
--- Return a list of object indexes that are currently spawned, indexed by their object id.
---@return number[]
function blam.getObjects()
local objects = {}
for objectIndex = 0, 2047 do
local object, objectId = blam.getObject(objectIndex)
if object and objectId then
objects[objectId] = objectIndex
-- objects[objectIndex] = objectId
end
end
return objects
end
-- Local reference to the original console_out function
local original_console_out = console_out
--- Print a console message. It also supports multi-line messages!
---@param message string
local function consoleOutput(message, ...)
-- Put the extra arguments into a table
local args = {...}
if (message == nil or #args > 5) then
consoleOutput(debug.traceback("Wrong number of arguments on console output function", 2),
consoleColors.error)
end
-- Output color
local colorARGB = {1, 1, 1, 1}
-- Get the output color from arguments table
if (isTable(args[1])) then
colorARGB = args[1]
elseif (#args == 3 or #args == 4) then
colorARGB = args
end
-- Set alpha channel if not set
if (#colorARGB == 3) then
table.insert(colorARGB, 1, 1)
end
if message then
if (isString(message)) then
-- Explode the string!!
for line in message:gmatch("([^\n]+)") do
-- Trim the line
local trimmedLine = trim(line)
-- Print the line
original_console_out(trimmedLine, table.unpack(colorARGB))
end
else
original_console_out(message, table.unpack(colorARGB))
end
end
end
--- Convert booleans to bits and bits to booleans
---@param bitOrBool number
---@return boolean | number
local function b2b(bitOrBool)
if (bitOrBool == 1) then
return true
elseif (bitOrBool == 0) then
return false
elseif (bitOrBool == true) then
return 1
elseif (bitOrBool == false) then
return 0
end
error("B2B error, expected boolean or bit value, got " .. tostring(bitOrBool) .. " " ..
type(bitOrBool))
end
------------------------------------------------------------------------------
-- Data manipulation and binding
------------------------------------------------------------------------------
local typesOperations
local function readBit(address, propertyData)
return b2b(read_bit(address, propertyData.bitLevel))
end
local function writeBit(address, propertyData, propertyValue)
return write_bit(address, propertyData.bitLevel, b2b(propertyValue))
end
local function readByte(address)
return read_byte(address)
end
local function writeByte(address, propertyData, propertyValue)
return write_byte(address, propertyValue)
end
local function readShort(address)
return read_short(address)
end
local function writeShort(address, propertyData, propertyValue)
return write_short(address, propertyValue)
end
local function readWord(address)
return read_word(address)
end
local function writeWord(address, propertyData, propertyValue)
return write_word(address, propertyValue)
end
local function readInt(address)
return read_int(address)
end
local function writeInt(address, propertyData, propertyValue)
return write_int(address, propertyValue)
end
local function readDword(address)
return read_dword(address)
end
local function writeDword(address, propertyData, propertyValue)
return write_dword(address, propertyValue)
end
local function readFloat(address)
return read_float(address)
end
local function writeFloat(address, propertyData, propertyValue)
return write_float(address, propertyValue)
end
local function readChar(address)
return read_char(address)
end
local function writeChar(address, propertyData, propertyValue)
return write_char(address, propertyValue)
end
local function readString(address)
return read_string(address)
end
local function writeString(address, propertyData, propertyValue)
return write_string(address, propertyValue)
end
--- Return the string of a unicode string given address
---@param address number
---@param rawRead? boolean
---@return string
function blam.readUnicodeString(address, rawRead)
local stringAddress
if rawRead then
stringAddress = address
else
stringAddress = read_dword(address)
end
local length = stringAddress / 2
local output = ""
-- TODO Refactor this to support full unicode char size
for i = 1, length do
local char = read_string(stringAddress + (i - 1) * 0x2)
if char == "" then
break
end
output = output .. char
end
return output
end
--- Writes a unicode string in a given address
---@param address number
---@param newString string
---@param rawWrite? boolean
function blam.writeUnicodeString(address, newString, rawWrite)
local stringAddress
if rawWrite then
stringAddress = address
else
stringAddress = read_dword(address)
end
-- Allow cancelling writing when the new string is a boolean false value
if newString == false then
return
end
local newString = tostring(newString)
-- TODO Refactor this to support writing ASCII and Unicode strings