obs = obslua local ffi = require("ffi") pcall(ffi.cdef, [[ typedef struct { long left, top, right, bottom; } OT2AC_RECT; typedef struct { long x, y; } OT2AC_POINT; typedef struct { unsigned long cbSize; OT2AC_RECT rcMonitor; OT2AC_RECT rcWork; unsigned long dwFlags; } OT2AC_MONITORINFO; void *GetTopWindow(void *hwnd); void *GetWindow(void *hwnd, unsigned int cmd); int GetClassNameA(void *hwnd, char *name, int size); int GetWindowTextA(void *hwnd, char *text, int size); int IsWindowVisible(void *hwnd); int IsIconic(void *hwnd); int GetClientRect(void *hwnd, OT2AC_RECT *rect); int ClientToScreen(void *hwnd, OT2AC_POINT *point); unsigned long GetWindowThreadProcessId(void *hwnd, unsigned long *pid); void *MonitorFromWindow(void *hwnd, unsigned long flags); int GetMonitorInfoA(void *monitor, OT2AC_MONITORINFO *info); void *OpenProcess(unsigned long access, int inherit, unsigned long pid); int CloseHandle(void *handle); int QueryFullProcessImageNameA(void *process, unsigned long flags, char *name, unsigned long *size); ]]) local C = ffi.C local GW_HWNDNEXT = 2 local MONITOR_DEFAULTTONEAREST = 2 local PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 local SLOTS = { {key = "main", label = "Game window", title = "Oregon Trail II", class = "MECCWClass"}, {key = "health", label = "Health Status", title = "Health Status", class = ""}, } local cfg = {exe = "otii32", hide = true, scene = ""} local slots = {} local text_buf = ffi.new("char[512]") local pid_buf = ffi.new("unsigned long[1]") local size_buf = ffi.new("unsigned long[1]") local rect = ffi.new("OT2AC_RECT") local point = ffi.new("OT2AC_POINT") local minfo = ffi.new("OT2AC_MONITORINFO") local pid_is_game = {} local pid_cache_size = 0 local function is_game_process(pid) local known = pid_is_game[pid] if known ~= nil then return known end local match = false local process = C.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) if process ~= nil then size_buf[0] = 512 if C.QueryFullProcessImageNameA(process, 0, text_buf, size_buf) ~= 0 then local path = ffi.string(text_buf, size_buf[0]):lower() local name = path:match("[^\\]+$") or path match = cfg.exe == "" or name:find(cfg.exe:lower(), 1, true) ~= nil end C.CloseHandle(process) end if pid_cache_size > 256 then pid_is_game, pid_cache_size = {}, 0 end pid_is_game[pid] = match pid_cache_size = pid_cache_size + 1 return match end local function game_windows() local found = {} local hwnd = C.GetTopWindow(nil) while hwnd ~= nil do if C.IsWindowVisible(hwnd) ~= 0 then C.GetWindowThreadProcessId(hwnd, pid_buf) if is_game_process(pid_buf[0]) then local n = C.GetWindowTextA(hwnd, text_buf, 512) local title = ffi.string(text_buf, n) n = C.GetClassNameA(hwnd, text_buf, 512) found[#found + 1] = {hwnd = hwnd, title = title, class = ffi.string(text_buf, n)} end end hwnd = C.GetWindow(hwnd, GW_HWNDNEXT) end return found end local function client_crop(hwnd) if C.IsIconic(hwnd) ~= 0 or C.GetClientRect(hwnd, rect) == 0 then return nil end point.x, point.y = 0, 0 C.ClientToScreen(hwnd, point) minfo.cbSize = ffi.sizeof(minfo) if C.GetMonitorInfoA(C.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), minfo) == 0 then return nil end local m = minfo.rcMonitor local width, height = tonumber(m.right - m.left), tonumber(m.bottom - m.top) local left, top = tonumber(point.x - m.left), tonumber(point.y - m.top) local right = width - (left + tonumber(rect.right)) local bottom = height - (top + tonumber(rect.bottom)) return { left = math.max(0, left), top = math.max(0, top), right = math.max(0, right), bottom = math.max(0, bottom), width = width, height = height, } end -- every scene's use of the source (or only the chosen scene's), including inside groups and nested scenes local function for_each_item(source_name, fn) local scenes = obs.obs_frontend_get_scenes() if scenes == nil then return end for _, scene_source in ipairs(scenes) do if cfg.scene == "" or obs.obs_source_get_name(scene_source) == cfg.scene then local scene = obs.obs_scene_from_source(scene_source) local item = obs.obs_scene_find_source_recursive(scene, source_name) if item ~= nil then fn(item) end end end obs.source_list_release(scenes) end local crop = obs.obs_sceneitem_crop() local function apply(slot, window) local want = window and client_crop(window.hwnd) if want and not slot.warned then local source = obs.obs_get_source_by_name(slot.source) if source ~= nil then local w, h = obs.obs_source_get_width(source), obs.obs_source_get_height(source) if w > 0 and (w ~= want.width or h ~= want.height) then obs.script_log(obs.LOG_WARNING, string.format( "'%s' is %dx%d, but the monitor with '%s' is %dx%d: capture that monitor in the source", slot.source, w, h, slot.title, want.width, want.height)) slot.warned = true end obs.obs_source_release(source) end end for_each_item(slot.source, function(item) if want then obs.obs_sceneitem_get_crop(item, crop) if crop.left ~= want.left or crop.top ~= want.top or crop.right ~= want.right or crop.bottom ~= want.bottom then crop.left, crop.top, crop.right, crop.bottom = want.left, want.top, want.right, want.bottom obs.obs_sceneitem_set_crop(item, crop) end end if cfg.hide and obs.obs_sceneitem_visible(item) ~= (want ~= nil) then obs.obs_sceneitem_set_visible(item, want ~= nil) end end) end local function crop_now() local windows = game_windows() for _, slot in ipairs(slots) do if slot.source ~= "" then local window = nil for _, w in ipairs(windows) do if (slot.title == "" or w.title == slot.title) and (slot.class == "" or w.class == slot.class) then window = w break end end apply(slot, window) end end end function script_description() return [[Oregon Trail II auto-crop
Crops Display Capture sources to the game's windows (client area only) when you press the button, and can hide a source whose window is closed or minimized. Unlike a Window Capture this also shows the game's dialogs and anything else lying over the window.

Add one Display Capture of the game's monitor for the game window and one for Health Status, pick them below, then press Crop to the windows now (again whenever a window has moved). Only in scene limits the crop (and the hiding) to one scene; the same capture used in other scenes is left as it is.]] end function script_properties() local props = obs.obs_properties_create() local captures = {} local sources = obs.obs_enum_sources() if sources ~= nil then for _, source in ipairs(sources) do if obs.obs_source_get_unversioned_id(source) == "monitor_capture" then captures[#captures + 1] = obs.obs_source_get_name(source) end end obs.source_list_release(sources) end table.sort(captures) for _, s in ipairs(SLOTS) do local list = obs.obs_properties_add_list(props, s.key .. "_source", s.label .. ": Display Capture", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) obs.obs_property_list_add_string(list, "(none)", "") for _, name in ipairs(captures) do obs.obs_property_list_add_string(list, name, name) end end local scene_list = obs.obs_properties_add_list(props, "scene", "Only in scene", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING) obs.obs_property_list_add_string(scene_list, "(all scenes)", "") local scenes = obs.obs_frontend_get_scenes() if scenes ~= nil then for _, scene_source in ipairs(scenes) do local name = obs.obs_source_get_name(scene_source) obs.obs_property_list_add_string(scene_list, name, name) end obs.source_list_release(scenes) end obs.obs_properties_add_bool(props, "hide", "Hide a source whose window is closed or minimized") obs.obs_properties_add_button(props, "crop_now", "Crop to the windows now", function() pid_is_game, pid_cache_size = {}, 0 for _, slot in ipairs(slots) do slot.warned = false end crop_now() return false end) return props end function script_defaults(settings) for _, s in ipairs(SLOTS) do obs.obs_data_set_default_string(settings, s.key .. "_source", "") end obs.obs_data_set_default_bool(settings, "hide", true) obs.obs_data_set_default_string(settings, "scene", "") end function script_update(settings) cfg.hide = obs.obs_data_get_bool(settings, "hide") cfg.scene = obs.obs_data_get_string(settings, "scene") pid_is_game, pid_cache_size = {}, 0 slots = {} for _, s in ipairs(SLOTS) do slots[#slots + 1] = { source = obs.obs_data_get_string(settings, s.key .. "_source"), title = s.title, class = s.class, warned = false, } end end