mirror of
https://github.com/tribes2/engine.git
synced 2026-01-20 03:34:48 +00:00
1284 lines
33 KiB
C++
1284 lines
33 KiB
C++
//-----------------------------------------------------------------------------
|
|
// V12 Engine
|
|
//
|
|
// Copyright (c) 2001 GarageGames.Com
|
|
// Portions Copyright (c) 2001 by Sierra Online, Inc.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
#include "ai/graphData.h"
|
|
#include "terrain/terrRender.h"
|
|
|
|
// 4 6 7 Layout for how we translate the
|
|
// 2 N 5 numbers 0 to 7 into the eight
|
|
// 0 1 3 surrounding grid locations.
|
|
const Point2I TerrainGraphInfo::gridOffs[8] = {
|
|
Point2I( -1, -1 ),
|
|
Point2I( 0, -1 ), Point2I( -1, 0 ),
|
|
Point2I( 1, -1 ), Point2I( -1, 1 ),
|
|
Point2I( 1, 0 ), Point2I( 0, 1 ),
|
|
Point2I( 1, 1 ) };
|
|
|
|
S32 TerrainGraphInfo::smVersion = 0;
|
|
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Indoor Graph Edges
|
|
|
|
bool GraphEdgeInfo::isAlgorithmic() const {
|
|
return to[0].flags.test(Algorithmic);
|
|
}
|
|
void GraphEdgeInfo::setAlgorithmic(bool yesOrNo) {
|
|
to[0].flags.set(Algorithmic, yesOrNo);
|
|
}
|
|
|
|
GraphEdgeInfo::GraphEdgeInfo()
|
|
{
|
|
segNormal.set(0,0,1);
|
|
segPoints[0] = segPoints[1] = segNormal;
|
|
}
|
|
|
|
bool GraphEdgeInfo::read(Stream & s)
|
|
{
|
|
for( S32 i = 0; i < 2; i++ ){
|
|
U32 mask;
|
|
if( s.read(&mask) ){
|
|
to[i].flags = mask;
|
|
if( s.read(&to[i].res) && s.read(&to[i].dest) )
|
|
continue;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (mathRead(s, &segPoints[0]))
|
|
if (mathRead(s, &segPoints[1]))
|
|
if (mathRead(s, &segNormal))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
bool GraphEdgeInfo::write(Stream & s) const
|
|
{
|
|
for( S32 i = 0; i < 2; i++ ){
|
|
U32 mask = to[i].flags;
|
|
if( s.write(mask) && s.write(to[i].res) && s.write(to[i].dest) )
|
|
continue;
|
|
return false;
|
|
}
|
|
|
|
if (mathWrite(s, segPoints[0]))
|
|
if (mathWrite(s, segPoints[1]))
|
|
if (mathWrite(s, segNormal))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Indoor Nodes
|
|
|
|
IndoorNodeInfo::IndoorNodeInfo()
|
|
{
|
|
pos.set(-1,-1,-1);
|
|
antecedent = -1;
|
|
unused = 0;
|
|
}
|
|
|
|
// The boolean setters & getters. Avoiding inlines since these should check
|
|
// invariant relationships among the flags.
|
|
bool IndoorNodeInfo::isAlgorithmic() const
|
|
{
|
|
return flags.test( Algorithmic );
|
|
}
|
|
|
|
void IndoorNodeInfo::setAlgorithmic(bool yesOrNo)
|
|
{
|
|
flags.set( Algorithmic, yesOrNo );
|
|
}
|
|
|
|
bool IndoorNodeInfo::isInventory() const
|
|
{
|
|
return flags.test(Inventory);
|
|
}
|
|
|
|
void IndoorNodeInfo::setInventory(bool yesOrNo)
|
|
{
|
|
flags.set(Inventory, yesOrNo);
|
|
}
|
|
|
|
bool IndoorNodeInfo::isBelowPortal() const
|
|
{
|
|
return flags.test(BelowPortal);
|
|
}
|
|
|
|
void IndoorNodeInfo::setBelowPortal(bool yesOrNo)
|
|
{
|
|
flags.set(BelowPortal, yesOrNo);
|
|
}
|
|
|
|
bool IndoorNodeInfo::isSeed() const
|
|
{
|
|
return flags.test(Seed);
|
|
}
|
|
|
|
void IndoorNodeInfo::setSeed(bool yesOrNo)
|
|
{
|
|
flags.set(Seed, yesOrNo);
|
|
}
|
|
|
|
bool IndoorNodeInfo::read(Stream & s)
|
|
{
|
|
U32 mask;
|
|
if (s.read(&mask))
|
|
{
|
|
flags = mask;
|
|
return s.read(&unused) && s.read(&antecedent) && mathRead(s, &pos);
|
|
}
|
|
return false;
|
|
}
|
|
bool IndoorNodeInfo::write(Stream & s) const
|
|
{
|
|
U32 mask = flags;
|
|
if (s.write(mask))
|
|
return s.write(unused) && s.write(antecedent) && mathWrite(s, pos);
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Outdoor (consolidated) node information
|
|
|
|
OutdoorNodeInfo::OutdoorNodeInfo()
|
|
{
|
|
level = 0xff;
|
|
flags = 0;
|
|
height = 0; //==> Not used - can be taken out.
|
|
x = y = -1;
|
|
}
|
|
|
|
bool OutdoorNodeInfo::read(Stream& s)
|
|
{
|
|
return s.read(&level) &&
|
|
s.read(&flags) &&
|
|
s.read(&height) &&
|
|
s.read(&x) &&
|
|
s.read(&y)
|
|
;
|
|
}
|
|
|
|
bool OutdoorNodeInfo::write(Stream & s) const
|
|
{
|
|
return s.write(level) &&
|
|
s.write(flags) &&
|
|
s.write(height) &&
|
|
s.write(x) &&
|
|
s.write(y)
|
|
;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
SpawnLocations::SpawnLocations()
|
|
{
|
|
reset();
|
|
}
|
|
|
|
void SpawnLocations::reset()
|
|
{
|
|
clear();
|
|
compact();
|
|
mSpheres.clear();
|
|
mSpheres.compact();
|
|
mRes0 = 0;
|
|
}
|
|
|
|
bool SpawnLocations::read(Stream& s)
|
|
{
|
|
if (s.read(&mRes0))
|
|
if (mathReadVector(*this, s))
|
|
return readVector1(s, mSpheres);
|
|
return false;
|
|
}
|
|
|
|
bool SpawnLocations::write(Stream& s) const
|
|
{
|
|
if (s.write(mRes0))
|
|
if (mathWriteVector(*this, s))
|
|
return writeVector1(s, mSpheres);
|
|
|
|
return false;
|
|
}
|
|
|
|
SpawnLocations::Sphere::Sphere(const Point3F& center, F32 radius)
|
|
: mSphere(center, radius)
|
|
{
|
|
mInside = false;
|
|
mCount = 0;
|
|
mOffset = 0;
|
|
mRes0 = 0;
|
|
}
|
|
|
|
bool SpawnLocations::Sphere::read(Stream& s)
|
|
{
|
|
if (s.read(&mRes0))
|
|
if (mathRead(s, &mSphere))
|
|
if (s.read(&mInside))
|
|
if (s.read(&mCount))
|
|
return s.read(&mOffset);
|
|
return false;
|
|
}
|
|
|
|
bool SpawnLocations::Sphere::write(Stream& s) const
|
|
{
|
|
if (s.write(mRes0))
|
|
if (mathWrite(s, mSphere))
|
|
if (s.write(mInside))
|
|
if (s.write(mCount))
|
|
return s.write(mOffset);
|
|
return false;
|
|
}
|
|
|
|
// Lookup a random location from our pre-computed data.
|
|
S32 SpawnLocations::getRandom(const SphereF& sphere, bool inside, U32 rnd)
|
|
{
|
|
// Find closest sphere matching inside/outside requirement:
|
|
const Sphere * closest = NULL;
|
|
F32 minDist = 1e22, d;
|
|
for (S32 i = 0; i < mSpheres.size(); i++)
|
|
{
|
|
const Sphere * S = & mSpheres[i];
|
|
if (S->mInside == inside && S->mCount > 0)
|
|
if ((d = (S->mSphere.center - sphere.center).lenSquared()) < minDist)
|
|
{
|
|
closest = S;
|
|
minDist = d;
|
|
}
|
|
}
|
|
|
|
// Get the random index in the list.
|
|
if (closest)
|
|
{
|
|
U32 count = closest->mCount;
|
|
return (closest->mOffset + ((rnd & 0x7FFFFF) % count));
|
|
}
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
void SpawnLocations::printInfo() const
|
|
{
|
|
Con::printf("--- %d Spheres were generated", mSpheres.size());
|
|
for (S32 i = 0; i < mSpheres.size(); i++)
|
|
{
|
|
const Sphere * S = & mSpheres[i];
|
|
const Point3F & P = S->mSphere.center;
|
|
|
|
Con::printf("%d : %s at center (%f, %f, %f) - generated %d",
|
|
i + 1,
|
|
S->mInside ? "Inside" : "Outside",
|
|
P.x, P.y, P.z,
|
|
S->mCount
|
|
);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Potential bridges.
|
|
|
|
void GraphBridgeInfo::init(S32 from, S32 to)
|
|
{
|
|
srcNode = from;
|
|
dstNode = to;
|
|
jetClear = 0;
|
|
howTo = 0;
|
|
res1 = 0;
|
|
res2 = 0;
|
|
res3 = 0;
|
|
}
|
|
|
|
bool GraphBridgeInfo::read(Stream& s)
|
|
{
|
|
return
|
|
( s.read(&dstNode) &&
|
|
s.read(&srcNode) &&
|
|
s.read(&jetClear) &&
|
|
s.read(&howTo) &&
|
|
s.read(&res1) &&
|
|
s.read(&res2) &&
|
|
s.read(&res3)
|
|
);
|
|
}
|
|
|
|
bool GraphBridgeInfo::write(Stream & s) const
|
|
{
|
|
return
|
|
( s.write(dstNode) &&
|
|
s.write(srcNode) &&
|
|
s.write(jetClear) &&
|
|
s.write(howTo) &&
|
|
s.write(res1) &&
|
|
s.write(res2) &&
|
|
s.write(res3)
|
|
);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Graph Bridge Data
|
|
|
|
void GraphBridgeData::init(U16 n0, U16 n1)
|
|
{
|
|
nodes[0] = n1; // for some reason, these are backwards, which wound up causing
|
|
nodes[1] = n0; // a problem for the replacement edges. The quick remedy is to
|
|
jetClear = 0; // change the edge replacement ON LOAD (graphJetting.cc) rather
|
|
howTo = 0; // than rebuild just to change a non-intuitive ordering here...
|
|
}
|
|
|
|
bool GraphBridgeData::read(Stream& s)
|
|
{
|
|
return( s.read(&nodes[0]) &&
|
|
s.read(&nodes[1]) &&
|
|
s.read(&jetClear) &&
|
|
s.read(&howTo)
|
|
);
|
|
}
|
|
|
|
bool GraphBridgeData::write(Stream & s) const
|
|
{
|
|
return( s.write(nodes[0]) &&
|
|
s.write(nodes[1]) &&
|
|
s.write(jetClear) &&
|
|
s.write(howTo)
|
|
);
|
|
}
|
|
|
|
BridgeDataList::BridgeDataList()
|
|
{
|
|
mReplaced[0] = mReplaced[1] = 0;
|
|
mSaveTotal = 0;
|
|
}
|
|
|
|
bool BridgeDataList::read(Stream& s)
|
|
{
|
|
mSaveTotal = 0;
|
|
if (readVector1(s, *this))
|
|
{
|
|
mSaveTotal = size();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool BridgeDataList::write(Stream& s) const
|
|
{
|
|
return writeVector1(s, *this);
|
|
}
|
|
|
|
// Given array of accumulation numbers for how many edges each node has, add
|
|
// in the bridges as well.
|
|
S32 BridgeDataList::accumEdgeCounts(U16 * edgeCounts)
|
|
{
|
|
S32 total = 0;
|
|
mReplaced[0] = mReplaced[1] = 0;
|
|
|
|
for (iterator it = begin(); it != end(); it++)
|
|
{
|
|
if (!it->isReplacement())
|
|
{
|
|
total += 2;
|
|
for (S32 i = 0; i < 2; i++)
|
|
edgeCounts[it->nodes[i]]++;
|
|
}
|
|
else
|
|
{
|
|
mReplaced[it->isUnreachable() != 0]++;
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
// Number of non-replacement bridges- basically how many edges it adds
|
|
// to the world (this is just used for memory tracking).
|
|
S32 BridgeDataList::numPositiveBridges() const
|
|
{
|
|
return (mSaveTotal << 1) - replaced();
|
|
}
|
|
|
|
S32 BridgeDataList::replaced() const
|
|
{
|
|
return mReplaced[0] + mReplaced[1];
|
|
}
|
|
|
|
// Read the old style and convert
|
|
#if 0
|
|
bool BridgeDataList::readOld(Stream& s)
|
|
{
|
|
BridgeInfoList oldList;
|
|
|
|
if (oldList.read(s))
|
|
{
|
|
S32 oldSz = oldList.size();
|
|
AssertFatal((oldSz % 2) == 0, "Old sizes were always even");
|
|
setSize(oldSz >>= 1);
|
|
|
|
// Old format very bloated and stores both ways, which turned out not to have any
|
|
// differences right now (later systems, maybe...).
|
|
while (oldSz--)
|
|
{
|
|
GraphBridgeInfo& bridgeOld = oldList[oldSz << 1];
|
|
GraphBridgeData& bridgeNew = (*this)[oldSz];
|
|
|
|
bridgeNew.nodes[0] = U16(bridgeOld.srcNode);
|
|
bridgeNew.nodes[1] = U16(bridgeOld.dstNode);
|
|
bridgeNew.howTo = bridgeOld.howTo;
|
|
bridgeNew.jetClear = mapJetHopToU8(bridgeOld.jetClear);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
bool GraphVolumeList::read(Stream& s)
|
|
{
|
|
return readVector1(s, *this) && mathReadVector(mPlanes, s);
|
|
}
|
|
|
|
bool GraphVolumeList::write(Stream& s) const
|
|
{
|
|
return writeVector1(s, *this) && mathWriteVector(mPlanes, s);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
S32 ChuteHints::findNear(const Box3F& box, ChutePtrList& list) const
|
|
{
|
|
list.clear();
|
|
return mBSP.getIntersecting(list, box);
|
|
}
|
|
|
|
// Patch for the query we usually do-
|
|
S32 ChuteHints::findNear(const Point3F& P, S32 xy, S32 z, ChutePtrList& list) const
|
|
{
|
|
Point3F boxMin(P.x - xy, P.y - xy, P.z - z);
|
|
Point3F boxMax(P.x + xy, P.y + xy, P.z + z);
|
|
Box3F theBox(boxMin, boxMax, true);
|
|
return findNear(theBox, list);
|
|
}
|
|
|
|
// Create the BSP. makeTree() needs a pointer list, so we must build one.
|
|
void ChuteHints::makeBSP()
|
|
{
|
|
VectorPtr<ChuteHint *> pointers;
|
|
pointers.setSize(size());
|
|
iterator c = begin();
|
|
for (S32 i = size() - 1; i >= 0; i--)
|
|
pointers[i] = c++;
|
|
mBSP.makeTree(pointers);
|
|
}
|
|
|
|
void ChuteHints::init(const Vector<Point3F>& list)
|
|
{
|
|
setSize(list.size());
|
|
Vector<Point3F>::const_iterator p = list.begin();
|
|
iterator c = begin();
|
|
for (S32 i = list.size() - 1; i >= 0; i--, p++, c++) {
|
|
c->x = p->x;
|
|
c->y = p->y;
|
|
c->z = p->z;
|
|
}
|
|
makeBSP();
|
|
}
|
|
|
|
bool ChuteHints::read(Stream& s)
|
|
{
|
|
if (readVector1(s, *this)) {
|
|
makeBSP();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ChuteHints::write(Stream& s) const
|
|
{
|
|
return writeVector1(s, *this);
|
|
}
|
|
|
|
// Given the verticle line of a (potential) jetting hop, see if it is one of the
|
|
// chute types. Build a box up as far as LOS goes to do this. We're at the
|
|
// top if the highest found chute hint is level with the given top point.
|
|
ChuteHints::Info ChuteHints::info(Point3F bottom, const Point3F& top)
|
|
{
|
|
const F32 boxR = 1.4;
|
|
|
|
if (mFabs(top.z - bottom.z) > 4.0 * /*Take out for moment-*/1e9) {
|
|
// Do these queries as a first pass to avoid LOS calls-
|
|
if (findNear(bottom, boxR, 2.0, mQuery) > 0) {
|
|
if (findNear(top, boxR, 2.0, mQuery) > 0) {
|
|
|
|
bottom.z += 1.0;
|
|
|
|
const U32 mask = (InteriorObjectType|TerrainObjectType);
|
|
const F32 maxH = 500.0;
|
|
Point3F upper(bottom.x, bottom.y, bottom.z + maxH);
|
|
RayInfo coll;
|
|
|
|
if (gServerContainer.castRay(bottom, upper, mask, &coll))
|
|
upper.z = coll.point.z;
|
|
|
|
Point3F boxMin(bottom.x - boxR, bottom.y - boxR, bottom.z - 2.0);
|
|
Point3F boxMax(upper.x + boxR, upper.y + boxR, upper.z);
|
|
Box3F theBox(boxMin, boxMax, true);
|
|
|
|
// Well, this should always return something at this point..
|
|
if (S32 numHints = findNear(theBox, mQuery)) {
|
|
// Find max-
|
|
F32 maxZ = -1e12, z;
|
|
for (S32 i = 0; i < numHints; i++)
|
|
if ((z = mQuery[i]->z) > maxZ)
|
|
maxZ = z;
|
|
|
|
if (mFabs(maxZ - top.z) < 2.0)
|
|
return ChuteTop;
|
|
else
|
|
return ChuteMid;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return NotAChute;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
bool PathXRefEntry::read(Stream& s)
|
|
{
|
|
U32 numEntries, bitWidth;
|
|
if (s.read(&numEntries) && s.read(&bitWidth)) {
|
|
setDims(numEntries, bitWidth);
|
|
return s.read(numBytes(), dataPtr());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PathXRefEntry::write(Stream& s) const
|
|
{
|
|
if (s.write(numEntries()) && s.write(bitWidth()))
|
|
return s.write(numBytes(), dataPtr());
|
|
return false;
|
|
}
|
|
|
|
PathXRefTable::~PathXRefTable()
|
|
{
|
|
clear();
|
|
}
|
|
|
|
bool PathXRefTable::read(Stream& s)
|
|
{
|
|
return constructVector(s, *this);
|
|
}
|
|
|
|
bool PathXRefTable::write(Stream& s) const
|
|
{
|
|
return writeVector1(s, *this);
|
|
}
|
|
|
|
void PathXRefTable::setSize(S32 size)
|
|
{
|
|
clear();
|
|
setSizeAndConstruct(*this, size);
|
|
}
|
|
|
|
void PathXRefTable::clear()
|
|
{
|
|
destructAndClear(*this);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
bool LOSXRefTable::read(Stream& s)
|
|
{
|
|
U32 numEntries, bitWidth;
|
|
if (s.read(&numEntries) && s.read(&bitWidth)) {
|
|
setDims(numEntries, bitWidth);
|
|
return s.read(numBytes(), dataPtr());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool LOSXRefTable::write(Stream& s) const
|
|
{
|
|
if (s.write(numEntries()) && s.write(bitWidth()))
|
|
return s.write(numBytes(), dataPtr());
|
|
return false;
|
|
}
|
|
|
|
U32 LOSXRefTable::value(S32 i1, S32 i2) const
|
|
{
|
|
if (i1 == i2)
|
|
return FullLOS;
|
|
else
|
|
{
|
|
U32 lookup = triangularTableIndex(i1, i2);
|
|
AssertFatal(lookup < numEntries(), "LOSXRefTable::value() read beyond end");
|
|
return getU17(lookup);
|
|
}
|
|
}
|
|
|
|
void LOSXRefTable::setEntry(S32 i1, S32 i2, U32 val)
|
|
{
|
|
setU17(triangularTableIndex(i1, i2), val);
|
|
}
|
|
|
|
bool LOSXRefTable::valid(S32 numNodes) const
|
|
{
|
|
S32 expectedTableSize = triangularTableIndex(numNodes + 1, 0);
|
|
return (numEntries() == expectedTableSize);
|
|
}
|
|
|
|
// Reset, free up memory.
|
|
void LOSXRefTable::clear()
|
|
{
|
|
setDims(0, 0);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// LOS Hash Table
|
|
|
|
LOSHashTable::LOSHashTable()
|
|
{
|
|
mTable = NULL;
|
|
mNumNodes = 0;
|
|
mRes0 = mRes1 = 0;
|
|
}
|
|
|
|
LOSHashTable::~LOSHashTable()
|
|
{
|
|
delete [] mTable;
|
|
}
|
|
|
|
bool LOSHashTable::valid(S32 numNodes) const
|
|
{
|
|
return (numNodes == mNumNodes);
|
|
}
|
|
|
|
// Do the node-to-node lookup.
|
|
U32 LOSHashTable::value(S32 i1, S32 i2) const
|
|
{
|
|
if (i1 == i2)
|
|
return FullLOS;
|
|
else
|
|
{
|
|
U16 from, to, seg;
|
|
|
|
if (i1 < i2) { // Lookup goes UP
|
|
from = i1;
|
|
seg = ((to = i2) >> SegShift);
|
|
}
|
|
else {
|
|
from = i2;
|
|
seg = ((to = i1) >> SegShift);
|
|
}
|
|
|
|
Key key(from, seg);
|
|
U32 hash = calcHash(key);
|
|
AssertFatal(hash < mTabSz, "LOSHashTable: bad hash value in lookup");
|
|
|
|
// Do the bucket "walk" -
|
|
if (S32 bucketSize = mTable[hash + 1] - mTable[hash])
|
|
{
|
|
const Segment * seg = &mSegments[mTable[hash]];
|
|
while (--bucketSize >= 0)
|
|
{
|
|
if (seg->mKey.mCompare == key.mCompare)
|
|
return seg->value(to & SegMask);
|
|
seg++;
|
|
}
|
|
}
|
|
|
|
// Nothing found, so it's hidden
|
|
return Hidden;
|
|
}
|
|
}
|
|
|
|
U32 LOSHashTable::mTabSz = 0;
|
|
|
|
// This needs to be improved (hence we sort after loading as well...)
|
|
U32 LOSHashTable::calcHash(Key key)
|
|
{
|
|
U32 n = key.mKey.mNode;
|
|
U32 s = key.mKey.mSeg;
|
|
U32 val = (n << 8 + (s & 7)) ^ (n + (n >> 1)) ^ ((s << 2) | (s & 1));
|
|
return val % mTabSz;
|
|
}
|
|
|
|
// Look for size that is relatively prime to first few primes (uh, can't hurt...)
|
|
U32 LOSHashTable::calcTabSize()
|
|
{
|
|
static const U32 nPrimes = 12;
|
|
static U32 primes[nPrimes] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
|
|
|
|
for (U32 nSegs = mSegments.size(); /* until we find one */ ; nSegs++ )
|
|
{
|
|
bool done = true;
|
|
|
|
for (U32 i = 0; i < nPrimes; i++)
|
|
if (!(nSegs % primes[i]))
|
|
done = false;
|
|
|
|
if (done) {
|
|
mTabSz = nSegs;
|
|
break;
|
|
}
|
|
}
|
|
return mTabSz;
|
|
}
|
|
|
|
S32 QSORT_CALLBACK LOSHashTable::cmpHashes(const void * a,const void * b)
|
|
{
|
|
U32 A = calcHash( ((Segment*)a)->mKey );
|
|
U32 B = calcHash( ((Segment*)b)->mKey );
|
|
AssertFatal(A < mTabSz && B < mTabSz, "LOSHashTable: hash has bad result");
|
|
return (A < B ? -1 : (A > B ? 1 : 0));
|
|
}
|
|
|
|
// This assumes the table size has been calculated-
|
|
void LOSHashTable::sortByHashVal()
|
|
{
|
|
dQsort((void* )(mSegments.address()), mSegments.size(), sizeof(Segment), cmpHashes);
|
|
}
|
|
|
|
LOSHashTable::Segment::Segment(Key key, const U32 losInfo[SegSize])
|
|
{
|
|
mKey = key;
|
|
dMemset(mLOS, 0, sizeof(mLOS));
|
|
for (U32 i = 0; i < SegAlloc; i++)
|
|
for (U32 j = 0; j < 4; j++)
|
|
mLOS[i] |= (losInfo[i * 4 + j] << (j * 2));
|
|
}
|
|
|
|
// Build the new better table from the old style. Return memory usage (for measuring,
|
|
// but it's also conceivable that we could try different seg sizes dynamically).
|
|
U32 LOSHashTable::convertTable(const LOSXRefTable& XRefTable, S32 numNodes)
|
|
{
|
|
S32 numUniform = 0;
|
|
S32 numSameSeg = 0;
|
|
|
|
mNumNodes = numNodes;
|
|
mSegments.clear();
|
|
|
|
for (S32 srcNode = 0; srcNode < numNodes - 1; srcNode++)
|
|
{
|
|
// Check all other segments (including ours if we're not the last in it). Note
|
|
// inclusive compare to catch last segment!
|
|
for (S32 seg = (srcNode + 1 >> SegShift); seg <= (numNodes >> SegShift); seg++)
|
|
{
|
|
S32 begin = (seg << SegShift);
|
|
S32 end = begin + SegSize;
|
|
|
|
if (end > numNodes)
|
|
end = numNodes;
|
|
|
|
// If in our own segment, only check node numbers higher...
|
|
bool sameSeg = (seg == (srcNode >> SegShift));
|
|
if (sameSeg)
|
|
{
|
|
begin = srcNode + 1;
|
|
end = ((srcNode + SegSize) & ~SegMask);
|
|
if (end > numNodes)
|
|
end = numNodes;
|
|
}
|
|
|
|
// Examine all the nodes in the segment for non-hidden
|
|
S32 losCount = 0;
|
|
U32 losInfo[SegSize];
|
|
dMemset(losInfo, 0, sizeof(losInfo));
|
|
for (U32 dstNode = begin; dstNode < end; dstNode++)
|
|
{
|
|
U32 info = XRefTable.value(U16(srcNode), U16(dstNode));
|
|
losInfo[dstNode & SegMask] = info;
|
|
losCount += (info != 0);
|
|
}
|
|
|
|
// Add the segment if something was seen.
|
|
if (losCount)
|
|
{
|
|
Key key(srcNode, seg);
|
|
Segment segment(key, losInfo);
|
|
mSegments.push_back(segment);
|
|
|
|
// See if they're all the same
|
|
S32 a;
|
|
for (a = 1; a < SegSize; a++)
|
|
if (losInfo[a] != losInfo[0])
|
|
break;
|
|
numUniform += (a == SegSize);
|
|
numSameSeg += sameSeg;
|
|
}
|
|
}
|
|
}
|
|
|
|
Con::printf("Found %d Uniform entries", numUniform);
|
|
Con::printf("Found %d SameSeg entries", numSameSeg);
|
|
|
|
// Sorts segment vector-
|
|
calcTabSize();
|
|
sortByHashVal();
|
|
|
|
// Set up mTable
|
|
U32 tabMemUse = makeTheTable();
|
|
|
|
return (mSegments.memSize() + tabMemUse);
|
|
}
|
|
|
|
// Given that we've got a vector of segments sorted by hash value, make mTable.
|
|
// Assumes table size has already been calculated.
|
|
// Return memory size of table.
|
|
U32 LOSHashTable::makeTheTable()
|
|
{
|
|
U32 deepest = 0, depth;
|
|
|
|
// Alloc and flag as uninitialized. Extra 1 needed for bucket size math at end.
|
|
delete [] mTable;
|
|
mTable = new IndexType[ mTabSz + 1 ];
|
|
U32 memSize = (mTabSz + 1) * sizeof(IndexType);
|
|
dMemset(mTable, 0xFF, memSize);
|
|
|
|
// Set up hash pointers-
|
|
for (U32 seg = 0; seg < mSegments.size(); seg++)
|
|
{
|
|
U32 hash = calcHash(mSegments[seg].mKey);
|
|
if (mTable[hash] == IndexType(-1))
|
|
mTable[hash] = seg;
|
|
else if ((depth = seg - mTable[hash]) > deepest)
|
|
deepest = depth;
|
|
}
|
|
|
|
// Fill in empty elements so bucket size math works
|
|
AssertFatal(mTable[mTabSz] == IndexType(-1), "LOS Hash table overwritten at end");
|
|
U32 fillValue = mSegments.size();
|
|
U32 numEmpty = 0, numFilled = 0;
|
|
for (U32 H = mTabSz; H; H--)
|
|
{
|
|
if (mTable[H] == IndexType(-1)) {
|
|
mTable[H] = fillValue;
|
|
numEmpty++;
|
|
}
|
|
else {
|
|
fillValue = mTable[H];
|
|
numFilled++;
|
|
}
|
|
}
|
|
|
|
Con::printf("Table size = %d, Segments = %d, Filled = %d, Deepest = %d",
|
|
mTabSz, mSegments.size(), numFilled, deepest );
|
|
|
|
return memSize;
|
|
}
|
|
|
|
bool LOSHashTable::Segment::read(Stream& s)
|
|
{
|
|
if (s.read(&mKey.mKey.mNode) && s.read(&mKey.mKey.mSeg))
|
|
return s.read(SegAlloc, mLOS);
|
|
return false;
|
|
}
|
|
|
|
bool LOSHashTable::Segment::write(Stream& s) const
|
|
{
|
|
if (s.write(mKey.mKey.mNode) && s.write(mKey.mKey.mSeg))
|
|
return s.write(SegAlloc, mLOS);
|
|
return false;
|
|
}
|
|
|
|
void LOSHashTable::clear()
|
|
{
|
|
mNumNodes = 0;
|
|
delete [] mTable;
|
|
mTable = NULL;
|
|
mSegments.clear();
|
|
mSegments.compact();
|
|
}
|
|
|
|
bool LOSHashTable::read(Stream& s)
|
|
{
|
|
if (s.read(&mNumNodes))
|
|
if (readVector1(s, mSegments))
|
|
if (s.read(&mRes0) && s.read(&mRes1))
|
|
{
|
|
calcTabSize();
|
|
sortByHashVal(); // For now we sort Segments on load since we're still
|
|
makeTheTable(); // tweaking the hash function
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool LOSHashTable::write(Stream& s) const
|
|
{
|
|
if (s.write(mNumNodes))
|
|
if (writeVector1(s, mSegments))
|
|
if (s.write(mRes0) && s.write(mRes1))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
// Header info for the Terrain Data.
|
|
|
|
TerrainGraphInfo::TerrainGraphInfo()
|
|
{
|
|
haveGraph = false;
|
|
dMemset(indOffs, 0xff, sizeof(indOffs));
|
|
nodeCount = numShadows = -1;
|
|
originWorld.set(-1,-1,-1);
|
|
originGrid.set(-1,-1);
|
|
gridDimensions = gridTopRight = originGrid;
|
|
}
|
|
|
|
bool TerrainGraphInfo::read(Stream & s)
|
|
{
|
|
S32 version;
|
|
|
|
haveGraph = false;
|
|
|
|
if( !s.read(&version) || version != smVersion )
|
|
return false;
|
|
|
|
bool Ok = s.read( & nodeCount) &&
|
|
mathRead(s, & originWorld) &&
|
|
mathRead(s, & originGrid) &&
|
|
mathRead(s, & gridDimensions) &&
|
|
mathRead(s, & gridTopRight) &&
|
|
readVector2(s, navigableFlags) &&
|
|
readVector2(s, neighborFlags) &&
|
|
readVector2(s, shadowHeights) &&
|
|
readVector2(s, roamRadii) &&
|
|
consolidated.read(s)
|
|
;
|
|
|
|
if( Ok )
|
|
doFinalDataSetup();
|
|
|
|
return( haveGraph = Ok );
|
|
}
|
|
|
|
bool TerrainGraphInfo::write(Stream & s) const
|
|
{
|
|
bool Ok = s.write( smVersion ) &&
|
|
s.write( nodeCount) &&
|
|
mathWrite(s, originWorld) &&
|
|
mathWrite(s, originGrid) &&
|
|
mathWrite(s, gridDimensions) &&
|
|
mathWrite(s, gridTopRight) &&
|
|
writeVector2(s, navigableFlags) &&
|
|
writeVector2(s, neighborFlags) &&
|
|
writeVector2(s, shadowHeights) &&
|
|
writeVector2(s, roamRadii) &&
|
|
consolidated.write(s)
|
|
;
|
|
|
|
return Ok;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Terrain graph info utility functions.
|
|
|
|
|
|
// Get the index of the given grid location if it's in our region, else -1.
|
|
// This doesn't check for obstruction though.
|
|
S32 TerrainGraphInfo::posToIndex(Point2I p) const
|
|
{
|
|
S32 idx = -1;
|
|
|
|
if( haveGraph )
|
|
{
|
|
p -= originGrid;
|
|
|
|
if( validArrayIndex(p.x, gridDimensions.x) )
|
|
if( validArrayIndex(p.y, gridDimensions.y) )
|
|
idx = p.y * gridDimensions.x + p.x;
|
|
|
|
AssertFatal( idx >= -1 && idx < nodeCount, "TerrainGraphInfo::posToIndex()" );
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
// Fetch the location at the given grid location - a straight mapping
|
|
// to world space, whether it's a valid node or not. But then return
|
|
// if it's a valid node.
|
|
bool TerrainGraphInfo::posToLoc(Point3F& loc, const Point2I& p)
|
|
{
|
|
S32 index = posToIndex(p);
|
|
bool isValid = (index >= 0 && !obstructed(index));
|
|
|
|
loc.x = F32(p.x << gNavGlobs.mSquareShift);
|
|
loc.y = F32(p.y << gNavGlobs.mSquareShift);
|
|
loc.z = 0.0f; // need actual position at some point.
|
|
loc += originWorld;
|
|
|
|
return isValid;
|
|
}
|
|
|
|
|
|
|
|
// Map the given world coordinate into our graph grid.
|
|
// Sort of a worldToGrid() routine.
|
|
S32 TerrainGraphInfo::locToIndex(Point3F loc) const
|
|
{
|
|
loc += gNavGlobs.mHalfSquare;
|
|
loc -= originWorld;
|
|
loc *= gNavGlobs.mInverseWidth;
|
|
return posToIndex( Point2I( mFloor(loc.x), mFloor(loc.y)) );
|
|
}
|
|
|
|
// Get the grid location. Returning a pointer since we may
|
|
// handle bad indices at some point with a NULL return value.
|
|
Point3F * TerrainGraphInfo::indexToLoc(Point3F & loc, S32 index)
|
|
{
|
|
Point2I & pos = indexToPos(index);
|
|
|
|
loc.x = F32(pos.x << gNavGlobs.mSquareShift);
|
|
loc.y = F32(pos.y << gNavGlobs.mSquareShift);
|
|
loc.z = 0.0f;
|
|
|
|
loc += originWorld;
|
|
|
|
return & loc;
|
|
}
|
|
|
|
bool TerrainGraphInfo::obstructed(const Point3F& loc) const
|
|
{
|
|
S32 index = locToIndex(loc);
|
|
return (index < 0) || obstructed(index);
|
|
}
|
|
|
|
Point2I & TerrainGraphInfo::indexToPos(S32 index) const
|
|
{
|
|
static Point2I sPoint;
|
|
sPoint.x = originGrid.x + index % gridDimensions.x;
|
|
sPoint.y = originGrid.y + index / gridDimensions.x;
|
|
return sPoint;
|
|
}
|
|
|
|
|
|
S32 TerrainGraphInfo::locToIndexAndSphere(SphereF & sphereOut, const Point3F & loc)
|
|
{
|
|
if( roamRadii.size() )
|
|
{
|
|
S32 idx = locToIndex(loc);
|
|
if( validArrayIndex(idx, roamRadii.size()) && !obstructed(idx) )
|
|
{
|
|
indexToLoc(sphereOut.center, idx);
|
|
sphereOut.radius = fixedToFloat(roamRadii[ idx ]);
|
|
if( sphereOut.isContained(loc) )
|
|
return idx;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// This function assumes that the grid and navigable flags have been set up.
|
|
//
|
|
void TerrainGraphInfo::computeRoamRadii()
|
|
{
|
|
Vector<Point2I> spiral;
|
|
Vector<F32> dists;
|
|
|
|
S32 radius = getMin( getMax(gridDimensions.x, gridDimensions.y), 20);
|
|
S32 scount = getGridSpiral(spiral, dists, radius);
|
|
S32 n, s;
|
|
|
|
// DMMNOTPRESENT
|
|
bool deadlyLiquids = false;
|
|
// bool deadlyLiquids = (TerrainRender::mLiquidType >= 4);
|
|
|
|
roamRadii.setSize(nodeCount);
|
|
|
|
for (n = 0; n < nodeCount; n++)
|
|
{
|
|
F32 roamDist = 0.0;
|
|
F32 extraAvoid = 0.0;
|
|
|
|
if (!obstructed(n))
|
|
{
|
|
Point2I basePos = indexToPos(n);
|
|
U8 saveType = squareType(n);
|
|
|
|
for (s = 0; s < scount; s++)
|
|
{
|
|
Point2I roamPos = basePos + spiral[s];
|
|
S32 roamInd = posToIndex( roamPos );
|
|
|
|
if (!validArrayIndex(roamInd, nodeCount) || obstructed(roamInd))
|
|
break;
|
|
|
|
if (steep(roamInd))
|
|
{
|
|
extraAvoid = 0.5;
|
|
break;
|
|
}
|
|
|
|
if (saveType != squareType(roamInd))
|
|
{
|
|
// If we run into deadly liquids, then cut our roam distance more-
|
|
if (deadlyLiquids && submerged(roamInd))
|
|
extraAvoid = 2.0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( s == scount ) // no hit - can probably just use largest dist
|
|
roamDist = dists[s - 1];
|
|
else
|
|
roamDist = (dists[s] - 1.4);
|
|
}
|
|
|
|
F32 roamGridDist = getMax(roamDist - extraAvoid, 0.0f);
|
|
|
|
roamRadii[n] = floatToFixed(roamGridDist * gNavGlobs.mSquareWidth);
|
|
}
|
|
}
|
|
|
|
// The following are used to find where to 'inbounds' when out of bounds. That is,
|
|
// a location on graph border to (find a closest node to) go to.
|
|
void TerrainGraphInfo::setSideSegs()
|
|
{
|
|
Point2I corners[4] = {
|
|
Point2I(originGrid.x + 0, originGrid.y + 0 ),
|
|
Point2I(originGrid.x + gridDimensions.x - 1, originGrid.y + 0 ),
|
|
Point2I(originGrid.x + gridDimensions.x - 1, originGrid.y + gridDimensions.y - 1 ),
|
|
Point2I(originGrid.x + 0, originGrid.y + gridDimensions.y - 1 )
|
|
};
|
|
|
|
bool Ok[4];
|
|
S32 numOk, p1, p2;
|
|
Point3F loc1, loc2;
|
|
|
|
for( numOk = p1 = 0; p1 < 4; p1++ )
|
|
{
|
|
p2 = (p1 + 1) % 4;
|
|
Ok[p1] = posToLoc(loc1, corners[p1] );
|
|
Ok[p2] = posToLoc(loc2, corners[p2] );
|
|
numOk += Ok[p1];
|
|
boundarySegs[p1] = LineSegment(loc1, loc2);
|
|
}
|
|
|
|
// We need to have warnings in the system if the boundaries don't have nodes.
|
|
if( numOk < 4 )
|
|
Con::printf( "Graph Warning! Only %d corners of grid are navigable!", numOk );
|
|
}
|
|
|
|
void TerrainGraphInfo::doFinalDataSetup()
|
|
{
|
|
// Set up grid area.
|
|
gridArea.point = originGrid;
|
|
gridArea.extent = gridDimensions;
|
|
|
|
setSideSegs();
|
|
|
|
// Offsets for finding neighboring squares when working with indices.
|
|
for (S32 dir = 0; dir < 8; dir++ ) {
|
|
Point2I off = gridOffs[dir];
|
|
indOffs[dir] = off.y * gridDimensions.x + off.x;
|
|
}
|
|
}
|
|
|
|
Point3F TerrainGraphInfo::whereToInbound(const Point3F & loc)
|
|
{
|
|
// Assuming that passed parameter has already filtered with inGraphArea().
|
|
AssertFatal(!inGraphArea(loc), "whereToInbound() only meant for out of bound locs");
|
|
AssertFatal(haveGraph, "whereToInbound() called without a graph set up");
|
|
|
|
S32 solution = -1;
|
|
F32 bestDist = 1e9;
|
|
|
|
for( S32 i = 0; i < 4; i++ )
|
|
{
|
|
F32 d = boundarySegs[i].distance(loc);
|
|
|
|
if( d < bestDist )
|
|
bestDist = d, solution = i;
|
|
}
|
|
|
|
return boundarySegs[solution].solution();
|
|
}
|
|
|
|
//
|
|
// See if you can go straight across terrain between the given points. Return
|
|
// what percentage of the way the system believes can be achieved.
|
|
//
|
|
// ==> Need to handle out of bounds checks properly. Basically clip to mission
|
|
// area and then enter this loop. If not intersecting, we probably return
|
|
// 100%, or maybe the check for outside should happen earlier and
|
|
// the bot would already know they are free to roam.
|
|
//
|
|
F32 TerrainGraphInfo::checkOpenTerrain(const Point3F & from, Point3F & to)
|
|
{
|
|
SphereF sphere;
|
|
S32 prevSphere = locToIndexAndSphere(sphere, from);
|
|
|
|
if( prevSphere < 0 ) {
|
|
to = from;
|
|
return 0.0f;
|
|
}
|
|
|
|
LineStepper linearPath(from, to);
|
|
|
|
while(true)
|
|
{
|
|
// Find outbound intersection on sphere. (Note WE tell linearPath when to step)
|
|
F32 dist = linearPath.getOutboundIntersection(sphere);
|
|
|
|
// make it all the way?
|
|
if( dist > linearPath.remainingDist() )
|
|
return 1.0;
|
|
|
|
// no intersection. only can happen theoretically due to rounding dullness.
|
|
if( dist < 0.0f )
|
|
break;
|
|
|
|
// See if we're in a new circle (after advancing the stepper).
|
|
S32 nextSphere = locToIndexAndSphere(sphere, linearPath.advanceToSolution());
|
|
|
|
// Keep going while we're inside a sphere (and it's not the same one!)
|
|
if( nextSphere >= 0 && nextSphere != prevSphere )
|
|
prevSphere = nextSphere;
|
|
else
|
|
break;
|
|
}
|
|
|
|
to = linearPath.getSolution();
|
|
return linearPath.distSoFar() / linearPath.totalDist();
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------
|
|
|
|
// Return if the given grid position is on the side of the area, and if so we return a
|
|
// bitset of all the valid neighbors it _could_ have. See consolidateData() for use.
|
|
U32 TerrainGraphInfo::onSideOfArea(S32 index)
|
|
{
|
|
Point2I p = indexToPos(index);
|
|
U32 retVal = 0xff;
|
|
|
|
// is it (well) inside?
|
|
if(p.x>originGrid.x && p.x<gridTopRight.x && p.y>originGrid.y && p.y<gridTopRight.y)
|
|
retVal = 0;
|
|
else{
|
|
// check left or right
|
|
if(p.x == originGrid.x)
|
|
retVal &= ~(BIT(GridBottomLeft)|BIT(GridTopLeft)|BIT(GridLeft));
|
|
else if(p.x == gridTopRight.x)
|
|
retVal &= ~(BIT(GridBottomRight)|BIT(GridTopRight)|BIT(GridRight));
|
|
// check bottom or top
|
|
if(p.y == originGrid.y)
|
|
retVal &= ~(BIT(GridBottomLeft)|BIT(GridBottomRight)|BIT(GridBottom));
|
|
else if(p.y == gridTopRight.y)
|
|
retVal &= ~(BIT(GridTopLeft)|BIT(GridTopRight)|BIT(GridTop));
|
|
}
|
|
|
|
return retVal;
|
|
}
|
|
|
|
|