Download CommonLua/Editor/ArtSpecEditor.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 55.7 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Editor/ArtSpecEditor.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/CommonLua/Editor/ArtSpecEditor.lua
-
curl -L -o ArtSpecEditor.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/CommonLua/Editor/ArtSpecEditor.lua
55.7 kB
| FadeCategories = { | |
| ["Auto 50%"] = { min = 50, max = 0 }, | |
| ["Auto 75%"] = { min = 75, max = 0 }, | |
| ["Auto"] = { min = 100, max = 0 }, | |
| ["Auto 150%"] = { min = 150, max = 0 }, | |
| ["Auto 200%"] = { min = 200, max = 0 }, | |
| ["Auto 300%"] = { min = 300, max = 0 }, | |
| ["Auto 400%"] = { min = 400, max = 0 }, | |
| ["Auto 500%"] = { min = 500, max = 0 }, | |
| ["Auto 600%"] = { min = 600, max = 0 }, | |
| ["Max"] = { min = 1000000, max = 1000000, }, | |
| ["Never"] = { min = -1, max = -1, }, | |
| } | |
| local NoCameraCollision = config.NoCameraCollision | |
| if FirstLoad then | |
| EntityValidCharacters = "[%w#_+]" | |
| g_AllEntities = false | |
| end | |
| function OnMsg.BinAssetsLoaded() | |
| g_AllEntities = GetAllEntities() | |
| end | |
| local CommonAssetFirstID = 100000 | |
| function GetAllEntitiesComboItems() | |
| g_AllEntities = g_AllEntities or GetAllEntities() | |
| return table.keys2(g_AllEntities, true, "") | |
| end | |
| local function GetEntitySpecComboItems(except) | |
| local items = {""} | |
| ForEachPreset(EntitySpec, function(spec) | |
| if not except or spec.id ~= except then | |
| items[#items + 1] = spec.id | |
| end | |
| end) | |
| return items | |
| end | |
| DefineClass.AssetSpec = { | |
| __parents = { "InitDone" }, | |
| properties = { | |
| -- stored in max script | |
| { maxScript = true, id = "name", name = "Name", editor = "text", default = "NONE" }, | |
| }, | |
| save_in = "", | |
| TypeColor = false, | |
| EditorView = Untranslated('<ChooseColor><class></color> "<name>"'), | |
| } | |
| function AssetSpec:ChooseColor() | |
| return self.TypeColor and string.format("<color %s %s %s>", GetRGB(self.TypeColor)) or "" | |
| end | |
| function AssetSpec:FindUniqueName(old_name) | |
| local entity_spec = GetParentTableOfKindNoCheck(self, "EntitySpec") | |
| local specs = entity_spec:GetSpecSubitems(self.class, not "inherit", self) -- exclude self | |
| local name, j = old_name, 0 | |
| while specs[name] do | |
| j = j + 1 | |
| name = old_name .. tostring(j) | |
| end | |
| return name | |
| end | |
| function AssetSpec:OnAfterEditorNew(parent, ged, is_paste) | |
| self.name = self:FindUniqueName(self.name) | |
| end | |
| function AssetSpec:OnEditorSetProperty(prop_id, old_value, ged) | |
| local entity_spec = GetParentTableOfKindNoCheck(self, "EntitySpec") | |
| if prop_id == "name" then | |
| -- don't allow a duplicated name among the subobjects | |
| self.name = self:FindUniqueName(self.name) | |
| if self:IsKindOf("MeshSpec") then | |
| -- update references to the old name | |
| for _, spec in pairs(entity_spec:GetSpecSubitems("StateSpec", not "inherit")) do | |
| if spec.mesh == old_value then | |
| spec.mesh = self.name | |
| end | |
| end | |
| end | |
| end | |
| entity_spec:SortSubItems() | |
| ObjModified(entity_spec) | |
| end | |
| function AssetSpec:GetError() | |
| if self.name == "" or self.name == "NONE" then | |
| return "Please specify asset name." | |
| end | |
| if not self.name:match("^[_#a-zA-Z0-9]*$") then | |
| return "The asset name has invalid characters." | |
| end | |
| end | |
| function AssetSpec:SetSaveIn(save_in) | |
| self.save_in = save_in ~= "" and save_in or nil | |
| end | |
| function AssetSpec:GetSaveIn() | |
| return self.save_in | |
| end | |
| DefineClass.MaskSpec = { | |
| __parents = { "AssetSpec" }, | |
| properties = { | |
| -- stored in max script | |
| { maxScript = true, id = "entity", editor = "text", no_edit = true, dont_save = true, default = "" }, | |
| }, | |
| TypeColor = RGB(175, 175, 0), | |
| } | |
| function MaskSpec:Less(other) -- compare to another MaskSpec | |
| if self.entity == other.entity then | |
| return self.name < other.name | |
| end | |
| return self.entity < other.entity | |
| end | |
| DefineClass.MeshSpec = { | |
| __parents = { "AssetSpec" }, | |
| properties = { | |
| -- stored in max script | |
| { maxScript = true, id = "lod" , name = "LOD" , editor = "number", min = 0, default = 1 }, | |
| { maxScript = true, id = "animated" , name = "Animated" , editor = "bool",default = false }, | |
| { maxScript = true, id = "entity", editor = "text", no_edit = true, dont_save = true, default = "" }, | |
| { maxScript = true, id = "material" , name = "Material Variations", editor="text", default = "", help = "Specify material variations separated by commas. No spaces allowed in the variation's name!"}, | |
| { maxScript = true, id = "spots", name = "Required spots", editor = "text", default = "" }, | |
| { maxScript = true, id = "surfaces", name = "Required surfaces", editor = "text", default = "" }, | |
| { maxScript = true, toNumber = true, id = "maxTexturesSize", name = "Max textures size" , editor = "choice", default = "2048", | |
| items = { "2048", "1024", "512" }, | |
| }, | |
| }, | |
| TypeColor = RGB(143, 0, 0), | |
| } | |
| function MeshSpec:GetMaterialsArray() | |
| local str_materials = string.gsub(self.material, " ", "") | |
| return string.tokenize(str_materials, ",") | |
| end | |
| function MeshSpec:Less(other) -- compare to another MeshSpec | |
| if self.entity == other.entity then | |
| if self.name == other.name then | |
| if self.lod == other.lod then | |
| return self.material < other.material | |
| end | |
| return self.lod < other.lod | |
| end | |
| return self.name < other.name | |
| end | |
| return self.entity < other.entity | |
| end | |
| DefineClass.StateSpec = { | |
| __parents = { "AssetSpec" }, | |
| properties = { | |
| { id = "category", name = "Category", editor = "choice", items = function() return ArtSpecConfig.ReturnAnimationCategories end, default = "All" }, | |
| { id = "SaveIn", name = "Save in", editor = "choice", default = "", items = function(obj) return obj:GetPresetSaveLocations() end, }, | |
| { maxScript = true, id = "entity", editor = "text", default = "", no_edit = true, dont_save = true }, | |
| { maxScript = true, id = "mesh", name = "Mesh", editor = "choice", | |
| items = function(self) | |
| local entity_spec = GetParentTableOfKind(self, "EntitySpec") | |
| local meshes = entity_spec:GetSpecSubitems("MeshSpec", "inherit") | |
| return table.keys2(meshes, "sorted", "NONE") | |
| end, | |
| default = "NONE" | |
| }, | |
| { maxScript = true, id = "animated", name = "Animated", editor = "bool", default = false ,read_only = true, dont_save = true,}, | |
| { maxScript = true, id = "looping", name = "Looping", editor = "bool", default = false }, | |
| { maxScript = true, id = "moments", name = "Required moments", editor="text", default = ""}, | |
| }, | |
| TypeColor = RGB(0, 143, 0), | |
| } | |
| function StateSpec:Getanimated() | |
| local entity_spec = GetParentTableOfKind(self, "EntitySpec") | |
| local mesh = entity_spec:GetMeshSpec(self.mesh) | |
| return mesh and mesh.animated | |
| end | |
| function StateSpec:Less(other) -- compare to another StateSpec | |
| if self.entity == other.entity then | |
| return self.name < other.name | |
| end | |
| return self.entity < other.entity | |
| end | |
| function StateSpec:GetError() | |
| if self.mesh == "" or self.mesh == "NONE" then | |
| return "Please specify mesh name." | |
| end | |
| end | |
| function StateSpec:GetPresetSaveLocations() | |
| return GetDefaultSaveLocations() | |
| end | |
| ----- EntitySpec | |
| local editor_artset_no_edit = function(obj) return obj.editor_exclude end | |
| local editor_category_no_edit = function(obj) return obj.editor_exclude end | |
| local editor_subcategory_no_edit = function(obj) return obj.editor_exclude or obj.editor_category == "" or not ArtSpecConfig[obj.editor_category.."Categories"] end | |
| local statuses = { | |
| { id = "Brief", help = "The entity is named, and now concept and technical specs for it need to be prepared." }, | |
| { id = "Ready for production", help = "The brief is done, and work on the entity can start." }, | |
| { id = "In production", help = "The entity is currently being produced in-house or via outsourcing, or has been delivered but not yet exported to the game." }, | |
| { id = "For approval", help = "The entity is produced and exported to the game." }, | |
| { id = "Ready", help = "The entity has been approved and can be used by level designers and programmers." }, | |
| } | |
| local _FadeCategoryComboItems = false | |
| function FadeCategoryComboItems() | |
| if not _FadeCategoryComboItems then | |
| local items = {} | |
| for k,v in pairs(FadeCategories) do | |
| table.insert(items, { value = k, text = k, sort_key = v.min } ) | |
| end | |
| table.sortby_field(items, "sort_key") | |
| _FadeCategoryComboItems = items | |
| end | |
| return _FadeCategoryComboItems | |
| end | |
| function GetArtSpecEditor() | |
| for id, ged in pairs(GedConnections) do | |
| if ged.app_template == EntitySpec.GedEditor then | |
| return ged | |
| end | |
| end | |
| end | |
| DefineClass.BasicEntitySpecProperties = { | |
| __parents = { "PropertyObject" }, | |
| properties = { | |
| { id = "class_parent", name = "Class", editor = "combo", items = PresetsPropCombo("EntitySpec", "class_parent", ""), default = "", category = "Misc", help = "Classes which this entity class should inherit (comma separated).", entitydata = true, }, | |
| { id = "fade_category", name = "Fade category" , editor = "choice", items = FadeCategoryComboItems, default = "Auto", category = "Misc", help = "How the entity should fade away when far from the camera.", entitydata = true, }, | |
| { id = "DetailClass", name = "Detail class", editor = "dropdownlist", category = "Misc", items = {"Essential", "Optional", "Eye Candy"}, default = "Essential", entitydata = true, }, | |
| }, | |
| } | |
| function BasicEntitySpecProperties:ExportEntityDataForSelf() | |
| local entity = {} | |
| for _, prop_meta in ipairs(self:GetProperties()) do | |
| local prop_id = prop_meta.id | |
| if prop_meta.entitydata and not self:IsPropertyDefault(prop_id) then | |
| if type(prop_meta.entitydata) == "function" then | |
| entity[prop_id] = prop_meta.entitydata(prop_meta, self) | |
| else | |
| entity[prop_id] = self[prop_id] | |
| end | |
| end | |
| end | |
| return next(entity) and { entity = entity } or {} | |
| end | |
| DefineClass.EntitySpecProperties = { | |
| __parents = { "BasicEntitySpecProperties" }, | |
| properties = { | |
| { id = "can_be_inherited", name = "Can be inherited", editor = "bool", default = false, category = "Entity Specification"}, | |
| { id = "inherit_entity", name = "Inherit entity", editor = "preset_id", default = "", preset_class = "EntitySpec", category = "Entity Specification", | |
| help = "Entity to inherit meshes/animations from; only entities with 'Can be inherited' checked are listed.", | |
| preset_filter = function(preset, self) return preset.can_be_inherited end, | |
| }, | |
| { id = "material_type", name = "Material type", category = "Misc", editor = "preset_id", default = "", preset_class = "ObjMaterial", help = "Physical material of this entity.", entitydata = true, }, | |
| { id = "on_collision_with_camera", name = "On collision with camera" , editor = "choice", items = { "", "no action", "become transparent", "repulse camera" }, default = "", category = "Misc", help = "Behavior of this entity when colliding with the camera.", entitydata = true, no_edit = NoCameraCollision }, | |
| { id = "wind_axis", name = "Wind trunk stiffness" , editor = "number", default = 800, category = "Misc", scale = 1000, min = 100, max = 10000, slider = true, help = "Vertex noise needs to be set in the entity material to be affected by wind.", entitydata = true, }, | |
| { id = "wind_radial", name = "Wind branch stiffness" , editor = "number", default = 1000, category = "Misc", scale = 1000, min = 500, max = 10000, slider = true, help = "Vertex noise needs to be set in the entity material to be affected by wind.", entitydata = true, }, | |
| { id = "wind_modifier_strength", name = "Wind modifier strength" , editor = "number", default = 1000, category = "Misc", scale = 1000, min = 100, max = 10000, slider = true, help = "Vertex noise needs to be set in the entity material to be affected by wind.", entitydata = true, }, | |
| { id = "wind_modifier_mask", name = "Wind modifier mask" , editor = "choice", default = 0, category = "Misc", items = const.WindModifierMaskComboItems, help = "Vertex noise needs to be set in the entity material to be affected by wind.", entitydata = true, }, | |
| { id = "winds", editor = "buttons", default = false, category = "Misc", buttons = { | |
| { name = "Stop wind", func = function() terrain.SetWindStrength(point20, 0) end }, | |
| { name = "N", func = function() terrain.SetWindStrength(axis_x, 2048) end }, | |
| { name = "N (strong)", func = function() terrain.SetWindStrength(axis_x, 4096) end }, | |
| { name = "E", func = function() terrain.SetWindStrength(axis_y, 2048) end }, | |
| { name = "E (strong)", func = function() terrain.SetWindStrength(axis_y, 4096) end }, | |
| { name = "S", func = function() terrain.SetWindStrength(-axis_x, 2048) end }, | |
| { name = "S (strong)", func = function() terrain.SetWindStrength(-axis_x, 4096) end }, | |
| { name = "W", func = function() terrain.SetWindStrength(-axis_y, 2048) end }, | |
| { name = "W (strong)", func = function() terrain.SetWindStrength(-axis_y, 4096) end }, }, | |
| }, | |
| { id = "DisableCanvasWindBlending", name = "Disable canvas wind blending", category = "Misc", | |
| default = false, editor = "bool", no_edit = function(self) | |
| if not rawget(g_Classes, "Canvas") then return true end | |
| local is_canvas = false | |
| for class in string.gmatch(self.class_parent, '([^,]+)') do | |
| if IsKindOf(g_Classes[class], "Canvas") then | |
| is_canvas = true | |
| break | |
| end | |
| end | |
| return not is_canvas | |
| end, | |
| entitydata = true, | |
| }, | |
| { category = "Defaults", id = "anim_components", name = "Anim components", | |
| editor = "nested_list", default = false, base_class = "AnimComponentWeight", inclusive = true, auto_expand = true, }, | |
| }, | |
| } | |
| function EntitySpecProperties:ExportEntityDataForSelf() | |
| local data = BasicEntitySpecProperties.ExportEntityDataForSelf(self) | |
| if self.anim_components and next(self.anim_components) then | |
| data.anim_components = table.map(self.anim_components, function(ac) | |
| local err, t = LuaCodeToTuple(TableToLuaCode(ac)) | |
| assert(not err) | |
| return t | |
| end) | |
| end | |
| return data | |
| end | |
| DefineClass.EntitySpec = { | |
| __parents = { "Preset", "EntitySpecProperties" }, | |
| properties = { | |
| { id = "produced_by", name = "Produced By" , editor = "combo", default = "HaemimontGames", items = function() return ArtSpecConfig.EntityProducers end, category = "Entity Specification" }, | |
| { id = "status", name = "Production status", editor = "choice", default = statuses[1].id, items = statuses, category = "Entity Specification" }, | |
| { id = "placeholder", name = "Allow placeholder use", editor = "bool", default = false, category = "Entity Specification" }, | |
| { id = "estimate", name = "Estimate (days)", editor = "number", default = 1, category = "Entity Specification" }, | |
| { id = "LastChange", name = "Last change", editor = "text", default = "", translate = false, read_only = true, category = "Entity Specification" }, | |
| -- { id = "inherit_mesh", name = "Inherit mesh", editor = "text", default = "mesh", category = "Entity Specification" }, | |
| -- Tags | |
| { id = "editor_exclude", name = "Exclude from Map Editor", editor = "bool", default = false, category = "Map Editor" }, | |
| { id = "editor_artset", name = "Art set", editor = "text_picker", no_edit = editor_category_no_edit, | |
| items = function() return ArtSpecConfig.ArtSets end, horizontal = true, name_on_top = true, default = "", category = "Map Editor", | |
| }, | |
| { id = "editor_category", name = "Category", editor = "text_picker", no_edit = editor_category_no_edit, | |
| items = function() return ArtSpecConfig.Categories end, horizontal = true, name_on_top = true, default = "", category = "Map Editor", | |
| }, | |
| { id = "editor_subcategory", name = "Subcategory", editor = "text_picker", horizontal = true, name_on_top = true, default = "", category = "Map Editor", | |
| items = function(obj) return ArtSpecConfig[obj.editor_category.."Categories"] or empty_table end, no_edit = editor_subcategory_no_edit, | |
| }, | |
| -- Misc | |
| { id = "HasBillboard", name = "Billboard" , editor = "bool", default = false, category = "Misc", read_only = true, buttons = {{ name = "Rebake", func = "ActionRebake" }} }, | |
| -- stored in max script | |
| { maxScript = true, id = "name", name = "Name", editor = false, default = "NONE", read_only = true, dont_save = true, }, | |
| { maxScript = true, id = "unique_id", name = "UniqueID", editor = "number", default = -1, read_only = true, dont_save = true }, | |
| { maxScript = true, id = "exportableToSVN", name = "Exportable to SVN", editor = "bool", default = true, category = "Entity Specification" }, | |
| { id = "Tools", editor = "buttons", default = false, category = "Entity Specification", buttons = { | |
| { name = "List Files", func = "ListEntityFilesButton" }, | |
| { name = "Delete Files", func = "DeleteEntityFilesButton" }, }, | |
| }, | |
| }, | |
| last_change_time = false, | |
| ContainerClass = "AssetSpec", | |
| GlobalMap = "EntitySpecPresets", | |
| GedEditor = "GedArtSpecEditor", | |
| EditorMenubarName = "Art Spec", | |
| EditorShortcut = "Ctrl-Alt-A", | |
| EditorMenubar = "Editors.Art", | |
| EditorIcon = "CommonAssets/UI/Icons/colour creativity palette.png", | |
| FilterClass = "EntitySpecFilter", | |
| PresetIdRegex = "^" .. EntityValidCharacters .. "*$", | |
| } | |
| function EntitySpec:ExportEntityDataForSelf() | |
| local data = EntitySpecProperties.ExportEntityDataForSelf(self) | |
| if not self.editor_exclude then | |
| data.editor_artset = self.editor_artset ~= "" and self.editor_artset or nil | |
| data.editor_category = self.editor_category ~= "" and self.editor_category or nil | |
| data.editor_subcategory = self.editor_subcategory ~= "" and self.editor_subcategory or nil | |
| end | |
| if self.default_colors then | |
| data.default_colors = {} | |
| SetColorizationNoSetter(data.default_colors, self.default_colors) | |
| end | |
| return data | |
| end | |
| function EntitySpec:GetHasBillboard() | |
| return table.find(hr.BillboardEntities, self.id) | |
| end | |
| function EntitySpec:ActionRebake() | |
| if table.find(hr.BillboardEntities, self.id) then | |
| BakeEntityBillboard(self.id) | |
| end | |
| end | |
| function EntitySpec:Getunique_id() | |
| return EntityIDs and EntityIDs[self.id] or -1 | |
| end | |
| function EntitySpec:Setunique_id() | |
| assert(false) | |
| end | |
| function EntitySpec:GetEditorViewPresetPrefix() | |
| g_AllEntities = g_AllEntities or GetAllEntities() | |
| return g_AllEntities[self.id] and "<color 0 128 0>" or self.exportableToSVN and "" or "<color 128 0 0>" | |
| end | |
| function EntitySpec:GetSaveFolder(save_in) | |
| save_in = save_in or self.save_in | |
| if save_in == "Common" then | |
| return string.format("CommonAssets/Spec/") | |
| else | |
| return string.format("svnAssets/Spec/") | |
| end | |
| end | |
| function OnMsg.ClassesBuilt() | |
| if not next(Presets.EntitySpec) and Platform.developer then | |
| for idx, file in ipairs(io.listfiles("CommonAssets/Spec", "*.lua")) do | |
| LoadPresets(file) | |
| end | |
| for idx, file in ipairs(io.listfiles("svnAssets/Spec", "*.lua")) do | |
| LoadPresets(file) | |
| end | |
| end | |
| end | |
| function EntitySpec:GenerateUniquePresetId(name) | |
| local id = name or self.id | |
| if not EntitySpecPresets[id] then | |
| return id | |
| end | |
| local new_id | |
| local n = 0 | |
| local id1, n1 = id:match("(.*)_(%d+)$") | |
| if id1 and n1 then | |
| id, n = id1, tonumber(n1) | |
| end | |
| repeat | |
| n = n + 1 | |
| new_id = string.format("%s_%02d", id, n) | |
| until not EntitySpecPresets[new_id] | |
| return new_id | |
| end | |
| function EntitySpec:GetSavePath(save_in, group) | |
| save_in = save_in or self.save_in or "" | |
| local folder = self:GetSaveFolder(save_in) | |
| if not folder then return end | |
| if save_in == "" then save_in = "base" end | |
| return string.format("%sArtSpec-%s.lua", folder, save_in) | |
| end | |
| function EntitySpec:GetLastChange() | |
| return self.last_change_time and os.date("%Y-%m-%d %a", self.last_change_time) or "" | |
| end | |
| function EntitySpec:GetCreationTime() -- the time is was marked as Ready in the art spec | |
| return self.status == "Ready" and self.last_change_time | |
| end | |
| function EntitySpec:GetModificationTime() -- the latest modification time as per the file system | |
| self:EditorData().entity_files = self:EditorData().entity_files or GetEntityFiles(self.id) | |
| local max = 0 | |
| for _, file_name in ipairs(self:EditorData().entity_files) do | |
| max = Max(max, GetAssetFileModificationTime(file_name)) | |
| end | |
| return max | |
| end | |
| function EntitySpec:OnEditorSetProperty(prop_id, old_value, ged) | |
| if prop_id == "SaveIn" then | |
| -- reset IDs when switching between project and common | |
| if old_value == "Common" or self.save_in == "Common" then | |
| local old_id = PreviousEntityIDs and PreviousEntityIDs[self.id] or nil | |
| if old_id and self.save_in == "Common" and old_id < CommonAssetFirstID then old_id = nil end | |
| if not next(PreviousEntityIDs) then PreviousEntityIDs = {} end | |
| PreviousEntityIDs[self.id] = EntityIDs[self.id] | |
| EntityIDs[self.id] = old_id | |
| end | |
| elseif prop_id == "status" then | |
| self.last_change_time = os.time(os.date("!*t")) | |
| elseif prop_id == "editor_exclude" then | |
| self.editor_artset = nil | |
| self.editor_category = nil | |
| self.editor_subcategory = nil | |
| elseif prop_id == "editor_category" then | |
| self.editor_subcategory = nil | |
| elseif prop_id == "wind_axis" or prop_id == "wind_radial" or prop_id == "wind_modifier_strength" or prop_id == "wind_modifier_mask" then | |
| local axis, radial, strength, mask = GetEntityWindParams(self.id) | |
| SetEntityWindParams(self.id, -1, self.wind_axis or axis, self.wind_radial or radial, self.wind_modifier_strength or strength, self.wind_modifier_mask or mask) | |
| DelayedCall(300, RecreateRenderObjects) | |
| elseif prop_id == "debris_list" then | |
| local list_item = Presets.DebrisList.Default[self.debris_list] | |
| if list_item then | |
| local classes_weights = list_item.debris_list | |
| self.debris_classes = {} | |
| for _, entry in ipairs(classes_weights) do | |
| local class_weight = DebrisWeight:new{DebrisClass = entry.DebrisClass, Weight = entry.Weight} | |
| table.insert(self.debris_classes, class_weight) | |
| end | |
| else | |
| self.debris_classes = false | |
| end | |
| GedObjectModified(self.debris_classes) | |
| GedObjectModified(self) | |
| elseif prop_id == "debris_classes" then | |
| if not self.debris_classes or #self.debris_classes == 0 then | |
| self.debris_list = "" | |
| self.debris_classes = false | |
| end | |
| end | |
| self:EditorData().entity_files = nil | |
| end | |
| function EntitySpec:SortSubItems() | |
| table.sort(self, function(a, b) if a.class == b.class then return a:Less(b) else return a.class < b.class end end) | |
| end | |
| function EntitySpec:PostLoad() | |
| SetEntityWindParams(self.id, -1, self.wind_axis, self.wind_radial, self.wind_modifier_strength, self.wind_modifier_mask) | |
| self:SortSubItems() | |
| Preset.PostLoad(self) | |
| end | |
| function EntitySpec:GetError() | |
| local has_mesh, has_state | |
| for _, asset_spec in ipairs(self) do | |
| has_mesh = has_mesh or asset_spec.class == "MeshSpec" | |
| has_state = has_state or asset_spec.class == "StateSpec" | |
| end | |
| if not has_mesh then | |
| return "Entity should have a MeshSpec" | |
| elseif not has_state then | |
| return "Entity should have a StateSpec" | |
| end | |
| if (self.editor_artset == "" or not table.find(ArtSpecConfig.ArtSets, self.editor_artset)) and not editor_artset_no_edit(self) then | |
| return "Please specify art set." | |
| elseif (self.editor_category == "" or not table.find(ArtSpecConfig.Categories, self.editor_category)) and not editor_category_no_edit(self) then | |
| return "Please specify entity category." | |
| elseif (self.editor_subcategory == "" or ArtSpecConfig[self.editor_category .. "Categories"] and not table.find(ArtSpecConfig[self.editor_category .. "Categories"], self.editor_subcategory)) and not editor_subcategory_no_edit(self) then | |
| return "Please specify entity subcategory." | |
| end | |
| if self.editor_category == "Decal" and not string.find(self.class_parent, "Decal", 1, true) then | |
| return "This entity is in the Decal category, but does not inherit the Decal class." | |
| end | |
| end | |
| function EntitySpec:Getname() | |
| return self.id | |
| end | |
| function EntitySpec:GetMeshSpec(meshName) | |
| for _, spec in ipairs(self) do | |
| if spec:IsKindOf("MeshSpec") and spec.name == meshName then | |
| return spec | |
| end | |
| end | |
| return false | |
| end | |
| function EntitySpec:OnEditorSelect(selected, ged) | |
| OnArtSpecSelectObject(self, selected) | |
| end | |
| function EntitySpec:ReserveEntityIDs() | |
| ForEachPreset(EntitySpec, function(ent_spec) | |
| local name = ent_spec.id | |
| if not EntityIDs[name] then | |
| if ent_spec.save_in == "Common" then | |
| ReserveCommonEntityID(name) | |
| else | |
| ReserveEntityID(name) | |
| end | |
| end | |
| end) | |
| if not LastEntityID then | |
| LastEntityID = GetUnusedEntityID() - 1 | |
| end | |
| end | |
| function EntitySpec:GetSpecSubitems(spec_type, inherit, exclude) | |
| -- go up the entity spec hierarchy to get inherited states and meshes | |
| local t, es = {}, self | |
| while es do | |
| for _, spec in ipairs(es) do | |
| if spec.class == spec_type and (not exclude or spec ~= exclude) then | |
| t[spec.name] = t[spec.name] or spec | |
| end | |
| end | |
| if not inherit then break end | |
| es = EntitySpecPresets[es.inherit_entity] | |
| end | |
| return t | |
| end | |
| function EntitySpec:SaveSpec(specs_class, filter, fn) | |
| local res = {} | |
| ForEachPreset(EntitySpec, function(ent_spec) | |
| if filter and not filter(ent_spec) then return end | |
| fn(ent_spec.id, ent_spec, res, ent_spec:GetSpecSubitems(specs_class, not "inherit")) | |
| end) | |
| table.sort(res) | |
| return string.format("#(\n\t%s\n)\n", table.concat(res, ",\n\t")) | |
| end | |
| function EntitySpec:SaveEntitySpec(filter) | |
| return self:SaveSpec(nil, filter, function(name, es, res) | |
| local id = EntityIDs[name] or -1 | |
| assert(id > 0, "Entities without ids present at ArtSpec save! Please call a developer!") | |
| res[#res + 1] = string.format('(EntitySpec name:"%s" id:%d exportableToSVN:%s)', | |
| name, | |
| id, | |
| tostring(es.exportableToSVN)) | |
| end) | |
| end | |
| function EntitySpec:SaveMeshSpec(res, filter) | |
| return self:SaveSpec("MeshSpec", filter, function(name, es, res, meshes) | |
| for _, mesh in pairs(meshes) do | |
| local materials = mesh:GetMaterialsArray() | |
| for lod = 1, mesh.lod do | |
| for m = 1, Max(#materials, 1) do | |
| local mat = materials[m] and string.format('material:"%s" ', materials[m]) or "" | |
| local spots = mesh.spots == "" and "" or string.format('spots:"%s" ', mesh.spots) | |
| local surfaces = mesh.surfaces == "" and "" or string.format('surfaces:"%s" ', mesh.surfaces) | |
| res[#res + 1] = string.format('(MeshSpec entity:"%s" name:"%s" lod:%d animated:%s %s%s%smaxTexturesSize:%d)', | |
| name, | |
| mesh.name, | |
| lod, | |
| tostring(mesh.animated), | |
| mat, | |
| spots, | |
| surfaces, | |
| mesh.maxTexturesSize) | |
| end | |
| end | |
| end | |
| end) | |
| end | |
| function EntitySpec:SaveMaskSpec(filter) | |
| return self:SaveSpec("MaskSpec", filter, function(name, es, res, masks) | |
| for _, mask in pairs(masks) do | |
| res[#res + 1] = string.format('(MaskSpec entity:"%s" name:"%s")', | |
| name, | |
| mask.name) | |
| end | |
| end) | |
| end | |
| function EntitySpec:SaveStateSpec(filter) | |
| return self:SaveSpec("StateSpec", filter, function(name, es, res, states) | |
| for _, state in pairs(states) do | |
| local mesh = es:GetMeshSpec(state.mesh) | |
| res[#res + 1] = string.format('(StateSpec entity:"%s" name:"%s" mesh:"%s" animated:%s looping:%s)', | |
| name, | |
| state.name, | |
| mesh.name, | |
| tostring(mesh.animated or false), | |
| tostring(state.looping)) | |
| end | |
| end) | |
| end | |
| function EntitySpec:SaveInheritanceSpec(filter) | |
| return self:SaveSpec(nil, filter, function(name, es, res) | |
| if es.inherit_entity ~= "" and es.inherit_entity ~= es.name then | |
| res[#res + 1] = string.format('(InheritSpec entity:"%s" inherit:"%s" mesh:"%s")', | |
| name, | |
| es.inherit_entity, | |
| "mesh") -- was es.inherit_mesh, this property was unused and removed | |
| end | |
| end) | |
| end | |
| function EntitySpec:ExportMaxScript(folder, file_suffix, filter) | |
| local filename | |
| if file_suffix then | |
| filename = string.format("%s/Spec/ArtSpec.%s.ms", folder, file_suffix) | |
| else | |
| filename = string.format("%s/Spec/ArtSpec.ms", folder) | |
| end | |
| local f,error_msg = io.open(filename, "w+") | |
| if f then | |
| f:write( "struct StateSpec(entity, name, mesh, looping, animated, moments, compensation)\n" ) | |
| f:write( "struct InheritSpec(entity, inherit, mesh)\n" ) | |
| f:write( "struct MeshSpec(entity, name, lod, animated, spots, surfaces, decal, hgShader, dontCompressVerts, maxVerts, maxTris, maxBones, material, sortKey, maxTexturesSize)\n" ) | |
| f:write( "struct EntitySpec(name, id, exportableToSVN)\n" ) | |
| f:write( "struct MaskSpec(entity, name)\n" ) | |
| f:write( "g_maxProjectBoneCount = " .. ArtSpecConfig.maxProjectBoneCount .. "\n" ) | |
| f:write( "g_Platforms = " .. ArtSpecConfig.platforms .. "\n" ) | |
| f:write( "g_EntitySpec = " .. self:SaveEntitySpec(filter) ) | |
| f:write( "g_MeshSpec = " .. self:SaveMeshSpec(nil, filter) ) | |
| f:write( "g_StateSpec = " .. self:SaveStateSpec(filter) ) | |
| f:write( "g_InheritSpec = " .. self:SaveInheritanceSpec(filter) ) | |
| f:write( "g_MaskSpec = " .. self:SaveMaskSpec(filter) ) | |
| f:write("\n") | |
| f:close() | |
| print( "Wrote " .. filename ) | |
| SVNAddFile(filename) | |
| return true | |
| else | |
| print("ERROR: [Save] Could not save " .. filename .. " - " .. error_msg) | |
| return false | |
| end | |
| end | |
| function EntitySpec:ExportDlcLists() | |
| local old = io.listfiles("svnAssets/Spec/", "*.*list") | |
| if next(old) then | |
| local err = AsyncFileDelete(old) | |
| assert(not err, err) | |
| end | |
| local by_dlc = {} | |
| ForEachPreset(EntitySpec, function(entity_data) | |
| local states = entity_data:GetSpecSubitems("StateSpec", not "inherit") | |
| local entity_key = entity_data.save_in | |
| local mesh_names = {} | |
| for mesh_name, mesh_spec in pairs(entity_data:GetSpecSubitems("MeshSpec")) do | |
| for i=1,mesh_spec.lod do | |
| mesh_names[#mesh_names+1] = mesh_name .. (( i == 1 ) and "" or ("." .. tonumber(i-1))) | |
| end | |
| end | |
| for _, state in sorted_pairs(states) do | |
| local state_key = (state.save_in == "") and entity_key or state.save_in | |
| if state_key ~= "Common" then | |
| local file = (state_key == "") and "$(file)" or "Animations/$(file)" | |
| state_key = state_key .. ".statelist" | |
| local list = by_dlc[state_key] or { "return {\n" } | |
| by_dlc[state_key] = list | |
| list[#list + 1] = string.format("\t['$(assets)/Bin/Common/Animations/%s_%s.hgacl'] = '%s',\n", entity_data:Getname(), state.name, file) | |
| end | |
| end | |
| if entity_key == "Common" then return end | |
| local file = (entity_key == "") and "$(file)" or "Meshes/$(file)" | |
| entity_key = entity_key .. ".meshlist" | |
| local list = by_dlc[entity_key] or { "return {\n" } | |
| by_dlc[entity_key] = list | |
| local sorted_list = {} | |
| for _, mesh_name in ipairs(mesh_names) do | |
| sorted_list[#sorted_list + 1] = string.format("\t['$(assets)/Bin/Common/Meshes/%s_%s.hgm'] = '%s',\n", entity_data:Getname(), mesh_name, file) | |
| end | |
| table.sort(sorted_list) | |
| table.iappend(list, sorted_list) | |
| while entity_data do | |
| if entity_data.inherit_entity ~= "" then | |
| local sorted_list = {} | |
| for _, mesh_name in ipairs(mesh_names) do | |
| sorted_list[#sorted_list + 1] = string.format("\t['$(assets)/Bin/Common/Meshes/%s_%s.hgm'] = '%s',\n", entity_data.inherit_entity, mesh_name, file) | |
| end | |
| table.sort(sorted_list) | |
| table.iappend(list, sorted_list) | |
| end | |
| entity_data = EntitySpecPresets[entity_data.inherit_entity] | |
| end | |
| end) | |
| local files_to_save = {} | |
| for save_in, files in pairs(by_dlc) do | |
| files = table.get_unique(files) | |
| files[#files + 1] = "}\n" | |
| local filename = "svnAssets/Spec/" .. save_in | |
| AsyncStringToFile(filename, files) | |
| table.insert(files_to_save, filename) | |
| end | |
| SVNAddFile(files_to_save) | |
| return true | |
| end | |
| function EntitySpec:ExportEntityProducers() | |
| local map = { } | |
| ForEachPreset("EntitySpec", function(preset, group, filters) | |
| if preset.save_in == "Common" then return end | |
| map[preset.id] = preset.produced_by | |
| end) | |
| local content = ValueToLuaCode(map, nil, pstr("return ", 256*1024)) | |
| local path = "svnAssets/Spec/EntityProducers.lua" | |
| AsyncStringToFile(path, content) | |
| SVNAddFile(path) | |
| return true | |
| end | |
| function EntitySpec:ExportEntityData() | |
| local entities_by_dlc = { | |
| ["Common"] = pstr("EntityData = {}\nif Platform.ged then return end\n"), | |
| [""] = pstr("if Platform.ged then return end\n") | |
| } | |
| ForEachPreset(EntitySpec, function(es) | |
| local entity_data = es:ExportEntityDataForSelf() | |
| if next(entity_data) then | |
| local save_in = es.save_in or "" | |
| entities_by_dlc[save_in] = entities_by_dlc[save_in] or pstr("") | |
| local dlc_pstr = entities_by_dlc[save_in] | |
| dlc_pstr:append("EntityData[\"", es.id, "\"] = ") | |
| dlc_pstr:appendt(entity_data) | |
| dlc_pstr:append("\n") | |
| end | |
| end) | |
| for dlc, data in pairs(entities_by_dlc) do | |
| local path | |
| if dlc == "Common" then | |
| path = "CommonLua/_EntityData.generated.lua" | |
| elseif dlc ~= "" then | |
| path = string.format("svnProject/Dlc/%s/Code/_EntityData.generated.lua", dlc) | |
| else | |
| path = "Lua/_EntityData.generated.lua" | |
| end | |
| if #data > 0 then | |
| local err = SaveSVNFile(path, data) | |
| if err then return not err end | |
| else | |
| SVNDeleteFile(path) | |
| end | |
| end | |
| return true | |
| end | |
| function EntitySpec:SaveAll(...) | |
| self:SortPresets() | |
| self:ReserveEntityIDs() | |
| local SaveFailed = function() | |
| print("Export failed") | |
| end | |
| local default_filter = function(es) return es.save_in ~= "Common" end | |
| if not self:ExportMaxScript("svnAssets", nil, default_filter) then SaveFailed() return end --combined file | |
| for i,produced_by in ipairs(ArtSpecConfig.EntityProducers) do | |
| -- separate file per art producer | |
| local producer_filter = function(es) return es.produced_by == produced_by and es.save_in ~= "Common" end | |
| if not self:ExportMaxScript("svnAssets", produced_by, producer_filter) then SaveFailed() return end | |
| end | |
| if not self:ExportEntityData() then SaveFailed() return end | |
| if not self:ExportDlcLists() then SaveFailed() return end | |
| if not self:ExportEntityProducers() then SaveFailed() return end | |
| local common_filter = function(es) return es.save_in == "Common" end | |
| if not self:ExportMaxScript("CommonAssets", nil, common_filter) then SaveFailed() return end | |
| -- force saving ArtSpec-base.lua every time | |
| local base_file_path = "svnAssets/Spec/ArtSpec-base.lua" | |
| local prev_dirty_status = g_PresetDirtySavePaths[base_file_path] | |
| g_PresetDirtySavePaths[base_file_path] = "EntitySpec" | |
| Preset.SaveAll(self, ...) | |
| g_PresetDirtySavePaths[base_file_path] = prev_dirty_status | |
| end | |
| function EntitySpec:OnEditorNew(parent, ged, is_paste) | |
| if not is_paste then | |
| self[1] = MeshSpec:new{ name = "mesh" } | |
| self[2] = StateSpec:new{ name = "idle", mesh = "mesh" } | |
| end | |
| local _, _, base_name, suffix = self.id:find("(.*)_(%d%d)$") | |
| if suffix == "01" and EntitySpecPresets[base_name] then | |
| self:SetId(self:GenerateUniquePresetId(base_name .. "_02")) | |
| end | |
| self.last_change_time = os.time(os.date("!*t")) | |
| end | |
| function EntitySpec:OnEditorDelete(parent, ged) | |
| EntityIDs[self.id] = nil | |
| self:DeleteEntityFiles() | |
| end | |
| function EntitySpec:EditorContext() | |
| local context = Preset.EditorContext(self) | |
| table.remove_value(context.classes, "AssetSpec") | |
| return context | |
| end | |
| function EntitySpec:GetAnimRevision(entity, anim) | |
| if not IsValidEntity(entity) or not HasState(entity, anim) then return 0 end | |
| return GetAssetFileRevision("Animations/" .. GetEntityAnimName(entity, anim)) | |
| end | |
| function EntitySpec:GetEntityFiles(entity) | |
| entity = entity or self.id | |
| local ef_list = GetEntityFiles(entity) | |
| local existing, non_existing = {}, {} | |
| for _, ef in ipairs(ef_list) do | |
| table.insert(io.exists(ef) and existing or non_existing, ef) | |
| end | |
| return existing, non_existing | |
| end | |
| function EntitySpec:ListEntityFilesButton(root, prop_id, ged) | |
| local entity = self.id | |
| local status = not IsValidEntity(entity) and "-> Invalid!" or "" | |
| local existing, non_existing = self:GetEntityFiles(entity) | |
| existing = table.map(existing, ConvertToOSPath) | |
| non_existing = table.map(non_existing, ConvertToOSPath) | |
| local output = {} | |
| table.sort(existing) | |
| table.iappend(output, existing) | |
| if #non_existing > 0 then | |
| output[#output + 1] = "\nMissing, but referenced and/or mandatory files:" | |
| table.sort(non_existing) | |
| table.iappend(output, non_existing) | |
| end | |
| output[#output + 1] = string.format("\nTotal files: %d present and %d non-existent", #existing, #non_existing) | |
| ged:ShowMessage(string.format("Files for entity: '%s' %s", entity, status), table.concat(output, "\n")) | |
| end | |
| function EntitySpec:DeleteEntityFilesButton(root, prop_id, ged) | |
| local result = ged:WaitQuestion("Confirm Deletion", "Delete all exported files for this entity?", "Yes", "No") | |
| if result ~= "ok" then | |
| return | |
| end | |
| CreateRealTimeThread(EntitySpec.DeleteEntityFiles, self) | |
| end | |
| function EntitySpec:DeleteEntityFiles(id) | |
| id = id or self.id | |
| print(string.format("Deleting '%s' entity files...", id)) | |
| local f_existing = self:GetEntityFiles(id) | |
| SVNDeleteFile(f_existing) | |
| print("Done") | |
| end | |
| function GedOpCleanupObsoleteAssets(ged, target, type) | |
| if type == "mappings" then | |
| CreateRealTimeThread(CleanupObsoleteMappingFiles) | |
| else | |
| CreateRealTimeThread(EntitySpec.CleanupObsoleteAssets, EntitySpec, ged) | |
| end | |
| end | |
| if FirstLoad then | |
| CheckEntityUsageThread = false | |
| end | |
| function CheckEntityUsage(ged, obj, selection) | |
| DeleteThread(CheckEntityUsageThread) | |
| CheckEntityUsageThread = CreateRealTimeThread(function() | |
| obj = obj or {} | |
| selection = selection or {} | |
| local art_specs = obj[selection[1][1]] or {} | |
| local selected_specs = selection[2] or {} | |
| local entities = {} | |
| for i, idx in ipairs(selected_specs) do | |
| entities[i] = art_specs[idx].id | |
| end | |
| if #entities == 0 then | |
| entities = table.keys(g_AllEntities or GetAllEntities()) | |
| end | |
| local all_files = {} | |
| local function AddSourceFiles(path) | |
| local err, files = AsyncListFiles(path, "*.lua", "recursive") | |
| if not err then | |
| table.iappend(all_files, files) | |
| end | |
| end | |
| AddSourceFiles("CommonLua") | |
| AddSourceFiles("Lua") | |
| AddSourceFiles("Data") | |
| AddSourceFiles("Dlc") | |
| AddSourceFiles("Maps") | |
| AddSourceFiles("Tools") | |
| AddSourceFiles("svnAssets/Spec") | |
| AddSourceFiles("CommonAssets/Spec") | |
| if #entities == 1 then | |
| print("Search for entity", entities[1], "in", #all_files, "files...") | |
| elseif #entities < 4 then | |
| print("Search for entities", table.concat(entities, ", "), "in", #all_files, "files...") | |
| else | |
| print("Search", #entities, "entities in", #all_files, "files...") | |
| end | |
| Sleep(1) | |
| local string_to_files = SearchStringsInFiles(entities, all_files) | |
| local filename = "AppData/EntityUsage.txt" | |
| local err = AsyncStringToFile(filename, TableToLuaCode(string_to_files)) | |
| if err then | |
| print("Failed to save report:", err) | |
| return | |
| end | |
| print("Report saved to:", ConvertToOSPath(filename)) | |
| OpenTextFileWithEditorOfChoice(filename) | |
| end) | |
| end | |
| function CollectAllReferencedAssets() | |
| local existing_assets = {} | |
| local non_ref_entities = {} | |
| g_AllEntities = g_AllEntities or GetAllEntities() | |
| -- collecting all used assets | |
| for entity_name in pairs(g_AllEntities) do | |
| local entity_specs = GetEntitySpec(entity_name, "expect_missing") | |
| if entity_specs then | |
| local existing = EntitySpec:GetEntityFiles(entity_name) | |
| for _, asset in ipairs(existing) do | |
| local folder = asset:match("(Materials)/") or asset:match("(Animations)/") or asset:match("(Meshes)/") or asset:match("(Textures.*)/") | |
| if folder then | |
| existing_assets[folder] = existing_assets[folder] or {} | |
| local asset_name = asset:match(folder.."/(.*)") | |
| local ref_folder = existing_assets[folder] | |
| ref_folder[asset_name] = "exists" | |
| end | |
| end | |
| else | |
| non_ref_entities[#non_ref_entities + 1] = entity_name | |
| end | |
| end | |
| return existing_assets, non_ref_entities | |
| end | |
| function CleanupObsoleteMappingFiles(existing_assets) | |
| if not CanYield() then | |
| CreateRealTimeThread(CleanupObsoleteMappingFiles, existing_assets) | |
| return | |
| end | |
| if not existing_assets then | |
| existing_assets = CollectAllReferencedAssets() | |
| end | |
| -- drop extensions | |
| local referenced_textures = {} | |
| for asset_name in pairs(existing_assets.Textures) do | |
| local texture_path = string.match(asset_name, "(.+)%.dds$") | |
| if texture_path then | |
| referenced_textures[texture_path] = true | |
| end | |
| end | |
| local err, files = AsyncListFiles("Mapping/", "*.json", "") | |
| if err then | |
| printf("Loading of texture remapping files failed: %s", err) | |
| return | |
| end | |
| local files_removed = 0 | |
| local texture_refs_removed = 0 | |
| parallel_foreach(files, function(file) | |
| file = ConvertToOSPath(file) | |
| local err, content = AsyncFileToString(file) | |
| if err then | |
| printf("Loading of texture mapping file %s failed: %s", file, err) | |
| return | |
| end | |
| local err, obj = JSONToLua(content) | |
| if err then | |
| printf("Loading of texture mapipng file %s failed : %s", file, err) | |
| return | |
| end | |
| local path, name, ext = SplitPath(file) | |
| local entity_id = EntityIDs[name] and tostring(EntityIDs[name]) | |
| local ids = table.keys(obj) | |
| for _, id in ipairs(ids) do | |
| if not referenced_textures[id] or | |
| (entity_id and not string.starts_with(id, entity_id)) then | |
| obj[id] = nil | |
| texture_refs_removed = texture_refs_removed + 1 | |
| end | |
| end | |
| if not next(obj) then | |
| local err = AsyncFileDelete(file) | |
| if err then print("Failed to delete file", file, err) end | |
| files_removed = files_removed + 1 | |
| else | |
| local err, json = LuaToJSON(obj, { pretty = true, sort_keys = true, }) | |
| if err then | |
| printf("Failed to serialize json.") | |
| return | |
| end | |
| local err = AsyncStringToFile(file, json) | |
| if err then print("Failed to write file", file, err) end | |
| end | |
| end) | |
| print("CleanupObsoleteMappingFiles - removed " .. files_removed .. " mapping files and " .. texture_refs_removed .. " texture references") | |
| end | |
| function EntitySpec:CleanupObsoleteAssets(ged) | |
| local result = ged:WaitQuestion("Confirm Deletion", "Cleanup all unreferenced art assets from entitites?", "Yes", "No") | |
| if result ~= "ok" then return end | |
| local existing_assets, non_ref_entities = CollectAllReferencedAssets() | |
| -- delete all non existent entities | |
| for _, name in ipairs(non_ref_entities) do | |
| EntitySpec:DeleteEntityFiles(name) | |
| end | |
| -- checking folders and deleting non used assets | |
| local assets = { | |
| "Materials", | |
| "Animations", | |
| "Meshes", | |
| "Textures", | |
| } | |
| local to_delete = {} | |
| for _, asset_type in ipairs(assets) do | |
| local assets_list = {} | |
| local entity_assets = existing_assets[asset_type] | |
| if asset_type == "Textures" then | |
| local texture_ids = {} | |
| for asset,_ in pairs(entity_assets) do | |
| local id = asset:match("(.*).dds") | |
| texture_ids[id] = "exists" | |
| end | |
| table.iappend(assets_list, io.listfiles("svnAssets/Bin/win32/Textures", "*.dds", "non recursive")) | |
| table.iappend(assets_list, io.listfiles("svnAssets/Bin/win32/Fallbacks/Textures", "*.dds", "non recursive")) | |
| table.iappend(assets_list, io.listfiles("svnAssets/Bin/Common/TexturesMeta", "*.lua", "non recursive")) | |
| for _, asset in ipairs(assets_list) do | |
| local asset_id = asset:match("Textures.*/(%d*)") | |
| if not texture_ids[asset_id] then | |
| table.insert(to_delete, asset) | |
| end | |
| end | |
| else | |
| assets_list = io.listfiles("svnAssets/Bin/Common/" ..asset_type) | |
| for _, asset in ipairs(assets_list) do | |
| local asset_name = asset:match(asset_type..".*/(.*)$") | |
| if not entity_assets[asset_name] then | |
| table.insert(to_delete, asset) | |
| end | |
| end | |
| end | |
| end | |
| print(string.format("Deleted assets count: %d", #to_delete)) | |
| SVNDeleteFile(to_delete) | |
| print("done") | |
| end | |
| function GedOpDeleteEntitySpecs(ged, presets, selection) | |
| local res = ged:WaitQuestion("Confirm Deletion", "Delete the selected entity specs and all exported files?", "Yes", "No") | |
| if res ~= "ok" then return end | |
| return GedOpPresetDelete(ged, presets, selection) | |
| end | |
| function GetEntitySpec(entity, expect_missing) | |
| g_AllEntities = g_AllEntities or GetAllEntities() | |
| if not g_AllEntities[entity] then | |
| assert(expect_missing, string.format("No such entity '%s'!", entity)) | |
| return false | |
| end | |
| local spec = EntitySpecPresets[entity] | |
| assert(spec or expect_missing, string.format("Entity '%s' not found in ArtSpec!", entity)) | |
| return spec | |
| end | |
| function GetStatesFromCategory(entity, category, walked_entities) | |
| if not category or category == "All" then | |
| return GetStates(entity) | |
| end | |
| walked_entities = walked_entities or {} | |
| if not walked_entities[entity] then | |
| walked_entities[entity] = true | |
| else | |
| return {} | |
| end | |
| if not table.find(ArtSpecConfig.ReturnAnimationCategories, category) then | |
| assert(false, string.format("No such animation category - '%s'!", category)) | |
| return GetStates(entity) | |
| end | |
| local entity_spec = EntitySpecPresets[entity] or GetEntitySpec(entity) | |
| if not entity_spec then return {} end | |
| local states = {} | |
| if entity_spec.inherit_entity ~= "" then | |
| local inherited_states = GetStatesFromCategory(entity_spec.inherit_entity, category, walked_entities) | |
| for i = 1, #inherited_states do | |
| if not table.find(states, inherited_states[i]) then | |
| states[#states + 1] = inherited_states[i] | |
| end | |
| end | |
| end | |
| for i = 1, #entity_spec do | |
| local spec = entity_spec[i] | |
| if spec.class == "StateSpec" and spec.category == category then | |
| if not table.find(states, spec.name) then | |
| states[#states + 1] = spec.name | |
| end | |
| end | |
| end | |
| return states | |
| end | |
| function GetStatesFromCategoryCombo(entity, category, ignore_underscore, ignore_error_states) | |
| local IsErrorState, GetStateIdx = IsErrorState, GetStateIdx | |
| local states = {} | |
| for _, state in ipairs(GetStatesFromCategory(entity, category)) do | |
| local is_error_state = IsErrorState(entity, GetStateIdx(state)) | |
| if (not ignore_underscore or not state:starts_with("_")) | |
| and (not ignore_error_states or not is_error_state) | |
| then | |
| if is_error_state then | |
| states[#states + 1] = state .. " *" | |
| else | |
| states[#states + 1] = state | |
| end | |
| end | |
| end | |
| table.sort(states) | |
| return states | |
| end | |
| if FirstLoad then | |
| EntityIDs = false | |
| PreviousEntityIDs = false | |
| LastEntityID = false | |
| LastCommonEntityID = false | |
| end | |
| function EntitySpec:GetSaveData(file_path, preset_list, ...) | |
| local code = Preset.GetSaveData(self, file_path, preset_list, ...) | |
| local save_in = preset_list[1] and preset_list[1].save_in | |
| local initializedIDsObject = false | |
| if save_in == "Common" then | |
| code:appendf("\nLastCommonEntityID = %d\n", LastCommonEntityID) | |
| for name, id in sorted_pairs(EntityIDs) do | |
| if id >= CommonAssetFirstID then | |
| if not initializedIDsObject then | |
| code:append("if not next(EntityIDs) then EntityIDs = {} end \n") | |
| initializedIDsObject = true | |
| end | |
| code:appendf("EntityIDs[\"%s\"] = %d\n", name, id) | |
| end | |
| end | |
| elseif save_in == "" then | |
| code:appendf("\nLastEntityID = %d\n\n", LastEntityID) | |
| for name, id in sorted_pairs(EntityIDs) do | |
| if id < CommonAssetFirstID then | |
| if not initializedIDsObject then | |
| code:append("if not next(EntityIDs) then EntityIDs = {} end \n") | |
| initializedIDsObject = true | |
| end | |
| code:appendf("EntityIDs[\"%s\"] = %d\n", name, id) | |
| end | |
| end | |
| end | |
| return code | |
| end | |
| function ReserveCommonEntityID(entity) | |
| if EntityIDs[entity] then | |
| assert(false, "Entity already has a reserved ID (%d)!", EntityIDs[entity]) | |
| return false | |
| end | |
| local id = GetUnusedCommonEntityID() | |
| if id then | |
| EntityIDs[entity] = id | |
| LastCommonEntityID = id | |
| return id | |
| end | |
| assert(false, "Could not reserve a new Entity ID!") | |
| return false | |
| end | |
| function GetUnusedCommonEntityID() | |
| if not LastCommonEntityID then | |
| local max_id = CommonAssetFirstID | |
| if not next(EntityIDs) then | |
| max_id = CommonAssetFirstID | |
| end | |
| for _, id in pairs(EntityIDs) do | |
| max_id = Max(max_id, id) | |
| end | |
| if max_id >= CommonAssetFirstID then | |
| LastCommonEntityID = max_id | |
| else | |
| assert(false, "GetUnusedCommonEntityID failed!") | |
| return false | |
| end | |
| end | |
| return LastCommonEntityID + 1 | |
| end | |
| function ReserveEntityID(entity) | |
| if EntityIDs[entity] then | |
| assert(false, "Entity already has a reserved ID (%d)!", EntityIDs[entity]) | |
| return false | |
| end | |
| local id = GetUnusedEntityID() | |
| if id then | |
| EntityIDs[entity] = id | |
| LastEntityID = id | |
| return id | |
| end | |
| assert(false, "Could not reserve a new Entity ID!") | |
| return false | |
| end | |
| function GetUnusedEntityID() | |
| if not LastEntityID then | |
| local max_id = -99999 | |
| if not next(EntityIDs) then | |
| max_id = 0 | |
| end | |
| local only_common = true | |
| for _, id in pairs(EntityIDs) do | |
| if id < CommonAssetFirstID then | |
| only_common = false | |
| max_id = Max(max_id, id) | |
| end | |
| end | |
| if only_common then | |
| max_id = 0 | |
| end | |
| if max_id >= 0 then | |
| LastEntityID = max_id | |
| else | |
| assert(false, "GetUnusedEntityID failed!") | |
| return false | |
| end | |
| end | |
| return LastEntityID + 1 | |
| end | |
| function ValidateEntityIDs() | |
| local used_ids, errors = {}, false | |
| for name, id in pairs(EntityIDs) do | |
| if used_ids[id] then | |
| StoreErrorSource(EntitySpecPresets[name], string.format("Duplicated entity id found - '%d' for entities '%s' and '%s'!", id, used_ids[id], name)) | |
| errors = true | |
| else | |
| used_ids[id] = name | |
| end | |
| end | |
| if errors then | |
| OpenVMEViewer() | |
| end | |
| end | |
| function OnMsg.GedOpened(ged_id) | |
| local gedApp = GedConnections[ged_id] | |
| if gedApp and gedApp.app_template == EntitySpec.GedEditor then | |
| ValidateEntityIDs() | |
| end | |
| end | |
| ----- Filtering | |
| DefineClass.EntitySpecFilter = { | |
| __parents = { "GedFilter" }, | |
| properties = { | |
| { id = "Class", editor = "combo", default = "", items = PresetsPropCombo("EntitySpec", "class_parent", "") }, | |
| { id = "NotOfClass", editor = "combo", default = "", items = PresetsPropCombo("EntitySpec", "class_parent", "") }, | |
| { id = "Category", editor = "choice", default = "", items = function() return table.iappend({""}, ArtSpecConfig.Categories) end }, | |
| { id = "produced_by", name = "Produced by", editor = "combo", default = "", items = function() return table.iappend({""}, ArtSpecConfig.EntityProducers) end, }, | |
| { id = "status", name = "Production status", editor = "choice", default = "", items = function() return table.iappend({{id = ""}}, statuses) end }, | |
| { id = "MaterialType", editor = "preset_id", default = "", preset_class = "ObjMaterial" }, | |
| { id = "OnCollisionWithCamera", editor = "choice", items = { "", "no action", "become transparent", "repulse camera" }, default = "", no_edit = NoCameraCollision }, | |
| { id = "fade_category", name = "Fade Category" , editor = "choice", items = FadeCategoryComboItems, default = "", }, | |
| { id = "HasBillboard", name = "Billboard" , editor = "choice", default = "", items = { "", "yes", "no" } }, | |
| { id = "HasCollision", name = "Collision" , editor = "choice", default = "any", items = { "any", "has collision", "has no collision" } }, | |
| { id = "ExportableToSVN", name = "Exportable to SVN", editor = "choice", default = "", items = { "", "true", "false" } }, | |
| { id = "Exported", name = "Is exported", editor = "choice", default = "", items = { "", "yes", "no" } }, | |
| { id = "FilterStateSpecDlc", name = "DLC", editor = "choice", default = false, items = DlcCombo{text = "Any", value = false} }, | |
| { id = "FilterID", name = "ID", editor = "number", default = 0, help = "Find an entity by its unique numeric id." }, | |
| }, | |
| billboard_entities = false, | |
| } | |
| function EntitySpecFilter:Init() | |
| self.ExportableToSVN = "true" | |
| self.billboard_entities = table.invert(hr.BillboardEntities) | |
| end | |
| function EntitySpecFilter:FilterObject(o) | |
| if not IsKindOf(o, "EntitySpec") then | |
| return true | |
| end | |
| if self.Class ~= "" and not string.find_lower(o.class_parent, self.Class) then | |
| return false | |
| end | |
| if self.NotOfClass ~= "" and string.find_lower(o.class_parent, self.NotOfClass) then | |
| return false | |
| end | |
| if self.Category ~= "" and o.editor_category ~= self.Category then | |
| return false | |
| end | |
| if self.produced_by ~= "" and o.produced_by ~= self.produced_by then | |
| return false | |
| end | |
| if self.status ~= "" and o.status ~= self.status then | |
| return false | |
| end | |
| if self.MaterialType ~= "" and o.material_type ~= self.MaterialType then | |
| return false | |
| end | |
| if not NoCameraCollision and self.OnCollisionWithCamera ~= "" and o.on_collision_with_camera ~= self.OnCollisionWithCamera then | |
| return false | |
| end | |
| if self.fade_category ~= "" and o.fade_category ~= self.fade_category then | |
| return false | |
| end | |
| if self.ExportableToSVN ~= "" and o.exportableToSVN ~= (self.ExportableToSVN == "true") then | |
| return false | |
| end | |
| g_AllEntities = g_AllEntities or GetAllEntities() | |
| local exported = g_AllEntities[o.id] | |
| if self.Exported == "yes" and not exported or self.Exported == "no" and exported then | |
| return false | |
| end | |
| if self.HasBillboard == "yes" and not self.billboard_entities[o.id] or self.HasBillboard == "no" and self.billboard_entities[o.id] then | |
| return false | |
| end | |
| if self.HasCollision ~= "any" then | |
| local has_collision = (exported and HasCollisions(o.id)) and "has collision" or "has no collision" | |
| if self.HasCollision ~= has_collision then | |
| return false | |
| end | |
| end | |
| if self.FilterStateSpecDlc ~= false and | |
| ((o.class == "EntitySpec" and o.save_in ~= self.FilterStateSpecDlc) or | |
| (o.class == "StateSpec" and o.save_in ~= self.FilterStateSpecDlc)) then | |
| return false | |
| end | |
| if self.FilterID > 0 and EntityIDs[o.id] ~= self.FilterID then | |
| return false | |
| end | |
| return true | |
| end | |
| function EntitySpecFilter:TryReset(ged, op, to_view) | |
| return false | |
| end | |
| ----- Preview entities from the selected entity specs | |
| if FirstLoad then | |
| ArtSpecEditorPreviewObjects = {} | |
| end | |
| function OnMsg.GedPropertyEdited(ged_id, obj, prop_id, old_value) | |
| local gedApp = GedConnections[ged_id] | |
| if gedApp and gedApp.app_template == EntitySpec.GedEditor then | |
| if prop_id:find("Editable", 1, true) then -- quick way to see whether a colorization property is edited | |
| for _, o in ipairs(ArtSpecEditorPreviewObjects) do | |
| if IsValid(o) then | |
| o:SetColorsFromTable(obj) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| function OnArtSpecSelectObject(entity_spec, selected) | |
| if GetMap() == "" then return end | |
| -- delete old objects | |
| local objs = ArtSpecEditorPreviewObjects | |
| for _, obj in ipairs(objs) do | |
| if IsValid(obj) then | |
| obj:delete() | |
| end | |
| end | |
| table.clear(objs) | |
| if not selected or IsTerrainEntityId(entity_spec.id) then return end | |
| -- create new objects, assign points starting at (0, 0) | |
| local all_names = { entity_spec.id } | |
| local _, _, base_name = entity_spec.id:find("(.*)_%d%d$") | |
| if base_name then | |
| local i = 1 | |
| local name = string.format("%s_%02d", base_name, i) | |
| local names = {} | |
| while EntitySpecPresets[name] do | |
| names[#names + 1] = name | |
| i = i + 1 | |
| name = string.format("%s_%02d", base_name, i) | |
| end | |
| if names[1] then | |
| all_names = names | |
| end | |
| end | |
| local positions | |
| local first_bbox, last_bbox | |
| local direction = Rotate(camera.GetDirection(), 90 * 60):SetZ(0) | |
| for _, name in ipairs(all_names) do | |
| local obj = Shapeshifter:new() | |
| obj:ChangeEntity(name) | |
| obj:ClearEnumFlags(const.efApplyToGrids) | |
| obj:SetColorsByColorizationPaletteName(g_DefaultColorsPalette) | |
| AutoAttachObjects(obj) | |
| obj:SetWarped(true) | |
| local bbox = obj:GetEntityBBox("idle") | |
| if positions then | |
| positions[#positions + 1] = positions[#positions] + SetLen(direction, last_bbox:sizey() / 2 + bbox:sizey() / 2 + 1 * guim) | |
| else | |
| first_bbox = bbox | |
| positions = { point(0, 0) } | |
| end | |
| last_bbox = bbox | |
| objs[#objs + 1] = obj | |
| local text = Text:new() | |
| text:SetText(name) | |
| objs[#objs + 1] = text | |
| end | |
| -- set positions centered at the center of the screen | |
| local angle = CalcOrientation(direction) + 90 * 60 | |
| local central_point = GetTerrainGamepadCursor():SetInvalidZ() - positions[#positions] / 2 | |
| local bottom_point, top_point = GetTerrainGamepadCursor(), GetTerrainGamepadCursor() | |
| for i = 1, #objs / 2 do | |
| local obj = objs[i * 2 - 1] | |
| local bbox = obj:GetEntityBBox("idle") | |
| local pos = positions[i] + central_point | |
| local objPos = pos:SetTerrainZ() - point(0, 0, bbox:minz() + guic / 10) | |
| obj:SetPos(objPos) | |
| obj:SetAngle(angle) | |
| objs[i * 2]:SetPos(pos:SetTerrainZ()) | |
| if objPos:z() - bbox:sizez() < top_point:z() then | |
| top_point = objPos - point(0, 0, bbox:sizez()) | |
| end | |
| end | |
| -- set camera "look at" position to the central point | |
| local ptEye, ptLookAt = GetCamera() | |
| local ptMoveVector = GetTerrainGamepadCursor() - ptLookAt | |
| ptEye, ptLookAt = ptEye + ptMoveVector, ptLookAt + ptMoveVector | |
| SetCamera(ptEye, ptLookAt) | |
| -- measure objects total screen width against screen size, and adjust camera to fit all of them | |
| CreateRealTimeThread(function() | |
| WaitNextFrame(3) | |
| if IsValid(objs[1]) and IsValid(objs[#objs - 1]) then | |
| local _, first_pos = GameToScreen(objs[1] :GetPos() - SetLen(direction, first_bbox:sizey() / 2)) | |
| local _, last_pos = GameToScreen(objs[#objs - 1]:GetPos() + SetLen(direction, last_bbox:sizey() / 2)) | |
| local _, bottom_pos = GameToScreen(bottom_point) | |
| local _, top_pos = GameToScreen(top_point) | |
| local objectsWidth = last_pos:x() - first_pos:x() | |
| local objectsHeight = top_pos:y() - bottom_pos:y() | |
| local w = MulDivRound(UIL.GetScreenSize():x(), 70, 100) | |
| local h = MulDivRound(UIL.GetScreenSize():y(), 25, 100) | |
| if objectsWidth > w or objectsHeight > h then | |
| local backDirection = ptEye - ptLookAt | |
| local len = Max(backDirection:Len() * objectsWidth / w, backDirection:Len() * objectsHeight / h) | |
| SetCamera(ptLookAt + SetLen(backDirection, len), ptLookAt) | |
| end | |
| end | |
| end) | |
| end | |