-
Notifications
You must be signed in to change notification settings - Fork 3
/
Simulator.lua
1762 lines (1363 loc) · 52.3 KB
/
Simulator.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Simulator.lua
-- Implements the Cuberite simulator
--[[
Usage:
local sim = require("Simulator").create()
-- Possibly use sim as an upvalue in special API function implementations
-- Extend the simulator's sandbox etc.
sim:run(options, api)
The simulator keeps a sandbox for the plugin files, lists of registered objects (hooks, command handlers,
simulated players and worlds etc.) and a queue of requests to call the plugin's methods. It can also directly
call any callback in the plugin. The Scenario uses this functionality to implement the various actions.
Use the processCallbackRequest() function to call into the plugin.
Use the queueCallbackRequest() function to add a call into the plugin into a queue.
Use the processAllQueuedCallbackRequests() function to process the callback queue until it is empty.
The callback request is a table containing the following members:
- Function - function in the plugin to call
- ParamValues - array of values that get passed as parameters. If not present, ParamTypes is used instead.
- ParamTypes - array of type descriptions (tables with {Type = <>}) from which the parameters are synthesized. Only used if ParamValues not present.
- Notes - string description of the callback (used for logging)
- AllowsStore - optional dictionary of index -> true, parameters at specified indices may be stored by the callback
--]]
--- Compatibility with both Lua 5.1 and LuaJit
-- (generates a LuaCheck 143 warning - Accessing undefined field of a table)
-- luacheck: push ignore 143
local unpack = unpack or table.unpack
-- luacheck: pop
--- Use the Scenario loading library:
local Scenario = dofile("Scenario.lua")
--- The protocol number to use for things that report client protocol
local PROTOCOL_NUMBER = 210 -- client version 1.10.0
--- The class (metatable) to be used for all simulators
local Simulator = {}
Simulator["__index"] = Simulator
--- Adds a new callback request to the queue to be processed
-- a_ParamTypes is an array of strings describing the param types
-- a_Notes is a description of the request for logging purposes
-- a_ParamValues is an optional array of params' values
function Simulator:addCallbackRequest(a_FnToCall, a_ParamTypes, a_Notes, a_ParamValues)
-- Check params:
assert(self)
assert(type(a_FnToCall) == "function")
local paramTypes = a_ParamTypes or {}
assert(type(paramTypes) == "table")
-- Add the request to the queue:
local n = self.callbackRequests.n + 1
self.callbackRequests[n] = { Function = a_FnToCall, ParamTypes = paramTypes, ParamValues = a_ParamValues, Notes = a_Notes }
self.callbackRequests.n = n
-- Call the notification callback:
self:callHooks(self.hooks.onAddedRequest)
end
--- Adds a CallbackRequest to call the specified command with the specified CommandSplit
-- a_Handler is the command's registered callback function
-- a_CommandSplit is an array-table of strings used as the splitted command
-- a_PlayerInstance is an optional cPlayer instance representing the player. If not given, a dummy one is created
function Simulator:addCommandCallbackRequest(a_Handler, a_CommandSplit, a_PlayerInstance)
local player = a_PlayerInstance or self:createInstance({Type = "cPlayer"})
local entireCmd = table.concat(a_CommandSplit, " ")
self:addCallbackRequest(a_Handler, nil, string.format("command \"%s\"", entireCmd), { a_CommandSplit, player, entireCmd })
end
--- Adds the hooks to the simulator, based on the options specified by the user
function Simulator:addHooks(a_Options)
if (a_Options.shouldClearObjects) then
table.insert(self.hooks.onBeforeCallCallback, Simulator.beforeCallClearObjects)
table.insert(self.hooks.onAfterCallCallback, Simulator.afterCallClearObjects)
end
if (a_Options.shouldGCObjects) then
table.insert(self.hooks.onBeforeCallCallback, Simulator.beforeCallGCObjects)
table.insert(self.hooks.onAfterCallCallback, Simulator.afterCallGCObjects)
end
end
--- Adds the specified file / folder redirections
-- The previous redirects are kept
function Simulator:addRedirects(a_Redirects)
-- Check params:
assert(self)
assert(type(a_Redirects) == "table")
-- Add the redirects:
for orig, new in pairs(a_Redirects) do
self.redirects[orig] = new
end
end
--- Adds the specified file / folder redirection.
-- Only redirects files inside the plugin folder.
function Simulator:addRedirectPluginFiles(a_Redirects)
assert(self)
assert(type(a_Redirects) == "table")
-- Add the redirects.
for orig, new in pairs(a_Redirects) do
table.insert(self.redirectPluginFiles, {
original = self.options.pluginPath .. "/" .. orig,
new = self.options.scenarioPath .. "/" .. new
})
end
end
--- Called by the simulator after calling the callback, when the ClearObjects is specified in the options
-- a_Params is an array-table of the params that were given to the callback
-- a_Returns is an array-table of the return values that the callback returned
function Simulator:afterCallClearObjects(a_Request, a_Params, a_Returns)
-- Check params:
assert(self)
assert(a_Params)
-- Change the objects in parameters so that any access to them results in an error:
local requestNotes = a_Request.Notes
for idx, param in ipairs(a_Params) do
if ((type(param) == "userdata") and not(a_Request.AllowsStore[idx])) then
getmetatable(param).__index = function()
self.logger:error(3, "Attempting to use an object that has been stored from a callback %q.", requestNotes)
end
end
end
end
--- Called by the simulator after calling the callback, when the GCObjects is specified in the options
-- a_Params is an array-table of the params that were given to the callback
-- a_Returns is an array-table of the return values that the callback returned
function Simulator:afterCallGCObjects(a_Request, a_Params, a_Returns)
-- Check params:
assert(self)
assert(a_Params)
-- Remove the references to the parameters:
for idx, param in ipairs(a_Params) do
if (type(param) == "userdata") then
a_Params[idx] = nil
end
end
-- Collect garbage, check if all the parameter references have been cleared:
collectgarbage()
for idx, t in pairs(a_Request.uncollectedParams) do
local info = debug.getinfo(a_Request.Function, "S")
if (info.source and string.sub(info.source, 1, 1) == "@") then
self.logger:error(1,
"Plugin has stored an instance of param #%d (%s) from callback %q for later reuse. Function defined in %s, lines %s - %s",
idx, t, a_Request.Notes,
string.sub(info.source, 2), info.linedefined or "<unknown>", info.lastlinedefined or "<unknown>"
)
else
self.logger:error(1,
"Plugin has stored an instance of param #%d (%s) from callback %q for later reuse. Function definition not found.",
idx, t, a_Request.Notes
)
end
end
end
--- Called by the simulator after calling the callback, when the ClearObjects is specified in the options
-- a_Request is a table describing the entire callback request
-- a_Params is an array-table of the params that are to be given to the callback
function Simulator:beforeCallClearObjects(a_Request, a_Params)
-- Nothing needed
end
--- Called by the simulator before calling the callback, when the GCObjects is specified in the options
-- a_Params is an array-table of the params that are to be given to the callback
function Simulator:beforeCallGCObjects(a_Request, a_Params)
-- We need to create a duplicate of the parameters, because they might still be stored somewhere below on the stack
-- which would interfere with the GC
a_Request.ParamValues = self:duplicateInstances(a_Params)
a_Params = a_Request.ParamValues
-- Make note of all the parameters and whether they are GCed:
a_Request.uncollectedParams = {}
for idx, param in ipairs(a_Params) do
local t = type(param)
if ((t == "userdata") and not (a_Request.AllowsStore[idx])) then
local paramType = self:typeOf(param)
a_Request.uncollectedParams[idx] = paramType
local mt = getmetatable(param)
local oldGC = mt.__gc
mt.__gc = function(...)
self.logger:trace("GCing param #%d (%s) of request %p", idx, paramType, a_Request.Notes)
a_Request.uncollectedParams[idx] = nil
if (oldGC) then
oldGC(...)
end
end
end
end
end
--- Calls all the hooks in the specified table with any params
function Simulator:callHooks(a_Hooks, ...)
-- Check params:
assert(type(a_Hooks) == "table")
-- Call the hooks:
for _, hook in ipairs(a_Hooks) do
hook(self, ...)
end
end
--- Checks whether the given parameters match the given function signature
-- Assumes that the parameter counts are the same (checked by the caller)
-- a_FnSignature is an array-table of params, as given by the API description
-- a_Params is an array-table of parameters received from the plugin
-- a_NumParams is the number of parameters in a_Params (specified separately due to possible nil parameters)
-- a_ClassName is the name of the class in which the function resides, or nil if the function is a global
-- Returns true if the signature matches, false and optional error message on mismatch
function Simulator:checkClassFunctionSignature(a_FnSignature, a_Params, a_NumParams, a_ClassName)
-- Check params:
assert(type(a_FnSignature) == "table")
assert(type(a_Params) == "table")
assert(type(a_NumParams) == "number")
-- If the function is in a class, check the "self" param:
local paramOffset = 0
if (a_ClassName) then
paramOffset = 1
if (a_FnSignature.IsStatic) then
-- For a static function, the first param should be the class itself:
if (type(a_Params[1]) ~= "table") then
return false, "The \"self\" parameter is not a class (table)"
end
local mt = getmetatable(a_Params[1])
if not(mt) then
return false, "The \"self\" parameter is not a class (metatable)"
end
if not(rawget(a_Params[1], "simulatorInternal_ClassName")) then
return false, "The \"self\" parameter is not a Cuberite class"
end
if not(self:classInheritsFrom(a_Params[1].simulatorInternal_ClassName, a_ClassName)) then
return false, string.format(
"The \"self\" parameter is a different class. Expected %s, got %s.",
a_ClassName, a_Params[1].simulatorInternal_ClassName
)
end
else
-- For a non-static function, the first param should be an instance of the class:
if (type(a_Params[1]) ~= "userdata") then
return false, "The \"self\" parameter is not a class (userdatum)"
end
local classMT = getmetatable(a_Params[1])
if not(classMT) then
return false, "The \"self\" parameter is not a class instance (class metatable)"
end
if not(rawget(classMT.__index, "simulatorInternal_ClassName")) then
return false, "The \"self\" parameter is not a Cuberite class instance"
end
if not(self:classInheritsFrom(classMT.__index.simulatorInternal_ClassName, a_ClassName)) then
return false, string.format(
"The \"self\" parameter is a different class instance. Expected %s, got %s.",
a_ClassName, classMT.__index.simulatorInternal_ClassName
)
end
end
end
-- Check the type of each plugin parameter:
for idx = paramOffset + 1, a_NumParams do
local signatureParam = a_FnSignature.Params[idx - paramOffset]
if not(signatureParam) then
return false, string.format("There are more parameters (%d) than in the signature (%d)", a_NumParams - paramOffset, idx - paramOffset - 1)
end
local param = a_Params[idx]
if ((param ~= nil) or not(signatureParam.IsOptional)) then -- Optional param always matches a nil
local paramType = self:typeOf(param)
if not(self:paramTypesMatch(paramType, signatureParam.Type)) then
return false, string.format("Param #%d doesn't match, expected %s, got %s",
idx - paramOffset, signatureParam.Type, paramType
)
end
end
end
-- All given params have matched, now check that all the leftover params in the signature are optional:
local idx = a_NumParams + 1
while (a_FnSignature.Params[idx]) do
if not(a_FnSignature.Params[idx].IsOptional) then
return false, string.format("Param #d (%s) is missing.", idx, a_FnSignature.Params[idx].Type)
end
idx = idx + 1
end
-- All params have matched
return true
end
--- Checks the inheritance tree
-- Returns true if class "a_ChildName" inherits from class "a_ParentName" (or they are the same)
function Simulator:classInheritsFrom(a_ChildName, a_ParentName)
-- Check params:
assert(self)
assert(type(a_ChildName) == "string")
assert(type(a_ParentName) == "string")
-- If they are the same class, consider them inheriting:
if (a_ChildName == a_ParentName) then
return true
end
-- Check the inheritance using the child class API:
local childClass = self.sandbox[a_ChildName]
if not(childClass) then
self.logger:warning("Attempting to check inheritance for non-existent class \"%s\".\n%s", a_ChildName, debug.traceback())
return false
end
local childApi = childClass.simulatorInternal_ClassApi or {}
for _, parent in ipairs(childApi.Inherits or {}) do
if (self:classInheritsFrom(parent, a_ParentName)) then
return true
end
end
-- None of the inherited classes matched
return false
end
--- Removes all state information previously added through a scenario
-- Removes worlds, players
function Simulator:clearState()
-- Check params:
assert(self)
self.worlds = {}
self.players = {}
end
--- Collapses the relative parts of the path, such as "folder/../"
function Simulator:collapseRelativePath(a_Path)
-- Check params:
assert(type(a_Path) == "string")
-- Split the path on each "/" and rebuild without the relativeness:
local res = {}
local idx = 0
while (idx) do
local lastIdx = idx + 1
idx = a_Path:find("/", lastIdx)
local part
if not(idx) then
part = a_Path:sub(lastIdx)
else
part = a_Path:sub(lastIdx, idx - 1)
end
if (part == "..") then
if ((#res > 0) and (res[#res - 1] ~= "..")) then -- The previous part is not relative
table.remove(res)
else
table.insert(res, part)
end
else
table.insert(res, part)
end
end
return table.concat(res, "/")
end
--- Simulates a player joining the game
-- a_PlayerDesc is a dictionary-table describing the player (name, worldName, gameMode, ip)
-- "name" is compulsory, the rest is optional
-- Calls all the hooks that are normally triggered for a joining player
-- If any of the hooks refuse the join, doesn't add the player and returns false
-- Returns true if the player was added successfully
function Simulator:connectPlayer(a_PlayerDesc)
-- Check params:
assert(self)
assert(type(a_PlayerDesc) == "table")
local playerName = a_PlayerDesc.name
assert(type(playerName) == "string")
assert(self.defaultWorldName, "No world in the simulator")
-- Create the player, with some reasonable defaults:
a_PlayerDesc.worldName = a_PlayerDesc.worldName or self.defaultWorldName
a_PlayerDesc.uniqueID = self:getNextUniqueID()
self.players[playerName] = a_PlayerDesc
-- Call the hooks to simulate the player joining:
local client = self:createInstance({Type = "cClientHandle"})
getmetatable(client).simulatorInternal_PlayerName = playerName
if (self:executeHookCallback("HOOK_LOGIN", client, PROTOCOL_NUMBER, playerName)) then
self.logger:trace("Plugin refused player \"%s\" to connect.", playerName)
self.players[playerName] = nil -- Remove the player
return false
end
self:executeHookCallback("HOOK_PLAYER_JOINED", self:getPlayerByName(playerName))
-- If the plugin kicked the player, abort:
if not(self.players[playerName]) then
self.logger:trace("Plugin kicked player \"%s\" while they were joining.", playerName)
return false
end
-- Spawn the player:
self:executeHookCallback("HOOK_PLAYER_SPAWNED", self:getPlayerByName(playerName))
if not(self.players[playerName]) then
self.logger:trace("Plugin kicked player \"%s\" while they were spawning.", playerName)
return false
end
return true
end
--- Creates an API endpoint (function, constant or variable) dynamically
-- a_ClassApi is the API description of the class or the Globals
-- a_SymbolName is the name of the symbol that is requested
-- a_ClassName is the name of the class (or "Globals") where the function resides; for logging purposes only
function Simulator:createApiEndpoint(a_ClassApi, a_SymbolName, a_ClassName)
-- CheckParams:
assert(self)
assert(type(a_ClassApi) == "table")
assert(a_ClassApi.Functions)
assert(a_ClassApi.Constants)
assert(a_ClassApi.Variables)
assert(type(a_SymbolName) == "string")
assert(type(a_ClassName) == "string")
-- Create the endpoint:
local res
if (a_ClassApi.Functions[a_SymbolName]) then
res = self:createClassFunction(a_ClassApi.Functions[a_SymbolName], a_SymbolName, a_ClassName)
elseif (a_ClassApi.Constants[a_SymbolName]) then
res = self:createClassConstant(a_ClassApi.Constants[a_SymbolName], a_SymbolName, a_ClassName)
elseif (a_ClassApi.Variables[a_SymbolName]) then
res = self:createClassVariable(a_ClassApi.Variables[a_SymbolName], a_SymbolName, a_ClassName)
end
if (res) then
return res
end
-- If not found, try to create it in the class parents:
for _, className in ipairs(a_ClassApi.Inherits or {}) do
res = self.sandbox[className][a_SymbolName]
if (res) then
return res
end
end
-- Endpoint not found:
return nil
end
--- Definitions for operators
-- This table pairs operators' APIDoc names with their Lua Meta-method names and names used for logging
local g_Operators =
{
{ docName = "operator_div", metaName = "__div", logName = "operator div" },
{ docName = "operator_eq", metaName = "__eq", logName = "operator eq" },
{ docName = "operator_minus", metaName = "__sub", logName = "operator minus" },
{ docName = "operator_minus", metaName = "__unm", logName = "operator unary-minus" },
{ docName = "operator_mul", metaName = "__mul", logName = "operator mul" },
{ docName = "operator_plus", metaName = "__add", logName = "operator plus" },
}
--- Creates a sandbox implementation of the specified API class
-- a_ClassName is the name of the class (used for error-reporting)
-- a_ClassApi is the class' API description
-- Returns a table that is to be stored in the sandbox under the class' name
function Simulator:createClass(a_ClassName, a_ClassApi)
-- Check params:
assert(self)
assert(type(a_ClassName) == "string")
assert(type(a_ClassApi) == "table")
assert(a_ClassApi.Functions)
assert(a_ClassApi.Constants)
assert(a_ClassApi.Variables)
self.logger:trace("Creating class \"%s\".", a_ClassName)
-- Create a metatable that dynamically creates the API endpoints, and stores info about the class:
local mt =
{
__index = function(a_Table, a_SymbolName)
if ((a_SymbolName == "__tostring") or (a_SymbolName == "__serialize")) then
-- Used by debuggers all the time, spamming the log. Bail out early.
return nil
end
self.logger:trace("Creating an API endpoint \"%s.%s\".", a_ClassName, a_SymbolName)
local endpoint = self:createApiEndpoint(a_ClassApi, a_SymbolName, a_ClassName)
if not(endpoint) then
self.logger:error(3, "Attempting to use a non-existent API: \"%s.%s\".", a_ClassName, a_SymbolName)
end
return endpoint
end,
}
-- If the class has a constructor, add it to the meta-table, because it doesn't go through the __index meta-method:
if (a_ClassApi.Functions.new) then
mt.__call = function (...)
self.logger:trace("Creating constructor for class %s.", a_ClassName)
local endpoint = self:createApiEndpoint(a_ClassApi, "new", a_ClassName)
if not(endpoint) then
self.logger:error(3, "Attempting to use a constructor for class %s that doesn't have one.", a_ClassName)
end
return endpoint(...)
end
end
if (a_ClassApi.Functions.constructor) then
mt.__call = function (...)
self.logger:trace("Creating constructor for class %s.", a_ClassName)
local endpoint = self:createApiEndpoint(a_ClassApi, "constructor", a_ClassName)
if not(endpoint) then
self.logger:error(3, "Attempting to use a constructor for class %s that doesn't have one.", a_ClassName)
end
return endpoint(...)
end
end
-- Add any operators to the class-table, because they don't go through the __index meta-method:
-- Also they apparently don't go through meta-table nesting, so need to create them directly in the class-table
local res = {}
res.__index = res
for _, op in ipairs(g_Operators) do
if (a_ClassApi.Functions[op.docName]) then
res[op.metaName] = function (...)
self.logger:trace("Creating %s for class %s", op.logName, a_ClassName)
local endpoint = self:createApiEndpoint(a_ClassApi, op.docName, a_ClassName)
if not(endpoint) then
self.logger:error(3, "Attempting to use %s for class %s that doesn't have one.", op.logName, a_ClassName)
end
return endpoint(...)
end
end
end
setmetatable(res, mt)
self.sandbox[a_ClassName] = res -- Store the class for the next time (needed at least for operator_eq)
res.simulatorInternal_ClassName = a_ClassName
res.simulatorInternal_ClassApi = a_ClassApi
return res
end
--- Creates a constant based on its API description
-- Provides a dummy value if no value is given
function Simulator:createClassConstant(a_ConstDesc, a_ConstName, a_ClassName)
-- Check params:
assert(self)
assert(type(a_ConstDesc) == "table")
assert(type(a_ConstName) == "string")
assert(type(a_ClassName) == "string")
-- If the value is specified, return it directly:
if (a_ConstDesc.Value) then
return a_ConstDesc.Value
end
-- Synthesize a dummy value of the proper type:
if not(a_ConstDesc.Type) then
self.logger:error(1, "Simulator error: API description for constant %s.%s doesn't provide value nor type", a_ClassName, a_ConstName)
end
return self:createInstance(a_ConstDesc)
end
--- Creates a variable based on its API description
function Simulator:createClassVariable(a_VarDesc, a_VarName, a_ClassName)
-- Check params:
assert(self)
assert(type(a_VarDesc) == "table")
assert(type(a_VarName) == "string")
assert(type(a_ClassName) == "string")
-- If the value is specified, return it directly:
if (a_VarDesc.Value) then
return a_VarDesc.Value
end
-- Synthesize a dummy value of the proper type:
return self:createInstance(a_VarDesc)
end
--- Creates a dummy API function implementation based on its API description:
function Simulator:createClassFunction(a_FnDesc, a_FnName, a_ClassName)
-- Check params:
assert(self)
assert(type(a_FnDesc) == "table")
assert(type(a_FnName) == "string")
assert(type(a_ClassName) == "string")
return function(...)
self.logger:trace("Calling function %s.%s.", a_ClassName, a_FnName)
local params = { ... }
self:callHooks(self.hooks.onApiFunctionCall, a_ClassName, a_FnName, params)
local signature, msgs = self:findClassFunctionSignatureFromParams(a_FnDesc, params, a_ClassName)
if not(signature) then
self.logger:error(3,
"Function %s.%s used with wrong parameters, there is no overload that can take these:\n\t%s\nMatcher messages:\n\t%s",
a_ClassName, a_FnName,
table.concat(self:listParamTypes(params), "\n\t"),
table.concat(msgs, "\n\t")
)
end
if (signature.Implementation) then
-- This function has a specific implementation, call it:
return signature.Implementation(self, ...)
else
-- Provide a default implementation by default-constructing the return values:
return unpack(self:createInstances(signature.Returns, params[1]))
end
end
end
--- Creates a single instance of a type - for a callback request or as an API function return value
function Simulator:createInstance(a_TypeDef)
-- Check params:
assert(self)
assert(type(a_TypeDef) == "table")
local t = a_TypeDef.Type
assert(t)
-- Remove modifiers:
t = t:gsub("const ", "")
-- If it is a built-in type, create it directly:
if (t == "string") then
self.testStringIndex = (self.testStringIndex + 1) % 5 -- Repeat the same string every 5 calls
return "TestString" .. self.testStringIndex
elseif (t == "number") then
self.testNumber = (self.testNumber + 1) % 5 -- Repeat the same number every 5 calls
return self.testNumber
elseif (t == "boolean") then
return true
elseif (t == "table") then
return { "testTable", testTable = "testTable" }
end
-- If it is a known enum, return a number:
local enumDef = self.enums[t]
if (enumDef) then
-- TODO: Choose a proper value for the enum
return 1
end
-- If it is a class param, create a class instance:
local classTable = self.sandbox[t]
if not(classTable) then
self.logger:error(2, "Requested an unknown param type for callback request: \"%s\".", t)
end
self.logger:trace("Created a new instance of %s", t)
local res = newproxy(true)
getmetatable(res).__index = classTable
assert(classTable.__index == classTable, string.format("Class %s is not properly injected", t))
return res
end
--- Creates all object instances required for a callback request
-- a_TypeDefList is an array of type descriptions ({Type = "string"})
-- a_Self is the value that should be returned whenever the type specifies "self" (used by chaining functions)
-- Returns the instances as an array-table
function Simulator:createInstances(a_TypeDefList, a_Self)
-- Check params:
assert(type(a_TypeDefList) == "table")
-- Create the instances:
local res = {}
for idx, td in ipairs(a_TypeDefList) do
if (td.Type == "self") then
res[idx] = a_Self
else
res[idx] = self:createInstance(td)
end
end
return res
end
--- Creates a new world with the specified parameters
-- a_WorldDesc is a dictionary-table containing the world description - name, dimension, default gamemode etc.
-- Only the name member is compulsory, the rest are optional
-- After creating the world, the world creation hooks are executed
function Simulator:createNewWorld(a_WorldDesc)
-- Check params:
assert(self)
assert(type(a_WorldDesc) == "table")
assert(type(a_WorldDesc.name) == "string")
-- Check if such a world already present:
local worldName = a_WorldDesc.name
if (self.worlds[worldName]) then
self.logger:error(2, "Cannot create world, a world with name \"%s\" already exists.", worldName)
end
-- Create the world:
self.logger:trace("Creating new world \"%s\".", worldName)
a_WorldDesc.dimension = a_WorldDesc.dimension or 0
a_WorldDesc.defaultGameMode = a_WorldDesc.defaultGameMode or 0
self.worlds[worldName] = a_WorldDesc
if not(self.defaultWorldName) then
self.defaultWorldName = worldName
end
-- Call the hooks for the world creation:
local world = self:createInstance({Type = "cWorld"})
getmetatable(world).simulatorInternal_worldName = worldName
self:executeHookCallback("HOOK_WORLD_STARTED", world)
end
--- Replacement for the sandbox's dofile function
-- Needs to apply the sandbox to the loaded code
function Simulator:dofile(a_FileName)
-- Check params:
assert(self)
assert(type(self.sandbox) == "table")
self.logger:trace("Executing file \"%s\".", a_FileName)
local res, msg = loadfile(a_FileName)
if not(res) then
self.logger:error(3, "Error while executing file \"%s\": %s", a_FileName, msg)
end
setfenv(res, self.sandbox)
return res()
end
--- Creates a duplicate of each given instance
-- a_Instance is any instance
-- Returns a copy of a_Instance
-- Note that tables are not duplicated, they are returned as-is instead
function Simulator:duplicateInstance(a_Instance)
local t = type(a_Instance)
if (t == "table") then
-- Do NOT duplicate tables
return a_Instance
elseif (t == "userdata") then
local res = newproxy(true)
local mt = getmetatable(res)
for k, v in pairs(getmetatable(a_Instance) or {}) do
mt[k] = v
end
return res
else
-- All the other types are value-types, no need to duplicate
return a_Instance
end
end
--- Creates a duplicate of each given instance
-- a_Instances is an array-table of any instances
-- Returns an array-table of copies of a_Instances
function Simulator:duplicateInstances(a_Instances)
local res = {}
for k, v in pairs(a_Instances) do
res[k] = self:duplicateInstance(v)
end
return res
end
--- Executes a callback request simulating the admin executing the specified console command
-- Calls the command execution hooks and if they allow, the command handler itself
-- Returns true if the command was executed, false if not.
function Simulator:executeConsoleCommand(a_CommandString)
-- Check params:
assert(self)
assert(type(a_CommandString) == "string")
-- Call the command execution hook:
local split = self:splitCommandString(a_CommandString)
if (self:executeHookCallback("HOOK_EXECUTE_COMMAND", nil, split, a_CommandString)) then
self.logger:trace("Plugin hook refused to execute console command \"%s\".", a_CommandString)
return false
end
-- Call the command handler:
split = self:splitCommandString(a_CommandString) -- Re-split, in case the hooks changed it
local cmdReg = self.registeredConsoleCommandHandlers[split[1]]
if not(cmdReg) then
self.logger:warning("Trying to execute console command \"%s\" for which there's no registered handler.", split[1])
return false
end
local res = self:processCallbackRequest(
{
Function = cmdReg.callback,
ParamValues = { split, a_CommandString },
Notes = "Console command " .. a_CommandString,
}
)
if ((type(res) == "table") and (type(res[2]) == "string")) then
self.logger:info("Console command \"%s\" returned string \"%s\".", a_CommandString, res[2])
end
return true
end
--- Executes a callback request simulating the specified hook type
-- a_HookTypeStr is the string name of the hook ("HOOK_PLAYER_DISCONNECTING" etc.)
-- All the rest of the params are given to the hook as-is
-- If a hook returns true (abort), stops processing the hooks and returns false
-- Otherwise returns false and the rest of the values returned by the hook
function Simulator:executeHookCallback(a_HookTypeStr, ...)
-- Check params:
assert(self)
assert(type(a_HookTypeStr) == "string")
local hookType = self.sandbox.cPluginManager[a_HookTypeStr]
assert(hookType)
-- Call all hook handlers:
self.logger:trace("Triggering hook handlers for %s", a_HookTypeStr)
local params = {...}
local hooks = self.registeredHooks[hookType] or {}
local res
for idx, callback in ipairs(hooks) do
res = self:processCallbackRequest(
{
Function = callback,
ParamValues = params,
Notes = a_HookTypeStr,
}
)
if (res[1]) then
-- The hook asked for an abort
self.logger:trace("Hook handler #%d for hook %s aborted the hook chain.", idx, a_HookTypeStr)
return true
end
-- TODO: Some hooks should have special processing - returning a value overwrites the param for the rest of the hooks
end
if (res) then
return false, unpack(res, 2)
else
return false
end
end
--- Executes a callback request simulating the player executing the specified command
-- Calls the command execution hooks and if they allow, the command handler itself
-- Returns true if the command was executed, false if not.
-- Doesn't care whether the player is in the list of connected players or not
function Simulator:executePlayerCommand(a_PlayerName, a_CommandString)
-- Check params:
assert(self)
assert(type(a_PlayerName) == "string")
assert(type(a_CommandString) == "string")
-- Call the command execution hook:
local split = self:splitCommandString(a_CommandString)
if (self:executeHookCallback("HOOK_EXECUTE_COMMAND", self:getPlayerByName(a_PlayerName), split, a_CommandString)) then
self.logger:trace("Plugin hook refused to execute command \"%s\" from player %s.", a_CommandString, a_PlayerName)
return false
end
-- Call the command handler:
split = self:splitCommandString(a_CommandString) -- Re-split, in case the hooks changed it
local cmdReg = self.registeredCommandHandlers[split[1]]
if not(cmdReg) then
self.logger:warning("Trying to execute command \"%s\" for which there's no registered handler.", split[1])
return false
end
self:processCallbackRequest(
{
Function = cmdReg.callback,
ParamValues = { split, self:getPlayerByName(a_PlayerName), a_CommandString },
Notes = "Command " .. a_CommandString,
}
)
return true
end