1
0
Fork 0
mirror of https://github.com/AquariaOSE/Aquaria.git synced 2024-11-25 09:44:02 +00:00

Add support for mod script compatibility layers

This uses a new <Compatibility script="..." /> tag in a mod's XML file.
A compatbility layer is a script that runs before mod-init.lua is loaded,
and before any mod scripts are loaded when resuming a saved game.

This is a better solution than shipping a fragile wrapper
with every mod, that tends to break and then needs to be updated.
Now this wrapper is centralized, easy to use, and easy to update.

Closes #31.
This commit is contained in:
fgenesis 2017-01-21 02:06:44 +01:00
parent 8411838636
commit 3b759294df
11 changed files with 419 additions and 19 deletions

View file

@ -2632,12 +2632,14 @@ std::string Continuity::getSaveFileName(int slot, const std::string &pfix)
return os.str();
}
void Continuity::loadFileData(int slot, XMLDocument &doc)
bool Continuity::loadFileData(int slot, XMLDocument &doc)
{
std::string teh_file = dsq->continuity.getSaveFileName(slot, "aqs");
if(!exists(teh_file))
teh_file = dsq->continuity.getSaveFileName(slot, "bin");
std::string err = "file not exist";
if (exists(teh_file))
{
unsigned long size = 0;
@ -2645,29 +2647,36 @@ void Continuity::loadFileData(int slot, XMLDocument &doc)
if (!buf)
{
errorLog("Failed to decompress save file: " + teh_file);
return;
return false;
}
if (doc.Parse(buf, size) != XML_SUCCESS)
bool good = doc.Parse(buf, size) == XML_SUCCESS;
delete [] buf;
if (good)
return true;
err = doc.GetErrorStr1();
}
else
{
teh_file = dsq->continuity.getSaveFileName(slot, "xml");
if (exists(teh_file))
{
errorLog("Failed to load save data: " + teh_file + " -- Error: " + doc.GetErrorStr1());
return;
if (readXML(teh_file, doc) == XML_SUCCESS)
return true;
err = doc.GetErrorStr1();
}
}
teh_file = dsq->continuity.getSaveFileName(slot, "xml");
if (exists(teh_file))
{
if (readXML(teh_file, doc) != XML_SUCCESS)
errorLog("Failed to load save data: " + teh_file);
}
errorLog("Failed to load save data: " + teh_file + " -- Error: " + err);
return false;
}
void Continuity::loadFile(int slot)
bool Continuity::loadFile(int slot)
{
dsq->user.save();
XMLDocument doc;
loadFileData(slot, doc);
if(!loadFileData(slot, doc))
return false;
XMLElement *startData = doc.FirstChildElement("StartData");
if (startData)
@ -2677,7 +2686,8 @@ void Continuity::loadFile(int slot)
#ifdef AQUARIA_DEMO
exit_error("The demo version does not support loading savegames from mods, sorry.");
#else
dsq->mod.load(startData->Attribute("mod"));
if(!dsq->mod.loadSavedGame(startData->Attribute("mod")))
return false;
#endif
}
}
@ -3201,8 +3211,8 @@ void Continuity::loadFile(int slot)
float timer = strtof(startData->Attribute("webTimer"), NULL);
webTimer.start(timer);
}
}
return true;
}
void Continuity::setNaijaModel(std::string model)

View file

@ -66,8 +66,8 @@ public:
void setStringFlag(std::string flag, std::string v);
void saveFile(int slot, Vector position=Vector(0,0,0), unsigned char *scrShotData=0, int scrShotWidth=0, int scrShotHeight=0);
void loadFileData(int slot, tinyxml2::XMLDocument &doc);
void loadFile(int slot);
bool loadFileData(int slot, tinyxml2::XMLDocument &doc);
bool loadFile(int slot);
void castSong(int num);

View file

@ -2976,8 +2976,8 @@ void DSQ::doSaveSlotMenu(SaveSlotMode ssm, const Vector &position)
}
else if (saveSlotMode == SSM_LOAD)
{
continuity.loadFile(selectedSaveSlot->getSlotIndex());
dsq->game->transitionToScene(dsq->game->sceneToLoad);
if(continuity.loadFile(selectedSaveSlot->getSlotIndex()))
dsq->game->transitionToScene(dsq->game->sceneToLoad);
}
// when gameover hits, load up this instead of that.
}

View file

