Rewrite settings handling to load on startup

Now uses json to store, manually editable

Closes #644
This commit is contained in:
Kurt 2026-01-27 19:27:18 -06:00
parent 0e26ad7099
commit a6d21f515f
11 changed files with 211 additions and 274 deletions

View File

@ -0,0 +1,117 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows.Forms;
namespace NHSE.WinForms;
/// <summary>
/// Application settings stored as JSON.
/// </summary>
public sealed class ApplicationSettings
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
/// <summary>
/// Last opened save file path.
/// </summary>
public string LastFilePath { get; set; } = string.Empty;
/// <summary>
/// SysBot connection and RAM editing settings.
/// </summary>
public SysBotSettings SysBot { get; set; } = new();
/// <summary>
/// Whether to automatically backup save files.
/// </summary>
public bool AutomaticBackup { get; set; } = true;
/// <summary>
/// Whether the backup prompt has been shown.
/// </summary>
public bool BackupPrompted { get; set; }
/// <summary>
/// UI language code (e.g., "en", "de", "jp").
/// </summary>
public string Language { get; set; } = "en";
/// <summary>
/// Dark mode setting.
/// </summary>
public SystemColorMode DarkMode { get; set; } = SystemColorMode.System;
/// <summary>
/// Loads settings from disk, or creates default settings if the file doesn't exist.
/// </summary>
/// <param name="path">Full path to the settings file.</param>
public static ApplicationSettings Load(string path)
{
if (!File.Exists(path))
return new ApplicationSettings();
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<ApplicationSettings>(json, JsonOptions) ?? new ApplicationSettings();
}
catch
{
// If deserialization fails, return defaults
return new ApplicationSettings();
}
}
/// <summary>
/// Saves the current settings to disk.
/// </summary>
/// <param name="path">Full path to the settings file.</param>
public void Save(string path)
{
try
{
var json = JsonSerializer.Serialize(this, JsonOptions);
File.WriteAllText(path, json);
}
catch
{
// Silently fail if we can't save settings
}
}
}
/// <summary>
/// SysBot connection and RAM editing settings.
/// </summary>
public sealed class SysBotSettings
{
/// <summary>
/// SysBot IP address.
/// </summary>
public string IP { get; set; } = "192.168.0.1";
/// <summary>
/// SysBot port number.
/// </summary>
public int Port { get; set; } = 6000;
/// <summary>
/// Whether the SysBot prompt has been shown.
/// </summary>
public bool Prompted { get; set; }
/// <summary>
/// RAM offset for pouch editing.
/// </summary>
public uint PouchOffset { get; set; }
/// <summary>
/// RAM offset for generic editing.
/// </summary>
public uint GenericOffset { get; set; }
}

View File

