Skip to content

Commit bbdcb38

Browse files
authored
Merge branch 'master' into convert-path-string-new
2 parents a8ce6a0 + 498948f commit bbdcb38

File tree

178 files changed

+3121
-1024
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

178 files changed

+3121
-1024
lines changed

osu.Android.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
1111
</PropertyGroup>
1212
<ItemGroup>
13-
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.223.0" />
13+
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
1414
</ItemGroup>
1515
<PropertyGroup>
1616
<!-- Fody does not handle Android build well, and warns when unchanged.

osu.Desktop/OsuGameDesktop.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ public OsuGameDesktop(string[]? args = null)
9292
[SupportedOSPlatform("windows")]
9393
private string? getStableInstallPathFromRegistry()
9494
{
95-
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu"))
96-
return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
95+
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
96+
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
9797
}
9898

9999
protected override UpdateManager CreateUpdateManager()

osu.Desktop/Program.cs

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Runtime.Versioning;
77
using osu.Desktop.LegacyIpc;
8+
using osu.Desktop.Windows;
89
using osu.Framework;
910
using osu.Framework.Development;
1011
using osu.Framework.Logging;
@@ -173,13 +174,16 @@ private static void setupSquirrel()
173174
{
174175
tools.CreateShortcutForThisExe();
175176
tools.CreateUninstallerRegistryEntry();
177+
WindowsAssociationManager.InstallAssociations();
176178
}, onAppUpdate: (_, tools) =>
177179
{
178180
tools.CreateUninstallerRegistryEntry();
181+
WindowsAssociationManager.UpdateAssociations();
179182
}, onAppUninstall: (_, tools) =>
180183
{
181184
tools.RemoveShortcutForThisExe();
182185
tools.RemoveUninstallerRegistryEntry();
186+
WindowsAssociationManager.UninstallAssociations();
183187
}, onEveryRun: (_, _, _) =>
184188
{
185189
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently

osu.Desktop/Windows/Icons.cs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System.IO;
5+
6+
namespace osu.Desktop.Windows
7+
{
8+
public static class Icons
9+
{
10+
/// <summary>
11+
/// Fully qualified path to the directory that contains icons (in the installation folder).
12+
/// </summary>
13+
private static readonly string icon_directory = Path.GetDirectoryName(typeof(Icons).Assembly.Location)!;
14+
15+
public static string Lazer => Path.Join(icon_directory, "lazer.ico");
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
2+
// See the LICENCE file in the repository root for full licence text.
3+
4+
using System;
5+
using System.IO;
6+
using System.Runtime.InteropServices;
7+
using System.Runtime.Versioning;
8+
using Microsoft.Win32;
9+
using osu.Framework.Localisation;
10+
using osu.Framework.Logging;
11+
using osu.Game.Localisation;
12+
13+
namespace osu.Desktop.Windows
14+
{
15+
[SupportedOSPlatform("windows")]
16+
public static class WindowsAssociationManager
17+
{
18+
private const string software_classes = @"Software\Classes";
19+
20+
/// <summary>
21+
/// Sub key for setting the icon.
22+
/// https://learn.microsoft.com/en-us/windows/win32/com/defaulticon
23+
/// </summary>
24+
private const string default_icon = @"DefaultIcon";
25+
26+
/// <summary>
27+
/// Sub key for setting the command line that the shell invokes.
28+
/// https://learn.microsoft.com/en-us/windows/win32/com/shell
29+
/// </summary>
30+
internal const string SHELL_OPEN_COMMAND = @"Shell\Open\Command";
31+
32+
private static readonly string exe_path = Path.ChangeExtension(typeof(WindowsAssociationManager).Assembly.Location, ".exe").Replace('/', '\\');
33+
34+
/// <summary>
35+
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
36+
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
37+
/// </summary>
38+
private const string program_id_prefix = "osu.File";
39+
40+
private static readonly FileAssociation[] file_associations =
41+
{
42+
new FileAssociation(@".osz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
43+
new FileAssociation(@".olz", WindowsAssociationManagerStrings.OsuBeatmap, Icons.Lazer),
44+
new FileAssociation(@".osr", WindowsAssociationManagerStrings.OsuReplay, Icons.Lazer),
45+
new FileAssociation(@".osk", WindowsAssociationManagerStrings.OsuSkin, Icons.Lazer),
46+
};
47+
48+
private static readonly UriAssociation[] uri_associations =
49+
{
50+
new UriAssociation(@"osu", WindowsAssociationManagerStrings.OsuProtocol, Icons.Lazer),
51+
new UriAssociation(@"osump", WindowsAssociationManagerStrings.OsuMultiplayer, Icons.Lazer),
52+
};
53+
54+
/// <summary>
55+
/// Installs file and URI associations.
56+
/// </summary>
57+
/// <remarks>
58+
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
59+
/// </remarks>
60+
public static void InstallAssociations()
61+
{
62+
try
63+
{
64+
updateAssociations();
65+
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
66+
NotifyShellUpdate();
67+
}
68+
catch (Exception e)
69+
{
70+
Logger.Error(e, @$"Failed to install file and URI associations: {e.Message}");
71+
}
72+
}
73+
74+
/// <summary>
75+
/// Updates associations with latest definitions.
76+
/// </summary>
77+
/// <remarks>
78+
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
79+
/// </remarks>
80+
public static void UpdateAssociations()
81+
{
82+
try
83+
{
84+
updateAssociations();
85+
86+
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
87+
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
88+
89+
NotifyShellUpdate();
90+
}
91+
catch (Exception e)
92+
{
93+
Logger.Error(e, @"Failed to update file and URI associations.");
94+
}
95+
}
96+
97+
public static void UpdateDescriptions(LocalisationManager localisationManager)
98+
{
99+
try
100+
{
101+
updateDescriptions(localisationManager);
102+
NotifyShellUpdate();
103+
}
104+
catch (Exception e)
105+
{
106+
Logger.Error(e, @"Failed to update file and URI association descriptions.");
107+
}
108+
}
109+
110+
public static void UninstallAssociations()
111+
{
112+
try
113+
{
114+
foreach (var association in file_associations)
115+
association.Uninstall();
116+
117+
foreach (var association in uri_associations)
118+
association.Uninstall();
119+
120+
NotifyShellUpdate();
121+
}
122+
catch (Exception e)
123+
{
124+
Logger.Error(e, @"Failed to uninstall file and URI associations.");
125+
}
126+
}
127+
128+
public static void NotifyShellUpdate() => SHChangeNotify(EventId.SHCNE_ASSOCCHANGED, Flags.SHCNF_IDLIST, IntPtr.Zero, IntPtr.Zero);
129+
130+
/// <summary>
131+
/// Installs or updates associations.
132+
/// </summary>
133+
private static void updateAssociations()
134+
{
135+
foreach (var association in file_associations)
136+
association.Install();
137+
138+
foreach (var association in uri_associations)
139+
association.Install();
140+
}
141+
142+
private static void updateDescriptions(LocalisationManager? localisation)
143+
{
144+
foreach (var association in file_associations)
145+
association.UpdateDescription(getLocalisedString(association.Description));
146+
147+
foreach (var association in uri_associations)
148+
association.UpdateDescription(getLocalisedString(association.Description));
149+
150+
string getLocalisedString(LocalisableString s)
151+
{
152+
if (localisation == null)
153+
return s.ToString();
154+
155+
var b = localisation.GetLocalisedBindableString(s);
156+
b.UnbindAll();
157+
return b.Value;
158+
}
159+
}
160+
161+
#region Native interop
162+
163+
[DllImport("Shell32.dll")]
164+
private static extern void SHChangeNotify(EventId wEventId, Flags uFlags, IntPtr dwItem1, IntPtr dwItem2);
165+
166+
private enum EventId
167+
{
168+
/// <summary>
169+
/// A file type association has changed. <see cref="Flags.SHCNF_IDLIST"/> must be specified in the uFlags parameter.
170+
/// dwItem1 and dwItem2 are not used and must be <see cref="IntPtr.Zero"/>. This event should also be sent for registered protocols.
171+
/// </summary>
172+
SHCNE_ASSOCCHANGED = 0x08000000
173+
}
174+
175+
private enum Flags : uint
176+
{
177+
SHCNF_IDLIST = 0x0000
178+
}
179+
180+
#endregion
181+
182+
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
183+
{
184+
private string programId => $@"{program_id_prefix}{Extension}";
185+
186+
/// <summary>
187+
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
188+
/// </summary>
189+
public void Install()
190+
{
191+
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
192+
if (classes == null) return;
193+
194+
// register a program id for the given extension
195+
using (var programKey = classes.CreateSubKey(programId))
196+
{
197+
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
198+
defaultIconKey.SetValue(null, IconPath);
199+
200+
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
201+
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
202+
}
203+
204+
using (var extensionKey = classes.CreateSubKey(Extension))
205+
{
206+
// set ourselves as the default program
207+
extensionKey.SetValue(null, programId);
208+
209+
// add to the open with dialog
210+
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
211+
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
212+
openWithKey.SetValue(programId, string.Empty);
213+
}
214+
}
215+
216+
public void UpdateDescription(string description)
217+
{
218+
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
219+
if (classes == null) return;
220+
221+
using (var programKey = classes.OpenSubKey(programId, true))
222+
programKey?.SetValue(null, description);
223+
}
224+
225+
/// <summary>
226+
/// Uninstalls the file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/shell/fa-file-types#deleting-registry-information-during-uninstallation
227+
/// </summary>
228+
public void Uninstall()
229+
{
230+
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
231+
if (classes == null) return;
232+
233+
using (var extensionKey = classes.OpenSubKey(Extension, true))
234+
{
235+
// clear our default association so that Explorer doesn't show the raw programId to users
236+
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
237+
if (extensionKey?.GetValue(null) is string s && s == programId)
238+
extensionKey.SetValue(null, string.Empty);
239+
240+
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
241+
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
242+
}
243+
244+
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
245+
}
246+
}
247+
248+
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
249+
{
250+
/// <summary>
251+
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
252+
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
253+
/// </summary>
254+
public const string URL_PROTOCOL = @"URL Protocol";
255+
256+
/// <summary>
257+
/// Registers an URI protocol handler in accordance with https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
258+
/// </summary>
259+
public void Install()
260+
{
261+
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
262+
if (classes == null) return;
263+
264+
using (var protocolKey = classes.CreateSubKey(Protocol))
265+
{
266+
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
267+
268+
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
269+
defaultIconKey.SetValue(null, IconPath);
270+
271+
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
272+
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
273+
}
274+
}
275+
276+
public void UpdateDescription(string description)
277+
{
278+
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
279+
if (classes == null) return;
280+
281+
using (var protocolKey = classes.OpenSubKey(Protocol, true))
282+
protocolKey?.SetValue(null, $@"URL:{description}");
283+
}
284+
285+
public void Uninstall()
286+
{
287+
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
288+
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
289+
}
290+
}
291+
}
292+
}

osu.Desktop/osu.Desktop.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@
3131
<ItemGroup Label="Resources">
3232
<EmbeddedResource Include="lazer.ico" />
3333
</ItemGroup>
34+
<ItemGroup Label="Windows Icons">
35+
<Content Include="*.ico" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
36+
</ItemGroup>
3437
</Project>

osu.Desktop/osu.nuspec

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<file src="**.dll" target="lib\net45\"/>
2121
<file src="**.config" target="lib\net45\"/>
2222
<file src="**.json" target="lib\net45\"/>
23+
<file src="**.ico" target="lib\net45\"/>
2324
<file src="icon.png" target=""/>
2425
</files>
2526
</package>

osu.Game.Benchmarks/osu.Game.Benchmarks.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
10+
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
1111
<PackageReference Include="nunit" Version="3.14.0" />
1212
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
1313
</ItemGroup>

osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
5353
[TestCase("3689906", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
5454
[TestCase("3949367", new[] { typeof(CatchModDoubleTime), typeof(CatchModEasy) })]
5555
[TestCase("112643")]
56+
[TestCase("1041052", new[] { typeof(CatchModHardRock) })]
5657
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
5758

5859
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

0 commit comments

Comments
 (0)