Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/core/ModCompat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,22 @@ ModCompat.ALIASES = {
["src.render.GbcPalette"] = "src.render.GBCFX",
}

-- Extend the data vocabulary without changing loader permissions or public
-- module objects. Existing aliases retain their native identities.
local Compatibility = require("src.mods.Gen1Compat")
for name, target in pairs(Compatibility.ALIASES) do
if ModCompat.ALIASES[name] == nil then ModCompat.ALIASES[name] = target end
end

local seen = {}
local installed = false

-- Resolve one alias. Returns the module, or nil plus a reason -- the reason
-- matters because "the target is missing too" is a build problem, not a mod
-- problem, and should not be reported as an unknown module.
function ModCompat.resolve(name)
local missing = Compatibility.unavailable(name)
if missing then return nil, missing end
local target = ModCompat.ALIASES[name]
if not target then return nil, nil end
local ok, value = pcall(require, target)
Expand All @@ -128,7 +137,23 @@ function ModCompat.install()
if installed then return false end
local loaders = package.loaders or package.searchers
if type(loaders) ~= "table" then return false end
loaders[#loaders + 1] = function(name)
local searcher
searcher = function(name)
local missing = Compatibility.unavailable(name)
if missing then
-- A later host/plugin provider still outranks an unsupported descriptor.
local following = false
for _, candidate in ipairs(loaders) do
if candidate == searcher then following = true
elseif following then
local provider, origin = candidate(name)
if type(provider) == "function" then return provider, origin end
end
end
-- Raise before require installs its in-progress sentinel. Retrying the
-- import must retain the diagnostic instead of reporting a fake loop.
error(missing, 0)
end
if not ModCompat.ALIASES[name] then
-- the standard "not found" contract: a string the require error joins
return "\n\tno mod-compat alias for '" .. tostring(name) .. "'"
Expand All @@ -142,6 +167,7 @@ function ModCompat.install()
return value
end
end
loaders[#loaders + 1] = searcher
installed = true
return true
end
Expand Down
33 changes: 33 additions & 0 deletions src/mods/Gen1Compat.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Compatibility spellings, kept separate from the loader's execution policy.
-- Only interchangeable native objects belong in ALIASES. Unsupported modules
-- fail at import with an actionable diagnostic instead of a misleading facade.
local C = {}
C.ALIASES = {
['src.world.gen2.WorldAPI'] = 'src.world.WorldAPI',
['src.world.gen2.Follower'] = 'src.world.OptionalFollower',
}
C.UNAVAILABLE = {
['src.import.pmd.Sprite'] = { 'mystery_dungeon_follower' },
['src.world.game3.Follower'] = { 'mystery_dungeon_follower' },
['src.core.gen2.Unown'] = { 'mystery_dungeon_follower' },
['src.core.game3.pokemon'] = { 'mystery_dungeon_follower' },
['src.core.game3.player'] = { 'mystery_dungeon_follower' },
}
-- Concrete host modules and registered providers always take precedence.
function C.hasProvider(name)
if type(name) ~= 'string' then return false end
if type(package.loaded[name]) == 'table' or type(package.preload[name]) == 'function' then return true end
if package.searchpath then
if package.searchpath(name, package.path or '') or package.searchpath(name, package.cpath or '') then return true end
end
local path = name:gsub('%.', '/') .. '.lua'
return love and love.filesystem and love.filesystem.getInfo
and love.filesystem.getInfo(path, 'file') ~= nil or false
end
function C.unavailable(name)
local consumers = C.UNAVAILABLE[name]
if not consumers or C.hasProvider(name) then return nil end
return ('Compatibility module %s is unavailable in this engine; affected mods: %s')
:format(name, table.concat(consumers, ', '))
end
return C
39 changes: 39 additions & 0 deletions tests/engine/compat_module_aliases.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
local T=require('tests.harness')
love=require('tests.love_stub')
local C=require('src.core.ModCompat')
C.install()
T.eq(require('src.world.gen2.WorldAPI'),require('src.world.WorldAPI'),'world API alias preserves object identity')
if loadfile('src/world/OptionalFollower.lua') then
T.eq(require('src.world.gen2.Follower'),require('src.world.OptionalFollower'),'Gen2 follower alias preserves object identity')
else
T.raises(function()require('src.world.gen2.Follower')end,'src.world.OptionalFollower','alias identifies an unavailable native follower provider')
end
local names=require('src.mods.Gen1Compat')
for name in pairs(names.UNAVAILABLE)do
if names.hasProvider(name)then
local ok,module=pcall(require,name)
T.check(ok,'concrete host provider loads '..name)
T.eq(type(module),'table','concrete host provider remains a module '..name)
T.eq(names.unavailable(name),nil,'existing provider is not marked unavailable '..name)
T.eq(require(name),module,'existing provider retains require identity '..name)
T.check(names.hasProvider(name),'provider remains available '..name)
else
local ok,reason=pcall(require,name)
T.check(not ok,'unsupported import fails '..name)
T.check(tostring(reason):find(name,1,true)~=nil,'failure identifies module '..name)
T.check(tostring(reason):find('mystery_dungeon_follower',1,true)~=nil,'failure identifies affected consumer '..name)
T.check(not tostring(reason):find('no file ',1,true),'failure does not masquerade as a filesystem search '..name)
local again,detail=pcall(require,name)
T.check(not again and reason==detail,'repeat import retains the exact diagnostic '..name)
end
end

local providers=package.loaders or package.searchers
local provided={marker='native-late-provider'}
local late=function(name)if name=='src.import.pmd.Sprite'then return function()return provided end end end
providers[#providers+1]=late
T.eq(require('src.import.pmd.Sprite'),provided,'late concrete provider outranks the missing-module descriptor')
T.check(names.hasProvider('src.import.pmd.Sprite'),'loaded provider is recognized')
T.eq(names.unavailable('src.import.pmd.Sprite'),nil,'loaded provider has no missing-module diagnostic')
table.remove(providers);package.loaded['src.import.pmd.Sprite']=nil
T.finish()