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