Skip to content

Commit db90124

Browse files
Merge branch 'main' into yolowingpixie-add-get-sensors
2 parents 35a733e + 619a719 commit db90124

4 files changed

Lines changed: 263 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- UnitService: `GetSensors` RPC to expose DCS `Unit:getSensors()` data (radar/IRST/RWR/optical).
1111
- Protos for sensors: `SensorCategory`, `Sensor`, `RadarSensor`, `IrstSensor`, `RwrSensor`, `OpticalSensor`, `DetectionDistanceAir`, `Hemisphere`.
1212
- Lua: `GRPC.methods.getSensors` implementation.
13+
- World: `SearchObjects` RPC mirroring `world.searchObjects` with full volume support (sphere, box, segment, pyramid).
14+
- Request uses `dcs.common.v0.ObjectCategory[]` and `SearchVolume` (with `InputPosition` for geo points).
15+
- Response returns `dcs.common.v0.Target[]` for consistent object union across services.
16+
- Lua implementation unwraps grpcui oneof wrapper (`volume.shape`) and supports both wrapped and flattened shapes.
1317

1418
## [0.8.1] 2024-11-05
1519

lua/DCS-gRPC/methods/world.lua

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
-- RPC world actions
33
-- https://wiki.hoggitworld.com/view/DCS_singleton_world
44
--
5+
-- luacheck: globals world coalition Object env coord
56

67
local world = world
78
local coalition = coalition
9+
local Object = Object
810
local GRPC = GRPC
911

1012
GRPC.methods.getAirbases = function(params)
@@ -53,4 +55,188 @@ end
5355