@ -1,7 +1,6 @@
using NHSE.Core;
using NHSE.Injection;
using NHSE.Sprites;
using NHSE.WinForms.Properties;
using System;
using System.Drawing;
using System.Drawing.Imaging;
@ -24,8 +23,6 @@ public sealed partial class Editor : Form
public Editor(HorizonSave file)
{
WinFormsUtil.SetApplicationTheme(int.Parse(Settings.Default.DarkMode));
InitializeComponent();
SAV = file;
@ -35,7 +32,7 @@ public Editor(HorizonSave file)
LoadMain();
var lang = Settings.Default.Language;
var lang = Program.Settings.Language;
var index = GameLanguage.GetLanguageIndex(lang);
Menu_Language.SelectedIndex = index; // triggers translation
// this.TranslateInterface(GameInfo.CurrentLanguage);
@ -57,9 +54,7 @@ private void Menu_Language_SelectedIndexChanged(object sender, EventArgs e)
var lang = GameInfo.SetLanguage2Char(Menu_Language.SelectedIndex);
this.TranslateInterface(lang);
var settings = Settings.Default;
settings.Language = lang;
settings.Save();
Program.Settings.Language = lang;
Task.Run(() =>
{
@ -163,26 +158,26 @@ private void Menu_ItemImages_Click(object sender, EventArgs e)
private void Menu_Theme_System_Click(object sender, EventArgs e)
{
Menu_Options.DropDown.Close();
ThemeChangeDialog(1);
ThemeChangeDialog(SystemColorMode.System);
}
private void Menu_Theme_Classic_Click(object sender, EventArgs e)
{
Menu_Options.DropDown.Close();
ThemeChangeDialog(0);
ThemeChangeDialog(SystemColorMode.Classic);
}
private void Menu_Theme_Dark_Click(object sender, EventArgs e)
{
Menu_Options.DropDown.Close();
ThemeChangeDialog(2);
ThemeChangeDialog(SystemColorMode.Dark);
}
private void ThemeChangeDialog(int theme)
private void ThemeChangeDialog(SystemColorMode theme)
{
TaskDialogButton yesButton = new(MessageStrings.MsgDialogButtonYes) { Tag = DialogResult.Yes };
TaskDialogButton noButton = new(MessageStrings.MsgDialogButtonNo) { Tag = DialogResult.No };
TaskDialogButton cancelButton = new(MessageStrings.MsgDialogButtonCancel) { Tag = DialogResult.Cancel };
var yesButton = new TaskDialogButton(MessageStrings.MsgDialogButtonYes) { Tag = DialogResult.Yes };
var noButton = new TaskDialogButton(MessageStrings.MsgDialogButtonNo) { Tag = DialogResult.No };
var cancelButton = new TaskDialogButton(MessageStrings.MsgDialogButtonCancel) { Tag = DialogResult.Cancel };
var buttons = new TaskDialogButtonCollection
{
yesButton,
@ -199,7 +194,7 @@ private void ThemeChangeDialog(int theme)
Buttons = buttons
};
TaskDialogButton resultButton = TaskDialog.ShowDialog(this, page);
var resultButton = TaskDialog.ShowDialog(this, page);
if (resultButton == yesButton)
{
SaveAll();
@ -213,14 +208,19 @@ private void ThemeChangeDialog(int theme)
return;
}
WinFormsUtil.Alert(MessageStrings.MsgSaveDataExportSuccess);
WinFormsUtil.SetApplicationTheme(theme);
Application.Restart();
}
else if (resultButton == noButton)
{
WinFormsUtil.SetApplicationTheme(theme);
Application.Restart();
// Don't save.
}
else
{
// Abort
return;
}
WinFormsUtil.SetApplicationTheme(theme);
Program.SaveSettings();
Application.Restart();
}
private void ReloadAll()

View File

@ -5,7 +5,6 @@
using NHSE.Core;
using NHSE.Injection;
using NHSE.Sprites;
using NHSE.WinForms.Properties;
namespace NHSE.WinForms;
@ -22,8 +21,6 @@ public partial class Main : Form
public Main()
{
WinFormsUtil.SetApplicationTheme(int.Parse(Settings.Default.DarkMode));
InitializeComponent();
// Flash to front
@ -40,6 +37,12 @@ public Main()
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
Program.SaveSettings();
base.OnFormClosing(e);
}
private static void Open(HorizonSave file)
{
bool sized = file.ValidateSizes();
@ -85,7 +88,7 @@ private void Menu_Open(object sender, EventArgs e)
}
else if ((ModifierKeys & Keys.Shift) != 0)
{
var path = Settings.Default.LastFilePath;
var path = Program.Settings.LastFilePath;
if (Directory.Exists(path))
{
Open(path);
@ -149,22 +152,27 @@ private static void OpenSaveFile(string path)
var file = HorizonSave.FromFolder(path);
Open(file);
var settings = Settings.Default;
var settings = Program.Settings;
settings.LastFilePath = path;
if (!settings.BackupPrompted)
{
settings.BackupPrompted = true;
var line1 = string.Format(MessageStrings.MsgBackupCreateLocation, BackupFolderName);
var line2 = MessageStrings.MsgBackupCreateQuestion;
var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, line1, line2);
settings.AutomaticBackup = prompt == DialogResult.Yes;
if (!Directory.Exists(BackupFolderName))
{
var line1 = string.Format(MessageStrings.MsgBackupCreateLocation, BackupFolderName);
var line2 = MessageStrings.MsgBackupCreateQuestion;
var prompt = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, line1, line2);
settings.AutomaticBackup = prompt == DialogResult.Yes;
}
else
{
settings.AutomaticBackup = true; // previously enabled, settings reset?
}
}
if (settings.AutomaticBackup)
BackupSaveFile(file, path, BackupPath);
settings.Save();
}
private static void BackupSaveFile(HorizonSave file, string path, string bak)

View File

@ -26,11 +26,6 @@
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>

View File

@ -1,31 +1,53 @@
using NHSE.WinForms.Properties;
using System;
using System;
using System.IO;
using System.Windows.Forms;
namespace NHSE.WinForms;
internal static class Program
{
/// <summary>
/// Directory where the executable is located.
/// </summary>
private static readonly string WorkingDirectory = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
/// <summary>
/// Settings file name.
/// </summary>
private const string ConfigFileName = "settings.json";
/// <summary>
/// Full path to the settings file.
/// </summary>
private static string PathConfig => Path.Combine(WorkingDirectory, ConfigFileName);
/// <summary>
/// Application settings, loaded at startup.
/// </summary>
public static ApplicationSettings Settings { get; }
/// <summary>
/// Static constructor to load settings as early as possible.
/// </summary>
static Program()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Settings = ApplicationSettings.Load(PathConfig);
WinFormsUtil.SetApplicationTheme(Settings.DarkMode);
}
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var args = Environment.GetCommandLineArgs();
if (args.Length > 1)
{
if (args.Contains("-dark"))
{
WinFormsUtil.SetApplicationTheme(2);
Settings.Default.DarkMode = "2";
Settings.Default.Save();
}
}
if (args.Contains("-dark"))
WinFormsUtil.SetApplicationTheme(SystemColorMode.Dark);
Application.Run(new Main());
}
public static void SaveSettings() => Settings.Save(PathConfig);
}

