Fort Wars

Llama

Overview | The Making | The Code | Changelog

The Code Behind Fort Wars

Questions/Comments here, AOE3 Heavengames, AOM Heavengames, or AgeOfEmpires.com.

pftq (January 28, 2022):

     The complexity of the code behind Fort Wars is something underappreciated, even by me. I was 14 when I wrote most of it and didn't think much of it other than it being a hobby (a notion sadly reinforced by everyone around me in real-life at the time). It wasn't until much later that I realized a lot of the concepts were very interesting and worth sharing. Unlike scenarios made in an editor interface, the Fort Wars custom map is coded entirely by hand in Notepad2 (thanks Elpea for getting me off Notepad vanilla). To keep load times down and performance optimized, it uses almost exclusively XS injection, a technique of bypassing the trigger / map scripting language to another lower-level language underneath. This technique was first used by TwentyOneScore and then again by Matei for Norse Wars. In developing Fort Wars, I took this even further by utilizing kbQueries to create effects like scattering and other area effects that previously were considered impossible in AOE3's engine (and AOM's since AOE3 copies it). To add to that, AOE3 itself was already unsupportive of user-created mods/content, as its editor was utterly broken and many triggers from Age of Mythology simply did not work (the joke was Fort Wars was the most popular multiplayer custom map because it was the only one with actual functioning trigger / scripting work).

     Below are some examples of the code in the map developed to get around all of that and make things work. Keep in mind that, as we wrote much of it to be hardcoded for performance, it isn't possible to just copy over if you wanted to use it in your own maps, but the overall concepts should help you write your own.


XS Injection #

     The first thing we should talk about is XS injection, since almost all the effects were handled that way. XS injection lets us bypass the trigger system for performance but also so we can mix and match elements of trigger effects to create something new. Below is the function we call to insert code this way by using a trick where AOE3's game engine evaluates quotes and slashes in the Send Chat trigger effect incorrectly. It is similar actually to SQL injection or eval in Javascript and other languages. We are basically writing code that calls a different programming language underneath the main one that is normally available. It's somewhat of a Matrix within a Matrix sort of headspin.

void addXS(string code="") {
   rmAddTriggerEffect("Send Chat");
   rmSetTriggerEffectParam("Message", "*/"+code+"/*", false);
}

     Since we're now technically coding in a language underneath the main one, this can be taken further by exiting the function that is supposed to be parsing triggers / random map scripts. This is useful for writing new functions in that underlying language as well as declaring global variables that can be referenced anywhere in the script.

void beginGlobals() {
        rmSwitchToTrigger(rmCreateTrigger("globals_start_"+guid()));
        rmSetTriggerActive(false);
        rmAddTriggerEffect("Send Chat");
        rmSetTriggerEffectParam("Message", "\"); }} /*", false);
}

void endGlobals() {
   rmAddTriggerEffect("Send Chat");
   rmSetTriggerEffectParam("Message", "*/ rule _globals_end_"+guid()+" minInterval 4 inactive { if (1==0) { trChatSend(0, \"", false);
}

In combination, it looks like this for declaring a bunch of custom functions and variables at the start of the game (usually in your first trigger before everything else). You'll notice also we are defining a lot of functions that already exist - this is because the names were too long, and we needed to reduce the filesize to minimize partial/corrupt downloads (yes, a real problem we faced).

// Global XS code insertion
beginGlobals();

addXS("int CUIA(string u=\"\", int p=0, string U=\"\", int r=0) {");
addXS("return(trCountUnitsInArea(u,p,U,r));}");

addXS("int CUIA2(string u=\"\", int p=0) {");
addXS("return(trCountUnitsInArea(u,p,\"Unit\",6));}");

addXS("int ppop(int p=0) {");
addXS("return(trPlayerGetPopulation(p));}");

addXS("float checkQV(string u=\"\") {");
addXS("return(trQuestVarGet(u));}");

addXS("void setQV(string u=\"\", float x=0.0) {");
addXS("trQuestVarSet(u, x);}");

addXS("int xgetCiv(int p=0) {");
for(i=1; <=cNumberNonGaiaPlayers)
{
        addXS("if(p=="+i+") {return("+rmGetPlayerCiv(i)+");}");
}
addXS("return(0);");
addXS("}");

