LCOV - code coverage report
Current view: top level - simulation/components - Formation.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 313 508 61.6 %
Date: 2023-04-02 12:52:40 Functions: 25 47 53.2 %

          Line data    Source code
       1             : function Formation() {}
       2             : 
       3           2 : Formation.prototype.Schema =
       4             :         "<element name='RequiredMemberCount' a:help='Minimum number of entities the formation should contain (at least 2).'>" +
       5             :                 "<data type='integer'>" +
       6             :                   "<param name='minInclusive'>"+
       7             :                     "2"+
       8             :                   "</param>"+
       9             :                 "</data>" +
      10             :         "</element>" +
      11             :         "<element name='DisabledTooltip' a:help='Tooltip shown when the formation is disabled.'>" +
      12             :                 "<text/>" +
      13             :         "</element>" +
      14             :         "<element name='SpeedMultiplier' a:help='The speed of the formation is determined by the minimum speed of all members, multiplied with this number.'>" +
      15             :                 "<ref name='nonNegativeDecimal'/>" +
      16             :         "</element>" +
      17             :         "<element name='FormationShape' a:help='Formation shape, currently supported are square, triangle and special, where special will be defined in the source code.'>" +
      18             :                 "<text/>" +
      19             :         "</element>" +
      20             :         "<element name='MaxTurningAngle' a:help='The turning angle in radian under which the formation attempts to turn and over which the formation positions are recomputed.'>" +
      21             :                 "<ref name='nonNegativeDecimal'/>" +
      22             :         "</element>" +
      23             :         "<element name='ShiftRows' a:help='Set the value to true to shift subsequent rows.'>" +
      24             :                 "<text/>" +
      25             :         "</element>" +
      26             :         "<element name='SortingClasses' a:help='Classes will be added to the formation in this order. Where the classes will be added first depends on the formation.'>" +
      27             :                 "<text/>" +
      28             :         "</element>" +
      29             :         "<optional>" +
      30             :                 "<element name='SortingOrder' a:help='The order of sorting. This defaults to an order where the formation is filled from the first row to the last, and each row from the center to the sides. Other possible sort orders are “fillFromTheSides”, where the most important units are on the sides of each row, and “fillToTheCenter”, where the most vulnerable units are in the center of the formation.'>" +
      31             :                         "<text/>" +
      32             :                 "</element>" +
      33             :         "</optional>" +
      34             :         "<element name='WidthDepthRatio' a:help='Average width-to-depth ratio, counted in number of units.'>" +
      35             :                 "<ref name='nonNegativeDecimal'/>" +
      36             :         "</element>" +
      37             :         "<element name='Sloppiness' a:help='The maximum difference between the actual and the perfectly aligned formation position, in meters.'>" +
      38             :                 "<ref name='nonNegativeDecimal'/>" +
      39             :         "</element>" +
      40             :         "<optional>" +
      41             :                 "<element name='MinColumns' a:help='When possible, this number of colums will be created. Overriding the wanted width-to-depth ratio.'>" +
      42             :                         "<data type='nonNegativeInteger'/>" +
      43             :                 "</element>" +
      44             :         "</optional>" +
      45             :         "<optional>" +
      46             :                 "<element name='MaxColumns' a:help='When possible within the number of units, and the maximum number of rows, this will be the maximum number of columns.'>" +
      47             :                         "<data type='nonNegativeInteger'/>" +
      48             :                 "</element>" +
      49             :         "</optional>" +
      50             :         "<optional>" +
      51             :                 "<element name='MaxRows' a:help='The maximum number of rows in the formation.'>" +
      52             :                         "<data type='nonNegativeInteger'/>" +
      53             :                 "</element>" +
      54             :         "</optional>" +
      55             :         "<optional>" +
      56             :                 "<element name='CenterGap' a:help='The size of the central gap, expressed in number of units wide.'>" +
      57             :                         "<ref name='nonNegativeDecimal'/>" +
      58             :                 "</element>" +
      59             :         "</optional>" +
      60             :         "<element name='UnitSeparationWidthMultiplier' a:help='Place the units in the formation closer or further to each other. The standard separation is the footprint size.'>" +
      61             :                 "<ref name='nonNegativeDecimal'/>" +
      62             :         "</element>" +
      63             :         "<element name='UnitSeparationDepthMultiplier' a:help='Place the units in the formation closer or further to each other. The standard separation is the footprint size.'>" +
      64             :                 "<ref name='nonNegativeDecimal'/>" +
      65             :         "</element>" +
      66             :         "<element name='AnimationVariants' a:help='Give a list of animation variants to use for the particular formation members, based on their positions.'>" +
      67             :                 "<text a:help='example text: “1..1,1..-1:animationVariant1;2..2,1..-1;animationVariant2”, this will set animationVariant1 for the first row, and animation2 for the second row. The first part of the numbers (1..1 and 2..2) means the row range. Every row between (and including) those values will switch animationvariants. The second part of the numbers (1..-1) denote the columns inside those rows that will be affected. Note that in both cases, you can use -1 for the last row/column, -2 for the second to last, etc.'/>" +
      68             :         "</element>";
      69             : 
      70             : // Distance at which we'll switch between column/box formations.
      71           2 : var g_ColumnDistanceThreshold = 128;
      72             : 
      73             : // Distance under which the formation will not try to turn towards the target position.
      74           2 : var g_RotateDistanceThreshold = 1;
      75             : 
      76           2 : Formation.prototype.variablesToSerialize = [
      77             :         "lastOrderVariant",
      78             :         "members",
      79             :         "memberPositions",
      80             :         "maxRowsUsed",
      81             :         "maxColumnsUsed",
      82             :         "finishedEntities",
      83             :         "idleEntities",
      84             :         "columnar",
      85             :         "rearrange",
      86             :         "formationMembersWithAura",
      87             :         "width",
      88             :         "depth",
      89             :         "twinFormations",
      90             :         "formationSeparation",
      91             :         "offsets"
      92             : ];
      93             : 
      94           2 : Formation.prototype.Init = function(deserialized = false)
      95             : {
      96           5 :         this.maxTurningAngle = +this.template.MaxTurningAngle;
      97           5 :         this.sortingClasses = this.template.SortingClasses.split(/\s+/g);
      98           5 :         this.shiftRows = this.template.ShiftRows == "true";
      99           5 :         this.separationMultiplier = {
     100             :                 "width": +this.template.UnitSeparationWidthMultiplier,
     101             :                 "depth": +this.template.UnitSeparationDepthMultiplier
     102             :         };
     103           5 :         this.sloppiness = +this.template.Sloppiness;
     104           5 :         this.widthDepthRatio = +this.template.WidthDepthRatio;
     105           5 :         this.minColumns = +(this.template.MinColumns || 0);
     106           5 :         this.maxColumns = +(this.template.MaxColumns || 0);
     107           5 :         this.maxRows = +(this.template.MaxRows || 0);
     108           5 :         this.centerGap = +(this.template.CenterGap || 0);
     109             : 
     110           5 :         if (this.template.AnimationVariants)
     111             :         {
     112           0 :                 this.animationvariants = [];
     113           0 :                 let differentAnimationVariants = this.template.AnimationVariants.split(/\s*;\s*/);
     114             :                 // Loop over the different rectangulars that will map to different animation variants.
     115           0 :                 for (let rectAnimationVariant of differentAnimationVariants)
     116             :                 {
     117             :                         let rect, replacementAnimationVariant;
     118           0 :                         [rect, replacementAnimationVariant] = rectAnimationVariant.split(/\s*:\s*/);
     119             :                         let rows, columns;
     120           0 :                         [rows, columns] = rect.split(/\s*,\s*/);
     121             :                         let minRow, maxRow, minColumn, maxColumn;
     122           0 :                         [minRow, maxRow] = rows.split(/\s*\.\.\s*/);
     123           0 :                         [minColumn, maxColumn] = columns.split(/\s*\.\.\s*/);
     124           0 :                         this.animationvariants.push({
     125             :                                 "minRow": +minRow,
     126             :                                 "maxRow": +maxRow,
     127             :                                 "minColumn": +minColumn,
     128             :                                 "maxColumn": +maxColumn,
     129             :                                 "name": replacementAnimationVariant
     130             :                         });
     131             :                 }
     132             :         }
     133             : 
     134           5 :         this.lastOrderVariant = undefined;
     135             :         // Entity IDs currently belonging to this formation.
     136           5 :         this.members = [];
     137           5 :         this.memberPositions = {};
     138           5 :         this.maxRowsUsed = 0;
     139           5 :         this.maxColumnsUsed = [];
     140             :         // Entities that have finished the original task.
     141           5 :         this.finishedEntities = new Set();
     142           5 :         this.idleEntities = new Set();
     143             :         // Whether we're travelling in column (vs box) formation.
     144           5 :         this.columnar = false;
     145             :         // Whether we should rearrange all formation members.
     146           5 :         this.rearrange = true;
     147             :         // Members with a formation aura.
     148           5 :         this.formationMembersWithAura = [];
     149           5 :         this.width = 0;
     150           5 :         this.depth = 0;
     151           5 :         this.twinFormations = [];
     152             :         // Distance from which two twin formations will merge into one.
     153           5 :         this.formationSeparation = 0;
     154             : 
     155           5 :         if (deserialized)
     156           0 :                 return;
     157             : 
     158           5 :         Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer)
     159             :                 .SetInterval(this.entity, IID_Formation, "ShapeUpdate", 1000, 1000, null);
     160             : };
     161             : 
     162           2 : Formation.prototype.Serialize = function()
     163             : {
     164           0 :         let result = {};
     165           0 :         for (let key of this.variablesToSerialize)
     166           0 :                 result[key] = this[key];
     167             : 
     168           0 :         return result;
     169             : };
     170             : 
     171           2 : Formation.prototype.Deserialize = function(data)
     172             : {
     173           0 :         this.Init(true);
     174           0 :         for (let key in data)
     175           0 :                 this[key] = data[key];
     176             : };
     177             : 
     178             : /**
     179             :  * Set the value from which two twin formations will become one.
     180             :  */
     181           2 : Formation.prototype.SetFormationSeparation = function(value)
     182             : {
     183           0 :         this.formationSeparation = value;
     184             : };
     185             : 
     186           2 : Formation.prototype.GetSize = function()
     187             : {
     188           0 :         return { "width": this.width, "depth": this.depth };
     189             : };
     190             : 
     191           2 : Formation.prototype.GetSpeedMultiplier = function()
     192             : {
     193           4 :         return +this.template.SpeedMultiplier;
     194             : };
     195             : 
     196           2 : Formation.prototype.GetMemberCount = function()
     197             : {
     198           0 :         return this.members.length;
     199             : };
     200             : 
     201           2 : Formation.prototype.GetMembers = function()
     202             : {
     203          16 :         return this.members;
     204             : };
     205             : 
     206           2 : Formation.prototype.GetClosestMember = function(ent, filter)
     207             : {
     208           0 :         let cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
     209           0 :         if (!cmpEntPosition || !cmpEntPosition.IsInWorld())
     210           0 :                 return INVALID_ENTITY;
     211             : 
     212           0 :         let entPosition = cmpEntPosition.GetPosition2D();
     213           0 :         let closestMember = INVALID_ENTITY;
     214           0 :         let closestDistance = Infinity;
     215           0 :         for (let member of this.members)
     216             :         {
     217           0 :                 if (filter && !filter(ent))
     218           0 :                         continue;
     219             : 
     220           0 :                 let cmpPosition = Engine.QueryInterface(member, IID_Position);
     221           0 :                 if (!cmpPosition || !cmpPosition.IsInWorld())
     222           0 :                         continue;
     223             : 
     224           0 :                 let pos = cmpPosition.GetPosition2D();
     225           0 :                 let dist = entPosition.distanceToSquared(pos);
     226           0 :                 if (dist < closestDistance)
     227             :                 {
     228           0 :                         closestMember = member;
     229           0 :                         closestDistance = dist;
     230             :                 }
     231             :         }
     232           0 :         return closestMember;
     233             : };
     234             : 
     235             : /**
     236             :  * Returns the 'primary' member of this formation (typically the most
     237             :  * important unit type), for e.g. playing a representative sound.
     238             :  * Returns undefined if no members.
     239             :  * TODO: Actually implement something like that. Currently this just returns
     240             :  * the arbitrary first one.
     241             :  */
     242           2 : Formation.prototype.GetPrimaryMember = function()
     243             : {
     244           0 :         return this.members[0];
     245             : };
     246             : 
     247             : /**
     248             :  * Get the formation animation variant for a certain member of this formation.
     249             :  * @param entity The entity ID to get the animation for.
     250             :  * @return The name of the animation variant as defined in the template,
     251             :  * e.g. "testudo_front" or undefined if does not exist.
     252             :  */
     253           2 : Formation.prototype.GetFormationAnimationVariant = function(entity)
     254             : {
     255          22 :         if (!this.animationvariants || !this.animationvariants.length || this.columnar || !this.memberPositions[entity])
     256          22 :                 return undefined;
     257           0 :         let row = this.memberPositions[entity].row;
     258           0 :         let column = this.memberPositions[entity].column;
     259           0 :         for (let i = 0; i < this.animationvariants.length; ++i)
     260             :         {
     261           0 :                 let minRow = this.animationvariants[i].minRow;
     262           0 :                 if (minRow < 0)
     263           0 :                         minRow += this.maxRowsUsed + 1;
     264           0 :                 if (row < minRow)
     265           0 :                         continue;
     266             : 
     267           0 :                 let maxRow = this.animationvariants[i].maxRow;
     268           0 :                 if (maxRow < 0)
     269           0 :                         maxRow += this.maxRowsUsed + 1;
     270           0 :                 if (row > maxRow)
     271           0 :                         continue;
     272             : 
     273           0 :                 let minColumn = this.animationvariants[i].minColumn;
     274           0 :                 if (minColumn < 0)
     275           0 :                         minColumn += this.maxColumnsUsed[row] + 1;
     276           0 :                 if (column < minColumn)
     277           0 :                         continue;
     278             : 
     279           0 :                 let maxColumn = this.animationvariants[i].maxColumn;
     280           0 :                 if (maxColumn < 0)
     281           0 :                         maxColumn += this.maxColumnsUsed[row] + 1;
     282           0 :                 if (column > maxColumn)
     283           0 :                         continue;
     284             : 
     285           0 :                 return this.animationvariants[i].name;
     286             :         }
     287           0 :         return undefined;
     288             : };
     289             : 
     290           2 : Formation.prototype.SetFinishedEntity = function(ent)
     291             : {
     292             :         // Rotate the entity to the correct angle.
     293           8 :         const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     294           8 :         const cmpEntPosition = Engine.QueryInterface(ent, IID_Position);
     295           8 :         if (cmpEntPosition && cmpEntPosition.IsInWorld() && cmpPosition && cmpPosition.IsInWorld())
     296           0 :                 cmpEntPosition.TurnTo(cmpPosition.GetRotation().y);
     297             : 
     298           8 :         this.finishedEntities.add(ent);
     299             : };
     300             : 
     301           2 : Formation.prototype.UnsetFinishedEntity = function(ent)
     302             : {
     303           0 :         this.finishedEntities.delete(ent);
     304             : };
     305             : 
     306           2 : Formation.prototype.ResetFinishedEntities = function()
     307             : {
     308          20 :         this.finishedEntities.clear();
     309             : };
     310             : 
     311           2 : Formation.prototype.AreAllMembersFinished = function()
     312             : {
     313           0 :         return this.finishedEntities.size === this.members.length;
     314             : };
     315             : 
     316           2 : Formation.prototype.SetIdleEntity = function(ent)
     317             : {
     318           0 :         this.idleEntities.add(ent);
     319             : };
     320             : 
     321           2 : Formation.prototype.UnsetIdleEntity = function(ent)
     322             : {
     323          11 :         this.idleEntities.delete(ent);
     324             : };
     325             : 
     326           2 : Formation.prototype.ResetIdleEntities = function()
     327             : {
     328           0 :         this.idleEntities.clear();
     329             : };
     330             : 
     331           2 : Formation.prototype.AreAllMembersIdle = function()
     332             : {
     333           2 :         return this.idleEntities.size === this.members.length;
     334             : };
     335             : 
     336             : /**
     337             :  * Set whether we are allowed to rearrange formation members.
     338             :  */
     339           2 : Formation.prototype.SetRearrange = function(rearrange)
     340             : {
     341          10 :         this.rearrange = rearrange;
     342             : };
     343             : 
     344             : /**
     345             :  * Initialize the members of this formation.
     346             :  * Must only be called once.
     347             :  * All members must implement UnitAI.
     348             :  */
     349           2 : Formation.prototype.SetMembers = function(ents)
     350             : {
     351           4 :         this.members = ents;
     352             : 
     353           4 :         for (let ent of this.members)
     354             :         {
     355          11 :                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     356          11 :                 cmpUnitAI.SetFormationController(this.entity);
     357             : 
     358          11 :                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
     359          11 :                 if (cmpAuras && cmpAuras.HasFormationAura())
     360             :                 {
     361           0 :                         this.formationMembersWithAura.push(ent);
     362           0 :                         cmpAuras.ApplyFormationAura(ents);
     363             :                 }
     364             :         }
     365             : 
     366           4 :         this.offsets = undefined;
     367             :         // Locate this formation controller in the middle of its members.
     368           4 :         this.MoveToMembersCenter();
     369             : 
     370             :         // Compute the speed etc. of the formation.
     371           4 :         this.ComputeMotionParameters();
     372             : };
     373             : 
     374             : /**
     375             :  * Remove the given list of entities.
     376             :  * The entities must already be members of this formation.
     377             :  * @param {boolean} rename - Whether the removal was part of an entity rename
     378             :         (prevents disbanding of the formation when under the member limit).
     379             :  */
     380           2 : Formation.prototype.RemoveMembers = function(ents, renamed = false)
     381             : {
     382           4 :         if (!ents.length)
     383           0 :                 return;
     384             : 
     385           4 :         this.offsets = undefined;
     386          11 :         this.members = this.members.filter(ent => !ents.includes(ent));
     387             : 
     388           4 :         for (const ent of ents)
     389             :         {
     390          11 :                 this.finishedEntities.delete(ent);
     391          11 :                 const cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     392          11 :                 cmpUnitAI.UpdateWorkOrders();
     393          11 :                 cmpUnitAI.UnsetFormationController();
     394             :         }
     395             : 
     396           4 :         for (let ent of this.formationMembersWithAura)
     397             :         {
     398           0 :                 const cmpAuras = Engine.QueryInterface(ent, IID_Auras);
     399           0 :                 cmpAuras.RemoveFormationAura(ents);
     400             : 
     401             :                 // The unit with the aura is also removed from the formation.
     402           0 :                 if (ents.includes(ent))
     403           0 :                         cmpAuras.RemoveFormationAura(this.members);
     404             :         }
     405             : 
     406           4 :         this.formationMembersWithAura = this.formationMembersWithAura.filter(ent => !ents.includes(ent));
     407             : 
     408             :         // If there's nobody left, destroy the formation
     409             :         // unless this is a rename where we can have 0 members temporarily.
     410           4 :         if (!renamed && this.members.length < +this.template.RequiredMemberCount)
     411             :         {
     412           0 :                 this.Disband();
     413           0 :                 return;
     414             :         }
     415             : 
     416           4 :         this.ComputeMotionParameters();
     417             : 
     418           4 :         if (!this.rearrange)
     419           1 :                 return;
     420             : 
     421             :         // Rearrange the remaining members.
     422           3 :         this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
     423             : };
     424             : 
     425           2 : Formation.prototype.AddMembers = function(ents)
     426             : {
     427           0 :         this.offsets = undefined;
     428             : 
     429           0 :         for (let ent of this.formationMembersWithAura)
     430             :         {
     431           0 :                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
     432           0 :                 cmpAuras.ApplyFormationAura(ents);
     433             :         }
     434             : 
     435           0 :         this.members = this.members.concat(ents);
     436             : 
     437           0 :         for (let ent of ents)
     438             :         {
     439           0 :                 let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     440           0 :                 cmpUnitAI.SetFormationController(this.entity);
     441           0 :                 if (!cmpUnitAI.GetOrders().length)
     442           0 :                         cmpUnitAI.SetNextState("FORMATIONMEMBER.IDLE");
     443             : 
     444           0 :                 let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
     445           0 :                 if (cmpAuras && cmpAuras.HasFormationAura())
     446             :                 {
     447           0 :                         this.formationMembersWithAura.push(ent);
     448           0 :                         cmpAuras.ApplyFormationAura(this.members);
     449             :                 }
     450             :         }
     451             : 
     452           0 :         this.ComputeMotionParameters();
     453             : 
     454           0 :         if (!this.rearrange)
     455           0 :                 return;
     456             : 
     457           0 :         this.MoveMembersIntoFormation(true, true, this.lastOrderVariant);
     458             : };
     459             : 
     460             : /**
     461             :  * Remove all members and destroy the formation.
     462             :  */
     463           2 : Formation.prototype.Disband = function()
     464             : {
     465           4 :         this.RemoveMembers(this.members);
     466             : 
     467             :         // Hack: switch to a clean state to stop timers.
     468           4 :         const cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
     469           4 :         cmpUnitAI.UnitFsm.SwitchToNextState(cmpUnitAI, "");
     470           4 :         Engine.QueryInterface(this.entity, IID_Position).MoveOutOfWorld();
     471           4 :         this.DeleteTwinFormations();
     472           4 :         Engine.DestroyEntity(this.entity);
     473             : };
     474             : 
     475             : /**
     476             :  * Set all members to form up into the formation shape.
     477             :  * @param {boolean} moveCenter - The formation center will be reinitialized
     478             :  * to the center of the units.
     479             :  * @param {boolean} force - All individual orders of the formation units are replaced,
     480             :  * otherwise the order to walk into formation is just pushed to the front.
     481             :  * @param {string | undefined} variant - Variant to be passed as order parameter.
     482             :  */
     483           2 : Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force, variant)
     484             : {
     485           9 :         if (!this.members.length)
     486           4 :                 return;
     487             : 
     488           5 :         let active = [];
     489           5 :         let positions = [];
     490             : 
     491           5 :         for (let ent of this.members)
     492             :         {
     493          19 :                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
     494          19 :                 if (!cmpPosition || !cmpPosition.IsInWorld())
     495           0 :                         continue;
     496             : 
     497          19 :                 active.push(ent);
     498             :                 // Query the 2D position as the exact height calculation isn't needed,
     499             :                 // but bring the position to the correct coordinates.
     500          19 :                 positions.push(cmpPosition.GetPosition2D());
     501             :         }
     502             : 
     503           5 :         const cmpFormationUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
     504           5 :         const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     505             :         // Reposition the formation if we're told to or if we don't already have a position.
     506           5 :         if (cmpPosition && (moveCenter || !cmpPosition.IsInWorld()))
     507             :         {
     508           5 :                 const avgpos = Vector2D.average(positions);
     509           5 :                 const targetPosition = cmpFormationUnitAI.GetTargetPositions()[0];
     510             : 
     511           5 :                 const oldRotation = cmpPosition.GetRotation().y;
     512           5 :                 const newRotation = targetPosition !== undefined && avgpos.distanceToSquared(targetPosition) > g_RotateDistanceThreshold ? avgpos.angleTo(targetPosition) : oldRotation;
     513             : 
     514             :                 // When we are out of world or the angle difference is big, trigger repositioning.
     515             :                 // Do this before setting up the position, because then we will always be in world.
     516           5 :                 if (!cmpPosition.IsInWorld() || !this.DoesAngleDifferenceAllowTurning(newRotation, oldRotation))
     517           5 :                         this.offsets = undefined;
     518             : 
     519           5 :                 this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, newRotation, true);
     520             :         }
     521             : 
     522             :         // Switch between column and box if necessary.
     523           5 :         const columnar = cmpFormationUnitAI.ComputeWalkingDistance() > g_ColumnDistanceThreshold;
     524           5 :         if (columnar != this.columnar)
     525             :         {
     526           3 :                 this.columnar = columnar;
     527           3 :                 this.offsets = undefined;
     528             :         }
     529             : 
     530           5 :         this.lastOrderVariant = variant;
     531             : 
     532           5 :         let offsetsChanged = false;
     533           5 :         if (!this.offsets)
     534             :         {
     535           5 :                 this.offsets = this.ComputeFormationOffsets(active, positions);
     536           5 :                 offsetsChanged = true;
     537             :         }
     538             : 
     539           5 :         let xMax = 0;
     540           5 :         let yMax = 0;
     541           5 :         let xMin = 0;
     542           5 :         let yMin = 0;
     543             : 
     544           5 :         if (force)
     545             :                 // Reset finishedEntities as FormationWalk is called.
     546           4 :                 this.ResetFinishedEntities();
     547             : 
     548           5 :         for (let i = 0; i < this.offsets.length; ++i)
     549             :         {
     550          19 :                 let offset = this.offsets[i];
     551             : 
     552          19 :                 let cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
     553          19 :                 if (!cmpUnitAI)
     554             :                 {
     555           0 :                         warn("Entities without UnitAI in formation are not supported.");
     556           0 :                         continue;
     557             :                 }
     558             : 
     559             :                 let data =
     560          19 :                 {
     561             :                         "target": this.entity,
     562             :                         "x": offset.x,
     563             :                         "z": offset.y,
     564             :                         "offsetsChanged": offsetsChanged,
     565             :                         "variant": variant
     566             :                 };
     567          19 :                 cmpUnitAI.AddOrder("FormationWalk", data, !force);
     568          19 :                 xMax = Math.max(xMax, offset.x);
     569          19 :                 yMax = Math.max(yMax, offset.y);
     570          19 :                 xMin = Math.min(xMin, offset.x);
     571          19 :                 yMin = Math.min(yMin, offset.y);
     572             :         }
     573           5 :         this.width = xMax - xMin;
     574           5 :         this.depth = yMax - yMin;
     575             : };
     576             : 
     577           2 : Formation.prototype.MoveToMembersCenter = function()
     578             : {
     579           4 :         let positions = [];
     580           4 :         let rotations = 0;
     581             : 
     582           4 :         for (let ent of this.members)
     583             :         {
     584          11 :                 let cmpPosition = Engine.QueryInterface(ent, IID_Position);
     585          11 :                 if (!cmpPosition || !cmpPosition.IsInWorld())
     586           0 :                         continue;
     587             : 
     588          11 :                 positions.push(cmpPosition.GetPosition2D());
     589          11 :                 rotations += cmpPosition.GetRotation().y;
     590             :         }
     591             : 
     592           4 :         let avgpos = Vector2D.average(positions);
     593           4 :         this.SetupPositionAndHandleRotation(avgpos.x, avgpos.y, rotations / positions.length, false);
     594             : };
     595             : 
     596             : /**
     597             :  * Set formation position.
     598             :  * If formation is not in world at time this is called, set new rotation and flag
     599             :  * for rangeManager. Also set the rotation if it is forced.
     600             :  */
     601           2 : Formation.prototype.SetupPositionAndHandleRotation = function(x, y, rot, forceRotation)
     602             : {
     603           9 :         const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     604           9 :         if (!cmpPosition)
     605           0 :                 return;
     606           9 :         const wasInWorld = cmpPosition.IsInWorld();
     607           9 :         cmpPosition.JumpTo(x, y);
     608             : 
     609           9 :         if (!forceRotation && wasInWorld)
     610           4 :                 return;
     611             : 
     612           5 :         cmpPosition.TurnTo(rot);
     613           5 :         if (!wasInWorld)
     614           0 :                 Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetEntityFlag(this.entity, "normal", false);
     615             : };
     616             : 
     617           2 : Formation.prototype.GetAvgFootprint = function(active)
     618             : {
     619           5 :         let footprints = [];
     620           5 :         for (let ent of active)
     621             :         {
     622          19 :                 let cmpFootprint = Engine.QueryInterface(ent, IID_Footprint);
     623          19 :                 if (cmpFootprint)
     624           0 :                         footprints.push(cmpFootprint.GetShape());
     625             :         }
     626           5 :         if (!footprints.length)
     627           5 :                 return { "width": 1, "depth": 1 };
     628             : 
     629           0 :         let r = { "width": 0, "depth": 0 };
     630           0 :         for (let shape of footprints)
     631             :         {
     632           0 :                 if (shape.type == "circle")
     633             :                 {
     634           0 :                         r.width += shape.radius * 2;
     635           0 :                         r.depth += shape.radius * 2;
     636             :                 }
     637           0 :                 else if (shape.type == "square")
     638             :                 {
     639           0 :                         r.width += shape.width;
     640           0 :                         r.depth += shape.depth;
     641             :                 }
     642             :         }
     643           0 :         r.width /= footprints.length;
     644           0 :         r.depth /= footprints.length;
     645           0 :         return r;
     646             : };
     647             : 
     648           2 : Formation.prototype.ComputeFormationOffsets = function(active, positions)
     649             : {
     650           5 :         let separation = this.GetAvgFootprint(active);
     651           5 :         separation.width *= this.separationMultiplier.width;
     652           5 :         separation.depth *= this.separationMultiplier.depth;
     653             : 
     654             :         let sortingClasses;
     655           5 :         if (this.columnar)
     656           3 :                 sortingClasses = ["Cavalry", "Infantry"];
     657             :         else
     658           2 :                 sortingClasses = this.sortingClasses.slice();
     659           5 :         sortingClasses.push("Unknown");
     660             : 
     661             :         // The entities will be assigned to positions in the formation in
     662             :         // the same order as the types list is ordered.
     663           5 :         let types = {};
     664           5 :         for (let i = 0; i < sortingClasses.length; ++i)
     665          13 :                 types[sortingClasses[i]] = [];
     666             : 
     667           5 :         for (let i in active)
     668             :         {
     669          19 :                 let cmpIdentity = Engine.QueryInterface(active[i], IID_Identity);
     670          19 :                 let classes = cmpIdentity.GetClassesList();
     671          19 :                 let done = false;
     672          19 :                 for (let c = 0; c < sortingClasses.length; ++c)
     673             :                 {
     674          41 :                         if (classes.indexOf(sortingClasses[c]) > -1)
     675             :                         {
     676           0 :                                 types[sortingClasses[c]].push({ "ent": active[i], "pos": positions[i] });
     677           0 :                                 done = true;
     678           0 :                                 break;
     679             :                         }
     680             :                 }
     681          19 :                 if (!done)
     682          19 :                         types.Unknown.push({ "ent": active[i], "pos": positions[i] });
     683             :         }
     684             : 
     685           5 :         let count = active.length;
     686             : 
     687           5 :         let shape = this.template.FormationShape;
     688           5 :         let shiftRows = this.shiftRows;
     689           5 :         let centerGap = this.centerGap;
     690           5 :         let sortingOrder = this.template.SortingOrder;
     691           5 :         let offsets = [];
     692             : 
     693             :         // Choose a sensible size/shape for the various formations, depending on number of units.
     694             :         let cols;
     695             : 
     696           5 :         if (this.columnar)
     697             :         {
     698           3 :                 shape = "square";
     699           3 :                 cols = Math.min(count, 3);
     700           3 :                 shiftRows = false;
     701           3 :                 centerGap = 0;
     702           3 :                 sortingOrder = null;
     703             :         }
     704             :         else
     705             :         {
     706           2 :                 let depth = Math.sqrt(count / this.widthDepthRatio);
     707           2 :                 if (this.maxRows && depth > this.maxRows)
     708           0 :                         depth = this.maxRows;
     709           2 :                 cols = Math.ceil(count / Math.ceil(depth) + (this.shiftRows ? 0.5 : 0));
     710           2 :                 if (cols < this.minColumns)
     711           0 :                         cols = Math.min(count, this.minColumns);
     712           2 :                 if (this.maxColumns && cols > this.maxColumns && this.maxRows != depth)
     713           0 :                         cols = this.maxColumns;
     714             :         }
     715             : 
     716             :         // Define special formations here.
     717           5 :         if (this.template.FormationShape == "special" && Engine.QueryInterface(this.entity, IID_Identity).GetGenericName() == "Scatter")
     718             :         {
     719           0 :                 let width = Math.sqrt(count) * (separation.width + separation.depth) * 2.5;
     720             : 
     721           0 :                 for (let i = 0; i < count; ++i)
     722             :                 {
     723           0 :                         let obj = new Vector2D(randFloat(0, width), randFloat(0, width));
     724           0 :                         obj.row = 1;
     725           0 :                         obj.column = i + 1;
     726           0 :                         offsets.push(obj);
     727             :                 }
     728             :         }
     729             : 
     730             :         // For non-special formations, calculate the positions based on the number of entities.
     731           5 :         this.maxColumnsUsed = [];
     732           5 :         this.maxRowsUsed = 0;
     733           5 :         if (shape != "special")
     734             :         {
     735           5 :                 offsets = [];
     736           5 :                 let r = 0;
     737           5 :                 let left = count;
     738             :                 // While there are units left, start a new row in the formation.
     739           5 :                 while (left > 0)
     740             :                 {
     741             :                         // Save the position of the row.
     742           9 :                         let z = -r * separation.depth;
     743             :                         // Alternate between the left and right side of the center to have a symmetrical distribution.
     744           9 :                         let side = 1;
     745             :                         let n;
     746             :                         // Determine the number of entities in this row of the formation.
     747           9 :                         if (shape == "square")
     748             :                         {
     749           9 :                                 n = cols;
     750           9 :                                 if (shiftRows)
     751           0 :                                         n -= r % 2;
     752             :                         }
     753           0 :                         else if (shape == "triangle")
     754             :                         {
     755           0 :                                 if (shiftRows)
     756           0 :                                         n = r + 1;
     757             :                                 else
     758           0 :                                         n = r * 2 + 1;
     759             :                         }
     760           9 :                         if (!shiftRows && n > left)
     761           2 :                                 n = left;
     762           9 :                         for (let c = 0; c < n && left > 0; ++c)
     763             :                         {
     764             :                                 // Switch sides for the next entity.
     765          19 :                                 side *= -1;
     766             :                                 let x;
     767          19 :                                 if (n % 2 == 0)
     768           4 :                                         x = side * (Math.floor(c / 2) + 0.5) * separation.width;
     769             :                                 else
     770          15 :                                         x = side * Math.ceil(c / 2) * separation.width;
     771          19 :                                 if (centerGap)
     772             :                                 {
     773             :                                         // Don't use the center position with a center gap.
     774           0 :                                         if (x == 0)
     775           0 :                                                 continue;
     776           0 :                                         x += side * centerGap / 2;
     777             :                                 }
     778          19 :                                 let column = Math.ceil(n / 2) + Math.ceil(c / 2) * side;
     779          19 :                                 let r1 = randFloat(-1, 1) * this.sloppiness;
     780          19 :                                 let r2 = randFloat(-1, 1) * this.sloppiness;
     781             : 
     782          19 :                                 offsets.push(new Vector2D(x + r1, z + r2));
     783          19 :                                 offsets[offsets.length - 1].row = r + 1;
     784          19 :                                 offsets[offsets.length - 1].column = column;
     785          19 :                                 left--;
     786             :                         }
     787           9 :                         ++r;
     788           9 :                         this.maxColumnsUsed[r] = n;
     789             :                 }
     790           5 :                 this.maxRowsUsed = r;
     791             :         }
     792             : 
     793             :         // Make sure the average offset is zero, as the formation is centered around that
     794             :         // calculating offset distances without a zero average makes no sense, as the formation
     795             :         // will jump to a different position any time.
     796           5 :         let avgoffset = Vector2D.average(offsets);
     797          19 :         offsets.forEach(function(o) {o.sub(avgoffset);});
     798             : 
     799             :         // Sort the available places in certain ways.
     800             :         // The places first in the list will contain the heaviest units as defined by the order
     801             :         // of the types list.
     802           5 :         if (sortingOrder == "fillFromTheSides")
     803           0 :                 offsets.sort(function(o1, o2) { return Math.abs(o1.x) < Math.abs(o2.x);});
     804           5 :         else if (sortingOrder == "fillToTheCenter")
     805           0 :                 offsets.sort(function(o1, o2) {
     806           0 :                         return Math.max(Math.abs(o1.x), Math.abs(o1.y)) < Math.max(Math.abs(o2.x), Math.abs(o2.y));
     807             :                 });
     808             : 
     809             :         // Query the 2D position of the formation.
     810           5 :         const realPositions = this.GetRealOffsetPositions(offsets);
     811             : 
     812             :         // Use realistic place assignment,
     813             :         // every soldier searches the closest available place in the formation.
     814           5 :         let newOffsets = [];
     815           5 :         for (const i of sortingClasses.reverse())
     816             :         {
     817          13 :                 const t = types[i];
     818          13 :                 if (!t.length)
     819           8 :                         continue;
     820           5 :                 let usedOffsets = offsets.splice(-t.length);
     821           5 :                 let usedRealPositions = realPositions.splice(-t.length);
     822           5 :                 for (let entPos of t)
     823             :                 {
     824          19 :                         let closestOffsetId = this.TakeClosestOffset(entPos, usedRealPositions, usedOffsets);
     825          19 :                         usedRealPositions.splice(closestOffsetId, 1);
     826          19 :                         newOffsets.push(usedOffsets.splice(closestOffsetId, 1)[0]);
     827          19 :                         newOffsets[newOffsets.length - 1].ent = entPos.ent;
     828             :                 }
     829             :         }
     830             : 
     831           5 :         return newOffsets;
     832             : };
     833             : 
     834             : /**
     835             :  * Search the closest position in the realPositions list to the given entity.
     836             :  * @param entPos - Object with entity position and entity ID.
     837             :  * @param realPositions - The world coordinates of the available offsets.
     838             :  * @param offsets
     839             :  * @return The index of the closest offset position.
     840             :  */
     841           2 : Formation.prototype.TakeClosestOffset = function(entPos, realPositions, offsets)
     842             : {
     843          19 :         let pos = entPos.pos;
     844          19 :         let closestOffsetId = -1;
     845          19 :         let offsetDistanceSq = Infinity;
     846          19 :         for (let i = 0; i < realPositions.length; ++i)
     847             :         {
     848          75 :                 let distSq = pos.distanceToSquared(realPositions[i]);
     849          75 :                 if (distSq < offsetDistanceSq)
     850             :                 {
     851          29 :                         offsetDistanceSq = distSq;
     852          29 :                         closestOffsetId = i;
     853             :                 }
     854             :         }
     855          19 :         this.memberPositions[entPos.ent] = { "row": offsets[closestOffsetId].row, "column": offsets[closestOffsetId].column };
     856          19 :         return closestOffsetId;
     857             : };
     858             : 
     859             : /**
     860             :  * Get the world positions for a list of offsets in this formation.
     861             :  */
     862           2 : Formation.prototype.GetRealOffsetPositions = function(offsets)
     863             : {
     864           5 :         const cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     865           5 :         const pos = cmpPosition.GetPosition2D();
     866           5 :         const rot = cmpPosition.GetRotation().y;
     867           5 :         const sin = Math.sin(rot);
     868           5 :         const cos = Math.cos(rot);
     869           5 :         let offsetPositions = [];
     870             :         // Calculate the world positions.
     871           5 :         for (let o of offsets)
     872          19 :                 offsetPositions.push(new Vector2D(pos.x + o.y * sin + o.x * cos, pos.y + o.y * cos - o.x * sin));
     873             : 
     874           5 :         return offsetPositions;
     875             : };
     876             : 
     877             : /**
     878             :  * Returns true if the difference between two given angles (in radians)
     879             :  * are smaller than the maximum turning angle of the formation and therfore allow
     880             :  * the formation turn without reassigning positions.
     881             :  */
     882             : 
     883           2 : Formation.prototype.DoesAngleDifferenceAllowTurning = function(a1, a2)
     884             : {
     885         721 :         const d = Math.abs(a1 - a2) % (2 * Math.PI);
     886         721 :         return d < this.maxTurningAngle || d > 2 * Math.PI - this.maxTurningAngle;
     887             : };
     888             : 
     889             : /**
     890             :  * Set formation controller's speed based on its current members.
     891             :  */
     892           2 : Formation.prototype.ComputeMotionParameters = function()
     893             : {
     894           8 :         if (!this.members.length)
     895           4 :                 return;
     896             : 
     897           4 :         let minSpeed = Infinity;
     898           4 :         let minAcceleration = Infinity;
     899           4 :         let maxClearance = 0;
     900           4 :         let maxPassClass = "default";
     901             : 
     902           4 :         const cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
     903           4 :         for (let ent of this.members)
     904             :         {
     905          11 :                 const cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
     906          11 :                 if (!cmpUnitMotion)
     907           0 :                         continue;
     908          11 :                 minSpeed = Math.min(minSpeed, cmpUnitMotion.GetWalkSpeed());
     909          11 :                 minAcceleration = Math.min(minAcceleration, cmpUnitMotion.GetAcceleration());
     910             : 
     911          11 :                 const passClass = cmpUnitMotion.GetPassabilityClassName();
     912          11 :                 const clearance = cmpPathfinder.GetClearance(cmpPathfinder.GetPassabilityClass(passClass));
     913          11 :                 if (clearance > maxClearance)
     914             :                 {
     915           4 :                         maxClearance = clearance;
     916           4 :                         maxPassClass = passClass;
     917             :                 }
     918             :         }
     919           4 :         minSpeed *= this.GetSpeedMultiplier();
     920             : 
     921           4 :         const cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
     922           4 :         cmpUnitMotion.SetSpeedMultiplier(minSpeed / cmpUnitMotion.GetWalkSpeed());
     923           4 :         cmpUnitMotion.SetAcceleration(minAcceleration);
     924           4 :         cmpUnitMotion.SetPassabilityClassName(maxPassClass);
     925             : };
     926             : 
     927           2 : Formation.prototype.ShapeUpdate = function()
     928             : {
     929           0 :         if (!this.rearrange)
     930           0 :                 return;
     931             : 
     932             :         // Check the distance to twin formations, and merge if
     933             :         // the formations could collide.
     934           0 :         for (let i = this.twinFormations.length - 1; i >= 0; --i)
     935             :         {
     936             :                 // Only do the check on one side.
     937           0 :                 if (this.twinFormations[i] <= this.entity)
     938           0 :                         continue;
     939           0 :                 let cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     940           0 :                 let cmpOtherPosition = Engine.QueryInterface(this.twinFormations[i], IID_Position);
     941           0 :                 let cmpOtherFormation = Engine.QueryInterface(this.twinFormations[i], IID_Formation);
     942           0 :                 if (!cmpPosition || !cmpOtherPosition || !cmpOtherFormation ||
     943             :                      !cmpPosition.IsInWorld() || !cmpOtherPosition.IsInWorld())
     944           0 :                         continue;
     945             : 
     946           0 :                 let thisPosition = cmpPosition.GetPosition2D();
     947           0 :                 let otherPosition = cmpOtherPosition.GetPosition2D();
     948             : 
     949           0 :                 let dx = thisPosition.x - otherPosition.x;
     950           0 :                 let dy = thisPosition.y - otherPosition.y;
     951           0 :                 let dist = Math.sqrt(dx * dx + dy * dy);
     952             : 
     953           0 :                 let thisSize = this.GetSize();
     954           0 :                 let otherSize = cmpOtherFormation.GetSize();
     955           0 :                 let minDist = Math.max(thisSize.width / 2, thisSize.depth / 2) +
     956             :                         Math.max(otherSize.width / 2, otherSize.depth / 2) +
     957             :                         this.formationSeparation;
     958             : 
     959           0 :                 if (minDist < dist)
     960           0 :                         continue;
     961             : 
     962             :                 // Merge the members from the twin formation into this one
     963             :                 // twin formations should always have exactly the same orders.
     964           0 :                 const otherMembers = cmpOtherFormation.members;
     965           0 :                 cmpOtherFormation.RemoveMembers(otherMembers);
     966           0 :                 this.AddMembers(otherMembers);
     967             :         }
     968             :         // Switch between column and box if necessary.
     969           0 :         let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
     970           0 :         let walkingDistance = cmpUnitAI.ComputeWalkingDistance();
     971           0 :         let columnar = walkingDistance > g_ColumnDistanceThreshold;
     972           0 :         if (columnar != this.columnar)
     973             :         {
     974           0 :                 this.offsets = undefined;
     975           0 :                 this.columnar = columnar;
     976             :                 // Disable moveCenter so we can't get stuck in a loop of switching
     977             :                 // shape causing center to change causing shape to switch back.
     978           0 :                 this.MoveMembersIntoFormation(false, true, this.lastOrderVariant);
     979             :         }
     980             : };
     981             : 
     982           2 : Formation.prototype.ResetOrderVariant = function()
     983             : {
     984           0 :         this.lastOrderVariant = undefined;
     985             : };
     986             : 
     987           2 : Formation.prototype.OnGlobalOwnershipChanged = function(msg)
     988             : {
     989             :         // When an entity is captured or destroyed, it should no longer be
     990             :         // controlled by this formation.
     991           0 :         if (this.members.indexOf(msg.entity) != -1)
     992           0 :                 this.RemoveMembers([msg.entity]);
     993           0 :         if (msg.entity === this.entity && msg.to !== INVALID_PLAYER)
     994           0 :                 Engine.QueryInterface(this.entity, IID_Visual)?.SetVariant("animationVariant", QueryPlayerIDInterface(msg.to, IID_Identity).GetCiv());
     995             : };
     996             : 
     997           2 : Formation.prototype.OnGlobalEntityRenamed = function(msg)
     998             : {
     999           0 :         if (this.members.indexOf(msg.entity) === -1)
    1000           0 :                 return;
    1001             : 
    1002           0 :         if (this.finishedEntities.delete(msg.entity))
    1003           0 :                 this.finishedEntities.add(msg.newentity);
    1004             : 
    1005             :         // Save rearranging to temporarily set it to false.
    1006           0 :         let temp = this.rearrange;
    1007           0 :         this.rearrange = false;
    1008             : 
    1009             :         // First remove the old member to be able to reuse its position.
    1010           0 :         this.RemoveMembers([msg.entity], true);
    1011           0 :         this.AddMembers([msg.newentity]);
    1012           0 :         this.memberPositions[msg.newentity] = this.memberPositions[msg.entity];
    1013             : 
    1014           0 :         this.rearrange = temp;
    1015             : };
    1016             : 
    1017           2 : Formation.prototype.RegisterTwinFormation = function(entity)
    1018             : {
    1019           0 :         let cmpFormation = Engine.QueryInterface(entity, IID_Formation);
    1020           0 :         if (!cmpFormation)
    1021           0 :                 return;
    1022           0 :         this.twinFormations.push(entity);
    1023           0 :         cmpFormation.twinFormations.push(this.entity);
    1024             : };
    1025             : 
    1026           2 : Formation.prototype.DeleteTwinFormations = function()
    1027             : {
    1028           4 :         for (let ent of this.twinFormations)
    1029             :         {
    1030           0 :                 let cmpFormation = Engine.QueryInterface(ent, IID_Formation);
    1031           0 :                 if (cmpFormation)
    1032           0 :                         cmpFormation.twinFormations.splice(cmpFormation.twinFormations.indexOf(this.entity), 1);
    1033             :         }
    1034           4 :         this.twinFormations = [];
    1035             : };
    1036             : 
    1037           2 : Formation.prototype.LoadFormation = function(newTemplate)
    1038             : {
    1039           0 :         const newFormation = ChangeEntityTemplate(this.entity, newTemplate);
    1040           0 :         return Engine.QueryInterface(newFormation, IID_UnitAI);
    1041             : };
    1042             : 
    1043             : 
    1044           2 : Formation.prototype.OnEntityRenamed = function(msg)
    1045             : {
    1046           0 :         const members = clone(this.members);
    1047           0 :         this.Disband();
    1048           0 :         Engine.QueryInterface(msg.newentity, IID_Formation).SetMembers(members);
    1049             : };
    1050             : 
    1051           2 : Engine.RegisterComponentType(IID_Formation, "Formation", Formation);

Generated by: LCOV version 1.14