Files
SpartanPort/tests/test_graphics_settings.cpp
Felipe Prado 3f780093f8 Fix(Settings): kept the saved upscaler when the launcher overrides it
EN-US

- Fixed a save in the game turning fsr3ui into fsr3 after the launcher's FSR 3.1 fallback
- Updated SPARTAN_UPSCALER to apply to one run, unless the menu picks another upscaler
- Added a settings test for the override and for a menu change

---

PT-BR

- Corrigido o salvamento no jogo que trocava fsr3ui por fsr3 depois do fallback do launcher para o FSR 3.1
- Atualizado SPARTAN_UPSCALER para valer só na execução, a não ser que o menu escolha outro upscaler
- Adicionado teste de configurações para a substituição e para uma troca pelo menu
2026-11-07 12:13:39 -03:00

824 lines
38 KiB
C++

// spartanport.ini graphics keys (spartanport_settings.cpp, spartanport_graphics.h).
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <limits>
#include <sstream>
#include <string>
#if defined(_WIN32)
#include <io.h>
#else
#include <unistd.h>
#endif
#include "spartanport_graphics.h"
#include "spartanport_settings.h"
#include "../gpu/spartanportgpu.h"
#include "../src/graphics_hook.h"
// src/probe.c's side of Options > Graphics (src/graphics_hook.h).
extern "C" uintptr_t spartanport_graphics_flag(unsigned id);
extern "C" void spartanport_graphics_status(unsigned id);
namespace G = SpartanGraphics;
static int SetEnvironment(const char* name, const char* value) {
#if defined(_WIN32)
return _putenv_s(name, value);
#else
return value[0] ? setenv(name, value, 1) : unsetenv(name);
#endif
}
static std::string Read(const char* path) {
std::ifstream in(path);
std::stringstream s;
s << in.rdbuf();
return s.str();
}
// SyncEngineFps's log lines (stdout) while fn runs.
template <typename Fn>
static std::string CaptureLog(Fn fn) {
std::fflush(stdout);
const char* file = "graphics-settings-test.log";
const int saved = dup(1);
FILE* log = std::fopen(file, "w");
assert(saved >= 0 && log);
dup2(fileno(log), 1);
fn();
std::fflush(stdout);
dup2(saved, 1);
close(saved);
std::fclose(log);
const std::string text = Read(file);
std::remove(file);
return text;
}
// F2 (docs/specs/2026-10-29-high-fps-f2-qte.md): the QTE cap of the engine rate.
static void TestEngineFpsCap(const char* path) {
auto& v = SpartanSettings::Get();
float live = 240;
int32_t live_int = 240;
v.engine_fps_global = &live;
v.engine_fps_int_global = &live_int;
v.applied_engine_fps = 240;
v.engine_fps = 144;
v.auto_safe_fps = true;
SpartanSettings::SyncEngineFps();
assert(live == 144 && live_int == 144);
// engine_fps_cap_limits_rate: 60 in effect, written in one sync, with one log line.
std::string log = CaptureLog([] {
spartanportgpu_engine_fps_cap_qte(1u << 0 | 1u << 2, 2); // update and repeat counting
spartanportgpu_engine_fps_cap(60);
assert(SpartanSettings::EngineFpsNow() == 60);
SpartanSettings::SyncEngineFps();
SpartanSettings::SyncEngineFps();
});
assert(live == 59.94f && live_int == 60);
assert(log == "Engine FPS: 60 during a QTE (was 144), kinds: update,repeat, prompt type 2\n");
// engine_fps_cap_not_saved: the file keeps the player's rate and never the cap.
SpartanSettings::Save();
std::string saved = Read(path);
assert(saved.find("engine_fps=144\n") != std::string::npos);
assert(saved.find("auto_safe_fps=1\n") != std::string::npos);
assert(saved.find("engine_fps_cap") == std::string::npos);
assert(v.engine_fps == 144);
// cycle_during_cap_keeps_choice: R3 + L1 + R1 goes on from 144, the cap stays until the QTE ends.
assert(SpartanSettings::CycleEngineFps() == 180);
assert(v.engine_fps == 180 && SpartanSettings::EngineFpsNow() == 60);
assert(Read(path).find("engine_fps=180\n") != std::string::npos);
log = CaptureLog([] { SpartanSettings::SyncEngineFps(); });
assert(live == 59.94f && log.empty());
// A cap held 20 s (runtime_pad.c): one line with the kinds still counting.
log = CaptureLog([] {
spartanportgpu_engine_fps_cap_held(1u << 1 | 1u << 4);
SpartanSettings::SyncEngineFps();
SpartanSettings::SyncEngineFps();
});
assert(live == 59.94f && log == "Engine FPS: QTE cap held 20 s, kinds: timed,circle\n");
log = CaptureLog([] {
spartanportgpu_engine_fps_cap(0);
SpartanSettings::SyncEngineFps();
SpartanSettings::SyncEngineFps();
});
assert(live == 180 && live_int == 180 && SpartanSettings::EngineFpsNow() == 180);
assert(log == "Engine FPS: back to 180 after the QTE\n");
log = CaptureLog([] {
spartanportgpu_engine_fps_cap_held(1u << 2); // late, the cap is gone: nothing
SpartanSettings::SyncEngineFps();
});
assert(log.empty());
// A menu change during the cap is kept the same way.
log = CaptureLog([] {
spartanportgpu_engine_fps_cap_qte(1u << 3, -1);
spartanportgpu_engine_fps_cap(60);
SpartanSettings::SyncEngineFps();
});
assert(log == "Engine FPS: 60 during a QTE (was 180), kinds: charged\n");
v.engine_fps = 240;
log = CaptureLog([] {
SpartanSettings::SyncEngineFps();
spartanportgpu_engine_fps_cap(0);
SpartanSettings::SyncEngineFps();
});
assert(live == 240 && log == "Engine FPS: back to 240 after the QTE\n");
// qte_fps_cap_disabled: auto_safe_fps=0 behaves as before (no cap, no line).
v.auto_safe_fps = false;
log = CaptureLog([] {
spartanportgpu_engine_fps_cap(60);
SpartanSettings::SyncEngineFps();
});
assert(SpartanSettings::EngineFpsNow() == 240 && live == 240 && log.empty());
// Turned on again in the menu while the prompt is up: the cap applies at once.
v.auto_safe_fps = true;
log = CaptureLog([] { SpartanSettings::SyncEngineFps(); });
assert(live == 59.94f && log == "Engine FPS: 60 during a QTE (was 240), kinds: charged\n");
// Turned off while capped: back to the player's rate.
v.auto_safe_fps = false;
log = CaptureLog([] { SpartanSettings::SyncEngineFps(); });
assert(live == 240 && log == "Engine FPS: back to 240 after the QTE\n");
spartanportgpu_engine_fps_cap(0);
v.auto_safe_fps = true;
// A player rate of 60: the cap changes nothing and logs nothing.
v.engine_fps = 60;
log = CaptureLog([] {
SpartanSettings::SyncEngineFps();
spartanportgpu_engine_fps_cap(60);
SpartanSettings::SyncEngineFps();
spartanportgpu_engine_fps_cap(0);
SpartanSettings::SyncEngineFps();
});
assert(live == 59.94f && log == "Engine FPS: 60\n");
// No frame rate patch (applied 60) or no globals: nothing.
v.engine_fps = 240;
v.applied_engine_fps = 60;
live = 59.94f;
log = CaptureLog([] {
spartanportgpu_engine_fps_cap(60);
SpartanSettings::SyncEngineFps();
spartanportgpu_engine_fps_cap(0);
SpartanSettings::SyncEngineFps();
});
assert(live == 59.94f && log.empty());
v.applied_engine_fps = 240;
v.engine_fps_global = nullptr;
v.engine_fps_int_global = nullptr;
log = CaptureLog([] {
spartanportgpu_engine_fps_cap(60);
SpartanSettings::SyncEngineFps();
spartanportgpu_engine_fps_cap(0);
SpartanSettings::SyncEngineFps();
});
assert(log.empty());
// auto_safe_fps in the ini: on by default, 0 read and written back; SPARTAN_AUTO_SAFE_FPS overrides.
{
std::ofstream out(path);
out << "engine_fps=240\n";
}
SpartanSettings::Load();
assert(v.auto_safe_fps);
{
std::ofstream out(path);
out << "engine_fps=240\nauto_safe_fps=0\nengine_fps_cap=60\n";
}
SpartanSettings::Load();
assert(!v.auto_safe_fps && v.engine_fps_cap == 0);
SpartanSettings::Save();
assert(Read(path).find("auto_safe_fps=0\n") != std::string::npos);
SetEnvironment("SPARTAN_AUTO_SAFE_FPS", "1");
SpartanSettings::Load();
assert(v.auto_safe_fps);
SetEnvironment("SPARTAN_AUTO_SAFE_FPS", "");
}
int main() {
assert(G::ParseDisplayMode("borderless") == G::Borderless && G::ParseDisplayMode("bogus") == G::Windowed);
// Monitor preference (window.cpp): empty means the primary display, and that default is distinct
// from an explicit index 0. The launcher's value is untrusted text, so anything else falls back.
assert(G::ParseMonitor("") == G::PrimaryMonitor && G::PrimaryMonitor != 0);
assert(G::ParseMonitor("0") == 0);
assert(G::ParseMonitor("1") == 1 && G::ParseMonitor("2") == 2 && G::ParseMonitor("42") == 42);
assert(G::ParseMonitor("007") == 7); // leading zeros are still an index
assert(G::ParseMonitor(" 1") == G::PrimaryMonitor && G::ParseMonitor("1 ") == G::PrimaryMonitor);
assert(G::ParseMonitor("abc") == G::PrimaryMonitor && G::ParseMonitor("1x") == G::PrimaryMonitor);
assert(G::ParseMonitor("-1") == G::PrimaryMonitor && G::ParseMonitor("+1") == G::PrimaryMonitor);
// Too large for an int falls back instead of wrapping into a bogus index: the exact INT_MAX + 1
// boundary, and an input far past any int.
const std::string widest = std::to_string(std::numeric_limits<int>::max());
assert(G::ParseMonitor(widest) == std::numeric_limits<int>::max());
assert(G::ParseMonitor(std::to_string(std::numeric_limits<int>::max() + 1LL)) == G::PrimaryMonitor);
assert(G::ParseMonitor("99999999999999999999") == G::PrimaryMonitor);
assert(G::ParseFpsLimit(240) == 240 && G::ParseFpsLimit(144) == 144 && G::ParseFpsLimit(180) == 180 && G::ParseFpsLimit(100) == 100 && G::ParseFpsLimit(75) == 0);
assert(G::ParseResolution("900p") == 3 && G::ParseResolution("1440p") == 4 && G::ParseResolution("8K") == 22);
// #238: 900p (1600x900) sits between 720p and 1440p in the built-in list.
assert(G::Resolutions[3] == "900p" && G::Resolutions[2] == "720p" && G::Resolutions[4] == "1440p");
// The window's starting size (window.cpp): built-in names, a mod's size, native otherwise.
assert(G::ResolutionSize("720p") == std::make_pair(1280, 720));
assert(G::ResolutionSize("900p") == std::make_pair(1600, 900));
assert(G::ResolutionSize("4K") == std::make_pair(3840, 2160));
assert(G::ResolutionSize("3440x1440") == std::make_pair(3440, 1440));
assert(G::ResolutionSize("native") == std::make_pair(1920, 1080));
assert(G::ResolutionSize("8K") == std::make_pair(7680, 4320));
assert(G::ParseResolution("invalid") == 0);
for (const auto name : G::Resolutions) assert(G::ResolutionSize(name).first > 0);
assert(G::ParseEngineFps(240) == 240 && G::ParseEngineFps(144) == 144 && G::ParseEngineFps(180) == 180 && G::ParseEngineFps(100) == 100 && G::ParseEngineFps(90) == 120);
// R3 + L1 + R1 / F9: the next rate this launch supports, back to 60 after the last.
assert(G::NextEngineFps(60, 240) == 100 && G::NextEngineFps(100, 240) == 120);
assert(G::NextEngineFps(120, 240) == 144 && G::NextEngineFps(144, 240) == 180 && G::NextEngineFps(180, 240) == 240 && G::NextEngineFps(240, 240) == 60);
assert(G::NextEngineFps(100, 120) == 120 && G::NextEngineFps(120, 120) == 60 && G::NextEngineFps(60, 60) == 60);
assert(G::ParseFramesQueued(2) == 2 && G::ParseFramesQueued(0) == 0 && G::ParseFramesQueued(5) == 1);
assert(G::ParseRcasStrength(100) == 100 && G::ParseRcasStrength(101) == 75 && G::ParseRcasStrength(-1) == 75);
assert(G::RcasAttenuation(100) == 0.0f && G::RcasAttenuation(0) == 2.0f && G::RcasAttenuation(75) == 0.5f);
assert(G::FsrPassRuns(true, false, 1280, 720, 2560, 1440) && !G::FsrPassRuns(false, false, 1280, 720, 2560, 1440));
assert(G::FsrPassRuns(false, true, 1920, 1080, 1920, 1080) && !G::FsrPassRuns(true, false, 1920, 1080, 1920, 1080));
assert(!G::FsrPassRuns(true, true, 2560, 1440, 1920, 1080)); // larger image: regular scaling
assert(G::EffectiveFps(0, 120) == 120 && G::EffectiveFps(240, 120) == 120 && G::EffectiveFps(60, 240) == 60);
// An unpatched engine steps 1/60 s per frame: presenting faster speeds the game up.
assert(G::PresentLimit(120, 60, 60) == 60 && G::PresentLimit(240, 120, 120) == 120);
assert(G::PresentLimit(240, 240, 240) == 240 && G::PresentLimit(30, 60, 240) == 30);
assert(G::PresentLimit(0, 60, 60) == 0 && G::PresentLimit(0, 240, 240) == 0); // 0: paced by vblank
// A live engine rate below the vblank (it runs at the highest rate): unlimited is the engine rate.
assert(G::PresentLimit(0, 60, 240) == 60 && G::PresentLimit(0, 100, 240) == 100);
assert(G::PresentLimit(240, 100, 240) == 100);
const char* path = "graphics-settings-test.ini";
{
std::ofstream out(path);
out << "display_mode=borderless\nvsync=0\nfps_limit=240\nasync_shaders=0\n"
"render_resolution=1440p\nengine_fps=240\ndeferred_readback=0\nstale_readback=1\n"
"fsr1=1\nrcas=0\nrcas_strength=40\nframes_queued=2\n";
}
SetEnvironment("SPARTAN_CONFIG", path);
SetEnvironment("SPARTAN_ASYNC_SHADERS", "");
auto& v = SpartanSettings::Get();
SpartanSettings::Load();
assert(v.display_mode == G::Borderless && !v.vsync && v.fps_limit == 240 && !v.async_shaders);
assert(v.render_resolution == 4 && v.engine_fps == 240 && !v.deferred_readback && v.stale_readback);
// FSR 1 left the menus in 2.2.0: a saved fsr1=1 (or SPARTAN_FSR1) no longer turns it on.
assert(!v.fsr1 && !v.rcas && v.rcas_strength == 40 && v.frames_queued == 2);
SetEnvironment("SPARTAN_FSR1", "1");
SpartanSettings::Load();
assert(!v.fsr1);
SetEnvironment("SPARTAN_FSR1", "");
assert(!SpartanSettings::GraphicsNeedRestart());
assert(v.applied_engine_fps == 240); // no SPARTAN_ENGINE_FPS: the configured rate
SetEnvironment("SPARTAN_ENGINE_FPS", "60"); // run.py: the frame rate patch did not apply
SpartanSettings::Load();
assert(v.engine_fps == 240 && v.applied_engine_fps == 60 && !SpartanSettings::GraphicsNeedRestart());
SetEnvironment("SPARTAN_ENGINE_FPS", "");
v.render_resolution = 0;
assert(SpartanSettings::GraphicsNeedRestart());
v.render_resolution = 4;
v.fps_limit = 60; // live: no restart because of them
v.engine_fps = 120;
assert(!SpartanSettings::GraphicsNeedRestart());
v.engine_fps = 240;
SpartanSettings::Save();
const std::string saved = Read(path);
for (const char* line : {"display_mode=borderless\n", "vsync=0\n", "fps_limit=60\n", "async_shaders=0\n",
"render_resolution=1440p\n", "engine_fps=240\n", "deferred_readback=0\n",
"stale_readback=1\n", "fsr1=0\n", "rcas=0\n", "rcas_strength=40\n",
"frames_queued=2\n"}) {
assert(saved.find(line) != std::string::npos);
}
{
// The launcher's SPARTAN_UPSCALER for one run (native HUD FSR outside 1920x1080) is not saved:
// the choice in the file stays, unless the menu picks another one.
{
std::ofstream out(path);
out << "upscaler=fsr3ui\n";
}
SetEnvironment("SPARTAN_UPSCALER", "fsr3");
SpartanSettings::Load();
assert(v.upscaler == SpartanSettings::UpscalerFsr3);
SpartanSettings::Save();
assert(Read(path).find("upscaler=fsr3ui\n") != std::string::npos);
v.upscaler = SpartanSettings::UpscalerFsr4;
SpartanSettings::Save();
assert(Read(path).find("upscaler=fsr4\n") != std::string::npos);
SetEnvironment("SPARTAN_UPSCALER", "");
}
{
std::ofstream out(path);
out << "render_resolution=8K\n";
}
SpartanSettings::Load();
assert(v.render_resolution == G::ParseResolution("8K"));
SpartanSettings::Save();
assert(Read(path).find("render_resolution=8K\n") != std::string::npos);
{
// The resolutions of the enabled mods' patch files (run.py: SPARTAN_EXTRA_RESOLUTIONS) follow the built-in
// ones and keep their names through a load and a save; repeated or built-in names are not added twice.
SetEnvironment("SPARTAN_EXTRA_RESOLUTIONS", "3840x1080;2880x1200;;2880x1200;4K;3440x1440");
const auto names = G::ResolutionNames();
assert(names.size() == 25 && names[22] == "8K" && names[23] == "3840x1080" && names[24] == "2880x1200");
{
std::ofstream out(path);
out << "render_resolution=2880x1200\n";
}
SpartanSettings::Load();
assert(v.render_resolution == 24);
SpartanSettings::Save();
assert(Read(path).find("render_resolution=2880x1200\n") != std::string::npos);
SetEnvironment("SPARTAN_EXTRA_RESOLUTIONS", ""); // the mod is off: its resolution is unknown, so native
SpartanSettings::Load();
assert(v.render_resolution == 0 && G::ResolutionNames().size() == G::Resolutions.size());
}
{
std::ofstream out(path);
out << "display_mode=huge\nfps_limit=7\nrender_resolution=invalid\nengine_fps=1\n"
"rcas_strength=250\nframes_queued=9\n";
}
SetEnvironment("SPARTAN_ASYNC_SHADERS", "1"); // the environment still wins at start
v.async_shaders = false;
SpartanSettings::Load();
assert(v.display_mode == G::Windowed && v.fps_limit == 0 && v.render_resolution == 0 && v.engine_fps == 120);
assert(v.rcas_strength == 75 && v.frames_queued == 1);
assert(v.async_shaders);
{
// Audio (src/runtime_audio.c): the master volume is 0 to 200 percent, the speaker layout is
// named by key, anything unknown is automatic; both survive a save and the environment wins.
namespace S = SpartanSettings;
assert(S::ParseAudioChannels("auto") == S::AudioChannelsAuto && S::ParseAudioChannels("stereo") == S::AudioStereo);
assert(S::ParseAudioChannels("5.1") == S::Audio51 && S::ParseAudioChannels("7.1") == S::Audio71);
assert(S::ParseAudioChannels("2.0") == S::AudioStereo && S::ParseAudioChannels("6") == S::Audio51);
assert(S::ParseAudioChannels("8") == S::Audio71 && S::ParseAudioChannels("") == S::AudioChannelsAuto);
assert(S::ParseAudioChannels("bogus") == S::AudioChannelsAuto && S::ParseAudioChannels("7") == S::AudioChannelsAuto);
for (int c = 0; c < S::AudioChannelsCount; ++c) assert(S::ParseAudioChannels(S::AudioChannelKeys[c]) == c);
{
std::ofstream out(path);
out << "master_volume=150\naudio_channels=5.1\n";
}
SpartanSettings::Load();
assert(v.master_volume == 150 && v.audio_channels == S::Audio51);
SpartanSettings::Save();
const std::string audio_ini = Read(path);
assert(audio_ini.find("master_volume=150\n") != std::string::npos);
assert(audio_ini.find("audio_channels=5.1\n") != std::string::npos);
{
std::ofstream out(path);
out << "master_volume=900\naudio_channels=7.1\n";
}
SpartanSettings::Load();
assert(v.master_volume == 200 && v.audio_channels == S::Audio71);
{
std::ofstream out(path);
out << "master_volume=-5\naudio_channels=bogus\n";
}
SpartanSettings::Load();
assert(v.master_volume == 0 && v.audio_channels == S::AudioChannelsAuto);
{
std::ofstream out(path);
out << "master_volume=\n";
}
SpartanSettings::Load();
assert(v.master_volume == 100);
SetEnvironment("SPARTAN_AUDIO_VOLUME", "80");
SetEnvironment("SPARTAN_AUDIO_CHANNELS", "stereo");
SpartanSettings::Load();
assert(v.master_volume == 80 && v.audio_channels == S::AudioStereo);
SetEnvironment("SPARTAN_AUDIO_VOLUME", "");
SetEnvironment("SPARTAN_AUDIO_CHANNELS", "");
}
{
// The game's motion blur (spartanport_motion_blur.h): on by default, a saved 0 turns it off,
// the environment wins, and an empty value is off like any other flag.
{
std::ofstream out(path);
out << "motion_blur=0\n";
}
SpartanSettings::Load();
assert(!v.motion_blur);
SpartanSettings::Save();
assert(Read(path).find("motion_blur=0\n") != std::string::npos);
SetEnvironment("SPARTAN_MOTION_BLUR", "1");
SpartanSettings::Load();
assert(v.motion_blur);
SetEnvironment("SPARTAN_MOTION_BLUR", "");
{
std::ofstream out(path);
out << "motion_blur=1\n";
}
SpartanSettings::Load();
assert(v.motion_blur);
SpartanSettings::Save();
assert(Read(path).find("motion_blur=1\n") != std::string::npos);
}
{
// The game's depth of field (spartanport_depth_of_field.h): the same way.
{
std::ofstream out(path);
out << "depth_of_field=0\n";
}
SpartanSettings::Load();
assert(!v.depth_of_field);
SpartanSettings::Save();
assert(Read(path).find("depth_of_field=0\n") != std::string::npos);
SetEnvironment("SPARTAN_DEPTH_OF_FIELD", "1");
SpartanSettings::Load();
assert(v.depth_of_field);
SetEnvironment("SPARTAN_DEPTH_OF_FIELD", "");
{
std::ofstream out(path);
out << "depth_of_field=1\n";
}
SpartanSettings::Load();
assert(v.depth_of_field);
SpartanSettings::Save();
assert(Read(path).find("depth_of_field=1\n") != std::string::npos);
}
{
// Anti-aliasing of the finished frame (aa_pass.cpp): off unless asked, text keys in the ini.
assert(G::ParseAntiAliasing("off") == G::AaOff && G::ParseAntiAliasing("fxaa") == G::AaFxaa &&
G::ParseAntiAliasing("smaa") == G::AaSmaa);
assert(G::ParseAntiAliasing("bogus") == G::AaOff && G::ParseAntiAliasing("2") == G::AaSmaa);
assert(G::ParseAntiAliasing("") == G::AaOff && G::ParseAntiAliasing("none") == G::AaOff);
// Too small an image is left alone, and so is "off".
assert(G::AaPassRuns(G::AaFxaa, 1920, 1080) && G::AaPassRuns(G::AaSmaa, 1280, 720));
assert(!G::AaPassRuns(G::AaOff, 1920, 1080) && !G::AaPassRuns(G::AaSmaa, 8, 8));
// Colour spaces: an sRGB view is sampled as linear light, the 10-bit and HDR buffers are not.
assert(G::AaInputIsLinear(false, false) && !G::AaInputIsLinear(true, false) &&
!G::AaInputIsLinear(false, true));
{
std::ofstream out(path);
out << "anti_aliasing=smaa\n";
}
SpartanSettings::Load();
assert(v.anti_aliasing == G::AaSmaa);
SpartanSettings::Save();
assert(Read(path).find("anti_aliasing=smaa\n") != std::string::npos);
SetEnvironment("SPARTAN_ANTI_ALIASING", "fxaa");
SpartanSettings::Load();
assert(v.anti_aliasing == G::AaFxaa);
SetEnvironment("SPARTAN_ANTI_ALIASING", "");
{
std::ofstream out(path);
out << "anti_aliasing=garbage\n";
}
SpartanSettings::Load();
assert(v.anti_aliasing == G::AaOff);
SpartanSettings::Save();
assert(Read(path).find("anti_aliasing=off\n") != std::string::npos);
{
std::ofstream out(path);
out << "vsync=1\n";
}
SpartanSettings::Load();
assert(v.anti_aliasing == G::AaOff); // not in the file: off
}
{
std::ofstream out(path);
out << "cheat_double_jumps=1\ncheat_very_hard=1\n";
}
SpartanSettings::Load();
SpartanSettings::Save();
const std::string cheats = Read(path);
assert(cheats.find("cheat_double_jumps=1\n") != std::string::npos);
assert(cheats.find("cheat_very_hard=1\n") != std::string::npos);
v.actor_watch_supported = false;
v.enemy_health_bar = false;
assert(spartanportgpu_enemy_health_bar(0) == 2);
assert(spartanportgpu_enemy_health_bar(1) == 2 && !v.enemy_health_bar);
v.actor_watch_supported = true;
assert(spartanportgpu_enemy_health_bar(0) == 0);
assert(spartanportgpu_enemy_health_bar(1) == 1 && v.enemy_health_bar);
assert(Read(path).find("enemy_health_bar=1\n") != std::string::npos);
v.enemy_health_bar = false;
SpartanSettings::Load();
assert(spartanportgpu_enemy_health_bar(0) == 1);
assert(spartanportgpu_enemy_health_bar(1) == 0 && !v.enemy_health_bar);
assert(Read(path).find("enemy_health_bar=0\n") != std::string::npos);
{
std::ofstream out(path);
out << "menu_confirm_swap=1\n";
}
SpartanSettings::Load();
assert(v.menu_confirm_swap == 1 && spartanportgpu_menu_confirm_swap() == 1);
SpartanSettings::Save();
assert(Read(path).find("menu_confirm_swap=1\n") != std::string::npos);
{
std::ofstream out(path);
out << "menu_confirm=circle\n";
}
SpartanSettings::Load();
assert(v.menu_confirm_swap == 1);
{
std::ofstream out(path);
out << "menu_confirm_swap=0\n";
}
SpartanSettings::Load();
assert(v.menu_confirm_swap == 0 && spartanportgpu_menu_confirm_swap() == 0);
SpartanSettings::Save();
assert(Read(path).find("menu_confirm_swap=0\n") != std::string::npos);
// Conflicting keys in INI: primary menu_confirm_swap always overrides alias menu_confirm
{
std::ofstream out(path);
out << "menu_confirm_swap=0\nmenu_confirm=circle\n";
}
SpartanSettings::Load();
assert(v.menu_confirm_swap == 0);
{
std::ofstream out(path);
out << "menu_confirm=circle\nmenu_confirm_swap=0\n";
}
SpartanSettings::Load();
assert(v.menu_confirm_swap == 0);
{
std::ofstream out(path);
out << "menu_confirm_swap=1\nmenu_confirm=cross\n";
}
SpartanSettings::Load();
assert(v.menu_confirm_swap == 1);
SetEnvironment("SPARTAN_MENU_CONFIRM_SWAP", "1");
SpartanSettings::Load();
assert(v.menu_confirm_swap == 1);
SetEnvironment("SPARTAN_MENU_CONFIRM_SWAP", "");
const auto text = [](unsigned id) { return reinterpret_cast<const char*>(spartanportgpu_native_setting(id)); };
v.anisotropy = 0;
assert(spartanportgpu_native_setting(0xf74) != 0);
assert(std::string(text(0xf94)) == "2x" && v.anisotropy == 2);
assert(std::string(text(0xf84)) == "Game default" && v.anisotropy == 0);
assert(spartanportgpu_native_setting(0xfa4) != 0);
assert(std::string(text(0xfa4)).find("PlayStation") != std::string::npos);
spartanportgpu_native_setting(0xfb1);
assert(std::string(text(0xfa4)).find("Xbox") != std::string::npos);
v.vibration = 0;
spartanportgpu_native_setting(0xfab);
assert(v.vibration == 0);
spartanportgpu_native_setting(0xfb2);
assert(v.vibration == 5);
v.mouse_aim_sensitivity = 200;
spartanportgpu_native_setting(0xfb3);
assert(v.mouse_aim_sensitivity == 200);
v.mouse_prompt_distance = 16;
spartanportgpu_native_setting(0xfae);
assert(v.mouse_prompt_distance == 16);
// Controls' free camera row: unavailable without the hook, else On/Off, toggled by 0xf6f.
v.free_camera_supported = false;
v.free_camera = false;
assert(std::string(text(0xf6f)).find("Unavailable") != std::string::npos && !v.free_camera);
v.free_camera_supported = true;
assert(std::string(text(0xf6d)) == "[ArrowSpacer]Off[ArrowSpacer]");
assert(std::string(text(0xf6f)) == "[ArrowSelectionL]On[ArrowSelectionR]" && v.free_camera);
assert(Read(path).find("free_camera=1\n") != std::string::npos);
assert(std::string(text(0xf6e)).find("On") != std::string::npos);
v.anisotropy = 16;
spartanportgpu_native_setting(0xfb7);
assert(v.vibration == 100 && v.mouse_aim_sensitivity == 100 && v.mouse_prompt_distance == 48);
assert(v.mouse_lock && v.mouse_aim_style == 0 && v.anisotropy == 16 && !v.free_camera);
v.cheats_supported = 0;
v.cheats[0] = false;
assert(std::string(text(0xf10)) == "Unavailable" && !v.cheats[0]);
v.cheats_supported = (1u << SPARTAN_CHEAT_COUNT) - 1;
for (unsigned i = 0; i < SPARTAN_CHEAT_COUNT; ++i) {
v.cheats[i] = false;
assert(std::string(text(0xf00 + i)) == "Off");
assert(std::string(text(0xf10 + i)) == "On" && v.cheats[i]);
assert(std::string(text(0xf10 + i)) == "Off" && !v.cheats[i]);
}
v.qte_supported = false;
v.auto_qte = false;
assert(std::string(text(0xf17)) == "Unavailable" && !v.auto_qte);
v.qte_supported = true;
assert(std::string(text(0xf07)) == "Off");
assert(std::string(text(0xf17)) == "On" && v.auto_qte);
assert(Read(path).find("auto_qte=1\n") != std::string::npos);
v.auto_qte = false;
SpartanSettings::Load();
assert(std::string(text(0xf07)) == "On");
assert(std::string(text(0xf17)) == "Off" && !v.auto_qte);
// Free camera: off by default, kept in the ini (the launcher writes the same key).
v.free_camera = true;
SpartanSettings::Save();
assert(Read(path).find("free_camera=1\n") != std::string::npos);
v.free_camera = false;
SpartanSettings::Load();
assert(v.free_camera);
v.free_camera = false;
SpartanSettings::Save();
v.red_orbs_supported = true;
v.orb_pickup_supported = true;
v.damage_supported = true;
v.red_orb_multiplier = 1.0f;
assert(std::string(text(0xf40)) == "1.10x");
assert(std::string(text(0xf30)) == "1.00x");
v.red_orb_multiplier = 2.0f;
assert(std::string(text(0xf40)) == "2.50x");
assert(std::string(text(0xf30)) == "2.00x");
v.red_orb_multiplier = 10.0f;
assert(std::string(text(0xf40)) == "11.00x");
assert(std::string(text(0xf30)) == "10.00x");
v.red_orb_multiplier = 1.23f;
assert(std::string(text(0xf30)) == "1.20x");
v.red_orb_multiplier = 0.1f;
assert(std::string(text(0xf30)) == "0.10x" && spartanportgpu_native_setting(0xf50) == 0);
v.red_orb_multiplier = 100.0f;
assert(std::string(text(0xf40)) == "100.00x" && spartanportgpu_native_setting(0xf50) == 100);
v.cheats[SPARTAN_CHEAT_ORBS] = true;
assert(std::string(text(0xf30)) == "Disabled" && v.red_orb_multiplier == 100.0f);
v.cheats[SPARTAN_CHEAT_ORBS] = false;
v.damage_supported = false;
const float damage = v.damage_dealt.load();
assert(std::string(text(0xf44)) == "Unavailable" && v.damage_dealt == damage);
assert(spartanportgpu_native_setting(0xf60) == 0 && v.red_orb_multiplier == 1.0f);
assert(Read(path).find("red_orb_multiplier=1.000\n") != std::string::npos);
assert(spartanportgpu_native_setting(0xf08) == 0 && spartanportgpu_native_setting(0xf26) == 0);
SetEnvironment("SPARTAN_LANGUAGE", "1");
v.frames_queued = 1;
assert(std::string(text(0xf70)) == "1");
assert(std::string(text(0xf90)) == "2" && v.frames_queued == 2);
assert(std::string(text(0xf90)) == "Unlimited" && v.frames_queued == 0);
assert(std::string(text(0xf80)) == "2" && v.frames_queued == 2);
v.async_shaders = false;
assert(std::string(text(0xf91)) == "On" && v.async_shaders);
assert(std::string(text(0xf81)) == "Off" && !v.async_shaders);
v.rcas = true;
v.rcas_applied = false;
v.image_width = 2560;
v.window_width = 1920;
v.image_height = v.window_height = 1080;
v.rcas_strength = 75;
assert(std::string(text(0xf93)) == "-" && v.rcas_strength == 75);
v.image_width = v.window_width = 1920;
v.image_height = v.window_height = 1080;
v.rcas = false;
assert(std::string(text(0xf92)) == "On" && v.rcas);
assert(std::string(text(0xf73)) == "75%"); // eligibility does not lag one frame after enabling RCAS
v.rcas_applied = true;
assert(std::string(text(0xf83)) == "70%" && v.rcas_strength == 70);
v.rcas_strength = 100;
assert(std::string(text(0xf93)) == "100%" && v.rcas_strength == 100);
v.rcas_strength = 0;
assert(std::string(text(0xf83)) == "0%" && spartanportgpu_native_setting(0xfa3) == 0);
v.rcas = false;
assert(std::string(text(0xf93)) == "-" && v.rcas_strength == 0);
assert(spartanportgpu_native_setting(0xfc8) == 0);
v.fps_limit = 144;
v.engine_fps = 180;
v.render_resolution = 4;
v.vsync = false;
v.anisotropy = 8;
v.cheats[0] = true;
v.damage_dealt = 2.0f;
assert(spartanportgpu_native_setting(0xfb0) == 0);
assert(v.frames_queued == 1 && v.async_shaders && v.rcas && v.rcas_strength == 75);
assert(v.fps_limit == 144 && v.engine_fps == 180 && v.render_resolution == 4 && !v.vsync);
assert(v.anisotropy == 0 && v.cheats[0] && v.damage_dealt == 2.0f);
v.rcas_strength = 0;
SpartanSettings::Load();
assert(v.rcas_strength == 75 && v.async_shaders && v.frames_queued == 1 && v.rcas);
SetEnvironment("SPARTAN_LANGUAGE", "17");
v.async_shaders = false;
assert(std::string(text(0xf71)) == "Desligada");
assert(std::string(text(0xf91)) == "Ligada");
assert(std::string(text(0xf74)) == "Padrão do jogo");
SetEnvironment("SPARTAN_LANGUAGE", "");
v.render_resolution = 0;
const auto initial_aspect = std::string(text(0xfc1));
assert(initial_aspect.find("16:9") != std::string::npos);
spartanportgpu_native_setting(0xfe1);
assert(std::string(text(0xfc1)).find("16:10") != std::string::npos);
assert(v.render_resolution != 0);
spartanportgpu_native_setting(0xfe0);
assert(std::string(text(0xfc1)).find("16:10") != std::string::npos);
v.monitors = {0, 2};
v.monitor = -1;
spartanportgpu_native_setting(0xfe4);
assert(v.monitor == 0);
spartanportgpu_native_setting(0xfe4);
assert(v.monitor == 2);
spartanportgpu_native_setting(0xfe4);
assert(v.monitor == -1);
v.hdr = false;
spartanportgpu_native_setting(0xfe7);
assert(v.hdr);
SpartanSettings::Save();
v.monitor = 2;
v.hdr = false;
SpartanSettings::Load();
assert(v.monitor == -1 && v.hdr);
v.hdr = !v.startup_hdr;
assert(SpartanSettings::GraphicsNeedRestart());
SetEnvironment("SPARTAN_RESTART_COMMAND", "");
assert(spartanportgpu_native_setting(0xfcb) == 0);
SetEnvironment("SPARTAN_RESTART_COMMAND", "test-only");
assert(spartanportgpu_native_setting(0xfcb) == 1 && v.restart_unconfirmed);
SetEnvironment("SPARTAN_RESTART_COMMAND", "");
v.render_resolution = v.startup_render_resolution;
v.monitor = v.startup_monitor;
v.hdr = v.startup_hdr;
v.restart_unconfirmed = false;
v.fps_limit = 0;
spartanportgpu_native_setting(0xfe2);
assert(v.fps_limit == 30);
spartanportgpu_native_setting(0xfd2);
assert(v.fps_limit == 0);
for (const char* language : {"1", "17"}) {
SetEnvironment("SPARTAN_LANGUAGE", language);
const char* english[] = {"Windowed", "Borderless", "Fullscreen"};
const char* portuguese[] = {"Janela", "Sem bordas", "Tela cheia"};
for (int mode = -1; mode <= G::DisplayModeCount; ++mode) {
v.display_mode = mode;
const int expected = std::clamp(mode, 0, G::DisplayModeCount - 1);
const char* label = language[0] == '1' && language[1] == '7' ? portuguese[expected] : english[expected];
assert(std::string(text(0xfc5)) == std::string("[ArrowSpacer]") + label + "[ArrowSpacer]");
}
}
SetEnvironment("SPARTAN_LANGUAGE", "");
v.display_mode = G::Windowed;
spartanportgpu_native_setting(0xfe5);
assert(v.display_mode == G::Borderless);
spartanportgpu_native_setting(0xfd5);
assert(v.display_mode == G::Windowed);
v.vsync = true;
spartanportgpu_native_setting(0xfe6);
assert(!v.vsync);
v.engine_fps_global = nullptr;
v.engine_fps = 240;
spartanportgpu_native_setting(0xfd3);
assert(v.engine_fps == 240 && std::string(text(0xfc3)).find('-') != std::string::npos);
float live_fps = 240;
int32_t live_fps_integer = 240;
v.engine_fps_global = &live_fps;
v.engine_fps_int_global = &live_fps_integer;
v.applied_engine_fps = 240;
spartanportgpu_native_setting(0xfd3);
assert(v.engine_fps == 180);
SpartanSettings::SyncEngineFps();
assert(live_fps == 180 && live_fps_integer == 180);
assert(!SpartanSettings::GraphicsNeedRestart());
v.engine_fps_global = nullptr;
v.engine_fps_int_global = nullptr;
v.glyph_style = v.startup_glyph_style;
assert(!SpartanSettings::GraphicsNeedRestart());
for (int style = 1; style <= 4; ++style) {
spartanportgpu_native_setting(0xfb1);
assert(v.glyph_style == style % 4);
}
spartanportgpu_native_setting(0xfb1);
assert(v.glyph_style == 1 && SpartanSettings::GraphicsNeedRestart());
assert(Read(path).find("glyph_style=xbox\n") != std::string::npos);
v.glyph_style = 0;
SpartanSettings::Load();
assert(v.glyph_style == 1 && !SpartanSettings::GraphicsNeedRestart());
assert(std::string(text(0xfa4)).find("Xbox") != std::string::npos);
// Options > Graphics: f75+i query, f85+i toggle/next, f95 (and f8d) prev; "Unavailable" until the hook reports in.
// Rows: 0 Texture Quality (f75, f85 next, f95/f8d prev), 1 Shadows (f76, f86), 2 Motion Blur (f77, f87), 3 Bloom (f78, f88).
assert(std::string(text(0xf78)) == "Unavailable" && std::string(text(0xf87)) == "Unavailable");
assert(v.bloom && v.motion_blur);
spartanport_graphics_status(SPARTAN_GRAPHICS_BLOOM);
spartanport_graphics_status(SPARTAN_GRAPHICS_MOTION_BLUR);
spartanport_graphics_status(SPARTAN_GRAPHICS_TEXTURE_QUALITY); // no toggle (no hook): ignored
spartanport_graphics_status(7); // no such toggle: ignored
assert(v.graphics_supported == ((1u << SPARTAN_GRAPHICS_BLOOM) | (1u << SPARTAN_GRAPHICS_MOTION_BLUR)));
assert(spartanport_graphics_flag(SPARTAN_GRAPHICS_BLOOM) == reinterpret_cast<uintptr_t>(&v.bloom) &&
spartanport_graphics_flag(SPARTAN_GRAPHICS_TEXTURE_QUALITY) == 0);
assert(std::string(text(0xf78)) == "On" && std::string(text(0xf88)) == "Off" && !v.bloom);
assert(std::string(text(0xf87)) == "Off" && !v.motion_blur && std::string(text(0xf77)) == "Off");
assert(Read(path).find("bloom=0\n") != std::string::npos && Read(path).find("motion_blur=0\n") != std::string::npos);
assert(std::string(text(0xf88)) == "On" && v.bloom);
assert(std::string(text(0xf76)) == "Unavailable"); // Shadows not reported
// Texture Quality: no hook, always available. f85 steps toward Very Low, f95 (and f8d) toward High;
// both stop at the ends.
assert(std::string(text(0xf75)) == "High" && SpartanSettings::TextureLodBias(v.texture_quality) == 0.0f);
assert(std::string(text(0xf95)) == "High" && v.texture_quality == 0);
assert(std::string(text(0xf85)) == "Medium" && SpartanSettings::TextureLodBias(v.texture_quality) == 1.0f);
assert(std::string(text(0xf85)) == "Low" && std::string(text(0xf85)) == "Very Low");
assert(std::string(text(0xf85)) == "Very Low" && SpartanSettings::TextureLodBias(v.texture_quality) == 3.0f);
assert(Read(path).find("texture_quality=3\n") != std::string::npos);
assert(std::string(text(0xf95)) == "Low" && std::string(text(0xf95)) == "Medium");
assert(std::string(text(0xf95)) == "High" && v.texture_quality == 0);
assert(std::string(text(0xf85)) == "Medium");
assert(std::string(text(0xf8d)) == "High" && v.texture_quality == 0);
const bool blur_before = v.motion_blur;
assert(std::string(text(0xf97)) == "On" && v.motion_blur != blur_before); // toggles toggle with f90 too
assert(SpartanSettings::TextureLodBias(-1) == 0.0f && SpartanSettings::TextureLodBias(9) == 0.0f);
TestEngineFpsCap(path);
std::remove(path);
std::puts("PASS: graphics settings");
}