...
endGlobals();

     The last piece of the puzzle is the below Custom Effect that actually runs your custom code as an effect during the game. Without this, your custom code just exists, like an editor mod or custom patch, but never does anything. The hack here you'll notice is hilariously simple, as we don't even use quotes or slashes (it simply is parsing our parenthesis as evaluated code).

void custEffect(string xs="") {
        rmAddTriggerEffect("SetIdleProcessing");
        rmSetTriggerEffectParam("IdleProc", "true); "+xs+" trSetUnitIdleProcessing(true");
}

     When actually running your custom code, it looks like this. The XS code defining scatter we'll show further below, but you get the point.

rmSwitchToTrigger(rmTriggerID("NormalTrigger"));
custEffect("scatter(1);");

kb Queries / Spawning System #

     This is something new that came later in Fort Wars 3.0. It was first used by TwentyOneScore in Age of Mythology around 2003, but there wasn't much documentation and the knowledge was quickly lost after he was gone. It was rediscovered and greatly expanded on by me in 2007, and a lot of it was published as the Advanced Triggers Set. The findings were extremely useful to improve performance in Fort Wars and finally get past the long load times + lag that came with large numbers of players.

     This requires XS injection to do and takes it one step further by actually calling functions that don't exist to the main map scripting language. Below are some samples, but you can find full use and explanation of them in the Advanced Triggers Set.

     Fort Wars mainly uses kb queries in its spawning system, so that we do not need a trigger for every unit choice. We just need one query to know what unit is selected and then spawn that unit. This cuts down on lag significantly as you can combine a lot of the functionality you want to do into one function instead of many active looping events per player. Simply put, we can now reference and interact with anything on the map without knowing its existence in advance. See below:

// Spawning done in XS injection for performance
addXS("void spawnla(int p=0, int xarmy=0, int u=0, int locx=0, int locz=0, int spox=0, int spoz=0, float spmult=0.0) {");
addXS("int unitsNearSelector = CUIA2(\"\"+u, p);");
addXS("if(unitsNearSelector>=1&&unitsNearSelector<=2&&checkQV(\"PlayerStatus\"+p)==1&&ppop(p)<checkQV(\"popcap\"+p)) {");
//addXS("trChatSendToPlayer(0, p, \"Units near hut: \"+unitsNearSelector+\", PlayerStatus: \"+checkQV(\"PlayerStatus\"+p)+\", Pop: \"+ppop(p)+\"/\"+checkQV(\"popcap\"+p));");
addXS("currentPlayerContext=xsGetContextPlayer(); xsSetContextPlayer(p); kbLookAtAllUnitsOnMap();");
addXS("int kbQueryID=kbUnitQueryCreate(\"spawn\"+p);");
addXS("kbUnitQuerySetPlayerID(kbQueryID, p);");
addXS("kbUnitQuerySetUnitType(kbQueryID, objID_UnitAllTypes);");
addXS("kbUnitQuerySetState(kbQueryID, 2);");
addXS("kbUnitQuerySetPosition(kbQueryID, kbUnitGetPosition(u));");
//addXS("trChatSendToPlayer(0, p, \"Spawn query loc: \"+kbUnitGetPosition(u));");
addXS("kbUnitQuerySetMaximumDistance(kbQueryID, 6);");
addXS("kbUnitQueryResetResults(kbQueryID);");
addXS("int queryUnitCt=kbUnitQueryExecute(kbQueryID);");
//addXS("trChatSendToPlayer(0, p, \"Spawn query results: \"+queryUnitCt);");
addXS("for(i=0;<queryUnitCt) {");
addXS("string spawnUnitName=kbGetProtoUnitName(kbGetUnitBaseTypeID(kbUnitQueryGetResult(kbQueryID, i)));");
//addXS("trChatSendToPlayer(0, p, \"Spawning: \"+spawnUnitName);");
addXS("if(trTime()-checkQV(\"SpawnTime\"+p+\"_\"+spawnUnitName)>=spawnXSTime(spawnUnitName)*spmult) {");
addXS("int spawnAmt=spawnXSCount(spawnUnitName);");
addXS("if(unitsNearSelector==1) spawnAmt=2*spawnAmt;");
addXS("trArmyDispatch(p+\",\"+xarmy, spawnUnitName, spawnAmt, locx,1,locz, 0, true);");
addXS("trArmySelect(p+\",\"+xarmy);");
addXS("trUnitMoveToPoint(spox,1,spoz, -1, true, true, 10);");
addXS("setQV(\"SpawnTime\"+p+\"_\"+spawnUnitName, trTime());");
addXS("}}xsSetContextPlayer(currentPlayerContext);");
addXS("}}");

