Skip to content

Commit 43280c5

Browse files
ImPanickclaude
authored andcommitted
feat(app): Game Tuner screen + Home tile; move it out of Custom (UI pass)
Per owner: Game Tuning is its own top-level feature, not a Custom category. - Home: a new Game Tuner mode tile (⚙ cog) next to Custom Editor — the row is now four tiles (Recovery / Lazy Max / Custom / Game Tuner) and all four navigate. Game Tuning was removed from the Custom editor sidebar. - GameTunerViewModel / GameTunerView: reads the client's Engine.ini and lists each tunable. Toggles disable an effect; numbers show a toggle + a slider AND a free-text number box, both bound to the same value which CLAMPS to the setting's stable-max (typing 99999 in the box snaps back to the cap). Apply (behind a confirm dialog) writes Engine.ini via the Core service — backed up + atomic; restart Icarus to take effect. - Wiring: ShellViewModel GameTunerKey + resolve; HomeViewModel OpenGameTunerCommand; MainWindow DataTemplate GameTunerViewModel→GameTunerView; DI registers GameTuningCatalog + GameTuningService + GameTunerViewModel (validated by ValidateOnBuild). Caveat shown in the confirm + GAME-TUNING.md: these are standard UE cvars; Icarus may ignore/ clamp some — live verification (GT-6) is owner-run. Verified: dotnet build -c Debug and -c Release 0/0, dotnet test 230/230, dotnet format clean, governance-lint clean, smoke launch renders + the full DI graph (incl. GameTunerViewModel) validates at startup. Click-through QA is owner-run. Agent: claude-code/2.1.149 Consulted: AGENTS.md, .agent/CONSTITUTION.md#III,#V, .agent/CODE_STYLE.md#3,#7, docs/GAME-TUNING.md, docs/IUUT-PROJECT-DOCUMENTATION.md#20, docs/UI-DESIGN-CONCEPT.md Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9977bd9 commit 43280c5

11 files changed

Lines changed: 368 additions & 16 deletions

docs/IMPLEMENTATION-PLAN.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@ SHA-1 load check passes) + `MountsModel`/`MountEditService` + **`FlagsFileCodec`
3232
- **Done:** navigation **shell** (in-window page swap — `INavigationService`/`ShellViewModel`,
3333
Home extracted to `Views/HomeView`, DataTemplate page rendering, Back/Home nav bar); the
3434
**Recovery screen** (wired to `RecoveryPlanner`/`RecoveryService`/`RecoveryAdvisor`: pick save →
35-
Scan → per-file plan + advisories → confirm → Repair → report); and the **Custom editor shell**
36-
(`CustomViewModel`/`CustomView`: profile selector + category sidebar → editor panel, incl. a new
37-
**Game Tuning** category per owner request). All three tiles on Home now navigate. DI uses
38-
`ValidateOnBuild` (whole graph validated at startup). Owner-run visual QA pending.
35+
Scan → per-file plan + advisories → confirm → Repair → report); the **Custom editor shell**
36+
(`CustomViewModel`/`CustomView`: profile selector + category sidebar → editor panel); and the
37+
**Game Tuner** — its own Home tile (⚙, next to Custom; moved OUT of the Custom sidebar) backed by
38+
the **Phase 7 Game Tuning Core, pulled forward** (`EngineIni` codec + `GameTuningCatalog` w/
39+
stable-max caps + `GameTuningService`). `GameTunerView`: per setting a toggle, or toggle + slider +
40+
**free-text number box clamped to the stable max**, applied to Engine.ini (backed up, atomic).
41+
**All four Home tiles navigate.** DI uses `ValidateOnBuild`. Owner-run visual QA pending.
3942
- **Remaining UI:** wire the Custom **category editors** to their (already-built) Core services
4043
(Account/Characters/Accolades-Bestiary/Stash/…), Settings, Advanced/Raw viewer, Troubleshooting
41-
modal, Game Tuning tab content, and a **polish pass**.
44+
modal, and a **polish pass**. (Game Tuner is functional; GT-6 live-cvar verification still owner-run.)
4245
- **Polish backlog (owner-noted, screenshots 2026-05-31):** general "not great"/spacing pass; the
4346
**Recovery header title+subtitle overlap** (two TextBlocks stacked in one grid cell — split into
4447
rows); revisit glass contrast/typography. Take note, don't act mid-feature.

