diff --git a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs index 70e1dcb466..e2576425d9 100644 --- a/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs +++ b/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs @@ -10,6 +10,7 @@ using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem.Content; using Ryujinx.HLE.HOS.Services.Time.Clock; using Ryujinx.HLE.Utilities; +using System; using System.Collections.Generic; using System.IO; @@ -117,6 +118,73 @@ namespace Ryujinx.HLE.HOS.Services.Time.TimeZone } } + public IEnumerable<(int Offset, string Location, string Abbr)> ParseTzOffsets() + { + var tzBinaryContentPath = GetTimeZoneBinaryTitleContentPath(); + + if (string.IsNullOrEmpty(tzBinaryContentPath)) + { + return new[] { (0, "UTC", "UTC") }; + } + + List<(int Offset, string Location, string Abbr)> outList = new List<(int Offset, string Location, string Abbr)>(); + var now = System.DateTimeOffset.Now.ToUnixTimeSeconds(); + using (IStorage ncaStorage = new LocalStorage(_virtualFileSystem.SwitchPathToSystemPath(tzBinaryContentPath), FileAccess.Read, FileMode.Open)) + using (IFileSystem romfs = new Nca(_virtualFileSystem.KeySet, ncaStorage).OpenFileSystem(NcaSectionType.Data, _fsIntegrityCheckLevel)) + { + foreach (string locName in LocationNameCache) + { + if (locName.StartsWith("Etc")) + { + continue; + } + + if (romfs.OpenFile(out IFile tzif, $"/zoneinfo/{locName}".ToU8Span(), OpenMode.Read).IsFailure()) + { + Logger.PrintError(LogClass.ServiceTime, $"Error opening /zoneinfo/{locName}"); + continue; + } + + using (tzif) + { + TimeZone.ParseTimeZoneBinary(out TimeZoneRule tzRule, tzif.AsStream()); + + TimeTypeInfo ttInfo; + if (tzRule.TimeCount > 0) // Find the current transition period + { + int fin = 0; + for (int i = 0; i < tzRule.TimeCount; ++i) + { + if (tzRule.Ats[i] <= now) + { + fin = i; + } + } + ttInfo = tzRule.Ttis[tzRule.Types[fin]]; + } + else if (tzRule.TypeCount >= 1) // Otherwise, use the first offset in TTInfo + { + ttInfo = tzRule.Ttis[0]; + } + else + { + Logger.PrintError(LogClass.ServiceTime, $"Couldn't find UTC offset for zone {locName}"); + continue; + } + + var abbrStart = tzRule.Chars.AsSpan(ttInfo.AbbreviationListIndex); + int abbrEnd = abbrStart.IndexOf('\0'); + + outList.Add((ttInfo.GmtOffset, locName, abbrStart.Slice(0, abbrEnd).ToString())); + } + } + } + + outList.Sort(); + + return outList; + } + private bool IsLocationNameValid(string locationName) { foreach (string cachedLocationName in LocationNameCache) diff --git a/Ryujinx/Ui/SettingsWindow.cs b/Ryujinx/Ui/SettingsWindow.cs index 493260c316..b488fdbbb8 100644 --- a/Ryujinx/Ui/SettingsWindow.cs +++ b/Ryujinx/Ui/SettingsWindow.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -22,59 +21,62 @@ namespace Ryujinx.Ui private static ListStore _gameDirsBoxStore; private static VirtualFileSystem _virtualFileSystem; + private TimeZoneContentManager _timeZoneContentManager; + private HashSet _validTzRegions; private long _systemTimeOffset; #pragma warning disable CS0649, IDE0044 - [GUI] CheckButton _errorLogToggle; - [GUI] CheckButton _warningLogToggle; - [GUI] CheckButton _infoLogToggle; - [GUI] CheckButton _stubLogToggle; - [GUI] CheckButton _debugLogToggle; - [GUI] CheckButton _fileLogToggle; - [GUI] CheckButton _guestLogToggle; - [GUI] CheckButton _fsAccessLogToggle; - [GUI] Adjustment _fsLogSpinAdjustment; - [GUI] CheckButton _dockedModeToggle; - [GUI] CheckButton _discordToggle; - [GUI] CheckButton _vSyncToggle; - [GUI] CheckButton _multiSchedToggle; - [GUI] CheckButton _ptcToggle; - [GUI] CheckButton _fsicToggle; - [GUI] CheckButton _ignoreToggle; - [GUI] CheckButton _directKeyboardAccess; - [GUI] ComboBoxText _systemLanguageSelect; - [GUI] ComboBoxText _systemRegionSelect; - [GUI] ComboBoxText _systemTimeZoneSelect; - [GUI] ComboBoxText _audioBackendSelect; - [GUI] SpinButton _systemTimeYearSpin; - [GUI] SpinButton _systemTimeMonthSpin; - [GUI] SpinButton _systemTimeDaySpin; - [GUI] SpinButton _systemTimeHourSpin; - [GUI] SpinButton _systemTimeMinuteSpin; - [GUI] Adjustment _systemTimeYearSpinAdjustment; - [GUI] Adjustment _systemTimeMonthSpinAdjustment; - [GUI] Adjustment _systemTimeDaySpinAdjustment; - [GUI] Adjustment _systemTimeHourSpinAdjustment; - [GUI] Adjustment _systemTimeMinuteSpinAdjustment; - [GUI] CheckButton _custThemeToggle; - [GUI] Entry _custThemePath; - [GUI] ToggleButton _browseThemePath; - [GUI] Label _custThemePathLabel; - [GUI] TreeView _gameDirsBox; - [GUI] Entry _addGameDirBox; - [GUI] Entry _graphicsShadersDumpPath; - [GUI] ComboBoxText _anisotropy; - [GUI] ComboBoxText _resScaleCombo; - [GUI] Entry _resScaleText; - [GUI] ToggleButton _configureController1; - [GUI] ToggleButton _configureController2; - [GUI] ToggleButton _configureController3; - [GUI] ToggleButton _configureController4; - [GUI] ToggleButton _configureController5; - [GUI] ToggleButton _configureController6; - [GUI] ToggleButton _configureController7; - [GUI] ToggleButton _configureController8; - [GUI] ToggleButton _configureControllerH; + [GUI] CheckButton _errorLogToggle; + [GUI] CheckButton _warningLogToggle; + [GUI] CheckButton _infoLogToggle; + [GUI] CheckButton _stubLogToggle; + [GUI] CheckButton _debugLogToggle; + [GUI] CheckButton _fileLogToggle; + [GUI] CheckButton _guestLogToggle; + [GUI] CheckButton _fsAccessLogToggle; + [GUI] Adjustment _fsLogSpinAdjustment; + [GUI] CheckButton _dockedModeToggle; + [GUI] CheckButton _discordToggle; + [GUI] CheckButton _vSyncToggle; + [GUI] CheckButton _multiSchedToggle; + [GUI] CheckButton _ptcToggle; + [GUI] CheckButton _fsicToggle; + [GUI] CheckButton _ignoreToggle; + [GUI] CheckButton _directKeyboardAccess; + [GUI] ComboBoxText _systemLanguageSelect; + [GUI] ComboBoxText _systemRegionSelect; + [GUI] Entry _systemTimeZoneEntry; + [GUI] EntryCompletion _systemTimeZoneCompletion; + [GUI] ComboBoxText _audioBackendSelect; + [GUI] SpinButton _systemTimeYearSpin; + [GUI] SpinButton _systemTimeMonthSpin; + [GUI] SpinButton _systemTimeDaySpin; + [GUI] SpinButton _systemTimeHourSpin; + [GUI] SpinButton _systemTimeMinuteSpin; + [GUI] Adjustment _systemTimeYearSpinAdjustment; + [GUI] Adjustment _systemTimeMonthSpinAdjustment; + [GUI] Adjustment _systemTimeDaySpinAdjustment; + [GUI] Adjustment _systemTimeHourSpinAdjustment; + [GUI] Adjustment _systemTimeMinuteSpinAdjustment; + [GUI] CheckButton _custThemeToggle; + [GUI] Entry _custThemePath; + [GUI] ToggleButton _browseThemePath; + [GUI] Label _custThemePathLabel; + [GUI] TreeView _gameDirsBox; + [GUI] Entry _addGameDirBox; + [GUI] Entry _graphicsShadersDumpPath; + [GUI] ComboBoxText _anisotropy; + [GUI] ComboBoxText _resScaleCombo; + [GUI] Entry _resScaleText; + [GUI] ToggleButton _configureController1; + [GUI] ToggleButton _configureController2; + [GUI] ToggleButton _configureController3; + [GUI] ToggleButton _configureController4; + [GUI] ToggleButton _configureController5; + [GUI] ToggleButton _configureController6; + [GUI] ToggleButton _configureController7; + [GUI] ToggleButton _configureController8; + [GUI] ToggleButton _configureControllerH; #pragma warning restore CS0649, IDE0044 public SettingsWindow(VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : this(new Builder("Ryujinx.Ui.SettingsWindow.glade"), virtualFileSystem, contentManager) { } @@ -87,6 +89,11 @@ namespace Ryujinx.Ui _virtualFileSystem = virtualFileSystem; + _timeZoneContentManager = new TimeZoneContentManager(); + _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, LibHac.FsSystem.IntegrityCheckLevel.None); + + _validTzRegions = new HashSet(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly. + //Bind Events _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player1); _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player2); @@ -97,6 +104,7 @@ namespace Ryujinx.Ui _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player7); _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Player8); _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, args, PlayerIndex.Handheld); + _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut; _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1"; @@ -186,19 +194,6 @@ namespace Ryujinx.Ui _custThemeToggle.Click(); } - TimeZoneContentManager timeZoneContentManager = new TimeZoneContentManager(); - - timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, LibHac.FsSystem.IntegrityCheckLevel.None); - - List locationNames = timeZoneContentManager.LocationNameCache.ToList(); - - locationNames.Sort(); - - foreach (string locationName in locationNames) - { - _systemTimeZoneSelect.Append(locationName, locationName); - } - Task.Run(() => { if (SoundIoAudioOut.IsSupported) @@ -223,9 +218,41 @@ namespace Ryujinx.Ui }); }); + // Custom EntryCompletion Columns. If added to glade, need to override more signals + ListStore tzList = new ListStore(typeof(string), typeof(string), typeof(string)); + _systemTimeZoneCompletion.Model = tzList; + + CellRendererText offsetCol = new CellRendererText(); + CellRendererText abbrevCol = new CellRendererText(); + + _systemTimeZoneCompletion.PackStart(offsetCol, false); + _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0); + _systemTimeZoneCompletion.TextColumn = 1; // Regions Column + _systemTimeZoneCompletion.PackStart(abbrevCol, false); + _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2); + + int maxLocationLength = 0; + + foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets()) + { + var hours = Math.DivRem(offset, 3600, out int seconds); + var minutes = Math.Abs(seconds) / 60; + + var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr; + + tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2); + _validTzRegions.Add(location); + + maxLocationLength = Math.Max(maxLocationLength, location.Length); + } + + _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width + _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(); + + _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc; + _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString()); _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString()); - _systemTimeZoneSelect.SetActiveId(timeZoneContentManager.SanityCheckDeviceLocationName()); _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString()); _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString()); @@ -290,6 +317,23 @@ namespace Ryujinx.Ui } //Events + private void TimeZoneEntry_FocusOut(Object sender, FocusOutEventArgs e) + { + if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text)) + { + _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(); + } + } + + private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter) + { + key = key.Trim().Replace(' ', '_'); + + return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region + ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr + ((string)compl.Model.GetValue(iter, 0)).Substring(3).StartsWith(key); // offset + } + private void SystemTimeSpin_ValueChanged(Object sender, EventArgs e) { int year = _systemTimeYearSpin.ValueAsInt; @@ -438,6 +482,11 @@ namespace Ryujinx.Ui { resScaleCustom = 1.0f; } + + if (_validTzRegions.Contains(_systemTimeZoneEntry.Text)) + { + ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text; + } ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active; ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active; @@ -459,7 +508,6 @@ namespace Ryujinx.Ui ConfigurationState.Instance.System.Language.Value = Enum.Parse(_systemLanguageSelect.ActiveId); ConfigurationState.Instance.System.Region.Value = Enum.Parse(_systemRegionSelect.ActiveId); ConfigurationState.Instance.System.AudioBackend.Value = Enum.Parse(_audioBackendSelect.ActiveId); - ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneSelect.ActiveId; ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset; ConfigurationState.Instance.Ui.CustomThemePath.Value = _custThemePath.Buffer.Text; ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text; diff --git a/Ryujinx/Ui/SettingsWindow.glade b/Ryujinx/Ui/SettingsWindow.glade index d4877a3a58..384fed4dcf 100644 --- a/Ryujinx/Ui/SettingsWindow.glade +++ b/Ryujinx/Ui/SettingsWindow.glade @@ -7,6 +7,11 @@ 1 10 + + True + True + 0 + 1 31 @@ -1224,11 +1229,12 @@ - + True - False + True Change System TimeZone 5 + _systemTimeZoneCompletion False