mirror of
https://github.com/ppy/osu.git
synced 2026-05-20 09:20:15 +08:00
Compare commits
626 Commits
@@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"nvika": {
|
||||
"version": "3.0.0",
|
||||
"version": "4.0.0",
|
||||
"commands": [
|
||||
"nvika"
|
||||
]
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check permissions
|
||||
run: |
|
||||
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte)
|
||||
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte tsunyoku stanriders)
|
||||
for i in "${ALLOWED_USERS[@]}"; do
|
||||
if [[ "${{ github.actor }}" == "$i" ]]; then
|
||||
exit 0
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# .NET Code Style
|
||||
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
|
||||
|
||||
# IDE0001: Simplify names
|
||||
dotnet_diagnostic.IDE0001.severity = warning
|
||||
|
||||
# IDE0002: Simplify member access
|
||||
dotnet_diagnostic.IDE0002.severity = warning
|
||||
|
||||
# IDE0003: Remove qualification
|
||||
dotnet_diagnostic.IDE0003.severity = warning
|
||||
|
||||
# IDE0004: Remove unnecessary cast
|
||||
dotnet_diagnostic.IDE0004.severity = warning
|
||||
|
||||
# IDE0005: Remove unnecessary imports
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
|
||||
# IDE0034: Simplify default literal
|
||||
dotnet_diagnostic.IDE0034.severity = warning
|
||||
|
||||
# IDE0036: Sort modifiers
|
||||
dotnet_diagnostic.IDE0036.severity = warning
|
||||
|
||||
# IDE0040: Add accessibility modifier
|
||||
dotnet_diagnostic.IDE0040.severity = warning
|
||||
|
||||
# IDE0049: Use keyword for type name
|
||||
dotnet_diagnostic.IDE0040.severity = warning
|
||||
|
||||
# IDE0055: Fix formatting
|
||||
dotnet_diagnostic.IDE0055.severity = warning
|
||||
|
||||
# IDE0051: Private method is unused
|
||||
dotnet_diagnostic.IDE0051.severity = silent
|
||||
|
||||
# IDE0052: Private member is unused
|
||||
dotnet_diagnostic.IDE0052.severity = silent
|
||||
|
||||
# IDE0073: File header
|
||||
dotnet_diagnostic.IDE0073.severity = warning
|
||||
|
||||
# IDE0130: Namespace mismatch with folder
|
||||
dotnet_diagnostic.IDE0130.severity = warning
|
||||
|
||||
# IDE1006: Naming style
|
||||
dotnet_diagnostic.IDE1006.severity = warning
|
||||
|
||||
#Disable operator overloads requiring alternate named methods
|
||||
dotnet_diagnostic.CA2225.severity = none
|
||||
|
||||
# Banned APIs
|
||||
dotnet_diagnostic.RS0030.severity = error
|
||||
|
||||
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
|
||||
# See: https://github.com/ppy/osu/pull/19677
|
||||
dotnet_diagnostic.OSUF001.severity = none
|
||||
@@ -14,10 +14,6 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
|
||||
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
|
||||
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
|
||||
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
|
||||
M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
|
||||
M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
|
||||
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
|
||||
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
|
||||
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# .NET Code Style
|
||||
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
|
||||
is_global = true
|
||||
|
||||
# IDE0001: Simplify names
|
||||
dotnet_diagnostic.IDE0001.severity = warning
|
||||
|
||||
# IDE0002: Simplify member access
|
||||
dotnet_diagnostic.IDE0002.severity = warning
|
||||
|
||||
# IDE0003: Remove qualification
|
||||
dotnet_diagnostic.IDE0003.severity = warning
|
||||
|
||||
# IDE0004: Remove unnecessary cast
|
||||
dotnet_diagnostic.IDE0004.severity = warning
|
||||
|
||||
# IDE0005: Remove unnecessary imports
|
||||
dotnet_diagnostic.IDE0005.severity = warning
|
||||
|
||||
# IDE0034: Simplify default literal
|
||||
dotnet_diagnostic.IDE0034.severity = warning
|
||||
|
||||
# IDE0036: Sort modifiers
|
||||
dotnet_diagnostic.IDE0036.severity = warning
|
||||
|
||||
# IDE0040: Add accessibility modifier
|
||||
dotnet_diagnostic.IDE0040.severity = warning
|
||||
|
||||
# IDE0049: Use keyword for type name
|
||||
dotnet_diagnostic.IDE0040.severity = warning
|
||||
|
||||
# IDE0055: Fix formatting
|
||||
dotnet_diagnostic.IDE0055.severity = warning
|
||||
|
||||
# IDE0051: Private method is unused
|
||||
dotnet_diagnostic.IDE0051.severity = silent
|
||||
|
||||
# IDE0052: Private member is unused
|
||||
dotnet_diagnostic.IDE0052.severity = silent
|
||||
|
||||
# IDE0073: File header
|
||||
dotnet_diagnostic.IDE0073.severity = warning
|
||||
|
||||
# IDE0130: Namespace mismatch with folder
|
||||
dotnet_diagnostic.IDE0130.severity = warning
|
||||
|
||||
# IDE1006: Naming style
|
||||
dotnet_diagnostic.IDE1006.severity = warning
|
||||
|
||||
# CA1305: Specify IFormatProvider
|
||||
# Too many noisy warnings for parsing/formatting numbers
|
||||
dotnet_diagnostic.CA1305.severity = none
|
||||
|
||||
# messagepack complains about "osu" not being title cased due to reserved words
|
||||
dotnet_diagnostic.CS8981.severity = none
|
||||
|
||||
# CA1507: Use nameof to express symbol names
|
||||
# Flags serialization name attributes
|
||||
dotnet_diagnostic.CA1507.severity = suggestion
|
||||
|
||||
# CA1806: Do not ignore method results
|
||||
# The usages for numeric parsing are explicitly optional
|
||||
dotnet_diagnostic.CA1806.severity = suggestion
|
||||
|
||||
# CA1822: Mark members as static
|
||||
# Potential false positive around reflection/too much noise
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
|
||||
# CA1826: Do not use Enumerable method on indexable collections
|
||||
dotnet_diagnostic.CA1826.severity = suggestion
|
||||
|
||||
# CA1859: Use concrete types when possible for improved performance
|
||||
# Involves design considerations
|
||||
dotnet_diagnostic.CA1859.severity = suggestion
|
||||
|
||||
# CA1860: Avoid using 'Enumerable.Any()' extension method
|
||||
dotnet_diagnostic.CA1860.severity = suggestion
|
||||
|
||||
# CA1861: Avoid constant arrays as arguments
|
||||
# Outdated with collection expressions
|
||||
dotnet_diagnostic.CA1861.severity = suggestion
|
||||
|
||||
# CA2007: Consider calling ConfigureAwait on the awaited task
|
||||
dotnet_diagnostic.CA2007.severity = warning
|
||||
|
||||
# CA2016: Forward the 'CancellationToken' parameter to methods
|
||||
# Some overloads are having special handling for debugger
|
||||
dotnet_diagnostic.CA2016.severity = suggestion
|
||||
|
||||
# CA2021: Do not call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types
|
||||
# Causing a lot of false positives with generics
|
||||
dotnet_diagnostic.CA2021.severity = none
|
||||
|
||||
# CA2101: Specify marshaling for P/Invoke string arguments
|
||||
# Reports warning for all non-UTF16 usages on DllImport; consider migrating to LibraryImport
|
||||
dotnet_diagnostic.CA2101.severity = none
|
||||
|
||||
# CA2201: Do not raise reserved exception types
|
||||
dotnet_diagnostic.CA2201.severity = warning
|
||||
|
||||
# CA2208: Instantiate argument exceptions correctly
|
||||
dotnet_diagnostic.CA2208.severity = suggestion
|
||||
|
||||
# CA2242: Test for NaN correctly
|
||||
dotnet_diagnostic.CA2242.severity = warning
|
||||
|
||||
# Banned APIs
|
||||
dotnet_diagnostic.RS0030.severity = error
|
||||
|
||||
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
|
||||
# See: https://github.com/ppy/osu/pull/19677
|
||||
dotnet_diagnostic.OSUF001.severity = none
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="osu! Rule Set" Description=" " ToolsVersion="16.0">
|
||||
<Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
|
||||
<Rule Id="CA1016" Action="None" />
|
||||
<Rule Id="CA1028" Action="None" />
|
||||
<Rule Id="CA1031" Action="None" />
|
||||
<Rule Id="CA1034" Action="None" />
|
||||
<Rule Id="CA1036" Action="None" />
|
||||
<Rule Id="CA1040" Action="None" />
|
||||
<Rule Id="CA1044" Action="None" />
|
||||
<Rule Id="CA1051" Action="None" />
|
||||
<Rule Id="CA1054" Action="None" />
|
||||
<Rule Id="CA1056" Action="None" />
|
||||
<Rule Id="CA1062" Action="None" />
|
||||
<Rule Id="CA1063" Action="None" />
|
||||
<Rule Id="CA1067" Action="None" />
|
||||
<Rule Id="CA1707" Action="None" />
|
||||
<Rule Id="CA1710" Action="None" />
|
||||
<Rule Id="CA1714" Action="None" />
|
||||
<Rule Id="CA1716" Action="None" />
|
||||
<Rule Id="CA1717" Action="None" />
|
||||
<Rule Id="CA1720" Action="None" />
|
||||
<Rule Id="CA1721" Action="None" />
|
||||
<Rule Id="CA1724" Action="None" />
|
||||
<Rule Id="CA1801" Action="None" />
|
||||
<Rule Id="CA1806" Action="None" />
|
||||
<Rule Id="CA1812" Action="None" />
|
||||
<Rule Id="CA1814" Action="None" />
|
||||
<Rule Id="CA1815" Action="None" />
|
||||
<Rule Id="CA1819" Action="None" />
|
||||
<Rule Id="CA1822" Action="None" />
|
||||
<Rule Id="CA1823" Action="None" />
|
||||
<Rule Id="CA2007" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="None" />
|
||||
<Rule Id="CA2227" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeQuality.CSharp.Analyzers" RuleNamespace="Microsoft.CodeQuality.CSharp.Analyzers">
|
||||
<Rule Id="CA1001" Action="None" />
|
||||
<Rule Id="CA1032" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.NetCore.Analyzers" RuleNamespace="Microsoft.NetCore.Analyzers">
|
||||
<Rule Id="CA1303" Action="None" />
|
||||
<Rule Id="CA1304" Action="None" />
|
||||
<Rule Id="CA1305" Action="None" />
|
||||
<Rule Id="CA1307" Action="None" />
|
||||
<Rule Id="CA1308" Action="None" />
|
||||
<Rule Id="CA1816" Action="None" />
|
||||
<Rule Id="CA1826" Action="None" />
|
||||
<Rule Id="CA2000" Action="None" />
|
||||
<Rule Id="CA2008" Action="None" />
|
||||
<Rule Id="CA2213" Action="None" />
|
||||
<Rule Id="CA2235" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.NetCore.CSharp.Analyzers" RuleNamespace="Microsoft.NetCore.CSharp.Analyzers">
|
||||
<Rule Id="CA1309" Action="Warning" />
|
||||
<Rule Id="CA2201" Action="Warning" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
+13
-1
@@ -18,9 +18,21 @@
|
||||
<ItemGroup Label="Code Analysis">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" />
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
|
||||
<!-- Rider compatibility: .globalconfig needs to be explicitly referenced instead of using the global file name. -->
|
||||
<GlobalAnalyzerConfigFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\osu.globalconfig" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Code Analysis">
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet>
|
||||
<AnalysisMode>Default</AnalysisMode>
|
||||
<AnalysisModeDesign>Default</AnalysisModeDesign>
|
||||
<AnalysisModeDocumentation>Recommended</AnalysisModeDocumentation>
|
||||
<AnalysisModeGlobalization>Recommended</AnalysisModeGlobalization>
|
||||
<AnalysisModeInteroperability>Recommended</AnalysisModeInteroperability>
|
||||
<AnalysisModeMaintainability>Recommended</AnalysisModeMaintainability>
|
||||
<AnalysisModeNaming>Default</AnalysisModeNaming>
|
||||
<AnalysisModePerformance>Minimum</AnalysisModePerformance>
|
||||
<AnalysisModeReliability>Recommended</AnalysisModeReliability>
|
||||
<AnalysisModeSecurity>Default</AnalysisModeSecurity>
|
||||
<AnalysisModeUsage>Default</AnalysisModeUsage>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Documentation">
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
||||
@@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu!
|
||||
|
||||
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
|
||||
|
||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores in early 2024.
|
||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
|
||||
|
||||
## Developing a custom ruleset
|
||||
|
||||
|
||||
+2
-2
@@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
|
||||
|
||||
+11
-2
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(X, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
||||
+11
-2
@@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(X, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
|
||||
|
||||
+2
-2
@@ -9,9 +9,9 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1128.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.115.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -13,7 +13,6 @@ using Android.Graphics;
|
||||
using Android.OS;
|
||||
using Android.Views;
|
||||
using osu.Framework.Android;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Database;
|
||||
using Debug = System.Diagnostics.Debug;
|
||||
using Uri = Android.Net.Uri;
|
||||
@@ -50,9 +49,23 @@ namespace osu.Android
|
||||
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||
|
||||
private OsuGameAndroid game = null!;
|
||||
private readonly OsuGameAndroid game;
|
||||
|
||||
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
|
||||
private bool gameCreated;
|
||||
|
||||
protected override Framework.Game CreateGame()
|
||||
{
|
||||
if (gameCreated)
|
||||
throw new InvalidOperationException("Framework tried to create a game twice.");
|
||||
|
||||
gameCreated = true;
|
||||
return game;
|
||||
}
|
||||
|
||||
public OsuGameActivity()
|
||||
{
|
||||
game = new OsuGameAndroid(this);
|
||||
}
|
||||
|
||||
protected override void OnCreate(Bundle? savedInstanceState)
|
||||
{
|
||||
@@ -95,25 +108,38 @@ namespace osu.Android
|
||||
|
||||
private void handleIntent(Intent? intent)
|
||||
{
|
||||
switch (intent?.Action)
|
||||
if (intent == null)
|
||||
return;
|
||||
|
||||
switch (intent.Action)
|
||||
{
|
||||
case Intent.ActionDefault:
|
||||
if (intent.Scheme == ContentResolver.SchemeContent)
|
||||
handleImportFromUris(intent.Data.AsNonNull());
|
||||
{
|
||||
if (intent.Data != null)
|
||||
handleImportFromUris(intent.Data);
|
||||
}
|
||||
else if (osu_url_schemes.Contains(intent.Scheme))
|
||||
game.HandleLink(intent.DataString);
|
||||
{
|
||||
if (intent.DataString != null)
|
||||
game.HandleLink(intent.DataString);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Intent.ActionSend:
|
||||
case Intent.ActionSendMultiple:
|
||||
{
|
||||
if (intent.ClipData == null)
|
||||
break;
|
||||
|
||||
var uris = new List<Uri>();
|
||||
|
||||
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
|
||||
for (int i = 0; i < intent.ClipData.ItemCount; i++)
|
||||
{
|
||||
var content = intent.ClipData?.GetItemAt(i);
|
||||
if (content != null)
|
||||
uris.Add(content.Uri.AsNonNull());
|
||||
var item = intent.ClipData.GetItemAt(i);
|
||||
if (item?.Uri != null)
|
||||
uris.Add(item.Uri);
|
||||
}
|
||||
|
||||
handleImportFromUris(uris.ToArray());
|
||||
|
||||
@@ -51,12 +51,9 @@ namespace osu.Desktop
|
||||
[Resolved]
|
||||
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
||||
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
|
||||
private IBindable<DiscordRichPresenceMode> privacyMode = null!;
|
||||
private IBindable<UserStatus> userStatus = null!;
|
||||
private IBindable<UserActivity?> userActivity = null!;
|
||||
|
||||
private readonly RichPresence presence = new RichPresence
|
||||
{
|
||||
@@ -71,8 +68,12 @@ namespace osu.Desktop
|
||||
private IBindable<APIUser>? user;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config, SessionStatics session)
|
||||
{
|
||||
privacyMode = config.GetBindable<DiscordRichPresenceMode>(OsuSetting.DiscordRichPresence);
|
||||
userStatus = config.GetBindable<UserStatus>(OsuSetting.UserOnlineStatus);
|
||||
userActivity = session.GetBindable<UserActivity?>(Static.UserOnlineActivity);
|
||||
|
||||
client = new DiscordRpcClient(client_id)
|
||||
{
|
||||
// SkipIdenticalPresence allows us to fire SetPresence at any point and leave it to the underlying implementation
|
||||
@@ -105,21 +106,11 @@ namespace osu.Desktop
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
|
||||
user = api.LocalUser.GetBoundCopy();
|
||||
user.BindValueChanged(u =>
|
||||
{
|
||||
status.UnbindBindings();
|
||||
status.BindTo(u.NewValue.Status);
|
||||
|
||||
activity.UnbindBindings();
|
||||
activity.BindTo(u.NewValue.Activity);
|
||||
}, true);
|
||||
|
||||
ruleset.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
status.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
userStatus.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
userActivity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
|
||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||
@@ -151,13 +142,13 @@ namespace osu.Desktop
|
||||
if (!client.IsInitialized)
|
||||
return;
|
||||
|
||||
if (status.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||
if (!api.IsLoggedIn || userStatus.Value == UserStatus.Offline || privacyMode.Value == DiscordRichPresenceMode.Off)
|
||||
{
|
||||
client.ClearPresence();
|
||||
return;
|
||||
}
|
||||
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
|
||||
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || userStatus.Value == UserStatus.DoNotDisturb;
|
||||
|
||||
updatePresence(hideIdentifiableInformation);
|
||||
client.SetPresence(presence);
|
||||
@@ -170,12 +161,12 @@ namespace osu.Desktop
|
||||
return;
|
||||
|
||||
// user activity
|
||||
if (activity.Value != null)
|
||||
if (userActivity.Value != null)
|
||||
{
|
||||
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
|
||||
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||
presence.State = clampLength(userActivity.Value.GetStatus(hideIdentifiableInformation));
|
||||
presence.Details = clampLength(userActivity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
|
||||
|
||||
if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
|
||||
if (userActivity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
|
||||
{
|
||||
presence.Buttons = new[]
|
||||
{
|
||||
|
||||
@@ -67,7 +67,12 @@ namespace osu.Desktop
|
||||
{
|
||||
try
|
||||
{
|
||||
stableInstallPath = getStableInstallPathFromRegistry();
|
||||
stableInstallPath = getStableInstallPathFromRegistry("osustable.File.osz");
|
||||
|
||||
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||
return stableInstallPath;
|
||||
|
||||
stableInstallPath = getStableInstallPathFromRegistry("osu!");
|
||||
|
||||
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||
return stableInstallPath;
|
||||
@@ -89,9 +94,9 @@ namespace osu.Desktop
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private string? getStableInstallPathFromRegistry()
|
||||
private string? getStableInstallPathFromRegistry(string progId)
|
||||
{
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey("osu!"))
|
||||
using (RegistryKey? key = Registry.ClassesRoot.OpenSubKey(progId))
|
||||
return key?.OpenSubKey(WindowsAssociationManager.SHELL_OPEN_COMMAND)?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", "");
|
||||
}
|
||||
|
||||
@@ -134,7 +139,6 @@ namespace osu.Desktop
|
||||
if (iconStream != null)
|
||||
host.Window.SetIconFromStream(iconStream);
|
||||
|
||||
host.Window.CursorState |= CursorState.Hidden;
|
||||
host.Window.Title = Name;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace osu.Desktop
|
||||
|
||||
var hostOptions = new HostOptions
|
||||
{
|
||||
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
|
||||
IPCPipeName = !tournamentClient ? OsuGame.IPC_PIPE_NAME : null,
|
||||
FriendlyGameName = OsuGameBase.GAME_NAME,
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace osu.Desktop.Windows
|
||||
public static class WindowsAssociationManager
|
||||
{
|
||||
private const string software_classes = @"Software\Classes";
|
||||
private const string software_registered_applications = @"Software\RegisteredApplications";
|
||||
|
||||
/// <summary>
|
||||
/// Sub key for setting the icon.
|
||||
@@ -36,7 +37,11 @@ namespace osu.Desktop.Windows
|
||||
/// Program ID prefix used for file associations. Should be relatively short since the full program ID has a 39 character limit,
|
||||
/// see https://learn.microsoft.com/en-us/windows/win32/com/-progid--key.
|
||||
/// </summary>
|
||||
private const string program_id_prefix = "osu.File";
|
||||
private const string program_id_file_prefix = "osu.File";
|
||||
|
||||
private const string program_id_protocol_prefix = "osu.Uri";
|
||||
|
||||
private static readonly ApplicationCapability application_capability = new ApplicationCapability(@"osu", @"Software\ppy\osu\Capabilities", "osu!(lazer)");
|
||||
|
||||
private static readonly FileAssociation[] file_associations =
|
||||
{
|
||||
@@ -56,14 +61,13 @@ namespace osu.Desktop.Windows
|
||||
/// Installs file and URI associations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void InstallAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
updateDescriptions(null); // write default descriptions in case `UpdateDescriptions()` is not called.
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -76,17 +80,13 @@ namespace osu.Desktop.Windows
|
||||
/// Updates associations with latest definitions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call <see cref="UpdateDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// Call <see cref="LocaliseDescriptions"/> in a timely fashion to keep descriptions up-to-date and localised.
|
||||
/// </remarks>
|
||||
public static void UpdateAssociations()
|
||||
{
|
||||
try
|
||||
{
|
||||
updateAssociations();
|
||||
|
||||
// TODO: Remove once UpdateDescriptions() is called as specified in the xmldoc.
|
||||
updateDescriptions(null); // always write default descriptions, in case of updating from an older version in which file associations were not implemented/installed
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -95,11 +95,19 @@ namespace osu.Desktop.Windows
|
||||
}
|
||||
}
|
||||
|
||||
public static void UpdateDescriptions(LocalisationManager localisationManager)
|
||||
// TODO: call this sometime.
|
||||
public static void LocaliseDescriptions(LocalisationManager localisationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
updateDescriptions(localisationManager);
|
||||
application_capability.LocaliseDescription(localisationManager);
|
||||
|
||||
foreach (var association in file_associations)
|
||||
association.LocaliseDescription(localisationManager);
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.LocaliseDescription(localisationManager);
|
||||
|
||||
NotifyShellUpdate();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -112,6 +120,8 @@ namespace osu.Desktop.Windows
|
||||
{
|
||||
try
|
||||
{
|
||||
application_capability.Uninstall();
|
||||
|
||||
foreach (var association in file_associations)
|
||||
association.Uninstall();
|
||||
|
||||
@@ -133,30 +143,16 @@ namespace osu.Desktop.Windows
|
||||
/// </summary>
|
||||
private static void updateAssociations()
|
||||
{
|
||||
application_capability.Install();
|
||||
|
||||
foreach (var association in file_associations)
|
||||
association.Install();
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.Install();
|
||||
}
|
||||
|
||||
private static void updateDescriptions(LocalisationManager? localisation)
|
||||
{
|
||||
foreach (var association in file_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
foreach (var association in uri_associations)
|
||||
association.UpdateDescription(getLocalisedString(association.Description));
|
||||
|
||||
string getLocalisedString(LocalisableString s)
|
||||
{
|
||||
if (localisation == null)
|
||||
return s.ToString();
|
||||
|
||||
var b = localisation.GetLocalisedBindableString(s);
|
||||
b.UnbindAll();
|
||||
return b.Value;
|
||||
}
|
||||
application_capability.RegisterFileAssociations(file_associations);
|
||||
application_capability.RegisterUriAssociations(uri_associations);
|
||||
}
|
||||
|
||||
#region Native interop
|
||||
@@ -182,9 +178,87 @@ namespace osu.Desktop.Windows
|
||||
|
||||
#endregion
|
||||
|
||||
private record FileAssociation(string Extension, LocalisableString Description, string IconPath)
|
||||
private class ApplicationCapability
|
||||
{
|
||||
private string programId => $@"{program_id_prefix}{Extension}";
|
||||
private string uniqueName { get; }
|
||||
private string capabilityPath { get; }
|
||||
private LocalisableString description { get; }
|
||||
|
||||
public ApplicationCapability(string uniqueName, string capabilityPath, LocalisableString description)
|
||||
{
|
||||
this.uniqueName = uniqueName;
|
||||
this.capabilityPath = capabilityPath;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an application capability according to <see href="https://learn.microsoft.com/en-us/windows/win32/shell/default-programs#registering-an-application-for-use-with-default-programs">
|
||||
/// Registering an Application for Use with Default Programs</see>.
|
||||
/// </summary>
|
||||
public void Install()
|
||||
{
|
||||
using (var capability = Registry.CurrentUser.CreateSubKey(capabilityPath))
|
||||
{
|
||||
capability.SetValue(@"ApplicationDescription", description.ToString());
|
||||
}
|
||||
|
||||
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
|
||||
registeredApplications?.SetValue(uniqueName, capabilityPath);
|
||||
}
|
||||
|
||||
public void RegisterFileAssociations(FileAssociation[] associations)
|
||||
{
|
||||
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
|
||||
if (capability == null) return;
|
||||
|
||||
using var fileAssociations = capability.CreateSubKey(@"FileAssociations");
|
||||
|
||||
foreach (var association in associations)
|
||||
fileAssociations.SetValue(association.Extension, association.ProgramId);
|
||||
}
|
||||
|
||||
public void RegisterUriAssociations(UriAssociation[] associations)
|
||||
{
|
||||
using var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true);
|
||||
if (capability == null) return;
|
||||
|
||||
using var urlAssociations = capability.CreateSubKey(@"UrlAssociations");
|
||||
|
||||
foreach (var association in associations)
|
||||
urlAssociations.SetValue(association.Protocol, association.ProgramId);
|
||||
}
|
||||
|
||||
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||
{
|
||||
using (var capability = Registry.CurrentUser.OpenSubKey(capabilityPath, true))
|
||||
{
|
||||
capability?.SetValue(@"ApplicationDescription", localisationManager.GetLocalisedString(description));
|
||||
}
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using (var registeredApplications = Registry.CurrentUser.OpenSubKey(software_registered_applications, true))
|
||||
registeredApplications?.DeleteValue(uniqueName, throwOnMissingValue: false);
|
||||
|
||||
Registry.CurrentUser.DeleteSubKeyTree(capabilityPath, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private class FileAssociation
|
||||
{
|
||||
public string ProgramId => $@"{program_id_file_prefix}{Extension}";
|
||||
|
||||
public string Extension { get; }
|
||||
private LocalisableString description { get; }
|
||||
private string iconPath { get; }
|
||||
|
||||
public FileAssociation(string extension, LocalisableString description, string iconPath)
|
||||
{
|
||||
Extension = extension;
|
||||
this.description = description;
|
||||
this.iconPath = iconPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a file extension association in accordance with https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
|
||||
@@ -195,10 +269,12 @@ namespace osu.Desktop.Windows
|
||||
if (classes == null) return;
|
||||
|
||||
// register a program id for the given extension
|
||||
using (var programKey = classes.CreateSubKey(programId))
|
||||
using (var programKey = classes.CreateSubKey(ProgramId))
|
||||
{
|
||||
programKey.SetValue(null, description.ToString());
|
||||
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
defaultIconKey.SetValue(null, iconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
@@ -206,23 +282,25 @@ namespace osu.Desktop.Windows
|
||||
|
||||
using (var extensionKey = classes.CreateSubKey(Extension))
|
||||
{
|
||||
// set ourselves as the default program
|
||||
extensionKey.SetValue(null, programId);
|
||||
// Clear out our existing default ProgramID. Default programs in Windows are handled internally by Explorer,
|
||||
// so having it here is just confusing and may override user preferences.
|
||||
if (extensionKey.GetValue(null) is string s && s == ProgramId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
// add to the open with dialog
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/how-to-include-an-application-on-the-open-with-dialog-box
|
||||
using (var openWithKey = extensionKey.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey.SetValue(programId, string.Empty);
|
||||
openWithKey.SetValue(ProgramId, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var programKey = classes.OpenSubKey(programId, true))
|
||||
programKey?.SetValue(null, description);
|
||||
using (var programKey = classes.OpenSubKey(ProgramId, true))
|
||||
programKey?.SetValue(null, localisationManager.GetLocalisedString(description));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -235,26 +313,34 @@ namespace osu.Desktop.Windows
|
||||
|
||||
using (var extensionKey = classes.OpenSubKey(Extension, true))
|
||||
{
|
||||
// clear our default association so that Explorer doesn't show the raw programId to users
|
||||
// the null/(Default) entry is used for both ProdID association and as a fallback friendly name, for legacy reasons
|
||||
if (extensionKey?.GetValue(null) is string s && s == programId)
|
||||
extensionKey.SetValue(null, string.Empty);
|
||||
|
||||
using (var openWithKey = extensionKey?.CreateSubKey(@"OpenWithProgIds"))
|
||||
openWithKey?.DeleteValue(programId, throwOnMissingValue: false);
|
||||
openWithKey?.DeleteValue(ProgramId, throwOnMissingValue: false);
|
||||
}
|
||||
|
||||
classes.DeleteSubKeyTree(programId, throwOnMissingSubKey: false);
|
||||
classes.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
|
||||
private record UriAssociation(string Protocol, LocalisableString Description, string IconPath)
|
||||
private class UriAssociation
|
||||
{
|
||||
/// <summary>
|
||||
/// "The <c>URL Protocol</c> string value indicates that this key declares a custom pluggable protocol handler."
|
||||
/// See https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85).
|
||||
/// </summary>
|
||||
public const string URL_PROTOCOL = @"URL Protocol";
|
||||
private const string url_protocol = @"URL Protocol";
|
||||
|
||||
public string Protocol { get; }
|
||||
private LocalisableString description { get; }
|
||||
private string iconPath { get; }
|
||||
|
||||
public UriAssociation(string protocol, LocalisableString description, string iconPath)
|
||||
{
|
||||
Protocol = protocol;
|
||||
this.description = description;
|
||||
this.iconPath = iconPath;
|
||||
}
|
||||
|
||||
public string ProgramId => $@"{program_id_protocol_prefix}.{Protocol}";
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
@@ -266,29 +352,38 @@ namespace osu.Desktop.Windows
|
||||
|
||||
using (var protocolKey = classes.CreateSubKey(Protocol))
|
||||
{
|
||||
protocolKey.SetValue(URL_PROTOCOL, string.Empty);
|
||||
protocolKey.SetValue(null, $@"URL:{description}");
|
||||
protocolKey.SetValue(url_protocol, string.Empty);
|
||||
|
||||
using (var defaultIconKey = protocolKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, IconPath);
|
||||
// clear out old data
|
||||
protocolKey.DeleteSubKeyTree(default_icon, throwOnMissingSubKey: false);
|
||||
protocolKey.DeleteSubKeyTree(@"Shell", throwOnMissingSubKey: false);
|
||||
}
|
||||
|
||||
using (var openCommandKey = protocolKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
// register a program id for the given protocol
|
||||
using (var programKey = classes.CreateSubKey(ProgramId))
|
||||
{
|
||||
using (var defaultIconKey = programKey.CreateSubKey(default_icon))
|
||||
defaultIconKey.SetValue(null, iconPath);
|
||||
|
||||
using (var openCommandKey = programKey.CreateSubKey(SHELL_OPEN_COMMAND))
|
||||
openCommandKey.SetValue(null, $@"""{exe_path}"" ""%1""");
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDescription(string description)
|
||||
public void LocaliseDescription(LocalisationManager localisationManager)
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
if (classes == null) return;
|
||||
|
||||
using (var protocolKey = classes.OpenSubKey(Protocol, true))
|
||||
protocolKey?.SetValue(null, $@"URL:{description}");
|
||||
protocolKey?.SetValue(null, $@"URL:{localisationManager.GetLocalisedString(description)}");
|
||||
}
|
||||
|
||||
public void Uninstall()
|
||||
{
|
||||
using var classes = Registry.CurrentUser.OpenSubKey(software_classes, true);
|
||||
classes?.DeleteSubKeyTree(Protocol, throwOnMissingSubKey: false);
|
||||
classes?.DeleteSubKeyTree(ProgramId, throwOnMissingSubKey: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.915" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1053" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
|
||||
<PackageReference Include="nunit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Foundation;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.iOS
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : GameApplicationDelegate
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuTestBrowser();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
using UIKit;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.iOS
|
||||
{
|
||||
public static class Application
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
GameApplication.Main(new OsuTestBrowser());
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@@ -21,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
|
||||
{
|
||||
private JuiceStream hitObject;
|
||||
private JuiceStream hitObject = null!;
|
||||
|
||||
private readonly ManualClock manualClock = new ManualClock();
|
||||
|
||||
@@ -193,6 +191,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
addVertexCheckStep(1, 0, times[0], positions[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeletingSecondVertexDeletesEntireJuiceStream()
|
||||
{
|
||||
double[] times = { 100, 400 };
|
||||
float[] positions = { 100, 150 };
|
||||
addBlueprintStep(times, positions);
|
||||
|
||||
addDeleteVertexSteps(times[1], positions[1]);
|
||||
AddAssert("juice stream deleted", () => EditorBeatmap.HitObjects, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVertexResampling()
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
}));
|
||||
}
|
||||
|
||||
public void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
public virtual void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
// The SV setting may need to be changed for the current path.
|
||||
var svBindable = hitObject.SliderVelocityMultiplierBindable;
|
||||
|
||||
@@ -138,5 +138,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
|
||||
EditorBeatmap?.EndChange();
|
||||
}
|
||||
|
||||
public override void UpdateHitObjectFromPath(JuiceStream hitObject)
|
||||
{
|
||||
base.UpdateHitObjectFromPath(hitObject);
|
||||
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||
EditorBeatmap?.Remove(hitObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,10 +88,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
if (!(result.Time is double snappedTime)) return;
|
||||
|
||||
HitObject.OriginalX = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
HitObject.StartTime = snappedTime;
|
||||
if (result.Time is double snappedTime)
|
||||
HitObject.StartTime = snappedTime;
|
||||
break;
|
||||
|
||||
case PlacementState.Active:
|
||||
@@ -107,21 +106,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
Vector2 startPosition = CatchHitObjectUtils.GetStartPosition(HitObjectContainer, HitObject);
|
||||
editablePath.Position = nestedOutlineContainer.Position = scrollingPath.Position = startPosition;
|
||||
|
||||
updateHitObjectFromPath();
|
||||
}
|
||||
if (lastEditablePathId != editablePath.PathId)
|
||||
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
|
||||
private void updateHitObjectFromPath()
|
||||
{
|
||||
if (lastEditablePathId == editablePath.PathId)
|
||||
return;
|
||||
|
||||
editablePath.UpdateHitObjectFromPath(HitObject);
|
||||
ApplyDefaultsToHitObject();
|
||||
|
||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||
|
||||
lastEditablePathId = editablePath.PathId;
|
||||
}
|
||||
|
||||
private double positionToTime(float relativeYPosition)
|
||||
|
||||
@@ -10,10 +10,12 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@@ -54,6 +56,12 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
[Resolved]
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||
|
||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@@ -119,6 +127,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (!IsSelected)
|
||||
return false;
|
||||
|
||||
if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed)
|
||||
{
|
||||
convertToStream();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(HitObject _)
|
||||
{
|
||||
computeObjectBounds();
|
||||
@@ -168,6 +190,50 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
lastSliderPathVersion = HitObject.Path.Version.Value;
|
||||
}
|
||||
|
||||
// duplicated in `SliderSelectionBlueprint.convertToStream()`
|
||||
// consider extracting common helper when applying changes here
|
||||
private void convertToStream()
|
||||
{
|
||||
if (editorBeatmap == null || beatDivisor == null)
|
||||
return;
|
||||
|
||||
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
|
||||
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
int i = 0;
|
||||
double time = HitObject.StartTime;
|
||||
|
||||
while (!Precision.DefinitelyBigger(time, HitObject.GetEndTime(), 1))
|
||||
{
|
||||
// positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()]
|
||||
// and indicates how many fractional spans of a slider have passed up to time.
|
||||
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
|
||||
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
|
||||
// every second span is in the reverse direction - need to reverse the path position.
|
||||
if (positionWithRepeats % 2 >= 1)
|
||||
pathPosition = 1 - pathPosition;
|
||||
|
||||
float fruitXValue = HitObject.OriginalX + HitObject.Path.PositionAt(pathPosition).X;
|
||||
|
||||
editorBeatmap.Add(new Fruit
|
||||
{
|
||||
StartTime = time,
|
||||
OriginalX = fruitXValue,
|
||||
NewCombo = i == 0 && HitObject.NewCombo,
|
||||
Samples = HitObject.Samples.Select(s => s.With()).ToList()
|
||||
});
|
||||
|
||||
i += 1;
|
||||
time = HitObject.StartTime + i * streamSpacing;
|
||||
}
|
||||
|
||||
editorBeatmap.Remove(HitObject);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
private IEnumerable<MenuItem> getContextMenuItems()
|
||||
{
|
||||
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||
@@ -177,6 +243,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
};
|
||||
|
||||
yield return new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
||||
@@ -18,7 +18,6 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
|
||||
@@ -159,27 +159,26 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
// Note that this implementation is shared with the osu! ruleset's implementation.
|
||||
// If a change is made here, OsuHitObject.cs should also be updated.
|
||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||
int index = lastObj?.ComboIndex ?? 0;
|
||||
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||
|
||||
if (this is BananaShower)
|
||||
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||
// - At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||
if (this is not BananaShower && (NewCombo || lastObj == null || lastObj is BananaShower))
|
||||
{
|
||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||
return;
|
||||
}
|
||||
|
||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||
if (NewCombo || lastObj == null || lastObj is BananaShower)
|
||||
{
|
||||
IndexInCurrentCombo = 0;
|
||||
ComboIndex++;
|
||||
ComboIndexWithOffsets += ComboOffset + 1;
|
||||
inCurrentCombo = 0;
|
||||
index++;
|
||||
indexWithOffsets += ComboOffset + 1;
|
||||
|
||||
if (lastObj != null)
|
||||
lastObj.LastInCombo = true;
|
||||
}
|
||||
|
||||
ComboIndex = index;
|
||||
ComboIndexWithOffsets = indexWithOffsets;
|
||||
IndexInCurrentCombo = inCurrentCombo;
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
@@ -210,11 +209,27 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
|
||||
|
||||
float IHasXPosition.X => OriginalX;
|
||||
float IHasXPosition.X
|
||||
{
|
||||
get => OriginalX;
|
||||
set => OriginalX = value;
|
||||
}
|
||||
|
||||
float IHasYPosition.Y => LegacyConvertedY;
|
||||
float IHasYPosition.Y
|
||||
{
|
||||
get => LegacyConvertedY;
|
||||
set => LegacyConvertedY = value;
|
||||
}
|
||||
|
||||
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
|
||||
Vector2 IHasPosition.Position
|
||||
{
|
||||
get => new Vector2(OriginalX, LegacyConvertedY);
|
||||
set
|
||||
{
|
||||
((IHasXPosition)this).X = value.X;
|
||||
((IHasYPosition)this).Y = value.Y;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Foundation;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.iOS
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : GameApplicationDelegate
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuTestBrowser();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
using UIKit;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.iOS
|
||||
{
|
||||
public static class Application
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
GameApplication.Main(new OsuTestBrowser());
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
[TestCase("basic")]
|
||||
[TestCase("zero-length-slider")]
|
||||
[TestCase("mania-specific-spinner")]
|
||||
[TestCase("20544")]
|
||||
[TestCase("100374")]
|
||||
[TestCase("1450162")]
|
||||
[TestCase("4869637")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"Mappings": [
|
||||
{
|
||||
"RandomW": 273071671,
|
||||
"RandomX": 842502087,
|
||||
"RandomY": 3579807591,
|
||||
"RandomZ": 273326509,
|
||||
"StartTime": 11783.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 11783.0,
|
||||
"EndTime": 15116.0,
|
||||
"Column": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"RandomW": 2659271247,
|
||||
"RandomX": 3579807591,
|
||||
"RandomY": 273326509,
|
||||
"RandomZ": 273071671,
|
||||
"StartTime": 91545.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 91545.0,
|
||||
"EndTime": 92735.0,
|
||||
"Column": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"RandomW": 3083635271,
|
||||
"RandomX": 273326509,
|
||||
"RandomY": 273071671,
|
||||
"RandomZ": 2659271247,
|
||||
"StartTime": 152497.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 152497.0,
|
||||
"EndTime": 153687.0,
|
||||
"Column": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"RandomW": 4073591514,
|
||||
"RandomX": 273071671,
|
||||
"RandomY": 2659271247,
|
||||
"RandomZ": 3083635271,
|
||||
"StartTime": 231545.0,
|
||||
"Objects": [
|
||||
{
|
||||
"StartTime": 231545.0,
|
||||
"EndTime": 232974.0,
|
||||
"Column": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 3
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:4
|
||||
OverallDifficulty:5
|
||||
ApproachRate:0
|
||||
SliderMultiplier:2.6
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
355,476.190476190476,4,2,1,60,1,0
|
||||
60652,-100,4,2,1,60,0,1
|
||||
92735,-100,4,2,1,60,0,0
|
||||
121485,-100,4,2,1,60,0,1
|
||||
153688,-100,4,2,1,60,0,0
|
||||
182497,-100,4,2,1,60,0,1
|
||||
213688,-100,4,2,1,60,0,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,11783,12,0,15116,0:0:0:0:
|
||||
256,192,91545,12,0,92735,0:0:0:0:
|
||||
256,192,152497,12,0,153687,0:0:0:0:
|
||||
256,192,231545,12,0,232974,0:0:0:0:
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -7,11 +7,13 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@@ -124,16 +126,109 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
protected override IEnumerable<ManiaHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
||||
{
|
||||
if (original is ManiaHitObject maniaOriginal)
|
||||
{
|
||||
yield return maniaOriginal;
|
||||
LegacyHitObjectType legacyType;
|
||||
|
||||
yield break;
|
||||
switch (original)
|
||||
{
|
||||
case ManiaHitObject maniaObj:
|
||||
{
|
||||
yield return maniaObj;
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
case IHasLegacyHitObjectType legacy:
|
||||
legacyType = legacy.LegacyType & LegacyHitObjectType.ObjectTypes;
|
||||
break;
|
||||
|
||||
case IHasPath:
|
||||
legacyType = LegacyHitObjectType.Slider;
|
||||
break;
|
||||
|
||||
case IHasDuration:
|
||||
legacyType = LegacyHitObjectType.Hold;
|
||||
break;
|
||||
|
||||
default:
|
||||
legacyType = LegacyHitObjectType.Circle;
|
||||
break;
|
||||
}
|
||||
|
||||
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
|
||||
foreach (ManiaHitObject obj in objects)
|
||||
yield return obj;
|
||||
double startTime = original.StartTime;
|
||||
double endTime = (original as IHasDuration)?.EndTime ?? startTime;
|
||||
Vector2 position = (original as IHasPosition)?.Position ?? Vector2.Zero;
|
||||
|
||||
PatternGenerator conversion;
|
||||
|
||||
switch (legacyType)
|
||||
{
|
||||
case LegacyHitObjectType.Circle:
|
||||
if (IsForCurrentRuleset)
|
||||
{
|
||||
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
|
||||
recordNote(startTime, position);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: The density is used during the pattern generator constructor, and intentionally computed first.
|
||||
computeDensity(startTime);
|
||||
conversion = new HitCirclePatternGenerator(Random, original, beatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
|
||||
recordNote(startTime, position);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LegacyHitObjectType.Slider:
|
||||
if (IsForCurrentRuleset)
|
||||
{
|
||||
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
|
||||
recordNote(original.StartTime, position);
|
||||
}
|
||||
else
|
||||
{
|
||||
var generator = new SliderPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
|
||||
conversion = generator;
|
||||
|
||||
for (int i = 0; i <= generator.SpanCount; i++)
|
||||
{
|
||||
double time = original.StartTime + generator.SegmentDuration * i;
|
||||
|
||||
recordNote(time, position);
|
||||
computeDensity(time);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LegacyHitObjectType.Spinner:
|
||||
// Note: Some older mania-specific beatmaps can have spinners that are converted rather than passed through.
|
||||
// Newer beatmaps will usually use the "hold" hitobject type below.
|
||||
conversion = new SpinnerPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
|
||||
recordNote(endTime, new Vector2(256, 192));
|
||||
computeDensity(endTime);
|
||||
break;
|
||||
|
||||
case LegacyHitObjectType.Hold:
|
||||
conversion = new PassThroughPatternGenerator(Random, original, beatmap, TotalColumns, lastPattern);
|
||||
recordNote(endTime, position);
|
||||
computeDensity(endTime);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Invalid legacy object type: {legacyType}", nameof(original));
|
||||
}
|
||||
|
||||
foreach (var newPattern in conversion.Generate())
|
||||
{
|
||||
if (conversion is HitCirclePatternGenerator circleGenerator)
|
||||
lastStair = circleGenerator.StairType;
|
||||
|
||||
if (conversion is HitCirclePatternGenerator || conversion is SliderPatternGenerator)
|
||||
lastPattern = newPattern;
|
||||
|
||||
foreach (var obj in newPattern.HitObjects)
|
||||
yield return obj;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LimitedCapacityQueue<double> prevNoteTimes = new LimitedCapacityQueue<double>(max_notes_for_density);
|
||||
@@ -156,135 +251,5 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
lastTime = time;
|
||||
lastPosition = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that generates hit objects for osu!mania specific beatmaps.
|
||||
/// </summary>
|
||||
/// <param name="original">The original hit object.</param>
|
||||
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
|
||||
/// <returns>The hit objects generated.</returns>
|
||||
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
|
||||
{
|
||||
var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
|
||||
|
||||
foreach (var newPattern in generator.Generate())
|
||||
{
|
||||
lastPattern = newPattern;
|
||||
|
||||
foreach (var obj in newPattern.HitObjects)
|
||||
yield return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Method that generates hit objects for non-osu!mania beatmaps.
|
||||
/// </summary>
|
||||
/// <param name="original">The original hit object.</param>
|
||||
/// <param name="originalBeatmap">The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap.</param>
|
||||
/// <returns>The hit objects generated.</returns>
|
||||
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
|
||||
{
|
||||
Patterns.PatternGenerator? conversion = null;
|
||||
|
||||
switch (original)
|
||||
{
|
||||
case IHasPath:
|
||||
{
|
||||
var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
|
||||
conversion = generator;
|
||||
|
||||
var positionData = original as IHasPosition;
|
||||
|
||||
for (int i = 0; i <= generator.SpanCount; i++)
|
||||
{
|
||||
double time = original.StartTime + generator.SegmentDuration * i;
|
||||
|
||||
recordNote(time, positionData?.Position ?? Vector2.Zero);
|
||||
computeDensity(time);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
{
|
||||
conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
|
||||
|
||||
recordNote(endTimeData.EndTime, new Vector2(256, 192));
|
||||
computeDensity(endTimeData.EndTime);
|
||||
break;
|
||||
}
|
||||
|
||||
case IHasPosition positionData:
|
||||
{
|
||||
computeDensity(original.StartTime);
|
||||
|
||||
conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
|
||||
|
||||
recordNote(original.StartTime, positionData.Position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (conversion == null)
|
||||
yield break;
|
||||
|
||||
foreach (var newPattern in conversion.Generate())
|
||||
{
|
||||
lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern;
|
||||
lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair;
|
||||
|
||||
foreach (var obj in newPattern.HitObjects)
|
||||
yield return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A pattern generator for osu!mania-specific beatmaps.
|
||||
/// </summary>
|
||||
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
|
||||
{
|
||||
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<Pattern> Generate()
|
||||
{
|
||||
yield return generate();
|
||||
}
|
||||
|
||||
private Pattern generate()
|
||||
{
|
||||
var positionData = HitObject as IHasXPosition;
|
||||
|
||||
int column = GetColumn(positionData?.X ?? 0);
|
||||
|
||||
var pattern = new Pattern();
|
||||
|
||||
if (HitObject is IHasDuration endTimeData)
|
||||
{
|
||||
pattern.Add(new HoldNote
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
|
||||
});
|
||||
}
|
||||
else if (HitObject is IHasXPosition)
|
||||
{
|
||||
pattern.Add(new Note
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Samples = HitObject.Samples,
|
||||
Column = column
|
||||
});
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -16,13 +16,16 @@ using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
{
|
||||
internal class HitObjectPatternGenerator : PatternGenerator
|
||||
/// <summary>
|
||||
/// Converter for legacy "HitCircle" hit objects.
|
||||
/// </summary>
|
||||
internal class HitCirclePatternGenerator : LegacyPatternGenerator
|
||||
{
|
||||
public PatternType StairType { get; private set; }
|
||||
|
||||
private readonly PatternType convertType;
|
||||
|
||||
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
|
||||
public HitCirclePatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
|
||||
double density, PatternType lastStair)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
@@ -114,10 +117,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
}
|
||||
|
||||
if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1
|
||||
// If we convert to 7K + 1, let's not overload the special key
|
||||
&& (TotalColumns != 8 || lastColumn != 0)
|
||||
// Make sure the last column was not the centre column
|
||||
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
|
||||
// If we convert to 7K + 1, let's not overload the special key
|
||||
&& (TotalColumns != 8 || lastColumn != 0)
|
||||
// Make sure the last column was not the centre column
|
||||
&& (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2))
|
||||
{
|
||||
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
|
||||
int column = RandomStart + TotalColumns - lastColumn - 1;
|
||||
+7
-9
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@@ -15,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// <summary>
|
||||
/// A pattern generator for legacy hit objects.
|
||||
/// </summary>
|
||||
internal abstract class PatternGenerator : Patterns.PatternGenerator
|
||||
internal abstract class LegacyPatternGenerator : PatternGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// The column index at which to start generating random notes.
|
||||
@@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// </summary>
|
||||
protected readonly LegacyRandom Random;
|
||||
|
||||
protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
|
||||
protected LegacyPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
|
||||
: base(hitObject, beatmap, totalColumns, previousPattern)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(random);
|
||||
@@ -96,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (conversionDifficulty != null)
|
||||
return conversionDifficulty.Value;
|
||||
|
||||
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
|
||||
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
|
||||
HitObject? lastObject = Beatmap.HitObjects.LastOrDefault();
|
||||
HitObject? firstObject = Beatmap.HitObjects.FirstOrDefault();
|
||||
|
||||
// Drain time in seconds
|
||||
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
|
||||
@@ -132,13 +130,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param>
|
||||
/// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param>
|
||||
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
|
||||
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
|
||||
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns">TotalColumns</see> is used.</param>
|
||||
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
|
||||
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
|
||||
/// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no
|
||||
/// <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
|
||||
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
|
||||
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null,
|
||||
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int>? nextColumn = null, [InstantHandle] Func<int, bool>? validation = null,
|
||||
params Pattern[] patterns)
|
||||
{
|
||||
lowerBound ??= RandomStart;
|
||||
@@ -189,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
|
||||
/// </summary>
|
||||
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
|
||||
/// <param name="upperBound">The maximum column index. If null, <see cref="Patterns.PatternGenerator.TotalColumns"/> is used.</param>
|
||||
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
|
||||
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
|
||||
|
||||
/// <summary>
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple generator which, for any object, if the hitobject has an end time
|
||||
/// it becomes a <see cref="HoldNote"/> or otherwise a <see cref="Note"/>.
|
||||
/// </summary>
|
||||
internal class PassThroughPatternGenerator : LegacyPatternGenerator
|
||||
{
|
||||
public PassThroughPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
}
|
||||
|
||||
public override IEnumerable<Pattern> Generate()
|
||||
{
|
||||
var positionData = HitObject as IHasXPosition;
|
||||
int column = GetColumn(positionData?.X ?? 0);
|
||||
|
||||
var pattern = new Pattern();
|
||||
|
||||
if (HitObject is IHasDuration endTimeData)
|
||||
{
|
||||
pattern.Add(new HoldNote
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
pattern.Add(new Note
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Samples = HitObject.Samples,
|
||||
Column = column
|
||||
});
|
||||
}
|
||||
|
||||
yield return pattern;
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -19,9 +17,9 @@ using osu.Game.Utils;
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// A pattern generator for IHasDistance hit objects.
|
||||
/// Converter for legacy "Slider" hit objects.
|
||||
/// </summary>
|
||||
internal class PathObjectPatternGenerator : PatternGenerator
|
||||
internal class SliderPatternGenerator : LegacyPatternGenerator
|
||||
{
|
||||
public readonly int StartTime;
|
||||
public readonly int EndTime;
|
||||
@@ -30,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
private PatternType convertType;
|
||||
|
||||
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
public SliderPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
convertType = PatternType.None;
|
||||
@@ -484,9 +482,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to retrieve node samples at.</param>
|
||||
private IList<IList<HitSampleInfo>> nodeSamplesAt(int time)
|
||||
private IList<IList<HitSampleInfo>>? nodeSamplesAt(int time)
|
||||
{
|
||||
if (!(HitObject is IHasPathWithRepeats curveData))
|
||||
if (HitObject is not IHasPathWithRepeats curveData)
|
||||
return null;
|
||||
|
||||
int index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||
+5
-2
@@ -12,12 +12,15 @@ using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
{
|
||||
internal class EndTimeObjectPatternGenerator : PatternGenerator
|
||||
/// <summary>
|
||||
/// Converter for legacy "Spinner" hit objects.
|
||||
/// </summary>
|
||||
internal class SpinnerPatternGenerator : LegacyPatternGenerator
|
||||
{
|
||||
private readonly int endTime;
|
||||
private readonly PatternType convertType;
|
||||
|
||||
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
public SpinnerPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
|
||||
@@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
||||
/// </summary>
|
||||
internal class Pattern
|
||||
{
|
||||
private List<ManiaHitObject> hitObjects;
|
||||
private HashSet<int> containedColumns;
|
||||
private List<ManiaHitObject>? hitObjects;
|
||||
private HashSet<int>? containedColumns;
|
||||
|
||||
/// <summary>
|
||||
/// All the hit objects contained in this pattern.
|
||||
@@ -72,6 +71,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
||||
containedColumns?.Clear();
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(hitObjects), nameof(containedColumns))]
|
||||
private void prepareStorage()
|
||||
{
|
||||
hitObjects ??= new List<ManiaHitObject>();
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Caption = "Use special (N+1) style",
|
||||
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||
Current = { Value = Beatmap.SpecialStyle }
|
||||
},
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
|
||||
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
|
||||
@@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
|
||||
#region LegacyBeatmapEncoder
|
||||
|
||||
float IHasXPosition.X => Column;
|
||||
float IHasXPosition.X
|
||||
{
|
||||
get => Column;
|
||||
set => Column = (int)value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 1: return colour_cyan;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 3:
|
||||
@@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 2: return colour_cyan;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 4:
|
||||
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 3: return colour_purple;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 5:
|
||||
@@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 4: return colour_cyan;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 6:
|
||||
@@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 5: return colour_pink;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 7:
|
||||
@@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 6: return colour_pink;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 8:
|
||||
@@ -266,7 +266,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 7: return colour_purple;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 9:
|
||||
@@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 8: return colour_purple;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
|
||||
case 10:
|
||||
@@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 9: return colour_purple;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
case 5: return colour_green;
|
||||
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Foundation;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.iOS
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : GameApplicationDelegate
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuTestBrowser();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
using UIKit;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.iOS
|
||||
{
|
||||
public static class Application
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
GameApplication.Main(new OsuTestBrowser());
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@@ -231,6 +233,193 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestControlClickDoesNotDiscardExistingSelectionEvenIfNothingHit()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add object", () => EditorBeatmap.AddRange([firstSlider]));
|
||||
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange([firstSlider]));
|
||||
|
||||
AddStep("move mouse to middle of playfield", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("control-click left mouse", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(100)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("also select third node", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("quick-delete fourth node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(3));
|
||||
InputManager.Click(MouseButton.Middle);
|
||||
});
|
||||
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(100)),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("also select third node", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("quick-delete second node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Middle);
|
||||
});
|
||||
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(100, 100),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint { Type = PathType.LINEAR },
|
||||
new PathControlPoint(new Vector2(50, 100)),
|
||||
new PathControlPoint(new Vector2(145, 100)),
|
||||
},
|
||||
ExpectedDistance = { Value = 162.62 }
|
||||
},
|
||||
};
|
||||
AddStep("add slider", () => EditorBeatmap.Add(slider));
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddStep("select last node", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("right click node", () => InputManager.Click(MouseButton.Right));
|
||||
AddUntilStep("context menu open", () => this.ChildrenOfType<ContextMenuContainer>().Single().ChildrenOfType<Menu>().All(m => m.State == MenuState.Open));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath()
|
||||
{
|
||||
var firstSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(10, 50),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
var secondSlider = new Slider
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = new Vector2(200, 0),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(-100, 100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
|
||||
AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider));
|
||||
|
||||
AddStep("move to marker", () =>
|
||||
{
|
||||
var marker = this.ChildrenOfType<SliderEndDragMarker>().First();
|
||||
var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
|
||||
InputManager.MoveMouseTo(position);
|
||||
});
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider));
|
||||
}
|
||||
|
||||
private ComposeBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
private void gridSizeIs(int size)
|
||||
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
|
||||
&& EditorBeatmap.BeatmapInfo.GridSize == size);
|
||||
&& EditorBeatmap.GridSize == size);
|
||||
|
||||
[Test]
|
||||
public void TestGridTypeToggling()
|
||||
|
||||
@@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
})
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
StackLeniency = 0,
|
||||
}
|
||||
StackLeniency = 0,
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
|
||||
@@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
StackLeniency = 0,
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
ApproachRate = 8.5f
|
||||
}
|
||||
},
|
||||
StackLeniency = 0,
|
||||
ControlPointInfo = controlPointInfo
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModRelax : OsuModTestScene
|
||||
{
|
||||
private readonly HitCircle hitObject;
|
||||
private readonly HitWindows hitWindows = new OsuHitWindows();
|
||||
|
||||
public TestSceneOsuModRelax()
|
||||
{
|
||||
hitWindows.SetDifficulty(9);
|
||||
|
||||
hitObject = new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(100, 100),
|
||||
HitWindows = hitWindows
|
||||
};
|
||||
}
|
||||
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
|
||||
|
||||
[Test]
|
||||
public void TestRelax() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModRelax(),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject> { hitObject }
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2()),
|
||||
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestRelaxLeniency() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModRelax(),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject> { hitObject }
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
|
||||
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
|
||||
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
protected partial class ModRelaxTestPlayer : ModTestPlayer
|
||||
{
|
||||
private readonly ModTestData currentTestData;
|
||||
|
||||
public ModRelaxTestPlayer(ModTestData data, bool allowFail)
|
||||
: base(data, allowFail)
|
||||
{
|
||||
currentTestData = data;
|
||||
}
|
||||
|
||||
protected override void PrepareReplay()
|
||||
{
|
||||
// We need to set IsLegacyScore to true otherwise the mod assumes that presses are already embedded into the replay
|
||||
DrawableRuleset?.SetReplayScore(new Score
|
||||
{
|
||||
Replay = new Replay { Frames = currentTestData.ReplayFrames! },
|
||||
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" }, IsLegacyScore = true, Mods = new Mod[] { new OsuModRelax() } },
|
||||
});
|
||||
|
||||
DrawableRuleset?.SetRecordTarget(Score);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
@@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Skinning;
|
||||
@@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotation()
|
||||
{
|
||||
createTest(() =>
|
||||
{
|
||||
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true);
|
||||
var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer)
|
||||
{
|
||||
NewPartScale = new Vector2(10)
|
||||
};
|
||||
|
||||
skinContainer.Child = legacyCursorTrail;
|
||||
|
||||
return skinContainer;
|
||||
});
|
||||
}
|
||||
|
||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||
{
|
||||
Clear();
|
||||
@@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private readonly IRenderer renderer;
|
||||
private readonly bool provideMiddle;
|
||||
private readonly bool provideCursor;
|
||||
private readonly bool enableRotation;
|
||||
|
||||
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
|
||||
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false)
|
||||
{
|
||||
this.renderer = renderer;
|
||||
this.provideMiddle = provideMiddle;
|
||||
this.provideCursor = provideCursor;
|
||||
this.enableRotation = enableRotation;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
@@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case OsuSkinConfiguration osuLookup:
|
||||
if (osuLookup == OsuSkinConfiguration.CursorTrailRotate)
|
||||
return SkinUtils.As<TValue>(new BindableBool(enableRotation));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
@@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
|
||||
}
|
||||
}
|
||||
|
||||
private partial class LegacyRotatingCursorTrail : LegacyCursorTrail
|
||||
{
|
||||
public LegacyRotatingCursorTrail([NotNull] ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
PartRotation += (float)(Time.Elapsed * 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap)
|
||||
{
|
||||
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
|
||||
beatmap.BeatmapInfo.StackLeniency = 0;
|
||||
beatmap.StackLeniency = 0;
|
||||
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
SliderMultiplier = 4,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.4" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
h.StackHeight = 0;
|
||||
|
||||
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
|
||||
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
|
||||
applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
|
||||
else
|
||||
applyStackingOld(beatmap.BeatmapInfo, hitObjects);
|
||||
applyStackingOld(beatmap, hitObjects);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
|
||||
private static void applyStacking(IBeatmap beatmap, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
|
||||
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
continue;
|
||||
|
||||
double endTime = stackBaseObject.GetEndTime();
|
||||
double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency;
|
||||
double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency;
|
||||
|
||||
if (objectN.StartTime - endTime > stackThreshold)
|
||||
// We are no longer within stacking range of the next object.
|
||||
@@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
OsuHitObject objectI = hitObjects[i];
|
||||
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
|
||||
|
||||
double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency;
|
||||
double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency;
|
||||
|
||||
/* If this object is a hitcircle, then we enter this "special" case.
|
||||
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
|
||||
@@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
|
||||
private static void applyStackingOld(IBeatmap beatmap, List<OsuHitObject> hitObjects)
|
||||
{
|
||||
for (int i = 0; i < hitObjects.Count; i++)
|
||||
{
|
||||
@@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
|
||||
for (int j = i + 1; j < hitObjects.Count; j++)
|
||||
{
|
||||
double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency;
|
||||
double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency;
|
||||
|
||||
if (hitObjects[j].StartTime - stackThreshold > startTime)
|
||||
break;
|
||||
|
||||
+17
-6
@@ -137,11 +137,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <summary>
|
||||
/// Delete all visually selected <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>Whether any change actually took place.</returns>
|
||||
public bool DeleteSelected()
|
||||
{
|
||||
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
||||
|
||||
if (!Delete(toRemove))
|
||||
return false;
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the specified <see cref="PathControlPoint"/>s.
|
||||
/// </summary>
|
||||
/// <returns>Whether any change actually took place.</returns>
|
||||
public bool Delete(List<PathControlPoint> toRemove)
|
||||
{
|
||||
// Ensure that there are any points to be deleted
|
||||
if (toRemove.Count == 0)
|
||||
return false;
|
||||
@@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
changeHandler?.BeginChange();
|
||||
RemoveControlPointsRequested?.Invoke(toRemove);
|
||||
changeHandler?.EndChange();
|
||||
|
||||
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
||||
foreach (var piece in Pieces)
|
||||
piece.IsSelected.Value = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
@@ -76,6 +77,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left;
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
|
||||
|
||||
@@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (hoveredControlPoint == null)
|
||||
return false;
|
||||
|
||||
hoveredControlPoint.IsSelected.Value = true;
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
if (hoveredControlPoint.IsSelected.Value)
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
else
|
||||
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -551,6 +554,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
// duplicated in `JuiceStreamSelectionBlueprint.convertToStream()`
|
||||
// consider extracting common helper when applying changes here
|
||||
private void convertToStream()
|
||||
{
|
||||
if (editorBeatmap == null || beatDivisor == null)
|
||||
@@ -621,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && DrawableObject.Body.Alpha > 0)
|
||||
return true;
|
||||
|
||||
if (ControlPointVisualiser == null)
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
},
|
||||
};
|
||||
|
||||
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize;
|
||||
Spacing.Value = editorBeatmap.GridSize;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
|
||||
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
|
||||
SpacingVector.Value = new Vector2(spacing.NewValue);
|
||||
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
|
||||
editorBeatmap.GridSize = (int)spacing.NewValue;
|
||||
}, true);
|
||||
|
||||
GridLinesRotation.BindValueChanged(rotation =>
|
||||
|
||||
@@ -53,9 +53,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
|
||||
.Append(new DrawableTernaryButton
|
||||
{
|
||||
Current = rectangularGridSnapToggle,
|
||||
Description = "Grid Snap",
|
||||
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap },
|
||||
})
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = "Stack Leniency",
|
||||
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||
Current = new BindableFloat(Beatmap.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
MinValue = 0,
|
||||
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
|
||||
Beatmap.StackLeniency = stackLeniency.Current.Value;
|
||||
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
/// <summary>
|
||||
/// How early before a hitobject's start time to trigger a hit.
|
||||
/// </summary>
|
||||
private const float relax_leniency = 3;
|
||||
public const float RELAX_LENIENCY = 12;
|
||||
|
||||
private bool isDownState;
|
||||
private bool wasLeft;
|
||||
@@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
|
||||
{
|
||||
// we are not yet close enough to the object.
|
||||
if (time < h.HitObject.StartTime - relax_leniency)
|
||||
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
|
||||
break;
|
||||
|
||||
// already hit or beyond the hittable end time.
|
||||
|
||||
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
// If samples aren't available at the exact start time of the object,
|
||||
// use samples (without additions) in the closest original hit object instead
|
||||
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList();
|
||||
obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.ALL_ADDITIONS.Contains(s.Name)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -377,13 +377,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
UpdateState(ArmedState.Idle);
|
||||
HeadCircle.SuppressHitAnimations();
|
||||
|
||||
foreach (var repeat in repeatContainer)
|
||||
repeat.SuppressHitAnimations();
|
||||
|
||||
TailCircle.SuppressHitAnimations();
|
||||
|
||||
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||
|
||||
if (Time.Current >= HitStateUpdateTime)
|
||||
{
|
||||
// Apply the slider's alpha to *only* the body.
|
||||
// This allows start and – more importantly – end circles to fade slower than the overall slider.
|
||||
if (Alpha < 1)
|
||||
Body.Alpha = Alpha;
|
||||
Alpha = 1;
|
||||
}
|
||||
|
||||
LifetimeEnd = HitStateUpdateTime + 700;
|
||||
}
|
||||
|
||||
internal void RestoreHitAnimations()
|
||||
{
|
||||
UpdateState(ArmedState.Hit);
|
||||
HeadCircle.RestoreHitAnimations();
|
||||
|
||||
foreach (var repeat in repeatContainer)
|
||||
repeat.RestoreHitAnimations();
|
||||
|
||||
TailCircle.RestoreHitAnimations();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@@ -163,5 +164,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
#region FOR EDITOR USE ONLY, DO NOT USE FOR ANY OTHER PURPOSE
|
||||
|
||||
internal void SuppressHitAnimations()
|
||||
{
|
||||
UpdateState(ArmedState.Idle);
|
||||
UpdateComboColour();
|
||||
|
||||
// This method is called every frame in editor contexts, thus the lack of need for transforms.
|
||||
|
||||
bool hit = Time.Current >= HitStateUpdateTime;
|
||||
|
||||
if (hit)
|
||||
{
|
||||
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
|
||||
AccentColour.Value = Color4.White;
|
||||
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
|
||||
}
|
||||
|
||||
Arrow.Alpha = hit ? 0 : 1;
|
||||
|
||||
LifetimeEnd = HitStateUpdateTime + 700;
|
||||
}
|
||||
|
||||
internal void RestoreHitAnimations()
|
||||
{
|
||||
UpdateState(ArmedState.Hit);
|
||||
UpdateComboColour();
|
||||
Arrow.Alpha = 1;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set => position.Value = value;
|
||||
}
|
||||
|
||||
public float X => Position.X;
|
||||
public float Y => Position.Y;
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Position.Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(Position.X, value);
|
||||
}
|
||||
|
||||
public Vector2 StackedPosition => Position + StackOffset;
|
||||
|
||||
@@ -175,27 +184,26 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
||||
// If a change is made here, CatchHitObject.cs should also be updated.
|
||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||
int index = lastObj?.ComboIndex ?? 0;
|
||||
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||
|
||||
if (this is Spinner)
|
||||
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||
// - At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
|
||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||
if (this is not Spinner && (NewCombo || lastObj == null || lastObj is Spinner))
|
||||
{
|
||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||
return;
|
||||
}
|
||||
|
||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
|
||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||
if (NewCombo || lastObj == null || lastObj is Spinner)
|
||||
{
|
||||
IndexInCurrentCombo = 0;
|
||||
ComboIndex++;
|
||||
ComboIndexWithOffsets += ComboOffset + 1;
|
||||
inCurrentCombo = 0;
|
||||
index++;
|
||||
indexWithOffsets += ComboOffset + 1;
|
||||
|
||||
if (lastObj != null)
|
||||
lastObj.LastInCombo = true;
|
||||
}
|
||||
|
||||
ComboIndex = index;
|
||||
ComboIndexWithOffsets = indexWithOffsets;
|
||||
IndexInCurrentCombo = inCurrentCombo;
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||
|
||||
@@ -5,12 +5,12 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@@ -75,44 +75,38 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
|
||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
|
||||
|
||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||
{
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
}
|
||||
else
|
||||
Scale = Vector2.One;
|
||||
|
||||
const float move_distance = -12;
|
||||
const float scale_amount = 1.3f;
|
||||
|
||||
const double move_out_duration = 35;
|
||||
const double move_in_duration = 250;
|
||||
const double total = 300;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
main.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
||||
.Then()
|
||||
.ScaleTo(1f, move_in_duration, Easing.Out)
|
||||
.Loop(total - (move_in_duration + move_out_duration));
|
||||
side
|
||||
.MoveToX(move_distance, move_out_duration, Easing.Out)
|
||||
.Then()
|
||||
.MoveToX(0, move_in_duration, Easing.Out)
|
||||
.Loop(total - (move_in_duration + move_out_duration));
|
||||
break;
|
||||
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||
|
||||
case ArmedState.Hit:
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
this.ScaleTo(1.5f, animDuration, Easing.Out);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (loopCurrentTime < move_out_duration)
|
||||
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||
else
|
||||
main.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (drawableRepeat.IsNotNull())
|
||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
if (loopCurrentTime < move_out_duration)
|
||||
side.X = Interpolation.ValueAt(loopCurrentTime, 0, move_distance, 0, move_out_duration, Easing.Out);
|
||||
else
|
||||
side.X = Interpolation.ValueAt(loopCurrentTime, move_distance, 0, move_out_duration, move_out_duration + move_in_duration, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@@ -40,37 +40,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
private void load(DrawableHitObject drawableObject)
|
||||
{
|
||||
drawableRepeat = (DrawableSliderRepeat)drawableObject;
|
||||
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||
protected override void Update()
|
||||
{
|
||||
const double move_out_duration = 35;
|
||||
const double move_in_duration = 250;
|
||||
const double total = 300;
|
||||
base.Update();
|
||||
|
||||
switch (state)
|
||||
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
InternalChild.ScaleTo(1.3f, move_out_duration, Easing.Out)
|
||||
.Then()
|
||||
.ScaleTo(1f, move_in_duration, Easing.Out)
|
||||
.Loop(total - (move_in_duration + move_out_duration));
|
||||
break;
|
||||
|
||||
case ArmedState.Hit:
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
InternalChild.ScaleTo(1.5f, animDuration, Easing.Out);
|
||||
break;
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const float scale_amount = 1.3f;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
const double move_out_duration = 35;
|
||||
const double move_in_duration = 250;
|
||||
const double total = 300;
|
||||
|
||||
if (drawableRepeat.IsNotNull())
|
||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % total;
|
||||
if (loopCurrentTime < move_out_duration)
|
||||
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1, scale_amount, 0, move_out_duration, Easing.Out));
|
||||
else
|
||||
Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, scale_amount, 1f, move_out_duration, move_out_duration + move_in_duration, Easing.Out));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCursor : SkinnableCursor
|
||||
{
|
||||
public static readonly int REVOLUTION_DURATION = 10000;
|
||||
|
||||
private const float pressed_scale = 1.3f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
@@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
if (spin)
|
||||
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
|
||||
ExpandTarget.Spin(REVOLUTION_DURATION, RotationDirection.Clockwise);
|
||||
}
|
||||
|
||||
public override void Expand()
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||
{
|
||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||
AllowPartRotation = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true;
|
||||
|
||||
Texture = skin.GetTexture("cursortrail");
|
||||
|
||||
|
||||
@@ -57,11 +57,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const string base_lookup = @"hitcircle";
|
||||
|
||||
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
|
||||
|
||||
// As a precondition, prefer that any *prefix* lookups are run against the skin which is providing "hitcircle".
|
||||
// This is to correctly handle a case such as:
|
||||
//
|
||||
// - Beatmap provides `hitcircle`
|
||||
// - User skin provides `sliderstartcircle`
|
||||
//
|
||||
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
|
||||
//
|
||||
// Of note, this consideration should only be used to decide whether to continue looking up the prefixed name or not.
|
||||
// The final lookups must still run on the full skin hierarchy as per usual in order to correctly handle fallback cases.
|
||||
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
|
||||
|
||||
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
|
||||
// otherwise fall back to the default prefix "hitcircle".
|
||||
string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
|
||||
string circleName = (priorityLookupPrefix != null && provider.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : base_lookup;
|
||||
|
||||
Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2;
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
@@ -51,8 +53,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
||||
|
||||
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
|
||||
shouldRotate = skinSource.GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value <= 1;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
|
||||
accentColour.BindValueChanged(c =>
|
||||
{
|
||||
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
|
||||
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > 600 / 255f ? Color4.Black : Color4.White;
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -80,36 +80,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
|
||||
protected override void Update()
|
||||
{
|
||||
const double duration = 300;
|
||||
const float rotation = 5.625f;
|
||||
base.Update();
|
||||
|
||||
switch (state)
|
||||
if (Time.Current >= drawableRepeat.HitStateUpdateTime && drawableRepeat.State.Value == ArmedState.Hit)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
if (shouldRotate)
|
||||
{
|
||||
InternalChild.ScaleTo(1.3f)
|
||||
.RotateTo(rotation)
|
||||
.Then()
|
||||
.ScaleTo(1f, duration)
|
||||
.RotateTo(-rotation, duration)
|
||||
.Loop();
|
||||
}
|
||||
else
|
||||
{
|
||||
InternalChild.ScaleTo(1.3f).Then()
|
||||
.ScaleTo(1f, duration, Easing.Out)
|
||||
.Loop();
|
||||
}
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
arrow.Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.4f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
}
|
||||
else
|
||||
{
|
||||
const double duration = 300;
|
||||
const float rotation = 5.625f;
|
||||
|
||||
break;
|
||||
double loopCurrentTime = (Time.Current - drawableRepeat.AnimationStartTime.Value) % duration;
|
||||
|
||||
case ArmedState.Hit:
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out);
|
||||
break;
|
||||
// Reference: https://github.com/peppy/osu-stable-reference/blob/2280c4c436f80d04f9c79d3c905db00ac2902273/osu!/GameplayElements/HitObjects/Osu/HitCircleSliderEnd.cs#L79-L96
|
||||
if (shouldRotate)
|
||||
{
|
||||
arrow.Rotation = Interpolation.ValueAt(loopCurrentTime, rotation, -rotation, 0, duration);
|
||||
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration));
|
||||
}
|
||||
else
|
||||
{
|
||||
arrow.Scale = new Vector2(Interpolation.ValueAt(loopCurrentTime, 1.3f, 1, 0, duration, Easing.Out));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +116,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
if (drawableRepeat.IsNotNull())
|
||||
{
|
||||
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
|
||||
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
CursorCentre,
|
||||
CursorExpand,
|
||||
CursorRotate,
|
||||
CursorTrailRotate,
|
||||
HitCircleOverlayAboveNumber,
|
||||
|
||||
// ReSharper disable once IdentifierTypo
|
||||
|
||||
@@ -34,19 +34,24 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
/// </summary>
|
||||
protected virtual float FadeExponent => 1.7f;
|
||||
|
||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||
private int currentIndex;
|
||||
private IShader shader;
|
||||
private double timeOffset;
|
||||
private float time;
|
||||
|
||||
/// <summary>
|
||||
/// The scale used on creation of a new trail part.
|
||||
/// </summary>
|
||||
public Vector2 NewPartScale = Vector2.One;
|
||||
public Vector2 NewPartScale { get; set; } = Vector2.One;
|
||||
|
||||
private Anchor trailOrigin = Anchor.Centre;
|
||||
/// <summary>
|
||||
/// The rotation (in degrees) to apply to trail parts when <see cref="AllowPartRotation"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
public float PartRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to rotate trail parts based on the value of <see cref="PartRotation"/>.
|
||||
/// </summary>
|
||||
protected bool AllowPartRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The trail part texture origin.
|
||||
/// </summary>
|
||||
protected Anchor TrailOrigin
|
||||
{
|
||||
get => trailOrigin;
|
||||
@@ -57,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
}
|
||||
}
|
||||
|
||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||
private Anchor trailOrigin = Anchor.Centre;
|
||||
private int currentIndex;
|
||||
private IShader shader;
|
||||
private double timeOffset;
|
||||
private float time;
|
||||
|
||||
public CursorTrail()
|
||||
{
|
||||
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
|
||||
@@ -220,6 +232,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
private float time;
|
||||
private float fadeExponent;
|
||||
private float angle;
|
||||
|
||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||
private Vector2 originPosition;
|
||||
@@ -239,6 +252,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
texture = Source.texture;
|
||||
time = Source.time;
|
||||
fadeExponent = Source.FadeExponent;
|
||||
angle = Source.AllowPartRotation ? float.DegreesToRadians(Source.PartRotation) : 0;
|
||||
|
||||
originPosition = Vector2.Zero;
|
||||
|
||||
@@ -279,6 +293,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
||||
|
||||
float sin = MathF.Sin(angle);
|
||||
float cos = MathF.Cos(angle);
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.InvalidationID == -1)
|
||||
@@ -289,7 +306,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
Position = rotateAround(
|
||||
new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
part.Position, sin, cos),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||
@@ -298,7 +317,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
Position = rotateAround(
|
||||
new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X,
|
||||
part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||
@@ -307,7 +328,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
Position = rotateAround(
|
||||
new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
part.Position, sin, cos),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||
@@ -316,7 +339,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
Position = rotateAround(
|
||||
new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
part.Position, sin, cos),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||
@@ -330,6 +355,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private static Vector2 rotateAround(Vector2 input, Vector2 origin, float sin, float cos)
|
||||
{
|
||||
float xTranslated = input.X - origin.X;
|
||||
float yTranslated = input.Y - origin.Y;
|
||||
|
||||
return new Vector2(xTranslated * cos - yTranslated * sin, xTranslated * sin + yTranslated * cos) + origin;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -36,6 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
/// </summary>
|
||||
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// The current rotation of the cursor.
|
||||
/// </summary>
|
||||
public float CurrentRotation => skinnableCursor.ExpandTarget?.Rotation ?? 0;
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -83,7 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
base.Update();
|
||||
|
||||
if (cursorTrail.Drawable is CursorTrail trail)
|
||||
{
|
||||
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
||||
trail.PartRotation = ActiveCursor.CurrentRotation;
|
||||
}
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
|
||||
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
protected override ResumeOverlay CreateResumeOverlay()
|
||||
{
|
||||
if (Mods.Any(m => m is OsuModAutopilot))
|
||||
if (Mods.Any(m => m is OsuModAutopilot or OsuModTouchDevice))
|
||||
return new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
|
||||
|
||||
return new OsuResumeOverlay();
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Foundation;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.iOS
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : GameApplicationDelegate
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuTestBrowser();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,16 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.iOS;
|
||||
using osu.Game.Tests;
|
||||
using UIKit;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.iOS
|
||||
{
|
||||
public static class Application
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
GameApplication.Main(new OsuTestBrowser());
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
|
||||
@@ -144,6 +144,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested, ref attributes);
|
||||
return;
|
||||
|
||||
case StrongNestedHitObject:
|
||||
// we never need to deal with these directly.
|
||||
// the only thing strong hits do in terms of scoring is double their object's score increase,
|
||||
// which is already handled at the parent object level via the `strongable.IsStrong` check lower down in this method.
|
||||
// not handling these here can lead to them falsely being counted as combo-increasing when handling strong drum rolls!
|
||||
return;
|
||||
}
|
||||
|
||||
if (hitObject is DrumRollTick tick)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Foundation;
|
||||
using osu.Framework.iOS;
|
||||
|
||||
namespace osu.Game.Tests.iOS
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : GameApplicationDelegate
|
||||
{
|
||||
protected override Framework.Game CreateGame() => new OsuTestBrowser();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.iOS;
|
||||
using UIKit;
|
||||
|
||||
namespace osu.Game.Tests.iOS
|
||||
{
|
||||
public static class Application
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
GameApplication.Main(new OsuTestBrowser());
|
||||
UIApplication.Main(args, null, typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,16 +80,16 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var metadata = beatmap.Metadata;
|
||||
|
||||
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
|
||||
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
|
||||
Assert.AreEqual(0, beatmap.AudioLeadIn);
|
||||
Assert.AreEqual(164471, metadata.PreviewTime);
|
||||
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
|
||||
Assert.AreEqual(0.7f, beatmap.StackLeniency);
|
||||
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
|
||||
Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
|
||||
Assert.IsFalse(beatmapInfo.SpecialStyle);
|
||||
Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
|
||||
Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate);
|
||||
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
|
||||
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
|
||||
Assert.IsFalse(beatmap.LetterboxInBreaks);
|
||||
Assert.IsFalse(beatmap.SpecialStyle);
|
||||
Assert.IsFalse(beatmap.WidescreenStoryboard);
|
||||
Assert.IsFalse(beatmap.SamplesMatchPlaybackRate);
|
||||
Assert.AreEqual(CountdownType.None, beatmap.Countdown);
|
||||
Assert.AreEqual(0, beatmap.CountdownOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var beatmapInfo = decoder.Decode(stream).BeatmapInfo;
|
||||
var beatmap = decoder.Decode(stream);
|
||||
|
||||
int[] expectedBookmarks =
|
||||
{
|
||||
@@ -109,13 +109,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
95901, 106450, 116999, 119637, 130186, 140735, 151285,
|
||||
161834, 164471, 175020, 185570, 196119, 206669, 209306
|
||||
};
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length);
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length);
|
||||
for (int i = 0; i < expectedBookmarks.Length; i++)
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]);
|
||||
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing);
|
||||
Assert.AreEqual(4, beatmapInfo.BeatDivisor);
|
||||
Assert.AreEqual(4, beatmapInfo.GridSize);
|
||||
Assert.AreEqual(2, beatmapInfo.TimelineZoom);
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]);
|
||||
Assert.AreEqual(1.8, beatmap.DistanceSpacing);
|
||||
Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor);
|
||||
Assert.AreEqual(4, beatmap.GridSize);
|
||||
Assert.AreEqual(2, beatmap.TimelineZoom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -993,15 +993,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0));
|
||||
Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f));
|
||||
Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False);
|
||||
Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False);
|
||||
Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False);
|
||||
Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False);
|
||||
Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False);
|
||||
Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.None));
|
||||
Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0));
|
||||
Assert.That(decoded.AudioLeadIn, Is.EqualTo(0));
|
||||
Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f));
|
||||
Assert.That(decoded.SpecialStyle, Is.False);
|
||||
Assert.That(decoded.LetterboxInBreaks, Is.False);
|
||||
Assert.That(decoded.WidescreenStoryboard, Is.False);
|
||||
Assert.That(decoded.EpilepsyWarning, Is.False);
|
||||
Assert.That(decoded.SamplesMatchPlaybackRate, Is.False);
|
||||
Assert.That(decoded.Countdown, Is.EqualTo(CountdownType.None));
|
||||
Assert.That(decoded.CountdownOffset, Is.EqualTo(0));
|
||||
Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1));
|
||||
Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0));
|
||||
});
|
||||
|
||||
@@ -51,14 +51,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
var beatmap = decodeAsJson(normal);
|
||||
var beatmapInfo = beatmap.BeatmapInfo;
|
||||
Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
|
||||
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
|
||||
Assert.AreEqual(false, beatmapInfo.SpecialStyle);
|
||||
Assert.AreEqual(0, beatmap.AudioLeadIn);
|
||||
Assert.AreEqual(0.7f, beatmap.StackLeniency);
|
||||
Assert.AreEqual(false, beatmap.SpecialStyle);
|
||||
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
|
||||
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks);
|
||||
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard);
|
||||
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
|
||||
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
|
||||
Assert.AreEqual(false, beatmap.LetterboxInBreaks);
|
||||
Assert.AreEqual(false, beatmap.WidescreenStoryboard);
|
||||
Assert.AreEqual(CountdownType.None, beatmap.Countdown);
|
||||
Assert.AreEqual(0, beatmap.CountdownOffset);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -73,13 +73,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
95901, 106450, 116999, 119637, 130186, 140735, 151285,
|
||||
161834, 164471, 175020, 185570, 196119, 206669, 209306
|
||||
};
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length);
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length);
|
||||
for (int i = 0; i < expectedBookmarks.Length; i++)
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]);
|
||||
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing);
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]);
|
||||
Assert.AreEqual(1.8, beatmap.DistanceSpacing);
|
||||
Assert.AreEqual(4, beatmapInfo.BeatDivisor);
|
||||
Assert.AreEqual(4, beatmapInfo.GridSize);
|
||||
Assert.AreEqual(2, beatmapInfo.TimelineZoom);
|
||||
Assert.AreEqual(4, beatmap.GridSize);
|
||||
Assert.AreEqual(2, beatmap.TimelineZoom);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
using MemoryStream = System.IO.MemoryStream;
|
||||
@@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFractionalObjectCoordinatesRounded()
|
||||
{
|
||||
IWorkingBeatmap beatmap = null!;
|
||||
MemoryStream outStream = null!;
|
||||
|
||||
// Ensure importer encoding is correct
|
||||
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
|
||||
AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
|
||||
|
||||
// Ensure exporter legacy conversion is correct
|
||||
AddStep("export", () =>
|
||||
{
|
||||
outStream = new MemoryStream();
|
||||
|
||||
new LegacyBeatmapExporter(LocalStorage)
|
||||
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
|
||||
});
|
||||
|
||||
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
|
||||
AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExportStability()
|
||||
{
|
||||
|
||||
@@ -112,5 +112,20 @@ namespace osu.Game.Tests.Beatmaps
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRepeatsGeneratedEvenForZeroLengthSlider()
|
||||
{
|
||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, 0, 2).ToArray();
|
||||
|
||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||
|
||||
Assert.That(events[1].Type, Is.EqualTo(SliderEventType.Repeat));
|
||||
Assert.That(events[1].Time, Is.EqualTo(span_duration));
|
||||
|
||||
Assert.That(events[3].Type, Is.EqualTo(SliderEventType.Tail));
|
||||
Assert.That(events[3].Time, Is.EqualTo(span_duration * 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Higher global_level has higher priority, the default global_level
|
||||
# is 100 for root .globalconfig and 0 for others
|
||||
# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#precedence
|
||||
is_global = true
|
||||
global_level = 101
|
||||
|
||||
dotnet_diagnostic.CA2007.severity = none
|
||||
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
|
||||
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
|
||||
|
||||
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5);
|
||||
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().EditorTimestamp = 5);
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(collectionChanges, Is.EqualTo(1));
|
||||
|
||||
@@ -539,5 +539,85 @@ namespace osu.Game.Tests.Editing
|
||||
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPuttingObjectBetweenBreakEndAndAnotherObjectForcesNewCombo()
|
||||
{
|
||||
var controlPoints = new ControlPointInfo();
|
||||
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||
var beatmap = new EditorBeatmap(new Beatmap
|
||||
{
|
||||
ControlPointInfo = controlPoints,
|
||||
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
|
||||
Difficulty =
|
||||
{
|
||||
ApproachRate = 10,
|
||||
},
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||
new HitCircle { StartTime = 4500 },
|
||||
new HitCircle { StartTime = 5000, NewCombo = true },
|
||||
},
|
||||
Breaks =
|
||||
{
|
||||
new BreakPeriod(2000, 4000),
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
|
||||
beatmapProcessor.PreProcess();
|
||||
beatmapProcessor.PostProcess();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[2]).NewCombo, Is.True);
|
||||
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[2]).ComboIndex, Is.EqualTo(3));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAutomaticallyInsertedBreakForcesNewCombo()
|
||||
{
|
||||
var controlPoints = new ControlPointInfo();
|
||||
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||
var beatmap = new EditorBeatmap(new Beatmap
|
||||
{
|
||||
ControlPointInfo = controlPoints,
|
||||
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
|
||||
Difficulty =
|
||||
{
|
||||
ApproachRate = 10,
|
||||
},
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 1000, NewCombo = true },
|
||||
new HitCircle { StartTime = 5000 },
|
||||
},
|
||||
});
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
|
||||
beatmapProcessor.PreProcess();
|
||||
beatmapProcessor.PostProcess();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[1]).NewCombo, Is.True);
|
||||
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[0]).ComboIndex, Is.EqualTo(1));
|
||||
Assert.That(((HitCircle)beatmap.HitObjects[1]).ComboIndex, Is.EqualTo(2));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -68,7 +68,9 @@ namespace osu.Game.Tests.Skins
|
||||
// Covers legacy rank display
|
||||
"Archives/modified-classic-20230809.osk",
|
||||
// Covers legacy key counter
|
||||
"Archives/modified-classic-20240724.osk"
|
||||
"Archives/modified-classic-20240724.osk",
|
||||
// Covers skinnable mod display
|
||||
"Archives/modified-default-20241207.osk",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -131,21 +131,6 @@ namespace osu.Game.Tests.Visual.Background
|
||||
assertNoBackgrounds();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayedConnectivity()
|
||||
{
|
||||
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(30));
|
||||
setSeasonalBackgroundMode(SeasonalBackgroundMode.Always);
|
||||
AddStep("go offline", () => dummyAPI.SetState(APIState.Offline));
|
||||
|
||||
createLoader();
|
||||
assertNoBackgrounds();
|
||||
|
||||
AddStep("go online", () => dummyAPI.SetState(APIState.Online));
|
||||
|
||||
assertAnyBackground();
|
||||
}
|
||||
|
||||
private void registerBackgroundsResponse(DateTimeOffset endDate)
|
||||
=> AddStep("setup request handler", () =>
|
||||
{
|
||||
@@ -185,7 +170,8 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
|
||||
background = backgroundLoader.LoadNextBackground();
|
||||
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
|
||||
if (background != null)
|
||||
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
|
||||
});
|
||||
|
||||
AddUntilStep("background loaded", () => background.IsLoaded);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user