Allow players to set rally points on allied buildings

Instead of storing a single flat list of positions and data per
building, RallyPoint now stores them keyed by player ID. This lets
mutual allies independently set and display rally points on each
other's structures.

The GUI now allows selecting allied buildings with a rally point
and only shows the viewing player's own rally point data.
GuiInterface gets an OnUpdate handler to keep displayed positions
in sync when rally point targets move.

GetRallyPointCommands now takes raw position and data arrays instead
of a component reference. The network command field is also renamed
from "entities" to "structures".

Fixes #3115
This commit is contained in:
mehmed-faheim-arslan 2026-06-02 21:14:51 +01:00 committed by Vantha
parent 8a999d63b7
commit 39b1311fac
11 changed files with 281 additions and 115 deletions

View file

@ -199,6 +199,7 @@
{ "nick": "mattlott", "name": "Matt Lott" },
{ "nick": "maveric", "name": "Anton Protko" },
{ "nick": "mbusy", "name": "Maxime Busy" },
{ "nick": "mehmedarslan", "name": "Mehmed Faheim Arslan/Sai Kaushik" },
{ "nick": "Mentula" },
{ "nick": "Micnasty", "name": "Travis Gorkin" },
{ "name": "Mikołaj \"Bajter\" Korcz" },

View file

