diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index e87616b724..30eeb3cfa8 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -691,31 +691,31 @@ var UnitFsmSpec = { }, "Timer": function(msg) { - // Check the target is still alive - if (this.TargetIsAlive(this.order.data.target)) + var target = this.order.data.target; + // Check the target is still alive and attackable + if (this.TargetIsAlive(target) && this.CanAttack(target)) { // Check we can still reach the target - if (this.CheckTargetRange(this.order.data.target, IID_Attack, this.attackType)) + if (this.CheckTargetRange(target, IID_Attack, this.attackType)) { - this.FaceTowardsTarget(this.order.data.target); + this.FaceTowardsTarget(target); var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - cmpAttack.PerformAttack(this.attackType, this.order.data.target); + cmpAttack.PerformAttack(this.attackType, target); return; } // Can't reach it - try to chase after it - if (this.ShouldChaseTargetedEntity(this.order.data.target, this.order.data.force)) + if (this.ShouldChaseTargetedEntity(target, this.order.data.force)) { - if (this.MoveToTargetRange(this.order.data.target, IID_Attack, this.attackType)) + if (this.MoveToTargetRange(target, IID_Attack, this.attackType)) { this.SetNextState("COMBAT.CHASING"); return; } } - } - // Can't reach it, or it doesn't exist any more - give up + // Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up if (this.FinishOrder()) return; @@ -837,15 +837,16 @@ var UnitFsmSpec = { }, "Timer": function(msg) { - // Check we can still reach the target - if (this.CheckTargetRange(this.order.data.target, IID_ResourceGatherer)) + var target = this.order.data.target; + // Check we can still reach and gather from the target + if (this.CheckTargetRange(target, IID_ResourceGatherer) && this.CanGather(target)) { // Gather the resources: var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); // Try to gather treasure - if (cmpResourceGatherer.TryInstantGather(this.order.data.target)) + if (cmpResourceGatherer.TryInstantGather(target)) return; // If we've already got some resources but they're the wrong type, @@ -854,7 +855,7 @@ var UnitFsmSpec = { cmpResourceGatherer.DropResources(); // Collect from the target - var status = cmpResourceGatherer.PerformGather(this.order.data.target); + var status = cmpResourceGatherer.PerformGather(target); // TODO: if exhausted, we should probably stop immediately // and choose a new target @@ -879,7 +880,7 @@ var UnitFsmSpec = { else { // Try to follow the target - if (this.MoveToTargetRange(this.order.data.target, IID_ResourceGatherer)) + if (this.MoveToTargetRange(target, IID_ResourceGatherer)) { this.SetNextState("APPROACHING"); return; @@ -1030,10 +1031,10 @@ var UnitFsmSpec = { "Timer": function(msg) { var target = this.order.data.target; - // Check we can still reach the target - if (!this.CheckTargetRange(target, IID_Builder)) + // Check we can still reach and repair the target + if (!this.CheckTargetRange(target, IID_Builder) || !this.CanRepair(target)) { - // Can't reach it, or it doesn't exist any more + // Can't reach it, no longer owned by ally, or it doesn't exist any more this.FinishOrder(); return; } @@ -1071,10 +1072,10 @@ var UnitFsmSpec = { } // If this building was e.g. a farmstead, we should look for nearby - // resources we can gather - var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); - if (cmpResourceDropsite) + // resources we can gather, if we are capable of doing so + if (this.CanReturnResource(msg.data.newentity)) { + var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, IID_ResourceDropsite); var types = cmpResourceDropsite.GetTypes(); var nearby = this.FindNearbyResource(function (ent, type) { return (types.indexOf(type.generic) != -1); @@ -1138,13 +1139,16 @@ var UnitFsmSpec = { "GARRISONED": { "enter": function() { - var cmpGarrisonHolder = Engine.QueryInterface(this.order.data.target, IID_GarrisonHolder); - if (cmpGarrisonHolder && cmpGarrisonHolder.Garrison(this.entity)) + var target = this.order.data.target; + // Check that we can still garrison here and that garrisoning succeeds + var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); + if (this.CanGarrison(target) && cmpGarrisonHolder.Garrison(this.entity)) { this.isGarrisoned = true; } else - { // Garrisoning failed for some reason, so finish the order + { + // Garrisoning failed for some reason, so finish the order if (this.FinishOrder()) return; } @@ -1205,7 +1209,8 @@ var UnitFsmSpec = { this.Attack(msg.data.attacker, false); } else if (this.template.NaturalBehaviour == "domestic") - { // Never flee, stop what we were doing + { + // Never flee, stop what we were doing this.SetNextState("IDLE"); } }, @@ -2436,17 +2441,30 @@ UnitAI.prototype.CanAttack = function(target) if (!cmpAttack) return false; - // TODO: verify that this is a valid target + // Verify that the target is owned by an enemy of this entity's player + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || !IsOwnedByEnemyOfPlayer(cmpOwnership.GetOwner(), target)) + return false; return true; }; UnitAI.prototype.CanGarrison = function(target) { + // Formation controllers should always respond to commands + // (then the individual units can make up their own minds) + if (this.IsFormationController()) + return true; + var cmpGarrisonHolder = Engine.QueryInterface(target, IID_GarrisonHolder); if (!cmpGarrisonHolder) return false; + // Verify that the target is owned by this entity's player + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) + return false; + // Don't let animals garrison for now // (If we want to support that, we'll need to change Order.Garrison so it // doesn't move the animal into an INVIDIDUAL.* state) @@ -2472,7 +2490,10 @@ UnitAI.prototype.CanGather = function(target) if (!cmpResourceGatherer.GetTargetGatherRate(target)) return false; - // TODO: should verify it's owned by the correct player, etc + // Verify that the target is owned by gaia or this entity's player + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || (!IsOwnedByGaia(target) && !IsOwnedByPlayer(cmpOwnership.GetOwner(), target))) + return false; return true; }; @@ -2500,7 +2521,10 @@ UnitAI.prototype.CanReturnResource = function(target) if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; - // TODO: should verify it's owned by the correct player, etc + // Verify that the dropsite is owned by this entity's player + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || !IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) + return false; return true; }; @@ -2517,7 +2541,10 @@ UnitAI.prototype.CanRepair = function(target) if (!cmpBuilder) return false; - // TODO: verify that this is a valid target + // Verify that the target is owned by an ally of this entity's player + var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); + if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)) + return false; return true; }; diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 0fd411fd2b..e3e59944f3 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -1,3 +1,7 @@ +// Setting this to true will display some warnings when commands +// are likely to fail, which may be useful for debugging AIs +var g_DebugCommands = false; + function ProcessCommand(player, cmd) { // Do some basic checks here that commanding player is valid @@ -12,6 +16,12 @@ function ProcessCommand(player, cmd) return; var controlAllUnits = cmpPlayer.CanControlAllUnits(); + // Note: checks of UnitAI targets are not robust enough here, as ownership + // can change after the order is issued, they should be checked by UnitAI + // when the specific behavior (e.g. attack, garrison) is performed. + // (Also it's not ideal if a command silently fails, it's nicer if UnitAI + // moves the entities closer to the target before giving up.) + // Now handle various commands switch (cmd.type) { @@ -27,7 +37,7 @@ function ProcessCommand(player, cmd) case "control-all": cmpPlayer.SetControlAllUnits(cmd.flag); break; - + case "reveal-map": // Reveal the map for all players, not just the current player, // primarily to make it obvious to everyone that the player is cheating @@ -43,66 +53,89 @@ function ProcessCommand(player, cmd) break; case "attack": - // Check if target is owned by player's enemy - if (IsOwnedByEnemyOfPlayer(player, cmd.target)) + if (g_DebugCommands && !IsOwnedByEnemyOfPlayer(player, cmd.target)) { - var entities = FilterEntityList(cmd.entities, player, controlAllUnits); - GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { - cmpUnitAI.Attack(cmd.target, cmd.queued); - }); + // This check is for debugging only! + warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd)); } + + // See UnitAI.CanAttack for target checks + var entities = FilterEntityList(cmd.entities, player, controlAllUnits); + GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { + cmpUnitAI.Attack(cmd.target, cmd.queued); + }); break; case "repair": // This covers both repairing damaged buildings, and constructing unfinished foundations - // Check if target building is owned by player or an ally - if (IsOwnedByAllyOfPlayer(player, cmd.target)) + if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target)) { - var entities = FilterEntityList(cmd.entities, player, controlAllUnits); - GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { - cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); - }); + // This check is for debugging only! + warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd)); } + + // See UnitAI.CanRepair for target checks + var entities = FilterEntityList(cmd.entities, player, controlAllUnits); + GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { + cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued); + }); break; case "gather": - // Check if target resource is owned by gaia or player - if (IsOwnedByGaia(cmd.target) || IsOwnedByPlayer(player, cmd.target)) + if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target))) { - var entities = FilterEntityList(cmd.entities, player, controlAllUnits); - GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { - cmpUnitAI.Gather(cmd.target, cmd.queued); - }); + // This check is for debugging only! + warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd)); } + + // See UnitAI.CanGather for target checks + var entities = FilterEntityList(cmd.entities, player, controlAllUnits); + GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { + cmpUnitAI.Gather(cmd.target, cmd.queued); + }); break; case "returnresource": // Check dropsite is owned by player - if (IsOwnedByPlayer(player, cmd.target)) + if (g_DebugCommands && IsOwnedByPlayer(player, cmd.target)) { - var entities = FilterEntityList(cmd.entities, player, controlAllUnits); - GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { - cmpUnitAI.ReturnResource(cmd.target, cmd.queued); - }); + // This check is for debugging only! + warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd)); } + + // See UnitAI.CanReturnResource for target checks + var entities = FilterEntityList(cmd.entities, player, controlAllUnits); + GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) { + cmpUnitAI.ReturnResource(cmd.target, cmd.queued); + }); break; case "train": + // Verify that the building can be controlled by the player if (CanControlUnit(cmd.entity, player, controlAllUnits)) { var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue); if (queue) queue.AddBatch(cmd.template, +cmd.count, cmd.metadata); } + else if (g_DebugCommands) + { + warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd)); + } break; case "stop-train": + // Verify that the building can be controlled by the player if (CanControlUnit(cmd.entity, player, controlAllUnits)) { var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue); if (queue) queue.RemoveBatch(cmd.id); } + else if (g_DebugCommands) + { + warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd)); + } break; case "construct": @@ -138,7 +171,7 @@ function ProcessCommand(player, cmd) if (ent == INVALID_ENTITY) { // Error (e.g. invalid template names) - error("Error creating foundation for '" + cmd.template + "'"); + error("Error creating foundation entity for '" + cmd.template + "'"); break; } @@ -147,52 +180,72 @@ function ProcessCommand(player, cmd) cmpPosition.JumpTo(cmd.x, cmd.z); cmpPosition.SetYRotation(cmd.angle); - // TODO: Build restrictions disabled for AI since it lacks a mechanism for checking most of them + // Check whether it's obstructed by other entities or invalid terrain + var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); + if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player)) + { + if (g_DebugCommands) + { + warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd)); + } + + var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" }); + + // Remove the foundation because the construction was aborted + Engine.DestroyEntity(ent); + break; + } + + // Check build limits + var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits); + if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) + { + if (g_DebugCommands) + { + warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd)); + } + + // TODO: The UI should tell the user they can't build this (but we still need this check) + + // Remove the foundation because the construction was aborted + Engine.DestroyEntity(ent); + break; + } + + // TODO: AI has no visibility info if (!cmpPlayer.IsAI()) { - // Check whether it's obstructed by other entities or invalid terrain - var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); - if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player)) - { - var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" }); - - // Remove the foundation because the construction was aborted - Engine.DestroyEntity(ent); - break; - } - - // Check build limits - var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits); - if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory())) - { - // TODO: The UI should tell the user they can't build this (but we still need this check) - - // Remove the foundation because the construction was aborted - Engine.DestroyEntity(ent); - break; - } - // Check whether it's in a visible region var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); var visible = (cmpRangeManager.GetLosVisibility(ent, player) == "visible"); if (!visible) { - // TODO: report error to player (the building site was not visible) - print("Building site was not visible\n"); + if (g_DebugCommands) + { + warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd)); + } + + var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" }); Engine.DestroyEntity(ent); break; } } - + var cmpCost = Engine.QueryInterface(ent, IID_Cost); if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts())) { + if (g_DebugCommands) + { + warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd)); + } + Engine.DestroyEntity(ent); break; } - + // Make it owned by the current player var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); cmpOwnership.SetOwner(player); @@ -214,7 +267,7 @@ function ProcessCommand(player, cmd) } break; - + case "delete-entities": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); for each (var ent in entities) @@ -246,13 +299,14 @@ function ProcessCommand(player, cmd) cmpRallyPoint.Unset(); } break; - + case "defeat-player": // Send "OnPlayerDefeated" message to player - Engine.PostMessage(playerEnt, MT_PlayerDefeated, null); + Engine.PostMessage(playerEnt, MT_PlayerDefeated, { "playerId": player } ); break; case "garrison": + // Verify that the building can be controlled by the player if (CanControlUnit(cmd.target, player, controlAllUnits)) { var entities = FilterEntityList(cmd.entities, player, controlAllUnits); @@ -260,9 +314,14 @@ function ProcessCommand(player, cmd) cmpUnitAI.Garrison(cmd.target); }); } + else if (g_DebugCommands) + { + warn("Invalid command: garrison target cannot be controlled by player "+player+": "+uneval(cmd)); + } break; - + case "unload": + // Verify that the building can be controlled by the player if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits)) { var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); @@ -274,9 +333,14 @@ function ProcessCommand(player, cmd) cmpGUIInterface.PushNotification(notification); } } + else if (g_DebugCommands) + { + warn("Invalid command: unload target cannot be controlled by player "+player+": "+uneval(cmd)); + } break; - + case "unload-all": + // Verify that the building can be controlled by the player if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits)) { var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder); @@ -288,6 +352,10 @@ function ProcessCommand(player, cmd) cmpGUIInterface.PushNotification(notification); } } + else if (g_DebugCommands) + { + warn("Invalid command: unload-all target cannot be controlled by player "+player+": "+uneval(cmd)); + } break; case "formation": @@ -302,6 +370,7 @@ function ProcessCommand(player, cmd) break; case "promote": + // No need to do checks here since this is a cheat anyway var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": "(Cheat - promoted units)"}); @@ -324,7 +393,7 @@ function ProcessCommand(player, cmd) break; default: - error("Ignoring unrecognised command type '" + cmd.type + "'"); + error("Invalid command: unknown command type: "+uneval(cmd)); } }