载入中...
搜索中...
未找到
RTSMovementGroups.cpp
浏览该文件的文档.
1#include "rts/RTS.h"
3
4#include <algorithm>
5#include <cmath>
6#include <unordered_set>
7
8namespace eve::rts {
9namespace {
10Result<void> groupFailure(const char* message) {
12}
13
14double remainingRoute(Unit& unit, const OrderRecord& order) {
15 auto motion = unit.motion();
16 auto navigation = unit.navigation();
17 WorldPosition previous{motion->x, motion->y};
18 double remaining = 0.0;
19 const auto add = [&](WorldPosition next) {
20 remaining += std::hypot(static_cast<double>(next.x) - previous.x, static_cast<double>(next.y) - previous.y);
21 previous = next;
22 };
23 if (navigation->plannedOrderId == order.id && !navigation->unreachable) {
24 for (std::size_t i = navigation->waypointIndex; i < navigation->waypoints.size(); ++i)
25 add(navigation->waypoints[i]);
26 }
27 add(order.target);
28 return remaining;
29}
30} // namespace
31
33 if (batch.units.size() < 2 || !std::isfinite(batch.leadDistance) || batch.leadDistance <= 0.f)
35 groupFailure("Movement group requires two units and positive finite lead distance").status());
37 group.state.leadDistance = batch.leadDistance;
38 group.handles.reserve(batch.units.size());
39 group.state.members.reserve(batch.units.size());
40 for (auto subject : batch.units) {
41 auto* unit = findUnit(subject);
42 if (!unit || !unit->durability()->alive || unit->containment()->container.isBound())
44 groupFailure("Movement group member must be a live uncontained owned unit").status());
45 group.handles.push_back(ecs::handle_of(unit));
46 group.epochs.push_back(unit->orders()->values.membershipEpoch_);
47 group.state.members.push_back({subject, {}});
48 }
49 movementGroups_.reserve(movementGroups_.size() + 1);
50 CommandSpec command;
51 command.kind = OrderKind::Move;
52 command.target = batch.target;
53 auto admitted = CommandFanOutSystem::fanOut(group.handles, command, batch.formation);
54 if (!admitted) return admitted;
55 for (std::size_t i = 0; i < group.state.members.size(); ++i)
56 group.state.members[i].orderId = admitted.value().orderIds[i];
57 movementGroups_.push_back(std::move(group));
58 return admitted;
59}
60
61Result<std::size_t> RTS::stepMovementGroups() {
62 // Derived factors are refreshed even when the last group dissolved. Native
63 // and Crowd motion consume the same projection after navigation/convoy.
64 {
65 auto view = ecs::View<Unit, Unit::Navigation>();
66 for (auto it = view.begin(); it != view.end(); ++it) {
67 auto [navigation] = *it;
68 navigation->formationSpeedFactor = 1.f;
69 navigation->formationTarget.reset();
70 }
71 }
72 std::size_t processed = 0;
73 for (auto& group : movementGroups_) {
74 MovementGroup active;
75 active.state.leadDistance = group.state.leadDistance;
76 std::vector<double> distances;
77 std::vector<WorldPosition> targets;
78 double offsetX = 0.0, offsetY = 0.0;
79 bool openTravel = true;
80 double furthest = 0.0;
81 for (std::size_t i = 0; i < group.handles.size(); ++i) {
82 auto* unit = dynamic_cast<Unit*>(ecs::try_get(group.handles[i]));
83 if (!unit || !unit->durability()->alive || unit->containment()->container.isBound() ||
84 unit->orders()->values.membershipEpoch_ != group.epochs[i])
85 continue;
86 auto current = systems_internal::readCurrent(unit->orders()->values);
87 if (!current) return Result<std::size_t>::failure(current.status());
88 if (!current.value() || current.value()->id != group.state.members[i].orderId ||
89 current.value()->kind != OrderKind::Move)
90 continue;
91 auto navigation = unit->navigation();
92 if (pathfinder_ && navigation->plannedOrderId == current.value()->id && !navigation->unreachable &&
93 navigation->waypointIndex + 1 < navigation->waypoints.size()) {
94 const WorldPosition position{unit->motion()->x, unit->motion()->y};
95 const auto waypoint = navigation->waypoints[navigation->waypointIndex];
96 const double tolerance =
97 std::max(static_cast<double>(unit->motion()->arrivalRadius),
98 static_cast<double>(unit->crowd()->radius) + navigationGrid_.cellSize * 0.1);
99 if (std::hypot(static_cast<double>(position.x) - waypoint.x,
100 static_cast<double>(position.y) - waypoint.y) <= tolerance &&
101 systems_internal::isFormationSegmentClear(*pathfinder_, navigationGrid_, position,
102 navigation->waypoints[navigation->waypointIndex + 1],
103 unit->crowd()->radius))
104 ++navigation->waypointIndex;
105 }
106 const double remaining = remainingRoute(*unit, *current.value());
107 if (!std::isfinite(remaining))
109 groupFailure("Movement group route distance must be finite").status());
110 // A traffic loser must not stop the winner from clearing its route.
111 // Keep membership so ordinary pacing resumes after the wait ends.
112 const bool yielding = unit->navigation()->trafficWaiting || unit->supply()->convoyWaiting ||
113 (unit->morale()->retreating && unit->tactics()->retreatCovering);
114 if (!yielding) furthest = std::max(furthest, remaining);
115 openTravel = openTravel && !yielding;
116 targets.push_back(current.value()->target);
117 offsetX += static_cast<double>(unit->motion()->x) - current.value()->target.x;
118 offsetY += static_cast<double>(unit->motion()->y) - current.value()->target.y;
119 distances.push_back(remaining);
120 active.handles.push_back(group.handles[i]);
121 active.epochs.push_back(group.epochs[i]);
122 active.state.members.push_back(group.state.members[i]);
123 }
124 group = std::move(active);
125 if (group.handles.size() < 2) continue;
126 offsetX /= static_cast<double>(group.handles.size());
127 offsetY /= static_cast<double>(group.handles.size());
128 const double offsetLength = std::hypot(offsetX, offsetY);
129 const double residual = offsetLength > 0.0 ? std::max(0.0, 1.0 - group.state.leadDistance / offsetLength) : 0.0;
130 std::vector<WorldPosition> movingTargets;
131 movingTargets.reserve(targets.size());
132 for (std::size_t i = 0; i < targets.size(); ++i) {
133 const WorldPosition target{static_cast<float>(targets[i].x + offsetX * residual),
134 static_cast<float>(targets[i].y + offsetY * residual)};
136 return Result<std::size_t>::failure(groupFailure("Moving formation target must be finite").status());
137 movingTargets.push_back(target);
138 if (pathfinder_ && openTravel) {
139 auto* unit = static_cast<Unit*>(ecs::try_get(group.handles[i]));
140 const WorldPosition from{unit->motion()->x, unit->motion()->y};
141 openTravel = systems_internal::isFormationSegmentClear(*pathfinder_, navigationGrid_, from, targets[i],
142 unit->crowd()->radius) &&
143 systems_internal::isFormationSegmentClear(*pathfinder_, navigationGrid_, from, target,
144 unit->crowd()->radius);
145 }
146 }
147 for (std::size_t i = 0; i < group.handles.size(); ++i) {
148 auto* unit = static_cast<Unit*>(ecs::try_get(group.handles[i]));
149 const double excess =
150 pathfinder_ && !openTravel ? 0.0 : std::max(0.0, furthest - distances[i] - group.state.leadDistance);
151 unit->navigation()->formationSpeedFactor =
152 static_cast<float>(std::clamp(1.0 - excess / group.state.leadDistance, 0.0, 1.0));
153 if (openTravel) {
154 // Translate the assigned destination slots to a shared moving
155 // anchor. The anchor is derived from current positions, so
156 // restore and member removal cannot leave an orphan leader.
157 unit->navigation()->formationTarget = movingTargets[i];
158 // A radius-clear direct route supersedes intermediate grid
159 // waypoints; retaining them would send the unit back afterward.
160 if (pathfinder_) unit->navigation()->waypointIndex = unit->navigation()->waypoints.size();
161 }
162 ++processed;
163 }
164 }
165 std::erase_if(movementGroups_, [](const auto& group) { return group.handles.size() < 2; });
166 return Result<std::size_t>::success(processed);
167}
168
169Result<std::vector<RTSMovementGroupSnapshot>> RTS::captureMovementGroups() const {
170 std::vector<RTSMovementGroupSnapshot> result;
171 for (const auto& group : movementGroups_) {
172 RTSMovementGroupSnapshot active;
173 active.leadDistance = group.state.leadDistance;
174 for (std::size_t i = 0; i < group.handles.size(); ++i) {
175 auto* unit = dynamic_cast<Unit*>(ecs::try_get(group.handles[i]));
176 if (!unit || !unit->durability()->alive || unit->containment()->container.isBound() ||
177 unit->orders()->values.membershipEpoch_ != group.epochs[i])
178 continue;
179 auto current = systems_internal::readCurrent(unit->orders()->values);
180 if (!current) return Result<std::vector<RTSMovementGroupSnapshot>>::failure(current.status());
181 if (current.value() && current.value()->kind == OrderKind::Move &&
182 current.value()->id == group.state.members[i].orderId)
183 active.members.push_back(group.state.members[i]);
184 }
185 if (active.members.size() >= 2) {
186 std::sort(active.members.begin(), active.members.end(), [](const auto& left, const auto& right) {
187 return left.subject.format() < right.subject.format();
188 });
189 result.push_back(std::move(active));
190 }
191 }
192 std::sort(result.begin(), result.end(), [](const auto& left, const auto& right) {
193 return left.members.front().subject.format() < right.members.front().subject.format();
194 });
195 return Result<std::vector<RTSMovementGroupSnapshot>>::success(std::move(result));
196}
197
198Result<std::vector<RTS::MovementGroup>> RTS::prepareMovementGroups(const RTSStateSnapshot& snapshot) const {
199 using Output = Result<std::vector<MovementGroup>>;
200 if (snapshot.version == 1 && !snapshot.movementGroups.empty())
201 return Output::failure(groupFailure("Version 1 RTS snapshots cannot contain movement groups").status());
202 std::vector<MovementGroup> result;
203 std::unordered_set<SubjectRef> claimed;
204 for (const auto& state : snapshot.movementGroups) {
205 if (state.members.size() < 2 || !std::isfinite(state.leadDistance) || state.leadDistance <= 0.f)
206 return Output::failure(groupFailure("Invalid movement-group snapshot geometry").status());
208 group.state = state;
209 for (const auto& member : state.members) {
210 auto* unit = findUnit(member.subject);
211 const auto saved = std::find_if(snapshot.units.begin(), snapshot.units.end(),
212 [&](const auto& candidate) { return candidate.subject == member.subject; });
213 if (!unit || saved == snapshot.units.end() || !claimed.insert(member.subject).second ||
214 !saved->durability.alive || saved->container.isValid())
215 return Output::failure(groupFailure("Invalid or duplicate movement-group member").status());
216 OrderComponent validator;
217 auto restored = validator.restoreState(saved->orders);
218 if (!restored) return Output::failure(restored.status());
219 auto current = validator.current();
220 if (!current || current.value().id != member.orderId || current.value().kind != OrderKind::Move)
221 return Output::failure(
222 groupFailure("Movement-group membership does not match its active order").status());
223 group.handles.push_back(ecs::handle_of(unit));
224 group.epochs.push_back(unit->orders()->values.membershipEpoch_);
225 }
226 result.push_back(std::move(group));
227 }
228 return Output::success(std::move(result));
229}
230} // namespace eve::rts
LogicalId target
bool & active
float y
Definition AnimClip.cpp:738
int subject
Definition AnimSmr.cpp:163
std::string from
building::EdgeCurveGroup group
std::string message
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
std::array< float, 3 > position
std::vector< std::int32_t > order
graphics::Canvas * previous
RTS module owner and phase-one composition profile entry point.
glm::mat4 view
double current
TacticalUnit * unit
float offsetX
float offsetY
float(ui::Theme::* member)[4]
std::map< std::string, std::span< const std::uint8_t > > members
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Result< FanOutReceipt > fanOut(std::span< const ecs::EntityHandle > unitHandles, const CommandSpec &command, const FormationSpec &formation)
Submit one command to every selected live Unit.
Unit * findUnit(SubjectRef subject) const noexcept
Resolve a module-owned unit by persistent subject identity.
Definition RTS.cpp:889
Result< FanOutReceipt > submitMovementGroup(const MovementGroupBatch &batch)
Commit a caller-built immediate Move group with coordinated pacing.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
Result< std::optional< OrderRecord > > readCurrent(OrderComponent &orders)
bool isFinitePosition(WorldPosition position)
EVENGINE_API_DOMAINS bool isFormationSegmentClear(const map::Pathfinder &pathfinder, const NavigationGrid &grid, WorldPosition from, WorldPosition to, float radius)
One domain-neutral command submitted to a unit's generic order queue.
Definition RTSTypes.h:101
WorldPosition target
Definition RTSTypes.h:103
Caller-built admission batch for an immediate coordinated Move command. @cost Linear in unit count; c...
Definition RTSSystems.h:48
float leadDistance
Positive world-space allowance before leaders slow for lagging members.
Definition RTSSystems.h:52
std::vector< SubjectRef > units
Owned stable identities; no entity pointers are retained.
Definition RTSSystems.h:49