Scatter Effect / Stop Effect #

     Now that you know what we are capable of, we can have some fun. Using the above methods, we created numerous special abilities and powers unique to Fort Wars. Below is an example custom effect created with XS injection and the kb query system to scatter enemy units when the "Scatter Bear" power is used. As this is the addXS function, it means it exists outside the game and needs to be called with custEffect later (described above).

addXS("void scatter(int v=0) {");
addXS("scatbug=unitquery(v, objID_Grizzly);");
addXS("currentPlayerContext=xsGetContextPlayer(); xsSetContextPlayer(0); kbLookAtAllUnitsOnMap();");
addXS("for(p=1;<"+cNumberPlayers+") {");
addXS("if(p!=v) {");
addXS("scatherug=kbUnitQueryCreate(\"vaarg\");");
addXS("kbUnitQuerySetPlayerID(scatherug, p);");
addXS("kbUnitQuerySetUnitType(scatherug, objID_UnitAllTypes);");
addXS("kbUnitQuerySetState(scatherug, 2);");
addXS("kbUnitQuerySetPosition(scatherug, kbUnitGetPosition(scatbug));");
addXS("kbUnitQuerySetMaximumDistance(scatherug, 15);");
addXS("kbUnitQuerySetAscendingSort(scatherug, true);");
addXS("kbUnitQueryResetResults(scatherug);");
addXS("scatres=kbUnitQueryExecute(scatherug);");
addXS("if(scatres>20) scatres=20;");
addXS("for(i=0;<scatres) {");
addXS("int revunit=kbUnitQueryGetResult(scatherug, i);");
addXS("trUnitSelectClear(); trUnitSelectByID(revunit);");
addXS("trQuestVarSetFromRand(\"fiddler\", 0, scatrange, true);");
addXS("trQuestVarSetFromRand(\"fiddlera\", 0, 1, true); fiddla=checkQV(\"fiddlera\");");
addXS("if(fiddla==0) fiddle=xsVectorGetX(kbUnitGetPosition(revunit))+checkQV(\"fiddler\");");
addXS("if(fiddla==1) fiddle=xsVectorGetX(kbUnitGetPosition(revunit))-checkQV(\"fiddler\");");
addXS("trQuestVarSetFromRand(\"fiddlera\", 0, 1, true); fiddla=checkQV(\"fiddlera\");");
addXS("scatmag=scatrange-checkQV(\"fiddler\");");
addXS("if(fiddla==0) sticks=xsVectorGetZ(kbUnitGetPosition(revunit))+scatmag;");
addXS("if(fiddla==1) sticks=xsVectorGetZ(kbUnitGetPosition(revunit))-scatmag;");
addXS("if(isStealth(revunit)!=\"\") trUnitChangeProtoUnit(isStealth(revunit));");
addXS("trUnitMoveToPoint(fiddle, xsVectorGetY(kbUnitGetPosition(revunit)), sticks, -1, false, true, 9999);");
addXS("trUnitHighlight(5, true);");
addXS("}}}xsSetContextPlayer(currentPlayerContext);}");

     This same trick can be used to stop units from moving, which became useful for nullifying the auto-formations behavior in AOE3 affecting units on the store/spawn islands.

addXS("void unitstopper(int p=0, float unito1=0, float unito2=0, int radius=0) {");
//addXS("trChatSendToPlayer(0, p, \"Units stopped.\");");
addXS("int lastpl = xsGetContextPlayer(); xsSetContextPlayer(p); kbLookAtAllUnitsOnMap();");
addXS("int revquery=kbUnitQueryCreate(\"unitstopper\"+p);");
addXS("kbUnitQuerySetPlayerID(revquery, p);");
addXS("kbUnitQuerySetUnitType(revquery, objID_UnitAllTypes);");
addXS("kbUnitQuerySetPosition(revquery, xsVectorSet(unito1,0,unito2));");
addXS("kbUnitQuerySetMaximumDistance(revquery, radius);");
addXS("kbUnitQuerySetState(revquery, 2);");
addXS("kbUnitQueryResetResults(revquery);");
addXS("int revresults=kbUnitQueryExecute(revquery);");
addXS("for(i=0;<revresults) {");
addXS("int revunit=kbUnitQueryGetResult(revquery, i);");
addXS("trUnitSelectClear(); trUnitSelectByID(revunit);");
addXS("int x=xsVectorGetX(kbUnitGetPosition(revunit));");
addXS("int y=xsVectorGetY(kbUnitGetPosition(revunit));");
addXS("int z=xsVectorGetZ(kbUnitGetPosition(revunit));");
addXS("trUnitMoveToPoint(x,y,z);");
addXS("}xsSetContextPlayer(lastpl);}");

