Browse Source

Merge pull request #1445 from Azaezel/aiSubsystem

Ai subsystem
Brian Roberts 5 months ago
parent
commit
13bf126418

+ 1 - 1
Engine/source/CMakeLists.txt

@@ -58,7 +58,7 @@ torqueAddSourceDirectories("platform" "platform/threads" "platform/async"
 
 torqueAddSourceDirectories("platform/nativeDialogs")
 # Handle T3D
-torqueAddSourceDirectories( "T3D" "T3D/assets" "T3D/decal" "T3D/examples" "T3D/fps" "T3D/fx" 
+torqueAddSourceDirectories( "T3D" "T3D/AI" "T3D/assets" "T3D/decal" "T3D/examples" "T3D/fps" "T3D/fx" 
                            "T3D/gameBase" "T3D/gameBase/std"
                            "T3D/lighting"
                            "T3D/physics"

+ 226 - 0
Engine/source/T3D/AI/AIAimTarget.cpp

@@ -0,0 +1,226 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+#include "AIAimTarget.h"
+#include "AIController.h"
+
+static U32 sAILoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType;
+
+F32 AIAimTarget::getTargetDistance(SceneObject* target, bool _checkEnabled)
+{
+   if (!target)
+   {
+      target = mObj.getPointer();
+      if (!target)
+         return F32_MAX;
+   }
+
+   if (_checkEnabled)
+   {
+      if (target->getTypeMask() & ShapeBaseObjectType)
+      {
+         ShapeBase* shapeBaseCheck = static_cast<ShapeBase*>(target);
+         if (shapeBaseCheck)
+            if (shapeBaseCheck->getDamageState() != ShapeBase::Enabled) return false;
+      }
+      else
+         return F32_MAX;
+   }
+
+   return (getPosition() - target->getPosition()).len();
+}
+
+bool AIAimTarget::checkInLos(SceneObject* target, bool _useMuzzle, bool _checkEnabled)
+{
+   ShapeBase* sbo = dynamic_cast<ShapeBase*>(getCtrl()->getAIInfo()->mObj.getPointer());
+   if (!target)
+   {
+      target = dynamic_cast<ShapeBase*>(mObj.getPointer());
+      if (!target)
+         return false;
+   }
+   if (_checkEnabled)
+   {
+      if (target->getTypeMask() & ShapeBaseObjectType)
+      {
+         ShapeBase* shapeBaseCheck = static_cast<ShapeBase*>(target);
+         if (shapeBaseCheck)
+            if (shapeBaseCheck->getDamageState() != ShapeBase::Enabled) return false;
+      }
+      else
+         return false;
+   }
+
+   RayInfo ri;
+
+   sbo->disableCollision();
+
+   S32 mountCount = target->getMountedObjectCount();
+   for (S32 i = 0; i < mountCount; i++)
+   {
+      target->getMountedObject(i)->disableCollision();
+   }
+
+   Point3F checkPoint;
+   if (_useMuzzle)
+      sbo->getMuzzlePoint(0, &checkPoint);
+   else
+   {
+      MatrixF eyeMat;
+      sbo->getEyeTransform(&eyeMat);
+      eyeMat.getColumn(3, &checkPoint);
+   }
+
+   bool hit = !gServerContainer.castRay(checkPoint, target->getBoxCenter(), sAILoSMask, &ri);
+   sbo->enableCollision();
+
+   for (S32 i = 0; i < mountCount; i++)
+   {
+      target->getMountedObject(i)->enableCollision();
+   }
+   return hit;
+}
+
+bool AIAimTarget::checkInFoV(SceneObject* target, F32 camFov, bool _checkEnabled)
+{
+   ShapeBase* sbo = dynamic_cast<ShapeBase*>(getCtrl()->getAIInfo()->mObj.getPointer());
+   if (!target)
+   {
+      target = dynamic_cast<ShapeBase*>(mObj.getPointer());
+      if (!target)
+         return false;
+   }
+   if (_checkEnabled)
+   {
+      if (target->getTypeMask() & ShapeBaseObjectType)
+      {
+         ShapeBase* shapeBaseCheck = static_cast<ShapeBase*>(target);
+         if (shapeBaseCheck)
+            if (shapeBaseCheck->getDamageState() != ShapeBase::Enabled) return false;
+      }
+      else
+         return false;
+   }
+
+   MatrixF cam = sbo->getTransform();
+   Point3F camPos;
+   VectorF camDir;
+
+   cam.getColumn(3, &camPos);
+   cam.getColumn(1, &camDir);
+
+   camFov = mDegToRad(camFov) / 2;
+
+   Point3F shapePos = target->getBoxCenter();
+   VectorF shapeDir = shapePos - camPos;
+   // Test to see if it's within our viewcone, this test doesn't
+   // actually match the viewport very well, should consider
+   // projection and box test.
+   shapeDir.normalize();
+   F32 dot = mDot(shapeDir, camDir);
+   return (dot > mCos(camFov));
+}
+
+DefineEngineMethod(AIController, setAimLocation, void, (Point3F target), ,
+   "@brief Tells the AIPlayer to aim at the location provided.\n\n"
+
+   "@param target An \"x y z\" position in the game world to target.\n\n"
+
+   "@see getAimLocation()\n")
+{
+   object->setAim(target);
+}
+
+DefineEngineMethod(AIController, getAimLocation, Point3F, (), ,
+   "@brief Returns the point the AIPlayer is aiming at.\n\n"
+
+   "This will reflect the position set by setAimLocation(), "
+   "or the position of the object that the bot is now aiming at.  "
+   "If the bot is not aiming at anything, this value will "
+   "change to whatever point the bot's current line-of-sight intercepts."
+
+   "@return World space coordinates of the object AI is aiming at. Formatted as \"X Y Z\".\n\n"
+
+   "@see setAimLocation()\n"
+   "@see setAimObject()\n")
+{
+   return object->getAim()->getPosition();
+}
+
+DefineEngineMethod(AIController, setAimObject, void, (const char* objName, Point3F offset), (Point3F::Zero), "( GameBase obj, [Point3F offset] )"
+   "Sets the bot's target object. Optionally set an offset from target location."
+   "@hide")
+{
+   // Find the target
+   SceneObject* targetObject;
+   if (Sim::findObject(objName, targetObject))
+   {
+
+      object->setAim(targetObject, 0.0f, offset);
+   }
+   else
+      object->setAim(0, 0.0f, offset);
+}
+
+DefineEngineMethod(AIController, clearAim, void, (), , "clears the bot's target.")
+{
+      object->clearAim();
+}
+
+DefineEngineMethod(AIController, getAimObject, S32, (), ,
+   "@brief Gets the object the AIPlayer is targeting.\n\n"
+
+   "@return Returns -1 if no object is being aimed at, "
+   "or the SimObjectID of the object the AIPlayer is aiming at.\n\n"
+
+   "@see setAimObject()\n")
+{
+   SceneObject* obj = dynamic_cast<GameBase*>(object->getAim()->mObj.getPointer());
+   return obj ? obj->getId() : -1;
+}
+
+
+DefineEngineMethod(AIController, getTargetDistance, F32, (SceneObject* obj, bool checkEnabled), (nullAsType<SceneObject*>(), false),
+   "@brief The distance to a given target.\n"
+   "@obj Object to check. (If blank, it will check the current target).\n"
+   "@checkEnabled check whether the object can take damage and if so is still alive.(Defaults to false)\n")
+{
+   return object->getAim()->getTargetDistance(obj, checkEnabled);
+}
+
+DefineEngineMethod(AIController, checkInLos, bool, (SceneObject* obj, bool useMuzzle, bool checkEnabled), (nullAsType<ShapeBase*>(), false, false),
+   "@brief Check whether an object is in line of sight.\n"
+   "@obj Object to check. (If blank, it will check the current target).\n"
+   "@useMuzzle Use muzzle position. Otherwise use eye position. (defaults to false).\n"
+   "@checkEnabled check whether the object can take damage and if so is still alive.(Defaults to false)\n")
+{
+   return object->getAim()->checkInLos(obj, useMuzzle, checkEnabled);
+}
+
+DefineEngineMethod(AIController, checkInFoV, bool, (SceneObject* obj, F32 fov, bool checkEnabled), (nullAsType<ShapeBase*>(), 45.0f, false),
+   "@brief Check whether an object is within a specified veiw cone.\n"
+   "@obj Object to check. (If blank, it will check the current target).\n"
+   "@fov view angle in degrees.(Defaults to 45)\n"
+   "@checkEnabled check whether the object can take damage and if so is still alive.(Defaults to false)\n")
+{
+   return object->getAim()->checkInFoV(obj, fov, checkEnabled);
+}

+ 41 - 0
Engine/source/T3D/AI/AIAimTarget.h

@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#ifndef _AIAIMTARGET_H_
+#define _AIAIMTARGET_H_
+
+#include "AIInfo.h"
+struct AIAimTarget : public AIInfo
+{
+   typedef AIInfo Parent;
+   Point3F mAimOffset;
+   bool mTargetInLOS;                  // Is target object visible?
+   Point3F getPosition() { return ((mObj.isValid()) ? mObj->getPosition() : mPosition) + mAimOffset; }
+   bool checkInLos(SceneObject* target = NULL, bool _useMuzzle = false, bool _checkEnabled = false);
+   bool checkInFoV(SceneObject* target = NULL, F32 camFov = 45.0f, bool _checkEnabled = false);
+   F32 getTargetDistance(SceneObject* target, bool _checkEnabled);
+   AIAimTarget() = delete;
+   AIAimTarget(AIController* controller) : Parent(controller) { mTargetInLOS = false; };
+   AIAimTarget(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn) : Parent(controller, objIn, radIn) { mTargetInLOS = false; };
+   AIAimTarget(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) { mTargetInLOS = false; };
+};
+
+#endif

+ 916 - 0
Engine/source/T3D/AI/AIController.cpp

@@ -0,0 +1,916 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+#include "AIController.h"
+#include "T3D/player.h"
+#include "T3D/rigidShape.h"
+#include "T3D/vehicles/wheeledVehicle.h"
+#include "T3D/vehicles/flyingVehicle.h"
+
+
+IMPLEMENT_CONOBJECT(AIController);
+
+//-----------------------------------------------------------------------------
+void AIController::throwCallback(const char* name)
+{
+   //Con::warnf("throwCallback: %s", name);
+   Con::executef(mControllerData, name, getIdString()); //controller data callbacks
+
+   GameBase* gbo = dynamic_cast<GameBase*>(getAIInfo()->mObj.getPointer());
+   if (!gbo) return;
+   Con::executef(gbo->getDataBlock(), name, getAIInfo()->mObj->getIdString()); //legacy support for object db callbacks
+}
+
+void AIController::initPersistFields()
+{
+   addProtectedField("ControllerData", TYPEID< AIControllerData >(), Offset(mControllerData, AIController),
+      &setControllerDataProperty, &defaultProtectedGetFn,
+      "Script datablock used for game objects.");
+   addFieldV("MoveSpeed", TypeRangedF32, Offset(mMovement.mMoveSpeed, AIController), &CommonValidators::PositiveFloat,
+      "@brief default move sepeed.");
+}
+
+bool AIController::setControllerDataProperty(void* obj, const char* index, const char* db)
+{
+   if (db == NULL || !db[0])
+   {
+      Con::errorf("AIController::setControllerDataProperty - Can't unset ControllerData on AIController objects");
+      return false;
+   }
+
+   AIController* object = static_cast<AIController*>(obj);
+   AIControllerData* data;
+   if (Sim::findObject(db, data))
+   {
+      object->mControllerData = data;
+      return true;
+   }
+   Con::errorf("AIController::setControllerDataProperty - Could not find ControllerData \"%s\"", db);
+   return false;
+}
+
+void AIController::setGoal(AIInfo* targ)
+{
+   if (mGoal) { delete(mGoal); mGoal = NULL; }
+
+   if (targ->mObj.isValid())
+   {
+      delete(mGoal);
+      mGoal = new AIGoal(this, targ->mObj, targ->mRadius);
+   }
+   else if (targ->mPosSet)
+   {
+      delete(mGoal);
+      mGoal = new AIGoal(this, targ->mPosition, targ->mRadius);
+   }
+}
+
+void AIController::setGoal(Point3F loc, F32 rad)
+{
+   if (mGoal) delete(mGoal);
+   mGoal = new AIGoal(this, loc, rad);
+}
+
+void AIController::setGoal(SimObjectPtr<SceneObject> objIn, F32 rad)
+{
+   if (mGoal) delete(mGoal);
+   mGoal = new AIGoal(this, objIn, rad);
+}
+
+void AIController::setAim(Point3F loc, F32 rad, Point3F offset)
+{
+   if (mAimTarget) delete(mAimTarget);
+   mAimTarget = new AIAimTarget(this, loc, rad);
+   mAimTarget->mAimOffset = offset;
+}
+
+void AIController::setAim(SimObjectPtr<SceneObject> objIn, F32 rad, Point3F offset)
+{
+   if (mAimTarget) delete(mAimTarget);
+   mAimTarget = new AIAimTarget(this, objIn, rad);
+   mAimTarget->mAimOffset = offset;
+}
+
+bool AIController::getAIMove(Move* movePtr)
+{
+   *movePtr = NullMove;
+   ShapeBase* sbo = dynamic_cast<ShapeBase*>(getAIInfo()->mObj.getPointer());
+   if (!sbo) return false;
+
+   // Use the eye as the current position.
+   MatrixF eye;
+   sbo->getEyeTransform(&eye);
+   Point3F location = eye.getPosition();
+   Point3F rotation = sbo->getTransform().toEuler();
+
+   // Test for target location in sight if it's an object. The LOS is
+   // run from the eye position to the center of the object's bounding,
+   // which is not very accurate.
+   if (getAim() && getAim()->mObj)
+   {
+      GameBase* gbo = dynamic_cast<GameBase*>(getAIInfo()->mObj.getPointer());
+      if (getAim()->checkInLos(gbo))
+      {
+         if (!getAim()->mTargetInLOS)
+         {
+            throwCallback("onTargetEnterLOS");
+            getAim()->mTargetInLOS = true;
+         }
+      }
+      else if (getAim()->mTargetInLOS)
+      {
+         throwCallback("onTargetExitLOS");
+         getAim()->mTargetInLOS = false;
+      }
+   }
+
+   if (sbo->getDamageState() == ShapeBase::Enabled && getGoal())
+   {
+#ifdef TORQUE_NAVIGATION_ENABLED
+      if (mMovement.mMoveState != ModeStop)
+         getNav()->updateNavMesh();
+
+      if (getNav()->mPathData.path.isNull())
+      {
+         if (getGoal()->getDist() > mControllerData->mFollowTolerance)
+         {
+            if (getGoal()->mObj.isValid())
+               getNav()->followObject(getGoal()->mObj, mControllerData->mFollowTolerance);
+            else if (getGoal()->mPosSet)
+               getNav()->setPathDestination(getGoal()->getPosition(true));
+         }
+      }
+      else
+      {
+         if (getGoal()->getDist() > mControllerData->mFollowTolerance)
+         {
+            SceneObject* obj = getAIInfo()->mObj->getObjectMount();
+            if (!obj)
+            {
+               obj = getAIInfo()->mObj;
+            }
+            RayInfo info;
+            if (obj->getContainer()->castRay(obj->getPosition(), obj->getPosition() - Point3F(0, 0, mControllerData->mHeightTolerance), StaticShapeObjectType, &info))
+            {
+               getNav()->repath();
+            }
+            getGoal()->mInRange = false;
+         }
+         if (getGoal()->getDist() < mControllerData->mFollowTolerance )
+         {
+            getNav()->clearPath();
+            mMovement.mMoveState = ModeStop;
+
+            if (!getGoal()->mInRange)
+            {
+               getGoal()->mInRange = true;
+               throwCallback("onTargetInRange");
+            }
+            else getGoal()->mInRange = false;
+         }
+         else
+         {
+            if (getGoal()->getDist() < mControllerData->mAttackRadius )
+            {
+               if (!getGoal()->mInFiringRange)
+               {
+                  getGoal()->mInFiringRange = true;
+                  throwCallback("onTargetInFiringRange");
+               }
+            }
+            else getGoal()->mInFiringRange = false;
+         }
+      }
+#else
+      if (getGoal()->getDist() > mControllerData->mFollowTolerance)
+      {
+         if (getGoal()->mObj.isValid())
+            getNav()->followObject(getGoal()->mObj, mControllerData->mFollowTolerance);
+         else if (getGoal()->mPosSet)
+            getNav()->setPathDestination(getGoal()->getPosition(true));
+
+         getGoal()->mInRange = false;
+      }
+      if (getGoal()->getDist() < mControllerData->mFollowTolerance)
+      {
+         mMovement.mMoveState = ModeStop;
+
+         if (!getGoal()->mInRange)
+         {
+            getGoal()->mInRange = true;
+            throwCallback("onTargetInRange");
+         }
+         else getGoal()->mInRange = false;
+      }
+      else
+      {
+         if (getGoal()->getDist() < mControllerData->mAttackRadius)
+         {
+            if (!getGoal()->mInFiringRange)
+            {
+               getGoal()->mInFiringRange = true;
+               throwCallback("onTargetInFiringRange");
+            }
+         }
+         else getGoal()->mInFiringRange = false;
+      }
+#endif // TORQUE_NAVIGATION_ENABLED
+   }
+   // Orient towards the aim point, aim object, or towards
+   // our destination.
+   if (getAim() || mMovement.mMoveState != ModeStop)
+   {
+      // Update the aim position if we're aiming for an object or explicit position
+      if (getAim())
+         mMovement.mAimLocation = getAim()->getPosition();
+      else
+         mMovement.mAimLocation = getNav()->getMoveDestination();
+
+      mControllerData->resolveYawPtr(this, location, movePtr);
+      mControllerData->resolvePitchPtr(this, location, movePtr);
+      mControllerData->resolveRollPtr(this, location, movePtr);
+
+      if (mMovement.mMoveState != AIController::ModeStop)
+      {
+         F32 xDiff = getNav()->getMoveDestination().x - location.x;
+         F32 yDiff = getNav()->getMoveDestination().y - location.y;
+         if (mFabs(xDiff) < mControllerData->mMoveTolerance && mFabs(yDiff) < mControllerData->mMoveTolerance)
+         {
+            getNav()->onReachDestination();
+         }
+         else
+         {
+            mControllerData->resolveSpeedPtr(this, location, movePtr);
+            mControllerData->resolveStuckPtr(this);
+         }
+      }
+   }
+
+   mControllerData->resolveTriggerStatePtr(this, movePtr);
+
+   getAIInfo()->mLastPos = getAIInfo()->getPosition();
+   return true;
+}
+
+void AIController::clearCover()
+{
+   // Notify cover that we are no longer on our way.
+   if (getCover() && !getCover()->mCoverPoint.isNull())
+      getCover()->mCoverPoint->setOccupied(false);
+   SAFE_DELETE(mCover);
+}
+
+void AIController::Movement::stopMove()
+{
+   mMoveState = ModeStop;
+#ifdef TORQUE_NAVIGATION_ENABLED
+   getCtrl()->getNav()->clearPath();
+   getCtrl()->clearCover();
+   getCtrl()->getNav()->clearFollow();
+#endif
+}
+void AIController::Movement::onStuck()
+{
+   mMoveState = AIController::ModeStuck;
+   getCtrl()->throwCallback("onMoveStuck");
+#ifdef TORQUE_NAVIGATION_ENABLED
+   if (!getCtrl()->getNav()->getPath().isNull())
+      getCtrl()->getNav()->repath();
+#endif
+}
+
+DefineEngineMethod(AIController, setMoveSpeed, void, (F32 speed), ,
+   "@brief Sets the move speed for an AI object.\n\n"
+
+   "@param speed A speed multiplier between 0.0 and 1.0.  "
+   "This is multiplied by the AIController controlled object's base movement rates (as defined in "
+   "its PlayerData datablock)\n\n"
+
+   "@see getMoveDestination()\n")
+{
+   object->mMovement.setMoveSpeed(speed);
+}
+
+DefineEngineMethod(AIController, getMoveSpeed, F32, (), ,
+   "@brief Gets the move speed of an AI object.\n\n"
+
+   "@return A speed multiplier between 0.0 and 1.0.\n\n"
+
+   "@see setMoveSpeed()\n")
+{
+   return object->mMovement.getMoveSpeed();
+}
+
+DefineEngineMethod(AIController, stop, void, (), ,
+   "@brief Tells the AIController controlled object to stop moving.\n\n")
+{
+   object->mMovement.stopMove();
+}
+
+
+/**
+ * Set the state of a movement trigger.
+ *
+ * @param slot The trigger slot to set
+ * @param isSet set/unset the trigger
+ */
+void AIController::TriggerState::setMoveTrigger(U32 slot, const bool isSet)
+{
+   if (slot >= MaxTriggerKeys)
+   {
+      Con::errorf("Attempting to set an invalid trigger slot (%i)", slot);
+   }
+   else
+   {
+      mMoveTriggers[slot] = isSet;   // set the trigger
+      mControllerRef->getAIInfo()->mObj->setMaskBits(ShapeBase::NoWarpMask);         // force the client to updateMove
+   }
+}
+
+/**
+ * Get the state of a movement trigger.
+ *
+ * @param slot The trigger slot to query
+ * @return True if the trigger is set, false if it is not set
+ */
+bool AIController::TriggerState::getMoveTrigger(U32 slot) const
+{
+   if (slot >= MaxTriggerKeys)
+   {
+      Con::errorf("Attempting to get an invalid trigger slot (%i)", slot);
+      return false;
+   }
+   else
+   {
+      return mMoveTriggers[slot];
+   }
+}
+
+/**
+ * Clear the trigger state for all movement triggers.
+ */
+void AIController::TriggerState::clearMoveTriggers()
+{
+   for (U32 i = 0; i < MaxTriggerKeys; i++)
+      setMoveTrigger(i, false);
+}
+
+//-----------------------------------------------------------------------------
+
+IMPLEMENT_CO_DATABLOCK_V1(AIControllerData);
+void AIControllerData::resolveYaw(AIController* obj, Point3F location, Move* move)
+{
+   F32 xDiff = obj->mMovement.mAimLocation.x - location.x;
+   F32 yDiff = obj->mMovement.mAimLocation.y - location.y;
+   Point3F rotation = obj->getAIInfo()->mObj->getTransform().toEuler();
+
+   if (!mIsZero(xDiff) || !mIsZero(yDiff))
+   {
+      // First do Yaw
+      // use the cur yaw between -Pi and Pi
+      F32 curYaw = rotation.z;
+      while (curYaw > M_2PI_F)
+         curYaw -= M_2PI_F;
+      while (curYaw < -M_2PI_F)
+         curYaw += M_2PI_F;
+
+      // find the yaw offset
+      F32 newYaw = mAtan2(xDiff, yDiff);
+      F32 yawDiff = newYaw - curYaw;
+
+      // make it between 0 and 2PI
+      if (yawDiff < 0.0f)
+         yawDiff += M_2PI_F;
+      else if (yawDiff >= M_2PI_F)
+         yawDiff -= M_2PI_F;
+
+      // now make sure we take the short way around the circle
+      if (yawDiff > M_PI_F)
+         yawDiff -= M_2PI_F;
+      else if (yawDiff < -M_PI_F)
+         yawDiff += M_2PI_F;
+
+      move->yaw = yawDiff;
+   }
+}
+
+
+void AIControllerData::resolveRoll(AIController* obj, Point3F location, Move* movePtr)
+{
+}
+
+void AIControllerData::resolveSpeed(AIController* obj, Point3F location, Move* movePtr)
+{
+   F32 xDiff = obj->getNav()->getMoveDestination().x - location.x;
+   F32 yDiff = obj->getNav()->getMoveDestination().y - location.y;
+   Point3F rotation = obj->getAIInfo()->mObj->getTransform().toEuler();
+
+   // Build move direction in world space
+   if (mIsZero(xDiff))
+      movePtr->y = (location.y > obj->getNav()->getMoveDestination().y) ? -1.0f : 1.0f;
+   else
+   {
+      if (mIsZero(yDiff))
+         movePtr->x = (location.x > obj->getNav()->getMoveDestination().x) ? -1.0f : 1.0f;
+      else
+      {
+         if (mFabs(xDiff) > mFabs(yDiff))
+         {
+            F32 value = mFabs(yDiff / xDiff);
+            movePtr->y = (location.y > obj->getNav()->getMoveDestination().y) ? -value : value;
+            movePtr->x = (location.x > obj->getNav()->getMoveDestination().x) ? -1.0f : 1.0f;
+         }
+         else
+         {
+            F32 value = mFabs(xDiff / yDiff);
+            movePtr->x = (location.x > obj->getNav()->getMoveDestination().x) ? -value : value;
+            movePtr->y = (location.y > obj->getNav()->getMoveDestination().y) ? -1.0f : 1.0f;
+         }
+      }
+   }
+   // Rotate the move into object space (this really only needs
+   // a 2D matrix)
+   Point3F newMove;
+   MatrixF moveMatrix;
+   moveMatrix.set(EulerF(0.0f, 0.0f, -(rotation.z + movePtr->yaw)));
+   moveMatrix.mulV(Point3F(movePtr->x, movePtr->y, 0.0f), &newMove);
+   movePtr->x = newMove.x;
+   movePtr->y = newMove.y;
+
+   // Set movement speed.  We'll slow down once we get close
+   // to try and stop on the spot...
+   if (obj->mMovement.mMoveSlowdown)
+   {
+      F32 speed = obj->mMovement.mMoveSpeed;
+      F32 dist = mSqrt(xDiff * xDiff + yDiff * yDiff);
+      F32 maxDist = mMoveTolerance * 2;
+      if (dist < maxDist)
+         speed *= dist / maxDist;
+      movePtr->x *= speed;
+      movePtr->y *= speed;
+   }
+   else
+   {
+      movePtr->x *= obj->mMovement.mMoveSpeed;
+      movePtr->y *= obj->mMovement.mMoveSpeed;
+   }
+}
+
+void AIControllerData::resolveTriggerState(AIController* obj, Move* movePtr)
+{
+   //check for scripted overides
+   for (U32 slot = 0; slot < MaxTriggerKeys; slot++)
+   {      
+      movePtr->trigger[slot] = obj->mTriggerState.mMoveTriggers[slot];
+   }
+}
+
+void AIControllerData::resolveStuck(AIController* obj)
+{
+   if (obj->mMovement.mMoveState < AIController::ModeSlowing) return;
+   if (!obj->getGoal()) return;
+   ShapeBase* sbo = dynamic_cast<ShapeBase*>(obj->getAIInfo()->mObj.getPointer());
+   // Don't check for ai stuckness if animation during
+   // an anim-clip effect override.
+   if (sbo->getDamageState() == ShapeBase::Enabled && !(sbo->anim_clip_flags & ShapeBase::ANIM_OVERRIDDEN) && !sbo->isAnimationLocked())
+   {
+      // We should check to see if we are stuck...
+      F32 locationDelta = (obj->getAIInfo()->getPosition() - obj->getAIInfo()->mLastPos).len();
+      if (locationDelta < mMoveStuckTolerance)
+      {
+         if (obj->mMovement.mMoveStuckTestCountdown > 0)
+            --obj->mMovement.mMoveStuckTestCountdown;
+         else
+         {
+            // If we are slowing down, then it's likely that our location delta will be less than
+            // our move stuck tolerance. Because we can be both slowing and stuck
+            // we should TRY to check if we've moved. This could use better detection.
+            if (obj->mMovement.mMoveState != AIController::ModeSlowing || locationDelta == 0)
+            {
+               obj->mMovement.onStuck();
+               obj->mMovement.mMoveStuckTestCountdown = obj->mControllerData->mMoveStuckTestDelay;
+            }
+         }
+      }
+   }
+}
+
+AIControllerData::AIControllerData()
+{
+   mMoveTolerance = 0.25f;
+   mAttackRadius = 2.0f;
+   mMoveStuckTolerance = 0.01f;
+   mMoveStuckTestDelay = 30;
+   mHeightTolerance = 0.001f;
+   mFollowTolerance = 1.0f;
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   mLinkTypes = LinkData(AllFlags);
+   mNavSize = AINavigation::Regular;
+   mFlocking.mChance = 90;
+   mFlocking.mMin = 1.0f;
+   mFlocking.mMax = 3.0f;
+   mFlocking.mSideStep = 0.01f;
+#endif
+   resolveYawPtr.bind(this, &AIControllerData::resolveYaw);
+   resolvePitchPtr.bind(this, &AIControllerData::resolvePitch);
+   resolveRollPtr.bind(this, &AIControllerData::resolveRoll);
+   resolveSpeedPtr.bind(this, &AIControllerData::resolveSpeed);
+   resolveTriggerStatePtr.bind(this, &AIControllerData::resolveTriggerState);
+   resolveStuckPtr.bind(this, &AIControllerData::resolveStuck);
+}
+
+AIControllerData::AIControllerData(const AIControllerData& other, bool temp_clone) : SimDataBlock(other, temp_clone)
+{
+   mMoveTolerance = other.mMoveTolerance;
+   mAttackRadius = other.mAttackRadius;
+   mMoveStuckTolerance = other.mMoveStuckTolerance;
+   mMoveStuckTestDelay = other.mMoveStuckTestDelay;
+   mHeightTolerance = other.mHeightTolerance;
+   mFollowTolerance = other.mFollowTolerance;
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   mLinkTypes = other.mLinkTypes;
+   mNavSize = other.mNavSize;
+   mFlocking.mChance = other.mFlocking.mChance;
+   mFlocking.mMin = other.mFlocking.mMin;
+   mFlocking.mMax = other.mFlocking.mMax;
+   mFlocking.mSideStep = other.mFlocking.mSideStep;
+#endif
+
+   resolveYawPtr.bind(this, &AIControllerData::resolveYaw);
+   resolvePitchPtr.bind(this, &AIControllerData::resolvePitch);
+   resolveRollPtr.bind(this, &AIControllerData::resolveRoll);
+   resolveSpeedPtr.bind(this, &AIControllerData::resolveSpeed);
+   resolveTriggerStatePtr.bind(this, &AIControllerData::resolveTriggerState);
+   resolveStuckPtr.bind(this, &AIControllerData::resolveStuck);
+}
+
+void AIControllerData::initPersistFields()
+{
+   docsURL;
+   addGroup("AI");
+
+   addFieldV("moveTolerance", TypeRangedF32, Offset(mMoveTolerance, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief Distance from destination before stopping.\n\n"
+      "When the AIController controlled object is moving to a given destination it will move to within "
+      "this distance of the destination and then stop.  By providing this tolerance "
+      "it helps the AIController controlled object from never reaching its destination due to minor obstacles, "
+      "rounding errors on its position calculation, etc.  By default it is set to 0.25.\n");
+
+   addFieldV("followTolerance", TypeRangedF32, Offset(mFollowTolerance, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief Distance from destination before stopping.\n\n"
+      "When the AIController controlled object is moving to a given destination it will move to within "
+      "this distance of the destination and then stop.  By providing this tolerance "
+      "it helps the AIController controlled object from never reaching its destination due to minor obstacles, "
+      "rounding errors on its position calculation, etc.  By default it is set to 0.25.\n");
+
+   addFieldV("AttackRadius", TypeRangedF32, Offset(mAttackRadius, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief Distance considered in firing range for callback purposes.");
+
+   addFieldV("moveStuckTolerance", TypeRangedF32, Offset(mMoveStuckTolerance, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief Distance tolerance on stuck check.\n\n"
+      "When the AIController controlled object controlled object is moving to a given destination, if it ever moves less than "
+      "this tolerance during a single tick, the AIController controlled object is considered stuck.  At this point "
+      "the onMoveStuck() callback is called on the datablock.\n");
+
+   addFieldV("HeightTolerance", TypeRangedF32, Offset(mHeightTolerance, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief Distance from destination before stopping.\n\n"
+      "When the AIController controlled object is moving to a given destination it will move to within "
+      "this distance of the destination and then stop.  By providing this tolerance "
+      "it helps the AIController controlled object from never reaching its destination due to minor obstacles, "
+      "rounding errors on its position calculation, etc.  By default it is set to 0.25.\n");
+   
+   addFieldV("moveStuckTestDelay", TypeRangedS32, Offset(mMoveStuckTestDelay, AIControllerData), &CommonValidators::PositiveInt,
+      "@brief The number of ticks to wait before testing if the AIController controlled object is stuck.\n\n"
+      "When the AIController controlled object is asked to move, this property is the number of ticks to wait "
+      "before the AIController controlled object starts to check if it is stuck.  This delay allows the AIController controlled object "
+      "to accelerate to full speed without its initial slow start being considered as stuck.\n"
+      "@note Set to zero to have the stuck test start immediately.\n");
+   endGroup("AI");
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   addGroup("Pathfinding");
+   addFieldV("FlockChance", TypeRangedS32, Offset(mFlocking.mChance, AIControllerData), &CommonValidators::S32Percent,
+      "@brief chance of flocking.");
+   addFieldV("FlockMin", TypeRangedF32, Offset(mFlocking.mMin, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief min flocking separation distance.");
+   addFieldV("FlockMax", TypeRangedF32, Offset(mFlocking.mMax, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief max flocking clustering distance.");
+   addFieldV("FlockSideStep", TypeRangedF32, Offset(mFlocking.mSideStep, AIControllerData), &CommonValidators::PositiveFloat,
+      "@brief Distance from destination before we stop moving out of the way.");
+
+   addField("allowWalk", TypeBool, Offset(mLinkTypes.walk, AIControllerData),
+      "Allow the character to walk on dry land.");
+   addField("allowJump", TypeBool, Offset(mLinkTypes.jump, AIControllerData),
+      "Allow the character to use jump links.");
+   addField("allowDrop", TypeBool, Offset(mLinkTypes.drop, AIControllerData),
+      "Allow the character to use drop links.");
+   addField("allowSwim", TypeBool, Offset(mLinkTypes.swim, AIControllerData),
+      "Allow the character to move in water.");
+   addField("allowLedge", TypeBool, Offset(mLinkTypes.ledge, AIControllerData),
+      "Allow the character to jump ledges.");
+   addField("allowClimb", TypeBool, Offset(mLinkTypes.climb, AIControllerData),
+      "Allow the character to use climb links.");
+   addField("allowTeleport", TypeBool, Offset(mLinkTypes.teleport, AIControllerData),
+      "Allow the character to use teleporters.");
+
+   endGroup("Pathfinding");
+#endif // TORQUE_NAVIGATION_ENABLED
+
+   Parent::initPersistFields();
+}
+
+void AIControllerData::packData(BitStream* stream)
+{
+   Parent::packData(stream);
+   stream->write(mMoveTolerance);
+   stream->write(mAttackRadius);
+   stream->write(mMoveStuckTolerance);
+   stream->write(mMoveStuckTestDelay);
+   stream->write(mHeightTolerance);
+   stream->write(mFollowTolerance);
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   //enums
+   stream->write(mLinkTypes.getFlags());
+   stream->write((U32)mNavSize);
+   // end enums
+   stream->write(mFlocking.mChance);
+   stream->write(mFlocking.mMin);
+   stream->write(mFlocking.mMax);
+   stream->write(mFlocking.mSideStep);
+#endif // TORQUE_NAVIGATION_ENABLED
+};
+
+void AIControllerData::unpackData(BitStream* stream)
+{
+   Parent::unpackData(stream);
+
+   stream->read(&mMoveTolerance);
+   stream->read(&mAttackRadius);
+   stream->read(&mMoveStuckTolerance);
+   stream->read(&mMoveStuckTestDelay);
+   stream->read(&mHeightTolerance);
+   stream->read(&mFollowTolerance);
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   //enums
+   U16 linkFlags;
+   stream->read(&linkFlags);
+   mLinkTypes = LinkData(linkFlags);
+   U32 navSize;
+   stream->read(&navSize);
+   mNavSize = (AINavigation::NavSize)(navSize);   
+   // end enums
+   stream->read(&(mFlocking.mChance));
+   stream->read(&(mFlocking.mMin));
+   stream->read(&(mFlocking.mMax));
+   stream->read(&(mFlocking.mSideStep));
+#endif // TORQUE_NAVIGATION_ENABLED
+};
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+IMPLEMENT_CO_DATABLOCK_V1(AIPlayerControllerData);
+void AIPlayerControllerData::resolvePitch(AIController* obj, Point3F location, Move* movePtr)
+{
+   Player* po = dynamic_cast<Player*>(obj->getAIInfo()->mObj.getPointer());
+   if (!po) return;//not a player
+
+   if (obj->getAim() || obj->mMovement.mMoveState != AIController::ModeStop)
+   {
+      // Next do pitch.
+      if (!obj->getAim())
+      {
+         // Level out if were just looking at our next way point.
+         Point3F headRotation = po->getHeadRotation();
+         movePtr->pitch = -headRotation.x;
+      }
+      else
+      {
+         F32 xDiff = obj->mMovement.mAimLocation.x - location.x;
+         F32 yDiff = obj->mMovement.mAimLocation.y - location.y;
+         // This should be adjusted to run from the
+         // eye point to the object's center position. Though this
+         // works well enough for now.
+         F32 vertDist = obj->mMovement.mAimLocation.z - location.z;
+         F32 horzDist = mSqrt(xDiff * xDiff + yDiff * yDiff);
+         F32 newPitch = mAtan2(horzDist, vertDist) - (M_PI_F / 2.0f);
+         if (mFabs(newPitch) > 0.01f)
+         {
+            Point3F headRotation = po->getHeadRotation();
+            movePtr->pitch = newPitch - headRotation.x;
+         }
+      }
+   }
+   else
+   {
+      // Level out if we're not doing anything else
+      Point3F headRotation = po->getHeadRotation();
+      movePtr->pitch = -headRotation.x;
+   }
+}
+
+void AIPlayerControllerData::resolveTriggerState(AIController* obj, Move* movePtr)
+{
+   Parent::resolveTriggerState(obj, movePtr);
+#ifdef TORQUE_NAVIGATION_ENABLED
+   if (obj->getNav()->mJump == AINavigation::Now)
+   {
+      movePtr->trigger[2] = true;
+      obj->getNav()->mJump = AINavigation::None;
+   }
+   else if (obj->getNav()->mJump == AINavigation::Ledge)
+   {
+      // If we're not touching the ground, jump!
+      RayInfo info;
+      if (!obj->getAIInfo()->mObj->getContainer()->castRay(obj->getAIInfo()->getPosition(), obj->getAIInfo()->getPosition() - Point3F(0, 0, 0.4f), StaticShapeObjectType, &info))
+      {
+         movePtr->trigger[2] = true;
+         obj->getNav()->mJump = AINavigation::None;
+      }
+   }
+#endif // TORQUE_NAVIGATION_ENABLED
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+IMPLEMENT_CO_DATABLOCK_V1(AIWheeledVehicleControllerData);
+
+void AIWheeledVehicleControllerData::resolveYaw(AIController* obj, Point3F location, Move* movePtr)
+{
+   if (obj->mMovement.mMoveState < AIController::ModeSlowing) return;
+   WheeledVehicle* wvo = dynamic_cast<WheeledVehicle*>(obj->getAIInfo()->mObj.getPointer());
+   if (!wvo)
+   {
+      //cover the case of a connection controling an object in turn controlling another
+      if (obj->getAIInfo()->mObj->getObjectMount())
+         wvo = dynamic_cast<WheeledVehicle*>(obj->getAIInfo()->mObj->getObjectMount());
+   }
+   if (!wvo) return;//not a WheeledVehicle
+
+   F32 lastYaw = wvo->getSteering().x;
+
+   Point3F right = wvo->getTransform().getRightVector();
+   right.normalize();
+   Point3F aimLoc = obj->mMovement.mAimLocation;
+
+   // Get the AI to Target vector and normalize it.
+   Point3F toTarg = (location + wvo->getVelocity() * TickSec) - aimLoc;
+   toTarg.normalize();
+
+   F32 dotYaw = -mDot(right, toTarg);
+   movePtr->yaw = -lastYaw;
+
+   VehicleData* vd = (VehicleData*)(wvo->getDataBlock());
+   F32 maxSteeringAngle = vd->maxSteeringAngle;
+
+   if (mFabs(dotYaw) > maxSteeringAngle*1.5f)
+      dotYaw *= -1.0f;
+
+   if (dotYaw > maxSteeringAngle) dotYaw = maxSteeringAngle;
+   if (dotYaw < -maxSteeringAngle) dotYaw = -maxSteeringAngle;
+
+   if (mFabs(dotYaw) > 0.05f)
+      movePtr->yaw = dotYaw - lastYaw;
+};
+
+void AIWheeledVehicleControllerData::resolveSpeed(AIController* obj, Point3F location, Move* movePtr)
+{
+   if (obj->mMovement.mMoveState < AIController::ModeSlowing) return;
+   WheeledVehicle* wvo = dynamic_cast<WheeledVehicle*>(obj->getAIInfo()->mObj.getPointer());
+   if (!wvo)
+   {
+      //cover the case of a connection controling an object in turn controlling another
+      if (obj->getAIInfo()->mObj->getObjectMount())
+         wvo = dynamic_cast<WheeledVehicle*>(obj->getAIInfo()->mObj->getObjectMount());
+   }
+   if (!wvo) return;//not a WheeledVehicle
+
+   Parent::resolveSpeed(obj, location, movePtr);
+
+   VehicleData* db =  static_cast<VehicleData *>(wvo->getDataBlock());
+   movePtr->x = 0;
+   movePtr->y *= mMax((db->maxSteeringAngle-mFabs(movePtr->yaw) / db->maxSteeringAngle),0.75f);
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+IMPLEMENT_CO_DATABLOCK_V1(AIFlyingVehicleControllerData);
+
+void AIFlyingVehicleControllerData::initPersistFields()
+{
+   docsURL;
+   addGroup("AI");
+
+   addFieldV("FlightFloor", TypeRangedF32, Offset(mFlightFloor, AIFlyingVehicleControllerData), &CommonValidators::F32Range,
+      "@brief Min height we can target.");
+
+   addFieldV("FlightCeiling", TypeRangedF32, Offset(mFlightCeiling, AIFlyingVehicleControllerData), &CommonValidators::F32Range,
+      "@brief Max height we can target.");
+
+   endGroup("AI");
+
+   Parent::initPersistFields();
+}
+
+AIFlyingVehicleControllerData::AIFlyingVehicleControllerData(const AIFlyingVehicleControllerData& other, bool temp_clone) : AIControllerData(other, temp_clone)
+{
+   mFlightCeiling = other.mFlightCeiling;
+   mFlightFloor = other.mFlightFloor;
+   resolveYawPtr.bind(this, &AIFlyingVehicleControllerData::resolveYaw);
+   resolvePitchPtr.bind(this, &AIFlyingVehicleControllerData::resolvePitch);
+   resolveSpeedPtr.bind(this, &AIFlyingVehicleControllerData::resolveSpeed);
+}
+
+void AIFlyingVehicleControllerData::resolveYaw(AIController* obj, Point3F location, Move* movePtr)
+{
+   if (obj->mMovement.mMoveState < AIController::ModeSlowing) return;
+   FlyingVehicle* fvo = dynamic_cast<FlyingVehicle*>(obj->getAIInfo()->mObj.getPointer());
+   if (!fvo)
+   {
+      //cover the case of a connection controling an object in turn controlling another
+      if (obj->getAIInfo()->mObj->getObjectMount())
+         fvo = dynamic_cast<FlyingVehicle*>(obj->getAIInfo()->mObj->getObjectMount());
+   }
+   if (!fvo) return;//not a FlyingVehicle
+
+   Point3F right = fvo->getTransform().getRightVector();
+   right.normalize();
+
+   // Get the Target to AI vector and normalize it.
+   Point3F aimLoc = obj->mMovement.mAimLocation;
+   aimLoc.z = mClampF(aimLoc.z, mFlightFloor, mFlightCeiling);
+   Point3F toTarg = (location + fvo->getVelocity() * TickSec) - aimLoc;
+   toTarg.normalize();
+
+   movePtr->yaw = 0;
+   F32 dotYaw = -mDot(right, toTarg);
+   if (mFabs(dotYaw) > 0.05f)
+      movePtr->yaw = dotYaw;
+};
+
+void AIFlyingVehicleControllerData::resolvePitch(AIController* obj, Point3F location, Move* movePtr)
+{
+   if (obj->mMovement.mMoveState < AIController::ModeSlowing) return;
+   FlyingVehicle* fvo = dynamic_cast<FlyingVehicle*>(obj->getAIInfo()->mObj.getPointer());
+   if (!fvo)
+   {
+      //cover the case of a connection controling an object in turn controlling another
+      if (obj->getAIInfo()->mObj->getObjectMount())
+         fvo = dynamic_cast<FlyingVehicle*>(obj->getAIInfo()->mObj->getObjectMount());
+   }
+   if (!fvo) return;//not a FlyingVehicle
+
+   Point3F up = fvo->getTransform().getUpVector();
+   up.normalize();
+
+
+   // Get the Target to AI vector and normalize it.
+   Point3F aimLoc = obj->mMovement.mAimLocation;
+   aimLoc.z = mClampF(aimLoc.z, mFlightFloor, mFlightCeiling);
+   Point3F toTarg = (location + fvo->getVelocity() * TickSec) - aimLoc;
+   toTarg.normalize();
+
+   movePtr->pitch = 0.0f;
+   F32 dotPitch = mDot(up, toTarg);
+   if (mFabs(dotPitch) > 0.05f)
+         movePtr->pitch = dotPitch * M_2PI_F;
+         
+}
+
+void AIFlyingVehicleControllerData::resolveSpeed(AIController* obj, Point3F location, Move* movePtr)
+{
+   if (obj->mMovement.mMoveState < AIController::ModeSlowing) return;
+   FlyingVehicle* fvo = dynamic_cast<FlyingVehicle*>(obj->getAIInfo()->mObj.getPointer());
+   if (!fvo)
+   {
+      //cover the case of a connection controling an object in turn controlling another
+      if (obj->getAIInfo()->mObj->getObjectMount())
+         fvo = dynamic_cast<FlyingVehicle*>(obj->getAIInfo()->mObj->getObjectMount());
+   }
+   if (!fvo) return;//not a FlyingVehicle
+
+   movePtr->x = 0;
+   movePtr->y = obj->mMovement.mMoveSpeed;
+}

+ 247 - 0
Engine/source/T3D/AI/AIController.h

@@ -0,0 +1,247 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#ifndef _AICONTROLLER_H_
+#define _AICONTROLLER_H_
+#include "navigation/coverPoint.h"
+#include "AIInfo.h"
+#include "AIGoal.h"
+#include "AIAimTarget.h"
+#include "AICover.h"
+#include "AINavigation.h"
+class AIControllerData;
+
+//-----------------------------------------------------------------------------
+class AIController : public SimObject {
+
+   typedef SimObject Parent;
+
+public:
+   AIControllerData* mControllerData;
+protected:
+   static bool setControllerDataProperty(void* object, const char* index, const char* data);
+public:
+   enum MoveState {
+      ModeStop,                       // AI has stopped moving.
+      ModeStuck,                      // AI is stuck, but wants to move.
+      ModeSlowing,                    // AI is slowing down as it reaches it's destination.
+      ModeMove,                       // AI is currently moving.
+      ModeReverse                     // AI is reversing
+   };
+
+private:
+   AIInfo*mAIInfo;
+public:
+   void setAIInfo(SimObjectPtr<SceneObject> objIn, F32 rad = 0.0f) { delete(mAIInfo); mAIInfo = new AIInfo(this, objIn, rad); }
+   AIInfo* getAIInfo() { return mAIInfo; }
+private:
+   AIGoal* mGoal;
+public:
+   void setGoal(AIInfo* targ);
+   void setGoal(Point3F loc, F32 rad = 0.0f);
+   void setGoal(SimObjectPtr<SceneObject> objIn, F32 rad = 0.0f);
+   AIGoal* getGoal() { return mGoal; }
+   void clearGoal() { SAFE_DELETE(mGoal); }
+private:
+   AIAimTarget* mAimTarget;
+public:
+   void setAim(Point3F loc, F32 rad = 0.0f, Point3F offset = Point3F(0.0f, 0.0f, 0.0f));
+   void setAim(SimObjectPtr<SceneObject> objIn, F32 rad = 0.0f, Point3F offset = Point3F(0.0f, 0.0f, 0.0f));
+   AIAimTarget* getAim() { return mAimTarget; }
+   void clearAim() { SAFE_DELETE(mAimTarget); }
+private:
+   AICover* mCover;
+public:
+   void setCover(Point3F loc, F32 rad = 0.0f) { delete(mCover); mCover = new AICover(this, loc, rad); }
+   void setCover(SimObjectPtr<SceneObject> objIn, F32 rad = 0.0f) { delete(mCover); mCover = new AICover(this, objIn, rad); }
+   AICover* getCover() { return mCover; }
+   bool findCover(const Point3F& from, F32 radius);
+   void clearCover();
+
+   // Utility Methods
+   void throwCallback(const char* name);
+   AINavigation* mNav;
+   AINavigation* getNav() { return mNav; };
+   struct Movement
+   {
+      AIController* mControllerRef;
+      AIController* getCtrl() { return mControllerRef; };
+      MoveState mMoveState;
+      F32 mMoveSpeed = 1.0;
+      void setMoveSpeed(F32 speed) { mMoveSpeed = speed; };
+      F32 getMoveSpeed() { return mMoveSpeed; };
+      bool mMoveSlowdown;                 // Slowdown as we near the destination
+      Point3F mLastLocation;              // For stuck check
+      S32 mMoveStuckTestCountdown;        // The current countdown until at AI starts to check if it is stuck
+      Point3F mAimLocation;
+      // move triggers
+      bool mMoveTriggers[MaxTriggerKeys];
+      void stopMove();
+      void onStuck();
+   } mMovement;
+
+   struct TriggerState
+   {
+      AIController* mControllerRef;
+      AIController* getCtrl() { return mControllerRef; };
+      bool mMoveTriggers[MaxTriggerKeys];
+      // Trigger sets/gets
+      void setMoveTrigger(U32 slot, const bool isSet = true);
+      bool getMoveTrigger(U32 slot) const;
+      void clearMoveTriggers();
+   } mTriggerState;
+   bool getAIMove(Move* move);
+
+   static void initPersistFields();
+   AIController()
+   {
+      for (S32 i = 0; i < MaxTriggerKeys; i++)
+         mTriggerState.mMoveTriggers[i] = false;
+
+      mMovement.mControllerRef = this;
+      mTriggerState.mControllerRef = this;
+      mControllerData = NULL;
+      mAIInfo = new AIInfo(this);
+      mNav = new AINavigation(this);
+      mGoal = NULL;
+      mAimTarget = NULL;
+      mCover = NULL;
+      mMovement.mMoveState = ModeStop;
+   };
+   ~AIController()
+   {
+      SAFE_DELETE(mAIInfo);
+      SAFE_DELETE(mNav);
+      clearGoal();
+      clearAim();
+      clearCover();
+   }
+   DECLARE_CONOBJECT(AIController);
+};
+
+//-----------------------------------------------------------------------------
+class AIControllerData : public SimDataBlock {
+
+   typedef SimDataBlock Parent;
+
+public:
+
+   AIControllerData();
+   AIControllerData(const AIControllerData&, bool = false);
+   ~AIControllerData() {};
+   void packData(BitStream* stream) override;
+   void unpackData(BitStream* stream) override;
+   static void initPersistFields();
+   DECLARE_CONOBJECT(AIControllerData);
+
+   F32 mMoveTolerance;                 // Distance from destination point before we stop
+   F32 mFollowTolerance;               // Distance from destination object before we stop
+   F32 mAttackRadius;                  // Distance to trigger weaponry calcs
+   S32 mMoveStuckTestDelay;            // The number of ticks to wait before checking if the AI is stuck
+   F32 mMoveStuckTolerance;            // Distance tolerance on stuck check
+   F32 mHeightTolerance;               // how high above the navmesh are we before we stop trying to repath
+#ifdef TORQUE_NAVIGATION_ENABLED
+   struct Flocking {
+      U32 mChance;                     // chance of flocking
+      F32 mMin;                        // min flocking separation distance
+      F32 mMax;                        // max flocking clustering distance
+      F32 mSideStep;                   // Distance from destination before we stop moving out of the way
+   } mFlocking;
+
+   /// Types of link we can use.
+   LinkData mLinkTypes;
+   AINavigation::NavSize mNavSize;
+#endif
+   Delegate<void(AIController* obj, Point3F location, Move* movePtr)> resolveYawPtr;
+   void resolveYaw(AIController* obj, Point3F location, Move* movePtr);
+
+   Delegate<void(AIController* obj, Point3F location, Move* movePtr)> resolvePitchPtr;
+   void resolvePitch(AIController* obj, Point3F location, Move* movePtr) {};
+
+   Delegate<void(AIController* obj, Point3F location, Move* movePtr)> resolveRollPtr;
+   void resolveRoll(AIController* obj, Point3F location, Move* movePtr);
+
+   Delegate<void(AIController* obj, Point3F location, Move* movePtr)> resolveSpeedPtr;
+   void resolveSpeed(AIController* obj, Point3F location, Move* movePtr);
+
+   Delegate<void(AIController* obj, Move* movePtr)> resolveTriggerStatePtr;
+   void resolveTriggerState(AIController* obj, Move* movePtr);
+
+   Delegate<void(AIController* obj)> resolveStuckPtr;
+   void resolveStuck(AIController* obj);
+};
+
+class AIPlayerControllerData : public AIControllerData
+{
+   typedef AIControllerData Parent;
+
+public:
+   AIPlayerControllerData()
+   {
+      resolvePitchPtr.bind(this, &AIPlayerControllerData::resolvePitch);
+      resolveTriggerStatePtr.bind(this, &AIPlayerControllerData::resolveTriggerState);
+   }
+   void resolvePitch(AIController* obj, Point3F location, Move* movePtr);
+   void resolveTriggerState(AIController* obj, Move* movePtr);
+   DECLARE_CONOBJECT(AIPlayerControllerData);
+};
+
+class AIWheeledVehicleControllerData : public AIControllerData
+{
+   typedef AIControllerData Parent;
+
+public:
+   AIWheeledVehicleControllerData()
+   {
+      resolveYawPtr.bind(this, &AIWheeledVehicleControllerData::resolveYaw);
+      resolveSpeedPtr.bind(this, &AIWheeledVehicleControllerData::resolveSpeed);
+      mHeightTolerance = 2.0f;
+   }
+   void resolveYaw(AIController* obj, Point3F location, Move* movePtr);
+   void resolveSpeed(AIController* obj, Point3F location, Move* movePtr);
+   DECLARE_CONOBJECT(AIWheeledVehicleControllerData);
+};
+
+class AIFlyingVehicleControllerData : public AIControllerData
+{
+   typedef AIControllerData Parent;
+
+   F32 mFlightFloor;
+   F32 mFlightCeiling;
+public:
+   AIFlyingVehicleControllerData()
+   {
+      resolveYawPtr.bind(this, &AIFlyingVehicleControllerData::resolveYaw);
+      resolvePitchPtr.bind(this, &AIFlyingVehicleControllerData::resolvePitch);
+      resolveSpeedPtr.bind(this, &AIFlyingVehicleControllerData::resolveSpeed);
+      mHeightTolerance = 200.0f;
+      mFlightCeiling = 200.0f;
+      mFlightFloor = 1.0;
+   }
+   AIFlyingVehicleControllerData(const AIFlyingVehicleControllerData&, bool = false);
+   static void initPersistFields();
+   void resolveYaw(AIController* obj, Point3F location, Move* movePtr);
+   void resolveSpeed(AIController* obj, Point3F location, Move* movePtr);
+   void resolvePitch(AIController* obj, Point3F location, Move* movePtr);
+
+   DECLARE_CONOBJECT(AIFlyingVehicleControllerData);
+};
+#endif //_AICONTROLLER_H_

+ 108 - 0
Engine/source/T3D/AI/AICover.cpp

@@ -0,0 +1,108 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#include "AICover.h"
+#include "AIController.h"
+
+struct CoverSearch
+{
+   Point3F loc;
+   Point3F from;
+   F32 dist;
+   F32 best;
+   CoverPoint* point;
+   CoverSearch() : loc(0, 0, 0), from(0, 0, 0)
+   {
+      best = -FLT_MAX;
+      point = NULL;
+      dist = FLT_MAX;
+   }
+};
+
+static void findCoverCallback(SceneObject* obj, void* key)
+{
+   CoverPoint* p = dynamic_cast<CoverPoint*>(obj);
+   if (!p || p->isOccupied())
+      return;
+   CoverSearch* s = static_cast<CoverSearch*>(key);
+   Point3F dir = s->from - p->getPosition();
+   dir.normalizeSafe();
+   // Score first based on angle of cover point to enemy.
+   F32 score = mDot(p->getNormal(), dir);
+   // Score also based on distance from seeker.
+   score -= (p->getPosition() - s->loc).len() / s->dist;
+   // Finally, consider cover size.
+   score += (p->getSize() + 1) / CoverPoint::NumSizes;
+   score *= p->getQuality();
+   if (score > s->best)
+   {
+      s->best = score;
+      s->point = p;
+   }
+}
+
+bool AIController::findCover(const Point3F& from, F32 radius)
+{
+   if (radius <= 0)
+      return false;
+
+   // Create a search state.
+   CoverSearch s;
+   s.loc = getAIInfo()->getPosition();
+   s.dist = radius;
+   // Direction we seek cover FROM.
+   s.from = from;
+
+   // Find cover points.
+   Box3F box(radius * 2.0f);
+   box.setCenter(getAIInfo()->getPosition());
+   getAIInfo()->mObj->getContainer()->findObjects(box, MarkerObjectType, findCoverCallback, &s);
+
+   // Go to cover!
+   if (s.point)
+   {
+      // Calling setPathDestination clears cover...
+      bool foundPath = getNav()->setPathDestination(s.point->getPosition());
+      setCover(s.point);
+      s.point->setOccupied(true);
+      return foundPath;
+   }
+   return false;
+}
+
+
+DefineEngineMethod(AIController, findCover, S32, (Point3F from, F32 radius), ,
+   "@brief Tells the AI to find cover nearby.\n\n"
+
+   "@param from   Location to find cover from (i.e., enemy position).\n"
+   "@param radius Distance to search for cover.\n"
+   "@return Cover point ID if cover was found, -1 otherwise.\n\n")
+{
+   if (object->findCover(from, radius))
+   {
+      CoverPoint* cover = object->getCover()->mCoverPoint.getObject();
+      return cover ? cover->getId() : -1;
+   }
+   else
+   {
+      return -1;
+   }
+}

+ 41 - 0
Engine/source/T3D/AI/AICover.h

@@ -0,0 +1,41 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#ifndef _AICOVER_H_
+#define _AICOVER_H_
+
+#include "AIInfo.h"
+#include "navigation/coverPoint.h"
+
+
+
+struct AICover : public AIInfo
+{
+   typedef AIInfo Parent;
+   /// Pointer to a cover point.
+   SimObjectPtr<CoverPoint> mCoverPoint;
+   AICover() = delete;
+   AICover(AIController* controller) : Parent(controller) {};
+   AICover(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn) : Parent(controller, objIn, radIn) { mCoverPoint = dynamic_cast<CoverPoint*>(objIn.getPointer());};
+   AICover(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) {};
+};
+
+#endif

+ 24 - 0
Engine/source/T3D/AI/AIGoal.cpp

@@ -0,0 +1,24 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+#include "AIGoal.h"
+#include "AIController.h"

+ 36 - 0
Engine/source/T3D/AI/AIGoal.h

@@ -0,0 +1,36 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#ifndef _AIGOAL_H_
+#define _AIGOAL_H_
+
+#include "AIInfo.h"
+
+struct AIGoal : public AIInfo
+{
+   typedef AIInfo Parent;
+   bool mInRange, mInFiringRange;
+   AIGoal() = delete;
+   AIGoal(AIController* controller) : Parent(controller) { mInRange = mInFiringRange = false; };
+   AIGoal(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn) : Parent(controller, objIn, radIn) { mInRange = mInFiringRange = false; };
+   AIGoal(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) { mInRange = mInFiringRange = false; };
+};
+#endif

+ 81 - 0
Engine/source/T3D/AI/AIInfo.cpp

@@ -0,0 +1,81 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+
+#include "AIInfo.h"
+#include "AIController.h"
+
+AIInfo::AIInfo(AIController* controller)
+{
+   mControllerRef = controller;
+   mObj = NULL;
+   mPosition = mLastPos = Point3F(0.0f, 0.0f, 0.0f);
+   mRadius = 0.0f;
+   mPosSet = false;
+};
+
+AIInfo::AIInfo(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn)
+{
+   mControllerRef = controller;
+   mObj = objIn;
+   mPosition = mLastPos = objIn->getPosition();
+   mRadius = radIn;
+   mPosSet = false;
+   if (radIn == 0.0f)
+      mRadius = mMax(objIn->getObjBox().len_x(), objIn->getObjBox().len_y()) * 0.5;
+};
+
+AIInfo::AIInfo(AIController* controller, Point3F pointIn, F32 radIn)
+{
+   mControllerRef = controller;
+   mObj = NULL;
+   mPosition = mLastPos = pointIn;
+   mRadius = radIn;
+   mPosSet = true;
+};
+
+Point3F AIInfo::getPosition(bool doCastray)
+{
+   Point3F pos = (mObj.isValid()) ? mObj->getPosition() : mPosition;
+   if (doCastray)
+   {
+      RayInfo info;
+      if (gServerContainer.castRay(pos, pos - Point3F(0, 0, getCtrl()->mControllerData->mHeightTolerance), StaticShapeObjectType, &info))
+      {
+         pos = info.point;
+      }
+   }
+
+   return pos;
+}
+
+F32 AIInfo::getDist()
+{
+   AIInfo* controlObj = getCtrl()->getAIInfo();
+   Point3F targPos = getPosition();
+
+   if (mFabs(targPos.z - controlObj->getPosition().z) < getCtrl()->mControllerData->mHeightTolerance)
+      targPos.z = controlObj->getPosition().z;
+
+   F32 ret = VectorF(controlObj->getPosition() - targPos).len();
+   ret -= controlObj->mRadius + mRadius;
+   return ret;
+}

+ 46 - 0
Engine/source/T3D/AI/AIInfo.h

@@ -0,0 +1,46 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#ifndef _AIINFO_H_
+#define _AIINFO_H_
+
+#ifndef _SCENEOBJECT_H_
+#include "scene/sceneObject.h"
+#endif
+
+class AIController;
+struct AIInfo
+{
+   AIController* mControllerRef;
+   AIController* getCtrl() { return mControllerRef; };
+   void setCtrl(AIController* controller) { mControllerRef = controller; };
+   SimObjectPtr<SceneObject> mObj;
+   Point3F mPosition, mLastPos;
+   bool mPosSet;
+   F32 mRadius;
+   Point3F getPosition(bool doCastray = false);
+   F32 getDist();
+   AIInfo() = delete;
+   AIInfo(AIController* controller);
+   AIInfo(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn = 0.0f);
+   AIInfo(AIController* controller,Point3F pointIn, F32 radIn = 0.0f);
+};
+#endif

+ 574 - 0
Engine/source/T3D/AI/AINavigation.cpp

@@ -0,0 +1,574 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#include "AINavigation.h"
+#include "AIController.h"
+#include "T3D/shapeBase.h"
+
+static U32 sAILoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType | AIObjectType;
+
+AINavigation::AINavigation(AIController* controller)
+{
+   mControllerRef = controller;
+#ifdef TORQUE_NAVIGATION_ENABLED
+   mJump = None;
+   mNavSize = Regular;
+#endif
+}
+
+AINavigation::~AINavigation()
+{
+#ifdef TORQUE_NAVIGATION_ENABLED
+   clearPath();
+   clearFollow();
+#endif
+}
+
+void AINavigation::setMoveDestination(const Point3F& location, bool slowdown)
+{
+   mMoveDestination = location;
+   getCtrl()->mMovement.mMoveState = AIController::ModeMove;
+   getCtrl()->mMovement.mMoveSlowdown = slowdown;
+   getCtrl()->mMovement.mMoveStuckTestCountdown = getCtrl()->mControllerData->mMoveStuckTestDelay;
+}
+
+bool AINavigation::setPathDestination(const Point3F& pos, bool replace)
+{
+#ifdef TORQUE_NAVIGATION_ENABLED
+   if (replace)
+      getCtrl()->setGoal(pos, getCtrl()->mControllerData->mMoveTolerance);
+
+   if (!mNavMesh)
+      updateNavMesh();
+
+   // If we can't find a mesh, just move regularly.
+   if (!mNavMesh)
+   {
+      //setMoveDestination(pos);
+      getCtrl()->throwCallback("onPathFailed");
+      return false;
+   }
+
+   // Create a new path.
+   NavPath* path = new NavPath();
+
+   path->mMesh = mNavMesh;
+   path->mFrom = getCtrl()->getAIInfo()->getPosition(true);
+   path->mTo = getCtrl()->getGoal()->getPosition(true);
+   path->mFromSet = path->mToSet = true;
+   path->mAlwaysRender = true;
+   path->mLinkTypes = getCtrl()->mControllerData->mLinkTypes;
+   path->mXray = true;
+   // Paths plan automatically upon being registered.
+   if (!path->registerObject())
+   {
+      delete path;
+      return false;
+   }
+
+   if (path->success())
+   {
+      // Clear any current path we might have.
+      clearPath();
+      getCtrl()->clearCover();
+      // Store new path.
+      mPathData.path = path;
+      mPathData.owned = true;
+      // Skip node 0, which we are currently standing on.
+      moveToNode(1);
+      getCtrl()->throwCallback("onPathSuccess");
+      return true;
+   }
+   else
+   {
+      // Just move normally if we can't path.
+      //setMoveDestination(pos, true);
+      //return;
+      getCtrl()->throwCallback("onPathFailed");
+      path->deleteObject();
+      return false;
+   }
+#else
+   setMoveDestination(pos, false);
+   return true;
+#endif
+}
+
+Point3F AINavigation::getPathDestination() const
+{
+#ifdef TORQUE_NAVIGATION_ENABLED
+   if (!mPathData.path.isNull())
+      return mPathData.path->mTo;
+   return Point3F(0, 0, 0);
+#else
+   return getMoveDestination();
+#endif
+}
+void AINavigation::onReachDestination()
+{
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   if (!getPath().isNull())
+   {
+      if (mPathData.index == getPath()->size() - 1)
+      {
+         // Handle looping paths.
+         if (getPath()->mIsLooping)
+            moveToNode(0);
+         // Otherwise end path.
+         else
+         {
+            clearPath();
+            getCtrl()->throwCallback("onReachDestination");
+         }
+      }
+      else
+      {
+         moveToNode(mPathData.index + 1);
+         // Throw callback every time if we're on a looping path.
+         //if(mPathData.path->mIsLooping)
+            //throwCallback("onReachDestination");
+      }
+   }
+   else
+#endif
+   {
+      getCtrl()->throwCallback("onReachDestination");
+      getCtrl()->mMovement.mMoveState = AIController::ModeStop;
+   }
+}
+
+void AINavigation::followObject()
+{
+   if (getCtrl()->getGoal()->getDist() < getCtrl()->mControllerData->mMoveTolerance)
+      return;
+
+   if (setPathDestination(getCtrl()->getGoal()->getPosition(true)))
+   {
+#ifdef TORQUE_NAVIGATION_ENABLED
+      getCtrl()->clearCover();
+#endif
+   }
+}
+
+void AINavigation::followObject(SceneObject* obj, F32 radius)
+{
+   getCtrl()->setGoal(obj, radius);
+   followObject();
+}
+
+void AINavigation::clearFollow()
+{
+   getCtrl()->clearGoal();
+}
+
+DefineEngineMethod(AIController, setMoveDestination, void, (Point3F goal, bool slowDown), (true),
+   "@brief Tells the AI to move to the location provided\n\n"
+
+   "@param goal Coordinates in world space representing location to move to.\n"
+   "@param slowDown A boolean value. If set to true, the bot will slow down "
+   "when it gets within 5-meters of its move destination. If false, the bot "
+   "will stop abruptly when it reaches the move destination. By default, this is true.\n\n"
+
+   "@note Upon reaching a move destination, the bot will clear its move destination and "
+   "calls to getMoveDestination will return \"0 0 0\"."
+
+   "@see getMoveDestination()\n")
+{
+   object->getNav()->setMoveDestination(goal, slowDown);
+}
+
+
+DefineEngineMethod(AIController, getMoveDestination, Point3F, (), ,
+   "@brief Get the AIPlayer's current destination.\n\n"
+
+   "@return Returns a point containing the \"x y z\" position "
+   "of the AIPlayer's current move destination. If no move destination "
+   "has yet been set, this returns \"0 0 0\"."
+
+   "@see setMoveDestination()\n")
+{
+   return object->getNav()->getMoveDestination();
+}
+
+DefineEngineMethod(AIController, setPathDestination, bool, (Point3F goal), ,
+   "@brief Tells the AI to find a path to the location provided\n\n"
+
+   "@param goal Coordinates in world space representing location to move to.\n"
+   "@return True if a path was found.\n\n"
+
+   "@see getPathDestination()\n"
+   "@see setMoveDestination()\n")
+{
+   return object->getNav()->setPathDestination(goal, true);
+}
+
+
+DefineEngineMethod(AIController, getPathDestination, Point3F, (), ,
+   "@brief Get the AIPlayer's current pathfinding destination.\n\n"
+
+   "@return Returns a point containing the \"x y z\" position "
+   "of the AIPlayer's current path destination. If no path destination "
+   "has yet been set, this returns \"0 0 0\"."
+
+   "@see setPathDestination()\n")
+{
+   return object->getNav()->getPathDestination();
+}
+
+DefineEngineMethod(AIController, followObject, void, (SimObjectId obj, F32 radius), ,
+   "@brief Tell the AIPlayer to follow another object.\n\n"
+
+   "@param obj ID of the object to follow.\n"
+   "@param radius Maximum distance we let the target escape to.")
+{
+   SceneObject* follow;
+#ifdef TORQUE_NAVIGATION_ENABLED
+   object->getNav()->clearPath();
+   object->clearCover();
+#endif
+   object->getNav()->clearFollow();
+
+   if (Sim::findObject(obj, follow))
+      object->getNav()->followObject(follow, radius);
+}
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+NavMesh* AINavigation::findNavMesh() const
+{
+   GameBase* gbo = dynamic_cast<GameBase*>(mControllerRef->getAIInfo()->mObj.getPointer());
+   // Search for NavMeshes that contain us entirely with the smallest possible
+   // volume.
+   NavMesh* mesh = NULL;
+   SimSet* set = NavMesh::getServerSet();
+   for (U32 i = 0; i < set->size(); i++)
+   {
+      NavMesh* m = static_cast<NavMesh*>(set->at(i));
+      if (m->getWorldBox().isContained(gbo->getWorldBox()))
+      {
+         // Check that mesh size is appropriate.
+         if (gbo->isMounted())
+         {
+            if (!m->mVehicles)
+               continue;
+         }
+         else
+         {
+            if ((getNavSize() == Small && !m->mSmallCharacters) ||
+               (getNavSize() == Regular && !m->mRegularCharacters) ||
+               (getNavSize() == Large && !m->mLargeCharacters))
+               continue;
+         }
+         if (!mesh || m->getWorldBox().getVolume() < mesh->getWorldBox().getVolume())
+            mesh = m;
+      }
+   }
+   return mesh;
+}
+
+void AINavigation::updateNavMesh()
+{
+   GameBase* gbo = dynamic_cast<GameBase*>(mControllerRef->getAIInfo()->mObj.getPointer());
+   NavMesh* old = mNavMesh;
+   if (mNavMesh.isNull())
+      mNavMesh = findNavMesh();
+   else
+   {
+      if (!mNavMesh->getWorldBox().isContained(gbo->getWorldBox()))
+         mNavMesh = findNavMesh();
+   }
+   // See if we need to update our path.
+   if (mNavMesh != old && !mPathData.path.isNull())
+   {
+      setPathDestination(mPathData.path->mTo);
+   }
+}
+
+void AINavigation::moveToNode(S32 node)
+{
+   if (mPathData.path.isNull())
+      return;
+
+   // -1 is shorthand for 'last path node'.
+   if (node == -1)
+      node = mPathData.path->size() - 1;
+
+   // Consider slowing down on the last path node.
+   setMoveDestination(mPathData.path->getNode(node), false);
+
+   // Check flags for this segment.
+   if (mPathData.index)
+   {
+      U16 flags = mPathData.path->getFlags(node - 1);
+      // Jump if we must.
+      if (flags & LedgeFlag)
+         mJump = Ledge;
+      else if (flags & JumpFlag)
+         mJump = Now;
+      else
+         // Catch pathing errors.
+         mJump = None;
+   }
+
+   // Store current index.
+   mPathData.index = node;
+}
+
+void AINavigation::repath()
+{
+   // Ineffectual if we don't have a path, or are using someone else's.
+   if (mPathData.path.isNull() || !mPathData.owned)
+      return;
+
+   if (mRandI(0, 100) < getCtrl()->mControllerData->mFlocking.mChance && flock())
+   {
+      mPathData.path->mTo = mMoveDestination;
+   }
+   else
+   {
+      // If we're following, get their position.
+      mPathData.path->mTo = getCtrl()->getGoal()->getPosition(true);
+   }
+
+   // Update from position and replan.
+   mPathData.path->mFrom = getCtrl()->getAIInfo()->getPosition(true);
+   mPathData.path->plan();
+
+   // Move to first node (skip start pos).
+   moveToNode(1);
+}
+
+
+void AINavigation::followNavPath(NavPath* path)
+{
+   // Get rid of our current path.
+   clearPath();
+   getCtrl()->clearCover();
+
+   // Follow new path.
+   mPathData.path = path;
+   mPathData.owned = false;
+   // Start from 0 since we might not already be there.
+   moveToNode(0);
+}
+
+void AINavigation::clearPath()
+{
+   // Only delete if we own the path.
+   if (!mPathData.path.isNull() && mPathData.owned)
+      mPathData.path->deleteObject();
+   // Reset path data.
+   mPathData = PathData();
+}
+
+bool AINavigation::flock()
+{
+   AIControllerData::Flocking flockingData = getCtrl()->mControllerData->mFlocking;
+   SimObjectPtr<SceneObject> obj = getCtrl()->getAIInfo()->mObj;
+
+   obj->disableCollision();
+   Point3F pos = obj->getBoxCenter();
+   Point3F searchArea = Point3F(flockingData.mMin / 2, flockingData.mMax / 2, getCtrl()->getAIInfo()->mObj->getObjBox().maxExtents.z / 2);
+
+   F32 maxFlocksq = flockingData.mMax * flockingData.mMax;
+   bool flocking = false;
+   U32 found = 0;
+   if (getCtrl()->getGoal())
+   {
+      Point3F dest = mMoveDestination;
+
+      if (getCtrl()->mMovement.mMoveState == AIController::ModeStuck)
+      {
+         Point3F shuffle = Point3F(mRandF() - 0.5, mRandF() - 0.5, 0);
+         shuffle.normalize();
+         dest += shuffle * flockingData.mMin;
+      }
+
+      dest.z = pos.z;
+      if ((pos - dest).len() > flockingData.mSideStep)
+      {
+         //find closest object
+         SimpleQueryList sql;
+         Box3F queryBox = Box3F(pos - searchArea, pos + searchArea);
+         obj->getContainer()->findObjects(queryBox, AIObjectType, SimpleQueryList::insertionCallback, &sql);
+         sql.mList.remove(obj);
+
+         Point3F avoidanceOffset = Point3F::Zero;
+
+         //avoid objects in the way
+         RayInfo info;
+         if (obj->getContainer()->castRay(pos, dest + Point3F(0, 0, obj->getObjBox().len_z() / 2), sAILoSMask, &info))
+         {
+            Point3F blockerOffset = (info.point - dest);
+            blockerOffset.z = 0;
+            avoidanceOffset += blockerOffset;
+         }
+
+         //avoid bots that are too close
+         for (U32 i = 0; i < sql.mList.size(); i++)
+         {
+            ShapeBase* other = dynamic_cast<ShapeBase*>(sql.mList[i]);
+            Point3F objectCenter = other->getBoxCenter();
+
+            F32 sumRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin;
+            F32 separation = getCtrl()->getAIInfo()->mRadius + other->getAIController()->getAIInfo()->mRadius;
+            sumRad += separation;
+
+            Point3F offset = (pos - objectCenter);
+            F32 offsetLensq = offset.lenSquared(); //square roots are expensive, so use squared val compares
+            if ((flockingData.mMin > 0) && (offsetLensq < (sumRad * sumRad)))
+            {
+               other->disableCollision();
+               if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask, &info))
+               {
+                  found++;
+                  offset.normalizeSafe();
+                  offset *= sumRad + separation;
+                  avoidanceOffset += offset; //accumulate total group, move away from that
+               }
+               other->enableCollision();
+            }
+         }
+         //if we don't have to worry about bumping into one another (nothing found lower than minFLock), see about grouping up
+         if (found == 0)
+         {
+            for (U32 i = 0; i < sql.mList.size(); i++)
+            {
+               ShapeBase* other = static_cast<ShapeBase*>(sql.mList[i]);
+               Point3F objectCenter = other->getBoxCenter();
+
+               F32 sumRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin;
+               F32 separation = getCtrl()->getAIInfo()->mRadius + other->getAIController()->getAIInfo()->mRadius;
+               sumRad += separation;
+
+               Point3F offset = (pos - objectCenter);
+               if ((flockingData.mMin > 0) && ((sumRad * sumRad) < (maxFlocksq)))
+               {
+                  other->disableCollision();
+                  if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask, &info))
+                  {
+                     found++;
+                     offset.normalizeSafe();
+                     offset *= sumRad + separation;
+                     avoidanceOffset -= offset; // subtract total group, move toward it
+                  }
+                  other->enableCollision();
+               }
+            }
+         }
+         if (found > 0)
+         {
+            avoidanceOffset.z = 0;
+            avoidanceOffset.x = (mRandF() * avoidanceOffset.x) * 0.5 + avoidanceOffset.x * 0.75;
+            avoidanceOffset.y = (mRandF() * avoidanceOffset.y) * 0.5 + avoidanceOffset.y * 0.75;
+            if (avoidanceOffset.lenSquared() < (maxFlocksq))
+            {
+               dest += avoidanceOffset;
+            }
+
+            //if we're not jumping...
+            if (mJump == None)
+            {
+               dest.z = obj->getPosition().z;
+               //make sure we don't run off a cliff
+               Point3F zlen(0, 0, getCtrl()->mControllerData->mHeightTolerance);
+               if (obj->getContainer()->castRay(dest + zlen, dest - zlen, TerrainObjectType | StaticShapeObjectType | StaticObjectType, &info))
+               {
+                  if ((mMoveDestination - dest).len() > getCtrl()->mControllerData->mMoveTolerance)
+                  {
+                     mMoveDestination = dest;
+                     flocking = true;
+                  }
+               }
+            }
+         }
+      }
+   }
+   obj->enableCollision();
+   return flocking;
+}
+
+
+DefineEngineMethod(AIController, followNavPath, void, (SimObjectId obj), ,
+   "@brief Tell the AIPlayer to follow a path.\n\n"
+
+   "@param obj ID of a NavPath object for the character to follow.")
+{
+   NavPath* path;
+   if (Sim::findObject(obj, path))
+      object->getNav()->followNavPath(path);
+}
+
+
+DefineEngineMethod(AIController, repath, void, (), ,
+   "@brief Tells the AI to re-plan its path. Does nothing if the character "
+   "has no path, or if it is following a mission path.\n\n")
+{
+   object->getNav()->repath();
+}
+
+DefineEngineMethod(AIController, findNavMesh, S32, (), ,
+   "@brief Get the NavMesh object this AIPlayer is currently using.\n\n"
+
+   "@return The ID of the NavPath object this character is using for "
+   "pathfinding. This is determined by the character's location, "
+   "navigation type and other factors. Returns -1 if no NavMesh is "
+   "found.")
+{
+   NavMesh* mesh = object->getNav()->getNavMesh();
+   return mesh ? mesh->getId() : -1;
+}
+
+DefineEngineMethod(AIController, getNavMesh, S32, (), ,
+   "@brief Return the NavMesh this AIPlayer is using to navigate.\n\n")
+{
+   NavMesh* m = object->getNav()->getNavMesh();
+   return m ? m->getId() : 0;
+}
+
+DefineEngineMethod(AIController, setNavSize, void, (const char* size), ,
+   "@brief Set the size of NavMesh this character uses. One of \"Small\", \"Regular\" or \"Large\".")
+{
+   if (!String::compare(size, "Small"))
+      object->getNav()->setNavSize(AINavigation::Small);
+   else if (!String::compare(size, "Regular"))
+      object->getNav()->setNavSize(AINavigation::Regular);
+   else if (!String::compare(size, "Large"))
+      object->getNav()->setNavSize(AINavigation::Large);
+   else
+      Con::errorf("AIPlayer::setNavSize: no such size '%s'.", size);
+}
+
+DefineEngineMethod(AIController, getNavSize, const char*, (), ,
+   "@brief Return the size of NavMesh this character uses for pathfinding.")
+{
+   switch (object->getNav()->getNavSize())
+   {
+   case AINavigation::Small:
+      return "Small";
+   case AINavigation::Regular:
+      return "Regular";
+   case AINavigation::Large:
+      return "Large";
+   }
+   return "";
+}
+#endif

+ 105 - 0
Engine/source/T3D/AI/AINavigation.h

@@ -0,0 +1,105 @@
+//-----------------------------------------------------------------------------
+// Copyright (c) 2012 GarageGames, LLC
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to
+// deal in the Software without restriction, including without limitation the
+// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+// sell copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+// IN THE SOFTWARE.
+//-----------------------------------------------------------------------------
+#ifndef _AINAVIGATION_H_
+#define _AINAVIGATION_H_
+
+#include "AIInfo.h"
+
+#include "navigation/navPath.h"
+#include "navigation/navMesh.h"
+
+class AIController;
+struct AINavigation
+{
+   AIController* mControllerRef;
+   AIController* getCtrl() { return mControllerRef; };
+
+   AINavigation() = delete;
+   AINavigation(AIController* controller);
+   ~AINavigation();
+   Point3F mMoveDestination;
+   void setMoveDestination(const Point3F& location, bool slowdown);
+   Point3F getMoveDestination() const { return mMoveDestination; };
+   bool setPathDestination(const Point3F& pos, bool replace = false);
+   Point3F getPathDestination() const;
+
+   void onReachDestination();
+
+   void followObject();
+   void followObject(SceneObject* obj, F32 radius);
+   void clearFollow();
+
+#ifdef TORQUE_NAVIGATION_ENABLED
+   /// Stores information about a path.
+   struct PathData {
+      /// Pointer to path object.
+      SimObjectPtr<NavPath> path;
+      /// Do we own our path? If so, we will delete it when finished.
+      bool owned;
+      /// Path node we're at.
+      U32 index;
+      /// Default constructor.
+      PathData() : path(NULL)
+      {
+         owned = false;
+         index = 0;
+      }
+   };
+
+   /// Should we jump?
+   enum JumpStates {
+      None,  ///< No, don't jump.
+      Now,   ///< Jump immediately.
+      Ledge, ///< Jump when we walk off a ledge.
+   };
+
+   enum NavSize {
+      Small,
+      Regular,
+      Large
+   } mNavSize;
+   void setNavSize(NavSize size) { mNavSize = size; updateNavMesh(); }
+   NavSize getNavSize() const { return mNavSize; }
+
+   /// NavMesh we pathfind on.
+   SimObjectPtr<NavMesh> mNavMesh;
+   NavMesh* findNavMesh() const;
+   void updateNavMesh();
+   NavMesh* getNavMesh() const { return mNavMesh; }
+   PathData mPathData;
+   JumpStates mJump;
+
+   /// Clear out the current path.
+   void clearPath();
+   void repath();
+
+   /// Get the current path we're following.
+   SimObjectPtr<NavPath> getPath() { return mPathData.path; };
+   void followNavPath(NavPath* path);
+
+   /// Move to the specified node in the current path.
+   void moveToNode(S32 node);
+   bool flock();
+#endif
+};
+
+#endif

+ 1 - 0
Engine/source/T3D/gameFunctions.cpp

@@ -669,6 +669,7 @@ static void RegisterGameFunctions()
    Con::setIntVariable("$TypeMasks::PathShapeObjectType",     PathShapeObjectType);
 // PATHSHAPE END
    Con::setIntVariable("$TypeMasks::TurretObjectType", TurretObjectType);
+   Con::setIntVariable("$TypeMasks::AIObjectType", AIObjectType);
 
    Con::addVariable("Ease::InOut", TypeS32, &gEaseInOut, 
       "InOut ease for curve movement.\n"

+ 1 - 1
Engine/source/T3D/objectTypes.h

@@ -174,7 +174,7 @@ enum SceneObjectTypes
    /// @see TurretShape
    TurretObjectType = BIT(29),
    N_A_31 = BIT(30),
-   N_A_32 = BIT(31),
+   AIObjectType = BIT(31),
 
    /// @}
 };

+ 1 - 10
Engine/source/T3D/player.cpp

@@ -461,7 +461,6 @@ PlayerData::PlayerData()
 
    physicsPlayerType = StringTable->EmptyString();
    mControlMap = StringTable->EmptyString();
-
    dMemset( actionList, 0, sizeof(actionList) );
 }
 
@@ -740,7 +739,7 @@ void PlayerData::initPersistFields()
    endGroup( "Camera" );
 
    addGroup( "Movement" );
-   addField("controlMap", TypeString, Offset(mControlMap, PlayerData),
+      addField("controlMap", TypeString, Offset(mControlMap, PlayerData),
       "@brief movemap used by these types of objects.\n\n");
    
       addFieldV( "maxStepHeight", TypeRangedF32, Offset(maxStepHeight, PlayerData), &CommonValidators::PositiveFloat,
@@ -1643,7 +1642,6 @@ Player::Player()
    mLastAbsoluteYaw = 0.0f;
    mLastAbsolutePitch = 0.0f;
    mLastAbsoluteRoll = 0.0f;
-   
    afx_init();
 }
 
@@ -1741,7 +1739,6 @@ bool Player::onAdd()
                            world );
       mPhysicsRep->setTransform( getTransform() );
    }
-
    return true;
 }
 
@@ -2258,12 +2255,6 @@ void Player::advanceTime(F32 dt)
       }
    }
 }
-
-bool Player::getAIMove(Move* move)
-{
-   return false;
-}
-
 void Player::setState(ActionState state, U32 recoverTicks)
 {
    if (state != mState) {

+ 6 - 2
Engine/source/T3D/player.h

@@ -50,6 +50,12 @@ class Player;
 class OpenVRTrackedObject;
 #endif
 
+#ifdef TORQUE_NAVIGATION_ENABLED
+#include "navigation/navPath.h"
+#include "navigation/navMesh.h"
+#include "navigation/coverPoint.h"
+#endif // TORQUE_NAVIGATION_ENABLED
+
 //----------------------------------------------------------------------------
 
 struct PlayerData: public ShapeBaseData {
@@ -758,8 +764,6 @@ public:
    Point3F getMomentum() const override;
    void    setMomentum(const Point3F &momentum) override;
    bool    displaceObject(const Point3F& displaceVector) override;
-   virtual bool    getAIMove(Move*);
-
    bool checkDismountPosition(const MatrixF& oldPos, const MatrixF& newPos);  ///< Is it safe to dismount here?
 
    //

+ 51 - 2
Engine/source/T3D/shapeBase.cpp

@@ -69,6 +69,7 @@
 #include "core/stream/fileStream.h"
 #include "T3D/accumulationVolume.h"
 #include "console/persistenceManager.h"
+#include "AI/AIController.h"
 
 IMPLEMENT_CO_DATABLOCK_V1(ShapeBaseData);
 
@@ -197,7 +198,8 @@ ShapeBaseData::ShapeBaseData()
    useEyePoint( false ),
    isInvincible( false ),
    renderWhenDestroyed( true ),
-   inheritEnergyFromMount( false )
+   inheritEnergyFromMount( false ),
+   mAIControllData(NULL)
 {
    INIT_ASSET(Shape);
    INIT_ASSET(DebrisShape);
@@ -548,6 +550,10 @@ void ShapeBaseData::initPersistFields()
       addField("silentBBoxValidation", TypeBool, Offset(silent_bbox_check, ShapeBaseData));
       INITPERSISTFIELD_SHAPEASSET(DebrisShape, ShapeBaseData, "The shape asset to use for auto-generated breakups via blowup(). @note may not be functional.");
    endGroup( "Shapes" );
+   addGroup("Movement");
+      addField("aiControllerData", TYPEID< AIControllerData >(), Offset(mAIControllData, ShapeBaseData),
+      "@brief ai controller used by these types of objects.\n\n");
+   endGroup("Movement");
 
    addGroup("Particle Effects");
       addField( "explosion", TYPEID< ExplosionData >(), Offset(explosion, ShapeBaseData),
@@ -999,7 +1005,8 @@ ShapeBase::ShapeBase()
    mCameraFov( 90.0f ),
    mIsControlled( false ),
    mLastRenderFrame( 0 ),
-   mLastRenderDistance( 0.0f )
+   mLastRenderDistance( 0.0f ),
+   mAIController(NULL)
 {
    mTypeMask |= ShapeBaseObjectType | LightObjectType;   
 
@@ -1050,6 +1057,7 @@ ShapeBase::~ShapeBase()
       cur->next = sFreeTimeoutList;
       sFreeTimeoutList = cur;
    }
+   if (mAIController) mAIController->deleteObject();
 }
 
 void ShapeBase::initPersistFields()
@@ -5467,3 +5475,44 @@ DefineEngineMethod(ShapeBase, getNodePoint, Point3F, (const char* nodeName), ,
 
    return pos;
 }
+
+bool ShapeBase::setAIController(SimObjectId controller)
+{
+   if (Sim::findObject(controller, mAIController) && mAIController->mControllerData)
+   {
+      mAIController->setAIInfo(this);
+      mTypeMask |= AIObjectType;
+      return true;
+   }
+   Con::errorf("unable to find AIController : %i", controller);
+   mAIController = NULL;
+   mTypeMask |= ~AIObjectType;
+   return false;
+}
+
+bool ShapeBase::getAIMove(Move* move)
+{
+   if (!isServerObject()) return false;
+   if (isControlled()) return false; //something else is steering us, so use that one's controller
+   if (!(mTypeMask & VehicleObjectType || mTypeMask & PlayerObjectType)) return false; //only support players and vehicles for now
+   if (mAIController)
+   {
+      mAIController->getAIMove(move); //actual result
+      mTypeMask |= AIObjectType;
+      return true;
+   }
+   mAIController = NULL;
+   mTypeMask &= ~AIObjectType;
+   return false;
+}
+
+
+DefineEngineMethod(ShapeBase, setAIController, bool, (S32 controller), , "")
+{
+   return object->setAIController(controller);
+}
+
+DefineEngineMethod(ShapeBase, getAIController, AIController*, (), , "")
+{
+   return object->getAIController();
+}

+ 10 - 2
Engine/source/T3D/shapeBase.h

@@ -88,6 +88,8 @@ class ShapeBase;
 class SFXSource;
 class SFXTrack;
 class SFXProfile;
+struct AIController;
+struct AIControllerData;
 
 typedef void* Light;
 
@@ -560,6 +562,7 @@ public:
    U32 cubeDescId;
    ReflectorDesc *reflectorDesc;
 
+   AIControllerData* mAIControllData;
    /// @name Destruction
    ///
    /// Everyone likes to blow things up!
@@ -1761,6 +1764,11 @@ public:
    /// Returns true if this object is controlling by something
    bool isControlled() { return(mIsControlled); }
 
+   AIController* mAIController;
+   bool setAIController(SimObjectId controller);
+   AIController* getAIController() { return mAIController; };
+   virtual bool getAIMove(Move* move);
+
    /// Returns true if this object is being used as a camera in first person
    bool isFirstPerson() const;
 
@@ -1902,7 +1910,6 @@ public:
    void   registerCollisionCallback(CollisionEventCallback*);
    void   unregisterCollisionCallback(CollisionEventCallback*);
 
-protected:
    enum { 
       ANIM_OVERRIDDEN     = BIT(0),
       BLOCK_USER_CONTROL  = BIT(1),
@@ -1910,6 +1917,8 @@ protected:
       BAD_ANIM_ID         = 999999999,
       BLENDED_CLIP        = 0x80000000,
    };
+   U8 anim_clip_flags;
+protected:
    struct BlendThread
    {
       TSThread* thread;
@@ -1917,7 +1926,6 @@ protected:
    };
    Vector<BlendThread> blend_clips;
    static U32 unique_anim_tag_counter;
-   U8 anim_clip_flags;
    S32 last_anim_id;
    U32 last_anim_tag;
    U32 last_anim_lock_tag;

+ 13 - 1
Engine/source/T3D/vehicles/vehicle.cpp

@@ -148,6 +148,7 @@ VehicleData::VehicleData()
    collDamageThresholdVel = 20;
    collDamageMultiplier = 0.05f;
    enablePhysicsRep = true;
+   mControlMap = StringTable->EmptyString();
 }
 
 
@@ -321,6 +322,11 @@ void VehicleData::initPersistFields()
       "velocity).\n\nCurrently unused." );
    endGroup("Collision");
 
+   addGroup("Movement");
+   addField("controlMap", TypeString, Offset(mControlMap, VehicleData),
+      "@brief movemap used by these types of objects.\n\n");
+   endGroup("Movement");
+
    addGroup("Steering");
    addField("controlMap", TypeString, Offset(mControlMap, VehicleData),
       "@brief movemap used by these types of objects.\n\n");
@@ -474,7 +480,6 @@ bool Vehicle::onAdd()
 void Vehicle::onRemove()
 {
    SAFE_DELETE(mPhysicsRep);
-
    U32 i=0;
 
    for( i=0; i<VehicleData::VC_NUM_DAMAGE_EMITTERS; i++ )
@@ -499,10 +504,17 @@ void Vehicle::processTick(const Move* move)
 {
    PROFILE_SCOPE( Vehicle_ProcessTick );
 
+   // If we're not being controlled by a client, let the
+   // AI sub-module get a chance at producing a move.
+   Move aiMove;
+   if (!move && isServerObject() && getAIMove(&aiMove))
+      move = &aiMove;
+
    ShapeBase::processTick(move);
    if ( isMounted() )
       return;
 
+
    // Warp to catch up to server
    if (mDelta.warpCount < mDelta.warpTicks)
    {

+ 6 - 2
Engine/source/T3D/vehicles/vehicle.h

@@ -27,6 +27,8 @@
 #include "T3D/rigidShape.h"
 #endif
 
+#include "T3D/AI/AIController.h"
+
 class ParticleEmitter;
 class ParticleEmitterData;
 class ClippedPolyList;
@@ -69,8 +71,8 @@ struct VehicleData : public RigidShapeData
    F32 damageLevelTolerance[ VC_NUM_DAMAGE_LEVELS ];
    F32 numDmgEmitterAreas;
 
-   StringTableEntry mControlMap;
    bool enablePhysicsRep;
+   StringTableEntry mControlMap;
 
    //
    VehicleData();
@@ -99,7 +101,6 @@ class Vehicle : public RigidShape
    Point2F mSteering;
    F32 mThrottle;
    bool mJetting;
-
    GFXStateBlockRef  mSolidSB;
 
    SimObjectPtr<ParticleEmitter> mDamageEmitterList[VehicleData::VC_NUM_DAMAGE_EMITTERS];
@@ -147,6 +148,9 @@ public:
    bool onAdd() override;
    void onRemove() override;
 
+   Point2F getSteering() { return mSteering; };
+   F32 getThrottle() { return mThrottle;};
+
    /// Interpolates between move ticks @see processTick
    /// @param   dt   Change in time between the last call and this call to the function
    void advanceTime(F32 dt) override;

+ 1 - 1
Engine/source/console/simObject.cpp

@@ -82,7 +82,7 @@ ImplementBitfieldType(GameTypeMasksType,
 { SceneObjectTypes::PathShapeObjectType, "$TypeMasks::PathShapeObjectType", "Path-following Objects.\n" },
 { SceneObjectTypes::TurretObjectType, "$TypeMasks::TurretObjectType", "Turret Objects.\n" },
 { SceneObjectTypes::N_A_31, "$TypeMasks::N_A_31", "unused 31st bit.\n" },
-{ SceneObjectTypes::N_A_32, "$TypeMasks::N_A_32", "unused 32nd bit.\n" },
+{ SceneObjectTypes::AIObjectType, "$TypeMasks::AIObjectType", "AIObjectType.\n" },
 
 EndImplementBitfieldType;
 

+ 72 - 10
Engine/source/navigation/guiNavEditorCtrl.cpp

@@ -37,6 +37,7 @@
 #include "gui/buttons/guiButtonCtrl.h"
 #include "gui/worldEditor/undoActions.h"
 #include "T3D/gameBase/gameConnection.h"
+#include "T3D/AI/AIController.h"
 
 IMPLEMENT_CONOBJECT(GuiNavEditorCtrl);
 
@@ -225,8 +226,29 @@ void GuiNavEditorCtrl::spawnPlayer(const Point3F &pos)
          SimGroup* missionCleanup = dynamic_cast<SimGroup*>(cleanup);
          missionCleanup->addObject(obj);
       }
-      mPlayer = static_cast<AIPlayer*>(obj);
-      Con::executef(this, "onPlayerSelected", Con::getIntArg(mPlayer->mLinkTypes.getFlags()));
+      mPlayer = obj;
+#ifdef TORQUE_NAVIGATION_ENABLED
+      AIPlayer* asAIPlayer = dynamic_cast<AIPlayer*>(obj);
+      if (asAIPlayer) //try direct
+      {
+         Con::executef(this, "onPlayerSelected", Con::getIntArg(asAIPlayer->mLinkTypes.getFlags()));
+      }
+      else
+      {
+         ShapeBase* sbo = dynamic_cast<ShapeBase*>(obj);
+         if (sbo->getAIController())
+         {
+            if (sbo->getAIController()->mControllerData)
+               Con::executef(this, "onPlayerSelected", Con::getIntArg(sbo->getAIController()->mControllerData->mLinkTypes.getFlags()));
+         }
+         else
+         {
+#endif
+            Con::executef(this, "onPlayerSelected");
+#ifdef TORQUE_NAVIGATION_ENABLED
+         }
+      }
+#endif
    }
 }
 
@@ -383,16 +405,56 @@ void GuiNavEditorCtrl::on3DMouseDown(const Gui3DMouseEvent & event)
       // Select/move character
       else
       {
-         if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType, &ri))
+         if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType | VehicleObjectType, &ri))
          {
-            if(dynamic_cast<AIPlayer*>(ri.object))
+            if(ri.object)
             {
-               mPlayer = dynamic_cast<AIPlayer*>(ri.object);
-               Con::executef(this, "onPlayerSelected", Con::getIntArg(mPlayer->mLinkTypes.getFlags()));
+               mPlayer = ri.object;
+#ifdef TORQUE_NAVIGATION_ENABLED
+               AIPlayer* asAIPlayer = dynamic_cast<AIPlayer*>(mPlayer.getPointer());
+               if (asAIPlayer) //try direct
+               {
+                  Con::executef(this, "onPlayerSelected", Con::getIntArg(asAIPlayer->mLinkTypes.getFlags()));
+               }
+               else
+               {
+                  ShapeBase* sbo = dynamic_cast<ShapeBase*>(mPlayer.getPointer());
+                  if (sbo->getAIController())
+                  {
+                     if (sbo->getAIController()->mControllerData)
+                        Con::executef(this, "onPlayerSelected", Con::getIntArg(sbo->getAIController()->mControllerData->mLinkTypes.getFlags()));
+                  }
+                  else
+                  {
+#endif
+                     Con::executef(this, "onPlayerSelected");
+                  }
+#ifdef TORQUE_NAVIGATION_ENABLED
+               }
+            }
+#endif
+         }
+         else if (!mPlayer.isNull() && gServerContainer.castRay(startPnt, endPnt, StaticObjectType, &ri))
+         {
+            AIPlayer* asAIPlayer = dynamic_cast<AIPlayer*>(mPlayer.getPointer());
+            if (asAIPlayer) //try direct
+            {
+#ifdef TORQUE_NAVIGATION_ENABLED
+               asAIPlayer->setPathDestination(ri.point);
+#else
+                asAIPlayer->setMoveDestination(ri.point,false);
+#endif
+            }
+            else
+            {
+               ShapeBase* sbo = dynamic_cast<ShapeBase*>(mPlayer.getPointer());
+               if (sbo->getAIController())
+               {
+                  if (sbo->getAIController()->mControllerData)
+                     sbo->getAIController()->getNav()->setPathDestination(ri.point, true);
+               }
             }
          }
-         else if(!mPlayer.isNull() && gServerContainer.castRay(startPnt, endPnt, StaticObjectType, &ri))
-            mPlayer->setPathDestination(ri.point);
       }
    }
 }
@@ -455,8 +517,8 @@ void GuiNavEditorCtrl::on3DMouseMove(const Gui3DMouseEvent & event)
 
    if(mMode == mTestMode)
    {
-      if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType, &ri))
-         mCurPlayer = dynamic_cast<AIPlayer*>(ri.object);
+      if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType | VehicleObjectType, &ri))
+         mCurPlayer = ri.object;
       else
          mCurPlayer = NULL;
    }

