diff --git a/API/RenderSession.cs b/API/RenderSession.cs index 680c508..56ad5da 100644 --- a/API/RenderSession.cs +++ b/API/RenderSession.cs @@ -520,7 +520,9 @@ public class RenderSession } RenderManager.Instance.levelPath = levelPath; - RenderManager.Instance.Configuration = renderConfig; + // The render uses its own copy of the configuration: values adjusted during the render + // (e.g. the audio sample rate in SetupPlay) stay in this copy and do not change the caller's object. + RenderManager.Instance.Configuration = renderConfig.Clone(); RenderManager.Instance.stage = RenderStage.Load; diff --git a/UI/Window/RenderConfigWindow.cs b/UI/Window/RenderConfigWindow.cs index 34c4950..147da04 100644 --- a/UI/Window/RenderConfigWindow.cs +++ b/UI/Window/RenderConfigWindow.cs @@ -9,6 +9,7 @@ using ADOFAIRenderer.Data; using ADOFAIRenderer.Localization; using ADOFAIRenderer.Util.OS; using MelonLoader; +using Newtonsoft.Json; using O5Kit.Control; using O5Kit.Core; using O5Kit.Factory; @@ -32,6 +33,14 @@ internal static class RenderConfigWindow private static Coroutine? _deleteResetCoroutine; private static string? _lastLoadedPresetName; + /// + /// Configuration edited by this window. Kept for the whole game session; a render started from the window + /// receives a copy of it (RenderSession.Start), so values adjusted during the render are not written back here. + /// + private static RenderConfig? _uiConfig; + + private static RenderConfig UiConfig => _uiConfig ??= new RenderConfig(); + // Controls cache for validation & sync private static O5Slider? _renderFpsSlider; private static O5Slider? _videoFPSSlider; @@ -111,8 +120,7 @@ internal static class RenderConfigWindow ReloadPresetDropdown(GlobalSettings.Current.LastPresetName); } - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - var config = RenderManager.Instance.Configuration ??= new RenderConfig(); + var config = UiConfig; ApplyConfigToUI(config); SetWindowActive(true); @@ -125,8 +133,7 @@ internal static class RenderConfigWindow var (_, scrollContent, _) = O5Factory.ScrollView(ctx, content, spacing: 10f, padding: 8f); - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - var config = RenderManager.Instance.Configuration ??= new RenderConfig(); + var config = UiConfig; EnsureDefaultConfig(config); double[] renderFpsSnap = { 23.976, 24, 29.97, 30, 59.94, 60, 144, @@ -744,11 +751,14 @@ internal static class RenderConfigWindow { EnsureDefaultConfig(config); - _renderFpsSlider?.Set((float) config.RenderFPS, false); - _videoFPSSlider?.Set((float) config.VideoFPS, false); - _renderWidthSlider?.Set(config.RenderWidth, false); - _renderHeightSlider?.Set(config.RenderHeight, false); - _delayAfterEndSlider?.Set((float) config.DelayAfterEnd, false); + // Sliders show the config values as they are (noFilter: true). + // O5Slider.Set applies the slider filter (snapping to the nearest snap value) unless noFilter is set; + // the filter is meant for dragging, and typed values are not snapped either (O5Slider.SetFromInput). + _renderFpsSlider?.Set(config.RenderFPS, false, noFilter: true); + _videoFPSSlider?.Set(config.VideoFPS, false, noFilter: true); + _renderWidthSlider?.Set(config.RenderWidth, false, noFilter: true); + _renderHeightSlider?.Set(config.RenderHeight, false, noFilter: true); + _delayAfterEndSlider?.Set(config.DelayAfterEnd, false, noFilter: true); _showJudgementToggle?.Set(config.ShowJudgementText, false); _codecDropdown?.Set(config.VideoConfig.Codec, false); @@ -756,13 +766,13 @@ internal static class RenderConfigWindow _bitrateInput?.Set(config.VideoConfig.Bitrate.ToString(), false); _maxBitrateInput?.Set(config.VideoConfig.MaxBitrate.ToString(), false); _qpInput?.Set(config.VideoConfig.QP.ToString(), false); - _keyframeIntervalSlider?.Set(config.VideoConfig.KeyframeInterval, false); + _keyframeIntervalSlider?.Set(config.VideoConfig.KeyframeInterval, false, noFilter: true); _forceSoftwareToggle?.Set(config.VideoConfig.ForceSoftwareEncoder, false); _renderAudioToggle?.Set(config.AudioConfig.RenderAudio, false); _sampleRateDropdown?.Set(config.AudioConfig.AudioSampleRate, false); _channelsDropdown?.Set(config.AudioConfig.AudioChannels, false); - _audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false); + _audioBitrateSlider?.Set(config.AudioConfig.AudioBitrate / 1000.0, false, noFilter: true); _outputDirInput?.Set(GlobalSettings.EffectiveOutputDirectory, false); @@ -789,17 +799,51 @@ internal static class RenderConfigWindow UpdateVideoCodecUI(config.VideoConfig.Codec, config); UpdateRateControlUI(config.VideoConfig.RateControlMode, noAnimate: true); - if (!string.IsNullOrEmpty(GlobalSettings.Current.LastPresetName)) + UpdatePresetLabel(config); + } + + /// + /// Sets the preset dropdown from the config values. + /// Every value equals the last loaded preset (_lastLoadedPresetName): the preset name is shown. + /// Any value differs (edited, or the preset file changed): the Custom label is shown (MarkConfigModified), + /// the same state as after editing a value in the window. + /// + private static void UpdatePresetLabel(RenderConfig config) + { + if (_presetDropdown == null) return; + + var presetName = _lastLoadedPresetName; + var preset = string.IsNullOrEmpty(presetName) + ? null + : PresetManager.GetPresets().FirstOrDefault(p => string.Equals(p.Name, presetName, StringComparison.OrdinalIgnoreCase)); + + if (preset == null || !ConfigValuesEqual(config, preset.Config)) { - _presetDropdown?.Set(GlobalSettings.Current.LastPresetName, false); - _lastLoadedPresetName = GlobalSettings.Current.LastPresetName; - UpdateDeleteButtonState(); + MarkConfigModified(); + return; } + + if (_presetDropdown.Values.Contains(CustomPresetLabel)) + { + _presetDropdown.SetValues(_presetDropdown.Values.Where(v => v != CustomPresetLabel).ToList()); + } + _presetDropdown.Set(preset.Name, false); + UpdateDeleteButtonState(); + } + + /// + /// Compares two configs by their serialized JSON (the same form as the preset files), so every saved value is compared. + /// + private static bool ConfigValuesEqual(RenderConfig a, RenderConfig b) + { + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new ColorJsonConverter()); + return JsonConvert.SerializeObject(a, settings) == JsonConvert.SerializeObject(b, settings); } private static void TryGoRender() { - var config = RenderManager.Instance.Configuration; + var config = UiConfig; SyncUItoConfig(config); // Sync Output Directory