-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathScriptingCommands.bb
More file actions
3404 lines (3151 loc) · 112 KB
/
ScriptingCommands.bb
File metadata and controls
3404 lines (3151 loc) · 112 KB
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
; Realm Crafter BVM Scripting command module by William "Mr.Bill" Steelhammer
; Most commands ported from Rob William' Scrpting Mosule
Function BVM_ACTOR%()
SI.ScriptInstance = Object.ScriptInstance(hSI%)
If SI <> Null
Result% = SI\AI
EndIf
Return Result%
End Function
Function BVM_CONTEXTACTOR%()
SI.ScriptInstance = Object.ScriptInstance(hSI%)
If SI <> Null
Result% = SI\AIContext
EndIf
Return Result%
End Function
Function BVM_PERSISTENT(Param1%)
SI.ScriptInstance = Object.ScriptInstance(hSI%)
If SI <> Null
SI\Persistent = Param1
EndIf
End Function
Function BVM_FINDACTOR%(Param1$, ActorType% = 3)
Param1$ = Upper$(Param1$)
If ActorType < 1 Or ActorType > 3 Then ActorType = 3
If Len(Param1$) > 0
For Actor.ActorInstance = Each ActorInstance
If Upper$(Actor\Name$) = Param1$
If (ActorType = 1 And Actor\RNID > -1) Or (ActorType = 2 And Actor\RNID = -1) Or ActorType = 3
Result% = Handle(Actor)
Exit
EndIf
EndIf
Next
EndIf
Return Result%
End Function
Function BVM_GETARMOURLEVEL%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Result% = GetArmourLevel(Actor\Inventory)
EndIf
Return Result%
End Function
Function BVM_THREADEXECUTE(Name$, Func$, AI%=0, AIContext%=0, Param$ = "")
; Propagate the caller's Privileged flag to the spawned script.
; Previously hard-coded to 0, which (a) silently neutered GM
; scripts that used ThreadExecute to call privileged helpers
; and (b) left an obvious refactor trap where someone would
; eventually add a `Privileged` arg and get the propagation wrong.
Local CurrentSI.ScriptInstance = Object.ScriptInstance(hSI)
Local CallerPriv% = 0
If CurrentSI <> Null Then CallerPriv = CurrentSI\Privileged
ThreadScript(Name$, Func$, AI, AIContext, Param$, CallerPriv)
End Function
Function BVM_SAVESTATE()
If Not BVM_RequirePrivileged() Then Return
WriteLog(MainLog, "SaveState running...")
SaveAccounts()
WriteLog(MainLog, "Saved accounts...")
SaveSuperGlobals("Data\Server Data\Superglobals.dat")
WriteLog(MainLog, "Saved superglobal variables...")
;For Ar.Area = Each Area : ServerSaveAreaOwnerships(Ar) : Next {##}
WriteLog(MainLog, "Saved zone ownerships...")
SaveEnvironment()
WriteLog(MainLog, "Saved environment settings...")
SaveDroppedItems("Data\Server Data\Dropped Items.dat")
WriteLog(MainLog, "Saved dropped items...")
WriteLog(MainLog, "SaveState complete")
End Function
Function BVM_PLAYERACCOUNTNAME$(Param%)
Actor.ActorInstance = Object.ActorInstance(Param)
If Actor <> Null
A.Account = Object.Account(Actor\Account)
If A <> Null Then Result$ = A\User$
EndIf
Return Result$
End Function
Function BVM_PLAYERACCOUNTEMAIL$(Param%)
Actor.ActorInstance = Object.ActorInstance(Param)
If Actor <> Null
A.Account = Object.Account(Actor\Account)
If A <> Null Then Result$ = A\Email$
EndIf
Return Result$
End Function
Function BVM_PLAYERISGM%(Param%)
Actor.ActorInstance = Object.ActorInstance(Param)
If Actor <> Null
A.Account = Object.Account(Actor\Account)
If A <> Null Then Result% = A\IsDM
EndIf
Return Result%
End Function
Function BVM_PLAYERISDM%(Param%)
Actor.ActorInstance = Object.ActorInstance(Param)
If Actor <> Null
A.Account = Object.Account(Actor\Account)
If A <> Null Then Result% = A\IsDM
EndIf
Return Result%
End Function
Function BVM_PLAYERISBANNED%(Param%)
Actor.ActorInstance = Object.ActorInstance(Param)
If Actor <> Null
A.Account = Object.Account(Actor\Account)
If A <> Null Then Result% = A\IsBanned
EndIf
Return Result%
End Function
; Returns True iff the currently-executing script was spawned via a code
; path that has already verified the caller is a GM. Used to gate
; admin-only BVM commands (Ban/Kick/Warp/GiveItem/SetGold/SetActorLevel).
;
; Without this gate, any NPC's Examine / Trade / RightClick / ItemUse
; script ran with the clicker's actor handle and could invoke BanPlayer
; on the clicker. The only GM check in the entire scripting surface was
; the chat `/script` command itself.
Function BVM_RequirePrivileged%()
SI.ScriptInstance = Object.ScriptInstance(hSI)
If SI = Null Then Return False
If SI\Privileged <> 0 Then Return True
BVM_ScriptLog("Privileged BVM call refused from non-privileged script: " + SI\Name)
Return False
End Function
; Allow the call if the script is privileged OR the target is the
; script's own actor / context. Use this for state-mutating BVM
; commands that take an actor handle but are safe when the target
; is the script's own actor (e.g. an NPC's own movement, an actor's
; own attribute change). Without this distinction, the privilege
; gate either lets non-privileged NPC scripts mutate arbitrary
; actors (current behaviour for several commands) or breaks every
; NPC's ability to move itself.
Function BVM_RequireSelfOrPrivileged%(Param1%)
SI.ScriptInstance = Object.ScriptInstance(hSI)
If SI = Null Then Return False
If SI\Privileged <> 0 Then Return True
If Param1% <> 0 And (Param1% = SI\AI Or Param1% = SI\AIContext) Then Return True
BVM_ScriptLog("BVM call refused: target is neither script's actor nor context (" + SI\Name + ")")
Return False
End Function
Function BVM_BANPLAYER(Param%)
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param)
If Actor <> Null
A.Account = Object.Account(Actor\Account)
If A <> Null Then A\IsBanned = 1
EndIf
End Function
Function BVM_KICKPLAYER(Param%)
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param%)
If Actor <> Null
DataAux$ = RCE_StrFromInt(Actor\RNID)
RCE_FSend(0, RCE_PlayerKicked, DataAux$, True, Len(DataAux$))
RCE_FSend(Actor\RNID, P_KickedPlayer, "", True, 0)
EndIf
End Function
Function BVM_ACTORX#(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null Then Result# = Actor\X#
Return Result#
End Function
Function BVM_ACTORY#(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null Then Result# = Actor\Y#
Return Result#
End Function
Function BVM_ACTORZ#(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null Then Result# = Actor\Z#
Return Result#
End Function
Function BVM_ACTORAGGRESSIVENESS%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Result% = Actor\Actor\Aggressiveness
EndIf
Return Result%
End Function
Function BVM_ACTORINTRIGGER%(Param1%, Param2%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
TriggerID = Param2%
; TriggerScript$/TriggerSize#/etc. are Dim'd 0..149 on AreaInstance.
; Reject any out-of-range index from the script before indexing.
If TriggerID >= 0 And TriggerID <= 149
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
If Len(AInstance\Area\TriggerScript$[TriggerID]) > 0
Size# = AInstance\Area\TriggerSize#[TriggerID] * AInstance\Area\TriggerSize#[TriggerID]
DistX# = Abs(Actor\X# - AInstance\Area\TriggerX#[TriggerID])
DistY# = Abs(Actor\Y# - AInstance\Area\TriggerY#[TriggerID])
DistZ# = Abs(Actor\Z# - AInstance\Area\TriggerZ#[TriggerID])
Dist# = (DistX# * DistX#) + (DistY# * DistY#) + (DistZ# * DistZ#)
If Dist# < Size# Then Result% = 1
EndIf
EndIf
EndIf
EndIf
Return Result%
End Function
Function BVM_ACTORSINZONE%(Param1$, Instance%=0)
ZoneName$ = Upper$(Param1$)
For Ar.Area = Each Area
If Upper$(Ar\Name$) = ZoneName$
Count = 0
; In all instances
If Instance = -1
For Instance = 0 To 99
AInstance.AreaInstance = Ar\Instances[Instance]
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
Count = Count + 1
A2 = A2\NextInZone
Wend
EndIf
Next
; In a specific instance
Else
; Bound the script-supplied Instance index. Instances
; is Dim'd 0..99; a wild value would read past the array
; (Blitz3D has no runtime Dim bounds check). Return 0 on
; out-of-range -- the caller's count comparison naturally
; handles "no actors found".
If Instance >= 0 And Instance <= 99
AInstance.AreaInstance = Ar\Instances[Instance]
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
Count = Count + 1
A2 = A2\NextInZone
Wend
EndIf
EndIf
EndIf
Result% = Count
Exit
EndIf
Next
Return Result%
End Function
Function BVM_PLAYERSINZONE%(Param1$, Instance%=0)
ZoneName$ = Upper$(Param1$)
For Ar.Area = Each Area
If Upper$(Ar\Name$) = ZoneName$
Count = 0
; In all instances
If Instance = -1
For Instance = 0 To 99
AInstance.AreaInstance = Ar\Instances[Instance]
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then Count = Count + 1
A2 = A2\NextInZone
Wend
EndIf
Next
; In a specific instance
Else
; Same bounds guard as BVM_ACTORSINZONE above.
If Instance >= 0 And Instance <= 99
AInstance.AreaInstance = Ar\Instances[Instance]
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then Count = Count + 1
A2 = A2\NextInZone
Wend
EndIf
EndIf
EndIf
Result% = Count
Exit
EndIf
Next
Return Result%
End Function
Function BVM_ZONEINSTANCEEXISTS%(Param1$, Param2%)
Zone.Area = FindArea(Param1$)
If Zone <> Null
Instance = Param2%
; Bound the script-supplied Instance index. Returning 0 on
; out-of-range matches "instance does not exist".
If Instance < 0 Or Instance > 99 Then Return 0
If Zone\Instances[Instance] <> Null Then Result% = 1
EndIf
Return Result%
End Function
Function BVM_CREATEZONEINSTANCE%(Param1$, Instance%=0)
Zone.Area = FindArea(Param1$)
If Zone <> Null
; Script requests a specific ID. Bound to the (Dim 0..99)
; Instances array; out-of-range returns 0 ("instance not
; created") rather than a Dim OOB write.
If Instance > 0 And Instance <= 99
If Zone\Instances[Instance] = Null
ServerCreateAreaInstance(Zone, Instance)
Result% = Instance
EndIf
ElseIf Instance = 0
; Use first free ID
For i = 1 To 99
If Zone\Instances[i] = Null
ServerCreateAreaInstance(Zone, i)
Result% = i
Exit
EndIf
Next
; Instance > 99 or negative: silently return 0 (caller's
; if-instance-was-created check fires the same "no slot"
; branch).
EndIf
Else
WriteLog(MainLog, "Instance can not be created, Zone " + Param1$ + " does not exist.")
EndIf
Return Result%
End Function
Function BVM_REMOVEZONEINSTANCE(Param1$, Instance%)
; Admin-only: tears down an entire zone instance -- moves every
; player out, frees every AI ActorInstance, deletes every dropped
; item, removes the on-disk ownership file. Without this gate any
; NPC's Examine / Trade / RightClick script could nuke an entire
; zone the clicker happens to be in. Equivalent-effect peer:
; there is no single gated BVM that does this, but every primitive
; this composes (KillActor, FreeActorInstance, file deletion) is
; gated or unreachable from a non-priv context. Closes the gap.
If Not BVM_RequirePrivileged() Then Return
Zone.Area = FindArea(Param1$)
If Zone <> Null
; Bound the script-supplied Instance index against Dim 0..99.
If Instance > 0 And Instance <= 99
If Zone\Instances[Instance] <> Null
; Move players to instance #0, and delete AI actor instances
Actor.ActorInstance = Zone\Instances[Instance]\FirstInZone
While Actor <> Null
A2.ActorInstance = Actor\NextInZone
If Actor\RNID > 0
SetArea(Actor, Zone, 0, -1, -1, Actor\X#, Actor\Y#, Actor\Z#)
Else
FreeActorScripts(Actor)
FreeActorInstance(Actor)
EndIf
Actor = A2
Wend
; Delete ownerships for instance from disk
DeleteFile("Data\Server Data\Areas\Ownerships\" + Zone\Name$ + " (" + Zone\Instances[Instance]\ID + ") Ownerships.dat")
; Delete dropped items. After-cursor walk: the body Deletes
; D, which would corrupt the For-Each cursor on the next
; iteration. Fires from BVM_REMOVEZONEINSTANCE -- a script
; cleanup of a zone with multiple dropped items would
; either skip past the corruption point (orphan items
; leak) or crash the server on the freed-next-pointer
; deref. Documented in CLAUDE.md (#247).
Local Drz.DroppedItem = First DroppedItem
Local DrzNext.DroppedItem = Null
While Drz <> Null
DrzNext = After Drz
AInstance.AreaInstance = Object.AreaInstance(Drz\ServerHandle)
If AInstance = Zone\Instances[Instance]
FreeItemInstance(Drz\Item)
Delete(Drz)
EndIf
Drz = DrzNext
Wend
; Free Owned Scenery for the instance {##}
;For i = 0 To 499
; If Zone\Instances[Instance]\OwnedScenery[i] <> Null
; If Zone\Instances[Instance]\OwnedScenery[i]\Inventory <> Null
; Delete Zone\Instances[Instance]\OwnedScenery[i]\Inventory
; EndIf
; Delete Zone\Instances[Instance]\OwnedScenery[i]
; EndIf
;Next
Delete Zone\Instances[Instance]
EndIf
EndIf
Else
WriteLog(MainLog, "Instance can not be created, Zone " + Param1$ + " does not exist.")
EndIf
End Function
Function BVM_COUNTPARTYMEMBERS%(Param%)
Actor.ActorInstance = Object.ActorInstance(Param%)
If Actor <> Null
Party.Party = Object.Party(Actor\PartyID)
If Party <> Null Then Result% = Party\Members - 1
EndIf
Return Result%
End Function
Function BVM_PARTYMEMBER%(Param1%, Param2%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Party.Party = Object.Party(Actor\PartyID)
If Party <> Null
Member = Param2%
If Member <= Party\Members - 1
Count = 0
For i = 0 To 7
If Party\Player[i] <> Null And Party\Player[i] <> Actor
Count = Count + 1
If Count = Member
Result = Handle(Party\Player[i])
Exit
EndIf
EndIf
Next
EndIf
EndIf
EndIf
Return Result%
End Function
Function BVM_KILLACTOR(Param1%, Param2%=0)
; Without this gate any NPC Examine / Trade / RightClick script
; could instantly kill any actor whose handle it could scan via
; BVM_NEXTACTOR.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Actor\Attributes\Value[HealthStat] = 0
Killer.ActorInstance = Object.ActorInstance(Param2%)
KillActor(Actor, Killer)
EndIf
End Function
Function BVM_CHANGEACTOR(Param1%, Param2%)
If Not BVM_RequirePrivileged() Then Return
Local Success% = False
ID% = Param2
;Test for valid ActorID
For aid.Actor = Each Actor
If aid\ID = ID Then Success = True : Exit
Next
If Success = True
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If ActorList(ID) <> Null
Actor\Actor = ActorList(ID)
If Actor\Actor\Genders = 2 And Actor\Gender <> 1 Then Actor\Gender = 1
If (Actor\Actor\Genders = 1 Or Actor\Actor\Genders = 3) And Actor\Gender <> 0 Then Actor\Gender = 0
; Tell other players in the area. Skip the broadcast loop
; if the actor's area lookup fails (mid-warp / freed zone)
; -- the appearance change still applies to the actor's
; in-memory state; only the network broadcast is dropped.
Pa$ = "C" + RCE_StrFromInt$(Actor\RuntimeID, 2) + RCE_StrFromInt$(ID, 2)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_AppearanceUpdate, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
EndIf
Else
WriteLog(MainLog, "Error: Invalid ActorID supplied in ChangeActor() command.")
EndIf
End Function
Function BVM_SPAWNITEM(Param1$, Param2%, Param3$, Param4#, Param5#, Param6#, Param7%=0)
ItemTemplate.Item = FindItem(Param1$)
If ItemTemplate <> Null
Zone.Area = FindArea(Param3$)
If Zone <> Null
D.DroppedItem = New DroppedItem
D\Item = CreateItemInstance(ItemTemplate)
D\Amount = Param2%
; Sanitise drop coords -- NaN/Inf poisons spatial code on
; every receiver that walks DroppedItem positions. Mirror
; the P_InventoryUpdate "D" flow (ServerNet.bb ~1467) which
; already clamps before persisting.
D\X# = ClampWorldCoord#(Param4#)
D\Y# = ClampWorldCoord#(Param5#)
D\Z# = ClampWorldCoord#(Param6#)
; Bound the script-supplied Instance index. Instances is
; Dim'd 0..99; a wild value would walk past the array on the
; first probe below. Clamp out-of-range to 0 (the default
; instance) and fall through to the existing "instance is
; Null" path which auto-falls back to 0 anyway.
Instance = Param7%
If Instance < 0 Or Instance > 99 Then Instance = 0
If Zone\Instances[Instance] = Null
Instance = 0
WriteLog(MainLog, "BVM_SPAWNITEM: requested instance does not exist in " + Zone\Name$ + ", spawning in instance 0")
EndIf
; If even instance 0 is uninitialised, give up cleanly --
; can't broadcast into a non-existent zone.
If Zone\Instances[Instance] = Null
WriteLog(MainLog, "BVM_SPAWNITEM: instance 0 also missing for " + Zone\Name$ + ", dropping spawn")
FreeItemInstance(D\Item)
Delete D
Return
EndIf
D\ServerHandle = Handle(Zone\Instances[Instance])
; Tell other players in the area
Pa$ = RCE_StrFromInt$(D\Amount, 2) + RCE_StrFromFloat$(D\X#) + RCE_StrFromFloat$(D\Y#) + RCE_StrFromFloat$(D\Z#)
Pa$ = Pa$ + RCE_StrFromInt$(Handle(D), 4) + ItemInstanceToString$(D\Item)
A2.ActorInstance = Zone\Instances[Instance]\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_InventoryUpdate, "D" + Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
End Function
Function BVM_SETACTORGENDER(Param1%, Param2%)
; Cosmetic appearance setter -- broadcasts P_AppearanceUpdate to
; every player in the area. Non-priv clicker exploit:
; SetActorGender(anotherPlayer, ...) forces an appearance flip
; on a victim, which is griefing rather than mechanical brick,
; but still unwanted. Same threat shape as the SET-name/tag pair.
; Quest reward scripts (race/gender change tokens) already run
; privileged. Per-Actor body/head clamping below is unchanged.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
; Param2% is 1-based on the script side (1=male, 2=female).
; Clamp the resulting 0-based gender to 0..1 before storing so a
; script can't drive Actor\Gender to arbitrary values that then
; flow into Chr$()/array indexing downstream.
Local NewGender = Param2% - 1
If NewGender < 0 Or NewGender > 1 Then NewGender = 0
Actor\Gender = NewGender
If Actor\Actor\Genders = 2 And Actor\Gender <> 1 Then Actor\Gender = 1
If (Actor\Actor\Genders = 1 Or Actor\Actor\Genders = 3) And Actor\Gender <> 0 Then Actor\Gender = 0
Pa$ = "G" + RCE_StrFromInt$(Actor\RuntimeID, 2) + Chr$(Actor\Gender)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_AppearanceUpdate, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
End Function
Function BVM_ACTORBEARD%(Param1%)
; Typo: was Param% (undeclared, silently 0) instead of Param1%.
; Every call resolved Object.ActorInstance(0) -> Null -> Result=0.
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null Then Result% = Actor\Beard + 1
Return Result%
End Function
Function BVM_SETACTORBEARD(Param1%, Param2%)
; Cosmetic appearance setter -- same threat shape as
; SETACTORGENDER. Clicker-griefing only, but consistent gating
; across the appearance cluster.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\Gender = 0
; Bound Param2% to the 5-slot BeardIDs array. Param2% is
; 1-based on the script side; clamp the resulting 0-based
; index to 0..4 before storing.
Local NewBeard = Param2% - 1
If NewBeard < 0 Or NewBeard > 4 Then NewBeard = 0
Actor\Beard = NewBeard
Pa$ = "D" + RCE_StrFromInt$(Actor\RuntimeID, 2) + Chr$(Actor\Beard)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_AppearanceUpdate, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
EndIf
End Function
Function BVM_ACTORHAIR%(Param1%)
; Typo: was Param% (undeclared, silently 0) instead of Param1%.
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null Then Result% = Actor\Hair + 1
Return Result%
End Function
Function BVM_SETACTORHAIR(Param1%, Param2%)
; Cosmetic appearance setter -- same gating rationale as
; SETACTORGENDER / SETACTORBEARD.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\Gender = 0
; Bound Param2% to the 5-slot Hair-IDs arrays (same shape
; as SETACTORBEARD). 1-based -> 0-based clamp.
Local NewHair = Param2% - 1
If NewHair < 0 Or NewHair > 4 Then NewHair = 0
Actor\Hair = NewHair
Pa$ = "D" + RCE_StrFromInt$(Actor\RuntimeID, 2) + Chr$(Actor\Hair)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_AppearanceUpdate, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
EndIf
End Function
Function BVM_ACTORCALLFORHELP(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null Then AICallForHelp(Actor)
End Function
Function BVM_SETACTORAISTATE(Param1%, Param2%)
; Gated. Clicker brick risk: SetActorAIState(SomeGuard, AI_Wait)
; from a non-priv NPC right-click script disables a hostile guard's
; AI -- the player who clicks the NPC walks away with a free
; sandbag. Pre-PR-#329 this stayed ungated because shipped content
; (AOE Damage Spell Template.rsl) called it from a non-priv
; spell-cast spawn to make targets aggressive on impact -- gating
; would have no-op'd the spell.
;
; Closed by the privileged-script allowlist (Scripting.bb's
; LoadPrivilegedScripts + the elevation point in ThreadScript). The
; AOE template's name is in Data\Server Data\Privileged Scripts.dat;
; spell-cast spawns that target it now get the elevation. Other
; non-priv callers still refuse, closing the brick vector.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Actor\AIMode = Param2%
EndIf
End Function
Function BVM_ACTORAISTATE%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Result% = Actor\AIMode
EndIf
Return Result%
End Function
Function BVM_ACTORTARGET%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\AITarget <> Null
Result% = Handle(Actor\AITarget)
EndIf
EndIf
Return Result%
End Function
Function BVM_SETACTORTARGET(Param1%, Param2%=0)
; Gated. Clicker-brick risk: SetActorTarget(SomeGuard,
; anotherPlayer) from a non-priv NPC right-click script weaponizes
; the guard against an arbitrary victim. Pre-PR-#329 this stayed
; ungated because shipped content needed it from non-priv spawns:
; - /Assist chat command (In-game Commands.rsl) targets the
; assisted player's current target.
; - AOE Damage Spell Template.rsl targets the player on spell
; impact (aggro-pull).
;
; Closed by the privileged-script allowlist. Both script names are
; in Data\Server Data\Privileged Scripts.dat; their ThreadScript
; spawns now get the elevation. Other non-priv callers refuse,
; closing the weaponization vector.
;
; The existing partial safety (Aggressiveness=3 non-combatants and
; friendly-faction targets rejected) is preserved as defense-in-
; depth -- privileged callers still hit those checks.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
Actor2.ActorInstance = Object.ActorInstance(Param2%)
If Actor <> Null
If Actor2 <> Null
If Actor\Actor\Aggressiveness <> 3 And Actor2\Actor\Aggressiveness <> 3
If Actor2\FactionRatings[Actor\HomeFaction] < 150 Then Actor\AITarget = Actor2
EndIf
Else
Actor\AITarget = Null
EndIf
EndIf
End Function
Function BVM_SETACTORDESTINATION(Param1%, Param2#, Param3#)
If Not BVM_RequireSelfOrPrivileged(Param1%) Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
; Sanitise -- NaN destination poisons the AI patrol move
; vector (XDist/ZDist) and propagates to clients via every
; broadcast that quotes Actor\DestX#/DestZ#.
Actor\DestX# = ClampWorldCoord#(Param2#)
Actor\DestZ# = ClampWorldCoord#(Param3#)
EndIf
End Function
Function BVM_GIVEKILLXP(Param1%, Param2%)
; Equivalent-effect bypass of gated BVM_SETACTORLEVEL: a flood of
; XP triggers the LevelUp script path (GiveXP -> ThreadScript
; "LevelUp") which can advance Level arbitrarily. Without this
; gate, any NPC's Examine / Trade / RightClick script could grant
; or deny arbitrary progression to the clicker. Match the gate on
; BVM_GIVEXP below and on BVM_SETACTORLEVEL.
If Not BVM_RequirePrivileged() Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
Actor2.ActorInstance = Object.ActorInstance(Param2%)
If Actor <> Null And Actor2 <> Null
Diff = Actor2\Level - Actor\Level
If Diff < 1 Then Diff = 1
XP = (Diff * Actor2\Actor\XPMultiplier) + Rand(0, 20)
GiveXP(Actor, XP)
EndIf
End Function
Function BVM_SPAWN%(Param1%, Param2$, Param3#, Param4#, Param5#, Param6$ = "", Param7$ = "", Param8%=0)
; Find actor
ID = Param1%
; ActorList is Dim'd 0..65535. Bound-check both sides before the
; Null check -- Blitz3D does not bounds-check Dim accesses, so a
; script-supplied ID of 99999 or -1 would read off the end of the
; array and crash the server. Same shape as P_KillActor handler.
If ID >= 0 And ID <= 65535
If ActorList(ID) <> Null
; Find zone
Name$ = Upper$(Param2$)
For Ar.Area = Each Area
If Upper$(Ar\Name$) = Name$
AI.ActorInstance = CreateActorInstance.ActorInstance(ActorList(ID))
AI\RNID = -1
AssignRuntimeID(AI)
Instance = Param8%
; Sanitise spawn coords -- SetArea writes them directly
; to A\X#/Y#/Z# which then broadcast on every update.
SetArea(AI, Ar, Instance, -1, -1, ClampWorldCoord#(Param3#), ClampWorldCoord#(Param4#), ClampWorldCoord#(Param5#))
AI\AIMode = AI_Wait
AI\Script$ = Param6$
AI\DeathScript$ = Param7$
WriteLog(MainLog, "Spawned AI actor from script: " + AI\Actor\Race$ + " in zone: " + Ar\Name$)
Result% = Handle(AI)
Exit
EndIf
Next
EndIf
EndIf
Return Result%
End Function
Function BVM_PARAMETER$(Param1%)
; Null-S guard: hSI is the per-call script-instance handle the
; VM populates before invoking a BVM_* command. If the command
; runs without a live ScriptInstance (BVM reentry, host-side
; invocation, a still-running command on a script that was
; just FreeScriptInstance'd), Object.ScriptInstance(0) returns
; Null and the bare S\Param$ deref faults.
Local S.ScriptInstance = Object.ScriptInstance(hSI)
If S = Null Then Return ""
Local Result$ = ""
If S\Param$ <> ""
Result$ = SafeSplit(S\Param$, Param1%, ",")
EndIf
Return Result$
End Function
Function BVM_ROTATEACTOR(Param1%, Param2#)
If Not BVM_RequireSelfOrPrivileged(Param1%) Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
; ClampSaneFloat catches NaN/Inf/extreme magnitudes -- a
; script-supplied NaN yaw poisons rotation matrices on
; every receiver.
Actor\Yaw# = ClampSaneFloat#(Param2#)
Pa$ = "R" + RCE_StrFromInt$(Actor\RuntimeID, 2) + RCE_StrFromFloat$(Actor\Yaw#)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_RepositionActor, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
End Function
Function BVM_MOVEACTOR(Param1%, Param2#, Param3#, Param4#, Param5%=0, Param6%=0)
If Not BVM_RequireSelfOrPrivileged(Param1%) Then Return
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
; Sanitise positions before they're persisted into the actor
; record and broadcast. A script supplying NaN/Inf would
; poison every receiving client's spatial code (collision,
; LOD culling, EntityDistance#). Mirrors the P_InventoryUpdate
; "D" drop-item flow (ServerNet.bb ~1467).
Actor\X# = ClampWorldCoord#(Param2#)
Actor\Y# = ClampWorldCoord#(Param3#)
Actor\Z# = ClampWorldCoord#(Param4#)
Actor\DestX# = Actor\X#
Actor\DestZ# = Actor\Z#
Pa$ = "M" + RCE_StrFromInt$(Actor\RuntimeID, 2) + RCE_StrFromFloat$(Actor\X#) + RCE_StrFromFloat$(Actor\Y#) + RCE_StrFromFloat$(Actor\Z#)
Pa$ = Pa$ + RCE_StrFromInt$(Param5%, 1) + RCE_StrFromInt$(Param6%, 1)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_RepositionActor, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
End Function
Function BVM_CREATEFLOATINGNUMBER(Param1%, Param2%, Param3%=255, Param4%=255, Param5%=255)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
Amount = Param2%
R = Param3%
G = Param4%
B = Param5%
Pa$ = RCE_StrFromInt$(Actor\RuntimeID, 2) + RCE_StrFromInt$(Amount, 4)
Pa$ = Pa$ + RCE_StrFromInt$(R, 1) + RCE_StrFromInt$(G, 1) + RCE_StrFromInt$(B, 1)
AInstance.AreaInstance = Object.AreaInstance(Actor\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_FloatingNumber, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
EndIf
End Function
Function BVM_ACTORRIDER%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1)
If Actor <> Null
If Actor\Rider <> Null Then Result = Handle(Actor\Rider)
EndIf
Return Result%
End Function
Function BVM_ACTORMOUNT%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\Mount <> Null Then Result% = Handle(Actor\Mount)
EndIf
Return Result%
End Function
Function BVM_ITEMID%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\Item\ID
Return Result%
End Function
Function BVM_ITEMVALUE%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\Item\Value
Return Result%
End Function
Function BVM_ITEMMASS%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\Item\Mass
Return Result%
End Function
Function BVM_ITEMRANGE#(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result# = Item\Item\Range#
Return Result#
End Function
Function BVM_ITEMDAMAGE%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\Item\WeaponDamage
Return Result%
End Function
Function BVM_ITEMDAMAGETYPE$(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result$ = DamageTypes$(Item\Item\WeaponDamageType)
Return Result$
End Function
Function BVM_ITEMWEAPONTYPE%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\Item\WeaponType
Return Result%
End Function
Function BVM_ITEMARMOR%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\Item\ArmourLevel
Return Result%
End Function
Function BVM_ITEMMISCDATA$(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result$ = Item\Item\MiscData$
Return Result$
End Function
Function BVM_ITEMHEALTH%(Param1%)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null Then Result% = Item\ItemHealth
Return Result%
End Function
Function BVM_SETITEMHEALTH(Param1%, Param2%)
; ItemHealth is the durability field; zeroing it bricks a player's
; equipped weapon / armour on next use (Items.bb breaks items at
; ItemHealth <= 0). A non-priv Examine / Trade / RightClick script
; could iterate the clicker's Inventory\Items[] and zero the
; ItemHealth on each, gutting all gear in one click. Note that
; Param1 is an ItemInstance handle (not an ActorInstance), so the
; self-or-priv shortcut doesn't apply -- there is no SI\AI for an
; item. RequirePrivileged is the only sensible gate. Quest reward
; scripts that legitimately repair / damage items run privileged.
If Not BVM_RequirePrivileged() Then Return
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null
Item\ItemHealth = Param2%
; If item belongs to a human player, tell them the new health
Done = False
For AI.ActorInstance = Each ActorInstance
If AI\RNID > 0
For i = 0 To Slots_Inventory
If AI\Inventory\Items[i] = Item
Pa$ = "H" + RCE_StrFromInt$(i, 1) + RCE_StrFromInt$(Item\ItemHealth, 1)
RCE_Send(Host, AI\RNID, P_InventoryUpdate, Pa$, True)
Done = True
Exit
EndIf
Next
EndIf
If Done = True Then Exit
Next
EndIf
End Function
Function BVM_ITEMATTRIBUTE%(Param1%, Param2$)
Item.ItemInstance = Object.ItemInstance(Param1%)
If Item <> Null
Attribute = FindAttribute(Param2$)
If Attribute > -1 Then Result% = Item\Attributes\Value[Attribute]
EndIf
Return Result%
End Function
Function BVM_PLAYERINGAME%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\RNID > 0 Then Result% = 1
EndIf
Return Result%
End Function
Function BVM_ACTORISHUMAN%(Param1%)
Actor.ActorInstance = Object.ActorInstance(Param1%)
If Actor <> Null
If Actor\RNID > -1 Then Result% = 1
EndIf
Return Result%
End Function
Function BVM_SETLEADER(Param1%, Param2%)
; The function-body guard `If Actor\RNID = -1` restricts Param1 to
; NPCs (a clicker can't make a player a pet), but Param2 (the new
; leader) can be any actor handle including the clicker itself --
; so a non-priv Examine / Trade / RightClick script could call
; SetLeader(SomeWorldGuard, clicker) to recruit world NPCs as