Upgrade System #

     AOE3's modify protounit trigger didn't work, so we had to be creative here. The upgrade system used a patchwork of existing AOE3 tech upgrades and manually modifying HP stats with modify protounit to cancel out the effects we didn't want. Elpea discovered a neat trick where you can apply the same tech upgrade twice if you first reset it to unobtainable status and then active status. The whole thing is written as XS injection code as well, so we can re-use the same function for every upgrade. You'll notice it's actually a function first written in RMS that then writes a function in XS injection. Just like with the scatter effect, it does nothing until actually called during the game with custEffect. Lastly, you'll notice we have XS versions of the normal trigger effects like Modify Protounit because we're not actually in the surface-level trigger system anymore and need to call the underlying code the normally runs when Modify Protounit is used - which is faster anyway and only leads to more performance gains.

void upgradesXS(string upgtype="") {

        addXS("void upgrade"+upgtype+"(int p=0, float num=1.0) {");
        
                string xtimes="num*";
                
                if((upgtype=="Attack")||(upgtype=="Multipliers")) {
                
                                addXS("for(var1=1;<=num) {");
                                        actTechXS(      HCArtilleryCombatFrench, HCArtilleryCombatFrench, HCXPInfantryDamageIroquois, HCXPCavalryDamageSPC, HCXPInfantryDamageIroquois, HCXPCavalryDamageSPC);
                                        stackTechXS( HCNativeCombat, "3"); // 0.3x=0.25y+0.3^; 0.3^ is from above
                                        //Super Units
                                        stackTechXS(GuardOprichniks, ""+(UberTech("Oprichnik")-1));
                                        modProtoXS("Oprichnik", 0, "-0.3*"+(UberTech("Oprichnik")-1)+"*"+baseHP("Oprichnik"));
                                        stackTechXS(WarriorSocietyInca, ""+UberTech("NatBolasWarrior")); // Super Bolas
                                        modProtoXS("NatBolasWarrior", 0, "-1.0*"+UberTech("NatBolasWarrior")+"*0.25*"+baseHP("NatBolasWarrior")); // cancels hp
                                        stackTechXS(VeteranCrossbowmen, ""+UberTech("Crossbowman"));
                                        modProtoXS("Crossbowman", 0, "-1.0*"+UberTech("Crossbowman")+"*0.2*"+baseHP("Crossbowman"));
                                addXS("}");
                                for(var1=1;<=gameunitnum) {
                                                if(gameUnits(var1, "Artillery")!=""&&gameUnits(var1, "Cannons")!="") {
                                                        modProtoXS(gameUnits(var1), 0, upgAttack+xtimes+baseHP(gameUnits(var1)));
                                                }
                                                if(gameUnits(var1, "Native")!="") {
                                                        modProtoXS(gameUnits(var1), 0, "-3.0*0.25*"+xtimes+baseHP(gameUnits(var1)));
                                                }
                                }
                }
                if((upgtype=="HP")||(upgtype=="Multipliers")) {
                        addXS("if(num>=1) {");
                        addXS("for(var1=1;<=num) {");
                                actTechXS(HCXPInfantryHitpointsIroquois, HCXPCavalryHitpointsIroquois, HCXPInfantryHitpointsIroquois, HCXPCavalryHitpointsIroquois);
                        addXS("}");
                        addXS("}");
                                for(var1=1;<=gameunitnum) {
                                        if(gameUnits(var1, "HInf")!="") {
                                                addXS("if(xgetCiv(p)=="+civAztec+") {");
                                                        modProtoXS(gameUnits(var1), 0, upgHP+xtimes+aztechp+"*"+baseHP(gameUnits(var1)));
                                                        if(gameUnits(var1)=="xpSkullKnight") {
                                                                modProtoXS(gameUnits(var1), 0, upgAttack+xtimes+aztechp+"*100");
                                                        }
                                                addXS("}");
                                        }
                                        if(gameUnits(var1, "Spawn")!=""&&gameUnits(var1, "Artillery")=="") {
                                                addXS("if(xgetCiv(p)=="+civRussian+") {");
                                                        modProtoXS(gameUnits(var1), 0, upgHP+xtimes+"(0.0-"+rusweak+")*"+baseHP(gameUnits(var1)));
                                                        if(gameUnits(var1)=="xpSkullKnight") {
                                                                modProtoXS(gameUnits(var1), 0, upgHP+xtimes+"(0.0+"+rusweak+")*100");
                                                        }
                                                addXS("}");
                                        }
                                        if(gameUnits(var1, "Artillery")!=""&&gameUnits(var1, "HeavyArt")==""&&gameUnits(var1, "Cannons")!="") {
                                                addXS("if(xgetCiv(p)!="+civRussian+") {");
                                                        modProtoXS(gameUnits(var1), 0, upgHP+xtimes+baseHP(gameUnits(var1)));
                                                addXS("}");
                                                addXS("if(xgetCiv(p)=="+civRussian+") {");
                                                        modProtoXS(gameUnits(var1), 0, upgHP+xtimes+"(1.0-"+rusweak+")*"+baseHP(gameUnits(var1)));
                                                addXS("}");
                                        }
                                        if(gameUnits(var1, "Spawn")!=""&&gameUnits(var1, "HeavyArt")!="") {
                                                addXS("if(xgetCiv(p)!="+civRussian+") {");
                                                        modProtoXS(gameUnits(var1), 0, upgHP+xtimes+"("+baseHP(gameUnits(var1))+"-200)");
                                                addXS("}");
                                                addXS("if(xgetCiv(p)=="+civRussian+") {");
                                                        modProtoXS(gameUnits(var1), 0, upgHP+xtimes+"((1.0-"+rusweak+")*("+baseHP(gameUnits(var1))+"-200))");
                                                addXS("}");
                                        }
                                        if(gameUnits(var1, "Native")!="") {
                                                modProtoXS(gameUnits(var1), 0, upgHP+xtimes+"("+NativeTechLoop+"*(0.25*"+baseHP(gameUnits(var1))+"))");
                                        }
                                        if(gameUnits(var1, "Other")!="") {
                                                modProtoXS(gameUnits(var1), 0, upgHP+xtimes+baseHP(gameUnits(var1)));
                                        }
                                        if(gameUnits(var1, "Explorer")!="") {
                                                modProtoXS(gameUnits(var1), 0, upgHP+xtimes+exphp+"*"+baseHP(gameUnits(var1)));
                                        }
                                }
                                modProtoXS("OutpostWagon", 0, upgHP+xtimes+"550"); // 550 extra HP in FW
                                modProtoXS("Crossbowman", 0, upgHP+xtimes+"500");
                                modProtoXS("xpSkullKnight", 0, upgAttack+xtimes+"100");
                                modProtoXS("NatBolasWarrior", 0, upgHP+xtimes+"2050");
                                modProtoXS("MercRonin", 0, upgHP+xtimes+UberTech("MercRonin"));
                                modProtoXS("RussianCannon", 0, upgHP+xtimes+"400");
                                modProtoXS("MercGreatCannon", 0, upgHP+xtimes+"300");
                                modProtoXS("Oprichnik", 0, upgHP+xtimes+"(2.0*"+baseHP("Oprichnik")+")");
                                //modProtoXS("xpColonialMilitia", 0, upgHP+xtimes+"3*"+baseHP("xpColonialMilitia"));
                                //modProtoXS("xpPetard", 0, upgHP+xtimes+"(-75)"); // HP base was lowered // too dangerous if AOE3 changes the HP
                                modProtoXS("PetGrizzly", 0, upgHP+xtimes+baseHP("PetGrizzly"));
                                modProtoXS("SPCWhiteBuffalo", 0, upgHP+xtimes+baseHP("SPCWhiteBuffalo"));
                                modProtoXS("GeorgeCrushington", 0, upgAttack+xtimes+"("+baseHP("GeorgeCrushington")+"-"+UberTech("GeorgeCrushington")+")");
                                modProtoXS("xpRam",  0, upgHP+xtimes+"0.5*"+baseHP("xpRam"));
                }
                if(upgtype=="Speed") {

                                for(var1=1;<=gameunitnum) {
                                        if(gameUnits(var1)!="") {
                                                modProtoXS(gameUnits(var1), 1, xtimes+upgSpeed);
                                        }
                                }
                }
                if(upgtype=="LOS") {

                        for(var1=1;<=gameunitnum) {
                                if(gameUnits(var1)!=""&&gameUnits(var1)!="Llama") {
                                        modProtoXS(gameUnits(var1), 2, xtimes+"4");
                                }
                        }
                        stackTechXS(ChurchGasLighting, "num"); // +4 LOS Buildings
                }
                if(upgtype=="BuildingHP") {

                        modQVXS("FortHP\"+p+\"", "+", xtimes+"0.3*"+forthp);
                        modProtoXS("FortFrontier", 0, xtimes+"0.3*"+forthp);
                        modProtoXS("Outpost", 0, xtimes+"0.3*"+towerhp);
                }
                if(upgtype=="BuildingAttack") { //25% off current upgrades, 100% of normal
                        modProtoXS("FortFrontier", 2, "-2*(num*6)*"+fortatk); // -6 los
                        modProtoXS("FortFrontier", 14, "-2*(num*6)*"+fortatk); // -6 Range
                        stackTechXS( FrontierOutpost, "num*6"); // 6*.5=0.25*4.0
                        stackTechXS( Revetment, "num*2*"+fortatk); // +6 Range and LOS for Fort too, 50% upgrades off base
                }
        addXS("}");
        
}

     The stackTechXS and modProtoXS calls are shorthand XS versions of the tech upgrade and modify protounit trigger effects, including automatically setting techs to unobtainable so they stack:

