// Fleet path planning: turning an ordered list of destinations into a flight plan. // // The strategic layer does NOT search for a route. The player (or the AI) picks the // destinations; this code walks the resulting chain one leg at a time and decides, for each // consecutive pair, *how* the fleet crosses that leg -- which is the waypoint kind the save // file records -- plus whether the order is legal at all. // // The whole module is pure. Installing a flight plan mints no ids but does mutate a fleet, // a player's gate-traffic total and every ship's pending action, so the split is the same // one game/combat uses: this returns a plan, and applying it belongs to whatever owns the // object store. // // CONFIDENCE: high on the decision order, the kind table and the flag meanings; see the // per-item notes for the parts that are weaker. #pragma once #include #include namespace sots::nav { // --------------------------------------------------------------------------------------- // Waypoint kinds // --------------------------------------------------------------------------------------- // The kind stored on each waypoint. It is not a property of the leg's geometry -- for every // leg the gate rule and the node rule decline, it is simply the drive the owning species // flies, so a fleet's kind is decided by who owns it and not by where it is going. // // CONFIDENCE: high -- the mapping is a dense jump table with one entry per species. enum class WaypointKind : int { None = 0, // no crossing is possible; also the drive of the gate-building and // the non-player races, which cross in normal space StraightA = 1, // a plain straight-line drive Stutter = 2, // the Liir drive, whose speed rises with distance from a star. It has // been called "node line" across earlier work; it has nothing to do // with node lines, which is why kind 3 and only kind 3 counts as a // node waypoint NodeRoute = 3, // travel along a discovered node line; the ONLY kind that carries a // route record GateToGate = 4, // instantaneous transit between two of the player's own gates GateProjected = 5, // a gate throw at a system with NO receiving gate; arrival is a roll StraightB = 6, // a second straight-line drive, distinct only by its number }; // The seven playable/NPC species, in the order every per-species table uses. Mirrors // game/sim's Species so this module can stand alone; keep them in step. enum class Species : int { Human = 0, Hiver = 1, Tarkas = 2, Liir = 3, NPC = 4, Zuul = 5, Morrigi = 6, }; // The drive a species flies, and therefore the waypoint kind of any leg that is neither a // gate transit nor a node route. // // THIS IS THE WHOLE OF THE "why is kind 2 never seen" QUESTION. Kind 2 is the Liir drive and // nothing else produces it; the two node-drive races are Human and Zuul, both mapped to 3. So // a Human or Zuul fleet can never carry a kind-2 waypoint and a Liir fleet can never carry a // kind-3 one. There is no unreachable branch and nothing to repair -- the observations that // found only kind 3 were taken on the two races the table forces to 3. // // CONFIDENCE: high -- one jump-table entry per species, resolved individually. WaypointKind DriveOf(Species s); // The two predicates the movement step branches on. Both accept only the values below and // are false for everything else, including values outside the enum. // gate transit: 4 or 5 -- what a player's gate-traffic total counts // node: 3 only -- NOT 2, which is a common and costly mistake to make because the // stutter drive was long mis-named "node line" // CONFIDENCE: high. bool IsGateTransitKind(int kind); bool IsNodeKind(int kind); // --------------------------------------------------------------------------------------- // Why an order was refused, or merely questioned // --------------------------------------------------------------------------------------- // One bit per problem, accumulated across every leg of the order. Only three of them refuse // the order; the rest are either advisory (the player is asked to confirm) or a complaint // about route quality that the server commits anyway. // // CONFIDENCE: high -- the refusal mask is a single literal in the order path, and the UI's // dry run tests the whole word, which is what separates the two groups. enum PathFlag : unsigned { kShipActionsWillCancel = 0x001u, // advisory: giving this order cancels ship actions kNodeLegOutOfRange = 0x002u, // the existing node line is beyond remaining fuel kGateTrafficExceeded = 0x004u, // over the player's gate capacity; the leg becomes None kCannotInterceptFleet = 0x008u, // REFUSES: target fleet is on a node line we cannot meet kFleetGrounded = 0x010u, // REFUSES: a ship's drive is destroyed kNoLineAndCannotBore = 0x020u, // no node line, and the fleet cannot make one kBoredLineOutOfRange = 0x040u, // a line was made, but the leg is still out of fuel kBoreFailed = 0x080u, // making the line failed kDestSystemNotFriendly = 0x100u, // point -> a system owned by no friend of ours kSourceSystemNotFriend = 0x200u, // a system owned by no friend of ours -> point kDestPointNotPermitted = 0x400u, // REFUSES: we may not move to that deep-space point kShipActionEight = 0x800u, // advisory: one particular ship action, called out alone }; // The three bits that make the order fail. Every other bit is shown to the player but does // not stop the plan being installed -- including the gate-capacity bit, which means an order // CAN be accepted over capacity, with a None-kind first waypoint. CONFIDENCE: high. constexpr unsigned kOrderRefusalMask = kCannotInterceptFleet | kFleetGrounded | kDestPointNotPermitted; constexpr bool OrderRefused(unsigned flags) { return (flags & kOrderRefusalMask) != 0u; } // --------------------------------------------------------------------------------------- // Map objects, reduced to what the leg rule reads // --------------------------------------------------------------------------------------- struct Vec3 { double x = 0, y = 0, z = 0; }; enum class ObjectKind : int { System = 0, Fleet = 1, Point = 2 }; // A destination, source, or intermediate stop. The three shapes share a tag and a position; // each of the three adds the fields the rule reads for that shape only. struct MapObject { ObjectKind kind = ObjectKind::System; int id = 0; // the network handle; this is what a saved waypoint stores Vec3 pos{}; // --- kind == System --- int systemIndex = -1; // dense index, used to key the node-line adjacency bool ownedByUs = false; // the moving player owns it bool ownerIsFriendly = false;// its owner has a positive relation with the moving player bool weHaveGateHere = false; // the moving player's gate mask covers it bool weCanRefuelHere = false;// a tanker of ours is parked here, or the owner permits it // --- kind == Point --- // The two per-player masks a point carries. Either one permits the move, and a // non-player-race fleet is permitted regardless -- fold that bypass into the first flag. bool pointVisibleToUs = false; bool pointKnownToUs = false; // --- kind == Fleet --- bool fleetOnNodeLeg = false; // its current waypoint is a node route // The system to aim at when we can meet it; unset means the meeting could not be solved. bool interceptSolved = false; const MapObject* interceptSystem = nullptr; }; // The moving fleet, reduced likewise. struct FleetState { Species ownerSpecies = Species::Human; bool hasShips = true; // Fuel. `rangeRemaining` is drawn down leg by leg and reset to `rangeFull` on reaching a // system where the fleet may refuel; `tankCapacity` caps whatever range a single leg is // allowed to claim. double rangeRemaining = 0.0; double rangeFull = 0.0; double tankCapacity = 0.0; bool anyShipGrounded = false; // some ship's drive is destroyed // The two advisory bits come from ONE bitmask of the actions in progress, with the // single action that gets its own bit taken out first -- so a fleet whose only busy ship // is on that action raises the second bit and NOT the first. bool anyShipActingOther = false;// some ship is mid-action, on an action other than "eight" bool anyShipActionEight = false;// that one action, called out on its own bool canBoreNodeLines = false; // the fleet carries the capability to make a node line // Gate transit accounting. int gateTrafficCost = 0; // this fleet's own cost, a signed 16-bit field bool alreadyOnGateLeg = false; // its current waypoint is a gate transit, so it is // already counted and must not be counted twice int gateTrafficUsed = 0; // the owner's running total int gateTrafficCapacity = 0; // gate count times per-gate traffic double gateProjectionRadius = 0.0; // how far past a gate a fleet can be thrown // Where the fleet is parked, when that is a system. Used only by the leading-destination // drop: ordering a fleet to the system it is already at drops that destination. const struct MapObject* currentSystem = nullptr; }; // --------------------------------------------------------------------------------------- // Geometry and fuel // --------------------------------------------------------------------------------------- // The distance a leg costs the fuel budget. // // Each component difference is rounded to float32, the three squares are summed at full // precision and the SUM is rounded once, then the square root is rounded again. Because a // float32 difference has 24 significand bits, every square and their sum are exact in double, // so accumulating in double and narrowing once is bit-identical -- but the narrowing of the // differences is not optional and neither is the one on the root. // CONFIDENCE: high. double LegLength(const Vec3& a, const Vec3& b); // Whether a leg is within a fleet's fuel. // // THE ONE PLACE A FLOATING-POINT DETAIL DECIDES AN OUTCOME. The squared distance is rounded // to float32; the range is `min(available, tank capacity)` with both candidates read back // from float32 fields; and then the range is SQUARED AT FULL PRECISION and compared against // the rounded squared distance. Squaring the range in float32 as well -- the natural mirror // of every other rounding here -- disagrees exactly at the boundary, which is where a fuel // check lives. The comparison is inclusive. // CONFIDENCE: high. bool LegInRange(double squaredDistanceRange, double rangeAvailable, double tankCapacity); bool LegInRange(const Vec3& a, const Vec3& b, double rangeAvailable, double tankCapacity); // Whether a gate at `from` can throw a fleet as far as `to`. // // Requires a positive projection radius, a gate at the source, NO gate at the destination // (with one at both ends the transit is the ordinary gate-to-gate kind), and a distance // within the radius, inclusive. The distance is the same two-rounding length as everywhere // else. CONFIDENCE: high. bool GateProjectionReaches(const FleetState& f, const MapObject& from, const MapObject& to); // --------------------------------------------------------------------------------------- // One leg // --------------------------------------------------------------------------------------- // What a node-route leg records on its waypoint. Written ONLY for kind 3; every other kind // leaves the default, and that is a save-visible invariant. struct NodeRoute { int pathIndex = -1; // the node line's index, or -1 when the line was just made or an // endpoint is a deep-space point int fromId = 0; // network handle of where the leg starts int toId = 0; // network handle of where it ends }; struct LegResult { WaypointKind kind = WaypointKind::None; unsigned flags = 0u; NodeRoute route{}; // default unless kind == NodeRoute double rangeAfter = 0.0;// the fuel budget the next leg inherits }; // A caller-supplied view of the node-line graph, because the graph itself is a hash of // discovered lines that this module does not model. struct NodeGraph { virtual ~NodeGraph() = default; // The path index of a line joining the two systems that the moving player has // discovered, or -1. The original returns the FIRST match in hash-bucket order, because // its ranking term turned out not to depend on the candidate -- so an implementation is // free to return any single match, but must not pretend to rank them. virtual int FindLine(int systemIndexA, int systemIndexB) const = 0; // Attempt to make a line. Only reached when the fleet can bore and no line exists. virtual bool BoreLine(int systemIndexA, int systemIndexB) const { (void)systemIndexA; (void)systemIndexB; return false; } }; // Classify one leg. `rangeIn` is the fuel budget entering the leg; the result carries the // budget leaving it (this function does not subtract the leg's own length -- the walk does, // so that the refuel reset lands in the right order). // // The decision order, and it matters: // 1. if the destination is a fleet, try to meet it // 2. raise the advisory and grounded bits // 3. if the destination is a point we may not use, refuse it here // 4. if we have a gate at one end, this is a gate transit -- check capacity, and note that // a successful gate transit CLEARS the grounded bit, because a gate does not care // whether the fleet's own drives work // 5. if the species does not fly the node drive, the leg is simply that species' drive, // with no range check and no route record // 6. otherwise solve the single node-line hop // CONFIDENCE: high. LegResult ClassifyLeg(const FleetState& f, const MapObject& from, const MapObject& to, double rangeIn, const NodeGraph* graph); // --------------------------------------------------------------------------------------- // The whole order // --------------------------------------------------------------------------------------- struct PathPlan { std::vector kinds; // one per destination std::vector routes; // one per destination unsigned flags = 0u; int firstFailingLeg = -1; // index of the first leg that raised anything bool refused = false; // flags & kOrderRefusalMask bool droppedLeadingDestination = false; // see the note on SolvePath }; // Walk the destination chain. // // `start` is where the first leg begins -- the order path passes the fleet's own position // object. If the first destination IS the start (the fleet itself, or the system the fleet is // already parked at) it is DROPPED, and `droppedLeadingDestination` says so. // // KNOWN DEFECT IN THE ORIGINAL, reproduced here behind that flag rather than silently fixed: // the original drops the destination from its own copy of the list only. Its caller still // builds one waypoint per destination in the UNDROPPED list and pairs waypoint i with kind i, // so after a drop every kind is shifted by one and the last waypoint gets the kind the output // array was initialised with, which is None. `kinds` here has one entry per destination in the // list as passed, filled the way the original fills it -- so a caller that reproduces the // original's waypoint construction reproduces the bug, and a caller that wants the sane // behaviour can drop the leading destination itself before calling. PathPlan SolvePath(const FleetState& f, const MapObject& start, const std::vector& dests, const NodeGraph* graph); } // namespace sots::nav