src/IUUT.App/App.xaml.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using IUUT.App.ViewModels;
44
using IUUT.Core.Abstractions;
55
using IUUT.Core.Catalog;
6+
using IUUT.Core.GameTuning;
67
using IUUT.Core.Io;
78
using IUUT.Core.Recovery;
89
using IUUT.Core.Services;
@@ -72,12 +73,17 @@ private static ServiceProvider ConfigureServices()
7273
services.AddSingleton<RecoveryAdvisor>();
7374
services.AddSingleton<RecoveryService>();
7475

76+
// --- Game Tuning (Engine.ini, master §20.1) ---------------------------
77+
services.AddSingleton<GameTuningCatalog>();
78+
services.AddSingleton<GameTuningService>();
79+
7580
// --- UI shell + pages -------------------------------------------------
7681
services.AddSingleton<ShellViewModel>();
7782
services.AddSingleton<INavigationService>(sp => sp.GetRequiredService<ShellViewModel>());
7883
services.AddSingleton<HomeViewModel>();
7984
services.AddSingleton<RecoveryViewModel>();
8085
services.AddSingleton<CustomViewModel>();
86+
services.AddSingleton<GameTunerViewModel>();
8187
services.AddSingleton<MainWindow>();
8288

8389
// ValidateOnBuild constructs every registration at startup, so a broken DI graph

src/IUUT.App/MainWindow.xaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
<DataTemplate DataType="{x:Type vm:CustomViewModel}">
3535
<views:CustomView />
3636
</DataTemplate>
37+
<DataTemplate DataType="{x:Type vm:GameTunerViewModel}">
38+
<views:GameTunerView />
39+
</DataTemplate>
3740
</ui:FluentWindow.Resources>
3841

3942
<Grid>

