Vehicle following navpath.

There are no stupid questions, just stupid answers.
9 posts Page 1 of 1
flysouth
Posts: 105
Joined: Sun May 15, 2016 10:16 am
by flysouth » Tue Jul 12, 2016 3:33 pm
Looking at the navmesh documentation seems to indicate that the methods have been implemented in the AiPlayer class. What happens if I want an Ai controlled hover vehicle that can follow nav paths for example?
All sane suggestions will be apreciated. :mrgreen:
flysouth
Posts: 105
Joined: Sun May 15, 2016 10:16 am
by flysouth » Wed Jul 13, 2016 11:15 pm
anybody know?
Steve_Yorkshire
Posts: 345
Joined: Tue Feb 03, 2015 10:30 pm
 
by Steve_Yorkshire » Thu Jul 14, 2016 3:24 am
AIPlayer uses the navmesh so you'd mount them on a vehicle as standard. AI should attempt to follow the route, though they won't understand steering arcs so are likely to miss waypoints.
flysouth
Posts: 105
Joined: Sun May 15, 2016 10:16 am
by flysouth » Thu Jul 14, 2016 11:02 am
Thanks for the suggestion.
Jason Campbell
Posts: 362
Joined: Fri Feb 13, 2015 2:51 am
 
by Jason Campbell » Thu Jul 14, 2016 8:17 pm
There was an AIWheeledVehicle resource over at GG but the code link is dead. You would have to re-write the AI code because as soon as stock AI enters a vehicle they drive around like a madman.
Azaezel
DEVGRU
Posts: 496
Joined: Tue Feb 03, 2015 9:50 pm
 
