AppendClass.EntitySpecProperties = { properties = { { id = "env_colorized", name = "EnvColorized Group", editor = "combo", items = GetEnvColorizedGroups, category = "Misc", default = "", entitydata = true, }, { id = "default_colors", name = "Default colors", editor = "nested_obj", base_class = "ColorizationPropSet", inclusive = true, default = false, }, }, } local function InsertClass(class_parent_str, new_class) if not new_class or new_class == "" then return class_parent_str end if not class_parent_str or class_parent_str == "" then class_parent_str = new_class elseif not string.find(class_parent_str, "EnvColorized") then class_parent_str = class_parent_str .. "," .. new_class end return class_parent_str end function OnMsg.ClassesGenerate() local old_ExportEntityDataForSelf = EntitySpecProperties.ExportEntityDataForSelf function EntitySpecProperties:ExportEntityDataForSelf() local data = old_ExportEntityDataForSelf(self) if self.env_colorized and self.env_colorized ~= "" and not self:IsPropertyDefault("env_colorized") then data.entity.class_parent = InsertClass(data.entity.class_parent, "EnvColorized") end return data end end --------- Environment ------------ EnvColorizedGroups = false local function CollectGroups() if not EnvColorizedGroups then EnvColorizedGroups = { "EnvColorized" } local class_list = ClassDescendantsListInclusive("EnvColorized") local groups = { ["EnvColorized"] = true } for _, class in ipairs(class_list) do local class_table = _G[class] if not groups[class_table.env_colorized] then groups[class_table.env_colorized] = true table.insert(EnvColorizedGroups, class_table.env_colorized) end end end return EnvColorizedGroups end function GetEnvColorizedGroups() local list = table.copy(CollectGroups()) table.insert(list, 1, "") return list end function GetEnvColorizedFilters(obj, prop_meta, validate_fn) if validate_fn == "validate_fn" then -- function for preset validation, checks whether the property value is from "items" return "validate_fn", function(value, obj, prop_meta) return table.find(CollectGroups(), value) or IsKindOf(g_Classes[value], "EnvColorized") end end local list = { } for _, group in ipairs(CollectGroups()) do local text = "GROUP " .. group if group == "EnvColorized" then text = "All EnvColorized Objects" end table.insert(list, { text = text, value = group } ) end local class_list = ClassDescendantsList("EnvColorized") for _, class in ipairs(class_list) do table.insert(list, { text = class, value = class } ) end return list end DefineClass.EnvColorized = { __parents = {"ColorizableObject", "CObject" }, properties = { { id = "env_colorized", name = "EnvColorized Group", editor = "text", read_only = true, dont_save = true}, }, flags = { cfEditorCallback = true, }, env_colorized = "EnvColorized", } function EnvColorized:ColorizationReadOnlyReason() return "Object is EnvColorized. Colorization for such objects is controlled by EnvironmentColorPalette Editor." end function EnvColorized:ColorizationPropsDontSave(i) return true end ------------------------------ EnvironmentColorEntryBase ------------------------------ DefineClass.EnvironmentColorEntryBase = { __parents = {"ColorizationPropSet"}, properties = {}, EditorExcludeAsNested = true, } function EnvironmentColorEntryBase:AcceptsClass(obj_class) return false end function EnvironmentColorEntryBase:AcceptsTerrain(terrain_id) return false end function EnvironmentColorEntryBase:__eq(b) return rawequal(self, b) end ------------------------------ EnvironmentColorEntry (Entity colorization) ------------------------------ DefineClass.EnvironmentColorEntry = { __parents = {"EnvironmentColorEntryBase"}, properties = { { id = "filter_class", editor = "choice", items = GetEnvColorizedFilters, default = "", }, { id = "hue_variation1", editor = "number", slider = true, min = 0, default = 0, max = 900, scale = 10, }, { id = "hue_variation2", editor = "number", slider = true, min = 0, default = 0, max = 900, scale = 10, }, { id = "hue_variation3", editor = "number", slider = true, min = 0, default = 0, max = 900, scale = 10, }, }, EditorExcludeAsNested = true, } function EnvironmentColorEntry:GetEditorView() local filter_name = "" if self.filter_class == "EnvColorized" then filter_name = "All EnvColorized Objects" elseif rawget(_G, self.filter_class) then filter_name = "Entity " .. self.filter_class else filter_name = "Group " .. self.filter_class end return Untranslated(filter_name .. " " .. _InternalTranslate(ColorizationPropSet.GetEditorView(self))) end local IsKindOf = IsKindOf function EnvironmentColorEntry:AcceptsClass(obj_class) local filter_value = self.filter_class if not filter_value or filter_value == "" then return false end local class_table = _G[obj_class] if not IsKindOf(class_table, "EnvColorized") then return false end if filter_value == "EnvColorized" then return true end if filter_value == obj_class then return true end if filter_value == class_table.env_colorized then return true end return false end ------------------------------ EnvironmentTerrainColorEntry (Terrain colorization) ------------------------------ DefineClass.EnvironmentTerrainColorEntry = { __parents = {"EnvironmentColorEntryBase"}, properties = { { id = "terrain_id", editor = "choice", items = PresetsCombo("TerrainObj"), default = "", }, }, EditorExcludeAsNested = true, } function EnvironmentTerrainColorEntry:GetEditorView() local filter_name = string.format("Terrain %s - %s", self.terrain_id, _InternalTranslate(ColorizationPropSet.GetEditorView(self))) return filter_name end function EnvironmentTerrainColorEntry:AcceptsTerrain(terrain_id) return terrain_id == self.terrain_id end DefineClass.EnvironmentColorPalette = { __parents = { "Preset" }, properties = { { category = "Match (AND)", id = "regions", editor = "string_list", default = false, items = function (self) return PresetsCombo("GameStateDef", "region") end, help = "Match if current region is any of the list. Leave empty to always match." }, { category = "Match (AND)", id = "lightmodels", editor = "preset_id_list", preset_class = "LightmodelPreset", default = false, help = "Match if current lightmodel is any of the list. Leave empty to always match." }, { category = "Match (AND)", id = "enabled", editor = "bool", default = true, help = "Should match?" }, }, GlobalMap = "EnvironmentColorPalettes", ContainerClass = "EnvironmentColorEntryBase", HasSortKey = true, HasGroups = false, EditorCustomActions = { { FuncName = "ApplyOnCurrentMap", Icon = "CommonAssets/UI/Ged/play", Menubar = "Test", Name = "Apply", Toolbar = "main", }, }, Documentation = "Changes the color of various aspects of the environment like vegetation, terrains or rocks.", } function EnvironmentColorPalette:GetEditorView() local regions = "any" if self.regions and #self.regions > 0 and self.regions[1] then regions = table.concat(table.map(self.regions or {}, function(v) return v or "" end ), ", ") end local lightmodels = "any" if self.lightmodels and #self.lightmodels > 0 and self.lightmodels[1] then lightmodels = table.concat(table.map(self.lightmodels or {}, function(v) return v or "" end ), ", ") end local act_string = false if not self.enabled then act_string = "disabled" elseif regions == "any" and lightmodels == "any" and self.enabled then act_string = "always matched" else act_string = "[RG] " .. regions .. " [LM] " .. lightmodels end local preset_name = self.id local is_active = LastEnvColorizedCache and LastEnvColorizedCache.EnvColorSource == self.id if is_active then preset_name = "" .. preset_name .. "" end return Untranslated(preset_name .. " - " .. act_string .. "") end if FirstLoad then LastEnvColorizedCache = false end envpalette_print = CreatePrint{ "envpalette", format = "printf", output = function() end, } function EnvironmentColorPalette:CalcEnvCache() local class_list = ClassDescendantsListInclusive("EnvColorized") local class_to_color = {} for _, class in ipairs(class_list) do class_to_color[class] = false end local terrain_to_color = {} ForEachPreset("TerrainObj", function(preset) terrain_to_color[preset.id] = false end) local IsKindOf = IsKindOf for _, child in ipairs(self) do for _, class in ipairs(class_list) do if child:AcceptsClass(class) then class_to_color[class] = child end end ForEachPreset("TerrainObj", function(preset) if child:AcceptsTerrain(preset.id) then terrain_to_color[preset.id] = child end end) end return { EnvColorizedToColor = class_to_color, TerrainToColor = terrain_to_color, EnvColorSource = self.id, EnvColorizedHash = table.hash(class_to_color), TerrainHash = table.hash(terrain_to_color), } end function ModifyHueByOffset(color, offset) if offset == 0 then return color end local r, g, b = GetRGB(color) local h, s, v = UIL.RGBtoHSV(r,g,b) h = h + offset if h < 0 then h = h + 256 else h = h % 256 end return RGB(UIL.HSVtoRGB(h, s, v)) end local xxhash = xxhash local MulDivRound = MulDivRound local function ApplyToObject(class_to_color, obj) local palette = class_to_color[obj:GetEntity()] or class_to_color[obj.class] if not palette then return end obj:SetColorization(palette) local x, y = obj:GetPosXYZ() local seed = xxhash(x, y) local offset1 = palette.hue_variation1 - MulDivRound((seed >> 0 ) & 0xFF, palette.hue_variation1 * 2, 0xFF) local offset2 = palette.hue_variation2 - MulDivRound((seed >> 8 ) & 0xFF, palette.hue_variation2 * 2, 0xFF) local offset3 = palette.hue_variation3 - MulDivRound((seed >> 16) & 0xFF, palette.hue_variation3 * 2, 0xFF) obj:SetEditableColor1(ModifyHueByOffset(obj:GetEditableColor1(), offset1 / 10)) obj:SetEditableColor2(ModifyHueByOffset(obj:GetEditableColor2(), offset2 / 10)) obj:SetEditableColor3(ModifyHueByOffset(obj:GetEditableColor3(), offset3 / 10)) return true end function ApplyCurrentEnvColorizedToObj(obj) if not LastEnvColorizedCache or not IsKindOf(obj, "EnvColorized") then return false end return ApplyToObject(LastEnvColorizedCache.EnvColorizedToColor, obj) end function EnvironmentColorPalette:ApplyOnCurrentMap(force) local oldEnvCache = LastEnvColorizedCache local envcache = self:CalcEnvCache() LastEnvColorizedCache = envcache if force or not oldEnvCache or oldEnvCache.EnvColorizedHash ~= envcache.EnvColorizedHash then MapForEach("map", "EnvColorized", function(obj, envcache) ApplyToObject(envcache.EnvColorizedToColor, obj) end, envcache) end if force or not oldEnvCache or oldEnvCache.TerrainHash ~= envcache.TerrainHash then ReloadTerrains() -- Moves terrain data form lua to C hr.TR_ForceReloadNoTextures = 1 -- Updates the terrain itself end ObjModified(Presets.EnvironmentColorPalette) end function EnvColorizedTerrainColor(terrain_obj) -- Called from C local color_mod = terrain_obj.color_modifier if LastEnvColorizedCache then local override_value = LastEnvColorizedCache.TerrainToColor[terrain_obj.id] if override_value then color_mod = override_value:GetEditableColor1() end end return color_mod end local ApplyCurrentEnvColorizedToObj = ApplyCurrentEnvColorizedToObj function OnMsg.EditorCallback(id, objects, ...) if id == "EditorCallbackPlace" or id == "EditorCallbackPlaceCursor" or id == "EditorCallbackClone" then for i = 1, #objects do local obj = objects[i] ApplyCurrentEnvColorizedToObj(obj) for _, attach in ipairs(obj:GetAttaches()) do ApplyCurrentEnvColorizedToObj(attach) end end end end local ignore_lightmodels = {"SatelliteView"} local function FindEnvColorPalette(region, lightmodel) for _, lm_name in ipairs(ignore_lightmodels) do if string.find(lightmodel, lm_name) then return false end end local best_match = false ForEachPreset(EnvironmentColorPalette, function(preset) local lm_found = not preset.lightmodels or #preset.lightmodels == 0 or table.find(preset.lightmodels, lightmodel) local region_found = not preset.regions or #preset.regions == 0 or table.find(preset.regions, region) if lm_found and region_found and preset.enabled and not best_match then best_match = preset end end) return best_match end function ApplyCurrentEnvironmentColorPalette(force) local lightmodel_id = CurrentLightmodel and CurrentLightmodel[1] and CurrentLightmodel[1].id local region_id = CurrentMap and CurrentMap ~= "" and MapData[CurrentMap] and MapData[CurrentMap].Region local envpalette = FindEnvColorPalette(region_id, lightmodel_id) envpalette_print("Applying palette '%s' from region '%s' and lightmodel '%s', forced '%s'. Previous '%s'", envpalette and envpalette.id or "none", region_id, lightmodel_id, not not force, LastEnvColorizedCache and LastEnvColorizedCache.EnvColorSource or "none") if envpalette then envpalette:ApplyOnCurrentMap(force) return true end end function OnMsg.LightmodelChange(view, lightmodel, time, prev_lm) if lightmodel then if not ChangingMap then ApplyCurrentEnvironmentColorPalette() end end end function OnMsg.NewMapLoaded() LastEnvColorizedCache = false ApplyCurrentEnvironmentColorPalette(true) end