|
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: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) |
|
|
|
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 |
|
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) |
|
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 |
|
|
|
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 |
|
|
|
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 |
|
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 |
|
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 |
|
|
|
|