Skip to content

Commit c0228de

Browse files
committed
Improve headless mode: implement Inflate/Deflate, NewFileSearch, and path resolution
- Inflate/Deflate via LuaJIT FFI + system zlib (Windows, Linux, macOS) - NewFileSearch with glob support (dir /b on Windows, ls on Unix) - Path resolution via debug.getinfo instead of empty strings - Add runtime/lua/ to package.path for headless module loading - UTF-8 require fallback for headless mode - Stdio JSON-RPC API activation via POB_API_STDIO=1 env var
1 parent 9b54d98 commit c0228de

1 file changed

Lines changed: 194 additions & 15 deletions

File tree

src/HeadlessWrapper.lua

Lines changed: 194 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,81 @@ function GetAsyncCount()
7575
return 0
7676
end
7777

78-
-- Search Handles
79-
function NewFileSearch() end
78+
-- [PoEW Patch] Basic file search for headless mode
79+
-- Supports exact paths and simple glob patterns (e.g. "*.part*")
80+
do
81+
local function matchGlob(filename, pattern)
82+
local luaPat = pattern:gsub("%.", "%%."):gsub("%*", ".*")
83+
return filename:match("^" .. luaPat .. "$") ~= nil
84+
end
85+
86+
local IS_WINDOWS = package.config:sub(1,1) == "\\"
87+
88+
local function listDir(dir)
89+
local files = {}
90+
local cmd
91+
if IS_WINDOWS then
92+
cmd = 'dir /b "' .. dir:gsub("/", "\\") .. '" 2>nul'
93+
else
94+
cmd = 'ls -1 "' .. dir .. '" 2>/dev/null'
95+
end
96+
local handle = io.popen(cmd)
97+
if handle then
98+
for line in handle:lines() do
99+
line = line:gsub("%s+$", "") -- trim trailing \r on Windows
100+
if line ~= "" then
101+
files[#files + 1] = line
102+
end
103+
end
104+
handle:close()
105+
end
106+
return files
107+
end
108+
109+
local fileSearchClass = {}
110+
fileSearchClass.__index = fileSearchClass
111+
112+
function fileSearchClass:GetFileName()
113+
return self.files[self.index]
114+
end
115+
116+
function fileSearchClass:GetFileModifiedTime()
117+
-- Return a fixed timestamp; the .bin cache check will just re-extract
118+
return 0
119+
end
120+
121+
function fileSearchClass:NextFile()
122+
self.index = self.index + 1
123+
return self.index <= #self.files
124+
end
125+
126+
function NewFileSearch(pattern)
127+
if not pattern or pattern == "" then return nil end
128+
local dir = pattern:gsub("[/\\][^/\\]*$", "")
129+
local filePattern = pattern:gsub(".*[/\\]", "")
130+
131+
if not filePattern:find("[%*%?]") then
132+
-- Exact path — just check existence
133+
local f = io.open(pattern, "r")
134+
if not f then return nil end
135+
f:close()
136+
local obj = setmetatable({ files = { filePattern }, index = 1, dir = dir }, fileSearchClass)
137+
return obj
138+
end
139+
140+
-- Glob pattern — list directory and filter
141+
local entries = listDir(dir)
142+
local matched = {}
143+
for _, name in ipairs(entries) do
144+
if matchGlob(name, filePattern) then
145+
matched[#matched + 1] = name
146+
end
147+
end
148+
table.sort(matched)
149+
if #matched == 0 then return nil end
150+
return setmetatable({ files = matched, index = 1, dir = dir }, fileSearchClass)
151+
end
152+
end
80153

81154
-- General Functions
82155
function SetWindowTitle(title) end
@@ -88,32 +161,83 @@ function ShowCursor(doShow) end
88161
function IsKeyDown(keyName) end
89162
function Copy(text) end
90163
function Paste() end
91-
function Deflate(data)
92-
-- TODO: Might need this
93-
return ""
94-
end
95-
function Inflate(data)
96-
-- TODO: And this
97-
return ""
164+
-- [PoEW Patch] Inflate/Deflate via LuaJIT FFI + system zlib
165+
do
166+
local ok, ffi = pcall(require, "ffi")
167+
if ok and ffi then
168+
ffi.cdef[[
169+
unsigned long compressBound(unsigned long sourceLen);
170+
int compress2(uint8_t *dest, unsigned long *destLen,
171+
const uint8_t *source, unsigned long sourceLen, int level);
172+
int uncompress(uint8_t *dest, unsigned long *destLen,
173+
const uint8_t *source, unsigned long sourceLen);
174+
]]
175+
local zlib
176+
-- Try loading zlib from common locations (Windows, Linux, macOS)
177+
local libs = { "zlib1", "z", "libz.so.1", "libz.1.dylib" }
178+
for _, name in ipairs(libs) do
179+
local lok, lib = pcall(ffi.load, name)
180+
if lok then zlib = lib; break end
181+
end
182+
if zlib then
183+
function Deflate(data)
184+
if not data or #data == 0 then return "" end
185+
local srcLen = #data
186+
local bound = zlib.compressBound(srcLen)
187+
local buf = ffi.new("uint8_t[?]", bound)
188+
local destLen = ffi.new("unsigned long[1]", bound)
189+
local ret = zlib.compress2(buf, destLen, data, srcLen, 9)
190+
if ret ~= 0 then return "" end
191+
return ffi.string(buf, destLen[0])
192+
end
193+
function Inflate(data)
194+
if not data or #data == 0 then return "" end
195+
local srcLen = #data
196+
-- Try progressively larger buffers (data can expand 10-50x)
197+
for mult = 10, 100, 10 do
198+
local destSize = srcLen * mult
199+
local buf = ffi.new("uint8_t[?]", destSize)
200+
local destLen = ffi.new("unsigned long[1]", destSize)
201+
local ret = zlib.uncompress(buf, destLen, data, srcLen)
202+
if ret == 0 then
203+
return ffi.string(buf, destLen[0])
204+
end
205+
-- ret == -5 means buffer too small, try larger
206+
if ret ~= -5 then return "" end
207+
end
208+
return ""
209+
end
210+
print("zlib loaded via FFI — Inflate/Deflate available")
211+
else
212+
print("WARNING: zlib not found — timeless jewel data unavailable")
213+
function Deflate(data) return "" end
214+
function Inflate(data) return "" end
215+
end
216+
else
217+
function Deflate(data) return "" end
218+
function Inflate(data) return "" end
219+
end
98220
end
99221
function GetTime()
100222
return 0
101223
end
224+
-- [PoEW Patch] Return actual paths for data file resolution
102225
function GetScriptPath()
103-
return ""
226+
return _G.POB_SCRIPT_DIR or "."
104227
end
105228
function GetRuntimePath()
106-
return ""
229+
local base = _G.POB_SCRIPT_DIR or "."
230+
return base .. "/../runtime"
107231
end
108232
function GetUserPath()
109-
return ""
233+
return _G.POB_SCRIPT_DIR or "."
234+
end
235+
function GetWorkDir()
236+
return _G.POB_SCRIPT_DIR or ""
110237
end
111238
function MakeDir(path) end
112239
function RemoveDir(path) end
113240
function SetWorkDir(path) end
114-
function GetWorkDir()
115-
return ""
116-
end
117241
function LaunchSubScript(scriptText, funcList, subList, ...) end
118242
function AbortSubScript(ssID) end
119243
function IsSubScriptRunning(ssID) end
@@ -170,16 +294,56 @@ function GetCloudProvider(fullPath)
170294
end
171295

172296

297+
298+
-- [PoEW Patch] Determine script directory for robust path resolution
299+
-- Uses debug.getinfo instead of io.popen('pwd') for cross-platform support
300+
local function _poew_get_script_dir()
301+
local info = debug and debug.getinfo and debug.getinfo(1, 'S')
302+
local src = info and info.source or ''
303+
if type(src) == 'string' and src:sub(1,1) == '@' then
304+
local path = src:sub(2)
305+
local dir = (path:gsub('[^/\\]+$', '')):gsub('[/\\]$', '')
306+
if dir ~= '' then return dir end
307+
end
308+
-- Fallback: probe for known files
309+
local probes = { '.', 'src' }
310+
for _, d in ipairs(probes) do
311+
local fh = io.open(d .. '/HeadlessWrapper.lua', 'r')
312+
if fh then fh:close(); return d end
313+
end
314+
return '.'
315+
end
316+
_G.POB_SCRIPT_DIR = _poew_get_script_dir()
317+
173318
local l_require = require
174319
function require(name)
175320
-- Hack to stop it looking for lcurl, which we don't really need
176321
if name == "lcurl.safe" then
177322
return
178323
end
324+
-- [PoEW Patch] UTF-8 fallback for headless mode (no native .so on macOS)
325+
if name == "lua-utf8" then
326+
local ok, mod = pcall(l_require, name)
327+
if ok and type(mod) == 'table' then return mod end
328+
local dir = _G.POB_SCRIPT_DIR or '.'
329+
local fok, fmod = pcall(dofile, dir .. '/lua-utf8.lua')
330+
if fok and type(fmod) == 'table' then return fmod end
331+
return {}
332+
end
179333
return l_require(name)
180334
end
181335

182336

337+
-- [PoEW Patch] Add runtime Lua libraries to package.path
338+
local base = _G.POB_SCRIPT_DIR or "."
339+
local runtimeLua = base .. "/../runtime/lua"
340+
local testPath = runtimeLua .. "/xml.lua"
341+
local fh = io.open(testPath, "r")
342+
if fh then
343+
fh:close()
344+
package.path = runtimeLua .. "/?.lua;" .. runtimeLua .. "/?/init.lua;" .. package.path
345+
end
346+
183347
dofile("Launch.lua")
184348

185349
-- Prevents loading of ModCache
@@ -217,3 +381,18 @@ function loadBuildFromJSON(getItemsJSON, getPassiveSkillsJSON)
217381
-- You now have a build without a correct main skill selected, or any configuration options set
218382
-- Good luck!
219383
end
384+
385+
-- [PoEW Patch] CLI flag detection
386+
local function _poew_has_flag(flag)
387+
if type(arg) ~= 'table' then return false end
388+
for i = 1, #arg do if arg[i] == flag then return true end end
389+
return false
390+
end
391+
392+
-- [PoEW Patch] JSON-RPC API server activation (env-gated)
393+
-- Set POB_API_STDIO=1 or pass --stdio to start the API server
394+
if os.getenv('POB_API_STDIO') == '1' or _poew_has_flag('--stdio') then
395+
local srvPath = (_G.POB_SCRIPT_DIR or '.') .. '/API/Server.lua'
396+
dofile(srvPath)
397+
return
398+
end

0 commit comments

Comments
 (0)