local slab_x = const.SlabSizeX local slab_y = const.SlabSizeY function GetGridMarkerTypesCombo() local marker_types = PresetGroupCombo("GridMarkerType", "Default")() ClassDescendantsList("GridMarker", function(name, class_def) local props = class_def.properties local prop = table.find_value(props, "id", "Type") table.insert_unique(marker_types,prop.default) end) return marker_types end DefineClass.VoxelSnappingObj = { __parents = { "EditorCallbackObject" } } function VoxelSnappingObj:SnapToVoxel() self:SetPos(SnapToVoxel(self:GetPos())) end function VoxelSnappingObj:EditorCallbackPlace() self:SnapToVoxel() end function VoxelSnappingObj:EditorCallbackMove() self:SnapToVoxel() end function GridMarkerFightAreaCombo(first) local markers = MapGetMarkers() local items = { first } for _, marker in ipairs(markers) do if marker.FightAreaId ~= "" then items[#items + 1] = marker.FightAreaId end end return items end DefineClass.GridMarker = { __parents = { "EditorMarker", "GameDynamicDataObject", "VoxelSnappingObj", "StripCObjectProperties", "StripComponentAttachProperties" }, enum_flags = { efCollision = false }, properties = { { category = "Grid Marker", id = "Type", name = "Type", editor = "dropdownlist", items = function() return GetGridMarkerTypesCombo() end, default = "Position" }, { category = "Grid Marker", id = "Groups", name = "Groups", editor = "string_list", items = function() return GridMarkerGroupsCombo() end, default = false, arbitrary_value = true, }, { category = "Grid Marker", id = "Group", name = "Group-for-load-compat", editor = "text", default = "", no_edit = true }, { category = "Grid Marker", id = "ID", name = "ID", editor = "text", help = "Unique ID of the marker - leave alone, unless necessary", default = "" }, { category = "Grid Marker", id = "Comment", name = "Comment", editor = "text", help = "Anything that would help you organize the markers", default = "" }, { category = "Grid Marker", id = "Documentation", editor = "documentation", dont_save = true, }, -- display collapsible Documentation at this position { category = "Marker", id = "AreaWidth", name = "Area Width", editor = "number", default = 1, help = "Defining a voxel-aligned rectangle with North-South and East-West axis" }, { category = "Marker", id = "AreaHeight", name = "Area Height", editor = "number", default = 1, help = "Defining a voxel-aligned rectangle with North-South and East-West axis" }, { category = "Marker", id = "Reachable", name = "Reachable only", editor = "bool", default = true, help = "Area of marker includes only tiles reachable from marker position, not the entire rectangle"}, { category = "Marker", id = "GroundVisuals", name = "Ground Visuals", editor = "bool", default = false, help = "Show ground mesh on the marker area"}, { category = "Marker", id = "DeployRolloverText", name = "Deploy Rollover Text", editor = "text", default = "", translate = true, no_edit = function(self) return self.Type ~= "Entrance" and self.Type ~= "DeployArea" end, help = "Show floating text when area is rollovered"}, { category = "Marker", id = "Color", no_edit = true, default = RGB(255, 255, 255), }, -- All GridMarkers can watch for a set of conditions (TriggerConditions), and execute some effects (TriggerEffects) depending on them (Trigger) { category = "Trigger Logic", id = "Trigger", name = "Trigger", editor = "dropdownlist", items = { "once", "activation", "deactivation", "always", "change", }, default = "once", help = "Effects are executed:\n once - once per game playthrough\n activation - every period when the conditions change from false to true\n always - every period when the conditions are true\n change - every time the conditions change between true and false"}, { category = "Trigger Logic", id = "TriggerConditions", name = "Trigger Conditions", editor = "nested_list", base_class = "Condition", default = false, help = "Conditions to check periodically" }, { category = "Trigger Logic", id = "SequentialTriggerEffects", name = "Execute Trigger Effects Sequentially", editor = "bool", default = true, help = "Whether effects should wait for each other when executing in order."}, { category = "Trigger Logic", id = "TriggerEffects", name = "Trigger Effects", editor = "nested_list", base_class = "Effect", default = false, help = "Effects to execute, depending of trigger and conditions result that are checked periodicaly"}, { category = "Enabled Logic", id = "EnabledConditions", name = "Enable Conditions", editor = "nested_list", base_class = "Condition", default = false, help = "Conditions that enable or disable the marker", }, { category = "Spawn Object", id = "Routine", editor = "combo", default = "Ambient", items = function (self) return UnitRoutines end, no_edit = function(self) return not IsGridMarkerWithDefenderRole(self) end }, { category = "Spawn Object", id = "RoutineArea", editor = "combo", default = "self", items = function (self) local g = table.copy(GridMarkerGroupsCombo()) g[1+#g] = "self" return g end, no_edit = function(self) return not IsGridMarkerWithDefenderRole(self) end }, { category = "Spawn Object", id = "Name", editor = "text", translate = true, default = "", lines = 1, no_edit = function(self) return not IsGridMarkerWithDefenderRole(self) end }, { category = "Spawn Object", id = "Suspicious", editor = "bool", default = false, help = "Set spawned units to Suspicious state", no_edit = function(self) return not IsGridMarkerWithDefenderRole(self) end }, { category = "Archetype", id = "Archetypes", name = "Preferred Archetypes", editor = "string_list", items = function() return PresetsCombo("EnemyRole") end, default = false, no_edit = function(self) return not IsGridMarkerWithDefenderRole(self) end, help = "Used for Defender priority markers", }, { id = "ArchetypesTriState", name="UnitRoles", category = "Archetype", editor = "set", default = false, three_state = true, items = function() return PresetsCombo("EnemyRole") end, help = "Only allow or forbid. Not functional if preffered archetypes is used."}, { category = "Archetype", id = "UnitDef", name = "Unit Definition", editor = "dropdownlist", default = false, items = function (self) return PresetsCombo("UnitDataCompositeDef") end, help = "Used for Defender priority markers", }, { category = "Fight Area", id = "FightAreaId", name = "Fight Area ID", editor = "text", default = "", help = "if non-empty the marker area will be registered as a fight area, allowing units to be assigned to it via this ID" }, { category = "Fight Area", id = "FightArea3d", name = "3D", editor = "bool", default = false, no_edit = function(self) return self.FightAreaId == "" end }, { id = "Handle", }, { id = "spawned_by_template", }, { id = "Angle", editor = "number", default = 0, scale = "deg" }, -- Required for map saving purposes only. { id = "CollectionIndex", name = "Collection Index", editor = "number", default = 0, read_only = true }, }, activation_thread = false, contour_polyline = false, area_ground_mesh = false, EditorRolloverText = "Base grid marker with area", EditorIcon = "CommonAssets/UI/Icons/radar.tga", last_conditions_eval = false, trigger_count = 0, -- debug (displayed in Ged) area_box = false, area_thickness_divisor = 30, area_positions = false, area_effect = false, area_outside_repulse = false, fl_text = false, ground_visuals = false, recalc_area_on_pass_rebuild = true, hide_reason = false, GetDocumentation = function(self) return GameMarkerDocs["GridMarker-"..self.class] or GameMarkerDocs["GridMarker-"..self.Type] end, } function GridMarker:Init() g_GridMarkersContainer:AddToLabel("GridMarker", self) g_GridMarkersContainer:AddToLabel(self.Type, self) self:UpdateVisuals(self.Type) end function GridMarker:GameInit() self.activation_thread = CreateGameTimeThread(self.TriggerThreadProc, self) end function GridMarker:Done() if IsValidThread(self.activation_thread) then DeleteThread(self.activation_thread) self.activation_thread = false end self:HideArea() RecalcGroups(self) self:RemoveFloatTxt() g_GridMarkersContainer:RemoveFromLabel("GridMarker", self) g_GridMarkersContainer:RemoveFromLabel(self.Type, self) end function GridMarker:SetPos(...) local lastPosX, lastPosY = WorldToVoxel(self:GetPos()) EditorMarker.SetPos(self, ...) self:RecalcAreaPositions() local posX, posY = WorldToVoxel(self:GetPos()) if self.Type == "BorderArea" and (lastPosX ~= posX or lastPosY ~= posY) then self:RecalcImpassableOutsideBorderArea(nil, { x = lastPosX, y = lastPosY}) end end function GridMarker:SetAreaWidth(val) self.AreaWidth = val self:RecalcAreaPositions() end function GridMarker:SetAreaHeight(val) self.AreaHeight = val self:RecalcAreaPositions() end function GetGridRangeContour(marker) local chamf_div = marker.area_thickness_divisor local contour_width = marker.Type == "BorderArea" and const.ContoursWidth or 2 * slab_x / chamf_div local radius2D = slab_x / chamf_div local contour if marker.Type == "BorderArea" then local bbox = marker:GetBBox() local mx, my, mz = marker:GetPosXYZ() local z = (mz or terrain.GetHeight(mx, my)) + const.ContoursOffsetZ local x1, y1, z1, x2, y2, z2 = bbox:xyzxyz() x1 = x1 - slab_x / 2 x2 = x2 + slab_x / 2 y1 = y1 - slab_y / 2 y2 = y2 + slab_y / 2 contour = { GetMapBorderPstr(box(x1, y1, z, x2, y2, z), contour_width, radius2D) } elseif marker.Reachable or marker.Type == "BorderArea" then local positions = marker:GetAreaPositions(true) contour = GetRangeContour(positions, contour_width, radius2D) else local bbox = marker:GetBBox() local mx, my, mz = marker:GetPosXYZ() local z = (mz or terrain.GetHeight(mx, my)) + const.ContoursOffsetZ local x1, y1, z1, x2, y2, z2 = bbox:xyzxyz() x1 = x1 - slab_x / 2 x2 = x2 + slab_x / 2 y1 = y1 - slab_y / 2 y2 = y2 + slab_y / 2 local box_contour = GetRectContourPStr(box(x1, y1, z, x2, y2, z), contour_width, radius2D) if box_contour then contour = { box_contour } end end return contour end function GridMarker:RecalcAreaPositions(force_show) self.area_positions = false if self.Type == "BorderArea" then g_BorderAreaRangeContour = GetGridRangeContour(self) or false end if force_show or self:IsAreaVisible() then self:ShowArea() end end function GridMarker:EditorEnter() if self:GetGameFlags(const.gofPermanent) == 0 then return end EditorMarker.EditorEnter(self) self:RecalcAreaPositions() self:SetVisible(true) end function GridMarker:EditorExit() self.area_box = nil -- clear cached value if self:GetGameFlags(const.gofPermanent) == 0 then return end self:SetVisible(false) EditorMarker.EditorExit(self) self:RecalcAreaPositions() end function GridMarker:RemoveFloatTxt() if self.fl_text then self.fl_text:delete() self.fl_text = false end end function GridMarker:SetVisible(bShow) if bShow then self:UpdateVisuals(self.Type) self:SetEnumFlags(const.efVisible) if self:IsAreaVisible() then self:ShowArea() end else if not self:IsAreaVisible() then self:HideArea() end self:DestroyAttaches("Text") self:ClearEnumFlags(const.efVisible) end end function GridMarker:SetColor(clr) self.Color = clr self:SetColorModifier(clr) end function GridMarker:EditorGetText() return false end function GridMarker:GetDuplicatedStateHash() return self:CalculatePersistHash() end function GridMarker:SetType(marker_type) self:UpdateVisuals(marker_type) g_GridMarkersContainer:RemoveFromLabel(self.Type, self) self.Type = marker_type g_GridMarkersContainer:AddToLabel(marker_type, self) if self.Type == "BorderArea" then if rawget(self, "Reachable") == nil then self:SetProperty("Reachable", false) end end if self:IsAreaVisible() then self:ShowArea() end end function GridMarker:SetGroups(groups) CObject.SetGroups(self, groups) self:UpdateVisuals(self.Type) end function GridMarker:SetID(id) self.ID = id self:UpdateVisuals(self.Type) end function GridMarker:UpdateVisuals(marker_type, force) if IsChangingMap() then return end assert(marker_type) local marker_type_item = Presets.GridMarkerType.Default[marker_type] if marker_type_item and (force or marker_type ~= self.Type or self:GetEntity() ~= marker_type_item.Entity) then self:ChangeEntity(marker_type_item.Entity or self.entity) self:SetScale(marker_type_item.Scale or 100) if self.AreaWidth == self:GetDefaultPropertyValue("AreaWidth") and self.AreaHeight == self:GetDefaultPropertyValue("AreaHeight") then self.AreaWidth = marker_type_item.AreaWidth self.AreaHeight = marker_type_item.AreaHeight end if (not self.Groups or #self.Groups == 0) and marker_type_item.MarkerGroup and marker_type_item.MarkerGroup ~= "" then self:AddToGroup(marker_type_item.MarkerGroup) end self.area_thickness_divisor = MulDivRound(slab_x, 2, marker_type_item.AreaThickness) end if marker_type_item then self:SetColorModifier(marker_type_item.Color) end self:UpdateText(marker_type_item) end function GridMarker:UpdateText(marker_type_item) self:DestroyAttaches("Text") local bbox = GetEntityBoundingBox(self:GetEntity()) local ztop = bbox:max():z() + 50 * guic local text = PlaceObject("Text") self:Attach(text) if self.Groups and #self.Groups > 0 and self.ID and self.ID ~= "" then text:SetText(string.format("%s-%s", table.concat(self.Groups, ","), self.ID)) elseif self.Groups and #self.Groups > 0 then text:SetText(table.concat(self.Groups, ",")) elseif self.ID and self.ID ~= "" then text:SetText(self.ID) else text:SetText("") end if marker_type_item then text:SetColorModifier(marker_type_item.Color) end text:SetAttachOffset(point(0, 0, ztop)) end function GridMarker:SnapToVoxel() VoxelSnappingObj.SnapToVoxel(self) RefreshOverlappingGridMarkersOffset() end function GridMarker:SetAngle(angle, ...) EditorMarker.SetAngle(self, CardinalDirection(angle), ...) end function GridMarker:SetAxisAngle(axis, angle, ...) EditorMarker.SetAxisAngle(self, axis, CardinalDirection(angle), ...) end function GridMarker:TriggerThreadProc() if not self.TriggerConditions and not self.TriggerEffects then return end Sleep(1) while IsValid(self) do self:ActivateTrigger({}) Sleep(1000) end end function GridMarker:ActivateTrigger(context) if self.Trigger == "once" and self.last_conditions_eval then return end if IsSetpiecePlaying() then return end if not Game or not Game.CampaignStarted then return end local prev_conditions_eval = self.last_conditions_eval self.last_conditions_eval = self:EvaluateTriggerConditions(context) if self.Trigger == "always" or (self.Trigger == "once" and self.last_conditions_eval) or ((self.Trigger == "activation" or self.Trigger == "change") and self.last_conditions_eval and not prev_conditions_eval) or ((self.Trigger == "deactivation" or self.Trigger == "change") and not self.last_conditions_eval and prev_conditions_eval) then self:ExecuteTriggerEffects(context) end end function GridMarker:EvaluateTriggerConditions(context) return self:IsMarkerEnabled() and EvalConditionList(self.TriggerConditions, self, context) end function GridMarker:ExecuteTriggerEffects(context) self.trigger_count = self.trigger_count + 1 ObjModified(self) if self.SequentialTriggerEffects then ExecuteSequentialEffects(self.TriggerEffects, "ObjAndContext", self.handle, context) else ExecuteEffectList(self.TriggerEffects, self, context) end end -- Defender Markers, Defender Priority Markers and Villain Defender Priority markers - conditions that enable or disable them function GridMarker:IsMarkerEnabled(context) if EvalConditionList(self.EnabledConditions, self, context) then return true end return false end function GridMarker:SetDynamicData(data) self.last_conditions_eval = data.last_conditions_eval self.trigger_count = data.trigger_count end function GridMarker:GetDynamicData(data) data.last_conditions_eval = self.last_conditions_eval or nil data.trigger_count = self.trigger_count ~= 0 and self.trigger_count or nil end function GridMarker:IsVoxelInsideArea2D(x, y) local markerOnTerrain = not self:IsValidZ() local pos_voxel_x, pos_voxel_y, pos_voxel_z = WorldToVoxel(self) local area_width = self.AreaWidth local area_left = pos_voxel_x - area_width / 2 if x < area_left then return end if x >= area_left + area_width then return end local area_height = self.AreaHeight local area_top = pos_voxel_y - area_height / 2 if y < area_top then return end if y >= area_top + area_height then return end return pos_voxel_z, markerOnTerrain end function GridMarker:IsVoxelInsideArea(x, y, z) local pos_voxel_z, markerOnTerrain = self:IsVoxelInsideArea2D(x, y) if not pos_voxel_z then return false end -- If both the marker and the position being querried are on terrain, they should be considered -- on the same height regardless of the difference as the terrain can vary local passX, passY, passZ = GetPassSlabXYZ(VoxelToWorld(x, y, z)) if passX and not passZ and markerOnTerrain then return true end -- If the position or the marker isn't on terrain then the allowed difference is up to 2 Z voxel (default deviation of GetPassSlab) -- and the slab at the height of the marker at the position shouldn't be passable if z then local markerX, markerY, markerZ = GetPassSlabXYZ(VoxelToWorld(x, y, pos_voxel_z)) if markerX and markerX == passX and markerY == passY and markerZ == passZ then markerX = nil end return not markerX and abs(pos_voxel_z - z) <= 2 end return true end function GridMarker:GetMarkerCornerPositions() local positions = self:GetAreaPositions() if not positions or #positions == 0 then return {} end local pos_voxel_x, pos_voxel_y = self:GetPosXYZ() local area_width = self.AreaWidth * slab_x local area_height = self.AreaHeight * slab_y local area_left = pos_voxel_x - area_width / 2 local area_right = area_left + area_width local area_top = pos_voxel_y - area_height / 2 local area_bottom = area_top + area_height local p1 = positions[1] local x, y = point_unpack(p1) local left_top_min, left_top_min_x, left_top_min_y = p1, x, y local right_top_min, right_top_min_x, right_top_min_y = p1, x, y local left_bottom_min, left_bottom_min_x, left_bottom_min_y = p1, x, y local right_bottom_min, right_bottom_min_x, right_bottom_min_y = p1, x, y for i = 2, #positions do local p = positions[i] local x, y = point_unpack(p) if IsCloser2D(area_left, area_top, x, y, left_top_min_x, left_top_min_y) then left_top_min = p left_top_min_x = x left_top_min_y = y end if IsCloser2D(area_right, area_top, x, y, right_top_min_x, right_top_min_y) then right_top_min = p right_top_min_x = x right_top_min_y = y end if IsCloser2D(area_left, area_bottom, x, y, left_bottom_min_x, left_bottom_min_y) then left_bottom_min = p left_bottom_min_x = x left_bottom_min_y = y end if IsCloser2D(area_right, area_bottom, x, y, right_bottom_min_x, right_bottom_min_y) then right_bottom_min = p right_bottom_min_x = x right_bottom_min_y = y end end local result = { { point(point_unpack(left_top_min)) }, { point(point_unpack(right_top_min)) }, { point(point_unpack(right_bottom_min)) }, { point(point_unpack(left_bottom_min)) }, } return result end local ignore_occupied_bit = 1 local outside_repulse_bit = 2 local skip_tunnels_bit = 4 local z_tolerance_bit = 8 function GridMarker:GetAreaPositions(ignore_occupied, outside_repulse, skip_tunnels, z_tolerance) local area_positions = self.area_positions if not area_positions then area_positions = {} self.area_positions = area_positions end local key = (ignore_occupied and ignore_occupied_bit or 0) | ((outside_repulse or outside_repulse == nil and self.area_outside_repulse) and outside_repulse_bit or 0) | (skip_tunnels and skip_tunnels_bit or 0) | (z_tolerance and z_tolerance_bit or 0) local positions = area_positions[key] if positions then return positions end if z_tolerance then local p = self:GetAreaPositions(ignore_occupied, outside_repulse, skip_tunnels) positions = self:FilterZTolerance(p) if #positions == #p then area_positions[key] = p return p end elseif outside_repulse or outside_repulse == nil and self.area_outside_repulse then local p = self:GetAreaPositions(ignore_occupied, false, skip_tunnels) positions = FilterPackedPositionsRepulsionZone(p) if #positions == #p then area_positions[key] = p return p end elseif skip_tunnels then local p = self:GetAreaPositions(ignore_occupied, false, false) positions = table.ifilter(p, function(_, packed_pos) return not pf.GetTunnel(point_unpack(packed_pos)) end) if #positions == #p then area_positions[key] = p return p end elseif not (IsEditorActive() and IsDeployMarker(self)) and self.Reachable then local width = self.AreaWidth * slab_x local height = self.AreaHeight * slab_y if width == 0 or height == 0 then return empty_table end local pos = GetPassSlab(self) or self:GetPos() local area_left = pos:x() - width/2 local area_top = pos:y() - height/2 local restrict_area = box(area_left, area_top, area_left + width, area_top + height) positions = GetCombatPathDestinations(nil, pos, nil, nil, nil, nil, restrict_area, ignore_occupied, "move_through_occupied", "avoid_mines") -- Apply IsOccupiedExploration to all positions outside of combat if not ignore_occupied and not g_Combat then local allUnits = MapGet("map", "Unit") -- this is used in SpawnSquads so we can't use g_Units local unitPositionsPacked = {} for i, u in ipairs(allUnits) do if not u:IsValidPos() then goto continue end if gv_Deployment and not IsUnitDeployed(u) then goto continue end unitPositionsPacked[point_pack(SnapToVoxel(u:GetPosXYZ()))] = true ::continue:: end local total = #positions for i = 1, total do local pos = positions[i] if unitPositionsPacked[pos] then positions[i] = nil end end table.compact(positions) end else positions = self:GetAllVoxels() end -- recached if #positions == 0 then area_positions[key] = empty_table return empty_table end local values = {} for _, v in ipairs(positions) do values[v] = true end positions.values = values area_positions[key] = positions return positions end function OnMsg.OnPassabilityChanged() for _, marker in ipairs(g_GridMarkersContainer and g_GridMarkersContainer.labels.GridMarker) do marker.area_positions = false end end function GridMarker:ResetRepulseAreaPositions() local area_positions = self.area_positions if not area_positions then return end area_positions["reachable|ignore_occupied|outside_repulse|skip_tunnels"] = nil area_positions["reachable|outside_repulse|skip_tunnels"] = nil area_positions["reachable|ignore_occupied|outside_repulse"] = nil area_positions["reachable|outside_repulse"] = nil end function GridMarker:GetBBox() local sizex = self.AreaWidth * slab_x local sizey = self.AreaHeight * slab_y local posx, posy, posz = self:GetPosXYZ() local bbox = sizebox(posx - sizex / 2, posy - sizey / 2, sizex, sizey) local border_box = not IsEditorActive() and GetBorderAreaLimits() if border_box then bbox = IntersectRects(bbox, border_box) end local minx, miny, minz, maxx, maxy, maxz = bbox:xyzxyz() local x1, y1 = VoxelToWorld(WorldToVoxel(minx, miny)) local x2, y2 = VoxelToWorld(WorldToVoxel(maxx, maxy)) if x1 < minx then x1 = x1 + (minx - x1 + slab_x - 1) / slab_x * slab_x end if y1 < miny then y1 = y1 + (miny - y1 + slab_y - 1) / slab_y * slab_y end if x2 >= maxx then x2 = x2 - (x2 - maxx + slab_x) / slab_x * slab_x end if y2 >= maxy then y2 = y2 - (y2 - maxy + slab_y) / slab_y * slab_y end local z = SnapToVoxelZ(posx, posy, posz) return box(x1, y1, z, x2, y2, z) end function GridMarker:GetAllVoxels(filter) local bbox = self:GetBBox() local x1, y1, z1, x2, y2, z2 = bbox:xyzxyz() local voxels = {} local insert = table.insert for x = x1, x2, slab_x do for y = y1, y2, slab_y do insert(voxels, point_pack(x, y, z1)) end end return voxels end -- a, b, c, d - vertices per voxel; A, B, C, D - vertices of the whole marker area local corner_offs = { A = {a = point(20*guic, 20*guic, 0)}, B = {b = point(-20*guic, 20*guic, 0)}, C = {c = point(-20*guic, -20*guic, 0)}, D = {d = point(20*guic, -20*guic, 0)}, } function GridMarker:GetAreaTrianglePtOffs(i, j, width, height) local corner = i == 1 and j == 1 and "A" or i == width and j == 1 and "B" or i == width and j == height and "C" or i == 1 and j == height and "D" return corner_offs[corner] or empty_table end function GridMarker:GetAreaTriangleFadeArg(pt, center, width, height) local w = width * slab_x local h = height * slab_y local max_dist, dist if w > h then max_dist = h / 2 dist = abs(pt:y() - center:y()) else max_dist = w / 2 dist = abs(pt:x() - center:x()) end return 100 - MulDivRound(dist, 100, max_dist) end function GridMarker:GetAreaBox() local area = self.area_box if not area then local center_x, center_y = self:GetPosXYZ() local width, height = self.AreaWidth, self.AreaHeight local x = center_x - (width / 2) * slab_x - slab_x / 2 local y = center_y - (height / 2) * slab_y - slab_y / 2 area = box(x, y, x + width * slab_x, y + height * slab_y) local border = GetBorderAreaLimits() if border then area = IntersectRects(border, area) end self.area_box = area end return area end function GridMarker:GetAreaTrianglePstr() local v_pstr = pstr("") if IsDeployMarker(self) then local voxels = self:GetAreaPositions(true) local points = table.imap(voxels, function(v) return point(point_unpack(v)) end) local xAvg, yAvg = AppendVerticesAOETilesWithDF(v_pstr, points, {--[[step_objs]]}, {--[[values]]}, RGB(255, 255, 255), 100) if true then return v_pstr end end local white = const.clrWhite local center = self:GetPos() local first_x = center:x() - (self.AreaWidth/2)*slab_x - slab_x/2 local first_y = center:y() - (self.AreaHeight/2)*slab_y - slab_y/2 local first_z = center:z() local voxels = {} local z_offset = guim/4 local width, height = self.AreaWidth, self.AreaHeight if not IsEditorActive() then local border = GetBorderAreaLimits() if border then local new_box = IntersectRects(border, box(first_x, first_y, first_x + self.AreaWidth*slab_x, first_y + self.AreaHeight*slab_y)) center = new_box:Center() width = (new_box:sizex() + slab_x/2) / slab_x height = (new_box:sizey() + slab_y/2) / slab_y first_x = new_box:minx() first_y = new_box:miny() end end for i = 1, width do for j = 1, height do local x = first_x + (i - 1) * slab_x local y = first_y + (j - 1) * slab_y local a = point(x, y, SnapToVoxelZ(x, y, first_z) + z_offset) local b = point(x + slab_x, y, SnapToVoxelZ(x + slab_x, y, first_z) + z_offset) local c = point(x + slab_x, y + slab_y, SnapToVoxelZ(x + slab_x, y + slab_y, first_z) + z_offset) local d = point(x, y + slab_y, SnapToVoxelZ(x, y + slab_y, first_z) + z_offset) local offs = self:GetAreaTrianglePtOffs(i, j) local a_arg = self:GetAreaTriangleFadeArg(a, center, width, height) local b_arg = self:GetAreaTriangleFadeArg(b, center, width, height) local c_arg = self:GetAreaTriangleFadeArg(c, center, width, height) local d_arg = self:GetAreaTriangleFadeArg(d, center, width, height) v_pstr:AppendVertex(a + (offs.a or point30), white, a_arg) v_pstr:AppendVertex(b + (offs.b or point30), white, b_arg) v_pstr:AppendVertex(d + (offs.d or point30), white, d_arg) v_pstr:AppendVertex(b + (offs.b or point30), white, b_arg) v_pstr:AppendVertex(c + (offs.c or point30), white, c_arg) v_pstr:AppendVertex(d + (offs.d or point30), white, d_arg) end end return v_pstr end function GridMarker:IsAreaVisible() if IsEditorActive() then return LocalStorage.FilteredCategories.GridMarker ~= "invisible" and (table.find(mv_SelectedGridMarkers, self) or self.Type == "Entrance" or self.Type == "BorderArea") end if not self:IsMarkerEnabled() then return false end local conflict = GetSectorConflict() if self.Type == "Entrance" then -- deploy attackers if gv_DeploymentStarted and gv_Deployment == "attack" then return true end -- travel to neighbor sectors if not gv_DeploymentStarted and not (conflict and conflict.disable_travel) then local exitInteractable = MapGetMarkers("ExitZoneInteractable", self.Groups and self.Groups[1]) exitInteractable = exitInteractable and exitInteractable[1] return exitInteractable and exitInteractable:GetNextSector() end end -- deploy defender if gv_DeploymentStarted and gv_Deployment == "defend" and (self.Type == "Defender" or self.Type == "DefenderPriority") then return true end if self.Type == "BorderArea" then if self.hide_reason and next(self.hide_reason) then return false end return true end return false end function GridMarker:IsAreaShown() return not not self.contour_polyline end function GridMarker:UpdateHideReason(reason, hide) if not hide then hide = nil end if not self.hide_reason then self.hide_reason = {} end self.hide_reason[reason] = hide if self:IsAreaVisible() then self:ShowArea() else self:HideArea() end end local updateBorderMarkerVisiblity = function(reason, hide) local marker = GetBorderAreaMarker() if marker then marker:UpdateHideReason(reason, hide) end end function OnMsg.SettingActionCamera() updateBorderMarkerVisiblity("actioncamera", true) end function OnMsg.ActionCameraRemoved() updateBorderMarkerVisiblity("actioncamera", false) end function OnMsg.SetpieceStarting() updateBorderMarkerVisiblity("setpiece", true) end function OnMsg.SetpieceEnding() updateBorderMarkerVisiblity("setpiece", false) end function GridMarker:ShowArea() self:HideArea() if self.AreaWidth == 0 or self.AreaHeight == 0 then return end local marker_type = Presets.GridMarkerType.Default[self.Type] local area_color = marker_type and marker_type.Color or self.Color local shader_or_material = IsEditorActive() and "default_mesh" local contour if self.Type == "BorderArea" then shader_or_material = CRM_RangeContourControllerPreset:GetById(IsEditorActive() and "MapBorderAreaEdgeEditor" or "MapBorderAreaEdge"):Clone() shader_or_material.fade_inout_start = RealTime() shader_or_material:SetIsInside(true) contour = g_BorderAreaRangeContour else contour = GetGridRangeContour(self) end if IsDeployMarker(self) and not IsEditorActive() then self.area_ground_mesh = GridMarkerDeploymentVisuals:new({ marker = self, }) self.area_ground_mesh:SetPos(point30) else self.contour_polyline = contour and PlaceContourPolyline(contour, area_color, shader_or_material) or false if self.ground_visuals or IsEditorActive() and self.GroundVisuals then local textstyle = "DeploymentArea" local mat = false self.area_ground_mesh = PlaceGroundRectMesh(self:GetAreaTrianglePstr(), textstyle, mat) end end end function GridMarker:HideArea() if self.contour_polyline then DestroyContourPolyline(self.contour_polyline) self.contour_polyline = false end if self.area_ground_mesh then self.area_ground_mesh:delete() self.area_ground_mesh = false end end function GridMarker:IsMarkerAreaPosition(pt) if not self.Reachable then return true end local x, y, z = SnapToPassSlabXYZ(pt) local packed_pos = x and point_pack(x, y, z) or IsPoint(pt) and point_pack(pt) or point_pack(pt:GetPosXYZ()) local area_positions = self:GetAreaPositions(true) if (area_positions.values or empty_table)[packed_pos] then return true end return false end function GridMarker:IsInsideArea2D(pt) if self.Reachable then return self:IsMarkerAreaPosition(pt) else local x, y = WorldToVoxel(pt) return self:IsVoxelInsideArea2D(x, y) end end function GridMarker:IsInsideArea(pt) if self.Reachable then return self:IsMarkerAreaPosition(pt) else local x, y, z = WorldToVoxel(pt) return self:IsVoxelInsideArea(x, y, z) end end function GridMarker:OnEditorSetProperty(prop_id, old_value) if prop_id == "AreaWidth" or prop_id == "AreaHeight" or prop_id == "Reachable" or prop_id == "Color" or prop_id == "GroundVisuals" then self:RecalcAreaPositions() if prop_id == "AreaWidth" or prop_id == "AreaHeight" then self:RecalcImpassableOutsideBorderArea(prop_id, old_value) end end end local function EditorSelectObjects(objects) if not IsEditorActive() then EditorActivate() end editor.ClearSel() editor.AddToSel(objects) end function GridMarker:EditorCallbackPlace() VoxelSnappingObj.EditorCallbackPlace(self) if GedGridMarkerEditor then UpdateGedGridMarkerRoot() ObjModified(GedGridMarkerEditorRoot) end editor.ClearSel() editor.AddToSel({self}) end function GridMarker:EditorCallbackClone(marker) if GedGridMarkerEditor then UpdateGedGridMarkerRoot("with_cursor_obj") ObjModified(GedGridMarkerEditorRoot) end end function GridMarker:EditorCallbackMove() EditorMarker.EditorCallbackMove(self) VoxelSnappingObj.EditorCallbackMove(self) self:RecalcAreaPositions("force show") end function GridMarker:EditorCallbackDelete() GedGridMarkerEditorRebuildRootOnDeletedMarker() end function GridMarker:OnEditorDelete() GedGridMarkerEditorRebuildRootOnDeletedMarker() end function GridMarker:GetError() local firstVal for name, value in pairs(self.ArchetypesTriState) do if type(firstVal) ~= "boolean" then firstVal = value end if value ~= firstVal then return "Marker has both allowed and forbidden entries in ArchetypesTriState." end end end function GridMarker:GetWarning() if self.Type == "Entrance" and (not self.Groups or #self.Groups == 0) then return "Entrance marker has no group!" end if self.Type == "Entrance" and not self.Reachable then return "Entrance marker should be set to reachable only to prevent units from getting stuck!" end end function GridMarker:AddPosition(positions, around_center) local pos, idx if around_center then idx = table.find(positions, point_pack(self:GetPos():SetInvalidZ())) if idx then pos = positions[idx] else local min_dist = max_int for i, packed_pt in ipairs(positions) do local pt = point(point_unpack(packed_pt)) local dist = pt:Dist(self:GetPos()) if dist < min_dist then pos = packed_pt idx = i min_dist = dist end end end end if not pos then pos, idx = table.interaction_rand(positions, "GridMarker") end if idx then positions[idx] = positions[#positions] positions[#positions] = nil end return point(point_unpack(pos)) end function GridMarker:GetRandomPositions(number, around_center, positions, req_pos, avoid_close_pos) positions = positions or self:GetAreaPositions() if not next(positions) then return empty_table, self:GetAngle() end assert(number <= #positions) local x, y, z if req_pos then x, y, z = req_pos:xyz() else if around_center then x, y, z = self:GetPosXYZ() else local packedPoint = table.interaction_rand(positions, "GridMarker") x, y, z = point_unpack(packedPoint) end end z = z or terrain.GetHeight(x, y) local level_z = SnapToVoxelZ(x, y, z) local first_x, first_y, first_z = SnapToPassSlabXYZ(x, y, level_z) -- SpawnMarkers are usually set to Reachable only but they are (there is a VME for this) -- accidentally toggled on some maps, prevent position overlap due to duplicate z if first_x and not self.Reachable and not first_z then first_z = level_z end local result = table.icopy(positions) -- Score positions based on avoid_close_pos -- The function will return closest positions always, but in the avoid_close_pos case -- it will prioritize positions at least some distance away, while in the false case it will -- return positions that are closer than that distance. if first_x then local close_dist = number*guim local scores = {} for i, packedPos in ipairs(positions) do local x, y, z = point_unpack(packedPos) if not z and first_z then z = terrain.GetHeight(x, y) end local distance = GetLen(x - first_x, y - first_y, z and first_z and z - first_z or 0) local score if avoid_close_pos then score = distance > close_dist and distance or max_int else score = distance < close_dist and distance or max_int end if z and first_z and z ~= first_z then score = score + close_dist + 100 end scores[packedPos] = score end local reqPositionPacked = point_pack(first_x, first_y, first_z) if not scores[reqPositionPacked] then table.insert(result, reqPositionPacked) end -- Fill preferred positions first scores[reqPositionPacked] = -1 table.stable_sort(result, function(a, b) return scores[a] < scores[b] end) end for i = #result, number + 1, -1 do result[i] = nil end for i, packed_pos in ipairs(result) do result[i] = point(point_unpack(packed_pos)) end return result end function GridMarker:GetExtraEditorText(texts) -- override in child classes, append Ts to texts end function GridMarker:GetGroupsText() if not self.Groups then return "" end return table.concat(self.Groups, ",") end function GridMarker:GetEditorTypeText() return Untranslated("[]") end function GridMarker:GetEditorText() local texts = {Untranslated(" ( triggers)")} if self.Comment ~= "" then texts[#texts+1] = Untranslated("\t") end GetEditorConditionsAndEffectsText(texts, self) self:GetExtraEditorText(texts) return table.concat(texts, "\n") end function GridMarker:SetGroup(g) self.Groups = { g } end local function SortMarkers(markers) table.sort(markers, function(a, b) local a_type = IsKindOf(a, "AmbientLifeMarker") and "AL" or a.Type local b_type = IsKindOf(b, "AmbientLifeMarker") and "AL" or b.Type if a_type < b_type then return true elseif a_type > b_type then return false else return (a.Groups and a.Groups[1] or "") < (b.Groups and b.Groups[1] or "") end end) end function GetGridMarkers(no_sorting, with_cursor_obj, no_AL_markers) if GetMap() == "" or IsChangingMap() then return {} end local function filter(marker, with_cursor_obj) return (with_cursor_obj or not EditorCursorObjs[marker]) and not marker:IsKindOf("SetpieceMarker") end local markers = MapGetMarkers("GridMarker", nil, filter, with_cursor_obj) or {} if not no_AL_markers then table.iappend(markers, MapGet("map", "AmbientLifeMarker", filter, with_cursor_obj)) end if not no_sorting then SortMarkers(markers) end return markers end function UpdateGedGridMarkerRoot(with_cursor_obj) local grid_markers = GetGridMarkers(nil, with_cursor_obj) table.clear(GedGridMarkerEditorRoot) for _, marker in ipairs(grid_markers) do table.insert(GedGridMarkerEditorRoot, marker) end end function GedGridMarkerEditorRebuildRootOnDeletedMarker() if not GedGridMarkerEditor then return end CreateRealTimeThread(function() -- wait for the marker to be removed UpdateGedGridMarkerRoot() ObjModified(GedGridMarkerEditorRoot) end) end if FirstLoad then XEditorShowGridMarkersAreas = config.ModdingToolsInUserMode or false end function XEditorUpdateGridMarkersAreas() if not IsEditorActive() then return end MapForEachMarker(nil, nil, function(marker) if XEditorShowGridMarkersAreas then marker:ShowArea() else if not table.find(mv_SelectedGridMarkers, marker) then marker:HideArea() end end end) end OnMsg.GameEnterEditor = XEditorUpdateGridMarkersAreas OnMsg.ChangeMapDone = XEditorUpdateGridMarkersAreas MapVar("gv_AllMarkersGroups", false) function GridMarkerGroupsCombo() if not gv_AllMarkersGroups then local groups = {} local markers = GetGridMarkers("no sorting", nil, "no AL markers") for _, marker in ipairs(markers) do for _, group in ipairs(marker.Groups or empty_table) do groups[group] = true end end gv_AllMarkersGroups = table.keys2(groups, true) end local items = table.icopy(gv_AllMarkersGroups) local groups = table.keys2(Groups or empty_table, "sorted") table.append(items, groups) return items end OnMsg.ChangeMapDone = function() gv_AllMarkersGroups = false gv_AllMarkersGroups = GridMarkerGroupsCombo() end if FirstLoad then GedGridMarkerEditor = false GedGridMarkerEditorRoot = false s_DestroyingContainers = false end function OverlappingGridMarkersOffset(markers) markers = markers or GetGridMarkers("no sorting", "with_cursor_obj") local pos_markers = {} for _, marker in ipairs(markers) do local pos = marker:GetPos() local id if pos:z() then id = string.format("%d-%d-%d", pos:x(), pos:y(), pos:z()) else id = string.format("%d-%d", pos:x(), pos:y()) end pos_markers[id] = pos_markers[id] or {} table.insert(pos_markers[id], marker) end for _, overlap_markers in pairs(pos_markers) do if #overlap_markers > 1 then OffsetOverlappingMarkers(overlap_markers) end end end if FirstLoad then OffsetMarkers = {} end local dir = point(0, slab_y/4) local up = point(0, 0, 4096) local angle = 90 * 60 function OffsetOverlappingMarkers(markers) local pos_packed = point_pack(markers[1]:GetPos()) for idx, marker in ipairs(markers) do if idx > 4 then break end table.insert(OffsetMarkers, marker) local pos = marker:GetPos() marker:SetPos(pos + (pos:IsValidZ() and dir:SetZ(0) or dir)) dir = RotateAxis(dir, up, angle) end end function RemoveOverlappingGridMarkersOffset() for _, marker in pairs(OffsetMarkers) do if IsValid(marker) then VoxelSnappingObj.SnapToVoxel(marker) end end OffsetMarkers = {} end function RefreshOverlappingGridMarkersOffset() if GedGridMarkerEditor and not terminal.IsKeyPressed(const.vkAlt) then RemoveOverlappingGridMarkersOffset() OverlappingGridMarkersOffset() end end function GedGridMarkerEditorContext() local classes = ClassDescendantsListInclusive("GridMarker") local context = {} context.rollovers = {} context.icons = {} for _, cls in ipairs(classes) do context.rollovers[cls] = g_Classes[cls].EditorRolloverText context.icons[cls] = g_Classes[cls].EditorIcon end context.WarningsUpdateRoot = "root" return context end function SelectMarkerInGedGridMarkerEditor(selected_markers) if not selected_markers or #selected_markers == 0 then return end assert(GedGridMarkerEditor) local selected_marker_indeces = {} for _, marker in ipairs(selected_markers) do table.insert(selected_marker_indeces, table.find(GedGridMarkerEditorRoot, marker)) end GedGridMarkerEditor:SetSelection("root", selected_marker_indeces) end function OpenGedGridMarkersEditor(selected_markers) if GedGridMarkerEditor then if selected_markers then SelectMarkerInGedGridMarkerEditor(selected_markers) end return end CreateRealTimeThread(function() if not GedGridMarkerEditor or not IsValid(GedGridMarkerEditor) then GedGridMarkerEditorRoot = GetGridMarkers() GedGridMarkerEditor = OpenGedApp("GedGridMarkerEditor", GedGridMarkerEditorRoot, GedGridMarkerEditorContext()) or false if GedGridMarkerEditor then OverlappingGridMarkersOffset(GedGridMarkerEditorRoot) end SelectMarkerInGedGridMarkerEditor(selected_markers, GedGridMarkerEditorRoot) end end) end function GridMarkerEditorSelect(root, obj, prop_id, socket) CreateRealTimeThread(function(root, obj, prop_id, socket) if not GedGridMarkerEditor then GedGridMarkerEditorRoot = GetGridMarkers() GedGridMarkerEditor = OpenGedApp("GedGridMarkerEditor", GedGridMarkerEditorRoot, GedGridMarkerEditorContext()) or false if GedGridMarkerEditor then OverlappingGridMarkersOffset(GedGridMarkerEditorRoot) end end local handle = string.match(prop_id, "h_(.*)_") local idx = table.find(GedGridMarkerEditorRoot, "handle", tonumber(handle or "0")) if idx then GedGridMarkerEditor:SetSelection("root", idx) end end,root, obj, prop_id, socket) end local SelectionFromGed function OnMsg.GedOnEditorSelect(obj, selected, ged_editor) if selected and ged_editor == GedGridMarkerEditor then SelectionFromGed = true EditorSelectObjects({obj}) SelectionFromGed = false end end function OnMsg.GedOnEditorMultiSelect(data, selected, ged_editor) if selected and ged_editor == GedGridMarkerEditor then SelectionFromGed = true EditorSelectObjects(data.__objects) SelectionFromGed = false end end function SelectInGedEditorSelected(objects) if not GedGridMarkerEditor or SelectionFromGed then return end CreateRealTimeThread(function(objects) -- wait for other msgs local ged_selection = {} for _, obj in ipairs(objects) do if IsKindOf(obj, "GridMarker") then local marker_ged_idx = table.find(GedGridMarkerEditorRoot,obj) if marker_ged_idx then table.insert(ged_selection, marker_ged_idx) end end end local filter = GedGridMarkerEditor:FindFilter("root") if filter then for _, idx in ipairs(ged_selection) do if not filter:FilterObject(GedGridMarkerEditorRoot[idx]) then GedGridMarkerEditor:ResetFilter("root") break end end end GedGridMarkerEditor:SetSelection("root", ged_selection) end, objects) end MapVar("mv_SelectedGridMarkers", {}) function OnMsg.EditorSelectionChanged(objects) objects = objects or {} local old_markers = mv_SelectedGridMarkers mv_SelectedGridMarkers = {} for _, obj in ipairs(old_markers) do if obj and IsValid(obj) then if not obj:IsAreaVisible() then if not XEditorShowGridMarkersAreas then obj:HideArea() end end end end for _, obj in ipairs(objects) do if IsKindOf(obj, "GridMarker") then obj:ShowArea() mv_SelectedGridMarkers[#mv_SelectedGridMarkers+1] = obj end end SelectInGedEditorSelected(objects) end DefineClass.GridMarkerFilter = { __parents = { "GedFilter" }, properties = { { id = "Type", name = "Type", editor = "dropdownlist", items = function() return GetGridMarkerTypesCombo()end, default = "" }, { id = "Group", name = "Group", editor = "dropdownlist", items = function() return GridMarkerGroupsCombo() end, default = "" }, { id = "QuestId", name = "Quest id", editor = "preset_id", default = "", preset_class = "QuestsDef" }, } } function GridMarkerFilter:CheckQuestFilter(marker) return CheckMarkerQuestDependencies(marker, self.QuestId) end function GridMarkerFilter:FilterObject(marker) if self.Type ~= "" and self.Type ~= marker.Type then return false end if self.Group ~= "" and not table.find(marker.Groups, self.Group) then return false end if self.QuestId ~= "" and not self:CheckQuestFilter(marker) then return false end return true end function GridMarkerFilter:DoneFiltering(displayed_items, filtered) local markers = GetGridMarkers() for idx, marker in ipairs(markers) do if marker:GetGameFlags(const.gofPermanent) ~= 0 then marker:SetVisible(not filtered[idx]) end end end function OnMsg.GedClosing(ged_id) if GedGridMarkerEditor and GedGridMarkerEditor.ged_id == ged_id then RemoveOverlappingGridMarkersOffset() GedGridMarkerEditor = false end end function OnMsg.GedPropertyEdited(ged_id, object, prop_id, old_value) if GedGridMarkerEditor and GedGridMarkerEditor.ged_id == ged_id then local obj = GedGridMarkerEditor:ResolveObj("root") if prop_id=="Groups" then -- recalc map vars RecalcGroups() end ObjModified(obj) end end OnMsg.SaveMap = RemoveOverlappingGridMarkersOffset OnMsg.PostSaveMap = OverlappingGridMarkersOffset function OnMsg.ChangeMap() if GedGridMarkerEditor then GedGridMarkerEditor:Send("rfnApp", "Exit") end end function GedOpPlaceGridMarker(socket, marker, marker_type) XEditorStartPlaceObject(marker_type == "GridMarker" and "GridMarker-Position" or marker_type) end function MapGetMarkers(marker_type, group, filter, ...) if group == "" then group = nil end if group and not Groups[group] then return end local all_markers = g_GridMarkersContainer.labels[marker_type or "GridMarker"] if not group and not filter then return all_markers end local markers for _, marker in ipairs(all_markers) do if (not group or marker:IsInGroup(group)) and (not filter or filter(marker, ...)) then if not markers then markers = {} end table.insert(markers, marker) end end return markers end function MapCountMarkers(marker_type, group, filter, ...) if group == "" then group = nil end if group and not Groups[group] then return 0 end local all_markers = g_GridMarkersContainer.labels[marker_type or "GridMarker"] local count = 0 for _, marker in ipairs(all_markers) do if (not group or marker:IsInGroup(group)) and (not filter or filter(marker, ...)) then count = count + 1 end end return count end function MapForEachMarker(marker_type, group, exec, ...) g_GridMarkersContainer:ForEachInLabel(marker_type or "GridMarker", function(marker, ...) if not group or group == "" or marker:IsInGroup(group) then exec(marker, ...) end end, ...) end function MapGetFirstMarker(marker_type, filter) return g_GridMarkersContainer:GetFirstInLabel(marker_type or "GridMarker", filter) end function UpdateEntranceAreasVisibility() if CurrentMap == "" or IsChangingMap() then return end MapForEachMarker("Entrance", false, function(marker) if marker:IsAreaVisible() then marker:ShowArea() else marker:HideArea() end end) end function OnMsg.ValidateMap() if not mapdata.GameLogic or not IsCampaignMap(GetMapName()) then return end local defender_markers = 0 local border_area_markers = 0 local first_ba_marker MapForEach("map", "GridMarker", function(marker) if marker.Type == "Entrance" and not SnapToPassSlabXYZ(marker) then StoreErrorSource(marker, string.format("Entrance marker '%s' on impassable!", marker.class)) end if marker.Type == "Defender" then defender_markers = defender_markers + 1 elseif marker.Type == "BorderArea" then if not first_ba_marker then first_ba_marker = marker end border_area_markers = border_area_markers + 1 end -- We have the info we need, we can stop counting early if defender_markers > 0 and border_area_markers > 1 then return "break" end end) -- Defender markers if defender_markers == 0 then local w, h = terrain.GetMapSize() StoreWarningSource(point(w/2, h/2), "This map has no defender markers, where should defenders be placed in case of a conflict?") end -- Border area markers local msg = BorderAreaMarkerMessage(border_area_markers) if msg then StoreErrorSource(first_ba_marker, msg) end end OnMsg.ValidateMap = ValidateGameObjectProperties("GridMarker") function GetBorderAreaMarker() if not g_GridMarkersContainer then return false end local label = g_GridMarkersContainer.labels["BorderArea"] assert(not label or #label <= 1) return label and label[1] end MapVar("g_InteractableAreaMarkers", {}) MapVar("g_BorderAreaRangeContour", false) MapVar("g_GridMarkersContainer", false) function OnMsg.NewMap() for _, marker in ipairs(g_InteractableAreaMarkers) do marker:RemoveFloatTxt() end table.clear(g_InteractableAreaMarkers) g_GridMarkersContainer = LabelContainer:new{} end function OnMsg.NewMapLoaded() MapForEachMarker("Entrance", nil, function(marker) table.insert(g_InteractableAreaMarkers, marker) end) SetImpassableOutsideBorderMarker() end local function GetMinMaxBorder(x, y, width, height, noOffset) local voxel_left = x - width / 2 local voxel_top = y - height / 2 local left, top = VoxelToWorld(voxel_left - 1, voxel_top - 1) local right, bottom = VoxelToWorld(voxel_left + width, voxel_top + height) local offset = noOffset and 0 or slab_x / 2 return left + offset, top + offset, right - offset, bottom - offset end function SetImpassableOutsideBorderMarker(clear) local bam = GetBorderAreaMarker() if IsValid(bam) then local x, y = WorldToVoxel(bam) local left, top, right, bottom = GetMinMaxBorder(x, y, bam.AreaWidth, bam.AreaHeight, true) local width, height = terrain.GetMapSize() -- the boxes are extened by const.PassTileSize if clear then terrain.SetForcedImpassableBox(0, 0, width, height, false) end terrain.SetForcedImpassableBox(0, 0, width, top, true) terrain.SetForcedImpassableBox(0, bottom, width, height, true) terrain.SetForcedImpassableBox(0, top, left, bottom, true) terrain.SetForcedImpassableBox(right, top, width, bottom, true) end end function GridMarker:RecalcImpassableOutsideBorderArea(prop_id, old_value) if not self.Type == "BorderArea" then return end local offset = slab_x / 2 local newX, newY = WorldToVoxel(self) local newAreaW = self.AreaWidth local newAreaH = self.AreaHeight local oldX, oldY, oldAreaW, oldAreaH if prop_id and old_value then -- coming from changeprop oldX = newX oldY = newY oldAreaW = prop_id == "AreaWidth" and old_value or self.AreaWidth oldAreaH = prop_id == "AreaHeight" and old_value or self.AreaHeight elseif not prop_id and old_value then -- coming from EditorCallbackMove oldX = old_value.x oldY = old_value.y oldAreaW = self.AreaWidth oldAreaH = self.AreaHeight end local newL, newT, newR, newB = GetMinMaxBorder(newX, newY, newAreaW, newAreaH) local oldL, oldT, oldR, oldB = GetMinMaxBorder(oldX, oldY, oldAreaW, oldAreaH) local intersect = BoxIntersectsBox(box(newL, newT, newR, newB), box(oldL, oldT, oldR, oldB)) if not intersect then SetImpassableOutsideBorderMarker("clear") terrain.RebuildPassability(box(newL, newT, newR, newB)) return end local function UpdateBorders() if newL < oldL then terrain.SetForcedImpassableBox(newL, newT, oldL, newB, false) terrain.RebuildPassability(box(newL, newT, oldL, newB)) elseif newL > oldL then terrain.SetForcedImpassableBox(oldL, oldT, newL - offset, oldB, true) terrain.RebuildPassability(box(oldL, oldT, newL, oldB)) end if newT < oldT then terrain.SetForcedImpassableBox(newL, newT, newR, oldT, false) terrain.RebuildPassability(box(newL, newT, newR, oldT)) elseif newT > oldT then terrain.SetForcedImpassableBox(oldL, oldT, oldR, newT - offset, true) terrain.RebuildPassability(box(oldL, oldT, oldR, newT)) end if newR < oldR then terrain.SetForcedImpassableBox(newR + offset, oldT, oldR, oldB, true) terrain.RebuildPassability(box(newR, oldT, oldR, oldB)) elseif newR > oldR then terrain.SetForcedImpassableBox(oldR, newT, newR, newB, false) terrain.RebuildPassability(box(oldR, newT, newR, newB)) end if newB < oldB then terrain.SetForcedImpassableBox(oldL, newB + offset, oldR, oldB, true) terrain.RebuildPassability(box(oldL, newB, oldR, oldB)) elseif newB > oldB then terrain.SetForcedImpassableBox(newL, oldB, newR, newB, false) terrain.RebuildPassability(box(newL, oldB, newR, newB)) end end UpdateBorders(newL, newT, newR, newB, oldL, oldT, oldR, oldB) end function OnMsg.PostNewMapLoaded() MapForEachMarker("GridMarker", nil, function(marker) marker:RecalcAreaPositions() end) UpdateEntranceAreasVisibility() end local batchedWork = false local maxPerTick = 4 local maxTicksPerTick = 3 local delay = 33 local function ProcessGridMarkersOnPassChanged() local c = 0 local ignoreCount = IsChangingMap() or IsEditorActive() local start = GetPreciseTicks() for marker, _ in pairs(batchedWork) do c = c + 1 batchedWork[marker] = nil --local s = GetPreciseTicks() if marker:IsAreaShown() and marker:GetGameFlags(const.gofPermanent) ~= 0 then marker:RecalcAreaPositions() end --print(marker.handle, marker.class, marker.Type, GetPreciseTicks() - s) if not ignoreCount and (c >= maxPerTick or GetPreciseTicks() - start >= maxTicksPerTick) then break end end if next(batchedWork) then DelayedCall(delay, ProcessGridMarkersOnPassChanged) else batchedWork = false end end function OnMsg.OnPassabilityChanged(clip) if IsEditorSaving() then return end batchedWork = batchedWork or {} clip = clip:grow(slab_x * 10) MapForEach(clip, "GridMarker", function(marker, batchedWork) if marker.recalc_area_on_pass_rebuild and marker.Reachable and marker.Type ~= "BorderArea" then batchedWork[marker] = true end end, batchedWork) DelayedCall(delay, ProcessGridMarkersOnPassChanged) end function BorderAreaMarkerMessage(count) if count > 1 then return "Border area markers should be no more than one per map." elseif count == 0 then return "Border area marker not present on map." end end function GetBorderAreaLimits() local bam = GetBorderAreaMarker() if not IsValid(bam) then return end local bam_pos_x, bam_pos_y = bam:GetPosXYZ() local border_min_x = bam_pos_x - slab_x * (bam.AreaWidth / 2) - slab_x / 2 local border_min_y = bam_pos_y - slab_y * (bam.AreaHeight / 2) - slab_y / 2 local border_max_x = border_min_x + bam.AreaWidth * slab_x local border_max_y = border_min_y + bam.AreaHeight * slab_y return box(border_min_x, border_min_y, border_max_x, border_max_y) end function RemoveNeighborVoxelsFromTable(pos, voxels) local x1, y1, z1 = VoxelToWorld(WorldToVoxel(pos)) for i = #voxels, 1, -1 do local x2, y2, z2 = VoxelToWorld(WorldToVoxel(point_unpack(voxels[i]))) if z1 == z2 and abs(x1 - x2) <= slab_x and abs(y1 - y2) <= slab_y then table.remove(voxels, i) end end end function GetReachablePositionsFromPos(pos, count) assert(count > 0) if count == 1 then local pfflags = const.pfmDestlock + const.pfmImpassableSource + const.pfmVoxelAligned local has_path, closest_pos = pf.HasPosPath(pos, pos, CalcPFClass("player1"), 0, 0, nil, 0, nil, pfflags) return { closest_pos or pos } end local width = count * slab_x local height = count * slab_y local path = PlaceObject("CombatPath") local area_left = pos:x() - width/2 local area_top = pos:y() - height/2 path.restrict_area = box(area_left, area_top, area_left + width, area_top + height) path:RebuildPaths(nil, 200000, pos) local voxels = table.keys(path.paths_ap, true) local pos1 = path.closest_free_pos and point(point_unpack(path.closest_free_pos)) or pos local positions = {pos1} RemoveNeighborVoxelsFromTable(pos1, voxels) for i = 1, count - 1 do local v = table.rand(voxels, point_pack(pos)) assert(v) local pos_v = point(point_unpack(v)) table.insert(positions, pos_v) RemoveNeighborVoxelsFromTable(pos_v, voxels) end return positions end function GetInteractableAreaMarkerRollover(m) if gv_DeploymentStarted then return (not SelectedObj or IsFirstSquadDeployment(SelectedObj.Squad)) and GetDeploymentAreaRollover(m) end end ArchetypesSorted = false function ArchetypesCombo() if not ArchetypesSorted then ArchetypesSorted = table.keys2(Archetypes, true) end return ArchetypesSorted end DefineClass.RepositionMarker = { __parents = {"GridMarker"}, properties = { { category = "Marker", id = "Color", name = "Color", editor = "color", default = RGB(255, 0, 255)}, -- no_edits { category = "Grid Marker", id = "Type", name = "Type", editor = "text", default = "Reposition", no_edit = true }, { category = "Grid Marker", id = "Groups", name = "Groups", editor = "string_list", no_edit = true }, { category = "Marker", id = "AreaHeight", name = "Area Height", editor = "number", default = 0, no_edit = true }, { category = "Marker", id = "AreaWidth", name = "Area Width", editor = "number", default = 0, no_edit = true }, { category = "Marker", id = "Reachable", name = "Reachable only", editor = "bool", default = false, no_edit = true }, { category = "Logic", id = "Trigger", name = "Trigger", editor = "dropdownlist", default = "once", items = { "once", "activation", "deactivation", "always", "change", }, no_edit = true }, { category = "Logic", id = "Effects", name = "Effects", editor = "nested_list", default = false, base_class = "Effect", no_edit = true }, { category = "Archetype", id = "Archetypes", name = "Preferred Archetypes", editor = "string_list", default = false, no_edit = true }, { category = "Logic", id = "TargetUnits", name = "Target Units", editor = "dropdownlist", items = PresetsCombo("UnitDataCompositeDef"), default = "" }, }, EditorRolloverText = "AI Reposition location", EditorIcon = "CommonAssets/UI/Icons/refresh repost retweet.tga", recalc_area_on_pass_rebuild = false, } function LightsMarkerGroups() local light_marker_groups = {} local groups = GetBehaviorGroups() for _, group in ipairs(groups) do local objects = Groups[group] for _, obj in ipairs(objects) do if IsKindOf(obj, "LightsMarker") then table.insert(light_marker_groups, group) break end end end return light_marker_groups end DefineClass.LightsMarker = { __parents = {"GridMarker"}, EditorRolloverText = "Lights Turn On/Off Marker", EditorIcon = "CommonAssets/UI/Icons/refresh repost retweet.tga", lights_off = false, lights_intensities = false, } function LightsMarker:TurnLightOff(light) self.lights_intensities = self.lights_intensities or {} if not self.lights_intensities[light] then self.lights_intensities[light] = light:GetIntensity() end light:SetIntensity(0) end function LightsMarker:TurnLightOn(light) if not self.lights_intensities then return end if self.lights_intensities[light] then light:SetIntensity(self.lights_intensities[light]) end self.lights_intensities[light] = nil if not next(self.lights_intensities) then self.lights_intensities = false end end function LightsMarker:GetDynamicData(data) data.lights_off = self.lights_off if not self.lights_intensities then return end data.lights_intensities = {} for light, intensity in pairs(self.lights_intensities) do if light.handle then data.lights_intensities[light.handle] = intensity end end end function LightsMarker:SetDynamicData(data) self.lights_off = data.lights_off if not data.lights_intensities then return end local invalid_handles = {} self.lights_intensities = {} for handle, intensity in pairs(data.lights_intensities) do local light = HandleToObject[handle] if light then self.lights_intensities[light] = intensity light:SetIntensity(0) else table.insert(invalid_handles, handle) end end for _, handle in ipairs(invalid_handles) do self.lights_intensities[handle] = nil end if not next(self.lights_intensities) then self.lights_intensities = false end end function MapGetLightsMarkers(marker_type, group, filter, ...) return MapGetMarkers(marker_type, group, function(marker, ...) return marker:IsKindOf("LightsMarker") and (not filter or filter(marker, ...)) end, ...) end function OnMsg.ZuluGameLoaded() local lights_markers = MapGetLightsMarkers() local lights = GetLights() for _, light in ipairs(lights) do for _, marker in ipairs(lights_markers) do if marker.lights_off and (not marker.lights_intensities or not marker.lights_intensities[light]) then if marker:IsInsideArea2D(light) then marker:TurnLightOff(light) end end end end end function GetEditorFilterNonLeafMarkerClasses() return {"UnitMarker"} end -- for current map function EditorViewAbridged(obj, id, filter_type) local value = obj.class if obj:HasMember("GetEditorView") then value = _InternalTranslate(T{obj:GetEditorView(), obj}) if not filter_type or filter_type=="quest" then -- reverse some of the ways GetEditorView mentions the name of this quest to make display more terse, -- without introducing yet another GetEditorView-like function. Note that other quests will be unaffected value = value:gsub(" %(" .. id .. "%)", "") value = value:gsub("[Qq]uest " .. id .. ":? ", "") elseif filter_type=="conversation" then value = value:gsub(" " .. id , "") elseif filter_type=="banter" then -- value=value end end return value end if FirstLoad then g_DebugMarkersInfo = false end function GatherMarkerScriptingData() local markers_data = {} local map_name = GetMapName() local grid_markers = GetGridMarkers(nil, nil, "no AL markers") for idx, marker in ipairs(grid_markers) do local data = {} local name = string.format("%s#%03d", marker.Type, marker.handle % 1000) if marker.ID ~= "" then name = name .. " " .. marker.ID end if PropObjHasMember(marker, "DisplayName") and marker.DisplayName and marker.DisplayName ~= "" then name = name .. " \"" .. _InternalTranslate(marker.DisplayName) .. "\"" end if marker.Groups and next(marker.Groups) then name = name .. " (" .. table.concat(marker.Groups, ", ") .. ")" end local BanterEffects = {} marker:ForEachSubObject("BanterFunctionObjectBase", function(effect, parents) table.insert_unique(BanterEffects, effect) end) local LootTableIds = {} marker:ForEachSubObject("ConditionalLoot", function(condLoot, parents) if condLoot.LootTableId then table.insert_unique(LootTableIds, condLoot.LootTableId) end end) local path = marker.Type .. " " .. marker.ID local data = { type = marker.Type, name = name, path = path, handle = marker.handle, map = map_name, Groups = marker.Groups, BanterGroups = next(marker.BanterGroups) and marker.BanterGroups or nil, SpecificBanters = next(marker.SpecificBanters) and marker.SpecificBanters or nil, BanterTriggerEffects = next(BanterEffects) and BanterEffects or nil, ApproachedBanters = next(marker.ApproachedBanters) and marker.ApproachedBanters or nil, ApproachBanterGroup = marker.ApproachBanterGroup or nil, LootTableIds = next(LootTableIds) and LootTableIds or nil, } local items = {} -- quests marker:ForEachSubObject("QuestFunctionObjectBase", function(obj, parents) table.insert_unique(items, { filter_type = "quest", type = obj.class, reference_id = obj.QuestId, editor_view_abridged = EditorViewAbridged(obj, obj.QuestId, "quest"), var = rawget(obj, "Prop") or rawget(obj, "Vars"), var2 = rawget(obj, "Prop2"), }) end) -- conversations --Conditions, Effects marker:ForEachSubObject("ConversationFunctionObjectBase", function(obj, parents) table.insert_unique(items, { filter_type = "conversation", type = obj.class, reference_id = obj.Conversation, editor_view_abridged = EditorViewAbridged(obj, obj.Conversation, "conversation"), }) end) -- banters --Conditions, Effects marker:ForEachSubObject("BanterFunctionObjectBase", function(obj, parents) for _, banter_str in ipairs(obj.Banters) do table.insert_unique(items, { filter_type = "banter", type = obj.class, reference_id = banter_str, editor_view_abridged = EditorViewAbridged(obj, banter_str, "banter"), }) end end) data.items = (next(items) or IsKindOf(marker, "UnitMarker")) and items or nil local hasBanter = data.BanterGroups or data.SpecificBanters or data.BanterTriggerEffects or data.ApproachedBanters or data.ApproachBanterGroup local hasLootTable = data.LootTableIds if IsKindOf(marker, "UnitMarker") or next(items) or hasBanter or hasLootTable then table.insert_unique(markers_data, data) end end if markers_data and next(markers_data) then table.sortby_field(markers_data, "handle") g_DebugMarkersInfo = g_DebugMarkersInfo or {} g_DebugMarkersInfo[map_name] = markers_data end return markers_data end function ForEachDebugMarkerData(info_type, preset, call_fn) -- filter for current quest_id for map, markers_data in sorted_pairs(g_DebugMarkersInfo or empty_table) do local currCampaignPreset = GetCurrentCampaignPreset() local currSectors = currCampaignPreset and currCampaignPreset.Sectors --local presetCampaign = preset and preset.campaign for _, marker in ipairs(markers_data) do if currSectors and table.find(currSectors, "Map", marker.map) then local items = marker.items for _, item in ipairs(items) do if item.filter_type == info_type and item.reference_id == preset.id then call_fn(marker, item) end end end end end end function IsGridMarkerWithDefenderRole(marker) local preset = marker and Presets.GridMarkerType.Default[marker.Type] return preset and preset.DefenderRole end --Catch markers outside border area function CheckMarkersPos() local allGridMarkers = GetGridMarkers("no sorting", nil, "no AL markers") local border = GetBorderAreaLimits() for _, marker in ipairs(allGridMarkers) do if not IsKindOf(marker, "ShowHideCollectionMarker") and border and not border:Point2DInside(marker:GetPos()) then StoreErrorSource(marker, "Marker placed outside the border.") end end end -- in editor mode function OnMsg.SaveMap() CheckMarkersPos() local markers_data = GatherMarkerScriptingData() local folder = GetMap() if markers_data and next(markers_data) then local path = folder .. "markers.debug.lua" SaveSVNFile(path, TableToLuaCode(markers_data, nil, pstr("", 1024))) end end local function LoadDebugMarkersInfo(map) local file = GetMapFolder(map) .. "markers.debug.lua" if not IsFSUnpacked() and MapData[map] and not MapData[map].ModMapPath then MountMap(map) end if io.exists(file) then local err, str = AsyncFileToString(GetMapFolder(map) .. "markers.debug.lua") if str then local _, markers_data = LuaCodeToTuple(str) local map_name = markers_data and markers_data[1].map if map_name then g_DebugMarkersInfo = g_DebugMarkersInfo or {} g_DebugMarkersInfo[map_name] = markers_data end end end end local function LoadMapPatchMarkersInfo(mapPatchPath) local path, name, ext = SplitPath(mapPatchPath) local mapName = string.gsub(name, ".debug", "") local err, str = AsyncFileToString(mapPatchPath) if err then return err end local _, markers_data = LuaCodeToTuple(str) if mapName then g_DebugMarkersInfo = g_DebugMarkersInfo or {} g_DebugMarkersInfo[mapName] = markers_data end end local function LoadModMapPatchDebugMarkers(modData) --debug markers data from ModItemMapPatch local err, mapPatchesMarkerData = AsyncListFiles(modData.mod_path .. "MapPatches", "*.debug.lua") if err then return err end for _, mapPatchMarkersPath in ipairs(mapPatchesMarkerData) do LoadMapPatchMarkersInfo(mapPatchMarkersPath) end end function OnMsg.LastEditedModChanged(mod) g_DebugMarkersInfo = {} for map, data in sorted_pairs(MapData) do --in the map data are both original and new or modified maps from ModItemSector LoadDebugMarkersInfo(map) end LoadModMapPatchDebugMarkers(mod) end function OnMsg.ChangeMapDone(map) if GetMap() ~= "" then MapForEachMarker(false, false, function(marker) marker:UpdateVisuals(marker.Type) end) end if Platform.developer then if not g_DebugMarkersInfo then g_DebugMarkersInfo = g_DebugMarkersInfo or {} for map, data in sorted_pairs(MapData) do LoadDebugMarkersInfo(map) end else LoadDebugMarkersInfo(map) end end if AreModdingToolsActive() and LastEditedMod then LoadModMapPatchDebugMarkers(LastEditedMod) end end function SaveDebugMarkersLootTablesCSV() local csv = {} for map, data in sorted_pairs(g_DebugMarkersInfo) do local map_data = {} for _, marker in ipairs(data) do for _, loot_table_id in ipairs(marker.LootTableIds) do map_data[loot_table_id] = map_data[loot_table_id] or 0 map_data[loot_table_id] = map_data[loot_table_id] + 1 end end for k,v in sorted_pairs(map_data) do csv[#csv+1] = { map = map, id = k, count = v } end end local fields = { "map", "id", "count" } local filename = "DebugMarkersLootTables.csv" local err = SaveCSV(filename, csv, fields, fields, ",") print(err or "CSV saved to", ":", ConvertToOSPath(filename), "") end function LootPerSectorCSV() if not SelectedObj or not SelectedObj:IsKindOf("Unit") then print("Run this with a merc selected on a gameplay map, please") return end local csv = {} local class_to_group = {} for group, items in pairs(Presets.InventoryItemCompositeDef) do if type(group) ~= "number" then for _, class in ipairs(items) do class_to_group[class.id] = group end end end local current_sector_id = gv_CurrentSectorId local map_to_sector = {} local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds" local campaign_presets = rawget(_G, "CampaignPresets") or empty_table for _, sector in ipairs(campaign_presets[campaign] and campaign_presets[campaign].Sectors or empty_table) do if sector.Map then map_to_sector[sector.Map] = sector gv_CurrentSectorId = sector.Id local _, enemySquads = GetSquadsInSector(sector.Id, "excludeTravelling", not "includeMilitia", "excludeArriving") local items = {} for i = #enemySquads, 1, -1 do for j = #enemySquads[i].units, 1, -1 do local id = enemySquads[i].units[j] local unit = gv_UnitData[id] Unit.DropLoot(unit) unit:ForEachItem(function(item, slot_name, left, top, items) csv[#csv+1] = { map =sector.Map, dropped_from = ObjectClass(unit), label1 = sector.Label1 or "none", label2 = sector.Label2 or "none", tier = string.format("%d.%d", sector.MapTier / 10, sector.MapTier%10), class = ObjectClass(item), item_group = class_to_group[ObjectClass(item)] or "none", count = item.Amount or 1, condition = item.Condition or 0, guaranteed_drop = item.guaranteed_drop and 1 or 0, drop_chance = item.drop_chance } end, items) end end end end for map, data in sorted_pairs(g_DebugMarkersInfo) do local map_data = {} local loot = {} local sector = map_to_sector[map] if sector then gv_CurrentSectorId = sector.Id for _, marker in ipairs(data) do for _, loot_table_id in ipairs(marker.LootTableIds) do if not LootDefs[loot_table_id] then print("Invalid loot id in ", marker) else LootDefs[loot_table_id]:GenerateLoot(SelectedObj, {}, AsyncRand(), loot) end end end for _, item in ipairs(loot) do csv[#csv+1] = { map = map, dropped_from = "Marker", label1 = sector.Label1 or "none", label2 = sector.Label2 or "none", tier = string.format("%d.%d", sector.MapTier / 10, sector.MapTier%10), class = ObjectClass(item), item_group = class_to_group[ObjectClass(item)] or "none", count = item.Amount or 1, condition = item.Condition or 0, guaranteed_drop = item.guaranteed_drop and 1 or 0, drop_chance = item.drop_chance } end end end gv_CurrentSectorId = current_sector_id local fields = { "map", "dropped_from", "tier", "label1", "label2", "class", "item_group", "count", "condition", "drop_chance", "guaranteed_drop" } local filename = "LootPerSector.csv" local err = SaveCSV(filename, csv, fields, fields, ",") print(err or "CSV saved to", ":", ConvertToOSPath(filename), "") end function OnMsg.EditorCategoryFilterChanged(c, filter) if c == "Markers" then local markers = GetGridMarkers("no sorting", nil, "no AL markers") for _, marker in ipairs(markers) do marker:SetVisible(filter ~= "invisible") end end end DefineClass.SaveMapCheckMarker = { __parents = {"Object"}, } if Platform.developer then function GetShowHideCollectionMarkerBiggestRange() local biggest ForEachMap(nil, function() local map_biggest MapForEach("map", "ShowHideCollectionMarker", function(marker) local collection_idx = marker:GetCollectionIndex() if collection_idx and collection_idx ~= 0 then MapForEach("map", "collection", collection_idx, true, function(o) local dist = marker:GetDist(o) map_biggest = ((not map_biggest) or (dist > map_biggest)) and dist or map_biggest end) end end) if map_biggest then biggest = ((not biggest) or (map_biggest > biggest)) and map_biggest or biggest print(string.format("%s: %d / %d", GetMapName(), map_biggest, biggest)) else print(string.format("No SaveMapCheckMarker objects on map %s", GetMapName())) end end) if biggest then print(string.format("Biggest Range: %d", biggest)) end return biggest end end