258 lines
9.8 KiB
C#
258 lines
9.8 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Text.Json;
|
|
|
|
namespace SharpEmu.GUI;
|
|
|
|
public sealed class GuiSettings
|
|
{
|
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
};
|
|
|
|
public List<string> GameFolders { get; set; } = new();
|
|
|
|
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
|
|
public List<string> ExcludedGames { get; set; } = new();
|
|
|
|
public string LogLevel { get; set; } = "Info";
|
|
|
|
public int ImportTraceLimit { get; set; }
|
|
|
|
public bool StrictDynlibResolution { get; set; }
|
|
|
|
/// <summary>
|
|
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
|
/// </summary>
|
|
public bool LogToFile { get; set; }
|
|
|
|
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
|
|
public string? LogFilePath { get; set; }
|
|
|
|
/// <summary>
|
|
/// If <see cref="OverrideLogFile"/> is false it appends <titleId>-<timestamp> to the filename specified by
|
|
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
|
|
/// </summary>
|
|
public bool OverrideLogFile { get; set; }
|
|
|
|
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
|
|
public bool PlayTitleMusic { get; set; } = true;
|
|
|
|
public string LibraryLayout { get; set; } = "Carousel";
|
|
|
|
public double EmbeddedConsoleHeight { get; set; } = 240;
|
|
public double ConsoleWindowWidth { get; set; } = 980;
|
|
public double ConsoleWindowHeight { get; set; } = 620;
|
|
public int? ConsoleWindowLeft { get; set; }
|
|
public int? ConsoleWindowTop { get; set; }
|
|
public bool ConsoleWindowMaximized { get; set; }
|
|
|
|
public string? EmulatorPath { get; set; }
|
|
|
|
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
|
|
public string Language { get; set; } = "en";
|
|
|
|
public string ConsoleType { get; set; } = "PS5";
|
|
|
|
public string ConsoleLanguage { get; set; } = "EnglishUS";
|
|
|
|
/// <summary>Default text-entry profile exposed to games.</summary>
|
|
public string DefaultProfile { get; set; } = "Sharp";
|
|
|
|
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
|
public bool DiscordRichPresence { get; set; } = true;
|
|
|
|
public bool CheckForUpdatesOnStartup { get; set; } = true;
|
|
|
|
public string WindowMode { get; set; } = "Windowed";
|
|
|
|
public string Resolution { get; set; } = "1920x1080";
|
|
|
|
public int DisplayIndex { get; set; }
|
|
|
|
public int RefreshRate { get; set; }
|
|
|
|
public string ScalingMode { get; set; } = "Fit";
|
|
|
|
public bool VSync { get; set; } = true;
|
|
|
|
public string HdrMode { get; set; } = "Auto";
|
|
|
|
public bool OverlayEnabled { get; set; } = true;
|
|
public string OverlayCorner { get; set; } = "TopRight";
|
|
public string OverlayMode { get; set; } = "TitleBar";
|
|
|
|
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
|
public List<string> EnvironmentToggles { get; set; } = ["SHARPEMU_WRITABLE_APP0", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"];
|
|
|
|
public string BinkPlaybackMode { get; set; } = "Guest";
|
|
|
|
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
|
public double RenderResolutionScale { get; set; } = 1.0;
|
|
|
|
/// <summary>
|
|
/// Discord application ID used for Rich Presence; the default is the
|
|
/// SharpEmu application. Override to rebrand what Discord shows as
|
|
/// "Playing …" (register at discord.com/developers/applications).
|
|
/// </summary>
|
|
public string DiscordClientId { get; set; } = "1525606762248540221";
|
|
|
|
// The emulator is portable and keeps its data next to the executable;
|
|
// the GUI follows the same convention.
|
|
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
|
|
|
public static GuiSettings Load()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(SettingsPath))
|
|
{
|
|
var json = File.ReadAllText(SettingsPath);
|
|
return NormalizeFromJson(json);
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Corrupt or unreadable settings fall back to defaults.
|
|
}
|
|
|
|
return new GuiSettings();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes settings and normalizes null references and null or empty list
|
|
/// entries introduced by JSON. Empty scalar strings remain unchanged.
|
|
/// </summary>
|
|
internal static GuiSettings NormalizeFromJson(string json)
|
|
{
|
|
var settings = JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
|
|
|
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
|
|
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
|
|
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles ?? ["SHARPEMU_WRITABLE_APP0", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"]);
|
|
settings.LogLevel ??= "Info";
|
|
settings.Language ??= "en";
|
|
var legacyProfile = settings.EnvironmentToggles
|
|
.Select(entry => entry.Split('=', 2, StringSplitOptions.TrimEntries))
|
|
.FirstOrDefault(parts =>
|
|
parts.Length == 2 &&
|
|
string.Equals(parts[0], "SHARPEMU_DEFAULT_PROFILE", StringComparison.OrdinalIgnoreCase));
|
|
settings.EnvironmentToggles.RemoveAll(entry =>
|
|
string.Equals(
|
|
entry.Split('=', 2, StringSplitOptions.TrimEntries)[0],
|
|
"SHARPEMU_DEFAULT_PROFILE",
|
|
StringComparison.OrdinalIgnoreCase));
|
|
settings.DefaultProfile = NormalizeDefaultProfile(
|
|
legacyProfile is { Length: 2 } ? legacyProfile[1] : settings.DefaultProfile);
|
|
settings.DiscordClientId ??= "1525606762248540221";
|
|
settings.LibraryLayout = NormalizeChoice(settings.LibraryLayout, "Carousel", "Grid");
|
|
settings.ConsoleType = NormalizeChoice(settings.ConsoleType, "PS5");
|
|
settings.ConsoleLanguage = NormalizeChoice(
|
|
settings.ConsoleLanguage,
|
|
"EnglishUS",
|
|
"Japanese",
|
|
"French",
|
|
"SpanishSpain",
|
|
"German",
|
|
"Italian",
|
|
"Dutch",
|
|
"PortuguesePortugal",
|
|
"Russian",
|
|
"Korean",
|
|
"ChineseTraditional",
|
|
"ChineseSimplified",
|
|
"Finnish",
|
|
"Swedish",
|
|
"Danish",
|
|
"Norwegian",
|
|
"Polish",
|
|
"PortugueseBrazil",
|
|
"EnglishUK",
|
|
"Turkish",
|
|
"SpanishLatinAmerica",
|
|
"Arabic",
|
|
"FrenchCanada",
|
|
"Czech",
|
|
"Hungarian",
|
|
"Greek",
|
|
"Romanian",
|
|
"Thai",
|
|
"Vietnamese",
|
|
"Indonesian",
|
|
"Ukrainian");
|
|
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
|
|
settings.Resolution = NormalizeResolution(settings.Resolution);
|
|
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
|
|
settings.HdrMode = NormalizeChoice(settings.HdrMode, "Auto", "On", "Off");
|
|
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Guest", "Host", "Skip");
|
|
settings.OverlayCorner = NormalizeChoice(settings.OverlayCorner, "TopRight", "TopLeft", "BottomRight", "BottomLeft");
|
|
settings.OverlayMode = NormalizeChoice(settings.OverlayMode, "TitleBar", "Full", "Minimal");
|
|
settings.DisplayIndex = Math.Max(0, settings.DisplayIndex);
|
|
settings.RefreshRate = Math.Clamp(settings.RefreshRate, 0, 1000);
|
|
settings.EmbeddedConsoleHeight = NormalizeConsoleSize(settings.EmbeddedConsoleHeight, 240, 120);
|
|
settings.ConsoleWindowWidth = NormalizeConsoleSize(settings.ConsoleWindowWidth, 980, 520);
|
|
settings.ConsoleWindowHeight = NormalizeConsoleSize(settings.ConsoleWindowHeight, 620, 320);
|
|
if (!settings.ConsoleWindowLeft.HasValue || !settings.ConsoleWindowTop.HasValue)
|
|
{
|
|
settings.ConsoleWindowLeft = null;
|
|
settings.ConsoleWindowTop = null;
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
// JSON can populate non-nullable lists with null references and entries.
|
|
private static List<string> FilterNullOrEmpty(List<string>? source)
|
|
{
|
|
if (source is null)
|
|
{
|
|
return [];
|
|
}
|
|
|
|
return source.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
|
|
}
|
|
|
|
private static string NormalizeChoice(string? value, string fallback, params string[] choices) =>
|
|
choices.Prepend(fallback).FirstOrDefault(
|
|
choice => string.Equals(choice, value, StringComparison.OrdinalIgnoreCase)) ?? fallback;
|
|
|
|
private static double NormalizeConsoleSize(double value, double fallback, double minimum) =>
|
|
double.IsFinite(value) && value >= minimum ? Math.Min(value, 16384) : fallback;
|
|
|
|
private static string NormalizeResolution(string? value)
|
|
{
|
|
if (!HostDisplayOptions.TryParseResolution(value, out var width, out var height))
|
|
{
|
|
return "1920x1080";
|
|
}
|
|
|
|
return $"{width}x{height}";
|
|
}
|
|
|
|
internal static string NormalizeDefaultProfile(string? value)
|
|
{
|
|
var trimmed = value?.Trim();
|
|
return string.IsNullOrEmpty(trimmed) ? "Sharp" : trimmed;
|
|
}
|
|
|
|
public void Save()
|
|
{
|
|
try
|
|
{
|
|
var directory = Path.GetDirectoryName(SettingsPath);
|
|
if (!string.IsNullOrEmpty(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(this, SerializerOptions));
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Settings persistence is best-effort.
|
|
}
|
|
}
|
|
}
|