|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
DefineClass.AutoAttachCallback = { |
|
__parents = {"InitDone"}, |
|
} |
|
|
|
function AutoAttachCallback:OnAttachToParent(parent, spot) |
|
end |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
if o:IsForcedLODMinAttach() and o:GetDetailClass() ~= "Essential" then |
|
DoneObject(o) |
|
o = nil |
|
else |
|
local top_parent = GetTopmostParent(o) |
|
ApplyCurrentEnvColorizedToObj(top_parent) |
|
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 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 |
|
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 |
|
|
|
return false |
|
end |
|
|
|
local IsKindOf = IsKindOf |
|
local shapeshifter_class_whitelist = { "Light", "AutoAttachSIModulator", "ParSystem" } |
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
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 = {} |
|
end |
|
|
|
function AutoAttachObjectsToPlacementCursor(obj) |
|
AutoAttachObjects(obj, "placementcursor") |
|
end |
|
|
|
|
|
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 |
|
|
|
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 |
|
item.show_at_placement = true |
|
elseif lw=="placement only" or lw=="placement_only" then |
|
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 |
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 = "<color 168 168 168>" .. str .. "</color>" |
|
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 <color 198 25 198>" .. (self.inherited_values and self.inherited_values["ParticlesName"] or "?") .. "</color>" |
|
else |
|
str = "Attach <color 75 105 198>" .. (self.attach_class or "?") .. "</color>" |
|
end |
|
str = str .. " (" .. self.DetailClass .. ")" |
|
if self.required_state ~= "" then |
|
str = str .. " : <color 20 120 20>" .. self.required_state .. "</color>" |
|
end |
|
if self.attach_class == "" then |
|
str = "<color 168 168 168>" .. str .. "</color>" |
|
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("<Color><name> <idx><opt(u(attach_class), ' - <color 32 192 32>')> <AnnotatedAutoattachMsg>"), |
|
parent = false, |
|
ContainerClass = "AutoAttachRuleBase", |
|
} |
|
|
|
function AutoAttachSpot:Color() |
|
return not self:HasSomethingAttached() and "<color 168 168 168>" 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 "<color 158 22 22>" .. 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() |
|
|
|
|
|
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() |
|
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) |
|
|
|
|
|
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() |
|
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 |