载入中...
搜索中...
未找到
TargetingPipeline.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
4
5#include <algorithm>
6#include <cmath>
7#include <limits>
8#include <utility>
9
10namespace eve::sensing {
11namespace {
12
13bool finite(float v) noexcept { return std::isfinite(v); }
14
15bool insideCone(float px, float py, float apexX, float apexY, float dirX, float dirY, float halfAngle,
16 float range) noexcept {
17 const float dx = px - apexX;
18 const float dy = py - apexY;
19 const float distSq = dx * dx + dy * dy;
20 if (distSq > range * range) return false;
21 if (distSq == 0.f) return true;
22 const float facingLen = std::hypot(dirX, dirY);
23 if (!(facingLen > 0.f)) return false;
24 const float invDist = 1.f / std::sqrt(distSq);
25 const float nx = dx * invDist;
26 const float ny = dy * invDist;
27 const float fx = dirX / facingLen;
28 const float fy = dirY / facingLen;
29 const float dot = std::clamp(nx * fx + ny * fy, -1.f, 1.f);
30 return std::acos(dot) <= halfAngle;
31}
32
33[[nodiscard]] bool matchesFactFilters(const Subject& subject, float distance, const QuerySpec& spec) {
34 for (const auto& tag : spec.requiredTags)
35 if (!subject.tags.count(tag)) return false;
36 for (const auto& tag : spec.excludedTags)
37 if (subject.tags.count(tag)) return false;
38 if (!spec.includeFactions.empty()) {
39 bool included = false;
40 for (const auto& faction : spec.includeFactions)
41 if (subject.faction == faction) {
42 included = true;
43 break;
44 }
45 if (!included) return false;
46 }
47 for (const auto& faction : spec.excludeFactions)
48 if (subject.faction == faction) return false;
49 if (spec.visibleTo && !subject.visibleTo.count(*spec.visibleTo)) return false;
50 if (distance < spec.minRange || distance > spec.maxRange) return false;
51 return true;
52}
53
54class SelectWorldTask final : public ITargetingTask {
55public:
56 [[nodiscard]] std::string_view id() const noexcept override { return "sensing.select.world"; }
57 [[nodiscard]] TargetingTaskKind kind() const noexcept override { return TargetingTaskKind::Select; }
58
59 [[nodiscard]] Result<void> execute(TargetingSourceContext& context, std::vector<RankedCandidate>& inout,
60 const TargetingTaskStep& step) const override {
61 if (context.world == nullptr) {
63 "sensing.select.world requires a SensingWorld",
64 "pipeline.world", {}, "sensing"));
65 }
66 QuerySpec spec = step.querySpec;
67 spec.countPolicy = CountPolicy::TruncateToMax;
68 if (spec.sortKey == SortKey::None) spec.sortKey = SortKey::DistanceAscending;
69 auto queried = context.world->query(context.origin, spec);
70 if (!queried) return Result<void>::failure(queried.status());
71 const auto ranked = queried.value().ranked();
72 inout.assign(ranked.begin(), ranked.end());
74 }
75};
76
77class FilterSpecTask final : public ITargetingTask {
78public:
79 [[nodiscard]] std::string_view id() const noexcept override { return "sensing.filter.spec"; }
80 [[nodiscard]] TargetingTaskKind kind() const noexcept override { return TargetingTaskKind::Filter; }
81
82 [[nodiscard]] Result<void> execute(TargetingSourceContext& context, std::vector<RankedCandidate>& inout,
83 const TargetingTaskStep& step) const override {
84 if (context.world == nullptr) {
86 "sensing.filter.spec requires a SensingWorld",
87 "pipeline.world", {}, "sensing"));
88 }
89 auto valid = step.querySpec.validate();
90 if (!valid) return Result<void>::failure(valid.status());
91
92 std::vector<RankedCandidate> kept;
93 kept.reserve(inout.size());
94 for (const auto& candidate : inout) {
95 const auto found = context.world->subjects().find(candidate.id);
96 if (found == context.world->subjects().end()) continue;
97 if (matchesFactFilters(found->second, candidate.distance, step.querySpec)) kept.push_back(candidate);
98 }
99 inout = std::move(kept);
101 }
102};
103
104class FilterConeTask final : public ITargetingTask {
105public:
106 [[nodiscard]] std::string_view id() const noexcept override { return "sensing.filter.cone"; }
107 [[nodiscard]] TargetingTaskKind kind() const noexcept override { return TargetingTaskKind::Filter; }
108
109 [[nodiscard]] Result<void> execute(TargetingSourceContext& context, std::vector<RankedCandidate>& inout,
110 const TargetingTaskStep& step) const override {
111 if (!finite(step.coneHalfAngle) || step.coneHalfAngle < 0.f || step.coneHalfAngle > 3.14159265f ||
112 !finite(step.coneRange) || step.coneRange < 0.f || !finite(context.dirX) || !finite(context.dirY) ||
113 !(std::hypot(context.dirX, context.dirY) > 0.f)) {
116 "sensing.filter.cone requires finite facing, halfAngle in [0,pi], range >= 0",
117 "pipeline.cone", {}, "sensing"));
118 }
119 std::vector<RankedCandidate> kept;
120 kept.reserve(inout.size());
121 for (const auto& candidate : inout) {
122 if (insideCone(candidate.x, candidate.y, context.origin.x, context.origin.y, context.dirX, context.dirY,
123 step.coneHalfAngle, step.coneRange)) {
124 kept.push_back(candidate);
125 }
126 }
127 inout = std::move(kept);
129 }
130};
131
132class FilterLosTask final : public ITargetingTask {
133public:
134 [[nodiscard]] std::string_view id() const noexcept override { return "sensing.filter.los"; }
135 [[nodiscard]] TargetingTaskKind kind() const noexcept override { return TargetingTaskKind::Filter; }
136
137 [[nodiscard]] Result<void> execute(TargetingSourceContext& context, std::vector<RankedCandidate>& inout,
138 const TargetingTaskStep& /*step*/) const override {
139 auto* los = cap::query<ILineOfSightQuery>();
140 if (los == nullptr) {
142 "sensing.filter.los requires ILineOfSightQuery capability",
143 "pipeline.los", {}, "sensing"));
144 }
145 auto from = WorldPoint::world2D(context.origin.x, context.origin.y);
146 if (!from) return Result<void>::failure(from.status());
147 std::vector<RankedCandidate> kept;
148 kept.reserve(inout.size());
149 for (const auto& candidate : inout) {
150 auto to = WorldPoint::world2D(candidate.x, candidate.y);
151 if (!to) return Result<void>::failure(to.status());
152 auto visible = los->query(TargetLocation{from.value()}, TargetLocation{to.value()});
153 if (!visible) return Result<void>::failure(visible.status());
154 if (visible.value().visible) kept.push_back(candidate);
155 }
156 inout = std::move(kept);
158 }
159};
160
161class SortDistanceTask final : public ITargetingTask {
162public:
163 [[nodiscard]] std::string_view id() const noexcept override { return "sensing.sort.distance"; }
164 [[nodiscard]] TargetingTaskKind kind() const noexcept override { return TargetingTaskKind::Sort; }
165
166 [[nodiscard]] Result<void> execute(TargetingSourceContext& /*context*/, std::vector<RankedCandidate>& inout,
167 const TargetingTaskStep& /*step*/) const override {
168 std::sort(inout.begin(), inout.end(), [](const RankedCandidate& a, const RankedCandidate& b) {
169 if (a.distance != b.distance) return a.distance < b.distance;
170 return a.id < b.id;
171 });
172 for (auto& candidate : inout) {
173 candidate.score = -candidate.distance;
174 candidate.scoreReason = "distance";
175 }
177 }
178};
179
180class SortTruncateTask final : public ITargetingTask {
181public:
182 [[nodiscard]] std::string_view id() const noexcept override { return "sensing.sort.truncate"; }
183 [[nodiscard]] TargetingTaskKind kind() const noexcept override { return TargetingTaskKind::Sort; }
184
185 [[nodiscard]] Result<void> execute(TargetingSourceContext& /*context*/, std::vector<RankedCandidate>& inout,
186 const TargetingTaskStep& step) const override {
187 if (inout.size() > static_cast<std::size_t>(step.truncateCount))
188 inout.resize(static_cast<std::size_t>(step.truncateCount));
190 }
191};
192
193} // namespace
194
196 TargetingPipeline pipeline;
197 pipeline.registerTask(std::make_unique<SelectWorldTask>()).ignore("builtin sensing.select.world");
198 pipeline.registerTask(std::make_unique<FilterSpecTask>()).ignore("builtin sensing.filter.spec");
199 pipeline.registerTask(std::make_unique<FilterConeTask>()).ignore("builtin sensing.filter.cone");
200 pipeline.registerTask(std::make_unique<FilterLosTask>()).ignore("builtin sensing.filter.los");
201 pipeline.registerTask(std::make_unique<SortDistanceTask>()).ignore("builtin sensing.sort.distance");
202 pipeline.registerTask(std::make_unique<SortTruncateTask>()).ignore("builtin sensing.sort.truncate");
203
204 TargetingPreset coneSelect;
205 coneSelect.id = "sensing.builtin.coneSelect";
206 {
207 TargetingTaskStep select;
208 select.taskId = "sensing.select.world";
209 select.querySpec.maxRange = 32.f;
210 select.querySpec.countPolicy = CountPolicy::TruncateToMax;
211 select.querySpec.sortKey = SortKey::None;
212 coneSelect.steps.push_back(std::move(select));
213 }
214 {
216 cone.taskId = "sensing.filter.cone";
217 cone.coneHalfAngle = 0.785398163f; // 45 degrees
218 cone.coneRange = 32.f;
219 coneSelect.steps.push_back(std::move(cone));
220 }
221 {
223 sort.taskId = "sensing.sort.distance";
224 coneSelect.steps.push_back(std::move(sort));
225 }
226 {
227 TargetingTaskStep truncate;
228 truncate.taskId = "sensing.sort.truncate";
229 truncate.truncateCount = 8;
230 coneSelect.steps.push_back(std::move(truncate));
231 }
232 pipeline.registerPreset(std::move(coneSelect)).ignore("builtin sensing.builtin.coneSelect");
233 return pipeline;
234}
235
237 static TargetingPipeline pipeline = withBuiltins();
238 return pipeline;
239}
240
241Result<void> TargetingPipeline::registerTask(std::unique_ptr<ITargetingTask> task) {
242 if (!task) {
244 "targeting task must not be null", "task", {}, "sensing"));
245 }
246 const std::string id{task->id()};
247 if (id.empty()) {
249 DiagnosticCode::InvalidArgument, "targeting task id must not be empty", "task.id", {}, "sensing"));
250 }
251 if (tasks_.count(id) != 0) {
253 DiagnosticCode::AlreadyExists, "targeting task id already registered", "task.id", {}, "sensing"));
254 }
255 tasks_.emplace(id, std::move(task));
257}
258
260 if (preset.id.empty()) {
262 DiagnosticCode::InvalidArgument, "targeting preset id must not be empty", "preset.id", {}, "sensing"));
263 }
264 if (preset.steps.empty()) {
266 "targeting preset must contain at least one step",
267 "preset.steps", {}, "sensing"));
268 }
269 for (const auto& step : preset.steps) {
270 if (step.taskId.empty() || tasks_.count(step.taskId) == 0) {
272 "targeting preset references unknown task id",
273 "preset.steps.taskId", {}, "sensing"));
274 }
275 }
276 presets_.insert_or_assign(preset.id, std::move(preset));
278}
279
280bool TargetingPipeline::hasPreset(std::string_view presetId) const noexcept {
281 return presets_.find(std::string(presetId)) != presets_.end();
282}
283
285 const TargetingPreset& preset) const {
286 if (context.world == nullptr) {
288 "TargetingPipeline requires a SensingWorld",
289 "pipeline.world", {}, "sensing"));
290 }
291 if (!finite(context.origin.x) || !finite(context.origin.y)) {
293 "TargetingSourceContext origin must be finite",
294 "pipeline.origin", {}, "sensing"));
295 }
296 if (preset.steps.empty()) {
298 "TargetingPreset must contain at least one step",
299 "preset.steps", {}, "sensing"));
300 }
301
302 std::vector<RankedCandidate> working;
303 for (const auto& step : preset.steps) {
304 const auto found = tasks_.find(step.taskId);
305 if (found == tasks_.end() || !found->second) {
307 Diagnostic::error(DiagnosticCode::NotFound, "TargetingPreset step task is not registered",
308 "preset.steps.taskId", {}, "sensing"));
309 }
310 auto ran = found->second->execute(context, working, step);
311 if (!ran) return Result<CandidateQueryResult>::failure(ran.status());
312 }
314}
315
317 std::string_view presetId) const {
318 const auto found = presets_.find(std::string(presetId));
319 if (found == presets_.end()) {
321 DiagnosticCode::NotFound, "targeting preset is not registered", "preset.id", {}, "sensing"));
322 }
323 return execute(context, found->second);
324}
325
326} // namespace eve::sensing
int subject
Definition AnimSmr.cpp:163
std::string from
float py
float nx
float ny
float v
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::vector< Colorf > px
TokenKind kind
Range range
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
const std::string * tag
bool finite
std::string id
Definition PlayHost.cpp:108
float distSq
bool found
float dy
float dx
std::uint32_t count
Ordered Select/Filter/Sort task pipeline over SensingWorld candidates.
bool visible
float step
Definition TreeMesh.cpp:314
const VegetationPresetContext & context
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
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
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 Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Owning ranked result; does not assign a primary target.
Definition Sensing.h:134
In-memory task + preset registry and synchronous executor.
static TargetingPipeline & sharedBuiltins()
Process-local builtins pipeline used by SensingWorld::executePreset. @ownership Process lifetime; cal...
bool hasPreset(std::string_view presetId) const noexcept
Returns whether a preset id is registered.
static TargetingPipeline withBuiltins()
Creates a pipeline with built-in sensing tasks and one coneSelect preset.
Result< void > registerTask(std::unique_ptr< ITargetingTask > task)
Registers a unique task; duplicate ids fail with AlreadyExists.
Result< CandidateQueryResult > executePreset(TargetingSourceContext &context, std::string_view presetId) const
Looks up a preset by id and executes it.
Result< CandidateQueryResult > execute(TargetingSourceContext &context, const TargetingPreset &preset) const
Executes a preset against a source context.
Result< void > registerPreset(TargetingPreset preset)
Registers or replaces a preset after validating task ids exist.
static Result< WorldPoint > world2D(float x, float y)
Creates a finite 2D world point.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
std::variant< WorldPoint, GridPoint > TargetLocation
Either a world point or a grid point, with the tag retained.
Definition Targeting.h:198
TargetingTaskKind
Task role inside a TargetingPreset.
@ TruncateToMax
Keep the best maxCount candidates after sorting (legacy circle/box limit).
std::vector< RuntimeTensor > execute(const Node &n, const std::vector< const RuntimeTensor * > &in, OnnxCompute *compute)
Execute.
Named ordered list of targeting tasks.
std::vector< TargetingTaskStep > steps
Runtime source for one pipeline execution.
Per-step parameters for a preset entry.