// Modify Protounit Global
void modProtoXS(string modunit="", int modfield=0, string modval="") {
        addXS("trModifyProtounit(\""+modunit+"\", p, "+modfield+", "+modval+");");
}

// Set Tech X Global
void setTechxXS(int techname=0, string status="") {
        int statusNum = 0;
        if(status == "Active") {statusNum = 2;}
        addXS("trTechSetStatus(p, "+techname+", "+statusNum+");");
}

// Stacking Techs Global
void stackTechXS(int techname=0, string num="") {
        addXS("for(i=1;<="+num+") {");
                setTechxXS(techname, "Unobtainable");
                setTechxXS(techname, "Active");
        addXS("}");
}

Unit Type IDs #

     A drawback to kb queries is it's very hardcoded - you use IDs for unit types and techs instead of the display names, etc. You'll notice we have a variable used in each query called objID_UnitAllTypes, which has to be defined as a number. This is also for every power we create represented by another object, such as the PetGrizzly bear, SPCWhiteBuffalo unit, etc. This led to every other AOE3 patch breaking Fort Wars because the IDs would keep getting changed. A method I came up with later to get around this uses XS and kb queries to actually search for the correct IDs at the start of the game and save them.

addXS("int objID_UnitAllTypes = "+objID_UnitAllTypes+";");
addXS("int objID_Grizzly = "+objID_Grizzly+";");
addXS("int objID_WhiteBuff = "+objID_WhiteBuff+";");

