Skip to content

Commit 88cff38

Browse files
committed
feat: Wrapper for: <pullEvent, queueEvent, startTimer, cancleTimer>
1 parent f508039 commit 88cff38

File tree

4 files changed

+267
-71
lines changed

4 files changed

+267
-71
lines changed

docs/events.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Events
2+
3+
__Includes:__
4+
```lua
5+
os.pullEvent()
6+
os.startTimer()
7+
os.queueEvent()
8+
```
9+
---
10+
11+
Restrictions:
12+
---
13+
14+
__YOU CANNOT USE EVENTS GLOBALLY.__
15+
16+
_You cannot yield the "main" coroutine, therefore os.pullevent() cannot just be set globally.
17+
Therefore You need to wrap everything you intent to run in the wrapper._
18+
19+
___Modules CANNOT yield outside of their fields__
20+
21+
```lua
22+
local TestModule = {}
23+
-- ...
24+
coroutine.yield() -- <- Breaks the EventManager, as the module cannot be wrapped
25+
-- ...
26+
return TestModule
27+
```
28+
29+
How to use:
30+
---
31+
32+
### Setup
33+
34+
1. Require the emulator-script for events and -
35+
2. create as many managed Instances as required:
36+
> ```lua
37+
> local EventModule = require("events")
38+
> local eventManager1 = EventModule()
39+
> local eventManager2 = EventModule()
40+
> ```
41+
3. Load your script through the wrapper:
42+
> ```lua
43+
> local path = "<path to your Script>.lua"
44+
> local file = loadfile(path, "t") -- Modules need to be loaded through loadfile, as require would run them instantly!
45+
> local yourModule = eventManager1:wrap(file, true) -- <false> for Functions, <true> for modules
46+
>```
47+
48+
49+
### Trigger Event from the Test-env
50+
51+
os.queueEvent will work from within any loaded Module, however if one wants to trigger an event while testing the Module from "outside", the best way to do this, is:
52+
> ```lua
53+
> eventManager:invoke(eventname, ...)
54+
> ```

events.lua

Lines changed: 107 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ local class = require("ccClass")
55
---@field eventArgs table | nil
66
---@field receivedBy table
77

8+
---@class _pullEventWrapper
9+
---@field expectedEventName string
10+
---@field firstStart boolean
11+
12+
---@class Promise
13+
---@field fullFilled boolean
14+
---@field currentValue any
15+
816
---@class timer
917
---@field id number
1018
---@field triggerAfter number
@@ -16,16 +24,16 @@ local class = require("ccClass")
1624
---@class subThread
1725
---@field originalFunction function
1826
---@field thread thread
19-
---@field waiting boolean
27+
---@field promise Promise
28+
---@field waiting boolean To confirm if a function was actually called or is just wrapped.
29+
-- Relevant for Events.run, as it resumes ALL coroutines - therefore invoking each method if not for this check
2030

