File size: 10,110 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
--- Holds a flag indicating whether there are any pending reasons to suspend the process.
--- Holds a flag indicating whether there are any pending reasons to suspend the process.
---
--- @type boolean
SuspendProcessReasons = nil

--- Holds a flag indicating whether the process is currently suspended.
---
--- @type boolean
SuspendedProcessing = nil

--- A function that checks the current execution timestamp.
---
--- @type function
CheckExecutionTimestamp = empty_func

--- A function that checks for any remaining reasons to suspend the process.
---
--- @type function
CheckRemainingReason = empty_func

--- A function that unpacks a table.
---
--- @type function
table_unpack = table.unpack

--- A function that checks if two tables are equal.
---
--- @type function
table_iequal = table.iequal
local SuspendProcessReasons
local SuspendedProcessing
local CheckExecutionTimestamp = empty_func
local CheckRemainingReason = empty_func
local table_unpack = table.unpack
local table_iequal = table.iequal

if FirstLoad then
	__process_params_meta = {
		__eq = function(t1, t2)
			if type(t1) ~= type(t2) or not rawequal(getmetatable(t1), getmetatable(t2)) then
				return false
			end
			local count = t1[1]
			if count ~= t2[1] then
				return false
			end
			for i=2,count do
				if t1[i] ~= t2[i] then
					return false
				end
			end
			return true
		end
	}
end

---
--- Packs the provided parameters into a table with a metatable that allows for efficient equality comparison.
---
--- @param obj any The first parameter to be packed.
--- @param ... any Additional parameters to be packed.
--- @return table A table containing the packed parameters, with a metatable that allows for efficient equality comparison.
---
local function PackProcessParams(obj, ...)
    local count = select("#", ...)
    if count == 0 then
        return obj or false
    end
    return setmetatable({count + 2, obj, ...}, __process_params_meta)
end

---
--- Unpacks the parameters from a table that was packed using the `PackProcessParams` function.
---
--- @param params table A table containing the packed parameters, with a metatable that allows for efficient equality comparison.
--- @return any The unpacked parameters.
---
function UnpackProcessParams(params)
    if type(params) ~= "table" or getmetatable(params) ~= __process_params_meta then
        return params
    end
    return table_unpack(params, 2, params[1])
end

function OnMsg.DoneMap()
	CheckRemainingReason()
	SuspendProcessReasons = false
	SuspendedProcessing = false
end

---
--- Executes any suspended functions for the given process.
---
--- @param process string The name of the process for which to execute suspended functions.
---
function ExecuteSuspended(process)
    -- Implementation details omitted for brevity
end
local function ExecuteSuspended(process)
    local delayed = SuspendedProcessing
    local funcs_to_params = delayed and delayed[process]
    if not funcs_to_params then
        return
    end
    delayed[process] = nil
    local procall = procall
    for _, funcname in ipairs(funcs_to_params) do
        local func = _G[funcname]
        for _, params in ipairs(funcs_to_params[funcname]) do
            dbg(CheckExecutionTimestamp(process, funcname, params, true))
            procall(func, UnpackProcessParams(params))
        end
    end
end

---
--- Cancels the processing of routines from a named process.
---
--- @param process string The name of the process for which to cancel processing.
---
function CancelProcessing(process)
    if not SuspendProcessReasons or not SuspendProcessReasons[process] then
        return
    end
    if SuspendedProcessing then
        SuspendedProcessing[process] = nil
    end
    SuspendProcessReasons[process] = nil
    Msg("ProcessingResumed", process, "cancel")
end

--[[@@@
Checks if the processing of routines from a named process is currently suspended
@function bool IsProcessingSuspended(string process)
--]]
function IsProcessingSuspended(process)
	local process_to_reasons = SuspendProcessReasons
	return process_to_reasons and next(process_to_reasons[process])
end

--[[@@@
Suspends the processing of routines from a named process. Multiple suspending with the same reason would lead to an error.
@function void SuspendProcessing(string process, type reason, bool ignore_errors)
@param string process - the name of the process, which routines should be suspended.
@param type reason - the reason to be used in order to resume the processing later. Could be any type.
@param bool ignore_errors - ignore suspending errors (e.g. process already suspended).
--]]
function SuspendProcessing(process, reason, ignore_errors)
	reason = reason or ""
	local reasons = SuspendProcessReasons and SuspendProcessReasons[process]
	if reasons and reasons[reason] then
		assert(ignore_errors)
		return
	end
	local now = GameTime()
	if reasons then
		reasons[reason] = now
		return
	end
	SuspendProcessReasons = table.set(SuspendProcessReasons, process, reason, now)
	Msg("ProcessingSuspended", process)
end

