2022-07-27 23:48:49 +00:00
-- Copyright (c) 2022 David Vogel
--
-- 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-07-27 23:48:49 +00:00
local NXML = require ( " luanxml.nxml " )
local Utils = require ( " noita-api.utils " )
local Vec2 = require ( " noita-api.vec2 " )
----------
-- 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:
-- DevBuild -> OS -> BuildDate -> Option -> Address.
local lookup = {
[ true ] = {
Windows = {
2022-08-08 00:54:48 +00:00
[ 0x00F77B0C ] = { _BuildString = " Build Apr 23 2021 18:36:55 " , -- GOG dev build.
2022-07-28 22:26:57 +00:00
mPostFxDisabled = 0x010E3B6C ,
mGuiDisabled = 0x010E3B6D ,
2022-07-29 14:09:38 +00:00
mGuiHalfSize = 0x010E3B6E ,
mFogOfWarOpenEverywhere = 0x010E3B6F ,
mTrailerMode = 0x010E3B70 ,
mDayTimeRotationPause = 0x010E3B71 ,
mPlayerNeverDies = 0x010E3B72 ,
2022-07-28 22:26:57 +00:00
mFreezeAI = 0x010E3B73 ,
} ,
2022-08-08 00:54:48 +00:00
[ 0x00F80384 ] = { _BuildString = " Build Apr 23 2021 18:40:40 " , -- Steam dev build.
2022-07-28 22:26:57 +00:00
mPostFxDisabled = 0x010EDEBC ,
mGuiDisabled = 0x010EDEBD ,
2022-07-29 14:09:38 +00:00
mGuiHalfSize = 0x010EDEBE ,
mFogOfWarOpenEverywhere = 0x010EDEBF ,
mTrailerMode = 0x010EDEC0 ,
mDayTimeRotationPause = 0x010EDEC1 ,
mPlayerNeverDies = 0x010EDEC2 ,
2022-07-28 22:26:57 +00:00
mFreezeAI = 0x010EDEC3 ,
} ,
} ,
} ,
2022-08-08 00:54:48 +00:00
[ 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.
} ,
} ,
} ,
2022-07-28 22:26:57 +00:00
}
-- Look up the tree and set options accordingly.
local level1 = lookup [ DebugGetIsDevBuild ( ) ]
if level1 == nil then return end
local level2 = level1 [ ffi.os ]
if level2 == nil then return end
local level3
for k , v in pairs ( level2 ) do
if ffi.string ( ffi.cast ( " char* " , k ) ) == v._BuildString then
level3 = v
break
end
end
for k , v in pairs ( memory ) do
local address = level3 [ k ]
if address ~= nil then
ffi.cast ( " char* " , address ) [ 0 ] = v
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-07-29 13:29:15 +00:00
-- Also disable screenshake.
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
2022-07-28 22:26:57 +00:00
if ModSettingGet ( " noita-mapcap.disable-shaders-gui-ai " ) then
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
2022-08-08 00:54:48 +00:00
if ModSettingGet ( " noita-mapcap.disable-mod-detection " ) then
memory [ " enableModDetection " ] = 0
else
memory [ " enableModDetection " ] = 1
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