@ -263,7 +263,7 @@ function determineAction(x, y, fromMiniMap)
if (!entState)
return undefined;
if (!selection.every(ownsEntity) &&
if (!selection.every(ent => ownsEntity(ent) || isMutualAllyBuilding(ent)) &&
!(g_SimState.players[g_ViewedPlayer] &&
g_SimState.players[g_ViewedPlayer].controlsAll))
return undefined;
@ -318,6 +318,13 @@ function ownsEntity(ent)
return entState && entState.player == g_ViewedPlayer;
}
function isMutualAllyBuilding(ent)
{
const entState = GetEntityState(ent);
return entState && entState.rallyPoint &&
g_Players[entState.player]?.isMutualAlly[g_ViewedPlayer];
}
function isAttackMovePressed()
{
return Engine.HotkeyIsPressed("session.attackmove") ||

View file

@ -331,6 +331,8 @@ async function init(initData, hotloadData)
resumeGame();
});
g_DiplomacyColors.updateDisplayedPlayerColors();
const promise = Promise.race([new Promise((_, reject) =>
{
if (g_IsNetworked)

View file

@ -1125,7 +1125,7 @@ var g_UnitActions =
Engine.PostNetworkCommand({
"type": "set-rallypoint",
"entities": selection,
"structures": selection,
"x": position.x,
"z": position.z,
"data": action.data,
@ -1355,7 +1355,7 @@ var g_UnitActions =
{
Engine.PostNetworkCommand({
"type": "unset-rallypoint",
"entities": selection
"structures": selection
});
// Remove displayed rally point

View file

@ -996,13 +996,13 @@ export const Entity = Class({
"setRallyPoint": function(target, command)
{
const data = { "command": command, "target": target.id() };
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "structures": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
return this;
},
"unsetRallyPoint": function()
{
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "structures": [this.id()] });
return this;
},

View file

@ -397,7 +397,7 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
const cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions(player)[0] }; // undefined or {x,z} object
const cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
@ -1035,6 +1035,16 @@ GuiInterface.prototype.GetNonGaiaEntities = function()
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* @param {number} entity - The entityID to verify.
* @param {number} player - The playerID to check against.
* @return {boolean}.
*/
function IsOwnedByPlayerOrMutualAlly(entity, player)
{
return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
}
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
@ -1046,14 +1056,12 @@ GuiInterface.prototype.GetNonGaiaEntities = function()
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
const cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (const ent of this.entsRallyPointsDisplayed)
for (const { ent } of this.entsRallyPointsDisplayed)
{
const cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
cmpRallyPointRenderer.Reset();
}
this.entsRallyPointsDisplayed = [];
@ -1071,11 +1079,8 @@ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
if (!cmpRallyPoint)
continue;
// Verify the owner.
const cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
if (!IsOwnedByPlayerOrMutualAlly(ent, player))
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
@ -1083,8 +1088,8 @@ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
// may return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions(player)[0];
if (pos)
{
@ -1093,23 +1098,52 @@ GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
if ("queued" in cmd)
{
if (cmd.queued == true)
{
// check by re adding all existing positions before appending the new queued one.
const existingPositions = cmpRallyPoint.GetPositions(player);
for (const posi of existingPositions)
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
}
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
{
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (const posi of cmpRallyPoint.GetPositions())
const positions = cmpRallyPoint.GetPositions(player);
for (const posi of positions)
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
}
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
this.entsRallyPointsDisplayed.push({ ent, player });
}
}
};
GuiInterface.prototype.OnUpdate = function()
{
for (const { ent, player } of this.entsRallyPointsDisplayed)
{
const cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
const cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
const positions = cmpRallyPoint.GetPositions(player);
// Update renderer positions so the path follows moving targets.
for (let i = 0; i < positions.length; i++)
cmpRallyPointRenderer.UpdatePosition(i, new Vector2D(positions[i].x, positions[i].z));
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
const ent = Engine.AddLocalEntity(cmd.template);

View file

@ -5,67 +5,68 @@ RallyPoint.prototype.Schema =
RallyPoint.prototype.Init = function()
{
this.pos = [];
this.data = [];
this.perPlayer = {};
};
RallyPoint.prototype.AddPosition = function(x, z)
RallyPoint.prototype.GetOwner = function()
{
this.pos.push({
"x": x,
"z": z
});
return Engine.QueryInterface(this.entity, IID_Ownership)?.GetOwner();
};
RallyPoint.prototype.HasPositions = function()
RallyPoint.prototype.AddPosition = function(x, z, player = this.GetOwner())
{
return this.pos.length > 0;
if (!this.perPlayer[player])
this.perPlayer[player] = { "pos": [], "data": [] };
this.perPlayer[player].pos.push({ "x": x, "z": z });
};
RallyPoint.prototype.HasPositions = function(player = this.GetOwner())
{
return !!this.perPlayer[player]?.pos.length;
};
RallyPoint.prototype.GetFirstPosition = function()
{
return this.pos.length ? Vector2D.from3D(this.pos[0]) : new Vector2D(-1, -1);
const pos = this.perPlayer[this.GetOwner()]?.pos;
return pos?.length ? Vector2D.from3D(pos[0]) : new Vector2D(-1, -1);
};
RallyPoint.prototype.GetPositions = function()
RallyPoint.prototype.GetPositions = function(player = this.GetOwner())
{
// Update positions for moving target entities
const playerEntry = this.perPlayer[player];
if (!playerEntry)
return [];
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
// We must not affect the simulation state here (modifications of the
// RallyPointRenderer are allowed though), so copy the state
var ret = [];
for (var i = 0; i < this.pos.length; i++)
// We must not affect the simulation state here, so copy the state
const ret = [];
for (let i = 0; i < playerEntry.pos.length; i++)
{
ret.push(this.pos[i]);
ret.push(playerEntry.pos[i]);
// Update the rallypoint coordinates if the target is alive
if (!this.data[i] || !this.data[i].target || !this.TargetIsAlive(this.data[i].target))
if (!playerEntry.data[i]?.target || !this.TargetIsAlive(playerEntry.data[i].target))
continue;
// and visible
if (cmpRangeManager && cmpOwnership &&
cmpRangeManager.GetLosVisibility(this.data[i].target, cmpOwnership.GetOwner()) != "visible")
// and visible to the player who set this rally point
if (cmpRangeManager &&
cmpRangeManager.GetLosVisibility(playerEntry.data[i].target, player) != "visible")
continue;
// Get the actual position of the target entity
var cmpPosition = Engine.QueryInterface(this.data[i].target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
const cmpPosition = Engine.QueryInterface(playerEntry.data[i].target, IID_Position);
if (!cmpPosition?.IsInWorld())
continue;
var targetPosition = cmpPosition.GetPosition2D();
const targetPosition = cmpPosition.GetPosition2D();
if (!targetPosition)
continue;
if (this.pos[i].x == targetPosition.x && this.pos[i].z == targetPosition.y)
if (playerEntry.pos[i].x == targetPosition.x && playerEntry.pos[i].z == targetPosition.y)
continue;
ret[i] = { "x": targetPosition.x, "z": targetPosition.y };
var cmpRallyPointRenderer = Engine.QueryInterface(this.entity, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.UpdatePosition(i, targetPosition);
}
return ret;
@ -73,31 +74,24 @@ RallyPoint.prototype.GetPositions = function()
// Extra data for the rally point, should have a command property and then helpful data for that command
// See getActionInfo in gui/input.js
RallyPoint.prototype.AddData = function(data)
RallyPoint.prototype.AddData = function(data, player = this.GetOwner())
{
this.data.push(data);
if (!this.perPlayer[player])
this.perPlayer[player] = { "pos": [], "data": [] };
this.perPlayer[player].data.push(data);
};
// Returns an array with the data associated with this rally point. Each element has the structure:
// {"type": "walk/gather/garrison/...", "target": targetEntityId, "resourceType": "tree/fruit/ore/..."} where target
// and resourceType (specific resource type) are optional, also target may be an invalid entity, check for existence.
RallyPoint.prototype.GetData = function()
RallyPoint.prototype.GetData = function(player = this.GetOwner())
{
return this.data;
return this.perPlayer[player]?.data ?? [];
};
RallyPoint.prototype.Unset = function()
RallyPoint.prototype.Unset = function(player = this.GetOwner())
{
this.pos = [];
this.data = [];
};
RallyPoint.prototype.Reset = function()
{
this.Unset();
var cmpRallyPointRenderer = Engine.QueryInterface(this.entity, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.Reset();
delete this.perPlayer[player];
};
/**
@ -107,50 +101,49 @@ RallyPoint.prototype.Reset = function()
*/
RallyPoint.prototype.OrderToRallyPoint = function(entity, ignore = [])
{
const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return;
const owner = cmpOwnership.GetOwner();
const cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (!cmpEntOwnership || cmpEntOwnership.GetOwner() != owner)
if (!cmpEntOwnership)
return;
const entOwner = cmpEntOwnership.GetOwner();
if (!this.HasPositions(entOwner))
return;
const commands = GetRallyPointCommands(this, [entity]);
const playerEntry = this.perPlayer[entOwner];
const commands = GetRallyPointCommands(playerEntry.pos, playerEntry.data, [entity]);
if (!commands.length ||
commands[0].target == this.entity && ignore.includes(commands[0].type))
return;
for (const command of commands)
ProcessCommand(owner, command);
ProcessCommand(entOwner, command);
};
RallyPoint.prototype.OnGlobalEntityRenamed = function(msg)
{
for (const data of this.data)
{
if (!data)
continue;
if (data.target && data.target == msg.entity)
data.target = msg.newentity;
if (data.source && data.source == msg.entity)
data.source = msg.newentity;
}
for (const playerEntry of Object.values(this.perPlayer))
for (const data of playerEntry.data)
{
if (data?.target == msg.entity)
data.target = msg.newentity;
if (data?.source == msg.entity)
data.source = msg.newentity;
}
if (msg.entity != this.entity)
return;
const cmpRallyPointNew = Engine.QueryInterface(msg.newentity, IID_RallyPoint);
if (cmpRallyPointNew)
{
const rallyCoords = this.GetPositions();
const rallyData = this.GetData();
for (let i = 0; i < rallyCoords.length; ++i)
for (const player in this.perPlayer)
{
cmpRallyPointNew.AddPosition(rallyCoords[i].x, rallyCoords[i].z);
cmpRallyPointNew.AddData(rallyData[i]);
const playerEntry = this.perPlayer[player];
for (let i = 0; i < playerEntry.pos.length; ++i)
{
cmpRallyPointNew.AddPosition(playerEntry.pos[i].x, playerEntry.pos[i].z, +player);
cmpRallyPointNew.AddData(playerEntry.data[i], +player);
}
}
}
};
RallyPoint.prototype.OnOwnershipChanged = function(msg)
@ -159,7 +152,7 @@ RallyPoint.prototype.OnOwnershipChanged = function(msg)
if (msg.from == INVALID_PLAYER || msg.to == INVALID_PLAYER)
return;
this.Reset();
this.perPlayer = {};
};
/**

View file

@ -229,7 +229,7 @@ Trainer.prototype.Item.prototype.Spawn = function()
const cmpRallyPoint = Engine.QueryInterface(this.trainer, IID_RallyPoint);
if (cmpRallyPoint)
{
const data = cmpRallyPoint.GetData()[0];
const data = cmpRallyPoint.GetData(this.player)[0];
if (data?.target && data.target == this.trainer && data.command == "garrison")
autoGarrison = true;
}
@ -294,7 +294,7 @@ Trainer.prototype.Item.prototype.Spawn = function()
}
if (spawnedEnts.length && cmpRallyPoint)
for (const com of GetRallyPointCommands(cmpRallyPoint, spawnedEnts))
for (const com of GetRallyPointCommands(cmpRallyPoint.GetPositions(this.player), cmpRallyPoint.GetData(this.player), spawnedEnts))
{
// Tag this command as coming from a rally point
com.fromRallyPoint = true;

View file

@ -8,6 +8,7 @@ function initialRallyPointTest(test_function)
ResetState();
const entityID = 123;
AddMock(entityID, IID_Ownership, { "GetOwner": () => 1 });
const cmpRallyPoint = ConstructComponent(entityID, "RallyPoint", {});
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(), []);
@ -50,12 +51,6 @@ initialRallyPointTest((cmpRallyPoint) =>
return true;
});
initialRallyPointTest((cmpRallyPoint) =>
{
cmpRallyPoint.Reset();
return true;
});
// Construction
initialRallyPointTest((cmpRallyPoint) =>
{
@ -83,3 +78,135 @@ initialRallyPointTest((cmpRallyPoint) =>
cmpRallyPoint.OnOwnershipChanged({ "from": 2, "to": 0 });
return true;
});
// Per-player rally point tests
{
ResetState();
const entityID = 123;
let ownerPlayer = 1;
AddMock(entityID, IID_Ownership, { "GetOwner": () => ownerPlayer });
const cmpRallyPoint = ConstructComponent(entityID, "RallyPoint", {});
const player2 = 2;
const player3 = 3;
// Initially no per-player positions
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player2), []);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(player2), []);
TS_ASSERT(!cmpRallyPoint.HasPositions(player2));
// Add per-player rally point for player 2
cmpRallyPoint.AddPosition(10, 20, player2);
cmpRallyPoint.AddData({ "command": "walk" }, player2);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player2), [{ "x": 10, "z": 20 }]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(player2), [{ "command": "walk" }]);
TS_ASSERT(cmpRallyPoint.HasPositions(player2));
// Add a second waypoint for player 2
cmpRallyPoint.AddPosition(30, 40, player2);
cmpRallyPoint.AddData({ "command": "garrison" }, player2);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player2),
[{ "x": 10, "z": 20 }, { "x": 30, "z": 40 }]);
// Player 3 is unaffected
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player3), []);
TS_ASSERT(!cmpRallyPoint.HasPositions(player3));
// Add per-player rally point for player 3
cmpRallyPoint.AddPosition(50, 60, player3);
cmpRallyPoint.AddData({ "command": "walk" }, player3);
TS_ASSERT(cmpRallyPoint.HasPositions(player3));
// Unset clears player 2 positions and data
cmpRallyPoint.Unset(player2);
TS_ASSERT(!cmpRallyPoint.HasPositions(player2));
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player2), []);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(player2), []);
// Player 3 is unaffected
TS_ASSERT(cmpRallyPoint.HasPositions(player3));
// Unset removes player 3 entry
cmpRallyPoint.Unset(player3);
TS_ASSERT(!cmpRallyPoint.HasPositions(player3));
// Per-player data is cleared on ownership change
cmpRallyPoint.AddPosition(10, 20, player2);
cmpRallyPoint.AddData({ "command": "walk" }, player2);
TS_ASSERT(cmpRallyPoint.HasPositions(player2));
cmpRallyPoint.OnOwnershipChanged({ "from": 1, "to": 2 });
ownerPlayer = 2;
TS_ASSERT(!cmpRallyPoint.HasPositions(player2));
// The owner's rally point entry does not affect allied players' entries
cmpRallyPoint.AddPosition(100, 200);
cmpRallyPoint.AddData({ "command": "walk" });
cmpRallyPoint.AddPosition(300, 400, player3);
cmpRallyPoint.AddData({ "command": "walk" }, player3);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), [{ "x": 100, "z": 200 }]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player3), [{ "x": 300, "z": 400 }]);
// Unset does not affect per-player data
cmpRallyPoint.Unset();
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(), []);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player3), [{ "x": 300, "z": 400 }]);
}
// Ownership change construction/destruction preserves per-player data
{
ResetState();
const entityID = 123;
const cmpRallyPoint = ConstructComponent(entityID, "RallyPoint", {});
const player2 = 2;
cmpRallyPoint.AddPosition(10, 20, player2);
cmpRallyPoint.AddData({ "command": "walk" }, player2);
// Construction: from INVALID_PLAYER should not clear per-player data
cmpRallyPoint.OnOwnershipChanged({ "from": INVALID_PLAYER, "to": 1 });
TS_ASSERT(cmpRallyPoint.HasPositions(player2));
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player2), [{ "x": 10, "z": 20 }]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetData(player2), [{ "command": "walk" }]);
// Destruction: to INVALID_PLAYER should not clear per-player data
cmpRallyPoint.OnOwnershipChanged({ "from": 1, "to": INVALID_PLAYER });
TS_ASSERT(cmpRallyPoint.HasPositions(player2));
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPoint.GetPositions(player2), [{ "x": 10, "z": 20 }]);
}
// OnGlobalEntityRenamed migrates per-player rally point data to the new entity
{
ResetState();
const oldEntityID = 123;
const newEntityID = 456;
const player2 = 2;
const player3 = 3;
AddMock(oldEntityID, IID_Ownership, { "GetOwner": () => 1 });
AddMock(newEntityID, IID_Ownership, { "GetOwner": () => 1 });
const cmpRallyPointOld = ConstructComponent(oldEntityID, "RallyPoint", {});
const cmpRallyPointNew = ConstructComponent(newEntityID, "RallyPoint", {});
cmpRallyPointOld.AddPosition(100, 200);
cmpRallyPointOld.AddData({ "command": "walk" });
cmpRallyPointOld.AddPosition(10, 20, player2);
cmpRallyPointOld.AddData({ "command": "walk" }, player2);
cmpRallyPointOld.AddPosition(30, 40, player3);
cmpRallyPointOld.AddData({ "command": "garrison" }, player3);
cmpRallyPointOld.OnGlobalEntityRenamed({ "entity": oldEntityID, "newentity": newEntityID });
// New entity receives owner and per-player rally point data
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPointNew.GetPositions(), [{ "x": 100, "z": 200 }]);
TS_ASSERT(cmpRallyPointNew.HasPositions(player2));
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPointNew.GetPositions(player2), [{ "x": 10, "z": 20 }]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPointNew.GetData(player2), [{ "command": "walk" }]);
TS_ASSERT(cmpRallyPointNew.HasPositions(player3));
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPointNew.GetPositions(player3), [{ "x": 30, "z": 40 }]);
TS_ASSERT_UNEVAL_EQUALS(cmpRallyPointNew.GetData(player3), [{ "command": "garrison" }]);
// Rename for an unrelated entity does not migrate to new entity
ResetState();
const cmpRP1 = ConstructComponent(oldEntityID, "RallyPoint", {});
const cmpRP2 = ConstructComponent(newEntityID, "RallyPoint", {});
cmpRP1.AddPosition(10, 20, player2);
cmpRP1.OnGlobalEntityRenamed({ "entity": 999, "newentity": newEntityID });
TS_ASSERT(!cmpRP2.HasPositions(player2));
}

View file

@ -434,27 +434,31 @@ var g_Commands = {
"set-rallypoint": function(player, cmd, data)
{
for (const ent of data.entities)
const structures = FilterEntityListWithAllies(cmd.structures || [], player, data.controlAllUnits);
for (const structure of structures)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
if (!cmd.queued)
cmpRallyPoint.Unset();
const cmpRallyPoint = Engine.QueryInterface(structure, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
cmpRallyPoint.AddPosition(cmd.x, cmd.z);
cmpRallyPoint.AddData(clone(cmd.data));
}
if (!cmd.queued)
cmpRallyPoint.Unset(player);
cmpRallyPoint.AddPosition(cmd.x, cmd.z, player);
cmpRallyPoint.AddData(clone(cmd.data), player);
}
},
"unset-rallypoint": function(player, cmd, data)
{
for (const ent of data.entities)
const structures = FilterEntityListWithAllies(cmd.structures || [], player, data.controlAllUnits);
for (const structure of structures)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Reset();
const cmpRallyPoint = Engine.QueryInterface(structure, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
cmpRallyPoint.Unset(player);
}
},

View file

@ -1,18 +1,16 @@
// Returns an array of commands suitable for ProcessCommand() based on the rally point data.
// This assumes that the rally point has a valid position.
function GetRallyPointCommands(cmpRallyPoint, spawnedEnts)
function GetRallyPointCommands(rallyPos, data, spawnedEnts)
{
const data = cmpRallyPoint.GetData();
const rallyPos = cmpRallyPoint.GetPositions();
const ret = [];
for (let i = 0; i < rallyPos.length; ++i)
{
// Look and see if there is a command in the rally point data, otherwise just walk there.
let command = data[i] && data[i].command ? data[i].command : "walk";
let command = data[i]?.command ?? "walk";
// If a target was set and the target no longer exists, or no longer
// has a valid position, then just walk to the rally point.
if (data[i] && data[i].target)
if (data[i]?.target)
{
const cmpPosition = Engine.QueryInterface(data[i].target, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())