-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathServerNet.bb
More file actions
3093 lines (2980 loc) · 129 KB
/
ServerNet.bb
File metadata and controls
3093 lines (2980 loc) · 129 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
;##############################################################################################################################
;HISTORY:
;
; by Rob W (rottbott), August 2004
;
;Actor moves/change areas standard update timing fix. 8/16/2007 Rofar. added handling messages for change area and position actor completion
;##############################################################################################################################
Type QueuedPacket
Field Connection, Destination, PacketType, Pa$, ReliableFlag, PlayerFrom
Field NextInQueue.QueuedPacket, PreviousInQueue.QueuedPacket
Field PreviousSentTime
End Type
; Sends the in-game /help output to the requesting player. Groups the
; 30 built-in slash commands by category (Chat, Party, Social, Info,
; DM-only) and filters DM-only entries when the caller isn't a DM.
;
; The Description text is hard-coded English for the moment -- the
; existing LanguageString$ table is line-indexed and adding new IDs
; for help-text strings requires touching Language.txt. Localising
; the help payload is a follow-up; the layout below is designed so
; a single InitChatHelpStrings() call can replace the literals when
; the localization slots exist.
;
; Each line is sent as a separate P_ChatMessage with the Chr$(254)
; "system message" prefix the rest of the dispatcher uses for
; server-originated chat (matches /time, /season, etc.).
Function SendChatHelp(AI.ActorInstance, Topic$)
Local IsDM% = False
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True Then IsDM = True
; Detail mode: /help <command>
Local T$ = Upper$(Trim$(Topic$))
If Len(T) > 0
SendChatHelpDetail(AI, T, IsDM)
Return
EndIf
; Header
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "--- Available slash commands ---", True)
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Use /help <command> for details on a specific command.", True)
; Chat
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Chat: /me /yell /pm /g /p", True)
; Party
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Party: /invite /accept /leave /pet", True)
; Social
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Social: /ignore /unignore /players /allplayers /trade", True)
; Info
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Info: /time /date /season", True)
; DM commands -- only listed for DMs
If IsDM = True
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "DM-only: /kick /xp /gold /setattribute /setattributemax /script /gm /warp /warpother /ability /give /weather /netdump", True)
EndIf
End Function
; Per-command detail for /help <command>. Centralised here so the
; command-name registry is the only thing that needs touching when a
; command is added.
Function SendChatHelpDetail(AI.ActorInstance, T$, IsDM%)
Local Line$ = ""
; Built-in non-DM commands
If T = "ME" Then Line = "/me <action> -- emote action in current area"
If T = "YELL" Then Line = "/yell <text> -- shout to every online player on the server"
If T = "PM" Then Line = "/pm <player>,<text> -- private message"
If T = "G" Then Line = "/g <text> -- guild chat"
If T = "P" Then Line = "/p <text> -- party chat"
If T = "INVITE" Then Line = "/invite <player> -- invite player to party"
If T = "ACCEPT" Then Line = "/accept -- accept a pending party invite"
If T = "LEAVE" Then Line = "/leave -- leave current party"
If T = "PET" Then Line = "/pet <name>,<command>[,<params>] -- command a pet (or 'all')"
If T = "IGNORE" Then Line = "/ignore <player> -- silence a player's chat"
If T = "UNIGNORE" Then Line = "/unignore <player> -- re-enable a player's chat"
If T = "PLAYERS" Then Line = "/players -- list players in your area"
If T = "ALLPLAYERS" Then Line = "/allplayers -- list all online players"
If T = "TRADE" Then Line = "/trade <player> -- open trade with player"
If T = "TIME" Then Line = "/time -- show in-world clock time"
If T = "DATE" Then Line = "/date -- show in-world date"
If T = "SEASON" Then Line = "/season -- show current season"
If T = "HELP" Or T = "?" Then Line = "/help [<command>] -- list commands, or detail on one"
; DM-only commands -- only describe when caller has DM rights.
; All "grant" commands target self -- the handlers in this file
; uniformly call GiveXP/AddSpell/etc. on AI, never on a Params-named
; player. /kick and /warpother are the exceptions that take a target.
If IsDM = True
If T = "KICK" Then Line = "/kick <player> -- disconnect a player"
If T = "XP" Then Line = "/xp <amount> -- grant XP to self"
If T = "GOLD" Then Line = "/gold <amount> -- adjust own gold (negative to deduct)"
If T = "SETATTRIBUTE" Then Line = "/setattribute <attr>,<value> -- set own attribute"
If T = "SETATTRIBUTEMAX" Then Line = "/setattributemax <attr>,<value> -- set own max attribute"
If T = "SCRIPT" Then Line = "/script <name>,<func> -- run script's function as self"
If T = "GM" Then Line = "/gm <text> -- broadcast as GM to all DMs"
If T = "WARP" Then Line = "/warp <area>[,<instance>] -- warp self to area's first portal"
If T = "WARPOTHER" Then Line = "/warpother <player>,<area>[,<instance>] -- warp another player"
If T = "ABILITY" Then Line = "/ability <ability>,<level> -- grant own ability at level"
If T = "GIVE" Then Line = "/give <item> -- spawn item into own inventory"
If T = "WEATHER" Then Line = "/weather <type> -- set weather in current area"
If T = "NETDUMP" Then Line = "/netdump -- start a network packet log"
EndIf
If Len(Line) = 0
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "No help available for /" + Lower$(T) + ". Type /help for a list.", True)
Else
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + Line, True)
EndIf
End Function
; Queues a packet (queued packets are delayed so that for each destination, only one is sent per 12 milliseconds)
Function SendQueued(Connection, Destination, PacketType, Pa$, ReliableFlag = False, PlayerFrom = 0)
; Create packet
Q.QueuedPacket = New QueuedPacket
Q\Connection = Connection
Q\Destination = Destination
Q\PacketType = PacketType
Q\Pa$ = Pa$
Q\ReliableFlag = ReliableFlag
Q\PlayerFrom = PlayerFrom
Q\PreviousSentTime = MilliSecs() - 8
; Attempt to find previous packet in queue
For Q2.QueuedPacket = Each QueuedPacket
If Q2\NextInQueue = Null And Q2\Destination = Destination And Q2\Connection = Connection
If Q2 <> Q
Q2\NextInQueue = Q
Q\PreviousInQueue = Q2
Exit
EndIf
EndIf
Next
End Function
; Processes all network messages
Function UpdateNetwork()
; ; Network data logging
; If LogNetwork > 0
; ; Write to file
; If MilliSecs() - LogNetworkTime >= 5000
; Players = 0
; For AI.ActorInstance = Each ActorInstance
; If AI\RNID > 0 Then Players = Players + 1
; Next
; TrafficIn = (RN_BytesReceived(Host) - LogNetworkBytesIn) / 5
; TrafficOut = (RN_BytesSent(Host) - LogNetworkBytesOut) / 5
; L = StartLog("Network Data Dump")
; WriteLog(L, "Players in game: " + Players)
; WriteLog(L, "Average traffic in: " + TrafficIn + " bytes/second", False)
; WriteLog(L, "Average traffic out: " + TrafficOut + " bytes/second", False)
; StopLog(L)
; LogNetworkBytesIn = RN_BytesReceived(Host)
; LogNetworkBytesOut = RN_BytesSent(Host)
; LogNetwork = LogNetwork - 1
; LogNetworkTime = MilliSecs()
; EndIf
; EndIf
; Send off any queued messages
For Q.QueuedPacket = Each QueuedPacket
If Q\PreviousInQueue = Null
If MilliSecs() - Q\PreviousSentTime >= 12
; Send it
RCE_Send(Q\Connection, Q\Destination, Q\PacketType, Q\Pa$, Q\ReliableFlag, Q\PlayerFrom)
; Tell next in queue when this one was sent
If Q\NextInQueue <> Null Then Q\NextInQueue\PreviousSentTime = MilliSecs()
; Remove from queue
Delete(Q)
EndIf
EndIf
Next
; Incoming messages
For M.RCE_Message = Each RCE_Message
Select M\MessageType
; Chat message
Case P_ChatMessage
AI.ActorInstance = FindActorInstanceFromRNID(M\FromID)
If Len(M\MessageData$) > 0 And AI <> Null
; Command
If Left$(M\MessageData$, 1) = "/" Or Left$(M\MessageData$, 1) = "\"
Command$ = Mid$(M\MessageData$, 2)
SpacePos = Instr(Command$, " ")
If SpacePos > 0
Params$ = Trim$(Mid$(Command$, SpacePos + 1))
Command$ = Upper$(Left$(Command$, SpacePos - 1))
Else
Command$ = Upper$(Command$)
Params$ = ""
EndIf
Select Command$
Case LanguageString$(LS_SCKick)
A.Account = Object.Account(AI\Account)
; Stale Account handle (mid-logout, freed account) returns
; Null from Object.Account -- bare A\IsDM crashes the server
; from a chat command. Guard every /command's DM gate.
If A <> Null And A\IsDM = True
A2.ActorInstance = FindActorInstanceFromName(Params$)
If A2 <> Null
If A2\RNID > 0
DataAux$ = RCE_StrFromInt(A2\RNID)
RCE_FSend(0, RCE_PlayerKicked, DataAux$, True, Len(DataAux$))
RCE_FSend(A2\RNID, P_KickedPlayer, "", True, 0)
EndIf
EndIf
EndIf
Case LanguageString$(LS_SCUnIgnore)
A2.ActorInstance = FindActorInstanceFromName(Params$)
If A2 <> Null And A2 <> AI
If A2\RNID >= 0
Pos = PlayerIgnoring(AI, A2)
If Pos > 0
Ac1.Account = Object.Account(AI\Account)
; Stale Account handle returns Null; bare
; \Ignore$ crashes the server.
If Ac1 <> Null
EndPos = Instr(Ac1\Ignore$, ",", Pos)
Ac1\Ignore$ = Left$(Ac1\Ignore$, Pos - 1) + Mid$(Ac1\Ignore$, EndPos + 1)
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(253) + LanguageString$(LS_UnIgnoring) + " " + Params$, True)
EndIf
EndIf
EndIf
EndIf
Case LanguageString$(LS_SCIgnore)
A2.ActorInstance = FindActorInstanceFromName(Params$)
If A2 <> Null And A2 <> AI
If A2\RNID >= 0
If PlayerIgnoring(AI, A2) = 0
Ac1.Account = Object.Account(AI\Account)
Ac2.Account = Object.Account(A2\Account)
; Either side's Account can be stale.
If Ac1 <> Null And Ac2 <> Null
Ac1\Ignore$ = Ac1\Ignore$ + Ac2\User$ + ","
EndIf
EndIf
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(253) + LanguageString$(LS_Ignoring) + " " + Params$, True)
EndIf
EndIf
Case LanguageString$(LS_SCNetDump)
A.Account = Object.Account(AI\Account)
If A <> Null And LogNetwork = False And A\IsDM = True
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Starting new net dump...", True)
L = StartLog("Network Data Dump")
WriteLog(L, "Starting new net dump...", True, True)
StopLog(L)
LogNetwork = 6
LogNetworkTime = MilliSecs()
;LogNetworkBytesIn = RCE_BytesReceived(Host)
;LogNetworkBytesOut = RCE_BytesSent(Host)
EndIf
Case LanguageString$(LS_SCPet)
If AI\NumberOfSlaves > 0
Name$ = Upper$(Trim$(Split$(Params$, 1, ",")))
Command$ = Trim$(Split$(Params$, 2, ","))
PetParams$ = Trim$(Split$(Params$, 3, ","))
; Walk AI's FirstSlave chain. The chain
; contains only AI's pets, so the explicit
; Leader filter and the NumberOfSlaves
; early-exit are no longer needed.
Local AI2.ActorInstance = AI\FirstSlave
While AI2 <> Null
If Upper$(AI2\Name$) = Name$ Or Name$ = "ALL"
CommandPet(AI2, Command$, PetParams$)
If Name$ <> "ALL" Then Exit
EndIf
AI2 = AI2\NextSlave
Wend
EndIf
Case LanguageString$(LS_SCLeave)
LeaveParty(AI)
Case LanguageString$(LS_SCOk)
Party.Party = Object.Party(AI\AcceptPending)
If Party <> Null
; Check there's a free space in the party
If Party\Members < 8
; Remove from old party if required
LeaveParty(AI)
; Add to new party and tell players
For i = 0 To 7
If Party\Player[i] <> Null
RCE_Send(Host, Party\Player[i]\RNID, P_ChatMessage, Chr$(254) + AI\Name$ + " " + LanguageString$(LS_XHasJoinedParty), True)
ElseIf AI\AcceptPending <> 0
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_YouHaveJoinedParty), True)
AI\PartyID = Handle(Party)
AI\AcceptPending = 0
Party\Player[i] = AI
Party\Members = Party\Members + 1
EndIf
Next
For i = 0 To 7
If Party\Player[i] <> Null Then SendPartyUpdate(Party\Player[i])
Next
; Run script
ThreadScript("Party", "Join", Handle(AI), 0)
; Party is full
Else
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_CouldNotJoinParty), True)
AI\AcceptPending = 0
EndIf
EndIf
Case LanguageString$(LS_SCInvite)
A2.ActorInstance = FindActorInstanceFromName(Params$)
If A2 <> Null And A2 <> AI
If A2\RNID > 0
If PlayerIgnoring(A2, AI) = 0
Party.Party = Object.Party(AI\PartyID)
; Create new party if required
If Party = Null
Party = New Party
AI\PartyID = Handle(Party)
Party\Members = 1
Party\Player[0] = AI
ThreadScript("Party", "Join", Handle(AI), 0)
EndIf
; Check there's a free space in the party
If Party\Members < 8
A2\AcceptPending = Handle(Party)
RCE_Send(Host, A2\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_PartyInvite) + " " + AI\Name$, True)
RCE_Send(Host, A2\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_PartyInviteInstruction), True)
; Party is full
Else
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_CouldNotInviteParty), True)
EndIf
EndIf
EndIf
EndIf
Case LanguageString$(LS_SCXP)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True Then GiveXP(AI, Int(Params$))
Case LanguageString$(LS_SCGold)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Change = Int(Params$)
AI\Gold = AI\Gold + Change
If Change > 0
Pa$ = "U" + RCE_StrFromInt$(Change, 4)
Else
Pa$ = "D" + RCE_StrFromInt$(Abs(Change), 4)
EndIf
RCE_Send(Host, AI\RNID, P_GoldChange, Pa$, True)
EndIf
Case LanguageString$(LS_SCSetAttribute)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Attribute = FindAttribute(Split$(Params$, 1, ","))
If Attribute > -1
If Attribute = HealthStat Or Attribute = SpeedStat Or Attribute = EnergyStat
UpdateAttribute(AI, Attribute, Int(Split$(Params$, 2, ",")))
Else
AI\Attributes\Value[Attribute] = Int(Split$(Params$, 2, ","))
Pa$ = RCE_StrFromInt$(AI\RuntimeID, 2) + RCE_StrFromInt$(Attribute, 1) + RCE_StrFromInt$(AI\Attributes\Value[Attribute], 2)
RCE_Send(Host, AI\RNID, P_StatUpdate, "A" + Pa$, True)
EndIf
EndIf
EndIf
Case LanguageString$(LS_SCSetAttributeMax)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Attribute = FindAttribute(Split$(Params$, 1, ","))
If Attribute > -1
If Attribute = HealthStat Or Attribute = SpeedStat Or Attribute = EnergyStat
UpdateAttributeMax(AI, Attribute, Int(Split$(Params$, 2, ",")))
Else
AI\Attributes\Maximum[Attribute] = Int(Split$(Params$, 2, ","))
Pa$ = RCE_StrFromInt$(AI\RuntimeID, 2) + RCE_StrFromInt$(Attribute, 1) + RCE_StrFromInt$(AI\Attributes\Maximum[Attribute], 2)
RCE_Send(Host, AI\RNID, P_StatUpdate, "M" + Pa$, True)
EndIf
EndIf
EndIf
Case LanguageString$(LS_SCScript)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Name$ = Trim$(Split$(Params$, 1, ","))
Func$ = Trim$(Split$(Params$, 2, ","))
; Privileged=1: this code path has verified
; the invoker is a GM, so the spawned script
; is allowed to call Ban/Kick/Warp/etc. via
; BVM_RequirePrivileged().
ThreadScript(Name$, Func$, Handle(AI), 0, "", 1)
EndIf
Case LanguageString$(LS_SCMe)
Pa$ = Chr$(252) + "* " + AI\Name$ + " " + Params$
AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0
If PlayerIgnoring(A2, AI) = 0
RCE_Send(Host, A2\RNID, P_ChatMessage, Pa$, True)
EndIf
EndIf
A2 = A2\NextInZone
Wend
If AInstance\Area = GameArea Then AddListBoxItem(Game\ChatText, Pa$ + Chr$(13))
EndIf
Case LanguageString$(LS_SCYell)
Pa$ = Chr$(253) + "<" + AI\Name$ + "> " + Params$
; Walk the FirstOnlinePlayer chain instead of
; every ActorInstance. The chain only contains
; RNID > 0 players, so the inner RNID filter is
; gone.
A2.ActorInstance = FirstOnlinePlayer
While A2 <> Null
If PlayerIgnoring(A2, AI) = 0
RCE_Send(Host, A2\RNID, P_ChatMessage, Pa$, True)
EndIf
A2 = A2\NextOnlinePlayer
Wend
AddListBoxItem(Game\ChatText, Pa$ + Chr$(13))
; The constant is `LS_SCGMSay` (= 205, "GM" in the
; defaults block at Language.bb:256), not `LS_SCGM`.
; Pre-fix the typo'd identifier read as 0 in this
; non-Strict file, so the case matched
; LanguageString$(0) (LS_ConnectingToServer) instead of
; the slash-command "GM" -- the /gm DM-broadcast chat
; command was unreachable. Renaming to the real
; constant restores the dispatch the rest of the case
; body was already coded against.
Case LanguageString$(LS_SCGMSay)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Pa$ = Chr$(254) + "<GM> <" + AI\Name$ + "> " + Params$
; Online-player chain walk; DM filter still
; needs the Account lookup.
A2.ActorInstance = FirstOnlinePlayer
While A2 <> Null
A.Account = Object.Account(A2\Account)
If A <> Null And A\IsDM = True Then RCE_Send(Host, A2\RNID, P_ChatMessage, Pa$, True)
A2 = A2\NextOnlinePlayer
Wend
EndIf
Case LanguageString$(LS_SCGuildSay)
If AI\TeamID > 0
Pa$ = Chr$(251) + "<G> <" + AI\Name$ + "> " + Params$
; Online-player chain walk; team filter still
; needed.
A2.ActorInstance = FirstOnlinePlayer
While A2 <> Null
If A2\TeamID = AI\TeamID Then RCE_Send(Host, A2\RNID, P_ChatMessage, Pa$, True)
A2 = A2\NextOnlinePlayer
Wend
EndIf
Case LanguageString$(LS_SCPartySay)
Party.Party = Object.Party(AI\PartyID)
If Party <> Null
Pa$ = Chr$(251) + "<PARTY> <" + AI\Name$ + "> " + Params$
For i = 0 To 7
If Party\Player[i] <> Null
If Party\Player[i] <> AI Then RCE_Send(Host, Party\Player[i]\RNID, P_ChatMessage, Pa$, True)
EndIf
Next
EndIf
Case LanguageString$(LS_SCPMSay)
Name$ = Upper$(Split$(Params$, 1, ","))
Params$ = Split$(Params$, 2, ",")
; Online-player chain walk for /pm target lookup.
A2.ActorInstance = FirstOnlinePlayer
While A2 <> Null
If Upper$(A2\Name$) = Name$
If PlayerIgnoring(A2, AI) = 0
RCE_Send(Host, A2\RNID, P_ChatMessage, Chr$(252) + AI\Name$ + ": " + Params$, True)
EndIf
Exit
EndIf
A2 = A2\NextOnlinePlayer
Wend
Case LanguageString$(LS_SCTrade)
; Player has been offered a trade and is accepting
If AI\IsTrading = 3
AI\IsTrading = 4
; Character to trade with has been deleted
If AI\TradingActor = Null
AI\IsTrading = 0
Goto OfferTrade
; Or left the game
ElseIf AI\TradingActor\RNID < 1
AI\IsTrading = 0
Goto OfferTrade
; Both players are here and have accepted
ElseIf AI\TradingActor\IsTrading = 4
RCE_Send(Host, AI\RNID, P_OpenTrading, "11P", True)
RCE_Send(Host, AI\TradingActor\RNID, P_OpenTrading, "11P", True)
EndIf
; Not currently trading, offer a trade
ElseIf AI\IsTrading = 0
.OfferTrade
If Params$ <> ""
A2.ActorInstance = FindPlayerFromName.ActorInstance(Params$)
If A2 <> Null
If A2\RNID > 0
If A2\IsTrading = 0
If PlayerIgnoring(A2, AI) = 0
AI\IsTrading = 4
A2\IsTrading = 3
AI\TradingActor = A2
A2\TradingActor = AI
Pa$ = LanguageString$(LS_TradeInviteInstruction)
ml = Len(Chr$(254) + LanguageString$(LS_TradeInvite) + " " + AI\Name$ + Pa$)
RCE_Send(Host, A2\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_TradeInvite) + " " + AI\Name$ + Pa$, True)
EndIf
EndIf
Else
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + Params$ + " " + LanguageString$(LS_XIsOffline), True)
EndIf
Else
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_PlayerNotFound) + " " + Params$, True)
EndIf
EndIf
EndIf
Case LanguageString$(LS_SCAllPlayers)
; Count via online-player chain walk. The
; chain only contains RNID > 0 players, so the
; previous `If A2\RNID > 0` filter is redundant.
Players = 0
A2.ActorInstance = FirstOnlinePlayer
While A2 <> Null
Players = Players + 1
A2 = A2\NextOnlinePlayer
Wend
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_PlayersInGame) + " " + Str$(Players - 1), True)
Case LanguageString$(LS_SCPlayers)
Players = 0
AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then Players = Players + 1
A2 = A2\NextInZone
Wend
EndIf
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_PlayersInZone) + " " + Str$(Players - 1), True)
Case LanguageString$(LS_SCWarp)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Ar.Area = FindArea(Trim$(Split$(Params$, 1, ",")))
If Ar <> Null
Instance = Split$(Params$, 2, ",")
For i = 0 To 99
If Ar\PortalName$[i] <> ""
SetArea(AI, Ar, Instance, -1, i)
Exit
EndIf
Next
EndIf
EndIf
Case LanguageString$(LS_SCWarpOther)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Name$ = Upper$(Trim$(Split$(Params$, 1, ",")))
; Online-player chain walk for /warpother
; target lookup.
A2.ActorInstance = FirstOnlinePlayer
While A2 <> Null
If Upper$(A2\Name$) = Name$
Ar.Area = FindArea(Trim$(Split$(Params$, 2, ",")))
; Sibling LS_SCWarp branch above
; checks `Ar <> Null` before the
; portal loop; this one didn't,
; so a DM typo (/warpother Alice,
; "Bad Name") crashed the entire
; server.
If Ar <> Null
Instance = Split$(Params$, 3, ",")
For i = 0 To 99
If Ar\PortalName$[i] <> ""
SetArea(A2, Ar, Instance, -1, i)
Exit
EndIf
Next
EndIf
Exit
EndIf
A2 = A2\NextOnlinePlayer
Wend
EndIf
Case LanguageString$(LS_SCAbility)
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
Params$ = Upper$(Params$)
Name$ = Trim$(SafeSplit$(Params$, 1, ","))
Level = Trim$(SafeSplit$(Params$, 2, ","))
For Sp.Spell = Each Spell
If Upper$(Sp\Name$) = Name$ Then AddSpell(AI, Sp\ID, Level) : Exit
Next
EndIf
Case LanguageString$(LS_SCGive)
; Make sure it's a GM account
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
; Find the requested item
Params$ = Upper$(Params$)
For It.Item = Each Item
If Upper$(It\Name$) = Params$
; Create the item
II.ItemInstance = CreateItemInstance(It)
II\Assignment = 1
II\AssignTo = AI
; Ask client to specify a slot to put it in
Pa$ = RCE_StrFromInt$(It\ID, 2) + RCE_StrFromInt$(II\Assignment, 2)
ml = Len("G" + RCE_StrFromInt$(Handle(II), 4) + Pa$)
RCE_Send(Host, AI\RNID, P_InventoryUpdate, "G" + RCE_StrFromInt$(Handle(II), 4) + Pa$, True)
Exit
EndIf
Next
EndIf
Case LanguageString$(LS_SCWeather)
Params$ = Trim$(Upper$(Params$))
; Make sure it's a GM account
A.Account = Object.Account(AI\Account)
If A <> Null And A\IsDM = True
AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea)
; Choose new weather
Select Params$
Case "SUN", "SUNNY", "NORMAL"
AInstance\CurrentWeather = W_Sun
Case "RAIN", "RAINY"
AInstance\CurrentWeather = W_Rain
Case "SNOW", "SNOWY"
AInstance\CurrentWeather = W_Snow
Case "FOG", "FOGGY"
AInstance\CurrentWeather = W_Fog
Case "WIND", "WINDY"
AInstance\CurrentWeather = W_Wind
Case "STORM", "STORMY", "THUNDER", "LIGHTNING"
AInstance\CurrentWeather = W_Storm
End Select
AInstance\CurrentWeatherTime = Rand(2500, 10000)
; Tell players in this area
Pa$ = RCE_StrFromInt$(Handle(AInstance), 4) + RCE_StrFromInt$(AInstance\CurrentWeather, 1)
AI.ActorInstance = AInstance\FirstInZone
While AI <> Null
If AI\RNID > 0 Then RCE_Send(Host, AI\RNID, P_WeatherChange, Pa$, True)
AI = AI\NextInZone
Wend
; Force an update for all areas with weather linked to this area
If AInstance\ID = 0
For Ar.Area = Each Area
If Ar\WeatherLinkArea = AInstance\Area
For i = 0 To 99
If Ar\Instances[i] <> Null
Ar\Instances[i]\CurrentWeatherTime = 0
EndIf
Next
EndIf
Next
EndIf
EndIf
Case LanguageString$(LS_SCTime)
Hour$ = Str$(TimeH)
If Len(Hour$) = 1 Then Hour$ = "0" + Hour$
Minute$ = Str$(TimeM)
If Len(Minute$) = 1 Then Minute$ = "0" + Minute$
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Time: " + Hour$ + ":" + Minute$, True)
Case LanguageString$(LS_SCDate)
Month = GetMonth()
If Month = 0
Date$ = Str$(Day + 1)
Else
Date$ = Str$((Day - MonthStartDay(Month)) + 1)
EndIf
If Right$(Date$, 1) = "1" And Date$ <> "11"
Date$ = Date$ + "st"
ElseIf Right$(Date$, 1) = "2" And Date$ <> "12"
Date$ = Date$ + "nd"
ElseIf Right$(Date$, 1) = "3" And Date$ <> "13"
Date$ = Date$ + "rd"
Else
Date$ = Date$ + "th"
EndIf
Date$ = Chr$(254) + MonthName$(Month) + " " + Date$ + ", " + Str$(Year)
RCE_Send(Host, AI\RNID, P_ChatMessage, Date$, True)
Case LanguageString$(LS_SCSeason)
ml = Len(Chr$(254) + LanguageString$(LS_Season) + " " + SeasonName$(GetSeason()))
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + LanguageString$(LS_Season) + " " + SeasonName$(GetSeason()), True)
; In-game discoverability for the 30-command slash
; system. English literals ("HELP" and "?") are
; intentional pending the LS_SCHelp localization
; entry -- the existing LanguageString$ table is
; line-numbered and adding a new ID requires
; touching Language.txt; deferred to a follow-up.
Case "HELP", "?"
SendChatHelp(AI, Params$)
Default
; Unknown command. Hand off to the project's
; "In-game Commands" user script if it exists;
; otherwise tell the player the command isn't
; recognised. Without this, an unknown command
; silently no-op'd and players had no signal.
If ScriptExists%("In-game Commands") = True
ThreadScript("In-game Commands", Command$, Handle(AI), 0, Params$)
Else
RCE_Send(Host, AI\RNID, P_ChatMessage, Chr$(254) + "Unknown command: /" + Lower$(Command$) + ". Type /help for a list.", True)
EndIf
End Select
; General chat - forward to other people in same area
Else
Pa$ = "<" + AI\Name$ + "> " + M\MessageData$
AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea)
If AInstance <> Null
A2.ActorInstance = AInstance\FirstInZone
While A2 <> Null
If A2\RNID > 0 Then RCE_Send(Host, A2\RNID, P_ChatMessage, Pa$, True)
A2 = A2\NextInZone
Wend
EndIf
If AInstance <> Null And AInstance\Area = GameArea
AddListBoxItem(Game\ChatText, Pa$ + Chr$(13))
If ChatLoggingMode > 0 Then WriteLog(ChatLog, Pa$, True, True)
ElseIf ChatLoggingMode = 2
WriteLog(ChatLog, Pa$, True, True)
EndIf
EndIf
EndIf
; Repositioning an actor (client has completed)
Case P_RepositionActor
AI.ActorInstance = FindActorInstanceFromRNID(M\FromID)
If AI <> Null
AI\IgnoreUpdate = 0
EndIf
; Client has completed zoning for a player
Case P_ChangeArea
AI.ActorInstance = FindActorInstanceFromRNID(M\FromID)
If AI <> Null
AI\IgnoreUpdate = 0
EndIf
; Scenery item selected {##}
;Case P_SelectScenery
; AI.ActorInstance = FindActorInstanceFromRNID(M\FromID)
; If AI <> Null
; ID = RCE_IntFromStr(Left$(M\MessageData$, 2))
; If ID > 0 And ID <= 500
; AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea)
; If AInstance\OwnedScenery[ID - 1] <> Null
; A.Account = Object.Account(AI\Account)
; If AInstance\OwnedScenery[ID - 1]\AccountName$ = A\User$
; If AInstance\OwnedScenery[ID - 1]\CharNumber = A\LoggedOn
; ; Allow scenery animation
; RCE_Send(Host, AI\RNID, P_SelectScenery, Right$(M\MessageData$, 4), True)
;
; ; Open trading window
; If AI\IsTrading = 0 And AInstance\OwnedScenery[ID - 1]\InventorySize > 0
; AI\IsTrading = 1
; AI\TradeResult$ = RCE_StrFromInt$(ID - 1, 2) + RCE_StrFromInt$(Handle(Ar), 4)
; Pa$ = "S"
; For i = 0 To AInstance\OwnedScenery[ID - 1]\InventorySize - 1
; If AInstance\OwnedScenery[ID - 1]\Inventory\Amounts[i] > 0 And AInstance\OwnedScenery[ID - 1]\Inventory\Items[i] <> Null
; CopiedII.ItemInstance = CopyItemInstance(AInstance\OwnedScenery[ID - 1]\Inventory\Items[i])
; CopiedII\Assignment = AInstance\OwnedScenery[ID - 1]\Inventory\Amounts[i]
; CopiedII\AssignTo = AI
; Pa$ = Pa$ + ItemInstanceToString$(AInstance\OwnedScenery[ID - 1]\Inventory\Items[i])
; Pa$ = Pa$ + RCE_StrFromInt$(AInstance\OwnedScenery[ID - 1]\Inventory\Amounts[i], 2) + RCE_StrFromInt$(Handle(CopiedII), 4)
; EndIf
; Next
; If Len(Pa$) < 999
; RCE_Send(Host, AI\RNID, P_OpenTrading, "11" + Pa$, True)
; ElseIf Len(Pa$) < 1998
; SendQueued(Host, AI\RNID, P_OpenTrading, "12" + Left$(Pa$, 998), True)
; SendQueued(Host, AI\RNID, P_OpenTrading, "22" + Mid$(Pa$, 999), True)
; Else
; SendQueued(Host, AI\RNID, P_OpenTrading, "13" + Left$(Pa$, 998), True)
; SendQueued(Host, AI\RNID, P_OpenTrading, "23" + Mid$(Pa$, 999, 998), True)
; SendQueued(Host, AI\RNID, P_OpenTrading, "33" + Mid$(Pa$, 1998), True)
; EndIf
; EndIf
; EndIf
; EndIf
; EndIf
; EndIf
; EndIf
; Update player -> player trading
Case P_UpdateTrading
AI.ActorInstance = FindActorInstanceFromRNID(M\FromID)
If AI <> Null
; Reject self-trade: if AI\TradingActor somehow points
; back at AI (script bug, logout race, or wire injection)
; the slot loop below aliases source = destination and
; the accept path later tries to consume the same slot
; twice. Also require reciprocal binding: A's partner
; must claim A as its own partner -- otherwise an
; injection that sets only one side's TradingActor
; would let A drain inventory into a one-way channel.
If AI\IsTrading = 4 And AI\TradingActor <> Null And AI\TradingActor <> AI And AI\TradingActor\TradingActor = AI
Slot = RCE_IntFromStr(Left$(M\MessageData$, 1))
Amount = RCE_IntFromStr(Mid$(M\MessageData$, 2, 2))
; Slot is a backpack-relative offset (0..31). Reject anything that
; would push the array index past the inventory bounds; without this,
; a crafted Slot reads (and the ItemInstanceToString$ helper writes)
; from adjacent ActorInstance fields.
If Slot >= 0 And Slot <= 31 And Slot + SlotI_Backpack <= Slots_Inventory
; Record the server-authoritative offer state.
; The accept-side swap reads from this rather than
; from the client-controlled accept-packet bytes;
; that closes the dupe where the client's accept
; payload claims a different stack than what the
; trade UI showed via P_UpdateTrading.
If Amount > AI\Inventory\Amounts[Slot + SlotI_Backpack]
Amount = AI\Inventory\Amounts[Slot + SlotI_Backpack]
EndIf
If Amount < 0 Then Amount = 0
AI\TradeOfferedAmount[Slot] = Amount
Pa$ = M\MessageData$
If Amount > 0 Then Pa$ = Pa$ + ItemInstanceToString$(AI\Inventory\Items[Slot + SlotI_Backpack])
RCE_Send(Host, AI\TradingActor\RNID, P_UpdateTrading, Pa$, True)
EndIf
EndIf
EndIf
; Trading complete
Case P_OpenTrading
AI.ActorInstance = FindActorInstanceFromRNID(M\FromID)
If AI <> Null
; Accept trading
If Len(M\MessageData$) > 0
; Player -> NPC or player -> scenery trading {##}
If AI\IsTrading = 1
AI\IsTrading = 0
Change = 0
If AI\TradeResult$ <> ""
ID = RCE_IntFromStr(Left$(AI\TradeResult$, 2))
AInstance.AreaInstance = Object.AreaInstance(AI\ServerArea)
;Owned.OwnedScenery = AInstance\OwnedScenery[ID]
EndIf
;Sold items
Offset = 193
For i = 0 To 31
SlotID = RCE_IntFromStr(Mid$(M\MessageData$, Offset, 1))
Amount = RCE_IntFromStr(Mid$(M\MessageData$, Offset + 1, 2))
Offset = Offset + 3
; Upper-bound SlotID against the inventory size — original check
; only rejected SlotID <= 0, so a crafted shop-sell packet with
; SlotID in the byte range past Slots_Inventory wrote into
; whatever ActorInstance fields follow the inventory array.
If SlotID > 0 And SlotID <= Slots_Inventory
If Amount > AI\Inventory\Amounts[SlotID] Then Amount = AI\Inventory\Amounts[SlotID]
If AI\Inventory\Items[SlotID] <> Null And Amount > 0
; Alter cost {##}
;If Owned = Null
Change = Change + (AI\Inventory\Items[SlotID]\Item\Value * Amount)
; Add item to scenery inventory if applicable
; Else
; For j = 0 To Owned\InventorySize - 1
; If Owned\Inventory\Items[j] = Null
; Owned\Inventory\Items[j] = CopyItemInstance(AI\Inventory\Items[SlotID])
; Owned\Inventory\Amounts[j] = Amount
; Exit
; ElseIf ItemInstancesIdentical(AI\Inventory\Items[SlotID], Owned\Inventory\Items[j])
; Owned\Inventory\Amounts[j] = Owned\Inventory\Amounts[j] + Amount
; Exit
; EndIf
; Next
; EndIf
; Remove item
AI\Inventory\Amounts[SlotID] = AI\Inventory\Amounts[SlotID] - Amount
If AI\Inventory\Amounts[SlotID] <= 0 Then FreeItemInstance(AI\Inventory\Items[SlotID])
; Tell player if required
If AI\RNID > 0
Pa$ = RCE_StrFromInt$(SlotID, 1) + RCE_StrFromInt$(Amount, 2)
RCE_Send(Host, AI\RNID, P_InventoryUpdate, "T" + Pa$, True)
EndIf
EndIf
EndIf
Next
; ; Bought items
Offset = 1
For i = 0 To 31
II.ItemInstance = Object.ItemInstance(RCE_IntFromStr(Mid$(M\MessageData$, Offset, 4)))
Amount = RCE_IntFromStr(Mid$(M\MessageData$, Offset + 4, 2))
Offset = Offset + 6
If II <> Null And Amount > 0
If II\Assignment > 0 And II\AssignTo = AI
If Amount < II\Assignment Then II\Assignment = Amount
; Alter cost {##}
; If Owned = Null
OldChange = Change
Change = Change - (II\Item\Value * II\Assignment)
;Remove from scenery inventory if applicable
; Else
; RemoveAmount = II\Assignment
; For j = 0 To Owned\InventorySize - 1
; If Owned\Inventory\Items[j] <> Null
; If ItemInstancesIdentical(II, Owned\Inventory\Items[j])
; If Owned\Inventory\Amounts[j] >= RemoveAmount
; Owned\Inventory\Amounts[j] = Owned\Inventory\Amounts[j] - RemoveAmount
; RemoveAmount = 0
; If Owned\Inventory\Amounts[j] = 0 Then FreeItemInstance(Owned\Inventory\Items[j])
; Else
; RemoveAmount = RemoveAmount - Owned\Inventory\Amounts[j]
; Owned\Inventory\Amounts[j] = 0
; FreeItemInstance(Owned\Inventory\Items[j])
; EndIf
; If RemoveAmount = 0 Then Exit
; EndIf
; EndIf
; Next
;EndIf
; Prevent cheating
If AI\Gold + Change >= 0
;Ask client to specify a slot to put it in
Pa$ = RCE_StrFromInt$(II\Item\ID, 2) + RCE_StrFromInt$(II\Assignment, 2)
RCE_Send(Host, AI\RNID, P_InventoryUpdate, "G" + RCE_StrFromInt$(Handle(II), 4) + Pa$, True)
Else
Change = OldChange
FreeItemInstance(II)
Exit
EndIf
EndIf
EndIf
Next
; Adjust gold level
;If Owned = Null
If Change <> 0
AI\Gold = AI\Gold + Change
If Change > 0
Pa$ = "U" + RCE_StrFromInt$(Change, 4)
Else
Pa$ = "D" + RCE_StrFromInt$(Abs(Change), 4)
EndIf
RCE_Send(Host, AI\RNID, P_GoldChange, Pa$, True)
EndIf
;EndIf
; Player -> player trading
ElseIf AI\IsTrading = 4
AI\IsTrading = 5
AI\TradeResult$ = M\MessageData$
; Character to trade with has been deleted
If AI\TradingActor = Null
AI\TradeResult$ = ""
AI\IsTrading = 0
RCE_Send(Host, AI\RNID, P_CloseTrading, "", True)
; Or left the game
ElseIf AI\TradingActor\RNID < 1
AI\TradeResult$ = ""
AI\IsTrading = 0
AI\TradingActor = Null
RCE_Send(Host, AI\RNID, P_CloseTrading, "", True)
; Both players are here and have accepted
ElseIf AI\TradingActor\IsTrading = 5
A2.ActorInstance = AI\TradingActor
; Server-authoritative cost check: both clients must report the same gold
; flow magnitude in their accept packet. The historical per-item validation
; block (still commented below) produced false negatives in practice; the
; per-slot swap loop further down already clamps Amount to the actual
; source-side inventory so item dupe via Amount inflation is blocked there.
; This cost check closes the gold-side scam where one party lied about the
; agreed amount.
Valid = True
A1Cost = RCE_IntFromStr(Left$(AI\TradeResult$, 4)) * -1
A2Cost = RCE_IntFromStr(Left$(A2\TradeResult$, 4))
If A1Cost <> A2Cost Then Valid = False
If A2Cost > 0 And A2Cost > A2\Gold Then Valid = False
If A2Cost < 0 And ( - A2Cost ) > AI\Gold Then Valid = False
; Historical per-item validation (kept commented for future repair —
; produced false negatives because the slot/amount packet offsets it
; assumed don't match what the live client sends).
; For i = 0 To 31
; A1SoldAmount = RCE_IntFromStr(Mid$(AI\TradeResult$, 101 + (i * 2), 2))
; If A1SoldAmount > 0
; For j = 0 To 31
; If RCE_IntFromStr(Mid$(A2\TradeResult$, 5 + (j * 3), 1)) = i
; Amount = RCE_IntFromStr(Mid$(A2\TradeResult$, 6 + (j * 3), 2))
; If Amount <> A1SoldAmount Then Valid = False
; Exit
; EndIf
; Next
; EndIf
; A2SoldAmount = RCE_IntFromStr(Mid$(A2\TradeResult$, 101 + (i * 2), 2))
; If A2SoldAmount > 0
; For j = 0 To 31
; If RCE_IntFromStr(Mid$(AI\TradeResult$, 5 + (j * 3), 1)) = i
; Amount = RCE_IntFromStr(Mid$(AI\TradeResult$, 6 + (j * 3), 2))
; If Amount <> A2SoldAmount Then Valid = False
; Exit
; EndIf
; Next
; EndIf
; Next
If Valid = True
; Swap money
AI\Gold = AI\Gold + A2Cost
A2\Gold = A2\Gold - A2Cost
If A2Cost > 0