From 0ebed43f46451b104649335a595acd2059a842b4 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 1 Oct 2026 17:57:20 +0200 Subject: [PATCH 1/2] ClosureEvents: listeners of a unit added in init start listening once it is indexed EventListener.add(unit, event) and onCast(unit, ability) file a listener under the unit's index, and the library indexes the units a map creates in init only after every init block has run. In init the index is 0, so add warned and dropped the listener and onCast filed it under slot 0, where the unit's casts never reached it. A listener of a unit without an index now waits and is filed, in the order it was added, on the next timer tick, which comes after the indexing. One destroyed while it waits is not filed. A unit that still has no index then gets what it got before: the warning for add, slot 0 for casts. --- wurst/closures/ClosureEvents.wurst | 83 +++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/wurst/closures/ClosureEvents.wurst b/wurst/closures/ClosureEvents.wurst index eab596b9..c3691e62 100644 --- a/wurst/closures/ClosureEvents.wurst +++ b/wurst/closures/ClosureEvents.wurst @@ -3,6 +3,7 @@ import public EventHelper import public RegisterEvents import ClosureTimers import UnitIndexer +import ArrayList import HashMap import KeyedMap import Table @@ -40,6 +41,10 @@ public constant EVENT_PLAYER_CHAT_FILTER = ConvertPlayerEvent(96) > + A listener of a single unit may be added in an init block, for a unit the map has created there: the library indexes + such units once every init block has run, and the listener starts to listen then, on the next timer tick. What + happens to the unit before that is not seen. + **IMPORTANT**: Make sure to always use the "EVENT_PLAYER_UNIT_*" variants of the eventids. Other supported events: @@ -54,10 +59,15 @@ public abstract class OnCastListener OnCastListener prev = null int abilId unit eventUnit = null + /** Whether the listener waits for its unit to get an index (see EventListener.awaitingIndex). */ + bool awaitsIndex = false abstract function fire(unit caster) ondestroy + if awaitsIndex + // Not in any list of casters yet; the lookup below finds nothing and changes nothing. + EventListener.stopAwaitingCast(this) if eventUnit != null let index = eventUnit.getIndex() let listener = EventListener.castMapCasters[index] @@ -164,9 +174,19 @@ public abstract class EventListener /** The handle ids of the same two events, to ask whether their triggers have been handed out. */ static int damagedHandleId = 0 static int damagingHandleId = 0 + /** The listeners of a single unit which were added before the unit had an index, in the order they were added. + The library indexes the units a map has at init, and those it creates in its init blocks, after the last init + block has run, so a listener added there can not be filed under its unit yet. It is filed on the next timer + tick, which comes after the indexing, instead of being dropped. */ + static constant awaitingIndex = new ArrayList() + static constant castAwaitingIndex = new ArrayList() + static var awaitingScheduled = false var eventId = 0 var uid = -1 + /** The unit and the event this listener waits to be indexed for, or null. */ + unit awaitedUnit = null + eventid awaitedEvent = null EventListener next = null EventListener prev = null @@ -182,10 +202,20 @@ public abstract class EventListener return listener static function add(unit u, eventid eventId, EventListener listener) returns EventListener + return addForUnit(u, eventId, listener, true) + + /** mayWait: a unit without an index may be one that is only about to get it (see awaitingIndex), and the listener + then waits a tick. When it is called again after that, a unit without an index is one that has none. */ + static private function addForUnit(unit u, eventid eventId, EventListener listener, bool mayWait) returns EventListener let uid = u.getIndex() if uid <= 0 - Log.warn("Attempting to register event listener for a unit that is not indexed by UnitIndexer. " + - "If this happens during initialization, consider registering the EventListener in a callback to nullTimer() instead.") + if mayWait + listener.awaitedUnit = u + listener.awaitedEvent = eventId + awaitingIndex.add(listener) + scheduleAwaiting() + else + Log.warn("Attempting to register event listener for a unit that is not indexed by UnitIndexer.") return listener hasUnitListeners = true listener.eventId = eventId.toIntId() @@ -218,11 +248,20 @@ public abstract class EventListener return addSpellInternal(null, abilId, listener) castTo OnPointCast static private function addSpellInternal(unit u, int abilId, OnCastListener listener) returns OnCastListener + return addSpellForUnit(u, abilId, listener, true) + + /** mayWait: see addForUnit. A unit which still has no index after the wait keeps the slot 0 it always got. */ + static private function addSpellForUnit(unit u, int abilId, OnCastListener listener, bool mayWait) returns OnCastListener hasCastListeners = true listener.abilId = abilId if u != null listener.eventUnit = u let index = u.getIndex() + if index <= 0 and mayWait + listener.awaitsIndex = true + castAwaitingIndex.add(listener) + scheduleAwaiting() + return listener if castMapCasters[index] != null castMapCasters[index].prev = listener listener.next = castMapCasters[index] @@ -236,6 +275,41 @@ public abstract class EventListener castMap.put(abilId, listener) return listener + static private function scheduleAwaiting() + if not awaitingScheduled + awaitingScheduled = true + nullTimer() -> + fileAwaiting() + + /** Files the listeners which waited for their unit's index, in the order they were added. Nothing runs while it + does, so the lists can not change under it. */ + static private function fileAwaiting() + awaitingScheduled = false + for i = 0 to awaitingIndex.size() - 1 + let listener = awaitingIndex.get(i) + let u = listener.awaitedUnit + let eventId = listener.awaitedEvent + listener.awaitedUnit = null + listener.awaitedEvent = null + addForUnit(u, eventId, listener, false) + awaitingIndex.clear() + for i = 0 to castAwaitingIndex.size() - 1 + let listener = castAwaitingIndex.get(i) + listener.awaitsIndex = false + addSpellForUnit(listener.eventUnit, listener.abilId, listener, false) + castAwaitingIndex.clear() + + /** A listener that is destroyed while it waits must not be filed afterwards. */ + static function stopAwaiting(EventListener listener) + let i = awaitingIndex.indexOf(listener) + if i >= 0 + awaitingIndex.removeAtOrdered(i) + + static function stopAwaitingCast(OnCastListener listener) + let i = castAwaitingIndex.indexOf(listener) + if i >= 0 + castAwaitingIndex.removeAtOrdered(i) + static function generalEventCallback() dispatch(GetTriggerEventId().toIntId()) @@ -303,6 +377,11 @@ public abstract class EventListener listener = nextListener ondestroy + if awaitedUnit != null + // Not in any list yet; the lookup below finds nothing and changes nothing. + stopAwaiting(this) + awaitedUnit = null + awaitedEvent = null if uid < 0 let listener = generalListenersFirsts[this.eventId] if listener == this From b00e8afdb3e6016e4049d9941da044ea40d1c5a3 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 1 Oct 2026 19:25:15 +0200 Subject: [PATCH 2/2] ClosureEvents: unregistering a unit also drops the listeners still waiting for it unregisterEventsForUnit cleared the lists a unit's listeners are filed in, and a listener that was still waiting for the unit's index was filed afterwards, so it could fire after the cleanup. It now destroys the waiting ones as well. A waiting listener has a flag of its own instead of being told by its unit not being null, and a null unit is not queued at all, as it can never get an index. --- wurst/closures/ClosureEvents.wurst | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/wurst/closures/ClosureEvents.wurst b/wurst/closures/ClosureEvents.wurst index c3691e62..5be54179 100644 --- a/wurst/closures/ClosureEvents.wurst +++ b/wurst/closures/ClosureEvents.wurst @@ -178,13 +178,14 @@ public abstract class EventListener The library indexes the units a map has at init, and those it creates in its init blocks, after the last init block has run, so a listener added there can not be filed under its unit yet. It is filed on the next timer tick, which comes after the indexing, instead of being dropped. */ - static constant awaitingIndex = new ArrayList() - static constant castAwaitingIndex = new ArrayList() - static var awaitingScheduled = false + private static constant awaitingIndex = new ArrayList() + private static constant castAwaitingIndex = new ArrayList() + private static var awaitingScheduled = false var eventId = 0 var uid = -1 - /** The unit and the event this listener waits to be indexed for, or null. */ + /** Whether the listener waits for its unit to get an index, and the unit and event it waits with. */ + bool awaitsIndex = false unit awaitedUnit = null eventid awaitedEvent = null EventListener next = null @@ -209,7 +210,9 @@ public abstract class EventListener static private function addForUnit(unit u, eventid eventId, EventListener listener, bool mayWait) returns EventListener let uid = u.getIndex() if uid <= 0 - if mayWait + // No unit has no index to wait for. + if mayWait and u != null + listener.awaitsIndex = true listener.awaitedUnit = u listener.awaitedEvent = eventId awaitingIndex.add(listener) @@ -289,6 +292,7 @@ public abstract class EventListener let listener = awaitingIndex.get(i) let u = listener.awaitedUnit let eventId = listener.awaitedEvent + listener.awaitsIndex = false listener.awaitedUnit = null listener.awaitedEvent = null addForUnit(u, eventId, listener, false) @@ -299,6 +303,18 @@ public abstract class EventListener addSpellForUnit(listener.eventUnit, listener.abilId, listener, false) castAwaitingIndex.clear() + /** The listeners still waiting for this unit's index go with the rest of its listeners (see unregisterEventsForUnit): + they would otherwise be filed after the cleanup. Backwards, since destroying one takes it out of its list. */ + static function destroyAwaiting(unit u) + for i = awaitingIndex.size() - 1 downto 0 + let listener = awaitingIndex.get(i) + if listener.awaitedUnit == u + destroy listener + for i = castAwaitingIndex.size() - 1 downto 0 + let listener = castAwaitingIndex.get(i) + if listener.eventUnit == u + destroy listener + /** A listener that is destroyed while it waits must not be filed afterwards. */ static function stopAwaiting(EventListener listener) let i = awaitingIndex.indexOf(listener) @@ -377,9 +393,10 @@ public abstract class EventListener listener = nextListener ondestroy - if awaitedUnit != null + if awaitsIndex // Not in any list yet; the lookup below finds nothing and changes nothing. stopAwaiting(this) + awaitsIndex = false awaitedUnit = null awaitedEvent = null if uid < 0 @@ -444,6 +461,7 @@ function registerEventId(eventid evnt) returns int public function unregisterEventsForUnit(unit u) if fireEvents(u) + EventListener.destroyAwaiting(u) let index = u.getIndex() unregisterEvents(index) if EventListener.castMapCasters[index] != null