2023-04-17 09:19:16 +00:00
-- Copyright (c) 2022-2023 David Vogel
2022-07-27 23:48:49 +00:00
--
-- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT
-- Noita settings/configuration modifications.
2022-07-28 22:26:57 +00:00
-- We try to keep persistent modifications to a minimum, but some things have to be changed in order for the mod to work correctly.
2022-07-27 23:48:49 +00:00
2022-07-29 09:29:14 +00:00
-- There are 4 ways Noita can be modified by code:
-- - `config.xml`: These are persistent, and Noita needs to be force closed when changed from inside a mod.
-- - `magic_numbers.xml`: Persistent per world, can only be applied at mod startup.
-- - Process memory: Volatile, can be modified at runtime. Needs correct memory addresses to function.
-- - File patching: Volatile, can only be applied at mod startup.
2022-07-27 23:48:49 +00:00
--------------------------
-- Load library modules --
--------------------------
2022-07-28 11:27:02 +00:00
local CameraAPI = require ( " noita-api.camera " )
local Coords = require ( " coordinates " )
2022-07-28 22:26:57 +00:00
local ffi = require ( " ffi " )
2022-08-27 12:07:37 +00:00
local Memory = require ( " memory " )
2022-07-27 23:48:49 +00:00
local NXML = require ( " luanxml.nxml " )
local Utils = require ( " noita-api.utils " )
local Vec2 = require ( " noita-api.vec2 " )
2023-04-17 09:19:16 +00:00
local DebugAPI = require ( " noita-api.debug " )
2022-07-27 23:48:49 +00:00
----------
-- Code --
----------
2022-07-29 13:29:15 +00:00
---Reads the current config from `config.xml` and returns it as table.
---@return table<string, string> config
function Modification . GetConfig ( )
local configFilename = Utils.GetSpecialDirectory ( " save-shared " ) .. " config.xml "
-- Read and modify config.
local f , err = io.open ( configFilename , " r " )
if not f then error ( string.format ( " failed to read config file: %s " , err ) ) end
local xml = NXML.parse ( f : read ( " *a " ) )
f : close ( )
return xml.attr
end
2022-07-27 23:48:49 +00:00
---Will update Noita's `config.xml` with the values in the given table.
---
---This will force close Noita!
---@param config table<string, string> -- List of `config.xml` attributes that should be changed.
function Modification . SetConfig ( config )
local configFilename = Utils.GetSpecialDirectory ( " save-shared " ) .. " config.xml "
-- Read and modify config.
local f , err = io.open ( configFilename , " r " )
if not f then error ( string.format ( " failed to read config file: %s " , err ) ) end
local xml = NXML.parse ( f : read ( " *a " ) )
for k , v in pairs ( config ) do
xml.attr [ k ] = v
end
f : close ( )
-- Write modified config back.
local f , err = io.open ( configFilename , " w " )
if not f then error ( string.format ( " failed to create config file: %s " , err ) ) end
f : write ( tostring ( xml ) )
f : close ( )
-- We need to force close Noita, so it doesn't have any chance to overwrite the file.
os.exit ( 0 )
end
---Will update Noita's `magic_numbers.xml` with the values in the given table.
---
---Should be called on mod initialization only.
---@param magic table<string, string> -- List of `magic_numbers.xml` attributes that should be changed.
function Modification . SetMagicNumbers ( magic )
local xml = NXML.new_element ( " MagicNumbers " , magic )
-- Write magic number file.
local f , err = io.open ( " mods/noita-mapcap/files/magic-numbers/generated.xml " , " w " )
if not f then error ( string.format ( " failed to create config file: %s " , err ) ) end
f : write ( tostring ( xml ) )
f : close ( )
ModMagicNumbersFileAdd ( " mods/noita-mapcap/files/magic-numbers/generated.xml " )
end
2022-07-28 22:26:57 +00:00
---Changes some options directly by manipulating process memory.
---
---Related issue: https://github.com/Dadido3/noita-mapcap/issues/14.
---@param memory table
function Modification . SetMemoryOptions ( memory )
-- Lookup table with the following hierarchy:
2022-08-27 12:07:37 +00:00
-- DevBuild -> OS -> BuildDate -> Option -> ModFunc.
2022-07-28 22:26:57 +00:00
local lookup = {
[ true ] = {
Windows = {
2023-06-21 16:43:57 +00:00
{ _Offset = 0x00F77B0C , _BuildString = " Build Apr 23 2021 18:36:55 " , -- GOG dev build.
2023-07-27 08:40:41 +00:00
mPostFxDisabled = function ( value ) ffi.cast ( " char* " , 0x010E3B6C ) [ 0 ] = value end , -- Can be found by using Cheat Engine while toggling options in the F7 menu.
2022-08-27 12:07:37 +00:00
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 ,
2022-07-28 22:26:57 +00:00
} ,
2023-06-21 16:43:57 +00:00
{ _Offset = 0x00F80384 , _BuildString = " Build Apr 23 2021 18:40:40 " , -- Steam dev build.
2022-08-27 12:07:37 +00:00
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 ,
2022-07-28 22:26:57 +00:00
} ,
2023-06-21 16:43:57 +00:00
{ _Offset = 0x00F8A7B4 , _BuildString = " Build Mar 11 2023 14:05:19 " , -- Steam dev build.
2023-04-16 17:13:53 +00:00
mPostFxDisabled = function ( value ) ffi.cast ( " char* " , 0x010F80EC ) [ 0 ] = value end ,
mGuiDisabled = function ( value ) ffi.cast ( " char* " , 0x010F80ED ) [ 0 ] = value end ,
mGuiHalfSize = function ( value ) ffi.cast ( " char* " , 0x010F80EE ) [ 0 ] = value end ,
mFogOfWarOpenEverywhere = function ( value ) ffi.cast ( " char* " , 0x010F80EF ) [ 0 ] = value end ,
mTrailerMode = function ( value ) ffi.cast ( " char* " , 0x010F80F0 ) [ 0 ] = value end ,
mDayTimeRotationPause = function ( value ) ffi.cast ( " char* " , 0x010F80F1 ) [ 0 ] = value end ,
mPlayerNeverDies = function ( value ) ffi.cast ( " char* " , 0x010F80F2 ) [ 0 ] = value end ,
mFreezeAI = function ( value ) ffi.cast ( " char* " , 0x010F80F3 ) [ 0 ] = value end ,
} ,
2023-06-21 16:43:57 +00:00
{ _Offset = 0x00F8A8A4 , _BuildString = " Build Jun 19 2023 14:14:52 " , -- Steam dev build.
2023-07-27 08:40:41 +00:00
mPostFxDisabled = function ( value ) ffi.cast ( " char* " , 0x010F810C ) [ 0 ] = value end ,
2023-06-21 16:43:57 +00:00
mGuiDisabled = function ( value ) ffi.cast ( " char* " , 0x010F810D ) [ 0 ] = value end ,
mGuiHalfSize = function ( value ) ffi.cast ( " char* " , 0x010F810E ) [ 0 ] = value end ,
mFogOfWarOpenEverywhere = function ( value ) ffi.cast ( " char* " , 0x010F810F ) [ 0 ] = value end ,
mTrailerMode = function ( value ) ffi.cast ( " char* " , 0x010F8110 ) [ 0 ] = value end ,
mDayTimeRotationPause = function ( value ) ffi.cast ( " char* " , 0x010F8111 ) [ 0 ] = value end ,
mPlayerNeverDies = function ( value ) ffi.cast ( " char* " , 0x010F8112 ) [ 0 ] = value end ,
mFreezeAI = function ( value ) ffi.cast ( " char* " , 0x010F8113 ) [ 0 ] = value end ,
} ,
2023-07-27 08:40:41 +00:00
{ _Offset = 0x00F82464 , _BuildString = " Build Jul 26 2023 23:06:16 " , -- Steam dev build.
mPostFxDisabled = function ( value ) ffi.cast ( " char* " , 0x010E9A5C ) [ 0 ] = value end ,
mGuiDisabled = function ( value ) ffi.cast ( " char* " , 0x010E9A5D ) [ 0 ] = value end ,
mGuiHalfSize = function ( value ) ffi.cast ( " char* " , 0x010E9A5E ) [ 0 ] = value end ,
mFogOfWarOpenEverywhere = function ( value ) ffi.cast ( " char* " , 0x010E9A5F ) [ 0 ] = value end ,
mTrailerMode = function ( value ) ffi.cast ( " char* " , 0x010E9A60 ) [ 0 ] = value end ,
mDayTimeRotationPause = function ( value ) ffi.cast ( " char* " , 0x010E9A61 ) [ 0 ] = value end ,
mPlayerNeverDies = function ( value ) ffi.cast ( " char* " , 0x010E9A62 ) [ 0 ] = value end ,
mFreezeAI = function ( value ) ffi.cast ( " char* " , 0x010E9A63 ) [ 0 ] = value end ,
} ,
2022-07-28 22:26:57 +00:00
} ,
} ,
2022-08-08 00:54:48 +00:00
[ false ] = {
Windows = {
2023-06-21 16:43:57 +00:00
{ _Offset = 0x00E1C550 , _BuildString = " Build Apr 23 2021 18:44:24 " , -- Steam build.
2022-08-27 12:07:37 +00:00
enableModDetection = function ( value )
2023-07-27 08:40:41 +00:00
local ptr = ffi.cast ( " char* " , 0x0063D8AD ) -- Can be found by searching for the pattern C6 80 20 01 00 00 >01< 8B CF E8 FB 1D. The pointer has to point to the highlighted byte.
2022-08-27 12:07:37 +00:00
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 ,
2022-08-08 00:54:48 +00:00
} ,
2023-06-21 16:43:57 +00:00
{ _Offset = 0x00E22E18 , _BuildString = " Build Mar 11 2023 14:09:24 " , -- Steam build.
enableModDetection = function ( value )
2023-07-27 08:40:41 +00:00
local ptr = ffi.cast ( " char* " , 0x006429ED )
2023-06-21 16:43:57 +00:00
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 ,
} ,
{ _Offset = 0x00E22E18 , _BuildString = " Build Jun 19 2023 14:18:46 " , -- Steam build.
2023-04-16 17:13:53 +00:00
enableModDetection = function ( value )
2023-07-27 08:40:41 +00:00
local ptr = ffi.cast ( " char* " , 0x006429ED )
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 ,
} ,
{ _Offset = 0x00E146D4 , _BuildString = " Build Jul 26 2023 23:10:16 " , -- Steam build.
enableModDetection = function ( value )
local ptr = ffi.cast ( " char* " , 0x0064390D )
2023-04-16 17:13:53 +00:00
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 ,
} ,
2022-08-08 00:54:48 +00:00
} ,
} ,
2022-07-28 22:26:57 +00:00
}
-- Look up the tree and set options accordingly.
local level1 = lookup [ DebugGetIsDevBuild ( ) ]
2022-08-27 12:07:37 +00:00
level1 = level1 or { }
2022-07-28 22:26:57 +00:00
local level2 = level1 [ ffi.os ]
2022-08-27 12:07:37 +00:00
level2 = level2 or { }
2022-07-28 22:26:57 +00:00
2023-03-15 22:57:17 +00:00
local level3 = { }
2023-06-21 16:43:57 +00:00
for _ , v in ipairs ( level2 ) do
if ffi.string ( ffi.cast ( " char* " , v._Offset ) ) == v._BuildString then
2022-07-28 22:26:57 +00:00
level3 = v
break
end
end
for k , v in pairs ( memory ) do
2022-08-27 12:07:37 +00:00
local modFunc = level3 [ k ]
if modFunc ~= nil then
modFunc ( v )
else
Message : ShowModificationUnsupported ( " processMemory " , k , v )
2022-07-28 22:26:57 +00:00
end
end
end
2022-07-29 09:29:14 +00:00
---Applies patches to game files based on in the given table.
---
---Should be called on mod initialization only.
---@param patches table
function Modification . PatchFiles ( patches )
-- Change constants in post_final.frag.
if patches.PostFinalConst then
local postFinal = ModTextFileGetContent ( " data/shaders/post_final.frag " )
for k , v in pairs ( patches.PostFinalConst ) do
2022-07-29 14:57:45 +00:00
postFinal = postFinal : gsub ( string.format ( " %s%%s+=[^;]+; " , k ) , string.format ( " %s = %s; " , k , tostring ( v ) ) , 1 )
2022-07-29 09:29:14 +00:00
end
ModTextFileSetContent ( " data/shaders/post_final.frag " , postFinal )
end
end
2022-07-27 23:48:49 +00:00
---Returns tables with user requested game configuration changes.
---@return table config -- List of `config.xml` attributes that should be changed.
---@return table magic -- List of `magic_number.xml` attributes that should be changed.
2022-07-28 22:26:57 +00:00
---@return table memory -- List of options in RAM of this process that should be changed.
2022-07-29 09:29:14 +00:00
---@return table patches -- List of patches that should be applied to game files.
2022-07-27 23:48:49 +00:00
function Modification . RequiredChanges ( )
2022-07-29 09:29:14 +00:00
local config , magic , memory , patches = { } , { } , { } , { }
2022-07-27 23:48:49 +00:00
-- Does the user request a custom resolution?
local customResolution = ( ModSettingGet ( " noita-mapcap.custom-resolution-live " ) and ModSettingGet ( " noita-mapcap.capture-mode " ) == " live " )
or ( ModSettingGet ( " noita-mapcap.custom-resolution-other " ) and ModSettingGet ( " noita-mapcap.capture-mode " ) ~= " live " )
if customResolution then
config [ " window_w " ] = tostring ( Vec2 ( ModSettingGet ( " noita-mapcap.window-resolution " ) ) . x )
config [ " window_h " ] = tostring ( Vec2 ( ModSettingGet ( " noita-mapcap.window-resolution " ) ) . y )
config [ " internal_size_w " ] = tostring ( Vec2 ( ModSettingGet ( " noita-mapcap.internal-resolution " ) ) . x )
config [ " internal_size_h " ] = tostring ( Vec2 ( ModSettingGet ( " noita-mapcap.internal-resolution " ) ) . y )
config [ " backbuffer_width " ] = config [ " window_w " ]
config [ " backbuffer_height " ] = config [ " window_h " ]
magic [ " VIRTUAL_RESOLUTION_X " ] = tostring ( Vec2 ( ModSettingGet ( " noita-mapcap.virtual-resolution " ) ) . x )
magic [ " VIRTUAL_RESOLUTION_Y " ] = tostring ( Vec2 ( ModSettingGet ( " noita-mapcap.virtual-resolution " ) ) . y )
2022-07-29 14:30:43 +00:00
-- Set virtual offset to prevent/reduce not correctly drawn pixels at the window border.
magic [ " GRID_RENDER_BORDER " ] = " 3 " -- This will widen the right side of the virtual rectangle. It also shifts the world coordinates to the right.
magic [ " VIRTUAL_RESOLUTION_OFFSET_X " ] = " -3 "
magic [ " VIRTUAL_RESOLUTION_OFFSET_Y " ] = " 0 "
2022-07-28 20:34:56 +00:00
else
2022-07-29 14:30:43 +00:00
-- Reset some values if there is no custom resolution requested.
2022-07-28 20:34:56 +00:00
config [ " internal_size_w " ] = " 1280 "
config [ " internal_size_h " ] = " 720 "
magic [ " VIRTUAL_RESOLUTION_X " ] = " 427 "
magic [ " VIRTUAL_RESOLUTION_Y " ] = " 242 "
2022-07-29 14:30:43 +00:00
magic [ " GRID_RENDER_BORDER " ] = " 2 "
magic [ " VIRTUAL_RESOLUTION_OFFSET_X " ] = " -1 "
magic [ " VIRTUAL_RESOLUTION_OFFSET_Y " ] = " -1 "
2022-07-27 23:48:49 +00:00
end
-- Always expect a fullscreen mode of 0 (windowed).
-- Capturing will not work in fullscreen.
config [ " fullscreen " ] = " 0 "
2022-08-27 12:32:01 +00:00
-- Also disable screen shake.
2022-07-29 13:29:15 +00:00
config [ " screenshake_intensity " ] = " 0 "
2022-07-28 17:42:43 +00:00
magic [ " DRAW_PARALLAX_BACKGROUND " ] = ModSettingGet ( " noita-mapcap.disable-background " ) and " 0 " or " 1 "
2022-07-29 14:09:38 +00:00
-- These magic numbers seem only to work in the dev build.
2022-07-28 17:42:43 +00:00
magic [ " DEBUG_PAUSE_GRID_UPDATE " ] = ModSettingGet ( " noita-mapcap.disable-physics " ) and " 1 " or " 0 "
magic [ " DEBUG_PAUSE_BOX2D " ] = ModSettingGet ( " noita-mapcap.disable-physics " ) and " 1 " or " 0 "
magic [ " DEBUG_DISABLE_POSTFX_DITHERING " ] = ModSettingGet ( " noita-mapcap.disable-postfx " ) and " 1 " or " 0 "
2022-07-29 09:29:14 +00:00
if ModSettingGet ( " noita-mapcap.disable-postfx " ) then
patches.PostFinalConst = {
ENABLE_REFRACTION = false ,
ENABLE_LIGHTING = false ,
ENABLE_FOG_OF_WAR = false ,
ENABLE_GLOW = false ,
ENABLE_GAMMA_CORRECTION = false ,
ENABLE_PATH_DEBUG = false ,
2022-07-29 14:57:45 +00:00
FOG_FOREGROUND = " vec4(0.0,0.0,0.0,1.0) " ,
FOG_BACKGROUND = " vec3(0.0,0.0,0.0) " ,
FOG_FOREGROUND_NIGHT = " vec4(0.0,0.0,0.0,1.0) " ,
FOG_BACKGROUND_NIGHT = " vec3(0.0,0.0,0.0) " ,
2022-07-29 09:29:14 +00:00
}
end
2023-04-17 09:19:16 +00:00
if ModSettingGet ( " noita-mapcap.disable-shaders-gui-ai " ) and DebugAPI.IsDevBuild ( ) then
2022-07-28 22:26:57 +00:00
memory [ " mPostFxDisabled " ] = 1
memory [ " mGuiDisabled " ] = 1
memory [ " mFreezeAI " ] = 1
2022-07-29 14:09:38 +00:00
memory [ " mTrailerMode " ] = 1 -- Is necessary for chunks to correctly load when DEBUG_PAUSE_GRID_UPDATE is enabled.
2022-07-28 22:26:57 +00:00
end
2023-04-17 09:19:16 +00:00
if ModSettingGet ( " noita-mapcap.disable-mod-detection " ) and not DebugAPI.IsDevBuild ( ) then
2022-08-08 00:54:48 +00:00
memory [ " enableModDetection " ] = 0
else
2022-08-27 12:07:37 +00:00
-- Don't actively (re)enable mod detection.
--memory["enableModDetection"] = 1
2022-08-08 00:54:48 +00:00
end
2022-07-29 15:37:58 +00:00
-- Disables or hides most of the UI.
-- The game is still somewhat playable this way.
if ModSettingGet ( " noita-mapcap.disable-ui " ) then
magic [ " INVENTORY_GUI_ALWAYS_VISIBLE " ] = " 0 "
magic [ " UI_BARS2_OFFSET_X " ] = " 100 "
else
-- Reset to default.
magic [ " INVENTORY_GUI_ALWAYS_VISIBLE " ] = " 1 "
magic [ " UI_BARS2_OFFSET_X " ] = " -40 "
end
2022-07-29 09:29:14 +00:00
return config , magic , memory , patches
2022-07-27 23:48:49 +00:00
end
2022-07-28 11:27:02 +00:00
---Sets the camera free if required by the mod settings.
---@param force boolean|nil -- If true, the camera will be set free regardless.
function Modification . SetCameraFree ( force )
if force ~= nil then CameraAPI.SetCameraFree ( force ) return end
local captureMode = ModSettingGet ( " noita-mapcap.capture-mode " )
local spiralOrigin = ModSettingGet ( " noita-mapcap.capture-mode-spiral-origin " )
-- Allow free roaming when in spiral mode with origin being the current position.
if captureMode == " spiral " and spiralOrigin == " current " then
CameraAPI.SetCameraFree ( true )
return
end
CameraAPI.SetCameraFree ( false )
end
2022-07-27 23:48:49 +00:00
---Will change the game settings according to `Modification.RequiredChanges()`.
---
---This will force close Noita!
function Modification . AutoSet ( )
local config , magic = Modification.RequiredChanges ( )
Modification.SetConfig ( config )
end
2022-07-28 22:26:57 +00:00
---Will reset all persistent settings that may have been changed by this mod.
2022-07-27 23:48:49 +00:00
---
---This will force close Noita!
function Modification . Reset ( )
local config = {
window_w = " 1280 " ,
window_h = " 720 " ,
internal_size_w = " 1280 " ,
internal_size_h = " 720 " ,
backbuffer_width = " 1280 " ,
backbuffer_height = " 720 " ,
2022-07-29 13:29:15 +00:00
screenshake_intensity = " 0.7 " ,
2022-07-27 23:48:49 +00:00
}
Modification.SetConfig ( config )
end