载入中...
搜索中...
未找到
Sensing.cpp
浏览该文件的文档.
1#include "sensing/Sensing.h"
2#include "common/Identity.h"
6
7#include <algorithm>
8#include <cmath>
9#include <cstdint>
10#include <functional>
11#include <iomanip>
12#include <limits>
13#include <set>
14#include <simplesquirrel/simplesquirrel.hpp>
15#include <sstream>
16#include <type_traits>
17#include <utility>
18#include <variant>
19#include "common/Json.h"
20
21namespace eve::sensing {
22namespace {
23std::set<std::string> csv(std::string_view s) {
24 std::set<std::string> out;
25 std::stringstream in{std::string(s)};
26 std::string v;
27 while (std::getline(in, v, ','))
28 if (!v.empty()) out.insert(v);
29 return out;
30}
31std::string quote(std::string_view s) {
32 std::ostringstream o;
33 o << '"';
34 for (char c : s) {
35 if (c == '"' || c == '\\') o << '\\';
36 o << c;
37 }
38 return o.str() + '"';
39}
40std::string join(const std::set<std::string>& s) {
41 std::string o;
42 for (auto& v : s) {
43 if (!o.empty()) o += ',';
44 o += v;
45 }
46 return o;
47}
48bool finite(float v) { return std::isfinite(v); }
49
50template <class Ref, class Proxy, class Release>
51ssq::Table makeOwnedProxy(HSQUIRRELVM vm, eve::Result<Ref>&& reference, Release&& release) {
53
54 const Ref ref = std::move(reference).takeValue();
55 auto object = eve::script::makeOwnedSquirrelInstance<Proxy>(vm, std::make_unique<Proxy>(ref));
56 if (!object) {
57 const eve::Status status = object.status();
58 object.ignore("failed to create owned sensing proxy");
59 std::invoke(std::forward<Release>(release), ref).ignore("rollback failed owned sensing allocation");
61 }
62
63 ssq::Object owned = std::move(object).takeValue();
65 result.set("value", owned);
66 result.set("ownership", std::string("owned"));
67 result.set("ownerEpoch", static_cast<std::int64_t>(ref.ownerEpoch));
68 result.set("handle", static_cast<std::int64_t>(ref.packed()));
69 return result;
70}
71} // namespace
72namespace {
73std::vector<std::string> toVector(const std::set<std::string>& values) {
74 return std::vector<std::string>(values.begin(), values.end());
75}
76
77bool insideCone(float px, float py, const QueryCone& cone) {
78 const float dx = px - cone.x;
79 const float dy = py - cone.y;
80 const float distSq = dx * dx + dy * dy;
81 if (distSq > cone.range * cone.range) return false;
82 if (distSq == 0.f) return true;
83 const float facingLen = std::hypot(cone.dirX, cone.dirY);
84 if (!(facingLen > 0.f)) return false;
85 const float invDist = 1.f / std::sqrt(distSq);
86 const float nx = dx * invDist;
87 const float ny = dy * invDist;
88 const float fx = cone.dirX / facingLen;
89 const float fy = cone.dirY / facingLen;
90 const float dot = std::clamp(nx * fx + ny * fy, -1.f, 1.f);
91 return std::acos(dot) <= cone.halfAngle;
92}
93
94bool insideShape(const Subject& subject, const QueryShape& shape) {
95 return std::visit(
96 [&](const auto& value) -> bool {
97 using T = std::decay_t<decltype(value)>;
98 if constexpr (std::is_same_v<T, std::monostate>) {
99 return true;
100 } else if constexpr (std::is_same_v<T, QueryCircle>) {
101 const float dx = subject.x - value.x;
102 const float dy = subject.y - value.y;
103 return dx * dx + dy * dy <= value.radius * value.radius;
104 } else if constexpr (std::is_same_v<T, QueryBox>) {
105 return subject.x >= value.minX && subject.x <= value.maxX && subject.y >= value.minY &&
106 subject.y <= value.maxY;
107 } else {
108 return insideCone(subject.x, subject.y, value);
109 }
110 },
111 shape);
112}
113
114eve::Result<QuerySpec> circleSpec(float x, float y, float radius, std::string_view requireTagsCsv,
115 std::string_view excludeTagsCsv, std::string_view includeFactionsCsv,
116 std::string_view excludeFactionsCsv, std::string_view visibleTo, int limit) {
117 if (!finite(x) || !finite(y) || !finite(radius) || radius < 0.f || limit < 0) {
120 "circle query requires finite coordinates, non-negative radius and limit", "query", {}, "sensing"));
121 }
122 QuerySpec spec;
123 spec.shape = QueryCircle{x, y, radius};
124 spec.requiredTags = toVector(csv(requireTagsCsv));
125 spec.excludedTags = toVector(csv(excludeTagsCsv));
126 spec.includeFactions = toVector(csv(includeFactionsCsv));
127 spec.excludeFactions = toVector(csv(excludeFactionsCsv));
128 if (!visibleTo.empty()) spec.visibleTo = std::string(visibleTo);
129 spec.minRange = 0.f;
130 spec.maxRange = radius;
131 spec.minCount = 0;
132 spec.maxCount = static_cast<std::uint32_t>(limit);
133 spec.countPolicy = CountPolicy::TruncateToMax;
134 spec.sortKey = SortKey::DistanceAscending;
135 return eve::Result<QuerySpec>::success(std::move(spec));
136}
137
138eve::Result<QuerySpec> boxSpec(float minX, float minY, float maxX, float maxY, std::string_view requireTagsCsv,
139 std::string_view excludeTagsCsv, std::string_view includeFactionsCsv,
140 std::string_view excludeFactionsCsv, std::string_view visibleTo, int limit) {
141 if (!finite(minX) || !finite(minY) || !finite(maxX) || !finite(maxY) || minX > maxX || minY > maxY || limit < 0) {
143 eve::DiagnosticCode::InvalidArgument, "box query requires finite ordered bounds and a non-negative limit",
144 "query", {}, "sensing"));
145 }
146 QuerySpec spec;
147 spec.shape = QueryBox{minX, minY, maxX, maxY};
148 spec.requiredTags = toVector(csv(requireTagsCsv));
149 spec.excludedTags = toVector(csv(excludeTagsCsv));
150 spec.includeFactions = toVector(csv(includeFactionsCsv));
151 spec.excludeFactions = toVector(csv(excludeFactionsCsv));
152 if (!visibleTo.empty()) spec.visibleTo = std::string(visibleTo);
153 spec.minCount = 0;
154 spec.maxCount = static_cast<std::uint32_t>(limit);
155 spec.countPolicy = CountPolicy::TruncateToMax;
156 spec.sortKey = SortKey::DistanceAscending;
157 return eve::Result<QuerySpec>::success(std::move(spec));
158}
159} // namespace
160
162 if (!std::isfinite(minRange) || minRange < 0.f) {
164 "QuerySpec.minRange must be finite and >= 0",
165 "query.minRange", {}, "sensing"));
166 }
167 if (!(std::isfinite(maxRange) || maxRange == std::numeric_limits<float>::infinity()) || maxRange < minRange) {
169 eve::DiagnosticCode::InvalidArgument, "QuerySpec.maxRange must be >= minRange (finite or +inf)",
170 "query.maxRange", {}, "sensing"));
171 }
172 if (minCount > maxCount) {
174 eve::DiagnosticCode::InvalidArgument, "QuerySpec.minCount exceeds maxCount", "query.count", {}, "sensing"));
175 }
176 for (const auto& tag : requiredTags) {
177 if (tag.empty())
179 "requiredTags cannot contain empty keys",
180 "query.requiredTags", {}, "sensing"));
181 }
182 for (const auto& tag : excludedTags) {
183 if (tag.empty())
185 "excludedTags cannot contain empty keys",
186 "query.excludedTags", {}, "sensing"));
187 }
188 for (const auto& faction : includeFactions) {
189 if (faction.empty())
191 "includeFactions cannot contain empty keys",
192 "query.includeFactions", {}, "sensing"));
193 }
194 for (const auto& faction : excludeFactions) {
195 if (faction.empty())
197 "excludeFactions cannot contain empty keys",
198 "query.excludeFactions", {}, "sensing"));
199 }
200 if (visibleTo && visibleTo->empty()) {
202 "visibleTo cannot be empty when set",
203 "query.visibleTo", {}, "sensing"));
204 }
205 const auto shapeOk = std::visit(
206 [](const auto& value) -> eve::Result<void> {
207 using T = std::decay_t<decltype(value)>;
208 if constexpr (std::is_same_v<T, QueryCircle>) {
209 if (!finite(value.x) || !finite(value.y) || !finite(value.radius) || value.radius < 0.f) {
212 "QueryCircle requires finite center and non-negative radius", "query.shape", {}, "sensing"));
213 }
214 } else if constexpr (std::is_same_v<T, QueryBox>) {
215 if (!finite(value.minX) || !finite(value.minY) || !finite(value.maxX) || !finite(value.maxY) ||
216 value.minX > value.maxX || value.minY > value.maxY) {
218 "QueryBox requires finite ordered bounds",
219 "query.shape", {}, "sensing"));
220 }
221 } else if constexpr (std::is_same_v<T, QueryCone>) {
222 if (!finite(value.x) || !finite(value.y) || !finite(value.dirX) || !finite(value.dirY) ||
223 !finite(value.halfAngle) || !finite(value.range) || value.halfAngle < 0.f ||
224 value.halfAngle > 3.14159265f || value.range < 0.f || !(std::hypot(value.dirX, value.dirY) > 0.f)) {
227 "QueryCone requires finite apex/dir, halfAngle in [0,pi], non-negative range, non-zero dir",
228 "query.shape", {}, "sensing"));
229 }
230 }
232 },
233 shape);
234 if (!shapeOk) return eve::Result<void>::failure(shapeOk.status());
236}
237
238eve::Result<void> SensingWorld::upsert(std::string_view id, float x, float y, std::string_view f, std::string_view t,
239 std::string_view v) {
240 if (id.empty() || !finite(x) || !finite(y)) {
242 "subject id and finite coordinates are required",
243 "subject", {}, "sensing"));
244 }
245 Subject subject{std::string(id), x, y, std::string(f), csv(t), csv(v)};
246 subjects_[subject.id] = subject;
247 if (spatialIndex_) indexSubject(subjects_[subject.id]);
249}
250
252 results_.clear();
253 const std::string key(id);
254 if (subjects_.erase(key) == 0)
256 eve::DiagnosticCode::NotFound, "subject is not registered", "subject.id", {}, "sensing"));
257 unindexSubject(key);
259}
260
261eve::Result<void> SensingWorld::setZones(std::string_view id, std::string_view zonesCsv) {
262 const std::string key(id);
263 auto it = subjects_.find(key);
264 if (it == subjects_.end())
266 eve::DiagnosticCode::NotFound, "subject is not registered", "subject.id", {}, "sensing"));
267 auto zones = csv(zonesCsv);
268 for (const auto& zone : zones) {
269 if (zone.empty())
271 "zones cannot contain empty keys", "subject.zones",
272 {}, "sensing"));
273 if (!eve::LogicalId::parse(zone))
275 "zones entries must be valid LogicalId texts",
276 "subject.zones", {}, "sensing"));
277 }
278 it->second.zones = std::move(zones);
280}
281
283 if (!enabled) {
284 clearSpatialIndex();
286 }
287 if (!finite(cellSize) || !(cellSize > 0.f)) {
289 "spatial index cellSize must be finite and > 0",
290 "spatial.cellSize", {}, "sensing"));
291 }
292 clearSpatialIndex();
293 spatialIndex_ = std::make_unique<eve::spatial::SpatialHash2D>(cellSize);
294 rebuildSpatialIndex();
296}
297
298bool SensingWorld::spatialIndexEnabled() const noexcept { return spatialIndex_ != nullptr; }
299
300void SensingWorld::clearSpatialIndex() {
301 spatialIndex_.reset();
302 subjectSpatialIds_.clear();
303 spatialIdSubjects_.clear();
304 nextSpatialId_ = 1;
305}
306
307void SensingWorld::rebuildSpatialIndex() {
308 subjectSpatialIds_.clear();
309 spatialIdSubjects_.clear();
310 nextSpatialId_ = 1;
311 if (!spatialIndex_) return;
312 spatialIndex_->clear();
313 for (const auto& [id, subject] : subjects_) {
314 (void)id;
315 indexSubject(subject);
316 }
317}
318
319void SensingWorld::indexSubject(const Subject& subject) {
320 if (!spatialIndex_) return;
321 auto it = subjectSpatialIds_.find(subject.id);
322 if (it == subjectSpatialIds_.end()) {
323 const int sid = nextSpatialId_++;
324 subjectSpatialIds_.emplace(subject.id, sid);
325 spatialIdSubjects_.emplace(sid, subject.id);
326 spatialIndex_->insert(sid, subject.x, subject.y, subject.x, subject.y);
327 } else {
328 spatialIndex_->update(it->second, subject.x, subject.y, subject.x, subject.y);
329 }
330}
331
332void SensingWorld::unindexSubject(const std::string& id) {
333 auto it = subjectSpatialIds_.find(id);
334 if (it == subjectSpatialIds_.end()) return;
335 const int sid = it->second;
336 if (spatialIndex_) spatialIndex_->remove(sid);
337 spatialIdSubjects_.erase(sid);
338 subjectSpatialIds_.erase(it);
339}
340
341bool SensingWorld::trySpatialBroadphase(const QueryOrigin& origin, const QuerySpec& spec,
342 std::vector<const Subject*>& out) const {
343 if (!spatialIndex_) return false;
344 const bool ran = std::visit(
345 [&](const auto& value) -> bool {
346 using T = std::decay_t<decltype(value)>;
347 if constexpr (std::is_same_v<T, std::monostate>) {
348 if (!(std::isfinite(spec.maxRange))) return false;
349 spatialIndex_->queryCircle(origin.x, origin.y, spec.maxRange);
350 return true;
351 } else if constexpr (std::is_same_v<T, QueryCircle>) {
352 spatialIndex_->queryCircle(value.x, value.y, value.radius);
353 return true;
354 } else if constexpr (std::is_same_v<T, QueryBox>) {
355 spatialIndex_->queryRect(value.minX, value.minY, value.maxX, value.maxY);
356 return true;
357 } else {
358 spatialIndex_->queryCircle(value.x, value.y, value.range);
359 return true;
360 }
361 },
362 spec.shape);
363 if (!ran) return false;
364 out.clear();
365 out.reserve(static_cast<std::size_t>(spatialIndex_->getResultCount()));
366 for (int i = 0; i < spatialIndex_->getResultCount(); ++i) {
367 const int sid = spatialIndex_->getResultId(i);
368 const auto map = spatialIdSubjects_.find(sid);
369 if (map == spatialIdSubjects_.end()) continue;
370 const auto subject = subjects_.find(map->second);
371 if (subject == subjects_.end()) continue;
372 out.push_back(&subject->second);
373 }
374 return true;
375}
376
377bool SensingWorld::accepts(const Subject& subject, const QuerySpec& spec) const {
378 for (const auto& tag : spec.requiredTags)
379 if (!subject.tags.count(tag)) return false;
380 for (const auto& tag : spec.excludedTags)
381 if (subject.tags.count(tag)) return false;
382 if (!spec.includeFactions.empty()) {
383 bool included = false;
384 for (const auto& faction : spec.includeFactions)
385 if (subject.faction == faction) {
386 included = true;
387 break;
388 }
389 if (!included) return false;
390 }
391 for (const auto& faction : spec.excludeFactions)
392 if (subject.faction == faction) return false;
393 if (spec.visibleTo && !subject.visibleTo.count(*spec.visibleTo)) return false;
394 return insideShape(subject, spec.shape);
395}
396
397void SensingWorld::publishResults(std::vector<RankedCandidate> ranked) {
398 results_.clear();
399 results_.reserve(ranked.size());
400 for (const auto& candidate : ranked)
401 results_.push_back(Candidate{candidate.id, candidate.x, candidate.y, candidate.distance});
402 lastQuery_.ranked = ranked;
403 lastQuery_.accepted = static_cast<std::uint32_t>(ranked.size());
404}
405
407 auto valid = spec.validate();
409 if (!finite(origin.x) || !finite(origin.y)) {
411 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "QueryOrigin coordinates must be finite",
412 "query.origin", {}, "sensing"));
413 }
414
415 std::vector<const Subject*> candidates;
416 const bool usedSpatial = trySpatialBroadphase(origin, spec, candidates);
417 if (!usedSpatial) {
418 candidates.clear();
419 candidates.reserve(subjects_.size());
420 for (const auto& [id, subject] : subjects_) {
421 (void)id;
422 candidates.push_back(&subject);
423 }
424 }
425
426 std::vector<RankedCandidate> ranked;
427 ranked.reserve(candidates.size());
428 for (const Subject* subjectPtr : candidates) {
429 const Subject& subject = *subjectPtr;
430 if (origin.subjectId && subject.id == *origin.subjectId) continue;
431 if (!accepts(subject, spec)) continue;
432 const float distance = std::hypot(subject.x - origin.x, subject.y - origin.y);
433 if (distance < spec.minRange || distance > spec.maxRange) continue;
434 RankedCandidate candidate;
435 candidate.id = subject.id;
436 candidate.x = subject.x;
437 candidate.y = subject.y;
438 candidate.distance = distance;
439 candidate.score = -distance;
440 candidate.scoreReason = "distance";
441 ranked.push_back(std::move(candidate));
442 }
443
445 std::sort(ranked.begin(), ranked.end(), [](const RankedCandidate& a, const RankedCandidate& b) {
446 if (a.distance != b.distance) return a.distance < b.distance;
447 return a.id < b.id;
448 });
449 } else {
450 std::sort(ranked.begin(), ranked.end(),
451 [](const RankedCandidate& a, const RankedCandidate& b) { return a.id < b.id; });
452 }
453
455 if (ranked.size() > static_cast<std::size_t>(spec.maxCount)) ranked.resize(static_cast<std::size_t>(spec.maxCount));
456 if (ranked.size() < static_cast<std::size_t>(spec.minCount)) {
459 "candidate count is below QuerySpec.minCount after truncation", "query.count", {}, "sensing"));
460 }
461 } else if (ranked.size() < static_cast<std::size_t>(spec.minCount) ||
462 ranked.size() > static_cast<std::size_t>(spec.maxCount)) {
465 "candidate count violates QuerySpec count policy", "query.count", {}, "sensing"));
466 }
467
468 lastQuery_.usedSpatial = usedSpatial;
469 lastQuery_.scanned = static_cast<std::uint32_t>(candidates.size());
470 lastQuery_.originX = origin.x;
471 lastQuery_.originY = origin.y;
472 lastQuery_.shapeKind = std::visit(
473 [](const auto& value) -> std::string {
474 using T = std::decay_t<decltype(value)>;
475 if constexpr (std::is_same_v<T, QueryCircle>) return "circle";
476 if constexpr (std::is_same_v<T, QueryBox>) return "box";
477 if constexpr (std::is_same_v<T, QueryCone>) return "cone";
478 return "none";
479 },
480 spec.shape);
481
482 publishResults(ranked);
484}
485
486eve::Result<int> SensingWorld::circle(float x, float y, float radius, std::string_view r, std::string_view e,
487 std::string_view i, std::string_view ef, std::string_view v, int limit) {
488 auto spec = circleSpec(x, y, radius, r, e, i, ef, v, limit);
489 if (!spec) return eve::Result<int>::failure(spec.status());
490 auto result = query(QueryOrigin{x, y, std::nullopt}, std::move(spec).takeValue());
491 if (!result) return eve::Result<int>::failure(result.status());
492 return eve::Result<int>::success(static_cast<int>(std::move(result).takeValue().size()));
493}
494
495eve::Result<int> SensingWorld::box(float a, float b, float c, float d, std::string_view r, std::string_view e,
496 std::string_view i, std::string_view ef, std::string_view v, int limit) {
497 auto spec = boxSpec(a, b, c, d, r, e, i, ef, v, limit);
498 if (!spec) return eve::Result<int>::failure(spec.status());
499 const float originX = (a + c) * 0.5f;
500 const float originY = (b + d) * 0.5f;
501 auto result = query(QueryOrigin{originX, originY, std::nullopt}, std::move(spec).takeValue());
502 if (!result) return eve::Result<int>::failure(result.status());
503 return eve::Result<int>::success(static_cast<int>(std::move(result).takeValue().size()));
504}
505
506eve::Result<int> SensingWorld::executePreset(std::string_view presetId, float originX, float originY, float dirX,
507 float dirY) {
509 context.world = this;
510 context.origin = QueryOrigin{originX, originY, std::nullopt};
511 context.dirX = dirX;
512 context.dirY = dirY;
513 auto executed = TargetingPipeline::sharedBuiltins().executePreset(context, presetId);
514 if (!executed) return eve::Result<int>::failure(executed.status());
515 auto rankedResult = std::move(executed).takeValue();
516 std::vector<RankedCandidate> ranked(rankedResult.ranked().begin(), rankedResult.ranked().end());
517 const int count = static_cast<int>(ranked.size());
518 publishResults(std::move(ranked));
520}
521
523 return i >= 0 && size_t(i) < results_.size() ? eve::OptionalRef<const Candidate>(std::cref(results_[size_t(i)]))
525}
527 std::ostringstream o;
528 o << std::setprecision(9);
529 o << "{\"schema\":\"eve.sensing.lastQuery\",\"version\":1"
530 << ",\"origin\":{\"x\":" << lastQuery_.originX << ",\"y\":" << lastQuery_.originY << '}'
531 << ",\"shape\":" << quote(lastQuery_.shapeKind)
532 << ",\"spatial\":{\"enabled\":" << (spatialIndex_ ? "true" : "false")
533 << ",\"used\":" << (lastQuery_.usedSpatial ? "true" : "false")
534 << ",\"scanned\":" << lastQuery_.scanned
535 << ",\"accepted\":" << lastQuery_.accepted << '}'
536 << ",\"ranked\":[";
537 bool first = true;
538 for (const auto& candidate : lastQuery_.ranked) {
539 if (!first) o << ',';
540 first = false;
541 o << "{\"id\":" << quote(candidate.id) << ",\"x\":" << candidate.x << ",\"y\":" << candidate.y
542 << ",\"distance\":" << candidate.distance << ",\"score\":" << candidate.score
543 << ",\"scoreReason\":" << quote(candidate.scoreReason) << '}';
544 }
545 o << "]}";
546 return o.str();
547}
548
549std::string SensingWorld::snapshotJson() const {
550 std::ostringstream o;
551 o << "{\"schema\":\"eve.sensing.world\",\"version\":1,\"subjects\":[";
552 bool first = true;
553 for (auto& [id, s] : subjects_) {
554 if (!first) o << ',';
555 first = false;
556 o << "{\"id\":" << quote(id) << ",\"x\":" << std::setprecision(9) << s.x << ",\"y\":" << s.y
557 << ",\"faction\":" << quote(s.faction) << ",\"tags\":" << quote(join(s.tags))
558 << ",\"visibleTo\":" << quote(join(s.visibleTo)) << ",\"zones\":" << quote(join(s.zones)) << '}';
559 }
560 return o.str() + "]}";
561}
563 std::string err;
564 auto d = eve::json::Document::parse(j, &err);
565 if (!d.valid() || !d.root().isObject() || d.root().getString("schema") != "eve.sensing.world" ||
566 d.root().getInt("version") != 1 || !d.root().get("subjects").isArray()) {
568 eve::DiagnosticCode::ParseError, err.empty() ? "invalid snapshot" : err, {}, {}, "sensing.restore"));
569 }
570 SensingWorld next;
571 auto a = d.root().get("subjects");
572 for (size_t n = 0; n < a.size(); ++n) {
573 auto v = a.at(n);
574 if (!v.isObject()) {
576 eve::DiagnosticCode::InvalidArgument, "invalid snapshot subject", {}, {}, "sensing.restore"));
577 }
578 auto subject = next.upsert(v.getString("id"), float(v.getDouble("x")), float(v.getDouble("y")),
579 v.getString("faction"), v.getString("tags"), v.getString("visibleTo"));
580 if (!subject.ok())
582 eve::DiagnosticCode::InvalidArgument, "invalid snapshot subject", {}, {}, "sensing.restore"));
583 if (v.has("zones")) {
584 auto zones = next.setZones(v.getString("id"), v.getString("zones"));
585 if (!zones.ok())
587 eve::DiagnosticCode::InvalidArgument, "invalid snapshot subject zones", {}, {},
588 "sensing.restore"));
589 }
590 }
591 subjects_ = std::move(next.subjects_);
592 results_.clear();
593 lastQuery_ = {};
594 if (spatialIndex_) rebuildSpatialIndex();
596}
598 return Sensing::create()->worlds_.emplace(std::make_unique<SensingWorld>());
599}
600
602 Sensing* module = ModuleManager::getInstance<Sensing>("Sensing");
603 return module ? module->worlds_.resolve(reference) : eve::script::Borrowed<SensingWorld>();
604}
605
607 Sensing* module = ModuleManager::getInstance<Sensing>("Sensing");
608 if (!module)
610 eve::DiagnosticCode::StaleHandle, "Sensing module is no longer loaded", "world", {}, "sensing"));
611 return module->worlds_.erase(reference);
612}
613
615 if (!reference.isValid()) return false;
616 Sensing* module = ModuleManager::getInstance<Sensing>("Sensing");
617 return !module || module->worlds_.isStale(reference);
618}
619
625
626Sensing::Sensing() { eve::cap::provide<eve::ISensingQuery>(this); }
627
628Sensing::~Sensing() { eve::cap::revoke<eve::ISensingQuery>(this); }
629
631 int count = 0;
632 worlds_.forEachLive([&](std::uint32_t, const SensingWorld&) { ++count; });
633 return count;
634}
635
636std::vector<eve::SensingWorldQuery> Sensing::lastQueries() const {
637 std::vector<eve::SensingWorldQuery> queries;
638 worlds_.forEachLive([&](std::uint32_t index, const SensingWorld& world) {
640 entry.index = static_cast<int>(index);
641 entry.lastQueryJson = world.debugLastQueryJson();
642 queries.push_back(std::move(entry));
643 });
644 return queries;
645}
646
648void Sensing::expose(ssq::Table& t) {
649 auto c = t.addClass<Candidate>("SensingCandidate", std::function<Candidate*()>([]() { return nullptr; }), false);
650 c.addFunc("getId", [](Candidate* v) { return v ? v->id : std::string{}; });
651 c.addFunc("getX", [](Candidate* v) { return v ? v->x : 0.f; });
652 c.addFunc("getY", [](Candidate* v) { return v ? v->y : 0.f; });
653 c.addFunc("getDistance", [](Candidate* v) { return v ? v->distance : 0.f; });
654 const HSQUIRRELVM vm = t.getHandle();
655 auto w = t.addClass<ScriptSensingWorld>("SensingWorld",
656 std::function<ScriptSensingWorld*()>([]() { return nullptr; }), false);
657 w.addFunc("ownership", [](ScriptSensingWorld*) { return std::string("owned"); });
658 w.addFunc("ownerEpoch", [](ScriptSensingWorld* value) {
659 return value ? static_cast<std::int64_t>(value->reference.ownerEpoch) : std::int64_t{0};
660 });
661 w.addFunc("handle", [](ScriptSensingWorld* value) {
662 return value ? static_cast<std::int64_t>(value->reference.packed()) : std::int64_t{0};
663 });
664 w.addFunc("isStale", [](ScriptSensingWorld* value) { return !value || Sensing::isStale(value->reference); });
665 w.addFunc("release", [vm](ScriptSensingWorld* value) {
666 if (!value)
669 "sensing world proxy must not be null", "world", {}, "sensing")));
671 });
672 w.addFunc("upsert", [vm](ScriptSensingWorld* value, const std::string& id, float x, float y,
673 const std::string& faction, const std::string& tags, const std::string& visibleTo) {
674 if (!value)
677 "sensing world proxy must not be null", "world", {}, "sensing")));
678 auto world = Sensing::resolve(value->reference);
679 if (!world.isBound())
682 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")));
683 return eve::script::projectResult(vm, world->upsert(id, x, y, faction, tags, visibleTo));
684 });
685 w.addFunc("remove", [vm](ScriptSensingWorld* value, const std::string& id) {
686 if (!value)
689 "sensing world proxy must not be null", "world", {}, "sensing")));
690 auto world = Sensing::resolve(value->reference);
691 if (!world.isBound())
694 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")));
695 return eve::script::projectResult(vm, world->remove(id));
696 });
697 w.addFunc("setZones", [vm](ScriptSensingWorld* value, const std::string& id, const std::string& zones) {
698 if (!value)
701 "sensing world proxy must not be null", "world", {}, "sensing")));
702 auto world = Sensing::resolve(value->reference);
703 if (!world.isBound())
706 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")));
707 return eve::script::projectResult(vm, world->setZones(id, zones));
708 });
709 auto projectCount = [vm](eve::Result<int>&& result) {
710 return eve::script::projectResult(vm, std::move(result),
711 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
712 };
713 w.addFunc("circle", [vm, projectCount](ScriptSensingWorld* value, float x, float y, float radius,
714 const std::string& required, const std::string& excluded,
715 const std::string& includedFactions, const std::string& excludedFactions,
716 const std::string& visibleTo, int limit) {
717 if (!value)
721 "sensing world proxy must not be null", "world", {}, "sensing")),
722 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
723 auto world = Sensing::resolve(value->reference);
724 if (!world.isBound())
726 vm,
728 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")),
729 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
730 return projectCount(
731 world->circle(x, y, radius, required, excluded, includedFactions, excludedFactions, visibleTo, limit));
732 });
733 w.addFunc("box", [vm, projectCount](ScriptSensingWorld* value, float minX, float minY, float maxX, float maxY,
734 const std::string& required, const std::string& excluded,
735 const std::string& includedFactions, const std::string& excludedFactions,
736 const std::string& visibleTo, int limit) {
737 if (!value)
741 "sensing world proxy must not be null", "world", {}, "sensing")),
742 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
743 auto world = Sensing::resolve(value->reference);
744 if (!world.isBound())
746 vm,
748 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")),
749 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
750 return projectCount(world->box(minX, minY, maxX, maxY, required, excluded, includedFactions, excludedFactions,
751 visibleTo, limit));
752 });
753 w.addFunc("executePreset", [vm, projectCount](ScriptSensingWorld* value, const std::string& presetId, float originX,
754 float originY, float dirX, float dirY) {
755 if (!value)
759 "sensing world proxy must not be null", "world", {}, "sensing")),
760 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
761 auto world = Sensing::resolve(value->reference);
762 if (!world.isBound())
764 vm,
766 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")),
767 [](int count) { return eve::Value(static_cast<std::int64_t>(count)); });
768 return projectCount(world->executePreset(presetId, originX, originY, dirX, dirY));
769 });
770 w.addFunc("resultAt", [](ScriptSensingWorld* value, int i) -> Candidate* {
771 if (!value) return nullptr;
772 auto world = Sensing::resolve(value->reference);
773 if (!world.isBound()) return nullptr;
774 auto candidate = world->resultAt(i);
775 return candidate ? const_cast<Candidate*>(&candidate->get()) : nullptr;
776 });
777 w.addFunc("snapshotJson", [vm](ScriptSensingWorld* value) {
778 if (!value)
782 "sensing world proxy must not be null", "world", {}, "sensing")),
783 [](std::string text) { return eve::Value(std::move(text)); });
784 auto world = Sensing::resolve(value->reference);
785 if (!world.isBound())
787 vm,
789 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")),
790 [](std::string text) { return eve::Value(std::move(text)); });
792 [](std::string text) { return eve::Value(std::move(text)); });
793 });
794 w.addFunc("restoreJson", [vm](ScriptSensingWorld* value, const std::string& json) {
795 if (!value)
798 "sensing world proxy must not be null", "world", {}, "sensing")));
799 auto world = Sensing::resolve(value->reference);
800 if (!world.isBound())
803 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")));
804 return eve::script::projectResult(vm, world->restoreJson(json));
805 });
806 w.addFunc("setSpatialIndexEnabled", [vm](ScriptSensingWorld* value, bool enabled, float cellSize) {
807 if (!value)
810 "sensing world proxy must not be null", "world", {}, "sensing")));
811 auto world = Sensing::resolve(value->reference);
812 if (!world.isBound())
815 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")));
816 return eve::script::projectResult(vm, world->setSpatialIndexEnabled(enabled, cellSize));
817 });
818 w.addFunc("spatialIndexEnabled", [](ScriptSensingWorld* value) {
819 if (!value) return false;
820 auto world = Sensing::resolve(value->reference);
821 return world.isBound() && world->spatialIndexEnabled();
822 });
823 w.addFunc("debugLastQueryJson", [vm](ScriptSensingWorld* value) {
824 if (!value)
828 "sensing world proxy must not be null", "world", {}, "sensing")),
829 [](std::string text) { return eve::Value(std::move(text)); });
830 auto world = Sensing::resolve(value->reference);
831 if (!world.isBound())
833 vm,
835 eve::DiagnosticCode::StaleHandle, "sensing world handle is stale", "world", {}, "sensing")),
836 [](std::string text) { return eve::Value(std::move(text)); });
838 [](std::string text) { return eve::Value(std::move(text)); });
839 });
840 auto cls = t.addClass(name, Sensing::create, false);
841 expose(cls);
842}
843void Sensing::expose(ssq::Class& c) {
844 c.addFunc("getName", &Sensing::getName);
845 c.addFunc("newWorld", [vm = c.getHandle()](Sensing*) {
846 return makeOwnedProxy<SensingWorldHandleRef, ScriptSensingWorld>(
847 vm, Sensing::newWorld(), [](SensingWorldHandleRef reference) { return Sensing::release(reference); });
848 });
849}
850} // namespace eve::sensing
double value
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
int subject
Definition AnimSmr.cpp:163
ActiveSource owned
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
float py
float nx
float ny
struct SQVM * HSQUIRRELVM
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::map< std::string, Var > values
std::uint32_t key
ShaderImageInput shape
wgpu::PopErrorScopeStatus status
glm::vec3 n
Definition Grass.cpp:63
double r
float v
std::int32_t c
std::int32_t first
Strong identity values shared by persistence, content and logical-name APIs.
std::vector< Colorf > px
std::string text
char quote
bool required
std::string name
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
const std::string * accepts
const std::string * tag
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
bool finite
float f
World3D * world
float radius
std::string id
Definition PlayHost.cpp:108
std::function< eve::Result< void >()> release
Definition Procgen.cpp:86
float d
float t
float distSq
V3 origin
Definition RoadBake.cpp:138
float dy
float dx
std::uint32_t count
The single Squirrel projection for common Result, Status and Value.
Ordered Select/Filter/Sort task pipeline over SensingWorld candidates.
int limit
Definition TreeMesh.cpp:164
float size
Definition TreeMesh.cpp:156
uint32_t index
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
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
virtual std::string getName() const =0
Returns the name.
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
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
Non-owning C++ observation with no implicit lifetime extension.
Owning ranked result; does not assign a primary target.
Definition Sensing.h:134
Gameplay-facing 2D candidate query service; it never values or selects targets.
Definition Sensing.h:151
eve::Result< CandidateQueryResult > query(const QueryOrigin &origin, const QuerySpec &spec)
Runs a configurable candidate query.
Definition Sensing.cpp:406
eve::Result< void > restoreJson(const std::string &json)
Restores a snapshot transactionally.
Definition Sensing.cpp:562
eve::Result< void > setSpatialIndexEnabled(bool enabled, float cellSize=64.f)
Enables or disables an optional SpatialHash2D broadphase (off by default).
Definition Sensing.cpp:282
eve::Result< int > circle(float x, float y, float radius, std::string_view requireTagsCsv, std::string_view excludeTagsCsv, std::string_view includeFactionsCsv, std::string_view excludeFactionsCsv, std::string_view visibleTo, int limit)
Queries a circle. Filters are CSV; empty fields disable that filter.
Definition Sensing.cpp:486
eve::Result< void > setZones(std::string_view id, std::string_view zonesCsv)
Replaces logical zone membership for an existing subject.
Definition Sensing.cpp:261
std::string snapshotJson() const
Exports deterministic JSON.
Definition Sensing.cpp:549
eve::Result< int > executePreset(std::string_view presetId, float originX, float originY, float dirX, float dirY)
Runs a registered TargetingPreset against this world and refreshes resultAt().
Definition Sensing.cpp:506
std::string debugLastQueryJson() const
Deterministic JSON dump of the last successful query for MCP/debug overlays.
Definition Sensing.cpp:526
eve::Result< void > upsert(std::string_view id, float x, float y, std::string_view faction, std::string_view tagsCsv, std::string_view visibleToCsv)
Inserts or replaces mirrored facts. CSV fields contain comma-separated stable keys.
Definition Sensing.cpp:238
eve::Result< int > box(float minX, float minY, float maxX, float maxY, std::string_view requireTagsCsv, std::string_view excludeTagsCsv, std::string_view includeFactionsCsv, std::string_view excludeFactionsCsv, std::string_view visibleTo, int limit)
Queries an axis-aligned box with the same filters as circle().
Definition Sensing.cpp:495
eve::Result< void > remove(std::string_view id)
Removes mirrored facts, or returns NotFound when the id is absent.
Definition Sensing.cpp:251
eve::OptionalRef< const Candidate > resultAt(int index) const
Returns a candidate from the most recent query, or null for an invalid index.
Definition Sensing.cpp:522
bool spatialIndexEnabled() const noexcept
Reports whether the optional spatial broadphase is active.
Definition Sensing.cpp:298
Script factory for independent sensing worlds.
Definition Sensing.h:277
~Sensing() override
Sensing.
Definition Sensing.cpp:628
static eve::Result< SensingWorldHandleRef > newWorld()
Script factory for independent sensing worlds.
Definition Sensing.cpp:597
static bool isStale(SensingWorldHandleRef reference) noexcept
Reports whether a world reference is stale.
Definition Sensing.cpp:614
int worldCount() const override
Number of live sensing worlds created by the project.
Definition Sensing.cpp:630
Sensing()
Registers the read-only sensing capability for automation hosts.
Definition Sensing.cpp:626
static eve::script::Borrowed< SensingWorld > resolve(SensingWorldHandleRef reference) noexcept
Resolves a live sensing world as a non-owning observation.
Definition Sensing.cpp:601
std::vector< eve::SensingWorldQuery > lastQueries() const override
Last completed query of every live world, in slot order.
Definition Sensing.cpp:636
static eve::Result< void > release(SensingWorldHandleRef reference)
Releases a module-owned sensing world and invalidates its handle.
Definition Sensing.cpp:606
static TargetingPipeline & sharedBuiltins()
Process-local builtins pipeline used by SensingWorld::executePreset. @ownership Process lifetime; cal...
Result< CandidateQueryResult > executePreset(TargetingSourceContext &context, std::string_view presetId) const
Looks up a preset by id and executes it.
double dot(const Vec2 &a, const Vec2 &b)
Dot.
Definition UrbanTypes.h:38
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
@ TruncateToMax
Keep the best maxCount candidates after sorting (legacy circle/box limit).
std::variant< std::monostate, QueryCircle, QueryBox, QueryCone > QueryShape
Optional broad-phase shape; monostate means range-only around the origin.
Definition Sensing.h:88
std::optional< std::reference_wrapper< T > > OptionalRef
Optional borrowed reference; it does not extend T's lifetime.
Definition BorrowedRef.h:24
bool enabled
Last completed query of one live sensing world.
std::string lastQueryJson
A deterministic candidate returned by a spatial query.
Definition Sensing.h:38
World2D origin used for range tests and distance sorting.
Definition Sensing.h:117
Configurable candidate query against SensingWorld.
Definition Sensing.h:98
std::vector< std::string > excludedTags
Definition Sensing.h:103
CountPolicy countPolicy
Definition Sensing.h:109
std::vector< std::string > excludeFactions
Definition Sensing.h:105
std::uint32_t minCount
Definition Sensing.h:107
eve::Result< void > validate() const
Validates ranges, counts, tags and shape invariants.
Definition Sensing.cpp:161
std::vector< std::string > requiredTags
Definition Sensing.h:102
std::uint32_t maxCount
Definition Sensing.h:108
std::optional< std::string > visibleTo
Definition Sensing.h:106
std::vector< std::string > includeFactions
Definition Sensing.h:104
One ranked candidate produced by SensingWorld::query.
Definition Sensing.h:124
SensingWorldHandleRef reference
Definition Sensing.cpp:623
ScriptSensingWorld(SensingWorldHandleRef value)
Definition Sensing.cpp:621
Mirrored gameplay facts used by candidate queries.
Definition Sensing.h:28
Runtime source for one pipeline execution.