|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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)) |
|
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)) |
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function CommandObject:SetCommand(command, ...) |
|
return self:DoSetCommand(nil, command, ...) |
|
end |
|
|
|
|
|
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 |
|
|
|
return true |
|
end |
|
local test_importance = importance or CommandImportance[command or false] or 0 |
|
if uninterruptable_importance >= test_importance then |
|
|
|
return true |
|
end |
|
self.uninterruptable_importance = false |
|
self.thread_running_destructors = false |
|
end |
|
|
|
DeleteThread(old_thread, true) |
|
if old_thread == CurrentThread() then |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
function CommandObject:ExecuteUninterruptable(func, ...) |
|
return self:ExecuteUninterruptableImportance(nil, func, ...) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
function CommandObject:InsertCommand(index, forced_importance, command, ...) |
|
return InsertCommand(self, index, forced_importance, command, ...) |
|
end |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
function CommandObject:TrySetCommand(cmd, ...) |
|
if not self:CanSetCommand(cmd) then |
|
return |
|
end |
|
return self:SetCommand(cmd, ...) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
function CommandObject:SetCommandImportance(importance, cmd, ...) |
|
assert(not importance or type(importance) == "number") |
|
return self:DoSetCommand(importance or nil, cmd, ...) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
self.command_change_gtime = gtime |
|
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 |
|
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) |
|
|
|
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))) |
|
|
|
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 |