myspace / CommonLua /Classes /CommandObject.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
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