+ 2 - 2
Engine/source/navigation/guiNavEditorCtrl.h

@@ -155,8 +155,8 @@ protected:
    /// @name Test mode
    /// @{
 
-   SimObjectPtr<AIPlayer> mPlayer;
-   SimObjectPtr<AIPlayer> mCurPlayer;
+   SimObjectPtr<SceneObject> mPlayer;
+   SimObjectPtr<SceneObject> mCurPlayer;
 
    /// @}
 

+ 17 - 0
Templates/BaseGame/game/core/gameObjects/datablocks/defaultDatablocks.tscript

@@ -169,3 +169,20 @@ datablock LightAnimData( SpinLightAnim )
    rotKeys[2] = "az";
    rotSmooth[2] = true;
 };
+
+datablock AIPlayerControllerData( aiPlayerControl )
+{
+    moveTolerance = 0.25; followTolerance = 1.0; mAttackRadius = 2;
+};
+
+datablock AIWheeledVehicleControllerData( aiCarControl )
+{
+    moveTolerance = 1.0; followTolerance = 2.0; mAttackRadius = 5.0;
+};
+
+datablock AIFlyingVehicleControllerData( aiPlaneControl )
+{
+    moveTolerance = 2.0; followTolerance = 5.0; mAttackRadius = 10.0;
+    FlightFloor = 15; FlightCeiling = 150;
+};
+

