//----------------------------------------------------------------------------- // V12 Engine // // Copyright (c) 2001 GarageGames.Com // Portions Copyright (c) 2001 by Sierra Online, Inc. //----------------------------------------------------------------------------- #include "ai/graph.h" #include "ai/graphLOS.h" #include "ai/graphBridge.h" //------------------------------------------------------------------------------------- // Controls generation- static bool sSpawnGraph = false; // Numbers that are different between the two types of graphs. Most jet connections // on NAV generation use the smaller lateral value, but we special case for large // terrain-to-terrain hops that are passed through. cf. FireStorm. #define NAVLateralMax 60.0 #define IslandHopLateralMax 120.0 #define SPNLateralMax 100.0 static const Point3F sBoxOffNAV(IslandHopLateralMax, IslandHopLateralMax, 10000.0); static const Point3F sBoxOffSPN(SPNLateralMax, SPNLateralMax, 10000.0); //------------------------------------------------------------------------------------- static const U32 sLOSMask = InteriorObjectType | StaticShapeObjectType | StaticObjectType | WaterObjectType | TerrainObjectType; static Point3F * sBreakPt1 = 0, * sBreakPt2 = 0; static SphereF * sMagSphere; static F32 sBreakRangeXY = 0.5, sBreakRangeZ = 2.0; static const S32 sGraphFanCount = 10; // ==> 1-11-1, reduced, should be fine. static const F32 sGraphFanWidth = 1.0; static const F32 sWideFanRaise = 1.3; // Clear distance assures they can get over a ledge, land Dist assures there's purchase. // Second clear dist is for case when one node is a portal (i.e. chutes) static const F32 sClearDists[2] = {1.3, 1.0}; static const F32 sLandDists[2] = {0.4, 0.2}; static const F32 sJumpAngDot = 0.707; // NOT SURE ABOUT THIS # Sometimes we want more - maybe allow other checks to insure // that things work when it's small (clear dist, etc.) There are some small ledge // hops which are needed - but hop over code will probably assure... static const F32 sMinXY = 2.0; static const F32 sMinXYSqrd = (sMinXY * sMinXY); static const Point3F sMinAboveVec(0,0,2.0); #define SlightlyOffFloor 0.157 #define MaxHopOverXY 37.0 // lhpatch- Make this # go on graph (was barely too small in one map at 17) #define MaxWalkableXY 20.0 #define MinLateralThresh 12.0 //------------------------------------------------------------------------------------- #define SquareOf(x) ((x)*(x)) static bool bridgeable(const Point3F& from, const Point3F& to, bool bothOutdoor) { F32 lateralThresh; if (sSpawnGraph) { lateralThresh = SPNLateralMax; } else { if (bothOutdoor) lateralThresh = IslandHopLateralMax; else lateralThresh = NAVLateralMax; // Pull down the lateral thresh for tall hops. Basically subtract off of it for // every meter of Z, but keep a minimum amount, which is necessary to assure // floating bases do get bridged (if only the downward hop is doable). if ( (lateralThresh -= mFabs(from.z - to.z)) < MinLateralThresh ) lateralThresh = MinLateralThresh; } Point2F lateral(from.x - to.x, from.y - to.y); return (lateral.lenSquared() < SquareOf(lateralThresh)); } //------------------------------------------------------------------------------------- // Get the fan vector in passed ref, and return number of increments. horzVec // is a horizontal vector guaranteed to have length. static S32 calcFanVector(const VectorF& horzVec, VectorF& fanIncOut) { fanIncOut.set(-horzVec.y, horzVec.x, 0); fanIncOut.normalize(sGraphFanWidth / F32(sGraphFanCount)); return sGraphFanCount; } //------------------------------------------------------------------------------------- // Some code for isolating cases in the debugger. Mainly for focusing on a couple of // nodes to see why they're not getting bridged. See navGraph.genDebug() in graph.cc static bool doBreak() {return true;} static bool checkEnds(const Point3F& n1, const Point3F& n2) { if (mFabs(n1.x - sBreakPt1->x) < sBreakRangeXY) if (mFabs(n2.x - sBreakPt2->x) < sBreakRangeXY) if (mFabs(n2.y - sBreakPt2->y) < sBreakRangeXY) if (mFabs(n1.y - sBreakPt1->y) < sBreakRangeXY) if (mFabs(n1.z - sBreakPt1->z) < sBreakRangeZ) if (mFabs(n2.z - sBreakPt2->z) < sBreakRangeZ) return doBreak(); return false; } //------------------------------------------------------------------------------------- GraphBridge::GraphBridge(const GraphNodeList& mainList, S32 islands, BridgeDataList& listOut) : mMainList(mainList), mIslands(islands), mBridgesOut(listOut) { heedSeeds(false); } //------------------------------------------------------------------------------------- // This is a bit indirect... but here's where we control which brdiges to attempt on // the two passes. On the first, all seeds are ignored. On the second we only bridge // where at least one useful seed is present, and NO useless seed is involved. bool GraphBridge::heedThese(const GraphNode* from, const GraphNode* to) { if (mHeedSeeds) if (from->usefulSeed() || to->usefulSeed()) return !(from->uselessSeed() || to->uselessSeed()); else return false; else return !(from->potentialSeed() || to->potentialSeed()); } void GraphBridge::heedSeeds(bool b) { mHeedSeeds = b; } //------------------------------------------------------------------------------------- // Try straight distance for the ratios. After we added good scale values on the // edges, our bridger makes too many edges since the ratios are off. Until this is // addressed (if indeed it really needs it), let's use straight distance on edges. F32 GraphBridge::getEdgeTime(const GraphEdge * edge) { return edge->mDist; } //------------------------------------------------------------------------------------- // Here's where we do all the work - try to see if a bridge exists. // bool GraphBridge::tryToBridge(const GraphNode* from, const GraphNode* to, F32 heuristic) { // This is actually where the separate passes (normal, then seeding) is controlled- if (!heedThese(from, to)) return false; // Check for in, or near, liquids. if (from->liquidZone() || to->liquidZone()) return false; S32 fromIndex = from->getIndex(); S32 toIndex = to->getIndex(); Point3F fromLoc = from->location(); Point3F toLoc = to->location(); bool oneIsPortal = (from->belowPortal() ^ to->belowPortal()); // Debug stopping points. if (sBreakPt1 && sBreakPt2) { if (checkEnds(fromLoc, toLoc) || checkEnds(toLoc, fromLoc)) if (oneIsPortal) doBreak(); } // NOTE: we can make the OffFloor amount smaller if outdoor node locations are // raised up to always be above terrain. (U16 rounding I think) fromLoc.z += SlightlyOffFloor; toLoc.z += SlightlyOffFloor; bool bothOutside = (from->outdoor() && to->outdoor()); if (from->neighbors(to)) return false; else { // Prefer to bridge going UP to insure that the ratio check gets handled (larger // travel times going up). if (fromLoc.z > toLoc.z) return false; else if (fromLoc.z == toLoc.z && fromIndex > toIndex) return false; } // Create low, high, mid (point above low, but on level with high). Point3F mid, low, high; low = fromLoc; high = toLoc; (mid = low).z = high.z; F32 zDist = (high.z - low.z); // Get 2D dist Point2F xyVec(toLoc.x - fromLoc.x, toLoc.y - fromLoc.y); F32 xyDistSq = xyVec.lenSquared(); // Handles all the LOS work Loser los(sLOSMask, true); if ((xyDistSq > 0.0001) && bridgeable(fromLoc, toLoc, bothOutside)) { Point3F aboveM, aboveH, clearPt, landRoom, across; bool isChute = false, chuteTop = false; bool walkable = false, gapWalk = false; bool canCheckJet = true, direct = false; bool makeBridge = false; F32 wall; // See if we shouldn't look for jetting. F32 xyDist = mSqrt(xyDistSq); if (from->inventory() || to->inventory() || xyDistSq < sMinXYSqrd) canCheckJet = false; else { if (from->getNormal().z < sJumpAngDot || to->getNormal().z < sJumpAngDot) canCheckJet = false; } bool checkLandRoom = (zDist > 1.0 && to->indoor()); Point3F aboveVec = sMinAboveVec; aboveM = mid + aboveVec; aboveH = high + aboveVec; across = (high - mid); across.normalize(); clearPt = mid + (sClearDists[oneIsPortal] * across); landRoom = high - (sLandDists[oneIsPortal] * across); // Move back on the up checks so the guy has room behind him. We have to // raise up the low a little bit to account for slope, but of course // we can't raise more than height. if (canCheckJet) { // There are two types of chute situations here, one for if we have this // connection goes to the top of the chute, the other for middle ones. // For the top we do a slope check off the collision up there and are lax // about other LOS checks. For the middle we require a couple more. if (ChuteHints::Info info = gNavGraph->getChutes().info(low, mid)) { isChute = true; if (info == ChuteHints::ChuteTop) chuteTop = true; } // Might still want to do a little of this on chutes... ? if (!isChute) { VectorF backUpVec = (across * 0.8); low -= backUpVec; low.z += getMin(2.0f, zDist); mid -= backUpVec; aboveM -= backUpVec; } } bool walkOff = los.haveLOS(mid, high); bool hopOver = false; if (!walkOff && xyDist < MaxHopOverXY) { hopOver = los.findHopLine(mid, high, 2, wall); if (hopOver) { mid.z += wall; high.z += wall; aboveM.z += wall; aboveH.z += wall; } } if (walkOff || hopOver) if (los.haveLOS(high, aboveH)) if (chuteTop || los.haveLOS(low, aboveM)) if (chuteTop || los.haveLOS(aboveM, aboveH)) { // Don't do walk checks between two outdoor nodes- if (!isChute && (!from->outdoor() || !to->outdoor())) { // do gap walk for special case of small zdiff or direct LOS direct = los.haveLOS(low, high); if (direct) gapWalk = los.walkOverGaps(low, high, 0.8); else if (zDist < gNavGlobs.mStepHeight) gapWalk = los.walkOverGaps(mid, high, 0.8); // If there's a wall to hop over, don't do the walk code. Also, if both // nodes are in the same island, we only consider walking if the // heuristic (ratio of best-path-so-far to straight line dist) is high. if (walkOff) if ((from->island() != to->island()) || (heuristic > 3.7)) walkable = los.walkOverBumps(aboveM, aboveH); } if (walkable || canCheckJet) { // Do the fanned LOS - just replicate the above slew of checks. VectorF fanInc; S32 numFan = calcFanVector(across, fanInc); // Added extra checks 1-11-1: Must do a wider fanned check on // low -> aboveM. Connections without room are slipping through // the tests because they are diagonal. Also must from a point // below the clear point, else they effectively can't clear on // tall hops. bool ignoreWide = (chuteTop || (zDist < sWideFanRaise + 0.1)); Point3F aboveL, belowC; if (!ignoreWide) { aboveL.set(low.x, low.y, low.z + sWideFanRaise); belowC.set(clearPt.x, clearPt.y, low.z + sWideFanRaise); } if (los.fannedLOS1(high, aboveH, fanInc, numFan)) if (los.fannedLOS1(high, mid, fanInc, numFan)) if (chuteTop || los.fannedLOS1(low, aboveM, fanInc, numFan)) if (chuteTop || los.fannedLOS2(aboveM, aboveH, fanInc, numFan)) if (ignoreWide || los.fannedLOS2(aboveL, aboveM, fanInc, numFan)) { if (!isChute) { if (gapWalk) if (direct) // in the direct case we need one more fanned LOS gapWalk = los.fannedLOS1(low, high, fanInc, numFan); if (gapWalk) walkable = true; else if (walkable) // (i.e. walkable so far in our checks) walkable = los.fannedBumpWalk(aboveM, aboveH, fanInc, numFan); } if (walkable) { // Well, it's too bad we had to do all this work on to find long // connections that we throw out, but we have to figure out if it // needs jet. makeBridge = (xyDist < MaxWalkableXY); } else { if (canCheckJet && (xyDist > GraphJetBridgeXY)) { // The clear check is only needed for jetting connections, // same with landing room check - which we should only use when // going up to indoor nodes (and one LOS failure is adequate) if (!checkLandRoom || !los.haveLOS(low, landRoom)) if (isChute || los.haveLOS(low, clearPt)) if (isChute || los.fannedLOS1(low, clearPt, fanInc, numFan)) if (ignoreWide || los.fannedLOS2(belowC, clearPt, fanInc, numFan)) { // Check for bonk due to jumps, which add a couple meters. // We want this to pass chutes and other indoor situations Ok, // so do LOS up at each end and use that as endpoints. F32 jumpAdd = (oneIsPortal && xyDist < LiberalBonkXY ? 0.6 : gNavGlobs.mJumpAdd); aboveM.z += (los.heightUp(aboveM, jumpAdd) - 0.1); aboveH.z += (los.heightUp(aboveH, jumpAdd) - 0.1); if (isChute || los.fannedLOS2(aboveM, aboveH, fanInc, numFan)) makeBridge = true; } // LOS keeps track of this at lowest level- don't allow // jetting connections through force fields. if (los.mHitForceField) makeBridge = false; } } } } } if (makeBridge) { GraphBridgeData bridge1(fromIndex, toIndex); if (walkable) bridge1.setWalkable(); if (hopOver) bridge1.setHop(wall); mBridgesOut.push_back(bridge1); return true; } } return false; } // We use a searcher to find those same-island nodes which we should try to bridge // to - namely those who are far on the path relative to straight line distance. // This will hopefully find for us all indoor jetting connections which are 'useful', // as well as finding which terrain nodes should be bridged (like over water). void GraphBridge::onQExtraction() { GraphNode * mExtractNode = extractedNode(); if (mExtractNode != mCurrent) { if (mSameIslandMark.test(mHead.mIndex)) { AssertFatal(mSameIslandCount > 0, "GraphBridge::onQExtraction()"); // done when we've visited all the possibilities in the same island if (--mSameIslandCount == 0) setEarlyOut(true); Point3F nodeLoc = mExtractNode->location(); F32 straightLineDist = (mStartLoc - nodeLoc).len(); if (straightLineDist > 0.01) { bool bothOutdoor = (mFromOutdoor && mExtractNode->outdoor()); F32 ratio = distSoFar() / straightLineDist; F32 thresh = mRatios[bothOutdoor]; if (ratio > thresh) { // Same-island bridges with good ratios are considered first... Candidate candidate(mExtractNode, -ratio); mCandidates.push_back(candidate); } } mSameIslandMark.clear(mHead.mIndex); } } } // Do all the bridging. Has two modes- spawn and regular nav. Each iteration performs // one loop of bridging- from one source node to all nodes that need to be bridged. S32 GraphBridge::findAllBridges() { S32 nodeCount = mMainList.size(); Vector islandCross(mIslands); GraphNodeList considerList; mSameIslandMark.setSize(nodeCount); mSameIslandMark.clear(); mRatios[1] = 1.6; // Outdoor-to-outdoor- try a little smaller ratio than- mRatios[0] = 2.2; // Other combinations of connections setSizeAndClear(islandCross, mIslands); for (S32 i = 0; i < nodeCount; i++) { if ((mCurrent = mMainList[i]) != NULL) { // Get list of all possibilities (bound box query) mStartLoc = mCurrent->location(); if (sMagSphere) { Point3F pt = sMagSphere->center; if ((pt -= mStartLoc).lenSquared() > SquareOf(sMagSphere->radius)) continue; } Point3F boxOff = (sSpawnGraph ? sBoxOffSPN : sBoxOffNAV); Box3F area(mStartLoc - boxOff, mStartLoc + boxOff); S32 curIsland = mCurrent->island(); gNavGraph->getNodesInBox(area, considerList); // Divy out those in separate island, and mark those in the same island for // consideration by the special seacher. mCandidates.clear(); mSameIslandCount = 0; for (S32 j = 0; j < considerList.size(); j++) { GraphNode * consider = considerList[j]; if (consider != mCurrent) { if (consider->island() == curIsland) { if (!sSpawnGraph) { mSameIslandMark.set(consider->getIndex()); mSameIslandCount++; } } else { // Bridges to different islands sorted by distance F32 dist = (mStartLoc - consider->location()).lenSquared(); Candidate candidate(consider, dist); mCandidates.push_back(candidate); } } } // Special search to find which same-island nodes need consideration. We want // to bridge same-island nodes if their ground travel distance is far relative // to their straight-line distance. if (!sSpawnGraph) { setEarlyOut(false); mFromOutdoor = mCurrent->outdoor(); performSearch(mCurrent, NULL); } // Candidates considered in order of best-ness heuristic mCandidates.sort(); // Try to bridge to all candidates which remain. setSizeAndClear(islandCross, mIslands); for (CandidateList::iterator c = mCandidates.begin(); c != mCandidates.end(); c++) { if (sSpawnGraph) { // For spawn we only need to cross to an island once. if (!islandCross[c->node->island()]) if (tryToBridge(mCurrent, c->node)) islandCross[c->node->island()] = true; } else { // For regular graph, allow a certain number of bridges to each island. // Need to sort list, and vary base on island size. We pass in the // heuristic # for avoiding too many walking connections. if (islandCross[c->node->island()] < 3) if (tryToBridge(mCurrent, c->node, -(c->heuristic))) islandCross[c->node->island()]++; } } } } return mBridgesOut.size(); } S32 gCountReplacementEdges, gCountUnreachable; // This checks to see if outdoor neighbor edges need to be modified or removed due // to steepness. If so, a special 'replacement' bridge is created, which either // removes the default one at startup, or converts it to a jetting connection. Note // that this also needs to remove steep edges underwater as well. void GraphBridge::checkOutdoor(const GraphNode* from, const GraphNode* to) { static const F32 stepD = 0.3; Point3F fromLoc = from->location(); Point3F toLoc = to->location(); // Vector for stepping sideways, and across. We're given that these are // non-zero vectors since from and to are separate neighbors. Point3F across = (toLoc - fromLoc); Point3F sideVec(across.y, -across.x, 0); sideVec.normalize(0.8); // Figure out step across vector, and number of iterations. across.z = 0; F32 dist2D = across.len(); S32 numSteps = S32(dist2D / stepD + 1.0); across /= F32(numSteps); // Get top and bottom points. Add amount should be enough given that the // consolidator generally won't pass really large peaks between nodes. F32 maxZ = getMax(fromLoc.z, toLoc.z); Point3F stepVec(fromLoc.x, fromLoc.y, maxZ); // Step three separate lines across to account for player width. VectorF normal; F32 height, previousZ, highestZ = -1e12; bool removeEdge = false; bool walkable = true; stepVec -= sideVec; for (F32 sideWise = 0.0; sideWise < 2.1 && !removeEdge; sideWise += 1.0) { Point3F point = (stepVec + sideVec * sideWise); if (gNavGraph->terrainHeight(point, &height)) highestZ = getMax(previousZ = height, highestZ); else removeEdge = true; // Do loop plus one extra for last point. Look for too steep a slope if our Z // is increasing. This means we can't walk. Also check to see if it's possible // to jet - namely if the highest point isn't too far above higher node. for (S32 N = 0; N <= numSteps && !removeEdge; N++, point += across) { if (gNavGraph->terrainHeight(point, &height, &normal)) { // Going up and too steep? if (normal.z < gNavGlobs.mWalkableDot && height > previousZ) walkable = false; highestZ = getMax(previousZ = height, highestZ); } else removeEdge = true; } } // Most of the time, this should happen- if (walkable && !removeEdge) return; GraphBridgeData bridge(from->getIndex(), to->getIndex()); bridge.setReplacement(); // See if we can at least jet. if (!removeEdge && (from->getNormal().z >= sJumpAngDot && to->getNormal().z >= sJumpAngDot) && (!from->liquidZone() && !to->liquidZone()) && (highestZ - maxZ < 5.0) ) { gCountReplacementEdges++; bridge.setHop(highestZ - maxZ + 1.0); } else { gCountUnreachable++; bridge.setUnreachable(); } mBridgesOut.push_back(bridge); } // Special pass to find those outdoor neighbors that are too steep to walk up. The // collision checking in checkOutdoor() only masks with terrain since it is already // established that there are no other obstructions along the ground. void GraphBridge::trimOutdoorSteep() { gCountReplacementEdges = 0; gCountUnreachable = 0; S32 nodeCount = mMainList.size(); for (S32 i = 0; i < nodeCount; i++) if (GraphNode * from = mMainList[i]) if (from->outdoor()) { GraphEdgeArray edges = from->getEdges(NULL); while (GraphEdge * e = edges++) if (!e->isJetting()) if (GraphNode * to = gNavGraph->lookupNode(e->mDest)) if (to->outdoor()) checkOutdoor(from, to); } } //------------------------------------------------------------------------------------- S32 QSORT_CALLBACK GraphBridge::CandidateList::cmpCandidates(const void * a,const void * b) { F32 A = ((Candidate*)a)->heuristic; F32 B = ((Candidate*)b)->heuristic; return (A < B ? -1 : (A > B ? 1 : 0)); } void GraphBridge::CandidateList::sort() { dQsort((void* )(this->address()), this->size(), sizeof(Candidate), cmpCandidates); } //------------------------------------------------------------------------------------- // Something to make it easy to quickly check out the graph generation in a particular // area by only doing bridge building in that vicinity. void NavigationGraph::setGenMagnify(const SphereF * sphere, const Point3F * end1, const Point3F * end2, F32 xyCheck, F32 zCheck) { static SphereF sSphere; static Point3F sPoint1, sPoint2; sMagSphere = NULL; sBreakPt2 = sBreakPt1 = NULL; if (sphere) *(sMagSphere = &sSphere) = *sphere; if (end1) *(sBreakPt1 = &sPoint1) = *end1; if (end2) *(sBreakPt2 = &sPoint2) = *end2; sBreakRangeXY = xyCheck; sBreakRangeZ = zCheck; } //------------------------------------------------------------------------------------- const char * NavigationGraph::findBridges() { const char * errorText = NULL; if (!mIslandPtrs.size()) errorText = "Error: islands haven't been marked in graph"; else { BridgeDataList bridges; GraphBridge builder(mNodeList, numIslands(), bridges); sSpawnGraph = mIsSpawnGraph; Loser::mCasts = 0; builder.findAllBridges(); builder.trimOutdoorSteep(); // set the graph variable: mBridgeList = bridges; } return errorText; } const char * NavigationGraph::pushBridges() { if (mPushedBridges) return "Bridges already pushed - do MakeGraph() to remove them"; if (mBridgeList.size() == 0) return "No bridges exist"; for (S32 i = 0; i < mBridgeList.size(); i++) { GraphBridgeData & B = mBridgeList[i]; if (!B.isReplacement()) { for (S32 k = 0; k < 2; k++) { RegularNode * src = dynamic_cast(mNodeList[B.nodes[k^0]]); RegularNode * dst = dynamic_cast(mNodeList[B.nodes[k^1]]); GraphEdge& edge = src->pushEdge(dst); if (B.mustJet()) { edge.setJetting(); if (B.jetClear) edge.setHop(B.jetClear); } mJetManager.initEdge(edge, src, dst); } } else { mJetManager.replaceEdge(B); } } Con::printf("Connected %d bridges", mBridgeList.size()); mPushedBridges = true; markIslands(); return NULL; } //------------------------------------------------------------------------------------- GraphSeeds::GraphSeeds(NavigationGraph& graph) : mGraph(graph), mLoser(InteriorObjectType) { // Save node count for later mapping into antecedent index table. mOriginalCount = graph.numNodes(); } // Add the seed. We have to remember our parent (antecedent) that spawned us thinking // that we might be potentially useful. void GraphSeeds::pushSeed(GraphNode* antecedent, const Point3F& pos, GraphSeed::Type type) { S32 index = antecedent->getIndex(); GraphSeed seed(index, pos, type); mAntecedents.push_back(index); push_back(seed); } Point3F sgCheckSeedUpper(-149.0, -136.5, 166.42); // Look at drop off from each edge of node. If it lands inside of an interior volume // with a modicum of containment, then we want to add a seed node there. void GraphSeeds::seedDropOffs(GraphNode * antecedent) { // Just for quick testing - look at our shape and see if we can get the behavior we // want locally here- // if (!within(antecedent->location(), sgCheckSeedUpper, 0.6)) // return; mVolume = mGraph.fetchVolume(antecedent, false); Vector &corners = mVolume.mCorners; if (S32 N = corners.size()) { // Simplify the loop- corners.push_back(corners[0]); // Do drop offs on each edge. for (S32 i = 0; i < N; i++) { Point3F p0 = corners[i + 0]; Point3F p1 = corners[i + 1]; Point3F drop = scaleBetween(p0, p1, 0.5); VectorF normal(p0.y - p1.y, p1.x - p0.x, 0); F32 len = normal.len(); if (len > 0.01) { // Go out at least by clear distance. Insure we're above our plane. normal *= ((sClearDists[0] * 1.2) / len); drop += normal; F32 floorZ = solveForZ(mVolume.mFloor, drop); drop.z = (floorZ + 0.5); if (mLoser.hitBelow(drop, 50.0f)) { // Now see if we landed inside of a (different) node that's large, // as defined by having good containment on this point. drop.z += 0.2; F32 containment; // if (within(drop, sgCheckSeedLower, 3.0)) if (GraphNode * closest = mGraph.closestNode(drop, &containment)) { if (closest == antecedent) { AssertFatal(containment > 0, "GraphSeeds::seedDropOffs()"); } else { if (containment < -1.3) { GraphSeed::Type seedType = GraphSeed::DropOff; // For those close to chute nodes, we make these regular nodes // that go through full bridging process. if (mGraph.getChutes().findNear(drop, 2.2, 1.0, mNearChutes)) seedType = GraphSeed::Regular; } } } } } } } } // Procedure is then to cast across any of the walls we are outside of and see if // the point that is cast across has decent containment within this selfsame node. // Probably the containment we are looking for is a little less than the cast // across distance. If Ok, then we create the node with our given antecedent. // See crossNearbyVolumes() in graphFind.cc where most of this work happens. void GraphSeeds::seedInventory(GraphNode * antecedent) { Vector crossings; const Point3F& nodeLoc = antecedent->location(); if (S32 N = mGraph.crossNearbyVolumes(antecedent->location(), crossings)) for (S32 i = 0; i < N; i++) if (mLoser.haveLOS(nodeLoc, crossings[i])) pushSeed(antecedent, crossings[i], GraphSeed::Inventory); } S32 GraphSeeds::scatter() { for (S32 i = 0; i < mGraph.numNodes(); i++) if (GraphNode * node = mGraph.lookupNode(i)) if (node->indoor()) if (node->inventory()) seedInventory(node); else if (NavigationGraph::sSeedDropOffs) seedDropOffs(node); return size(); } // Find those seeds which have antecedents that aren't part of the largest island // in the graph. These are the seeds we will want to use. For the drop off seeds // we only attempt to bridge to their antecedent. void GraphSeeds::markUsefulSeeds() { // Make sure offsets are managed Ok - all seeds should now be nodes. AssertFatal(mOriginalCount + size() == mGraph.numNodes(), "markUsefulSeeds()"); for (iterator it = begin(); it != end(); it++) { GraphNode * antecedent = mGraph.lookupNode(it->antecedent()); if (antecedent->island() != mGraph.largestIsland()) { S32 mapIndex = mOriginalCount + (it - begin()); GraphNode * seed = mGraph.lookupNode(mapIndex); // Bridge builder uses this bit- seed->setUsefulSeed(); } } } //------------------------------------------------------------------------------------- #define SeedBoxW 0.3 #define SeedBoxH 2.0 // Here's where we're adding the seed into the (persisted) data lists. They will // take effect as run time nodes on the next call to makeGraph(). void NavigationGraph::installSeed(const Point3F& pos) { IndoorNodeInfo nodeInfo; nodeInfo.pos = pos; nodeInfo.setSeed(true); mNodeInfoList.push_back(nodeInfo); mNodeVolumes.addVolume(pos, SeedBoxW, SeedBoxH); } // // What was previously several calls from script (makeGraph, findBridges, pushBridges) // has now been rolled altogether for the purposes of trying to fill the last holes // that we are seeing in the graph. This assumes that script has already uploaded // the groundPlan and floorPlan information. // bool NavigationGraph::assemble() { sSpawnGraph = mIsSpawnGraph; Loser::mCasts = 0; // First we need a made graph for the seeder to work. makeGraph(); // Look for problem areas and scatter seeds. GraphSeeds seeds(*this); seeds.scatter(); for (S32 i = 0; i < seeds.size(); i++) installSeed(seeds[i].location()); // Remake to create the run time seed nodes. makeGraph(); // Now build bridges WITHOUT using the seed nodes- BridgeDataList bridges; GraphBridge bridgeBuilder(mNodeList, numIslands(), bridges); bridgeBuilder.heedSeeds(false); bridgeBuilder.findAllBridges(); // Set the bridges and connect. This finds largest island, which puts us - mBridgeList = bridges; pushBridges(); // - in a position to know which seeds might matter (those with stranded parents) seeds.markUsefulSeeds(); // Now bridge seed nodes- bridges.clear(); bridgeBuilder.heedSeeds(true); bridgeBuilder.findAllBridges(); // Still need to trim outdoor steep connections (this adds to bridges) bridgeBuilder.trimOutdoorSteep(); // Add the new bridges to the list and rebuild everything- mBridgeList.merge(bridges); makeGraph(); pushBridges(); // Done return 0; }