local type_tile = const.TypeTileSize local height_tile = const.HeightTileSize local grass_tile = const.GrassTileSize local height_scale = const.TerrainHeightScale local empty_table = empty_table local GetClassFlags = CObject.GetClassFlags local SetGameFlags = CObject.SetGameFlags local developer = Platform.developer and Platform.desktop local unpack = table.unpack local def_size = point(20*guim, 20*guim) local capture_items = {"Height", "Terrain", "Grass"} local def_capt = set(table.unpack(capture_items)) local mask_max = 255 local invalid_type_value = 255 local invalid_grass_value = 255 local transition_max_pct = 30 function GetTerrainGridsMaxGranularity() local granularity = Max(grass_tile, type_tile, height_tile) assert(granularity % grass_tile == 0) assert(granularity % type_tile == 0) assert(granularity % height_tile == 0) return granularity end local function ApplyHeightOpCombo(first) local items = { {value = '=', text = "Equals"}, {value = '<', text = "Min"}, {value = '>', text = "Max"}, {value = '~', text = "Average"}, {value = '+', text = "Add"}, } if first then table.insert(items, 1, first) end return items end local post_process_items = { { value = 0, text = "" }, { value = 1, text = "fill holes"}, { value = 2, text = "adjust capture"}, } assert(type_tile == const.HeightTileSize) if FirstLoad then PrefabMarkers = {} ExportedPrefabs = {} PrefabTypes = {} PrefabTypeToPrefabs = {} PrefabDimensions = empty_table end PrefabMarkerVersion = '1' local p_dprint = CreatePrint{ "RM", format = print_format, output = DebugPrint } local p_print = developer and CreatePrint{ "RM", format = print_format, color = yellow} or p_dprint function GetPrefabFileObjs(name) return string.format("Prefabs/%s.bin", name) end function GetPrefabFileHeight(name) return string.format("Prefabs/%s.h.grid", name) end function GetPrefabFileType(name) return string.format("Prefabs/%s.t.grid", name) end function GetPrefabFileGrass(name) return string.format("Prefabs/%s.g.grid", name) end function GetPrefabFileMask(name) return string.format("Prefabs/%s.m.grid", name) end local function GetRotationModesCombo() return { { value = false, text = "" }, { value = "slope", text = "Follow Slope Angle" }, { value = "map", text = "Follow Map Orientation" }, } end local function GetPoiAreas(self) local names = {} local poi_preset = PrefabPoiToPreset[self.PoiType] for _, group in pairs(poi_preset and poi_preset.PrefabTypeGroups) do names[#names + 1] = group.id end table.sort(names) return names end local function GetPoiAreasCount(self) local poi_preset = PrefabPoiToPreset[self.PoiType] return #(poi_preset and poi_preset.PrefabTypeGroups or "") end ---- DefineClass.PrefabObj = { __parents = { "Object", "EditorVisibleObject" }, flags = { efWalkable = false, efCollision = false, efApplyToGrids = false }, Scale = 250, } function PrefabObj:Init() self:SetScale(self.Scale) self:SetVisible(IsEditorActive()) end ---- DefineClass.DebugOverlayControl = { __parents = { "PropertyObject" }, } ---- DefineClass.PrefabMarker = { __parents = { "MapMarkerObj", "PrefabObj", "DebugOverlayControl" }, flags = { efWalkable = false, efApplyToGrids = false, efCollision = false, gofAlwaysRenderable = true }, entity = "WayPointBig", properties = { { id = "PrefabName", name = "Name", editor = "text", default = "", category = "Prefab", read_only = true, dont_save = true, }, { id = "ExportedName", name = "Exported", editor = "text", default = "", category = "Prefab", read_only = true, buttons = {{name = "Export", func = "ActionPrefabExport"}, {name = "Revision", func = "ActionPrefabRevision"}, {name = "Folder", func = "ActionExploreTo"}}}, { id = "ExportError", name = "Export Error", editor = "text", default = "", category = "Prefab", read_only = true, no_edit = function(self) return self.ExportError == "" end }, { id = "ExportedHash", editor = "number", default = false, category = "Prefab", export = "hash", no_edit = true }, { id = "AssetsRevision", name = "Assets Revision", editor = "number", default = false, category = "Prefab", export = "revision", read_only = true }, { id = "PrefabType", name = "Type", editor = "preset_id", default = "", category = "Prefab", export = "type", compatibility = true, preset_class = "PrefabType", }, { id = "PrefabWeight", name = "Weight", editor = "number", default = 100, category = "Prefab", export = "weight", compatibility = true, min = 0 }, { id = "PrefabMaxCount", name = "Max Count", editor = "number", default = -1, category = "Prefab", export = "max_count", compatibility = true }, { id = "RepeatReduct", name = "Repeat Reduct (%)", editor = "number", default = 0, category = "Prefab", export = "repeat_reduct", compatibility = true, min = 0, max = 100, slider = true, no_edit = function(self) return self.PoiType ~= "" end }, { id = "PrefabOrient", name = "Orientation", editor = "point", default = axis_x, category = "Prefab", helper = "relative_pos", compatibility = true, helper_outside_object = true, use_object = true, help = "Used to specify a common orientation for a set of prefabs." }, { id = "PrefabAngle", name = "Angle (deg)", editor = "number", default = 0, category = "Prefab", export = "angle", compatibility = true, scale = 60, read_only = true, dont_save = true }, { id = "PrefabAngleVar", name = "Angle Variate (deg)", editor = "number", default = 180*60, category = "Prefab", export = "angle_variation", compatibility = true, min = 0, max = 180*60, slider = true, scale = 60, help = "Random variation around the prefab angle" }, { id = "PrefabRotateMode", name = "Rotation Mode", editor = "choice", default = "map", category = "Prefab", export = "rotation_mode", compatibility = true, items = GetRotationModesCombo }, { id = "DecorObstruct", name = "Decor Obstruct", editor = "bool", default = false, category = "Prefab", export = "decor_obstruct", compatibility = true, help = "Obstruct placement of other decor prefabs in its area. Option valid only when placed as decor." }, { id = "SaveCollections", name = "Important Collections", editor = "bool", default = false, category = "Prefab", export = "save_collections", help = "If specified, the collections in the prefab will be persisted after the generation." }, { id = "Tags", name = "Tags", editor = "set", default = empty_table, category = "Prefab", export = "tags", compatibility = true, items = function() return PrefabTagsCombo() end, help = "Keywords used to group similar prefabs" }, { id = "AllTags", name = "All Tags", editor = "text", default = "", category = "Prefab", read_only = true, dont_save = true, help = "Includes the tags inherited from prefab type and POI type" }, { id = "PoiType", name = "POI Type", editor = "preset_id", default = "", category = "Prefab", export = "poi_type", compatibility = true, preset_class = "PrefabPOI", }, { id = "PoiArea", name = "POI Area", editor = "choice", default = "", category = "Prefab", export = "poi_area", compatibility = true, items = GetPoiAreas, no_edit = function(self) return GetPoiAreasCount(self) ==0 end, help = "Disregard prefab type and use POI area instead." }, { id = "CaptureSet", name = "Capture", editor = "set", default = def_capt, category = "Terrain", items = capture_items }, { id = "CaptureSize", name = "Size", editor = "point", default = def_size, category = "Terrain", export = "size", scale = "m", min = 0, granularity = GetTerrainGridsMaxGranularity(), helper = "terrain_rect", terrain_rect_color = RGBA(64, 196, 0, 96), terrain_rect_step = guim/2, terrain_rect_zoffset = guim/4, terrain_rect_depth_test = true, terrain_rect_grid = true, buttons = { {name = "Capture", func = "ActionCaptureTerrain"}, {name = "Clear", func = "ActionClearTerrain"}, }, }, { id = "Centered", name = "Centered", editor = "bool", default = false, category = "Terrain", compatibility = true, help = "Specify if the marker is in the center of the capture area"}, { id = "HeightOp", name = "Apply Height", editor = "dropdownlist", default = '+', category = "Terrain", export = "height_op", items = function() return ApplyHeightOpCombo() end, help = "Specify how the captured terrain height would be applied over the existing" }, { id = "InvalidTerrain", name = "Invalid Terrain", editor = "dropdownlist", category = "Terrain", items = function() return GetTerrainNamesCombo() end, help = "Tiles with invalid terrain type wont be captured" }, { id = "InvalidGrass", name = "Invalid Grass", editor = "number", default = -1, category = "Terrain", help = "Tiles with invalid grass density wont be captured. The final values will be remapped if possible to fit the whole density range." }, { id = "SkippedTerrains", name = "Skipped Terrains", editor = "string_list", default = false, category = "Terrain", items = function() return GetTerrainNamesCombo() end, help = "Tiles with invalid terrain type wont be captured" }, { id = "ApplyTerrain", name = "Apply Terrain", editor = "dropdownlist", default = '', category = "Terrain", items = {'', 'invalid'}, help = "Specify how the captured terrain type would be applied over the existing", }, { id = "TransitionZone", name = "Transition Zone", editor = "number", default = 64*guim, category = "Terrain", compatibility = true, min = 0, max = function(o) return o:GetMaxTransitionDist() end, slider = true, scale = "m", granularity = type_tile, help = "Transition zone for smooth stitching" }, { id = "CircleMask", name = "Circle Mask", editor = "bool", default = false, category = "Terrain",}, { id = "CircleMaskRadius", name = "Custom Mask Radius",editor = "number", default = false, category = "Terrain", scale = "m", no_edit = PropChecker("CircleMask", false) }, { id = "PostProcess", name = "Post Process", editor = "dropdownlist", default = 2, category = "Terrain", items = post_process_items, no_edit = function(self) return self.CircleMask end}, { id = "TerrainPreview", name = "Terrain Preview", editor = "bool", default = true, category = "Terrain" }, { id = "HeightMap", name = "Height Map", editor = "grid", default = false, category = "Terrain", read_only = true, dont_save = true, min = 128, max = 256, no_edit = function(self) return not self.TerrainPreview end, grid_offset = function(self) return self.HeightOffset end }, { id = "HeightHash", editor = "number", default = false, category = "Terrain", export = "height_hash", no_edit = true }, { id = "HeightOffset", editor = "number", default = 0, category = "Terrain", export = "height_offset", no_edit = true }, { id = "HeightMin", editor = "point", default = point30, category = "Terrain", export = "min", no_edit = true }, { id = "HeightMax", editor = "point", default = point30, category = "Terrain", export = "max", no_edit = true }, { id = "TypeMap", name = "Terrain Type", editor = "grid", default = false, category = "Terrain", read_only = true, dont_save = true, min = 128, max = 256, color = true, invalid_value = invalid_type_value, no_edit = function(self) return not self.TerrainPreview end, }, { id = "TypeHash", editor = "number", default = false, category = "Terrain", export = "type_hash", no_edit = true }, { id = "TypeNames", editor = "prop_table", default = false, category = "Terrain", export = "type_names", no_edit = true,}, { id = "GrassMap", name = "Grass", editor = "grid", default = false, category = "Terrain", read_only = true, dont_save = true, min = 128, max = 256, invalid_value = invalid_grass_value, no_edit = function(self) return not self.TerrainPreview end, }, { id = "GrassHash", editor = "number", default = false, category = "Terrain", export = "grass_hash", no_edit = true }, { id = "MaskMap", name = "Transition Mask", editor = "grid", default = false, category = "Terrain", read_only = true, dont_save = true, min = 128, max = 256, no_edit = function(self) return not self.TerrainPreview end, }, { id = "MaskHash", editor = "number", default = false, category = "Terrain", export = "mask_hash", no_edit = true }, { id = "RequiredMemory", name = "Required Memory (KB)", editor = "number", default = 0, category = "Stats", export = "required_memory", read_only = true, scale = 1024 }, { id = "TerrainCaptureTime", name = "Terrain Capture (ms)", editor = "number", default = 0, category = "Stats", read_only = true, dont_save = true }, { id = "PlayArea", editor = "number", default = 0, category = "Stats", export = "play_area", compatibility = true, no_edit = true }, { id = "TotalArea", editor = "number", default = 0, category = "Stats", export = "total_area", compatibility = true, no_edit = true }, { id = "RadiusMin", editor = "number", default = 0, category = "Stats", export = "min_radius", compatibility = true, no_edit = true }, { id = "RadiusMax", editor = "number", default = 0, category = "Stats", export = "max_radius", compatibility = true, no_edit = true }, { id = "PlayAreaRatio", name = "Play Area (%)", editor = "number", default = 0, category = "Stats", read_only = true, dont_save = true }, { id = "MapTotalArea", name = "Total Area (m^2)", editor = "number", default = 0, category = "Stats", scale = guim*guim, read_only = true, dont_save = true }, { id = "MapRadiusMin", name = "Min Radius (m)", editor = "number", default = 0, category = "Stats", scale = guim, read_only = true, dont_save = true }, { id = "MapRadiusMax", name = "Max Radius (m)", editor = "number", default = 0, category = "Stats", scale = guim, read_only = true, dont_save = true }, { id = "HeightRougness", name = "Height Rougness", editor = "number", default = 0, category = "Stats", read_only = true, help = "Quantative estimation of the maximum height map roughness" }, { id = "ObjCount", name = "Obj Count", editor = "number", default = 0, category = "Stats", export = "obj_count", compatibility = true, read_only = true, help = "Relative to the maximum allowed object density" }, { id = "ObjMaxCount", name = "Obj Max Count", editor = "number", default = 0, category = "Stats", read_only = true, help = "Maximum allowed objects for the current size of the prefab" }, { id = "ObjRadiusMin", name = "Obj Radius Min (m)", editor = "number", default = 0, category = "Stats", export = "obj_min_radius", read_only = true, scale = guim, }, { id = "ObjRadiusMax", name = "Obj Radius Max (m)", editor = "number", default = 0, category = "Stats", export = "obj_max_radius", read_only = true, scale = guim, }, { id = "ObjRadiusAvg", name = "Obj Radius Avg (m)", editor = "number", default = 0, category = "Stats", export = "obj_avg_radius", read_only = true, scale = guim, }, { id = "NestedColls", name = "Nested Collections", editor = "prop_table", default = false, category = "Stats", export = "nested_colls", read_only = true, indent = ' '}, { id = "NestedOptObjs", name = "Nested Optional Objs", editor = "prop_table", default = false, category = "Stats", export = "nested_opt_objs", read_only = true, indent = ' '}, }, InvalidTerrain = "", -- will be assigned later from the constant defs } function OnMsg.ClassesGenerate() PrefabMarker.InvalidTerrain = table.get(const, "Prefab", "InvalidTerrain") or "" end function PrefabMarker:GetPrefabAngle() return CalcOrientation(self.PrefabOrient) end local function PrefabFilter(pstyles, ptypes, revision, version) revision = revision or AssetsRevision version = version or max_int local matched local markers = PrefabMarkers for i=1,#markers do local marker = markers[i] if (not pstyles or marker.style == "" or pstyles[marker.style]) and (not ptypes or marker.type == "" or ptypes[marker.type]) and revision >= (marker.revision or 0) and version >= (marker.version or 1) then matched = matched or {} matched[#matched + 1] = marker end end return matched or empty_table end function PrefabMarker:GetPlayAreaRatio() return self.TotalArea > 0 and MulDivRound(100, self.PlayArea, self.TotalArea) or 0 end function PrefabMarker:GetMapTotalArea() return self.TotalArea * type_tile * type_tile end function PrefabMarker:GetMapRadiusMin() return self.RadiusMin * type_tile end function PrefabMarker:GetMapRadiusMax() return self.RadiusMax * type_tile end function PrefabComposeName(props) local prefab_name = props.name or "" if #prefab_name > 0 then local prefab_type = props.type or "" if prefab_type == "" then prefab_type = "Any" end prefab_name = prefab_type .. "." .. prefab_name local prefab_style = props.style or "" if #prefab_style ~= 0 then prefab_name = prefab_style .. "." .. prefab_name end end return prefab_name end function PrefabMarker:GetPrefabName() return PrefabComposeName{name = self.MarkerName, type = self.PrefabType} end function PrefabMarker:GetAllTags() local poi_tags = table.get(PrefabPoiToPreset, self.PoiType, "Tags") local ptype_tags = table.get(PrefabTypeToPreset, self.PrefabType, "Tags") local marker_tags = self.Tags local tags = {} table.append(tags, ptype_tags) table.append(tags, marker_tags) table.append(tags, poi_tags) return table.concat(table.keys(tags, true), ", ") end ---- function GetTypeRemapping(name_to_idx) local type_remapping local TerrainTextures = TerrainTextures local GetTerrainTextureIndex = GetTerrainTextureIndex for name, idx in pairs(name_to_idx or empty_table) do if TerrainTextures[idx].id ~= name then local new_idx = GetTerrainTextureIndex(name) if not new_idx then assert(false, "No such terrain type: " .. name) else type_remapping = type_remapping or {} type_remapping[idx] = new_idx end end end if type_remapping then for i = 0, MaxTerrainTextureIdx() do type_remapping[i] = type_remapping[i] or i end end return type_remapping end function PrefabMarker:GetMaxTransitionDist() local min_size = Min(self.CaptureSize:xy()) return Min(type_tile * mask_max, min_size * transition_max_pct / 100) end function PrefabMarker:GetTransitionDist() return Max(0, Min(self.TransitionZone, self:GetMaxTransitionDist())) end ---- local function PrefabUpdateExported() ExportedPrefabs = {} local err, list = AsyncListFiles("Prefabs", "*.bin") if err then p_print("PrefabUpdateExported: ", err) return end for i=1,#list do local file = list[i] local dir, name, ext = SplitPath(file) if ExportedPrefabs[name] then p_print("Duplicated exported prefab name:", name) else ExportedPrefabs[name] = true end end end function PrefabCalcStats(prefabs) local min_prefab_radius, max_prefab_radius = max_int, min_int local avg_prefab_radius, radius_prefabs = 0, 0 local min_height, max_height = max_int, min_int local avg_max_height, avg_min_height, height_prefabs = 0, 0, 0 local min_play_area, max_play_area = max_int, min_int local avg_play_area, play_prefabs = 0, 0 local function get_avg(avg, count, value) return (avg * count + value) / (count + 1) end for i = 1,#prefabs do local prefab = prefabs[i] local play_area = prefab.play_area or 0 if play_area > 0 then min_play_area = Min(min_play_area, play_area) max_play_area = Max(max_play_area, play_area) avg_play_area = get_avg(avg_play_area, play_prefabs, play_area) play_prefabs = play_prefabs + 1 end local radius = prefab.max_radius or 0 if radius > 0 then min_prefab_radius = Min(min_prefab_radius, radius) max_prefab_radius = Max(max_prefab_radius, radius) avg_prefab_radius = get_avg(avg_prefab_radius, radius_prefabs, radius) radius_prefabs = radius_prefabs + 1 end if prefab.max and prefab.min then max_height = Max(max_height, prefab.max:z()) min_height = Min(min_height, prefab.min:z()) avg_max_height = get_avg(avg_max_height, height_prefabs, max_height) avg_min_height = get_avg(avg_min_height, height_prefabs, min_height) height_prefabs = height_prefabs + 1 end end return { MinRadius = min_prefab_radius, MaxRadius = max_prefab_radius, AvgRadius = avg_prefab_radius, MinPlayArea = min_play_area, MaxPlayArea = max_play_area, AvgPlayArea = avg_play_area, MaxHeight = max_height, MinHeight = min_height, AvgMaxHeight = avg_max_height, AvgMinHeight = avg_min_height, } end local function ConvertTags(tags) return tags and table.invert(tags) or nil end function PrefabUpdateMarkers() local markers = Markers local hash_keys = {"hash", "height_hash", "type_hash", "mask_hash"} local versions = {} local prefabs = {} local deprecated = 0 local type_to_prefabs = {} local defaults = {} for _, prop in ipairs(PrefabMarker:GetProperties()) do local export_id = prop.export if export_id then defaults[export_id] = PrefabMarker:GetProperty(prop.id) end end local PrefabTypeToPreset = PrefabTypeToPreset local ExportedPrefabs = ExportedPrefabs local PrefabComposeName = PrefabComposeName local prefab_meta = { __index = defaults } local any_type = {} for i = 1,#markers do local marker = markers[i] local data = marker.type == "Prefab" and marker.data if data then local prefab = dostring(data) if not prefab then p_print("Prefab", marker.name, "unserialize props error!") else local name = PrefabComposeName(prefab) if not ExportedPrefabs[name] then p_print("No exported prefab found ", name, "on", marker.map, "at", marker.pos) elseif prefabs[name] then local prev = prefabs[name] p_print("Duplicated prefabs:\n\t1.", name, "on", marker.map, "at", marker.pos, "\n\t2.", prefabs[prev], "on", prev.marker.map, "at", prev.marker.pos) else local ptype = prefab.type or "" if ptype == "" or PrefabTypeToPreset[ptype] then setmetatable(prefab, prefab_meta) prefabs[#prefabs + 1] = prefab prefabs[name] = prefab prefabs[prefab] = name local type_prefabs = ptype == "" and any_type or type_to_prefabs[ptype] if type_prefabs then type_prefabs[#type_prefabs + 1] = prefab else type_to_prefabs[prefab.type] = {prefab} end if marker.data_version ~= PrefabMarkerVersion then deprecated = deprecated + 1 end local version = prefab.version or 1 local info = versions[version] or {count = 0} versions[version] = info info.count = info.count + 1 for _, key in ipairs(hash_keys) do info[key] = xxhash(prefab[key], info[key]) end prefab.marker = marker end end end end end for ptype, prefabs in ipairs(type_to_prefabs) do table.iappend(prefabs, any_type) end if deprecated > 0 then p_print(deprecated, "deprecated prefabs need re-export") end if const.RandomMap.PrefabVersionLog then p_dprint("Prefab marker versions:", TableToLuaCode(versions)) end table.sort(prefabs, function(a, b) return prefabs[a] < prefabs[b] end) PrefabMarkers = prefabs PrefabDimensions = PrefabCalcStats(prefabs) PrefabTypeToPrefabs = type_to_prefabs PrefabTypes = table.keys(type_to_prefabs, true) Msg("PrefabMarkersChanged") DelayedCall(0, ReloadShortcuts) end function PrefabSaveCmp(cmp_version, filename, cmp_fmt, cmp_props) filename = filename or "cmp.txt" cmp_props = cmp_props or {"hash", "height_hash", "type_hash", "mask_hash"} cmp_fmt = cmp_fmt or "%40s | %12s | %12s | %12s | %12s |\n" local props = {} local cmp_list = {string.format(cmp_fmt, "name", unpack(cmp_props)), string.rep('-', 120), "\n"} for i, marker in ipairs(PrefabMarkers) do if not cmp_version or (marker.version or 1) == cmp_version then local name = PrefabMarkers[marker] or "" for i, key in ipairs(cmp_props) do props[i] = marker[key] or "" end cmp_list[#cmp_list + 1] = string.format(cmp_fmt, name, unpack(props)) end end return AsyncStringToFile(filename, cmp_list) end function OnMsg.DataLoaded() CreateRealTimeThread(function() PrefabUpdateExported() PrefabUpdateMarkers() end) end function PrefabPreload(prefab, params_meta, skip) local name = PrefabMarkers[prefab] if not ExportedPrefabs[name] then p_print("No such exported prefab", name) return end local load_err, height_grid, type_grid, grass_grid, mask_grid, type_remapping, height_op, height_offset local skip_height = skip and skip.Height local height_file = not skip_height and prefab.height_hash and GetPrefabFileHeight(name) if height_file then height_grid, load_err = GridReadFile(height_file) if load_err then p_print("Failed to load height map of", name, ":", load_err or "failed") return end if developer and xxhash(height_grid) ~= prefab.height_hash then p_print("Detected changes in the height map of", name) end height_op = prefab.height_op height_offset = prefab.height_offset end local skip_type = skip and skip.Type local type_file = not skip_type and prefab.type_hash and GetPrefabFileType(name) if type_file then type_grid, load_err = GridReadFile(type_file) if load_err then p_print("Failed to load type map of", name, ":", load_err or "failed") return end if developer and xxhash(type_grid) ~= prefab.type_hash then p_print("Detected changes in the type map of", name) end type_remapping = GetTypeRemapping(prefab.type_names) end local skip_grass = skip and skip.Grass local grass_file = not skip_grass and prefab.grass_hash and GetPrefabFileGrass(name) if grass_file then grass_grid, load_err = GridReadFile(grass_file) if load_err then p_print("Failed to load grass map of", name, ":", load_err or "failed") return end if developer and xxhash(grass_grid) ~= prefab.grass_hash then p_print("Detected changes in the grass map of", name) end end local mask_file = prefab.mask_hash and GetPrefabFileMask(name) if mask_file then mask_grid, load_err = GridReadFile(mask_file) if load_err then p_print("Failed to load mask of", name, ":", load_err or "failed") return end if developer and xxhash(mask_grid) ~= prefab.mask_hash then p_print("Detected changes in the mask of", name) end end local params = { height_grid = height_grid, height_op = height_op, height_offset = height_offset, type_grid = type_grid, type_remapping = type_remapping, grass_grid = grass_grid, mask_grid = mask_grid, } if params_meta then setmetatable(params, params_meta) end return params end function PlacePrefab(name, prefab_pos, prefab_angle, seed, place_params) if (name or "") == "" or not ExportedPrefabs[name] then return "no exported prefab found" end local filename = GetPrefabFileObjs(name) local err, bin = AsyncFileToString(filename, nil, nil, "pstr") if err then return err end local defs = Unserialize(bin) if not defs then return "Failed to unserialize objects" end local prefab = PrefabMarkers[name] if not prefab then return "no such prefab marker" end local raster_params = PrefabPreload(prefab) if not raster_params then return "prefab loading failed" end local existing_objs = MapGet(prefab_pos, prefab.max_radius * type_tile, "attached", false, function(obj) return not IsClutterObj(obj) and GetClassFlags(obj, const.cfCodeRenderable) == 0 end) or {} place_params = place_params or empty_table local change_height = not not raster_params.height_grid local change_type = not not raster_params.type_grid local change_grass = not not raster_params.grass_grid local create_undo = place_params.create_undo if create_undo then XEditorUndo:BeginOp{ name = "PlacePrefab", height = change_height, terrain_type = change_type, grass_density = change_grass, objects = existing_objs, } end prefab_angle = (prefab_angle or 0) - (prefab.angle or 0) local inv_bbox if change_height or change_type or change_grass then raster_params.pos = prefab_pos raster_params.angle = prefab_angle raster_params.dither_seed = seed err, inv_bbox = AsyncGridSetTerrain(raster_params) if err then if create_undo then XEditorUndo:EndOp(existing_objs) end return "failed to apply terrain" elseif inv_bbox then if change_height then terrain.InvalidateHeight(inv_bbox) end if change_type then terrain.InvalidateType(inv_bbox) end end end local gof = const.gofPermanent | const.gofGenerated local cofComponentRandomMap = const.cofComponentRandomMap local g_Classes = g_Classes local GetPrefabObjPos, SetPrefabObjPos = GetPrefabObjPos, SetPrefabObjPos local PropObjSetProperty = PropObjSetProperty local dont_clamp_objects = place_params.dont_clamp_objects local ignore_ground_offset = place_params.ignore_ground_offset local fadein = place_params.fadein local save_collections = prefab.save_collections local placed_cols = 0 local remap_col_idx, last_col_idx local objs = {} local base_prop_count = const.RandomMap.PrefabBasePropCount SuspendPassEdits("PlacePrefab") for _, def in ipairs(defs) do local class, dpos, angle, daxis, scale, rmf_flags, fade_dist, ground_offset, normal_offset, coll_idx, color, mirror = unpack(def, 1, base_prop_count) assert(class ~= "Collection") local class_def = g_Classes[class] assert(class_def) if dont_clamp_objects then fade_dist = false end if ignore_ground_offset then ground_offset = false end local new_pos, new_angle, new_axis = GetPrefabObjPos( dpos, angle, daxis, fade_dist, prefab_pos, prefab_angle, ground_offset, normal_offset) if class_def and new_pos then local components = 0 if rmf_flags then components = components | cofComponentRandomMap end local obj = class_def:new(nil, components) if fadein then obj:SetOpacity(0) if fadein ~= -1 then obj:SetOpacity(100, fadein) end end SetPrefabObjPos(obj, new_pos, new_angle, new_axis, scale, color, mirror) for i=base_prop_count+1,#def,2 do PropObjSetProperty(obj, def[i], def[i + 1]) end if rmf_flags then obj:SetRandomMapFlags(rmf_flags) end if coll_idx and save_collections then local placed_idx = remap_col_idx and remap_col_idx[coll_idx] if not placed_idx then placed_idx = (last_col_idx or 0) + 1 local collections = Collections while collections[placed_idx] do placed_idx = placed_idx + 1 end if placed_idx <= const.GameObjectMaxCollectionIndex then local col = Collection:new() col:SetIndex(placed_idx) col:SetName(string.format("MapGen_%s", placed_idx)) SetGameFlags(col, gof) remap_col_idx = table.create_set(remap_col_idx, coll_idx, placed_idx) else assert(false, "Too many collections created!") placed_idx = 0 end remap_col_idx[coll_idx] = placed_idx end if placed_idx ~= 0 then obj:SetCollectionIndex(placed_idx) end end SetGameFlags(obj, gof) objs[#objs + 1] = obj end end for _, obj in ipairs(objs) do if obj.__ancestors.Object then obj:PostLoad() end end if IsEditorActive() then for _, obj in ipairs(objs) do if obj.__ancestors.EditorObject then obj:EditorEnter() end end end if change_height then local ClearCachedZ = CObject.ClearCachedZ local IsValidZ = CObject.IsValidZ for _, obj in ipairs(existing_objs) do if IsValid(obj) and not IsValidZ(obj) then ClearCachedZ(obj) end end end ResumePassEdits("PlacePrefab") if create_undo then table.iappend(existing_objs, objs) XEditorUndo:EndOp(existing_objs) end Msg("PrefabPlaced", name, objs) return nil, objs, inv_bbox end ---- RandomMapFlags = { { id = "IgnoreHeightOffset", name = "Ignore Height Offset", flag = const.rmfNoGroundOffset, help = "Disregard the original terrain height offset when placing the object" }, { id = "KeepNormalOffset", name = "Keep Normal Offset", flag = const.rmfNormalOffset, help = "Keep the original terrain normal offset when placing the object" }, { id = "OptionalPlacement", name = "Optional Placement", flag = const.rmfOptionalPlacement, help = "The object could be removed when placing its prefab" }, { id = "MeshOverlapCheck", name = "Mesh Overlap Check", flag = const.rmfMeshOverlapCheck, help = "Check mesh overlap ratio to detect prefab out-of-bounds objects. Otherwise only the object's position is checked" }, { id = "DeleteOnSteepSlope", name = "Delete On Steep Slope", flag = const.rmfDeleteOnSteepSlope, help = "Will be deleted if placed on a too steep slope" }, } function GetDefRandomMapFlags(classdef) local flags = 0 for _, info in ipairs(RandomMapFlags) do if classdef[info.id] then flags = flags | info.flag end end return flags end DefineClass.StripRandomMapProps = { __parents = { "PropertyObject" }, properties = {}, } for _, info in ipairs(RandomMapFlags) do local id = info.id local flag = info.flag CObject[id] = false local prop = { category = "Random Map", id = id, name = info.name, editor = "bool", help = info.help } table.insert(CObject.properties, prop) table.insert(StripCObjectProperties.properties, { id = id } ) table.insert(StripRandomMapProps.properties, { id = id }) CObject["Set" .. id] = function(self, set) local flags = self:GetRandomMapFlags() local def_flags if not flags then flags = GetDefRandomMapFlags(self) def_flags = flags end if set then flags = flags | flag else flags = flags & ~flag end if flags == def_flags then return end self:SetRandomMapFlags(flags) end CObject["Get" .. id] = function(self) local flags = self:GetRandomMapFlags() if not flags then return self[id] end return (flags & flag) ~= 0 end end DefineClass.PrefabSourceInfo = { __parents = { "Object", "EditorCallbackObject" }, properties = { { category = "Random Map", id = "Prefab", name = "Placed From", editor = "text", default = "", read_only = true, developer = true, buttons = {{name = "Goto", func = "GotoPrefabAction"}} }, }, } function PrefabSourceInfo:EditorCallbackGenerate(generator, object_source) local prefab = object_source[self] self.Prefab = prefab and PrefabMarkers[prefab] end if not Platform.developer then PrefabSourceInfo.SetPrefab = empty_func end AppendClass.FXSource = { __parents = { "PrefabSourceInfo" }, }