Fully implement "disable-mod-detection" setting

- Catch exceptions in OnPausedChanged
- Unhide "disable-mod-detection" setting
- Add error message for unsupported modifications
- Change memory modification lookup to contain functions
- Don't (re)enable mod detection automatically
- Add memory.lua library that allows to change the protection of memory regions.
This commit is contained in:
David Vogel 2022-08-27 14:07:37 +02:00
parent 28c07dfd25
commit 18682ed441
5 changed files with 115 additions and 31 deletions

View File

@ -0,0 +1,58 @@
-- Copyright (c) 2022 David Vogel
--
-- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT
local ffi = require("ffi")
local Memory = {}
if ffi.abi'64bit' then
ffi.cdef([[
typedef uint64_t __uint3264;
]])
else
ffi.cdef([[
typedef uint32_t __uint3264;
]])
end
ffi.cdef([[
typedef void VOID;
typedef VOID *LPVOID;
typedef int BOOL;
typedef __uint3264 ULONG_PTR, *PULONG_PTR;
typedef ULONG_PTR SIZE_T, *PSIZE_T;
typedef unsigned long DWORD;
typedef DWORD *PDWORD;
BOOL VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
]])
Memory.PAGE_NOACCESS = 0x01
Memory.PAGE_READONLY = 0x02
Memory.PAGE_READWRITE = 0x04
Memory.PAGE_WRITECOPY = 0x08
Memory.PAGE_EXECUTE = 0x10
Memory.PAGE_EXECUTE_READ = 0x20
Memory.PAGE_EXECUTE_READWRITE = 0x40
Memory.PAGE_EXECUTE_WRITECOPY = 0x80
Memory.PAGE_GUARD = 0x100
Memory.PAGE_NOCACHE = 0x200
Memory.PAGE_WRITECOMBINE = 0x400
---Changes the protection on a region of committed pages in the virtual address space of the calling process.
---@param addr ffi.cdata*
---@param size integer
---@param newProtect integer
---@return ffi.cdata* oldProtect
function Memory.VirtualProtect(addr, size, newProtect)
local oldprotect = ffi.new('DWORD[1]')
if not ffi.C.VirtualProtect(addr, size, newProtect, oldprotect) then
error(string.format("failed to call VirtualProtect(%s, %s, %s)", addr, size, newProtect))
end
return oldprotect
end
return Memory

View File

@ -176,3 +176,21 @@ function Message:ShowGeneralInstallationProblem(...)
Lines = { ... }, Lines = { ... },
} }
end end
---Tell the user that some modifications couldn't be applied because it is unsupported.
---@param realm "config"|"magicNumbers"|"processMemory"|"filePatches"
---@param name string
---@param value any
function Message:ShowModificationUnsupported(realm, name, value)
self.List = self.List or {}
self.List["ModificationFailed"] = {
Type = "warning",
Lines = {
string.format("Couldn't modify %q in %q realm.", name, realm),
" ",
"This simply means that this modification is not supported for the Noita version you are using.",
"Feel free to open an issue at https://github.com/Dadido3/noita-mapcap.",
},
}
end

View File

