DefineClass.BehaviorFilter = { __parents = { "GedFilter" }, properties = { { id = "bins", name = "Bins", editor = "set", items = { "A", "B", "C", "D", "E", "F", "G", "H" }, max_items_in_set = 1 }, }, bins = set(), } function BehaviorFilter:FilterObject(o) if not self.bins or not IsSet(self.bins) then return false end for bin, value in pairs(self.bins) do if value and o:HasMember("bins") and not o.bins[bin] then return false -- filter item end end return true -- don't filter item end DefineClass.ParticleSystemSubItem = { __parents = { "InitDone" }, EditorName = "Particle System Element", EditorSubmenu = "Other", } DefineClass.ParticleSystemPreset = { __parents = { "Preset" }, properties = { { id = "ui", name = "UI Particle System" , editor = "bool", default = false }, { id = "simul_fps", name = "Simul FPS", editor = "number", slider = true, min = 1, max = 60, help = "The simulation framerate" }, { id = "speed_up", name = "Speed-up", editor = "number", slider = true, min = 10, max = 10000, scale = 1000, help = "How many times the particle simulation is being sped up." }, { id = "presim_time", name = "Presim time", editor = "number", scale = "sec", help = "How many seconds to presimulate, before showing the system for the first time", min = 0, max = 120000, slider = true }, { id = "max_initial_catchup_time", name = "Initial Catchup Time", editor = "number", scale = 1000, help = "How much work should newly created renderobjs do before displaying them. 0 here is equivalent to ignore_game_object_age"}, { id = "rand_start_time", name = "Max starting phase time", editor = "number", scale = "sec", help = "Maximum additional presim time used to randomize the starting phase of the particle system.", min = 0, max = 10000, slider = true }, { id = "distance_bias", name = "Camera distance offset", editor = "number", min = -100000, max = 100000, scale = 1000, help = "How much to offset the particle system relative to the camera. It is used to make the particle system to appear always on top of some other transparent object.", slider = true }, { id = "particles_scale_with_object", name = "Particles scale with object", editor = "bool", help = "Particle size scales with the object scale" }, { id = "game_time_animated", name = "Game time animated", editor = "bool", help = "Will animate in game time, i.e. will pause when the game is paused or slowed down", read_only = function(self) return self.ui end }, { id = "ignore_game_object_age", name = "Ignore GameObject age", editor = "bool", default = false, help = "Long running particles remember when they were created, and try to catch up if they lose their state. Enables/disables this behaviour.", read_only = function(self) return self.ui end }, { id = "vanish", name = "Vanish when killed", editor = "bool", help = "Fx will disappear completely when destroyed, without waiting the existing particles to reach their age" }, { id = "post_lighting", name = "Post Lighting" , editor = "bool", default = false, help = "Render after lighting related post processing." }, { id = "stable_cam_distance", name = "Stable Camera Distance" , editor = "bool", default = false, help = "Render in consistent order while the camera & the particle system are not moving. This has a performance penalty for systems with particles far away from the center." }, { id = "testcode", category = "Custom Test Setup", name = "Test code", editor = "func", params = "self, ged, enabled", default = false, no_edit = function(self) return not self.ui end, }, }, simul_fps = 30, speed_up = 1000, presim_time = 0, max_initial_catchup_time = 2000, -- 0 is a better default, but 2000 is the "old" one, so we don't have to rework parsys rand_start_time = 0, distance_bias = 0, particles_scale_with_object = false, game_time_animated = false, vanish = false, -- Preset settings SingleFile = false, Actions = false, GlobalMap = "ParticleSystemPresets", ContainerClass = "ParticleSystemSubItem", -- can be ParticleBehavior or ParticleParam SubItemFilterClass = "BehaviorFilter", GedEditor = "GedParticleEditor", SingleGedEditorInstance = false, EditorMenubarName = false, EditorMenubar = false, } if FirstLoad then ParticleSystemPreset_FXDetailThreshold = false end function ParticleSystemPreset:GetTextureFolders() return { {"svnAssets/Source/Textures/Particles/"} } end function ParticleSystemPreset:GetTextureBasePath() return "svnAssets/Source/" end function ParticleSystemPreset:GetTextureTargetPath() return "Textures/Particles/" end function ParticleSystemPreset:GetTextureTargetGamePath() return "Textures/Particles" end function ParticleSystemPreset:DynamicParams() return self:EditorData().dynamic_params or false end function ParticleSystemPreset:RefreshThread() return self:EditorData().refresh_thread or false end function ParticleSystemPreset:OverrideEmitterFuncs() end function GedOpOpenParticleEditor(ged, obj, locked) obj:OpenEditor(locked) end function ParticleSystemPreset:OpenEditor(lock_preset) if not IsRealTimeThread() then CreateRealTimeThread(ParticleSystemPreset.OpenEditor, self, lock_preset) return end lock_preset = not not lock_preset local context = ParticleSystemPreset:EditorContext() context.lock_preset = lock_preset local ged = OpenPresetEditor("ParticleSystemPreset", context) ged:SetSelection("root", PresetGetPath(self)) end function GedListParticleSystemBehaviors(obj, filter, format, restrict_class) if not IsKindOf(obj, "ParticleSystemPreset") then return {} end local format = T{format} local objects, ids = {}, {} for i = 1, #obj do local item = obj[i] objects[#objects + 1] = type(item) == "string" and item or _InternalTranslate(format, item, false) ids[#ids + 1] = tostring(item) end -- Do filtering local filtered = {} if filter then for i = 1, #obj do local item = obj[i] -- filter by bins for bin, value in pairs(filter.bins) do if value and item:HasMember("bins") and not item.bins[bin] then filtered[i] = true end end end end objects.filtered = filtered objects.ids = ids return objects end if FirstLoad then l_streams_to_update = false g_ParticleLuaDefsLoaded = false end local load_lua_defs = Platform.developer and Platform.pc and IsFSUnpacked() local particle_editors = {} function OnMsg.GedOpened(ged_id) local gedApp = GedConnections[ged_id] if gedApp and gedApp.app_template == "GedParticleEditor" then hr.TrackParticleTimes = 1 ParticleSystemPreset_FXDetailThreshold = hr.FXDetailThreshold if load_lua_defs and not g_ParticleLuaDefsLoaded then LoadLuaParticleSystemPresets() if Platform.developer then ParticleSystemPreset.TryUpdateKnownInvalidStreams(l_streams_to_update) end end particle_editors[ged_id] = true end end function OnMsg.GedClosing(ged_id) if particle_editors[ged_id] then hr.FXDetailThreshold = ParticleSystemPreset_FXDetailThreshold particle_editors[ged_id] = nil end end if FirstLoad then UIParticlesTestControl = false UIParticlesTestId = false end local function UpdateGedStatus() GedSetUiStatus("select_xcontrol", "Select UI control to attach this particle to.") end function GedTestUIParticle(ged, enabled) if UIParticlesTestControl and UIParticlesTestControl.window_state == "open" and UIParticlesTestControl:HasParticle(UIParticlesTestId) then UIParticlesTestControl:KillParSystem(UIParticlesTestId) UIParticlesTestControl = false UIParticlesTestId = false return end UIParticlesTestControl = false UIParticlesTestId = false local particle_sys = ged:ResolveObj("SelectedPreset") if not particle_sys then XRolloverMode(false) return end if particle_sys.testcode then particle_sys.testcode(particle_sys, ged, enabled) return end if not enabled then XRolloverMode(false) return end if not particle_sys.ui then ged:ShowMessage("Invalid selection", "Select a UI particle first!") XRolloverMode(false) return end GedSetUiStatus("select_xcontrol", "Select UI control to attach this particle to.") XRolloverMode(true, function(window, status) if window and window:IsKindOf("XControl") then XFlashWindow(window) if status == "done" then -- spawn particles UIParticlesTestControl = window UIParticlesTestId = window:AddParSystem(UIParticlesTestId, particle_sys.id, UIParticleInstance:new({ lifetime = -1, })) end end if status == "done" or status == "cancel" then GedSetUiStatus("select_xcontrol") end end) end function GedSetParticleEmitDetail(ged, detail_name) local levels = OptionsData.Options.Effects local idx = table.find(levels, "value", detail_name) assert(idx) EngineOptions.Effects = levels[idx].value Options.ApplyEngineOptions(EngineOptions) local selected_preset = ged:ResolveObj("SelectedPreset") if selected_preset and selected_preset:IsKindOf("ParticleSystemPreset") then if selected_preset.ui then if UIParticlesTestControl and UIParticlesTestControl:HasParticle(UIParticlesTestId) then UIParticlesTestControl:KillParSystem(UIParticlesTestId) end else selected_preset:ResetParSystemInstances() end end print(string.format("Particle detail level '%s' preview set.", levels[idx].value)) local detail_level_names = {"Low", "Medium", "High", "Ultra"} for i = 1, 4 do ged:Send("rfnApp", "SetActionToggled", "Preview" .. detail_level_names[i], detail_level_names[i] == levels[idx].value) end end if FirstLoad then ParticleSystemPresetCommitThread = false end function GedParticleSystemPresetCommit() ParticleSystemPresetCommitThread = IsValidThread(ParticleSystemPresetCommitThread) or CreateRealTimeThread(function() local assets_path = ConvertToOSPath("svnAssets/") local project_path = ConvertToOSPath("svnProject/") local err, exit_code = AsyncExec(string.format("cmd /c %s/Build TexturesParticles-win32", project_path)) if err then assert("Failed to build particle textures" and false) return end local err, exit_code = AsyncExec("cmd /c Build ParticlesSeparateFallbacks", project_path, true, true) if not err and exit_code == 0 then print("Fallbacks updated.") else print("Fallbacks failed to update", err, exit_code) assert("Failed to build particle fallbacks" and false) return end err, exit_code = AsyncExec(string.format("cmd /c TortoiseProc /command:commit /path:%s", assets_path)) if err then assert("Failed to open commit dialog" and false) end end) end function ParticleSystemPreset:GetPresetStatusText() if IsValidThread(UpdateTexturesListThread) or IsValidThread(ParticleSystemPresetCommitThread) then return "Compress tasks in progress..." end return "" end function ParticleSystemPreset:SwitchParam(obj, prop, id) obj:ToggleProperty(prop, self:DynamicParams()) ObjModified(obj) end function ParticleSystemPreset:BindParam(idx, userdata) local param = self[idx] local dp = { index = userdata, type = param.type, default_value = param.default_value } if param.type == "number" then dp.size = 1 elseif param.type == "color" then dp.size = 1 elseif param.type == "point" then dp.size = 3 elseif param.type == "bool" then dp.size = 1 end self:DynamicParams()[param.label] = dp return dp.size end function ParticleSystemPreset:BindParams() local idx = 1 -- start at 1 because custom data [0] is the particle system creation time self:EditorData().dynamic_params = {} for i = 1, #self do if IsKindOf(self[i], "ParticleParam") then idx = idx + self:BindParam(i, idx) if idx > const.CustomDataCount - 1 then print(string.format("warning: parameter %s exceeded the available userdata values!", self[i].label)) end end end end function ParticleSystemPreset:EnableDynamicToggles() for i = 1, #self do if self[i]:IsKindOf("ParticleBehavior") then self[i]:EnableDynamicToggle(self:DynamicParams()) end end end function ParticleSystemPreset:BindParamsAndUpdateProperties() self:BindParams() self:EnableDynamicToggles() end function OnMsg.DataLoading() for _, folder in ipairs(ParticleDirectories()) do LoadingBlacklist[folder] = true end end function LoadLuaParticleSystemPresets() if g_ParticleLuaDefsLoaded then return g_ParticleLuaDefsLoaded end local start_time = GetPreciseTicks() for _, folder in ipairs(ParticleDirectories()) do LoadingBlacklist[folder] = false LoadPresetFolder(folder) end local old_load_lua_defs = load_lua_defs load_lua_defs = false local count = 0 ForEachPreset("ParticleSystemPreset", function(preset) preset:PostLoad() count = count + 1 end) load_lua_defs = old_load_lua_defs if Platform.developer then --print("Loaded", count, "particle lua defs in", GetPreciseTicks() - start_time, "ms"); end ObjModified(Presets.ParticleSystemPreset) -- Replace bins with lua defs in the engine. Log which happened to be different (outdated bins) and in case we want to resave them later local streams_to_update = ParticlesReload(false, false) l_streams_to_update = l_streams_to_update or {} for _, parsys in ipairs(streams_to_update) do local err = ParticleSystemPresets[parsys]:TestStream() if err then GameTestsError("ParSys", parsys, "error: ", err) table.insert(l_streams_to_update, parsys) end end g_ParticleLuaDefsLoaded = true return g_ParticleLuaDefsLoaded end function OnMsg.DataLoaded() local failed_to_load = {} for _, folder in ipairs(ParticleDirectories()) do LoadStreamParticlesFromDir(folder, failed_to_load) end l_streams_to_update = failed_to_load if load_lua_defs then LoadLuaParticleSystemPresets() end end function ParticleSystemPreset:OnEditorNew(parent, ged, is_paste) if load_lua_defs then ParticlesReload(self.id) end g_PresetLastSavePaths[self] = nil if Platform.editor then XEditorUpdateObjectPalette() end end function ParticleSystemPreset:OnEditorSelect(now_selected, ged) if now_selected then self:RefreshBehaviorUsageIndicators() self:BindParamsAndUpdateProperties() ged:Send("rfnApp", "SetIsUIParticle", self.ui) else self:ResetParSystemInstances() end end function BinAssetsUpdateInvalidParticleStreams() ParticleUpdateBinaryStreams() end function ParticleSystemPreset.TryUpdateKnownInvalidStreams(l_streams_to_update) if not l_streams_to_update or #l_streams_to_update == 0 then return end local streams_to_update = table.copy(l_streams_to_update) table.clear(l_streams_to_update) CreateRealTimeThread(function() local changed_outlines = ParticleUpdateOutlines() if #changed_outlines > 0 then print("Saving", #changed_outlines, "particles with modified outlines") for i=1,#changed_outlines do SaveParticleSystem(changed_outlines[i]) end QueueCompressParticleTextures() end if #streams_to_update > 0 then ParticleNameListSaveToStream(streams_to_update) end ParticleUpdateBinaryStreams("create_missing_only") end) end function ParticleSystemPreset:RefreshBehaviorUsageIndicators(do_now) local editor_data = self:EditorData() local refresh_func = function(self) -- Search the behaviors to identify those with disabled emitters. -- Scheduled in a thread to avoid executing simultaneous refresh requests. for i = 1, #self do local behavior = self[i] if behavior:IsKindOf("ParticleBehavior") and not behavior:IsKindOf("ParticleEmitter") then local behavior_bins = behavior.bins local active_emitters = 0 for j = 1, #self do local emitter = self[j] if emitter:IsKindOf("ParticleEmitter") and emitter.enabled then local emitter_bins = emitter.bins for bin, value in pairs(emitter_bins) do if value and behavior_bins[bin] then active_emitters = active_emitters + 1 end end end end local new_active = active_emitters > 0 if new_active ~= behavior.active then behavior.active = new_active ObjModified(self) end local flags = ParticlesGetBehaviorFlags(self.id, i - 1) if flags then local str = "" flags["emitter"] = nil for name, active in sorted_pairs(flags) do if GetDarkModeSetting() then active = not active end local color = active and RGB(74, 74, 74) or RGB(192, 192, 192) local r,g,b = GetRGB(color) str = string.format("%s%s", str, r,g,b, string.sub(name, 1, 1)) end behavior.flags_label = str end end end editor_data.refresh_thread = nil end local refresh_thread = self:RefreshThread() if do_now and not refresh_thread then refresh_func(self) else editor_data.refresh_thread = refresh_thread or CreateRealTimeThread(refresh_func, self) end end function ParticleSystemPreset:PostLoad() Preset.PostLoad(self) if Platform.developer then self:CheckIntegrity() end self:BindParams() if load_lua_defs then ParticlesReload(self.id) end end function ParticleSystemPreset:CheckIntegrity() local count = table.maxn(self) for i = count, 1, -1 do if not rawget(self, i) then assert(false, "Particle system '" .. self.name .. "' initialization failed at: " .. tostring(i)) self[i] = false table.remove(self, i) end end end local function KillParticlesWithName(name) if UIParticlesTestControl and UIParticlesTestControl:GetParticleName(UIParticlesTestId) == name then UIParticlesTestControl = false UIParticlesTestId = false end local xcontrols = GetChildrenOfKind(terminal.desktop, "XControl") for _, control in ipairs(xcontrols) do control:KillParticlesWithName(name) end end function OnMsg.GedPropertyEdited(ged_id, object, prop_id, old_value) if not GedConnections[ged_id] then return end local parent = GetParentTableOfKindNoCheck(object, "ParticleSystemPreset") if object:IsKindOf("ParticleParam") and parent then parent:BindParamsAndUpdateProperties() g_DynamicParamsDefs = {} -- invalidate the cached params ParticlesReload(parent:GetId()) end if object:IsKindOf("ParticleSystemPreset") and prop_id == "Id" then KillParticlesWithName(old_value) ParticlesReload() ObjModified(GedConnections[ged_id]:ResolveObj("root")) if Platform.editor then XEditorUpdateObjectPalette() end elseif (object:IsKindOf("ParticleBehavior") or object:IsKindOf("ParticleSystemPreset")) and parent then parent:RefreshBehaviorUsageIndicators() if object:IsKindOf("ParticleEmitter") and object:IsOutlineProp(prop_id) then object:GenerateOutlines("forced") end ParticlesReload(parent:GetId()) g_DynamicParamsDefs = {} -- invalidate the cached params end end function ParticleSystemPreset:OnEditorSetProperty(prop_id, old_value, ged) if prop_id == "ui" then for _, behaviour in ipairs(self) do if IsKindOf(behaviour, "ParticleEmitter") then behaviour:Setui(self.ui) end end end ParticlesReload(self:GetId()) self:RefreshBehaviorUsageIndicators() if prop_id == "NewBehavior" then ParticleSystemPreset.ActionAddBehavior(ged:ResolveObj("root"), self, prop_id, ged) end Preset.OnEditorSetProperty(self, prop_id, old_value, ged) end function ParticleSystemPreset:ResetParSystemInstances() MapForEach("map", "ParSystem", function(x) if x:GetParticlesName() == self.id then x:SetParticlesName(self.id) -- resets time and flags in GO x:DestroyRenderObj() end end) end function GedResetAllParticleSystemInstances() MapForEach("map", "ParSystem", function(obj) obj:SetParticlesName(obj:GetParticlesName()) obj:DestroyRenderObj() end) end function ParticleSystemPreset:SaveToStream(bin_name, skip_adding_to_svn) bin_name = bin_name or self:GetBinFileName() local id = self:GetId() if bin_name ~= "" then ParticlesReload(self:GetId(), false) local count, err = ParticlesSaveToStream(bin_name, id, true) self:EditorData().stream_error = err if err then printf("\"%s\" while trying to persist \"%s\" in \"%s\"", err, id, bin_name) return false, err end if not skip_adding_to_svn then SVNAddFile(bin_name) end end return bin_name end function ParticleSystemPreset:DeleteStream(bin_name) bin_name = bin_name or self:GetBinFileName() if bin_name ~= "" then SVNDeleteFile(bin_name) end end function ParticleSystemPreset:GetBinFileName(id) local path = self:GetSavePath() if not path or path == "" then return "" end return path:gsub(".lua$", ".bin") end function ParticleSystemPreset:TestStream() local binPath = self:GetBinFileName() if binPath then local id = self.id local err = ParticlesTestStream(binPath, id) self:EditorData().stream_error = err if err then return err end end end function ParticleSystemPreset:GetError() if #self > 55 then return "Too many particle behaviors." elseif #self < 1 then return "There are no particle behaviors. Please add some." end if self.ui then -- TODO: Figure out how this code can go in the ParticleEmitter for _, behavior in ipairs(self) do if IsKindOf(behavior, "ParticleEmitter") then if behavior.softness ~= 0 and behavior.enabled then return "A particle emitter with softness > 0 found. They are not supported in UI particles." end end end end local editor_data = self:EditorData() if editor_data.stream_error then return "Persist error:" .. editor_data.stream_error end end ParticleSystemPreset.ReloadWaitThread = false function ParticleSystemPreset:AddSourceTexturesToSVN() local textures = {} for i = 1, #self do local behavior = self[i] if IsKindOf(behavior, "ParticleEmitter") then if behavior.texture ~= "" then textures[#textures + 1] = "svnAssets/Source/" .. behavior.texture end if behavior.normalmap ~= "" then textures[#textures + 1] = "svnAssets/Source/" .. behavior.normalmap end end end if #textures > 0 then SVNAddFile(textures) end end function ParticleSystemPreset:OnPreSave(user_requested) local file_exists = io.exists(self:GetBinFileName()) local last_save_path = g_PresetLastSavePaths[self] for i = 1, #self do local behavior = self[i] if IsKindOf(behavior, "ParticleEmitter") then behavior:GenerateOutlines() end end self:BindParamsAndUpdateProperties() -- generates the "fake" properties for dynamic params -- remove no longer used bin files. if last_save_path and last_save_path ~= self:GetSavePath() then local old_bin_path = (last_save_path or ""):gsub(".lua", ".bin") if old_bin_path ~= "" then self:DeleteStream(old_bin_path) end end QueueCompressParticleTextures() self:AddSourceTexturesToSVN() self:SaveToStream(false, not "dont_add_to_svn") print(#self, "particle behaviors in", self:GetId(), "saved") ObjModified(self) end function ParticleSystemPreset:OnEditorDelete(...) self:DeleteStream() end function GetParticleBehaviorsCombo() local list = {} ClassDescendants("ParticleBehavior", function(name, class_def, list) if rawget(class_def, "EditorName") then list[#list + 1] = {value = name, text = class_def.EditorName} end end, list) table.sortby_field(list, "text") table.insert(list, 1, {value = "", text = ""}) return list end function ParticleUpdateOutlines() local updated = {} ClearOutlinesCache() local list = GetParticleSystemList() for i = 1,#list do local parsys = list[i] for j = 1, #parsys do local behavior = parsys[j] if IsKindOf(behavior, "ParticleEmitter") then local success, generated = behavior:GenerateOutlines("update") if success then updated[parsys] = true end if generated and CanYield() then Sleep(10) end end end end updated = table.keys(updated) table.sort(updated, function(a, b) return CmpLower(a.id, b.id) end) return updated end function ParticleUpdateBinaryStreams(create_missing_only) if not g_ParticleLuaDefsLoaded then print("ParticleUpdateBinaryStreams: Particle defs not loaded.") return end local existing = {} for _, folder in ipairs(ParticleDirectories()) do local err, files = AsyncListFiles(folder, "*.bin") if err then print("Particle files listing failed:", err) else for i = 1, #files do existing[files[i]] = true end end end local particle_systems = GetParticleSystemList() local streams = {} local to_create = {} for i = 1, #particle_systems do local parsys = particle_systems[i] local stream = parsys:GetBinFileName() streams[stream] = true if not create_missing_only or not existing[stream] then to_create[#to_create + 1] = parsys end end if #to_create > 0 then local created = {} print("Creating", #to_create, "particle streams...") for i = 1, #to_create do local parsys = to_create[i] local stream = parsys:GetBinFileName() local success, err = parsys:SaveToStream(stream, "skip adding to svn") if success then created[#created + 1] = stream end end SVNAddFile(created) print("Created", #created, "/", #to_create, "particle streams.") end --[[ if #updated > 0 then print(#updated, "particle stream(s) saved:") for i=1,#updated do print("\t", i, updated[i]) end end --]] local to_delete = {} for stream in pairs(existing) do if not streams[stream] then to_delete[#to_delete + 1] = stream end end if #to_delete > 0 then print("Deleting", #to_delete, "particle streams...") local result, err = SVNDeleteFile(to_delete) if not result then err = err or "" printf("Failed to delete binary streams! %s", tostring(err)) end end end function ParticleSystemPreset:Getname() return self.id end function LoadStreamParticlesFromDir(dir, failed_to_load) local err, files = AsyncListFiles(dir, "*.bin") if err then print("Particle files listing failed:", err, " directory ", dir) else local start = GetPreciseTicks() local success = 0 local failed_due_to_ver = 0 for i = 1, #files do local err, count = ParticlesLoadFromStream(files[i]) if not err and count ~= 0 then success = success + 1 elseif err == "persist_version" then failed_due_to_ver = failed_due_to_ver + 1 local _, parsys, __ = SplitPath(files[i]) table.insert(failed_to_load, parsys) else print("Particles", files[i], "loading failed!", err) local _, parsys, __ = SplitPath(files[i]) table.insert(failed_to_load, parsys) end end DebugPrint(print_format(success, "/", #files, "particle streams loaded in", GetPreciseTicks() - start, "ms.\n")) if failed_due_to_ver > 0 then print("Particle streams could not be loaded. Using", failed_due_to_ver, "lua descriptions instead. Reason: Persist version mismatch.") end end end function ParticleNameListSaveToStream(streams_to_update) local updated = {} print("Updating", #streams_to_update, "particle streams...") for i = 1, #streams_to_update do local parsys = GetParticleSystem(streams_to_update[i]) if parsys then local success, err = parsys:SaveToStream() if success then updated[#updated + 1] = parsys end end end print("Updated", #updated, "/", #streams_to_update, "particle streams.") end function CheckParticleTextures() local source_path = "svnAssets/Source/Textures/Particles/" local packed_path = "Textures/Particles/" if not io.exists(source_path) then print("You need to checkout source textures for particles.") return {} end local err, rel_paths = AsyncListFiles(source_path, "*", "relative, recursive") local packed_paths = {} table.map(rel_paths, function(rel_path) packed_paths[#packed_paths+1] = packed_path .. rel_path end) local refs = {} local refs_lower = {} local instances = GetParticleSystemList() for i=1, #instances do local parsystem = instances[i] for b=1, #parsystem do local behavior = parsystem[b] if IsKindOf(behavior, "ParticleEmitter") then refs[behavior.texture] = parsystem:GetId() refs[behavior.normalmap] = parsystem:GetId() refs_lower[string.lower(behavior.texture)] = parsystem:GetId() refs_lower[string.lower(behavior.normalmap)] = parsystem:GetId() end end end refs[""] = nil local unref = {} local present = {} local present_lower = {} local missing = {} local wrong_casing = {} for i=1, #packed_paths do local texture = packed_paths[i] local texture_lower = string.lower(texture) present[texture] = true present_lower[texture_lower] = true if not refs[texture] then if refs_lower[texture_lower] then wrong_casing[#wrong_casing+1] = texture else unref[#unref+1] = texture end end end for texture, parsys in pairs(refs) do if not present_lower[string.lower(texture)] then missing[texture] = parsys end end return refs, unref, wrong_casing, missing end if FirstLoad then UpdateTexturesListThread = false end function QueueCompressParticleTextures() if Platform.ged then return end if UpdateTexturesListThread then DeleteThread(UpdateTexturesListThread) UpdateTexturesListThread = false end UpdateTexturesListThread = CreateRealTimeThread(function() Sleep(300) local filepath = "svnProject/Data/ParticleSystemPreset/Textures.txt" local refs, _, wrong_casing, missing = CheckParticleTextures() local idx = {} local full_os_path = ConvertToOSPath("svnAssets/Source/"):gsub("\\", "/") local os_path = string.match(full_os_path, "/([^/]+/Source/)$") for texture, _ in sorted_pairs(refs) do if not missing[texture] and not table.find(wrong_casing, texture) then idx[#idx+1] = texture .. "=" .. os_path .. texture end end AsyncStringToFile(filepath, table.concat(idx, "\r\n")) print("Textures.txt updated") local dir = ConvertToOSPath("svnProject/") local err, exit_code, other = AsyncExec("cmd /c Build TexturesParticles", dir, true, true) if err or exit_code ~= 0 then print("Particles failed to compress", err, exit_code, other) end local err, exit_code = AsyncExec("cmd /c Build ParticlesSeparateFallbacks", dir, true, true) if not err and exit_code == 0 then print("Fallbacks updated.") else print("Fallbacks failed to update", err, exit_code) end UpdateTexturesListThread = false end) end DefineClass.ParticleSystem = { __parents = {"PropertyObject"}, StoreAsTable = false, properties = {{id = 'name', editor = 'text', default = '', },}, simul_fps = 30, speed_up = 1000, presim_time = 0, max_initial_catchup_time = 2000, rand_start_time = 0, distance_bias = 0, particles_scale_with_object = false, game_time_animated = false, vanish = false, } function OnMsg.ClassesGenerate() table.iappend(ParticleSystem.properties, ParticleSystemPreset.properties) end function ParticleSystem:__fromluacode(...) local obj = PropertyObject.__fromluacode(self, ...) local converted = ParticleSystemPreset:new(obj) converted:SetId(obj.name) converted:SetGroup("Default") return converted end