mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 06:59:45 +08:00
Add per-game launch overrides (log level, import-trace limit, strict dynlib resolution, log-to-file, and SHARPEMU_* environment toggles) with three-tier resolution (per-game override -> global preference -> built-in default), stored one file per game at user/custom_configs/<titleId>.json. Editable from a new "Game settings..." context-menu dialog. Introduce a shared SettingRow control and adopt it across the Options page and the per-game dialog so the two read as one app. Fully localized (reusing the existing Options.* keys), with the actions pinned in the dialog footer.
This commit is contained in:
@@ -104,6 +104,7 @@ public partial class MainWindow : Window
|
||||
string EbootPath,
|
||||
string DisplayName,
|
||||
string? TitleId,
|
||||
string LogLevel,
|
||||
SharpEmuRuntimeOptions RuntimeOptions);
|
||||
|
||||
public MainWindow()
|
||||
@@ -204,6 +205,7 @@ public partial class MainWindow : Window
|
||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Clipboard.Path");
|
||||
CtxCopyTitleId.Click += async (_, _) =>
|
||||
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Clipboard.TitleId");
|
||||
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
|
||||
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
|
||||
|
||||
Opened += async (_, _) => await OnOpenedAsync();
|
||||
@@ -573,6 +575,7 @@ public partial class MainWindow : Window
|
||||
CtxOpenFolder.Header = loc.Get("Library.Context.OpenFolder");
|
||||
CtxCopyPath.Header = loc.Get("Library.Context.CopyPath");
|
||||
CtxCopyTitleId.Header = loc.Get("Library.Context.CopyTitleId");
|
||||
CtxGameSettings.Header = loc.Get("Library.Context.GameSettings");
|
||||
CtxRemove.Header = loc.Get("Library.Context.Remove");
|
||||
|
||||
EmptyAddFolderButton.Content = loc.Get("Library.Empty.AddFolder");
|
||||
@@ -582,27 +585,27 @@ public partial class MainWindow : Window
|
||||
EnvTabItem.Header = loc.Get("Options.Env.Tab");
|
||||
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
|
||||
EnvDesc.Text = loc.Get("Options.Env.Desc");
|
||||
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
|
||||
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
|
||||
EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.Desc");
|
||||
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
|
||||
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||
EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
|
||||
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
|
||||
EnvBthidRow.Description = loc.Get("Options.Env.Bthid.Desc");
|
||||
EnvLoopGuardRow.Description = loc.Get("Options.Env.LoopGuard.Desc");
|
||||
EnvWritableApp0Row.Description = loc.Get("Options.Env.WritableApp0.Desc");
|
||||
EnvVkValidationRow.Description = loc.Get("Options.Env.VkValidation.Desc");
|
||||
EnvDumpSpirvRow.Description = loc.Get("Options.Env.DumpSpirv.Desc");
|
||||
EnvLogDirectMemoryRow.Description = loc.Get("Options.Env.LogDirectMemory.Desc");
|
||||
EnvLogIoRow.Description = loc.Get("Options.Env.LogIo.Desc");
|
||||
EnvLogNpRow.Description = loc.Get("Options.Env.LogNp.Desc");
|
||||
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
|
||||
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
|
||||
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
|
||||
|
||||
CpuEngineLabel.Text = loc.Get("Options.CpuEngine.Label");
|
||||
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||
CpuEngineRow.Label = loc.Get("Options.CpuEngine.Label");
|
||||
CpuEngineRow.Description = loc.Get("Options.CpuEngine.Desc");
|
||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
||||
|
||||
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||
StrictRow.Label = loc.Get("Options.Strict.Label");
|
||||
StrictRow.Description = loc.Get("Options.Strict.Desc");
|
||||
|
||||
LogLevelLabel.Text = loc.Get("Options.LogLevel.Label");
|
||||
LogLevelDesc.Text = loc.Get("Options.LogLevel.Desc");
|
||||
LogLevelRow.Label = loc.Get("Options.LogLevel.Label");
|
||||
LogLevelRow.Description = loc.Get("Options.LogLevel.Desc");
|
||||
LogLevelTraceItem.Content = loc.Get("Options.LogLevel.Trace");
|
||||
LogLevelDebugItem.Content = loc.Get("Options.LogLevel.Debug");
|
||||
LogLevelInfoItem.Content = loc.Get("Options.LogLevel.Info");
|
||||
@@ -610,29 +613,29 @@ public partial class MainWindow : Window
|
||||
LogLevelErrorItem.Content = loc.Get("Options.LogLevel.Error");
|
||||
LogLevelCriticalItem.Content = loc.Get("Options.LogLevel.Critical");
|
||||
|
||||
TraceImportsLabel.Text = loc.Get("Options.TraceImports.Label");
|
||||
TraceImportsDesc.Text = loc.Get("Options.TraceImports.Desc");
|
||||
TraceImportsRow.Label = loc.Get("Options.TraceImports.Label");
|
||||
TraceImportsRow.Description = loc.Get("Options.TraceImports.Desc");
|
||||
|
||||
LogToFileLabel.Text = loc.Get("Options.LogToFile.Label");
|
||||
LogToFileDesc.Text = loc.Get("Options.LogToFile.Desc");
|
||||
LogToFileRow.Label = loc.Get("Options.LogToFile.Label");
|
||||
LogToFileRow.Description = loc.Get("Options.LogToFile.Desc");
|
||||
|
||||
LogFilePathLabel.Text = loc.Get("Options.LogFilePath.Label");
|
||||
LogFilePathRow.Label = loc.Get("Options.LogFilePath.Label");
|
||||
SelectLogFilePathButton.Content = loc.Get("Options.LogFilePath.Select");
|
||||
UpdateLogFilePathText();
|
||||
|
||||
OverrideLogFileLabel.Text = loc.Get("Options.OverrideLogFile.Label");
|
||||
OverrideLogFileDesc.Text = loc.Get("Options.OverrideLogFile.Desc");
|
||||
OverrideLogFileRow.Label = loc.Get("Options.OverrideLogFile.Label");
|
||||
OverrideLogFileRow.Description = loc.Get("Options.OverrideLogFile.Desc");
|
||||
|
||||
LanguageLabel.Text = loc.Get("Options.Language.Label");
|
||||
LanguageDesc.Text = loc.Get("Options.Language.Desc");
|
||||
LanguageRow.Label = loc.Get("Options.Language.Label");
|
||||
LanguageRow.Description = loc.Get("Options.Language.Desc");
|
||||
|
||||
TitleMusicLabel.Text = loc.Get("Options.TitleMusic.Label");
|
||||
TitleMusicDesc.Text = loc.Get("Options.TitleMusic.Desc");
|
||||
TitleMusicRow.Label = loc.Get("Options.TitleMusic.Label");
|
||||
TitleMusicRow.Description = loc.Get("Options.TitleMusic.Desc");
|
||||
|
||||
DiscordLabel.Text = loc.Get("Options.Discord.Label");
|
||||
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
|
||||
AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
|
||||
AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
|
||||
DiscordRow.Label = loc.Get("Options.Discord.Label");
|
||||
DiscordRow.Description = loc.Get("Options.Discord.Desc");
|
||||
AutoUpdateRow.Label = loc.Get("Updater.Auto.Label");
|
||||
AutoUpdateRow.Description = loc.Get("Updater.Auto.Desc");
|
||||
|
||||
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
|
||||
{
|
||||
@@ -952,7 +955,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void UpdateLogFilePathText()
|
||||
{
|
||||
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||
LogFilePathRow.Description = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||
? Localization.Instance.Get("Options.LogFilePath.Default")
|
||||
: _settings.LogFilePath;
|
||||
}
|
||||
@@ -1379,6 +1382,25 @@ public partial class MainWindow : Window
|
||||
GameList.SelectedItem = game;
|
||||
CtxLaunch.IsEnabled = !_isRunning;
|
||||
CtxCopyTitleId.IsEnabled = game.TitleId is not null;
|
||||
CtxGameSettings.IsEnabled = !string.IsNullOrWhiteSpace(game.TitleId);
|
||||
}
|
||||
|
||||
private void OpenSelectedGameSettings()
|
||||
{
|
||||
if (GameList.SelectedItem is not GameEntry game)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(game.TitleId))
|
||||
{
|
||||
AppendConsoleLine(
|
||||
"[GUI][WARN] Per-game settings require a title ID, which this game does not have.",
|
||||
WarningLineBrush);
|
||||
return;
|
||||
}
|
||||
|
||||
_ = new PerGameSettingsDialog(game.TitleId, game.Name, _settings).ShowDialog(this);
|
||||
}
|
||||
|
||||
private void OpenSelectedGameFolder()
|
||||
@@ -1671,34 +1693,39 @@ public partial class MainWindow : Window
|
||||
return;
|
||||
}
|
||||
|
||||
var resolvedTitleId = string.IsNullOrWhiteSpace(titleId)
|
||||
? _allGames.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?.TitleId
|
||||
: titleId;
|
||||
var effective = EffectiveLaunchSettings.Resolve(_settings, PerGameSettings.Load(resolvedTitleId));
|
||||
|
||||
_sndPreview.Stop();
|
||||
_consoleLines.Clear();
|
||||
_allConsoleLines.Clear();
|
||||
|
||||
DropFileLog();
|
||||
if (_settings.LogToFile)
|
||||
if (effective.LogToFile)
|
||||
{
|
||||
OpenFileLog(titleId);
|
||||
OpenFileLog(resolvedTitleId);
|
||||
}
|
||||
|
||||
// The isolated game child inherits these diagnostics. Keep them on the
|
||||
// launcher process so every platform receives the same launch options.
|
||||
foreach (var staleName in _appliedEnvironmentVariables)
|
||||
{
|
||||
if (!_settings.EnvironmentToggles.Contains(staleName))
|
||||
if (!effective.EnvironmentToggles.Contains(staleName))
|
||||
{
|
||||
Environment.SetEnvironmentVariable(staleName, null);
|
||||
}
|
||||
}
|
||||
|
||||
_appliedEnvironmentVariables.Clear();
|
||||
foreach (var name in _settings.EnvironmentToggles)
|
||||
foreach (var name in effective.EnvironmentToggles)
|
||||
{
|
||||
Environment.SetEnvironmentVariable(name, "1");
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
if (SharpEmuLog.TryParseLevel(_settings.LogLevel, out var logLevel))
|
||||
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
||||
{
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
}
|
||||
@@ -1706,15 +1733,14 @@ public partial class MainWindow : Window
|
||||
var runtimeOptions = new SharpEmuRuntimeOptions
|
||||
{
|
||||
CpuEngine = CpuExecutionEngine.NativeOnly,
|
||||
StrictDynlibResolution = _settings.StrictDynlibResolution,
|
||||
ImportTraceLimit = Math.Max(0, _settings.ImportTraceLimit),
|
||||
StrictDynlibResolution = effective.StrictDynlibResolution,
|
||||
ImportTraceLimit = Math.Max(0, effective.ImportTraceLimit),
|
||||
};
|
||||
|
||||
_isRunning = true;
|
||||
_runningGameName = displayName;
|
||||
SessionGameTitle.Text = displayName;
|
||||
_runningGameTitleId = titleId ?? _allGames
|
||||
.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?.TitleId;
|
||||
_runningGameTitleId = resolvedTitleId;
|
||||
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
StatusDot.Fill = SuccessLineBrush;
|
||||
StatusText.Text = Localization.Instance.Format("Launch.Running", displayName);
|
||||
@@ -1727,6 +1753,7 @@ public partial class MainWindow : Window
|
||||
Path.GetFullPath(ebootPath),
|
||||
displayName,
|
||||
_runningGameTitleId,
|
||||
effective.LogLevel,
|
||||
runtimeOptions);
|
||||
|
||||
if (_gameSurfaceHost?.Surface is { } surface)
|
||||
@@ -1895,7 +1922,7 @@ public partial class MainWindow : Window
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"--cpu-engine=native",
|
||||
$"--log-level={_settings.LogLevel}",
|
||||
$"--log-level={launch.LogLevel}",
|
||||
};
|
||||
if (launch.RuntimeOptions.StrictDynlibResolution)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user