-
Notifications
You must be signed in to change notification settings - Fork 175
/
Copy pathsh_util.lua
1183 lines (980 loc) · 33.6 KB
/
sh_util.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
--- Various useful helper functions.
-- @module ix.util
ix.type = ix.type or {
[2] = "string",
[4] = "text",
[8] = "number",
[16] = "player",
[32] = "steamid",
[64] = "character",
[128] = "bool",
[1024] = "color",
[2048] = "vector",
string = 2,
text = 4,
number = 8,
player = 16,
steamid = 32,
character = 64,
bool = 128,
color = 1024,
vector = 2048,
optional = 256,
array = 512
}
ix.blurRenderQueue = {}
--- Includes a lua file based on the prefix of the file. This will automatically call `include` and `AddCSLuaFile` based on the
-- current realm. This function should always be called shared to ensure that the client will receive the file from the server.
-- @realm shared
-- @string fileName Path of the Lua file to include. The path is relative to the file that is currently running this function
-- @string[opt] realm Realm that this file should be included in. You should usually ignore this since it
-- will be automatically be chosen based on the `SERVER` and `CLIENT` globals. This value should either be `"server"` or
-- `"client"` if it is filled in manually
function ix.util.Include(fileName, realm)
if (!fileName) then
error("[Helix] No file name specified for including.")
end
-- Only include server-side if we're on the server.
if ((realm == "server" or fileName:find("sv_")) and SERVER) then
return include(fileName)
-- Shared is included by both server and client.
elseif (realm == "shared" or fileName:find("shared.lua") or fileName:find("sh_")) then
if (SERVER) then
-- Send the file to the client if shared so they can run it.
AddCSLuaFile(fileName)
end
return include(fileName)
-- File is sent to client, included on client.
elseif (realm == "client" or fileName:find("cl_")) then
if (SERVER) then
AddCSLuaFile(fileName)
else
return include(fileName)
end
end
end
--- Includes multiple files in a directory.
-- @realm shared
-- @string directory Directory to include files from
-- @bool[opt] bFromLua Whether or not to search from the base `lua/` folder, instead of contextually basing from `schema/`
-- or `gamemode/`
-- @see ix.util.Include
function ix.util.IncludeDir(directory, bFromLua)
-- By default, we include relatively to Helix.
local baseDir = "helix"
-- If we're in a schema, include relative to the schema.
if (Schema and Schema.folder and Schema.loading) then
baseDir = Schema.folder.."/schema/"
else
baseDir = baseDir.."/gamemode/"
end
-- Find all of the files within the directory.
for _, v in ipairs(file.Find((bFromLua and "" or baseDir)..directory.."/*.lua", "LUA")) do
-- Include the file from the prefix.
ix.util.Include(directory.."/"..v)
end
end
--- Removes the realm prefix from a file name. The returned string will be unchanged if there is no prefix found.
-- @realm shared
-- @string name String to strip prefix from
-- @treturn string String stripped of prefix
-- @usage print(ix.util.StripRealmPrefix("sv_init.lua"))
-- > init.lua
function ix.util.StripRealmPrefix(name)
local prefix = name:sub(1, 3)
return (prefix == "sh_" or prefix == "sv_" or prefix == "cl_") and name:sub(4) or name
end
--- Returns `true` if the given input is a color table. This is necessary since the engine `IsColor` function only checks for
-- color metatables - which are not used for regular Lua color types.
-- @realm shared
-- @param input Input to check
-- @treturn bool Whether or not the input is a color
function ix.util.IsColor(input)
return istable(input) and
isnumber(input.a) and isnumber(input.g) and isnumber(input.b) and (input.a and isnumber(input.a) or input.a == nil)
end
--- Returns a dimmed version of the given color by the given scale.
-- @realm shared
-- @color color Color to dim
-- @number multiplier What to multiply the red, green, and blue values by
-- @number[opt=255] alpha Alpha to use in dimmed color
-- @treturn color Dimmed color
-- @usage print(ix.util.DimColor(Color(100, 100, 100, 255), 0.5))
-- > 50 50 50 255
function ix.util.DimColor(color, multiplier, alpha)
return Color(color.r * multiplier, color.g * multiplier, color.b * multiplier, alpha or 255)
end
--- Sanitizes an input value with the given type. This function ensures that a valid type is always returned. If a valid value
-- could not be found, it will return the default value for the type. This only works for simple types - e.g it does not work
-- for player, character, or Steam ID types.
-- @realm shared
-- @ixtype type Type to check for
-- @param input Value to sanitize
-- @return Sanitized value
-- @see ix.type
-- @usage print(ix.util.SanitizeType(ix.type.number, "123"))
-- > 123
-- print(ix.util.SanitizeType(ix.type.bool, 1))
-- > true
function ix.util.SanitizeType(type, input)
if (type == ix.type.string) then
return tostring(input)
elseif (type == ix.type.text) then
return tostring(input)
elseif (type == ix.type.number) then
return tonumber(input or 0) or 0
elseif (type == ix.type.bool) then
return tobool(input)
elseif (type == ix.type.color) then
return istable(input) and
Color(tonumber(input.r) or 255, tonumber(input.g) or 255, tonumber(input.b) or 255, tonumber(input.a) or 255) or
color_white
elseif (type == ix.type.vector) then
return isvector(input) and input or vector_origin
elseif (type == ix.type.array) then
return input
else
error("attempted to sanitize " .. (ix.type[type] and ("invalid type " .. ix.type[type]) or "unknown type " .. type))
end
end
do
local typeMap = {
string = ix.type.string,
number = ix.type.number,
Player = ix.type.player,
boolean = ix.type.bool,
Vector = ix.type.vector
}
local tableMap = {
[ix.type.character] = function(value)
return getmetatable(value) == ix.meta.character
end,
[ix.type.color] = function(value)
return ix.util.IsColor(value)
end,
[ix.type.steamid] = function(value)
return isstring(value) and (value:match("STEAM_(%d+):(%d+):(%d+)")) != nil
end
}
--- Returns the `ix.type` of the given value.
-- @realm shared
-- @param value Value to get the type of
-- @treturn ix.type Type of value
-- @see ix.type
-- @usage print(ix.util.GetTypeFromValue("hello"))
-- > 2 -- i.e the value of ix.type.string
function ix.util.GetTypeFromValue(value)
local result = typeMap[type(value)]
if (result) then
return result
end
if (istable(value)) then
for k, v in pairs(tableMap) do
if (v(value)) then
return k
end
end
end
end
end
function ix.util.Bind(self, callback)
return function(_, ...)
return callback(self, ...)
end
end
-- Returns the address:port of the server.
function ix.util.GetAddress()
local address = tonumber(GetConVarString("hostip"))
if (!address) then
return "127.0.0.1"..":"..GetConVarString("hostport")
end
local ip = {}
ip[1] = bit.rshift(bit.band(address, 0xFF000000), 24)
ip[2] = bit.rshift(bit.band(address, 0x00FF0000), 16)
ip[3] = bit.rshift(bit.band(address, 0x0000FF00), 8)
ip[4] = bit.band(address, 0x000000FF)
return table.concat(ip, ".")..":"..GetConVarString("hostport")
end
--- Returns a cached copy of the given material, or creates and caches one if it doesn't exist. This is a quick helper function
-- if you aren't locally storing a `Material()` call.
-- @realm shared
-- @string materialPath Path to the material
-- @treturn[1] material The cached material
-- @treturn[2] nil If the material doesn't exist in the filesystem
function ix.util.GetMaterial(materialPath)
-- Cache the material.
ix.util.cachedMaterials = ix.util.cachedMaterials or {}
ix.util.cachedMaterials[materialPath] = ix.util.cachedMaterials[materialPath] or Material(materialPath)
return ix.util.cachedMaterials[materialPath]
end
--- Attempts to find a player by matching their name or Steam ID.
-- @realm shared
-- @string identifier Search query
-- @bool[opt=false] bAllowPatterns Whether or not to accept Lua patterns in `identifier`
-- @treturn player Player that matches the given search query - this will be `nil` if a player could not be found
function ix.util.FindPlayer(identifier, bAllowPatterns)
if (#identifier == 0) then return end
if (string.find(identifier, "STEAM_(%d+):(%d+):(%d+)")) then
return player.GetBySteamID(identifier)
end
if (!bAllowPatterns) then
identifier = string.PatternSafe(identifier)
end
for _, v in player.Iterator() do
if (ix.util.StringMatches(v:Name(), identifier)) then
return v
end
end
end
--- Checks to see if two strings are equivalent using a fuzzy manner. Both strings will be lowered, and will return `true` if
-- the strings are identical, or if `b` is a substring of `a`.
-- @realm shared
-- @string a First string to check
-- @string b Second string to check
-- @treturn bool Whether or not the strings are equivalent
function ix.util.StringMatches(a, b)
if (a and b) then
local a2, b2 = a:utf8lower(), b:utf8lower()
-- Check if the actual letters match.
if (a == b) then return true end
if (a2 == b2) then return true end
-- Be less strict and search.
if (a:find(b)) then return true end
if (a2:find(b2)) then return true end
end
return false
end
--- Returns a string that has the named arguments in the format string replaced with the given arguments.
-- @realm shared
-- @string format Format string
-- @tparam tab|... Arguments to pass to the formatted string. If passed a table, it will use that table as the lookup table for
-- the named arguments. If passed multiple arguments, it will replace the arguments in the string in order.
-- @usage print(ix.util.FormatStringNamed("Hi, my name is {name}.", {name = "Bobby"}))
-- > Hi, my name is Bobby.
-- @usage print(ix.util.FormatStringNamed("Hi, my name is {name}.", "Bobby"))
-- > Hi, my name is Bobby.
function ix.util.FormatStringNamed(format, ...)
local arguments = {...}
local bArray = false -- Whether or not the input has numerical indices or named ones
local input
-- If the first argument is a table, we can assumed it's going to specify which
-- keys to fill out. Otherwise we'll fill in specified arguments in order.
if (istable(arguments[1])) then
input = arguments[1]
else
input = arguments
bArray = true
end
local i = 0
local result = format:gsub("{(%w-)}", function(word)
i = i + 1
return tostring((bArray and input[i] or input[word]) or word)
end)
return result
end
do
local upperMap = {
["ooc"] = true,
["looc"] = true,
["afk"] = true,
["url"] = true
}
--- Returns a string that is the given input with spaces in between each CamelCase word. This function will ignore any words
-- that do not begin with a capital letter. The words `ooc`, `looc`, `afk`, and `url` will be automatically transformed
-- into uppercase text. This will not capitalize non-ASCII letters due to limitations with Lua's pattern matching.
-- @realm shared
-- @string input String to expand
-- @bool[opt=false] bNoUpperFirst Whether or not to avoid capitalizing the first character. This is useful for lowerCamelCase
-- @treturn string Expanded CamelCase string
-- @usage print(ix.util.ExpandCamelCase("HelloWorld"))
-- > Hello World
function ix.util.ExpandCamelCase(input, bNoUpperFirst)
input = bNoUpperFirst and input or input:utf8sub(1, 1):utf8upper() .. input:utf8sub(2)
-- extra parentheses to select first return value of gsub
return string.TrimRight((input:gsub("%u%l+", function(word)
if (upperMap[word:utf8lower()]) then
word = word:utf8upper()
end
return word .. " "
end)))
end
end
function ix.util.GridVector(vec, gridSize)
if (gridSize <= 0) then
gridSize = 1
end
for i = 1, 3 do
vec[i] = vec[i] / gridSize
vec[i] = math.Round(vec[i])
vec[i] = vec[i] * gridSize
end
return vec
end
do
local i
local value
local character
local function iterator(table)
repeat
i = i + 1
value = table[i]
character = value and value:GetCharacter()
until character or value == nil
return value, character
end
--- Returns an iterator for characters. The resulting key/values will be a player and their corresponding characters. This
-- iterator skips over any players that do not have a valid character loaded.
-- @realm shared
-- @treturn Iterator
-- @usage for client, character in ix.util.GetCharacters() do
-- print(client, character)
-- end
-- > Player [1][Bot01] character[1]
-- > Player [2][Bot02] character[2]
-- -- etc.
function ix.util.GetCharacters()
i = 0
return iterator, player.GetAll()
end
end
--- Retrieves the client associated with a character by their character ID.
-- @realm shared
-- @tparam number ID The ID of the character to find the associated client
-- @treturn player|nil The client associated with the character, or `nil` if no client is found
-- @usage
-- local client = ix.util.GetUserByCharacterID(123)
-- if IsValid(client) then
-- print(client:Nick() .. " is the player associated with the character ID.")
-- else
-- print("No client found for that character ID.")
-- end
function ix.util.GetUserByCharacterID(ID)
ID = tonumber(ID)
for client, character in ix.util.GetCharacters() do
if not character then continue end
if character:GetID() == ID then return client end
end
return nil
end
if (CLIENT) then
local blur = ix.util.GetMaterial("pp/blurscreen")
local surface = surface
--- Blurs the content underneath the given panel. This will fall back to a simple darkened rectangle if the player has
-- blurring disabled.
-- @realm client
-- @tparam panel panel Panel to draw the blur for
-- @number[opt=5] amount Intensity of the blur. This should be kept between 0 and 10 for performance reasons
-- @number[opt=0.2] passes Quality of the blur. This should be kept as default
-- @number[opt=255] alpha Opacity of the blur
-- @usage function PANEL:Paint(width, height)
-- ix.util.DrawBlur(self)
-- end
function ix.util.DrawBlur(panel, amount, passes, alpha)
amount = amount or 5
if (ix.option.Get("cheapBlur", false)) then
surface.SetDrawColor(50, 50, 50, alpha or (amount * 20))
surface.DrawRect(0, 0, panel:GetWide(), panel:GetTall())
else
surface.SetMaterial(blur)
surface.SetDrawColor(255, 255, 255, alpha or 255)
local x, y = panel:LocalToScreen(0, 0)
for i = -(passes or 0.2), 1, 0.2 do
-- Do things to the blur material to make it blurry.
blur:SetFloat("$blur", i * amount)
blur:Recompute()
-- Draw the blur material over the screen.
render.UpdateScreenEffectTexture()
surface.DrawTexturedRect(x * -1, y * -1, ScrW(), ScrH())
end
end
end
--- Draws a blurred rectangle with the given position and bounds. This shouldn't be used for panels, see `ix.util.DrawBlur`
-- instead.
-- @realm client
-- @number x X-position of the rectangle
-- @number y Y-position of the rectangle
-- @number width Width of the rectangle
-- @number height Height of the rectangle
-- @number[opt=5] amount Intensity of the blur. This should be kept between 0 and 10 for performance reasons
-- @number[opt=0.2] passes Quality of the blur. This should be kept as default
-- @number[opt=255] alpha Opacity of the blur
-- @usage hook.Add("HUDPaint", "MyHUDPaint", function()
-- ix.util.DrawBlurAt(0, 0, ScrW(), ScrH())
-- end)
function ix.util.DrawBlurAt(x, y, width, height, amount, passes, alpha)
amount = amount or 5
if (ix.option.Get("cheapBlur", false)) then
surface.SetDrawColor(30, 30, 30, amount * 20)
surface.DrawRect(x, y, width, height)
else
surface.SetMaterial(blur)
surface.SetDrawColor(255, 255, 255, alpha or 255)
local scrW, scrH = ScrW(), ScrH()
local x2, y2 = x / scrW, y / scrH
local w2, h2 = (x + width) / scrW, (y + height) / scrH
for i = -(passes or 0.2), 1, 0.2 do
blur:SetFloat("$blur", i * amount)
blur:Recompute()
render.UpdateScreenEffectTexture()
surface.DrawTexturedRectUV(x, y, width, height, x2, y2, w2, h2)
end
end
end
--- Pushes a 3D2D blur to be rendered in the world. The draw function will be called next frame in the
-- `PostDrawOpaqueRenderables` hook.
-- @realm client
-- @func drawFunc Function to call when it needs to be drawn
function ix.util.PushBlur(drawFunc)
ix.blurRenderQueue[#ix.blurRenderQueue + 1] = drawFunc
end
--- Draws some text with a shadow.
-- @realm client
-- @string text Text to draw
-- @number x X-position of the text
-- @number y Y-position of the text
-- @color color Color of the text to draw
-- @number[opt=TEXT_ALIGN_LEFT] alignX Horizontal alignment of the text, using one of the `TEXT_ALIGN_*` constants
-- @number[opt=TEXT_ALIGN_LEFT] alignY Vertical alignment of the text, using one of the `TEXT_ALIGN_*` constants
-- @string[opt="ixGenericFont"] font Font to use for the text
-- @number[opt=color.a * 0.575] alpha Alpha of the shadow
function ix.util.DrawText(text, x, y, color, alignX, alignY, font, alpha)
color = color or color_white
return draw.TextShadow({
text = text,
font = font or "ixGenericFont",
pos = {x, y},
color = color,
xalign = alignX or TEXT_ALIGN_LEFT,
yalign = alignY or TEXT_ALIGN_LEFT
}, 1, alpha or (color.a * 0.575))
end
--- Wraps text so it does not pass a certain width. This function will try and break lines between words if it can,
-- otherwise it will break a word if it's too long.
-- @realm client
-- @string text Text to wrap
-- @number maxWidth Maximum allowed width in pixels
-- @string[opt="ixChatFont"] font Font to use for the text
function ix.util.WrapText(text, maxWidth, font)
font = font or "ixChatFont"
surface.SetFont(font)
local words = string.Explode("%s", text, true)
local lines = {}
local line = ""
local lineWidth = 0 -- luacheck: ignore 231
-- we don't need to calculate wrapping if we're under the max width
if (surface.GetTextSize(text) <= maxWidth) then
return {text}
end
for i = 1, #words do
local word = words[i]
local wordWidth = surface.GetTextSize(word)
-- this word is very long so we have to split it by character
if (wordWidth > maxWidth) then
local newWidth
for i2 = 1, word:utf8len() do
local character = word[i2]
newWidth = surface.GetTextSize(line .. character)
-- if current line + next character is too wide, we'll shove the next character onto the next line
if (newWidth > maxWidth) then
lines[#lines + 1] = line
line = ""
end
line = line .. character
end
lineWidth = newWidth
continue
end
local space = (i == 1) and "" or " "
local newLine = line .. space .. word
local newWidth = surface.GetTextSize(newLine)
if (newWidth > maxWidth) then
-- adding this word will bring us over the max width
lines[#lines + 1] = line
line = word
lineWidth = wordWidth
else
-- otherwise we tack on the new word and continue
line = newLine
lineWidth = newWidth
end
end
if (line != "") then
lines[#lines + 1] = line
end
return lines
end
local cos, sin, abs, rad1, log, pow = math.cos, math.sin, math.abs, math.rad, math.log, math.pow
-- arc drawing functions
-- by bobbleheadbob
-- https://facepunch.com/showthread.php?t=1558060
function ix.util.DrawArc(cx, cy, radius, thickness, startang, endang, roughness, color)
surface.SetDrawColor(color)
ix.util.DrawPrecachedArc(ix.util.PrecacheArc(cx, cy, radius, thickness, startang, endang, roughness))
end
function ix.util.DrawPrecachedArc(arc) -- Draw a premade arc.
for _, v in ipairs(arc) do
surface.DrawPoly(v)
end
end
function ix.util.PrecacheArc(cx, cy, radius, thickness, startang, endang, roughness)
local quadarc = {}
-- Correct start/end ang
startang = startang or 0
endang = endang or 0
-- Define step
-- roughness = roughness or 1
local diff = abs(startang - endang)
local smoothness = log(diff, 2) / 2
local step = diff / (pow(2, smoothness))
if startang > endang then
step = abs(step) * -1
end
-- Create the inner circle's points.
local inner = {}
local outer = {}
local ct = 1
local r = radius - thickness
for deg = startang, endang, step do
local rad = rad1(deg)
local cosrad, sinrad = cos(rad), sin(rad) --calculate sin, cos
local ox, oy = cx + (cosrad * r), cy + (-sinrad * r) --apply to inner distance
inner[ct] = {
x = ox,
y = oy,
u = (ox - cx) / radius + .5,
v = (oy - cy) / radius + .5
}
local ox2, oy2 = cx + (cosrad * radius), cy + (-sinrad * radius) --apply to outer distance
outer[ct] = {
x = ox2,
y = oy2,
u = (ox2 - cx) / radius + .5,
v = (oy2 - cy) / radius + .5
}
ct = ct + 1
end
-- QUAD the points.
for tri = 1, ct do
local p1, p2, p3, p4
local t = tri + 1
p1 = outer[tri]
p2 = outer[t]
p3 = inner[t]
p4 = inner[tri]
quadarc[tri] = {p1, p2, p3, p4}
end
-- Return a table of triangles to draw.
return quadarc
end
--- Resets all stencil values to known good (i.e defaults)
-- @realm client
function ix.util.ResetStencilValues()
render.SetStencilWriteMask(0xFF)
render.SetStencilTestMask(0xFF)
render.SetStencilReferenceValue(0)
render.SetStencilCompareFunction(STENCIL_ALWAYS)
render.SetStencilPassOperation(STENCIL_KEEP)
render.SetStencilFailOperation(STENCIL_KEEP)
render.SetStencilZFailOperation(STENCIL_KEEP)
render.ClearStencil()
end
-- luacheck: globals derma
-- Alternative to SkinHook that allows you to pass more arguments to skin methods
function derma.SkinFunc(name, panel, a, b, c, d, e, f, g)
local skin = (ispanel(panel) and IsValid(panel)) and panel:GetSkin() or derma.GetDefaultSkin()
if (!skin) then
return
end
local func = skin[name]
if (!func) then
return
end
return func(skin, panel, a, b, c, d, e, f, g)
end
-- Alternative to Color that retrieves from the SKIN.Colours table
function derma.GetColor(name, panel, default)
default = default or ix.config.Get("color")
local skin = panel:GetSkin()
if (!skin) then
return default
end
return skin.Colours[name] or default
end
hook.Add("OnScreenSizeChanged", "ix.OnScreenSizeChanged", function(oldWidth, oldHeight)
hook.Run("ScreenResolutionChanged", oldWidth, oldHeight)
end)
end
-- Vector extension, courtesy of code_gs
do
local VECTOR = FindMetaTable("Vector")
local CrossProduct = VECTOR.Cross
local right = Vector(0, -1, 0)
function VECTOR:Right(vUp)
if (self[1] == 0 and self[2] == 0) then
return right
end
if (vUp == nil) then
vUp = vector_up
end
local vRet = CrossProduct(self, vUp)
vRet:Normalize()
return vRet
end
function VECTOR:Up(vUp)
if (self[1] == 0 and self[2] == 0) then return Vector(-self[3], 0, 0) end
if (vUp == nil) then
vUp = vector_up
end
local vRet = CrossProduct(self, vUp)
vRet = CrossProduct(vRet, self)
vRet:Normalize()
return vRet
end
end
-- luacheck: globals FCAP_IMPULSE_USE FCAP_CONTINUOUS_USE FCAP_ONOFF_USE
-- luacheck: globals FCAP_DIRECTIONAL_USE FCAP_USE_ONGROUND FCAP_USE_IN_RADIUS
FCAP_IMPULSE_USE = 0x00000010
FCAP_CONTINUOUS_USE = 0x00000020
FCAP_ONOFF_USE = 0x00000040
FCAP_DIRECTIONAL_USE = 0x00000080
FCAP_USE_ONGROUND = 0x00000100
FCAP_USE_IN_RADIUS = 0x00000200
function ix.util.IsUseableEntity(entity, requiredCaps)
if (IsValid(entity)) then
local caps = entity:ObjectCaps()
if (bit.band(caps, bit.bor(FCAP_IMPULSE_USE, FCAP_CONTINUOUS_USE, FCAP_ONOFF_USE, FCAP_DIRECTIONAL_USE))) then
if (bit.band(caps, requiredCaps) == requiredCaps) then
return true
end
end
end
end
do
local function IntervalDistance(x, x0, x1)
-- swap so x0 < x1
if (x0 > x1) then
local tmp = x0
x0 = x1
x1 = tmp
end
if (x < x0) then
return x0-x
elseif (x > x1) then
return x - x1
end
return 0
end
local NUM_TANGENTS = 8
local tangents = {0, 1, 0.57735026919, 0.3639702342, 0.267949192431, 0.1763269807, -0.1763269807, -0.267949192431}
local traceMin = Vector(-16, -16, -16)
local traceMax = Vector(16, 16, 16)
function ix.util.FindUseEntity(player, origin, forward)
local tr
local up = forward:Up()
-- Search for objects in a sphere (tests for entities that are not solid, yet still useable)
local searchCenter = origin
-- NOTE: Some debris objects are useable too, so hit those as well
-- A button, etc. can be made out of clip brushes, make sure it's +useable via a traceline, too.
local useableContents = bit.bor(MASK_SOLID, CONTENTS_DEBRIS, CONTENTS_PLAYERCLIP)
-- UNDONE: Might be faster to just fold this range into the sphere query
local pObject
local nearestDist = 1e37
-- try the hit entity if there is one, or the ground entity if there isn't.
local pNearest = NULL
for i = 1, NUM_TANGENTS do
if (i == 0) then
tr = util.TraceLine({
start = searchCenter,
endpos = searchCenter + forward * 1024,
mask = useableContents,
filter = player
})
tr.EndPos = searchCenter + forward * 1024
else
local down = forward - tangents[i] * up
down:Normalize()
tr = util.TraceHull({
start = searchCenter,
endpos = searchCenter + down * 72,
mins = traceMin,
maxs = traceMax,
mask = useableContents,
filter = player
})
tr.EndPos = searchCenter + down * 72
end
pObject = tr.Entity
local bUsable = ix.util.IsUseableEntity(pObject, 0)
while (IsValid(pObject) and !bUsable and pObject:GetMoveParent()) do
pObject = pObject:GetMoveParent()
bUsable = ix.util.IsUseableEntity(pObject, 0)
end
if (bUsable) then
local delta = tr.EndPos - tr.StartPos
local centerZ = origin.z - player:WorldSpaceCenter().z
delta.z = IntervalDistance(tr.EndPos.z, centerZ - player:OBBMins().z, centerZ + player:OBBMaxs().z)
local dist = delta:Length()
if (dist < 80) then
pNearest = pObject
-- if this is directly under the cursor just return it now
if (i == 0) then
return pObject
end
end
end
end
-- check ground entity first
-- if you've got a useable ground entity, then shrink the cone of this search to 45 degrees
-- otherwise, search out in a 90 degree cone (hemisphere)
if (IsValid(player:GetGroundEntity()) and ix.util.IsUseableEntity(player:GetGroundEntity(), FCAP_USE_ONGROUND)) then
pNearest = player:GetGroundEntity()
end
if (IsValid(pNearest)) then
-- estimate nearest object by distance from the view vector
local point = pNearest:NearestPoint(searchCenter)
nearestDist = util.DistanceToLine(searchCenter, forward, point)
end
for _, v in pairs(ents.FindInSphere(searchCenter, 80)) do
if (!ix.util.IsUseableEntity(v, FCAP_USE_IN_RADIUS)) then
continue
end
-- see if it's more roughly in front of the player than previous guess
local point = v:NearestPoint(searchCenter)
local dir = point - searchCenter
dir:Normalize()
local dot = dir:Dot(forward)
-- Need to be looking at the object more or less
if (dot < 0.8) then
continue
end
local dist = util.DistanceToLine(searchCenter, forward, point)
if (dist < nearestDist) then
-- Since this has purely been a radius search to this point, we now
-- make sure the object isn't behind glass or a grate.
local trCheckOccluded = {}
util.TraceLine({
start = searchCenter,
endpos = point,
mask = useableContents,
filter = player,
output = trCheckOccluded
})
if (trCheckOccluded.fraction == 1.0 or trCheckOccluded.Entity == v) then
pNearest = v
nearestDist = dist
end
end
end
return pNearest
end
end
ALWAYS_RAISED = {}
ALWAYS_RAISED["weapon_physgun"] = true
ALWAYS_RAISED["gmod_tool"] = true
ALWAYS_RAISED["ix_poshelper"] = true
function ix.util.FindEmptySpace(entity, filter, spacing, size, height, tolerance)
spacing = spacing or 32
size = size or 3
height = height or 36
tolerance = tolerance or 5
local position = entity:GetPos()
local mins, maxs = Vector(-spacing * 0.5, -spacing * 0.5, 0), Vector(spacing * 0.5, spacing * 0.5, height)
local output = {}
for x = -size, size do
for y = -size, size do
local origin = position + Vector(x * spacing, y * spacing, 0)
local data = {}
data.start = origin + mins + Vector(0, 0, tolerance)
data.endpos = origin + maxs
data.filter = filter or entity
local trace = util.TraceLine(data)
data.start = origin + Vector(-maxs.x, -maxs.y, tolerance)
data.endpos = origin + Vector(mins.x, mins.y, height)
local trace2 = util.TraceLine(data)
if (trace.StartSolid or trace.Hit or trace2.StartSolid or trace2.Hit or !util.IsInWorld(origin)) then
continue
end
output[#output + 1] = origin
end
end
table.sort(output, function(a, b)
return a:DistToSqr(position) < b:DistToSqr(position)
end)
return output
end
-- Time related stuff.
do
--- Gets the current time in the UTC time-zone.
-- @realm shared
-- @treturn number Current time in UTC
function ix.util.GetUTCTime()
local date = os.date("!*t")
local localDate = os.date("*t")
localDate.isdst = false
return os.difftime(os.time(date), os.time(localDate))
end
-- Setup for time strings.
local TIME_UNITS = {}
TIME_UNITS["s"] = 1 -- Seconds
TIME_UNITS["m"] = 60 -- Minutes
TIME_UNITS["h"] = 3600 -- Hours
TIME_UNITS["d"] = TIME_UNITS["h"] * 24 -- Days
TIME_UNITS["w"] = TIME_UNITS["d"] * 7 -- Weeks
TIME_UNITS["mo"] = TIME_UNITS["d"] * 30 -- Months
TIME_UNITS["y"] = TIME_UNITS["d"] * 365 -- Years
--- Gets the amount of seconds from a given formatted string. If no time units are specified, it is assumed minutes.
-- The valid values are as follows:
--
-- - `s` - Seconds
-- - `m` - Minutes
-- - `h` - Hours
-- - `d` - Days
-- - `w` - Weeks
-- - `mo` - Months
-- - `y` - Years
-- @realm shared
-- @string text Text to interpret a length of time from
-- @treturn[1] number Amount of seconds from the length interpreted from the given string
-- @treturn[2] 0 If the given string does not have a valid time