@ -68,6 +68,7 @@ void Mod::clear()
hasMap = false;
blockEditor = false;
mapRevealMethod = REVEAL_UNSPECIFIED;
compatScript = "";
}
bool Mod::isDebugMenu()
@ -98,6 +99,18 @@ const std::string& Mod::getBaseModPath() const
return baseModPath;
}
bool Mod::loadSavedGame(const std::string& path)
{
load(path);
if(loadCompatScript())
return true;
debugLog("MOD: loadSavedGame/compatScript failed");
setActive(false);
dsq->title();
return false;
}
void Mod::load(const std::string &p)
{
clear();
@ -131,6 +144,12 @@ void Mod::load(const std::string &p)
if (props->Attribute("worldMapRevealMethod"))
mapRevealMethod = (WorldMapRevealMethod) props->IntAttribute("worldMapRevealMethod");
}
XMLElement *compat = mod->FirstChildElement("Compatibility");
if(compat)
{
if(const char *script = compat->Attribute("script"))
compatScript = script;
}
}
dsq->secondaryTexturePath = path + "graphics/";
@ -221,6 +240,15 @@ void Mod::applyStart()
dsq->continuity.reset();
dsq->scriptInterface.reset();
// Before loading init.lua, load a compatibility layer, if necessary
if(!loadCompatScript())
{
debugLog("MOD: compatScript failed");
setActive(false);
dsq->title();
return;
}
// load the mod-init.lua file
// which is in the root of the mod's folder
// e.g. _mods/recachetest/
@ -259,6 +287,7 @@ void Mod::setActive(bool a)
if (!active)
{
dsq->unloadMods();
compatScript = "";
mapRevealMethod = REVEAL_UNSPECIFIED;
setLocalisationModPath("");
@ -326,3 +355,12 @@ ModType Mod::getTypeFromXML(XMLElement *xml) // should be <AquariaMod>...</Aquar
}
return MODTYPE_MOD; // the default
}
bool Mod::loadCompatScript()
{
if(!compatScript.length() || dsq->runScript("scripts/compat/" + compatScript + ".lua"))
return true;
dsq->scriptInterface.reset();
return false;
}

View file

@ -37,6 +37,7 @@ public:
void start();
void stop();
void load(const std::string &path);
bool loadSavedGame(const std::string& path);
void update(float dt);
@ -61,6 +62,7 @@ public:
WorldMapRevealMethod mapRevealMethod;
protected:
bool loadCompatScript();
bool shuttingDown;
bool active;
bool hasMap;
@ -73,6 +75,7 @@ protected:
std::string name;
std::string path;
Precacher modcache;
std::string compatScript;
};

View file

@ -0,0 +1,16 @@
-- Removes more functions than to no-deprecated
-- Some original game scripts will not work with this!
dofile("scripts/compat/no-deprecated.lua")
incrFlag = nil
decrFlag = nil
entity_sound = nil
entity_toggleBone = nil
isPlat = nil -- should not matter on the Lua side what the platform is
toggleVersionLabel = nil
setVersionLabelText = nil
quit = nil -- mods should not do this ever
doModSelect = nil
doLoadMenu = nil
appendUserDataPath = nil

View file

@ -0,0 +1,100 @@
-- Script loader hijack
-- Intercepts loads of script files and patches interface functions on the fly
assert(AQUARIA_VERSION)
local rawset = rawset
local rawget = rawget
local debug = rawget(_G, "debug")
local errorLog_o = assert(errorLog, "errorLog() not present")
-- IMPORTANT that this gets loaded only once. Exit & re-enter the mod if the file was changed.
if rawget(_G, ".__COMPAT_LOADED") then
errorLog_o("ERROR: Compat loader already loaded")
return
end
local function formatStack(lvl)
if debug then
return debug.traceback("", lvl or 1) or "[No traceback available]"
end
return "[No debug library available]"
end
local function errorLogWrap(s, level)
return errorLog_o(s .. "\n" .. formatStack(level or 2))
end
rawset(_G, "errorLog", errorLogWrap)
---------------------------------------------------------------------
---- Create hook scripts for entity interface function detouring ----
---------------------------------------------------------------------
local WARNINGS = isDeveloperKeys()
local HOOKS = dofile("mod-compat.lua") or {}
assert(type(HOOKS) == "table", "mod-compat.lua must return nothing or table, not " .. type(HOOKS))
local v_meta
if WARNINGS then
local function _warnUndefInstance(tab, key)
if WARNINGS then
errorLog("WARNING: script tried to get/call undefined instance variable " .. tostring(key), 3)
rawset(tab, key, false) -- warn only once, not spam
end
end
v_meta = { __index = _warnUndefInstance }
end
-- Callback function, to be called whenever a new script is loaded and stored by the scripting interface.
-- Note: This function must never raise an error, otherwise the program will crash!
local function onCreateScript(scriptname, functable)
debugLog("=== Creating script instance: " .. tostring(scriptname) .. " ===")
assert(type(functable) == "table")
-- detour init() function to patch v to produce reasonable stackdumps,
-- and optionally call custom hook
local oldinit = functable.init
local function newinit(me)
assert(v)
setmetatable(v, v_meta) -- global lookup: uses entity context's v
if oldinit then
oldinit(me)
end
if me and me ~= 0 then
local hook = HOOKS.init
if hook then
return hook(scriptname, me)
end
end
end
functable.init = newinit
local postInitHook = HOOKS.postInit
if postInitHook then
local oldpostinit = functable.postInit
function functable.postInit(me)
oldpostinit(me)
postInitHook(me)
end
end
local f = HOOKS.onCreateScript
if f then
f(scriptname, functable)
end
end
-- Hidden, secret global table that stores interface functions for script templates
local _scriptfuncs = assert(_scriptfuncs, "_scriptfuncs missing")
-- Intercept writes to _scriptfuncs. Game uses lua_setfield() to populate the table, which honors metatables.
setmetatable(_scriptfuncs, {
__newindex = function(tab, scriptname, functable)
onCreateScript(scriptname, functable)
-- do the set, or it will crash
rawset(tab, scriptname, functable)
end
})
rawset(_G, ".__COMPAT_LOADED", true)
debugLog("COMPAT/loader: Installed")

View file

@ -0,0 +1,107 @@
-- Compatibility wrapper [interface detouring]
-- allows to run scripts written for the old script interface (1.1.0, 1.1.1, 1.1.2)
-- on 1.1.3+ / OSE and its new scripting interface.
-- Note that this is a gross hack and a lot of guesswork is involved,
-- but any sanely written mod should run without changes.
-- Notes:
-- * Any assignment to the global "v" will break things. Like v = 0.
-- * Variables whose names only contain all-uppercase, underscores, and numbers are considered globals.
-- Assignments to those will not cause warnings and go to _G.
-- Assignments to non-globals will go to _G.v.
-- Undefined reads from either _G or v will cause a warning (in dev mode).
-- * Mods can provide their own compatibility hooks.
local rawset = rawset
local rawget = rawget
local _G = _G
local debug = rawget(_G, "debug") -- not present in some 1.1.3+ versions
local function looksLikeGlobal(s)
return not s:match("[^_%u%d]")
end
dofile("scripts/compat/internal/oldfunctions.lua")
-- loading entityinclude.lua is no longer necessary and would do more bad than good,
-- so make sure it's not loaded even if scripts explicitly load that file.
local o_dofile = dofile
local function dofileWrap(file)
if file:lower() ~= "scripts/entities/entityinclude.lua" then -- already in
--debugLog("dofile(" .. file .. ")")
return o_dofile(file)
end
end
rawset(_G, "dofile", dofileWrap)
-----------------------------------------------------
---- Fixup functions that differ in behavior now ----
-----------------------------------------------------
local createEntity_o = createEntity
rawset(_G, "createEntity", function(e, name, x, y)
if type(e) == "string" and #e > 0 then
return createEntity_o(e, name, x, y)
end
return 0
end)
-- Prepare interface function lookup table
local INTERFACE_LUT = {}
do
local names = getInterfaceFunctionNames()
--errorLog(table.concat(names, ", "))
for i = 1, #names do
INTERFACE_LUT[names[i]] = true
end
end
-- Detour global reads and writes to the currently active instance table
-- n = 0 will become v.n = 0
-- local x = entity_x(n) will become local x = entity_x(v.n)
-- important here is that no instance-local variables accidentally become globals.
-- globals that get written into the instance table are no problem. (if it ever happens - doubt it)
local _G_meta =
{
__index = function(tab, key)
if not INTERFACE_LUT[key] then
local v = rawget(_G, "v")
return v and v[key]
end
end,
__newindex = function(tab, key, val)
if key == "v" then -- v is set by the engine whenever script context is switched, this is no problem
if val ~= nil and type(val) ~= "table" then
errorLog("WARNING: COMPAT: Setting v to non-table (" .. type(v) .. ")! This is BAD and WILL break something!\n", 3)
-- but do it anyway.
end
rawset(tab, key, val)
return
end
if INTERFACE_LUT[key] then
--debugLog("Setting interface: " .. tostring(key) .. " = " .. tostring(val))
rawset(tab, key, val)
return
end
if type(key) == "string" and looksLikeGlobal(key) then
--debugLog("Setting global: " .. tostring(key) .. " = " .. tostring(val))
rawset(tab, key, val)
return
end
local v = rawget(_G, "v")
if v then
--debugLog("Setting v." .. tostring(key) .. " = " .. tostring(val))
v[key] = val
return
end
errorLog("Variable not set: " .. tostring(key) .. " = " .. tostring(val))
end
}
setmetatable(_G, _G_meta)
rawset(_G, "OLD_SCRIPT_INTERFACE_COMPATIBLE", true) -- mod-init.lua should check for this
debugLog("COMPAT/1.1: Redirecting global writes to v")

View file

@ -0,0 +1,99 @@
-- Registers old functions that have been deprecated in 1.1.1 already,
-- but some mods may still use them.
-- Most that had no function are dummied out, some existed but threw errors,
-- and others were registered under different names.
local WARN_FUNCTIONS =
{
getAngleBetween = true,
getAngleBetweenEntities = true,
getNearestNode = true,
getNodeFromEntity = true,
healEntity = true,
killEntity = true,
entity_warpToPathStart = true,
sendEntityMessage = true,
entity_fireShot = true,
entity_resetTimer = true,
}
local DUMMY_FUNCTIONS =
{
entity_setCollideWithAvatar = true,
entity_setTouchDamage = true,
bone_setTouchDamage = true,
entity_setClampOnSwitchDir = true,
entity_addGroupVel = true,
entity_avgVel = true,
entity_fireAtTarget = true,
entity_flipHToAvatar = true,
entity_getBehaviorType = true,
entity_isSaying = true,
entity_moveTowardsGroupCenter = true,
entity_moveTowardsGroupHeading = true,
entity_say = true,
entity_setAffectedBySpells = true,
entity_setBehaviorType = true,
entity_setNodeGroupActive = true,
entity_setSayPosition = true,
entity_setTouchPush = true,
entity_setPauseInConversation = true,
learnSpell = true,
reloadTextures = true,
setGameOver = true,
setGLNearest = true,
setNaijaModel = true,
shot_setBounceType = true,
showControls = true,
stopCursorGlow = true,
toggleTransitFishRide = true,
entity_stopTimer = true,
}
local REPLACED_FUNCTIONS =
{
-- alternate names
entity_getPositionX = entity_x,
entity_getPositionY = entity_y,
entity_applyRandomForce = entity_addRandomVel,
getNodeByName = getNode,
getEntityByName = getEntity,
entity_flipTo = entity_fhTo,
bone_getidx = bone_getIndex,
}
----------------------------------------------------
---- Functors to generate replacement function -----
----------------------------------------------------
local function mkwarn(name, param)
local err = "Dummy function: " .. name .. "() - no longer present in the current API, fix the script!"
return function() errorLog(err) end
end
local function dummy(name, param)
end
local function mkdummy(name, param)
return dummy
end
local function mkalias(name, param)
return assert(param, name)
end
local function makestubs(tab, gen)
for name, param in pairs(tab) do
if rawget(_G, name) then
errorLog("WARNING: oldfunctions.lua: function " .. name .. " already exists")
else
local f = gen(name, param)
rawset(_G, name, f)
end
end
end
----------------
---- Do it! ----
----------------
makestubs(WARN_FUNCTIONS, mkwarn)
makestubs(DUMMY_FUNCTIONS, mkdummy)
makestubs(REPLACED_FUNCTIONS, mkalias)

View file

@ -0,0 +1,5 @@
-- 1.1.3+ compatibility wrapper
-- allows to run scripts written for the old script interface.
dofile("scripts/compat/internal/loader.lua")
dofile("scripts/compat/internal/old_1.1.lua")

View file

@ -0,0 +1,22 @@
-- Functions unused by game scripts; might be removed at some point
user_set_demo_intro = nil
user_save = nil
entity_followEntity = nil
entityFollowEntity = nil
entity_isFollowingEntity = nil
entity_slowToStopPath = nil
entity_isSlowingToStopPath = nil
entity_isNearGround = nil
entity_resumePath = nil
entity_move = nil
createSpore = nil
spawnAroundEntity = nil
node_isEntityPast = nil
entity_pathBurst = nil
avatar_clampPosition = nil
bedEffects = nil
entity_grabTarget = nil
entity_releaseTarget = nil
entity_offsetUpdate = nil
avatar_updatePosition = nil