diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs new file mode 100644 index 0000000000..6e63b2d799 --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Tournament.Screens; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneStablePathSelectScreen : TournamentTestScene + { + public TestSceneStablePathSelectScreen() + { + AddStep("Add screen", () => Add(new StablePathSelectTestScreen())); + } + + private class StablePathSelectTestScreen : StablePathSelectScreen + { + protected override void ChangePath() + { + Expire(); + } + + protected override void AutoDetect() + { + Expire(); + } + } + } +} diff --git a/osu.Game.Tournament/Components/IPCErrorDialog.cs b/osu.Game.Tournament/Components/IPCErrorDialog.cs new file mode 100644 index 0000000000..dc039cd3bc --- /dev/null +++ b/osu.Game.Tournament/Components/IPCErrorDialog.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Tournament.Components +{ + public class IPCErrorDialog : PopupDialog + { + public IPCErrorDialog(string headerText, string bodyText) + { + Icon = FontAwesome.Regular.SadTear; + HeaderText = headerText; + BodyText = bodyText; + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Alright.", + Action = () => Expire() + } + }; + } + } +} diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index de4d482d13..999ce61ac8 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using JetBrains.Annotations; using Microsoft.Win32; using osu.Framework.Allocation; using osu.Framework.Logging; @@ -20,6 +21,8 @@ namespace osu.Game.Tournament.IPC { public class FileBasedIPC : MatchIPCInfo { + public Storage IPCStorage { get; private set; } + [Resolved] protected IAPIProvider API { get; private set; } @@ -32,45 +35,46 @@ namespace osu.Game.Tournament.IPC [Resolved] private LadderInfo ladder { get; set; } + [Resolved] + private StableInfo stableInfo { get; set; } + private int lastBeatmapId; private ScheduledDelegate scheduled; private GetBeatmapRequest beatmapLookupRequest; - public Storage Storage { get; private set; } - [BackgroundDependencyLoader] private void load() { - LocateStableStorage(); + var stablePath = stableInfo.StablePath ?? findStablePath(); + initialiseIPCStorage(stablePath); } - public Storage LocateStableStorage() + [CanBeNull] + private Storage initialiseIPCStorage(string path) { scheduled?.Cancel(); - Storage = null; + IPCStorage = null; try { - var path = findStablePath(); - if (string.IsNullOrEmpty(path)) return null; - Storage = new DesktopStorage(path, host as DesktopGameHost); + IPCStorage = new DesktopStorage(path, host as DesktopGameHost); const string file_ipc_filename = "ipc.txt"; const string file_ipc_state_filename = "ipc-state.txt"; const string file_ipc_scores_filename = "ipc-scores.txt"; const string file_ipc_channel_filename = "ipc-channel.txt"; - if (Storage.Exists(file_ipc_filename)) + if (IPCStorage.Exists(file_ipc_filename)) { scheduled = Scheduler.AddDelayed(delegate { try { - using (var stream = Storage.GetStream(file_ipc_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_filename)) using (var sr = new StreamReader(stream)) { var beatmapId = int.Parse(sr.ReadLine()); @@ -104,7 +108,7 @@ namespace osu.Game.Tournament.IPC try { - using (var stream = Storage.GetStream(file_ipc_channel_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_channel_filename)) using (var sr = new StreamReader(stream)) { ChatChannel.Value = sr.ReadLine(); @@ -117,7 +121,7 @@ namespace osu.Game.Tournament.IPC try { - using (var stream = Storage.GetStream(file_ipc_state_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_state_filename)) using (var sr = new StreamReader(stream)) { State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine()); @@ -130,7 +134,7 @@ namespace osu.Game.Tournament.IPC try { - using (var stream = Storage.GetStream(file_ipc_scores_filename)) + using (var stream = IPCStorage.GetStream(file_ipc_scores_filename)) using (var sr = new StreamReader(stream)) { Score1.Value = int.Parse(sr.ReadLine()); @@ -149,54 +153,106 @@ namespace osu.Game.Tournament.IPC Logger.Error(e, "Stable installation could not be found; disabling file based IPC"); } - return Storage; + return IPCStorage; } + /// + /// Manually sets the path to the directory used for inter-process communication with a cutting-edge install. + /// + /// Path to the IPC directory + /// Whether the supplied path was a valid IPC directory. + public bool SetIPCLocation(string path) + { + if (path == null || !ipcFileExistsInDirectory(path)) + return false; + + var newStorage = initialiseIPCStorage(stableInfo.StablePath = path); + if (newStorage == null) + return false; + + stableInfo.SaveChanges(); + return true; + } + + /// + /// Tries to automatically detect the path to the directory used for inter-process communication + /// with a cutting-edge install. + /// + /// Whether an IPC directory was successfully auto-detected. + public bool AutoDetectIPCLocation() => SetIPCLocation(findStablePath()); + + private static bool ipcFileExistsInDirectory(string p) => p != null && File.Exists(Path.Combine(p, "ipc.txt")); + + [CanBeNull] private string findStablePath() { - static bool checkExists(string p) => File.Exists(Path.Combine(p, "ipc.txt")); + var stableInstallPath = findFromEnvVar() ?? + findFromRegistry() ?? + findFromLocalAppData() ?? + findFromDotFolder(); - string stableInstallPath = string.Empty; + Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); + return stableInstallPath; + } + + private string findFromEnvVar() + { + try + { + Logger.Log("Trying to find stable with environment variables"); + string stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + + if (ipcFileExistsInDirectory(stableInstallPath)) + return stableInstallPath; + } + catch + { + } + + return null; + } + + private string findFromLocalAppData() + { + Logger.Log("Trying to find stable in %LOCALAPPDATA%"); + string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + + if (ipcFileExistsInDirectory(stableInstallPath)) + return stableInstallPath; + + return null; + } + + private string findFromDotFolder() + { + Logger.Log("Trying to find stable in dotfolders"); + string stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + + if (ipcFileExistsInDirectory(stableInstallPath)) + return stableInstallPath; + + return null; + } + + private string findFromRegistry() + { + Logger.Log("Trying to find stable in registry"); try { - try - { - stableInstallPath = Environment.GetEnvironmentVariable("OSU_STABLE_PATH"); + string stableInstallPath; - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - try - { - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); - - if (checkExists(stableInstallPath)) - return stableInstallPath; - } - catch - { - } - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); - if (checkExists(stableInstallPath)) + if (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; - - stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); - if (checkExists(stableInstallPath)) - return stableInstallPath; - - return null; } - finally + catch { - Logger.Log($"Stable path for tourney usage: {stableInstallPath}"); } + + return null; } } } diff --git a/osu.Game.Tournament/Models/StableInfo.cs b/osu.Game.Tournament/Models/StableInfo.cs new file mode 100644 index 0000000000..0b0050a245 --- /dev/null +++ b/osu.Game.Tournament/Models/StableInfo.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using Newtonsoft.Json; +using osu.Framework.Platform; + +namespace osu.Game.Tournament.Models +{ + /// + /// Holds the path to locate the osu! stable cutting-edge installation. + /// + [Serializable] + public class StableInfo + { + /// + /// Path to the IPC directory used by the stable (cutting-edge) install. + /// + public string StablePath { get; set; } + + /// + /// Fired whenever stable info is successfully saved to file. + /// + public event Action OnStableInfoSaved; + + private const string config_path = "tournament/stable.json"; + + private readonly Storage storage; + + public StableInfo(Storage storage) + { + this.storage = storage; + + if (!storage.Exists(config_path)) + return; + + using (Stream stream = storage.GetStream(config_path, FileAccess.Read, FileMode.Open)) + using (var sr = new StreamReader(stream)) + { + JsonConvert.PopulateObject(sr.ReadToEnd(), this); + } + } + + public void SaveChanges() + { + using (var stream = storage.GetStream(config_path, FileAccess.Write, FileMode.Create)) + using (var sw = new StreamWriter(stream)) + { + sw.Write(JsonConvert.SerializeObject(this, + new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + })); + } + + OnStableInfoSaved?.Invoke(); + } + } +} diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index cf8eb8bd6c..98bc292901 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -15,6 +15,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tournament.IPC; +using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; @@ -30,12 +31,18 @@ namespace osu.Game.Tournament.Screens [Resolved] private MatchIPCInfo ipc { get; set; } + [Resolved] + private StableInfo stableInfo { get; set; } + [Resolved] private IAPIProvider api { get; set; } [Resolved] private RulesetStore rulesets { get; set; } + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + private Bindable windowSize; [BackgroundDependencyLoader] @@ -53,6 +60,7 @@ namespace osu.Game.Tournament.Screens }; api.LocalUser.BindValueChanged(_ => Schedule(reload)); + stableInfo.OnStableInfoSaved += () => Schedule(reload); reload(); } @@ -62,21 +70,16 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; - fillFlow.Children = new Drawable[] { new ActionableInfo { Label = "Current IPC source", - ButtonText = "Refresh", - Action = () => - { - fileBasedIpc?.LocateStableStorage(); - reload(); - }, - Value = fileBasedIpc?.Storage?.GetFullPath(string.Empty) ?? "Not found", - Failing = fileBasedIpc?.Storage == null, - Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation, and that it is registered as the default osu! install." + ButtonText = "Change source", + Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()), + Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", + Failing = fileBasedIpc?.IPCStorage == null, + Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." }, new ActionableInfo { diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs new file mode 100644 index 0000000000..b4d56f60c7 --- /dev/null +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -0,0 +1,164 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osu.Game.Tournament.IPC; +using osu.Game.Tournament.Components; +using osuTK; + +namespace osu.Game.Tournament.Screens +{ + public class StablePathSelectScreen : TournamentScreen + { + [Resolved] + private GameHost host { get; set; } + + [Resolved(canBeNull: true)] + private TournamentSceneManager sceneManager { get; set; } + + [Resolved] + private MatchIPCInfo ipc { get; set; } + + private DirectorySelector directorySelector; + private DialogOverlay overlay; + + [BackgroundDependencyLoader(true)] + private void load(Storage storage, OsuColour colours) + { + var initialStorage = (ipc as FileBasedIPC)?.IPCStorage ?? storage; + var initialPath = new DirectoryInfo(initialStorage.GetFullPath(string.Empty)).Parent?.FullName; + + AddRangeInternal(new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 10, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.5f, 0.8f), + Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.8f), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Please select a new location", + Font = OsuFont.Default.With(size: 40) + }, + }, + new Drawable[] + { + directorySelector = new DirectorySelector(initialPath) + { + RelativeSizeAxes = Axes.Both, + } + }, + new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new Drawable[] + { + new TriangleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Select stable path", + Action = ChangePath + }, + new TriangleButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 300, + Text = "Auto detect", + Action = AutoDetect + }, + } + } + } + } + } + }, + }, + new BackButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + State = { Value = Visibility.Visible }, + Action = () => sceneManager?.SetScreen(typeof(SetupScreen)) + } + }); + } + + protected virtual void ChangePath() + { + var target = directorySelector.CurrentDirectory.Value.FullName; + var fileBasedIpc = ipc as FileBasedIPC; + Logger.Log($"Changing Stable CE location to {target}"); + + if (!fileBasedIpc?.SetIPCLocation(target) ?? true) + { + overlay = new DialogOverlay(); + overlay.Push(new IPCErrorDialog("This is an invalid IPC Directory", "Select a directory that contains an osu! stable cutting edge installation and make sure it has an empty ipc.txt file in it.")); + AddInternal(overlay); + Logger.Log("Folder is not an osu! stable CE directory"); + return; + } + + sceneManager?.SetScreen(typeof(SetupScreen)); + } + + protected virtual void AutoDetect() + { + var fileBasedIpc = ipc as FileBasedIPC; + + if (!fileBasedIpc?.AutoDetectIPCLocation() ?? true) + { + overlay = new DialogOverlay(); + overlay.Push(new IPCErrorDialog("Failed to auto detect", "An osu! stable cutting-edge installation could not be auto detected.\nPlease try and manually point to the directory.")); + AddInternal(overlay); + } + else + { + sceneManager?.SetScreen(typeof(SetupScreen)); + } + } + } +} diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 718c8ee644..5fc1d03f6d 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -53,6 +53,8 @@ namespace osu.Game.Tournament ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + dependencies.CacheAs(new StableInfo(storage)); + dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); }