-- Copyright (c) 2022-2025 David Vogel
--
-- This software is released under the MIT License.
-- https://opensource.org/licenses/MIT

-- Noita settings/configuration modifications.
-- We try to keep persistent modifications to a minimum, but some things have to be changed in order for the mod to work correctly.

-- 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.

--------------------------
-- Load library modules --
--------------------------

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")
local DebugAPI = require("noita-api.debug")

----------
-- Code --
----------

---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

---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

---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 -> ModFunc.
	local lookup = {
		[true] = {
			Windows = {
				{_Offset = 0x00F77B0C, _BuildString = "Build Apr 23 2021 18:36:55", -- GOG dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x010E3B6C)[0] = value end, -- Can be found by using Cheat Engine while toggling options in the F7 menu.
					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,
				},
				{_Offset = 0x00F80384, _BuildString = "Build Apr 23 2021 18:40:40", -- Steam dev build.
					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,
				},
				{_Offset = 0x00F8A7B4, _BuildString = "Build Mar 11 2023 14:05:19", -- Steam dev build.
					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,
				},
				{_Offset = 0x00F8A8A4, _BuildString = "Build Jun 19 2023 14:14:52", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x010F810C)[0] = value end,
					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,
				},
				{_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,
				},
				{_Offset = 0x00FA654C, _BuildString = "Build Dec 19 2023 18:34:31", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x011154BC)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x011154BD)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x011154BE)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x011154BF)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x011154C0)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x011154C1)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x011154C2)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x011154C3)[0] = value end,
				},
				{_Offset = 0x00F8A9DC, _BuildString = "Build Dec 21 2023 00:07:29", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x010F814C)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x010F814D)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x010F814E)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F814F)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x010F8150)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F8151)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F8152)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x010F8153)[0] = value end,
				},
				{_Offset = 0x00F71DE4, _BuildString = "Build Dec 29 2023 23:36:18", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0111758C)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0111758D)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0111758E)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0111758F)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x01117590)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01117591)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x01117592)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x01117593)[0] = value end,
				},
				{_Offset = 0x00F74FA8, _BuildString = "Build Dec 30 2023 19:37:04", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0111A5BC)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0111A5BD)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0111A5BE)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0111A5BF)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x0111A5C0)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0111A5C1)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x0111A5C2)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x0111A5C3)[0] = value end,
				},
				{_Offset = 0x00F8BA04, _BuildString = "Build Jan 18 2024 12:57:44", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x010F91FC+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x010F91FC+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x010F91FC+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x010F91FC+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x010F91FC+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x010F91FC+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x010F91FC+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x010F91FC+7)[0] = value end,
				},
				{_Offset = 0x0117091C, _BuildString = "Build Feb  2 2024 14:29:06", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0130585C)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0130585D)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0130585E)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0130585F)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x01305860)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01305861)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x01305862)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x01305863)[0] = value end,
				},
				{_Offset = 0x01173F34, _BuildString = "Build Feb  6 2024 15:54:02", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0130982C+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0130982C+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0130982C+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0130982C+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x0130982C+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0130982C+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x0130982C+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x0130982C+7)[0] = value end,
				},
				{_Offset = 0x01182F70, _BuildString = "Build Mar 25 2024 17:42:49", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x013198AC+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x013198AC+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x013198AC+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x013198AC+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x013198AC+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x013198AC+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x013198AC+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x013198AC+7)[0] = value end,
				},
				{_Offset = 0x011871FC, _BuildString = "Build Apr  6 2024 20:50:04", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0131D89C+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0131D89C+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0131D89C+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0131D89C+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x0131D89C+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0131D89C+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x0131D89C+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x0131D89C+7)[0] = value end,
				},
				{_Offset = 0x0118718C, _BuildString = "Build Apr  8 2024 18:07:16", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0131D8DC+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0131D8DC+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0131D8DC+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0131D8DC+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x0131D8DC+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0131D8DC+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x0131D8DC+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x0131D8DC+7)[0] = value end,
				},
				{_Offset = 0x0118FD3C, _BuildString = "Build Aug 12 2024 21:10:13", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x01327D3C+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x01327D3C+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x01327D3C+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x01327D3C+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x01327D3C+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01327D3C+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x01327D3C+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x01327D3C+7)[0] = value end,
				},
				{_Offset = 0x0118FD3C, _BuildString = "Build Aug 12 2024 21:43:22", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x01327D3C+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x01327D3C+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x01327D3C+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x01327D3C+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x01327D3C+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x01327D3C+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x01327D3C+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x01327D3C+7)[0] = value end,
				},
				{_Offset = 0x01191A34, _BuildString = "Build Jan 25 2025 15:51:09", -- Steam dev build.
					mPostFxDisabled = function(value) ffi.cast("char*", 0x0132ADDC+0)[0] = value end,
					mGuiDisabled = function(value) ffi.cast("char*", 0x0132ADDC+1)[0] = value end,
					mGuiHalfSize = function(value) ffi.cast("char*", 0x0132ADDC+2)[0] = value end,
					mFogOfWarOpenEverywhere = function(value) ffi.cast("char*", 0x0132ADDC+3)[0] = value end,
					mTrailerMode = function(value) ffi.cast("char*", 0x0132ADDC+4)[0] = value end,
					mDayTimeRotationPause = function(value) ffi.cast("char*", 0x0132ADDC+5)[0] = value end,
					mPlayerNeverDies = function(value) ffi.cast("char*", 0x0132ADDC+6)[0] = value end,
					mFreezeAI = function(value) ffi.cast("char*", 0x0132ADDC+7)[0] = value end,
				},
			},
		},
		[false] = {
			Windows = {
				{_Offset = 0x00E1C550, _BuildString = "Build Apr 23 2021 18:44:24", -- Steam build.
					enableModDetection = function(value)
						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.
						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 Mar 11 2023 14:09:24", -- Steam build.
					enableModDetection = function(value)
						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 = 0x00E22E18, _BuildString = "Build Jun 19 2023 14:18:46", -- Steam build.
					enableModDetection = function(value)
						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)
						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 = 0x00E333F4, _BuildString = "Build Dec 19 2023 18:38:23", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x00624C5D)
						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 = 0x00E23EC4, _BuildString = "Build Dec 21 2023 00:11:06", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x0064246D)
						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 = 0x00E14FA0, _BuildString = "Build Dec 29 2023 23:40:18", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x00625FFD)
						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 = 0x00E180E8, _BuildString = "Build Dec 30 2023 19:40:49", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x00626EFD)
						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 = 0x00E24E6C, _BuildString = "Build Jan 18 2024 13:01:21", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x00642A47+6)
						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 = 0x00FECE94, _BuildString = "Build Feb  2 2024 14:33:26", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006AD407)
						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 = 0x00FEEFC0, _BuildString = "Build Feb  6 2024 15:58:22", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006AD611+6)
						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 = 0x00FF21D8, _BuildString = "Build Feb  9 2024 15:52:49", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006AE101+6)
						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 = 0x00FF22D4, _BuildString = "Build Feb 12 2024 19:07:19", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006AE161+6)
						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 = 0x00E23E80, _BuildString = "Build Feb 14 2024 07:46:57", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006427A7+6)
						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 = 0x00FFDB54, _BuildString = "Build Mar 25 2024 17:48:04", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006B1FD8+6)
						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 = 0x01001DDC, _BuildString = "Build Apr  6 2024 20:54:23", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006B35B5+6)
						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 = 0x01001DF4, _BuildString = "Build Apr  8 2024 18:11:27", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006B3355+6)
						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 = 0x01007CA4, _BuildString = "Build Aug 12 2024 21:14:23", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006B3925+6)
						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 = 0x01007CA4, _BuildString = "Build Aug 12 2024 21:48:01", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006B3925+6)
						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 = 0x01009D3C, _BuildString = "Build Jan 25 2025 15:55:41", -- Steam build.
					enableModDetection = function(value)
						local ptr = ffi.cast("char*", 0x006B3AD5+6)
						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,
				},
			},
		},
	}

	-- Look up the tree and set options accordingly.

	local level1 = lookup[DebugAPI.IsDevBuild()]
	level1 = level1 or {}

	local level2 = level1[ffi.os]
	level2 = level2 or {}

	local level3 = {}
	for _, v in ipairs(level2) do
		if ffi.string(ffi.cast("char*", v._Offset)) == v._BuildString then
			level3 = v
			break
		end
	end

	for k, v in pairs(memory) do
		local modFunc = level3[k]
		if modFunc ~= nil then
			modFunc(v)
		else
			Message:ShowModificationUnsupported("processMemory", k, v)
		end
	end
