|
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 |
|
end |
|
end |
|
|
|
return true |
|
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, |
|
rand_start_time = 0, |
|
distance_bias = 0, |
|
particles_scale_with_object = false, |
|
game_time_animated = false, |
|
vanish = false, |
|
|
|
|
|
|
|
SingleFile = false, |
|
Actions = false, |
|
GlobalMap = "ParticleSystemPresets", |
|
ContainerClass = "ParticleSystemSubItem", |
|
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 |
|
|
|
|
|
local filtered = {} |
|
if filter then |
|
for i = 1, #obj do |
|
local item = obj[i] |
|
|
|
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 |
|
|
|
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 |
|
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 |
|
|
|
end |
|
|
|
ObjModified(Presets.ParticleSystemPreset) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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<color %s %s %s>%s</color>", 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 = {} |
|
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 = {} |
|
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) |
|
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 |
|
|
|
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() |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |