[System] StructuredDD StructuredDD Preface: StructuredDD is a damage detection system which enables users to register a pseudo-generic "unit damaged" event. Many systems exist to accomplish the same result, but the intended design paradigms represented by StructuredDD make it unique: Code Simplicity: StructuredDD is well commented and is self-documented. The design concept is easy to understand for programmers and JASS users with intermediate experience. Fully independent of UnitUserData. Many inexperienced spell programmers use UnitUserData to create their abilities, and I want to ensure that it is always available for that. Cohesion: The system does exactly what it intends to do, and nothing more. There are no required libraries, and any additional functionality is handled in separate components. Design Explanation: User defines one or more code to be executed on damage detection User indexes units they want to be used for damage detection (alternatively StructuredDD can auto-index all units) StructuredDD handles instances of detection triggers, each tied to a "bucket" of units StructuredDD automatically checks each trigger periodically if the entire bucket contains null units before destroying the trigger Limitations: StructuredDD requires vJass There is currently no support for deallocating handlers for dynamic use There is currently no support for periodic cleanup timeouts that self-optimize. There is currently no support for "long-term" unit buckets built to handle units that aren't removed from the game (for example heroes) There is no included "safe" method for damaging a unit - it is trivial for the client to do this in their own handler(s). There is no included method to check for physical/spell/code damage - the computational cost for such systems is a higher order of magnitude than the system itself, thus these systems are built as extensions. Handlers are added in order to an array and are executed in order. If you have handlers that depend on each other, it is your job to avoid race conditions. The script: Code (vJASS): // StructuredDD is a damage detection system for handling the generic "unit is // damaged" event response. The API follows: // // // StructuredDD.addHandler(function f) // // * Registers function f as a callback function, which will occur // whenever a unit, which has been added to the system, is damaged. // * In the context of a handler function f, use standard jass natives // like: // - GetTriggerUnit() - returns the damaged unit // - GetEventDamageSource() - returns the attacking unit // - GetEventDamage() - returns how much damage was dealt // // // StructuredDD.add(unit u) // // * Manually adds a unit the damage detection system, so that all // damage handlers are called when it receives damage. // * Note that if `ADD_ALL_UNITS` is enabled, there is no need to // manually add a unit. // // library StructuredDD globals /////////////////////////////////////////////////////////////////////// // CONFIGURATION /////////////////////////////////////////////////////////////////////// // When enabled, all units in the map will be added to the damage // detection system. private constant boolean ADD_ALL_UNITS = true // The bucket size determines how many units should be added to each // damage detection bucket. Many variables impact the performance // for this process. If in doubt, use a number between 10 and 30. private constant integer BUCKET_SIZE = 20 // Controls the period, in seconds, for the vacuum interval. Higher // values will have higher success rate, but may become more costly. If // in doubt, use a number between 30. and 180. private constant real PER_CLEANUP_TIMEOUT = 60. /////////////////////////////////////////////////////////////////////// // END CONFIGURATION /////////////////////////////////////////////////////////////////////// endglobals // A bucket represents a fragment of the units that are managed by the // damage detection engine. Essentially, each bucket has a trigger and a // set of units. When all the units in the bucket die, the trigger is // destroed. This allows more granular recycling of triggers, rather than // rebuilding one huge trigger synchronously, as traditional methods do. private struct bucket integer bucketIndex = 0 trigger trig = CreateTrigger() unit array members[BUCKET_SIZE] endstruct // The StructuredDD struct is just a wrapper, which provides the API with // "dot" syntax. struct StructuredDD extends array // The conditions array manages *all* conditions in the context of the // damage detector. When a new bucket is instantiated, all conditions // are added to it. When a condition is added, all existing bucket's // triggers receive the condition. private static boolexpr array conditions private static bucket array bucketDB private static integer conditionsIndex = -1 private static integer dbIndex = -1 private static integer maxDBIndex = -1 // This auxilliary method is used for getting the next available // bucket. Since buckets get units added to them one at a time, but // have capacity for many, this method arbitrates when to use the last // bucket and when to build a new one. private static method getBucket takes nothing returns integer local integer index = 0 local integer returner = -1 local bucket tempDat if thistype.dbIndex != -1 and thistype.bucketDB[thistype.dbIndex].bucketIndex < BUCKET_SIZE then // A non-full bucket context is already known, so use it. return thistype.dbIndex else // This is either the first bucket requested, or the last // bucket used is now full - instantiate a new one. set thistype.maxDBIndex = thistype.maxDBIndex + 1 set thistype.dbIndex = thistype.maxDBIndex set tempDat = bucket.create() set thistype.bucketDB[.maxDBIndex] = tempDat // Add all known handlers to the new bucket. loop exitwhen index > thistype.conditionsIndex call TriggerAddCondition(tempDat.trig, thistype.conditions[index]) set index = index + 1 endloop return thistype.dbIndex endif // This line never executes, but some versions of JassHelper will // flag an error without it. return -1 endmethod // Adds a new "handler" function to the list of functions that are // executed when a unit is damaged. Adding a handler will immediately // enable it in all buckets. Removing handlers is not supported, so // this should not be used dynamically. public static method addHandler takes code func returns nothing local bucket tempDat local integer index = 0 set thistype.conditionsIndex = thistype.conditionsIndex + 1 set thistype.conditions[thistype.conditionsIndex] = Condition(func) // Immediately add this new handler to all buckets. loop exitwhen index > thistype.maxDBIndex set tempDat = thistype.bucketDB[index] call TriggerAddCondition(tempDat.trig, thistype.conditions[thistype.conditionsIndex]) set index = index + 1 endloop endmethod // Adds a unit to the damage detection system using some bucket. When // the unit dies or is removed, the bucket will eventually empty and // get recycled. If you enable the `ADD_ALL_UNITS` configuration // variable, then there is no need to use this method. public static method add takes unit member returns nothing local bucket tempDat local integer whichBucket = thistype.getBucket() set tempDat = thistype.bucketDB[whichBucket] set tempDat.bucketIndex = tempDat.bucketIndex+1 set tempDat.members[tempDat.bucketIndex] = member // When a unit is added to a bucket, the event for it is // immediately enabled. call TriggerRegisterUnitEvent(tempDat.trig,member, EVENT_UNIT_DAMAGED) endmethod // This auxilliary method is part of the implementation for // `ADD_ALL_UNITS`. It adds a unit to the damage detection context, // when triggered below. static if ADD_ALL_UNITS then private static method autoAddC takes nothing returns boolean call thistype.add(GetTriggerUnit()) return false endmethod endif // This auxilliary method is used to check if a bucket is empty, and // is used to arbitrate when a bucket (and its associated trigger) can // be deallocated. This occurs as part of the periodic cleanup system // and can be disabled. private static method bucketIsEmpty takes integer which returns boolean local bucket tempDat = thistype.bucketDB[which] local integer index = 0 loop exitwhen index == BUCKET_SIZE // If a unit is removed, it's `TypeId` will return 0, at least // for some period before its pointer leaves cache or is // reused. if GetUnitTypeId(tempDat.members[index]) != 0 then return false endif set index = index + 1 endloop return true endmethod // This method is called periodically and checks the buckets contents, // and recycles them if they are empty. A better implementation would // cycle through one bucket a time per iteration, rather than // synchronously iterating through all buckets. private static method perCleanup takes nothing returns nothing local integer index = 0 loop exitwhen index > thistype.maxDBIndex if index != thistype.dbIndex and thistype.bucketIsEmpty(index) then // The bucket at this index is empty, so begin // deallocating. call DestroyTrigger(thistype.bucketDB[index].trig) call thistype.bucketDB[index].destroy() set thistype.bucketDB[index] = thistype.bucketDB[thistype.maxDBIndex] set thistype.maxDBIndex = thistype.maxDBIndex - 1 if thistype.maxDBIndex == thistype.dbIndex then set thistype.dbIndex = index endif set index = index - 1 endif set index = index + 1 endloop endmethod // This struct initialization method is necessary for setting up the // damage detection system. private static method onInit takes nothing returns nothing local group grp local region reg local trigger autoAddUnits local timer perCleanup local unit FoG // If the `ADD_ALL_UNITS` configuration is enabled, turns on a // trigger which allocates all new units to a bucket. Also checks // units that are on the map at initialization. static if ADD_ALL_UNITS then // Add pre-placed units. set grp = CreateGroup() call GroupEnumUnitsInRect(grp, bj_mapInitialPlayableArea, null) loop set FoG = FirstOfGroup(grp) exitwhen FoG == null call thistype.add(FoG) call GroupRemoveUnit(grp,FoG) endloop // Add units that enter the map using a trigger. set autoAddUnits = CreateTrigger() set reg = CreateRegion() call RegionAddRect(reg, bj_mapInitialPlayableArea) call TriggerRegisterEnterRegion(autoAddUnits, reg, null) call TriggerAddCondition(autoAddUnits, Condition(function thistype.autoAddC)) set autoAddUnits = null set reg = null endif // Enable the periodic cleanup module, which vacuums each bucket // periodically. set perCleanup = CreateTimer() call TimerStart(perCleanup, PER_CLEANUP_TIMEOUT, true, function thistype.perCleanup) set perCleanup = null endmethod endstruct endlibrary Example Test Script: Code (vJASS): // This script requires that StructuredDD.ADD_ALL_UNITS is enabled. scope Test initializer init private function h takes nothing returns nothing local string attackerName = GetUnitName(GetEventDamageSource()) local string damage = R2S(GetEventDamage()) local string struckName = GetUnitName(GetTriggerUnit()) call DisplayTextToPlayer(GetLocalPlayer(), 0., 0., attackerName + " dealt " + damage + " to " + struckName) endfunction private function init takes nothing returns nothing call FogMaskEnable(false) call FogEnable(false) call StructuredDD.addHandler(function h) endfunction endscope Change Log: list 2015.09.07 - fixed up white space and improved documentation 2013.05.25 - removed UnitAlive declaration and reduced length of processed code using static if 2013.05.24 - made one small change (StructuredDD extends array) which will reduce the size of the compiled code. This change does not affect the API. 2013.05.13 - updated the API and documentation 2013.01.18 - big update - updated API, removed some components, fixed a massive bug. Please note that I have also stopped maintaining the version pastebin mirrors as they were a waste of time to maintain. 2012.06.03 - Updated the null check to use GetUnitTypeId() which fixes a deindexing bug where DEAD_UNITS_ARE_NULL was false. The script now declares native UnitAlive, thus IsUnitType() has been replaced with UnitAlive(). Will revert this change if it is not recommended. 2012.06.02 - The periodic cleanup now uses a timer instead of a trigger for efficiency reasons. 2012.06.01 - Updating the script to follow proper CamelCase. The API has also been changed to reflect only 1 use of addHandler, which takes code as an argument as suggested by fellow hive users. Also fixed two major bugs in the decrement portion of the stack. 2012.05.30 #2 - Updated submission with additional method and changed constant nomenclature. 2012.05.30 - Initial submission to Hive: Jass Resources: Submissions Special Thanks: PurplePoot who thought of bucket-based damage detection systems and created it in the first place, but never released his version. Nestharus and Troll-Brain for explaining how to use GetUnitTypeId() for null checks. -Kobas- for creating a map submission template, which turned out useful for system submissions as well. Vexorian for developing JassHelper. Without vJass, I almost certainly would not still be scripting for wc3. The various developers of JNGP including PitzerMike and MindworX.