--[[@@@
Resumes the processing of routines from a named process. Resuming an already resumed process, or resuming it with time delay, would lead to an error.
@function void ResumeProcessing(string process, type reason, bool ignore_errors)
@param string process - the name of the process, which routines should be suspended.
@param type reason - the reason to be used in order to resume the processing later. Could be any type.
@param bool ignore_errors - ignore resume errors (e.g. process already resumed).
--]]
function ResumeProcessing(process, reason, ignore_errors)
	reason = reason or ""
	local reasons = SuspendProcessReasons and SuspendProcessReasons[process]
	local suspended = reasons and reasons[reason]
	if not suspended then
		return
	end
	assert(ignore_errors or suspended == GameTime())
	local now = GameTime()
	reasons[reason] = nil
	if next(reasons) ~= nil then
		return
	end
	assert(not IsProcessingSuspended(process))
	ExecuteSuspended(process)
	Msg("ProcessingResumed", process)
end

--[[@@@
Execute a routine from a named process. If the process is currently suspended, the call will be registered in ordered to be executed once the process is resumed. Multiple calls with the same context will be registered as one.
@function void ExecuteProcess(string process, function func, table obj)
@param string process - the name of the process, which routines should be suspended.
@param function func - the function to be executed.
@param table obj - optional function context.
--]]
function ExecuteProcess(process, funcname, obj, ...)
	if not IsProcessingSuspended(process) then
		dbg(CheckExecutionTimestamp(process, funcname, obj))
		return procall(_G[funcname], obj, ...)
	end
	local params = PackProcessParams(obj, ...)
	local suspended = SuspendedProcessing
	if not suspended then
		suspended = {}
		SuspendedProcessing = suspended
	end
	local funcs_to_params = suspended[process]
	if not funcs_to_params then
		suspended[process] = { funcname, [funcname] = {params} }
		return
	end
	local objs = funcs_to_params[funcname]
	if not objs then
		funcs_to_params[#funcs_to_params + 1] = funcname
		funcs_to_params[funcname] = {params}
		return
	end
	table.insert_unique(objs, params)
end

----

if Platform.asserts then

local ExecutionTimestamps

function OnMsg.DoneMap()
	ExecutionTimestamps = false
end

-- Rise an error if a routine from a process is executed twice in the same time
    --[[@@@
Checks if a process routine has been executed more than once in the same time frame.
@function void CheckExecutionTimestamp(string process, string funcname, table obj, boolean delayed)
@param string process - the name of the process
@param string funcname - the name of the function being executed
@param table obj - the object context of the function being executed
@param boolean delayed - whether the function call is delayed
@return boolean - true if the function has been executed more than once, false otherwise
--]]
    CheckExecutionTimestamp = function(process, funcname, obj, delayed)
        if not config.DebugSuspendProcess then
            return
        end
        if not ExecutionTimestamps then
            ExecutionTimestamps = {}
            CreateRealTimeThread(function()
                Sleep(1)
                ExecutionTimestamps = false
            end)
        end
        local func_to_objs = ExecutionTimestamps[process]
        if not func_to_objs then
            func_to_objs = {}
            ExecutionTimestamps[process] = func_to_objs
        end
        local objs_to_timestamp = func_to_objs[funcname]
        if not objs_to_timestamp then
            objs_to_timestamp = {}
            func_to_objs[funcname] = objs_to_timestamp
        end
        obj = obj or false
        local rtime, gtime = RealTime(), GameTime()
        local timestamp = xxhash(rtime, gtime)
        if timestamp == objs_to_timestamp[obj] then
            print("Duplicated processing:", process, funcname, "time:", gtime, "obj:", obj and obj.class,
                obj and obj.handle)
            assert(false, string.format("Duplicated process routine: %s.%s", process, funcname))
        else
            objs_to_timestamp[obj] = timestamp
            --[[
		if IsValid(obj) then
			local pos = obj:GetVisualPos()
			local seed = xxhash(obj and obj.handle)
			local len = 5*guim + BraidRandom(seed, 10*guim)
			DbgAddVector(pos, len, RandColor(seed))
			DbgAddText(funcname, pos + point(0, 0, len), RandColor(obj and obj.handle))
		end
		--]]
        end
    end

    ---
    --- Checks if there are any remaining reasons for suspending a process.
    --- If there are any remaining reasons, an assertion is triggered with the process name and reason.
    ---
    --- @function CheckRemainingReason
    --- @return nil
    CheckRemainingReason = function()
        local process = next(SuspendProcessReasons)
        local reason = process and next(SuspendProcessReasons[process])
        if reason then
            assert(false, string.format("Process '%s' not resumed: %s", process, ValueToStr(reason)))
        end
    end

end -- Platform.asserts