diff --git a/src/HeadlessWrapper.lua b/src/HeadlessWrapper.lua index 774f3ae214..22442a9e97 100644 --- a/src/HeadlessWrapper.lua +++ b/src/HeadlessWrapper.lua @@ -75,8 +75,81 @@ function GetAsyncCount() return 0 end --- Search Handles -function NewFileSearch() end +-- [PoEW Patch] Basic file search for headless mode +-- Supports exact paths and simple glob patterns (e.g. "*.part*") +do + local function matchGlob(filename, pattern) + local luaPat = pattern:gsub("%.", "%%."):gsub("%*", ".*") + return filename:match("^" .. luaPat .. "$") ~= nil + end + + local IS_WINDOWS = package.config:sub(1,1) == "\\" + + local function listDir(dir) + local files = {} + local cmd + if IS_WINDOWS then + cmd = 'dir /b "' .. dir:gsub("/", "\\") .. '" 2>nul' + else + cmd = 'ls -1 "' .. dir .. '" 2>/dev/null' + end + local handle = io.popen(cmd) + if handle then + for line in handle:lines() do + line = line:gsub("%s+$", "") -- trim trailing \r on Windows + if line ~= "" then + files[#files + 1] = line + end + end + handle:close() + end + return files + end + + local fileSearchClass = {} + fileSearchClass.__index = fileSearchClass + + function fileSearchClass:GetFileName() + return self.files[self.index] + end + + function fileSearchClass:GetFileModifiedTime() + -- Return a fixed timestamp; the .bin cache check will just re-extract + return 0 + end + + function fileSearchClass:NextFile() + self.index = self.index + 1 + return self.index <= #self.files + end + + function NewFileSearch(pattern) + if not pattern or pattern == "" then return nil end + local dir = pattern:gsub("[/\\][^/\\]*$", "") + local filePattern = pattern:gsub(".*[/\\]", "") + + if not filePattern:find("[%*%?]") then + -- Exact path — just check existence + local f = io.open(pattern, "r") + if not f then return nil end + f:close() + local obj = setmetatable({ files = { filePattern }, index = 1, dir = dir }, fileSearchClass) + return obj + end + + -- Glob pattern — list directory and filter + local entries = listDir(dir) + local matched = {} + for _, name in ipairs(entries) do + if matchGlob(name, filePattern) then + matched[#matched + 1] = name + end + end + table.sort(matched) + if #matched == 0 then return nil end + return setmetatable({ files = matched, index = 1, dir = dir }, fileSearchClass) + end +end -- General Functions function SetWindowTitle(title) end @@ -88,32 +161,83 @@ function ShowCursor(doShow) end function IsKeyDown(keyName) end function Copy(text) end function Paste() end -function Deflate(data) - -- TODO: Might need this - return "" -end -function Inflate(data) - -- TODO: And this - return "" +-- [PoEW Patch] Inflate/Deflate via LuaJIT FFI + system zlib +do + local ok, ffi = pcall(require, "ffi") + if ok and ffi then + ffi.cdef[[ + unsigned long compressBound(unsigned long sourceLen); + int compress2(uint8_t *dest, unsigned long *destLen, + const uint8_t *source, unsigned long sourceLen, int level); + int uncompress(uint8_t *dest, unsigned long *destLen, + const uint8_t *source, unsigned long sourceLen); + ]] + local zlib + -- Try loading zlib from common locations (Windows, Linux, macOS) + local libs = { "zlib1", "z", "libz.so.1", "libz.1.dylib" } + for _, name in ipairs(libs) do + local lok, lib = pcall(ffi.load, name) + if lok then zlib = lib; break end + end + if zlib then + function Deflate(data) + if not data or #data == 0 then return "" end + local srcLen = #data + local bound = zlib.compressBound(srcLen) + local buf = ffi.new("uint8_t[?]", bound) + local destLen = ffi.new("unsigned long[1]", bound) + local ret = zlib.compress2(buf, destLen, data, srcLen, 9) + if ret ~= 0 then return "" end + return ffi.string(buf, destLen[0]) + end + function Inflate(data) + if not data or #data == 0 then return "" end + local srcLen = #data + -- Try progressively larger buffers (data can expand 10-50x) + for mult = 10, 100, 10 do + local destSize = srcLen * mult + local buf = ffi.new("uint8_t[?]", destSize) + local destLen = ffi.new("unsigned long[1]", destSize) + local ret = zlib.uncompress(buf, destLen, data, srcLen) + if ret == 0 then + return ffi.string(buf, destLen[0]) + end + -- ret == -5 means buffer too small, try larger + if ret ~= -5 then return "" end + end + return "" + end + print("zlib loaded via FFI — Inflate/Deflate available") + else + print("WARNING: zlib not found — timeless jewel data unavailable") + function Deflate(data) return "" end + function Inflate(data) return "" end + end + else + function Deflate(data) return "" end + function Inflate(data) return "" end + end end function GetTime() return 0 end +-- [PoEW Patch] Return actual paths for data file resolution function GetScriptPath() - return "" + return _G.POB_SCRIPT_DIR or "." end function GetRuntimePath() - return "" + local base = _G.POB_SCRIPT_DIR or "." + return base .. "/../runtime" end function GetUserPath() - return "" + return _G.POB_SCRIPT_DIR or "." +end +function GetWorkDir() + return _G.POB_SCRIPT_DIR or "" end function MakeDir(path) end function RemoveDir(path) end function SetWorkDir(path) end -function GetWorkDir() - return "" -end function LaunchSubScript(scriptText, funcList, subList, ...) end function AbortSubScript(ssID) end function IsSubScriptRunning(ssID) end @@ -170,16 +294,56 @@ function GetCloudProvider(fullPath) end + +-- [PoEW Patch] Determine script directory for robust path resolution +-- Uses debug.getinfo instead of io.popen('pwd') for cross-platform support +local function _poew_get_script_dir() + local info = debug and debug.getinfo and debug.getinfo(1, 'S') + local src = info and info.source or '' + if type(src) == 'string' and src:sub(1,1) == '@' then + local path = src:sub(2) + local dir = (path:gsub('[^/\\]+$', '')):gsub('[/\\]$', '') + if dir ~= '' then return dir end + end + -- Fallback: probe for known files + local probes = { '.', 'src' } + for _, d in ipairs(probes) do + local fh = io.open(d .. '/HeadlessWrapper.lua', 'r') + if fh then fh:close(); return d end + end + return '.' +end +_G.POB_SCRIPT_DIR = _poew_get_script_dir() + local l_require = require function require(name) -- Hack to stop it looking for lcurl, which we don't really need if name == "lcurl.safe" then return end + -- [PoEW Patch] UTF-8 fallback for headless mode (no native .so on macOS) + if name == "lua-utf8" then + local ok, mod = pcall(l_require, name) + if ok and type(mod) == 'table' then return mod end + local dir = _G.POB_SCRIPT_DIR or '.' + local fok, fmod = pcall(dofile, dir .. '/lua-utf8.lua') + if fok and type(fmod) == 'table' then return fmod end + return {} + end return l_require(name) end +-- [PoEW Patch] Add runtime Lua libraries to package.path +local base = _G.POB_SCRIPT_DIR or "." +local runtimeLua = base .. "/../runtime/lua" +local testPath = runtimeLua .. "/xml.lua" +local fh = io.open(testPath, "r") +if fh then + fh:close() + package.path = runtimeLua .. "/?.lua;" .. runtimeLua .. "/?/init.lua;" .. package.path +end + dofile("Launch.lua") -- Prevents loading of ModCache @@ -217,3 +381,18 @@ function loadBuildFromJSON(getItemsJSON, getPassiveSkillsJSON) -- You now have a build without a correct main skill selected, or any configuration options set -- Good luck! end + +-- [PoEW Patch] CLI flag detection +local function _poew_has_flag(flag) + if type(arg) ~= 'table' then return false end + for i = 1, #arg do if arg[i] == flag then return true end end + return false +end + +-- [PoEW Patch] JSON-RPC API server activation (env-gated) +-- Set POB_API_STDIO=1 or pass --stdio to start the API server +if os.getenv('POB_API_STDIO') == '1' or _poew_has_flag('--stdio') then + local srvPath = (_G.POB_SCRIPT_DIR or '.') .. '/API/Server.lua' + dofile(srvPath) + return +end