File size: 25,019 Bytes
b6a38d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
local DebugCommand = (Platform.developer or Platform.asserts) and not Platform.console
local Trace_SetCommand = DebugCommand and "log"

local CommandImportance = const.CommandImportance or empty_table
local WeakImportanceThreshold = CommandImportance.WeakImportanceThreshold

--[[@@@
@class CommandObject
It is often necessary to ensure that an object is doing one thing – and one thing only. The command system is used to accomplish just that.

A CommandObject has a single thread executing its current command (if any). A command is just a function. When the current command finishes (the function returs), the current command changes to "Idle". A call to SetCommand (or a similar function) interrupts the currently executed command (deletes the thread) and creates a new thread to run the new command.

For example, imagine a `Citizen` called Hulio who is walking to work and gets murdered by a `Soldier`. We'd like to have Hulio fall on the ground – dead – and interrupt his workday for good. 

~~~~ Lua
  -- This sets Hulio's command to "CmdGoWork"
  function Citizen:FindWork()
    ...
    self:SetCommand("CmdGoWork", workplace)
  end

  -- This is called by the soldier who will kill Hulio
  function Soldier:DoKill(obj)
    ...
    if not IsDead(obj) then
      -- This cancels Hulio's "GoWork" command
      obj:SetCommand("CmdGoDie", "Eliminated")
    end
  end
~~~~


## Destructors

When a command gets interrupted, the object can remain in an unpredictable state. Destructors solve that problem.

Each command or a function called from a command can push one or more destructors and *must* later pop them. If the command gets interrupted, any active destructors get executed from the most recently pushed one to the oldest one.

~~~~ Lua
  function Citizen:CmdUseWaterDispenser(dispencer)
    assert(dispencer.in_use == false)
    dispencer.in_use = true
    self:PushDestructor(function(self) -- run this in case someone interrupts the Citizen (e.g. kidnaps him) while using the dispenser
      dispenser.in_use = false
    end)
    self:Goto(dispenser) -- Goto probably pushes (and pops) its own destructor
    self:SetAnim("UseWaterDispenser")
    Sleep(self:TimeToAnimEnd())
    self:PopAndCallDestructor() -- removes and executes the destuctor above, which will get also executed if the command is interrupted before reaching this code
  end
~~~~


## Importance of commands

A CommandObject can execute hundreds of different commands it is quite difficult to figure out if an event should interrupt the current command or not.

We assign *importance* to each command - a number in most cases taken from const.CommandImportance[command], although some functions take *importance* as a parameter.
This allows us to implement methods such as TrySetCommand(cmd, ...) and CanInterruptCommand(cmd).

For example, if a stone hits a Citizen going to work, the Citizen should hold his head and scream with pain. If the Citizen is unconscious, nothing should happen. Command importance provides an elegant way to do that.

~~~~ Lua
    -- a stone has hit a citizen
    citizen:TrySetCommand("CmdInPain") -- will be set only if running a less important command than CmdInPain
~~~~

In the example above, if CmdGoDie has higher importance than CmdInPain (as it should) it will not be interrupted while CmdUseWaterDispenser will be correctly interrupted.

## Queue

Commands can be queued for execution after the current command completes. 

For example, a unit should complete something important (run from an enemy) and then return to whatever it was doing.

Another example is when the player has given a unit several commands to execute in order: kill this guy then kill that guy then return to the base for repairs.

--]]
DefineClass.CommandObject =
{
	__parents = { "InitDone" },

	command = false,
	command_queue = false,
	dont_clear_queue = false,
	command_destructors = false,
	command_thread = false,
	thread_running_destructors = false,
	command_call_stack = false,
	forced_cmd_importance = false,
	trace_setcmd = Trace_SetCommand,
	last_error_time = false,
	uninterruptable_importance = false,

	CreateThread = CreateGameTimeThread,
	IsValid = IsValid,
}

DefineClass.RealTimeCommandObject =
{
	__parents = { "CommandObject" },
	
	CreateThread = CreateRealTimeThread,
	IsValid = function() return true end,
	NetUpdateHash = function () end,
}

function RealTimeCommandObject:Done()
	self.IsValid = empty_func
end

--[[@@@
When deleted, the command object interrupts the currently executed command. All present destructors will be called in another thread.
@function void CommandObject:Done()
--]]
function CommandObject:Done()
	if self.command and CurrentThread() ~= self.command_thread then
		self:SetCommand(false)
	end
	self.command_queue = nil
end

function CommandObject:Idle()
	self[false](self)
end

function CommandObject:CmdInterrupt()
end

CommandObject[false] = function(self)
	self.command = nil
	self.command_thread = nil
	self.command_destructors = nil
	self.thread_running_destructors = nil
	Halt()
end

--[[@@@
Called whenever a new command starts executing. It might be faster to do some simple cleanup here instead of pushing a destructor often.
@function void CommandObject:OnCommandStart()
--]]
AutoResolveMethods.OnCommandStart = true
CommandObject.OnCommandStart = empty_func

local SetCommandErrorChecks = empty_func
local SleepOnInfiniteLoop = empty_func

local function GetNextDestructor(obj, destructors)
	local count = destructors[1]
	if count == 0 then
		return empty_func
	end
	local dstor = destructors[count + 1]
	destructors[count + 1] = false
	destructors[1] = count - 1
	
	if type(dstor) == "string" then
		assert(obj[dstor], string.format("Missing destructor: %s.%s", obj.class, dstor))
		dstor = obj[dstor] or empty_func
	elseif type(dstor) == "table" then
		assert(type(dstor[1]) == "string")
		assert(obj[dstor[1]], string.format("Missing destructor: %s.%s", obj.class, dstor[1]))
		assert(#dstor == table.maxn(dstor)) -- make sure table.unpack works properly
		return obj[dstor[1]] or empty_func, obj, table.unpack(dstor, 2)
	end
	return dstor, obj
end

local function CommandThreadProc(self, command, ...)
	dbg(SleepOnInfiniteLoop(self))
	
	-- wait the thread calling destructors to finish
	local destructors = self.command_destructors
	local thread_running_destructors = self.thread_running_destructors
	if thread_running_destructors then
		while IsValidThread(self.thread_running_destructors) and not WaitMsg(destructors, 100) do
		end
	end
	local thread = CurrentThread()
	if self.command_thread ~= thread then return end
	assert(not self.uninterruptable_importance)
	assert(not self.thread_running_destructors)

	local command_func = type(command) == "function" and command or self[command]
	local packed_command
	while true do
	
		if destructors and destructors[1] > 0 then
			self.thread_running_destructors = thread
			while destructors[1] > 0 do
				sprocall(GetNextDestructor(self, destructors))
			end
			self.thread_running_destructors = false
			if self.command_thread ~= thread then
				Msg(destructors)
				return
			end
		end
		if not self:IsValid() then
			return
		end

		self:NetUpdateHash("Command", type(command) == "function" and "function" or command, ...)
		self:OnCommandStart()
		local success, err
		if packed_command == nil then
			success, err = sprocall(command_func, self, ...)
		else
			success, err = sprocall(command_func, self, unpack_params(packed_command, 3))
		end
		assert(self.command_thread == thread)
		if not success and not IsBeingDestructed(self) then
			if self.last_error_time == now()  then
				-- throttle in case of an error right after another error to avoid infinite loops
				Sleep(1000)
			end
			self.last_error_time = now()
		end
		local forced_cmd_importance
		local queue = self.command_queue
		packed_command = queue and table.remove(queue, 1)
		if packed_command then
			if type(packed_command) == "table" then
				forced_cmd_importance = packed_command[1] or nil
				command = packed_command[2]
			else
				command = packed_command
			end
			command_func = type(command) == "function" and command or self[command]
		else
			dbg(not success or SetCommandErrorChecks(self, "->Idle", ...))
			command = "Idle"
			command_func = self.Idle
		end
		self.forced_cmd_importance = forced_cmd_importance
		self.command = command
		destructors = self.command_destructors
	end
	self.command_thread = nil
end

--[[@@@
Changes the current command unconditionally. Any present destructors form the previous command will be called before executing it. The method can fail if the current command thread cannot be deleted. When invoked, the self is passed as a first param.
@function bool CommandObject:SetCommand(string command, ...)
@function bool CommandObject:SetCommand(function command_func, ...)
@param string command - Name of the command. Should be an object's method name.
@param function command_func - Alternatively, the command to execute can be provided as a function param.
@result bool - Command change success. 
--]]
function CommandObject:SetCommand(command, ...)
	return self:DoSetCommand(nil, command, ...)
end

-- Use with SetCommand or SetCommandImportance
function CommandObject:DoSetCommand(importance, command, ...)
	self:NetUpdateHash("SetCommand", type(command) == "function" and "function" or command, ...)
	dbg(SetCommandErrorChecks(self, command, ...))
	self.command = command or nil
	if not self.dont_clear_queue then
		self.command_queue = nil
	end
	self.dont_clear_queue = nil
	local old_thread = self.command_thread
	local new_thread = self.CreateThread(CommandThreadProc, self, command, ...)
	self.command_thread = new_thread
	self.forced_cmd_importance = importance or nil
	ThreadsSetThreadSource(new_thread, "Command", command)
	if old_thread == self.thread_running_destructors then
		local uninterruptable_importance = self.uninterruptable_importance
		if not uninterruptable_importance then
			-- wait the current thread to finish destructor execution
			return true
		end
		local test_importance = importance or CommandImportance[command or false] or 0
		if uninterruptable_importance >= test_importance then
			-- wait the current thread to finish uninterruptable execution
			return true
		end
		self.uninterruptable_importance = false
		self.thread_running_destructors = false
	end
	
	DeleteThread(old_thread, true)
	if old_thread == CurrentThread() then
		-- the old thread failed to be deleted, revert!!!
		DeleteThread(new_thread)
		self.command_thread = old_thread
		return false
	end
	return true
end

function CommandObject:TestInfiniteLoop()
	self:SetCommand("TestInfiniteLoop2")
end

function CommandObject:TestInfiniteLoop2()
	self:SetCommand("TestInfiniteLoop")
end

function CommandObject:GetCommandText()
	return tostring(self.command)
end

local function IsCommandThread(self, thread)
	thread = thread or CurrentThread()
	return thread and (thread == self.command_thread or thread == self.thread_running_destructors)
end
CommandObject.IsCommandThread = IsCommandThread

--[[@@@
Pushes a destructor to be executed if the command is interrupted. The destructor stack is a LIFO structure. When invoked, the self is passed as a first param.
@function int CommandObject:PushDestructor(function dtor)
@function int CommandObject:PushDestructor(string dtor)
@function int CommandObject:PushDestructor(table dtor)
@param function dtor - Destructor function.
@param string dtor - Destructor name. Should be an object's method name.
@param table dtor - Destructor table, containing a method name and the params to be passed.
@result number - The count of the destructors pushed in the destructor stack. 
Example:
~~~~
	local orig_name = unit.name
	unit:PushDestructor(function(unit)
		unit.name = orig_name
	end)
~~~~
--]]
function CommandObject:PushDestructor(dtor)
	assert(IsCommandThread(self))
	local destructors = self.command_destructors
	if destructors then
		destructors[1] = destructors[1] + 1
		destructors[destructors[1] + 1] = dtor
		return destructors[1]
	else
		self.command_destructors = { 1, dtor }
		return 1
	end
end

--[[@@@
Pops and calls the last pushed destructor to be executed if the command is interrupted.
@function void CommandObject:PopAndCallDestructor(int check_count = false)
@param int check_count - And optional param used to check for destructor stack consistency.
--]]
function CommandObject:PopAndCallDestructor(check_count)
	local destructors = self.command_destructors
	
	assert(destructors and destructors[1] > 0)
	assert(not check_count or check_count == destructors[1])
	assert(IsCommandThread(self))
	
	local old_thread_running_destructors = self.thread_running_destructors
	if not IsValidThread(old_thread_running_destructors) then
		self.thread_running_destructors = CurrentThread()
		assert(not old_thread_running_destructors)
		old_thread_running_destructors = false
	end
	sprocall(GetNextDestructor(self, destructors))
	
	if not old_thread_running_destructors then
		self.thread_running_destructors = false
		if self.command_thread ~= CurrentThread() then
			Msg(destructors)
			Halt()
		end
	end
end

--[[@@@
Same as PopAndCallDestructor but the destructor isn't invoked.
@function void CommandObject:PopDestructor(int check_count)
--]]
function CommandObject:PopDestructor(check_count)
	local destructors = self.command_destructors
	
	assert(destructors and destructors[1] > 0)
	assert(not check_count or check_count == destructors[1])
	assert(IsCommandThread(self))
	
	destructors[destructors[1] + 1] = false
	destructors[1] = destructors[1] - 1
end

function CommandObject:GetDestructorsCount()
	local destructors = self.command_destructors
	return destructors and destructors[1] or 0
end

--[[@@@
Executes a function, interruptable only by commands with higher importance than the specified one. The execution immitates a destructor call, meaning that if the new command fails to interrupt, that will happen immediately after the uninterruptable execution terminates. The self is pased as a first param when called.
@function void CommandObject:ExecuteUninterruptableImportance(int importance, function func, ...)
@function void CommandObject:ExecuteUninterruptableImportance(int importance, string method_name, ...)
@param int importance - Command importance threshold.
@param function func - Function to be executed.
@param string method_name - Alternatively, the function to execute can be provided as a object's method name.
--]]
function CommandObject:ExecuteUninterruptableImportance(importance, func, ...)
	local thread = CurrentThread()
	local func_to_execute = type(func) == "function" and func or self[func]

	if self.command_thread ~= thread or self.thread_running_destructors then
		assert((self.uninterruptable_importance or max_int) >= (importance or max_int))
		sprocall(func_to_execute, self, ...)
		return
	end
	
	local destructors = self.command_destructors
	if not destructors then
		-- the destructors table is needed to sync command threads
		destructors = { 0 }
		self.command_destructors = destructors
	end
	
	self.uninterruptable_importance = importance
	self.thread_running_destructors = thread

	sprocall(func_to_execute, self, ...)
	
	self.uninterruptable_importance = false
	self.thread_running_destructors = false
	
	if self.command_thread == thread then
		return
	end
	
	Msg(destructors)
	Halt()
end

--[[@@@
A shortcut to invoke [ExecuteUninterruptableImportance](#CommandObject:ExecuteUninterruptableImportance) with maximum importance, disallowing interruption by any commands
@function void CommandObject:ExecuteUninterruptable(function func, ...)
--]]
function CommandObject:ExecuteUninterruptable(func, ...)
	return self:ExecuteUninterruptableImportance(nil, func, ...)
end

--[[@@@
A shortcut to invoke [ExecuteUninterruptableImportance](#CommandObject:ExecuteUninterruptableImportance) with WeakImportanceThreshold, allowing interruption by all commands with higher importance.
@function void CommandObject:ExecuteWeakUninterruptable(function func, ...)
--]]
function CommandObject:ExecuteWeakUninterruptable(func, ...)
	assert(WeakImportanceThreshold)
	return self:ExecuteUninterruptableImportance(WeakImportanceThreshold, func, ...)
end

function CommandObject:IsIdleCommand()
	return (self.command or "Idle") == "Idle"
end

local function InsertCommand(self, index, forced_importance, command, ...)
	if self:IsIdleCommand() then
		return self:SetCommand(command, ...)
	end
	local packed_command = not forced_importance and count_params(...) == 0 and command or pack_params(forced_importance or false, command or false, ...)
	local queue = self.command_queue
	if not queue then
		self.command_queue = { packed_command }
	else
		if index then
			table.insert(queue, index, packed_command)
		else
			queue[#queue + 1] = packed_command
		end
	end
end

-- queue command to be executed after the current and all other queued commands complete
function CommandObject:QueueCommand(command, ...)
	return InsertCommand(self, false, false, command, ...)
end

function CommandObject:QueueCommandImportance(forced_importance, command, ...)
	return InsertCommand(self, false, forced_importance, command, ...)
end

-- insert command at the specified place in the queue to be executed right after the current one completes
-- this is often used with 1 to place a command to be executed ASAP before continuing with the rest of the queue
function CommandObject:InsertCommand(index, forced_importance, command, ...)
	return InsertCommand(self, index, forced_importance, command, ...)
end

-- Like setcommand, but without clearing the queue. Useful when we want current command to terminate immediately, 
-- regardless of current stack position, start the new command and preserve the queue.
function CommandObject:SetCommandKeepQueue(command, ...)
	self.dont_clear_queue = true
	self:SetCommand(command, ...)
end

function CommandObject:HasCommandsInQueue()
	return #(self.command_queue or "") > 0
end

function CommandObject:ClearCommandQueue()
	self.command_queue = nil
end


----- Command importance

function CommandObject:GetCommandImportance(command)
	if not command then
		return self.forced_cmd_importance or CommandImportance[self.command]
	else
		return CommandImportance[command or false]
	end
end

--[[@@@
Checks if the current command can be changed by the given one.
@function bool CommandObject:CanSetCommand(string command, int importance = false)
@param string command - Name of the command to test.
@param int importance - Optional custom importance.
@result bool - Command change test success. 
--]]
function CommandObject:CanSetCommand(command, importance)
	assert(not importance or type(importance) == "number")
	local current_importance = self.forced_cmd_importance or CommandImportance[self.command] or 0
	importance = importance or CommandImportance[command or false] or 0
	return current_importance <= importance
end

--[[@@@
Same as [SetCommand](#CommandObject:SetCommand) but may fail if the current command has a higher importance.
@function bool CommandObject:TrySetCommand(string command, ...)
--]]
function CommandObject:TrySetCommand(cmd, ...)
	if not self:CanSetCommand(cmd) then
		return
	end
	return self:SetCommand(cmd, ...)
end

--[[@@@
Same as [SetCommand](#CommandObject:SetCommand) but a custom importance is forced. The command importances are specified in the CommandImportance const group.
@function bool CommandObject:SetCommandImportance(int importance, string command, ...)
@param int importance - A custom importance to replace the default command importance.
--]]
function CommandObject:SetCommandImportance(importance, cmd, ...)
	assert(not importance or type(importance) == "number")
	return self:DoSetCommand(importance or nil, cmd, ...)
end

--[[@@@
See [SetCommandImportance](#CommandObject:SetCommandImportance), [TrySetCommand](#CommandObject:TrySetCommand)
@function bool CommandObject:TrySetCommandImportance(int importance, string command, ...)
--]]
function CommandObject:TrySetCommandImportance(importance, cmd, ...)
	if not self:CanSetCommand(cmd, importance) then
		return
	end
	return self:SetCommandImportance(importance, cmd, ...)
end

function CommandObject:ExecuteInCommand(method_name, ...)
	if CanYield() and IsCommandThread(self) then
		self[method_name](self, ...)
		return true
	end
	return self:TrySetCommand(method_name, ...)
end

SuspendCommandObjectInfiniteChangeDetection = empty_func
ResumeCommandObjectInfiniteChangeDetection = empty_func

----

if DebugCommand then

CommandObject.command_change_prev = false
CommandObject.command_change_count = 0
CommandObject.command_change_gtime = 0
CommandObject.command_change_rtime = 0
CommandObject.command_change_loops = 0

local lCommandChangeLoopDetection = true

function SuspendCommandObjectInfiniteChangeDetection()
	lCommandChangeLoopDetection = false
end

function ResumeCommandObjectInfiniteChangeDetection()
	lCommandChangeLoopDetection = true
end

local infinite_command_changes = 10

SleepOnInfiniteLoop = function(self)
	if not lCommandChangeLoopDetection then return end

	local rtime, gtime = RealTime(), GameTime()
	if self.command_change_rtime ~= rtime or self.command_change_gtime ~= gtime then
		self.command_change_rtime = rtime -- real time to avoid false positive on paused game
		self.command_change_gtime = gtime -- game time to avoid false positive on falling behind gametime
		self.command_change_count = nil
		return
	end
	local command_change_count = self.command_change_count
	if command_change_count <= infinite_command_changes then
		self.command_change_count = command_change_count + 1
		return
	end
	self.command_change_loops = self.command_change_loops + 1
	Sleep(50 * self.command_change_loops)
	self.command_change_count = nil
end

SetCommandErrorChecks = function(self, command, ...)
	local destructors = self.command_destructors
	local prev_command = self.command
	if command == "->Idle" and destructors and destructors[1] > 0 then -- the command should pop all its destructors
		print("Command", self.class .. "." .. tostring(prev_command), "remaining destructors:")
		for i = 1,destructors[1] do
			local destructor = destructors[i + 1]
			if type(destructor) == "string" then
				printf("\t%d. %s.%s", i, self.class, destructor)
			elseif type(destructor) == "table" then
				printf("\t%d. %s.%s", i, self.class, destructor[1])
			else
				local info = debug.getinfo(destructor, "S") or empty_table
				local source = info.source or "Unknown"
				local line = info.linedefined or -1
				printf("\t%d. %s(%d)", i, source, line)
			end
		end
		error(string.format("Command %s.%s did not pop its destructors.", self.class, tostring(self.command)), 2)
		-- remove the remaining destructors to avoid having the error all the time
		while destructors[1] > 0 do
			self:PopDestructor()
		end
	end
	if command and command ~= "->Idle" then
		if type(command) ~= "function" and not self:HasMember(command) then
			error(string.format("Invalid command %s:%s", self.class, tostring(command)), 3)
		end
		if IsBeingDestructed(self) then
			error(string.format("%s:SetCommand('%s') called from Done() or delete()", self.class, tostring(command)), 3)
		end
	end
	if command ~= "->Idle" or prev_command ~= "Idle" then
		self.command_call_stack = GetStack(3)
		if self.trace_setcmd then
			if self.trace_setcmd == "log" then
				self:Trace("SetCommand {1}", tostring(command), self.command_call_stack, ...)
			else
				error(string.format("%s:SetCommand(%s) time %d, old command %s", self.class, concat_params(", ", tostring(command), ...), GameTime(), tostring(self.command)), 3)
			end
		end
	end
	if self.command_change_count == infinite_command_changes then
		assert(false, string.format("Infinite command change in %s: %s -> %s -> %s", self.class, tostring(self.command_change_prev), tostring(prev_command), tostring(command)))
		--StoreErrorSource(self, "Infinite command change") Pause("Debug")
	end
	self.command_change_prev = prev_command
end

local function __DbgForEachMethod(passed, obj, callback, ...)
	if not obj then
		return
	end
	for name, value in pairs(obj) do
		if type(value) == "function" and not passed[name] then
			passed[name] = true
			callback(name, value, ...)
		end
	end
	return __DbgForEachMethod(passed, getmetatable(obj), callback, ...)
end

function DbgForEachMethod(obj, callback, ...)
	return __DbgForEachMethod({}, obj, callback, ...)
end

function DbgBreakRemove(obj)
	DbgForEachMethod(obj, function(name, value, obj)
		obj[name] = nil
	end, obj)
end

function DbgBreakSchedule(obj, methods)
	DbgBreakRemove(obj)
	if methods == "string" then methods = { methods } end
	DbgForEachMethod(obj, function(name, value, obj)
		if not methods or table.find(methods, name) then
			local new_value = function(...)
				if IsCommandThread(obj) then
					DbgBreakRemove(obj)
					print("Break removed")
					bp(true, 1) 
				end
				return value(...)
			end
			obj[name] = new_value
		end
	end, obj)
	print("Break schedule")
end

function CommandObject:AsyncCheatDebugger()
	DbgBreakSchedule(self)
end

end -- DebugCommand