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.
API:
Customization
StructuredDD.ADD_ALL_UNITS
- Set this to true if you want all units in your map to be automatically added to StructuredDD. Otherwise you will have to manually add them with StructuredDD.add(unit).
StructuredDD.BUCKET_SIZE
- This is the amount of units that exist in each trigger bucket. This number should be something between 5 and 30. A good starting value will be an estimate of your map's average count of units, divided by 10. When in doubt, just use 20.
StructuredDD.PER_CLEANUP_TIMEOUT
- This is how often StructuredDD will search for empty buckets. If your map has units being created and dying often, a lower value is better. Anything between 10 and 180 is good. When in doubt just use 60.
Methods
StructuredDD.addHandler(code)
- This method is for adding a handler to the system. Whenever a handler is added, damage detection will immediately trigger that handler. There is no way to deallocate a handler, so don't try to do this dynamically (!) Support for handler deallocation is possible (please contact me)
StructuredDD.add(unit)
- This method adds a unit to the damage detection system. If ADD_ALL_UNITS is enabled, you don't have to (and should not) ever use this method. If you add a unit that's already in the system, it will be added that second time and hence damage to it will fire all handlers twice.
The script:
Jass:
//* API: //* boolean ADD_ALL_UNITS: If enabled, a trigger turns on which automatically //* registers all units in the map. //* integer BUCKET_SIZE: How many units to add to each 'bucket' - a larger //* bucket will have their trigger refresh less frequently but will be //* more computationally expensive. A good starting value is about 20. //* real PER_CLEANUP_TIMEOUT: How many seconds to wait in between each //* scan for empty buckets. This value should be lower if units die often //* in your map. A good starting value is about 60. //* static method addHandler: Registers a callback function to the generic //* unit damage event. Example: call StructuredDD.addHandler(function h) //* static method add: Adds a unit to a bucket. If ADD_ALL_UNITS is enabled, //* this method need not be used. library StructuredDD globals
//<< BEGIN SETTINGS SECTION
//* Set this to true if you want all units in your map to be //* automatically added to StructuredDD. Otherwise you will have to //* manually add them with StructuredDD.add(u). privateconstantboolean ADD_ALL_UNITS=true
//* This is the amount of units that exist in each trigger bucket. //* This number should be something between 5 and 30. A good starting //* value will be an estimate of your map's average count of units, //* divided by 10. When in doubt, just use 20. privateconstantinteger BUCKET_SIZE=20
//* This is how often StructuredDD will search for empty buckets. If //* your map has units being created and dying often, a lower value //* is better. Anything between 10 and 180 is good. When in doubt, //* just use 60. privateconstantreal PER_CLEANUP_TIMEOUT=60.
//>> END SETTINGS SECTION
endglobals
//* Our bucket struct which contains a trigger and its associated contents. privatestruct bucket integer bucketIndex=0 trigger trig=CreateTrigger() unitarray members[BUCKET_SIZE] endstruct
//* Our wrapper struct. We never intend to actually instanciate "a //* StructuredDD", we just use this for a pretty, java-like API :3 struct StructuredDD extendsarray privatestaticboolexprarray conditions privatestatic bucket array bucketDB privatestaticinteger conditionsIndex=-1 privatestaticinteger dbIndex=-1 privatestaticinteger maxDBIndex=-1
//* This method gets a readily available bucket for a unit to be added. //* If the "current" bucket is full, it returns a new one, otherwise //* it just returns the current bucket. privatestaticmethod getBucket takesnothingreturnsinteger localinteger index=0 localinteger returner=-1 local bucket tempDat ifthistype.dbIndex!=-1andthistype.bucketDB[thistype.dbIndex].bucketIndex<BUCKET_SIZE then returnthistype.dbIndex else setthistype.maxDBIndex=thistype.maxDBIndex+1 setthistype.dbIndex=thistype.maxDBIndex set tempDat=bucket.create() setthistype.bucketDB[.maxDBIndex]=tempDat loop exitwhen index>thistype.conditionsIndex callTriggerAddCondition(tempDat.trig,thistype.conditions[index]) set index=index+1 endloop returnthistype.dbIndex endif return-1 endmethod
//* This method is for adding a handler to the system. Whenever a //* handler is added, damage detection will immediately trigger that //* handler. There is no way to deallocate a handler, so don't try to //* do this dynamically (!) Support for handler deallocation is //* feasible (please contact me) publicstaticmethod addHandler takescode func returnsnothing local bucket tempDat localinteger index=0 setthistype.conditionsIndex=thistype.conditionsIndex+1 setthistype.conditions[thistype.conditionsIndex]=Condition(func) loop exitwhen index>thistype.maxDBIndex set tempDat=thistype.bucketDB[index] callTriggerAddCondition(tempDat.trig,thistype.conditions[thistype.conditionsIndex]) set index=index+1 endloop endmethod
//* This method adds a unit to the damage detection system. If //* ADD_ALL_UNITS is enabled, this method need not be used. publicstaticmethod add takesunit member returnsnothing local bucket tempDat localinteger whichBucket=thistype.getBucket() set tempDat=thistype.bucketDB[whichBucket] set tempDat.bucketIndex=tempDat.bucketIndex+1 set tempDat.members[tempDat.bucketIndex]=member callTriggerRegisterUnitEvent(tempDat.trig,member,EVENT_UNIT_DAMAGED) endmethod
//* This is just an auxillary function for ADD_ALL_UNITS' implementation staticif ADD_ALL_UNITS then privatestaticmethod autoAddC takesnothingreturnsboolean callthistype.add(GetTriggerUnit()) returnfalse endmethod endif
//* This method is used to check if a given bucket is empty (and thus //* can be deallocated) - this is an auxillary reoutine for the //* periodic cleanup system. privatestaticmethod bucketIsEmpty takesinteger which returnsboolean local bucket tempDat=thistype.bucketDB[which] localinteger index=0 loop exitwhen index==BUCKET_SIZE //GetUnitTypeId(unit)==0 means that the unit has been removed. ifGetUnitTypeId(tempDat.members[index])!=0then returnfalse endif set index=index+1 endloop returntrue endmethod
//* This method cleans up any empty buckets periodically by checking //* if it has been fully allocated and then checking if all its //* members no longer exist. privatestaticmethod perCleanup takesnothingreturnsnothing localinteger index=0 loop exitwhen index>thistype.maxDBIndex if index!=thistype.dbIndexandthistype.bucketIsEmpty(index)then callDestroyTrigger(thistype.bucketDB[index].trig) callthistype.bucketDB[index].destroy() setthistype.bucketDB[index]=thistype.bucketDB[thistype.maxDBIndex] setthistype.maxDBIndex=thistype.maxDBIndex-1 ifthistype.maxDBIndex==thistype.dbIndexthen setthistype.dbIndex=index endif set index=index-1 endif set index=index+1 endloop endmethod
//* This is a initialization function necessary for the setup of //* StructuredDD. privatestaticmethod onInit takesnothingreturnsnothing localgroup grp localregion reg localtrigger autoAddUnits localtimer perCleanup localunit FoG staticif ADD_ALL_UNITS then //Add starting units set grp=CreateGroup() callGroupEnumUnitsInRect(grp,bj_mapInitialPlayableArea,null) loop set FoG=FirstOfGroup(grp) exitwhen FoG==null callthistype.add(FoG) callGroupRemoveUnit(grp,FoG) endloop //Add entering units set autoAddUnits=CreateTrigger() set reg=CreateRegion() callRegionAddRect(reg,bj_mapInitialPlayableArea) callTriggerRegisterEnterRegion(autoAddUnits,reg,null) callTriggerAddCondition(autoAddUnits,Condition(functionthistype.autoAddC)) set autoAddUnits=null set reg=null endif //enable periodic cleanup: set perCleanup=CreateTimer() callTimerStart(perCleanup,PER_CLEANUP_TIMEOUT,true,functionthistype.perCleanup) set perCleanup=null endmethod endstruct endlibrary
Example Test Scope:
test
Jass:
//This script assumes that StructuredDD.ADD_ALL_UNITS is true
scope test initializer i globals privategroup grp=CreateGroup() endglobals
privatefunction h takesnothingreturnsnothing callDisplayTextToPlayer(GetLocalPlayer(),0.,0.,GetUnitName(GetEventDamageSource())+" dealt "+R2S(GetEventDamage())+" to: "+GetUnitName(GetTriggerUnit())) endfunction
privatefunction i takesnothingreturnsnothing callFogMaskEnable(false) callFogEnable(false) call StructuredDD.addHandler(function h) endfunction endscope
(Yes, it's that simple)
Change Log:
list
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. Integrating JassHelper and TESH is a godsend.
Last edited by Cokemonkey11; Yesterday at 01:24 AM.
Reason: updated API and documentation
Please remove your signature. Also, APILIKETHISISHARDLYREADABLE, IT_IS_MUCH_MORE_READABLE_LIKE_THIS. Also, boolexpr's as an argument is pretty ugly syntax, I recommend using code arguments and convert them to boolexpr's yourself, saving the user the workload. Also, bj_mapInitialPlayableArea does not cover the whole map where units may be and you also miss out on Locust units this way (not that you need them for a damage detection system, but it's possible). Basing this off an indexer makes a lot more sense.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
Also, boolexpr's as an argument is pretty ugly syntax, I recommend using code arguments and convert them to boolexpr's yourself, saving the user the workload.
That's your opinion. Blizzard seems to disagree since they take boolexpr's as arguments, but regardless I've added a new method to the API which takes code as an argument instead.
Quote:
Also, bj_mapInitialPlayableArea does not cover the whole map where units may be and you also miss out on Locust units this way (not that you need them for a damage detection system, but it's possible).
The ADD_ALL_UNITS functionality is just for users who want to make using my system easy. If they are doing something complex like locust units outside the initial playable map are on initialization, they can write their own "add all units" script. Regardless, I've added this as a note in the API to save users the trouble of reading my script.
Quote:
Basing this off an indexer makes a lot more sense.
I don't use indexing systems (and neither should you). This script is open source however, if you'd like to change it, you're more than welcome.
>> Blizzard seems to disagree since they take boolexpr's as arguments
If you are not using the true or false utility of boolexpr's, accept code as the argument.
>> I don't use indexing systems (and neither should you).
Indexing systems are great. You have provided no reason why you nor I shouldn't use indexing systems. GUI Unit Indexer has been a really great help for lots of users recently, which was unprecedented seeing as how Jesus4Lyf's "GUI AIDS" never really took off.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
If you are not using the true or false utility of boolexpr's, accept code as the argument.
Yep I've added that to the API as I said. But I have no plans to use TriggerAddAction. If you want to use TSA in your DD handlers, don't use this script.
Quote:
Indexing systems are great. You have provided no reason why you nor I shouldn't use indexing systems. GUI Unit Indexer has been a really great help for lots of users recently, which was unprecedented seeing as how Jesus4Lyf's "GUI AIDS" never really took off.
Because unit indexing systems use unit custom value and/or hashtables. My system isn't intended for "new" or "casual" developers, but instead for people who want to use efficient vJass. Chances are the users of this system know how to make it themself, but simply don't want to spend the time (I know I've been putting this off for awhile)
You don't need to. Trigger conditions can return nothing and therefore give the same speed benefit. However, when using vJass which is slaved to pJass, pJass blocks those kind of "returns nothing" conditions from compiling. The way to bypass it is to pass the code argument to a function and that function calls the "Filter/Condition" native to compile it without pJass' obsolete safety. Thus it is less verbose to the user and preserves conditions as replacements to actions.
>> My system is ... for people who want to use efficient vJass.
UnitUserData / hashtables are too slow to be useful? Don't go down this slippery slope of efficiency. Once you crop you can't stop. Seriously, no one cares. Except Nestharus but he really is addicted to it. Your system is not the most efficient anyway, you might as well focus on more important areas.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
You don't need to. Trigger conditions can return nothing and therefore give the same speed benefit. However, when using vJass which is slaved to pJass, pJass blocks those kind of "returns nothing" conditions from compiling. The way to bypass it is to pass the code argument to a function and that function calls the "Filter/Condition" native to compile it without pJass' obsolete safety. Thus it is less verbose to the user and preserves conditions as replacements to actions.
That's quite nice to know actually, can you show me how in an example?
Quote:
UnitUserData / hashtables are too slow to be useful? Don't go down this slippery slope of efficiency. Once you crop you can't stop. Seriously, no one cares. Except Nestharus but he really is addicted to it. Your system is not the most efficient anyway, you might as well focus on more important areas.
UnitUserData is too limited because it only supports 1 value per unit.
Hashtables are too slow for unit control and spells. They are useful in other ways, like when O(n) is slower than the hashtable counter-part, but for my own preferences I'm going to stick with my linear stacks.
I understand you don't care about efficiency (Or you wouldn't still be making systems for GUI) but that doesn't mean that "no one" cares. Seriously don't generalize stuff like that.
>> Hashtables are too slow for unit control and spells.
I use hashtables pretty frequently in Retro for really comprehensive data, my poor-grade Intel Atom N450 can handle it even on the scale of 100+ units without dropping in FPS.
>> UnitUserData is too limited because it only supports 1 value per unit.
Too limited? It provides an array index for every unit, it is a supplementary utility. You don't need more than one UnitUserData.
>> When O(n) is slower than the hashtable counter-part.
Which is essentially every time "n" exceeds 2 or 3. JASS reading is extremely "slow" thanks to it being an interpreted language. Calling hashtables slow is like saying the FLASH is slower than SUPERMAN. Who cares? Which brings us to...
>> That doesn't mean that "no one" cares.
No one cares about a 0.0000001% margin, which is the case here. If people think they care, they are overanalyzing and need to look at real-world scenarios. It's ridiculous percentiles like this that allows Intel to sell i3-2130's at a price difference 5x the difference between the 2100 and the 2120, despite providing only half the edge in performance (a 100MHz overclock instead of a 200MHz overclock costing $25 more instead of $5 more resulting in a huge, totally worth the money 3% performance gain).
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
// ... localcode c =function Hi callTriggerAddCondition(t,Filter(c)) // ...
The compiler can't stop you from doing this since you're passing a variable ;)
Of course, when the code is a parameter, you would need to make your function 2 lines long (add a return statement after the actual code) so that it wouldn't inline.
If it inlines, pJass says "Woah bro, that function doesn't return a boolean. You're in big trouble Mister."
LeP, it's fine for your own resources, but for public resources people don't like to download a new .exe just to run your script. I know about that resource and yes one can get it to work but no one cannot expect everyone to have it.
__________________
How to post your triggers on the Hive Workshop. JPAG - Bettering the cause of readable source code.
LeP, it's fine for your own resources, but for public resources people don't like to download a new .exe just to run your script. I know about that resource and yes one can get it to work but no one cannot expect everyone to have it.
But it's okay for a public resource to circumvent the syntax-checker?
Cool idea.
__________________
___________________________________________________ /!\ Don't call it Schnitzel /!\
Please excuse my english. I know that it's not that good, but i'm doing my best.
There's no reason as to why workarounds are bad, hence, they are not bad, in other words, it's fine to use them.
They were technically bad (desync).
They are still semantically bad (TriggerAddCondition, and we all know a conditions return something truthy or falsy.).
______
Also, when your needs change you adopt your tools.
"Hey heard of this new tool called JassHelper?", "Yeah, user don't want to download any programm.", "Okay, let's continue using Jass the way it is."
__________________
___________________________________________________ /!\ Don't call it Schnitzel /!\
Please excuse my english. I know that it's not that good, but i'm doing my best.