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

View File

@ -150,10 +150,13 @@ end
---@param isPaused boolean
---@param isInventoryPause boolean
function OnPausedChanged(isPaused, isInventoryPause)
-- Set some stuff based on mod settings.
-- Normally this would be in `OnModSettingsChanged`, but that doesn't seem to be called.
local config, magic, memory, patches = Modification.RequiredChanges()
Modification.SetMemoryOptions(memory)
Message:CatchException("OnPausedChanged", function()
-- Set some stuff based on mod settings.
-- Normally this would be in `OnModSettingsChanged`, but that doesn't seem to be called.
local config, magic, memory, patches = Modification.RequiredChanges()
Modification.SetMemoryOptions(memory)
end)
end
---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",
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.",
hidden = true,
value_default = false,
scope = MOD_SETTING_SCOPE_RUNTIME,
},