diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7bc6b..8640112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ ### Added +- **VR works from an installed release.** The OpenXR loader used to be + looked for only against the working directory and the game's source -- + right for the dev tree, wrong for a release install, where importing + the mod lands it in the game's SAVE DIRECTORY (or keeps it zipped) and + the VR row silently failed to start. The search now also asks the + mount that actually holds the mod and the save directory itself; and + if nothing on disk answers -- the mod imported as an archive -- the + DLL is copied once out of the mod into the save directory and loaded + from there, so every install shape reaches the headset. + - **SELECT walks the VOXEL ladder.** In free roam, the pad's SELECT button makes exactly the step hotkey 3 makes -- OFF through the angle rungs to 1ST and round, stepping over FULL, clearing TILT and GBC FX diff --git a/README.md b/README.md index 2536f98..0c6fd81 100644 --- a/README.md +++ b/README.md @@ -44,4 +44,24 @@ alongside. | right stick left / right | *1ST only* — snap-turn 45° | | grip squeeze + raise / lower that hand | *diorama only* — drag the table's height | | head | *1ST and battles* — look; FreeMove walks where you look | -| left hand | *1ST and battles* — the Pokédex: menus, dialogs and the 2D battle screen on its screen | \ No newline at end of file +| left hand | *1ST and battles* — the Pokédex: menus, dialogs and the 2D battle screen on its screen | + +## Licenses + +This mod redistributes one third-party binary: + +- **`assets/vr/openxr_loader.dll`** — the Khronos OpenXR loader + (version 1.0.10.2, x64, unmodified), © The Khronos Group Inc., + licensed under the **Apache License 2.0**. The full license text ships + alongside the DLL at + [`assets/vr/LICENSE-openxr_loader.txt`](assets/vr/LICENSE-openxr_loader.txt), + as the license requires; keep the two files together if you + redistribute this mod. Source: + [KhronosGroup/OpenXR-SDK](https://github.com/KhronosGroup/OpenXR-SDK). + +Everything else in this mod is original to it, except that the voxel +geometry and shape profiles are derived from the tile and sprite data of +the original game, as documented by the +[pret/pokered](https://github.com/pret/pokered) disassembly. No ROM +data, artwork or audio is included; the mod reads the assets the host +game already has. \ No newline at end of file diff --git a/lib/VRXR.lua b/lib/VRXR.lua index 7e04154..454f7c4 100644 --- a/lib/VRXR.lua +++ b/lib/VRXR.lua @@ -361,18 +361,58 @@ local function check(r, what) end -- Where the loader DLL might really be on disk. mod.path is --- PHYSFS-relative; ffi.load needs an OS path, so try it against the --- process's working directory and against the game's source directory. +-- PHYSFS-relative; ffi.load needs an OS path. The dev tree answers to +-- the working directory or the source directory -- but an INSTALLED +-- release is neither: importing a mod there lands it in the game's SAVE +-- DIRECTORY, so the mount that actually holds the mod +-- (getRealDirectory) and the save directory itself are both tried too. +local DLL_REL = "assets/vr/openxr_loader.dll" + local function loaderCandidates() - local rel = (V.path or "mods/DramaticShapeVoxelMod") - .. "/assets/vr/openxr_loader.dll" + local rel = (V.path or "mods/DramaticShapeVoxelMod") .. "/" .. DLL_REL local list = { rel } + local okR, real = pcall(function() + return love.filesystem.getRealDirectory(rel) + end) + if okR and type(real) == "string" then + list[#list + 1] = real .. "/" .. rel + end local ok, src = pcall(function() return love.filesystem.getSource() end) if ok and type(src) == "string" then list[#list + 1] = src .. "/" .. rel end + local okS, save = pcall(function() + return love.filesystem.getSaveDirectory() + end) + if okS and type(save) == "string" then list[#list + 1] = save .. "/" .. rel end list[#list + 1] = "openxr_loader" -- last resort: the system search path return list end +VRXR._loaderCandidates = loaderCandidates -- named for the suite + +-- The escape hatch for a mod imported as an ARCHIVE (or mounted anywhere +-- else ffi.load cannot see): copy the DLL out of the mount into the save +-- directory -- a real folder on every platform -- and load it from +-- there. Written once and reused; rewritten only if the size changed (a +-- mod update shipping a new loader). +local EXTRACTED = "dramatic_shape_openxr_loader.dll" + +local function extractLoader() + local okB, bytes = pcall(function() + return V.mod and V.mod.read and V.mod:read(DLL_REL) + end) + if not (okB and type(bytes) == "string" and #bytes > 0) then return nil end + local ok, path = pcall(function() + local have = love.filesystem.getInfo and love.filesystem.getInfo(EXTRACTED) + if not (have and have.size == #bytes) then + if not love.filesystem.write(EXTRACTED, bytes) then return nil end + end + local save = love.filesystem.getSaveDirectory() + if type(save) ~= "string" then return nil end + return save .. "/" .. EXTRACTED + end) + return ok and path or nil +end + local function loadLoader() ffi = require("ffi") pcall(ffi.cdef, CDEF) @@ -383,8 +423,15 @@ local function loadLoader() if ok then return lib end errs[#errs + 1] = tostring(lib) end - error("openxr_loader.dll not loadable (is the mod running from a real " - .. "folder, not an archive?): " .. table.concat(errs, " | "), 0) + -- nothing loadable in place: extract a copy to the save directory + local extracted = extractLoader() + if extracted then + local ok, lib = pcall(ffi.load, extracted) + if ok then return lib end + errs[#errs + 1] = tostring(lib) + end + error("openxr_loader.dll not loadable from the mod, the save directory " + .. "or the system: " .. table.concat(errs, " | "), 0) end local function makeSwapchain(w, h, format) diff --git a/mod.card b/mod.card index 1d7ba9b..4866e2d 100644 --- a/mod.card +++ b/mod.card @@ -47,7 +47,7 @@ return { "1ST needs the 3D pass like every rung; without it the level still persists but the world stays 2D and the grid walk stays in charge", "in 1ST, scripted walks, ledge hops and spinner slides play out as the grid moves they are, with the camera riding along; free control resumes when they land", "rooms have no ceilings, so a first-person look over an interior wall shows the void the diorama always had behind it", - "VR is Windows x64 only (the shipped loader and the Win32 GL binding): on any other platform -- mobile above all -- the VR row is absent from the OPTIONS menu and the manager's page both, and a stored vr=true carried over in a save is ignored. On Windows it renders the scene once per eye (heavy with WATER FULL or AA up) and cannot start from a mounted .love archive -- the FFI needs the loader DLL on a real disk path; pad/keyboard/mouse keep working alongside the XR controllers", + "VR is Windows x64 only (the shipped loader and the Win32 GL binding): on any other platform -- mobile above all -- the VR row is absent from the OPTIONS menu and the manager's page both, and a stored vr=true carried over in a save is ignored. On Windows it renders the scene once per eye (heavy with WATER FULL or AA up); pad/keyboard/mouse keep working alongside the XR controllers. The loader DLL is found wherever the mod was put -- the dev tree, an installed release's save directory, or an imported archive, from which it is copied once into the save directory so the FFI has a real disk path", "VR on and off are both the VR row's job (options menu or manager); no controller button does either", "the Pokedex needs a TRACKED left controller; without one there is no device in hand and the floating panel carries the UI as before", "while the VR row is ON, 3D-BTL is held ON and BACK SPRITES held OFF (the headset's battle staging assumes both), and both rows leave the OPTIONS menu until VR goes off -- their stored values come back with them", diff --git a/tests/dramatic_shape_test.lua b/tests/dramatic_shape_test.lua index 40cf0b9..0bf3ee2 100644 --- a/tests/dramatic_shape_test.lua +++ b/tests/dramatic_shape_test.lua @@ -3998,6 +3998,17 @@ T.eq(type(VRMod.supported), "function", "the platform gate exists -- off Windows the row is not offered at all") T.eq(VRMod.supported(), true, "and a headless run (no love.system) counts as supported, harmlessly") + +-- the loader search covers every install shape: the mod-relative path +-- first, the system name last, and (with a filesystem to ask) the real +-- mount and the save directory in between +local VRXR_ = run.loader.exports.DRAMATIC_SHAPE.lib.require("VRXR") +local cands = VRXR_._loaderCandidates() +T.check(#cands >= 2, "the loader has candidates to try") +T.check(cands[1]:find("assets/vr/openxr_loader%.dll") ~= nil, + "the first is the mod's own path") +T.eq(cands[#cands], "openxr_loader", + "and the system search path is the last resort") T.eq(type(VRMod.leave), "function", "VR.leave stays as the programmatic door out -- no controller button " .. "is wired to it")