end

---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
			postFinal = postFinal:gsub(string.format(" %s%%s+=[^;]+;", k), string.format(" %s = %s;", k, tostring(v)), 1)
		end
		ModTextFileSetContent("data/shaders/post_final.frag", postFinal)
	end
	if patches.PostFinalReplace then
		local postFinal = ModTextFileGetContent("data/shaders/post_final.frag")
		for k, v in pairs(patches.PostFinalReplace) do
			postFinal = postFinal:gsub(k, v)
		end
		ModTextFileSetContent("data/shaders/post_final.frag", postFinal)
	end
end

---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.
---@return table memory -- List of options in RAM of this process that should be changed.
---@return table patches -- List of patches that should be applied to game files.
function Modification.RequiredChanges()
	local config, magic, memory, patches = {}, {}, {}, {}

	-- 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"]
		config["fullscreen"] = "0"
		magic["VIRTUAL_RESOLUTION_X"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).x)
		magic["VIRTUAL_RESOLUTION_Y"] = tostring(Vec2(ModSettingGet("noita-mapcap.virtual-resolution")).y)
		-- 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"
		magic["STREAMING_CHUNK_TARGET"] = "16" -- Keep more chunks alive.
		magic["GRID_MAX_UPDATES_PER_FRAME"] = "1024" -- Allow more pixel physics simulation steps (in 32x32 regions) per frame. With too few, objects can glitch through the terrain/explode.
		magic["GRID_MIN_UPDATES_PER_FRAME"] = "0" -- Also allow no updates.
	else
		-- Reset some values if there is no custom resolution requested.
		config["internal_size_w"] = "1280"
		config["internal_size_h"] = "720"
		magic["VIRTUAL_RESOLUTION_X"] = "427"
		magic["VIRTUAL_RESOLUTION_Y"] = "242"
		magic["GRID_RENDER_BORDER"] = "2"
		magic["VIRTUAL_RESOLUTION_OFFSET_X"] = "-1"
		magic["VIRTUAL_RESOLUTION_OFFSET_Y"] = "-1"
	end

	-- Also disable screen shake.
	config["screenshake_intensity"] = "0"

	-- And disable the cursor being rendered by Noita itself, which would make it appear in screen captures.
	config["application_rendered_cursor"] = "0"

	magic["DRAW_PARALLAX_BACKGROUND"] = ModSettingGet("noita-mapcap.disable-background") and "0" or "1"

	-- These magic numbers seem only to work in the dev build.
	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"

	-- These magic numbers stop any grid updates (pixel physics), even in the release build.
	-- But any Box2D objects glitch, therefore the mod option (disable-physics) is disabled and hidden in the non dev build.
	--magic["GRID_MAX_UPDATES_PER_FRAME"] = ModSettingGet("noita-mapcap.disable-physics") and "0" or "128"
	--magic["GRID_MIN_UPDATES_PER_FRAME"] = ModSettingGet("noita-mapcap.disable-physics") and "1" or "40"

	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,
			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)",
		}

		-- Disable color grading, which may make the world look a tad more blue when there is freezing/snowing weather.
		-- This is dependent on the seed and the PC's wall clock, and it only snows in December, January or February.
		patches.PostFinalReplace = {
			["color%.rgb = mix%( color, additive_overlay_color%.rgb, additive_overlay_color%.a %);"] = "",
			["color = mix%(color, vec3%(%(color%.r %+ color%.g %+ color%.b%) %* 0%.3333%), color_grading%.a%);"] = "",
			["color = color %* color_grading%.rgb;"] = "// Here lies the remains of the tone-mapping/color grading code. 2019-2024. RIP",
		}
	end

	if ModSettingGet("noita-mapcap.disable-shaders-gui-ai") and DebugAPI.IsDevBuild() then
		memory["mPostFxDisabled"] = 1
		memory["mGuiDisabled"] = 1
		memory["mFreezeAI"] = 1
	end

	if DebugAPI.IsDevBuild() and magic["DEBUG_PAUSE_GRID_UPDATE"] == "1" then
		memory["mTrailerMode"] = 1 -- This is necessary for chunks to correctly load when DEBUG_PAUSE_GRID_UPDATE is enabled.
	end

	if ModSettingGet("noita-mapcap.disable-mod-detection") and not DebugAPI.IsDevBuild() then
		memory["enableModDetection"] = 0
	else
		-- Don't actively (re)enable mod detection.
		--memory["enableModDetection"] = 1
	end

	-- 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

	return config, magic, memory, patches
end

---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

---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

---Will reset all persistent settings that may have been changed by this mod.
---
---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",
		screenshake_intensity = "0.7",
	}

	Modification.SetConfig(config)
end
