Download Lua/AutoPlacedSoundSource.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/AutoPlacedSoundSource.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/AutoPlacedSoundSource.lua
-
curl -L -o AutoPlacedSoundSource.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/AutoPlacedSoundSource.lua
18.3 kB
| function EmitterTypeCombo() | |
| local emitters = {""} | |
| for _, group in ipairs(Presets.RuleAutoPlaceSoundSources) do | |
| for _, rule in ipairs(group) do | |
| table.insert_unique(emitters, rule.EmitterType) | |
| end | |
| end | |
| table.sort(emitters) | |
| return emitters | |
| end | |
| DefineClass.AutoPlacedSoundSource = { | |
| __parents = {"SoundSource", "DecorGameStatesFilter"}, | |
| properties = { | |
| {category = "Auto Sound", id = "rule_id", editor = "text", read_only = true, default = ""}, | |
| {category = "Auto Sound", id = "emitter_type", editor = "text", read_only = true, default = ""}, | |
| {category = "Auto Sound", id = "manual", editor = "bool", default = false}, | |
| {id = "ActivationRequiredStates"}, | |
| }, | |
| color_modifier = RGB(255, 255, 255), | |
| editor_text_offset = point(0, 0, 50 * guic), | |
| } | |
| function AutoPlacedSoundSource:Setmanual(manual) | |
| self.manual = manual | |
| if manual then | |
| self.color_modifier = RGB(0, 100, 0) | |
| self:SetColorModifier(self.color_modifier) | |
| self:EditorTextUpdate(true) | |
| end | |
| end | |
| function AutoPlacedSoundSource:EditorGetText(sep, sep2) | |
| sep = sep or "\n " | |
| sep2 = sep2 or " : " | |
| local text = SoundSource.EditorGetText(self) | |
| local sounds = {} | |
| for _, sound in ipairs(self.Sounds) do | |
| local sound_conds = {} | |
| for state, active in pairs(sound.GameStatesFilter) do | |
| if active then | |
| table.insert(sound_conds, state) | |
| end | |
| end | |
| if #sound_conds > 0 then | |
| table.insert(sounds, table.concat({sound.Sound, sep2, table.concat(sound_conds, ",")}, "")) | |
| else | |
| table.insert(sounds, sound.Sound) | |
| end | |
| end | |
| local banks = table.concat(sounds, sep) | |
| return string.format("%s%s%s%s%s", self.manual and "MANUAL" or "AUTO", sep, text, sep, banks) | |
| end | |
| function AutoPlacedSoundSource:EditorGetTextColor() | |
| return MatchGameState(self.ActivationRequiredStates) and DecorGameStatesFilter.EditorGetTextColor(self) or const.clrRed | |
| end | |
| function AutoPlacedSoundSource:CheckUnderground() | |
| if self.manual then | |
| SoundSource.CheckUnderground(self) | |
| else | |
| if self:IsUnderground() then | |
| StoreErrorSource(self, string.format( | |
| "AutoPlacedSoundSource underground - run '%s' rule to replace them!", self.rule_id)) | |
| end | |
| end | |
| end | |
| local manual_override = {"SetPos", "SetAngle", "SetAxis", "SetScale", "SetProperty"} | |
| for _, func_name in ipairs(manual_override) do | |
| AutoPlacedSoundSource[func_name] = function(self, ...) | |
| local dialog = GetDialog("XPlaceObjectTool") | |
| local cursor_obj = dialog and dialog.cursor_object | |
| if not ChangingMap and self ~= cursor_obj and not GameInitThreads[self] then | |
| self:Setmanual(true) | |
| end | |
| SoundSource[func_name](self, ...) | |
| end | |
| end | |
| DefineClass.AutoPlacedSoundSourceWeight = { | |
| __parents = {"PropertyObject"}, | |
| properties = { | |
| {id = "Sound", editor = "preset_id", default = "", preset_class = "SoundPreset" }, | |
| {id = "ActivationRequiredStates", | |
| name = "States Required for Activation", editor = "set", default = set(), | |
| items = function() return GetGameStateFilter() end | |
| }, | |
| {id = "Weight", name = "Weight", editor = "number", default = 10 }, | |
| }, | |
| EditorView = Untranslated("<Sound> (Weight: <Weight>)"), | |
| } | |
| DefineClass.ClassPattern = { | |
| __parents = {"PropertyObject"}, | |
| properties = { | |
| {id = "OriginClass", name = "Origin Class", editor = "text", default = "", | |
| help = "Places the emmiter next to objects of this class" | |
| }, | |
| {id = "OriginSpot", name = "Origin Spot", editor = "text", default = "Origin", | |
| help = "Places the emmiter around that spot" | |
| }, | |
| {id = "OriginOffsetMethod", name = "Origin Offset Method", editor = "dropdownlist", default = "Offset", | |
| items = {"Offset", "Radius"}, help = "Whether to use offset above the spot or sphere around the spot", | |
| }, | |
| {id = "OriginOffset", name = "Origin Offset", editor = "number", default = 0, scale = "m", | |
| no_edit = function(self) return self.OriginOffsetMethod ~= "Offset" end, | |
| help = "Places the emmiter that high above the OriginSpot", | |
| }, | |
| {id = "OriginRadius", name = "Origin Radius", editor = "number", default = 0, | |
| no_edit = function(self) return self.OriginOffsetMethod ~= "Radius" end, | |
| help = "Places the emmiter in the semi-sphere around OriginSpot" | |
| }, | |
| }, | |
| } | |
| function ClassPattern:GetEditorView() | |
| if self.OriginOffsetMethod == "Offset" then | |
| return Untranslated("<OriginClass>: <OriginSpot>(<OriginOffset> above)", self) | |
| else | |
| return Untranslated("<OriginClass>: <OriginSpot>(<OriginRadius> around)", self) | |
| end | |
| end | |
| function ClassPattern:GetError() | |
| local classes = ExpandRuleClasses(self.OriginClass, self.OriginSpot, self.OriginOffsetMethod, self.OriginOffset, self.OriginRadius) | |
| if #classes == 0 then | |
| return string.format("No classes expanded for OriginClass='%s'!", self.OriginClass) | |
| end | |
| end | |
| DefineClass.EmitterTypeClass = { | |
| __parents = {"PropertyObject"}, | |
| properties = { | |
| {id = "EmitterType", name = "Emitter Type", editor = "combo", default = "", | |
| items = EmitterTypeCombo | |
| }, | |
| }, | |
| EditorView = Untranslated("<EmitterType>"), | |
| } | |
| DefineClass.ClassCountAround = { | |
| __parents = {"PropertyObject"}, | |
| properties = { | |
| {id = "Class", name = "Class", editor = "text", default = "" }, | |
| {id = "Radius", name = "Radius", editor = "number", default = 10 * guim, scale = "m", slider = true, | |
| min = 0, max = 100 * guim}, | |
| {id = "CountMin", name = "Count Min", editor = "number", default = 5}, | |
| {id = "CountMax", name = "Count Max", editor = "number", default = 10}, | |
| }, | |
| EditorView = Untranslated("<Class>:<Radius> - [<CountMin>-<CountMax>]"), | |
| } | |
| function ClassCountAround:GetError() | |
| local classes = ExpandRuleClasses(self.Class) | |
| local err | |
| if #classes == 0 then | |
| err = string.format("%s\nNo classes expanded for Class='%s'!", err or "", self.Class) | |
| end | |
| if self.CountMin == 0 and self.CountMax == 0 then | |
| err = string.format("%s\nCountMin(%d) or CountMax(%d) must be greater than zero!", | |
| err or "", self.CountMin, self.CountMax) | |
| end | |
| if self.CountMin > self.CountMax then | |
| err = string.format("%s\n[CountMin-CountMax] must be well defined interval but it's [%d-%d]!", | |
| err or "", self.CountMin, self.CountMax) | |
| end | |
| return err | |
| end | |
| if FirstLoad then | |
| s_ClassPatternExpandedClassesCache = {} | |
| s_APSS_RandSeed = false | |
| end | |
| local xxhash = xxhash | |
| function ExpandRuleClasses(class_pattern, spot_name, spot_offset_type, spot_offset, spot_radius) | |
| local classes = {} | |
| if not class_pattern or class_pattern == "" then | |
| return classes | |
| end | |
| local hash = xxhash(class_pattern, spot_name, spot_offset_type, spot_offset, spot_radius) | |
| local cached = s_ClassPatternExpandedClassesCache[hash] | |
| if cached then | |
| return cached | |
| end | |
| if g_Classes[class_pattern] then | |
| table.insert(classes, class_pattern) | |
| classes[class_pattern] = {spot_name = spot_name, spot_offset_type = spot_offset_type, spot_offset = spot_offset, spot_radius = spot_radius} | |
| else | |
| for class_name in pairs(g_Classes) do | |
| if string.match(class_name, class_pattern) then | |
| table.insert(classes, class_name) | |
| if spot_name or spot_offset_type or spot_offset or spot_radius then | |
| classes[class_name] = {spot_name = spot_name, spot_offset_type = spot_offset_type, spot_offset = spot_offset, spot_radius = spot_radius} | |
| end | |
| end | |
| end | |
| end | |
| s_ClassPatternExpandedClassesCache[hash] = classes | |
| return classes | |
| end | |
| local function GetMarkerDescr(marker, classes) | |
| if classes[marker.class] then | |
| return classes[marker.class] | |
| end | |
| for class_name, class_descr in ipairs(classes) do | |
| if IsKindOf(marker, class_name) then | |
| classes[marker.class] = class_descr | |
| return class_descr | |
| end | |
| end | |
| end | |
| local is_water = terrain.IsWater | |
| -- VERY NAIVE - some areas may not emit sample at all | |
| local function SampleRandomPoints(rule, samples, emitters_away, objs_away, areas, emitters, tries) | |
| tries = tries or 30 | |
| local class_weights, total_weight = {}, 0 | |
| for idx, entry in ipairs(rule.SoundCandidates) do | |
| total_weight = total_weight + entry.Weight | |
| class_weights[idx] = total_weight | |
| end | |
| local terrains | |
| if rule.Terrain then | |
| local terrains_selected = type(rule.Terrain) == "table" and rule.Terrain or {rule.Terrain} | |
| terrains = {} | |
| for _, terrain_type in ipairs(terrains_selected) do | |
| terrains[terrain_type] = true | |
| end | |
| end | |
| local min_dist2 = rule.MinDist * rule.MinDist | |
| local function close_to(sample_pos, samples) | |
| for _, sample in ipairs(samples) do | |
| if sample_pos:Dist2(sample:GetVisualPos()) < min_dist2 then | |
| return true | |
| end | |
| end | |
| end | |
| local function check_req(sample_pos) | |
| if not rule:MatchBorderRelation(sample_pos) then | |
| return false | |
| end | |
| -- check water requirements | |
| if rule.OnWater then | |
| if not is_water(sample_pos) then | |
| return false | |
| end | |
| elseif rule.WaterNearBy > 0 then | |
| if not terrain.IsWaterNearby(sample_pos, rule.WaterNearBy) then | |
| local dist2d2 = rule.WaterNearBy * rule.WaterNearBy | |
| local water_nearby = MapFindNearest(sample_pos, sample_pos, rule.WaterNearBy, "WaterObj", | |
| function (obj, pos) | |
| local closest_pt = ClampPoint(pos, obj:GetObjectBBox()) | |
| return closest_pt:Dist2D2(pos) <= dist2d2 | |
| end, sample_pos) | |
| if not water_nearby then | |
| return false | |
| end | |
| end | |
| end | |
| -- check land requirements | |
| if rule.OnLand then | |
| if is_water(sample_pos) then | |
| return false | |
| end | |
| elseif rule.LandNearBy > 0 then | |
| if not terrain.IsLandNearby(sample_pos, rule.LandNearBy) then | |
| return false | |
| end | |
| end | |
| -- check terrain type underneath | |
| if terrains then | |
| local terrain_preset = TerrainTextures[terrain.GetTerrainType(sample_pos)] | |
| if not terrains[terrain_preset.id] then | |
| return false | |
| end | |
| end | |
| -- check for required objects around | |
| if #(rule.ClassCountAround or empty_table) > 0 then | |
| local at_least_one_around_req_met | |
| for _, around in ipairs(rule.ClassCountAround) do | |
| if around.CountMin > 0 or around.CountMax > 0 then | |
| local classes = ExpandRuleClasses(around.Class) | |
| local classes_around = MapCount(sample_pos, around.Radius, classes) | |
| if classes_around >= around.CountMin and classes_around <= around.CountMax then | |
| at_least_one_around_req_met = true | |
| break | |
| end | |
| end | |
| end | |
| if not at_least_one_around_req_met then | |
| return false | |
| end | |
| end | |
| -- check for samples/emitters/objects around within min distanc | |
| close_to(sample_pos, objs_away) | |
| if close_to(sample_pos, samples) or close_to(sample_pos, emitters_away) or close_to(sample_pos, objs_away) then | |
| return false | |
| end | |
| return true | |
| end | |
| s_APSS_RandSeed = s_APSS_RandSeed or AsyncRand() | |
| local rand = BraidRandomCreate(s_APSS_RandSeed) | |
| local samples_tested = 0 | |
| for _, area in ipairs(areas) do | |
| local randomized = (area.spot_offset_type == "Radius") and not area.water | |
| local area_tries = randomized and tries or 1 | |
| for try = 1, area_tries do | |
| local sample_pos | |
| if randomized then | |
| sample_pos = rule:GetPlacePos(rand, area.spot_pos, area.spot_radius) | |
| else | |
| sample_pos = area.spot_pos + point(0, 0, area.spot_offset) | |
| end | |
| samples_tested = samples_tested + 1 | |
| if check_req(sample_pos) then | |
| local obj = PlaceObject("AutoPlacedSoundSource") | |
| obj:SetPos(sample_pos) | |
| obj.rule_id = rule.id | |
| obj.emitter_type = rule.EmitterType | |
| if rule.EmitterType and rule.EmitterType ~= "" then | |
| emitters[rule.EmitterType] = emitters[rule.EmitterType] or {} | |
| table.insert(emitters[rule.EmitterType], obj) | |
| end | |
| if #(rule.SoundCandidates or empty_table) > 0 then | |
| for s = 1, rule.SoundSamples do | |
| local slot = rand(total_weight) | |
| local idx = GetRandomItemByWeight(class_weights, slot) | |
| local entry = rule.SoundCandidates[idx] | |
| obj:AddSoundsEntry(entry.Sound, nil, entry.ActivationRequiredStates) | |
| end | |
| end | |
| table.insert(samples, obj) | |
| if IsEditorActive() then | |
| obj:EditorEnter() | |
| end | |
| break | |
| end | |
| end | |
| end | |
| return samples_tested | |
| end | |
| function IsCoast(pos, step) | |
| step = step or 10 * guim | |
| local x, y = pos:xy() | |
| local left_point = point(x - step, y) | |
| local right_point = point(x + step, y) | |
| local down_point = point(x, y + step) | |
| local up_point = point(x, y - step) | |
| return is_water(left_point) or is_water(right_point) or is_water(down_point) or is_water(up_point) | |
| end | |
| function IsBeachPoint(pos, step) | |
| return not is_water(pos) and IsCoast(pos, step or 10 * guim) | |
| end | |
| local function GetRuleClassesMarkers(rule) | |
| local classes, markers = {}, {} | |
| if #(rule.ClassPatterns or empty_table) > 0 then | |
| for _, entry in ipairs(rule.ClassPatterns) do | |
| local pattern = ExpandRuleClasses(entry.OriginClass, entry.OriginSpot, entry.OriginOffsetMethod, entry.OriginOffset, entry.OriginRadius) | |
| for k, v in pairs(pattern) do | |
| classes[k] = (type(v) == "table") and table.copy(v) or v | |
| end | |
| end | |
| MapForEach("map", classes, function(obj) | |
| if rule:Filter(obj) then | |
| markers = markers or {} | |
| table.insert(markers, obj) | |
| end | |
| end) | |
| end | |
| if rule.BeachPoints then | |
| local width, height = terrain.GetMapSize() | |
| local step = rule.BeachPointsWaterStep | |
| for y = step, height - step, step do | |
| for x = step, width - step, step do | |
| local pos = point(x, y) | |
| if IsBeachPoint(pos, step) and rule:Filter(pos) then | |
| markers = markers or {} | |
| table.insert(markers, pos) | |
| end | |
| end | |
| end | |
| end | |
| return classes, markers | |
| end | |
| function ProcessAutoPlacedSoundSources(rules, select_new) | |
| local time_total = GetPreciseTicks() | |
| PauseInfiniteLoopDetection("ProcessAutoPlacedSoundSources") | |
| rules = table.ifilter(rules, function(_, rule) | |
| return not rule.Regions or #rule.Regions == 0 or table.find_value(rule.Regions, mapdata.Region) | |
| end) | |
| if #rules == 0 then return end | |
| local to_delete = {} | |
| for _, rule in ipairs(rules) do | |
| if rule.DeleteOld then | |
| to_delete[rule.id] = true | |
| end | |
| end | |
| -- remove old auto placed sound sources and collect manual markers | |
| local manuals, emitters = {}, {} | |
| MapForEach("map", "AutoPlacedSoundSource", function(apss) | |
| if not apss.manual and to_delete[apss.rule_id] then | |
| DoneObject(apss) | |
| else | |
| table.insert(manuals, apss) | |
| if apss.emitter_type and apss.emitter_type ~= "" then | |
| emitters[apss.emitter_type] = emitters[apss.emitter_type] or {} | |
| table.insert(emitters[apss.emitter_type], apss) | |
| end | |
| end | |
| end) | |
| local total_auto, total_manuals, total_samples_tested = 0, 0, 0 | |
| for _, rule in ipairs(rules) do | |
| local time_rule = GetPreciseTicks() | |
| local classes, markers = GetRuleClassesMarkers(rule) | |
| local areas = {} | |
| if #classes > 0 or rule.BeachPoints then | |
| for _, marker in ipairs(markers) do | |
| if IsPoint(marker) then | |
| table.insert(areas, {water = true, spot_pos = marker, spot_offset = 0, spot_radius = 0}) | |
| else | |
| local descr = GetMarkerDescr(marker, classes) | |
| local spot_index = marker:GetSpotBeginIndex(descr.spot_name) | |
| local spot_pos = marker:GetSpotPos(spot_index) | |
| table.insert(areas, {marker = marker, spot_pos = spot_pos, spot_offset_type = descr.spot_offset_type, | |
| spot_offset = descr.spot_offset, spot_radius = descr.spot_radius}) | |
| end | |
| end | |
| end | |
| local samples = {} | |
| for idx, manual in ipairs(manuals) do | |
| samples[idx] = manual | |
| end | |
| local samples_count = #samples | |
| local emitters_away = {} | |
| for _, emitter_away in ipairs(rule.EmittersAway) do | |
| local emitters_type = emitters[emitter_away.EmitterType] or empty_table | |
| for _, emitter in ipairs(emitters_type) do | |
| table.insert(emitters_away, emitter) | |
| end | |
| end | |
| local away_classes = ExpandRuleClasses(rule.OriginAwayClass) | |
| local objs_away = {} | |
| if #away_classes > 0 then | |
| MapForEach("map", away_classes, function(obj) | |
| table.insert(objs_away, obj) | |
| end) | |
| end | |
| local samples_tested = SampleRandomPoints(rule, samples, emitters_away, objs_away, areas, emitters) | |
| time_rule = GetPreciseTicks() - time_rule | |
| print(string.format("Rule %s(%.3fs): Placed %d AutoPlacedSoundSources, Manual Markers: %d, Samples tested: %d", rule.id, time_rule / 1000.0, #samples - samples_count, #manuals, samples_tested)) | |
| total_auto = total_auto + #samples - samples_count | |
| total_manuals = total_manuals + #manuals | |
| total_samples_tested = total_samples_tested + samples_tested | |
| if select_new and IsEditorActive() then | |
| local new_sounds = {} | |
| for i = samples_count + 1, #samples do | |
| local sample = samples[i] | |
| if IsKindOf(sample, "AutoPlacedSoundSource") and sample.rule_id == select_new then | |
| table.insert(new_sounds, samples[i]) | |
| end | |
| end | |
| editor.AddToSel(new_sounds) | |
| end | |
| end | |
| ResumeInfiniteLoopDetection("ProcessAutoPlacedSoundSources") | |
| time_total = GetPreciseTicks() - time_total | |
| print(string.format("All Rules(%.3fs): Placed %d AutoPlacedSoundSources, Manual Markers: %d, Total samples tested: %d", time_total / 1000.0, total_auto, total_manuals, total_samples_tested)) | |
| end | |
| local function GetRules(ged, obj) | |
| if IsKindOf(obj, "GedMultiSelectAdapter") then | |
| local group = obj.__objects[1].group | |
| for _, obj in ipairs(obj.__objects) do | |
| if group ~= obj.group then | |
| ged:ShowMessage("Error", "Selected rules are from diffrent groups - select only single group for execution!") | |
| return | |
| end | |
| end | |
| return Presets.RuleAutoPlaceSoundSources[group] | |
| elseif IsKindOf(obj, "RuleAutoPlaceSoundSources") then | |
| return Presets.RuleAutoPlaceSoundSources[obj.group] | |
| elseif type(obj) == "table" then | |
| return obj | |
| end | |
| end | |
| function RunSingleRule(ged, obj) | |
| local select_new = IsKindOf(obj, "RuleAutoPlaceSoundSources") and obj.id | |
| if select_new and IsEditorActive() then | |
| editor.ClearSel() | |
| end | |
| ProcessAutoPlacedSoundSources(GetRules(ged, obj), select_new) | |
| end | |
| local function RuleSelectedChanged(obj, selected, ged_editor) | |
| if not ged_editor.context or ged_editor.context.PresetClass ~= "RuleAutoPlaceSoundSources" then | |
| return | |
| end | |
| local rules = GetRules(obj) | |
| local selected_rules = {} | |
| for _, rule in ipairs(rules) do | |
| selected_rules[rule.id] = true | |
| end | |
| MapForEach("map", "AutoPlacedSoundSource", function(apss) | |
| if selected then | |
| if selected_rules[apss.rule_id] then | |
| apss:EditorEnter() | |
| end | |
| else | |
| apss:EditorExit() | |
| end | |
| end) | |
| end | |
| function OnMsg.GedClosed(ged_editor) | |
| if ged_editor.context.PresetClass ~= "RuleAutoPlaceSoundSources" then return end | |
| local rules = {} | |
| for _, group in ipairs(Presets.RuleAutoPlaceSoundSources) do | |
| table.iappend(rules, group) | |
| end | |
| RuleSelectedChanged(rules, "selected", ged_editor) | |
| end | |
| OnMsg.GedOnEditorSelect= RuleSelectedChanged | |
| OnMsg.GedOnEditorMultiSelect = RuleSelectedChanged | |