Download CommonLua/Classes/CommandObject.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 25 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Classes/CommandObject.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Classes/CommandObject.lua
-
curl -L -o CommandObject.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Classes/CommandObject.lua
25 kB
| 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 |