Jump to content

Portable 0.A.D with C#, .NET, Windows 8 Modern / RT


mahdi
 Share

Recommended Posts

Hi there,

I have been porting 0.A.D to C# and .NET Framework and DirectX since a year ago. must of the job is done and i am debugging AI and save/load.

The big point is the compatibility with Windows 8 Modern API and pure C# code, making it totally portable and running even on Tablets and Phones. It is App Store ready.

Since i have ported must of the code, i have a big understanding of both C++ and Javascript codes, knowing lots of todos in code. I am also a geek in graphics programming specially DirectX/HLSL, but also OpenGL/GLSL.

I am not a good C++ writer, but a good C++ reader and totally talented in C#.

Lets your words jump up.

Edited by mahdi
Link to comment
Share on other sites

Do you have the source code somewhere for us to look at? I would be interested to see your port. (also if you distribute your port this is a legal requirement).

I'm curious about your motivation for this, 0 A.D. is already portable, Ykkrosh successfully managed to get things running on an android phone. It sounds like a fun project to do anyway :).

Did you use automated code translation tools to do this? Also did you port the js to C# or keep that interpreted?

Link to comment
Share on other sites

Do you have the source code somewhere for us to look at? I would be interested to see your port. (also if you distribute your port this is a legal requirement).

I'm curious about your motivation for this, 0 A.D. is already portable, Ykkrosh successfully managed to get things running on an android phone. It sounds like a fun project to do anyway :).

Did you use automated code translation tools to do this? Also did you port the js to C# or keep that interpreted?

Of course. I will putting the source code somewhere online, probably on Assembla svn servers. The game can run on Windows Phone 8 though i know it already runs on Android.

I didnt use any code translation tools, coz they suck to translate or at least i am not familiar with them. The js also translated to C#, coz Win8 Modern Apps do not allow scripts interpreted.

Is it a full port to C# or a wrapper around the C++ somehow? A full mirror of the original code base would seem awfully cumbersome to keep in sync.

A full port in pure C#.

Link to comment
Share on other sites

Yes. The Modern API supports C++ but programming in it is totally different from the way you write code for old-style, so called Desktop, applications. You cannot compile 0.A.D. for modern api without a lot of effort.

EDIT:

OpenGL is not supported on modern api. you have to make it DirectX.

Edited by mahdi
Link to comment
Share on other sites

Here is a screenshot of my port. Please click the link below.

http://www.hilva.com/0ad/0AD.png

  • I disabled shadows due to an issue.
  • GUI is not at good quality, i removed them, too.
  • It is running 182 FPS at 1920x1080 pixels on my PC.

My PC configurations:

  • NVidia Geforce 520 GT
  • Intel Core 2 - 1.86 GHz
  • Windows 8

Edited by mahdi
Link to comment
Share on other sites

Here is a screenshot of my port. Please click the link below.

http://www.hilva.com/0ad/0AD.png

  • I disabled shadows due to an issue.
  • GUI is not at good quality, i removed them, too.
  • It is running 182 FPS at 1920x1080 pixels on my PC.

My PC configurations:

  • NVidia Geforce 520 GT
  • Intel Core 2 - 1.86 GHz
  • Windows 8

i see only the upper part of the screenshot.

just for curiosity: if you disable GUI and shadows in the C++ 0AD, what is your framerate?

Link to comment
Share on other sites

i see only the upper part of the screenshot. just for curiosity: if you disable GUI and shadows in the C++ 0AD, what is your framerate?

The image is massive. Please refresh the page to see the complete image. Google Chrome handles such situations very well.

Good question. I already tried to get FPS in 0.A.D. but no luck. it seems i have to check it back in a better time. maybe tomorrow.

Link to comment
Share on other sites

I think ribez is right, me too can only see the top part of the picture. Perhaps it stopped uploading from your PC for some reason. Converting it to JPEG would make it much smaller as an option.

Like quantumstate asked before it would be nice to have a link to the code itself since this is the most interesting thing in this topic (at least for me).

Edited by FeXoR
Link to comment
Share on other sites

just for curiosity: if you disable GUI and shadows in the C++ 0AD, what is your framerate?

OK i compared the frame rates of the two versions the with shadows and GUI enabled. Both on 1920x1080 pixels screen size.

My C# port runs 167 fps.

Original C++ version runs [41-54] fps.

Same scenario, same camera view.

Link to comment
Share on other sites

Thou C# is slower than C++ (i say C# is 95% of C++), the main reason for getting this gain would be:

  • I used DirectX 11 Feature Level, so i did GPU-Instancing for static models/particles and GPU-Skinning for animated ones; Which saves millions of matrix multiplications done by CPU per frame. The OpenGL code is eating CPU which makes the game compatible with any computer, But please consider that most of users have graphics cards not older than 4 years, so they support modern GPU features.
  • I translated Javascript to C#, so they are running code closer to machine language. I bet Microsoft's C# compiler beats Mozilla's Javascript interpreter, called SpiderMonkey (see file mozjs185-ps-release-1.0.dll in binaries folder).

At the moment, i am planning to debug, maintain and contribute to 0.A.D. Also i have to check legal stuff before distributing the code. So there would be no plan on moving to OpenGL/Mono; the game is already OpenGL and runs on Linux/Android.

EDIT:

DirectX is not faster than OpenGL, it completely depends on the feature level you ask the device. 0.A.D's opengl level is equivalent to DirectX 9.0, but my port uses DirectX 11. Migrating current opengl level to a higher one results in the same performance improvement, but less compatibility with devices. I think Android and iOS do not support advanced opengl codes.

Edited by mahdi
Link to comment
Share on other sites

This seems like a huge effort for just one person. How hard will it be to maintain? Won't you have to go through everyday to convert and add all of the new bug fixes to the c++ version?

Would it be possible in the c++ version to use the higher opengl feature level if the device supports it and use a fallback level if it doesn't?

Lastly if the javascript does cause a big slowdown maybe it would be a good idea to convert them to another language. Just some questions and thoughts.

Link to comment
Share on other sites

- Having SVN, it is not a very hard job to keep the source code in sync. Because the SVN shows you the exact file names changed in a particular revision and which lines of code and how it changed. Thanks to Diff. This can be done with every stable version or per revision.

- Yes, possible. many games do this for years.

- I don't know any better cross platform javascript interpreter for C++. But on Windows and C++.NET, you can still run C# as both compiled code and/or runtime interpreted code (Thanks to Microsoft's Roslyn). I had used Roslyn in several cases (for other projects, not 0.A.D. port), thou it runs as a script, it's still much faster than compiled C# code. Weird. Roslyn is still a CTP.

I hope these answers help.

Link to comment
Share on other sites

Awesome work my friend.

A few questions. The 0 A.D. development team has discussed abandoning some of the old rendering code. I wonder if this might affect your efforts?

If your playing 0 A.D. on a tablet with a touch screen how do you do a right click command like a mouse would do?

I'm excited to try this in the future :)

Link to comment
Share on other sites

- Having SVN, it is not a very hard job to keep the source code in sync. Because the SVN shows you the exact file names changed in a particular revision and which lines of code and how it changed. Thanks to Diff. This can be done with every stable version or per revision.

- Yes, possible. many games do this for years.

- I don't know any better cross platform javascript interpreter for C++. But on Windows and C++.NET, you can still run C# as both compiled code and/or runtime interpreted code (Thanks to Microsoft's Roslyn). I had used Roslyn in several cases (for other projects, not 0.A.D. port), thou it runs as a script, it's still much faster than compiled C# code. Weird. Roslyn is still a CTP.

I hope these answers help.

Thank you that does help.

Link to comment
Share on other sites

Awesome work my friend.

A few questions. The 0 A.D. development team has discussed abandoning some of the old rendering code. I wonder if this might affect your efforts?

If your playing 0 A.D. on a tablet with a touch screen how do you do a right click command like a mouse would do?

I'm excited to try this in the future :)

Thanks,

- No i have changed model rendering system to enable GPU animations and GPU instancing, though the public interface of the code is as same as 0 A.D.

- I have not experienced an RTS on Tablet yet, but i'll have to struggle about it when i buy a Windows 8 RT tablet. Yet, tablets will have USB port (iPad dont have) so we can connect a mouse. However right clicking in a tablet would be holding your finger 2 more seconds, or tap a finger and swiftly tap another finger. I have played an RTS title on Xbox with GamePad. It was a great XP and maybe a source of inspiration.

Link to comment
Share on other sites

  • 1 month later...

Here's C# code for UnitAI component, Originally created in Javascript.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ZeroAD.Helpers;
namespace ZeroAD.Components
{
public class CCmpUnitAI : ICmpUnitAI
{
public static void RegisterMessageTypes(CComponentManager mgr)
{
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.Create);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.Destroy);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.OwnershipChanged);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.DiplomacyChanged);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.MotionChanged);
mgr.SubscribeGloballyToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.ConstructionFinished);
mgr.SubscribeGloballyToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.EntityRenamed);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.Attacked);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.HealthChanged);
mgr.SubscribeToMessageType(ComponentTypeId.CID_UnitAI, MessageTypeId.RangeUpdate);
}
private static readonly SortedDictionary<string, UnitStance> g_Stances = new SortedDictionary<string, UnitStance>
{
{
"violent", new UnitStance
{
targetVisibleEnemies= true,
targetAttackersAlways= true,
targetAttackersPassive= true,
respondFlee= false,
respondChase= true,
respondChaseBeyondVision= true,
respondStandGround= false,
respondHoldGround= false,
}
},
{
"aggressive", new UnitStance
{
targetVisibleEnemies= true,
targetAttackersAlways= false,
targetAttackersPassive= true,
respondFlee= false,
respondChase= true,
respondChaseBeyondVision= false,
respondStandGround= false,
respondHoldGround= false,
}
},
{
"defensive", new UnitStance
{
targetVisibleEnemies= true,
targetAttackersAlways= false,
targetAttackersPassive= true,
respondFlee= false,
respondChase= false,
respondChaseBeyondVision= false,
respondStandGround= false,
respondHoldGround= true,
}
},
{
"passive", new UnitStance
{
targetVisibleEnemies= false,
targetAttackersAlways= false,
targetAttackersPassive= true,
respondFlee= true,
respondChase= false,
respondChaseBeyondVision= false,
respondStandGround= false,
respondHoldGround= false,
}
},
{
"standground", new UnitStance
{
targetVisibleEnemies= true,
targetAttackersAlways= false,
targetAttackersPassive= true,
respondFlee= false,
respondChase= false,
respondChaseBeyondVision= false,
respondStandGround= true,
respondHoldGround= false,
}
},
};


ZeroAD.Helpers.FSM UnitFsm;
private bool isIdle;
private bool isGarrisoned;
private List<OrderItem> orderQueue = new List<OrderItem>();
OrderItem order;
private string attackType;
private uint formationController;
private uint? losRangeQuery;
private uint? losHealRangeQuery;
private uint? losGaiaRangeQuery;
private bool resyncAnimation;
private int? lastAttacked;
private int? lastHealed;
private ICmpAttack.AttackTimer attackTimers;
private string stance;
public string fsmNextState;
public string fsmStateName;
public bool fsmReenter;
private int? timer;
CVector3D? heldPosition;
private string lastFormationName;
private ICmpHeal.HealTimer healTimers;