View File

@ -1,149 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace NHSE.WinForms.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string LastFilePath {
get {
return ((string)(this["LastFilePath"]));
}
set {
this["LastFilePath"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("192.168.0.1")]
public string SysBotIP {
get {
return ((string)(this["SysBotIP"]));
}
set {
this["SysBotIP"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("6000")]
public int SysBotPort {
get {
return ((int)(this["SysBotPort"]));
}
set {
this["SysBotPort"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool SysBotPrompted {
get {
return ((bool)(this["SysBotPrompted"]));
}
set {
this["SysBotPrompted"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
public uint SysBotPouchOffset {
get {
return ((uint)(this["SysBotPouchOffset"]));
}
set {
this["SysBotPouchOffset"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
public uint SysBotGenericOffset {
get {
return ((uint)(this["SysBotGenericOffset"]));
}
set {
this["SysBotGenericOffset"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool AutomaticBackup {
get {
return ((bool)(this["AutomaticBackup"]));
}
set {
this["AutomaticBackup"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool BackupPrompted {
get {
return ((bool)(this["BackupPrompted"]));
}
set {
this["BackupPrompted"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("en")]
public string Language {
get {
return ((string)(this["Language"]));
}
set {
this["Language"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("1")]
public string DarkMode
{
get
{
return ((string)(this["DarkMode"]));
}
set
{
this["DarkMode"] = value;
}
}
}
}

View File

@ -1,33 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="NHSE.WinForms.Properties" GeneratedClassName="Settings">
<Profiles />
<Settings>
<Setting Name="LastFilePath" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="SysBotIP" Type="System.String" Scope="User">
<Value Profile="(Default)">192.168.0.1</Value>
</Setting>
<Setting Name="SysBotPort" Type="System.Int32" Scope="User">
<Value Profile="(Default)">6000</Value>
</Setting>
<Setting Name="SysBotPrompted" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="SysBotPouchOffset" Type="System.UInt32" Scope="User">
<Value Profile="(Default)">2890343376</Value>
</Setting>
<Setting Name="SysBotGenericOffset" Type="System.UInt32" Scope="User">
<Value Profile="(Default)">2890343376</Value>
</Setting>
<Setting Name="AutomaticBackup" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="BackupPrompted" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="Language" Type="System.String" Scope="User">
<Value Profile="(Default)">en</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -1,22 +1,18 @@
using System.Windows.Forms;
using NHSE.WinForms.Properties;
namespace NHSE.WinForms;
public partial class SettingsEditor : Form
{
private readonly Settings obj;
public SettingsEditor()
{
obj = Settings.Default;
InitializeComponent();
PG_Settings.SelectedObject = obj;
PG_Settings.SelectedObject = Program.Settings;
}
private void SettingsEditor_FormClosing(object sender, FormClosingEventArgs e)
{
obj.Save();
// Settings are saved when Main form closes
}
private void SettingsEditor_KeyDown(object sender, KeyEventArgs e)

View File

@ -1,17 +1,17 @@
using System;
using System.Windows.Forms;
using NHSE.Injection;
using NHSE.WinForms.Properties;
namespace NHSE.WinForms;
public sealed class SysBotController(InjectionType type)
{
public readonly SysBot Bot = new();
private readonly Settings _settings = Settings.Default;
public string IP => _settings.SysBotIP;
public string Port => _settings.SysBotPort.ToString();
private static SysBotSettings Config => Program.Settings.SysBot;
public string IP => Config.IP;
public string Port => Config.Port.ToString();
public bool Connect(string ip, string port)
{
@ -28,17 +28,16 @@ public bool Connect(string ip, string port)
return false;
}
_settings.SysBotIP = ip;
_settings.SysBotPort = p;
_settings.Save();
Config.IP = ip;
Config.Port = p;
return true;
}
public uint GetDefaultOffset() => type switch
{
InjectionType.Generic => _settings.SysBotGenericOffset,
InjectionType.Pouch => _settings.SysBotPouchOffset,
InjectionType.Generic => Config.GenericOffset,
InjectionType.Pouch => Config.PouchOffset,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
@ -46,11 +45,10 @@ public void SetOffset(uint value)
{
switch (type)
{
case InjectionType.Generic: _settings.SysBotGenericOffset = value; break;
case InjectionType.Pouch: _settings.SysBotPouchOffset = value; break;
case InjectionType.Generic: Config.GenericOffset = value; break;
case InjectionType.Pouch: Config.PouchOffset = value; break;
default: return;
}
_settings.Save();
}
public void HexEdit(uint offset, int length)
@ -78,12 +76,11 @@ public void HexEdit(uint offset, int length)
public void PopPrompt()
{
if (_settings.SysBotPrompted)
if (Config.Prompted)
return;
WinFormsUtil.Alert(MessageStrings.MsgSysBotInfo, MessageStrings.MsgSysBotRequired);
_settings.SysBotPrompted = true;
_settings.Save();
Config.Prompted = true;
}
public void WriteBytes(byte[] data, uint offset)

View File

@ -4,7 +4,6 @@
using System.Linq;
using System.Windows.Forms;
using NHSE.Core;
using NHSE.WinForms.Properties;
namespace NHSE.WinForms;
#if DEBUG
@ -66,7 +65,7 @@ private static void UpdateTranslations()
private static void LoadSpecialForms()
{
// For forms that require more complete initialization (dynamically added user controls)
var path = Settings.Default.LastFilePath;
var path = Program.Settings.LastFilePath;
var sav = HorizonSave.FromFolder(path);
using var editor = new Editor(sav);
using var so = new SingleObjectEditor<object>(new object(), PropertySort.NoSort, false);

View File

@ -1,5 +1,4 @@
using NHSE.WinForms.Properties;
using System;
using System;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Linq;
@ -95,23 +94,9 @@ internal static void CenterToForm(this Control child, Control parent)
/// Sets the application color mode based on the <paramref name="theme"/> <typeparamref name="int"/> passed to it and stores it in the application <see cref="Settings"/>.
/// </summary>
/// <param name="theme"></param>
public static void SetApplicationTheme(int theme)
public static void SetApplicationTheme(SystemColorMode theme)
{
switch (theme)
{
case 0:
Application.SetColorMode(SystemColorMode.Classic);
break;
case 1:
Application.SetColorMode(SystemColorMode.System);
break;
case 2:
Application.SetColorMode(SystemColorMode.Dark);
break;
}
var settings = Settings.Default;
settings.DarkMode = theme.ToString();
settings.Save();
Application.SetColorMode(theme);
Program.Settings.DarkMode = theme;
}
}