by Azaezel » Thu Jul 14, 2016 9:22 pm
This will need conversion work but here's a direct extract from one of our old games for folks to play with based on the resource Jason mentioned:
PT1 - strategic AI (shared across the racers so they all get more agressive the better folks do. 10-1 skippable, but there's references)

Code: Select all

#ifndef _AIStrategy_H_ #define _AIStrategy_H_ #ifndef _GAMEBASE_H_ #include "console/simBase.h" #endif #include "console/console.h" #include "console/consoleTypes.h" #include "core/bitStream.h" class SAI : public SimObject { typedef SimObject Parent; public: F32 mAgression; F32 mPrecision; SAI(); ~SAI(); bool onAdd(); void onRemove(); DECLARE_CONOBJECT(SAI); static void initPersistFields(); }; DECLARE_CONSOLETYPE(SAI) #endif

Code: Select all

#include "AIStrategy.h" IMPLEMENT_CONOBJECT(SAI); SAI::SAI() { mAgression = 0.1; mPrecision = 0.1; } SAI::~SAI() { } void SAI::initPersistFields() { addField("Agression", TypeF32, Offset(mAgression, SAI)); addField("Precision", TypeF32, Offset(mPrecision, SAI)); } bool SAI::onAdd() { if(!Parent::onAdd()) return false; return true; } void SAI::onRemove() { Parent::onRemove(); }
Pt 2- our lil bot-heads

Code: Select all

// AIWheeledVehicleBrain.h // Defines a wheeled vehicle that is driven by AI #ifndef _AIWheeledVehicleBrain_h #define _AIWheeledVehicleBrain_h #ifndef _AIStrategy_H_ #include "game/AI/AIStrategy.h" #endif #ifndef _WHEELEDVEHICLE_H_ #include "game/vehicles/wheeledVehicle.h" #endif #include "stdio.h" class PathPoint { public: Point3F m_Point; float m_desiredSpeed; }; class AIWheeledVehicleBrain : public WheeledVehicle { typedef WheeledVehicle Parent; public: enum MoveState { ModeStop, ModeMove, ModeStuck, ModeReverse, }; enum DrivingState { SteerNull, Left, Right, Straight, TurnAround }; enum FeelerState { FrontFeeler, LeftFeeler, RightFeeler, Open, }; protected: //vehicle info Point3F mVehiclePos; F32 mCurrentSpeed; MoveState mMoveState; F32 mMoveSpeed; F32 mMoveTolerance; // Distance from destination before we stop Point3F mMoveDestination; // Destination for movement Point3F mLastLocation; // For stuck check bool mbBetweenLines; F32 mTurnAngle; S32 mCurrentNode; S32 mLastNode; bool mbInit; int mTargetNode; F32 mMovecount; Point3F mstartPos; F32 mDistance; Point3F mIntersection; VectorF mRacelinedir; VectorF mVehicledir; Vector<PathPoint> mPath; Vector<F32> mvecStop; F32 followRacingLine(); int CalcPrevNode(int node); int CalcNextNode(int node); F32 mStuckTolerance; //minimal motion before were considered stuck S32 mMaxStuckCount; //max cycles before stuck turns into totally stuck S32 mStuckCycles; //cycles we've been stuck S32 mStuckCount; //times we've exceeded max stuck cycles allowable F32 mLostDistance; //maximum distance before were considered lost F32 mPrecision; bool mstart; void SetLastNode(); // Utility Methods void throwCallback( const char *name ); virtual bool getAIMove(Move* move); public: AIWheeledVehicleBrain(); void setMoveTolerance( const F32 tolerance ); F32 getMoveTolerance() const { return mMoveTolerance; } void setMoveDestination( const Point3F &location ); Point3F getMoveDestination() const { return mMoveDestination; } F32 avoidCollisions(void); void addPathPoint( const Point3F &location, const float &speed); void setStartNode(int startnode); void BuildStopVector(); void InitAI(); static void initPersistFields(); F32 getDistanceToNode(U32 node); void updateVehicleData(); F32 getMaxTurnAngle(F32 currentSpeed); F32 getSlowDownDistance(F32 currentspeed,F32 desiredspeed); F32 TurnToRaceLine(Point3F vehicledir,Point3F racelinedir,F32 maxTurnAngle); // Steering DrivingState steerState; F32 mLastSteered; F32 getSteeringAngle(); virtual bool onNewSAI(SAI* dptr); SAI* mSAI; bool setSAI(SAI* dptr); SAI* getSAI() { return mSAI; } F32 mAvoidanceTick; bool mAvoiding; DECLARE_CONOBJECT(AIWheeledVehicleBrain); }; bool DistancePointToLine( Point3F Point, Point3F LineStart, Point3F LineEnd, F32 &distance, Point3F &Intersection ); #endif

Code: Select all

#include "AIWheeledVehicleBrain.h" #include "math/mMatrix.h" #include "math/mPoint.h" #include "core/realComp.h" //TWEAKABLE constants #define STOPSAFETY 15 //How we pad the stop distance value to make sure car can stop in time #define TURNANGLE 0.5 #define LOSTDISTANCE 30 //When vehicle is this far treat as lost #define MAXLOSTSPEED 20 //Max speed when lost IMPLEMENT_CO_NETOBJECT_V1(AIWheeledVehicleBrain); AIWheeledVehicleBrain::AIWheeledVehicleBrain() : WheeledVehicle() { mMoveDestination.set( 0.0f, 0.0f, 0.0f ); mMoveSpeed = 1.0f; mMoveTolerance = 0.25f; mbInit = false; mDistance = 1e20f; mMovecount = 0; mCurrentNode = 0; mStuckCount = 0; mMaxStuckCount = 50; mStuckCycles = 0; mLostDistance = LOSTDISTANCE; mPrecision = 20; mStuckTolerance = 0.01f; mstart = false; mAvoidanceTick = 2; mAvoiding = false; } void AIWheeledVehicleBrain::initPersistFields() { Parent::initPersistFields(); addField("StuckTolerance",TypeF32,Offset(mStuckTolerance,AIWheeledVehicleBrain)); addField("MaxStuckCount",TypeS32,Offset(mMaxStuckCount,AIWheeledVehicleBrain)); addField("LostDistance",TypeF32,Offset(mLostDistance,AIWheeledVehicleBrain)); addField("Precision",TypeF32,Offset(mPrecision,AIWheeledVehicleBrain)); addField("AvoidanceTick",TypeF32,Offset(mAvoidanceTick,AIWheeledVehicleBrain)); } /** * Sets how far away from the move location is considered * "on target" * * @param tolerance Movement tolerance for error */ void AIWheeledVehicleBrain::setMoveTolerance( const F32 tolerance ) { mMoveTolerance = getMax( 0.1f, tolerance ); } void AIWheeledVehicleBrain::addPathPoint( const Point3F &location, const float &speed) { PathPoint v; v.m_Point = location; v.m_desiredSpeed = speed; mPath.push_back(v); } void AIWheeledVehicleBrain::InitAI() { BuildStopVector(); mbInit = true; return; } /* Adjust this function for the performance of your vehicle mvecStop is a vector that store the stop distance need by the vehicle at a certain speed */ void AIWheeledVehicleBrain::BuildStopVector() { int maxSpeed = 250; F32 factor = 0.05f; F32 value = 0; for (int i=0;i<maxSpeed;i++) { value = (i*i)*factor; mvecStop.push_back(value); } } /** * Sets the location for the bot to run to * * @param location Point to run to */ void AIWheeledVehicleBrain::setMoveDestination( const Point3F &location) { mMoveDestination = location; mMoveState = ModeMove; } //Try to have vehicle follow racing line / Path F32 AIWheeledVehicleBrain::followRacingLine() { Point3F v1 = mIntersection-mVehiclePos; v1.z = 0; Point3F v2 = mIntersection-mPath[mCurrentNode].m_Point; v2.z = 0; v1.normalize(); v2.normalize(); Point3F lv = mCross(v1,v2); //Use cross product to figure out if we are left or right of racing line if (lv.z<0) { //vehicle is left of racing line return TurnToRaceLine(mVehicledir,mRacelinedir,TURNANGLE); } else { //vehicle is right of racing line return TurnToRaceLine(mVehicledir,mRacelinedir,-TURNANGLE); } return 0; } F32 AIWheeledVehicleBrain::TurnToRaceLine(Point3F vehicledir,Point3F racelinedir,F32 maxTurnAngle) { Point3F desiredDir; F32 ratio = mDistance/10; if (ratio>1) ratio = 1; else if (ratio<(1/10)) ratio = 0; //Square this value to have a smaller turn angle as we get closer to the racing line ratio =ratio *ratio; F32 turnAngle = ratio*maxTurnAngle; F32 targetDistance = getDistanceToNode(mTargetNode); if (targetDistance<mPrecision) { //Just aim right for the node instead of following the racing line desiredDir = mPath[mTargetNode].m_Point - mVehiclePos; desiredDir.normalize(); //skip turning //return 0.0; } else { //rotate about the z axis so that we will turn twords racing line desiredDir.x = racelinedir.x*cos(turnAngle)+racelinedir.y*sin(turnAngle); desiredDir.y = racelinedir.x*-sin(turnAngle)+racelinedir.y*cos(turnAngle); desiredDir.z = 0; desiredDir.normalize(); } F32 dot = mDot(desiredDir,vehicledir); F32 turn; //This section of code could use some improvement //Try to have vehicledir match desiredDir //Don't turn if (dot>0.999) turn = 0; else if (dot>0.995f) turn = 0.2f; else if (dot>0.98f) turn = 0.4f; else if (dot>0.97f) turn = 0.6f; else if (dot>0.96f) turn = 0.8f; else turn = 1; F32 cz = desiredDir.x*vehicledir.y - desiredDir.y*vehicledir.x; if (cz<0) turn *=-1; return turn; } /** Calculates the max turn angle based on current speed that won't flip the vehicle currently does nothing */ F32 AIWheeledVehicleBrain::getMaxTurnAngle(F32 currentSpeed) { F32 maxTurnAngle = 50; return maxTurnAngle; } /** * Calculates the distance needed to slow down to a desired speed * */ F32 AIWheeledVehicleBrain::getSlowDownDistance(F32 currentspeed,F32 desiredspeed) { if (currentspeed<=desiredspeed) return 0.0; F32 distance = mvecStop[(int)currentspeed] - mvecStop[(int)desiredspeed]; return distance; } void AIWheeledVehicleBrain::setStartNode(int startnode) { mCurrentNode = startnode; SetLastNode(); setMoveDestination(mPath[mCurrentNode].m_Point); } int AIWheeledVehicleBrain::CalcNextNode(int node) { int nextnode = node+1; if (nextnode>mPath.size()-1) nextnode = 0; return nextnode; } int AIWheeledVehicleBrain::CalcPrevNode(int node) { int prevnode = node-1; if (prevnode<0) prevnode = mPath.size()-1; return prevnode; } void AIWheeledVehicleBrain::SetLastNode() { mLastNode = CalcPrevNode(mCurrentNode); } F32 AIWheeledVehicleBrain::avoidCollisions() { disableCollision(); F32 thetaScale = 200.0f; F32 safeDistance = (getVelocity() * thetaScale).magnitudeSafe(); F32 steering = 0.0; Point3F start = getPosition(); Point3F feeler,temp; F32 velocity = getVelocity().len() * thetaScale; Point3F ForwardRot,LeftRot,RightRot; getTransform().getColumn(0,&LeftRot); getTransform().getColumn(1,&ForwardRot); RightRot = -LeftRot; Point3F frontFeeler = (ForwardRot * velocity) + start; frontFeeler.z = start.z; Point3F leftFeeler = (LeftRot * velocity) + start; leftFeeler.z = start.z; Point3F rightFeeler = (RightRot * velocity) + start; rightFeeler.z = start.z; // Find closest intersection with wall (static shape) line if any bool intersectionFound = false; F32 closestDis; RayInfo closestRay; FeelerState closestFeelerState = Open; U32 avoidanceMask = STATIC_COLLISION_MASK|DAMAGEABLE_MASK; RayInfo rayInfo_1; closestRay.distance = rayInfo_1.distance = (frontFeeler-start).len(); if (getContainer()->castRay( start, frontFeeler, avoidanceMask , &rayInfo_1 )) { if (rayInfo_1.distance < closestRay.distance) { closestRay = rayInfo_1; intersectionFound = true; closestFeelerState = FrontFeeler; } } RayInfo rayInfo_2; rayInfo_2.distance = (leftFeeler-start).len(); if (getContainer()->castRay( start, leftFeeler, avoidanceMask , &rayInfo_2 )) { intersectionFound = true; if (rayInfo_2.distance < closestRay.distance) { closestRay = rayInfo_2; closestFeelerState = LeftFeeler; } } RayInfo rayInfo_3; rayInfo_3.distance = (rightFeeler-start).len(); if (getContainer()->castRay( start, rightFeeler, avoidanceMask , &rayInfo_3 )) { intersectionFound = true; if (rayInfo_3.distance < closestRay.distance) { closestRay = rayInfo_3; closestFeelerState = RightFeeler; } } if (!intersectionFound) { mMoveSpeed = 1.0; steering = 0.0; } else { switch (closestFeelerState){ case FrontFeeler: mMoveSpeed -= 0.05f; if (mMoveSpeed<0.01f) mMoveSpeed = 0.01f; case LeftFeeler: if (closestRay.distance >0) { steering = mDataBlock->maxSteeringAngle / (closestRay.distance / safeDistance); mMoveSpeed -= 0.01f; if (mMoveSpeed<0.01) mMoveSpeed = 0.01f; break; } case RightFeeler: if (closestRay.distance >0) { steering = -mDataBlock->maxSteeringAngle / (closestRay.distance / safeDistance); mMoveSpeed -= 0.01f; if (mMoveSpeed<0.01f) mMoveSpeed = 0.01f; break; } } } enableCollision(); return steering; } void AIWheeledVehicleBrain::updateVehicleData() { //Update vehicle data mVehiclePos = getPosition(); mCurrentSpeed = fabs(getVelocity().len()); MatrixF mat = getTransform(); VectorF vehicledir; vehicledir.set(0,1,0); mat.mulV(vehicledir); //ignore the z part because we can't fix that vehicledir.z = 0; vehicledir.normalize(); mVehicledir = vehicledir; Point3F pt1 = mPath[mCurrentNode].m_Point; Point3F pt2 = mPath[mLastNode].m_Point; mbBetweenLines = false; //Figure distance and intersection to racing line mTargetNode = mCurrentNode; bool b = true; if (!DistancePointToLine(mVehiclePos,pt2,pt1,mDistance,mIntersection)) { //Try the next node mTargetNode = CalcNextNode(mCurrentNode); pt1 = mPath[mTargetNode].m_Point; pt2 = mPath[mCurrentNode].m_Point; if (DistancePointToLine(mVehiclePos,pt2,pt1,mDistance,mIntersection)) { mLastNode = mCurrentNode; mCurrentNode = mTargetNode; mbBetweenLines = true; } else { //In between line segments Point3F inter = mPath[mTargetNode].m_Point; mDistance = getDistanceToNode(mTargetNode); mbBetweenLines = true; } } F32 distance = getDistanceToNode(mTargetNode); //pretty basic right now. need to add better conditions to being 'stuck' if (mCurrentSpeed < mStuckTolerance) { mStuckCount++; if (mStuckCount>mMaxStuckCount) { mStuckCycles++; if (mStuckCycles>mMaxStuckCount) { mStuckCount = 0; mStuckCycles = 0; Con::executef(this, 1, "onStuck"); } } else mMoveState = ModeMove; if (distance>mLostDistance) { mStuckCycles = 0; mStuckCount = 0; Con::executef(this, 1, "onLost"); } } else mStuckCycles = mStuckCount = 0; if (distance<mPrecision) { //Use the next node mTargetNode = CalcNextNode(mCurrentNode); pt1 = mPath[mTargetNode].m_Point; pt2 = mPath[mCurrentNode].m_Point; } VectorF racelinedir; racelinedir = pt1-pt2; racelinedir.normalize(); mRacelinedir = racelinedir; } // Think - figure out speed(accelerate or apply brakes) and steer the vehicle bool AIWheeledVehicleBrain::getAIMove(Move *movePtr) { if (!mbInit) return false; if (mDisableMove) { mRigid.setAtRest(); return true; } *movePtr = NullMove; updateVehicleData(); Point3F pt1 = mPath[mCurrentNode].m_Point; Point3F pt2 = mPath[mLastNode].m_Point; // Orient towards our destination. mMovecount++; if (mMovecount > mAvoidanceTick) { mMovecount = 1; movePtr->yaw = avoidCollisions(); movePtr->y = mMoveSpeed; } else { if (mMoveState == ModeMove || mMoveState == ModeReverse) { mTurnAngle = followRacingLine(); movePtr->yaw = mTurnAngle; } // Move towards the destination if (mMoveState == ModeMove) { movePtr->y = mMoveSpeed; setMoveDestination(mPath[mCurrentNode].m_Point); //Do we need to slow down or speed up? F32 targetSpeed = mPath[mCurrentNode].m_desiredSpeed; if (mCurrentSpeed>targetSpeed * mSAI->mAgression) { F32 distance = getDistanceToNode(mCurrentNode); F32 slowdistance = getSlowDownDistance(mCurrentSpeed,mPath[mCurrentNode].m_desiredSpeed); if (distance<(slowdistance+STOPSAFETY)) { movePtr->y = 0.0; movePtr->trigger[2] = true; } else if ((mPath[mCurrentNode].m_desiredSpeed>mCurrentSpeed)&&(mPath[mLastNode].m_desiredSpeed>mCurrentSpeed)) { movePtr->y = 0.0; movePtr->trigger[2] = true; } else { if ((mPath[mCurrentNode].m_desiredSpeed<999)&&(mPath[mLastNode].m_desiredSpeed<999)) { movePtr->y = mMoveSpeed; } else { movePtr->y = mMoveSpeed; } } } else { movePtr->y = mMoveSpeed; } } else if(mMoveState == ModeReverse) { movePtr->y = -1 * mMoveSpeed; } else if(mMoveState == ModeStop) { movePtr->y = 0; } } //Don't apply gas if vehicle is badly sliding Point3F vel = getVelocity(); vel.z = 0; vel.normalize(); F32 d = mDot(vel,mVehicledir); bool bSlide = false; if (mCurrentSpeed>400) { if (d<0.95) { movePtr->y = 0; movePtr->trigger[2] = true; } } // Replicate the trigger state into the move so that // triggers can be controlled from scripts. for( int i = 0; i < MaxTriggerKeys; i++ ) movePtr->trigger[i] = getImageTriggerState(i); return true; } F32 AIWheeledVehicleBrain::getDistanceToNode(U32 node) { Point3F v = mVehiclePos-mPath[node].m_Point; return v.len(); } /** * Utility function to throw callbacks. Callbacks always occure * on the datablock class. * * @param name Name of script function to call */ void AIWheeledVehicleBrain::throwCallback( const char *name ) { Con::executef(getDataBlock(), 2, name, scriptThis()); } //Perpendicular distance from point to line //returns false if the point is not perpendicular to line //Ignores the z value bool DistancePointToLine( Point3F Point, Point3F LineStart, Point3F LineEnd, F32 &distance, Point3F &Intersection ) { F32 LineMag; F32 U; //Ignore z value Intersection.set(0,0,0); Point.z = 0; LineEnd.z = 0; LineStart.z = 0; Point3F l = LineEnd-LineStart; LineMag = l.len(); if (LineMag!=0.0f) { U = ( ( ( Point.x - LineStart.x ) * ( LineEnd.x - LineStart.x ) ) + ( ( Point.y - LineStart.y ) * ( LineEnd.y - LineStart.y ) ))/ ( LineMag * LineMag ); } if( U < 0.0f || U > 1.0f ) return false; // closest point does not fall within the line segment Intersection.x = LineStart.x + U * ( LineEnd.x - LineStart.x ); Intersection.y = LineStart.y + U * ( LineEnd.y - LineStart.y ); Point3F l2 = Point - Intersection; distance = l2.len(); return true; } // -------------------------------------------------------------------------------------------- // Console Functions // -------------------------------------------------------------------------------------------- ConsoleMethod( AIWheeledVehicleBrain, init, void, 2, 2, "()" "Initialize AI for vehicle") { object->InitAI(); } ConsoleMethod( AIWheeledVehicleBrain, setStartNode, void, 3, 3, "( int startnode )" "Sets the move speed for an AI object.") { object->setStartNode( dAtoi( argv[2] ) ); } ConsoleMethod( AIWheeledVehicleBrain, setMoveTolerance, void, 3, 3, "(float speed)" "Sets the movetolerance") { object->setMoveTolerance(dAtof(argv[2])); } ConsoleMethod( AIWheeledVehicleBrain, addPathPoint, void, 4, 4, "(Point3F goal)(float speed)" "Add a point to the vehicle travel path") { float speed = 0; Point3F v( 0.0f, 0.0f, 0.0f ); dSscanf( argv[2], "%f %f %f", &v.x, &v.y, &v.z ); dSscanf( argv[3], "%f", &speed ); object->addPathPoint( v, speed ); } ConsoleMethod( AIWheeledVehicleBrain, getMoveDestination, const char *, 2, 2, "()" "Returns the point the AI is set to move to.") { Point3F movePoint = object->getMoveDestination(); char *returnBuffer = Con::getReturnBuffer( 256 ); dSprintf( returnBuffer, 256, "%f %f %f", movePoint.x, movePoint.y, movePoint.z ); return returnBuffer; } //============================================================================================== bool AIWheeledVehicleBrain::setSAI(SAI* dptr) { if (isGhost() || isProperlyAdded()) { if (mSAI != dptr) return onNewSAI(dptr); } else mSAI = dptr; return true; } bool AIWheeledVehicleBrain::onNewSAI(SAI* dptr) { mSAI = dptr; if (!mSAI) return false; setMaskBits(DataBlockMask); return true; } //---------------------------------------------------------------------------- ConsoleMethod( AIWheeledVehicleBrain, getSAI, S32, 2, 2, "()" "Return the SAI this AIWheeledVehicleBrain is using.") { return object->getSAI()? object->getSAI()->getId(): 0; } //---------------------------------------------------------------------------- ConsoleMethod(AIWheeledVehicleBrain, setSAI, bool, 3, 3, "(SAI db)" "Assign this AIWheeledVehicleBrain to use the specified SAI.") { SAI* data; if (Sim::findObject(argv[2],data)) { return object->setSAI(data); } Con::errorf("Could not find SAI Template \"%s\"!",argv[2]); return false; }
Not working on a racing game this time around, so hope it helps.
Last edited by Azaezel on Thu Jul 14, 2016 9:29 pm, edited 1 time in total.
Azaezel
DEVGRU
Posts: 496
Joined: Tue Feb 03, 2015 9:50 pm
 
by Azaezel » Thu Jul 14, 2016 9:24 pm
Jason Campbell
Posts: 362
Joined: Fri Feb 13, 2015 2:51 am
 
by Jason Campbell » Fri Jul 15, 2016 1:51 am
That is very cool Azaezel! Plus I didn't notice that mirror until I clicked your link. The mirror is still alive and aiwheeledvehocle.zip is there. Thanks man.
flysouth
Posts: 105
Joined: Sun May 15, 2016 10:16 am
by flysouth » Fri Jul 15, 2016 10:14 am
Thanks for the code and links.
Wow it looks like T3D can do very little unless you are prepared to mod the source code. :(
9 posts Page 1 of 1

Who is online

Users browsing this forum: No registered users and 12 guests