public CCmpUnitAI()
{
StateNode stateROOT = new StateNode();

// Default event handlers:
stateROOT.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
// ignore spurious movement messages
// (these can happen when stopping moving at the same time
// as switching states)
});
stateROOT.Handlers["MoveStarted"] = new Action<dynamic>((msg) =>
{
// ignore spurious movement messages
});
stateROOT.Handlers["ConstructionFinished"] = new Action<dynamic>((msg) =>
{
// ignore uninteresting construction messages
});
stateROOT.Handlers["LosRangeUpdate"] = new Action<dynamic>((msg) =>
{
// ignore newly-seen units by default
});
stateROOT.Handlers["LosGaiaRangeUpdate"] = new Action<dynamic>((msg) =>
{
// ignore newly-seen Gaia units by default
});
stateROOT.Handlers["LosHealRangeUpdate"] = new Action<dynamic>((msg) =>
{
// ignore newly-seen injured units by default
});
stateROOT.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
// ignore attacker
});
stateROOT.Handlers["HealthChanged"] = new Action<dynamic>((msg) =>
{
// ignore
});
stateROOT.Handlers["EntityRenamed"] = new Action<dynamic>((msg) =>
{
// ignore
});
// Formation handlers:
stateROOT.Handlers["FormationLeave"] = new Action<dynamic>((msg) =>
{
// ignore when we're not in FORMATIONMEMBER
});
// Called when being told to walk as part of a formation
stateROOT.Handlers["Order.FormationWalk"] = new Action<dynamic>((msg) =>
{
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
cmpUnitMotion.MoveToFormationOffset((uint)msg.data.target, (float)msg.data.x, (float)msg.data.z);
this.SetNextState("FORMATIONMEMBER.WALKING");
});
// Special orders:
// (these will be overridden by various states)
stateROOT.Handlers["Order.LeaveFoundation"] = new Action<dynamic>((msg) =>
{
if (!PlayerHelper.IsOwnedByAllyOfEntity(Engine, this.entity, (uint)msg.data.target))
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit((uint)msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.WALKING");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
});
stateROOT.Handlers["Order.Stop"] = new Action<dynamic>((msg) =>
{
// We have no control over non-domestic animals.
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
// Stop moving immediately.
this.StopMoving();
this.FinishOrder();
// No orders left, we're an individual now
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.IDLE");
}
else
{
this.SetNextState("INDIVIDUAL.IDLE");
}
});
stateROOT.Handlers["Order.Walk"] = new Action<dynamic>((msg) =>
{
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
this.SetHeldPosition((float)this.order.data.x, (float)this.order.data.z);
this.MoveToPoint((float)this.order.data.x, (float)this.order.data.z);
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.WALKING");
}
else
{
this.SetNextState("INDIVIDUAL.WALKING");
}
});
stateROOT.Handlers["Order.WalkToTarget"] = new Action<dynamic>((msg) =>
{
// Let players move captured domestic animals around
if (this.IsAnimal() && !this.IsDomestic())
{
this.FinishOrder();
return;
}
var ok = this.MoveToTarget((uint)this.order.data.target);
if (ok)
{
// We've started walking to the given point
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.WALKING");
}
else
{
this.SetNextState("INDIVIDUAL.WALKING");
}
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
});
stateROOT.Handlers["Order.Flee"] = new Action<dynamic>((msg) =>
{
// TODO: if we were attacked by a ranged unit, we need to flee much further away
var ok = this.MoveToTargetRangeExplicit((uint)this.order.data.target, this.template["FleeDistance"].ToFloat(), -1);
if (ok)
{
// We've started fleeing from the given target
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.FLEEING");
}
else
{
this.SetNextState("INDIVIDUAL.FLEEING");
}
}
else
{
// We are already at the target, or can't move at all
this.StopMoving();
this.FinishOrder();
}
});
stateROOT.Handlers["Order.Attack"] = new Action<dynamic>((msg) =>
{
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Work out how to attack the given target
var type = this.GetBestAttackAgainst(this.order.data.target);
if (type == null)
{
// Oops, we can't attack at all
this.FinishOrder();
return;
}
this.attackType = type;
// If we are already at the target, try attacking it from here
if (this.CheckTargetRange(this.order.data.target, InterfaceId.Attack, this.attackType))
{
this.StopMoving();
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.COMBAT.ATTACKING");
}
else
{
this.SetNextState("INDIVIDUAL.COMBAT.ATTACKING");
}
return;
}
// If we can't reach the target, but are standing ground, then abandon this attack order.
// Unless we're hunting, that's a special case where we should continue attacking our target.
if (this.GetStance().respondStandGround && !this.order.data.force && !this.order.data.hunting)
{
this.FinishOrder();
return;
}
// Try to move within attack range
if (this.MoveToTargetRange(this.order.data.target, InterfaceId.Attack, this.attackType))
{
// We've started walking to the given point
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.COMBAT.APPROACHING");
}
else
{
this.SetNextState("INDIVIDUAL.COMBAT.APPROACHING");
}
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this attack order
this.FinishOrder();
});
stateROOT.Handlers["Order.Heal"] = new Action<dynamic>((msg) =>
{
// Check the target is alive
if (!this.TargetIsAlive(this.order.data.target))
{
this.FinishOrder();
return;
}
// Healers can't heal themselves.
if (this.order.data.target == this.entity)
{
this.FinishOrder();
return;
}
// Check if the target is in range
if (this.CheckTargetRange(this.order.data.target, InterfaceId.Heal))
{
this.StopMoving();
this.SetNextState("INDIVIDUAL.HEAL.HEALING");
return;
}
// If we can't reach the target, but are standing ground,
// then abandon this heal order
if (this.GetStance().respondStandGround && !this.order.data.force)
{
this.FinishOrder();
return;
}
// Try to move within heal range
if (this.MoveToTargetRange(this.order.data.target, InterfaceId.Heal))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.HEAL.APPROACHING");
return;
}
// We can't reach the target, and can't move towards it,
// so abandon this heal order
this.FinishOrder();
});
stateROOT.Handlers["Order.Gather"] = new Action<dynamic>((msg) =>
{
// If the target is still alive, we need to kill it first
if (this.MustKillGatherTarget((uint)this.order.data.target) && this.CheckTargetVisible((uint)this.order.data.target))
{
// Make sure we can attack the target, else we'll get very stuck
if (this.GetBestAttackAgainst((uint)this.order.data.target) == null)
{
// Oops, we can't attack at all - give up
// TODO: should do something so the player knows why this failed
this.FinishOrder();
return;
}
this.PushOrderFront("Attack", new AllInOneData { target = this.order.data.target, force = false, hunting = true });
return;
}
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, InterfaceId.ResourceGatherer))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.GATHER.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try gathering it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextStateAlwaysEntering("INDIVIDUAL.GATHER.GATHERING");
}
});
stateROOT.Handlers["Order.GatherNearPosition"] = new Action<dynamic>((msg) =>
{
// Move the unit to the position to gather from.
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("INDIVIDUAL.GATHER.WALKING");
});
stateROOT.Handlers["Order.ReturnResource"] = new Action<dynamic>((msg) =>
{
// Try to move to the dropsite
if (this.MoveToTarget((uint)this.order.data.target))
{
// We've started walking to the target
this.SetNextState("INDIVIDUAL.RETURNRESOURCE.APPROACHING");
}
else
{
// Oops, we can't reach the dropsite.
// Maybe we should try to pick another dropsite, to find an
// accessible one?
// For now, just give up.
this.StopMoving();
this.FinishOrder();
return;
}
});
stateROOT.Handlers["Order.Trade"] = new Action<dynamic>((msg) =>
{
if (this.MoveToMarket(this.order.data.firstMarket))
{
// We've started walking to the first market
this.SetNextState("INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
}
});
stateROOT.Handlers["Order.Repair"] = new Action<dynamic>((msg) =>
{
// Try to move within range
if (this.MoveToTargetRange(this.order.data.target, InterfaceId.Builder, null))
{
// We've started walking to the given point
this.SetNextState("INDIVIDUAL.REPAIR.APPROACHING");
}
else
{
// We are already at the target, or can't move at all,
// so try repairing it from here.
// TODO: need better handling of the can't-reach-target case
this.StopMoving();
this.SetNextState("INDIVIDUAL.REPAIR.REPAIRING");
}
});
stateROOT.Handlers["Order.Garrison"] = new Action<dynamic>((msg) =>
{
if (this.MoveToTarget(this.order.data.target))
{
this.SetNextState("INDIVIDUAL.GARRISON.APPROACHING");
}
else
{
// We do a range check before actually garrisoning
this.StopMoving();
this.SetNextState("INDIVIDUAL.GARRISON.GARRISONED");
}
});
stateROOT.Handlers["Order.Cheering"] = new Action<dynamic>((msg) =>
{
this.SetNextState("INDIVIDUAL.CHEERING");
});
{
// States for the special entity representing a group of units moving in formation:
var stateFORMATIONCONTROLLER = stateROOT.SubStates["FORMATIONCONTROLLER"] = new StateNode();
stateFORMATIONCONTROLLER.Handlers["Order.Walk"] = new Action<dynamic>((msg) =>
{
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.SetHeldPosition((float)msg.data.x, (float)msg.data.z));
this.MoveToPoint(this.order.data.x, this.order.data.z);
this.SetNextState("WALKING");
});
// Only used by other orders to walk there in formation
stateFORMATIONCONTROLLER.Handlers["Order.WalkToTargetRange"] = new Action<dynamic>((msg) =>
{
if (this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
{
this.SetNextState("WALKING");
}
else
{
this.FinishOrder();
}
});
stateFORMATIONCONTROLLER.Handlers["Order.Stop"] = new Action<dynamic>((msg) =>
{
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.Stop(false));
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.Handlers["Order.Attack"] = new Action<dynamic>((msg) =>
{
// TODO: we should move in formation towards the target,
// then break up into individuals when close enough to it
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.Attack((uint)msg.data.target, false));
// TODO: we should wait until the target is killed, then
// move on to the next queued order.
// Don't bother now, just disband the formation immediately.
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.Handlers["Order.Heal"] = new Action<dynamic>((msg) =>
{
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.Heal((uint)msg.data.target, false));
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.Handlers["Order.Repair"] = new Action<dynamic>((msg) =>
{
// TODO on what should we base this range?
// Check if we are already in range, otherwise walk there
if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
{
if (!this.TargetIsAlive(msg.data.target))
// The building was finished or destroyed
this.FinishOrder();
else
// Out of range move there in formation
this.PushOrderFront("WalkToTargetRange", new AllInOneData { target = msg.data.target, min = 0, max = 10 });
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
// We don't want to rearrange the formation if the individual units are carrying
// out a task and one of the members dies/leaves the formation.
cmpFormation.SetRearrange(false);
cmpFormation.CallMemberFunction(ai => ai.Repair((uint)msg.data.target, (bool)msg.data.autocontinue, false));
this.SetNextState("REPAIR");
});
stateFORMATIONCONTROLLER.Handlers["Order.Gather"] = new Action<dynamic>((msg) =>
{
// TODO: see notes in Order.Attack
// If the resource no longer exists, send a GatherNearPosition order
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
if (this.CanGather(msg.data.target))
{
cmpFormation.CallMemberFunction(ai => ai.Gather((uint)msg.data.target, false));
}
else
{
cmpFormation.CallMemberFunction(ai => ai.GatherNearPosition(msg.data.lastPos.X, msg.data.lastPos.Z, msg.data.type, msg.data.template, false));
}
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.Handlers["Order.GatherNearPosition"] = new Action<dynamic>((msg) =>
{
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.GatherNearPosition(msg.data.x, msg.data.z, msg.data.type, msg.data.template, false));
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.Handlers["Order.ReturnResource"] = new Action<dynamic>((msg) =>
{
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.ReturnResource((uint)msg.data.target, false));
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.Handlers["Order.Garrison"] = new Action<dynamic>((msg) =>
{
// TODO: see notes in Order.Attack
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.CallMemberFunction(ai => ai.Garrison((uint)msg.data.target, false));
cmpFormation.Disband();
});
stateFORMATIONCONTROLLER.SubStates["IDLE"] = new StateNode();
{
var stateFORMATIONCONTROLLER_WALKING = stateFORMATIONCONTROLLER.SubStates["WALKING"] = new StateNode();
stateFORMATIONCONTROLLER_WALKING.Handlers["MoveStarted"] = new Action<dynamic>((msg) =>
{
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.SetRearrange(true);
cmpFormation.MoveMembersIntoFormation(true);
});
stateFORMATIONCONTROLLER_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
if (this.FinishOrder())
return;
// If this was the last order, attempt to disband the formation.
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.FindInPosition();
});
}
{
var stateFORMATIONCONTROLLER_REPAIR = stateFORMATIONCONTROLLER.SubStates["REPAIR"] = new StateNode();
stateFORMATIONCONTROLLER_REPAIR.Handlers["ConstructionFinished"] = new Action<dynamic>((msg) =>
{
if (msg.data.entity != this.order.data.target)
{
return;
}
if (this.FinishOrder())
{
return;
}
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
cmpFormation.Disband();
});
}
}
{
// States for entities moving as part of a formation:
var stateFORMATIONMEMBER = stateROOT.SubStates["FORMATIONMEMBER"] = new StateNode();
stateFORMATIONMEMBER.Handlers["FormationLeave"] = new Action<dynamic>((msg) =>
{
// Stop moving as soon as the formation disbands
this.StopMoving();
// If the controller handled an order but some members rejected it,
// they will have no orders and be in the FORMATIONMEMBER.IDLE state.
if (this.orderQueue.Count != 0)
{
// We're leaving the formation, so stop our FormationWalk order
if (this.FinishOrder())
{
return;
}
}
// No orders left, we're an individual now
if (this.IsAnimal())
{
this.SetNextState("ANIMAL.IDLE");
}
else
{
this.SetNextState("INDIVIDUAL.IDLE");
}
});
// Override the LeaveFoundation order since we're not doing
// anything more important (and we might be stuck in the WALKING
// state forever and need to get out of foundations in that case)
stateFORMATIONMEMBER.Handlers["Order.LeaveFoundation"] = new Action<dynamic>((msg) =>
{
if (!PlayerHelper.IsOwnedByAllyOfEntity(Engine, this.entity, msg.data.target))
{
this.FinishOrder();
return;
}
// Move a tile outside the building
var range = 4;
var ok = this.MoveToTargetRangeExplicit(msg.data.target, range, range);
if (ok)
{
// We've started walking to the given point
this.SetNextState("WALKINGTOPOINT");
}
else
{
// We are already at the target, or can't move at all
this.FinishOrder();
}
});
{
var stateFORMATIONMEMBER_IDLE = stateFORMATIONMEMBER.SubStates["IDLE"] = new StateNode();
stateFORMATIONMEMBER_IDLE.Handlers["enter"] = new Action<dynamic>((msg) => this.SelectAnimation("idle"));
}
{
var stateFORMATIONMEMBER_WALKING = stateFORMATIONMEMBER.SubStates["WALKING"] = new StateNode();
stateFORMATIONMEMBER_WALKING.Handlers["enter"] = new Action<dynamic>((msg) => this.SelectAnimation("move"));
// Occurs when the unit has reached its destination and the controller
// is done moving. The controller is notified, and will disband the
// formation if all units are in formation and no orders remain.
stateFORMATIONMEMBER_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
var cmpFormation = Engine.QueryInterface(this.formationController, InterfaceId.Formation) as ICmpFormation;
cmpFormation.SetInPosition(this.entity);
});
}
{
var stateFORMATIONMEMBER_WALKINGTOPOINT = stateFORMATIONMEMBER.SubStates["WALKINGTOPOINT"] = new StateNode();
stateFORMATIONMEMBER_WALKINGTOPOINT.Handlers["enter"] = new Action<dynamic>((msg) =>
{
var cmpFormation = Engine.QueryInterface(this.formationController, InterfaceId.Formation) as ICmpFormation;
cmpFormation.UnsetInPosition(this.entity);
this.SelectAnimation("move");
});
stateFORMATIONMEMBER_WALKINGTOPOINT.Handlers["MoveCompleted"] = new Action<dynamic>((msg) => { this.FinishOrder(); });
}
}
{
var stateINDIVIDUAL = stateROOT.SubStates["INDIVIDUAL"] = new StateNode();
stateINDIVIDUAL.Handlers["enter"] = new Action<dynamic>((msg) =>
{
// Sanity-checking
if (this.IsAnimal())
{
//error("Animal got moved into INDIVIDUAL.* state");
}
});
stateINDIVIDUAL.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
// Respond to attack if we always target attackers, or if we target attackers
// during passive orders (e.g. gathering/repairing are never forced)
if (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && (this.order == null || this.order.data == null || !this.order.data.force)))
{
this.RespondToTargetedEntities(new[] { (uint)msg.data.attacker });
}
});
{
var stateINDIVIDUAL_IDLE = stateINDIVIDUAL.SubStates["IDLE"] = new StateNode();
stateINDIVIDUAL_IDLE.Handlers["enter"] = new Func<dynamic, bool>((msg) =>
{
// Switch back to idle animation to guarantee we won't
// get stuck with an incorrect animation
this.SelectAnimation("idle");
// The GUI and AI want to know when a unit is idle, but we don't
// want to send frequent spurious messages if the unit's only
// idle for an instant and will quickly go off and do something else.
// So we'll set a timer here and only report the idle event if we
// remain idle
this.StartTimer(1000, 0);
// If a unit can heal and attack we first want to heal wounded units,
// so check if we are a healer and find whether there's anybody nearby to heal.
// (If anyone approaches later it'll be handled via LosHealRangeUpdate.)
// If anyone in sight gets hurt that will be handled via LosHealRangeUpdate.
if (this.IsHealer() && this.FindNewHealTargets())
{
return true; // (abort the FSM transition since we may have already switched state)
}
// If we entered the idle state we must have nothing better to do,
// so immediately check whether there's anybody nearby to attack.
// (If anyone approaches later, it'll be handled via LosRangeUpdate.)
if (this.FindNewTargets())
{
return true; // (abort the FSM transition since we may have already switched state)
}
// Nobody to attack - stay in idle
return false;
});
stateINDIVIDUAL_IDLE.Handlers["leave"] = new Action<dynamic>((msg) =>
{
var rangeMan = (ICmpRangeManager)Engine.QueryInterface(InterfaceId.RangeManager);
rangeMan.DisableActiveQuery(this.losRangeQuery.Value);
if (this.losHealRangeQuery != null)
{
rangeMan.DisableActiveQuery(this.losHealRangeQuery.Value);
}
this.StopTimer();
if (this.isIdle)
{
this.isIdle = false;
Engine.PostMessage(this.entity, new CMessageUnitIdleChanged { idle = this.isIdle });
}
});
stateINDIVIDUAL_IDLE.Handlers["LosRangeUpdate"] = new Action<dynamic>((msg) =>
{
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackEntitiesByPreference(msg.data.added);
}
});
stateINDIVIDUAL_IDLE.Handlers["LosGaiaRangeUpdate"] = new Action<dynamic>((msg) =>
{
if (this.GetStance().targetVisibleEnemies)
{
// Start attacking one of the newly-seen enemy (if any)
this.AttackGaiaEntitiesByPreference(msg.data.added);
}
});
stateINDIVIDUAL_IDLE.Handlers["LosHealRangeUpdate"] = new Action<dynamic>((msg) =>
{
this.RespondToHealableEntities(msg.data.added);
});
stateINDIVIDUAL_IDLE.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
if (!this.isIdle)
{
this.isIdle = true;
Engine.PostMessage(this.entity, new CMessageUnitIdleChanged { idle = this.isIdle });
}
});
}
{
var stateINDIVIDUAL_WALKING = stateINDIVIDUAL.SubStates["WALKING"] = new StateNode();
stateINDIVIDUAL_WALKING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
});
stateINDIVIDUAL_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.FinishOrder();
});
}
{
var stateINDIVIDUAL_FLEEING = stateINDIVIDUAL.SubStates["FLEEING"] = new StateNode();
stateINDIVIDUAL_FLEEING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.PlaySound("panic");
// Run quickly
var speed = this.GetRunSpeed();
this.SelectAnimation("move");
this.SetMoveSpeed(speed);
});
stateINDIVIDUAL_FLEEING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
// Reset normal speed
this.SetMoveSpeed(this.GetWalkSpeed());
});
stateINDIVIDUAL_FLEEING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
// When we've run far enough, stop fleeing
this.FinishOrder();
});
// TODO: what if we run into more enemies while fleeing?
}
{
var stateINDIVIDUAL_COMBAT = stateINDIVIDUAL.SubStates["COMBAT"] = new StateNode();
stateINDIVIDUAL_COMBAT.Handlers["Order.LeaveFoundation"] = new Func<dynamic, bool>((msg) =>
{
// Ignore the order as we're busy.
//return new { discardOrder = true };
return true;
});
stateINDIVIDUAL_COMBAT.Handlers["EntityRenamed"] = new Action<dynamic>((msg) =>
{
if (this.order.data.target == msg.entity)
{
this.order.data.target = msg.newentity;
// If we're hunting, that means we have a queued gather
// order whose target also needs to be updated.
if (this.order.data.hunting && this.orderQueue[1] != null && this.orderQueue[1].type == "Gather")
{
this.orderQueue[1].data.target = msg.newentity;
}
}
});
stateINDIVIDUAL_COMBAT.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
// If we're already in combat mode, ignore anyone else
// who's attacking us
});
{
var stateINDIVIDUAL_COMBAT_APPROACHING = stateINDIVIDUAL_COMBAT.SubStates["APPROACHING"] = new StateNode();
stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
});
stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
// Show carried resources when walking.
this.SetGathererAnimationOverride(false);
this.StopTimer();
});
stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
if (this.ShouldAbandonChase(this.order.data.target, this.order.data.force, InterfaceId.Attack))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
{
this.WalkToHeldPosition();
}
}
});
stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.SetNextState("ATTACKING");
});
stateINDIVIDUAL_COMBAT_APPROACHING.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
// If we're attacked by a close enemy, we should try to defend ourself
// but only if we're not forced to target something else
if ((string)msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !(bool)this.order.data.force)))
{
this.RespondToTargetedEntities(new[] { (uint)msg.data.attacker });
}
});
}
{
var stateINDIVIDUAL_COMBAT_ATTACKING = stateINDIVIDUAL_COMBAT.SubStates["ATTACKING"] = new StateNode();
stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
this.attackTimers = cmpAttack.GetTimers(this.attackType);
// If the repeat time since the last attack hasn't elapsed,
// delay this attack to avoid attacking too fast.
var prepare = this.attackTimers.prepare;
if (this.lastAttacked != null)
{
var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer;
var repeatLeft = this.lastAttacked.GetValueOrDefault() + this.attackTimers.repeat - cmpTimer.GetTime();
prepare = Math.Max(prepare, repeatLeft);
}
this.SelectAnimation("melee", false, 1.0f, "attack");
this.SetAnimationSync(prepare, this.attackTimers.repeat);
this.StartTimer(prepare, this.attackTimers.repeat);
// TODO: we should probably only bother syncing projectile attacks, not melee
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.attackTimers.prepare) ? true : false;
this.FaceTowardsTarget((uint)this.order.data.target);
});
stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
});
stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
var target = (uint)this.order.data.target;
// Check the target is still alive and attackable
if (this.TargetIsAlive(target) && this.CanAttack(target, (bool)this.order.data.forceResponse))
{
// Check we can still reach the target
if (this.CheckTargetRange(target, InterfaceId.Attack, this.attackType))
{
var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer;
this.lastAttacked = cmpTimer.GetTime() - (int)msg.lateness;
this.FaceTowardsTarget(target);
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
cmpAttack.PerformAttack(this.attackType, target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.attackTimers.repeat, this.attackTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, InterfaceId.Attack, this.attackType))
{
this.SetNextState("COMBAT.CHASING");
return;
}
}
}
// Can't reach it, no longer owned by enemy, or it doesn't exist any more - give up
if (this.FinishOrder())
{
return;
}
// See if we can switch to a new nearby enemy
if (this.FindNewTargets())
{
// Attempt to immediately re-enter the timer function, to avoid wasting the attack.
this.TimerHandler(msg.data, msg.lateness);
{
return;
}
}
// Return to our original position
if (this.GetStance().respondHoldGround)
{
this.WalkToHeldPosition();
}
});
// TODO: respond to target deaths immediately, rather than waiting
// until the next Timer event
stateINDIVIDUAL_COMBAT_ATTACKING.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
if (this.order.data.target != msg.data.attacker)
{
// If we're attacked by a close enemy, stronger than our current target,
// we choose to attack it, but only if we're not forced to target something else
if (msg.data.type == "Melee" && (this.GetStance().targetAttackersAlways || (this.GetStance().targetAttackersPassive && !this.order.data.force)))
{
var ents = new List<uint> { (uint)this.order.data.target, (uint)msg.data.attacker };
EntityHelper.SortEntitiesByPriority(Engine, ents);
if (ents[0] != this.order.data.target)
{
this.RespondToTargetedEntities(ents.ToArray());
}
}
}
});
}
{
var stateINDIVIDUAL_COMBAT_CHASING = stateINDIVIDUAL_COMBAT.SubStates["CHASING"] = new StateNode();
stateINDIVIDUAL_COMBAT_CHASING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
// Show weapons rather than carried resources.
this.SetGathererAnimationOverride(true);
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
});
stateINDIVIDUAL_COMBAT_CHASING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
// Show carried resources when walking.
this.SetGathererAnimationOverride(false);
this.StopTimer();
});
stateINDIVIDUAL_COMBAT_CHASING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
if (this.ShouldAbandonChase((uint)this.order.data.target, (bool)this.order.data.force, InterfaceId.Attack))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
{
this.WalkToHeldPosition();
}
}
});
stateINDIVIDUAL_COMBAT_CHASING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.SetNextState("ATTACKING");
});
}
}
{
var stateINDIVIDUAL_GATHER = stateINDIVIDUAL.SubStates["GATHER"] = new StateNode();
{
var stateINDIVIDUAL_GATHER_APPROACHING = stateINDIVIDUAL_GATHER.SubStates["APPROACHING"] = new StateNode();
stateINDIVIDUAL_GATHER_APPROACHING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
});
stateINDIVIDUAL_GATHER_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
if ((bool)msg.data.error)
{
// We failed to reach the target
// Save the current order's data in case we need it later
var oldType = this.order.data.type;
var oldTarget = (uint)this.order.data.target;
var oldTemplate = (string)this.order.data.template;
// Try the next queued order if there is any
if (this.FinishOrder())
{
return;
}
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource((ent, type, template) =>
{
return (
ent != oldTarget
&& ((type.generic == "treasure" && oldType.generic == "treasure")
|| (type.specific == oldType.specific
&& (type.specific != "meat" || oldTemplate == template)))
);
});
if (nearby != null)
{
this.PerformGather(nearby.Value, false, false);
return;
}
// Couldn't find anything else. Just try this one again,
// maybe we'll succeed next time
this.PerformGather(oldTarget, false, false);
return;
}
// We reached the target - start gathering from it now
this.SetNextState("GATHERING");
});
}
{
var stateINDIVIDUAL_GATHER_WALKING = stateINDIVIDUAL_GATHER.SubStates["WALKING"] = new StateNode();
stateINDIVIDUAL_GATHER_WALKING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
});
stateINDIVIDUAL_GATHER_WALKING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
var resourceType = this.order.data.type;
var resourceTemplate = this.order.data.template;
// Try to find another nearby target of the same specific type
// Also don't switch to a different type of huntable animal
var nearby = this.FindNearbyResource((ent, type, template) =>
{
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
// If there is a nearby resource start gathering
if (nearby != null)
{
this.PerformGather(nearby.Value, false, false);
return;
}
// Couldn't find nearby resources, so give up
this.FinishOrder();
});
}
{
var stateINDIVIDUAL_GATHER_GATHERING = stateINDIVIDUAL_GATHER.SubStates["GATHERING"] = new StateNode();
stateINDIVIDUAL_GATHER_GATHERING.Handlers["enter"] = new Func<dynamic, bool>((msg) =>
{
var target = (uint)this.order.data.target;
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
this.order.data.force = false;
this.order.data.autoharvest = true;
// Calculate timing based on gather rates
// This allows the gather rate to control how often we gather, instead of how much.
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
var rate = cmpResourceGatherer.GetTargetGatherRate(target);
if (rate == 0)
{
// Try to find another target if the current one stopped existing
if (Engine.QueryInterface(target, InterfaceId.Identity) == null)
{
// Let the Timer logic handle this
this.StartTimer(0, null);
return false;
}
// No rate, give up on gathering
this.FinishOrder();
return true;
}
// Scale timing interval based on rate, and start timer
// The offset should be at least as long as the repeat time so we use the same value for both.
var offset = 1000 / rate;
var repeat = offset;
this.StartTimer(offset, repeat);
// We want to start the gather animation as soon as possible,
// but only if we're actually at the target and it's still alive
// (else it'll look like we're chopping empty air).
// (If it's not alive, the Timer handler will deal with sending us
// off to a different target.)
if (this.CheckTargetRange(target, InterfaceId.ResourceGatherer))
{
var typename = "gather_" + this.order.data.type.specific;
this.SelectAnimation(typename, false, 1.0f, typename);
}
return false;
});
stateINDIVIDUAL_GATHER_GATHERING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
// Show the carried resource, if we've gathered anything.
this.SetGathererAnimationOverride(false);
});
stateINDIVIDUAL_GATHER_GATHERING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
var target = this.order.data.target;
var resourceTemplate = this.order.data.template;
var resourceType = this.order.data.type as Name;
// Check we can still reach and gather from the target
if (this.CheckTargetRange(target, InterfaceId.ResourceGatherer) && this.CanGather(target))
{
// Gather the resources:
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
// Try to gather treasure
if (cmpResourceGatherer.TryInstantGather(target))
{
return;
}
// If we've already got some resources but they're the wrong type,
// drop them first to ensure we're only ever carrying one type
if (cmpResourceGatherer.IsCarryingAnythingExcept(resourceType.generic))
{
cmpResourceGatherer.DropResources();
}
// Collect from the target
var status = cmpResourceGatherer.PerformGather(target);
// If we've collected as many resources as possible,
// return to the nearest dropsite
if (status.filled)
{
var nearby = this.FindNearestDropsite(resourceType.generic);
if (nearby != null)
{
// (Keep this Gather order on the stack so we'll
// continue gathering after returning)
this.PushOrderFront("ReturnResource", new AllInOneData { target = nearby.Value, force = false });
return;
}
// Oh no, couldn't find any drop sites. Give up on gathering.
this.FinishOrder();
return;
}
// We can gather more from this target, do so in the next timer
if (!status.exhausted)
{
return;
}
}
else
{
// Try to follow the target
if (this.MoveToTargetRange(target, InterfaceId.ResourceGatherer))
{
this.SetNextState("APPROACHING");
return;
}
// Can't reach the target, or it doesn't exist any more
// We want to carry on gathering resources in the same area as
// the old one. So try to get close to the old resource's
// last known position
var maxRange = 8; // get close but not too close
if (this.order.data.lastPos != null && this.MoveToPointRange(this.order.data.lastPos.Value.X, this.order.data.lastPos.Value.Z, 0, maxRange))
{
this.SetNextState("APPROACHING");
return;
}
}
// We're already in range, can't get anywhere near it or the target is exhausted.
// Give up on this order and try our next queued order
if (this.FinishOrder())
{
return;
}
// No remaining orders - pick a useful default behaviour
// Try to find a new resource of the same specific type near our current position:
// Also don't switch to a different type of huntable animal
var nearby0 = this.FindNearbyResource((ent, type, template) =>
{
return (
(type.generic == "treasure" && resourceType.generic == "treasure")
|| (type.specific == resourceType.specific
&& (type.specific != "meat" || resourceTemplate == template))
);
});
if (nearby0 != null)
{
this.PerformGather(nearby0.Value, false, false);
return;
}
// Nothing else to gather - if we're carrying anything then we should
// drop it off, and if not then we might as well head to the dropsite
// anyway because that's a nice enough place to congregate and idle
nearby0 = this.FindNearestDropsite(resourceType.generic);
if (nearby0 != null)
{
this.PushOrderFront("ReturnResource", new AllInOneData { target = nearby0.Value, force = false });
return;
}
// No dropsites - just give up
});
}
}
{
var stateINDIVIDUAL_HEAL = stateINDIVIDUAL.SubStates["HEAL"] = new StateNode();
{
var stateINDIVIDUAL_HEAL_APPROACHING = stateINDIVIDUAL_HEAL.SubStates["APPROACHING"] = new StateNode();
stateINDIVIDUAL_HEAL_APPROACHING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
});
stateINDIVIDUAL_HEAL_APPROACHING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
});
stateINDIVIDUAL_HEAL_APPROACHING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
if (this.ShouldAbandonChase((uint)this.order.data.target, (bool)this.order.data.force, InterfaceId.Heal))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
{
this.WalkToHeldPosition();
}
}
});
stateINDIVIDUAL_HEAL_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.SetNextState("HEALING");
});
}
{
var stateINDIVIDUAL_HEAL_HEALING = stateINDIVIDUAL_HEAL.SubStates["HEALING"] = new StateNode();
stateINDIVIDUAL_HEAL_HEALING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
var cmpHeal = Engine.QueryInterface(this.entity, InterfaceId.Heal) as ICmpHeal;
this.healTimers = cmpHeal.GetTimers();
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
var prepare = this.healTimers.prepare;
if (this.lastHealed != null)
{
var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer;
var repeatLeft = this.lastHealed + this.healTimers.repeat - cmpTimer.GetTime();
prepare = Math.Max(prepare, repeatLeft.Value);
}
this.SelectAnimation("heal", false, 1.0f, "heal");
this.SetAnimationSync(prepare, this.healTimers.repeat);
this.StartTimer(prepare, this.healTimers.repeat);
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = (prepare != this.healTimers.prepare) ? true : false;
this.FaceTowardsTarget(this.order.data.target);
});
stateINDIVIDUAL_HEAL_HEALING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
});
stateINDIVIDUAL_HEAL_HEALING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
var target = (uint)this.order.data.target;
// Check the target is still alive and healable
if (this.TargetIsAlive(target) && this.CanHeal(target))
{
// Check if we can still reach the target
if (this.CheckTargetRange(target, InterfaceId.Heal))
{
var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer;
this.lastHealed = cmpTimer.GetTime() - msg.lateness;
this.FaceTowardsTarget(target);
var cmpHeal = Engine.QueryInterface(this.entity, InterfaceId.Heal) as ICmpHeal;
cmpHeal.PerformHeal(target);
if (this.resyncAnimation)
{
this.SetAnimationSync(this.healTimers.repeat, this.healTimers.repeat);
this.resyncAnimation = false;
}
return;
}
// Can't reach it - try to chase after it
if (this.ShouldChaseTargetedEntity(target, this.order.data.force))
{
if (this.MoveToTargetRange(target, InterfaceId.Heal))
{
this.SetNextState("HEAL.CHASING");
return;
}
}
}
// Can't reach it, healed to max hp or doesn't exist any more - give up
if (this.FinishOrder())
{
return;
}
// Heal another one
if (this.FindNewHealTargets())
{
return;
}
// Return to our original position
if (this.GetStance().respondHoldGround)
{
this.WalkToHeldPosition();
}
});
}
{
var stateINDIVIDUAL_HEAL_CHASING = stateINDIVIDUAL_HEAL.SubStates["CHASING"] = new StateNode();
stateINDIVIDUAL_HEAL_CHASING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
this.StartTimer(1000, 1000);
});
stateINDIVIDUAL_HEAL_CHASING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
});
stateINDIVIDUAL_HEAL_CHASING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
if (this.ShouldAbandonChase((uint)this.order.data.target, (bool)this.order.data.force, InterfaceId.Heal))
{
this.StopMoving();
this.FinishOrder();
// Return to our original position
if (this.GetStance().respondHoldGround)
{
this.WalkToHeldPosition();
}
}
});
stateINDIVIDUAL_HEAL_CHASING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.SetNextState("HEALING");
});
}
}
{
var stateINDIVIDUAL_RETURNRESOURCE = stateINDIVIDUAL.SubStates["RETURNRESOURCE"] = new StateNode();
{
var stateINDIVIDUAL_RETURNRESOURCE_APPROACHING = stateINDIVIDUAL_RETURNRESOURCE.SubStates["APPROACHING"] = new StateNode();
stateINDIVIDUAL_RETURNRESOURCE_APPROACHING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
// Work out what we're carrying, in order to select an appropriate animation
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
var type = cmpResourceGatherer.GetLastCarriedType();
if (type != null)
{
var typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
{
typename = "carry_" + type.specific;
}
this.SelectAnimation(typename, false, this.GetWalkSpeed());
}
else
{
// We're returning empty-handed
this.SelectAnimation("move");
}
});
stateINDIVIDUAL_RETURNRESOURCE_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
// Switch back to idle animation to guarantee we won't
// get stuck with the carry animation after stopping moving
this.SelectAnimation("idle");
// Check the dropsite is in range and we can return our resource there
// (we didn't get stopped before reaching it)
if (this.CheckTargetRange((uint)this.order.data.target, InterfaceId.ResourceGatherer) && this.CanReturnResource((uint)this.order.data.target, true))
{
var cmpResourceDropsite = Engine.QueryInterface((uint)this.order.data.target, InterfaceId.ResourceDropsite) as ICmpResourceDropsite;
if (cmpResourceDropsite != null)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
cmpResourceGatherer.CommitResources(dropsiteTypes);
// Our next order should always be a Gather,
// so just switch back to that order
this.FinishOrder();
return;
}
}
// The dropsite was destroyed, or we couldn't reach it, or ownership changed
// Look for a new one.
var cmpResourceGatherer0 = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
var genericType = cmpResourceGatherer0.GetMainCarryingType();
var nearby = this.FindNearestDropsite(genericType);
if (nearby != null)
{
this.FinishOrder();
this.PushOrderFront("ReturnResource", new AllInOneData { target = nearby.Value, force = false });
return;
}
// Oh no, couldn't find any drop sites. Give up on returning.
this.FinishOrder();
});
}
}
{
var stateINDIVIDUAL_TRADE = stateINDIVIDUAL.SubStates["TRADE"] = new StateNode();
stateINDIVIDUAL_TRADE.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
// Ignore attack
// TODO: Inform player
});
{
var stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET = stateINDIVIDUAL_TRADE.SubStates["APPROACHINGFIRSTMARKET"] = new StateNode();
stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
});
stateINDIVIDUAL_TRADE_APPROACHINGFIRSTMARKET.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.PerformTradeAndMoveToNextMarket(this.order.data.firstMarket, this.order.data.secondMarket, "INDIVIDUAL.TRADE.APPROACHINGSECONDMARKET");
});
}
{
var stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET = stateINDIVIDUAL_TRADE.SubStates["APPROACHINGSECONDMARKET"] = new StateNode();
stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
});
stateINDIVIDUAL_TRADE_APPROACHINGSECONDMARKET.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.order.data.firstPass = false;
this.PerformTradeAndMoveToNextMarket(this.order.data.secondMarket, this.order.data.firstMarket, "INDIVIDUAL.TRADE.APPROACHINGFIRSTMARKET");
});
}
}
{
var stateINDIVIDUAL_REPAIR = stateINDIVIDUAL.SubStates["REPAIR"] = new StateNode();
stateINDIVIDUAL_REPAIR.Handlers["ConstructionFinished"] = new Action<dynamic>((msg) =>
{
if (msg.data.entity != this.order.data.target)
return; // ignore other buildings
// Save the current order's data in case we need it later
var oldData = this.order.data;
// Save the current state so we can continue walking if necessary
// FinishOrder() below will switch to IDLE if there's no order, which sets the idle animation.
// Idle animation while moving towards finished construction looks weird (ghosty).
var oldState = this.GetCurrentState();
// We finished building it.
// Switch to the next order (if any)
if (this.FinishOrder())
return;
// No remaining orders - pick a useful default behaviour
// If autocontinue explicitly disabled (e.g. by AI) then
// do nothing automatically
if (!oldData.autocontinue)
return;
// If this building was e.g. a farm of ours, the entities that recieved
// the build command should start gathering from it
if ((oldData.force || oldData.autoharvest) && this.CanGather(msg.data.newentity))
{
this.PerformGather(msg.data.newentity, true, false);
return;
}
// If this building was e.g. a farmstead of ours, entities that received
// the build command should look for nearby resources to gather
if ((oldData.force || oldData.autoharvest) && this.CanReturnResource(msg.data.newentity, false))
{
var cmpResourceDropsite = Engine.QueryInterface(msg.data.newentity, InterfaceId.ResourceDropsite) as ICmpResourceDropsite;
var types = cmpResourceDropsite.GetTypes();
// TODO: Slightly undefined behavior here, we don't know what type of resource will be collected,
// may cause problems for AIs (especially hunting fast animals), but avoid ugly hacks to fix that!
var nearby = this.FindNearbyResource((ent, type, template) =>
{
return (types.indexOf(type.generic) != -1);
});
if (nearby != null)
{
this.PerformGather(nearby.Value, true, false);
return;
}
}
// Look for a nearby foundation to help with
var nearbyFoundation = this.FindNearbyFoundation();
if (nearbyFoundation != null)
{
this.AddOrder("Repair", new AllInOneData { target = nearbyFoundation.Value, autocontinue = (bool)oldData.autocontinue, force = false }, true);
return;
}
// Unit was approaching and there's nothing to do now, so switch to walking
if (oldState == "INDIVIDUAL.REPAIR.APPROACHING")
{
// We're already walking to the given point, so add this as a order.
this.WalkToTarget(msg.data.newentity, true);
}
});
{
var stateINDIVIDUAL_REPAIR_APPROACHING = stateINDIVIDUAL_REPAIR.SubStates["APPROACHING"] = new StateNode();
stateINDIVIDUAL_REPAIR_APPROACHING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("move");
});
stateINDIVIDUAL_REPAIR_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.SetNextState("REPAIRING");
});
}
{
var stateINDIVIDUAL_REPAIR_REPAIRING = stateINDIVIDUAL_REPAIR.SubStates["REPAIRING"] = new StateNode();
stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["enter"] = new Func<dynamic, bool>((msg) =>
{
// If this order was forced, the player probably gave it, but now we've reached the target
// switch to an unforced order (can be interrupted by attacks)
if ((bool)this.order.data.force)
{
this.order.data.autoharvest = true;
}
this.order.data.force = false;
var target = this.order.data.target;
// Check we can still reach and repair the target
if (!this.CheckTargetRange(target, InterfaceId.Builder) || !this.CanRepair(target))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return true;
}
else
{
var cmpFoundation = Engine.QueryInterface(target, InterfaceId.Foundation) as ICmpFoundation;
if (cmpFoundation != null)
{
cmpFoundation.AddBuilder(this.entity);
}
}
this.SelectAnimation("build", false, 1.0f, "build");
this.StartTimer(1000, 1000);
return false;
});
stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
});
stateINDIVIDUAL_REPAIR_REPAIRING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
var target = (uint)this.order.data.target;
// Check we can still reach and repair the target
if (!this.CheckTargetRange(target, InterfaceId.Builder) || !this.CanRepair(target))
{
// Can't reach it, no longer owned by ally, or it doesn't exist any more
this.FinishOrder();
return;
}
var cmpBuilder = Engine.QueryInterface(this.entity, InterfaceId.Builder) as ICmpBuilder;
cmpBuilder.PerformBuilding(target);
});
}
}
{
var stateINDIVIDUAL_GARRISON = stateINDIVIDUAL.SubStates["GARRISON"] = new StateNode();
{
var stateINDIVIDUAL_GARRISON_APPROACHING = stateINDIVIDUAL_GARRISON.SubStates["APPROACHING"] = new StateNode();
stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["enter"] = new Action<dynamic>((msg) =>
{
this.SelectAnimation("walk", false, this.GetWalkSpeed());
});
stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["MoveCompleted"] = new Action<dynamic>((msg) =>
{
this.SetNextState("GARRISONED");
});
stateINDIVIDUAL_GARRISON_APPROACHING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
});
}
{
var stateINDIVIDUAL_GARRISON_GARRISONED = stateINDIVIDUAL_GARRISON.SubStates["GARRISONED"] = new StateNode();
stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["enter"] = new Func<dynamic, bool>((msg) =>
{
var target = this.order.data.target;
var cmpGarrisonHolder = Engine.QueryInterface(target, InterfaceId.GarrisonHolder) as ICmpGarrisonHolder;
// Check that we can garrison here
if (this.CanGarrison(target))
{
// Check that we're in range of the garrison target
if (this.CheckGarrisonRange(target))
{
// Check that garrisoning succeeds
if (cmpGarrisonHolder.Garrison(this.entity))
{
this.isGarrisoned = true;
// Check if we are garrisoned in a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, InterfaceId.ResourceDropsite) as ICmpResourceDropsite;
if (cmpResourceDropsite != null)
{
// Dump any resources we can
var dropsiteTypes = cmpResourceDropsite.GetTypes();
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
if (cmpResourceGatherer != null)
{
cmpResourceGatherer.CommitResources(dropsiteTypes);
}
}
return false;
}
}
else
{
// Unable to reach the target, try again
// (or follow if it's a moving target)
if (this.MoveToTarget(target))
{
this.SetNextState("APPROACHING");
return false;
}
}
}
// Garrisoning failed for some reason, so finish the order
this.FinishOrder();
return true;
});
stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["Order.Ungarrison"] = new Action<dynamic>((msg) =>
{
if (this.FinishOrder())
{
return;
}
});
stateINDIVIDUAL_GARRISON_GARRISONED.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.isGarrisoned = false;
});
}
}
{
var stateINDIVIDUAL_CHEERING = stateINDIVIDUAL.SubStates["CHEERING"] = new StateNode();
stateINDIVIDUAL_CHEERING.Handlers["enter"] = new Func<dynamic, bool>((msg) =>
{
// Unit is invulnerable while cheering
var cmpDamageReceiver = Engine.QueryInterface(this.entity, InterfaceId.Armour) as ICmpArmour;
cmpDamageReceiver.SetInvulnerability(true);
this.SelectAnimation("promotion");
this.StartTimer(4000, 4000);
return false;
});
stateINDIVIDUAL_CHEERING.Handlers["leave"] = new Action<dynamic>((msg) =>
{
this.StopTimer();
var cmpDamageReceiver = Engine.QueryInterface(this.entity, InterfaceId.Armour) as ICmpArmour;
cmpDamageReceiver.SetInvulnerability(false);
});
stateINDIVIDUAL_CHEERING.Handlers["Timer"] = new Action<dynamic>((msg) =>
{
this.FinishOrder();
});
}
}
{
var stateANIMAL = stateROOT.SubStates["ANIMAL"] = new StateNode();
stateANIMAL.Handlers["Attacked"] = new Action<dynamic>((msg) =>
{
if (this.template["NaturalBehaviour"].ToString() == "skittish" ||
this.template["NaturalBehaviour"].ToString() == "passive")
{
this.Flee((uint)msg.data.attacker, false);
}
else if (this.IsDangerousAnimal() || this.template["NaturalBehaviour"].ToString() == "defensive")
{
if (this.CanAttack((uint)msg.data.attacker, false))
{
this.Attack((uint)msg.data.attacker, false);
}
}
else if (this.template["NaturalBehaviour"].ToString() == "domestic")
{
// Never flee, stop what we were doing
this.SetNextState("IDLE");
}
});
stateANIMAL.Handlers["Order.LeaveFoundation"] = new Action<dynamic>((msg) =>
{
// Run away from the foundation
this.MoveToTargetRangeExplicit(msg.data.target, this.template["FleeDistance"].ToFloat(), this.template["FleeDistance"].ToFloat());
this.SetNextState("FLEEING");
});
{
var stateANIMAL_IDLE = stateANIMAL.SubStates["IDLE"] = new StateNode();
}
{
var stateANIMAL_ROAMING = stateANIMAL.SubStates["ROAMING"] = new StateNode();
}
{
var stateANIMAL_FEEDING = stateANIMAL.SubStates["FEEDING"] = new StateNode();
}
{
var stateANIMAL_FLEEING = stateANIMAL.SubStates["FLEEING"] = new StateNode();
}
{
var stateANIMAL_COMBAT = stateANIMAL.SubStates["COMBAT"] = new StateNode();
}
{
var stateANIMAL_WALKING = stateANIMAL.SubStates["WALKING"] = new StateNode();
}
}

