| 03-09-2007, 04:32 AM | #1 |
To use textmacros you must have JassHelper (enabled by Grimoire or WEHelper). You can download it here. If you haven't already read Vexorian's tutorial on struct usage, I recommend that you do so before reading this tutorial. This tutorial will cover three main points: I. What is a textmacro? II. Using textmacros to reduce repetitive coding III. Generic functions and textmacros I. What is a textmacro? Think of textmacros as instructions that the preprocessor uses to create a function or a segment of code. For example: JASS://! textmacro TEST call BJDebugMsg("this is only a test") //! endtextmacro //! runtextmacro TEST() When you run this textmacro the preprocessor will replace //! runtextmacro TEST() with "call BJDebugMsg("this is only a test")" when it compiles your script. Textmacros can also take arguments JASS://! textmacro TEST takes NAME function $NAME$ takes nothing returns nothing call BJDebugMsg("This function's name is: $NAME$") endfunction //! endtextmacro Every instance of $NAME$ will be replaced by whatever you make NAME be when you run the textmacro. JASS://! runtextmacro TEST("Test1") //! runtextmacro TEST("Test2") //! runtextmacro TEST("Test3") When the preprocessor finds these lines it will create three functions and place them where you ran the textmacros. JASS:function Test1 takes nothing returns nothing call BJDebugMsg("This function's name is: Test1") endfunction function Test2 takes nothing returns nothing call BJDebugMsg("This function's name is: Test2") endfunction function Test3 takes nothing returns nothing call BJDebugMsg("This function's name is: Test3") endfunction It's as simple as that. You just created three different functions quickly and easily. II. Using textmacros to reduce repetitive coding Now to apply this to something useful. If you read Vexorian's tutorial on struct usage, you saw the method he used to create structs and loop through an array of them periodically performing actions. It involves creating three globals and also some repetitive moving of array values, starting and pausing timers, and setting global integer values. It's not that big of a deal to do this for just one spell, but imagine if you have 20 different spells that also have periodic events. This is where textmacros come in handy. First, look through the script and find the actions you will be doing for every spell. JASS:function OpticalFlare_Timer takes nothing returns nothing local integer i=0 local flaredata dat local real nx local real ny local real a local real d loop exitwhen i==OpticalFlare_total set dat= OpticalFlare_Ar[i] if (dat.destroyplease) then //This is tricky. // We already have the ith flaredata referenced by dat, so we // will first remove it from the array, since the order does not matter // we can simply move the last flaredata to this position and decrease // the total set OpticalFlare_Ar[i]= OpticalFlare_Ar[ OpticalFlare_total - 1] set OpticalFlare_total=OpticalFlare_total-1 //now we are free to destroy the dat call dat.destroy() //another way to destroy struct objects. //If we don't substract i, it will skip the new object we just moved // to the ith position (we are increasing i later) set i=i-1 else //Notice how this ressembles the original expire function set nx=GetUnitX(dat.b) set ny=GetUnitY(dat.b) set a=ModuloReal( Atan2(ny-dat.y , nx-dat.x ) , 2*bj_PI) set d= SquareRoot(Pow(dat.x-nx,2) +Pow(dat.y-ny,2)) if ModuloReal(a+bj_PI/4,bj_PI*2)<=bj_PI then set d=d*40 endif call SetUnitPosition(dat.sh,nx+d*Cos(a),ny+d*Sin(a) ) set dat.x=nx set dat.y=ny call SetUnitFacing( dat.sh,GetUnitFacing( dat.b )) call SetUnitFlyHeight( dat.sh, GetUnitFlyHeight( dat.b )+120,0) endif set i=i+1 endloop if (OpticalFlare_total==0) then // let's pause the timer, it is not needed anymore call PauseTimer(OpticalFlare_timer) endif endfunction function OpticalFlare_Effect takes unit b, integer l returns nothing local real ac=GetUnitAcquireRange(b) local unit sh=CreateUnit( GetOwningPlayer(b), OpticalFlare_FakeSightUnit(), GetUnitX(b), GetUnitY(b), 0) local integer abi=OpticalFlareDetectDetector(b ) local flaredata dat = flaredata.create() call SetUnitPathing(sh,false) call UnitRemoveAbility(b, abi ) call UnitAddAbility(b, OpticalFlare_SightDestructorSpellId() ) call UnitMakeAbilityPermanent(b,true,OpticalFlare_SightDestructorSpellId()) call UnitMakeAbilityPermanent(b,true,OpticalFlare_BuffId()) set dat.x=0. //struct fields aren't automatically initialized to 0, unlike handle vars set dat.y=0. // It is also possible to make them initialize to a default value inside the struct block set dat.b=b set dat.sh=sh if(OpticalFlare_total==0) then //There are no elements in the array so the timer is inactive call TimerStart(OpticalFlare_timer,0.01,true,function OpticalFlare_Timer) //restart it endif set OpticalFlare_total=OpticalFlare_total+1 //increase the number of elements set OpticalFlare_Ar[ OpticalFlare_total-1 ] = dat //add the flaredata to the array, notice that these are arrays //that begin with the [0] index... loop exitwhen IsUnitDeadBJ(b) call TriggerSleepAction(0) exitwhen not UnitHasBuffBJ(b, OpticalFlare_BuffId() ) endloop call UnitRemoveAbility(b, OpticalFlare_SightDestructorSpellId() ) call UnitAddAbility(b, abi ) call RemoveUnit(sh) set dat.destroyplease = true //Will send a signal to the timer function so it destroys this object... set sh=null endfunction The only things that will change in the parts I pulled out below are the variables. JASS:if(OpticalFlare_total==0) then call TimerStart(OpticalFlare_timer,0.01,true,function OpticalFlare_Timer) endif set OpticalFlare_total=OpticalFlare_total+1 set OpticalFlare_Ar[ OpticalFlare_total-1 ] = dat // // set OpticalFlare_Ar[i]= OpticalFlare_Ar[ OpticalFlare_total - 1] set OpticalFlare_total=OpticalFlare_total-1 call dat.destroy() // // if (OpticalFlare_total==0) then call PauseTimer(OpticalFlare_timer) endif The variables are: The global integer used to count how many structs are active: OpticalFlare_total The global struct array used to store the structs after you create them: OpticalFlare_Ar The timer: OpticalFlare_timer Also the timer callback function will change: OpticalFlare_Timer And we need to pass the specific instance of the struct to the function so we need to add STRUCTNAME as an argument. It appears then that our textmacro will need five arguments: TOTAL, STRUCTARRAY, TIMER, STRUCTNAME, and FUNCTION. A textmacro can contain instructions for more than one function, which is good since we can't have all the actions in the same function. We'll need three different functions. JASS://! textmacro PeriodicSpell takes TOTAL, STRUCTARRAY, TIMER, FUNCTION, STRUCTNAME function Add2Struct takes $STRUCTNAME$ dat, real period returns nothing// the integer dat is the newly created struct. if $TOTAL$ == 0 then call TimerStart($TIMER$, period, TRUE, function $FUNCTION$) endif set $TOTAL$ = $TOTAL$ + 1 set $STRUCTARRAY$[$TOTAL$-1] = dat endfunction function RecycleStruct takes $STRUCTNAME$ dat, integer i returns nothing set $STRUCTARRAY$[i] = $STRUCTARRAY$[$TOTAL$-1] set $TOTAL$ = $TOTAL$ - 1 call $STRUCTNAME$.destroy(dat) endfunction function StopTimer takes nothing returns nothing if $TOTAL$ == 0 then call PauseTimer($TIMER$) endif endfunction //! endtextmacro //! runtextmacro PeriodicSpell("OpticalFlare_total","OpticalFlare_Ar","OpticalFlare_timer","OpticalFlare","OpticalFlare_Timer") The preprocessor will create the following functions when it compiles: JASS://textmacro instance: PeriodicSpell("OpticalFlare_total","OpticalFlare_Ar","OpticalFlare_timer","OpticalFlare_Timer","flaredata") function Add2Struct takes integer dat,real period returns nothing if OpticalFlare_total == 0 then call TimerStart(OpticalFlare_timer , period , TRUE , function OpticalFlare_Timer) endif set OpticalFlare_total = OpticalFlare_total + 1 set OpticalFlare_Ar[OpticalFlare_total - 1]=dat endfunction function RecycleStruct takes integer dat,integer i returns nothing set OpticalFlare_Ar[i]=OpticalFlare_Ar[OpticalFlare_total - 1] set OpticalFlare_total = OpticalFlare_total - 1 call s__flaredata_destroy(dat) endfunction function StopTimer takes nothing returns nothing if OpticalFlare_total == 0 then call PauseTimer(OpticalFlare_timer) endif endfunction //end of: PeriodicSpell("OpticalFlare_total","OpticalFlare_Ar","OpticalFlare_timer","OpticalFlare_Timer","flaredata") By changing the values of the textmacro arguments you can create functions that will work for all your other spells Still, it's not very fun to have to type out all those different argument values. It would be nice to reduce the number of arguments necessary. If you follow the same pattern of naming your variables you can reduce the arguments taken to just one. Also you can add the globals declaration to the textmacro to further reduce how much coding you need to do. JASS://! textmacro PeriodicSpell_1 takes STRUCTNAME globals $STRUCTNAME$ array $STRUCTNAME$_ar timer $STRUCTNAME$_timer = CreateTimer() integer $STRUCTNAME$_total = 0 endglobals function RecycleStruct takes $STRUCTNAME$ dat, integer i returns nothing set $STRUCTNAME$_ar[i] = $STRUCTNAME$_ar[$STRUCTNAME$_total-1] set $STRUCTNAME$_total = $STRUCTNAME$_total - 1 call $STRUCTNAME$.destroy(dat) endfunction function StopTimer takes nothing returns nothing if $STRUCTNAME$_total == 0 then call PauseTimer($STRUCTNAME$_timer) endif endfunction //! endtextmacro //! textmacro PeriodicSpell_2 takes STRUCTNAME function Add2Struct takes $STRUCTNAME$ dat, real period returns nothing// the integer dat is the newly created struct. if $STRUCTNAME$_total == 0 then call TimerStart($STRUCTNAME$_timer, period, TRUE, function $STRUCTNAME$_timer_callback) endif set $STRUCTNAME$_total = $STRUCTNAME$_total + 1 set $STRUCTNAME$_ar[$STRUCTNAME$_total-1] = dat endfunction //! endtextmacro I split the textmacro into two because the Add2Struct function needs to be below the timer callback function (since that function calls the timer callback function) and the RecycleStruct and StopTimer functions must be above the timer callback function (since they are called in the timer callback function). There is just one more thing to consider. You may have noticed that the function names are constant in the textmacros we created. This means that if you run the textmacro more than once you will create two functions with the same name. Clearly that is a problem. There are several solutions to the problem. One method is to use a scope. All you need to do is put the entire set of functions for each spell inside its own scope and make the functions in the textmacro private. First make a library for your textmacros. JASS:library PeriodicSpellTextmacros //! textmacro PeriodicSpell_1 takes STRUCTNAME globals $STRUCTNAME$ array $STRUCTNAME$_ar timer $STRUCTNAME$_timer = CreateTimer() integer $STRUCTNAME$_total = 0 endglobals private function RecycleStruct takes $STRUCTNAME$ dat, integer i returns nothing set $STRUCTNAME$_ar[i] = $STRUCTNAME$_ar[$TOTAL$-1] set $STRUCTNAME$_total = $STRUCTNAME$_total - 1 call $STRUCTNAME$.destroy(dat) endfunction private function StopTimer takes nothing returns nothing if $STRUCTNAME$_total == 0 then call PauseTimer($STRUCTNAME$_timer) endif endfunction //! endtextmacro //! textmacro PeriodicSpell_2 takes STRUCTNAME private function Add2StructAr takes $STRUCTNAME$ dat, real period returns nothing// the integer dat is the newly created struct. if $STRUCTNAME$_total == 0 then call TimerStart($STRUCTNAME$_timer, period, TRUE, function $STRUCTNAME$_timer_callback) endif set $STRUCTNAME$_total = $STRUCTNAME$_total + 1 set $STRUCTNAME$_ar[$STRUCTNAME$_total-1] = dat endfunction //! endtextmacro endlibrary Now make your scope. Private functions inside a scope may only be called by other functions within the scope. JASS://! scope OpticalFlare struct OpticalFlare unit b unit sh real x real y boolean destroyplease = false endstruct //! runtextmacro PeriodicSpell_1("OpticalFlare") function OpticalFlare_timer_callback takes nothing returns nothing local integer i=0 local OpticalFlare dat loop exitwhen i==OpticalFlare_total set dat= OpticalFlare_ar[i] if (dat.destroyplease) then call RecycleStruct(dat, i) set i=i-1 else //do periodic actions endif set i=i+1 endloop call StopTimer() endfunction //! runtextmacro PeriodicSpell_2("OpticalFlare") function OpticalFlare_Effect takes unit b, integer l returns nothing local flaredata dat = flaredata.create() //set struct values call Add2StructAr(dat, .1) endfunction //! endscope If you want to add another set of functions just put them in their own scope. JASS://! scope other struct other unit caster integer time endstruct //! runtextmacro PeriodicSpell_1("other") function other_timer_callback takes nothing returns nothing local other dat local integer i loop exitwhen i == other_count set dat = other_ar[i] set dat.time = dat.time - 1 if dat.time == 0 then call RecycleStruct(dat, i) else //do actions set i = i + 1 endif endloop call StopTimer() endfunction //! runtextmacro PeriodicSpell_2("other") function createother takes unit u returns nothing local other dat = other.create() set dat.caster = u call Add2StructAr(dat, 1) endfunction //! endscope There is another way you could do this. Don't make functions inside the textmacros. Instead make four different textmacros. The first should have the globals. The last three should look like this: JASS://! textmacro Recycle takes STRUCTNAME set $STRUCTNAME$_ar[i] = $STRUCTNAME$_ar[$STRUCTNAME$_total-1] set $STRUCTNAME$_total = $STRUCTNAME$_total - 1 call $STRUCTNAME$.destroy(dat) //! endtextmacro //! textmacro Add takes STRUCTNAME if $STRUCTNAME$_total == 0 then call TimerStart($STRUCTNAME$_timer, period, TRUE, function $STRUCTNAME$_timer_callback) endif set $STRUCTNAME$_total = $STRUCTNAME$_total + 1 set $STRUCTNAME$_ar[$STRUCTNAME$_total-1] = dat //! endtextmacro //! textmacro Stop takes STRUCTNAME if $STRUCTNAME$_total == 0 then call PauseTimer($STRUCTNAME$_timer) endif //! endtextmacro Now you can run the textmacros within the function: JASS:function other_timer_callback takes nothing returns nothing local other dat local integer i loop exitwhen i == other_count set dat = other_ar[i] set dat.time = dat.time - 1 if dat.time == 0 then //! runtextmacro Recycle("other") else //do actions set i = i + 1 endif endloop //! runtextmacro Stop("other") endfunction function createother takes unit u returns nothing local other dat = other.create() set dat.caster = u //! runtextmacro Add("other") endfunction You probably see other parts of the code that you could make into a textmacro as well. On to the final section: III. Generic functions and textmacros Textmacros allow us to create many different useful functions from one generic function. Let's say that for some reason you have decided to make a set of functions that handle basic arithmetic calculations for you. You create four functions (+,-,*,/) that take integer values and four that take reals. Like so: JASS:function add_reals takes real a, real b returns real return (a + b) endfunction function sub_reals takes real a, real b returns real return (a-b) endfunction etc... function add_integers takes integer a, integer b returns integer return (a+b) endfunction etc... You can create these functions much faster using a textmacro JASS://! textmacro arithmetic takes NAME, OPERATOR, TYPE function $NAME$$TYPE$s takes $TYPE$ a, $TYPE$ b returns $TYPE$ return (a$OPERATOR$b) endfunction //! endtextmacro //! runtextmacro arithmetic("add","+","integer") //! runtextmacro arithmetic("sub","-","real") Another example: JASS://! textmacro compare takes TYPE function Compare_$TYPE$ takes $TYPE$ a, $TYPE$ b returns boolean return (a == b) endfunction //! endtextmacro //! runtextmacro compare("trigger") //! runtextmacro compare("unit") //! runtextmacro compare("handle") //! runtextmacro compare("real") Vexorian uses the following example in the JassHelper readme file. It shows how the same textmacro can quickly create all the local handle variable functions even though they all return a different type. JASS://! textmacro GetSetHandle takes TYPE, TYPENAME function GetHandle$TYPENAME$ takes handle h, string k returns $TYPE$ return GetStoredInteger(udg_handlevars, I2S(H2I(h)), k) return null endfunction function SetHandle$TYPENAME$ takes handle h, string k, $TYPE$ v returns nothing call StoredInteger(udg_handlevars,I2S(H2I(h)),k, H2I(v)) endfunction //! endtextmacro //! runtextmacro GetSetHandle("unit","Unit") //! runtextmacro GetSetHandle("location","Loc") //! runtextmacro GetSetHandle("item","Item") If you use this textmacro with either a real or integer type there will be a problem, because null (which is returned in the GetHandle$TYPENAME$ function) is not an integer or a real. To fix this you could either make another textmacro just for integers and reals (probably not worth the trouble), just code the integer and real functions manually (there are only two after all), or you could add another argument to the textmacro. JASS://! textmacro GetSetHandle takes TYPE, TYPENAME, RETURN function GetHandle$TYPENAME$ takes handle h, string k returns $TYPE$ return GetStoredInteger(udg_handlevars, I2S(H2I(h)), k) return $RETURN$ endfunction function SetHandle$TYPENAME$ takes handle h, string k, $TYPE$ v returns nothing call StoredInteger(udg_handlevars,I2S(H2I(h)),k, H2I(v)) endfunction //! endtextmacro //! runtextmacro GetSetHandle("unit","Unit","null") //! runtextmacro GetSetHandle("integer",Integer","0") There you have it. Hopefully you see how powerful textmacros can be. You should now be able to make your own. Be aware that using many textmacros (particularly those inside functions) can make your code much more difficult to read and understand, especially if you've forgotten what the textmacro was supposed to do. |
| 03-09-2007, 11:32 AM | #2 |
About time someone made a tut on textmacros.. I'll try to learn this when I can.. |
| 03-09-2007, 11:37 AM | #3 |
Probably lacks a section about generics I'd like to mention that I really hate textmacros. Think of them as a necessary evil. People should use them as the last resort, of course if you really want to you are free to abuse them like hell. |
| 03-09-2007, 09:42 PM | #4 |
When you are talking about generics, am I correct in assuming that you are referring to something like the SetGetHandle textmacro in the Jasshelper readme file? I was going to discuss that but it slipped my mind. I will add a section about using textmacros in that way. Edit: Updated the tutorial. |
| 03-11-2007, 09:20 PM | #5 |
Cool, approved. |
| 05-25-2007, 07:59 PM | #6 |
Nice tutorial, very useful. It's too bad that I don't know what to test it on........ About this part: JASS://! textmacro compare takes $TYPE$ function Compare_$TYPE$ takes $TYPE$ a, $TYPE$ b returns boolean return (a == b) endfunction //! endtextmacro //! runtextmacro compare("trigger") //! runtextmacro compare("unit") //! runtextmacro compare("handle") //! runtextmacro compare("real") in the first line, shouldn't it be TYPE instead of $TYPE$? |
| 06-02-2007, 09:32 PM | #7 |
That is correct. |
| 06-03-2007, 02:42 AM | #8 | |
Quote:
Whoops, all fixed. Thanks for pointing that out. |