src/IUUT.App/ViewModels/CustomViewModel.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,6 @@ private static IReadOnlyList<CustomCategory> BuildCategories() =>
155155
Status = "Core ready — FlagsFileCodec.",
156156
},
157157
new()
158-
{
159-
Glyph = "⚙",
160-
Label = "Game Tuning",
161-
Description = "Engine.ini performance/visual cvar toggles — \"Buff FPS\", fog/volumetrics, quality scalars, FPS + net.",
162-
Status = "Future — Phase 7 (docs/GAME-TUNING.md). Bundled/sideloaded, offline; not yet built.",
163-
},
164-
new()
165158
{
166159
Glyph = "🧾",
167160
Label = "Advanced / Raw",
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Collections.ObjectModel;
2+
using CommunityToolkit.Mvvm.ComponentModel;
3+
using CommunityToolkit.Mvvm.Input;
4+
using IUUT.Core.GameTuning;
5+
using IUUT.Core.Services;
6+
7+
namespace IUUT.App.ViewModels;
8+
9+
/// <summary>
10+
/// The Game Tuner page (master §20.1, docs/GAME-TUNING.md): reads the client's Engine.ini, shows
11+
/// each tunable as a toggle (or toggle + slider/number box for numbers), and applies the changes.
12+
/// Writes only Engine.ini (never save files), backed up + atomic via the Core service.
13+
/// </summary>
14+
public sealed class GameTunerViewModel : ObservableObject
15+
{
16+
private readonly GameTuningService _service;
17+
private readonly GameTuningCatalog _catalog;
18+
19+
private string _saveRoot;
20+
private bool _isBusy;
21+
private string _statusMessage = "Reads the game's Engine.ini.";
22+
23+
/// <summary>Creates the Game Tuner over the tuning service + catalog.</summary>
24+
public GameTunerViewModel(GameTuningService service, GameTuningCatalog catalog)
25+
{
26+
ArgumentNullException.ThrowIfNull(service);
27+
ArgumentNullException.ThrowIfNull(catalog);
28+
_service = service;
29+
_catalog = catalog;
30+
_saveRoot = SaveDiscoveryService.ResolveDefaultSaveRoot();
31+
32+
Settings = [];
33+
LoadCommand = new RelayCommand(Load);
34+
}
35+
36+
/// <summary>The tunable settings.</summary>
37+
public ObservableCollection<GameTuningSettingViewModel> Settings { get; }
38+
39+
/// <summary>(Re)reads Engine.ini.</summary>
40+
public IRelayCommand LoadCommand { get; }
41+
42+
/// <summary>The Icarus save root (Engine.ini is under <c>Config\WindowsNoEditor\</c>).</summary>
43+
public string SaveRoot
44+
{
45+
get => _saveRoot;
46+
set => SetProperty(ref _saveRoot, value);
47+
}
48+
49+
/// <summary>The resolved Engine.ini path (for display).</summary>
50+
public string EngineIniPath => GameTuningService.ResolveEngineIniPath(SaveRoot);
51+
52+
/// <summary>True while applying.</summary>
53+
public bool IsBusy
54+
{
55+
get => _isBusy;
56+
private set => SetProperty(ref _isBusy, value);
57+
}
58+
59+
/// <summary>Status-bar message.</summary>
60+
public string StatusMessage
61+
{
62+
get => _statusMessage;
63+
private set => SetProperty(ref _statusMessage, value);
64+
}
65+
66+
/// <summary>Reads the current Engine.ini state into the settings list.</summary>
67+
public void Load()
68+
{
69+
try
70+
{
71+
Settings.Clear();
72+
foreach (var state in _service.ReadCurrent(SaveRoot, _catalog))
73+
{
74+
Settings.Add(new GameTuningSettingViewModel(state));
75+
}
76+
77+
StatusMessage = $"{Settings.Count(s => s.Enabled)} of {Settings.Count} settings active · {EngineIniPath}";
78+
}
79+
#pragma warning disable CA1031 // UI boundary: surface, never crash.
80+
catch (Exception ex)
81+
{
82+
StatusMessage = $"Could not read Engine.ini: {ex.Message}";
83+
}
84+
#pragma warning restore CA1031
85+
}
86+
87+
/// <summary>Applies the settings to Engine.ini (call after a user confirm), then refreshes.</summary>
88+
public async Task ApplyAsync()
89+
{
90+
if (IsBusy)
91+
{
92+
return;
93+
}
94+
95+
IsBusy = true;
96+
try
97+
{
98+
var ok = await _service.ApplyAsync(SaveRoot, Settings.Select(s => s.State).ToList());
99+
StatusMessage = ok
100+
? "Applied to Engine.ini — restart Icarus for the changes to take effect."
101+
: "Apply failed; the original Engine.ini is unchanged.";
102+
}
103+
#pragma warning disable CA1031 // UI boundary: surface, never crash.
104+
catch (Exception ex)
105+
{
106+
StatusMessage = $"Apply failed: {ex.Message}";
107+
}
108+
#pragma warning restore CA1031
109+
finally
110+
{
111+
IsBusy = false;
112+
}
113+
114+
Load();
115+
}
116+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using CommunityToolkit.Mvvm.ComponentModel;
2+
using IUUT.Core.GameTuning;
3+
4+
namespace IUUT.App.ViewModels;
5+
6+
/// <summary>
7+
/// Binding-shape for one tunable Engine.ini setting. <see cref="Value"/> clamps to the setting's
8+
/// stable maximum, so both the slider and the free-text number box stay within the safe cap.
9+
/// </summary>
10+
public sealed class GameTuningSettingViewModel : ObservableObject
11+
{
12+
private readonly GameTuningState _state;
13+
14+
/// <summary>Wraps a tuning state.</summary>
15+
public GameTuningSettingViewModel(GameTuningState state)
16+
{
17+
ArgumentNullException.ThrowIfNull(state);
18+
_state = state;
19+
}
20+
21+
/// <summary>The underlying state (passed back to the service on apply).</summary>
22+
public GameTuningState State => _state;
23+
24+
/// <summary>Setting label.</summary>
25+
public string Label => _state.Setting.Label;
26+
27+
/// <summary>Setting description.</summary>
28+
public string Description => _state.Setting.Description;
29+
30+
/// <summary>Whether this is a numeric setting (shows the slider + number box).</summary>
31+
public bool IsNumber => _state.Setting.Kind == GameTuningKind.Number;
32+
33+
/// <summary>Slider minimum.</summary>
34+
public double Minimum => _state.Setting.Min;
35+
36+
/// <summary>Slider maximum — the stable cap.</summary>
37+
public double Maximum => _state.Setting.StableMax;
38+
39+
/// <summary>Slider step.</summary>
40+
public double Step => _state.Setting.Step;
41+
42+
/// <summary>Optional unit suffix.</summary>
43+
public string Unit => _state.Setting.Unit ?? "";
44+
45+
/// <summary>Whether IUUT manages this cvar (on → written; off → removed/game default).</summary>
46+
public bool Enabled
47+
{
48+
get => _state.Enabled;
49+
set
50+
{
51+
if (_state.Enabled != value)
52+
{
53+
_state.Enabled = value;
54+
OnPropertyChanged();
55+
}
56+
}
57+
}
58+
59+
/// <summary>The numeric value, clamped to [<see cref="Minimum"/>, <see cref="Maximum"/>] (the stable cap).</summary>
60+
public double Value
61+
{
62+
get => _state.Value;
63+
set
64+
{
65+
var clamped = Math.Clamp(value, Minimum, Maximum);
66+
if (Math.Abs(_state.Value - clamped) > double.Epsilon)
67+
{
68+
_state.Value = clamped;
69+
OnPropertyChanged();
70+
}
71+
}
72+
}
73+
}

src/IUUT.App/ViewModels/HomeViewModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public HomeViewModel(HomeService home, LazyMaxApplyService apply, INavigationSer
4141
LoadCommand = new AsyncRelayCommand(LoadAsync);
4242
OpenRecoveryCommand = new RelayCommand(() => _navigation.NavigateTo(ShellViewModel.RecoveryKey));
4343
OpenCustomCommand = new RelayCommand(() => _navigation.NavigateTo(ShellViewModel.CustomKey));
44+
OpenGameTunerCommand = new RelayCommand(() => _navigation.NavigateTo(ShellViewModel.GameTunerKey));
4445
}
4546

4647
/// <summary>Discovered save profiles for the dropdown.</summary>
@@ -55,6 +56,9 @@ public HomeViewModel(HomeService home, LazyMaxApplyService apply, INavigationSer
5556
/// <summary>Opens the Custom editor page.</summary>
5657
public IRelayCommand OpenCustomCommand { get; }
5758

59+
/// <summary>Opens the Game Tuner (Engine.ini) page.</summary>
60+
public IRelayCommand OpenGameTunerCommand { get; }
61+
5862
/// <summary>The save root being scanned (editable; Browse updates it).</summary>
5963
public string SaveRoot
6064
{

src/IUUT.App/ViewModels/ShellViewModel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public sealed class ShellViewModel : ObservableObject, INavigationService
2121
/// <summary>The Custom editor page key.</summary>
2222
public const string CustomKey = "Custom";
2323

24+
/// <summary>The Game Tuner page key.</summary>
25+
public const string GameTunerKey = "GameTuner";
26+
2427
private readonly IServiceProvider _services;
2528
private readonly Stack<string> _back = new();
2629

@@ -95,6 +98,7 @@ private void SetPage(string key)
9598
HomeKey => _services.GetRequiredService<HomeViewModel>(),
9699
RecoveryKey => _services.GetRequiredService<RecoveryViewModel>(),
97100
CustomKey => _services.GetRequiredService<CustomViewModel>(),
101+
GameTunerKey => _services.GetRequiredService<GameTunerViewModel>(),
98102
_ => throw new ArgumentOutOfRangeException(nameof(key), key, "Unknown page key."),
99103
};
100104
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<UserControl x:Class="IUUT.App.Views.GameTunerView"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
mc:Ignorable="d">
7+
8+
<!-- Game Tuner (master §20.1, docs/GAME-TUNING.md). DataContext = GameTunerViewModel. -->
9+
<Grid>
10+
<Grid.RowDefinitions>
11+
<RowDefinition Height="Auto" />
12+
<RowDefinition Height="*" />
13+
<RowDefinition Height="Auto" />
14+
</Grid.RowDefinitions>
15+
16+
<!-- Header -->
17+
<Border Grid.Row="0" Style="{StaticResource GlassCard}" Padding="16">
18+
<Grid>
19+
<Grid.ColumnDefinitions>
20+
<ColumnDefinition Width="*" />
21+
<ColumnDefinition Width="Auto" />
22+
</Grid.ColumnDefinitions>
23+
<StackPanel Grid.Column="0">
24+
<TextBlock Text="⚙ Game Tuner" Style="{StaticResource TitleText}" />
25+
<TextBlock Text="Engine.ini performance &amp; visual tweaks — toggle effects off, set quality/FPS. Standard Unreal cvars; restart Icarus to apply. A backup is taken before any write."
26+
Style="{StaticResource MutedText}" TextWrapping="Wrap" Margin="0,4,0,0" />
27+
<TextBlock Text="{Binding EngineIniPath}" Style="{StaticResource MonoText}" Margin="0,6,0,0" TextTrimming="CharacterEllipsis" />
28+
</StackPanel>
29+
<Button Grid.Column="1" Content="Reload" Command="{Binding LoadCommand}" VerticalAlignment="Top" Padding="14,4" />
30+
</Grid>
31+
</Border>
32+
33+
<!-- Settings list -->
34+
<Border Grid.Row="1" Style="{StaticResource GlassCard}" Padding="12" Margin="0,12,0,0">
35+
<ScrollViewer VerticalScrollBarVisibility="Auto">
36+
<ItemsControl ItemsSource="{Binding Settings}">
37+
<ItemsControl.ItemTemplate>
38+
<DataTemplate>
39+
<Border Margin="0,0,0,8" Padding="14,10" CornerRadius="4"
40+
Background="{StaticResource DataBackdropBrush}"
41+
BorderBrush="{StaticResource GlassStrokeBrush}" BorderThickness="1">
42+
<Grid>
43+
<Grid.ColumnDefinitions>
44+
<ColumnDefinition Width="*" />
45+
<ColumnDefinition Width="Auto" />
46+
<ColumnDefinition Width="Auto" />
47+
</Grid.ColumnDefinitions>
48+
49+
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="0,0,12,0">
50+
<TextBlock Text="{Binding Label}" Style="{StaticResource BodyText}" />
51+
<TextBlock Text="{Binding Description}" Style="{StaticResource MutedText}" TextWrapping="Wrap" Margin="0,2,0,0" />
52+
</StackPanel>
53+
54+
<CheckBox Grid.Column="1" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center" Margin="0,0,16,0" />
55+
56+
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
57+
<StackPanel.Style>
58+
<Style TargetType="StackPanel">
59+
<Setter Property="Visibility" Value="Visible" />
60+
<Style.Triggers>
61+
<DataTrigger Binding="{Binding IsNumber}" Value="False">
62+
<Setter Property="Visibility" Value="Collapsed" />
63+
</DataTrigger>
64+
</Style.Triggers>
65+
</Style>
66+
</StackPanel.Style>
67+
<Slider Width="150" VerticalAlignment="Center"
68+
Minimum="{Binding Minimum}" Maximum="{Binding Maximum}"
69+
Value="{Binding Value, Mode=TwoWay}"
70+
TickFrequency="{Binding Step}" IsSnapToTickEnabled="True"
71+
IsEnabled="{Binding Enabled}" />
72+
<TextBox Width="62" Margin="10,0,4,0" VerticalAlignment="Center"
73+
Text="{Binding Value, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
74+
IsEnabled="{Binding Enabled}" />
75+
<TextBlock Text="{Binding Unit}" Style="{StaticResource MutedText}" VerticalAlignment="Center" />
76+
</StackPanel>
77+
</Grid>
78+
</Border>
79+
</DataTemplate>
80+
</ItemsControl.ItemTemplate>
81+
</ItemsControl>
82+
</ScrollViewer>
83+
</Border>
84+
85+
<!-- Status + apply -->
86+
<Grid Grid.Row="2" Margin="0,12,0,0">
87+
<Grid.ColumnDefinitions>
88+
<ColumnDefinition Width="*" />
89+
<ColumnDefinition Width="Auto" />
90+
</Grid.ColumnDefinitions>
91+
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" Style="{StaticResource MutedText}"
92+
VerticalAlignment="Center" TextWrapping="Wrap" Margin="0,0,12,0" />
93+
<Button Grid.Column="1" Content="⚙ Apply to Engine.ini" Click="OnApply" Padding="18,8" FontWeight="SemiBold" />
94+
</Grid>
95+
</Grid>
96+
</UserControl>

0 commit comments

Comments
 (0)