-- [[ -- Author: @iixisii -- Date: 10.21.25 -- reversion: 3.20.26 -- Version: 5.6.1 -- Description: Hide2Show filter for OBS Studio --]] -- [[ Hide2Show 5.6.1 BLOCK ]] -- [[ variable data ]] os = require("os") bit = require("bit") APP = { SHOW = 1; HIDE = 2; QK = 30; VF = 16; NR = 130;SL=500; STEPS = 10;ACT = { ACT1 = "act1";ACT2 = "act2"; ACT3 = "act3" } } on_exit=false local __settings__ = nil local animation_list = { {id = "lr";name = "From left to right";opt_out = { "act1","act2","act3" },ignore_opt = { "anime_rt_list","anime_rt_cst" }},{id = "rl";name = "From right to left";opt_out = { "act1","act2","act3" },ignore_opt = { "anime_rt_list","anime_rt_cst" }}, {id = "tb";name = "From top to bottom";opt_out = { "act1","act2","act3" },ignore_opt = { "anime_rt_list","anime_rt_cst" }},{id = "bt";name = "From bottom to top";opt_out = { "act1","act2","act3" }, ignore_opt = { "anime_rt_list","anime_rt_cst" }}, } local rotation_list = { {id = "rt_90"; name = "90°"}, {id = "rt_180"; name = "180°"}, {id ="rt_360"; name = "360°"}, {id= "cst", name= "Custom"} } -- [[ Functionality ]] function update_api(src, key, value) local api= src.settings.arr("api") local api_event=api.find("key",key) if not api_event or api_event.data == nil then api_event= obs.PairStack() api.insert(api_event.data) end api_event.str("key",key).bul( "value", value ).bul("is_event", true) api_event.free();api.free() return true end function setup(settings) local Hide2Show= obs.script.filter({ name="Hide2Show| v5.6.1", id="hns-filter-iixisii-vs5.6.0", }); local function init_default(src) if not src or not src.item or not src.item.data then return end src.transform= src.item.transform() src.default= { pos= src.item.pos();scale=src.item.scale(); width=src.item.width();height=src.item.height(); } if src.item.isHidden() then src.defAction= APP.HIDE else src.defAction= APP.SHOW end src.initDefault= false local default = obs.PairStack() default.dbl("pos.x", src.default.pos.x).dbl("pos.y", src.default.pos.y).dbl( "scale.x", src.default.scale.x ).dbl("scale.y", src.default.scale.y).dbl("width", src.default.width).dbl( "height", src.default.height ) src.settings.obj("source_default", default.data) return default.free() end local function init_reset(src) if not src or not src.item or not src.item.data then return end src.item.transform(src.transform) if src.defAction == APP.HIDE then src.item.hide() else src.item.show() end src.initReset= true return true end function Hide2Show.setup(src) src.currTime=os.clock() src.action=APP.HIDE src.init=false src.timer=nil src.anime_target="def" src.anime_rt=nil src.anime_rt_value=nil end function Hide2Show.defaults(settings) settings.bul("action_hide_group",true, true) settings.str("hide_interval_type","mis", true) settings.int("hide_time",1, true) settings.bul("hide_time_random",false, true) -- settings.bul("action_show_group",true, true) settings.str("show_interval_type","mis", true) settings.int("show_time",1, true) settings.bul("show_time_random",false, true) -- settings.int("defAction",-1, true) settings.int("anime_velcity_value", 10, true) settings.str("anime_style_list","def", true) -- api local api= obs.ArrayStack() settings.arr("api", api.data, true) api.free() end function Hide2Show.properties(src) local ui= obs.script.create() local ui_action= obs.script.create() -- [[ Hide ]] local hide_time= obs.script.group(ui, "action_hide_group","Hide Time") local hide_interval_type= hide_time.add.options("hide_interval_type","") hide_interval_type.onchange(function(value) obs.script.get("hide_time").suffix(" " .. tostring(value)) return true end) hide_interval_type.add.str( "milliseconds", "mis" ).add.str("seconds", "sc").add.str( "minutes", "ms" ).add.str("hours", "hr") hide_time.add.number(1, 999999999,1, "hide_time","") hide_time.add.bool("hide_time_random","Randomness") -- [[ Show ]] local show_time= obs.script.group(ui, "action_show_group","Show Time") local show_interval_type= show_time.add.options("show_interval_type","") show_interval_type.onchange(function(value) obs.script.get("show_time").suffix(" " .. tostring(value)) return true end) show_interval_type.add.str( "milliseconds", "mis" ).add.str("seconds", "sc").add.str( "minutes", "ms" ).add.str("hours", "hr") show_time.add.number(1, 999999999,1, "show_time","") show_time.add.bool("show_time_random","Randomness") -- [[ Animation ]] local anime= obs.script.group(ui, "animation_group","Animation") -- [[ Animation List ]] -- [[ Animation Style ]] local anime_style_list= anime.add.options("anime_style_list","Animation: ") anime_style_list.onchange(function(value, p, pp, settings) local speed_list=obs.script.get("anime_speed_list") local velcity_input=obs.script.get("anime_velcity_value") local cooldown= obs.script.get("anime_cooldown") if value ~= "def" then if speed_list then speed_list.show() end if velcity_input then velcity_input.show() end if cooldown then cooldown.show() end else if speed_list then speed_list.hide() end if velcity_input then velcity_input.hide() end if cooldown then cooldown.hide() end end return true end) anime_style_list.add.str( "View animations ( optional )","def" ) for _, iter in pairs(animation_list) do if iter and iter.opt_out ~= nil then local isvalid = true for _, itemValue in pairs(iter.opt_out) do if itemValue == value then isvalid = false break end end if isvalid then anime_style_list.add.str(iter.name, iter.id) if iter.id == current_anime then is_listed = true end end else anime_style_list.add.str(iter.name, iter.id) if iter.id == current_anime then is_listed = true end end end -- [[ Animation Rotation ]] -- [[ Custom Rotation ]] -- [[ Animation Speed ]] local anime_speed_list= anime.add.options("anime_speed_list","Interval: ") anime_speed_list.add.str( "Normal ( system default )", "NR" ).add.str("Quick","QK").add.str( "Very fast","VF" ).add.str("Slow", "SL").hide() -- [[ Animation Velocity ]] local anime_velcity_value= anime.add.number( 1, 1000,1, "anime_velcity_value","Velocity: ", obs.enum.number.int,obs.enum.number.slider ).hide() -- [[ ANIMATION COOLDOWN ]] local anime_cooldown= anime.add.number(0,99999999, 1, "anime_cooldown","Cooldown(ms):").hide() if src.settings.get_str("anime_style_list") and src.settings.get_str("anime_style_list") ~= "def" and src.settings.get_str("anime_style_list") ~= "" then anime_speed_list.show() anime_velcity_value.show() anime_cooldown.show() else anime_speed_list.hide() anime_velcity_value.hide() anime_cooldown.hide() end return ui end function Hide2Show.update(src) if src.action == nil then src.action= APP.HIDE end src.currTime= os.clock() src.time2 = src.settings.get_int("hide_time") src.rnd2 = src.settings.get_bul("hide_time_random") local hide_time_type= src.settings.str("hide_interval_type") if hide_time_type == "sc" then src.time2 = src.time2 * 1000 elseif hide_time_type == "ms" then src.time2= src.time2 * 60000 elseif hide_time_type == "hr" then src.time2= src.time2 * 3600000 end -- src.time1 = src.settings.get_int("show_time") src.rnd1 = src.settings.get_bul("show_time_random") local show_time_type= src.settings.str("show_interval_type") if show_time_type == "sc" then src.time1 = src.time1 * 1000 elseif show_time_type == "ms" then src.time1= src.time1 * 60000 elseif show_time_type == "hr" then src.time1= src.time1 * 3600000 end -- src.cooldown= src.settings.get_int("anime_cooldown") or 0 if type(src.cooldown) ~= "number" or src.cooldown < 0 then src.cooldown= 0 end src.anime_steps = src.settings.get_int("anime_velcity_value") src.anime = src.settings.get_str("anime_style_list") src.anime_time = src.settings.get_str("anime_speed_list") end function Hide2Show.finally(src) local scene_item= obs.front.source(src.source) if scene_item and scene_item.data then src.item= scene_item local defaults= src.settings.get_obj("source_default") if not defaults or not defaults.data or (defaults.dbl("width") == nil or defaults.dbl("width") <= 0) then init_default(src) else src.transform= src.item.transform() if src.item.isHidden() then src.defAction= APP.HIDE else src.defAction= APP.SHOW end src.default= { pos= { x= defaults.dbl("pos.x"), y= defaults.dbl("pos.y"), },scale= { x= defaults.dbl("scale.x"), y= defaults.dbl("scale.y") }, width= defaults.dbl("width"), height= defaults.dbl("height") } end if defaults.data then defaults.free() end Hide2Show.update(src) end end function Hide2Show.video_tick(src) if obs.utils.script_shutdown then return end if not src.isInitialized or not src.item or not src.item.data then return end if not src.filter or not obslua.obs_source_enabled(src.filter) then if not obslua.obs_source_enabled(src.filter) then src.initDefault= true if not src.initReset then init_reset(src) end end src.isLoading= false return end if src.isLoading then return end if src.initDefault then init_default(src) src.initReset= false end local vtime= APP.SL;if src.action == APP.HIDE then if src.rnd2 == true then vtime= math.random(1, src.time2) else vtime= src.time2 end else if src.rnd1 == true then vtime= math.random(1, src.time1) else vtime= src.time1 end end if src.isFromAnimation and(src.cooldown and src.cooldown > 0) then src.isFromAnimation=false src.isLoading=true return obs.time.schedule(function() src.isLoading=false; src.isFromAnimation=false end).after(src.cooldown) end src.isLoading= true;obs.time.schedule(function() if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading=false return end local function execute() if src.action == APP.HIDE then src.action= APP.SHOW src.isLoading= false return src.item.hide() else src.action= APP.HIDE src.isLoading= false return src.item.show() end end if src.default and src.anime ~="def" then src.isFromAnimation=true src.item.show() -- make the source visible so the user sees the animation. if src.anime == "lr" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.x >= src.default.pos.x then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = src.default.pos.x}) return execute() else src.item.pos({x = pos.x + src.anime_steps}) end else if pos.x >= sc.width then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = -(src.item.width())}) return execute() else src.item.pos({x = pos.x + src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) elseif src.anime == "rl" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.x <= src.default.pos.x then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = src.default.pos.x}) return execute() else src.item.pos({x = pos.x - src.anime_steps}) end else if pos.x <= -(src.item.width()) then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = sc.width}) return execute() else src.item.pos({x = pos.x - src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) elseif src.anime == "tb" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.y >= src.default.pos.y then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = src.default.pos.y}) return execute() else src.item.pos({y = pos.y + src.anime_steps}) end else if pos.y >= sc.height then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = -(src.item.height())}) return execute() else src.item.pos({y = pos.y + src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) elseif src.anime == "bt" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.y <= src.default.pos.y then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = src.default.pos.y}) return execute() else src.item.pos({y = pos.y - src.anime_steps}) end else if pos.y <= -(src.item.height()) then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = sc.height}) return execute() else src.item.pos({y = pos.y - src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) else return execute() end else return execute() end end).after(vtime) end function Hide2Show.destroy(src) if src.item and src.item.data then init_reset(src) src.item.free() end end obs.script:ui(function(ui) return ui:label(IndexPage()) end) end function IndexPage() return [[
Select a source and add a filter called 'Hide2Show'.
]] end -- function script_unload() -- for _, filter in pairs(obs.utils.filters) do -- if filter and filter.src and filter.src.timer then -- obslua.timer_remove(filter.src.timer) -- filter.src.timer=nil -- end -- end -- end -- [[ END OF Hide2Show BLOCK ]] --[[ Author: iixisii contact: @iixisii ]] -- [[ OBS CUSTOM API BEGIN ]] -- [[ OBS CUSTOM CALLBACKS ]] function script_load(settings) -- print("[OBS CUSTOM WRAPPER API]") local ck= os.clock() obs.utils.script_shutdown = false obs.utils.settings = obs.PairStack(settings, nil, nil, true) -- print("api_exit: " .. tostring(obs.utils.settings.bul("obs_custom_api_exited"))) if obs.utils.settings.get_bul("obs_custom_api_exited") == false then obs.utils.loaded=true end if setup and type(setup) == "function" then setup(obs.utils.settings) end for _, filter in pairs(obs.utils.filters) do obslua.obs_register_source(filter) end local script_is_unloading= false obslua.obs_frontend_add_event_callback(function(event_id) -- print("[OBS CUSTOM API] Frontend event triggered: " .. tostring(event_id)) if event_id == obslua.OBS_FRONTEND_EVENT_FINISHED_LOADING then -- print("[OBS CUSTOM API] Finished loading, initializing script... took " .. string.format("%.2f", (os.clock() - ck) * 1000) .. "ms") obs.utils.first_time_load= true obs.utils.loaded = true obs.utils.settings.bul("obs_custom_api_exited", false) elseif event_id == obslua.OBS_FRONTEND_EVENT_EXIT or (script_is_unloading and event_id == obslua.OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) then obs.utils.obs_closing= true elseif event_id == obslua.OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN then script_is_unloading = true end end) obs.time.schedule(function() obs.utils.loaded = true end).after(1200) end function script_save(settings) -- print("[UNI-SAVE-IT]") obs.utils.settings.bul("obs_custom_api_exited", true) if obs.utils.script_shutdown then return end -- [[ OBS REGISTER HOTKEY SAVE DATA]] for name, iter in pairs(obs.register.hotkey_id_list) do local new_data = obslua.obs_hotkey_save(iter.id) if new_data then obs.utils.settings.arr(name, new_data) obslua.obs_data_array_release(new_data) end end -- [[ OBS REGISTER HOTKEY SAVE DATA END]] if type(onSaving) == "function" then return onSaving(obs.PairStack(settings, nil, nil, true)) end end function script_unload() obs.utils.script_shutdown = true -- if obs.utils.scheduled then -- for _, clb in pairs(obs.utils.scheduled) do -- obslua.timer_remove(clb) -- end -- obs.utils.scheduled = {} -- end -- for _, iter in pairs(obs.mem.freeup) do -- if iter and iter.data then -- iter.free() -- end -- end if obs._unload and type(obs._unload) == "table" then for _, iter in pairs(obs._unload) do if type(iter) == "table" and iter.data and iter.free then iter.free() elseif type(iter) == "function" then pcall(function() iter() end) end end end for _, event in pairs(obs.register.event_id_list) do if event and type(event.remove) == "function" then event.remove(true) end end if unset and type(unset) == "function" then return unset() end end function script_defaults(settings, pp) settings= obs.PairStack(settings, nil, nil, true) settings.bul("obs_custom_api_exited",false, true) if type(defaults) == "function" then return defaults(settings) end end function script_properties() if obs.utils.ui and type(obs.utils.ui) == "function" then return obs.utils.ui() end end -- [[ OBS CUSTOM CALLBACKS END ]] obs = { utils = { scheduled = {}, script_shutdown = false, OBS_SCENEITEM_TYPE = 1, OBS_SRC_TYPE = 2, OBS_OBJ_TYPE = 3, OBS_ARR_TYPE = 4, OBS_SCENE_TYPE = 5, OBS_SCENEITEM_LIST_TYPE = 6, OBS_SRC_LIST_TYPE = 7, OBS_UN_IN_TYPE = -1, OBS_SRC_WEAK_TYPE = 8, table = {}, expect_wrapper = {}, properties = { list = {}, options = {}, }, filters = {}, _queue = {},loaded= false,first_time_load= false, obs_closing= false }, time = {}, scene = {}, client = {}, mem = { freeup = {} }, script = {}, enum = { path = { read = obslua.OBS_PATH_FILE, write = obslua.OBS_PATH_FILE_SAVE, folder = obslua.OBS_PATH_DIRECTORY }, button = { default = obslua.OBS_BUTTON_DEFAULT, url = obslua.OBS_BUTTON_URL, }, list = { string = obslua.OBS_EDITABLE_LIST_TYPE_STRINGS, url = obslua.OBS_EDITABLE_LIST_TYPE_FILES_AND_URLS, file = obslua.OBS_EDITABLE_LIST_TYPE_FILES }, text = { error = obslua.OBS_TEXT_INFO_ERROR, default = obslua.OBS_TEXT_INFO, warn = obslua.OBS_TEXT_INFO_WARNING, input = obslua.OBS_TEXT_DEFAULT, password = obslua.OBS_TEXT_PASSWORD, textarea = obslua.OBS_TEXT_MULTILINE, }, group = { normal = obslua.OBS_GROUP_NORMAL, checked = obslua.OBS_GROUP_CHECKABLE, }, options = { string = obslua.OBS_COMBO_FORMAT_STRING, int = obslua.OBS_COMBO_FORMAT_INT, float = obslua.OBS_COMBO_FORMAT_FLOAT, bool = obslua.OBS_COMBO_FORMAT_BOOL, edit = obslua.OBS_COMBO_TYPE_EDITABLE, default = obslua.OBS_COMBO_TYPE_LIST, radio = obslua.OBS_COMBO_TYPE_RADIO, }, number = { int = obslua.OBS_COMBO_FORMAT_INT, float = obslua.OBS_COMBO_FORMAT_FLOAT, slider = 1000, input = 2000 }, bound = { none = obslua.OBS_BOUNDS_NONE, scale_inner = obslua.OBS_BOUNDS_SCALE_INNER, scale_outer = obslua.OBS_BOUNDS_SCALE_OUTER, stretch = obslua.OBS_BOUNDS_STRETCH, scale_width = obslua.OBS_BOUNDS_SCALE_WIDTH, scale_height = obslua.OBS_BOUNDS_SCALE_HEIGHT, max = obslua.OBS_BOUNDS_MAX_ONLY, } }, register = { hotkey_id_list = {}, event_id_list = {} }, front = {}, shared = {}, _unload= {} }; bit = require('bit') os = require('os') -- dkjson= require('dkjson') math.randomseed(os.time()) -- schedule an event -- [[ MEMORY MANAGE API ]] local ffi = require("ffi") function obs.shared.api(named_api) local arr_data_t = nil local function init_obs_data_t() for _, scene_name in pairs(obs.scene:names()) do local a_scene = obs.scene:get_scene(scene_name) if a_scene and a_scene.source then local s_data_t = obs.PairStack( obslua.obs_source_get_settings(a_scene.source) ) if not s_data_t or s_data_t.data == nil then a_scene.free() else if arr_data_t and arr_data_t.data then -- replace data to the current s_data_t.arr(named_api, arr_data_t.data) else -- register data to the current arr_data_t = s_data_t.arr(named_api) if not arr_data_t or arr_data_t.data == nil then arr_data_t = obs.ArrayStack() s_data_t.arr(named_api, arr_data_t.data) arr_data_t.free() arr_data_t = nil end end s_data_t.free() a_scene.free() end end end if not arr_data_t or arr_data_t.data == nil then arr_data_t = obs.ArrayStack() end end init_obs_data_t() function arr_data_t.save() init_obs_data_t() end function arr_data_t.del() local del_count = 0 for _, scene_name in pairs(obs.scene:names()) do local a_scene = obs.scene:get_scene(scene_name) if a_scene and a_scene.source then local s_data_t = obs.PairStack( obslua.obs_source_get_settings(a_scene.source) ) if not s_data_t or s_data_t.data == nil then a_scene.free() else s_data_t.del(named_api) del_count = del_count + 1 s_data_t.free() end a_scene.free() end end return del_count end -- obs.utils.table.append(obj_data_t, arr_data_t) return arr_data_t end function obs.expect(callback) return function(...) local args = { ... } local data = nil local caller = "" for i, v in ipairs(args) do if caller ~= "" then caller = caller .. "," end caller = caller .. "args[" .. tostring(i) .. "]" end caller = "return function(callback,args) return callback(" .. caller .. ") end"; local run = loadstring(caller) local success, result = pcall(function() data = run()(callback, args) end) local free_count = 0 if not success then for _, iter in pairs(obs.utils.expect_wrapper) do if iter and type(iter.free) == "function" then local s, r = pcall(function() iter.free() end) if s then free_count = free_count + 1 end end end obslua.script_log(obslua.LOG_ERROR, "[ErrorWrapper ERROR] => " .. tostring(result)) end return data end end function obs.ArrayStack(stack, name, fallback, unsafe) if fallback == nil then fallback = true end local self = nil self = { index = 0, get = function(index) if type(index) ~= "number" or index < 0 or index > self.size() then return nil end return obs.PairStack(obslua.obs_data_array_item(self.data, index), nil, true) end, next = obs.expect(function(__index) if type(self.index) ~= "number" or self.index < 0 or self.index > self.size() then return assert(false, "[ArrayStack] Invalid data provided or corrupted data for (" .. tostring(name) .. ")") end return coroutine.wrap(function() if self.size() <= 0 then return nil end local i = 0 if __index == nil or type(__index) ~= "number" or __index < 0 or __index > self.size() then __index = 0 end for i = __index, self.size() - 1 do coroutine.yield(i, obs.PairStack( obslua.obs_data_array_item(self.data, i), nil, false )) end end) -- local temp = self.index;self.index = self.index + 1 -- return obs.PairStack(obslua.obs_data_array_item(self.data, temp), nil, true) end), find = function(key, value) local index = 0 for itm in self.next() do if itm and type(itm) == "table" and itm.data then if itm.get_str(key) == value or itm.get_int(key) == value or itm.get_bul(key) == value or itm.get_dbl(key) == value then return itm, index end index = index + 1 itm.free() end end return nil, nil end, free = function() if self.data == nil or unsafe then return false end obslua.obs_data_array_release(self.data) self.data = nil return true end, insert = obs.expect(function(value) if type(value) ~= "userdata" and type(value) == "table" and value["data"] and type(value["data"]) == "userdata" then value = value.data end if value == nil or type(value) ~= "userdata" then obslua.script_log("FAILED TO INSERT OBJECT INTO [ArrayStack]") return false end obslua.obs_data_array_push_back(self.data, value) return self end), size = obs.expect(function() if self.data == nil then return 0 end return obslua.obs_data_array_count(self.data); end), rm = obs.expect(function(idx) if type(idx) ~= "number" or idx < 0 or self.size() <= 0 or idx > self.size() then obslua.script_log("FAILED TO RM DATA FROM [ArrayStack] (INVALID INDEX)") return false end obslua.obs_data_array_erase(self.data, idx) return self end) } if stack and name then self.data = obslua.obs_data_get_array(stack, name) elseif not stack and fallback then self.data = obslua.obs_data_array_create() else self.data = stack end return self end function obs.time.schedule(scheduler_callback) local self;self={ after= function(cond_or_time_ms) if type(cond_or_time_ms) == "number" then if cond_or_time_ms <= 0 then return type(scheduler_callback) == "function" and scheduler_callback() or nil else local tm = nil local tick= os.clock() tm = obs.time.tick(function(t, now) local tknow=(now - tick) * 1000 -- print("[OBS CUSTOM API SCHEDULER] Tick: " .. string.format("%.2f", tknow) .. "ms / " .. tostring(cond_or_time_ms) .. "ms") if tknow >= cond_or_time_ms then if type(scheduler_callback) == "function" then scheduler_callback() end if tm and type(tm.clear) == "function" then tm.clear() end end end) end elseif type(cond_or_time_ms) == "function" then local tm = nil tm = obs.time.tick(function() if cond_or_time_ms() then if type(scheduler_callback) == "function" then scheduler_callback() end if tm and type(tm.clear) == "function" then tm.clear() end end end) end end } return self end function obs.time.tick(fn, interval) local tm = nil local wrapper = function() if obs.utils.script_shutdown then return end return fn(tm, os.clock()) end if not interval or type(interval) ~= "number" or interval == 0 or (interval <= 0 and not interval > 0) then interval = 1 end tm = { clear = function() return obslua.timer_remove(wrapper) end } obslua.timer_add(wrapper, interval) return tm end function obs.wrap(self) if not self or self == nil then self = { type = obs.utils.OBS_UN_IN_TYPE, data = nil, item = nil } end if not self.data then self.data = self.item end if not self.item then self.item = self.data end -- Debugging name helper for k, v in pairs(obs.utils) do if v == self.type then self.type_name = tostring(k) end end function self.get_source() if not self.data then return nil end if self.type == obs.utils.OBS_SRC_TYPE then return self.data elseif self.type == obs.utils.OBS_SCENEITEM_TYPE then return obslua.obs_sceneitem_get_source(self.data) else return self.data end end function self.free() if self.released or not self.data then return end if self.unsafe then self.data = nil; self.released = true return end -- 4. Actual Release Logic if self.type == obs.utils.OBS_SCENE_TYPE then obslua.obs_scene_release(self.data) elseif self.type == obs.utils.OBS_SRC_WEAK_TYPE then obslua.obs_weak_source_release(self.data) elseif self.type == obs.utils.OBS_SRC_TYPE then obslua.obs_source_release(self.data) elseif self.type == obs.utils.OBS_ARR_TYPE then obslua.obs_data_array_release(self.data) elseif self.type == obs.utils.OBS_OBJ_TYPE then obslua.obs_data_release(self.data) elseif self.type == obs.utils.OBS_SCENEITEM_TYPE then obslua.obs_sceneitem_release(self.data) elseif self.type == obs.utils.OBS_SCENEITEM_LIST_TYPE then if type(self.data) == "table" then for _, itm in ipairs(self.data) do obslua.obs_sceneitem_release(itm) end elseif obslua.sceneitem_list_release then obslua.sceneitem_list_release(self.data) end elseif self.type == obs.utils.OBS_SRC_LIST_TYPE then if type(self.data) == "table" then for _, src_ptr in ipairs(self.data) do obslua.obs_source_release(src_ptr) end elseif obslua.source_list_release then obslua.source_list_release(self.data) end end self.data = nil; self.item = nil; self.released = true end return self end function obs.PairStack(stack, name, fallback, unsafe) if fallback == nil then fallback = true end local self = nil; self = { free = function() if self.data == nil or unsafe or obs.utils.script_shutdown then return false end obslua.obs_data_release(self.data) self.data = nil return true end, json = function(p) if not p then return obslua.obs_data_get_json(self.data) else return obslua.obs_data_get_json_pretty(self.data) end end, -- ... (rest of PairStack methods are fine) ... str = obs.expect(function(name, value, def) if name and value == nil then return self.get_str(name) end if self.data and name then if def then obslua.obs_data_set_default_string(self.data, name, value) else obslua.obs_data_set_string(self.data, name, value) end end return self end), int = obs.expect(function(name, value, def) value = tonumber(value) if name and value == nil then return self.get_int(name) end if self.data and name then if def then obslua.obs_data_set_default_int(self.data, name, value) else obslua.obs_data_set_int(self.data, name, value) end end return self end), dbl = obs.expect(function(name, value, def) value= tonumber(value) if name and value == nil then return self.get_dbl(name) end if self.data and name then if def then obslua.obs_data_set_default_double(self.data, name, value) else obslua.obs_data_set_double(self.data, name, value) end end return self end), bul = obs.expect(function(name, value, def) if name and type(value) ~= "boolean" then return self.get_bul(name) end if self.data and name then if def then obslua.obs_data_set_default_bool(self.data, name, value) else obslua.obs_data_set_bool(self.data, name, value) end end return self end), arr = obs.expect(function(name, value, def) if name and value == nil then return self.get_arr(name) end -- Unwrap wrapper if passed if type(value) ~= "userdata" and type(value) == "table" and value["data"] then value = value.data end if self.data and name and value then if def then obslua.obs_data_set_default_array(self.data, name, value) else obslua.obs_data_set_array(self.data, name, value) end end return self end), obj = obs.expect(function(name, value, def) if name and value == nil then return self.get_obj(name) end if type(value) ~= "userdata" and type(value) == "table" and value["data"] then value = value.data end if self.data and name and value then if def then obslua.obs_data_set_default_obj(self.data, name, value) else obslua.obs_data_set_obj(self.data, name, value) end end return self end), -- Getters (Simplified for brevity, logic unchanged) get_str = obs.expect(function(name, def) return def and obslua.obs_data_get_default_string(self.data, name) or obslua.obs_data_get_string(self.data, name) end), get_int = obs.expect(function(name, def) return def and obslua.obs_data_get_default_int(self.data, name) or obslua.obs_data_get_int(self.data, name) end), get_dbl = obs.expect(function(name, def) return def and obslua.obs_data_get_default_double(self.data, name) or obslua.obs_data_get_double(self.data, name) end), get_bul = obs.expect(function(name, def) if def ~= nil and type(def) ~= "boolean" then def = nil end if def ~= true then return obslua.obs_data_get_bool(self.data, name) else return obslua.obs_data_get_default_bool(self.data, name) end end), get_obj = obs.expect(function(name, def) local res = def and obslua.obs_data_get_default_obj(self.data, name) or obslua.obs_data_get_obj(self.data, name) return obs.PairStack(res, nil, false) -- Return safe wrapper end), get_arr = obs.expect(function(name, def) local res = def and obslua.obs_data_get_default_array(self.data, name) or obslua.obs_data_get_array(self.data, name) return obs.ArrayStack(res, nil, false) end), del = obs.expect(function(name) obslua.obs_data_erase(self.data, name) return true end), } if stack and name then self.data = obslua.obs_data_get_obj(stack, name) elseif not stack and fallback then -- print("[PairStack] No stack provided, creating new data object for (" .. tostring(name) .. ")") self.data = obslua.obs_data_create() else if type(stack) == "string" then self.data = obslua.obs_data_create_from_json(stack) if not self.data then -- print("[PairStack] Failed to create data from JSON string for (" .. tostring(name) .. "), creating empty data object instead.") self.data = obslua.obs_data_create() end elseif type(stack) == "userdata" then -- print("[PairStack] Using provided userdata stack for (" .. tostring(name) .. ")") self.data = stack else -- print("[PairStack] Invalid stack type provided for (" .. tostring(name) .. "), creating empty data object instead.") self.data = obslua.obs_data_create() end end return self end -- [[ MEMORY MANAGE API END ]] -- [[ OBS REGISTER CUSTOM API]] function obs.register:remove_all() for _, iter in pairs(obs.register.hotkey_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.hotkey_id_list = {} for _, iter in pairs(obs.register.event_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.event_id_list = {} end function obs.register:hotkey_remove() for _, iter in pairs(obs.register.hotkey_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.hotkey_id_list = {} end function obs.register:event_remove() for _, iter in pairs(obs.register.event_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.event_id_list = {} end function obs.register.hotkey(unique_id, title, callback) local script_path_value = script_path() unique_id = tostring(script_path_value) .. "_" .. tostring(unique_id) local hotkey_id = obslua.obs_hotkey_register_frontend( unique_id, title, callback ) -- load from data local hotkey_load_data = obs.utils.settings.get_arr(unique_id); if hotkey_load_data and hotkey_load_data.data ~= nil then obslua.obs_hotkey_load(hotkey_id, hotkey_load_data.data) hotkey_load_data.free() end obs.register.hotkey_id_list[unique_id] = { id = hotkey_id, title = title, callback = callback, remove = function(rss) if rss == nil then rss = false end -- obs.utils.settings.del(unique_id) if rss then if obs.register.hotkey_id_list[unique_id] and type(obs.register.hotkey_id_list[unique_id].callback) == "function" then obslua.obs_hotkey_unregister( obs.register.hotkey_id_list[unique_id].callback ) end end obs.register.hotkey_id_list[unique_id] = nil end } return obs.register.hotkey_id_list[unique_id] end function obs.register.get_hotkey(unique_id) unique_id = tostring(script_path()) .. "_" .. tostring(unique_id) if obs.register.hotkey_id_list[unique_id] then return obs.register.hotkey_id_list[unique_id] end return nil end function obs.register.event(unique_id, callback) if not callback and unique_id and type(unique_id) == "function" then callback = unique_id unique_id = tostring(script_path()) .. "_" .. obs.utils.get_unique_id(3) .. "_event" else unique_id = tostring(script_path()) .. "_" .. tostring(unique_id) .. "_event" end if type(callback) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[OBS REGISTER EVENT] Invalid callback provided") return nil end local event_id = obslua.obs_frontend_add_event_callback(callback) obs.register.event_id_list[unique_id] = { id = event_id, callback = callback, unique_id = unique_id, remove = function(rss) if rss == nil then rss = false end if rss then obslua.obs_frontend_remove_event_callback(callback) end obs.register.event_id_list[unique_id] = nil end }; end function obs.register.get_event(unique_id) unique_id = tostring(script_path()) .. "_" .. tostring(unique_id) .. "_event" if obs.register.event_id_list[unique_id] then return obs.register.event_id_list[unique_id] end return nil end -- [[ OBS REGISTER CUSTOM API END]] -- [[ OBS FILTER CUSTOM API]] function obs.script.filter(filter) local self; self = { id = filter and filter.id or obs.utils.get_unique_id(3), type = filter and filter.type or obslua.OBS_SOURCE_TYPE_FILTER, output_flags = filter and filter.output_flags or bit.bor(obslua.OBS_SOURCE_VIDEO), get_height = function(src) return src and src.height or 0 end, get_width = function(src) return src and src.width or 0 end, enable= function(src, enabled) if filter and type(filter) == "table" and type(filter["enable"]) == "function" then return filter["enable"](src, enabled) end end, update = function(_, settings) if not _ or not _.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if filter and type(filter) == "table" and filter["update"] and type(filter["update"]) == "function" then return filter.update(_, obs.PairStack(settings, nil, nil, true)) end end, activate= function(src) if filter and type(filter) == "table" and type(filter["activate"]) == "function" then return filter["activate"](src) end end,deactivate= function(src) if filter and type(filter) == "table" and type(filter["deactivate"]) == "function" then return filter["deactivate"](src) end end, hide= function(src) if filter and type(filter) == "table" and type(filter["hide"]) == "function" then return filter["hide"](src) end end,show= function(src) if filter and type(filter) == "table" and type(filter["show"]) == "function" then return filter["show"](src) end end, create = function(settings, source) if filter and type(filter) == "table" and filter["create"] and type(filter["create"]) == "function" then local src = filter.create(obs.PairStack(settings, nil, nil, true)) if src ~= nil and type(src) == "table" then self.src = src src.filter = source src.is_custom = true src.isAlive = true src.settings = obs.PairStack(settings, nil, nil, true) if filter["setup"] and type(filter["setup"]) == "function" then filter.setup(src) end return src end end local src = { filter = source, source = nil, params = nil, height = 0, width = 0, isAlive = true, settings = obs.PairStack(settings, nil, nil, true), } if source ~= nil then local target = obslua.obs_filter_get_parent(source) if target ~= nil then src.source = target src.width = obslua.obs_source_get_base_width(target) src.height = obslua.obs_source_get_base_height(target) end end shader = [[ uniform float4x4 ViewProj; uniform texture2d image; uniform int width; uniform int height; sampler_state textureSampler { Filter = Linear; AddressU = Border; AddressV = Border; BorderColor = 00000000; }; struct VertData { float4 pos : POSITION; float2 uv : TEXCOORD0; }; float4 ps_get(VertData v_in) : TARGET { return image.Sample(textureSampler, v_in.uv.xy); } VertData VSDefault(VertData v_in) { VertData vert_out; vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); vert_out.uv = v_in.uv; return vert_out; } technique Draw { pass { vertex_shader = VSDefault(v_in); pixel_shader = ps_get(v_in); } } ]] obslua.obs_enter_graphics() src.shader = obslua.gs_effect_create(shader, nil, nil) obslua.obs_leave_graphics() if src.shader ~= nil then src.params = { width = obslua.gs_effect_get_param_by_name(src.shader, "width"), height = obslua.gs_effect_get_param_by_name(src.shader, "height"), image = obslua.gs_effect_get_param_by_name(src.shader, "image"), } else return self.destroy() end if filter and filter["setup"] and type(filter["setup"]) == "function" then filter.setup(src, src.settings) end self.src = src obs.time.schedule(function() -- print("[OBS CUSTOM API FILTER] Running post-creation setup...") -- Debug log obs.time.schedule(function() if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if src.filter then src.source = obslua.obs_filter_get_parent(src.filter) if filter and filter["finally"] and type(filter["finally"]) == "function" then -- print("[OBS CUSTOM API FILTER] Running filter.finally...") -- Debug log filter.finally(src) end end src.isInitialized = true end).after(obs.utils.first_time_load and 500 or 1) end).after(function() return obs.utils.loaded end) return src end, destroy = function(src) if not src then return end src.isAlive = false if src and type(src) == "table" and src.shader then obslua.obs_enter_graphics() obslua.gs_effect_destroy(src.shader) obslua.obs_leave_graphics() end if filter and type(filter) == "table" and filter["destroy"] and type(filter["destroy"]) == "function" then filter.destroy(src) end src.source = nil src.filter = nil src.params = nil end, video_tick = function(src, fps) if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if src.source == nil and src.filter then src.source = obslua.obs_filter_get_parent(src.filter) end if src.source and src.filter then src.width = obslua.obs_source_get_base_width(src.source) src.height = obslua.obs_source_get_base_height(src.source) else src.width = 0; src.height = 0 end local __tick = (filter["video_tick"] or filter["tick"]) or function() end __tick(src, fps) end, video_render = function(src) if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if filter and type(filter) == "table" and filter["video_render"] and type(filter["video_render"]) == "function" then local result = filter.video_render(src) if src.is_custom then return result end end if src.source == nil and src.filter then src.source = obslua.obs_filter_get_parent(src.filter) end if src.source and src.filter then src.width = obslua.obs_source_get_base_width(src.source) src.height = obslua.obs_source_get_base_height(src.source) end if src.filter then local width = src.width; local height = src.height if not width or not height or (width <= 0 or height <= 0) then obslua.obs_source_skip_video_filter(src.filter) return nil end if not obslua.obs_source_process_filter_begin( src.filter, obslua.GS_RGBA, obslua.OBS_NO_DIRECT_RENDERING ) then obslua.obs_source_skip_video_filter(src.filter) return nil end if not src.params then obslua.obs_source_process_filter_end(src.filter, src.shader, width, height) return nil end if type(width) == "number" then obslua.gs_effect_set_int(src.params.width, width) end if type(height) == "number" then obslua.gs_effect_set_int(src.params.height, height) end obslua.gs_blend_state_push() obslua.gs_blend_function( obslua.GS_BLEND_ONE, obslua.GS_BLEND_INVSRCALPHA ) if width and height then obslua.obs_source_process_filter_end(src.filter, src.shader, width, height) end obslua.gs_blend_state_pop() end return true end, get_name = function() return filter and filter.name or "Custom Filter" end, get_defaults = function(settings) local defaults = nil if filter and type(filter) == "table" then if filter["get_defaults"] and type(filter["get_defaults"]) == "function" then defaults = filter.get_defaults elseif filter["defaults"] and type(filter["defaults"]) == "function" then defaults = filter.defaults end end if defaults and type(defaults) == "function" then return defaults(obs.PairStack(settings, nil, nil, true)) end end, get_properties = function(src) local properties = nil if filter and type(filter) == "table" then if filter["get_properties"] and type(filter["get_properties"]) == "function" then properties = filter.get_properties elseif filter["properties"] and type(filter["properties"]) == "function" then properties = filter.properties end end if properties and type(properties) == "function" then return properties(src) end return nil end } table.insert(obs.utils.filters, self) if not filter or type(filter) ~= "table" then filter = {} end filter.get_name = self.get_name if not filter.id then filter.id = self.id end filter.get_width = self.get_width filter.get_height = self.get_height filter.type = self.type filter.output_flags = self.output_flags return filter end -- [[ OBS FILTER CUSTOM API END]] -- [[ OBS SCENE API CUSTOM ]] function obs.scene:get_scene(scene_name) local scene; local source_scene; if not scene_name or not type(scene_name) == "string" then source_scene = obslua.obs_frontend_get_current_scene() if not source_scene then return nil end scene = obslua.obs_scene_from_source(source_scene) else source_scene = obslua.obs_get_source_by_name(scene_name) if not source_scene then return nil end scene = obslua.obs_scene_from_source(source_scene) end local obj_scene_t; obj_scene_t = { group_names = function() local scene_items_list = obs.wrap({ data = obslua.obs_scene_enum_items(scene), type = obs.utils.OBS_SCENEITEM_LIST_TYPE }) if scene_items_list == nil or scene_items_list.data == nil then return nil end local list = {} for _, item in ipairs(scene_items_list.data) do local source = obslua.obs_sceneitem_get_source(item) if source ~= nil then local sourceName = obslua.obs_source_get_name(source) if obslua.obs_sceneitem_is_group(item) then table.insert(list, sourceName) end end end scene_items_list.free() return list end, source_names = function(source_id_type) local scene_nodes_name_list = {} local scene_items_list = obs.wrap({ data = obslua.obs_scene_enum_items(scene), type = obs.utils.OBS_SCENEITEM_LIST_TYPE }) for _, item in ipairs(scene_items_list.data) do local source = obslua.obs_sceneitem_get_source(item) if source ~= nil then local sourceName = obslua.obs_source_get_name(source) if source_id_type == nil or type(source_id_type) ~= "string" or source_id_type == "" then table.insert(scene_nodes_name_list, sourceName) else local sourceId = obslua.obs_source_get_id(source) if sourceId == source_id_type then table.insert(scene_nodes_name_list, sourceName) end end source = nil end end scene_items_list.free() return scene_nodes_name_list end, get = function(source_name) if not scene then return nil end local c = 1 local scene_item; local scene_items_list = obs.wrap({ data = obslua.obs_scene_enum_items(scene), type = obs.utils.OBS_SCENEITEM_LIST_TYPE }) if scene_items_list == nil or scene_items_list.data == nil then return nil end for _, item in ipairs(scene_items_list.data) do c = c + 1 local src = obslua.obs_sceneitem_get_source(item) local src_name = obslua.obs_source_get_name(src) if src ~= nil and src_name == source_name then obslua.obs_sceneitem_addref(item) scene_item = obs.wrap({ data = item, type = obs.utils.OBS_SCENEITEM_TYPE, name = source_name }) break end end scene_items_list.free() if scene_item == nil or scene_item.data == nil then return nil end local obj_source_t; obj_source_t = { free = scene_item.free, item = scene_item.data, data = scene_item.data, _busy = false, _queue = {}, _timer = nil, _frame_time = 0.016, _last_run = os.clock(), _cached_info = obslua.obs_transform_info(), _cached_crop = obslua.obs_sceneitem_crop(), _cached_pos = obslua.vec2(), _cached_scale = obslua.vec2(), _virtual = { initialized = false, pos = { x = 0, y = 0 }, scale = { x = 1, y = 1 }, rot = 0, bounds = { x = 0, y = 0 }, alignment = 0, bounds_type = 0 }, _sync_shadow = function() -- if obj_source_t._virtual.initialized then return end obslua.obs_sceneitem_get_info2(obj_source_t.data, obj_source_t._cached_info) obj_source_t._virtual.pos = { x = obj_source_t._cached_info.pos.x, y = obj_source_t._cached_info.pos.y } obj_source_t._virtual.scale = { x = obj_source_t._cached_info.scale.x, y = obj_source_t._cached_info.scale.y } obj_source_t._virtual.rot = obj_source_t._cached_info.rot obj_source_t._virtual.bounds = { x = obj_source_t._cached_info.bounds.x, y = obj_source_t._cached_info.bounds.y } obj_source_t._virtual.alignment = obj_source_t._cached_info.alignment obj_source_t._virtual.bounds_type = obj_source_t._cached_info.bounds_type obj_source_t._virtual.initialized = true -- Initialize cached structs obslua.obs_sceneitem_get_info2(obj_source_t.data, obj_source_t._cached_info) obslua.obs_sceneitem_get_crop(obj_source_t.data, obj_source_t._cached_crop) obslua.obs_sceneitem_get_pos(obj_source_t.data, obj_source_t._cached_pos) obslua.obs_sceneitem_get_scale(obj_source_t.data, obj_source_t._cached_scale) end, _safe_run = function(func) return pcall(function() return func() end) end, rename= function(new_name) if type(new_name) ~= "string" then return false end return obj_source_t._safe_run(function() obslua.obs_source_set_name(obj_source_t.get_source(), new_name) obj_source_t.name = new_name return true end) end, pos = function(val) if val == nil or not (type(val) == "table") or (val.x == nil and val.y == nil) then return { x = obj_source_t._virtual.pos.x, y = obj_source_t._virtual.pos.y } end return obj_source_t._safe_run(function() return obj_source_t.transform({ pos = val }) end) end, scale = function(val) if val == nil or not (type(val) == "table") then return { x = obj_source_t._virtual.scale.x, y = obj_source_t._virtual.scale.y } end return obj_source_t._safe_run(function() return obj_source_t.transform({ scale = val }) end) end, rot = function(val) if val == nil then return obj_source_t._virtual.rot end return obj_source_t._safe_run(function() return obj_source_t.transform({ rot = val }) end) end, align = function(val) if val == nil then return obj_source_t._virtual.alignment end return obj_source_t._safe_run(function() return obj_source_t.transform({ alignment = val }) end) end, bounds = function(size) if size == nil or not (type(size) == "table") then return { x = obj_source_t._virtual.bounds.x, y = obj_source_t._virtual.bounds.y } end return obj_source_t._safe_run(function() return obj_source_t.transform({ bounds = size }) end) end, order= function(obs_enum_order_or_idx) if not obs_enum_order_or_idx or not (type(obs_enum_order_or_idx) == "number") then return obslua.obs_sceneitem_get_order_position(obj_source_t.data) end if obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_UP or obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_DOWN or obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_TOP or obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_BOTTOM then return obslua.obs_sceneitem_set_order(obj_source_t.data, obs_enum_order_or_idx) else return obslua.obs_sceneitem_set_order_position(obj_source_t.data, obs_enum_order_or_idx) end end, ground=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_BOTTOM) end,up=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_UP) end,down=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_DOWN) end,top=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_TOP) end, width = function(val) if val == nil or type(val) ~= "number" then local is_bounded = obj_source_t._virtual.bounds_type ~= obslua.OBS_BOUNDS_NONE local base_w = obslua.obs_source_get_base_width(obj_source_t.get_source()) local crop = obj_source_t._cached_crop -- Calculate effective width after crop local cropped_w = math.max(0, base_w - crop.left - crop.right) return is_bounded and obj_source_t._virtual.bounds.x or (cropped_w * obj_source_t._virtual.scale.x) end return obj_source_t._safe_run(function() return obj_source_t.size({ width = val }) end) end, height = function(val) if val == nil or type(val) ~= "number" then local is_bounded = obj_source_t._virtual.bounds_type ~= obslua.OBS_BOUNDS_NONE local base_h = obslua.obs_source_get_base_height(obj_source_t.get_source()) local crop = obj_source_t._cached_crop -- Calculate effective height after crop local cropped_h = math.max(0, base_h - crop.top - crop.bottom) return is_bounded and obj_source_t._virtual.bounds.y or (cropped_h * obj_source_t._virtual.scale.y) end return obj_source_t._safe_run(function() return obj_source_t.size({ height = val }) end) end, size = function(size) local is_bounded = obj_source_t._virtual.bounds_type ~= obslua.OBS_BOUNDS_NONE local base_w = obslua.obs_source_get_base_width(obj_source_t.get_source()) local base_h = obslua.obs_source_get_base_height(obj_source_t.get_source()) local crop = obj_source_t._cached_crop -- Calculate effective dimensions after crop local cropped_w = math.max(0, base_w - crop.left - crop.right) local cropped_h = math.max(0, base_h - crop.top - crop.bottom) if size == nil or not (type(size) == "table") then return { x = is_bounded and obj_source_t._virtual.bounds.x or (cropped_w * obj_source_t._virtual.scale.x), y = is_bounded and obj_source_t._virtual.bounds.y or (cropped_h * obj_source_t._virtual.scale.y), width = is_bounded and obj_source_t._virtual.bounds.x or (cropped_w * obj_source_t._virtual.scale.x), height = is_bounded and obj_source_t._virtual.bounds.y or (cropped_h * obj_source_t._virtual.scale.y) } end return obj_source_t._safe_run(function() if is_bounded then return obj_source_t.transform({ bounds = { x = (size.x and size.x or size.width), y = (size.y and size.y or size.height) } }) else if cropped_w > 0 and cropped_h > 0 then return obj_source_t.transform({ scale = { x = (size.x and size.x or size.width) / cropped_w, y = (size.y and size.y or size.height) / cropped_h } }) end end end) end, crop = function(c) if c == nil then obslua.obs_sceneitem_get_crop(obj_source_t.data, obj_source_t._cached_crop) return obj_source_t._cached_crop end return obj_source_t._safe_run(function() -- Use Cached Crop Struct obslua.obs_sceneitem_get_crop(obj_source_t.data, obj_source_t._cached_crop) if c.top then obj_source_t._cached_crop.top = c.top end if c.bottom then obj_source_t._cached_crop.bottom = c.bottom end if c.left then obj_source_t._cached_crop.left = c.left end if c.right then obj_source_t._cached_crop.right = c.right end obslua.obs_sceneitem_set_crop(obj_source_t.data, obj_source_t._cached_crop) return true end) end, transform = function(tf) if obslua.obs_source_removed(obj_source_t.get_source()) then return nil end obj_source_t._sync_shadow() if not tf or not (type(tf) == "userdata" or type(tf) == "table") then return { pos = { x = obj_source_t._virtual.pos.x, y = obj_source_t._virtual.pos.y }, scale = { x = obj_source_t._virtual.scale.x, y = obj_source_t._virtual.scale.y }, rot = obj_source_t._virtual.rot, bounds = { x = obj_source_t._virtual.bounds.x, y = obj_source_t._virtual.bounds.y }, alignment = obj_source_t._virtual.alignment, bounds_type = obj_source_t._virtual.bounds_type } end return obj_source_t._safe_run(function() local info; if type(tf) == "userdata" then info = tf elseif type(tf) == "table" then for k, v in pairs(tf) do if k == "pos" and type(v) == "table" then if type(v.x) == "number" then obj_source_t._virtual.pos.x = v.x obj_source_t._cached_info.pos.x = v.x end if type(v.y) == "number" then obj_source_t._virtual.pos.y = v.y obj_source_t._cached_info.pos.y = v.y end elseif k == "scale" and type(v) == "table" then if type(v.x) == "number" then obj_source_t._virtual.scale.x = v.x obj_source_t._cached_info.scale.x = v.x end if type(v.y) == "number" then obj_source_t._virtual.scale.y = v.y obj_source_t._cached_info.scale.y = v.y end elseif k == "rot" and type(v) == "number" then obj_source_t._virtual.rot = v obj_source_t._cached_info.rot = v elseif k == "bounds" and type(v) == "table" then if type(v.x) == "number" then obj_source_t._virtual.bounds.x = v.x obj_source_t._cached_info.bounds.x = v.x end if type(v.y) == "number" then obj_source_t._virtual.bounds.y = v.y obj_source_t._cached_info.bounds.y = v.y end elseif k == "alignment" and type(v) == "number" then obj_source_t._virtual.alignment = v obj_source_t._cached_info.alignment = v elseif k == "bounds_type" and type(v) == "number" then obj_source_t._virtual.bounds_type = v obj_source_t._cached_info.bounds_type = v end end info = obj_source_t._cached_info else return end obslua.obs_sceneitem_defer_update_begin(obj_source_t.data) obslua.obs_sceneitem_set_info2(obj_source_t.data, info) obslua.obs_sceneitem_defer_update_end(obj_source_t.data) return true end) end, get_source = function() return obslua.obs_sceneitem_get_source(obj_source_t.data) end, get_name = function() return obslua.obs_source_get_name(obj_source_t.get_source()) end, bounding = function() if not obj_source_t or not obj_source_t.data then return 0 end return obslua.obs_sceneitem_get_bounds_type(obj_source_t.data) end, remove = function() if obj_source_t.data == nil then return true end obslua.obs_sceneitem_remove(obj_source_t.data) obj_source_t.free(); obj_source_t.data = nil; obj_source_t.item = nil return true end, hide = function() return obslua.obs_sceneitem_set_visible(obj_source_t.data, false) end, show = function() return obslua.obs_sceneitem_set_visible(obj_source_t.data, true) end, isHidden = function() return obslua.obs_sceneitem_visible(obj_source_t.data) end, insert = { filter = function(filter_source_or_id, name) local filter_ptr = nil local created_locally = false -- 1. Determine if we are creating new (String ID) or adding existing (Source Object) if type(filter_source_or_id) == "string" then local function insert_filter(id) local source_name= obj_source_t.get_name() if not source_name then return nil end local source = obslua.obs_get_source_by_name(source_name) if source == nil then return end local settings = obslua.obs_data_create() local a_name= name or id local filter = obslua.obs_source_create_private(id, a_name, settings) obslua.obs_source_filter_add(source, filter) obslua.obs_data_release(settings) obslua.obs_source_release(source) return filter end filter_ptr= insert_filter(filter_source_or_id) elseif type(filter_source_or_id) == "table" and filter_source_or_id.data then -- Handle custom wrapper object filter_ptr = filter_source_or_id.data obslua.obs_source_filter_add(src, filter_ptr) elseif type(filter_source_or_id) == "userdata" then -- Handle raw userdata filter_ptr = filter_source_or_id obslua.obs_source_filter_add(src, filter_ptr) end if not filter_ptr then return nil end -- 4. Construct and return the wrapper (consistent with obj_source_t.filter) local filter_wrapper = obs.wrap({ data = filter_ptr, type = obs.utils.OBS_SRC_TYPE }) local self; self = { remove = function() local src=nil;local source_name= obj_source_t.get_name() if source_name then src= obslua.obs_get_source_by_name(source_name) end if src then obslua.obs_source_filter_remove(src, filter_wrapper.data) obslua.obs_source_release(src) end self.free() self = nil return true end, commit = function() if self.settings and self.settings.data then obslua.obs_source_update(filter_wrapper.data, self.settings.data) end return self end, data = filter_wrapper.data, free = function() if self.settings and self.settings.data then self.settings.free() end if filter_wrapper and filter_wrapper.data then filter_wrapper.free(); filter_wrapper = nil end end, settings = obs.PairStack(obslua.obs_source_get_settings(filter_wrapper.data)), id = function() return obslua.obs_source_get_unversioned_id(filter_wrapper.data) end, } return self end }, filter = function(name_or_id) local source = obj_source_t.get_source() if not source then return nil end local found_ptr = obslua.obs_source_get_filter_by_name(source, name_or_id) local filter_wrapper = nil if not found_ptr then local fb = function(parent, filter, param) local id = obslua.obs_source_get_unversioned_id(filter) local id2= obslua.obs_source_get_id(filter) if id == name_or_id or id2 == name_or_id then found_ptr = obslua.obs_source_get_ref(filter) return true end return false end local filter_list = obs.wrap({ data = obslua.obs_source_enum_filters(source), type = obs.utils .OBS_SRC_LIST_TYPE }) for _, filter in ipairs(filter_list.data) do if fb(source, filter, nil) then break end end filter_list.free() end if not found_ptr then return nil end filter_wrapper = obs.wrap({ data = found_ptr, type = obs.utils.OBS_SRC_TYPE }) if not filter_wrapper or not filter_wrapper.data then return nil end local self; self = { remove = function() obslua.obs_source_filter_remove(source, filter_wrapper.data) self.free() self = nil return true end, data = filter_wrapper.data, commit = function() if self.settings and self.settings.data and filter_wrapper and filter_wrapper.data then if obslua.obs_source_removed(filter_wrapper.data) then return self end obslua.obs_source_update(filter_wrapper.data, self.settings.data) end return self end, free = function() if self.settings and self.settings.data then self.settings.free() end if filter_wrapper and filter_wrapper.data then filter_wrapper.free(); filter_wrapper = nil end end, settings = obs.PairStack(obslua.obs_source_get_settings(filter_wrapper.data)), id = function() return obslua.obs_source_get_unversioned_id(filter_wrapper.data) end, } return self end, style = { grad = { enable = function() local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end src.bul("gradient", true) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, disable = function() local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end src.bul("gradient", false) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, dir = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("gradient_dir"); src.free(); return tempv end src.dbl("gradient_dir", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, color = function(r, g, b) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if not r or not g or not b then local tempv = src.int("gradient_color"); src.free(); return obs.utils.argb_to_rgb(tempv) end src.int("gradient_color", obs.utils.rgb_to_argb(r, g, b)) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() return true end, opacity = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("gradient_opacity"); src.free(); return tempv end src.dbl("gradient_opacity", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end }, bg_opacity = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("bk_opacity"); src.free(); return tempv end src.dbl("bk_opacity", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, opacity = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("opacity"); src.free(); return tempv end src.dbl("opacity", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, }, font = { size = function(font_size) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end local font = src.get_obj("font") if not font or not font.data then font = obs.PairStack() --font.str("face","Arial") end if font_size == nil or not type(font_size) == "number" or font_size <= 0 then font_size = font.get_int("size") font.free(); src.free(); return font_size else font.int("size", font_size) end font.free(); obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() return true end, face = function(face_name) end }, text = function(txt) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end local res = true if txt == nil or txt == "" or type(txt) ~= "string" then res = src.get_str("text") if not res == nil then res = "" end else src.str("text", txt) end obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() return res end, } function obj_source_t.style.bg_color(r, g, b) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end if not r or not g or not b then local tempv = src.int("bk_color") src.free() return obs.utils.argb_to_rgb(tempv) end src.int("bk_color", obs.utils.rgb_to_argb(r, g, b)) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end function obj_source_t.style.color(r, g, b) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end if not r or not g or not b then local tempv = src.int("color") src.free() return obs.utils.argb_to_rgb(tempv) end local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end src.int("color", obs.utils.rgb_to_argb(r, g, b)) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end function obj_source_t.style.get() local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end local json = src.json(true) src.free() return json end function obj_source_t.style.set(val) local src = obs.PairStack(val) if not src or not src.data then return nil end obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end function obj_source_t.opacity(val) local color_filter= obj_source_t.filter("color_filter_v2") if not color_filter or not color_filter.data then color_filter= obj_source_t.insert.filter("color_filter_v2", "Color Correction") if not color_filter or not color_filter.data then return end end if val == nil or type(val) ~= "number" then local tempv = color_filter.settings.dbl("opacity") color_filter.free() return tempv end color_filter.settings.dbl("opacity", val) return color_filter.commit().free() end pcall(function() return obj_source_t._sync_shadow() end) return obj_source_t end, add = function(source) if not source then return false end local sceneitem = obslua.obs_scene_add(scene, source) if sceneitem == nil then return nil end obslua.obs_sceneitem_addref(sceneitem) local dt = obs.wrap({ data = sceneitem, type = obs.utils.OBS_SCENEITEM_TYPE }) return dt end, free = function() if not source_scene then return end obslua.obs_source_release(source_scene) scene = nil end, release = function() return obj_scene_t.free() end, get_width = function() local w= obslua.obs_source_get_width(source_scene) return obslua.obs_source_get_base_width(source_scene) end, get_height = function() local h= obslua.obs_source_get_height(source_scene) return obslua.obs_source_get_base_height(source_scene) end, data = scene, item = scene, source = source_scene }; return obj_scene_t end function obs.scene:scene_from(source) if not source or type(source) == 'string' then return nil end local sc = obslua.obs_scene_from_source(source) local ss = obslua.obs_scene_get_source(sc) return obs.scene:get_scene(obslua.obs_source_get_name(ss)) end function obs.scene:name() source_scene = obslua.obs_frontend_get_current_scene() if not source_scene then return nil end local source_name = obslua.obs_source_get_name(source_scene) obslua.obs_source_release(source_scene) return source_name end function obs.scene:add_to_scene(source) if not source then return false end local current_source_scene = obslua.obs_frontend_get_current_scene() if not current_source_scene then return false end local current_scene = obslua.obs_scene_from_source(current_source_scene) if not current_scene then obslua.obs_source_release(current_source_scene) return false end obslua.obs_scene_add(current_scene, source) obslua.obs_source_release(current_source_scene) return true end function obs.scene:names() local scenes = obs.wrap({ data = obslua.obs_frontend_get_scenes(), type = obs.utils.OBS_SRC_LIST_TYPE }) local obj_table_t = {} for _, a_scene in pairs(scenes.data) do if a_scene then local scene_source_name = obslua.obs_source_get_name(a_scene) table.insert(obj_table_t, scene_source_name) end end scenes.free() return obj_table_t end function obs.scene:size() local scene = obs.scene:get_scene() if not scene or not scene.data then return nil end local w = scene:get_width() local h = scene:get_height() scene.free() return { width = w, height = h } end -- [[ OBS SCENE API CUSTOM END ]] -- [[ OBS FRONT API ]] function obs.front.source_names() local list = {} local all_sources = obs.wrap({ data = obslua.obs_enum_sources(), type = obs.utils.OBS_SRC_LIST_TYPE }) for _, source in pairs(all_sources.data) do if source then local source_name = obslua.obs_source_get_name(source) table.insert(list, source_name) end end all_sources.free() return list end function obs.front.source(source) local scene; local source_name; if source and type(source) ~= "string" and type(source) == "userdata" then scene = obs.scene:scene_from(source) source_name = obslua.obs_source_get_name(source) elseif type(source) == "string" then source_name = source local temp = obslua.obs_get_source_by_name(source_name) if not temp then return nil end scene = obs.scene:scene_from(temp) obslua.obs_source_release(temp) end if not scene or not scene.data then return end local sct = scene.get(source_name) scene.free() return sct end -- [[ OBS FRONT API END ]] -- [[ OBS SCRIPT PROPERTIES CUSTOM API]] function obs.script:ui(clb, s) if obs.utils.ui then obslua.script_log(obslua.LOG_ERROR, "[SCRIPT.UI] UI is already created") return false end if type(clb) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[SCRIPT.UI] Invalid callback provided") return false end obs.utils.ui = function() obs.utils.properties = { list = {}, options = {}, } local p = obs.script.create(s) local self = {}; for key, fnc in pairs(obs.script) do self[key] = function(...) return fnc(p, ...) end end clb(self, p) return p end return true end function obs.script.create(settings) local p = obslua.obs_properties_create() if type(settings) == "userdata" then settings = obs.PairStack(settings, nil, nil, true) end obs.utils.properties[p] = settings return p end function obs.script.options(p, unique_id, desc, enum_type_id, enum_format_id) if not desc or type(desc) ~= "string" then desc = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if enum_format_id == nil then enum_format_id = obs.enum.options.string; end if enum_type_id == nil then enum_type_id = obs.enum.options.default; end local obj = obslua.obs_properties_add_list(p, unique_id, desc, enum_type_id, enum_format_id); if not obj then obslua.script_log(obslua.LOG_ERROR, "[obsapi_custom.lua] Failed to create options property: " .. tostring(unique_id) .. " description: " .. tostring(desc) .. " enum_type_id: " .. tostring(enum_type_id) .. " enum_format_id: " .. tostring(enum_format_id)) return nil end obs.utils.properties.options[unique_id] = { enum_format_id = enum_format_id, enum_type_id = enum_type_id, type = enum_format_id } obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch(obj, p) return obs.utils.properties[unique_id] end function obs.script.button(p, unique_id, label, callback) if not label or type(label) ~= "string" then label = "button" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if type(callback) ~= "function" then callback = function() end end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_button(p, unique_id, label, function(properties_t, property_t) return callback( property_t, properties_t, obs.utils.properties[properties_t] and obs.utils.properties[properties_t] or obs.utils.settings ) end) , p) return obs.utils.properties[unique_id] end function obs.script.label(p, unique_id, text, enum_type) if not text or type(text) ~= "string" then text = "" end if not unique_id or type(unique_id) == nil or unique_id == "" or type(unique_id) ~= "string" then unique_id = obs.utils.get_unique_id(20) end local default_enum_type = obslua.OBS_TEXT_INFO; if (enum_type == nil) then enum_type = default_enum_type end local obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_text(p, unique_id, text, default_enum_type), p) if enum_type == obs.enum.text.error then obj.error(text) elseif enum_type == obs.enum.text.warn then obj.warn(text) end obj.type = enum_type; obs.utils.properties[unique_id] = obj return obj; end function obs.script.group(p, unique_id, desc, enum_type) local pp = obs.script.create() if not desc or type(desc) ~= "string" then desc = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if enum_type == nil then enum_type = obs.enum.group.normal; end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_group(p, unique_id, desc, enum_type, pp), pp) obs.utils.properties[unique_id].parent = pp obs.utils.properties[unique_id].add = {} for key, fnc in pairs(obs.script) do obs.utils.properties[unique_id].add[key] = function(...) return fnc(obs.utils.properties[unique_id].parent, ...) end end return obs.utils.properties[unique_id] end function obs.script.bool(p, unique_id, desc) if not desc or type(desc) ~= "string" then desc = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_bool(p, unique_id, desc), p) return obs.utils.properties[unique_id] end function obs.script.path(p, unique_id, desc, enum_type_id, filter_string, default_path_string) if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if not desc or type(desc) ~= "string" then desc = "" end if enum_type_id == nil or type(enum_type_id) ~= "number" then enum_type_id = obs.enum.path.read end if filter_string == nil or type(filter_string) ~= "string" then filter_string = "" end if default_path_string == nil or type(default_path_string) ~= "string" then default_path_string = "" end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_path(p, unique_id, desc, enum_type_id, filter_string, default_path_string), p) return obs.utils.properties[unique_id] end function obs.script.form(properties, title, unique_id) local pp = obs.script.create(); local __exit_click_callback__ = nil; local __onexit_type__ = 1; local __cancel_click_callback__ = nil; local __oncancel_type__ = 1; if unique_id == nil then unique_id = obs.utils.get_unique_id(20) end local group_form = obs.script.group(properties, unique_id, "", pp, obs.enum.group.normal) local label = obs.script.label(pp, unique_id .. "_label", title, obslua.OBS_TEXT_INFO); obs.script.label(pp, "form_tt", "
", obslua.OBS_TEXT_INFO); local ipp = obs.script.create() local group_inner = obs.script.group(pp, unique_id .. "_inner", "", ipp, obs.enum.group.normal) local exit = obs.script.button(pp, unique_id .. "_exit", "Confirm", function(pp, s, ss) if __exit_click_callback__ and type(__exit_click_callback__) == "function" then __exit_click_callback__(pp, s, obs.PairStack(ss, nil, nil, true)) end if __onexit_type__ == -1 then group_form.free() elseif __onexit_type__ == 1 then group_form.hide() end return true end) local cancel = obs.script.button(pp, unique_id .. "_cancel", "Cancel", function(pp, s, ss) if __cancel_click_callback__ and type(__cancel_click_callback__) == "function" then __cancel_click_callback__(pp, s, obs.PairStack(ss, nil, nil, true)) end if __oncancel_type__ == -1 then group_form.free() elseif __oncancel_type__ == 1 then group_form.hide() end return true end) local obj_t; obj_t = { add = { button = function(...) return obs.script.button(ipp, ...) end, options = function(...) return obs.script.options(ipp, ...) end, label = function(...) return obs.script.label(ipp, ...) end, group = function(...) return obs.script.group(ipp, ...) end, bool = function(...) return obs.script.bool(ipp, ...) end, path = function(...) return obs.script.path(ipp, ...) end, input = function(...) return obs.script.input(ipp, ...) end, number = function(...) return obs.script.number(ipp, ...) end }, get = function(name) return obs.script.get(name) end, free = function() group_form.free() ipp = nil pp = nil return true end, data = ipp, item = ipp, confirm = {}, onconfirm = {}, oncancel = {}, cancel = {} } function obj_t.confirm:click(clb) __exit_click_callback__ = clb return obj_t end; function obj_t.confirm:text(title_value) if not title_value or type(title_value) ~= "string" or title_value == "" then return false end exit.text(title_value) return true end function obj_t.onconfirm:hide() __onexit_type__ = 1 return obj_t end; function obj_t.onconfirm:remove() __onexit_type__ = -1 return obj_t end; function obj_t.onconfirm:idle() __onexit_type__ = 0 return obj_t end function obj_t.cancel:click(clb) __cancel_click_callback__ = clb return obj_t end; function obj_t.cancel:text(txt) if not txt or type(txt) ~= "string" or txt == "" then return false end cancel.text(txt) return true end function obj_t.oncancel:idle() __oncancel_type__ = 0 return obj_t end; function obj_t.oncancel:remove() __oncancel_type__ = -1 return obj_t end; function obj_t.oncancel:hide() __oncancel_type__ = 1 return obj_t end function obj_t.show() return group_form.show(); end; function obj_t.hide() return group_form.hide(); end; function obj_t.remove() return obj_t.free() end obs.utils.properties[unique_id] = obj_t return obj_t end function obs.script.fps(properties_t, unique_id, title) if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_frame_rate(properties_t, unique_id, title), properties_t ) return obs.utils.properties[unique_id] end function obs.script.list(properties_t, unique_id, title, enum_type_id, filter_string, default_path_string) if not filter_string or type(filter_string) ~= "string" then filter_string = "" end if not default_path_string or type(default_path_string) ~= "string" then default_path_string = "" end if not enum_type_id or type(enum_type_id) ~= "number" or ( enum_type_id ~= obs.enum.list.string and enum_type_id ~= obs.enum.list.file and enum_type_id ~= obs.enum.list.url ) then enum_type_id = obs.enum.list.string end if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_editable_list( properties_t, unique_id, title, enum_type_id, filter_string, default_path_string ), properties_t) return obs.utils.properties[unique_id] end function obs.script.input(p, unique_id, title, enum_type_id, callback) if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if not enum_type_id == nil or ( enum_type_id ~= obs.enum.text.input and enum_type_id ~= obs.enum.text.textarea and enum_type_id ~= obs.enum.text.password) then enum_type_id = obs.enum.text.input end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_text( p, unique_id, title, enum_type_id ), p) return obs.utils.properties[unique_id] end function obs.script.color(properties_t, unique_id, title) if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_color_alpha( properties_t, unique_id, title ), properties_t) return obs.utils.properties[unique_id] end function obs.script.number(properties_t, min, max, steps, unique_id, title, enum_number_type_id, enum_type_id) if not enum_number_type_id then enum_number_type_id = obs.enum.number.int end if not enum_type_id then enum_type_id = obs.enum.number.input end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end local obj; if enum_type_id == obs.enum.number.slider then if enum_number_type_id == obs.enum.number.float then obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_float( properties_t, unique_id, title, min, max, steps )) else obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_int_slider( properties_t, unique_id, title, min, max, steps )) end else if enum_number_type_id == obs.enum.number.float then obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_float( properties_t, unique_id, title, min, max, steps )) else obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_int( properties_t, unique_id, title, min, max, steps )) end end if obj then obj["type"] = enum_number_type_id end obs.utils.properties[unique_id] = obj return obj end function obs.script.get(name) return obs.utils.properties[name] end -- [[ OBS SCRIPT PROPERTIES CUSTOM API END ]] -- [[ API UTILS ]] function obs.utils.rgb_to_argb(r, g, b) r = math.max(0, math.min(255, math.floor(r))) g = math.max(0, math.min(255, math.floor(g))) b = math.max(0, math.min(255, math.floor(b))) -- OBS expects the order to be Blue-Green-Red (BGR) -- Red: bits 0-7 (multiply by 1) -- Green: bits 8-15 (multiply by 2^8) -- Blue: bits 16-23 (multiply by 2^16) return (b * 2 ^ 16) + (g * 2 ^ 8) + r end function obs.utils.argb_to_rgb(val) if type(val) ~= "number" then return nil end local b = math.floor(val / 2 ^ 16) % 256 local g = math.floor(val / 2 ^ 8) % 256 local r = val % 256 return r, g, b end function obs.utils.obs_api_properties_patch(pp, pp_t, cb) -- if pp_t ~= nil and not obs.utils.properties[pp] then -- obs.utils.properties[pp]=pp_t; -- end local pp_unique_name = obslua.obs_property_name(pp) local obs_pp_t = pp; -- extra -- onchange [Event Handler] local __onchange_list = {} local item = nil; local objText; local objInput; local objGlobal; objGlobal = { cb = cb, disable = function() obslua.obs_property_set_disabled(pp, true) return nil end, enable = function() obslua.obs_property_set_disabled(obs_pp_t, false) return nil end, onchange = function(callback) if type(callback) ~= "function" then return false end table.insert(__onchange_list, callback) return true end, hide = function() obslua.obs_property_set_visible(obs_pp_t, false) end, show = function() obslua.obs_property_set_visible(obs_pp_t, true) return nil end, get = function() return obs_pp_t end, hint = function(txt) if txt == nil or type(txt) ~= "string" or txt == "" then return obslua.obs_property_long_description(obs_pp_t) end item = obslua.obs_property_set_long_description(obs_pp_t, txt) return nil end, free = function() obs.utils.properties[pp_unique_name] = nil local pv = obslua.obs_properties_get_parent(pp_t) obslua.obs_properties_remove_by_name(pp_t, pp_unique_name) while pv do obslua.obs_properties_remove_by_name(pv, pp_unique_name) pv = obslua.obs_properties_get_parent(pv) end return true end, remove = function() return objGlobal.free() end, data = pp, item = pp, title = function(txt) if txt == nil or type(txt) ~= "string" then return obslua.obs_property_description(pp) end obslua.obs_property_set_description(pp, txt) return objGlobal end, parent = pp_t }; objText = { error = function(txt) if txt == nil or type(txt) ~= "string" then return obslua.obs_property_description(pp) end obslua.obs_property_text_set_info_type(pp, obslua.OBS_TEXT_INFO_ERROR) obslua.obs_property_set_description(pp, txt) return objText end, text = function(txt) local id_name = obslua.obs_property_name(pp) objText.type = obs.enum.text.default obslua.obs_property_text_set_info_type(pp, objText.type) if txt ~= nil and type(txt) == "string" then obslua.obs_property_set_description(pp, txt) end return objText end, warn = function(txt) local id_name = obslua.obs_property_name(pp) local textarea_id = id_name .. "_obsapi_hotfix_textarea" local input_id = id_name .. "_obsapi_hotfix_input" local property = obs.script.get(id_name) local textarea_property = obs.script.get(textarea_id) local input_property = obs.script.get(input_id) objText.type = obs.enum.text.input if property then property.show() end if input_property then input_property.hide() end if textarea_property then textarea_property.hide() end objText.type = obs.enum.text.warn obslua.obs_property_text_set_info_type(pp, objText.type) if txt ~= nil and type(txt) == "string" then obslua.obs_property_set_description(pp, txt) end return objText end, type = -1 }; objInput = { value = obs.expect(function(txt) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if txt ~= nil and type(txt) == "string" then if settings then settings.str(pp_unique_name, txt) end end if settings then return settings.str(pp_unique_name) end return nil end), type = -1 }; local objOption; objOption = { item = nil, clear = function() objOption.item = obslua.obs_property_list_clear(pp) return objOption end, add = { str = function(title, id) if id == nil or type(id) ~= "string" or id == "" then --id= obs.utils.get_unique_id(20) obslua.script_log(obslua.LOG_INFO, "[obs.script.options.str] id is nil or invalid!") return objOption end objOption.item = obslua.obs_property_list_add_string(pp, title, id) return objOption end, int = function(title, id) if id == nil or type(id) ~= "number" then --id= obs.utils.get_unique_id(20) obslua.script_log(obslua.LOG_INFO, "[obs.script.options.int] id is nil or invalid!") return objOption end objOption.item = obslua.obs_property_list_add_int(pp, title, id) return objOption end, dbl = function(title, id) if id == nil or type(id) ~= "number" then --id= obs.utils.get_unique_id(20) obslua.script_log(obslua.LOG_INFO, "[obs.script.options.dbl] id is nil or invalid!") return objOption end objOption.item = obslua.obs_property_list_add_float(pp, title, id) return objOption end, bul = function(title, id) if id == nil or type(id) ~= "boolean" then id = obs.utils.get_unique_id(20) end objOption.item = obslua.obs_property_list_add_bool(pp, title, id) return objOption end }, cursor = function(index) if index == nil or type(index) ~= "number" or index < 0 then if type(index) == "string" then -- find the index by the id value for i = 0, obslua.obs_property_list_item_count(pp) - 1 do if obslua.obs_property_list_item_string(pp, i) == index then index = i break end end if type(index) ~= "number" then return nil end else index = objOption.item; if type(index) ~= "number" or index < 0 then index = obslua.obs_property_list_item_count(pp) - 1 end end end local info_title; local info_id info_title = obslua.obs_property_list_item_name(pp, index) if obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.string then info_id = obslua.obs_property_list_item_string(pp, index) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.int then info_id = obslua.obs_property_list_item_int(pp, index) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.float then info_id = obslua.obs_property_list_item_float(pp, index) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.bool then info_id = obslua.obs_property_list_item_bool(pp, index) else info_id = nil end local nn_obj = nil; nn_obj = { disable = function() obslua.obs_property_list_item_disable(pp, index, true) return nn_obj end, enable = function() obslua.obs_property_list_item_disable(pp, index, false) return nn_obj end, remove = function() obslua.obs_property_list_item_remove(pp, index) return true end, title = info_title, value = info_id, index = index, ret = function() return objOption end, isDisabled = function() return obslua.obs_property_list_item_disabled(pp, index) end } return nn_obj; end, current = function() local current_selected_option = nil local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.string then current_selected_option = settings.str(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.int then current_selected_option = settings.int(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.float then current_selected_option = settings.float(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.bool then current_selected_option = settings.bool(pp_unique_name) end return objOption.cursor(current_selected_option) end }; local fr_rt = false local objButton; objButton = { item = nil, click = function(callback) if type(callback) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[button.click] invalid callback type " .. type(callback) .. " expected function") return objButton end local tk = os.clock() objButton.item = obslua.obs_property_set_modified_callback(pp, function(properties_t, property_t, obs_data_t) if os.clock() - tk <= 0.01 then return true end return callback(properties_t, property_t, obs.PairStack(obs_data_t, nil, nil, true)) end) return objButton end, text = function(txt) if txt == nil or type(txt) ~= "string" or txt == "" then return obslua.obs_property_description(pp) end obslua.obs_property_set_description(pp, txt) return objButton end, url = function(url) if not url or type(url) ~= "string" or url == "" then obslua.script_log(obslua.LOG_ERROR, "[button.url] invalid url type, expected string, got " .. type(url)) return objButton --obslua.obs_property_button_get_url(pp) end obslua.obs_property_button_set_url(pp, url) return objButton end, type = function(button_type) if button_type == nil or (button_type ~= obs.enum.button.url and button_type ~= obs.enum.button.default) then obslua.script_log(obslua.LOG_ERROR, "[button.type] invalid type, expected obs.enum.button.url | obs.enum.button.default, got " .. type(button_type)) return objButton --obslua.obs_property_button_get_type(pp) end obslua.obs_property_button_set_type(pp, button_type) return objButton end }; -- [[ GROUP ]] local objGroup; objGroup = { }; -- local objBool; objBool = { checked = function(bool_value) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if not settings then obslua.script_log(obslua.LOG_ERROR, "[obs.utils.settings] is not set, please use 'script_load' to set it") return nil end local property_id = obslua.obs_property_name(pp) if bool_value == nil or type(bool_value) ~= "boolean" then return settings.get_bul(property_id) end settings.bul(property_id, bool_value) return objBool end, }; local objColor; objColor = { value = obs.expect(function(r_color, g_color, b_color, alpha_value) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if r_color == nil then return settings.int(pp_unique_name) end if type(r_color) ~= "number" or type(g_color) ~= "number" or type(b_color) ~= "number" then return false end if alpha_value == nil then alpha_value = 1 end local color_value = bit.bor( bit.lshift(alpha_value * 255, 24), bit.lshift(b_color, 16), bit.lshift(g_color, 8), r_color ) --(alpha_value << 24) | (b_color << 16) | (g_color << 8) | r_color settings.int(pp_unique_name, color_value) return color_value end), type = obslua.OBS_PROPERTY_COLOR_ALPHA } local objList; objList = { insert = function(value, selected, hidden) if type(value) ~= "string" then return objList end if type(selected) ~= "boolean" then selected = false end if type(hidden) ~= "boolean" then hidden = false end local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end local unique_id = obs.utils.get_unique_id(20) local obs_data_t = obs.PairStack() obs_data_t.str("value", value) obs_data_t.bul("selected", selected) obs_data_t.bul("hidden", hidden) obs_data_t.str("uuid", unique_id) local obs_curr_data_t = settings.arr(pp_unique_name) obs_curr_data_t.insert(obs_data_t.data) obs_data_t.free(); obs_curr_data_t.free() return objList end, filter = function() return obslua.obs_property_editable_list_filter(pp) end, default = function() return obslua.obs_property_editable_list_default_path(pp) end, type = function() return obslua.obs_property_editable_list_type(pp) end, }; local objNumber; objNumber = { suffix = function(text) obslua.obs_property_float_set_suffix(pp, text) obslua.obs_property_int_set_suffix(pp, text) return objNumber end, value = function(value) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if objNumber.type == obs.enum.number.int then settings.int(pp_unique_name, value) elseif objNumber.type == obs.enum.number.float then settings.dbl(pp_unique_name, value) else return nil end return value end, type = nil } local property_type = obslua.obs_property_get_type(pp) -- [[ ON-CHANGE EVENT HANDLE FOR ANY KIND OF USER INTERACTIVE INPUT ]] if property_type == obslua.OBS_PROPERTY_COLOR or property_type == obslua.OBS_PROPERTY_COLOR_ALPHA or property_type == obslua.OBS_PROPERTY_BOOL or property_type == obslua.OBS_PROPERTY_LIST or property_type == obslua.OBS_PROPERTY_EDITABLE_LIST or property_type == obslua.OBS_PROPERTY_PATH or (property_type == obslua.OBS_PROPERTY_TEXT and ( obslua.obs_property_text_type(pp) == obs.enum.text.textarea or obslua.obs_property_text_type(pp) == obs.enum.text.input or obslua.obs_property_text_type(pp) == obs.enum.text.password )) then local tk = os.clock() obslua.obs_property_set_modified_callback(obs_pp_t, function(properties_t, property_t, settings) if os.clock() - tk <= 0.01 then return true end settings = obs.PairStack(settings, nil, nil, true) local pp_unique_name = obslua.obs_property_name(property_t) local current_value; property_type = obslua.obs_property_get_type(property_t) if property_type == obslua.OBS_PROPERTY_BOOL then current_value = settings.bul(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_TEXT or property_type == obslua.OBS_PROPERTY_PATH or property_type == obslua.OBS_PROPERTY_BUTTON then current_value = settings.str(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_INT or property_type == obslua.OBS_PROPERTY_COLOR_ALPHA or property_type == obslua.OBS_PROPERTY_COLOR then current_value = settings.int(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_FLOAT then current_value = settings.dbl(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_LIST then if obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.string then current_value = settings.str(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.int then current_value = settings.int(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.float then current_value = settings.dbl(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.bool then current_value = settings.bul(pp_unique_name) end elseif property_type == obslua.OBS_PROPERTY_FONT then current_value = settings.obj(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_EDITABLE_LIST then current_value = settings.arr(pp_unique_name) end local result = nil for _, vclb in pairs(__onchange_list) do local temp = vclb(current_value, obs.script.get(obslua.obs_property_name(property_t)), properties_t, settings) if result == nil then result = temp end end if type(current_value) == "table" then current_value.free() end return result end); end if property_type == obslua.OBS_PROPERTY_GROUP then obs.utils.table.append(objGroup, objGlobal) return objGroup; elseif property_type == obslua.OBS_PROPERTY_EDITABLE_LIST then obs.utils.table.append(objList, objGlobal) return objList elseif property_type == obslua.OBS_PROPERTY_LIST then obs.utils.table.append(objOption, objGlobal) return objOption; elseif property_type == obslua.OBS_PROPERTY_INT or property_type == obslua.OBS_PROPERTY_FLOAT then obs.utils.table.append(objNumber, objGlobal) return objNumber elseif property_type == obslua.OBS_PROPERTY_BUTTON then obs.utils.table.append(objButton, objGlobal) return objButton elseif property_type == obslua.OBS_PROPERTY_COLOR_ALPHA or property_type == obslua.OBS_PROPERTY_COLOR then obs.utils.table.append(objColor, objGlobal) return objColor elseif property_type == obslua.OBS_PROPERTY_TEXT then local obj_enum_type_id = obslua.obs_property_text_type(pp) if obj_enum_type_id == obs.enum.text.textarea or obj_enum_type_id == obs.enum.text.input or obj_enum_type_id == obs.enum.text.password then objInput.type = obj_enum_type_id obs.utils.table.append(objInput, objGlobal) return objInput; else objText.type = obj_enum_type_id obs.utils.table.append(objText, objGlobal) return objText; end elseif property_type == obslua.OBS_PROPERTY_BOOL then obs.utils.table.append(objBool, objGlobal) return objBool; else return objGlobal; end end function obs.utils.get_unique_id(rs, i, mpc, cmpc) if type(rs) ~= "number" then rs = 2 end local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" if i == nil then i = true; end if mpc == nil or type(mpc) ~= "string" then mpc = tostring(os.time()); mpc = obs.utils.get_unique_id(rs, false, mpc, true) elseif cmpc == true then chars = mpc end local index = math.random(1, #chars) local c = chars:sub(index, index) if c == nil then c = "" end if rs <= 0 then return c; end local val = obs.utils.get_unique_id(rs - 1, false, mpc, cmpc) if i == true and mpc ~= nil and type(mpc) == "string" and #val > 1 then val = val .. "_" .. mpc end return c .. val end function obs.utils.table.append(tb, vv) for k, v in pairs(vv) do if type(v) == "function" then local old_v = v v = function(...) local retValue = old_v(...) if retValue == nil then return tb; end return retValue; end end if type(k) == "string" then tb[k] = v; else table.insert(tb, k, v) end end end function obs.utils.json(s) local i = 1 local function v() i = s:find("%S", i) -- Find next non-whitespace if not i then return nil end local c = s:sub(i, i); i = i + 1 if c == '{' then local r = {} if s:match("^%s*}", i) then i = s:find("}", i) + 1 return r end repeat local k = v() i = s:find(":", i) + 1 r[k] = v() i = s:find("[%,%}]", i) local x = s:sub(i, i) i = i + 1 until x == '}' return r elseif c == '[' then local r = {} if s:match("^%s*]", i) then i = s:find("]", i) + 1 return r end repeat r[#r + 1] = v() i = s:find("[%,%]]", i) local x = s:sub(i, i) i = i + 1 until x == ']' return r elseif c == '"' then local _, e = i, i repeat _, e = s:find('"', e) until s:sub(e - 1, e - 1) ~= "\\" local res = s:sub(i, e - 1):gsub("\\", "") i = e + 1 return res end local n = s:match("^([%-?%d%.eE]+)()", i - 1) if n then i = i + #n - 1 return tonumber(n) end local l = { t = true, f = false, n = nil } i = i + (c == 'f' and 4 or 3) return l[c] end return v() end -- [[ API UTILS END ]] -- [[ OBS CUSTOM API END ]]