UnitFsm = new FSM(stateROOT);
}
public override void Init(CParamNode paramNode)
{
this.orderQueue = new List<OrderItem>(); // current order is at the front of the list
this.order = null; // always == this.orderQueue[0]
this.formationController = CComponentManager.INVALID_ENTITY; // entity with IID_Formation that we belong to
this.isGarrisoned = false;
this.isIdle = false;
this.lastFormationName = "";
// For preventing increased action rate due to Stop orders or target death.
this.lastAttacked = null;
this.lastHealed = null;
this.SetStance(this.template["DefaultStance"].ToString());
}
public override void HandleMessage(CMessage msg, bool global)
{
//OnVisionRangeChanged(CMessage msg);
switch (msg.GetTypeID())
{
case MessageTypeId.Create:
OnCreate();
break;
case MessageTypeId.Destroy:
OnDestroy();
break;
case MessageTypeId.OwnershipChanged:
OnOwnershipChanged(msg as CMessageOwnershipChanged);
break;
case MessageTypeId.DiplomacyChanged:
OnDiplomacyChanged(msg as CMessageDiplomacyChanged);
break;
case MessageTypeId.MotionChanged:
OnMotionChanged(msg as CMessageMotionChanged);
break;
case MessageTypeId.ConstructionFinished:
OnGlobalConstructionFinished(msg as CMessageConstructionFinished);
break;
case MessageTypeId.EntityRenamed:
OnGlobalEntityRenamed(msg as CMessageEntityRenamed);
break;
case MessageTypeId.Attacked:
OnAttacked(msg as CMessageAttacked);
break;
case MessageTypeId.HealthChanged:
OnHealthChanged(msg as CMessageHealthChanged);
break;
case MessageTypeId.RangeUpdate:
OnRangeUpdate(msg as CMessageRangeUpdate);
break;
}
}
public override bool IsFormationController()
{
return string.Equals(this.template["FormationController"], "true", StringComparison.OrdinalIgnoreCase);
}
public override bool IsAnimal()
{
return (this.template["NaturalBehaviour"].IsOk() ? true : false);
}
public override bool IsDangerousAnimal()
{
return (this.IsAnimal() && (this.template["NaturalBehaviour"].ToString().ToLower() == "violent" || this.template["NaturalBehaviour"].ToString().ToLower() == "aggressive"));
}
public override bool IsDomestic()
{
var cmpIdentity = Engine.QueryInterface(this.entity, InterfaceId.Identity) as ICmpIdentity;
if (cmpIdentity == null)
{
return false;
}
return cmpIdentity.HasClass("Domestic");
}
public override bool IsHealer()
{
return Engine.QueryInterface(this.entity, InterfaceId.Heal) != null;
}
public override bool IsIdle()
{
return this.isIdle;
}
public override bool IsGarrisoned()
{
return this.isGarrisoned;
}
public override bool IsWalking()
{
var state = this.GetCurrentState().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
return (state == "WALKING");
}
public override bool CanAttackGaia()
{
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack);
if (cmpAttack == null)
{
return false;
}
// Rejects Gaia (0) and INVALID_PLAYER (-1)
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null || cmpOwnership.GetOwner() <= 0)
{
return false;
}
return true;
}
void OnCreate()
{
if (this.IsAnimal())
{
UnitFsm.Init(this, "ANIMAL.FEEDING");
}
else if (this.IsFormationController())
{
UnitFsm.Init(this, "FORMATIONCONTROLLER.IDLE");
}
else
{
UnitFsm.Init(this, "INDIVIDUAL.IDLE");
}
}
void OnDiplomacyChanged(CMessageDiplomacyChanged msg)
{
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership != null && cmpOwnership.GetOwner() == msg.player)
{
this.SetupRangeQuery();
}
}
void OnOwnershipChanged(CMessageOwnershipChanged msg)
{
this.SetupRangeQueries();
// If the unit isn't being created or dying, clear orders and reset stance.
if (msg.to != -1 && msg.from != -1)
{
this.SetStance(this.template["DefaultStance"].ToString());
this.Stop(false);
}
}
void OnDestroy()
{
// Clean up any timers that are now obsolete
this.StopTimer();
// Clean up range queries
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
if (this.losRangeQuery != null)
{
rangeMan.DestroyActiveQuery(this.losRangeQuery.Value);
}
if (this.losHealRangeQuery != null)
{
rangeMan.DestroyActiveQuery(this.losHealRangeQuery.Value);
}
if (this.losGaiaRangeQuery != null)
{
rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery.Value);
}
}
void OnVisionRangeChanged(CMessage msg)
{
// Update range queries
//if (this.entity == msg.entity)
//{
// this.SetupRangeQueries();
//}
}
// Wrapper function that sets up the normal, healer, and Gaia range queries.
public override void SetupRangeQueries()
{
this.SetupRangeQuery();
if (this.IsHealer())
this.SetupHealRangeQuery();
if (this.CanAttackGaia() || this.losGaiaRangeQuery != null)
this.SetupGaiaRangeQuery();
}
// Set up a range query for all enemy units within LOS range
// which can be attacked.
// This should be called whenever our ownership changes.
public override void SetupRangeQuery()
{
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager;
if (this.losRangeQuery != null)
{
rangeMan.DestroyActiveQuery(this.losRangeQuery.Value);
this.losRangeQuery = null;
}
var players = new List<int>();
if (owner != -1)
{
// If unit not just killed, get enemy players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), InterfaceId.Player) as ICmpPlayer;
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{
// Exclude gaia, allies, and self
// TODO: How to handle neutral players - Special query to attack military only?
if (cmpPlayer.IsEnemy(i))
players.Add(i);
}
}
var range = this.GetQueryRange(InterfaceId.Attack);
this.losRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.Min, range.Max, players.ToArray(), InterfaceId.Armour, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losRangeQuery.Value);
}
// Set up a range query for all own or ally units within LOS range
// which can be healed.
// This should be called whenever our ownership changes.
public override void SetupHealRangeQuery()
{
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager;
if (this.losHealRangeQuery != null)
{
rangeMan.DestroyActiveQuery(this.losHealRangeQuery.Value);
}
var players = new List<int>();
players.Add(owner);
if (owner != -1)
{
// If unit not just killed, get ally players via diplomacy
var cmpPlayer = Engine.QueryInterface(playerMan.GetPlayerByID(owner), InterfaceId.Player) as ICmpPlayer;
var numPlayers = playerMan.GetNumPlayers();
for (var i = 1; i < numPlayers; ++i)
{
// Exclude gaia and enemies
if (cmpPlayer.IsAlly(i))
{
players.Add(i);
}
}
}
var range = this.GetQueryRange(InterfaceId.Heal);
this.losHealRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.Min, range.Max, players.ToArray(), InterfaceId.Health, rangeMan.GetEntityFlagMask("injured"));
rangeMan.EnableActiveQuery(this.losHealRangeQuery.Value);
}
// Set up a range query for Gaia units within LOS range which can be attacked.
// This should be called whenever our ownership changes.
public override void SetupGaiaRangeQuery()
{
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
var owner = cmpOwnership.GetOwner();
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager;
if (this.losGaiaRangeQuery != null)
{
rangeMan.DestroyActiveQuery(this.losGaiaRangeQuery.Value);
this.losGaiaRangeQuery = null;
}
// Only create the query if Gaia is our enemy and we can attack.
if (this.CanAttackGaia())
{
var range = this.GetQueryRange(InterfaceId.Attack);
// This query is only interested in Gaia entities that can attack.
this.losGaiaRangeQuery = rangeMan.CreateActiveQuery(this.entity, range.Min, range.Max, new[] { 0 }, InterfaceId.Attack, rangeMan.GetEntityFlagMask("normal"));
rangeMan.EnableActiveQuery(this.losGaiaRangeQuery.Value);
}
}
public override void SetNextState(string state)
{
UnitFsm.SetNextState(this, state);
}
public override void SetNextStateAlwaysEntering(string state)
{
UnitFsm.SetNextStateAlwaysEntering(this, state);
}
public override void DeferMessage(OrderItem msg)
{
UnitFsm.DeferMessage(this, msg);
}
public override string GetCurrentState()
{
return UnitFsm.GetCurrentState(this);
}
public override void FsmStateNameChanged(string state)
{
Engine.PostMessage(this.entity, new CMessageUnitAIStateChanged { to = state });
}
public override bool FinishOrder()
{
if (this.orderQueue.Count == 0)
{
var cmpTemplateManager = Engine.QueryInterface(InterfaceId.TemplateManager) as ICmpTemplateManager;
var template = cmpTemplateManager.GetCurrentTemplateName(this.entity);
throw new Exception("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty");
}
this.orderQueue.shift();
if (this.orderQueue.Count > 0)
{
this.order = this.orderQueue[0];
}
else
{
this.order = null;
}
if (this.orderQueue.Count != 0)
{
var msg = new ZeroAD.Components.CCmpUnitAI.OrderItem { type = "Order." + this.order.type, data = this.order.data };
var ret = UnitFsm.ProcessMessage(this, msg);

Engine.PostMessage(this.entity, new CMessageUnitAIOrderDataChanged { to = this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret != null && ret is bool && (bool)ret)
{
return this.FinishOrder();
}
// Otherwise we've successfully processed a new order
return true;
}
else
{
this.SetNextState("IDLE");
return false;
}
}
public override void PushOrder(string type, AllInOneData data)
{
var order = new OrderItem { type = type, data = data };
this.orderQueue.Add(order);
// If we didn't already have an order, then process this new one
if (this.orderQueue.Count == 1)
{
this.order = order;
var ret = UnitFsm.ProcessMessage(this, new OrderItem { type = "Order." + this.order.type, data = this.order.data });
Engine.PostMessage(this.entity, new CMessageUnitAIOrderDataChanged { to = this.GetOrderData() });
// If the order was rejected then immediately take it off
// and process the remaining queue
if (ret != null && ret is bool && (bool)ret)
{
this.FinishOrder();
}
}
}
public override void PushOrderFront(string type, AllInOneData data)
{
var order = new OrderItem { type = type, data = data };
// If current order is cheering then add new order after it
if (this.order != null && this.order.type == "Cheering")
{
var cheeringOrder = this.orderQueue.shift();
this.orderQueue.unshift(cheeringOrder, order);
}
else
{
this.orderQueue.unshift(order);
this.order = order;
var ret = UnitFsm.ProcessMessage(this, new OrderItem { type = "Order." + this.order.type, data = this.order.data });
Engine.PostMessage(this.entity, new CMessageUnitAIOrderDataChanged { to = this.GetOrderData() });
// If the order was rejected then immediately take it off again;
// assume the previous active order is still valid (the short-lived
// new order hasn't changed state or anything) so we can carry on
// as if nothing had happened
if (ret != null && ret is bool && (bool)ret)
{
this.orderQueue.shift();
this.order = this.orderQueue[0];
}
}
}
public override void ReplaceOrder(string type, AllInOneData data)
{
// If current order is cheering then add new order after it
if (this.order != null && this.order.type == "Cheering")
{
var order = new OrderItem
{
type = type,
data = data
};
var cheeringOrder = this.orderQueue.shift();
this.orderQueue = new List<OrderItem>(new[] { cheeringOrder, order });
}
else
{
this.orderQueue = new List<OrderItem>();
this.PushOrder(type, data);
}
}
public override OrderItem[] GetOrders()
{
return orderQueue.ToArray();
}
public override void AddOrders(IEnumerable<OrderItem> orders)
{
foreach (var order in orders)
{
this.PushOrder(order.type, order.data);
}
}
public override List<dynamic> GetOrderData()
{
var orders = new List<dynamic>();
foreach (var i in this.orderQueue)
{
if (!object.ReferenceEquals(i.data, null))
{
orders.Add(i.data);
}
}
return orders;
}
public override void TimerHandler(dynamic data, int lateness)
{
// Reset the timer
if (data.timerRepeat == null)
{
this.timer = null;
}
UnitFsm.ProcessMessage(this, new OrderItem { type = "Timer", data = data, lateness = lateness });
}
public override void StartTimer(float offset, float? repeat)
{
if (this.timer.HasValue)
{
// error("Called StartTimer when there's already an active timer");
}
var data = new { timerRepeat = repeat };
var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer;
if (repeat == null)
{
this.timer = cmpTimer.SetTimeout(this.entity, InterfaceId.UnitAI, () => TimerHandler(data, 0), offset);
}
else
{
this.timer = cmpTimer.SetInterval(this.entity, InterfaceId.UnitAI, () => TimerHandler(data, 0), offset, repeat.Value);
}
}
public override void StopTimer()
{
if (this.timer == null)
{
return;
}
var cmpTimer = Engine.QueryInterface(InterfaceId.Timer) as ICmpTimer;
cmpTimer.CancelTimer(this.timer.Value);
this.timer = null;
}
void OnMotionChanged(CMessageMotionChanged msg)
{
if (msg.starting && !msg.error)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "MoveStarted", data = msg });
}
else if (!msg.starting || msg.error)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "MoveCompleted", data = msg });
}
}
void OnGlobalConstructionFinished(CMessageConstructionFinished msg)
{
// TODO: This is a bit inefficient since every unit listens to every
// construction message - ideally we could scope it to only the one we're building
UnitFsm.ProcessMessage(this, new OrderItem { type = "ConstructionFinished", data = msg });
}
void OnGlobalEntityRenamed(CMessageEntityRenamed msg)
{
UnitFsm.ProcessMessage(this, new OrderItem{ type = "EntityRenamed", entity = msg.entity, newentity = msg.newentity });
}
void OnAttacked(CMessageAttacked msg)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "Attacked", data = msg });
}
void OnHealthChanged(CMessageHealthChanged msg)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "HealthChanged", from = msg.from, to = msg.to });
}
void OnRangeUpdate(CMessageRangeUpdate msg)
{
if (msg.tag == this.losRangeQuery)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "LosRangeUpdate", data = msg });
}
else if (msg.tag == this.losGaiaRangeQuery)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "LosGaiaRangeUpdate", data = msg });
}
else if (msg.tag == this.losHealRangeQuery)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "LosHealRangeUpdate", data = msg });
}
}
public override float GetWalkSpeed()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.GetWalkSpeed();
}
public override float GetRunSpeed()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.GetRunSpeed();
}
public override bool TargetIsAlive(uint ent)
{
var cmpHealth = Engine.QueryInterface(ent, InterfaceId.Health) as ICmpHealth;
if (cmpHealth == null)
{
return false;
}
return (cmpHealth.GetHitpoints() != 0);
}
public override bool MustKillGatherTarget(uint ent)
{
var cmpResourceSupply = Engine.QueryInterface(ent, InterfaceId.ResourceSupply) as ICmpResourceSupply;
if (cmpResourceSupply == null)
{
return false;
}
if (!cmpResourceSupply.GetKillBeforeGather())
{
return false;
}
return this.TargetIsAlive(ent);
}
public override uint? FindNearbyResource(Func<uint, Name, string, bool> filter)
{
var range = 64; // TODO: what's a sensible number?
var playerMan = Engine.QueryInterface(InterfaceId.PlayerManager) as ICmpPlayerManager;
// We accept resources owned by Gaia or any player
var players = new List<int>(0);
players.Add(0);
for (var i = 1; i < playerMan.GetNumPlayers(); ++i)
{
players.Add(i);
}
var cmpTemplateManager = Engine.QueryInterface(InterfaceId.TemplateManager) as ICmpTemplateManager;
var cmpRangeManager = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var nearby = cmpRangeManager.ExecuteQuery(this.entity, 0, range, players, InterfaceId.ResourceSupply);
foreach (var ent in nearby)
{
var cmpResourceSupply = Engine.QueryInterface(ent, InterfaceId.ResourceSupply) as ICmpResourceSupply;
var type = cmpResourceSupply.GetType();
var amount = cmpResourceSupply.GetCurrentAmount();
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
// Remove "resource|" prefix from template names, if present.
if (template.IndexOf("resource|") != -1)
{
template = template.Substring(9);
}
if (amount > 0 && filter(ent, type, template))
{
return ent;
}
}
return null;
}
public override uint? FindNearestDropsite(string genericType)
{
// Find dropsites owned by this unit's player
var players = new List<int>();
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership != null)
{
players.Add(cmpOwnership.GetOwner());
}
// Ships are unable to reach land dropsites and shouldn't attempt to do so.
var excludeLand = (Engine.QueryInterface(this.entity, InterfaceId.Identity) as ICmpIdentity).HasClass("Ship");
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var nearby = rangeMan.ExecuteQuery(this.entity, 0, -1, players, InterfaceId.ResourceDropsite);
if (excludeLand)
{
nearby = nearby.Where((e) =>
{
var iid = Engine.QueryInterface(e, InterfaceId.Identity) as ICmpIdentity;
return iid != null && iid.HasClass("Naval");
}).ToList();
}
foreach (var ent in nearby)
{
var cmpDropsite = Engine.QueryInterface(ent, InterfaceId.ResourceDropsite) as ICmpResourceDropsite;
if (!cmpDropsite.AcceptsType(genericType))
continue;
return ent;
}
return null;
}
public override uint? FindNearbyFoundation()
{
var range = 64; // TODO: what's a sensible number?
// Find buildings owned by this unit's player
var players = new List<int>();
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership != null)
{
players.Add(cmpOwnership.GetOwner());
}
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var nearby = rangeMan.ExecuteQuery(this.entity, 0, range, players, InterfaceId.Foundation);
foreach (var ent in nearby)
{
// Skip foundations that are already complete. (This matters since
// we process the ConstructionFinished message before the foundation
// we're working on has been deleted.)
var cmpFoundation = Engine.QueryInterface(ent, InterfaceId.Foundation) as ICmpFoundation;
if (cmpFoundation.IsFinished())
{
continue;
}
return ent;
}
return null;
}
public override void PlaySound(string name)
{
// If we're a formation controller, use the sounds from our first member
if (this.IsFormationController())
{
var cmpFormation = Engine.QueryInterface(this.entity, InterfaceId.Formation) as ICmpFormation;
var member = cmpFormation.GetPrimaryMember();
if (member != 0)
{
SoundHelper.PlaySound(Engine, name, member);
}
}
else
{
// Otherwise use our own sounds
SoundHelper.PlaySound(Engine, name, this.entity);
}
}
public override void SetGathererAnimationOverride(bool disable)
{
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
if (cmpResourceGatherer == null)
return;
var cmpVisual = Engine.QueryInterface(this.entity, InterfaceId.Visual) as ICmpVisual;
if (cmpVisual == null)
return;
// Remove the animation override, so that weapons are shown again.
if (disable)
{
cmpVisual.ResetMoveAnimation("walk");
return;
}
// Work out what we're carrying, in order to select an appropriate animation
var type = cmpResourceGatherer.GetLastCarriedType();
if (type != null)
{
var typename = "carry_" + type.generic;
// Special case for meat
if (type.specific == "meat")
{
typename = "carry_" + type.specific;
}
cmpVisual.ReplaceMoveAnimation("walk", typename);
}
else
{
cmpVisual.ResetMoveAnimation("walk");
}
}
public override void SelectAnimation(string name, bool once = false, float speed = 1.0f, string sound = null)
{
var cmpVisual = Engine.QueryInterface(this.entity, InterfaceId.Visual) as ICmpVisual;
if (cmpVisual == null)
{
return;
}
// Special case: the "move" animation gets turned into a special
// movement mode that deals with speeds and walk/run automatically
if (name == "move")
{
// Speed to switch from walking to running animations
var runThreshold = (this.GetWalkSpeed() + this.GetRunSpeed()) / 2;
cmpVisual.SelectMovementAnimation(runThreshold);
return;
}
string soundgroup = null;
if (!string.IsNullOrEmpty(sound))
{
var cmpSound = Engine.QueryInterface(this.entity, InterfaceId.Sound) as ICmpSound;
if (cmpSound != null)
{
soundgroup = cmpSound.GetSoundGroup(sound);
}
}
// Set default values if unspecified
if (soundgroup == null)
{
soundgroup = "";
}
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
}
public override void SetAnimationSync(int actiontime, int repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, InterfaceId.Visual) as ICmpVisual;
if (cmpVisual == null)
{
return;
}
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
}
public override void StopMoving()
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
cmpUnitMotion.StopMoving();
}
public override bool MoveToPoint(float x, float z)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.MoveToPointRange(x, z, 0, 0);
}
public override bool MoveToPointRange(float x, float z, float rangeMin, float rangeMax)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.MoveToPointRange(x, z, rangeMin, rangeMax);
}
public override bool MoveToTarget(uint target)
{
if (!this.CheckTargetVisible(target))
{
return false;
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.MoveToTargetRange(target, 0, 0);
}
public override bool MoveToTargetRange(uint target, InterfaceId iid, string type = null)
{
if (!this.CheckTargetVisible(target))
{
return false;
}
var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.MoveToTargetRange(target, range.Min, range.Max);
}
public override bool MoveToTargetRangeExplicit(uint target, float min, float max)
{
if (!this.CheckTargetVisible(target))
{
return false;
}
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.MoveToTargetRange(target, min, max);
}
public override bool CheckTargetRange(uint target, InterfaceId iid, string type = null)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent;
var range = cmpRanged.GetRange(type);
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.IsInTargetRange(target, range.Min, range.Max);
}
public override bool CheckTargetRangeExplicit(uint target, float min, float max)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.IsInTargetRange(target, min, max);
}
public override bool CheckGarrisonRange(uint target)
{
var cmpGarrisonHolder = Engine.QueryInterface(target, InterfaceId.GarrisonHolder) as ICmpGarrisonHolder;
var range = cmpGarrisonHolder.GetLoadingRange();
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
return cmpUnitMotion.IsInTargetRange(target, range.Min, range.Max);
}
public override bool CheckTargetVisible(uint target)
{
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null)
{
return false;
}
var cmpRangeManager = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
if (cmpRangeManager == null)
{
return false;
}
if (cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner(), false) == ICmpRangeManager.ELosVisibility.VIS_HIDDEN)
{
return false;
}
// Either visible directly, or visible in fog
return true;
}
public override void FaceTowardsTarget(uint target)
{
var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition;
if (cmpPosition == null || !cmpPosition.IsInWorld())
{
return;
}
var cmpTargetPosition = Engine.QueryInterface(target, InterfaceId.Position) as ICmpPosition;
if (cmpTargetPosition == null || !cmpTargetPosition.IsInWorld())
{
return;
}
var pos = cmpPosition.GetPosition();
var targetpos = cmpTargetPosition.GetPosition();
var angle = Math.Atan2(targetpos.X - pos.X, targetpos.Z - pos.Z);
var rot = cmpPosition.GetRotation();
var delta = (rot.Y - angle + Math.PI) % (2 * Math.PI) - Math.PI;
if (Math.Abs(delta) > 0.2)
{
var cmpUnitMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
if (cmpUnitMotion != null)
{
cmpUnitMotion.FaceTowardsPoint(targetpos.X, targetpos.Z);
}
}
}
public override bool CheckTargetDistanceFromHeldPosition(uint target, InterfaceId iid, string type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent;
//var range = iid != InterfaceId.Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(type);
var range = cmpRanged.GetRange(type);
var cmpPosition = Engine.QueryInterface(target, InterfaceId.Position) as ICmpPosition;
if (cmpPosition == null || !cmpPosition.IsInWorld())
{
return false;
}
var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision;
if (cmpVision == null)
{
return false;
}
var halfvision = cmpVision.GetRange() / 2;
var pos = cmpPosition.GetPosition();
var dx = this.heldPosition.Value.X - pos.X;
var dz = this.heldPosition.Value.Z - pos.Z;
var dist = Math.Sqrt(dx * dx + dz * dz);
return dist < halfvision + range.Max;
}
public override bool CheckTargetIsInVisionRange(uint target)
{
var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision;
if (cmpVision == null)
{
return false;
}
var range = cmpVision.GetRange();
var distance = EntityHelper.DistanceBetweenEntities(Engine, this.entity, target);
return distance < range;
}
public override string GetBestAttack()
{
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
if (cmpAttack == null)
{
return null;
}
return cmpAttack.GetBestAttack();
}
public override string GetBestAttackAgainst(uint target)
{
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
if (cmpAttack == null)
{
return null;
}
return cmpAttack.GetBestAttackAgainst(target);
}
public override bool AttackVisibleEntity(IEnumerable<uint> ents, bool forceResponse)
{
foreach (var target in ents)
{
if (this.CanAttack(target, forceResponse))
{
this.PushOrderFront("Attack", new AllInOneData { target = target, force = false, forceResponse = forceResponse });
return true;
}
}
return false;
}
public override bool AttackEntityInZone(IEnumerable<uint> ents, bool forceResponse)
{
foreach (var target in ents)
{
var type = this.GetBestAttackAgainst(target);
if (this.CanAttack(target, forceResponse) && this.CheckTargetDistanceFromHeldPosition(target, InterfaceId.Attack, type))
{
this.PushOrderFront("Attack", new AllInOneData { target = target, force = false, forceResponse = forceResponse });
return true;
}
}
return false;
}
public override bool RespondToTargetedEntities(uint[] ents)
{
if (ents.Length == 0)
{
return false;
}
if (this.GetStance().respondChase)
return this.AttackVisibleEntity(ents, true);
if (this.GetStance().respondStandGround)
return this.AttackVisibleEntity(ents, true);
if (this.GetStance().respondHoldGround)
return this.AttackEntityInZone(ents, true);
if (this.GetStance().respondFlee)
{
this.PushOrderFront("Flee", new AllInOneData { target = ents[0], force = false });
return true;
}
return false;
}
public override bool RespondToHealableEntities(uint[] ents)
{
if (ents.Length == 0)
{
return false;
}
foreach (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", new AllInOneData { target = ent, force = false });
return true;
}
}
return false;
}
public override bool ShouldAbandonChase(uint target, bool force, InterfaceId iid)
{
// Forced orders shouldn't be interrupted.
if (force)
{
return false;
}
// Stop if we're in hold-ground mode and it's too far from the holding point
if (this.GetStance().respondHoldGround)
{
if (!this.CheckTargetDistanceFromHeldPosition(target, iid, this.attackType))
{
return true;
}
}
// Stop if it's left our vision range, unless we're especially persistent
if (!this.GetStance().respondChaseBeyondVision)
{
if (!this.CheckTargetIsInVisionRange(target))
{
return true;
}
}
// (Note that CCmpUnitMotion will detect if the target is lost in FoW,
// and will continue moving to its last seen position and then stop)
return false;
}
public override bool ShouldChaseTargetedEntity(uint target, bool force)
{
if (this.GetStance().respondChase)
{
return true;
}
if (force)
{
return true;
}
return false;
}
public override void SetFormationController(uint ent)
{
this.formationController = ent;
// Set obstruction group, so we can walk through members
// of our own formation (or ourself if not in formation)
var cmpObstruction = Engine.QueryInterface(this.entity, InterfaceId.Obstruction) as ICmpObstruction;
if (cmpObstruction != null)
{
if (ent == CComponentManager.INVALID_ENTITY)
{
cmpObstruction.SetControlGroup(this.entity);
}
else
{
cmpObstruction.SetControlGroup(ent);
}
}
// If we were removed from a formation, let the FSM switch back to INDIVIDUAL
if (ent == CComponentManager.INVALID_ENTITY)
{
UnitFsm.ProcessMessage(this, new OrderItem { type = "FormationLeave" });
}
}
public override uint GetFormationController()
{
return this.formationController;
}
public override void SetLastFormationName(string name)
{
this.lastFormationName = name;
}
public override string GetLastFormationName()
{
return this.lastFormationName;
}
public override float ComputeWalkingDistance()
{
var distance = 0.0f;
var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition;
if (cmpPosition == null || !cmpPosition.IsInWorld())
{
return 0;
}
// Keep track of the position at the start of each order
var pos = cmpPosition.GetPosition();
for (var i = 0; i < this.orderQueue.Count; ++i)
{
var order = this.orderQueue[i];
switch (order.type)
{
case "Walk":
case "GatherNearPosition":
// Add the distance to the target point
var dx0 = (float)order.data.x - pos.X;
var dz0 = (float)order.data.z - pos.Z;
var d0 = (float)Math.Sqrt(dx0 * dx0 + dz0 * dz0);
distance += d0;
// Remember this as the start position for the next order
pos = new CVector3D(order.data.x, 0, order.data.z);
break; // and continue the loop
case "WalkToTarget":
case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
case "Flee":
case "LeaveFoundation":
case "Attack":
case "Heal":
case "Gather":
case "ReturnResource":
case "Repair":
case "Garrison":
{
// Find the target unit's position
var cmpTargetPosition = Engine.QueryInterface(order.data.target, InterfaceId.Position) as ICmpPosition;
if (cmpTargetPosition == null || !cmpTargetPosition.IsInWorld())
{
return distance;
}
var targetPos = cmpTargetPosition.GetPosition();
// Add the distance to the target unit
var dx = targetPos.X - pos.X;
var dz = targetPos.Z - pos.Z;
var d = (float)Math.Sqrt(dx * dx + dz * dz);
distance += d;
// Return the total distance to the target
return distance;
}
case "Stop":
{
return 0;
}
default:
{
//error("ComputeWalkingDistance: Unrecognised order type '" + order.type + "'");
return distance;
}
}
}
// Return the total distance to the end of the order queue
return distance;
}
public override void AddOrder(string type, dynamic data, bool queued)
{
if (queued)
{
this.PushOrder(type, data);
}
else
{
this.ReplaceOrder(type, data);
}
}
public override void Walk(float x, float z, bool queued)
{
this.AddOrder("Walk", new AllInOneData { x = x, z = z, force = true }, queued);
}
/// <summary>
/// Adds stop order to queue, forced by the player.
/// </summary>
public override void Stop(bool queued)
{
this.AddOrder("Stop", null, queued);
}
public override void WalkToTarget(uint target, bool queued)
{
this.AddOrder("WalkToTarget", new AllInOneData { target = target, force = true }, queued);
}
public override void LeaveFoundation(uint target)
{
// If we're already being told to leave a foundation, then
// ignore this new request so we don't end up being too indecisive
// to ever actually move anywhere
if (this.order != null && this.order.type == "LeaveFoundation")
{
return;
}
this.PushOrderFront("LeaveFoundation", new AllInOneData { target = target, force = true });
}
public override void Attack(uint target, bool queued)
{
if (!this.CanAttack(target, false))
{
// We don't want to let healers walk to the target unit so they can be easily killed.
// Instead we just let them get into healing range.
if (this.IsHealer())
{
this.MoveToTargetRange(target, InterfaceId.Heal);
}
else
{
this.WalkToTarget(target, queued);
}
return;
}
this.AddOrder("Attack", new AllInOneData { target = target, force = true }, queued);
}
public override void Garrison(uint target, bool queued)
{
if (!this.CanGarrison(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Garrison", new AllInOneData { target = target, force = true }, queued);
}
public override void Ungarrison()
{
if (this.IsGarrisoned())
{
this.AddOrder("Ungarrison", null, false);
}
}
/// <summary>
/// Adds gather order to the queue, forced by the player until the target is reached .
/// </summary>
public override void Gather(uint target, bool queued)
{
this.PerformGather(target, queued, true);
}
public override void PerformGather(uint target, bool queued, bool force)
{
if (!this.CanGather(target))
{
this.WalkToTarget(target, queued);
return;
}
// Save the resource type now, so if the resource gets destroyed
// before we process the order then we still know what resource
// type to look for more of
var cmpResourceSupply = Engine.QueryInterface(target, InterfaceId.ResourceSupply) as ICmpResourceSupply;
var type = cmpResourceSupply.GetType();
// Also save the target entity's template, so that if it's an animal,
// we won't go from hunting slow safe animals to dangerous fast ones
var cmpTemplateManager = Engine.QueryInterface(InterfaceId.TemplateManager) as ICmpTemplateManager;
var template = cmpTemplateManager.GetCurrentTemplateName(target);
// Remove "resource|" prefix from template name, if present.
if (template.IndexOf("resource|") != -1)
{
template = template.Substring(9);
}
// Remember the position of our target, if any, in case it disappears
// later and we want to head to its last known position
// (TODO: if the target moves a lot (e.g. it's an animal), maybe we
// need to update this lastPos regularly rather than just here?)
CVector3D? lastPos = null;
var cmpPosition = Engine.QueryInterface(target, InterfaceId.Position) as ICmpPosition;
if (cmpPosition != null && cmpPosition.IsInWorld())
{
lastPos = cmpPosition.GetPosition();
}
this.AddOrder("Gather", new AllInOneData { target = target, type = type, template = template, lastPos = lastPos, force = force }, queued);
}
public override void GatherNearPosition(float x, float z, Name type, string template, bool queued)
{
// Remove "resource|" prefix from template name, if present.
if (template.IndexOf("resource|") != -1)
{
template = template.Substring(9);
}
this.AddOrder("GatherNearPosition", new AllInOneData { type = type, template = template, x = x, z = z, force = false }, queued);
}
public override void Heal(uint target, bool queued)
{
if (!this.CanHeal(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Heal", new AllInOneData { target = target, force = true }, queued);
}
public override void ReturnResource(uint target, bool queued)
{
if (!this.CanReturnResource(target, true))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("ReturnResource", new AllInOneData { target = target, force = true }, queued);
}
public override void SetupTradeRoute(uint target, uint source, bool queued)
{
if (!this.CanTrade(target))
{
this.WalkToTarget(target, queued);
return;
}
var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader;
var marketsChanged = cmpTrader.SetTargetMarket(target, source);
if (marketsChanged)
{
if (cmpTrader.HasBothMarkets())
{
this.AddOrder("Trade", new AllInOneData { firstMarket = cmpTrader.GetFirstMarket(), secondMarket = cmpTrader.GetSecondMarket(), force = false }, queued);
}
else
{
this.WalkToTarget(cmpTrader.GetFirstMarket(), queued);
}
}
}
public override bool MoveToMarket(uint targetMarket)
{
if (this.MoveToTarget(targetMarket))
{
// We've started walking to the market
return true;
}
else
{
// We can't reach the market.
// Give up.
this.StopMoving();
this.StopTrading();
return false;
}
}
public override void PerformTradeAndMoveToNextMarket(uint currentMarket, uint nextMarket, string nextFsmStateName)
{
if (!this.CanTrade(currentMarket))
{
this.StopTrading();
return;
}
if (this.CheckTargetRange(currentMarket, InterfaceId.Trader))
{
this.PerformTrade();
if (this.MoveToMarket(nextMarket))
{
// We've started walking to the next market
this.SetNextState(nextFsmStateName);
}
}
else
{
// If the current market is not reached try again
this.MoveToMarket(currentMarket);
}
}
public override void PerformTrade()
{
var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader;
cmpTrader.PerformTrade();
}
public override void StopTrading()
{
this.FinishOrder();
var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader;
cmpTrader.StopTrading();
}
public override void Repair(uint target, bool autocontinue, bool queued)
{
if (!this.CanRepair(target))
{
this.WalkToTarget(target, queued);
return;
}
this.AddOrder("Repair", new AllInOneData { target = target, autocontinue = autocontinue, force = true }, queued);
}
public override void Flee(uint target, bool queued)
{
this.AddOrder("Flee", new AllInOneData { target = target, force = false }, queued);
}
public override void Cheer()
{
this.AddOrder("Cheering", new AllInOneData { force = true }, false);
}
public override void SetStance(string stance)
{
if (g_Stances.ContainsKey(stance))
{
this.stance = stance;
}
else
{
//error("UnitAI: Setting to invalid stance '" + stance + "'");
}
}
public override void SwitchToStance(string stance)
{
var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition;
if (cmpPosition == null || !cmpPosition.IsInWorld())
{
return;
}
var pos = cmpPosition.GetPosition();
this.SetHeldPosition(pos.X, pos.Z);
this.SetStance(stance);
// Stop moving if switching to stand ground
// TODO: Also stop existing orders in a sensible way
if (stance == "standground")
{
this.StopMoving();
}
// Reset the range queries, since the range depends on stance.
this.SetupRangeQueries();
}

public override bool FindNewTargets()
{
if (this.losRangeQuery == null)
{
return false;
}
if (!this.GetStance().targetVisibleEnemies)
{
return false;
}
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
if (this.AttackEntitiesByPreference(rangeMan.ResetActiveQuery(this.losRangeQuery.Value)))
{
return true;
}
// If no regular enemies were found, attempt to attack a hostile Gaia entity.
else if (this.losGaiaRangeQuery != null)
{
return this.AttackGaiaEntitiesByPreference(rangeMan.ResetActiveQuery(this.losGaiaRangeQuery.Value));
}
return false;
}
public override bool FindNewHealTargets()
{
if (this.losHealRangeQuery == null)
{
return false;
}
var rangeMan = Engine.QueryInterface(InterfaceId.RangeManager) as ICmpRangeManager;
var ents = rangeMan.ResetActiveQuery(this.losHealRangeQuery.Value);
foreach (var ent in ents)
{
if (this.CanHeal(ent))
{
this.PushOrderFront("Heal", new AllInOneData { target = ent, force = false });
return true;
}
}
// We haven't found any target to heal
return false;
}
public override Range GetQueryRange(InterfaceId iid)
{
var ret = new Range { Min = 0, Max = 0 };
if (this.GetStance().respondStandGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent;
if (cmpRanged == null)
{
return ret;
}
//var range = iid !== IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
ret.Min = range.Min;
ret.Max = range.Max;
}
else if (this.GetStance().respondChase)
{
var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision;
if (cmpVision == null)
{
return ret;
}
var range = cmpVision.GetRange();
ret.Max = range;
}
else if (this.GetStance().respondHoldGround)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid) as IRangedComponent;
if (cmpRanged == null)
{
return ret;
}
//var range = iid != IID_Attack ? cmpRanged.GetRange() : cmpRanged.GetRange(cmpRanged.GetBestAttack());
var range = cmpRanged.GetRange(cmpRanged.GetBestAttack());
var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision;
if (cmpVision == null)
{
return ret;
}
var halfvision = cmpVision.GetRange() / 2;
ret.Max = range.Max + halfvision;
}
// We probably have stance 'passive' and we wouldn't have a range,
// but as it is the default for healers we need to set it to something sane.
else if (iid == InterfaceId.Heal)
{
var cmpVision = Engine.QueryInterface(this.entity, InterfaceId.Vision) as ICmpVision;
if (cmpVision == null)
{
return ret;
}
var range = cmpVision.GetRange();
ret.Max = range;
}
return ret;
}
public override UnitStance GetStance()
{
return g_Stances[this.stance];
}
public override string GetStanceName()
{
return this.stance;
}
public override void SetMoveSpeed(float speed)
{
var cmpMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
cmpMotion.SetSpeed(speed);
}
public override void SetHeldPosition(float x, float z)
{
this.heldPosition = new CVector3D(x, 0, z);
}
public override CVector3D? GetHeldPosition(object pos)
{
return this.heldPosition;
}
public override bool WalkToHeldPosition()
{
if (this.heldPosition.HasValue)
{
this.AddOrder("Walk", new AllInOneData { x = this.heldPosition.Value.X, z = this.heldPosition.Value.Z, force = false }, false);
return true;
}
return false;
}
public override bool CanAttack(uint target, bool forceResponse)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
return true;
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
if (cmpAttack == null)
return false;
if (!cmpAttack.CanAttack(target))
return false;
// Verify that the target is alive
if (!this.TargetIsAlive(target))
return false;
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null)
return false;
// Verify that the target is an attackable resource supply like a domestic animal
// or that it isn't owned by an ally of this entity's player or is responding to
// an attack.
var owner = cmpOwnership.GetOwner();
if (!this.MustKillGatherTarget(target)
&& !(PlayerHelper.IsOwnedByEnemyOfPlayer(Engine, owner, target)
|| PlayerHelper.IsOwnedByNeutralOfPlayer(Engine, owner, target)
|| (forceResponse && !PlayerHelper.IsOwnedByPlayer(Engine, owner, target))))
{
return false;
}
return true;
}
public override bool CanGarrison(uint 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, InterfaceId.GarrisonHolder) as ICmpGarrisonHolder;
if (cmpGarrisonHolder == null)
{
return false;
}
// Verify that the target is owned by this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null || !PlayerHelper.IsOwnedByPlayer(Engine, 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)
if (this.IsAnimal())
{
return false;
}
return true;
}
public override bool CanGather(uint target)
{
// The target must be a valid resource supply.
var cmpResourceSupply = Engine.QueryInterface(target, InterfaceId.ResourceSupply) as ICmpResourceSupply;
if (cmpResourceSupply == null)
{
return false;
}
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
{
return true;
}
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
if (cmpResourceGatherer == null)
{
return false;
}
// Verify that we can gather from this target
if (cmpResourceGatherer.GetTargetGatherRate(target) <= 0)
{
return false;
}
// No need to verify ownership as we should be able to gather from
// a target regardless of ownership.
return true;
}
public override bool CanHeal(uint target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
{
return true;
}
// Verify that we're able to respond to Heal commands
var cmpHeal = Engine.QueryInterface(this.entity, InterfaceId.Heal) as ICmpHeal;
if (cmpHeal == null)
{
return false;
}
// Verify that the target is alive
if (!this.TargetIsAlive(target))
{
return false;
}
// Verify that the target is owned by the same player as the entity or of an ally
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null || !(PlayerHelper.IsOwnedByPlayer(Engine, cmpOwnership.GetOwner(), target) || PlayerHelper.IsOwnedByAllyOfPlayer(Engine, cmpOwnership.GetOwner(), target)))
{
return false;
}
// Verify that the target is not unhealable (or at max health)
var cmpHealth = Engine.QueryInterface(target, InterfaceId.Health) as ICmpHealth;
if (cmpHealth == null || cmpHealth.IsUnhealable())
{
return false;
}
// Verify that the target has no unhealable class
var cmpIdentity = Engine.QueryInterface(target, InterfaceId.Identity) as ICmpIdentity;
if (cmpIdentity == null)
{
return false;
}
foreach (var unhealableClass in cmpHeal.GetUnhealableClasses())
{
if (cmpIdentity.HasClass(unhealableClass))
{
return false;
}
}
// Verify that the target is a healable class
var healable = false;
foreach (var healableClass in cmpHeal.GetHealableClasses())
{
if (cmpIdentity.HasClass(healableClass))
{
healable = true;
}
}
if (!healable)
{
return false;
}
return true;
}
public override bool CanReturnResource(uint target, bool checkCarriedResource)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
{
return true;
}
// Verify that we're able to respond to ReturnResource commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, InterfaceId.ResourceGatherer) as ICmpResourceGatherer;
if (cmpResourceGatherer == null)
{
return false;
}
// Verify that the target is a dropsite
var cmpResourceDropsite = Engine.QueryInterface(target, InterfaceId.ResourceDropsite) as ICmpResourceDropsite;
if (cmpResourceDropsite == null)
{
return false;
}
if (checkCarriedResource)
{
// Verify that we are carrying some resources,
// and can return our current resource to this target
var type = cmpResourceGatherer.GetMainCarryingType();
if (string.IsNullOrEmpty(type) || !cmpResourceDropsite.AcceptsType(type))
{
return false;
}
}
// Verify that the dropsite is owned by this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null || !PlayerHelper.IsOwnedByPlayer(Engine, cmpOwnership.GetOwner(), target))
{
return false;
}
return true;
}
public override bool CanTrade(uint target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
{
return true;
}
// Verify that we're able to respond to Trade commands
var cmpTrader = Engine.QueryInterface(this.entity, InterfaceId.Trader) as ICmpTrader;
if (cmpTrader == null || !cmpTrader.CanTrade(target))
{
return false;
}
return true;
}
public override bool CanRepair(uint target)
{
// Formation controllers should always respond to commands
// (then the individual units can make up their own minds)
if (this.IsFormationController())
{
return true;
}
// Verify that we're able to respond to Repair (Builder) commands
var cmpBuilder = Engine.QueryInterface(this.entity, InterfaceId.Builder) as ICmpBuilder;
if (cmpBuilder == null)
{
return false;
}
// Verify that the target is owned by an ally of this entity's player
var cmpOwnership = Engine.QueryInterface(this.entity, InterfaceId.Ownership) as ICmpOwnership;
if (cmpOwnership == null || !PlayerHelper.IsOwnedByAllyOfPlayer(Engine, cmpOwnership.GetOwner(), target))
{
return false;
}
return true;
}
public override void MoveRandomly(float distance)
{
// We want to walk in a random direction, but avoid getting stuck
// in obstacles or narrow spaces.
// So pick a circular range from approximately our current position,
// and move outwards to the nearest point on that circle, which will
// lead to us avoiding obstacles and moving towards free space.
// TODO: we probably ought to have a 'home' point, and drift towards
// that, so we don't spread out all across the whole map
var cmpPosition = Engine.QueryInterface(this.entity, InterfaceId.Position) as ICmpPosition;
if (cmpPosition == null)
{
return;
}
if (!cmpPosition.IsInWorld())
{
return;
}
var pos = cmpPosition.GetPosition();
var jitter = 0.5f;
Random rnd = new Random();
// Randomly adjust the range's center a bit, so we tend to prefer
// moving in random directions (if there's nothing in the way)
var tx = pos.X + (2 * (float)rnd.NextDouble() - 1) * jitter;
var tz = pos.Z + (2 * (float)rnd.NextDouble() - 1) * jitter;
var cmpMotion = Engine.QueryInterface(this.entity, InterfaceId.UnitMotion) as ICmpUnitMotion;
cmpMotion.MoveToPointRange(tx, tz, distance, distance);
}
public override bool AttackEntitiesByPreference(IEnumerable<uint> ents)
{
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
if (cmpAttack == null)
{
return false;
}
var xx = ents.Where(v => cmpAttack.CanAttack(v)).ToList();
xx.Sort((a, => cmpAttack.CompareEntitiesByPreference(a, );
return this.RespondToTargetedEntities(xx.ToArray());
}
public override bool AttackGaiaEntitiesByPreference(IEnumerable<uint> ents)
{
var cmpAttack = Engine.QueryInterface(this.entity, InterfaceId.Attack) as ICmpAttack;
if (cmpAttack == null)
{
return false;
}
Func<uint, bool> filter = (e) =>
{
var cmpUnitAI = Engine.QueryInterface(e, InterfaceId.UnitAI) as ICmpUnitAI;
return (cmpUnitAI != null && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()));
};
var xx = ents.Where(v => cmpAttack.CanAttack(v) && filter(v)).ToList();
xx.Sort((a, => cmpAttack.CompareEntitiesByPreference(a, );
return this.RespondToTargetedEntities(xx.ToArray());
}
}
}

CCmpUnitAI.cs.txt

Link to comment
Share on other sites

  • 1 month later...

Spending nearly 1 year to port the game from C++, JS and OpenGL to a Windows-base platform C# and DirectX, it seems i cannot continue to finish the rest of job without getting support from a sponsor.

The latest status is this:

- Graphics almost done, except shadows and water effects. Terrain rendering works fine (the same as original CPP version) but it could be optimized to some extend.

- Artificial Intelligence needs tons of work. Specially on porting Bot scripts from JS.

- User Interface has minimum progress.

- Needs a new user-experience to let the player play the game without needing a mouse device. Only touch would be enough for Modern (Metro) UI and Windows Phone.

- The game could be commercial, worth 6-8 US dollar (has to check license and copyright stuff).

- The game is currently in playable states, if you do not care about enemy AI.

If there are any candidate sponsors for the project, i would share the source code to them for their insurance (this is not a fake project).

The main reason to port the game to C# and DirectX is to hit Microsoft Stores (Windows 8 and Windows Phone).

I wish to get comments from you readers, even if you cannot sponsor.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...