diff --git a/files/libraries/memory.lua b/files/libraries/memory.lua new file mode 100644 index 0000000..2933f32 --- /dev/null +++ b/files/libraries/memory.lua @@ -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 diff --git a/files/message.lua b/files/message.lua index bb5ba7e..8042e39 100644 --- a/files/message.lua +++ b/files/message.lua @@ -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 diff --git a/files/modification.lua b/files/modification.lua index 31b2e5d..f092e16 100644 --- a/files/modification.lua +++ b/files/modification.lua @@ -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. diff --git a/init.lua b/init.lua index f8c39ca..576e80c 100644 --- a/init.lua +++ b/init.lua @@ -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. diff --git a/settings.lua b/settings.lua index 08d90e9..c5f6b61 100644 --- a/settings.lua +++ b/settings.lua @@ -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, },