Re-enables build restrictions for AIs since they have basic compliance now.

Moves target ownership checks from Commands.js to UnitAI.
Adds more robust target checking in UnitAI by calling CanFoo functions
more frequently.
Adds optional debugging mode to Commands.js (useful for AI developers).

This was SVN commit r10570.
This commit is contained in:
historic_bruno 2011-11-22 00:16:35 +00:00
parent e9041a392b
commit 5cc856aedc
2 changed files with 183 additions and 87 deletions

View File

@ -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;
};

View File

@ -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)
{
@ -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,39 +180,54 @@ 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;
@ -189,6 +237,11 @@ function ProcessCommand(player, cmd)
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;
}
@ -249,10 +302,11 @@ function ProcessCommand(player, cmd)
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));
}
}