myspace / CommonLua /FXSource.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
18 kB
local Behaviors
local BehaviorsList
MapVar("BehaviorLabels", {})
MapVar("BehaviorLabelsUpdate", {})
MapVar("BehaviorAreaUpdate", sync_set())
----
local function GatherFXSourceTags()
local tags = {}
Msg("GatherFXSourceTags", tags)
ForEachPreset("FXSourcePreset", function(preset, group, tags)
for tag in pairs(preset.Tags) do
tags[tag] = true
end
end, tags)
return table.keys(tags, true)
end
----
function FXSourceUpdate(self, game_state_changed, forced_match, forced_update)
assert(not DisableSoundFX)
if not IsValid(self) or not forced_update and self.update_disabled then
return
end
local preset = self:GetPreset() or empty_table
local fx_event = preset.Event
if fx_event then
local match = forced_match
if match == nil then
match = MatchGameState(self.game_states)
end
if not match then
fx_event = false
end
end
if fx_event and Behaviors then
for name, set in pairs(preset.Behaviors) do
local behavior = Behaviors[name]
if behavior then
local enabled = behavior:IsFXEnabled(self, preset)
if set and not enabled or not set and enabled then
fx_event = false
break
end
end
end
end
local current_fx = self.current_fx
if fx_event == current_fx and not forced_update then
return
end
if current_fx then
PlayFX(current_fx, "end", self)
end
if fx_event then
PlayFX(fx_event, "start", self)
end
self.current_fx = fx_event or nil
if game_state_changed then
if current_fx and not fx_event and preset.PlayOnce then
self.update_disabled = true
end
self:OnGameStateChanged()
end
end
----
DefineClass.FXSourceBehavior = {
__parents = { "PropertyObject" },
id = false,
CreateLabel = false,
LabelUpdateMsg = false,
LabelUpdateDelay = 0,
LabelUpdateDelayStep = 50,
IsFXEnabled = return_true,
}
function FXSourceBehavior:GetEditorView()
return Untranslated(self.id or self.class)
end
function FXSourceUpdateBehaviorLabels()
local now = GameTime()
local labels_to_update = BehaviorLabelsUpdate
local sources_to_update = BehaviorAreaUpdate
if not next(sources_to_update) then
sources_to_update = false
end
local labels = BehaviorLabels
local next_time = max_int64
local pass_edits
local FXSourceUpdate = FXSourceUpdate
for _, name in ipairs(labels_to_update) do
local def = Behaviors[name]
local label = def and labels[name]
if label then
local delay = def.LabelUpdateDelay
local time = labels_to_update[name]
if now < time then
if next_time < time then
next_time = time
end
elseif now <= time + delay then
if not pass_edits then
pass_edits = true
SuspendPassEdits("FXSource")
end
if delay == 0 then
for _, source in ipairs(label) do
FXSourceUpdate(source)
if sources_to_update then
sources_to_update:remove(source)
end
end
else
local step = def.LabelUpdateDelayStep
local steps = 1 + delay / step
local BraidRandom = BraidRandom
local seed = xxhash(name, MapLoadRandom)
for i, source in ipairs(label) do
local delta
delta, seed = BraidRandom(seed, steps)
local time_i = time + delta * step
if now == time_i then
FXSourceUpdate(source)
if sources_to_update then
sources_to_update:remove(source)
end
elseif now < time_i and next_time > time_i then
next_time = time_i
end
end
end
end
end
end
if pass_edits then
ResumePassEdits("FXSource")
end
return (next_time > now and next_time < max_int) and (next_time - now) or nil
end
--ErrorOnMultiCall("FXSourceUpdate")
MapGameTimeRepeat("FXSourceUpdateBehaviorLabels", nil, function()
local sleep = FXSourceUpdateBehaviorLabels()
WaitWakeup(sleep)
end)
function FXSourceUpdateBehaviorLabel(id)
if not BehaviorLabels[id] then
return
end
local list = BehaviorLabelsUpdate
if not list[id] then
list[#list + 1] = id
end
list[id] = GameTime()
WakeupPeriodicRepeatThread("FXSourceUpdateBehaviorLabels")
end
function FXSourceUpdateBehaviorArea()
local sources_to_update = BehaviorAreaUpdate
if not next(sources_to_update) then return end
SuspendPassEdits("FXSource")
local FXSourceUpdate = FXSourceUpdate
for _, source in ipairs(sources_to_update) do
FXSourceUpdate(source)
end
table.clear(sources_to_update, true)
ResumePassEdits("FXSource")
end
function FXSourceUpdateBehaviorAround(id, pos, radius)
local def = Behaviors[id]
local label = def and BehaviorLabels[id]
if not label then
return
end
local list = BehaviorAreaUpdate
MapForEach(pos, radius, "FXSource", function(source, label, list)
if label[source] then
list:insert(source)
end
end, label, list)
if not next(list) then
return
end
WakeupPeriodicRepeatThread("FXSourceUpdateBehaviorArea")
end
MapGameTimeRepeat("FXSourceUpdateBehaviorArea", nil, function()
FXSourceUpdateBehaviorArea()
WaitWakeup()
end)
function OnMsg.ClassesBuilt()
ClassDescendants("FXSourceBehavior", function(class, def)
local id = def.id
if id then
Behaviors = table.create_set(Behaviors, id, def)
assert(not def.LabelUpdateMsg or def.CreateLabel)
if def.CreateLabel and def.LabelUpdateMsg then
OnMsg[def.LabelUpdateMsg] = function()
FXSourceUpdateBehaviorLabel(id)
end
end
end
end)
BehaviorsList = Behaviors and table.keys(Behaviors, true)
end
local function RegisterBehaviors(source, labels, preset)
if not Behaviors then
return
end
preset = preset or source:GetPreset()
if not preset or not preset.Event then
return
end
for name, set in pairs(preset.Behaviors) do
local behavior = Behaviors[name]
if behavior and behavior.CreateLabel then
labels = labels or BehaviorLabels
local label = labels[name]
if not label then
labels[name] = {source, [source] = true}
elseif not label[source] then
label[#label + 1] = source
label[source] = true
end
end
end
end
function FXSourceRebuildLabels()
BehaviorLabels = {}
BehaviorLabelsUpdate = BehaviorLabelsUpdate or {}
MapForEach("map", "FXSource", const.efMarker, RegisterBehaviors, BehaviorLabels)
end
local function UnregisterBehaviors(source, labels)
for name, label in pairs(labels or BehaviorLabels) do
if label[source] then
table.remove_value(label, source)
label[source] = nil
end
end
end
----
DefineClass.FXBehaviorChance = {
__parents = { "FXSourceBehavior" },
id = "Chance",
CreateLabel = true,
properties = {
{ category = "FX: Chance", id = "EnableChance", name = "Chance", editor = "number", default = 100, min = 0, max = 100, scale = "%", slider = true },
{ category = "FX: Chance", id = "ChangeInterval", name = "Change Interval", editor = "number", default = 0, min = 0, scale = function(self) return self.IntervalScale end, help = "Time needed to change the chance result." },
{ category = "FX: Chance", id = "IntervalScale", name = "Interval Scale", editor = "choice", default = false, items = function() return table.keys(const.Scale, true) end },
{ category = "FX: Chance", id = "IsGameTime", name = "Game Time", editor = "bool", default = false, help = "Change interval time type. Game Time is needed for events messing with the game logic." },
},
}
function FXBehaviorChance:IsFXEnabled(source, preset)
local chance = preset and preset.EnableChance or 100
if chance >= 100 then return true end
local time = (preset.IsGameTime and GameTime() or RealTime()) / Max(1, preset.ChangeInterval or 0)
local seed = xxhash(source.handle, time, MapLoadRandom)
return (seed % 100) < chance
end
----
DefineClass.FXSourcePreset = {
__parents = { "Preset" },
properties = {
{ category = "FX", id = "Event", name = "FX Event", editor = "combo", default = false, items = function(fx) return ActionFXClassCombo(fx) end },
{ category = "FX", id = "GameStates", name = "Game State", editor = "set", default = set(), three_state = true, items = function() return GetGameStateFilter() end },
{ category = "FX", id = "PlayOnce", name = "Play Once", editor = "bool", default = false, help = "Kill the object if the FX is no more matched after changing game state", },
{ category = "FX", id = "EditorPlay", name = "Editor Play", editor = "choice", default = "force play", items = {"no change", "force play", "force stop"}, developer = true },
{ category = "FX", id = "Entity", name = "Editor Entity", editor = "combo", default = false, items = function() return GetAllEntitiesCombo() end },
{ category = "FX", id = "Tags", name = "Tags", editor = "set", default = set(), items = GatherFXSourceTags, help = "Help the game logic find this source if needed", },
{ category = "FX", id = "Behaviors", name = "Behaviors", editor = "set", default = set(), items = function() return BehaviorsList end, three_state = true, },
{ category = "FX", id = "ConditionText", name = "Condition", editor = "text", default = "", read_only = true, no_edit = function(self) return not next(self.Behaviors) end },
{ category = "FX", id = "Actor", name = "FX Actor", editor = "combo", default = false, items = function(fx) return ActorFXClassCombo(fx) end},
{ category = "FX", id = "Scale", name = "Scale", editor = "number", default = false },
{ category = "FX", id = "Color", name = "Color", editor = "color", default = false },
{ category = "FX", id = "FXButtons", editor = "buttons", default = false, buttons = {{ name = "Map Select", func = "ActionSelect" }} },
},
GlobalMap = "FXSourcePresets",
EditorMenubarName = "FX Sources",
EditorMenubar = "Editors.Art",
EditorIcon = "CommonAssets/UI/Icons/atoms electron physic.png",
}
function FXSourcePreset:GetConditionText()
local texts = {}
for name, set in pairs(self.Behaviors) do
if set then
texts[#texts + 1] = name
else
texts[#texts + 1] = "not " .. name
end
end
return table.concat(texts, " and ")
end
function FXSourcePreset:ActionSelect()
if GetMap() == "" then
return
end
editor.ClearSel()
editor.AddToSel(MapGet("map", "FXSource", const.efMarker, function(obj, id)
return obj.FxPreset == id
end, self.id))
end
function FXSourcePreset:OnEditorSetProperty(prop_id)
if GetMap() == "" then
return
end
local prop = self:GetPropertyMetadata(prop_id)
if not prop or prop.category ~= "FX" then
return
end
MapForEach("map", "FXSource", const.efMarker, function(obj, self)
if obj.FxPreset == self.id then
obj:SetPreset(self)
end
end, self)
end
function FXSourcePreset:GetProperties()
local orig_props = Preset.GetProperties(self)
local props = orig_props
if Behaviors then
for name, set in pairs(self.Behaviors) do
local classdef = Behaviors[name]
local propsi = classdef and classdef.properties
if propsi and #propsi > 0 then
if props == orig_props then
props = table.icopy(props)
end
props = table.iappend(props, propsi)
end
end
end
return props
end
DefineClass("FXSourceAutoResolve")
DefineClass.FXSource = {
__parents = { "Object", "FXObject", "EditorEntityObject", "EditorCallbackObject", "EditorTextObject", "FXSourceAutoResolve" },
flags = { efMarker = true, efWalkable = false, efCollision = false, efApplyToGrids = false },
editor_text_offset = point(0, 0, -guim),
editor_text_style = "FXSourceText",
editor_entity = "ParticlePlaceholder",
entity = "InvisibleObject",
properties = {
{ category = "FX Source", id = "FxPreset", name = "FX Preset", editor = "preset_id", default = false, preset_class = "FXSourcePreset", buttons = {{ name = "Start", func = "ActionStart" }, { name = "End", func = "ActionEnd" }} },
{ category = "FX Source", id = "Playing", name = "Playing", editor = "bool", default = false, dont_save = true, read_only = true },
},
current_fx = false,
update_disabled = false,
game_states = false,
prefab_no_fade_clamp = true,
}
function FXSource:GetPlaying()
return not not self.current_fx
end
function FXSource:EditorGetText()
return (self.FxPreset or "") ~= "" and self.FxPreset or self.class
end
function FXSource:GetEditorLabel()
local label = self.class
if (self.FxPreset or "") ~= "" then
label = label .. " (" .. self.FxPreset .. ")"
end
return label
end
function FXSource:GetError()
if not self.FxPreset then
return "FX source has no FX preset assigned."
end
end
function FXSource:GameInit()
if ChangingMap then
return -- sound FX are disabled during map changing
end
FXSourceUpdate(self)
end
FXSourceAutoResolve.EditorExit = FXSourceUpdate
function FXSourceAutoResolve:EditorEnter()
CreateRealTimeThread(function()
WaitChangeMapDone()
if GetMap() == "" then
return
end
local match
if IsEditorActive() then
local preset = self:GetPreset() or empty_table
local editor_play = preset.EditorPlay
if editor_play == "force play" then
match = true
elseif editor_play == "force stop" then
match = false
end
end
FXSourceUpdate(self, nil, match)
end)
end
function FXSourceAutoResolve:OnEditorSetProperty(prop_id)
if prop_id == "FxPreset" then
FXSourceUpdate(self, nil, self:GetPlaying(), true)
self:EditorTextUpdate()
end
end
MapVar("FXSourceStates", false)
MapVar("FXSourceUpdateThread", false)
function FXSource:SetGameStates(states)
states = states or false
local prev_states = self.game_states
if prev_states == states then
return
end
local counters = FXSourceStates or {}
FXSourceStates = counters
for state in pairs(states) do
counters[state] = (counters[state] or 0) + 1
end
for state in pairs(prev_states) do
local count = counters[state] or 0
assert(count > 0)
if count > 1 then
counters[state] = count - 1
else
counters[state] = nil
end
end
self.game_states = states or nil
end
function FXSource:OnGameStateChanged()
end
function FXSource:SetFxPreset(id)
if (id or "") == "" then
self.FxPreset = nil
self:SetPreset()
return
end
self.FxPreset = id
self:SetPreset(FXSourcePresets[id])
end
function FXSource:SetPreset(preset)
UnregisterBehaviors(self)
if not preset then
self:SetGameStates(false)
self:ChangeEntity(FXSource.entity)
self.fx_actor_class = nil
self:SetState("idle")
self:SetScale(100)
self:SetColorModifier(const.clrNoModifier)
FXSourceUpdate(self, nil, false)
return
end
RegisterBehaviors(self, nil, preset)
self:SetGameStates(preset.GameStates)
if preset.Entity then
self.editor_entity = preset.Entity
if IsEditorActive() then
self:ChangeEntity(preset.Entity)
end
end
if preset.Actor then
self.fx_actor_class = preset.Actor
end
if preset.State then
self:SetState(preset.State)
end
if preset.Scale then
self:SetScale(preset.Scale)
end
if preset.Color then
self:SetColorModifier(preset.Color)
end
if self.current_fx then
FXSourceUpdate(self, nil, true)
end
end
function FXSource:GetPreset()
return FXSourcePresets[self.FxPreset]
end
function FXSource:Done()
self:SetGameStates(false) -- unregister from FXSourceStates
FXSourceUpdate(self, nil, false)
UnregisterBehaviors(self)
end
function FXSource:ActionStart()
FXSourceUpdate(self, nil, true, true)
ObjModified(self)
end
function FXSource:ActionEnd()
FXSourceUpdate(self, nil, false, true)
ObjModified(self)
end
local function FXSourceUpdateAll(area, ...)
SuspendPassEdits("FXSource")
MapForEach(area, "FXSource", const.efMarker, FXSourceUpdate, ...)
ResumePassEdits("FXSource")
end
function FXSourceUpdateOnGameStateChange(delay)
if GetMap() == "" then
return
end
delay = delay or GameTime() == 0 and 0 or config.MapSoundUpdateDelay or 1000
DeleteThread(FXSourceUpdateThread)
FXSourceUpdateThread = CreateGameTimeThread(function(delay)
if delay <= 0 then
FXSourceUpdateAll("map", "game_state_changed")
else
local boxes = GetMapBoxesCover(config.MapSoundBoxesCoverParts or 8, "MapSoundBoxesCover")
local count = #boxes
for i, box in ipairs(boxes) do
FXSourceUpdateAll(box, "game_state_changed")
Sleep((i + 1) * delay / count - i * delay / count)
end
end
FXSourceUpdateThread = false
end, delay)
end
function OnMsg.ChangeMapDone()
FXSourceUpdateOnGameStateChange()
end
function OnMsg.GameStateChanged(changed)
if ChangingMap or GetMap() == "" then return end
local GameStateDefs, FXSourceStates = GameStateDefs, FXSourceStates
if not FXSourceStates then return end
for id in sorted_pairs(changed) do
if GameStateDefs[id] and (FXSourceStates[id] or 0) > 0 then -- if a game state is changed, update sound sources
FXSourceUpdateOnGameStateChange()
break
end
end
end
if Platform.developer then
local function ReplaceWithSources(objs, fx_src_preset)
if #(objs or "") == 0 then
return 0
end
XEditorUndo:BeginOp{ objects = objs, name = "ReplaceWithFXSource" }
editor.ClearSel()
local sources = {}
for _, obj in ipairs(objs) do
local pos, axis, angle, scale, coll = obj:GetPos(), obj:GetAxis(), obj:GetAngle(), obj:GetScale(), obj:GetCollectionIndex()
DoneObject(obj)
local src = PlaceObject("FXSource")
src:SetGameFlags(const.gofPermanent)
src:SetAxisAngle(axis, angle)
src:SetScale(scale)
src:SetPos(pos)
src:SetCollectionIndex(coll)
src:SetFxPreset(fx_src_preset)
sources[#sources + 1] = src
end
Msg("EditorCallback", "EditorCallbackPlace", sources)
editor.AddToSel(sources)
XEditorUndo:EndOp(sources)
return #sources
end
function ReplaceMapSounds(snd_name, fx_src_preset)
local objs = MapGet("map", "SoundSource", function(obj)
for _, entry in ipairs(obj.Sounds) do
if entry.Sound == snd_name then
return true
end
end
end)
local count = ReplaceWithSources(objs, fx_src_preset)
print(count, "sounds replaced and selected")
end
function ReplaceMapParticles(prtcl_name, fx_src_preset)
local objs = MapGet("map", "ParSystem", function(obj)
return obj:GetParticlesName() == prtcl_name
end)
local count = ReplaceWithSources(objs, fx_src_preset)
print(count, "particles replaced and selected")
end
end