+ 1 - 1
Templates/BaseGame/game/tools/navEditor/NavEditorGui.gui

@@ -485,7 +485,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
                   VertSizing = "bottom";
                   Extent = "90 18";
                   text = "Stop";
-                  command = "NavEditorGui.getPlayer().stop();";
+                  command = "NavEditorGui.stop();";
                };
             };
          };

+ 2 - 1
Templates/BaseGame/game/tools/navEditor/main.tscript

@@ -87,7 +87,7 @@ function NavEditorPlugin::onWorldEditorStartup(%this)
 
    // Add ourselves to the Editor Settings window.
    exec("./NavEditorSettingsTab.gui");
-   //ESettingsWindow.addTabPage(ENavEditorSettingsPage);
+   ESettingsWindow.addTabPage(ENavEditorSettingsPage);
    ENavEditorSettingsPage.init();
 
    // Add items to World Editor Creator
@@ -104,6 +104,7 @@ function ENavEditorSettingsPage::init(%this)
 {
    // Initialises the settings controls in the settings dialog box.
    %this-->SpawnClassOptions.clear();
+   %this-->SpawnClassOptions.add("Player");
    %this-->SpawnClassOptions.add("AIPlayer");
    %this-->SpawnClassOptions.setFirstSelected();
 }

+ 19 - 2
Templates/BaseGame/game/tools/navEditor/navEditor.tscript

@@ -459,6 +459,14 @@ function NavEditorGui::onLinkSelected(%this, %flags)
 
 function NavEditorGui::onPlayerSelected(%this, %flags)
 {
+   if (!isObject(%this.getPlayer().aiController) && (!(%this.getPlayer().isMemberOfClass("AIPlayer"))))
+   {
+      %this.getPlayer().aiController = new AIController(){ ControllerData = %this.getPlayer().getDatablock().aiControllerData; };
+      %this.getPlayer().setAIController(%this.getPlayer().aiController);
+   }
+   NavMeshIgnore(%this.getPlayer(), true);
+   %this.getPlayer().setDamageState("Enabled");
+   
    updateLinkData(NavEditorOptionsWindow-->TestProperties, %flags);
 }
 
@@ -526,7 +534,7 @@ function NavEditorGui::findCover(%this)
       %text = NavEditorOptionsWindow-->TestProperties->CoverPosition.getText();
       if(%text !$= "")
          %pos = eval("return " @ %text);
-      %this.getPlayer().findCover(%pos, NavEditorOptionsWindow-->TestProperties->CoverRadius.getText());
+      %this.getPlayer().getAIController().findCover(%pos, NavEditorOptionsWindow-->TestProperties->CoverRadius.getText());
    }
 }
 
@@ -547,10 +555,19 @@ function NavEditorGui::followObject(%this)
             toolsMessageBoxOk("Error", "Cannot find object" SPC %text);
       }
       if(isObject(%obj))
-         %this.getPlayer().followObject(%obj, NavEditorOptionsWindow-->TestProperties->FollowRadius.getText());
+         %this.getPlayer().getAIController().followObject(%obj, NavEditorOptionsWindow-->TestProperties->FollowRadius.getText());
    }
 }
 
+function NavEditorGui::stop(%this)
+{
+    if (isObject(%this.getPlayer().aiController))
+        %this.getPlayer().aiController.stop();
+    else
+    {
+        NavEditorGui.getPlayer().stop();
+    }
+}
 function NavInspector::inspect(%this, %obj)
 {
    %name = "";