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:
parent
8411838636
commit
3b759294df
11 changed files with 419 additions and 19 deletions
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
16
files/scripts/compat/forward-compatible.lua
Normal file
16
files/scripts/compat/forward-compatible.lua
Normal 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
|
100
files/scripts/compat/internal/loader.lua
Normal file
100
files/scripts/compat/internal/loader.lua
Normal 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")
|
107
files/scripts/compat/internal/old_1.1.lua
Normal file
107
files/scripts/compat/internal/old_1.1.lua
Normal 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")
|
99
files/scripts/compat/internal/oldfunctions.lua
Normal file
99
files/scripts/compat/internal/oldfunctions.lua
Normal 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)
|
5
files/scripts/compat/legacy.lua
Normal file
5
files/scripts/compat/legacy.lua
Normal 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")
|
22
files/scripts/compat/no-deprecated.lua
Normal file
22
files/scripts/compat/no-deprecated.lua
Normal 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
|
Loading…
Reference in a new issue