addXS("void fixObjectIDs() {");

addXS("if(kbGetUnitTypeName(objID_UnitAllTypes)!=\"Unit\") {");
addXS("while(kbGetUnitTypeName(objID_UnitAllTypes)!=\"Unit\" && objID_UnitAllTypes<=9999) { objID_UnitAllTypes=objID_UnitAllTypes+1; }"); 
addXS("while(kbGetUnitTypeName(objID_Grizzly)!=\"PetGrizzly\" && objID_Grizzly<=9999) { objID_Grizzly=objID_Grizzly+1; }"); 
addXS("while(kbGetUnitTypeName(objID_WhiteBuff)!=\"SPCWhiteBuffalo\" && objID_WhiteBuff<=9999) { objID_WhiteBuff=objID_WhiteBuff+1; }"); 

//addXS("trChatSendToPlayer(0, 1, \"New Unit ID: \"+objID_UnitAllTypes);"); addXS("trChatSendToPlayer(0, 1, \"New Grizzly ID: \"+objID_Grizzly);"); addXS("trChatSendToPlayer(0, 1, \"New WhiteBuff ID: \"+objID_WhiteBuff);");

addXS("}");
addXS("}");

The Small Details #

     Lastly, AOM's scripting language lacks many basic programming constructs like arrays and math functions (at the time anyway). Luckily, since I didn't know calculus yet, Matei wrote these for his own Norse Wars which then were re-used for Fort Wars, essentially extending AOE3's programming language to be more full-fledged. AOE3 actually did have arrays through some obscure xsArray- functions undocumented, but they were cumbersome to type and we came up with shorthands for thoe too. And lastly let's not forget Elpea's massive functions for storing unit costs and display names, which were basically massive if-then statements generated from the AOE3 game files via his SQL/PHP prowess. :P Those basically replicated the functionality of a Dictionary/Hashtable which didn't exist either.

