From d7111729842e40a56e3ae993848a24189ac443cd Mon Sep 17 00:00:00 2001 From: Frank Date: Mon, 17 Aug 2026 18:08:43 +0200 Subject: [PATCH] Scenery --- Moose Development/Moose/Core/Database.lua | 2 +- Moose Development/Moose/Core/Zone.lua | 2 +- .../Moose/Python/MooseBridge.lua | 181 ++++++++++++++++-- .../MooseBridgeAuftragExecutionExtension.lua | 18 +- Moose Development/Moose/Wrapper/Scenery.lua | 7 +- 5 files changed, 184 insertions(+), 26 deletions(-) diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 461d0bf24..cd395d6cd 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -418,7 +418,7 @@ do -- Zones and Pathlines --- self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName)) - + Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, ZoneData.verticies) --for i,vec2 in pairs(ZoneData.verticies) do diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 255da83f9..bcf31b706 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1180,7 +1180,7 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) local function EvaluateZone( ZoneObject ) --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 - + if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then -- Get object category. diff --git a/Moose Development/Moose/Python/MooseBridge.lua b/Moose Development/Moose/Python/MooseBridge.lua index 3835bd979..3a5dd5a4a 100644 --- a/Moose Development/Moose/Python/MooseBridge.lua +++ b/Moose Development/Moose/Python/MooseBridge.lua @@ -454,6 +454,30 @@ function MOOSE_BRIDGE:_DcsPoint(object) return self:_DcsCall(object, "getPoint") end +function MOOSE_BRIDGE:_ScenerySnapshot(object, fallback_point, fallback_name, fallback_type_name, resolution_source) + local point = self:_DcsPoint(object) or fallback_point + if not point then return nil end + local coordinates = self:_CoordinatesForPoint(point, "ll") + local descriptor = self:_DcsCall(object, "getDesc") + local name = self:_DcsCall(object, "getName") or fallback_name + local type_name = self:_DcsCall(object, "getTypeName") or fallback_type_name + return { + object_id="SCENERY:" .. safe_tostring(name), + name=name and safe_tostring(name) or nil, + type_name=type_name and safe_tostring(type_name) or nil, + display_name=type(descriptor) == "table" and descriptor.displayName or nil, + life=tonumber(self:_DcsCall(object, "getLife")), + exists=self:_DcsCall(object, "isExist"), + queryable=object ~= nil, + resolution_source=resolution_source or (object and "world_search" or "reference"), + x=coordinates.x, + y=coordinates.y, + z=coordinates.z, + latitude=coordinates.latitude, + longitude=coordinates.longitude, + } +end + function MOOSE_BRIDGE:_PointFromMooseObject(object) if not object then return nil end local coordinate = self:_SafeCall(object, "GetCoordinate") @@ -757,6 +781,34 @@ function MOOSE_BRIDGE:_MarkPoint(point, text) return {x=point.x, y=point.y or 0, z=point.z, text=mark_text} end +function MOOSE_BRIDGE:_CreateMapMarker(params) + if not trigger or not trigger.action or not trigger.action.markToAll then + error("DCS trigger.action.markToAll is not available") + end + local point = self:_DebugMarkupPoint(params.point or params) + local text = self:_OptionalString(params.text) or "MOOSE Bridge marker" + if #text > 180 then error("map.marker.create text accepts at most 180 characters") end + local coalition_id = self:_DebugMarkupCoalition(params.coalition or "all") + local read_only = params.read_only == true + local mark_id = self:_NextMarkId() + if coalition_id == -1 then + trigger.action.markToAll(mark_id, text, point, read_only, "") + else + if not trigger.action.markToCoalition then error("DCS trigger.action.markToCoalition is not available") end + trigger.action.markToCoalition(mark_id, text, point, coalition_id, read_only, "") + end + return { + action="map.marker.create", + mark_id=mark_id, + text=text, + coalition=coalition_id, + read_only=read_only, + x=point.x, + y=point.y or 0, + z=point.z, + } +end + function MOOSE_BRIDGE:_CountTable(value) if type(value) ~= "table" then return 0 end local count = 0 @@ -1175,6 +1227,20 @@ function MOOSE_BRIDGE:_ZonePolygonVertices(zone) return vertices end +function MOOSE_BRIDGE:_ZoneProperties(zone) + local source = zone and zone.Properties + if type(source) ~= "table" then return nil end + local properties = {} + for key, value in pairs(source) do + local value_type = type(value) + if value_type == "string" or value_type == "number" or value_type == "boolean" then + properties[safe_tostring(key)] = value + end + end + if next(properties) == nil then return nil end + return properties +end + function MOOSE_BRIDGE:_BuildZoneSnapshotItem(zone_name, zone, source) local name = self:_ZoneName(zone_name, zone) if not name then return nil end @@ -1187,7 +1253,7 @@ function MOOSE_BRIDGE:_BuildZoneSnapshotItem(zone_name, zone, source) local vertices = self:_ZonePolygonVertices(zone) local radius = nil if not vertices then radius = self:_SafeCall(zone, "GetRadius") or zone.radius end - local item = {object_id="ZONE:"..safe_tostring(name),dcs_name=safe_tostring(name),object_type="ZONE",category="ZONE",class_name=zone.ClassName,shape=vertices and "polygon" or "circle",source=source,radius=radius,vertices=vertices} + local item = {object_id="ZONE:"..safe_tostring(name),dcs_name=safe_tostring(name),object_type="ZONE",category="ZONE",class_name=zone.ClassName,shape=vertices and "polygon" or "circle",source=source,radius=radius,vertices=vertices,properties=self:_ZoneProperties(zone)} if point then self:_AddPointFields(item, point) end return item end @@ -1972,6 +2038,10 @@ function MOOSE_BRIDGE:RegisterDefaultCommands() return self:_MarkPoint(point, p.text or "MOOSE Bridge mark") end) + self:RegisterCommand("map.marker.create", function(cmd) + return self:_CreateMapMarker(cmd.params or {}) + end) + self:RegisterCommand("map.overlay.draw", function(cmd) return self:_DrawDebugOverlay(cmd.params or {}) end) @@ -2035,26 +2105,8 @@ function MOOSE_BRIDGE:RegisterDefaultCommands() truncated = true return false end - local point = self:_DcsPoint(object) - if point then - local coordinates = self:_CoordinatesForPoint(point, "ll") - local descriptor = self:_DcsCall(object, "getDesc") - local name = self:_DcsCall(object, "getName") - local type_name = self:_DcsCall(object, "getTypeName") - objects[#objects + 1] = { - object_id="SCENERY:" .. safe_tostring(name or (#objects + 1)), - name=name and safe_tostring(name) or nil, - type_name=type_name and safe_tostring(type_name) or nil, - display_name=type(descriptor) == "table" and descriptor.displayName or nil, - life=tonumber(self:_DcsCall(object, "getLife")), - exists=self:_DcsCall(object, "isExist"), - x=coordinates.x, - y=coordinates.y, - z=coordinates.z, - latitude=coordinates.latitude, - longitude=coordinates.longitude, - } - end + local snapshot = self:_ScenerySnapshot(object) + if snapshot then objects[#objects + 1] = snapshot end return true end) local center_coordinates = self:_CoordinatesForPoint(center, "ll") @@ -2069,6 +2121,93 @@ function MOOSE_BRIDGE:RegisterDefaultCommands() } end) + self:RegisterCommand("scenery.resolve", function(cmd) + local p = cmd.params or {} + if not world or not world.searchObjects or not world.VolumeType then + error("DCS world.searchObjects is not available") + end + if not Object or not Object.Category or Object.Category.SCENERY == nil then + error("DCS scenery object category is not available") + end + if type(p.references) ~= "table" then error("scenery.resolve requires references") end + if #p.references > 500 then error("scenery.resolve accepts at most 500 references") end + local radius = tonumber(p.search_radius_m) or 150 + if radius <= 0 or radius > 500 then error("search_radius_m must be in range 0..500") end + local objects = {} + local unresolved = {} + for index, reference in ipairs(p.references) do + if type(reference) ~= "table" then error("Invalid scenery reference at index " .. safe_tostring(index)) end + local object_id = self:_OptionalString(reference.object_id) + local prefix, expected_name = self:_SplitObjectId(object_id) + if prefix ~= "SCENERY" or not expected_name or expected_name == "" then + error("Invalid scenery object_id at index " .. safe_tostring(index)) + end + local zone_name = self:_OptionalString(reference.zone_name) + local center = nil + local zone = nil + local assigned_type_name = nil + if zone_name then + zone = ZONE and ZONE.FindByName and ZONE:FindByName(zone_name) or nil + center = self:_PointFromMooseObject(zone) + assigned_type_name = zone and self:_SafeCallArg(zone, "GetProperty", "NAME") or nil + else + local ok, point = pcall(function() return self:_DebugMarkupPoint(reference) end) + if ok then center = point end + end + local found = nil + if center then + local volume = {id=world.VolumeType.SPHERE, params={point=center, radius=radius}} + world.searchObjects(Object.Category.SCENERY, volume, function(object) + local name = self:_DcsCall(object, "getName") + if name ~= nil and safe_tostring(name) == expected_name then + found = object + return false + end + return true + end) + end + local snapshot = nil + if found then + snapshot = self:_ScenerySnapshot( + found, + center, + expected_name, + assigned_type_name, + zone_name and "mission_editor_assignment" or "saved_position" + ) + elseif zone_name and center then + -- Some fixed map models can be assigned in the Mission Editor but are + -- omitted by world.searchObjects. Keep the authoritative assignment as + -- baseline evidence while leaving its live state explicitly unknown. + snapshot = self:_ScenerySnapshot( + nil, + center, + expected_name, + assigned_type_name, + "mission_editor_assignment_unqueryable" + ) + end + if snapshot then + objects[#objects + 1] = snapshot + end + if not found then + unresolved[#unresolved + 1] = { + object_id=object_id, + zone_name=zone_name, + reason=(zone_name and center) and "assigned_object_not_queryable" + or (center and "not_found_near_reference" or "reference_position_unavailable"), + } + end + end + return { + action="scenery.resolve", + count=#objects, + unresolved_count=#unresolved, + objects=objects, + unresolved=unresolved, + } + end) + self:RegisterCommand("terrain.closest_road_points", function(cmd) local p = cmd.params or {} if not land or not land.getClosestPointOnRoads then error("DCS land.getClosestPointOnRoads is not available") end diff --git a/Moose Development/Moose/Python/MooseBridgeAuftragExecutionExtension.lua b/Moose Development/Moose/Python/MooseBridgeAuftragExecutionExtension.lua index 4c0652265..318e28da5 100644 --- a/Moose Development/Moose/Python/MooseBridgeAuftragExecutionExtension.lua +++ b/Moose Development/Moose/Python/MooseBridgeAuftragExecutionExtension.lua @@ -304,7 +304,21 @@ function MOOSE_BRIDGE:_ResolveCoordinateFromInputs(inputs) local z = inputs.z ~= nil and tonumber(inputs.z) or nil local y = inputs.y ~= nil and tonumber(inputs.y) or 0 - if not x or not z then return nil, "Coordinate target requires numeric x and z" end + if not x or not z then + local latitude = inputs.latitude ~= nil and tonumber(inputs.latitude) or nil + local longitude = inputs.longitude ~= nil and tonumber(inputs.longitude) or nil + if latitude and longitude then + if not coord or not coord.LLtoLO then return nil, "coord.LLtoLO is not available" end + local point = coord.LLtoLO({lat=latitude, lon=longitude, alt=0}) + if point then + x = tonumber(point.x) + z = tonumber(point.z) + y = tonumber(point.y) or y + end + end + end + + if not x or not z then return nil, "Coordinate target requires numeric x/z or latitude/longitude" end if not COORDINATE or not COORDINATE.New then return nil, "COORDINATE:New is not available" end return COORDINATE:New(x, y, z), nil @@ -456,6 +470,8 @@ function MOOSE_BRIDGE:_CommonAuftragCommandInputs(cmd) x=p.x or legacy_params.x, y=p.y or legacy_params.y, z=p.z or legacy_params.z, + latitude=p.latitude or legacy_params.latitude, + longitude=p.longitude or legacy_params.longitude, dropoff_x=p.dropoff_x or legacy_params.dropoff_x, dropoff_y=p.dropoff_y or legacy_params.dropoff_y, dropoff_z=p.dropoff_z or legacy_params.dropoff_z, diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index ea1f8f8f0..221990b45 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -50,7 +50,7 @@ _SCENERY = {} --@param Core.Zone#ZONE_POLYGON SceneryZone (optional) The zone object. --@return #SCENERY Scenery object. function SCENERY:Register( SceneryName, SceneryObject, SceneryZone ) - + local ID = (SceneryObject and SceneryObject.getID) and SceneryObject:getID() or SceneryName if _SCENERY[ID] and _SCENERY[ID].SceneryObject == nil then @@ -322,7 +322,10 @@ function SCENERY:FindByName(Name, Coordinate, Radius, Role, Zone) scenery = SceneryScan(Coordinate, radius, name) end - if not scenery then scenery = SCENERY:Register(Name,nil,Zone) end + if not scenery then + self:I(string.format("No scenery object %s found ==> Registering MOOSE SCENERY Object without DCS object", tostring(Name))) + scenery = SCENERY:Register(Name,nil,Zone) + end return scenery end