From 47dad828fc36aeb607e8f2ae91b631e0ce611d38 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 10:51:34 +0100 Subject: [PATCH 1/8] Guard for getEnterLeaveUnit --- wurst/event/OnUnitEnterLeave.wurst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wurst/event/OnUnitEnterLeave.wurst b/wurst/event/OnUnitEnterLeave.wurst index 4039eb3e3..1f564586c 100644 --- a/wurst/event/OnUnitEnterLeave.wurst +++ b/wurst/event/OnUnitEnterLeave.wurst @@ -37,6 +37,8 @@ function popUnit() /** Returns the unit that caused the enter/leave event to happen */ public function getEnterLeaveUnit() returns unit + if tempUnitsCount <= 0 + return null return tempUnits[tempUnitsCount - 1] /** Adds a callback that is run when a unit enters the map */ From a0ff0ba7d7a75a49adc32711c5dc47ffadc89585 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 10:51:49 +0100 Subject: [PATCH 2/8] Use new native for group iterator --- wurst/_handles/Group.wurst | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/wurst/_handles/Group.wurst b/wurst/_handles/Group.wurst index 1e4807ce5..901ee2a97 100755 --- a/wurst/_handles/Group.wurst +++ b/wurst/_handles/Group.wurst @@ -111,14 +111,32 @@ public function group.get(int index) returns unit return BlzGroupUnitAt(this, index) /* Group iterator */ +class GroupIterator + private group g + private int i = 0 + private int n = 0 -group iterGroup + construct(group source) + g = CreateGroup() + g.add(source) + n = g.size() + + function hasNext() returns bool + return i < n + + function next() returns unit + let u = g.get(i) + i += 1 + return u + + function close() + g.clear() + g.destr() + destroy this /** Creates a new iterator for this group. */ -public function group.iterator() returns group - iterGroup = CreateGroup() - iterGroup.add(this) - return iterGroup +public function group.iterator() returns GroupIterator + return new GroupIterator(this) /** Returns whether the iterator has the next item */ public function group.hasNext() returns bool From 6cd38af1cc074f292ec6494be4ec2b14bdaa0735 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 10:53:02 +0100 Subject: [PATCH 3/8] Update Group.wurst --- wurst/_handles/Group.wurst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wurst/_handles/Group.wurst b/wurst/_handles/Group.wurst index 901ee2a97..521d1652c 100755 --- a/wurst/_handles/Group.wurst +++ b/wurst/_handles/Group.wurst @@ -130,9 +130,11 @@ class GroupIterator return u function close() + destroy this + + ondestroy g.clear() g.destr() - destroy this /** Creates a new iterator for this group. */ public function group.iterator() returns GroupIterator From b191c71626ac6dd79c921e57ef833d63f4fb277c Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 11:06:58 +0100 Subject: [PATCH 4/8] Delete DamageDetection.wurst --- wurst/event/DamageDetection.wurst | 71 ------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 wurst/event/DamageDetection.wurst diff --git a/wurst/event/DamageDetection.wurst b/wurst/event/DamageDetection.wurst deleted file mode 100644 index 91e1d32aa..000000000 --- a/wurst/event/DamageDetection.wurst +++ /dev/null @@ -1,71 +0,0 @@ -package DamageDetection -import OnUnitEnterLeave - -/** - Use `addOnDamageFunc` to add a listener to any damage event that happens on the map. - Example: - - init - addOnDamageFunc() -> - print("Damage Event with dmg: " + GetEventDamage().toString()) -*/ - -@configurable constant SWAP_TIMEOUT = 600. - -boolexpr array func -int funcNext = 0 -trigger current = null -trigger toDestroy = null - -constant filter = Filter(() -> addUnit(GetFilterUnit())) - -function addUnit(unit u) - current.registerUnitEvent(u, EVENT_UNIT_DAMAGED) - -/** Add a Action to the damage Event. */ -public function addOnDamageFunc(code c) - let cf = Condition(c) - current.addCondition(cf) - func[funcNext] = cf - funcNext++ - -/** Add a Condition(Action) to the damage Event. */ -public function addOnDamageFunc(boolexpr cf) - current.addCondition(cf) - func[funcNext] = cf - funcNext++ - -/** Disables the damage events */ -public function disableDamageDetect() - DisableTrigger(current) - -/** Enables the damage events */ -public function enableDamageDetect() - EnableTrigger(current) - -/** Periodic Cleanup-Function */ -function swap() - let isEnabled = IsTriggerEnabled(current) - - current.disable() - if toDestroy != null - toDestroy.destr() - - toDestroy = current - current = CreateTrigger() - - if not isEnabled - current.disable() - - ENUM_GROUP.enumUnitsAll(filter) - - for i = 0 to funcNext - 1 - current.addCondition(func[i]) - -init - current = CreateTrigger() - for i = 0 to funcNext - 1 - current.addCondition(func[i]) - - onEnter(() -> addUnit(getEnterLeaveUnit())) - CreateTimer().startPeriodic(SWAP_TIMEOUT, function swap) From d2504789ea574aba6b7728fe662c06b467948594 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 11:16:24 +0100 Subject: [PATCH 5/8] Update ClosureForGroups.wurst --- wurst/closures/ClosureForGroups.wurst | 156 +++++++++++++++++++++----- 1 file changed, 129 insertions(+), 27 deletions(-) diff --git a/wurst/closures/ClosureForGroups.wurst b/wurst/closures/ClosureForGroups.wurst index 4dee21321..49dc42c39 100644 --- a/wurst/closures/ClosureForGroups.wurst +++ b/wurst/closures/ClosureForGroups.wurst @@ -3,34 +3,75 @@ package ClosureForGroups public interface ForGroupCallback function callback(unit u) +/** Return true to continue iteration, false to stop. */ +public interface ForGroupCallbackUntil + function callback(unit u) returns bool + /** This is used as dummy in the GroupEnum calls and should not be used */ constant DUMMY_GROUP = CreateGroup() // ====Callback stack for nested usage==== ForGroupCallback array tempCallbacks +ForGroupCallbackUntil array tempCallbacksUntil +int array callbackMode +int array countLimits +int array countIters +bool array stopRequested var tempCallbacksCount = 0 -var maxCount = INT_MAX -var iterCount = 0 constant filter = Filter(() -> filterCallback(GetFilterUnit())) +constant MODE_NORMAL = 0 +constant MODE_COUNTED = 1 +constant MODE_UNTIL = 2 function filterCallback(unit filter) - if iterCount < maxCount + let idx = tempCallbacksCount - 1 + let mode = callbackMode[idx] + if mode == MODE_NORMAL currentCallback().callback(filter) - iterCount++ + else if mode == MODE_COUNTED + if countIters[idx] < countLimits[idx] + currentCallback().callback(filter) + countIters[idx]++ + else if not stopRequested[idx] + if not currentCallbackUntil().callback(filter) + stopRequested[idx] = true function pushCallback(ForGroupCallback c) - tempCallbacks[tempCallbacksCount] = c + let idx = tempCallbacksCount + tempCallbacks[idx] = c + callbackMode[idx] = MODE_NORMAL + tempCallbacksCount++ + +function pushCallbackUntil(ForGroupCallbackUntil c) + let idx = tempCallbacksCount + tempCallbacksUntil[idx] = c + callbackMode[idx] = MODE_UNTIL + stopRequested[idx] = false + tempCallbacksCount++ + +function pushCallbackCounted(ForGroupCallback c, int count) + let idx = tempCallbacksCount + tempCallbacks[idx] = c + callbackMode[idx] = MODE_COUNTED + countLimits[idx] = count + countIters[idx] = 0 tempCallbacksCount++ - iterCount = 0 - maxCount = INT_MAX -function popCallback() +function popCallback(bool destroyCallback) DUMMY_GROUP.clear() tempCallbacksCount-- - destroy tempCallbacks[tempCallbacksCount] + let idx = tempCallbacksCount + if destroyCallback + if callbackMode[idx] == MODE_UNTIL + destroy tempCallbacksUntil[idx] + else + destroy tempCallbacks[idx] function currentCallback() returns ForGroupCallback return tempCallbacks[tempCallbacksCount - 1] + +function currentCallbackUntil() returns ForGroupCallbackUntil + return tempCallbacksUntil[tempCallbacksCount - 1] // ====Stack end==== /** Executes the given closure for every unit in this group, @@ -52,44 +93,41 @@ public function group.forEachIn(ForGroupCallback cb) public function forUnitsOfType(string unitname, ForGroupCallback c) pushCallback(c) GroupEnumUnitsOfType(DUMMY_GROUP, unitname, filter) - popCallback() + popCallback(true) /** Executes the given closure for every unit of the given player. Cancels itself after *count* iterations */ public function forUnitsOfTypeCounted(string unitname, int count, ForGroupCallback c) - pushCallback(c) - maxCount = count + pushCallbackCounted(c, count) GroupEnumUnitsOfType(DUMMY_GROUP, unitname, filter) - popCallback() + popCallback(true) /** Executes the given closure for every unit of the given player */ public function forUnitsOfPlayer(player p, ForGroupCallback c) pushCallback(c) GroupEnumUnitsOfPlayer(DUMMY_GROUP, p, filter) - popCallback() + popCallback(true) /** Executes the given closure for every existing unit */ public function forUnitsAll(ForGroupCallback c) for i = 0 to bj_MAX_PLAYER_SLOTS - 1 pushCallback(c) GroupEnumUnitsOfPlayer(DUMMY_GROUP, players[i], filter) - DUMMY_GROUP.clear() - tempCallbacksCount-- + popCallback(false) destroy c /** Executes the given closure for every unit in the given rect */ public function forUnitsInRect(rect r, ForGroupCallback c) pushCallback(c) GroupEnumUnitsInRect(DUMMY_GROUP, r, filter) - popCallback() + popCallback(true) /** Executes the given closure for every unit in the given rect. Cancels itself after *count* iterations */ public function forUnitsInRectCounted(rect r, int count, ForGroupCallback c) - pushCallback(c) - maxCount = count + pushCallbackCounted(c, count) GroupEnumUnitsInRect(DUMMY_GROUP, r, filter) - popCallback() + popCallback(true) /** Executes the given closure for every unit in range of the given position */ public function forUnitsInRange(vec2 pos, real radius, ForGroupCallback c) @@ -104,25 +142,24 @@ public function forUnitsInRange(vec2 pos, real radius, bool collisionSizeFilteri for u from ENUM_GROUP if IsUnitInRangeXY(u, pos.x, pos.y, radius) c.callback(u) - popCallback() + popCallback(true) else pushCallback(c) GroupEnumUnitsInRange(DUMMY_GROUP, pos.x, pos.y, radius, filter) - popCallback() + popCallback(true) /** Executes the given closure for every unit in range of the given position Cancels itself after *count* iterations */ public function forUnitsInRangeCounted(vec2 pos, real radius, int count, ForGroupCallback c) - pushCallback(c) - maxCount = count + pushCallbackCounted(c, count) GroupEnumUnitsInRange(DUMMY_GROUP, pos.x, pos.y, radius, filter) - popCallback() + popCallback(true) /** Executes the given closure for every unit selected by the given player */ public function forUnitsSelected(player p, ForGroupCallback c) pushCallback(c) GroupEnumUnitsSelected(DUMMY_GROUP, p, filter) - popCallback() + popCallback(true) /** Executes the given closure for the closest unit inside the given range of the given position, matching the provided filter. If there is no unit in range, the closure will be run with "null" */ @@ -137,7 +174,72 @@ public function forNearestUnit(vec2 pos, real range, filterfunc filter, ForGroup nearest = u bestDist = distSq c.callback(nearest) - popCallback() + popCallback(true) + +/** Executes the given closure for every unit in this group, + removing the units from the group as processed. + Return true to continue iteration, false to stop. */ +public function group.forEachFromUntil(ForGroupCallbackUntil cb) + while this.hasNext() + if not cb.callback(this.next()) + break + destroy cb + +/** Executes the given closure for every unit in this group, + keeping all units in the group. + Return true to continue iteration, false to stop. */ +public function group.forEachInUntil(ForGroupCallbackUntil cb) + let itr = this.iterator() + while itr.hasNext() + if not cb.callback(itr.next()) + break + itr.close() + destroy cb + +/** Executes the given closure for every unit of the given type. + Return true to continue iteration, false to stop callback invocation. */ +public function forUnitsOfTypeUntil(string unitname, ForGroupCallbackUntil c) + pushCallbackUntil(c) + GroupEnumUnitsOfType(DUMMY_GROUP, unitname, filter) + popCallback(true) + +/** Executes the given closure for every unit of the given player. + Return true to continue iteration, false to stop callback invocation. */ +public function forUnitsOfPlayerUntil(player p, ForGroupCallbackUntil c) + pushCallbackUntil(c) + GroupEnumUnitsOfPlayer(DUMMY_GROUP, p, filter) + popCallback(true) + +/** Executes the given closure for every existing unit. + Return true to continue iteration, false to stop callback invocation. */ +public function forUnitsAllUntil(ForGroupCallbackUntil c) + pushCallbackUntil(c) + for i = 0 to bj_MAX_PLAYER_SLOTS - 1 + GroupEnumUnitsOfPlayer(DUMMY_GROUP, players[i], filter) + if stopRequested[tempCallbacksCount - 1] + break + popCallback(true) + +/** Executes the given closure for every unit in the given rect. + Return true to continue iteration, false to stop callback invocation. */ +public function forUnitsInRectUntil(rect r, ForGroupCallbackUntil c) + pushCallbackUntil(c) + GroupEnumUnitsInRect(DUMMY_GROUP, r, filter) + popCallback(true) + +/** Executes the given closure for every unit in range of the given position. + Return true to continue iteration, false to stop callback invocation. */ +public function forUnitsInRangeUntil(vec2 pos, real radius, ForGroupCallbackUntil c) + pushCallbackUntil(c) + GroupEnumUnitsInRange(DUMMY_GROUP, pos.x, pos.y, radius, filter) + popCallback(true) + +/** Executes the given closure for every unit selected by the given player. + Return true to continue iteration, false to stop callback invocation. */ +public function forUnitsSelectedUntil(player p, ForGroupCallbackUntil c) + pushCallbackUntil(c) + GroupEnumUnitsSelected(DUMMY_GROUP, p, filter) + popCallback(true) // Destructables // ====Callback stack for nested usage==== From 0299f8425250d1ef96856cf1d06324ce5caf4ee6 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 11:25:28 +0100 Subject: [PATCH 6/8] Update ClosureEvents.wurst --- wurst/closures/ClosureEvents.wurst | 74 +++++++++++++++--------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/wurst/closures/ClosureEvents.wurst b/wurst/closures/ClosureEvents.wurst index 19dcc9115..966aa4bf2 100644 --- a/wurst/closures/ClosureEvents.wurst +++ b/wurst/closures/ClosureEvents.wurst @@ -187,39 +187,40 @@ public abstract class EventListener // Unit Listeners if trigUnit != null let index = trigUnit.getIndex() - if fireEvents(trigUnit) and index > 0 and unitListenersFirsts[index] != null + if index > 0 var listener = unitListenersFirsts[index] - while listener != null - let nextListener = listener.next - if listener.eventId == id - listener.onEvent() - listener = nextListener + if listener != null and fireEvents(trigUnit) + while listener != null + let nextListener = listener.next + if listener.eventId == id + listener.onEvent() + listener = nextListener // Global Listeners - if generalListenersFirsts[id] != null - var listener = generalListenersFirsts[id] - while listener != null - let nextListener = listener.next - listener.onEvent() - listener = nextListener + var globalListener = generalListenersFirsts[id] + if globalListener != null + while globalListener != null + let nextListener = globalListener.next + globalListener.onEvent() + globalListener = nextListener static function onSpellEffect() let trigUnit = GetTriggerUnit() let abilId = GetSpellAbilityId() + let caster = GetSpellAbilityUnit() let index = trigUnit.getIndex() - if castMapCasters[index] != null - var listener = castMapCasters[index] - while listener != null - let nextListener = listener.next - if listener.abilId == -1 or listener.abilId == abilId - listener.fire(GetSpellAbilityUnit()) - listener = nextListener - if castMap.has(abilId) - var listener = castMap.get(abilId) - while listener != null - let nextListener = listener.next - if listener.eventUnit == null or listener.eventUnit == trigUnit - listener.fire(GetSpellAbilityUnit()) - listener = nextListener + var listener = castMapCasters[index] + while listener != null + let nextListener = listener.next + if listener.abilId == -1 or listener.abilId == abilId + listener.fire(caster) + listener = nextListener + + listener = castMap.get(abilId) + while listener != null + let nextListener = listener.next + if listener.eventUnit == null or listener.eventUnit == trigUnit + listener.fire(caster) + listener = nextListener ondestroy if uid < 0 @@ -240,7 +241,6 @@ public abstract class EventListener next = null prev = null -let unitTrig = CreateTrigger() let leaveTrig = CreateTrigger() let keyTrig = CreateTrigger() @@ -257,10 +257,16 @@ function registerEventId(eventid evnt) returns int let eventId = evnt.getHandleId() eventTypeCounter++ eventidToIndex[eventId] = eventTypeCounter - if evnt.isPlayerunitEvent() + if evnt == EVENT_UNIT_DAMAGED + eventidToIndex[EVENT_PLAYER_UNIT_DAMAGED.getHandleId()] = eventTypeCounter + registerPlayerUnitEvent(EVENT_PLAYER_UNIT_DAMAGED, function EventListener.generalEventCallback) + else if evnt == EVENT_PLAYER_UNIT_DAMAGED + eventidToIndex[EVENT_UNIT_DAMAGED.getHandleId()] = eventTypeCounter + registerPlayerUnitEvent(EVENT_PLAYER_UNIT_DAMAGED, function EventListener.generalEventCallback) + else if evnt.isPlayerunitEvent() registerPlayerUnitEvent(ConvertPlayerUnitEvent(eventId), function EventListener.generalEventCallback) - else if evnt != EVENT_UNIT_DAMAGED and evnt != EVENT_PLAYER_LEAVE and evnt != EVENT_PLAYER_CHAT_FILTER and not evnt.isKeyboardEvent() and not evnt.isMouseEvent() - error("registering handleid: " + eventId.toString() + " non-playerunitevent. Except EVENT_UNIT_DAMAGED and EVENT_PLAYER_LEAVE these are not supported right now.") + else if evnt != EVENT_PLAYER_LEAVE and evnt != EVENT_PLAYER_CHAT_FILTER and not evnt.isKeyboardEvent() and not evnt.isMouseEvent() + error("registering handleid: " + eventId.toString() + " non-playerunitevent. Except EVENT_PLAYER_LEAVE these are not supported right now.") if evnt.isMouseEvent() and not EventListener.useMouseEvents EventListener.useMouseEvents = true for i = 0 to bj_MAX_PLAYERS - 1 @@ -269,10 +275,6 @@ function registerEventId(eventid evnt) returns int ..registerPlayerEvent(players[i], EVENT_PLAYER_MOUSE_MOVE) return eventTypeCounter -function registerEventsForUnit(unit u) - if fireEvents(u) - unitTrig.registerUnitEvent(u, EVENT_UNIT_DAMAGED) - public function unregisterEventsForUnit(unit u) if fireEvents(u) let index = u.getIndex() @@ -298,12 +300,10 @@ public function unregisterEvents(int id) init - // Un/Register Events when unit enters map - onUnitIndex(() -> registerEventsForUnit(getIndexingUnit())) + // Clean up per-unit listener lists when units are deindexed. onUnitDeindex(() -> unregisterEventsForUnit(getIndexingUnit())) nullTimer() -> - unitTrig.addAction(() -> EventListener.generalEventCallback()) leaveTrig.addAction(() -> EventListener.generalEventCallback()) keyTrig.addAction(() -> EventListener.generalEventCallback()) From e79d8694793fc8583e8cdfd9b5a7f747814d17e2 Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 11:37:41 +0100 Subject: [PATCH 7/8] more micro opts --- wurst/_handles/Trigger.wurst | 3 ++- wurst/event/DamageEvent.wurst | 6 ++++-- wurst/event/OnUnitEnterLeave.wurst | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/wurst/_handles/Trigger.wurst b/wurst/_handles/Trigger.wurst index b624b456c..4f8d714aa 100644 --- a/wurst/_handles/Trigger.wurst +++ b/wurst/_handles/Trigger.wurst @@ -76,7 +76,8 @@ public function trigger.getExecCount() returns int return GetTriggerExecCount(this) public function trigger.registerAnyUnitEvent(playerunitevent whichEvent) - TriggerRegisterAnyUnitEventBJ(this, whichEvent) + for i = 0 to bj_MAX_PLAYER_SLOTS - 1 + TriggerRegisterPlayerUnitEvent(this, Player(i), whichEvent, null) public function trigger.registerDeathEvent(widget whichWidget) returns event return TriggerRegisterDeathEvent(this, whichWidget) diff --git a/wurst/event/DamageEvent.wurst b/wurst/event/DamageEvent.wurst index cde0f1aea..72659f943 100644 --- a/wurst/event/DamageEvent.wurst +++ b/wurst/event/DamageEvent.wurst @@ -279,7 +279,8 @@ public class DamageEvent else if listener.prev != null listener.prev.next = listener.next - listener.next.prev = listener.prev + if listener.next != null + listener.next.prev = listener.prev /** Adds a damage event listener that fires before any damage reduction is applied (such as armor). If The order of firing is important, use addListener(priority, listener) */ @@ -315,7 +316,8 @@ public class DamageEvent else if listener.prev != null listener.prev.next = listener.next - listener.next.prev = listener.prev + if listener.next != null + listener.next.prev = listener.prev /* GETTERS */ /** Returns the id of the damage instance being currently fired */ diff --git a/wurst/event/OnUnitEnterLeave.wurst b/wurst/event/OnUnitEnterLeave.wurst index 1f564586c..b540c2252 100644 --- a/wurst/event/OnUnitEnterLeave.wurst +++ b/wurst/event/OnUnitEnterLeave.wurst @@ -77,7 +77,9 @@ init // Process preplaced units preplacedUnits.enumUnitsInRect(boundRect) - ForGroup(preplacedUnits, () -> prepareUnit(GetEnumUnit())) + let count = preplacedUnits.size() + for i = 0 to count - 1 + prepareUnit(preplacedUnits.get(i)) preplacedUnits..clear()..destr() @compiletime function generateAbility() From 0518ed9f2e3d7a7fc09134ebc1463c75ed13913a Mon Sep 17 00:00:00 2001 From: Frotty Date: Tue, 17 Feb 2026 11:56:07 +0100 Subject: [PATCH 8/8] Update PrimitivesTests.wurst --- wurst/_handles/primitives/PrimitivesTests.wurst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wurst/_handles/primitives/PrimitivesTests.wurst b/wurst/_handles/primitives/PrimitivesTests.wurst index 1d71e1beb..6de2d19cb 100644 --- a/wurst/_handles/primitives/PrimitivesTests.wurst +++ b/wurst/_handles/primitives/PrimitivesTests.wurst @@ -45,9 +45,9 @@ function testBoolean() @Test function testArrayLength() - let x = [1, 2, 3] - print("length: " + x.length.toString()) - x.length.assertEquals(3) + let _x = [1, 2, 3] + print("length: " + _x.length.toString()) + _x.length.assertEquals(3) @Test function testParity()