5456
GRPC.methods.getTheatre = function()
5557
return GRPC.success({theatre = env.mission.theatre})
58+
end
59+
60+
-- https://wiki.hoggitworld.com/view/DCS_func_searchObjects
61+
GRPC.methods.searchObjects = function(params)
62+
GRPC.logInfo("searchObjects: invoked")
63+
if params.categories == nil or #params.categories == 0 then
64+
return GRPC.errorInvalidArgument("categories must not be empty")
65+
end
66+
67+
if params.volume == nil then
68+
return GRPC.errorInvalidArgument("volume is required")
69+
end
70+
71+
local v = params.volume
72+
local volume
73+
74+
-- Debug: log incoming volume keys
75+
do
76+
local keys = {}
77+
for k, _ in pairs(v or {}) do keys[#keys+1] = tostring(k) end
78+
GRPC.logInfo("searchObjects: volume keys="..table.concat(keys, ","))
79+
end
80+
81+
-- If grpcui/serde wrapped oneof under a `shape` table, unwrap it
82+
if v and v.shape and type(v.shape) == "table" then
83+
v = v.shape
84+
local keys = {}
85+
for k, _ in pairs(v or {}) do keys[#keys+1] = tostring(k) end
86+
GRPC.logInfo("searchObjects: unwrapped shape; inner keys="..table.concat(keys, ","))
87+
end
88+
89+
-- If grpcui sent a discriminator value, prefer it
90+
local discriminator = nil
91+
if params.volume and params.volume.shape and type(params.volume.shape) ~= "table" then
92+
if type(params.volume.shape) == "string" then
93+
discriminator = string.lower(params.volume.shape)
94+
elseif type(params.volume.shape) == "number" then
95+
-- Best-effort mapping (fallback only)
96+
local map = { [1] = "sphere", [2] = "box", [3] = "segment", [4] = "pyramid" }
97+
discriminator = map[params.volume.shape]
98+
end
99+
GRPC.logInfo("searchObjects: discriminator="..tostring(discriminator))
100+
end
101+
102+
-- Detect sphere
103+
if (discriminator == "sphere")
104+
or v.sphere ~= nil
105+
or (
106+
v.center ~= nil and v.radius ~= nil and
107+
v.min == nil and v.from == nil and v.forward == nil
108+
)
109+
then
110+
local s = v.sphere or v
111+
local center = s.center
112+
local radius = s.radius
113+
if center == nil or radius == nil then
114+
return GRPC.errorInvalidArgument("sphere center and radius are required")
115+
end
116+
local lo = coord.LLtoLO(center.lat, center.lon)
117+
GRPC.logInfo("searchObjects: volume SPHERE radius="..tostring(radius))
118+
volume = {
119+
id = world.VolumeType.SPHERE,
120+
params = {
121+
point = { x = lo.x, y = center.alt or 0, z = lo.z },
122+
radius = radius,
123+
}
124+
}
125+
-- Detect box
126+
elseif (discriminator == "box") or v.box ~= nil or (v.min ~= nil and v.max ~= nil) then
127+
local b = v.box or v
128+
local min = b.min
129+
local max = b.max
130+
if min == nil or max == nil then
131+
return GRPC.errorInvalidArgument("box min and max are required")
132+
end
133+
local minLo = coord.LLtoLO(min.lat, min.lon)
134+
local maxLo = coord.LLtoLO(max.lat, max.lon)
135+
GRPC.logInfo("searchObjects: volume BOX")
136+
volume = {
137+
id = world.VolumeType.BOX,
138+
params = {
139+
min = { x = minLo.x, y = min.alt or 0, z = minLo.z },
140+
max = { x = maxLo.x, y = max.alt or 0, z = maxLo.z },
141+
}
142+
}
143+
-- Detect segment
144+
elseif (discriminator == "segment") or v.segment ~= nil or (v.from ~= nil and v.to ~= nil) then
145+
local seg = v.segment or v
146+
local from = seg.from
147+
local to = seg.to
148+
if from == nil or to == nil then
149+
return GRPC.errorInvalidArgument("segment from and to are required")
150+
end
151+
local fromLo = coord.LLtoLO(from.lat, from.lon)
152+
local toLo = coord.LLtoLO(to.lat, to.lon)
153+
GRPC.logInfo("searchObjects: volume SEGMENT")
154+
volume = {
155+
id = world.VolumeType.SEGMENT,
156+
params = {
157+
from = { x = fromLo.x, y = from.alt or 0, z = fromLo.z },
158+
to = { x = toLo.x, y = to.alt or 0, z = toLo.z },
159+
}
160+
}
161+
-- Detect pyramid
162+
elseif (discriminator == "pyramid")
163+
or v.pyramid ~= nil
164+
or (
165+
v.center ~= nil and v.forward ~= nil and v.right ~= nil and v.up ~= nil and
166+
v.length ~= nil and (
167+
v.halfAngleHorizontal ~= nil and v.halfAngleVertical ~= nil
168+
)
169+
)
170+
then
171+
local pv = v.pyramid or v
172+
if pv.center == nil or pv.length == nil or pv.halfAngleHorizontal == nil or pv.halfAngleVertical == nil then
173+
return GRPC.errorInvalidArgument("pyramid center, length and angles are required")
174+
end
175+
176+
local fwd = pv.forward
177+
local up = pv.up
178+
local right = pv.right
179+
180+
local function normalize(vec)
181+
local mag = math.sqrt((vec.x or 0)^2 + (vec.y or 0)^2 + (vec.z or 0)^2)
182+
if mag == 0 then return { x = 0, y = 1, z = 0 } end
183+
return { x = vec.x / mag, y = vec.y / mag, z = vec.z / mag }
184+
end
185+
186+
if fwd == nil or right == nil or up == nil then
187+
return GRPC.errorInvalidArgument("pyramid requires forward/right/up vectors")
188+
end
189+
fwd = normalize(fwd)
190+
right = normalize(right)
191+
up = normalize(up)
192+
193+
local centerLo = coord.LLtoLO(pv.center.lat, pv.center.lon)
194+
GRPC.logInfo("searchObjects: volume PYRAMID length="..tostring(pv.length))
195+
volume = {
196+
id = world.VolumeType.PYRAMID,
197+
params = {
198+
pos = {
199+
p = { x = centerLo.x, y = pv.center.alt or 0, z = centerLo.z },
200+
x = fwd,
201+
y = up,
202+
z = right,
203+
},
204+
length = pv.length,
205+
halfAngleHor = pv.halfAngleHorizontal,
206+
halfAngleVer = pv.halfAngleVertical,
207+
}
208+
}
209+
else
210+
GRPC.logWarning("searchObjects: unknown volume type; expected sphere/box/segment/pyramid")
211+
return GRPC.errorInvalidArgument("unknown volume type")
212+
end
213+
214+
local result = {}
215+
local function handler(object, _)
216+
local cat = object:getCategory()
217+
if cat == Object.Category.UNIT then
218+
result[#result+1] = { target = { unit = GRPC.exporters.unit(object) } }
219+
elseif cat == Object.Category.WEAPON then
220+
result[#result+1] = { target = { weapon = GRPC.exporters.weapon(object) } }
221+
elseif cat == Object.Category.STATIC then
222+
result[#result+1] = { target = { static = GRPC.exporters.static(object) } }
223+
elseif cat == Object.Category.SCENERY then
224+
result[#result+1] = { target = { scenery = GRPC.exporters.scenery(object) } }
225+
elseif cat == Object.Category.BASE then
226+
result[#result+1] = { target = { airbase = GRPC.exporters.airbase(object) } }
227+
elseif cat == Object.Category.CARGO then
228+
result[#result+1] = { target = { cargo = GRPC.exporters.cargo() } }
229+
else
230+
result[#result+1] = { target = { unknown = GRPC.exporters.unknown(object) } }
231+
end
232+
return true
233+
end
234+
235+
for _, cat in ipairs(params.categories) do
236+
GRPC.logInfo("searchObjects: calling world.searchObjects for category="..tostring(cat))
237+
world.searchObjects(cat, volume, handler, nil)
238+
end
239+
240+
GRPC.logInfo("searchObjects: returning objects count="..tostring(#result))
241+
return GRPC.success({objects = result})
56242
end

protos/dcs/world/v0/world.proto

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ service WorldService {
1414

1515
// Returns the theatre (Map name) of the mission
1616
rpc GetTheatre(GetTheatreRequest) returns (GetTheatreResponse) {}
17+
18+
// https://wiki.hoggitworld.com/view/DCS_func_searchObjects
19+
rpc SearchObjects(SearchObjectsRequest) returns (SearchObjectsResponse) {}
1720
}
1821

1922
message GetAirbasesRequest {
@@ -36,4 +39,66 @@ message GetTheatreRequest {
3639

3740
message GetTheatreResponse {
3841
string theatre = 1;
42+
}
43+
44+
/**
45+
* Search objects inside a 3D volume by category.
46+
*
47+
*/
48+
message SearchObjectsRequest {
49+
// Object categories to search for. If empty, no objects will be returned.
50+
repeated dcs.common.v0.ObjectCategory categories = 1;
51+
52+
// The search volume.
53+
SearchVolume volume = 2;
54+
}
55+
56+
message SearchObjectsResponse {
57+
// Objects found inside the volume.
58+
repeated dcs.common.v0.Target objects = 1;
59+
}
60+
61+
// Volume used by world.searchObjects.
62+
message SearchVolume {
63+
oneof shape {
64+
SphereVolume sphere = 1;
65+
BoxVolume box = 2;
66+
SegmentVolume segment = 3;
67+
PyramidVolume pyramid = 4;
68+
}
69+
}
70+
71+
message SphereVolume {
72+
// Center point in geographic coordinates.
73+
dcs.common.v0.InputPosition center = 1;
74+
// Radius in meters.
75+
double radius = 2;
76+
}
77+
78+
message BoxVolume {
79+
// Minimum corner in geographic coordinates.
80+
dcs.common.v0.InputPosition min = 1;
81+
// Maximum corner in geographic coordinates.
82+
dcs.common.v0.InputPosition max = 2;
83+
}
84+
85+
message SegmentVolume {
86+
// Start point in geographic coordinates.
87+
dcs.common.v0.InputPosition from = 1;
88+
// End point in geographic coordinates.
89+
dcs.common.v0.InputPosition to = 2;
90+
}
91+
92+
message PyramidVolume {
93+
// Center point of the pyramid vertex in geographic coordinates.
94+
dcs.common.v0.InputPosition center = 1;
95+
// Orientation unit vectors defining the pyramid. Should be normalized.
96+
dcs.common.v0.Vector forward = 2;
97+
dcs.common.v0.Vector right = 3;
98+
dcs.common.v0.Vector up = 4;
99+
// Max distance from vertex to objects considered inside the pyramid.
100+
double length = 5;
101+
// Horizontal and vertical half-angles in radians.
102+
double half_angle_horizontal = 6;
103+
double half_angle_vertical = 7;
39104
}

src/rpc/world.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,12 @@ impl WorldService for MissionRpc {
2929
let res = self.request("getTheatre", request).await?;
3030
Ok(Response::new(res))
3131
}
32+
33+
async fn search_objects(
34+
&self,
35+
request: Request<world::v0::SearchObjectsRequest>,
36+
) -> Result<Response<world::v0::SearchObjectsResponse>, Status> {
37+
let res = self.request("searchObjects", request).await?;
38+
Ok(Response::new(res))
39+
}
3240
}

0 commit comments

Comments
 (0)