Download CommonLua/MapGen/PrefabMarkerEdit.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 59.3 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/MapGen/PrefabMarkerEdit.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/MapGen/PrefabMarkerEdit.lua
-
curl -L -o PrefabMarkerEdit.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/MapGen/PrefabMarkerEdit.lua
59.3 kB
| if not Platform.editor then | |
| return | |
| end | |
| if FirstLoad then | |
| l_LockedCol = false | |
| l_ResaveAllMapsThread = false | |
| end | |
| local height_tile = const.HeightTileSize | |
| local type_tile = const.TypeTileSize | |
| local height_scale = const.TerrainHeightScale | |
| local gofPermanent = const.gofPermanent | |
| local height_roughness_unity = 1000 | |
| local height_roughness_err = 1200 | |
| local height_outline_offset_err = guim | |
| local invalid_type_value = 255 | |
| local GetHeight = terrain.GetHeight | |
| local mask_max = 255 | |
| local invalid_type_value = 255 | |
| local invalid_grass_value = 255 | |
| local transition_max_pct = 30 | |
| local GetClassFlags = CObject.GetClassFlags | |
| local cfCodeRenderable = const.cfCodeRenderable | |
| local granularity = GetTerrainGridsMaxGranularity() | |
| local clrNoModifier = SetA(const.clrNoModifier, 255) | |
| local function get_surface(...) | |
| return Max(GetWalkableZ(...), GetHeight(...)) | |
| end | |
| local function set_surface_z(pt, offset) | |
| return pt:SetZ(get_surface(pt) + (offset or 0)) | |
| end | |
| function OnMsg.MarkersRebuildStart() | |
| if mapdata.IsPrefabMap then | |
| GridStatsReset() | |
| end | |
| l_LockedCol = Collection.GetLockedCollection() | |
| if l_LockedCol then | |
| l_LockedCol:SetLocked(false) | |
| end | |
| end | |
| function OnMsg.MarkersRebuildEnd() | |
| if mapdata.IsPrefabMap then | |
| local stat_usage = GridStatsUsage() or "" | |
| if #stat_usage > 0 then | |
| DebugPrint("Grid ops:\n") | |
| DebugPrint(print_format(stat_usage)) | |
| DebugPrint("\n") | |
| end | |
| end | |
| end | |
| local function PrefabIsResaving() | |
| return IsValidThread(l_ResaveAllMapsThread) | |
| end | |
| function OnMsg.MarkersChanged() | |
| if PrefabIsResaving() then | |
| return | |
| end | |
| if l_LockedCol then | |
| l_LockedCol:SetLocked(true) | |
| end | |
| l_LockedCol = false | |
| PrefabUpdateMarkers() | |
| end | |
| local function get_marker_terrains(obj) | |
| local items = {} | |
| local bbox = obj:GetBBox() | |
| local invalid_terrain_idx = GetTerrainTextureIndex(obj.InvalidTerrain) or -1 | |
| local mask = GridGetTerrainMask(bbox, invalid_terrain_idx) | |
| local skipped_terrain_idxs = obj:GetSkippedTextureList() | |
| local _, types = GridGetTerrainType(bbox, mask, invalid_type_value, invalid_terrain_idx, skipped_terrain_idxs) | |
| for _, idx in ipairs(types) do | |
| local texture = TerrainTextures[idx] | |
| if texture then | |
| items[#items + 1] = texture.id | |
| end | |
| end | |
| table.sort(items) | |
| table.insert(items, 1, "") | |
| return items | |
| end | |
| local debug_show_types = { | |
| "capture_box", "radius", "flat_zone", "crit_slope", "roughness", | |
| "height_offset", "height_lims", "missing_types", "transition", | |
| "large_objs", "optional_objs", "collections", | |
| } | |
| local def_show_types = set("capture_box") | |
| DefineClass.PrefabMarkerEdit = { | |
| __parents = { "InitDone" }, | |
| properties = { | |
| { category = "Checks", id = "CheckObjCount", name = "Objects Count", editor = "bool", default = true, help = "Perform check of the object's density on export" }, | |
| { category = "Checks", id = "CheckObjRadius", name = "Objects Radius", editor = "bool", default = true, help = "Perform check of the object's max radius on export" }, | |
| { category = "Checks", id = "CheckRougness", name = "Height Rougness", editor = "bool", default = true, help = "Perform check of the height map rougness" }, | |
| { category = "Checks", id = "CheckRadiusRatio", name = "Radius Ratio", editor = "bool", default = true, help = "Perform check of the max to min radius ratio" }, | |
| { category = "Checks", id = "CheckVisibility", name = "Objects Visibility", editor = "bool", default = true, help = "Search for invisible or completely transparent objects" }, | |
| { category = "Stats", id = "ClassToCountStat", name = "Objs by Class", editor = "text", default = "", lines = 1, max_lines = 10, read_only = true, developer = true, dont_save = true }, | |
| { category = "Stats", id = "ClassToCount", editor = "prop_table", default = false, no_edit = true }, | |
| { category = "Stats", id = "ExportTime", name = "Prefab Export (ms)", editor = "number", default = 0, read_only = true, dont_save = true }, | |
| { category = "Debug", id = "source", name = "Source", editor = "prop_table", default = false, export = "marker", read_only = true, indent = ' ', buttons = {{name = "View", func = "ActionViewSource"}}}, | |
| { category = "Debug", id = "place_mark", name = "Place Mark", editor = "number", default = -1, read_only = true, dont_save = true }, | |
| { category = "Debug", id = "apply_pos", name = "Apply Pos", editor = "point", default = false, read_only = true, dont_save = true }, | |
| { category = "Debug", id = "DebugShow", name = "Debug Show", editor = "set", default = def_show_types, items = debug_show_types, developer = true, dont_save = true, help = "collections and optional_objs cannot be used at the same time"}, | |
| { category = "Debug", id = "ShowType", name = "Show Terrain Type", editor = "set", default = set(), items = get_marker_terrains, developer = true, dont_save = true, buttons = {{name = "Update", func = "ActionShowTypeUpdate"}}, }, | |
| { category = "Debug", id = "OverlayAlpha", name = "Overlay Alpha (%)", editor = "number", default = 30, slider = true, min = 0, max = 100, dont_save = true }, | |
| }, | |
| editor_objects_visible = false, | |
| editor_objects = false, | |
| object_colors = false, | |
| editor_update_thread = false, | |
| editor_update_time = false, | |
| DebugErrorShow = false, | |
| } | |
| function PrefabMarkerEdit:ActionShowTypeUpdate() | |
| self:DbgShowTypes() | |
| end | |
| function DebugOverlayControl:SetOverlayAlpha(alpha) | |
| hr.TerrainDebugAlphaPerc = alpha | |
| end | |
| function DebugOverlayControl:GetOverlayAlpha() | |
| return hr.TerrainDebugAlphaPerc | |
| end | |
| if FirstLoad then | |
| dbg_common_grid = false | |
| end | |
| function OnMsg.ChangeMap() | |
| DbgHideTerrainGrid(dbg_common_grid) | |
| dbg_common_grid = false | |
| end | |
| function DbgUpdateTypesGrid() | |
| local tgrid, dest_grid, type_to_palette, palette, mask, tmp | |
| local last_palette_idx = 1 | |
| local markers = MapGet("map", "PrefabMarker") | |
| for _, marker in ipairs(markers) do | |
| local remap, grid | |
| for tname, show in pairs(IsValid(marker) and marker.ShowType or empty_table) do | |
| local type_idx = show and GetTerrainTextureIndex(tname) | |
| if type_idx then | |
| type_to_palette = type_to_palette or {} | |
| local palette_idx = type_to_palette[type_idx] | |
| if not palette_idx then | |
| palette_idx = last_palette_idx + 1 | |
| last_palette_idx = palette_idx | |
| type_to_palette[type_idx] = palette_idx | |
| palette = palette or {0} | |
| palette[palette_idx] = RandColor(xxhash(tname)) | |
| end | |
| remap = remap or {} | |
| remap[type_idx] = palette_idx | |
| end | |
| end | |
| if remap then | |
| tgrid = tgrid or terrain.GetTypeGrid() | |
| dest_grid = dest_grid or GridDest(tgrid, true) | |
| mask = mask or GridDest(tgrid) | |
| tmp = tmp or GridDest(tgrid) | |
| mask:clear() | |
| GridDrawBox(mask, marker:GetBBox():grow(type_tile / 2) / type_tile, 1) | |
| GridMulDiv(tgrid, tmp, mask, 1) | |
| GridReplace(tmp, remap, 0) | |
| GridAdd(dest_grid, tmp) | |
| end | |
| end | |
| if not dest_grid then | |
| DbgHideTerrainGrid(dbg_common_grid) | |
| dbg_common_grid = false | |
| else | |
| DbgShowTerrainGrid(dest_grid, palette) | |
| dbg_common_grid = dest_grid | |
| end | |
| end | |
| function PrefabMarkerEdit:DbgShowTypes() | |
| CreateRealTimeThread(DbgUpdateTypesGrid) | |
| end | |
| local function ApplyObjectColors(obj_colors, apply) | |
| for obj, obj_color in pairs(obj_colors or empty_table) do | |
| local new_clr, orig_clr = table.unpack(obj_color) | |
| if IsValid(obj) then | |
| local prev_clr = obj:GetColorModifier() | |
| if apply and new_clr ~= prev_clr then | |
| obj_color[2] = prev_clr | |
| obj:SetColorModifier(new_clr) | |
| elseif not apply and orig_clr and prev_clr == new_clr then | |
| obj:SetColorModifier(orig_clr) | |
| end | |
| end | |
| end | |
| end | |
| function PrefabMarkerEdit:EditorObjectsDestroy() | |
| DoneObjects(self.editor_objects ) | |
| ApplyObjectColors(self.object_colors, false) | |
| self.editor_objects = nil | |
| self.object_colors = nil | |
| end | |
| function PrefabMarkerEdit:PostLoad(reason) | |
| self:EditorObjectsCreate() | |
| end | |
| function PrefabMarkerEdit:Done() | |
| self:EditorObjectsDestroy() | |
| end | |
| local function GridExtrem(grid) | |
| local laplacian = { | |
| -8, -11, -8, | |
| -11, 76, -11, | |
| -8, -11, -8, | |
| } | |
| local extrem = GridFilter(grid, laplacian, 76) | |
| GridMulDiv(extrem, height_scale * height_roughness_unity, height_tile) | |
| GridAbs(extrem) | |
| return extrem | |
| end | |
| local function PrefabEvalPlayableArea(height_map, mask, tile_size, play_zone, border) | |
| local mw, mh = height_map:size() | |
| local flat_zone = GridSlope(height_map, tile_size, height_scale) | |
| local max_play_sin = sin(const.RandomMap.PrefabMaxPlayAngle) | |
| GridMulDiv(flat_zone, 4096, 1) | |
| GridMask(flat_zone, 0, max_play_sin) | |
| if mask then | |
| GridAnd(flat_zone, mask) | |
| end | |
| border = border and (border + tile_size - 1) / tile_size or 0 | |
| if border > 0 then | |
| assert(border >= 0 and 2 * border < Min(mw, mh), "Invalid border size") | |
| GridFrame(flat_zone, border, 0) | |
| end | |
| if play_zone then | |
| play_zone:clear() | |
| end | |
| local play_area = 0 | |
| local min_play_radius = const.RandomMap.PrefabMinPlayRadius | |
| local radius = min_play_radius / tile_size | |
| local min_area = radius * radius * 22 / 7 | |
| local zones = GridEnumZones(flat_zone, min_area) | |
| local level_dist = GridDest(flat_zone) | |
| for i=1,#zones do | |
| local zone = zones[i] | |
| assert(zone.size >= min_area) | |
| GridMask(flat_zone, level_dist, zone.level) | |
| GridDistance(level_dist, tile_size, min_play_radius) | |
| local minv, maxv = GridMinMax(level_dist) | |
| if maxv >= min_play_radius then | |
| play_area = play_area + zone.size | |
| if play_zone then | |
| GridOr(play_zone, level_dist) | |
| end | |
| end | |
| end | |
| return play_area | |
| end | |
| function PrefabMarkerEdit:EditorObjectsCreate() | |
| if not self:IsValidPos() then | |
| StoreErrorSource("silent", self, "Object on invalid pos!") | |
| return | |
| end | |
| self:EditorObjectsDestroy() | |
| local show = self.DebugShow | |
| if self.DebugErrorShow then | |
| show = table.copy(show) | |
| show[self.DebugErrorShow] = true | |
| end | |
| local objects, obj_colors = {}, {} | |
| local points, colors = {}, {} | |
| local function add_line() | |
| if #(points or "") == 0 then | |
| return | |
| end | |
| local v_pstr = pstr("") | |
| local line = PlaceObject("Polyline") | |
| for i, point in ipairs(points) do | |
| v_pstr:AppendVertex(point, colors[i]) | |
| end | |
| --line:SetDepthTest(true) | |
| line:SetMesh(v_pstr) | |
| line:SetPos(AveragePoint(points)) | |
| objects[#objects + 1] = line | |
| points, colors = {}, {} | |
| end | |
| local function add_vector(pt, vec, color) | |
| vec = vec or 10*guim | |
| if type(vec) == "number" then | |
| vec = point(0, 0, vec) | |
| end | |
| local v_pstr = pstr("") | |
| v_pstr:AppendVertex(pt, color) | |
| v_pstr:AppendVertex(pt + vec) | |
| local line = PlaceObject("Polyline") | |
| line:SetMesh(v_pstr) | |
| line:SetPos(pt) | |
| objects[#objects + 1] = line | |
| end | |
| local function add_circle(center, radius, color) | |
| local circle = PlaceTerrainCircle(center, radius, color) | |
| --circle:SetDepthTest(false) | |
| objects[#objects + 1] = circle | |
| end | |
| local pt1 = self:GetVisualPos() | |
| local x0, y0, z0 = pt1:xyz() | |
| local clr_mod = SetA(self:GetColorModifier(), 255) | |
| local color = clr_mod ~= clrNoModifier and clr_mod or self.ExportError ~= "" and red or editor.IsSelected(self) and cyan or white | |
| local terrain_lines = {} | |
| local angle = self:GetAngle() | |
| local w0, h0 = self.CaptureSize:xy() | |
| local selected = editor.IsSelected(self) | |
| if show.capture_box and w0 > type_tile and h0 > type_tile then | |
| if angle == 0 then | |
| local box = PlaceBox(box(x0, y0, guim, x0 + w0 - type_tile, y0 + h0 - type_tile, guim), color, nil, "depth test") | |
| box:AddMeshFlags(const.mfTerrainDistorted) | |
| objects[#objects + 1] = box | |
| else | |
| local w = w0 - type_tile | |
| local h = h0 - type_tile | |
| local edges = { | |
| point(-w,-h), | |
| point( w,-h), | |
| point( w, h), | |
| point(-w, h), | |
| } | |
| if not self.Centered then | |
| for i=1,#edges do | |
| edges[i] = edges[i] + point(w0, h0) | |
| end | |
| end | |
| if angle then | |
| for i=1,#edges do | |
| edges[i] = Rotate(edges[i], angle) | |
| end | |
| end | |
| for i=1,#edges do | |
| edges[i] = pt1 + edges[i] / 2 | |
| end | |
| edges[#edges + 1] = edges[1] | |
| for i=1,#edges-1 do | |
| local line = PlaceTerrainLine(edges[i], edges[i + 1], color) | |
| objects[#objects + 1] = line | |
| end | |
| end | |
| end | |
| local minr, maxr = self.RadiusMin, self.RadiusMax * type_tile | |
| if show.radius and maxr > 0 then | |
| local prefab = { | |
| min_radius = self.RadiusMin * type_tile, | |
| max_radius = self.RadiusMax * type_tile, | |
| total_area = self.TotalArea * type_tile * type_tile, | |
| } | |
| local pos = pt1 + Rotate(self.CaptureSize / 2, angle) | |
| local estimators = PrefabRadiusEstimators() | |
| local items = PrefabRadiusEstimItems() | |
| for _, item in ipairs(items) do | |
| local estimator = estimators[item.value] | |
| add_circle(pos, estimator(prefab), item.color) | |
| local r, g, b = GetRGB(item.color) | |
| printf("once", "<color %d %d %d>%s</color>", r, g, b, item.text) | |
| end | |
| end | |
| local function add_box(center, size, color) | |
| local edges = { | |
| point(-1,-1) * size / 2, | |
| point( 1,-1) * size / 2, | |
| point( 1, 1) * size / 2, | |
| point(-1, 1) * size / 2, | |
| } | |
| for i=1,#edges do | |
| local pt = edges[i] + center | |
| edges[i] = set_surface_z(pt, guim/2) | |
| end | |
| local dz = point(0, 0, 2*size) | |
| edges[#edges + 1] = edges[1] | |
| local N = 1 | |
| for i=1,#edges do | |
| points[N] = edges[i] | |
| colors[N] = color | |
| N = N + 1 | |
| end | |
| add_line() | |
| end | |
| local height_map = self.HeightMap | |
| local type_map = self.TypeMap | |
| local mask = self.MaskMap | |
| local msize = mask and mask:size() or 0 | |
| local require_ex = show.flat_zone or show.crit_slope or show.roughness or show.transition or show.height_offset | |
| local height_map_ex = require_ex and height_map and GridRepack(height_map, "F") | |
| local height_offset = self.HeightOffset | |
| if height_map_ex then | |
| GridAdd(height_map_ex, height_offset) | |
| end | |
| local mask_ex = require_ex and mask and GridRepack(mask, "F") | |
| local xc2, yc2 = 2 * x0 + w0 + 1, 2 * y0 + h0 + 1 | |
| local function show_grid(grid, minv, maxv, color0, color1) | |
| local w, h = grid:size() | |
| local min_step = type_tile | |
| local max_step = type_tile * Min(w, h) / 64 | |
| local step = Max(min_step, Min(max_step, type_tile)) | |
| color0 = color0 or red | |
| minv = minv or min_int | |
| maxv = maxv or max_int | |
| local count, max_count = 0, 8*1024 | |
| local data = {} | |
| GridForeach(grid, function(v, x, y) | |
| count = count + 1 | |
| if count >= max_count then | |
| return | |
| end | |
| local px = (xc2 + (2 * x - w + 1) * type_tile) / 2 | |
| local py = (yc2 + (2 * y - h + 1) * type_tile) / 2 | |
| local clr = color1 and InterpolateRGB(color0, color1, v - minv, maxv - minv) or color0 | |
| data[#data + 1] = {point(px, py), clr} | |
| end, minv, maxv, step, type_tile) | |
| if count >= max_count then | |
| print("Debug show cancelled, too much to draw!") | |
| end | |
| for i=1,#data do | |
| local pos, clr = table.unpack(data[i]) | |
| add_box(pos, step - min_step/2, clr) | |
| if count < 100 then | |
| add_vector(set_surface_z(pos), 10*guim, clr) | |
| end | |
| end | |
| end | |
| if show.flat_zone and height_map_ex and mask_ex then | |
| local flat_zone = GridDest(height_map_ex) | |
| PrefabEvalPlayableArea(height_map_ex, mask_ex, type_tile, flat_zone) | |
| show_grid(flat_zone, 0, max_int, green) | |
| end | |
| local hsize = height_map_ex and height_map_ex:size() or 0 | |
| local function grid_to_world(gx, gy) | |
| if not gy then | |
| return point(grid_to_world(gx:xy())) | |
| end | |
| local x = (xc2 + (2 * gx - hsize + 1) * height_tile) / 2 | |
| local y = (yc2 + (2 * gy - hsize + 1) * height_tile) / 2 | |
| return x, y | |
| end | |
| if show.crit_slope and hsize > 0 then | |
| local crit_angle = const.MaxPassableTerrainSlope | |
| local tol_angle = 3*60 + 30 | |
| local mesh = {} | |
| local slope = GridSlope(height_map_ex, height_tile, height_scale) | |
| GridASin(slope, true, 180*60) | |
| GridAdd(slope, -crit_angle) | |
| GridAbs(slope) | |
| show_grid(slope, 0, tol_angle, red, yellow) | |
| end | |
| if show.height_lims and hsize > 0 then | |
| local minv, maxv, minp, maxp = GridMinMax(height_map, true) | |
| minp = grid_to_world(minp):SetZ(minv) | |
| maxp = grid_to_world(maxp):SetZ(maxv) | |
| add_vector(minp, 100*guim, red) | |
| add_vector(maxp, 100*guim, green) | |
| add_circle(minp, 2*guim, red) | |
| add_circle(maxp, 2*guim, green) | |
| end | |
| if show.roughness and hsize > 0 then | |
| local extrem = GridExtrem(height_map_ex, height_tile, height_scale) | |
| show_grid(extrem, height_roughness_err) | |
| end | |
| if show.height_offset and height_map and msize then | |
| local mesh = {} | |
| local outline = GridDest(mask_ex) | |
| GridOutline(mask_ex, outline, true) | |
| GridMulDiv(outline, height_map_ex, 1) | |
| GridAbs(outline) | |
| show_grid(outline, height_outline_offset_err) | |
| end | |
| if show.transition and msize > 0 then | |
| show_grid(mask_ex, 1, 255 - 1, yellow, red) | |
| end | |
| local objs | |
| if show.large_objs then | |
| local obj_max_radius = const.RandomMap.PrefabMaxObjRadius or GetEntityMaxSurfacesRadius() | |
| objs = objs or self:CollectObjs() | |
| for _, obj in ipairs(objs) do | |
| if obj:GetRadius() > obj_max_radius then | |
| local bbox = PlaceBox(ObjectHierarchyBBox(obj), red, nil, "depth test") | |
| objects[#objects + 1] = bbox | |
| StoreErrorSource(obj, "Too large object") | |
| end | |
| end | |
| end | |
| if show.optional_objs then | |
| objs = objs or self:CollectObjs() | |
| for _, obj in ipairs(objs) do | |
| if obj:GetOptionalPlacement() then | |
| obj_colors[obj] = {cyan, 0} | |
| end | |
| end | |
| elseif show.collections then | |
| objs = objs or self:CollectObjs() | |
| local hsb_max = 1020 | |
| local Collections = Collections | |
| local GetCollectionIndex = CObject.GetCollectionIndex | |
| local clrNoModifier = const.clrNoModifier | |
| local topmost_coll, nested_count, parents_count = {}, {}, {} | |
| for _, obj in ipairs(objs) do | |
| local col_idx = GetCollectionIndex(obj) or 0 | |
| if col_idx ~= 0 and not parents_count[col_idx] then | |
| local topmost_idx = col_idx | |
| local parents = 0 | |
| while true do | |
| local topmost = Collections[topmost_idx] | |
| local parent_idx = GetCollectionIndex(topmost) or 0 | |
| if parent_idx == 0 then | |
| break | |
| end | |
| parents = parents + 1 | |
| topmost_idx = parent_idx | |
| end | |
| nested_count[topmost_idx] = Max(nested_count[topmost_idx] or 0, parents) | |
| parents_count[col_idx] = parents | |
| topmost_coll[col_idx] = topmost_idx | |
| end | |
| end | |
| local col_to_color, topmost_colors = {}, {} | |
| for _, obj in ipairs(objs) do | |
| local col_idx = obj:GetCollectionIndex() or 0 | |
| if col_idx ~= 0 then | |
| local color = col_to_color[col_idx] | |
| if not color then | |
| local topmost_idx = topmost_coll[col_idx] | |
| local topmost_color = topmost_colors[topmost_idx] | |
| local nested_max = nested_count[topmost_idx] | |
| if not topmost_color then | |
| local max_dist, h, s | |
| local b = (hsb_max * 2 - hsb_max * Min(4, nested_max) / 4) / 3 | |
| local i = 0 | |
| local rand = BraidRandomCreate(col_idx) | |
| while true do | |
| h, s = rand(hsb_max + 1), rand(hsb_max + 1) | |
| local rand_clr = HSB(h, s, b, hsb_max) | |
| if ColorDist(rand_clr, clrNoModifier) > 100 then | |
| i = i + 1 | |
| if i == 10 then | |
| break | |
| end | |
| local min_dist = max_int | |
| for _, clr in pairs(topmost_colors) do | |
| min_dist = Min(min_dist, ColorDist(clr, rand_clr)) | |
| end | |
| if not max_dist or max_dist < min_dist then | |
| max_dist = min_dist | |
| topmost_color = rand_clr | |
| if max_dist > 100 then | |
| break | |
| end | |
| end | |
| end | |
| end | |
| topmost_colors[topmost_idx] = topmost_color | |
| end | |
| local parents = parents_count[col_idx] | |
| if parents == 0 then | |
| color = topmost_color | |
| else | |
| local h, s, b = RGBtoHSB(topmost_color, hsb_max) | |
| local s1 = (s > hsb_max * 2 / 3) and (hsb_max / 3) or hsb_max | |
| local ds = (s1 - s) * Min(4, nested_max) / 4 | |
| local db = hsb_max - b | |
| s = s + ds * parents / nested_max | |
| b = b + db * parents / nested_max | |
| color = HSB(h, s, b, hsb_max) | |
| end | |
| col_to_color[col_idx] = color | |
| end | |
| obj_colors[obj] = {color, 0} | |
| end | |
| end | |
| end | |
| ApplyObjectColors(obj_colors, true) | |
| self.object_colors = obj_colors | |
| self.editor_objects = objects | |
| end | |
| function PrefabMarkerEdit:EditorObjectsUpdate() | |
| self:EditorObjectsDestroy() | |
| self.editor_update_time = RealTime() + 30 | |
| if IsValidThread(self.editor_update_thread) then | |
| return | |
| end | |
| self.editor_update_thread = CreateRealTimeThread(function() | |
| while RealTime() < self.editor_update_time do | |
| Sleep(self.editor_update_time - RealTime()) | |
| end | |
| if IsValid(self) then | |
| self:EditorObjectsShow() | |
| end | |
| end) | |
| end | |
| function PrefabMarkerEdit:EditorObjectsShow(show) | |
| if show == nil then show = IsEditorActive() end | |
| local prev_show = self.editor_objects and self.editor_objects_visible | |
| if prev_show == show then | |
| return | |
| end | |
| self.editor_objects_visible = show | |
| if not self.editor_objects and show then | |
| self:EditorObjectsCreate() | |
| end | |
| for _, object in ipairs(self.editor_objects or empty_table) do | |
| if IsValid(object) then | |
| object:SetVisible(show) | |
| end | |
| end | |
| ApplyObjectColors(self.object_colors, show) | |
| self:SetVisible(show) | |
| if IsValid(self.editor_text_obj) then | |
| self.editor_text_obj:SetVisible(show) | |
| end | |
| ObjModified(self) | |
| PropertyHelpers_Refresh(self) | |
| end | |
| function PrefabMarkerEdit:EditorEnter() | |
| self:EditorObjectsShow(true) | |
| end | |
| function PrefabMarkerEdit:EditorExit() | |
| self:EditorObjectsShow(false) | |
| end | |
| function PrefabMarkerEdit:OnEditorSetProperty(prop_id, old_value) | |
| if prop_id == "DebugShow" then | |
| local show = self.DebugShow | |
| if show.collections and show.optional_objs then | |
| show = table.copy(show) | |
| if old_value.collections then -- collections and optional objects are mutually exclusive | |
| show.collections = nil | |
| else | |
| show.optional_objs = nil | |
| end | |
| self.DebugShow = show | |
| end | |
| self:EditorObjectsUpdate() | |
| end | |
| if prop_id == "Centered" or prop_id == "ColorModifier" then | |
| self:EditorObjectsUpdate() | |
| end | |
| if prop_id == "ShowType" then | |
| self:DbgShowTypes() | |
| elseif prop_id == "CaptureSize" then | |
| local w, h = self.CaptureSize:xy() | |
| if w < 0 then w = max_int end | |
| if h < 0 then h = max_int end | |
| local x, y = self:GetVisualPosXYZ() | |
| w = Min(w, terrain.GetMapWidth() - x) | |
| h = Min(h, terrain.GetMapHeight() - y) | |
| w = (w / granularity) * granularity | |
| h = (h / granularity) * granularity | |
| self.CaptureSize = point(w, h) | |
| self:EditorObjectsUpdate() | |
| elseif prop_id == "CircleMaskRadius" then | |
| self:CaptureTerrain() | |
| end | |
| end | |
| PrefabMarkerEdit.EditorCallbackPlace = PrefabMarkerEdit.EditorObjectsUpdate | |
| PrefabMarkerEdit.EditorCallbackRotate = PrefabMarkerEdit.EditorObjectsUpdate | |
| PrefabMarkerEdit.EditorCallbackMove = PrefabMarkerEdit.EditorObjectsUpdate | |
| local function GetMaxNegShape(mask) | |
| GridNot(mask) | |
| local zones = GridEnumZones(mask, 32, max_int, 256) | |
| if #zones == 256 then | |
| return "Too many shapes found" | |
| end | |
| local zone = table.max(zones, "size") | |
| if not zone then | |
| return "No shapes found" | |
| end | |
| GridMask(mask, zone.level) | |
| end | |
| function PrefabMarkerEdit:GetSkippedTextureList() | |
| local indexes | |
| for _, terrain in ipairs(self.SkippedTerrains or empty_table) do | |
| local idx = GetTerrainTextureIndex(terrain) | |
| if not idx then | |
| StoreErrorSource(self, "Terrain name not found:", terrain) | |
| else | |
| indexes = indexes or {} | |
| indexes[#indexes + 1] = idx | |
| end | |
| end | |
| return indexes or empty_table | |
| end | |
| function PrefabMarkerEdit:GetBBox() | |
| local bbox | |
| local x, y = self:GetVisualPosXYZ() | |
| local w, h = self.CaptureSize:xy() | |
| if w <= 0 or h <= 0 then | |
| return | |
| end | |
| if self.Centered then | |
| local dx, dy = w / 2, h / 2 | |
| return box(x - dx, y - dy, x + dx, y + dy) | |
| else | |
| return box(x, y, x + w, y + h) | |
| end | |
| end | |
| function PrefabMarkerEdit:SetBBox(bbox) | |
| local x, y, z = self:GetPosXYZ() | |
| local bw, bh = bbox:size():xy() | |
| local x1, y1 | |
| if self.Centered then | |
| x1, y1 = bbox:Center():xy() | |
| else | |
| x1, y1 = bbox:minxyz() | |
| end | |
| self:SetPos(x1, y1, z) | |
| self.CaptureSize = point(bw, bh) | |
| end | |
| function PrefabMarkerEdit:CaptureTerrain(shrinked, extended) | |
| local st = GetPreciseTicks() | |
| self:ClearTerrain() | |
| local bbox = self:GetBBox() | |
| if not bbox then | |
| return | |
| end | |
| local abox = bbox:Align(granularity) | |
| if abox ~= bbox then | |
| bbox = abox | |
| self:SetBBox(abox) | |
| end | |
| local memory = 0 | |
| local x, y, z = self:GetVisualPosXYZ() | |
| local w, h = self.CaptureSize:xy() | |
| local mask | |
| local invalid_terrain_idx = -1 | |
| if self.InvalidTerrain ~= "" then | |
| invalid_terrain_idx = GetTerrainTextureIndex(self.InvalidTerrain) or -1 | |
| if invalid_terrain_idx == -1 then | |
| StoreErrorSource(self, "Invalid terrain type specified") | |
| end | |
| end | |
| local transition_dist = self:GetTransitionDist() | |
| if self.CircleMask then | |
| mask = GridGetEmptyMask(bbox) | |
| local radius = self.CircleMaskRadius or Min(w, h) / 2 | |
| GridCircleSet(mask, mask_max, w / 2, h / 2, radius, transition_dist, type_tile) | |
| elseif invalid_terrain_idx ~= -1 then | |
| local extend_retries = 0 | |
| local post_process = self.PostProcess or 0 | |
| while true do | |
| mask = GridGetTerrainMask(bbox, invalid_terrain_idx) | |
| if not mask then | |
| return | |
| elseif GridEquals(mask, 0) then | |
| StoreErrorSource(self, "Only invalid terrain found!") | |
| return | |
| elseif not GridFind(mask, 0) then | |
| StoreErrorSource(self, "Invalid terrain not found!") | |
| return | |
| end | |
| if post_process > 0 then | |
| local err = GetMaxNegShape(mask) or GetMaxNegShape(mask) | |
| if err then | |
| StoreErrorSource(self, err) | |
| return | |
| end | |
| end | |
| if extended or post_process < 2 then | |
| break | |
| end | |
| local mw, mh = mask:size() | |
| local minx, miny, maxx, maxy = GridBBox(mask) | |
| if minx > 0 and maxx < mw and miny > 0 and maxy < mh then | |
| if extend_retries == 0 then | |
| break | |
| end | |
| self:SetBBox(bbox) | |
| return self:CaptureTerrain(false, true) | |
| elseif extend_retries > 100 then | |
| StoreErrorSource(self, "Capture size extend error. Adjustment disabled!") | |
| return self:CaptureTerrain(true, true) | |
| end | |
| local bminx, bminy, bmaxx, bmaxy = bbox:xyxy() | |
| if minx <= 0 then bminx = bminx - granularity end | |
| if miny <= 0 then bminy = bminy - granularity end | |
| if maxx >= mw then bmaxx = bmaxx + granularity end | |
| if maxy >= mh then bmaxy = bmaxy + granularity end | |
| bbox = box(bminx, bminy, bmaxx, bmaxy) | |
| assert(bbox == bbox:Align(granularity)) | |
| extend_retries = extend_retries + 1 | |
| end | |
| if not shrinked and post_process > 1 then | |
| local minx, miny, maxx, maxy = GridBBox(mask) | |
| local new_bbox = box( | |
| x + (minx - 1) * type_tile, y + (miny - 1) * type_tile, | |
| x + (maxx + 1) * type_tile, y + (maxy + 1) * type_tile) | |
| new_bbox = new_bbox:Align(granularity) | |
| if new_bbox ~= bbox then | |
| self:SetBBox(new_bbox) | |
| return self:CaptureTerrain(true, true) | |
| end | |
| end | |
| if transition_dist > 0 then | |
| GridNot(mask) | |
| local fmask = GridRepack(mask, "F") | |
| GridDistance(fmask, type_tile, transition_dist, false) -- no approximation, will take longer to compute | |
| GridMulDiv(fmask, mask_max, transition_dist) | |
| GridRound(fmask) | |
| GridRepack(fmask, mask) | |
| fmask:free() | |
| else | |
| GridNormalize(mask, 0, mask_max) | |
| end | |
| end | |
| assert(bbox == bbox:Align(granularity)) | |
| if mask then | |
| self.MaskHash = mask and xxhash(mask) | |
| self.MaskMap = mask | |
| memory = memory + GridGetSizeInBytes(mask) | |
| end | |
| local capture_type = self.CaptureSet | |
| if capture_type.Terrain then | |
| local skipped_terrain_idxs = self:GetSkippedTextureList() | |
| local type_map, types = GridGetTerrainType(bbox, mask, invalid_type_value, invalid_terrain_idx, skipped_terrain_idxs) | |
| if not type_map then | |
| return | |
| end | |
| local type_names | |
| for _, idx in ipairs(types or empty_table) do | |
| local texture = TerrainTextures[idx] | |
| local name = texture and texture.id | |
| if name then | |
| type_names = type_names or {} | |
| type_names[name] = idx | |
| end | |
| end | |
| self.TypeHash = xxhash(type_map) | |
| self.TypeMap = type_map | |
| self.TypeNames = type_names | |
| memory = memory + GridGetSizeInBytes(type_map) | |
| end | |
| --DbgClearVectors() DbgAddTerrainRect(bbox) | |
| if capture_type.Height then | |
| local terrain_z = z / height_scale | |
| local height_map, hmin, hmax = GridGetTerrainHeight(bbox, mask, terrain_z) | |
| if not height_map then | |
| return | |
| end | |
| self.HeightHash = xxhash(height_map) | |
| self.HeightMap = height_map | |
| self.HeightOffset = -terrain_z | |
| self.HeightMin = hmin - point(0, 0, terrain_z) | |
| self.HeightMax = hmax - point(0, 0, terrain_z) | |
| memory = memory + GridGetSizeInBytes(height_map) | |
| end | |
| if capture_type.Grass then | |
| local grass_map = GridGetTerrainGrass(bbox, mask, invalid_grass_value, self.InvalidGrass) | |
| if not grass_map then | |
| return | |
| end | |
| if not GridEquals(grass_map, 0) then | |
| self.GrassHash = xxhash(grass_map) | |
| self.GrassMap = grass_map | |
| end | |
| memory = memory + GridGetSizeInBytes(grass_map) | |
| end | |
| self.TerrainCaptureTime = GetPreciseTicks() - st | |
| self.RequiredMemory = memory | |
| self:EditorObjectsUpdate() | |
| return bbox | |
| end | |
| function PrefabMarkerEdit:ActionCaptureTerrain() | |
| self:CaptureTerrain() | |
| ObjModified(self) | |
| PropertyHelpers_Refresh(self) | |
| end | |
| function PrefabMarkerEdit:ActionClearTerrain() | |
| self:ClearTerrain() | |
| ObjModified(self) | |
| PropertyHelpers_Refresh(self) | |
| end | |
| function PrefabMarkerEdit:TerrainRectIsCentered() | |
| return self.Centered | |
| end | |
| function PrefabMarkerEdit:TerrainRectIsEnabled(prop_id) | |
| return prop_id == "CaptureSize" and next(self.CaptureSet) and not self.HeightHash and not self.TypeHash and not self.GrassHash | |
| end | |
| function PrefabMarkerEdit:OnEditorSelect(selected) | |
| self:EditorObjectsUpdate() | |
| end | |
| function PrefabMarkerEdit:ClearTerrain() | |
| self.HeightMap = nil | |
| self.HeightHash = nil | |
| self.HeightOffset = nil | |
| self.HeightMin = nil | |
| self.HeightMax = nil | |
| self.TypeMap = nil | |
| self.TypeHash = nil | |
| self.TypeNames = nil | |
| self.MaskMap = nil | |
| self.MaskHash = nil | |
| self.GrassMap = nil | |
| self.GrassHash = nil | |
| end | |
| function PrefabMarkerEdit:EditorCallbackDelete() | |
| self:EditorObjectsDestroy() | |
| self:DeleteExports() | |
| end | |
| function PrefabMarkerEdit:CollectObjs(bbox) | |
| bbox = bbox or self:GetBBox() | |
| return bbox and (MapGet(bbox, "attached", false, nil, nil, gofPermanent) or empty_table) or {self} | |
| end | |
| function PrefabMarkerEdit:ForceEditorMode(set) | |
| if not IsEditorActive() then | |
| return | |
| end | |
| MapForEach(self:GetBBox(), "attached", false, "EditorObject", nil, nil, gofPermanent, function(obj, set, self) | |
| if set then | |
| obj:EditorEnter() | |
| else | |
| obj:EditorExit() | |
| end | |
| end, set, self) | |
| end | |
| function PrefabMarkerEdit:ActionPrefabExport(root) | |
| local err, objs, defs = self:ExportPrefab() | |
| if err then | |
| print("Prefab", self:GetPrefabName(), "export failed:", err) | |
| else | |
| DebugPrint(TableToLuaCode(defs)) | |
| end | |
| end | |
| function PrefabMarkerEdit:ActionPrefabRevision() | |
| local info = self:GetRevisionInfo() | |
| if info then | |
| self:ShowMessage(info, "Revision") | |
| end | |
| end | |
| function PrefabMarkerEdit:ActionExploreTo() | |
| local exported = self.ExportedName or "" | |
| if exported ~= "" and IsFSUnpacked() and ExportedPrefabs[exported] then | |
| local filename = GetPrefabFileObjs(exported) | |
| local dir, file, ext = SplitPath(filename) | |
| AsyncExec("explorer " .. ConvertToOSPath(dir)) | |
| end | |
| end | |
| local function svn_process(file, prev_hash, new_hash) | |
| if new_hash then | |
| if SvnToAdd then | |
| SvnToAdd[#SvnToAdd + 1] = file | |
| return | |
| end | |
| return SVNAddFile(file) | |
| elseif not new_hash and prev_hash then | |
| if SvnToDel then | |
| SvnToDel[#SvnToDel + 1] = file | |
| return | |
| end | |
| return SVNDeleteFile(file) | |
| end | |
| end | |
| if FirstLoad then | |
| SvnToAdd = false | |
| SvnToDel = false | |
| end | |
| function OnMsg.MarkersRebuildStart() | |
| SvnToAdd, SvnToDel = {}, {} | |
| end | |
| function OnMsg.MarkersRebuildEnd() | |
| SVNAddFile(SvnToAdd) | |
| SVNDeleteFile(SvnToDel) | |
| SvnToAdd, SvnToDel = false, false | |
| end | |
| local function PrefabToMarkerName(name) | |
| return name and ("Prefab." .. name) or "" | |
| end | |
| function ShowPrefabMarker(prefab_name, ged) | |
| local marker_name = PrefabToMarkerName(prefab_name) | |
| local marker = Markers[marker_name] | |
| if not marker then | |
| print("No such prefab marker:", prefab_name) | |
| return | |
| end | |
| EditorWaitViewMapObjectByHandle(marker.handle, marker.map, ged) | |
| end | |
| function GotoPrefabAction(root, obj, prop_id, ged) | |
| local name = obj[prop_id] or "" | |
| if name == "" then | |
| print("No prefab provided") | |
| return | |
| end | |
| ShowPrefabMarker(name, ged) | |
| end | |
| local function GetMarkerSource(prefab_name) | |
| local marker_props = PrefabMarkers[prefab_name] | |
| local source = marker_props and marker_props.marker | |
| if not source then | |
| local marker_name = PrefabToMarkerName(prefab_name) | |
| source = Markers[marker_name] | |
| end | |
| return source or empty_table | |
| end | |
| function PrefabMarkerEdit:DeleteExports() | |
| local name = self.ExportedName or "" | |
| if name == "" or not ExportedPrefabs[name] then | |
| return | |
| end | |
| local marker = GetMarkerSource(name) | |
| if marker.handle ~= self.handle or marker.map ~= GetMapName() then | |
| return | |
| end | |
| ExportedPrefabs[name] = nil | |
| SVNDeleteFile{ | |
| GetPrefabFileObjs(name), | |
| GetPrefabFileType(name), | |
| GetPrefabFileGrass(name), | |
| GetPrefabFileHeight(name), | |
| GetPrefabFileMask(name), | |
| } | |
| end | |
| function PrefabMarkerEdit:DbgShow(what) | |
| self.DebugErrorShow = what | |
| end | |
| function PrefabMarkerEdit:GetClassToCountStat() | |
| local list = {} | |
| for class,count in pairs(self.ClassToCount or empty_table) do | |
| list[#list+1] = {class = class, count = count} | |
| end | |
| table.sortby_field_descending(list, "count") | |
| for i, entry in ipairs(list) do | |
| list[i] = string.format("%s = %d\n", entry.class, entry.count) | |
| end | |
| return table.concat(list) | |
| end | |
| function PrefabMarkerEdit:GetCaptureCenter() | |
| return self:GetVisualPos() + Rotate(self.CaptureSize:SetZ(0), self:GetAngle()) / 2 | |
| end | |
| function PrefabMarkerEdit:ExportPrefab() | |
| self:ForceEditorMode(false) | |
| local err, param1, param2 = self:DoExport() | |
| self:ForceEditorMode(true) | |
| self.ExportError = err | |
| self:EditorObjectsUpdate() | |
| self:EditorTextUpdate() | |
| return err, param1, param2 | |
| end | |
| local function save_grid(grid, filename, old_hash, new_hash) | |
| if old_hash == new_hash and io.exists(filename) then | |
| return | |
| end | |
| if grid then | |
| local success, err = GridWriteFile(grid, filename, true) | |
| if err then | |
| return err | |
| end | |
| end | |
| svn_process(filename, old_hash, new_hash) | |
| end | |
| local function DumpObjDiffs(filename, defs, name, bin, hash, prev_hash) | |
| print("DumpObjDiffs", name) | |
| local err, prev_bin = AsyncFileToString(filename, nil, nil, "pstr") | |
| if err then | |
| print("-", err) | |
| return | |
| end | |
| if prev_bin == bin then | |
| print("- same binary!!!") | |
| return | |
| end | |
| local prev_defs = Unserialize(prev_bin) | |
| assert(prev_defs) | |
| if not prev_defs then | |
| return | |
| end | |
| print("prev_defs", #prev_defs, "prev_bin", #prev_bin, "prev_hash", prev_hash) | |
| print(" new_defs", #defs, " new_bin", #bin, " new_hash", hash) | |
| if #prev_defs ~= #defs then | |
| print("- defs count changed") | |
| return | |
| end | |
| local total = 0 | |
| for i, def in ipairs(defs) do | |
| local prev_def = prev_defs[i] | |
| local found | |
| for j, v in ipairs(def) do | |
| local prev_v = prev_def[j] | |
| if prev_v ~= v then | |
| if found then | |
| print("- def", i) | |
| end | |
| print("- -", j, prev_v, "-->", v) | |
| total = total + 1 | |
| end | |
| end | |
| end | |
| if total == 0 then | |
| print("- no diff found!!!") | |
| end | |
| end | |
| function PrefabMarkerEdit:DoExport(name) | |
| self.ExportTime = nil | |
| local start_time = GetPreciseTicks() | |
| self:DbgShow() | |
| if not IsFSUnpacked() then | |
| return "unpacked sources required" | |
| end | |
| if self.PrefabType ~= "" and not PrefabTypeToPreset[self.PrefabType] then | |
| return "no such prefab type" | |
| end | |
| if self.PoiType ~= "" then | |
| local poi_preset = PrefabPoiToPreset[self.PoiType] | |
| if not poi_preset then | |
| return "no such POI type" | |
| end | |
| local poi_areas = poi_preset.PrefabTypeGroups or empty_table | |
| if #poi_areas > 0 then | |
| if self.PoiArea == "" or not table.find(poi_areas, "id", self.PoiArea) then | |
| return "missing POI area" | |
| end | |
| end | |
| end | |
| name = name or self:GetPrefabName() | |
| if #name == 0 then | |
| return "no name" | |
| end | |
| local marker_name = PrefabToMarkerName(name) | |
| local marker = Markers[marker_name] | |
| if marker and (marker.handle ~= self.handle or marker.map ~= GetMapName()) then | |
| return "duplicated prefab", marker.map, marker.pos | |
| end | |
| if self:GetGameFlags(gofPermanent) == 0 then | |
| return "invalid prefab" | |
| end | |
| local old_height_hash = self.HeightHash | |
| local old_type_hash = self.TypeHash | |
| local old_grass_hash = self.GrassHash | |
| local old_mask_hash = self.MaskHash | |
| self:SetAngle(0) | |
| self:SetInvalidZ() | |
| local bbox, adjusted = self:CaptureTerrain() | |
| if not bbox then | |
| return "capture failed" | |
| end | |
| local grass_map = self.GrassMap | |
| local height_map = self.HeightMap | |
| local type_map = self.TypeMap | |
| if type_map then | |
| local valid_types = GridDest(type_map) | |
| GridReplace(type_map, valid_types, invalid_type_value, 0) | |
| GridMask(valid_types, 0, MaxTerrainTextureIdx()) | |
| if not GridEquals(valid_types, 1) then | |
| self:DbgShow("missing_types") | |
| return "unknown terrain types detected" | |
| end | |
| end | |
| local mask = self.MaskMap | |
| if (height_map or type_map) and not mask then | |
| return "Prefab mask unavailable" | |
| end | |
| self.PlayArea = nil | |
| self.HeightRougness = nil | |
| if height_map then | |
| local height_offset = self.HeightOffset | |
| local height_map_ex = GridRepack(height_map, "F") | |
| local mask_ex = mask and GridRepack(mask, "F") | |
| GridAdd(height_map_ex, height_offset) | |
| local extrem = GridExtrem(height_map_ex, height_tile, height_scale) | |
| local mine, maxe = GridMinMax(extrem) | |
| self.HeightRougness = maxe | |
| if maxe > height_roughness_err and self.CheckRougness then | |
| self:DbgShow("roughness") | |
| return "height map too rough!", maxe, height_roughness_err | |
| end | |
| self.PlayArea = PrefabEvalPlayableArea(height_map_ex, mask_ex, type_tile) | |
| local outline | |
| if not mask_ex then | |
| outline = GridDest(height_map_ex, true) | |
| GridFrame(outline, 1, 1) | |
| elseif self:GetTransitionDist() == 0 then | |
| outline = GridDest(mask_ex) | |
| GridOutline(mask_ex, outline, true) | |
| end | |
| if outline then | |
| GridMulDiv(outline, height_map_ex, 1) | |
| local minh, maxh = GridMinMax(outline) | |
| if maxh > height_outline_offset_err then | |
| self:DbgShow("height_offset") | |
| return "height offset found at the prefab border", maxh, height_outline_offset_err | |
| end | |
| end | |
| end | |
| self.TotalArea = nil | |
| self.RadiusMin = nil | |
| self.RadiusMax = nil | |
| local total_area | |
| if mask then | |
| local bmask = GridDest(mask) | |
| local mw, mh = mask:size() | |
| local gcenter = point(mw / 2, mh / 2) | |
| GridNot(mask, bmask) | |
| local minx, miny, maxx, maxy = GridMinMaxDist(bmask, gcenter) | |
| local radius_min = gcenter:Dist2D(minx, miny) | |
| GridNot(bmask) | |
| local minx, miny, maxx, maxy = GridMinMaxDist(bmask, gcenter) | |
| local radius_max = gcenter:Dist2D(maxx, maxy) | |
| assert(radius_min <= radius_max) | |
| total_area = GridCount(mask, 0, max_int) | |
| self.TotalArea = total_area | |
| self.RadiusMin = radius_min | |
| self.RadiusMax = radius_max | |
| if self.CheckRadiusRatio and (2 * radius_min < radius_max) then | |
| self:DbgShow("radius") | |
| return "max to min radius ratio is too big", radius_max, radius_min | |
| end | |
| end | |
| local new_height_hash = self.HeightHash | |
| local new_type_hash = self.TypeHash | |
| local new_grass_hash = self.GrassHash | |
| local new_mask_hash = self.MaskHash | |
| local IsOptional = CObject.GetOptionalPlacement | |
| local objs = self:CollectObjs(bbox) | |
| local rmin, rmax, rsum, rcount = max_int, 0, 0, 0 | |
| local efCollision = const.efCollision | |
| local efVisible = const.efVisible | |
| local HasAnySurfaces = HasAnySurfaces | |
| local HasMeshWithCollisionMask = HasMeshWithCollisionMask | |
| local GetEnumFlags = CObject.GetEnumFlags | |
| local GetClassEnumFlags = GetClassEnumFlags | |
| local class_to_count = {} | |
| for i=#objs,1,-1 do | |
| local obj = objs[i] | |
| assert(GetClassFlags(obj, cfCodeRenderable) == 0) | |
| local class = obj.class | |
| local obj_err = obj:GetError() | |
| if obj_err then | |
| StoreErrorSource(obj, obj_err) | |
| return "object with errors" | |
| end | |
| if obj.__ancestors.PrefabObj then | |
| table.remove(objs, i) | |
| else | |
| class_to_count[class] = (class_to_count[class] or 0) + 1 | |
| if GetEnumFlags(obj, efCollision) ~= 0 and not HasCollisions(obj) then | |
| obj:ClearEnumFlags(efCollision) | |
| print("Removed collision flags for", obj.class, "at", obj:GetPos(), "in", name) | |
| end | |
| if self.CheckVisibility then | |
| if obj:GetEnumFlags(efVisible) == 0 and GetClassEnumFlags(obj, efVisible) ~= 0 then | |
| StoreErrorSource(obj, "Invisible object") | |
| return "invisible objects detected" | |
| elseif obj:GetOpacity() == 0 then | |
| StoreErrorSource(obj, "Transparent object") | |
| return "transparent objects detected" | |
| end | |
| end | |
| local r = obj:GetRadius() | |
| if r > 0 then | |
| rmin = Min(rmin, r) | |
| rmax = Max(rmax, r) | |
| rsum = rsum + r | |
| rcount = rcount + 1 | |
| end | |
| end | |
| end | |
| self.ObjRadiusMin = rmin | |
| self.ObjRadiusMax = rmax | |
| self.ObjRadiusAvg = rcount > 0 and (rsum / rcount) or 0 | |
| self.ClassToCount = next(class_to_count) and class_to_count | |
| self.HeightMap = height_map | |
| self.TypeMap = type_map | |
| self.GrassMap = grass_map | |
| self.MaskMap = mask | |
| local obj_max_radius = const.RandomMap.PrefabMaxObjRadius or GetEntityMaxSurfacesRadius() | |
| if self.CheckObjRadius and rmax > obj_max_radius then | |
| self:DbgShow("large_objs") | |
| return "too large objects detected", rmax, obj_max_radius | |
| end | |
| local nObjs2x = 0 | |
| for _,obj in ipairs(objs) do | |
| if not IsOptional(obj) then | |
| nObjs2x = nObjs2x + 2 | |
| else | |
| nObjs2x = nObjs2x + 1 | |
| end | |
| end | |
| self.ObjCount = nObjs2x/2 | |
| self.ObjMaxCount = nil | |
| if total_area then | |
| local obj_avg_radius = const.RandomMap.PrefabAvgObjRadius | |
| local max_objs = MulDivRound(total_area, type_tile * type_tile * 7, obj_avg_radius * obj_avg_radius * 22) | |
| self.ObjMaxCount = max_objs | |
| if self.CheckObjCount and self.ObjCount > max_objs then | |
| return "too many objects", self.ObjCount, max_objs | |
| end | |
| end | |
| local center = self:GetCaptureCenter() | |
| local prev_hash = self.ExportedHash | |
| local prev_name = self.ExportedName or "" | |
| local prev_rev = self.AssetsRevision | |
| self.ExportedHash = nil | |
| self.ExportedName = nil | |
| self.RequiredMemory = nil | |
| self.AssetsRevision = nil | |
| local table_find = table.find | |
| local Collections = Collections | |
| local GetCollectionIndex = CObject.GetCollectionIndex | |
| local coll_idx_found, optional_objs = {}, {} | |
| for _, obj in ipairs(objs) do | |
| local col_idx = GetCollectionIndex(obj) | |
| if col_idx ~= 0 then | |
| if not table_find(coll_idx_found, col_idx) then | |
| coll_idx_found[#coll_idx_found + 1] = col_idx | |
| end | |
| if IsOptional(obj) then | |
| optional_objs[col_idx] = (optional_objs[col_idx] or 0) + 1 | |
| end | |
| end | |
| end | |
| local nested_colls | |
| local function ProcessCollection(col_idx) | |
| local col = Collections[col_idx] | |
| if not col then | |
| return | |
| end | |
| local parent_idx = GetCollectionIndex(col) | |
| if parent_idx == 0 then | |
| return | |
| end | |
| nested_colls = nested_colls or {} | |
| local subcolls = nested_colls[parent_idx] | |
| if not subcolls then | |
| nested_colls[parent_idx] = { col_idx } | |
| elseif not table_find(subcolls, col_idx) then | |
| subcolls[#subcolls + 1] = col_idx | |
| else | |
| return | |
| end | |
| return ProcessCollection(parent_idx) | |
| end | |
| for _, col_idx in ipairs(coll_idx_found) do | |
| ProcessCollection(col_idx) | |
| end | |
| local function GetNestedOptionalObjs(col_idx) | |
| local count = optional_objs[col_idx] or 0 | |
| for _, sub_col_idx in ipairs(nested_colls and nested_colls[col_idx]) do | |
| count = count + GetNestedOptionalObjs(sub_col_idx) | |
| end | |
| return count | |
| end | |
| local nested_opt_objs | |
| for _, col_idx in ipairs(coll_idx_found) do | |
| local count = GetNestedOptionalObjs(col_idx) | |
| if count > 0 then | |
| nested_opt_objs = nested_opt_objs or {} | |
| nested_opt_objs[col_idx] = count | |
| end | |
| end | |
| self.NestedColls = nested_colls | |
| self.NestedOptObjs = nested_opt_objs | |
| local class_to_defaults = {} | |
| local prop_eval = prop_eval | |
| local GetClassValue = GetClassValue | |
| local GetDefRandomMapFlags = GetDefRandomMapFlags | |
| local ListPrefabObjProps = ListPrefabObjProps | |
| local ignore_props = { | |
| CollectionIndex = true, | |
| Pos = true, Axis = true, Angle = true, | |
| ColorModifier = true, Scale = true, | |
| Entity = true, Mirrored = true, | |
| } | |
| for _, info in ipairs(RandomMapFlags) do | |
| ignore_props[info.id] = true | |
| end | |
| local defs = {} | |
| local base_prop_count = const.RandomMap.PrefabBasePropCount | |
| for i, obj in ipairs(objs) do | |
| local class = obj.class | |
| local props = obj:GetProperties() | |
| local default_props = class_to_defaults[class] | |
| if not default_props then | |
| default_props = {} | |
| class_to_defaults[class] = default_props | |
| local get_default = obj.GetDefaultPropertyValue | |
| for _, prop in ipairs(props) do | |
| local id = prop.id | |
| if not ignore_props[id] and not prop_eval(prop.dont_save, obj, prop) and prop_eval(prop.editor, obj, prop) then | |
| default_props[id] = get_default(obj, id, prop) | |
| end | |
| end | |
| end | |
| local def_rmf = GetDefRandomMapFlags(obj) | |
| local def_entity = GetClassValue(obj, "entity") or class | |
| local dpos, angle, daxis, coll_idx, | |
| scale, color, entity, mirror, fade_dist, | |
| rmf_flags, ground_offset, normal_offset = ListPrefabObjProps(obj, center, def_rmf, def_entity, obj.prefab_no_fade_clamp) | |
| local def = { | |
| class, dpos, angle, daxis, scale, rmf_flags, fade_dist, | |
| ground_offset, normal_offset, coll_idx, color, mirror | |
| } | |
| local count = #def | |
| assert(count == base_prop_count) | |
| local prop_get = obj.GetProperty | |
| for _, prop in ipairs(props) do | |
| local id = prop.id | |
| local default_value = default_props[id] | |
| if default_value ~= nil then | |
| local value = prop_get(obj, id) | |
| if value ~= nil and value ~= default_value then | |
| assert(value == "" or not IsT(value) and not ObjectClass(value)) | |
| count = count + 2 | |
| def[count - 1] = id | |
| def[count] = value | |
| end | |
| end | |
| end | |
| while not def[count] do | |
| def[count] = nil | |
| count = count - 1 | |
| end | |
| defs[i] = def | |
| end | |
| local bin, err = SerializePstr(defs) | |
| if not bin then | |
| return err | |
| end | |
| if FindSerializeError(bin, defs) then | |
| return "Objects serialization mismatch" | |
| end | |
| local new_hash = xxhash(bin) | |
| local filename = GetPrefabFileObjs(name) | |
| if prev_hash ~= new_hash or not io.exists(filename) then | |
| --DumpObjDiffs(filename, defs, name, bin, new_hash, prev_hash) | |
| local err = AsyncStringToFile(filename, bin) | |
| if err then | |
| return "Failed to save prefab", filename, err | |
| end | |
| svn_process(filename, prev_hash, new_hash) | |
| end | |
| local filename_height = GetPrefabFileHeight(name) | |
| local err = save_grid(height_map, filename_height, old_height_hash, new_height_hash) | |
| if err then | |
| return "Failed to save grid", filename_height, err | |
| end | |
| local filename_type = GetPrefabFileType(name) | |
| local err = save_grid(type_map, filename_type, old_type_hash, new_type_hash) | |
| if err then | |
| return "Failed to save grid", filename_type, err | |
| end | |
| local filename_grass = GetPrefabFileGrass(name) | |
| local err = save_grid(grass_map, filename_grass, old_grass_hash, new_grass_hash) | |
| if err then | |
| return "Failed to save grid", filename_grass, err | |
| end | |
| local filename_mask = GetPrefabFileMask(name) | |
| local err = save_grid(mask, filename_mask, old_mask_hash, new_mask_hash) | |
| if err then | |
| return "Failed to save grid", filename_mask, err | |
| end | |
| ExportedPrefabs[name] = true | |
| if prev_name ~= name then | |
| self:DeleteExports() | |
| end | |
| self.ExportedName = name | |
| self.ExportedHash = new_hash | |
| self.AssetsRevision = prev_name == name and prev_rev or AssetsRevision | |
| self.ExportTime = GetPreciseTicks() - start_time | |
| return nil, objs, defs | |
| end | |
| function PrefabMarkerEdit:ActionViewSource(root) | |
| if self.source then | |
| ViewMarker(root, self.source) | |
| end | |
| end | |
| function PrefabMarkerEdit:GetRevisionInfo() | |
| if ExportedPrefabs[self.ExportedName] then return end | |
| return SVNLocalRevInfo(GetPrefabFileObjs(self.ExportedName)) | |
| end | |
| local function GetPrefabVersion(map_name) | |
| map_name = map_name or GetMapName() | |
| if string.find_lower(map_name, "gameplay") then | |
| return 0 | |
| end | |
| local version_str = map_name and string.match(map_name, "_[Vv](%d+)$") | |
| return version_str and tonumber(version_str) or 1 | |
| end | |
| function PrefabMarker:CreateMarker() | |
| assert(self:GetGameFlags(gofPermanent) ~= 0) | |
| local err, param1, param2 = self:ExportPrefab() | |
| if err then | |
| StoreErrorSource("silent", self, "Failed to export", self:GetPrefabName(), ":", err, param1, param2) | |
| return false, err | |
| end | |
| local map_name = GetMapName() | |
| local version = GetPrefabVersion(map_name) | |
| local props = { "name", name = self.MarkerName, } | |
| local get_prop = self.GetProperty | |
| local get_default = self.GetDefaultPropertyValue | |
| for _, prop in ipairs(self:GetProperties()) do | |
| local export_id = prop.export | |
| if export_id then | |
| local prop_id = prop.id | |
| local value = get_prop(self, prop_id) | |
| if value ~= get_default(self, prop_id, prop) then | |
| props[export_id] = value | |
| props[#props + 1] = export_id | |
| end | |
| end | |
| end | |
| if mapdata.LockMarkerChanges then | |
| local err = self:CheckCompatibility(props) | |
| if err then | |
| StoreErrorSource("silent", self, "Prefab", self:GetPrefabName(), "compatibility error:", err) | |
| return false, err | |
| end | |
| return | |
| end | |
| local data_concat = {"return {"} | |
| local tmp_concat = {"", "=", "", ","} | |
| for _, id in ipairs(props) do | |
| tmp_concat[1] = id | |
| tmp_concat[3] = ValueToLuaCode(props[id], ' ') | |
| data_concat[#data_concat + 1] = table.concat(tmp_concat) | |
| end | |
| data_concat[#data_concat + 1] = "}" | |
| local data_str = table.concat(data_concat) | |
| local marker = PlaceObject('Marker', { | |
| name = PrefabToMarkerName(self.ExportedName), | |
| type = "Prefab", | |
| handle = self.handle, | |
| pos = self:GetPos(), | |
| map = map_name, | |
| data = data_str, | |
| data_version = PrefabMarkerVersion, | |
| }) | |
| self.source = { | |
| handle = self.handle, | |
| map = map_name, | |
| } | |
| return marker | |
| end | |
| function PrefabMarkerEdit:CheckCompatibility(new_props) | |
| local marker_name = PrefabToMarkerName(self.ExportedName) | |
| local marker = Markers[marker_name] | |
| if not marker then | |
| return "Missing marker" | |
| end | |
| local data = marker.type == "Prefab" and marker.data | |
| local props = data and dostring(data) | |
| if not props then | |
| return "Unserialize props error" | |
| end | |
| local max_pct_err = 5 | |
| local to_check = {} | |
| for _, prop in ipairs(self:GetProperties()) do | |
| local export_id = prop.export | |
| if export_id and prop.compatibility then | |
| to_check[#to_check + 1] = export_id | |
| end | |
| end | |
| for _, prop in ipairs(to_check) do | |
| local value = props[prop] | |
| local new_value = new_props[prop] | |
| if new_value ~= value then | |
| local ptype = type(new_value) | |
| if ptype ~= type(value) then | |
| return "Changed type of prop " .. prop | |
| end | |
| if ptype == "number" then | |
| if abs(new_value - value) > MulDivRound(value, max_pct_err, 100) then | |
| return "Too big difference in value of prop " .. prop | |
| end | |
| elseif IsPoint(value) then | |
| if new_value:Dist(value) > MulDivRound(value:Len(), max_pct_err, 100) then | |
| return "Too big difference in value of prop " .. prop | |
| end | |
| elseif not compare(value, new_value) then | |
| return "Changed value of prop " .. prop | |
| end | |
| end | |
| end | |
| end | |
| ---- | |
| function OnMsg.PreSaveMap() | |
| local prefabs = MapCount("map", "PrefabMarker", nil, nil, gofPermanent) | |
| if mapdata.IsPrefabMap then | |
| if prefabs == 0 then | |
| mapdata.IsPrefabMap = false | |
| print("This map is no more a prefab map.") | |
| else | |
| MapClearGameFlags(gofPermanent, "map", "PropertyHelper", "CameraObj") | |
| end | |
| else | |
| if prefabs ~= 0 then | |
| mapdata.IsPrefabMap = true | |
| print("This map is now declared as a prefab map.") | |
| end | |
| end | |
| if mapdata.IsPrefabMap then | |
| SaveTerrainWaterObjArea() | |
| MapForEach("map", "PrefabMarker", function(prefab) | |
| prefab:EditorObjectsShow(false) | |
| end) | |
| end | |
| end | |
| function OnMsg.PostSaveMap() | |
| if mapdata.IsPrefabMap then | |
| MapForEach("map", "PrefabMarker", function(prefab) | |
| prefab:EditorObjectsShow() | |
| end) | |
| end | |
| end | |
| ---- | |
| AppendClass.PrefabMarker = { | |
| __parents = { "PrefabMarkerEdit" }, | |
| editor_text_depth_test = false, | |
| } | |
| function PrefabMarker:EditorGetText(line_separator) | |
| local name = self:GetPrefabName() | |
| line_separator = line_separator or "\n" | |
| if self.ExportError ~= "" then | |
| name = name .. line_separator .. "Error: " .. self.ExportError | |
| elseif self.PoiType ~= "" then | |
| name = name .. line_separator .. self.PoiType | |
| if self.PoiArea ~= "" then | |
| name = name .. "." .. self.PoiArea | |
| end | |
| end | |
| return name | |
| end | |
| function PrefabMarker:EditorGetTextColor() | |
| if self.ExportError ~= "" then | |
| return red | |
| end | |
| local poi_preset = self.PoiType ~= "" and PrefabPoiToPreset[self.PoiType] | |
| if poi_preset then | |
| return poi_preset.OverlayColor or RandColor(xxhash(self.PoiType)) | |
| end | |
| return EditorTextObject.EditorGetTextColor(self) | |
| end | |
| ---- | |
| function ResaveAllPrefabs(version) | |
| if IsValidThread(l_ResaveAllMapsThread) then | |
| return | |
| end | |
| l_ResaveAllMapsThread = CreateRealTimeThread(function() | |
| local start_time = GetPreciseTicks() | |
| if not version then | |
| local err, files = AsyncListFiles("Prefabs", "*") | |
| if #files > 0 then | |
| print("Deleting all prefabs...") | |
| for i=1,#files do | |
| local success, err = os.remove(files[i]) | |
| if err then | |
| print("Error", err, "deleting prefab file", files[i]) | |
| end | |
| end | |
| end | |
| ExportedPrefabs = {} | |
| end | |
| print("Resaving maps...") | |
| local prefab_maps = {} | |
| for map, data in pairs(MapData) do | |
| if data.IsPrefabMap then | |
| prefab_maps[map] = true | |
| end | |
| end | |
| for i = 1,#Markers do | |
| local marker = Markers[i] | |
| if marker.type == "Prefab" and MapData[marker.map] then | |
| prefab_maps[marker.map] = true | |
| end | |
| end | |
| if version then | |
| for map in pairs(prefab_maps) do | |
| if version ~= GetPrefabVersion(map) then | |
| prefab_maps[map] = nil | |
| end | |
| end | |
| end | |
| prefab_maps = table.keys(prefab_maps, true) | |
| LoadingScreenOpen("idLoadingScreen", "ResaveAllPrefabs") | |
| EditorActivate() | |
| ForEachMap(prefab_maps, function() | |
| print("Resaving map ", GetMap()) | |
| SaveMap("no backup") | |
| end) | |
| LoadingScreenClose("idLoadingScreen", "ResaveAllPrefabs") | |
| l_ResaveAllMapsThread = false | |
| PrefabUpdateMarkers() | |
| print("Resaving all prefabs complete in", DivRound(GetPreciseTicks() - start_time, 1000), "sec") | |
| end) | |
| end | |
| function ResaveAllGameMaps(filter) | |
| if IsValidThread(l_ResaveAllMapsThread) then | |
| return | |
| end | |
| l_ResaveAllMapsThread = CreateRealTimeThread(function() | |
| local start_time = GetPreciseTicks() | |
| print("Resaving maps...") | |
| local maps = GetAllGameMaps() | |
| EditorActivate() | |
| for _, map in ipairs(maps) do | |
| local data = MapData[map] | |
| if not filter or filter(map, data) then | |
| local logic | |
| if not config.ResaveAllGameMapsKeepsGameLogic then | |
| logic = data.GameLogic | |
| data.GameLogic = false -- avoid starting game stuff that could spawn objects / modify the map | |
| end | |
| print("Resaving map ", map) | |
| ChangeMap(map) | |
| if not config.ResaveAllGameMapsKeepsGameLogic then | |
| data.GameLogic = logic | |
| else | |
| Msg("GameEnterEditor") | |
| end | |
| if not filter or filter(map, data, "loaded check") then | |
| SaveMap("no backup") | |
| end | |
| end | |
| end | |
| l_ResaveAllMapsThread = false | |
| print("Resaving", #maps, "maps complete in", DivRound(GetPreciseTicks() - start_time, 1000), "sec") | |
| end) | |
| end | |
| function RegenerateMap(map, reload_on_finish) | |
| map = map or GetMapName() or "" | |
| map = GetOrigMapName(map) | |
| if map == "" then | |
| return | |
| end | |
| if not IsValidThread(l_ResaveAllMapsThread) then | |
| l_ResaveAllMapsThread = CreateRealTimeThread(RegenerateMap, map, reload_on_finish) | |
| return | |
| elseif l_ResaveAllMapsThread ~= CurrentThread() then | |
| return | |
| end | |
| local data = MapData[map] | |
| if not data or not data.IsRandomMap then | |
| print("Not a random map") | |
| return | |
| end | |
| local active = IsEditorActive() | |
| if not active then | |
| EditorActivate() | |
| end | |
| print("Regenerating map", map, "...") | |
| local logic = data.GameLogic | |
| if logic then | |
| data.GameLogic = false -- avoid starting game stuff that could spawn objects / modify the map | |
| local st = GetPreciseTicks() | |
| ChangeMap(map) | |
| printf("Map reloaded without game logic in %.3f s", (GetPreciseTicks() - st) * 0.001) | |
| Sleep(1) | |
| end | |
| SetGameSpeed("pause") | |
| assert(mapdata == data) | |
| assert(not data.GameLogic) | |
| local st = GetPreciseTicks() | |
| Presets.MapGen.Default.BiomeCreator:Run() | |
| printf("Map gen finished in %.3f s", (GetPreciseTicks() - st) * 0.001) | |
| Sleep(1) | |
| data.GameLogic = logic | |
| SaveMap("no backup") | |
| Sleep(1) | |
| if not active then | |
| EditorDeactivate() | |
| end | |
| if logic and reload_on_finish then | |
| local st = GetPreciseTicks() | |
| ChangeMap(map) | |
| printf("Map reloaded with game logic in %.3f s", (GetPreciseTicks() - st) * 0.001) | |
| Sleep(1) | |
| end | |
| end | |
| function GetRandomMaps(filter, ...) | |
| local maps = {} | |
| for id, map_data in pairs(MapData) do | |
| if map_data.IsRandomMap and GameMapFilter(id, map_data) and (not filter or filter(id, map_data, ...)) then | |
| maps[#maps + 1] = id | |
| end | |
| end | |
| table.sort(maps) | |
| return maps | |
| end | |
| function TimeToHHMMSS(ms) | |
| local sec = DivRound(ms, 1000) | |
| local hours = sec / (60 * 60) | |
| sec = sec - hours * (60 * 60) | |
| local mins = sec / 60 | |
| sec = sec - mins * 60 | |
| return string.format("%02d:%02d:%02d", hours, mins, sec) | |
| end | |
| function RegenerateRandomMaps(maps) | |
| if IsValidThread(l_ResaveAllMapsThread) then | |
| return | |
| end | |
| l_ResaveAllMapsThread = CreateRealTimeThread(function() | |
| local start_time = GetPreciseTicks() | |
| local old_ignoreerrors = IgnoreDebugErrors(true) | |
| print("Regenerating maps...") | |
| maps = maps or GetRandomMaps() | |
| local active = IsEditorActive() | |
| if not active then | |
| EditorActivate() | |
| end | |
| for i, map in ipairs(maps) do | |
| printf("Regenerating map %d / %d...", i, #maps) | |
| local success, err = sprocall(RegenerateMap, map) | |
| if not success then | |
| print("<color 255 0 0>Critical error for</color>", map, err) | |
| end | |
| end | |
| print("Regenerating", #maps, "maps complete in", TimeToHHMMSS(GetPreciseTicks() - start_time)) | |
| if not active then | |
| EditorDeactivate() | |
| end | |
| ChangeMap("") | |
| IgnoreDebugErrors(old_ignoreerrors) | |
| l_ResaveAllMapsThread = false | |
| end) | |
| end | |
| function GetRegenerateMapLists() | |
| return GatherMsgItems("GatherRegenerateMapLists") | |
| end | |
| function RegenerateMapList(list_name) | |
| local maps = GetRegenerateMapLists()[list_name] | |
| if maps then | |
| RegenerateRandomMaps(maps) | |
| end | |
| end | |
| local function GetFilesHashes(path) | |
| local hashes = { } | |
| for _, file in ipairs(io.listfiles(path)) do | |
| local err, hash = AsyncFileToString(file, nil, nil, "hash") | |
| if err then | |
| GameTestsError("Failed to open " .. file .. " for " .. map .. " due to err: " .. err) | |
| return | |
| end | |
| hashes[file] = hash | |
| end | |
| return hashes | |
| end | |
| TestNightlyPrefabMethods = {} | |
| function TestNightlyPrefabMethods.TestDoesPrefabMapSavingGenerateFakeDeltas(map, result) | |
| SaveMap("no backup") | |
| local path = "svnAssets/Source/Maps/" .. map .. "/" | |
| local hashes_before = GetFilesHashes(path) | |
| SaveMap("no backup") | |
| local hashes_after = GetFilesHashes(path) | |
| for file, hash in pairs(hashes_before or empty_table) do | |
| if hash ~= hashes_after[file] then | |
| result["fake deltas"] = result["fake deltas"] or { | |
| err = "Resaving prefab maps produced differences!", | |
| texts = {} | |
| } | |
| table.insert(result["fake deltas"].texts, map .. ": difference in " .. file) | |
| end | |
| end | |
| end | |
| function GameTestsNightly.TestPrefabMaps() | |
| WaitSaveGameDone() | |
| StopAutosaveThread() | |
| table.change(config, "TestPrefabMaps", { | |
| AutosaveSuspended = true, | |
| }) | |
| local thread = CreateRealTimeThread(function() | |
| WaitDataLoaded() | |
| if not IsEditorActive() then | |
| EditorActivate() | |
| end | |
| local test_times = { } | |
| local result = { } | |
| for map, data in sorted_pairs(MapData) do | |
| if data.IsPrefabMap then | |
| assert(not data.GameLogic) | |
| GameTestsPrint("Testing map", map) | |
| ChangeMap(map) | |
| if GetMapName() ~= map then | |
| GameTestsError("Failed to change map to " .. map .. "! ") | |
| return | |
| end | |
| for method_name, method in sorted_pairs(TestNightlyPrefabMethods) do | |
| local start = GetPreciseTicks() | |
| method(map, result) | |
| test_times[method_name] = (test_times[method_name] or 0) + (GetPreciseTicks() - start) | |
| end | |
| end | |
| end | |
| if IsEditorActive() then | |
| EditorDeactivate() | |
| end | |
| for _, res in sorted_pairs(result) do | |
| GameTestsError(res.err) | |
| for _, text in ipairs(res.texts) do | |
| GameTestsPrint(text) | |
| end | |
| end | |
| for method_name, time in sorted_pairs(test_times) do | |
| GameTestsPrint(method_name, "took", time, "ms") | |
| end | |
| Msg(CurrentThread()) | |
| end) | |
| while IsValidThread(thread) do | |
| WaitMsg(thread, 1000) | |
| end | |
| table.restore(config, "TestPrefabMaps") | |
| end | |