refactor(settings): OP9 — retire the dead F11 settings surface + fully-superseded GameplaySettings
Campaign OP slice OP9 code retirement (docs/plans/2026-08-10-options-panel-campaign.md
§OP9). The retail four-tab Options panel (OP1-OP8) is now acdream's one
in-client settings surface (D1) — this commit deletes the pre-retail-UI
surfaces it fully superseded. Pure retirement: no behavior change to
anything live, verified by dispositioning every one of the 15 src files
and 8 test files that referenced the deleted types before touching any
of them.
1. SettingsVM + SettingsPanel (the old F11 IPanel surface, unrendered
since Campaign V slice V11's ImGui deletion) deleted outright, plus
their two dedicated test files. IPanel/IPanelRenderer stay — ChatPanel/
DebugPanel/VitalsPanel still implement IPanel, so the contract does NOT
become unreferenced.
2. RuntimeSettingsController's SettingsVM binding seam deleted:
CreateViewModel, CreateViewModelBinding, RuntimeSettingsViewModelBinding,
the _viewModel field, UnbindViewModel, and every _viewModel? notification
call (ToggleFrameRate, LoadCharacterContext, RestoreDefaultCharacterContext,
SyncChatFromServerOptions). CreateViewModelBinding had zero production
callers (test-only). HasDraftPreview/DisplayPreview/AudioPreview stay on
IRuntimeSettingsPreviewSource (WorldRenderFrameBuilder and
SettingsParticleRangeSource still consume it) but now trivially mirror
the committed Display/Audio snapshot — HasDraftPreview was already
always false in production. GameWindowLifetime's matching
"settings view model" shutdown stage is deleted.
3. IDevToolsGameplayCommands + DevToolsGameplayCommands deleted from
GameplayInputCommandController.cs. All three members were dead:
ToggleSettingsPanel() had ZERO dispatch sites (ToggleOptionsPanel always
routed to _retained, never to _devTools); ToggleDebugPanel()/
FocusChatInput() had dispatch sites (F1/Ctrl+F1, Tab) but empty no-op
bodies (their ImGui DebugPanel/ChatPanel targets were already gone).
The two live dispatch sites are kept as inert `return true;` cases
(still consuming the key, matching the prior no-op's "handled"
contract) instead of falling through to a lower-priority scope.
SessionPlayerComposition.cs's `new DevToolsGameplayCommands()`
construction is removed. No `ToggleSettingsPanel` InputAction exists
(only `ToggleOptionsPanel`, rebound at OP3) — nothing to remove there
or from KeyBindings.RetailDefaults()/keymap fixtures.
4. GameplaySettings deleted entirely (the type, SettingsStore's
LoadGameplay/SaveGameplay/BuildGameplayObject, RuntimeSettingsController's
Gameplay property/SetAcceptLootPermits). Verified all 13 remaining
members (ToggleRun, AdvancedCombatUI, ShowTooltips,
VividTargetingIndicator, SideBySideVitals, CoordinatesOnRadar,
SpellDuration, AllowGive, ShowHelm, ShowCloak, LockUI, UseMouseTurning,
AcceptLootPermits — the three combat ones already died at OP4/AP-196)
were ALREADY bound through CharacterOptionTable/
CharacterOptionsPageController's server-bit seam at OP4 before deleting
the client-local mirror — no (c)-case genuinely-client-local member
was found; disposition (b) covers 100% of the surface. SetUiLocked
rewritten to compare only against the last value actually pushed to
_runtimeTargets (MUST-FIX 4's guard), with no second store left to
read or write. LiveSessionRuntimeFactory's SetAcceptLootPermits binding
now sends the wire option only (the GameplaySettings write-behind call
removed as dead output). CharacterSettings/DisplaySettings/
AudioSettings/ChatSettings and their SettingsStore Load/Save surfaces
are UNTOUCHED per the campaign contract.
Per-file disposition (15 src + 8 test files that referenced
GameplaySettings before this commit):
- GameplaySettings.cs, SettingsVM.cs, SettingsPanel.cs: the types
themselves — deleted.
- SettingsStore.cs, RuntimeSettingsController.cs,
LiveSessionRuntimeFactory.cs: real usage — API deleted/rewritten.
- RetailUiRuntime.cs, InteractionRetainedUiComposition.cs,
SessionPlayerComposition.cs, CombatUiController.cs,
LiveCombatAttackOperations.cs, LivePresentationComposition.cs,
FrameRootComposition.cs, RuntimeCharacterState.cs,
CombatCameraTargetSource.cs: doc-comment-only or interface-name
substring matches (ICombatGameplaySettingsSource) — left as accurate
historical record, no forward reference to the deleted type.
- Tests: RuntimeSettingsControllerTests.cs and SettingsStoreTests.cs
rewritten (Gameplay-specific tests deleted; SaveDisplay/SaveAudio/
SaveChat tests re-targeted off the now-public methods instead of the
retired SettingsVM draft/Save() indirection); GameplaySettingsTests.cs/
SettingsVMTests.cs/SettingsPanelTests.cs deleted; the remaining three
(CharacterOptionCombatSettingsSourceTests.cs,
CombatCameraTargetSourceTests.cs, LiveCombatAttackOperationsTests.cs)
were comment/interface-name-only, untouched.
5. Register: AP-196 (OP4's partial GameplaySettings retirement, which left
five fields as write-behind mirrors) is fully retired now that the
record is gone outright — marked ~~AP-196~~ RETIRED with its retirement
note, active-row count 143 -> 142. No other row cited the deleted types
directly (AP-194/AP-193 cite CharacterOptionTable.cs, not
GameplaySettings.cs).
6. Settings.json migration honesty: SettingsStore no longer reads or
writes the "gameplay" top-level key, so an existing file carrying one
from a pre-OP9 build is neither parsed nor dropped — the existing
SaveSection raw-JSON-text preservation mechanism (unknown top-level
keys survive every subsequent save) carries it forward untouched.
Two new targeted tests
(LeftoverGameplaySection_FromAnOlderSettingsJson_DoesNotBreakOtherLoads,
LeftoverGameplaySection_SurvivesAnUnrelatedSave) pin this.
InputAction.ToggleOptionsPanel's stale doc comment (still describing the
retired ImGui SettingsPanel) and a handful of other dangling doc
references (DisplaySettings.cs, ChatOpacityLink.cs,
SettingsDevToolsComposition.cs, InputDispatcherCaptureTests.cs) are
reworded to point at the current retail Options panel / OP8
KeyboardConfigController.
Build: dotnet build -c Release green, 0 errors. Tests: dotnet test -c
Release --no-build — 13,075 passed / 4 skipped / 0 failed (13,079 total),
down from the stated baseline of 13,155 passed / 4 skipped / 0 failed
(13,159 total) — the -80 delta is exactly the deleted SettingsVM/
SettingsPanel/GameplaySettings test surface (three whole files plus the
Gameplay-specific cases trimmed from RuntimeSettingsControllerTests.cs/
SettingsStoreTests.cs), with zero regressions elsewhere.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1a57f96efe
commit
371197a345
22 changed files with 228 additions and 3260 deletions
|
|
@ -1,5 +1,4 @@
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using AcDream.UI.Abstractions.Settings;
|
||||
|
||||
|
|
@ -15,8 +14,6 @@ internal interface IRuntimeSettingsStorage
|
|||
|
||||
AudioSettings LoadAudio();
|
||||
|
||||
GameplaySettings LoadGameplay();
|
||||
|
||||
ChatSettings LoadChat();
|
||||
|
||||
CharacterSettings LoadCharacter(string toonKey);
|
||||
|
|
@ -29,8 +26,6 @@ internal interface IRuntimeSettingsStorage
|
|||
|
||||
void SaveAudio(AudioSettings audio);
|
||||
|
||||
void SaveGameplay(GameplaySettings gameplay);
|
||||
|
||||
void SaveChat(ChatSettings chat);
|
||||
|
||||
void SaveCharacter(string toonKey, CharacterSettings character);
|
||||
|
|
@ -57,8 +52,6 @@ internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
|
|||
|
||||
public AudioSettings LoadAudio() => _store.LoadAudio();
|
||||
|
||||
public GameplaySettings LoadGameplay() => _store.LoadGameplay();
|
||||
|
||||
public ChatSettings LoadChat() => _store.LoadChat();
|
||||
|
||||
public CharacterSettings LoadCharacter(string toonKey) =>
|
||||
|
|
@ -70,9 +63,6 @@ internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
|
|||
|
||||
public void SaveAudio(AudioSettings audio) => _store.SaveAudio(audio);
|
||||
|
||||
public void SaveGameplay(GameplaySettings gameplay) =>
|
||||
_store.SaveGameplay(gameplay);
|
||||
|
||||
public void SaveChat(ChatSettings chat) => _store.SaveChat(chat);
|
||||
|
||||
public void SaveCharacter(string toonKey, CharacterSettings character) =>
|
||||
|
|
@ -85,39 +75,10 @@ internal sealed class JsonRuntimeSettingsStorage : IRuntimeSettingsStorage
|
|||
internal sealed record RuntimeSettingsSnapshot(
|
||||
DisplaySettings Display,
|
||||
AudioSettings Audio,
|
||||
GameplaySettings Gameplay,
|
||||
ChatSettings Chat,
|
||||
CharacterSettings Character,
|
||||
QualitySettings Quality);
|
||||
|
||||
/// <summary>
|
||||
/// Expected-owner lease for the optional developer settings view model.
|
||||
/// Failed optional composition can withdraw only the instance it installed.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeSettingsViewModelBinding : IDisposable
|
||||
{
|
||||
private readonly RuntimeSettingsController _owner;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeSettingsViewModelBinding(
|
||||
RuntimeSettingsController owner,
|
||||
SettingsVM viewModel)
|
||||
{
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
ViewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
|
||||
}
|
||||
|
||||
public SettingsVM ViewModel { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_owner.UnbindViewModel(ViewModel);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal interface IRuntimeSettingsStartupTarget
|
||||
{
|
||||
void ApplyDisplay(DisplaySettings display);
|
||||
|
|
@ -186,16 +147,15 @@ internal sealed class RuntimeSettingsController :
|
|||
private readonly Func<QualityPreset, QualitySettings> _resolveQuality;
|
||||
private readonly Action<string> _log;
|
||||
private IRuntimeSettingsTargets? _runtimeTargets;
|
||||
private SettingsVM? _viewModel;
|
||||
private CharacterSettings _defaultCharacter;
|
||||
private bool _startupDisplayApplied;
|
||||
private bool _startupAudioApplied;
|
||||
private bool _startupApplied;
|
||||
private bool _uiLockConverged = true;
|
||||
// MUST-FIX 4 (OP4 review-fix round, 2026-08-11, blast M3): the last
|
||||
// `locked` value actually pushed to `_runtimeTargets.ApplyUiLock` —
|
||||
// the guard `SetUiLocked` compares against, decoupled from whatever
|
||||
// `Gameplay.LockUI`'s own persisted/draft snapshot currently holds.
|
||||
// the guard `SetUiLocked` compares against. Originally decoupled from
|
||||
// the retired client-local `GameplaySettings.LockUI` mirror (OP9
|
||||
// deleted that record outright); this is now the ONLY store.
|
||||
private bool? _lastAppliedUiLocked;
|
||||
|
||||
public RuntimeSettingsController(
|
||||
|
|
@ -209,7 +169,6 @@ internal sealed class RuntimeSettingsController :
|
|||
|
||||
Display = _storage.LoadDisplay();
|
||||
Audio = _storage.LoadAudio();
|
||||
Gameplay = _storage.LoadGameplay();
|
||||
Chat = _storage.LoadChat();
|
||||
_defaultCharacter = _storage.LoadCharacter(DefaultToonKey);
|
||||
Character = _defaultCharacter;
|
||||
|
|
@ -217,7 +176,6 @@ internal sealed class RuntimeSettingsController :
|
|||
Startup = new RuntimeSettingsSnapshot(
|
||||
Display,
|
||||
Audio,
|
||||
Gameplay,
|
||||
Chat,
|
||||
Character,
|
||||
ResolvedQuality);
|
||||
|
|
@ -233,19 +191,24 @@ internal sealed class RuntimeSettingsController :
|
|||
|
||||
public AudioSettings Audio { get; private set; }
|
||||
|
||||
public GameplaySettings Gameplay { get; private set; }
|
||||
|
||||
public ChatSettings Chat { get; private set; }
|
||||
|
||||
public CharacterSettings Character { get; private set; }
|
||||
|
||||
public QualitySettings ResolvedQuality { get; private set; }
|
||||
|
||||
public bool HasDraftPreview => _viewModel is not null;
|
||||
// OP9: the optional developer-tools draft-preview view model
|
||||
// (SettingsVM) was retired — it had zero production construction
|
||||
// sites (only tests ever called CreateViewModel). HasDraftPreview is
|
||||
// therefore always false in production and DisplayPreview/AudioPreview
|
||||
// always mirror the committed Display/Audio snapshot; the properties
|
||||
// stay on IRuntimeSettingsPreviewSource because WorldRenderFrameBuilder
|
||||
// and SettingsParticleRangeSource still consume the interface.
|
||||
public bool HasDraftPreview => false;
|
||||
|
||||
public DisplaySettings DisplayPreview => _viewModel?.DisplayDraft ?? Display;
|
||||
public DisplaySettings DisplayPreview => Display;
|
||||
|
||||
public AudioSettings AudioPreview => _viewModel?.AudioDraft ?? Audio;
|
||||
public AudioSettings AudioPreview => Audio;
|
||||
|
||||
public void ApplyStartup(IRuntimeSettingsStartupTarget target)
|
||||
{
|
||||
|
|
@ -316,94 +279,23 @@ internal sealed class RuntimeSettingsController :
|
|||
.UnbindRuntimeTargets(_expected);
|
||||
}
|
||||
|
||||
public SettingsVM CreateViewModel(
|
||||
KeyBindings persistedBindings,
|
||||
InputDispatcher dispatcher,
|
||||
Action<KeyBindings> saveBindings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(persistedBindings);
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(saveBindings);
|
||||
if (_viewModel is not null)
|
||||
throw new InvalidOperationException("A settings view model is already bound.");
|
||||
|
||||
_viewModel = new SettingsVM(
|
||||
persistedBindings,
|
||||
dispatcher,
|
||||
saveBindings,
|
||||
Display,
|
||||
SaveDisplay,
|
||||
Audio,
|
||||
SaveAudio,
|
||||
Gameplay,
|
||||
SaveGameplay,
|
||||
Chat,
|
||||
SaveChat,
|
||||
Character,
|
||||
SaveCharacter);
|
||||
return _viewModel;
|
||||
}
|
||||
|
||||
public RuntimeSettingsViewModelBinding CreateViewModelBinding(
|
||||
KeyBindings persistedBindings,
|
||||
InputDispatcher dispatcher,
|
||||
Action<KeyBindings> saveBindings) =>
|
||||
new(
|
||||
this,
|
||||
CreateViewModel(persistedBindings, dispatcher, saveBindings));
|
||||
|
||||
public void UnbindViewModel(SettingsVM? expected = null)
|
||||
{
|
||||
if (expected is null || ReferenceEquals(_viewModel, expected))
|
||||
_viewModel = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// OP9: rewritten off the retired client-local <c>GameplaySettings.LockUI</c>
|
||||
/// mirror (MUST-FIX 4, OP4 review-fix round, 2026-08-11, blast M3) — the
|
||||
/// guard now compares directly against <see cref="_lastAppliedUiLocked"/>,
|
||||
/// the last value actually pushed to <see cref="_runtimeTargets"/>, with
|
||||
/// no persisted store of its own left to read or write: the server bit
|
||||
/// (<c>RuntimeCharacterOptionsState</c>, read through
|
||||
/// <c>CharacterOptionId.LockUI</c>) is the sole authority, exactly as
|
||||
/// D7's Group-C re-point already made it at OP4.
|
||||
/// </summary>
|
||||
public void SetUiLocked(bool locked)
|
||||
{
|
||||
// MUST-FIX 4 (OP4 review-fix round, 2026-08-11, blast M3): the
|
||||
// guard used to compare `locked` against `Gameplay.LockUI` — valid
|
||||
// only while `ToggleUiLock` computed `locked` AS `!Gameplay.LockUI`
|
||||
// (pre-OP4). OP4 re-pointed `ToggleUiLock` to derive `locked` from
|
||||
// the SERVER bit (`RuntimeCharacterOptionsState.GetOptionBit`)
|
||||
// instead, a DIFFERENT store that can already equal the persisted
|
||||
// `Gameplay.LockUI` without this method ever having pushed that
|
||||
// value to `_runtimeTargets` — the guard must compare against what
|
||||
// was ACTUALLY applied, not a value from an unrelated store.
|
||||
if (_lastAppliedUiLocked == locked && _uiLockConverged)
|
||||
if (_lastAppliedUiLocked == locked)
|
||||
return;
|
||||
|
||||
_uiLockConverged = false;
|
||||
Gameplay = Gameplay with { LockUI = locked };
|
||||
_runtimeTargets?.ApplyUiLock(locked);
|
||||
_lastAppliedUiLocked = locked;
|
||||
_viewModel?.SetGameplay(
|
||||
_viewModel.GameplayDraft with { LockUI = locked });
|
||||
|
||||
try
|
||||
{
|
||||
_storage.SaveGameplay(Gameplay);
|
||||
_viewModel?.ApplyExternalGameplayChange(gameplay => gameplay with
|
||||
{
|
||||
LockUI = locked,
|
||||
});
|
||||
_uiLockConverged = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log($"settings: radar lock save failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAcceptLootPermits(bool enabled)
|
||||
{
|
||||
Gameplay = Gameplay with { AcceptLootPermits = enabled };
|
||||
_viewModel?.SetGameplay(
|
||||
_viewModel.GameplayDraft with { AcceptLootPermits = enabled });
|
||||
_storage.SaveGameplay(Gameplay);
|
||||
_viewModel?.ApplyExternalGameplayChange(gameplay => gameplay with
|
||||
{
|
||||
AcceptLootPermits = enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -415,10 +307,6 @@ internal sealed class RuntimeSettingsController :
|
|||
public void ToggleFrameRate()
|
||||
{
|
||||
Display = Display with { ShowFps = !Display.ShowFps };
|
||||
_viewModel?.ApplyExternalDisplayChange(display => display with
|
||||
{
|
||||
ShowFps = Display.ShowFps,
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -434,7 +322,7 @@ internal sealed class RuntimeSettingsController :
|
|||
/// Campaign OP slice OP3: the five client-local preferences the "Use
|
||||
/// Mouse Turning Settings" Gameplay-tab macro reads/writes. Read
|
||||
/// directly through storage (no startup-snapshot cache, unlike
|
||||
/// <see cref="Display"/>/<see cref="Gameplay"/>/<see cref="Chat"/>) —
|
||||
/// <see cref="Display"/>/<see cref="Chat"/>) —
|
||||
/// this section has no UI surface of its own yet (Campaign OP slice
|
||||
/// OP6's Config tab), so there is nothing today that needs a cached,
|
||||
/// change-notified copy.
|
||||
|
|
@ -468,14 +356,12 @@ internal sealed class RuntimeSettingsController :
|
|||
ArgumentException.ThrowIfNullOrWhiteSpace(characterName);
|
||||
ActiveToonKey = characterName;
|
||||
Character = _storage.LoadCharacter(characterName);
|
||||
_viewModel?.LoadCharacterContext(Character);
|
||||
_log($"settings: loaded character[{characterName}] preferences");
|
||||
}
|
||||
|
||||
public void RestoreDefaultCharacterContext()
|
||||
{
|
||||
Character = _defaultCharacter;
|
||||
_viewModel?.LoadCharacterContext(Character);
|
||||
}
|
||||
|
||||
public void ResetActiveCharacterKey() => ActiveToonKey = DefaultToonKey;
|
||||
|
|
@ -500,7 +386,7 @@ internal sealed class RuntimeSettingsController :
|
|||
/// same shape as the already-public <see cref="SaveCameraTurning"/>,
|
||||
/// now also called directly by <c>ConfigOptionsPageController</c>'s
|
||||
/// Display-backed rows (resolution/fullscreen/vsync/FOV/gamma/quality
|
||||
/// family) rather than only through the (soon-retired) SettingsVM
|
||||
/// family) rather than only through the (OP9-retired) SettingsVM
|
||||
/// callback wiring.</summary>
|
||||
public void SaveDisplay(DisplaySettings display)
|
||||
{
|
||||
|
|
@ -540,24 +426,6 @@ internal sealed class RuntimeSettingsController :
|
|||
}
|
||||
}
|
||||
|
||||
private void SaveGameplay(GameplaySettings gameplay)
|
||||
{
|
||||
try
|
||||
{
|
||||
_storage.SaveGameplay(gameplay);
|
||||
Gameplay = gameplay;
|
||||
_uiLockConverged = false;
|
||||
_runtimeTargets?.ApplyUiLock(gameplay.LockUI);
|
||||
_lastAppliedUiLocked = gameplay.LockUI;
|
||||
_uiLockConverged = true;
|
||||
_log($"settings: gameplay saved to {_storage.Location}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log($"settings: gameplay save failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Widened from private to public at Campaign OP slice OP6,
|
||||
/// same reason as <see cref="SaveDisplay"/> — the Config tab's Chat
|
||||
/// Font Face/Size rows (store-only, no diff against the five wired
|
||||
|
|
@ -580,7 +448,7 @@ internal sealed class RuntimeSettingsController :
|
|||
|
||||
// CH3 (2026-08-09): retail toggles a Hear*Chat option and pushes
|
||||
// SetSingleCharacterOption (0x0005) in the same step (mirrors
|
||||
// SaveGameplay's ApplyUiLock push above) — ACE's handler both flips
|
||||
// SetUiLocked's ApplyUiLock push above) — ACE's handler both flips
|
||||
// the option AND joins/leaves the matching Turbine room.
|
||||
PublishHearOptionChange(
|
||||
previous.HearGeneralChat, chat.HearGeneralChat,
|
||||
|
|
@ -627,11 +495,10 @@ internal sealed class RuntimeSettingsController :
|
|||
/// </summary>
|
||||
public void SyncChatFromServerOptions(uint options2)
|
||||
{
|
||||
// Applied identically to BOTH the persisted snapshot and the live
|
||||
// draft (mirrors ApplyExternalGameplayChange's own idempotent-update
|
||||
// shape) so an unsaved draft edit to an unrelated field (font size,
|
||||
// timestamps, ...) survives the reseed instead of being clobbered by
|
||||
// a value computed once against the persisted snapshot.
|
||||
// Reseed function applied to the persisted snapshot below so an
|
||||
// unsaved edit to an unrelated field (font size, timestamps, ...)
|
||||
// survives the reseed instead of being clobbered by a value
|
||||
// computed once against a stale snapshot.
|
||||
ChatSettings Reseed(ChatSettings current) => current with
|
||||
{
|
||||
HearGeneralChat = (options2
|
||||
|
|
@ -651,7 +518,6 @@ internal sealed class RuntimeSettingsController :
|
|||
return;
|
||||
|
||||
Chat = synced;
|
||||
_viewModel?.ApplyExternalChatChange(Reseed);
|
||||
try
|
||||
{
|
||||
_storage.SaveChat(synced);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue