Fix compatibility stuff

- Make dofile more conform to standard lua
- Move dofile from live-reload.lua to compatibility.lua
- Let dofile and require throw errors on script errors
- Fix bug in recursion detection
- Remove Noita's dofile annotation
- Fix some EmmyLua annotations
- Improve print replacement
This commit is contained in:
David Vogel 2022-07-30 00:32:11 +02:00
parent e863ba459b
commit 403167b366
4 changed files with 48 additions and 50 deletions

View File

@ -1416,10 +1416,6 @@ function SetValueBool(key, value) end
---@param default_value number
---@return boolean
function GetValueBool(key, default_value) end
---Returns the script's return value, if any. Returns nil,error_string if the script had errors.
---@param filename string
---@return any script_return_type, string|nil error_string
function dofile(filename) end
---Runs the script only once per lua context, returns the script's return value, if any. Returns nil,error_string if the script had errors. For performance reasons it is recommended scripts use dofile_once(), unless the standard dofile behaviour is required.
---@param filename string
---@return any script_return_type, string|nil error_string

View File

@ -10,14 +10,16 @@
if _NoitaAPICompatibilityWrapperGuard_ then return function(dummy) end end
_NoitaAPICompatibilityWrapperGuard_ = true
-- Override print function to behave more like the standard lua one.
local oldPrint = print
function print(...)
local arg = { ... }
local stringArgs = {}
for i, v in ipairs(arg) do
table.insert(stringArgs, tostring(v))
-- Emulated print function that behaves more like the standard lua one.
function print(...)
local n = select("#", ...)
--for i, v in ipairs(arg) do
local stringArgs = {}
for i = 1, n do
table.insert(stringArgs, tostring(select(i, ...)))
end
oldPrint(unpack(stringArgs))
@ -42,25 +44,40 @@ package.loaded = package.loaded or {
--os = os,
}
local oldDofile = dofile
---Emulated dofile to execute a lua script from disk and circumvent any caching.
---Noita for some reason caches script files (Or loads them into its virtual filesystem)(Or caches compiled bytecode), so reloading script files from disk does not work without this.
---
---This conforms more with standard lua.
---@param path string
---@return any ...
function dofile(path)
local func, err = loadfile(path)
if not func then error(err) end
return func()
end
local oldRequire = require
local recursionSet = {}
local recursionLast
---Emulated require function in case the Lua API is restricted.
---It's probably good enough for most usecases.
---
---We need to override the default require in any case, as only dofile can access stuff in the virtual filesystem.
---We need to override the default require in any case, as only dofile and loadfile can access stuff in the virtual filesystem.
---@param modName string
---@return any
---@return any ...
function require(modName)
-- Check if package was already loaded, return previous result.
if package.loaded[modName] ~= nil then return package.loaded[modName] end
if recursionSet[modName] then
error(string.format("Cyclic dependency with module %q called by %q", modName, recursionLast))
recursionSet = {}
error(string.format("Cyclic dependency with module %q", modName))
end
recursionSet[modName], recursionLast = true, modName
recursionSet[modName] = true
local notFoundStr = ""
@ -71,7 +88,7 @@ function require(modName)
if res == nil then res = true end
package.loaded[modName] = res
recursionSet[modName], recursionLast = nil, nil
recursionSet[modName] = nil
return res
else
notFoundStr = notFoundStr .. string.format("\tno field package.preload['%s']\n", modName)
@ -84,14 +101,22 @@ function require(modName)
local filePath = string.gsub(pathEntry, "?", modPath, 1) -- Insert modPath into "?" placeholder.
local fixedPath = string.gsub(filePath, "^%.[\\/]", "") -- Need to remove "./" or ".\" at the beginning, as Noita allows only "data" and "mods".
if fixedPath:sub(1, 4) == "data" or fixedPath:sub(1, 4) == "mods" then -- Ignore everything other than data and mod root path elements. It's not perfect, but this is just there to prevent console spam.
local res, err = dofile(fixedPath)
if err then
notFoundStr = notFoundStr .. string.format("\tno file '%s'\n", filePath)
else
local func, err = loadfile(fixedPath)
if func then
local state, res = pcall(func)
if not state then
recursionSet = {}
error(res)
end
if res == nil then res = true end
package.loaded[modName] = res
recursionSet[modName], recursionLast = nil, nil
recursionSet[modName] = nil
return res
elseif err and err:sub(1, 45) == "Error loading lua script - file doesn't exist" then -- I hate to do that.
notFoundStr = notFoundStr .. string.format("\tno file '%s'\n", filePath)
else
recursionSet = {}
error(err)
end
else
notFoundStr = notFoundStr .. string.format("\tnot allowed '%s'\n", filePath)
@ -102,14 +127,14 @@ function require(modName)
if oldRequire then
local ok, res = pcall(oldRequire, modName)
if ok then
recursionSet[modName], recursionLast = nil, nil
recursionSet[modName] = nil
return res
else
notFoundStr = notFoundStr .. string.format("\toriginal require:%s", res)
end
end
recursionSet[modName], recursionLast = nil, nil
recursionSet = {}
error(string.format("module %q not found:\n%s", modName, notFoundStr))
end

View File

@ -5,31 +5,12 @@
-- Allows Noita mods to reload themselves every now and then.
-- This helps dramatically with development, as we don't have to restart Noita for every change.
-- To accomplish this, we need to override the default behavior of dofile and some other things.
local LiveReload = {}
local oldDofile = dofile
---Overwritten dofile to execute a lua script from disk and cirumvent any caching.
---Noita for some reason caches script files (Or loads them into its virtual filesystem)(Or caches compiled bytecode), so reloading script files from disk does not work without this.
---
---This is not fully conform the the standard lua implementation, but so isn't Noita's implementation.
---@param path string
---@return any result
---@return string|nil err
function dofile(path) ---TODO: Consider moving dofile into compatibility.lua
local func, err = loadfile(path)
if not func then return nil, err end
local status, res = pcall(func)
if not status then return nil, res end
return res, nil
end
---Reloads the mod's init file in the given interval in frames.
---For reloading to work correctly, the mod has to be structured in a special way.
---Like the usage of require and namespaces.
---Like the usage of require, namespaces, correct error handling, ...
---
---Just put this into your `OnWorldPreUpdate` or `OnWorldPostUpdate` callback:
---
@ -42,11 +23,7 @@ function LiveReload:Reload(modPath, interval)
if self.Counter < interval then return end
self.Counter = nil
local res, err = dofile(modPath .. "init.lua")
if err then
print(string.format("Error reloading mod: %s", err))
error(string.format("Error reloading mod: %s", err))
end
dofile(modPath .. "init.lua")
end
return LiveReload

View File

@ -24,7 +24,7 @@ end
local CameraAPI = require("noita-api.camera")
local Coords = require("coordinates")
local DebugAPI = require("noita-api.debug")
--local LiveReload = require("noita-api.live-reload")
local LiveReload = require("noita-api.live-reload")
local Vec2 = require("noita-api.vec2")
-----------------------
@ -120,7 +120,7 @@ function OnWorldPostUpdate()
Message:CatchException("OnWorldPostUpdate", function()
-- Reload mod every 60 frames.
-- This allows live updates to the mod while Noita is running.
-- !!! DISABLE THIS LINE AND THE CORRESPONDING REQUIRE BEFORE COMMITTING !!!
-- !!! DISABLE THE FOLLOWING LINE BEFORE COMMITTING !!!
--LiveReload:Reload("mods/noita-mapcap/", 60)
-- Run checks every 60 frames.