function GetObjStateAttaches(obj, entity) entity = entity or (obj:GetEntity() or obj.entity) local state = obj and GetStateName(obj:GetState()) or "idle" local entity_attaches = Attaches[entity] return entity_attaches and entity_attaches[state] end function GetEntityAutoAttachModes(obj, entity) local attaches = GetObjStateAttaches(obj, entity) local modes = {""} for _, attach in ipairs(attaches or empty_table) do if attach.required_state then local mode = string.trim_spaces(attach.required_state) table.insert_unique(modes, mode) end end return modes end --[[@@@ @class AutoAttachCallback Inherit this if you want a callback when an objects is autoattached to its parent --]] DefineClass.AutoAttachCallback = { __parents = {"InitDone"}, } function AutoAttachCallback:OnAttachToParent(parent, spot) end --[[@@@ @class AutoAttachObject Objects from this type are able to attach a preset of objects on their creation based on their spot annotations. --]] DefineClass.AutoAttachObject = { __parents = { "Object", "ComponentAttach" }, auto_attach_props_description = false, properties = { { id = "AutoAttachMode", editor = "choice", default = "", items = function(obj) return GetEntityAutoAttachModes(obj) or {} end }, { id = "AllAttachedLightsToDetailLevel", editor = "choice", default = false, items = {"Essential", "Optional", "Eye Candy"}}, }, auto_attach_at_init = true, auto_attach_mode = false, is_forced_lod_min = false, max_colorization_materials_attaches = 0, } local gofAutoAttach = const.gofAutoAttach function IsAutoAttach(attach) return attach:GetGameFlags(gofAutoAttach) ~= 0 end function AutoAttachObject:DestroyAutoAttaches() self:DestroyAttaches(IsAutoAttach) end function AutoAttachObject:ClearAttachMembers() local attaches = GetObjStateAttaches(self) for _, attach in ipairs(attaches) do if attach.member then self[attach.member] = nil end end end function AutoAttachObject:SetAutoAttachMode(value) self.auto_attach_mode = value self:DestroyAutoAttaches() self:ClearAttachMembers() self:AutoAttachObjects() end function AutoAttachObject:OnEditorSetProperty(prop_id, old_value, ged) if prop_id == "AllAttachedLightsToDetailLevel" or prop_id == "StateText" then self:SetAutoAttachMode(self:GetAutoAttachMode()) end Object.OnEditorSetProperty(self, prop_id, old_value, ged) end function AutoAttachObject:GetAutoAttachMode(mode) local mode_set = GetEntityAutoAttachModes(self) if not mode_set then return "" end if table.find(mode_set, mode or self.auto_attach_mode) then return self.auto_attach_mode end return mode_set[1] or "" end function AutoAttachObject:GetAttachModeSet() return GetEntityAutoAttachModes(self) end if FirstLoad then s_AutoAttachedLightDetailsBaseObject = false end function AutoAttachObjects(obj, context) if not s_AutoAttachedLightDetailsBaseObject and obj.AllAttachedLightsToDetailLevel then s_AutoAttachedLightDetailsBaseObject = obj end local selectable = obj:GetEnumFlags(const.efSelectable) ~= 0 local attaches = GetObjStateAttaches(obj) local max_colorization_materials = 0 for i = 1, #(attaches or "") do local attach = attaches[i] local class = GetAttachClass(obj, attach[2]) local spot_attaches = {} local place, detail_class = PlaceCheck(obj, attach, class, context) if place then local o = PlaceAtSpot(obj, attach.spot_idx, class, context) if o then if attach.mirrored then o:SetMirrored(true) end if attach.offset then o:SetAttachOffset(attach.offset) end if attach.axis and attach.angle and attach.angle ~= 0 then o:SetAttachAxis(attach.axis) o:SetAttachAngle(attach.angle) end if selectable then o:SetEnumFlags(const.efSelectable) end if attach.inherited_properties then for key, value in sorted_pairs(attach.inherited_properties) do o:SetProperty(key, value) end end if IsKindOf(o, "SubstituteByRandomChildEntity") then -- NOTE: when substituting entity the object is still not attached so it can't be -- destroyed in SubstituteByRandomChildEntity and we have to do it manually here if o:IsForcedLODMinAttach() and o:GetDetailClass() ~= "Essential" then DoneObject(o) o = nil else local top_parent = GetTopmostParent(o) ApplyCurrentEnvColorizedToObj(top_parent) -- entity changed, possibly colorization too. top_parent:DestroyRenderObj(true) end else o:SetDetailClass(detail_class) end if o then if attach.inherit_colorization then o:SetGameFlags(const.gofInheritColorization) max_colorization_materials = Max(max_colorization_materials, o:GetMaxColorizationMaterials()) end o:SetForcedLODMin(rawget(obj, "is_forced_lod_min") or obj:GetForcedLODMin()) spot_attaches[#spot_attaches+1] = o end end end if context ~= "placementcursor" then SetObjMembers(obj, attach, spot_attaches) end end if max_colorization_materials > AutoAttachObject.max_colorization_materials_attaches then obj.max_colorization_materials_attaches = max_colorization_materials end if s_AutoAttachedLightDetailsBaseObject == obj then s_AutoAttachedLightDetailsBaseObject = false end end function AutoAttachObject:GetMaxColorizationMaterials() return Max(self.max_colorization_materials_attaches, CObject.GetMaxColorizationMaterials(self)) end function AutoAttachObject:CanBeColorized() return self.max_colorization_materials_attaches and self.max_colorization_materials_attaches > 1 or CObject.CanBeColorized(self) end AutoAttachObject.AutoAttachObjects = AutoAttachObjects function RemoveObjMembers(obj, attach, list) if attach.member then local o = obj[attach.member] for i = 1, #list do if o == list[i] then obj[attach.member] = false break end end end if attach.memberlist and obj[attach.memberlist] and type(obj[attach.memberlist]) == "table" then table.remove_entry(obj[attach.memberlist], list) end end -- local functions used in the class methods local AutoAttachObjects, RemoveObjMembers = AutoAttachObjects, RemoveObjMembers function AutoAttachObject:Init() if self.auto_attach_at_init then AutoAttachObjects(self, "init") end end function AutoAttachObject:__fromluacode(props, arr, handle) local obj = ResolveHandle(handle) if obj and obj[true] then StoreErrorSource(obj, "Duplicate handle", handle) assert(false, string.format("Duplicate handle %d: new '%s', prev '%s'", handle, self.class, obj.class)) obj = nil end local idx = table.find(props, "AllAttachedLightsToDetailLevel") local attached_lights_detail = idx and props[idx + 1] if attached_lights_detail then obj.AllAttachedLightsToDetailLevel = attached_lights_detail end local idx = table.find(props, "LowerLOD") if idx ~= nil then obj.is_forced_lod_min = props[idx + 1] else idx = table.find(props, "ForcedLODState") obj.is_forced_lod_min = idx and (props[idx + 1] == "Minimum") end obj = self:new(obj) SetObjPropertyList(obj, props) SetArray(obj, arr) obj.is_forced_lod_min = nil return obj end AutoAttachObject.ShouldAttach = return_true AutoResolveMethods.ShouldAttach = "and" function AutoAttachObject:OnAttachCreated(attach, spot) end function AutoAttachObject:MarkAttachEntities(entities) if not IsValid(self) then return entities end entities = entities or {} self:__MarkEntities(entities) local cur_mode = self.auto_attach_mode local modes = self:GetAttachModeSet() for _, mode in ipairs(modes) do self:SetAutoAttachMode(mode) self:__MarkEntities(entities) end self:SetAutoAttachMode(cur_mode) return entities end function SetObjMembers(obj, attach, list) if attach.member then local name = attach.member if #list == 0 then if not rawget(obj, name) then obj[name] = false -- initialize on init end else assert(#list == 1 and not rawget(obj, name), 'Duplicate member "'..name..'" in the auto-attaches of class "'..obj.class..'"') obj[name] = list[1] end end if attach.memberlist then local name = attach.memberlist if not rawget(obj, name) or not type(obj[name]) == "table" or not IsValid(obj[name][1]) then obj[name] = {} end if #list > 0 then obj[name][#obj[name] + 1] = list end end end function GetAttachClass(self, classes) if type(classes) == "string" then return classes end assert(type(classes) == "table") local rnd = self:Random(100) local cur_prob = 0 for class, prob in pairs(classes) do cur_prob = cur_prob + prob if rnd <= cur_prob then return class end end -- probability of nothing left return false end local IsKindOf = IsKindOf local shapeshifter_class_whitelist = { "Light", "AutoAttachSIModulator", "ParSystem" } -- classes that are alowed to be instantiated even in shapeshifters local function IsObjectClassAllowedInShapeshifter(class_to_spawn) for _, class_name in ipairs(shapeshifter_class_whitelist) do if IsKindOf(class_to_spawn, class_name) then return true end end return false end local gofDetailClassMask = const.gofDetailClassMask function PlaceCheck(obj, attach, class, context) if not obj:ShouldAttach(attach) then return false end -- placement cursor check if context == "placementcursor" then if not attach.show_at_placement and not attach.placement_only then return false end elseif attach.placement_only then return false end if attach.required_state and IsKindOf(obj, "AutoAttachObject") and attach.required_state ~= obj.auto_attach_mode then return false end -- condition check local condition = attach.condition if condition then assert(type(condition) == "function" or type(condition) == "string") if type(condition) == "function" then if not condition(obj, attach) then return false end else if obj:HasMember(condition) and not obj[condition] then return false end end end local detail_class = s_AutoAttachedLightDetailsBaseObject and IsKindOf(g_Classes[class], "Light") and s_AutoAttachedLightDetailsBaseObject.AllAttachedLightsToDetailLevel detail_class = detail_class or (attach.DetailClass ~= "Default" and attach.DetailClass) if not detail_class then -- try to extract from the class local detail_mask = GetClassGameFlags(class, gofDetailClassMask) local detail_from_class = GetDetailClassMaskName(detail_mask) detail_class = detail_from_class ~= "Default" and detail_from_class end local forced_lod_min = rawget(obj, "is_forced_lod_min") or obj:GetForcedLODMin() if forced_lod_min and detail_class ~= "Essential" then return false end return true, detail_class end function PlaceAtSpot(obj, spot, class, context) local o if g_Classes[class] then if context == "placementcursor" then if g_Classes[class]:IsKindOfClasses("TerrainDecal", "BakedTerrainDecal") then o = PlaceObject("PlacementCursorAttachmentTerrainDecal") else o = PlaceObject("PlacementCursorAttachment") end o:ChangeClass(class) AutoAttachObjects(o, "placementcursor") elseif context == "shapeshifter" and not IsObjectClassAllowedInShapeshifter(g_Classes[class]) then o = PlaceObject("Shapeshifter", nil, const.cofComponentAttach) if IsValidEntity(class) then o:ChangeEntity(class) end else o = PlaceObject(class, nil, const.cofComponentAttach) end else print("once", 'AutoAttach: unknown class/particle "' .. class .. '" for [object "' .. obj.class .. '", spot "' .. obj:GetSpotName(spot) .. '"]') end if not o then return end local err = obj:Attach(o, spot) if err then print("once", "Error attaching", o.class, "to", obj.class, ":", err) return end o:SetGameFlags(const.gofAutoAttach) if not IsKindOf(obj, "Shapeshifter") then obj:OnAttachCreated(o, spot) end if IsKindOf(o, "AutoAttachCallback") then o:OnAttachToParent(obj, spot) end return o end if FirstLoad then Attaches = {} -- global table that keeps the inherited auto_attaches end function AutoAttachObjectsToPlacementCursor(obj) AutoAttachObjects(obj, "placementcursor") end --[Deprecated] function AutoAttachObjectsToShapeshifter(obj) AutoAttachObjects(obj) end function AutoAttachShapeshifterObjects(obj) AutoAttachObjects(obj, "shapeshifter") end local function CanInheritColorization(parent_entity, child_entity) return true end function GetEntityAutoAttachTable(entity, auto_attach) auto_attach = auto_attach or false local states = GetStates(entity) for _, state in ipairs(states) do local spbeg, spend = GetAllSpots(entity, state) for spot = spbeg, spend do local str = GetSpotAnnotation(entity, spot) if str and #str > 0 then local item for w in string.gmatch(str,"%s*(.[^,]+)[, ]?") do local lw = string.lower(w) if not item then -- auto attach description if lw ~= "att" and lw~="autoattach" then break end item = {} item.spot_idx = spot elseif lw=="show at placement" or lw=="show_at_placement" or lw=="show" then -- show at placement item.show_at_placement = true elseif lw=="placement only" or lw=="placement_only" then -- placement only item.placement_only = true elseif lw=="mirrored" or lw=="mirror" then item.mirrored = true elseif not item[2] then item[2] = w if not g_Classes[w] then print("once", "Invalid autoattach", w, "for entity", entity) end end end if item then item.inherit_colorization = CanInheritColorization(entity, item[2]) auto_attach = auto_attach or {} auto_attach[state] = auto_attach[state] or {} table.insert(auto_attach[state], item) end end end end return auto_attach end local function IsAutoAttachObject(entity) local entity_data = EntityData and EntityData[entity] and EntityData[entity].entity local classes = entity_data and entity_data.class_parent and entity_data.class_parent or "" for class in string.gmatch(classes, "[^%s,]+%s*") do if IsKindOf(g_Classes[class], "AutoAttachObject") then return true end end end local function TransferMatchingIdleAttachesToAllState(auto_attach, states) local idle_attaches = auto_attach["idle"] if not idle_attaches then return end local attach_modes for _, attach in ipairs(idle_attaches) do if attach.required_state then attach_modes = true break end end if not attach_modes then return end for _, state in ipairs(states) do auto_attach[state] = auto_attach[state] or {} table.iappend(auto_attach[state], idle_attaches) end end -- build autoattach table function RebuildAutoattach() if not config.LoadAutoAttachData then return end local ae = GetAllEntities() for entity, _ in sorted_pairs(ae) do local auto_attach = IsAutoAttachObject(entity) and GetEntityAutoAttachTable(entity) auto_attach = GetEntityAutoAttachTableFromPresets(entity, auto_attach) if auto_attach then local states = GetStates(entity) table.remove_value(states, "idle") if #states > 0 then -- transfer auto attaches to all states since AutoAttachEditor can define only in "idle" state TransferMatchingIdleAttachesToAllState(auto_attach, states) end Attaches[entity] = auto_attach else Attaches[entity] = nil end end end OnMsg.EntitiesLoaded = RebuildAutoattach function OnMsg.PresetSave(name) local class = g_Classes[name] if IsKindOf(class, "AutoAttachPreset") then RebuildAutoattach() end end local function PlaceFadingObjects(category, init_pos) local ae = GetAllEntities() local init_pos = init_pos or GetTerrainCursor() local pos = init_pos for k,v in pairs(ae) do if EntityData[k] and EntityData[k].entity and EntityData[k].entity.fade_category == category then local o = PlaceObject(k) o:ChangeEntity(k) o:SetPos(pos) o:SetGameFlags(const.gofPermanent) pos = pos + point(10*guim, 0) if (pos:x() / (600*guim) > 0) then pos = point(init_pos:x(), pos:y() + 20*guim) end elseif not EntityData[k] then print("No EntityData for: ", k) elseif EntityData[k] and not EntityData[k].entity then print("No EntityData[].entity for: ", k) end end end function TestFadeCategories() local cat = { "PropsUltraSmall", "PropsSmall", "PropsMedium", "PropsBig", } local pos = point(100*guim, 100*guim) for i=1, #cat do PlaceFadingObjects(cat[i], pos) pos = pos + point(0, 100*guim) end end function GetEntitiesAutoattachCount(filter_count) local el = GetAllEntities() local filter_count = filter_count or 30 for k,v in pairs(el) do local s,e = GetSpotRange(k, EntityStates["idle"], "Autoattach") if (e-s) > filter_count then print(k, e-s) end end end function ListEntityAutoattaches(entity) local s,e = GetSpotRange(entity, EntityStates["idle"], "Autoattach") for i=s, e do local annotation = GetSpotAnnotation(entity, i) print(i, annotation) end end ---------------------- AutoAttach editor ---------------------- local function FindArtSpecById(id) local spec = EntitySpecPresets[id] if not spec then local idx = string.find(id, "_[0-9]+$") if idx then spec = EntitySpecPresets[string.sub(id, 0, idx - 1)] end end return spec end local function GenerateMissingEntities() local all_entities = GetAllEntities() local to_create = {} for entity in pairs(all_entities) do local spec = FindArtSpecById(entity) if spec and not AutoAttachPresets[entity] and string.find(spec.class_parent, "AutoAttachObject", 1, true) then table.insert(to_create, entity) end end if #to_create > 0 then for _, entity in ipairs(to_create) do local preset = AutoAttachPreset:new({id = entity}) preset:Register() preset:UpdateSpotData() Sleep(1) end AutoAttachPreset:SortPresets() ObjModified(Presets.AutoAttachPreset) end end function GetEntitySpots(entity) if not IsValidEntity(entity) then return {} end local states = GetStates(entity) local idle = table.find(states, "idle") if not idle then print("WARNING: No idle state for", entity, "cannot fetch spots.") return {} end local spots = {} local spbeg, spend = GetAllSpots(entity, "idle") for spot = spbeg, spend do local str = GetSpotName(entity, spot) spots[str] = spots[str] or {} table.insert(spots[str], spot) end return spots end local zeropoint = point(0, 0, 0) function GetEntityAutoAttachTableFromPresets(entity, attach_table) local preset = AutoAttachPresets[entity] if not preset then return attach_table end local spots for _, spot in ipairs(preset) do for _, rule in ipairs(spot) do attach_table = rule:FillAutoAttachTable(attach_table, entity, preset) end end return attach_table end DefineClass.AutoAttachRuleBase = { __parents = { "PropertyObject" }, parent = false, } function AutoAttachRuleBase:FillAutoAttachTable(attach_table, entity, preset) return attach_table end function AutoAttachRuleBase:IsActive() return false end function AutoAttachRuleBase:OnEditorNew(parent, ged, is_paste) self.parent = parent end local function GetSpotsCombo(entity_name) local t = {} local spots = GetEntitySpots(entity_name) for spot_name, indices in sorted_pairs(spots) do for i = 1, #indices do table.insert(t, spot_name .. " " .. i) end end return t end DefineClass.AutoAttachRuleInherit = { __parents = { "AutoAttachRuleBase" }, properties = { { id = "parent_entity", category = "Rule", name = "Parent Entity", editor = "combo", items = function() return ClassDescendantsCombo("AutoAttachObject") end, default = "", }, { id = "spot", category = "Rule", name = "Spot", editor = "combo", items = function(obj) return GetSpotsCombo(obj:GetParentEntity()) end, default = "", }, }, } function AutoAttachRuleInherit:GetParentEntity() return self.parent_entity end function AutoAttachRuleInherit:GetSpotAndIdx() local spot = self.spot local break_idx = string.find(spot, "%d+$") if not break_idx then return end local spot_name = string.sub(spot, 1, break_idx - 2) local spot_idx = tonumber(string.sub(spot, break_idx)) if spot_name and spot_idx then return spot_name, spot_idx end end function AutoAttachRuleInherit:GetEditorView() local str = string.format("Inherit %s from %s", self.spot or "[SPOT]", self:GetParentEntity() or "[ENTITY]") if not self:FindInheritedSpot() then str = "" .. str .. "" end return str end function AutoAttachRuleInherit:FindInheritedSpot() local entity = self:GetParentEntity() local parent_preset = AutoAttachPresets[entity] if not parent_preset then return end local spot_name, spot_idx = self:GetSpotAndIdx() if not spot_name or not spot_idx then return end local aaspot_idx, aapost_obj = parent_preset:GetSpot(spot_name, spot_idx) if not aapost_obj then return end return aapost_obj, entity, parent_preset end function AutoAttachRuleInherit:FillAutoAttachTable(attach_table, entity, preset) local spot, parent_entity, parent_preset = self:FindInheritedSpot() if not spot then return attach_table end for _, rule in ipairs(spot) do attach_table = rule:FillAutoAttachTable(attach_table, parent_entity, parent_preset, self.parent) end return attach_table end function AutoAttachRuleInherit:IsActive() local spot, _, _ = self:FindInheritedSpot() return not not spot end DefineClass.AutoAttachRule = { __parents = { "AutoAttachRuleBase" }, properties = { { id = "attach_class", category = "Rule", name = "Object Class", editor = "combo", items = function() return ClassDescendantsCombo("CObject") end, default = "", }, { id = "quick_modes", default = false, no_save = true, editor = "buttons", category = "Rule", buttons = { {name = "ParSystem", func = "QuickSetToParSystem"}, }}, { id = "offset", category = "Rule", name = "Offset", editor = "point", default = point(0, 0, 0), }, { id = "axis" , category = "Rule", name = "Axis", editor = "point", default = point(0, 0, 0), }, { id = "angle", category = "Rule", name = "Angle", editor = "number", default = 0, scale = "deg" }, { id = "member", category = "Rule", name = "Member", help = "The name of the property of the parent object that should be pointing to the attach object.", editor = "text", default = "", }, { id = "required_state", category = "Rule", name = "Attach State", help = "Conditional attachment", default = "", editor = "combo", items = function(obj) return obj and obj.parent and obj.parent.parent and obj.parent.parent:GuessPossibleAutoattachStates() or {} end, }, { id = "GameStatesFilter", name="Game State", category = "Rule", editor = "set", default = set(), three_state = true, items = function() return GetGameStateFilter() end }, { id = "DetailClass", category = "Rule", name = "Detail Class Override", editor = "dropdownlist", items = {"Default", "Essential", "Optional", "Eye Candy"}, default = "Default", }, { id = "inherited_values", no_edit = true, editor = "prop_table", default = false, }, }, parent = false, } function AutoAttachRule:IsActive() return self.attach_class ~= "" end function AutoAttachRule:QuickSetToParSystem() self.attach_class = "ParSystem" ObjModified(self) end function AutoAttachRule:ResolveConditionFunc() local gamestates_filters = self.GameStatesFilter if not gamestates_filters or not next(gamestates_filters) then return false end return function(obj, attach) if gamestates_filters then for key, value in pairs(gamestates_filters) do if value then if not GameState[key] then return false end else if GameState[key] then return false end end end end return true end end function AutoAttachRule:GetCleanInheritedPropertyValues() local inherited_values = self.inherited_values if not inherited_values then return false end local inherited_props = self:GetInheritedProps() if not inherited_props or #inherited_props == 0 then return false end local clean_value_list = {} for _, prop in ipairs(inherited_props) do local value = inherited_values[prop.id] if value ~= nil then clean_value_list[prop.id] = value end end return clean_value_list end function AutoAttachRule:FillAutoAttachTable(attach_table, entity, preset, spot) if self.attach_class == "" then return attach_table end spot = spot or self.parent attach_table = attach_table or {} local attach_table_idle = attach_table["idle"] or {} attach_table["idle"] = attach_table_idle local istart, iend = GetSpotRange(spot.parent.id, "idle", spot.name) if istart < 0 then print(string.format("Warning: Could not find '%s' spot range for '%s'", spot.name, entity)) else table.insert(attach_table_idle, { spot_idx = istart + spot.idx - 1, [2] = self.attach_class, offset = self.offset, axis = self.axis ~= zeropoint and self.axis, angle = self.angle ~= 0 and self.angle, member = self.member ~= "" and self.member, required_state = self.required_state ~= "" and self.required_state or false, condition = self:ResolveConditionFunc() or false, DetailClass = self.DetailClass ~= "Default" and self.DetailClass, inherited_properties = self:GetCleanInheritedPropertyValues(), inherit_colorization = preset.PropagateColorization and CanInheritColorization(entity, self.attach_class), }) end return attach_table end function AutoAttachRule:GetEditorView() local str if self.attach_class == "ParSystem" then str = "Particles " .. (self.inherited_values and self.inherited_values["ParticlesName"] or "?") .. "" else str = "Attach " .. (self.attach_class or "?") .. "" end str = str .. " (" .. self.DetailClass .. ")" if self.required_state ~= "" then str = str .. " : " .. self.required_state .. "" end if self.attach_class == "" then str = "" .. str .. "" end return str end function AutoAttachRule:Setattach_class(value) if self.parent and self.parent.parent and self.parent.parent.id == value then value = "" return false end self.attach_class = value end function AutoAttachRule:GetInheritedProps() local properties = {} local class_obj = g_Classes[self.attach_class] if not class_obj then return properties end local orig_properties = PropertyObject.GetProperties(self) local properties_of_target_entity = class_obj:GetProperties() for _, prop in ipairs(properties_of_target_entity) do if prop.autoattach_prop then assert(not table.find(orig_properties, "id", prop.id), string.format("Property %s conflict between AutoAttachRule and %s", prop.id, self.attach_class)) prop = table.copy(prop) prop.dont_save = true table.insert(properties, prop) end end return properties end function AutoAttachRule:GetProperties() local properties = PropertyObject.GetProperties(self) local class_obj = g_Classes[self.attach_class] if not class_obj then return properties end properties = table.copy(properties) properties = table.iappend(properties, self:GetInheritedProps()) return properties end function AutoAttachRule:SetProperty(id, value) if table.find(self:GetInheritedProps(), "id", id) then self.inherited_values = self.inherited_values or {} self.inherited_values[id] = value return end PropertyObject.SetProperty(self, id, value) end function AutoAttachRule:GetProperty(id) if self.inherited_values and self.inherited_values[id] ~= nil then return self.inherited_values[id] end return PropertyObject.GetProperty(self, id) end function AutoAttachRule:OnEditorSetProperty(prop_id, old_value, ged) RebuildAutoattach() ged:ResolveObj("SelectedPreset"):RecreateDemoObject(ged) local id = self.parent.parent.id local class = rawget(_G, id) if class and not class:IsKindOf("AutoAttachObject") then return false end MapForEach("map", id, function(obj) obj:SetAutoAttachMode(obj:GetAutoAttachMode()) end) end function AutoAttachRule:GetMaxColorizationMaterials() if IsKindOf(_G[self.attach_class], "WaterObj") then return 3 end return self.attach_class ~= "" and IsValidEntity(self.attach_class) and ColorizationMaterialsCount(self.attach_class) or 0 end function AutoAttachRule:ColorizationReadOnlyReason() return false end function AutoAttachRule:ColorizationPropsNoEdit(i) if self.parent.parent.PropagateColorization then return true end return self:GetMaxColorizationMaterials() < i end DefineClass.AutoAttachSpot = { __parents = { "PropertyObject", "Container" }, properties = { { id = "name", name = "Spot Name", editor = "text", default = "", read_only = true }, { id = "idx", name = "Number", editor = "number", default = -1, read_only = true, }, { id = "original_index", name = "Original Index", editor = "number", default = -1, read_only = true, }, }, annotated_autoattach = false, EditorView = Untranslated(" ')> "), parent = false, ContainerClass = "AutoAttachRuleBase", } function AutoAttachSpot:Color() return not self:HasSomethingAttached() and "" or "" end function AutoAttachSpot:HasSomethingAttached() if #self == 0 then return false end for _, rule in ipairs(self) do if rule:IsActive() then return true end end return false end function AutoAttachSpot:AnnotatedAutoattachMsg() if not self.annotated_autoattach then return "" end return "" .. self.annotated_autoattach end function AutoAttachSpot.CreateRule(root, obj) obj[#obj + 1] = AutoAttachRule:new({parent = obj}) ObjModified(root) ObjModified(obj) end function CommonlyUsedAttachItems() local ret = {} ForEachPreset("AutoAttachPreset", function(preset) for _, rule in ipairs(preset) do for _, subrule in ipairs(rule) do local class = rawget(subrule, "attach_class") if class and class ~= "" then ret[class] = (ret[class] or 0) + 1 end end end end) for class, count in pairs(ret) do if count == 1 then ret[class] = nil end end return table.keys2(ret, "sorted") end DefineClass.AutoAttachPresetFilter = { __parents = { "GedFilter" }, properties = { { id = "NonEmpty", name = "Only show non-empty entries", default = false, editor = "bool" }, { id = "HasAttach", name = "Has attach of class", default = false, editor = "combo", items = CommonlyUsedAttachItems }, { id = "_", editor = "buttons", default = false, buttons = { { name = "Add new AutoAttach entity", func = "AddEntity" } } }, }, } function AutoAttachPresetFilter:FilterObject(obj) if self.NonEmpty then for _, rule in ipairs(obj) do for _, subrule in ipairs(rule) do if subrule:IsKindOf("AutoAttachRule") and subrule.attach_class ~= "" then return true end end end return false end local class = self.HasAttach if class then for _, rule in ipairs(obj) do for _, subrule in ipairs(rule) do if subrule:IsKindOf("AutoAttachRule") and subrule.attach_class == class then return true end end end return false end return true end function AutoAttachPresetFilter:AddEntity(root, prop_id, ged) local entities = {} ForEachPreset("EntitySpec", function(preset) if not string.find(preset.class_parent, "AutoAttachObject", 1, true) and not preset.id:starts_with("#") then entities[#entities + 1] = preset.id end end) local entity = ged:WaitListChoice(entities, "Choose entity to add:") if not entity then return end local spec = EntitySpecPresets[entity] if spec.class_parent == "" then spec.class_parent = "AutoAttachObject" else spec.class_parent = spec.class_parent .. ",AutoAttachObject" end GedSetUiStatus("add_autoattach_entity", "Saving ArtSpec...") EntitySpec:SaveAll() self.NonEmpty = false self.HasAttach = false GenerateMissingEntities() ged:SetSelection("root", { 1, table.find(Presets.AutoAttachPreset.Default, "id", entity) }) GedSetUiStatus("add_autoattach_entity") ged:ShowMessage(Untranslated("Attention!"), Untranslated("You need to commit both the assets and the project folder!")) end DefineClass.AutoAttachPreset = { __parents = { "Preset" }, properties = { { id = "Id", read_only = true, }, { id = "SaveIn", read_only = true, }, { id = "help", editor = "buttons", buttons = {{name = "Go to ArtSpec", func = "GotoArtSpec"}}, default = false,}, { id = "PropagateColorization", editor = "bool", default = true }, }, GlobalMap = "AutoAttachPresets", ContainerClass = "AutoAttachSpot", GedEditor = "GedAutoAttachEditor", EditorMenubar = "Editors.Art", EditorMenubarName = "AutoAttach Editor", EditorIcon = "CommonAssets/UI/Icons/attach attachment paperclip.png", FilterClass = "AutoAttachPresetFilter", EnableReloading = false, } function AutoAttachPreset:GuessPossibleAutoattachStates() return GetEntityAutoAttachModes(nil, self.id) end function AutoAttachPreset:EditorContext() local context = Preset.EditorContext(self) context.Classes = {} context.ContainerTree = true return context end function AutoAttachPreset:EditorItemsMenu() return {} end function AutoAttachPreset:GotoArtSpec(root) local editor = OpenPresetEditor("EntitySpec") local spec = self:GetEntitySpec() local root = editor:ResolveObj("root") local group_idx = table.find(root, root[spec.group]) local idx = table.find(root[spec.group], spec) editor:SetSelection("root", {group_idx, idx}) end function AutoAttachPreset:PostLoad() for idx, item in ipairs(self) do item.parent = self for _, subitem in ipairs(item) do subitem.parent = item end end Preset.PostLoad(self) end function AutoAttachPreset:GenerateCode(code) self:UpdateSpotData() -- to read save_in -- drop redundant/unneeded data local has_something_attached = false for i = #self, 1, -1 do local spot = self[i] if not spot:HasSomethingAttached() then table.remove(self, i) else spot.original_index = nil spot.annotated_autoattach = nil has_something_attached = true if not spot[#spot]:IsActive() then table.remove(spot, #spot) end end end if has_something_attached then Preset.GenerateCode(self, code) end self:UpdateSpotData() -- to write original_index and annotated_autoattach back to the structure end function AutoAttachPreset:GetSpot(name, idx) for i, value in ipairs(self) do if value.name == name and value.idx == idx then return i, value end end end function AutoAttachPreset:UpdateSpotData() local spec = self:GetEntitySpec() if not spec then return end self.save_in = spec:GetSaveIn() local spots = GetEntitySpots(self.id) -- drop additional spots for i = #self, 1, -1 do local entry = self[i] if entry.idx > (spots[entry.name] and #spots[entry.name] or -1) then table.remove(self, i) end end for spot_name, indices in pairs(spots) do for idx = 1, #indices do local internal_idx, spot = self:GetSpot(spot_name, idx) if spot then spot.original_index = indices[idx] spot.annotated_autoattach = GetSpotAnnotation(self.id, indices[idx]) else spot = AutoAttachSpot:new({ name = spot_name, idx = idx, original_index = indices[idx], annotated_autoattach = GetSpotAnnotation(self.id, indices[idx]), }) table.insert(self, spot) end spot.parent = self end end table.sort(self, function(a, b) if a.name < b.name then return true end if a.name > b.name then return false end if a.idx < b.idx then return true end return false end) end if FirstLoad then GedAutoAttachEditorLockedObject = {} GedAutoAttachDemos = {} end DefineClass.AutoAttachPresetDemoObject = { __parents = {"Shapeshifter", "AutoAttachObject"} } AutoAttachPresetDemoObject.ShouldAttach = return_true function AutoAttachPresetDemoObject:ChangeEntity(entity) self:DestroyAutoAttaches() self:ClearAttachMembers() Shapeshifter.ChangeEntity(self, entity) self:DestroyAutoAttaches() self:ClearAttachMembers() AutoAttachShapeshifterObjects(self) end function AutoAttachPresetDemoObject:CreateLightHelpers() self:ForEachAttach(function(attach) if IsKindOf(attach, "Light") then PropertyHelpers_Init(attach) end end) end function AutoAttachPresetDemoObject:AutoAttachObjects() AutoAttachShapeshifterObjects(self) self:CreateLightHelpers() end function AutoAttachPreset:ViewDemoObject(ged) local demo_obj = GedAutoAttachDemos[ged] if demo_obj and IsValid(demo_obj) then ViewObject(demo_obj) end end function AutoAttachPreset:RecreateDemoObject(ged) if CurrentMap == "" then return end if ged and ged.context.lock_preset then local obj = GedAutoAttachEditorLockedObject[ged] obj:DestroyAutoAttaches() obj:ClearAttachMembers() AutoAttachObjects(GedAutoAttachEditorLockedObject[ged], "init") return end local demo_obj = GedAutoAttachDemos[ged] if not demo_obj or not IsValid(demo_obj) then demo_obj = PlaceObject("AutoAttachPresetDemoObject") local look_at = GetTerrainGamepadCursor() look_at = look_at:SetZ(terrain.GetSurfaceHeight(look_at)) demo_obj:SetPos(look_at) end GedAutoAttachDemos[ged] = demo_obj demo_obj:ChangeEntity(self.id) end function OnMsg.GedClosing(ged_id) local demo_obj = GedAutoAttachDemos[GedConnections[ged_id]] DoneObject(demo_obj) GedAutoAttachDemos[GedConnections[ged_id]] = nil end function AutoAttachPreset:OnEditorSelect(selected, ged) if selected then self:UpdateSpotData() self:RecreateDemoObject(ged) end end function AutoAttachPreset:GetError() if not self:GetEntitySpec() then return "Could not find the ArtSpec." end end function AutoAttachPreset:GetEntitySpec() return FindArtSpecById(self.id) end function OnMsg.GedOpened(ged_id) local ged = GedConnections[ged_id] if ged and ged:ResolveObj("root") == Presets.AutoAttachPreset then CreateRealTimeThread(GenerateMissingEntities) end end function OpenAutoattachEditor(objlist, lock_entity) if not IsRealTimeThread() then CreateRealTimeThread(OpenAutoattachEditor, entity) return end lock_entity = not not lock_entity local target_entity if objlist and objlist[1] and IsValid(objlist[1]) then target_entity = objlist[1] end if not target_entity and lock_entity then print("No entity selected.") return end if target_entity then GenerateMissingEntities() -- make sure all entities are generated. Otherwise the selection may fail end local context = AutoAttachPreset:EditorContext() context.lock_preset = lock_entity local ged = OpenPresetEditor("AutoAttachPreset", context) if target_entity then ged:SetSelection("root", PresetGetPath(AutoAttachPresets[target_entity:GetEntity()])) GedAutoAttachEditorLockedObject[ged] = target_entity end end DefineClass.AutoAttachSIModulator = { __parents = {"CObject", "PropertyObject"}, properties = { { id = "SIModulation", editor = "number", default = 100, min = 0, max = 255, slider = true, autoattach_prop = true }, } } function AutoAttachSIModulator:SetSIModulation(value) local parent = self:GetParent() if not parent.SIModulationManual then parent:SetSIModulation(value) end end