/
functions.nut
1843 lines (1638 loc) · 74 KB
/
functions.nut
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
/*****************************************************************************
** MAKE_CLIP
**
** All functions here are as minimalist and feature-full as possible designed
** to create or modify entities with single lines of code. This is by far the
** most used "make_" function and is used to replace _commentary.txt's job of
** patching exploits with env_player_blockers that don't feel like real clips.
**
** All parameters except angles are required. BlockTypes changed to string forms
** such as "Everyone", with InitialState only 0 if part of I/O. It's important
** to observe that if all angles are "0 0 0" then env_physics_blocker only pays
** attention to "mins" and "maxs"; if they are non-0 like "1 0 0" they'll only
** pay attention to "boxmins" and "boxmaxs". The entity's only limitation is
** that if a clip requires angles and therefore uses boxmins/boxmaxs, then that
** clip unavoidably blocks Physics props like gnomes/gascans -- luckily, rotated
** clips are rarely used or placed high-up or on displacements when they are.
**
** All confusion is easily avoided by always setting all 4 Keyvalues. Even if
** only the specific ones necessary are set, if a "Survivor" blocker has non-0
** angles it absolutely must use boxmins/boxmaxs to have collision with the caveat
** that it'll inevitably block gascans/gnomes, too. It's all about placement.
**
** "SI Players and AI" does not block Commons, but "All and Physics" blocks all
** these (enforced even if all angles are 0):
**
** Survivors + SI Players and AI + Common Corpses + Gascans + Gnome
**
** If "developer 1" one-line entity descriptions are printed to console.
** This is designed to be used with ShowUpdate() to visualize created blockers.
**
** Entity functions almost always assume users give each entity a unique name.
*****************************************************************************/
scripthelp_make_clip <- "Creates env_physics_blocker. user_strBlockType: 'Everyone', 'Survivors', 'SI Players', 'SI Players and AI', 'All and Physics'";
function make_clip ( user_strTargetname,
user_strBlockType,
user_intInitialState,
user_strMins,
user_strMaxs,
user_strOrigin,
user_strAngles = "0 0 0" )
{
local intBlockType = null;
// ShowUpdate() colors mimic "r_drawclipbrushes" and provided for reference.
switch( user_strBlockType )
{
case "Everyone": intBlockType = 0; break; // RED
case "Survivors": intBlockType = 1; break; // PINK
case "SI Players": intBlockType = 2; break; // GREEN
case "SI Players and AI": intBlockType = 3; break; // BLUE
case "All and Physics": intBlockType = 4; break; // LT BLUE
}
local vecMins = StringToVector_Valve( user_strMins, " " );
local vecMaxs = StringToVector_Valve( user_strMaxs, " " );
SpawnEntityFromTable( "env_physics_blocker",
{
targetname = g_UpdateName + user_strTargetname,
BlockType = intBlockType,
initialstate = user_intInitialState,
mins = vecMins,
boxmins = vecMins,
maxs = vecMaxs,
boxmaxs = vecMaxs,
origin = StringToVector_Valve( user_strOrigin, " " ),
angles = StringToVector_Valve( user_strAngles, " " )
} );
if ( developer() > 0 )
{
printl( "env_physics_blocker \"" + user_strTargetname + "\" blocks " + user_strBlockType
+ " w/ initialstate " + user_intInitialState + " @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_BRUSH
**
** Brushes block everything that env_physics_blockers don't. This includes:
**
** Alive Commons + Pipebomb + Molotov + SI vomit/tongue + bullet hitreg
**
** Used extensively by "_losfix"-prefixed entities to provide SI Players with
** an invisible obstruction that blocks LOS -- done cautiously given that it
** still has collision which sometimes needs to be encased within new clips!
**
** The next useful purpose is blocking Alive Commons to fake blocks they can
** actually climb or mitigate prop navigational failures that force crouching.
** Brushes also block Survivor corpses and are thus used for defib-trick fixes,
** whereas env_physics_blockers only block Common corpses.
**
** ShowUpdate() draws in LT GREEN and rotation is unsupported by engine.
*****************************************************************************/
scripthelp_make_brush <- "Creates func_brush. Can block Alive Commons + Pipebomb + Molotov + SI vomit/tongue + bullet hitreg";
function make_brush ( user_strTargetname,
user_strMins,
user_strMaxs,
user_strOrigin )
{
local brush = SpawnEntityFromTable( "func_brush",
{
targetname = g_UpdateName + user_strTargetname,
origin = StringToVector_Valve( user_strOrigin, " " )
} );
DoEntFire( "!self", "AddOutput", "mins " + user_strMins, 0, null, brush );
DoEntFire( "!self", "AddOutput", "maxs " + user_strMaxs, 0, null, brush );
DoEntFire( "!self", "AddOutput", "solid 2", 0, null, brush );
if ( developer() > 0 )
{
printl( "func_brush \"" + user_strTargetname + "\" created @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_NAVBLOCK
**
** Quickly create func_nav_blockers to fix nav bugs or add LOS blockers.
**
** Accepts "Everyone", "Survivors", or "Infected" where "Apply" immediately
** adds BLOCKED_SURVIVOR and/or BLOCKED_ZOMBIE to the nav area or "Remove"
** used to spawn the navblocker initially disabled. Manual firings using
** either "BlockNav" or "UnblockNav" can override these in I/O at any time.
**
** ShowUpdate() draws in ORANGE and rotation is unsupported by engine. It's
** critical to note that its mins/maxs must be impeccable -- while clips support
** a mins/maxs like "-50 25 -50" "50 50 50", func_nav_blocker does not, and
** if used the blocked attributes remove unreliably on UnblockNav or Kill and
** if rotation is used it'll apply attributes as if it wasn't rotated.
**
** The engine automatically clears blocked attributes on round transition.
*****************************************************************************/
scripthelp_make_navblock <- @"Creates func_nav_blocker.
user_strTeamBlock: 'Everyone', 'Survivors', 'Infected'
user_strState: 'Apply', 'Remove'";
function make_navblock ( user_strTargetname,
user_strTeamBlock,
user_strState,
user_strMins,
user_strMaxs,
user_strOrigin )
{
local intTeamBlock = null;
switch( user_strTeamBlock )
{
case "Everyone": intTeamBlock = -1; break;
case "Survivors": intTeamBlock = 2; break;
case "Infected": intTeamBlock = 3; break;
}
local blocker = SpawnEntityFromTable( "func_nav_blocker",
{
targetname = g_UpdateName + user_strTargetname,
teamToBlock = intTeamBlock,
origin = StringToVector_Valve( user_strOrigin, " " )
} );
DoEntFire( "!self", "AddOutput", "mins " + user_strMins, 0, null, blocker );
DoEntFire( "!self", "AddOutput", "maxs " + user_strMaxs, 0, null, blocker );
DoEntFire( "!self", "AddOutput", "solid 2", 0, null, blocker );
switch( user_strState )
{
case "Apply": DoEntFire( "!self", "BlockNav", "", 0, null, blocker ); break;
case "Remove": DoEntFire( "!self", "UnblockNav", "", 0, null, blocker ); break;
}
if ( developer() > 0 )
{
printl( "func_nav_blocker \"" + user_strTargetname + "\" blocks " + user_strTeamBlock
+ " w/ " + user_strState + " @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_TRIGPUSH
**
** Pusher volume for "Everything", "Survivor", "Infected" or "Physics".
**
** When it comes to trigger_multiple and trigger_once their Keyvalues are so
** specific it's more efficient to not have functions -- except push & hurt.
**
** While versatile, undeletable SI Player wrongway signs aren't applicable here
** because trigger_push cannot be used as a one-way barrier -- make_axiswarp()
** was added as a way to create a one-way barrier. Even though max velocity of
** 1000 (where even 1001 spams "DataTable warning: player: Out-of-range value")
** technically works, it was deemed far too exploitable. Use cases are reduced
** to Quality-of-Life Infected ladder improvements and niche Survivor fixes.
**
** "Everything" (64) even if it were instead "spawnflags 2" for NPC cannot ever
** push Commons (Alive/Corpses) -- triggers were updated to detect weapons like
** Gascans but not SI vomit/tongue (tongue keeps full control if pulled through).
** Pushing is only supported on Clients (Players/AI), Physics Objects (trashbin),
** Gnome, Pipebomb, SI acid balls, and Tank rocks. Default is always Clients (1)
** only and if "Physics" (8) will exclude Clients. Filter Override requires the
** full name and can be anything (i.e. an optimized filter_multi).
**
** Push Direction format is "x y z" where x 90 is down -90 is up and z doesn't
** effect any change. Only y is ever used.
**
** ShowUpdate() draws all triggers YELLOW and trigger_push CANNOT be rotated;
** however, Angles are optional because they still have unknown influence. In
** the case of Death Toll 5's Rockslide, if Angles are all-0 instead of "0 35 0"
** it will fail, and integrating the difference into Push Direction won't help.
** ShowUpdate() will draw Angles but activators inside are only pushed by the
** invisible all-0 volume. Technically Angles aren't supported but do have a
** mild enough influence to keep as an option -- but all-0 is recommended.
*****************************************************************************/
scripthelp_make_trigpush <- @"Creates trigger_push. user_strActivator: 'Everything', 'Survivor', 'Infected', 'Physics'"
function make_trigpush ( user_strTargetname,
user_strActivator,
user_intSpeed,
user_strDirection,
user_strMins,
user_strMaxs,
user_strOrigin,
user_strAngles = "0 0 0",
user_strFilterOverride = "" )
{
// Determine activator filter. Type always Clients (1) unless explicitly not.
local strFiltername = "";
local intSpawnflags = 1;
switch( user_strActivator )
{
case "Everything": intSpawnflags = 64; break;
case "Survivor": strFiltername = "anv_globalfixes_filter_survivor"; break;
case "Infected": strFiltername = "anv_globalfixes_filter_infected"; break;
case "Physics": intSpawnflags = 8; break;
}
// Allow custom filters to be used independent of activator type.
if ( user_strFilterOverride != "" )
{
strFiltername = user_strFilterOverride;
}
// Spawn the pusher.
local trigger = SpawnEntityFromTable( "trigger_push",
{
targetname = g_UpdateName + user_strTargetname,
filtername = strFiltername,
spawnflags = intSpawnflags,
speed = user_intSpeed,
pushdir = StringToVector_Valve( user_strDirection, " " ),
origin = StringToVector_Valve( user_strOrigin, " " ),
angles = StringToVector_Valve( user_strAngles, " " )
} );
DoEntFire( "!self", "AddOutput", "mins " + user_strMins, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "maxs " + user_strMaxs, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "solid 2", 0, null, trigger );
if ( developer() > 0 )
{
printl( "trigger_push \"" + user_strTargetname + "\" pushes " + user_strActivator
+ " w/ speed " + user_intSpeed + " @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_TRIGHURT
**
** Insta-kill volume for "Everyone", "Survivor", "Infected" or "Ghost".
**
** Kill volumes have broad exploit patching applications but are also used
** for "_pullcharge"-prefixed Smoker/Charger combos that get Survivors out of
** bounds killing them instead of allowing further map escape.
**
** Spawnflags 1 (Client) is all that's needed because 2 (NPC) kills Commons
** which will be problematic for some uses -- supplement with AddOutput for
** rare cases to change "spawnflags" (instant effect change).
**
** Damage type for trigger_hurt is usually CRUSH (1) -- trigger_hurt_ghost
** has a couple things to note, though:
**
** + "Everyone" kills Survivor/Infected but never Ghost
** + trigger_hurt with "Infected" kills spawned SI but not ghosted
** + _ghost only kills spawned SI if CRUSH (1)
** + _ghost requires FALL (32) to kill ghost SI <-- BUG
** + _ghost will always kill both with FALL (32)
** + _ghost does not support killing just ghost SI
**
** To avoid exaggerated Infected ragdoll deaths both use "nodmgforce 1".
**
** Damages always set to insta-kill. Vast majority of uses will have hurts
** always enabled and sometimes Killed, so "StartDisabled 0" is assumed.
**
** ShowUpdate() draws all triggers YELLOW and trigger_hurt CANNOT be rotated.
*****************************************************************************/
scripthelp_make_trighurt <- @"Creates trigger_hurt or trigger_hurt_ghost if Ghost activator specified.
user_strActivator: 'Everyone', 'Survivor', 'Infected', 'Ghost'";
function make_trighurt ( user_strTargetname,
user_strActivator,
user_strMins,
user_strMaxs,
user_strOrigin )
{
// Both trigger_hurt and trigger_hurt_ghost have these in common.
local tblKeyvalues =
{
targetname = g_UpdateName + user_strTargetname,
spawnflags = 1,
nodmgforce = 1,
damage = 10000,
damagecap = 10000,
origin = StringToVector_Valve( user_strOrigin, " " )
};
// Now reconcile their differences.
local strClassname = null;
switch( user_strActivator )
{
case "Everyone":
strClassname = "trigger_hurt";
tblKeyvalues.damagetype <- 1;
break;
case "Survivor":
strClassname = "trigger_hurt";
tblKeyvalues.filtername <- "anv_globalfixes_filter_survivor";
tblKeyvalues.damagetype <- 1;
break;
case "Infected":
strClassname = "trigger_hurt";
tblKeyvalues.filtername <- "anv_globalfixes_filter_infected";
tblKeyvalues.damagetype <- 1;
break;
case "Ghost":
strClassname = "trigger_hurt_ghost";
tblKeyvalues.filtername <- "anv_globalfixes_filter_infected";
tblKeyvalues.damagetype <- 32;
break;
}
// We're done defining so spawn it.
local trigger = SpawnEntityFromTable( strClassname, tblKeyvalues );
// Finally apply mins/maxs.
DoEntFire( "!self", "AddOutput", "mins " + user_strMins, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "maxs " + user_strMaxs, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "solid 2", 0, null, trigger );
if ( developer() > 0 )
{
printl( strClassname + " \"" + user_strTargetname + "\" insta-kills " + user_strActivator
+ " @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_TRIGDUCK
**
** Creates a "trigger_auto_crouch" volume. Note similar make_trigmove() "Duck"
** but different in that these require proximity to a small passage to work.
**
** Commons (NPC) duck fine but sometimes SI bots (Clients) do not, therefore
** these correct SI bot behavior. It's also less intrusive than make_trigmove()
** because the bot or player must be actively pressing into the passage for
** the auto-crouch to happen where letting go without entry will un-crouch.
**
** ShowUpdate() draws all triggers YELLOW with rotation irrelevant by nature.
*****************************************************************************/
scripthelp_make_trigduck <- "Creates trigger_auto_crouch.";
function make_trigduck ( user_strTargetname,
user_strMins,
user_strMaxs,
user_strOrigin )
{
local trigger = SpawnEntityFromTable( "trigger_auto_crouch",
{
targetname = g_UpdateName + user_strTargetname,
spawnflags = 1,
origin = StringToVector_Valve( user_strOrigin, " " )
} );
DoEntFire( "!self", "AddOutput", "mins " + user_strMins, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "maxs " + user_strMaxs, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "solid 2", 0, null, trigger );
if ( developer() > 0 )
{
printl( "trigger_auto_crouch \"" + user_strTargetname + "\" created @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_TRIGMOVE
**
** Creates a "trigger_playermovement" entity with Options:
**
** "Duck" Force Client to always Duck
** "Walk" Force Client to always Walk
** "Jump" Prevent Client from Jumping
**
** Contains extra features over make_trigduck(). Mainly is used to force-dodge
** prop_static obstructions with new Infected ladders or as force-feedback for
** new 360-degree Beam ladders.
**
** Uses are few and far between for team filters to be safely ignored. Caution
** that deleting the trigger after a movement change has been enforced will
** not un-apply the effect -- never fire Kill to these!
**
** ShowUpdate() draws all triggers YELLOW and these CANNOT be rotated.
*****************************************************************************/
scripthelp_make_trigmove <- "Creates trigger_playermovement. user_strOption: 'Duck', 'Walk', 'Jump'";
function make_trigmove ( user_strTargetname,
user_strOption,
user_strMins,
user_strMaxs,
user_strOrigin )
{
// Always Clients (1) with Option added.
local intSpawnflags = 0;
switch( user_strOption )
{
case "Duck": intSpawnflags = 2048; break;
case "Walk": intSpawnflags = 4096; break;
case "Jump": intSpawnflags = 8192; break;
}
local trigger = SpawnEntityFromTable( "trigger_playermovement",
{
targetname = g_UpdateName + user_strTargetname,
spawnflags = 1 + intSpawnflags,
origin = StringToVector_Valve( user_strOrigin, " " )
} );
DoEntFire( "!self", "AddOutput", "mins " + user_strMins, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "maxs " + user_strMaxs, 0, null, trigger );
DoEntFire( "!self", "AddOutput", "solid 2", 0, null, trigger );
if ( developer() > 0 )
{
printl( "trigger_playermovement \"" + user_strTargetname + "\" with Option " + user_strOption
+ " @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_PROP
**
** This function combines the most useful features from four entities:
**
** "dynamic" https://developer.valvesoftware.com/wiki/Prop_dynamic
** "dynamic_ovr" https://developer.valvesoftware.com/wiki/Prop_dynamic_override
** "physics" https://developer.valvesoftware.com/wiki/Prop_physics
** "physics_ovr" https://developer.valvesoftware.com/wiki/Prop_physics_override
**
** Parameters (required unless specified):
**
** user_strType One of the four above (i.e. "dynamic")
** user_strTargetname Entity name
** user_strModel Full Hammer-style file path to model
** user_strOrigin Location
** user_strAngles OPTIONAL: Rotation of the prop
** user_strShadows OPTIONAL: Either "shadow_yes" or "shadow_no"
** user_strSolidity OPTIONAL: Either "solid_yes" or "solid_no"
** user_strRenderColor OPTIONAL: Where "0 0 0" is black
** user_flFadeMinDist OPTIONAL: Best left at default -1.0
** user_flFadeMaxDist OPTIONAL: Best left at default 0.0
** user_flMassScale OPTIONAL: Weight of prop_physics (1.0 default)
**
** Fade min/max are only intended for Infected "wrongway" signs. Mass scale
** is used to override prop_physics models that are too heavy for Tanks to hit.
** Excluded are "StartDisabled", skins, size scaling, parenting, animations,
** and "lightingorigin": these can be supplemented by EntFire() as required
** but parenting will be the only one ever used by any Valve fix.
**
** Dynamic and physics props are identical except four key differences:
**
** 1. Dynamic are solid when "solid 6" and non-solid when "solid 0".
** Physics are solid when "spawnflags 0" and non-solid when "spawnflags 4".
** Much of the code is resolving this distinction, where "spawnflags 4"
** is instead turning physics props into "debris".
**
** 2. Non-solid dynamic cannot be shot or hit at all whereas non-solid
** physics ("debris") can still be shot or hit by Tanks (but won't do
** any damage to victims in its trajectory).
**
** 3. Ghost Infected can freely move through physics props but dynamic
** props will stop them. For example, Ghosts are used to being able
** to run through cars, but it's the dynamic glass model that stops them.
**
** 4. Physics props are all that can use "user_flMassScale". The code
** ensures this parameter is ignored if it's a dynamic.
**
** Otherwise shadows, coloring, and fade min/max are the same for all four.
**
** Internal massScale default is 0.0 but 1.0 behaves identical and looks better.
**
** Also note "prop_physics_override" generates "prop_physics", not _overrides.
**
** CAUTION:
**
** Function forces props prefixed with "_solidify" to not render. Solidifying
** non-solid 1:1 prop_statics is critical for improving Versus LOS, with those
** entities spawned by InfectedHumEnts_Spawn() -- which technically could do
** it with an EntFire() but "reported ENTITY_CHANGE_NONE" spams for each. All
** are exact copies and only cause overlap artifacts if rendered twice.
*****************************************************************************/
scripthelp_make_prop <- @"Creates prop_dynamic, prop_physics, prop_dynamic_override or prop_physics_override based on passed user_strType.
user_strType: 'dynamic', 'physics', 'dynamic_ovr', 'physics_ovr'
user_strTargetname: if targetname starts with '_solidify' spawned prop will be invisible
user_strShadows: 'shadow_no', 'shadow_yes'
user_strSolidity: 'solid_no', 'solid_yes'";
function make_prop ( user_strType,
user_strTargetname,
user_strModel,
user_strOrigin,
user_strAngles = "0 0 0",
user_strShadows = "shadow_yes",
user_strSolidity = "solid_yes",
user_strRenderColor = "255 255 255",
user_flFadeMinDist = -1.0,
user_flFadeMaxDist = 0.0,
user_flMassScale = 1.0 )
{
local tblKeyvalues =
{
targetname = g_UpdateName + user_strTargetname,
model = user_strModel,
origin = StringToVector_Valve( user_strOrigin, " " ),
angles = StringToVector_Valve( user_strAngles, " " ),
rendercolor = StringToVector_Valve( user_strRenderColor, " " ),
fademindist = user_flFadeMinDist,
fademaxdist = user_flFadeMaxDist
};
switch( user_strShadows )
{
case "shadow_yes": tblKeyvalues.disableshadows <- 0; break;
case "shadow_no": tblKeyvalues.disableshadows <- 1; break;
}
// Resolve the most significant distinction between classes.
switch( user_strSolidity )
{
case "solid_yes":
if ( user_strType == "dynamic" || user_strType == "dynamic_ovr" )
{
tblKeyvalues.solid <- 6;
}
if ( user_strType == "physics" || user_strType == "physics_ovr" )
{
tblKeyvalues.spawnflags <- 0;
}
break;
case "solid_no":
if ( user_strType == "dynamic" || user_strType == "dynamic_ovr" )
{
tblKeyvalues.solid <- 0;
}
if ( user_strType == "physics" || user_strType == "physics_ovr" )
{
tblKeyvalues.spawnflags <- 4;
}
break;
}
// Mass is only relevant to physics props.
if ( user_strType == "physics" || user_strType == "physics_ovr" )
{
tblKeyvalues.massScale <- user_flMassScale;
}
// If "user_strTargetname" begins with "_solidify" then change
// the default from NORMAL = 0 to NONE = 10. Note that Disable
// still blocks LOS but results in bumpy collision and doesn't
// block bullets -- the below avoids both problems. Note "find"
// is case sensitive so always use lowercase.
if ( user_strTargetname.find( "_solidify" ) == 0 )
{
tblKeyvalues.rendermode <- 10;
}
local strClassname = null;
switch( user_strType )
{
case "dynamic": strClassname = "prop_dynamic"; break;
case "dynamic_ovr": strClassname = "prop_dynamic_override"; break;
case "physics": strClassname = "prop_physics"; break;
case "physics_ovr": strClassname = "prop_physics_override"; break;
}
// We're done defining so spawn it.
SpawnEntityFromTable( strClassname, tblKeyvalues );
// Dump almost everything but require rarer stuff to be relevant.
if ( developer() > 0 )
{
print( strClassname + " \"" + user_strTargetname + "\" "
+ user_strShadows + " & " + user_strSolidity
+ " w/ \"" + user_strModel + "\"" );
// Color change could just be pass-through to the others,
// so just dump everything else that's cool. Fade Min/Max
// are excluded since they'll have "wrongway" in the name.
if ( user_strRenderColor != "255 255 255" )
{
print( " & color \"" + user_strRenderColor + "\"" );
}
if ( user_flMassScale != 1.0 )
{
print( " & mass " + user_flMassScale );
}
printl( " @ setpos_exact " + user_strOrigin + "\n" );
}
}
/*****************************************************************************
** MAKE_DECAL
**
** Paints an infodecal onto the nearest surface.
**
** Nameless infodecals auto-Activate and are necessary to have them apply to
** the world early enough to persist player's rejoins to the server. Note it
** is impossible to remove early-spawned decals from the map like No Mercy 5's
** wall outlets -- the only exception is if they're applied to a breakable
** Infected wall in which case the 2nd Versus team suffers non-persistence.
** If not applied long before "round_start" they're lost on player rejoin.
**
** See file "mapspawn.nut" for more details and all instant application calls.
*****************************************************************************/
scripthelp_make_decal <- "Creates infodecal that immediately applies strTexture decal. (see functions.nut for more information)";
function make_decal( strTexture, strOrigin )
{
SpawnEntityFromTable( "infodecal",
{
texture = strTexture,
origin = StringToVector_Valve( strOrigin, " " )
} );
if ( developer() > 0 )
{
printl( "infodecal \"" + strTexture + "\" painted @ setpos_exact " + strOrigin + "\n" );
}
}
/*****************************************************************************
** ENTITY HANDLING
**
** These tiny functions are as powerful as they are potentially dangerous, SO
** a ton of hours (and minidumps) later these should be crash/error-proof.
**
** Function SafelyExists() accepts a Handle to an entity and returns "true"
** if it safely exists. First and foremost the Handle must not be "null" to
** avoid abrupt script terminations from entities we're modifying not existing,
** THEN since it exists that much ensure it IsValid() to avoid console errors.
**
** Function kill_entity() accepts a Handle then deletes that entity, which
** could be Dark Carnival 5's Versus defibrillators or func_breakables deleted
** to make room for Dead Center 1/3 ladders. Kill() needs entity to exist.
** Note that EntFire() has a minor delay so deleting entities already in the
** map to re-create them could backfire since it'll delete the replacement,
** but Kill() deletes it immediately.
**
** Function clone_model() use cases range from cloning Dark Carnival 5's brush
** trigger that deletes players to replace it to adding new Infected ladders.
** It relies heavily on SafelyExists() because if it returns a brush that isn't
** the one requested the game will either spawn in the wrong brush which could
** be anything or Crash-to-Desktop because the model isn't precached. Before
** Kerry's C++ update to add GetModelName() & IsModelPrecached(), a more risky
** method found the "m_nModelIndex" NetProp then did -1 to yield the String,
** in addition to ensuring it was > 0 so it's not null or worldspawn. Conversion
** arithmetic has been obsoleted with GetModelName(). By design this can clone
** anything and safety ultimately relies on the user feeding in a sane Handle
** from an entity that already exists i.e. with FindByClassnameNearest(). With
** these protections and proper use it's 100% safe.
**
** Functions prefixed with "modify_" make necessary use of SafelyExists().
*****************************************************************************/
scripthelp_SafelyExists <- "Returns true if hndEntity is not null and is a valid entity";
function SafelyExists( hndEntity )
{
if ( hndEntity != null && hndEntity.IsValid() )
{
return true;
}
else
{
return false;
}
}
////////////////////////////////////////////////////////////////////////////////////
// Dumps the entity Handle itself (which includes Classname and can be anything).
// This isn't used in other functions because (1) its developer output would "echo"
// and (2) script flow concludes entities exist by other means.
////////////////////////////////////////////////////////////////////////////////////
scripthelp_kill_entity <- "Kills hndEntity if it safely exists.";
function kill_entity( hndEntity )
{
if ( SafelyExists( hndEntity ) )
{
hndEntity.Kill();
if ( developer() > 0 )
{
printl( "Deleted " + hndEntity + " @ setpos_exact "
+ VectorToString_Valve( hndEntity.GetOrigin() ) + "\n" );
}
}
}
////////////////////////////////////////////////////////////////////////////////////
// Returns the "*#"-styled model of any entity. It will never be the wrong model as
// long as all make_ladder() calls are thoroughly tested. The only potential danger
// is Engine Error crashes for "UTIL_SetModel: not precached: *-1" which will not
// happen because of the safety redundancy.
//
// This allows BSP's to be recompiled and their model indices to change while never
// breaking cloned brushes (like Infected ladders) because they're cloned by exact
// targetname or coordinates. In contrast, recompiles would break lump implementation.
//
// NO DEVELOPER OUTPUT: Functions like make_ladder() which call this output their
// own formats with the String returned from here -- some may use con_comment() to
// explain what's going on instead, i.e. Dark Carnival 5 player-deleting trigger fix.
////////////////////////////////////////////////////////////////////////////////////
scripthelp_clone_model <- "If hndEntity safely exists get its model name and return the model name if it's precached, otherwise return null";
function clone_model( hndEntity )
{
if ( SafelyExists( hndEntity ) )
{
local strModel = hndEntity.GetModelName();
// Maybe impossible to be false but stay safe!
if ( IsModelPrecached( strModel ) )
{
// Return "*#"-styled Keyvalue String.
return strModel;
}
else
{
return null;
}
}
}
/*****************************************************************************
** INFECTED LADDERS
**
** Workflow suite of ladder fixing functions. Every ladder needs a place to
** exist, then needs to be found for cloning, then if already exists but is
** just broken just fix it instead, or make a new one, then spawn them in.
**
** kill_funcinfclip()
**
** Useful for fixing stuck spawns on Taaannnkk! Mutation most notably
** No Mercy 3, but also necessary on maps like Blood Harvest 5 to add
** new ladders like the barricade's -- meticulously re-creating every
** deleted clip to prevent the introduction of exploits. Several of them
** block off existing Infected ladders that are impossible to use.
**
** The best way to uniquely identify these is "m_Collision.m_flRadius"
** with values like "4564.93" -- just "m_flRadius" could be used but it
** has "1.73922e-035" notation. As it's a brush its "m_vecOrigin" is all
** 0's often enough to disregard that for identification. Other options
** are "m_Collision.m_vecMins"/Maxs but the radius is already LengthSqr()
** of both so they're already represented.
**
** Floats must be converted to String due to rare compare imprecision.
** Before any are deleted their radius is verified unique.
**
** find_ladder()
**
** NetProp "m_vecSpecifiedSurroundingMaxs" is magic for several reasons:
**
** 1. If it's used correctly with "m_Collision." it will be all
** 0's -- if it's used wrong it returns the func_simpleladder's
** exact world origin, presumably the same used by ent_text.
**
** 2. While m_vecMins/Maxs would also work for unique identification
** of ladders, the origin is much easier to understand -- but
** the Mins/Maxs are useful for ShowUpdate() to draw them.
**
** 3. VSSM (for short) is all 0's during and for a bit after map
** load -- it takes ~8 seconds to exist, varying drastically
** based on players and server performance.
**
** VSSM is LengthSqr() compared to a manual user-discovered "origin",
** looping through all func_simpleladders in the map until one within very
** close proximity is found, then returns that ladder as a Handle.
**
** This is the first step to patching or cloning ladders.
**
** patch_ladder()
**
** Accepts an "origin" to a known-existing ladder which find_ladder()
** discovers then accepts another parameter to change its location.
** This is used to correct Dark Carnival 4's electrical box ladder.
**
** IMPORTANT: The "origin" changed to is relative from the
** ladder's original location -- it has its own "center" that
** is independent from worldspawn's "0 0 0".
**
** An optional third parameter accepts new Normals to correct existing
** ladders that are unclimbable due to wrong faces. This is used to
** correct Dark Carnival 1's parking lot fence ladder.
**
** Facing towards the ladder's "TOOLS/CLIMB_VERSUS" texture this is how
** the Normals work -- there is a rough tolerance of 0.2 on the "raycast"
** that determines the Normal, so 0.8 and 1.0 may yield identical results:
**
** 0 1 0 exactly North
** 0 -1 0 exactly South
** 1 0 0 exactly West
** -1 0 0 exactly East
**
** Now that we can fix existing ladders, let's start making our own !!
**
** make_ladder()
**
** Best practices for cloning Infected ladders:
**
** + Don't clone ladders within extreme close radius of each other
** + Only rotate a cloned ladder when there's no alternatives
** + Even if you rotate and it's still climbable still update Normals
** + Use "nav_edit 1; nav_show_compass 1" to visualize Normals
** + Average the 0.2 tolerance, so if both 0.5 and 0.7 work use 0.6
** + If the VSSM parameter must be rounded never round more than 0.1
** + The radius exists as BSP recompile and float imprecision fail-safe
** + Avoid very tall ladders (No Mercy 4 shaft / Blood Harvest 5 start)
**
** Normal parameter is optional. If a ladder isn't rotated the original
** climbable face is ported over. Override Normals when there's rotation.
**
** InfectedLadders_Spawn()
**
** Each map with new ladders declares InfectedLadders() and then calls
** this function, both under "ALL MODES". A logic_timer "retry Think"
** gives a little extra time after the first VSSM becomes non-0 to then
** spawn in the ladders -- a necessary delay for find_ladder() to work.
** Failure to do this will mean ladders don't spawn for the first round.
** Second round this happens again but completes almost immediately because
** once VSSM's are populated it's forever.
**
** InfectedLadders() is declared in "ALL MODES" which this function relies
** on to have been created. New ladders can be connected to NAV for bots.
**
** ShowUpdate draws new/cloned Infected ladders in WHITE. Supplemental use of
** make_clip() creates the "guides" on both sides of the ladder so Survivors
** don't bump into them and make_prop() creates the visual pipes. Cloned ladders
** do not come with new NAV connections, so SI bots and Commons can't use them.
*****************************************************************************/
//////////////////////////////////////////////////////////////////////////////
// kill_funcinfclip:
//
// Accepts the Radius of the func_playerinfected_clip to delete.
//
// Radius is equivalent to a "checksum" that verifies it as unique. Using map
// Blood Harvest 5 as example, its five clips already integrate vecMins/Maxs:
//
// flRadius vecMins vecMaxs (mins-maxs).Length() / 2
//
// 4564.93 -3225, -2373, -393 2665, 4397, 1289 4564.92
// 3833.37 8191, -161, -97 13393, 5121, 1857.1 3833.37
// 2022.66 12415, -993, -2049 13057, -159, 1857 2022.66
// 572.373 -203, -529, -81 203, 529, 81 572.375
// 3279.91 9471, -4545, -2049 13057, -683, 1857 3279.91
//
// Thus if "m_Collision.m_flRadius" is manually confirmed unique from all other
// clips, it has 100% identification integrity.
//
// In Blood Harvest 5's case, clip "4564.93" (origin "7320 1220 568.0625") and
// clip "3833.37" (origin "0 0 0") are deleted for new barricade and one-way
// drop ladders -- respectively with the far edge tree matte and out of bounds
// areas meticulously re-clipped to prevent adding new exploits. Its other clips
// have two under the bridge and another obstructing nothing all kept & harmless.
//
// Again, can't use FindByClassnameNearest() because "origin" is not unique.
//////////////////////////////////////////////////////////////////////////////
scripthelp_kill_funcinfclip <- "Kill all func_playerinfected_clip where their 'm_Collision.m_flRadius' is equal to flDesiredRadius";
function kill_funcinfclip( flDesiredRadius )
{
local hndFuncInfClip = null;
local flCompareRadius = null;
while( ( hndFuncInfClip = Entities.FindByClassname( hndFuncInfClip, "func_playerinfected_clip" ) ) != null )
{
flCompareRadius = NetProps.GetPropFloat( hndFuncInfClip, "m_Collision.m_flRadius" );
// Convert tostring for identical precision. A < > tolerance of 0.001
// was experimented with but failed for some clips. There's no universe
// where we care about Radius in terms of it being a number; it's purely
// a checksum hash and can be thought of as an alternative targetname.
if ( flDesiredRadius.tostring() == flCompareRadius.tostring() )
{
// Don't use kill_entity() to avoid double console output.
// Mandatory safety for Community Servers given that they'll
// have deleted ALL of these and this code would've failed.
if ( SafelyExists( hndFuncInfClip ) )
{
hndFuncInfClip.Kill();
if ( developer() > 0 )
{
// With just GetOrigin() it's most likely all 0's
// for most maps. When it's all 0's, print vecMins
// instead as a very good approximation.
local vecLocation = hndFuncInfClip.GetOrigin();
if ( vecLocation.x == 0 && vecLocation.y == 0 && vecLocation.z == 0 )
{
vecLocation = NetProps.GetPropVector( hndFuncInfClip, "m_Collision.m_vecMins" );
}
printl( "Deleted func_playerinfected_clip with Radius checksum "
+ flDesiredRadius + " @ approximate setpos_exact "
+ VectorToString_Valve( vecLocation ) + "\n" );
}
}
break;
}
}
}
//////////////////////////////////////////////////////////////////////////////
// find_ladder:
//
// Accepts strDesiredVSSM where "Desired" always refers to the location of the
// ladder sought to be modified or cloned -- there's no guarantee that it will
// produce a result, hence all the clone_model() safeguards.
//
// The VSSM must be found manually via developer dump functions.
//
// Euclidean Calculus:
//
// LengthSqr() returns "squared length" of vector "x^2 + y^2 + z^2" and is
// less expensive than Length() because it skips square root calculation.
// We only need to know the (any order) difference and not exact length.
//
// Floats can be imprecise by 0.000006. While locational acquisition of
// the model is immune to recompile, if the ladder is physically moved
// then it will not be found.
//
// Radius of 1.4 is used so the operand is roughly 2 (1.96), which should
// always work -- if it doesn't it's easy to update -- and allows ladders
// extremely close to other ladders to be fixed, of which Dark Carnival 1's
// parking lot fence ladder is close but has 3x+ this wiggle room.
//
// Radius of 1.4 supports a shift of up to 0.47 on all 3 axis -- never round
// VSSM's more than 0.1 (mapfixes.nut rounds to nearest 0.01 or less).
//
// Function patch_ladder() tweaks ladders found with this and make_ladder()
// passes the found ladder to clone_model() to get its "model" Keyvalue so it
// can be cloned. If there's no match to "return hndLadder", then it wasn't