21-
---@class ccEvent
31+
---@class ccEvent : subThread
2232
---@field FIFOEventList Event[]
2333
---@field TimerList timerList
24-
---@field thread thread
2534
---@field subThreads table<string, subThread>
2635
---@field time number eq. os.time("ingame") from ccTweaked)
2736
---@field epoch number eq. os.epoch("ingame") from ccTweaked)
28-
---@field private run thread
2937
local Events = class(
3038
function(baseClass)
3139
---@cast baseClass ccEvent
@@ -34,32 +42,36 @@ local Events = class(
3442
baseClass.time = 0
3543
baseClass.epoch = 0
3644
baseClass.subThreads = {}
37-
baseClass.run = coroutine.create(
38-
function()
39-
while true do
40-
while #baseClass.FIFOEventList > 0 do -- TODO if a DID trigger something, stop?
41-
local event = table.remove(baseClass.FIFOEventList, 1)
42-
---@cast event Event
43-
if coroutine.status(baseClass.thread) == "suspended" then -- Modules should be "dead"
44-
coroutine.resume(baseClass.thread, event.eventName, table.unpack(event.eventArgs))
45-
-- just empty the list until an event was valid OR no Events are left
46-
end
47-
for key, value in pairs(baseClass.subThreads) do
48-
if value.waiting then
49-
assert(coroutine.resume(value.thread, event.eventName, table.unpack(event.eventArgs)))
50-
if coroutine.status(value.thread) == "dead" then
51-
value.waiting = false
52-
end
53-
end
54-
end
55-
end
56-
coroutine.yield("tick")
57-
end
58-
end
59-
)
6045
end
6146
)
6247

48+
---comment
49+
---@param threadHolder subThread
50+
---@param ... any
51+
function Events:resumeThread(threadHolder, ...)
52+
local ok
53+
local wrapper
54+
local result
55+
56+
ok, result = coroutine.resume(threadHolder.thread, ...)
57+
---@cast result _pullEventWrapper | any
58+
59+
wrapper = type(result) =="table" and (result.firstStart or result.firstStart == false)
60+
local status = coroutine.status(threadHolder.thread)
61+
threadHolder.promise.currentValue = wrapper and result.expectedEventName or result
62+
threadHolder.promise.fullFilled = status == "dead"
63+
threadHolder.waiting = not threadHolder.promise.fullFilled
64+
assert(ok, debug.traceback("coroutine Error: "..tostring(result))) --TODO: Checkout current behaviour
65+
if wrapper and result.firstStart and status == "suspended" then
66+
result.firstStart = false
67+
-- TODO: If there is a coroutine.yield with a table containing this value -> I need a better way to guard...
68+
-- this is required bc. the test-script would normaly be in the "main" thread, therefore it would resume instantly
69+
-- on os.pullEvent, assuming there is an Event already waiting prior
70+
self:checkForUpdates()
71+
end
72+
return ok, threadHolder.promise.currentValue
73+
end
74+
6375
---@generic T
6476
---@param func function | T
6577
---@param wrapModule? boolean This will modify the Module!
@@ -73,11 +85,12 @@ function Events:wrap(func, wrapModule, ...)
7385
env.os = setmetatable({
7486

7587
pullEvent = function(expectedEventName)
76-
local firstStart = true
88+
---@type _pullEventWrapper
89+
local _pullEventWrapper = {expectedEventName = expectedEventName, firstStart = true}
7790
local event
78-
while firstStart or (event[1] ~= expectedEventName) do
79-
event = {coroutine.yield(expectedEventName)}
80-
firstStart = false
91+
while _pullEventWrapper.firstStart or (expectedEventName ~= nil and (event[1] ~= expectedEventName)) do
92+
event = {coroutine.yield(_pullEventWrapper)}
93+
---@cast event Event
8194
end
8295
return table.unpack(event)
8396
end,
@@ -97,39 +110,53 @@ function Events:wrap(func, wrapModule, ...)
97110
setfenv(func, env)
98111

99112

100-
self.thread = coroutine.create(func)
101113

102114
if(not wrapModule) then
115+
self.originalFunction = func
103116
return function(...)
104-
local ok, result = coroutine.resume(self.thread, ...)
117+
if self.thread == nil or coroutine.status(self.thread) == "dead" then
118+
self.waiting = true
119+
self.promise = {
120+
currentValue = nil,
121+
fullFilled = false
122+
}
123+
-- "restart" function => create new Thread
124+
self.thread = coroutine.create(self.originalFunction)
125+
126+
self.waiting = false
127+
end
128+
local ok, result = self:resumeThread(self, ...)
105129
assert(ok, "coroutine Error: "..tostring(result))
106130
return result
107131
end
108132
end
109-
110-
local ok, result = coroutine.resume(self.thread, ...)
133+
local thread = coroutine.create(func)
134+
local ok, result = coroutine.resume(thread, ...)
111135
assert(ok, "Could not load Module")
112-
assert(type(result) == "table")
136+
assert(type(result) == "table", "Module could not be loaded")
113137
for k,v in pairs(result) do
114138
if type(v) == "function" and (self.subThreads[k] == nil)then
115139
local thread = coroutine.create(v)
116-
self.subThreads[k] = {
117-
thread = thread,
118-
originalFunction = v,
119-
waiting = false
140+
self.subThreads[k] = {
141+
thread = thread,
142+
originalFunction = v,
143+
waiting = false,
144+
promise = {
145+
currentValue = nil,
146+
fullFilled = false
120147
}
148+
}
121149
result[k] = function(...)
122-
local ok, result = coroutine.resume(self.subThreads[k].thread, ...)
150+
123151
local status = coroutine.status(self.subThreads[k].thread)
124152
if status == "dead" then
125153
-- "restart" function => create new Thread
126154
self.subThreads[k].thread = coroutine.create(self.subThreads[k].originalFunction)
127155
self.subThreads[k].waiting = false
128-
else
129-
self.subThreads[k].waiting = true
130156
end
131-
assert(ok, "coroutine Error: "..tostring(result))
132-
return result
157+
self:resumeThread(self.subThreads[k], ...)
158+
159+
return self.subThreads[k].promise
133160
end
134161
end
135162
end
@@ -164,7 +191,7 @@ end
164191
function Events:passTime(time)
165192
assert(type(time) == "number")
166193
time = time * 1000
167-
self.time = (self.time + (time / 60 / 24)) % 24 -- TODO: Test
194+
self.time = (self.time + (time / 60 / 24)) % 24
168195
self.epoch = self.epoch + time
169196

170197
for key, value in pairs(self.TimerList.timers) do
@@ -180,8 +207,40 @@ function Events:invoke(eventName, ...)
180207
---@type Event
181208
local event = {eventName = eventName, receivedBy = {}, eventArgs = {...}}
182209
table.insert(self.FIFOEventList, event)
183-
self.newEventAdded = true
184-
assert(coroutine.resume(self.run, "tick"))
210+
self:checkForUpdates()
185211
end
186212

213+
function Events:checkForUpdates()
214+
local modifier = 0
215+
216+
local removeEvent = function(i)
217+
table.remove(self.FIFOEventList, i + modifier)
218+
modifier = modifier - 1;
219+
end
220+
221+
for i = 1,#self.FIFOEventList do
222+
local read = false
223+
local event = self.FIFOEventList[i + modifier]
224+
---@cast event Event
225+
if self.waiting and coroutine.status(self.thread) == "suspended" then -- On it Modules should be "dead"
226+
227+
removeEvent(i)
228+
assert(self:resumeThread(self, event.eventName, table.unpack(event.eventArgs)))
229+
230+
end
231+
for key, value in pairs(self.subThreads) do -- should be empty on Function
232+
-- if status = running -> The thread making the call,- obvious skip
233+
if value.waiting and coroutine.status(value.thread) ~= "normal" then
234+
removeEvent(i)
235+
assert(self:resumeThread(value, event.eventName, table.unpack(event.eventArgs)))
236+
if coroutine.status(value.thread) == "dead" then
237+
value.waiting = false
238+
end
239+
break;
240+
end
241+
end
242+
end
243+
end
244+
245+
187246
return Events

0 commit comments

Comments
 (0)