@ -19,6 +19,7 @@
local CameraAPI = require("noita-api.camera") local CameraAPI = require("noita-api.camera")
local Coords = require("coordinates") local Coords = require("coordinates")
local ffi = require("ffi") local ffi = require("ffi")
local Memory = require("memory")
local NXML = require("luanxml.nxml") local NXML = require("luanxml.nxml")
local Utils = require("noita-api.utils") local Utils = require("noita-api.utils")
local Vec2 = require("noita-api.vec2") local Vec2 = require("noita-api.vec2")
@ -92,38 +93,40 @@ end
---@param memory table ---@param memory table
function Modification.SetMemoryOptions(memory) function Modification.SetMemoryOptions(memory)
-- Lookup table with the following hierarchy: -- Lookup table with the following hierarchy:
-- DevBuild -> OS -> BuildDate -> Option -> Address. -- DevBuild -> OS -> BuildDate -> Option -> ModFunc.
local lookup = { local lookup = {
[true] = { [true] = {
Windows = { Windows = {
[0x00F77B0C] = { _BuildString = "Build Apr 23 2021 18:36:55", -- GOG dev build. [0x00F77B0C] = { _BuildString = "Build Apr 23 2021 18:36:55", -- GOG dev build.
mPostFxDisabled = 0x010E3B6C, mPostFxDisabled = function(value) ffi.cast("char*", 0x010E3B6C)[0] = value end,
mGuiDisabled = 0x010E3B6D, mGuiDisabled = function(value) ffi.cast("char*", 0x010E3B6D)[0] = value end,
mGuiHalfSize = 0x010E3B6E, mGuiHalfSize = function(value) ffi.cast("char*", 0x010E3B6E)[0] = value end,
mFogOfWarOpenEverywhere = 0x010E3B6F, mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010E3B6F)[0] = value end,
mTrailerMode = 0x010E3B70, mTrailerMode = function(value) ffi.cast("char*", 0x010E3B70)[0] = value end,
mDayTimeRotationPause = 0x010E3B71, mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010E3B71)[0] = value end,
mPlayerNeverDies = 0x010E3B72, mPlayerNeverDies = function(value) ffi.cast("char*", 0x010E3B72)[0] = value end,
mFreezeAI = 0x010E3B73, mFreezeAI = function(value) ffi.cast("char*", 0x010E3B73)[0] = value end,
}, },
[0x00F80384] = { _BuildString = "Build Apr 23 2021 18:40:40", -- Steam dev build. [0x00F80384] = { _BuildString = "Build Apr 23 2021 18:40:40", -- Steam dev build.
mPostFxDisabled = 0x010EDEBC, mPostFxDisabled = function(value) ffi.cast("char*", 0x010EDEBC)[0] = value end,
mGuiDisabled = 0x010EDEBD, mGuiDisabled = function(value) ffi.cast("char*", 0x010EDEBD)[0] = value end,
mGuiHalfSize = 0x010EDEBE, mGuiHalfSize = function(value) ffi.cast("char*", 0x010EDEBE)[0] = value end,
mFogOfWarOpenEverywhere = 0x010EDEBF, mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010EDEBF)[0] = value end,
mTrailerMode = 0x010EDEC0, mTrailerMode = function(value) ffi.cast("char*", 0x010EDEC0)[0] = value end,
mDayTimeRotationPause = 0x010EDEC1, mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010EDEC1)[0] = value end,
mPlayerNeverDies = 0x010EDEC2, mPlayerNeverDies = function(value) ffi.cast("char*", 0x010EDEC2)[0] = value end,
mFreezeAI = 0x010EDEC3, mFreezeAI = function(value) ffi.cast("char*", 0x010EDEC3)[0] = value end,
}, },
}, },
}, },
[false] = { [false] = {
Windows = { Windows = {
[0x00E1C550] = { _BuildString = "Build Apr 23 2021 18:44:24", -- Steam build. [0x00E1C550] = { _BuildString = "Build Apr 23 2021 18:44:24", -- Steam build.
--enableModDetection = 0x0063D8AD, -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled. enableModDetection = function(value)
-- The page this is in is not writable, so this will crash Noita. local ptr = ffi.cast("char*", 0x0063D8AD)
-- This modification can be applied manually with some other tool that changes the permission prior to writing, like Cheat Engine. Memory.VirtualProtect(ptr, 1, Memory.PAGE_EXECUTE_READWRITE)
ptr[0] = value -- This basically just changes the value that Noita forces to the "mods_have_been_active_during_this_run" member of the WorldStateComponent when any mod is enabled.
end,
}, },
}, },
}, },
@ -132,10 +135,10 @@ function Modification.SetMemoryOptions(memory)
-- Look up the tree and set options accordingly. -- Look up the tree and set options accordingly.
local level1 = lookup[DebugGetIsDevBuild()] local level1 = lookup[DebugGetIsDevBuild()]
if level1 == nil then return end level1 = level1 or {}
local level2 = level1[ffi.os] local level2 = level1[ffi.os]
if level2 == nil then return end level2 = level2 or {}
local level3 local level3
for k, v in pairs(level2) do for k, v in pairs(level2) do
@ -146,9 +149,11 @@ function Modification.SetMemoryOptions(memory)
end end
for k, v in pairs(memory) do for k, v in pairs(memory) do
local address = level3[k] local modFunc = level3[k]
if address ~= nil then if modFunc ~= nil then
ffi.cast("char*", address)[0] = v modFunc(v)
else
Message:ShowModificationUnsupported("processMemory", k, v)
end end
end end
end end
@ -243,7 +248,8 @@ function Modification.RequiredChanges()
if ModSettingGet("noita-mapcap.disable-mod-detection") then if ModSettingGet("noita-mapcap.disable-mod-detection") then
memory["enableModDetection"] = 0 memory["enableModDetection"] = 0
else else
memory["enableModDetection"] = 1 -- Don't actively (re)enable mod detection.
--memory["enableModDetection"] = 1
end end
-- Disables or hides most of the UI. -- Disables or hides most of the UI.

View File

@ -150,10 +150,13 @@ end
---@param isPaused boolean ---@param isPaused boolean
---@param isInventoryPause boolean ---@param isInventoryPause boolean
function OnPausedChanged(isPaused, isInventoryPause) function OnPausedChanged(isPaused, isInventoryPause)
-- Set some stuff based on mod settings. Message:CatchException("OnPausedChanged", function()
-- Normally this would be in `OnModSettingsChanged`, but that doesn't seem to be called. -- Set some stuff based on mod settings.
local config, magic, memory, patches = Modification.RequiredChanges() -- Normally this would be in `OnModSettingsChanged`, but that doesn't seem to be called.
Modification.SetMemoryOptions(memory) local config, magic, memory, patches = Modification.RequiredChanges()
Modification.SetMemoryOptions(memory)
end)
end end
---Will be called when the game is unpaused, if player changed any mod settings while the game was paused. ---Will be called when the game is unpaused, if player changed any mod settings while the game was paused.

View File

@ -318,7 +318,6 @@ modSettings = {
id = "disable-mod-detection", id = "disable-mod-detection",
ui_name = " Disable mod detection", ui_name = " Disable mod detection",
ui_description = "If enabled, Noita will behave as if no mods are enabled.\nTherefore secrets like the cauldron will be generated.", ui_description = "If enabled, Noita will behave as if no mods are enabled.\nTherefore secrets like the cauldron will be generated.",
hidden = true,
value_default = false, value_default = false,
scope = MOD_SETTING_SCOPE_RUNTIME, scope = MOD_SETTING_SCOPE_RUNTIME,
}, },