// MATH FUNCTIONS

float PI = 3.1415926535897932384626433832795;

float powc(float x = 0,int p = 0) {
        float x2 = 1;
        float x4 = 1;
        float x8 = 1;
        float x16 = 1;
        float x32 = 1;
        float x64 = 1;

        if(p>=2) x2 = x*x;
        if(p>=4) x4 = x2*x2;
        if(p>=8) x8 = x4*x4;
        if(p>=16) x16 = x8*x8;
        if(p>=32) x32 = x16*x16;
        if(p>=64) x64 = x32*x32;

        float ret = 1;

        while(p>=64) {
                ret = ret * x64;
                p = p - 64;
        }
        if(p>=32) {
                ret = ret * x32;
                p = p - 32;
        }
        if(p>=16) {
                ret = ret * x16;
                p = p - 16;
        }
        if(p>=8) {
                ret = ret * x8;
                p = p - 8;
        }
        if(p>=4) {
                ret = ret * x4;
                p = p - 4;
        }
        if(p>=2) {
                ret = ret * x2;
                p = p - 2;
        }
        if(p>=1) {
                ret = ret * x;
                p = p - 1;
        }

        return (ret);
}

float atanc(float n = 0) {
        float m = n;
        if(n > 1) m = 1.0 / n;
        if(n < -1) m = -1.0 / n;
        float r = m;
        for(i = 1; < 100) {
                int j = i * 2 + 1;
                float k = powc(m,j) / j;
                if(k == 0) break;
                if(i % 2 == 0) r = r + k;
                if(i % 2 == 1) r = r - k;
        }
        if(n > 1 || n < -1) r = PI / 2.0 - r;
        if(n < -1) r = 0.0 - r;
        return (r);
}

float atan2c(float z = 0,float x = 0) {
        if(x > 0) return (atanc(z / x));
        if(x < 0) {
                if(z < 0) return (atanc(z / x) - PI);
                if(z > 0) return (atanc(z / x) + PI);
                return (PI);
        }
        if(z > 0) return (PI / 2.0);
        if(z < 0) return (0.0 - (PI / 2.0));
        return (0);
}

float factorial(float n = 0) {
        float r = 1;
        for(i = 2; <= n) {
                r = r * i;
        }
        return (r);
}

float cosc(float n = 0) {
        float r = 1;
        for(i = 1; < 100) {
                int j = i * 2;
                float k = powc(n,j) / factorial(j);
                if(k == 0) break;
                if(i % 2 == 0) r = r + k;
                if(i % 2 == 1) r = r - k;
        }
        return (r);
}

float sinc(float n = 0) {
        float r = n;
        for(i = 1; < 100) {
                int j = i * 2 + 1;
                float k = powc(n,j) / factorial(j);
                if(k == 0) break;
                if(i % 2 == 0) r = r + k;
                if(i % 2 == 1) r = r - k;
        }
        return (r);
}

// Arrays
int getInt(int army=0, int val=0) {
        return(xsArrayGetInt(army, val));
}
float getFl(int army=0, int val=0) {
        return(xsArrayGetFloat(army, val));
}
string getString(int army=0, int val=0) {
        return(xsArrayGetString(army, val));
}
void setInt(int army=0, int xval=0, int val=0) {
        xsArraySetInt(army, xval, val);
}
void setFl(int army=0, int xval=0, float val=0) {
        xsArraySetFloat(army, xval, val);
}
void setString(int army=0, int xval=0, string val="") {
        xsArraySetString(army, xval, val);
}

Last Words...

        All this code has been buried for some 16 years, as we obfuscate the map on each release to prevent hacking/copying (which was rampant in 2006). That itself is another process and big thanks to Mokon who created the tools. In any case, I hope people out there find this helpful and interesting. If you have any questions or comments, discuss them here, at AOE3 Heavengames, at AOM Heavengames, or at AgeOfEmpires.com.

-pftq