mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 13:22:55 +08:00
Merge branch 'master' into fl-slider
This commit is contained in:
commit
bb4e8add4d
@ -27,10 +27,10 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2021.725.0",
|
||||
"version": "2021.1210.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -77,10 +77,6 @@ jobs:
|
||||
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
|
||||
|
||||
build-only-ios:
|
||||
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
|
||||
# See https://github.com/ppy/osu-framework/issues/4677 for the details.
|
||||
# The job can be unblocked once those issues are resolved and game deployments can happen again.
|
||||
if: false
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
|
1
.idea/.idea.osu.iOS/.idea/.name
Normal file
1
.idea/.idea.osu.iOS/.idea/.name
Normal file
@ -0,0 +1 @@
|
||||
osu.iOS
|
8
.idea/.idea.osu.iOS/.idea/indexLayout.xml
Normal file
8
.idea/.idea.osu.iOS/.idea/indexLayout.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
6
.idea/.idea.osu.iOS/.idea/misc.xml
Normal file
6
.idea/.idea.osu.iOS/.idea/misc.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
|
||||
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
|
||||
</component>
|
||||
</project>
|
6
.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml
Normal file
6
.idea/.idea.osu.iOS/.idea/projectSettingsUpdater.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RiderProjectSettingsUpdater">
|
||||
<option name="vcsConfiguration" value="2" />
|
||||
</component>
|
||||
</project>
|
6
.idea/.idea.osu.iOS/.idea/vcs.xml
Normal file
6
.idea/.idea.osu.iOS/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -1,6 +1,6 @@
|
||||
# Contributing Guidelines
|
||||
|
||||
Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
|
||||
Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
|
||||
|
||||
These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
|
||||
|
||||
@ -32,7 +32,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
|
||||
|
||||
* **Provide more information when asked to do so.**
|
||||
|
||||
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
|
||||
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local osu! database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
|
||||
|
||||
* **When submitting a feature proposal, please describe it in the most understandable way you can.**
|
||||
|
||||
@ -54,7 +54,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
|
||||
|
||||
We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
|
||||
|
||||
However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
|
||||
However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(stable) first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
|
||||
|
||||
Here are some key things to note before jumping in:
|
||||
|
||||
@ -128,7 +128,7 @@ Here are some key things to note before jumping in:
|
||||
|
||||
* **Don't mistake criticism of code for criticism of your person.**
|
||||
|
||||
As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
|
||||
As mentioned before, we are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
|
||||
|
||||
* **Feel free to reach out for help.**
|
||||
|
||||
|
@ -8,4 +8,8 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.
|
||||
M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)
|
||||
T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.
|
||||
T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
T:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.
|
||||
M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast<T>() instead.
|
||||
M:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.
|
||||
M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
A free-to-win rhythm game. Rhythm is just a *click* away!
|
||||
|
||||
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
|
||||
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
|
||||
|
||||
## Status
|
||||
|
||||
@ -48,7 +48,7 @@ You can see some examples of custom rulesets by visiting the [custom ruleset dir
|
||||
|
||||
Please make sure you have the following prerequisites:
|
||||
|
||||
- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) or higher installed.
|
||||
- A desktop platform with the [.NET 5.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
|
||||
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
|
||||
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
|
||||
|
@ -12,7 +12,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.EmptyFreeform
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
return new DifficultyAttributes(mods, skills, 0);
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
@ -12,7 +12,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
return new DifficultyAttributes(mods, skills, 0);
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
@ -12,7 +12,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.EmptyScrolling
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
return new DifficultyAttributes(mods, skills, 0);
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
@ -12,7 +12,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Pippidon
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
return new DifficultyAttributes(mods, skills, 0);
|
||||
return new DifficultyAttributes(mods, 0);
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
|
||||
|
@ -51,11 +51,11 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1108.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1221.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
<PackageReference Include="Realm" Version="10.6.0" />
|
||||
<PackageReference Include="Realm" Version="10.7.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -108,7 +108,10 @@ namespace osu.Desktop
|
||||
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
|
||||
|
||||
// update ruleset
|
||||
presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";
|
||||
int onlineID = ruleset.Value.OnlineID;
|
||||
bool isLegacyRuleset = onlineID >= 0 && onlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
|
||||
|
||||
presence.Assets.SmallImageKey = isLegacyRuleset ? $"mode_{onlineID}" : "mode_custom";
|
||||
presence.Assets.SmallImageText = ruleset.Value.Name;
|
||||
|
||||
client.SetPresence(presence);
|
||||
|
@ -0,0 +1,18 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// A difficulty calculation request from the legacy client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synchronise any changes with osu!stable.
|
||||
/// </remarks>
|
||||
public class LegacyIpcDifficultyCalculationRequest
|
||||
{
|
||||
public string BeatmapFile { get; set; }
|
||||
public int RulesetId { get; set; }
|
||||
public int Mods { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// A difficulty calculation response returned to the legacy client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synchronise any changes with osu!stable.
|
||||
/// </remarks>
|
||||
public class LegacyIpcDifficultyCalculationResponse
|
||||
{
|
||||
public double StarRating { get; set; }
|
||||
}
|
||||
}
|
53
osu.Desktop/LegacyIpc/LegacyIpcMessage.cs
Normal file
53
osu.Desktop/LegacyIpc/LegacyIpcMessage.cs
Normal file
@ -0,0 +1,53 @@
|
||||
// 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.Platform;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IpcMessage"/> that can be used to communicate to and from legacy clients.
|
||||
/// <para>
|
||||
/// In order to deserialise types at either end, types must be serialised as their <see cref="System.Type.AssemblyQualifiedName"/>,
|
||||
/// however this cannot be done since osu!stable and osu!lazer live in two different assemblies.
|
||||
/// <br />
|
||||
/// To get around this, this class exists which serialises a payload (<see cref="LegacyIpcMessage.Data"/>) as an <see cref="System.Object"/> type,
|
||||
/// which can be deserialised at either end because it is part of the core library (mscorlib / System.Private.CorLib).
|
||||
/// The payload contains the data to be sent over the IPC channel.
|
||||
/// <br />
|
||||
/// At either end, Json.NET deserialises the payload into a <see cref="JObject"/> which is manually converted back into the expected <see cref="LegacyIpcMessage.Data"/> type,
|
||||
/// which then further contains another <see cref="JObject"/> representing the data sent over the IPC channel whose type can likewise be lazily matched through
|
||||
/// <see cref="LegacyIpcMessage.Data.MessageType"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Synchronise any changes with osu-stable.
|
||||
/// </remarks>
|
||||
public class LegacyIpcMessage : IpcMessage
|
||||
{
|
||||
public LegacyIpcMessage()
|
||||
{
|
||||
// Types/assemblies are not inter-compatible, so always serialise/deserialise into objects.
|
||||
base.Type = typeof(object).FullName;
|
||||
}
|
||||
|
||||
public new string Type => base.Type; // Hide setter.
|
||||
|
||||
public new object Value
|
||||
{
|
||||
get => base.Value;
|
||||
set => base.Value = new Data
|
||||
{
|
||||
MessageType = value.GetType().Name,
|
||||
MessageData = value
|
||||
};
|
||||
}
|
||||
|
||||
public class Data
|
||||
{
|
||||
public string MessageType { get; set; }
|
||||
public object MessageData { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
121
osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
Normal file
121
osu.Desktop/LegacyIpc/LegacyTcpIpcProvider.cs
Normal file
@ -0,0 +1,121 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Desktop.LegacyIpc
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides IPC to legacy osu! clients.
|
||||
/// </summary>
|
||||
public class LegacyTcpIpcProvider : TcpIpcProvider
|
||||
{
|
||||
private static readonly Logger logger = Logger.GetLogger("legacy-ipc");
|
||||
|
||||
public LegacyTcpIpcProvider()
|
||||
: base(45357)
|
||||
{
|
||||
MessageReceived += msg =>
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Add("Processing legacy IPC message...");
|
||||
logger.Add($" {msg.Value}", LogLevel.Debug);
|
||||
|
||||
// See explanation in LegacyIpcMessage for why this is done this way.
|
||||
var legacyData = ((JObject)msg.Value).ToObject<LegacyIpcMessage.Data>();
|
||||
object value = parseObject((JObject)legacyData!.MessageData, legacyData.MessageType);
|
||||
|
||||
return new LegacyIpcMessage
|
||||
{
|
||||
Value = onLegacyIpcMessageReceived(value)
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Add($"Processing IPC message failed: {msg.Value}", exception: ex);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private object parseObject(JObject value, string type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case nameof(LegacyIpcDifficultyCalculationRequest):
|
||||
return value.ToObject<LegacyIpcDifficultyCalculationRequest>()
|
||||
?? throw new InvalidOperationException($"Failed to parse request {value}");
|
||||
|
||||
case nameof(LegacyIpcDifficultyCalculationResponse):
|
||||
return value.ToObject<LegacyIpcDifficultyCalculationResponse>()
|
||||
?? throw new InvalidOperationException($"Failed to parse request {value}");
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported object type {type}");
|
||||
}
|
||||
}
|
||||
|
||||
private object onLegacyIpcMessageReceived(object message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case LegacyIpcDifficultyCalculationRequest req:
|
||||
try
|
||||
{
|
||||
var ruleset = getLegacyRulesetFromID(req.RulesetId);
|
||||
|
||||
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();
|
||||
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile, _ => ruleset);
|
||||
|
||||
return new LegacyIpcDifficultyCalculationResponse
|
||||
{
|
||||
StarRating = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods).StarRating
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new LegacyIpcDifficultyCalculationResponse();
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported message type {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Ruleset getLegacyRulesetFromID(int rulesetId)
|
||||
{
|
||||
switch (rulesetId)
|
||||
{
|
||||
case 0:
|
||||
return new OsuRuleset();
|
||||
|
||||
case 1:
|
||||
return new TaikoRuleset();
|
||||
|
||||
case 2:
|
||||
return new CatchRuleset();
|
||||
|
||||
case 3:
|
||||
return new ManiaRuleset();
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Invalid ruleset id");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -70,7 +70,9 @@ namespace osu.Desktop
|
||||
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
|
||||
return stableInstallPath;
|
||||
}
|
||||
catch { }
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
|
||||
@ -113,7 +115,7 @@ namespace osu.Desktop
|
||||
base.LoadComplete();
|
||||
|
||||
if (!noVersionOverlay)
|
||||
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
|
||||
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
|
||||
|
||||
LoadComponentAsync(new DiscordRichPresence(), Add);
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Desktop.LegacyIpc;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Logging;
|
||||
@ -18,8 +19,10 @@ namespace osu.Desktop
|
||||
{
|
||||
private const string base_game_name = @"osu";
|
||||
|
||||
private static LegacyTcpIpcProvider legacyIpc;
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
@ -69,14 +72,28 @@ namespace osu.Desktop
|
||||
throw new TimeoutException(@"IPC took too long to send");
|
||||
}
|
||||
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// we want to allow multiple instances to be started when in debug.
|
||||
if (!DebugUtils.IsDebugBuild)
|
||||
{
|
||||
Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error);
|
||||
return 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (host.IsPrimaryInstance)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Log("Starting legacy IPC provider...");
|
||||
legacyIpc = new LegacyTcpIpcProvider();
|
||||
legacyIpc.Bind();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Failed to start legacy IPC provider");
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,8 +101,6 @@ namespace osu.Desktop
|
||||
host.Run(new TournamentGame());
|
||||
else
|
||||
host.Run(new OsuGameDesktop(args));
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,10 @@ namespace osu.Desktop.Updater
|
||||
}
|
||||
else
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
notification.Close();
|
||||
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace osu.Desktop.Windows
|
||||
{
|
||||
private Bindable<bool> disableWinKey;
|
||||
private IBindable<bool> localUserPlaying;
|
||||
private IBindable<bool> isActive;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
@ -24,13 +25,16 @@ namespace osu.Desktop.Windows
|
||||
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
|
||||
localUserPlaying.BindValueChanged(_ => updateBlocking());
|
||||
|
||||
isActive = host.IsActive.GetBoundCopy();
|
||||
isActive.BindValueChanged(_ => updateBlocking());
|
||||
|
||||
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
|
||||
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
|
||||
}
|
||||
|
||||
private void updateBlocking()
|
||||
{
|
||||
bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
|
||||
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value;
|
||||
|
||||
if (shouldDisable)
|
||||
host.InputThread.Scheduler.Add(WindowsKey.Disable);
|
||||
|
@ -9,7 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Rulesets.Catch.Tests.iOS
|
||||
@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
UIApplication.Main(args, "GameUIApplication", "AppDelegate");
|
||||
UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,5 +32,7 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })]
|
||||
[TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })]
|
||||
[TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })]
|
||||
[TestCase("basic-hyperdash")]
|
||||
public new void Test(string name, params Type[] mods) => base.Test(name, mods);
|
||||
|
||||
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
HitObject = hitObject;
|
||||
startTime = 0;
|
||||
position = 0;
|
||||
hyperDash = false;
|
||||
}
|
||||
|
||||
private double startTime;
|
||||
@ -88,8 +90,17 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
set => position = value;
|
||||
}
|
||||
|
||||
private bool hyperDash;
|
||||
|
||||
public bool HyperDash
|
||||
{
|
||||
get => (HitObject as PalpableCatchHitObject)?.HyperDash ?? hyperDash;
|
||||
set => hyperDash = value;
|
||||
}
|
||||
|
||||
public bool Equals(ConvertValue other)
|
||||
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
|
||||
&& Precision.AlmostEquals(Position, other.Position, conversion_lenience);
|
||||
&& Precision.AlmostEquals(Position, other.Position, conversion_lenience)
|
||||
&& HyperDash == other.HyperDash;
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
checkHyperDash(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLastBananaShouldClearPlateOnMiss()
|
||||
{
|
||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||
checkPlate(1);
|
||||
AddStep("miss banana", () => attemptCatch(new Banana { X = 100 }));
|
||||
checkPlate(1);
|
||||
AddStep("miss last banana", () => attemptCatch(new Banana { LastInCombo = true, X = 100 }));
|
||||
checkPlate(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLastBananaShouldClearPlateOnCatch()
|
||||
{
|
||||
AddStep("catch fruit", () => attemptCatch(new Fruit()));
|
||||
checkPlate(1);
|
||||
AddStep("catch banana", () => attemptCatch(new Banana()));
|
||||
checkPlate(2);
|
||||
AddStep("catch last banana", () => attemptCatch(new Banana { LastInCombo = true }));
|
||||
checkPlate(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCatcherRandomStacking()
|
||||
{
|
||||
|
@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
|
||||
{
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
|
||||
var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.CreateInfo()));
|
||||
var testSkinProvider = new SkinProvidingContainer(skin);
|
||||
var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
return new CatchDifficultyAttributes { Mods = mods };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||
Skills = skills
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,11 @@
|
||||
// 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.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchPerformanceAttributes : PerformanceAttributes
|
||||
{
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
}
|
||||
|
||||
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||
public override PerformanceAttributes Calculate()
|
||||
{
|
||||
mods = Score.Mods;
|
||||
|
||||
@ -44,15 +44,11 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
|
||||
int numTotalHits = totalComboHits();
|
||||
|
||||
// Longer maps are worth more
|
||||
double lengthBonus =
|
||||
0.95 + 0.3 * Math.Min(1.0, numTotalHits / 2500.0) +
|
||||
(numTotalHits > 2500 ? Math.Log10(numTotalHits / 2500.0) * 0.475 : 0.0);
|
||||
|
||||
// Longer maps are worth more
|
||||
value *= lengthBonus;
|
||||
|
||||
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
||||
value *= Math.Pow(0.97, misses);
|
||||
|
||||
// Combo scaling
|
||||
@ -80,17 +76,17 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
}
|
||||
|
||||
if (mods.Any(m => m is ModFlashlight))
|
||||
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
|
||||
value *= 1.35 * lengthBonus;
|
||||
|
||||
// Scale the aim value with accuracy _slightly_
|
||||
value *= Math.Pow(accuracy(), 5.5);
|
||||
|
||||
// Custom multipliers for NoFail. SpunOut is not applicable.
|
||||
if (mods.Any(m => m is ModNoFail))
|
||||
value *= 0.90;
|
||||
|
||||
return value;
|
||||
return new CatchPerformanceAttributes
|
||||
{
|
||||
Total = value
|
||||
};
|
||||
}
|
||||
|
||||
private double accuracy() => totalHits() == 0 ? 0 : Math.Clamp((double)totalSuccessfulHits() / totalHits(), 0, 1);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 369,
|
||||
"Objects": [{
|
||||
"StartTime": 369,
|
||||
"Position": 0,
|
||||
"HyperDash": true
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 450,
|
||||
"Objects": [{
|
||||
"StartTime": 450,
|
||||
"Position": 512,
|
||||
"HyperDash": false
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
Mode: 2
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:6
|
||||
CircleSize:4
|
||||
OverallDifficulty:9.6
|
||||
ApproachRate:9.6
|
||||
SliderMultiplier:1.9
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
2169,266.666666666667,4,2,1,70,1,0
|
||||
|
||||
|
||||
[HitObjects]
|
||||
0,192,369,1,0,0:0:0:0:
|
||||
512,192,450,1,0,0:0:0:0:
|
@ -3,147 +3,183 @@
|
||||
"StartTime": 369,
|
||||
"Objects": [{
|
||||
"StartTime": 369,
|
||||
"Position": 177
|
||||
"Position": 177,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 450,
|
||||
"Position": 216.539276
|
||||
"Position": 216.539276,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 532,
|
||||
"Position": 256.5667
|
||||
"Position": 256.5667,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 614,
|
||||
"Position": 296.594116
|
||||
"Position": 296.594116,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 696,
|
||||
"Position": 336.621521
|
||||
"Position": 336.621521,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 778,
|
||||
"Position": 376.99762
|
||||
"Position": 376.99762,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 860,
|
||||
"Position": 337.318878
|
||||
"Position": 337.318878,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 942,
|
||||
"Position": 297.291443
|
||||
"Position": 297.291443,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1024,
|
||||
"Position": 257.264038
|
||||
"Position": 257.264038,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1106,
|
||||
"Position": 217.2366
|
||||
"Position": 217.2366,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1188,
|
||||
"Position": 177
|
||||
"Position": 177,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1270,
|
||||
"Position": 216.818192
|
||||
"Position": 216.818192,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1352,
|
||||
"Position": 256.8456
|
||||
"Position": 256.8456,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1434,
|
||||
"Position": 296.873047
|
||||
"Position": 296.873047,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1516,
|
||||
"Position": 336.900452
|
||||
"Position": 336.900452,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1598,
|
||||
"Position": 376.99762
|
||||
"Position": 376.99762,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1680,
|
||||
"Position": 337.039948
|
||||
"Position": 337.039948,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1762,
|
||||
"Position": 297.0125
|
||||
"Position": 297.0125,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1844,
|
||||
"Position": 256.9851
|
||||
"Position": 256.9851,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1926,
|
||||
"Position": 216.957672
|
||||
"Position": 216.957672,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2008,
|
||||
"Position": 177
|
||||
"Position": 177,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2090,
|
||||
"Position": 217.097137
|
||||
"Position": 217.097137,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2172,
|
||||
"Position": 257.124573
|
||||
"Position": 257.124573,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2254,
|
||||
"Position": 297.152
|
||||
"Position": 297.152,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2336,
|
||||
"Position": 337.179443
|
||||
"Position": 337.179443,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2418,
|
||||
"Position": 376.99762
|
||||
"Position": 376.99762,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2500,
|
||||
"Position": 336.760956
|
||||
"Position": 336.760956,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2582,
|
||||
"Position": 296.733643
|
||||
"Position": 296.733643,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2664,
|
||||
"Position": 256.7062
|
||||
"Position": 256.7062,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2746,
|
||||
"Position": 216.678772
|
||||
"Position": 216.678772,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2828,
|
||||
"Position": 177
|
||||
"Position": 177,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2909,
|
||||
"Position": 216.887909
|
||||
"Position": 216.887909,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2991,
|
||||
"Position": 256.915344
|
||||
"Position": 256.915344,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 3073,
|
||||
"Position": 296.942749
|
||||
"Position": 296.942749,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 3155,
|
||||
"Position": 336.970184
|
||||
"Position": 336.970184,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 3237,
|
||||
"Position": 376.99762
|
||||
"Position": 376.99762,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
@ -3,71 +3,88 @@
|
||||
"StartTime": 369,
|
||||
"Objects": [{
|
||||
"StartTime": 369,
|
||||
"Position": 65
|
||||
"Position": 65,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 450,
|
||||
"Position": 482
|
||||
"Position": 482,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 532,
|
||||
"Position": 164
|
||||
"Position": 164,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 614,
|
||||
"Position": 315
|
||||
"Position": 315,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 696,
|
||||
"Position": 145
|
||||
"Position": 145,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 778,
|
||||
"Position": 159
|
||||
"Position": 159,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 860,
|
||||
"Position": 310
|
||||
"Position": 310,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 942,
|
||||
"Position": 441
|
||||
"Position": 441,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1024,
|
||||
"Position": 428
|
||||
"Position": 428,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1106,
|
||||
"Position": 243
|
||||
"Position": 243,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1188,
|
||||
"Position": 422
|
||||
"Position": 422,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1270,
|
||||
"Position": 481
|
||||
"Position": 481,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1352,
|
||||
"Position": 104
|
||||
"Position": 104,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1434,
|
||||
"Position": 473
|
||||
"Position": 473,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1516,
|
||||
"Position": 135
|
||||
"Position": 135,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1598,
|
||||
"Position": 360
|
||||
"Position": 360,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 1680,
|
||||
"Position": 123
|
||||
"Position": 123,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
@ -3,231 +3,264 @@
|
||||
"StartTime": 369,
|
||||
"Objects": [{
|
||||
"StartTime": 369,
|
||||
"Position": 258
|
||||
"Position": 258,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 450,
|
||||
"Objects": [{
|
||||
"StartTime": 450,
|
||||
"Position": 254
|
||||
"Position": 254,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 532,
|
||||
"Objects": [{
|
||||
"StartTime": 532,
|
||||
"Position": 241
|
||||
"Position": 241,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 614,
|
||||
"Objects": [{
|
||||
"StartTime": 614,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 696,
|
||||
"Objects": [{
|
||||
"StartTime": 696,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 778,
|
||||
"Objects": [{
|
||||
"StartTime": 778,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 860,
|
||||
"Objects": [{
|
||||
"StartTime": 860,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 942,
|
||||
"Objects": [{
|
||||
"StartTime": 942,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1024,
|
||||
"Objects": [{
|
||||
"StartTime": 1024,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1106,
|
||||
"Objects": [{
|
||||
"StartTime": 1106,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1188,
|
||||
"Objects": [{
|
||||
"StartTime": 1188,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1270,
|
||||
"Objects": [{
|
||||
"StartTime": 1270,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1352,
|
||||
"Objects": [{
|
||||
"StartTime": 1352,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1434,
|
||||
"Objects": [{
|
||||
"StartTime": 1434,
|
||||
"Position": 258
|
||||
"Position": 258,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1516,
|
||||
"Objects": [{
|
||||
"StartTime": 1516,
|
||||
"Position": 253
|
||||
"Position": 253,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1598,
|
||||
"Objects": [{
|
||||
"StartTime": 1598,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1680,
|
||||
"Objects": [{
|
||||
"StartTime": 1680,
|
||||
"Position": 260
|
||||
"Position": 260,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1762,
|
||||
"Objects": [{
|
||||
"StartTime": 1762,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1844,
|
||||
"Objects": [{
|
||||
"StartTime": 1844,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 1926,
|
||||
"Objects": [{
|
||||
"StartTime": 1926,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2008,
|
||||
"Objects": [{
|
||||
"StartTime": 2008,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2090,
|
||||
"Objects": [{
|
||||
"StartTime": 2090,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2172,
|
||||
"Objects": [{
|
||||
"StartTime": 2172,
|
||||
"Position": 243
|
||||
"Position": 243,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2254,
|
||||
"Objects": [{
|
||||
"StartTime": 2254,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2336,
|
||||
"Objects": [{
|
||||
"StartTime": 2336,
|
||||
"Position": 278
|
||||
"Position": 278,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2418,
|
||||
"Objects": [{
|
||||
"StartTime": 2418,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2500,
|
||||
"Objects": [{
|
||||
"StartTime": 2500,
|
||||
"Position": 258
|
||||
"Position": 258,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2582,
|
||||
"Objects": [{
|
||||
"StartTime": 2582,
|
||||
"Position": 256
|
||||
"Position": 256,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2664,
|
||||
"Objects": [{
|
||||
"StartTime": 2664,
|
||||
"Position": 242
|
||||
"Position": 242,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2746,
|
||||
"Objects": [{
|
||||
"StartTime": 2746,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2828,
|
||||
"Objects": [{
|
||||
"StartTime": 2828,
|
||||
"Position": 238
|
||||
"Position": 238,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2909,
|
||||
"Objects": [{
|
||||
"StartTime": 2909,
|
||||
"Position": 271
|
||||
"Position": 271,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2991,
|
||||
"Objects": [{
|
||||
"StartTime": 2991,
|
||||
"Position": 254
|
||||
"Position": 254,
|
||||
"HyperDash": false
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
@ -3,14 +3,16 @@
|
||||
"StartTime": 3368,
|
||||
"Objects": [{
|
||||
"StartTime": 3368,
|
||||
"Position": 374
|
||||
"Position": 374,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 3501,
|
||||
"Objects": [{
|
||||
"StartTime": 3501,
|
||||
"Position": 446
|
||||
"Position": 446,
|
||||
"HyperDash": false
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
@ -1 +1,71 @@
|
||||
{"Mappings":[{"StartTime":19184.0,"Objects":[{"StartTime":19184.0,"Position":320.0},{"StartTime":19263.0,"Position":311.730255},{"StartTime":19343.0,"Position":324.6205},{"StartTime":19423.0,"Position":343.0907},{"StartTime":19503.0,"Position":372.2917},{"StartTime":19582.0,"Position":385.194733},{"StartTime":19662.0,"Position":379.0426},{"StartTime":19742.0,"Position":385.1066},{"StartTime":19822.0,"Position":391.624664},{"StartTime":19919.0,"Position":386.27832},{"StartTime":20016.0,"Position":380.117035},{"StartTime":20113.0,"Position":381.664154},{"StartTime":20247.0,"Position":370.872864}]}]}
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 19184,
|
||||
"Objects": [{
|
||||
"StartTime": 19184,
|
||||
"Position": 320,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19263,
|
||||
"Position": 311.730255,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19343,
|
||||
"Position": 324.6205,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19423,
|
||||
"Position": 343.0907,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19503,
|
||||
"Position": 372.2917,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19582,
|
||||
"Position": 385.194733,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19662,
|
||||
"Position": 379.0426,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19742,
|
||||
"Position": 385.1066,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19822,
|
||||
"Position": 391.624664,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19919,
|
||||
"Position": 386.27832,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 20016,
|
||||
"Position": 380.117035,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 20113,
|
||||
"Position": 381.664154,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 20247,
|
||||
"Position": 370.872864,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
@ -3,18 +3,21 @@
|
||||
"StartTime": 2589,
|
||||
"Objects": [{
|
||||
"StartTime": 2589,
|
||||
"Position": 256
|
||||
"Position": 256,
|
||||
"HyperDash": false
|
||||
}]
|
||||
},
|
||||
{
|
||||
"StartTime": 2915,
|
||||
"Objects": [{
|
||||
"StartTime": 2915,
|
||||
"Position": 65
|
||||
"Position": 65,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 2916,
|
||||
"Position": 482
|
||||
"Position": 482,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -22,11 +25,13 @@
|
||||
"StartTime": 3078,
|
||||
"Objects": [{
|
||||
"StartTime": 3078,
|
||||
"Position": 164
|
||||
"Position": 164,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 3079,
|
||||
"Position": 315
|
||||
"Position": 315,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -34,11 +39,13 @@
|
||||
"StartTime": 3241,
|
||||
"Objects": [{
|
||||
"StartTime": 3241,
|
||||
"Position": 145
|
||||
"Position": 145,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 3242,
|
||||
"Position": 159
|
||||
"Position": 159,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -46,11 +53,13 @@
|
||||
"StartTime": 3404,
|
||||
"Objects": [{
|
||||
"StartTime": 3404,
|
||||
"Position": 310
|
||||
"Position": 310,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 3405,
|
||||
"Position": 441
|
||||
"Position": 441,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -58,7 +67,8 @@
|
||||
"StartTime": 5197,
|
||||
"Objects": [{
|
||||
"StartTime": 5197,
|
||||
"Position": 256
|
||||
"Position": 256,
|
||||
"HyperDash": false
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
@ -3,71 +3,88 @@
|
||||
"StartTime": 18500,
|
||||
"Objects": [{
|
||||
"StartTime": 18500,
|
||||
"Position": 65
|
||||
"Position": 65,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18559,
|
||||
"Position": 482
|
||||
"Position": 482,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18618,
|
||||
"Position": 164
|
||||
"Position": 164,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18678,
|
||||
"Position": 315
|
||||
"Position": 315,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18737,
|
||||
"Position": 145
|
||||
"Position": 145,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18796,
|
||||
"Position": 159
|
||||
"Position": 159,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18856,
|
||||
"Position": 310
|
||||
"Position": 310,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18915,
|
||||
"Position": 441
|
||||
"Position": 441,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 18975,
|
||||
"Position": 428
|
||||
"Position": 428,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19034,
|
||||
"Position": 243
|
||||
"Position": 243,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19093,
|
||||
"Position": 422
|
||||
"Position": 422,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19153,
|
||||
"Position": 481
|
||||
"Position": 481,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19212,
|
||||
"Position": 104
|
||||
"Position": 104,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19271,
|
||||
"Position": 473
|
||||
"Position": 473,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19331,
|
||||
"Position": 135
|
||||
"Position": 135,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19390,
|
||||
"Position": 360
|
||||
"Position": 360,
|
||||
"HyperDash": false
|
||||
},
|
||||
{
|
||||
"StartTime": 19450,
|
||||
"Position": 123
|
||||
"Position": 123,
|
||||
"HyperDash": false
|
||||
}
|
||||
]
|
||||
}]
|
||||
|
@ -210,6 +210,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
catchResult.CatcherAnimationState = CurrentState;
|
||||
catchResult.CatcherHyperDash = HyperDashing;
|
||||
|
||||
// Ignore JuiceStreams and BananaShowers
|
||||
if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return;
|
||||
|
||||
var hitObject = palpableObject.HitObject;
|
||||
@ -244,6 +245,14 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
|
||||
else if (!(hitObject is Banana))
|
||||
CurrentState = CatcherAnimationState.Fail;
|
||||
|
||||
if (palpableObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Explode();
|
||||
else
|
||||
Drop();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result)
|
||||
|
@ -6,11 +6,9 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
@ -72,18 +70,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result)
|
||||
{
|
||||
Catcher.OnNewResult(hitObject, result);
|
||||
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
if (hitObject.HitObject.LastInCombo)
|
||||
{
|
||||
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
|
||||
Catcher.Explode();
|
||||
else
|
||||
Catcher.Drop();
|
||||
}
|
||||
|
||||
comboDisplay.OnNewResult(hitObject, result);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Rulesets.Mania.Tests.iOS
|
||||
@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
UIApplication.Main(args, "GameUIApplication", "AppDelegate");
|
||||
UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,5 +32,7 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -55,13 +56,13 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestDefaultSkin()
|
||||
{
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacySkin()
|
||||
{
|
||||
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
|
||||
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
yield return v;
|
||||
|
||||
// Todo: osu!mania doesn't output MaxCombo attribute for some reason.
|
||||
yield return (ATTRIB_ID_STRAIN, StarRating);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
|
||||
}
|
||||
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
|
||||
StarRating = values[ATTRIB_ID_STRAIN];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
return new ManiaDifficultyAttributes { Mods = mods };
|
||||
|
||||
HitWindows hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate),
|
||||
ScoreMultiplier = getScoreMultiplier(mods),
|
||||
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
|
||||
Skills = skills
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaPerformanceAttributes : PerformanceAttributes
|
||||
{
|
||||
[JsonProperty("difficulty")]
|
||||
public double Difficulty { get; set; }
|
||||
|
||||
[JsonProperty("accuracy")]
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
[JsonProperty("scaled_score")]
|
||||
public double ScaledScore { get; set; }
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
}
|
||||
|
||||
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||
public override PerformanceAttributes Calculate()
|
||||
{
|
||||
mods = Score.Mods;
|
||||
scaledScore = Score.TotalScore;
|
||||
@ -61,48 +61,46 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
if (mods.Any(m => m is ModEasy))
|
||||
multiplier *= 0.5;
|
||||
|
||||
double strainValue = computeStrainValue();
|
||||
double accValue = computeAccuracyValue(strainValue);
|
||||
double difficultyValue = computeDifficultyValue();
|
||||
double accValue = computeAccuracyValue(difficultyValue);
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(strainValue, 1.1) +
|
||||
Math.Pow(difficultyValue, 1.1) +
|
||||
Math.Pow(accValue, 1.1), 1.0 / 1.1
|
||||
) * multiplier;
|
||||
|
||||
if (categoryDifficulty != null)
|
||||
return new ManiaPerformanceAttributes
|
||||
{
|
||||
categoryDifficulty["Strain"] = strainValue;
|
||||
categoryDifficulty["Accuracy"] = accValue;
|
||||
}
|
||||
|
||||
return totalValue;
|
||||
Difficulty = difficultyValue,
|
||||
Accuracy = accValue,
|
||||
ScaledScore = scaledScore,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeStrainValue()
|
||||
private double computeDifficultyValue()
|
||||
{
|
||||
// Obtain strain difficulty
|
||||
double strainValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
|
||||
|
||||
// Longer maps are worth more
|
||||
strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
|
||||
if (scaledScore <= 500000)
|
||||
strainValue = 0;
|
||||
difficultyValue = 0;
|
||||
else if (scaledScore <= 600000)
|
||||
strainValue *= (scaledScore - 500000) / 100000 * 0.3;
|
||||
difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;
|
||||
else if (scaledScore <= 700000)
|
||||
strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
|
||||
difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
|
||||
else if (scaledScore <= 800000)
|
||||
strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
|
||||
difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
|
||||
else if (scaledScore <= 900000)
|
||||
strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
|
||||
difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
|
||||
else
|
||||
strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
|
||||
difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
|
||||
|
||||
return strainValue;
|
||||
return difficultyValue;
|
||||
}
|
||||
|
||||
private double computeAccuracyValue(double strainValue)
|
||||
private double computeAccuracyValue(double difficultyValue)
|
||||
{
|
||||
if (Attributes.GreatHitWindow <= 0)
|
||||
return 0;
|
||||
@ -110,12 +108,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
||||
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667)
|
||||
* strainValue
|
||||
* difficultyValue
|
||||
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
|
||||
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
||||
// accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
||||
|
||||
return accuracyValue;
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@ -46,12 +45,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<WorkingBeatmap> working { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
|
@ -7,16 +7,12 @@ using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class ManiaSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
|
@ -15,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public JudgementResult Result { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; }
|
||||
|
||||
private SkinnableDrawable skinnableExplosion;
|
||||
|
||||
public PoolableHitExplosion()
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Rulesets.Osu.Tests.iOS
|
||||
@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
UIApplication.Main(args, "GameUIApplication", "AppDelegate");
|
||||
UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,5 +32,7 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
AddStep("create slider", () =>
|
||||
{
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
|
||||
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
|
||||
|
||||
Child = new SkinProvidingContainer(tintingSkin)
|
||||
|
@ -5,7 +5,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
[JsonProperty("aim_strain")]
|
||||
public double AimStrain { get; set; }
|
||||
[JsonProperty("aim_difficulty")]
|
||||
public double AimDifficulty { get; set; }
|
||||
|
||||
[JsonProperty("speed_strain")]
|
||||
public double SpeedStrain { get; set; }
|
||||
[JsonProperty("speed_difficulty")]
|
||||
public double SpeedDifficulty { get; set; }
|
||||
|
||||
[JsonProperty("flashlight_rating")]
|
||||
public double FlashlightRating { get; set; }
|
||||
[JsonProperty("flashlight_difficulty")]
|
||||
public double FlashlightDifficulty { get; set; }
|
||||
|
||||
[JsonProperty("slider_factor")]
|
||||
public double SliderFactor { get; set; }
|
||||
@ -43,15 +43,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_AIM, AimStrain);
|
||||
yield return (ATTRIB_ID_SPEED, SpeedStrain);
|
||||
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_STRAIN, StarRating);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightRating())
|
||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightRating);
|
||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||
|
||||
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
|
||||
}
|
||||
@ -60,13 +60,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values);
|
||||
|
||||
AimStrain = values[ATTRIB_ID_AIM];
|
||||
SpeedStrain = values[ATTRIB_ID_SPEED];
|
||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_STRAIN];
|
||||
FlashlightRating = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new OsuDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
return new OsuDifficultyAttributes { Mods = mods };
|
||||
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
@ -74,9 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
AimStrain = aimRating,
|
||||
SpeedStrain = speedRating,
|
||||
FlashlightRating = flashlightRating,
|
||||
AimDifficulty = aimRating,
|
||||
SpeedDifficulty = speedRating,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
@ -85,7 +85,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
HitCircleCount = hitCirclesCount,
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
Skills = skills
|
||||
};
|
||||
}
|
||||
|
||||
|
26
osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
Normal file
26
osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
public class OsuPerformanceAttributes : PerformanceAttributes
|
||||
{
|
||||
[JsonProperty("aim")]
|
||||
public double Aim { get; set; }
|
||||
|
||||
[JsonProperty("speed")]
|
||||
public double Speed { get; set; }
|
||||
|
||||
[JsonProperty("accuracy")]
|
||||
public double Accuracy { get; set; }
|
||||
|
||||
[JsonProperty("flashlight")]
|
||||
public double Flashlight { get; set; }
|
||||
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
}
|
||||
|
||||
public override double Calculate(Dictionary<string, double> categoryRatings = null)
|
||||
public override PerformanceAttributes Calculate()
|
||||
{
|
||||
mods = Score.Mods;
|
||||
accuracy = Score.Accuracy;
|
||||
@ -45,7 +45,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
if (mods.Any(m => m is OsuModNoFail))
|
||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
|
||||
|
||||
@ -72,42 +71,35 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
|
||||
) * multiplier;
|
||||
|
||||
if (categoryRatings != null)
|
||||
return new OsuPerformanceAttributes
|
||||
{
|
||||
categoryRatings.Add("Aim", aimValue);
|
||||
categoryRatings.Add("Speed", speedValue);
|
||||
categoryRatings.Add("Accuracy", accuracyValue);
|
||||
categoryRatings.Add("Flashlight", flashlightValue);
|
||||
categoryRatings.Add("OD", Attributes.OverallDifficulty);
|
||||
categoryRatings.Add("AR", Attributes.ApproachRate);
|
||||
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
|
||||
}
|
||||
|
||||
return totalValue;
|
||||
Aim = aimValue,
|
||||
Speed = speedValue,
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeAimValue()
|
||||
{
|
||||
double rawAim = Attributes.AimStrain;
|
||||
double rawAim = Attributes.AimDifficulty;
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
rawAim = Math.Pow(rawAim, 0.8);
|
||||
|
||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
// Longer maps are worth more.
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
|
||||
aimValue *= lengthBonus;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (effectiveMissCount > 0)
|
||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||
aimValue *= getComboScalingFactor();
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (Attributes.ApproachRate > 10.33)
|
||||
@ -136,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
// It is important to also consider accuracy difficulty when doing that.
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
|
||||
|
||||
return aimValue;
|
||||
@ -144,9 +136,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeSpeedValue()
|
||||
{
|
||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
// Longer maps are worth more.
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
speedValue *= lengthBonus;
|
||||
@ -155,9 +146,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (effectiveMissCount > 0)
|
||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||
speedValue *= getComboScalingFactor();
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (Attributes.ApproachRate > 10.33)
|
||||
@ -227,14 +216,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (!mods.Any(h => h is OsuModFlashlight))
|
||||
return 0.0;
|
||||
|
||||
double rawFlashlight = Attributes.FlashlightRating;
|
||||
double rawFlashlight = Attributes.FlashlightDifficulty;
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
|
||||
|
||||
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
|
||||
|
||||
// Add an additional bonus for HDFL.
|
||||
if (mods.Any(h => h is OsuModHidden))
|
||||
flashlightValue *= 1.3;
|
||||
|
||||
@ -242,9 +230,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (effectiveMissCount > 0)
|
||||
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
|
||||
|
||||
// Combo scaling.
|
||||
if (Attributes.MaxCombo > 0)
|
||||
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||
flashlightValue *= getComboScalingFactor();
|
||||
|
||||
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
|
||||
@ -276,6 +262,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
|
||||
}
|
||||
|
||||
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
}
|
||||
|
@ -11,49 +11,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
{
|
||||
public class OsuDifficultyHitObject : DifficultyHitObject
|
||||
{
|
||||
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
|
||||
private const int min_delta_time = 25;
|
||||
private const float maximum_slider_radius = normalized_radius * 2.4f;
|
||||
private const float assumed_slider_radius = normalized_radius * 1.8f;
|
||||
private const float maximum_slider_radius = normalised_radius * 2.4f;
|
||||
private const float assumed_slider_radius = normalised_radius * 1.8f;
|
||||
|
||||
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public double JumpDistance { get; private set; }
|
||||
public readonly double StrainTime;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// <para>
|
||||
/// The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public double MovementDistance { get; private set; }
|
||||
public double LazyJumpDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
|
||||
/// Normalised shortest distance to consider for a jump between the previous <see cref="OsuDifficultyHitObject"/> and this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is bounded from above by <see cref="LazyJumpDistance"/>, and is smaller than the former if a more natural path is able to be taken through the previous <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// Suppose a linear slider - circle pattern.
|
||||
/// <br />
|
||||
/// Following the slider lazily (see: <see cref="LazyJumpDistance"/>) will result in underestimating the true end position of the slider as being closer towards the start position.
|
||||
/// As a result, <see cref="LazyJumpDistance"/> overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end,
|
||||
/// such that the jump is felt as only starting from the slider's true end position.
|
||||
/// <br />
|
||||
/// Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
|
||||
/// In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
|
||||
/// </example>
|
||||
public double MinimumJumpDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time taken to travel through <see cref="MinimumJumpDistance"/>, with a minimum value of 25ms.
|
||||
/// </summary>
|
||||
public double MinimumJumpTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalised distance between the start and end position of this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public double TravelDistance { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
|
||||
/// </summary>
|
||||
public double TravelTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
|
||||
/// Calculated as the angle between the circles (current-2, current-1, current).
|
||||
/// </summary>
|
||||
public double? Angle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public double MovementTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public double TravelTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
|
||||
/// </summary>
|
||||
public readonly double StrainTime;
|
||||
|
||||
private readonly OsuHitObject lastLastObject;
|
||||
private readonly OsuHitObject lastObject;
|
||||
|
||||
@ -71,12 +87,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
private void setDistances(double clockRate)
|
||||
{
|
||||
if (BaseObject is Slider currentSlider)
|
||||
{
|
||||
computeSliderCursorPosition(currentSlider);
|
||||
TravelDistance = currentSlider.LazyTravelDistance;
|
||||
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
|
||||
}
|
||||
|
||||
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
|
||||
if (BaseObject is Spinner || lastObject is Spinner)
|
||||
return;
|
||||
|
||||
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
|
||||
float scalingFactor = normalised_radius / (float)BaseObject.Radius;
|
||||
|
||||
if (BaseObject.Radius < 30)
|
||||
{
|
||||
@ -85,29 +108,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
}
|
||||
|
||||
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
|
||||
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||
|
||||
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
|
||||
MinimumJumpTime = StrainTime;
|
||||
MinimumJumpDistance = LazyJumpDistance;
|
||||
|
||||
if (lastObject is Slider lastSlider)
|
||||
{
|
||||
computeSliderCursorPosition(lastSlider);
|
||||
TravelDistance = lastSlider.LazyTravelDistance;
|
||||
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
|
||||
MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
|
||||
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
|
||||
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
|
||||
|
||||
//
|
||||
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
|
||||
//
|
||||
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
|
||||
//
|
||||
// <======o==> ← slider
|
||||
// | ← most natural jump path
|
||||
// o ← a follow-up hitcircle
|
||||
//
|
||||
// In this case the most natural jump path is approximated by LazyJumpDistance.
|
||||
//
|
||||
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
|
||||
//
|
||||
// <======o==>---o
|
||||
// ↑
|
||||
// most natural jump path
|
||||
//
|
||||
// In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
|
||||
//
|
||||
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
|
||||
//
|
||||
|
||||
// Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
|
||||
float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
|
||||
|
||||
// For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
|
||||
// such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
|
||||
// In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
|
||||
// Additional distance is removed based on position of jump relative to slider follow circle radius.
|
||||
// JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible.
|
||||
MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
|
||||
}
|
||||
else
|
||||
{
|
||||
MovementTime = StrainTime;
|
||||
MovementDistance = JumpDistance;
|
||||
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
|
||||
}
|
||||
|
||||
if (lastLastObject != null && !(lastLastObject is Spinner))
|
||||
@ -139,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
|
||||
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
|
||||
var currCursorPosition = slider.StackedPosition;
|
||||
double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
|
||||
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
|
||||
|
||||
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
|
||||
{
|
||||
@ -167,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
||||
else if (currMovementObj is SliderRepeat)
|
||||
{
|
||||
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
|
||||
requiredMovement = normalized_radius;
|
||||
requiredMovement = normalised_radius;
|
||||
}
|
||||
|
||||
if (currMovementLength > requiredMovement)
|
||||
|
@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
|
||||
|
||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
|
||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||
|
||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||
if (osuLastObj.BaseObject is Slider && withSliders)
|
||||
{
|
||||
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
|
||||
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
|
||||
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
|
||||
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
|
||||
|
||||
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
|
||||
}
|
||||
|
||||
// As above, do the same for the previous hitobject.
|
||||
double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
|
||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
||||
|
||||
if (osuLastLastObj.BaseObject is Slider && withSliders)
|
||||
{
|
||||
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
|
||||
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
|
||||
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
|
||||
|
||||
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
|
||||
}
|
||||
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||
}
|
||||
|
||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
||||
@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||
{
|
||||
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
||||
prevVelocity = (osuLastObj.JumpDistance + osuLastObj.TravelDistance) / osuLastObj.StrainTime;
|
||||
currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime;
|
||||
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
|
||||
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
|
||||
|
||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
|
||||
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
// Reward for % distance slowed down compared to previous, paying attention to not award overlap
|
||||
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
|
||||
// do not award overlap
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.JumpDistance, osuLastObj.JumpDistance) / 100)), 2);
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
|
||||
|
||||
// Choose the largest bonus, multiplied by ratio.
|
||||
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
|
||||
@ -128,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
|
||||
}
|
||||
|
||||
if (osuCurrObj.TravelTime != 0)
|
||||
if (osuLastObj.TravelTime != 0)
|
||||
{
|
||||
// Reward sliders based on velocity.
|
||||
sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
|
||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
}
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
}
|
||||
|
||||
private double skillMultiplier => 0.15;
|
||||
private double skillMultiplier => 0.07;
|
||||
private double strainDecayBase => 0.15;
|
||||
protected override double DecayWeight => 1.0;
|
||||
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
|
||||
@ -43,26 +43,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
double result = 0.0;
|
||||
|
||||
OsuDifficultyHitObject lastObj = osuCurrent;
|
||||
|
||||
// This is iterating backwards in time from the current object.
|
||||
for (int i = 0; i < Previous.Count; i++)
|
||||
{
|
||||
var osuPrevious = (OsuDifficultyHitObject)Previous[i];
|
||||
var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject);
|
||||
var currentObj = (OsuDifficultyHitObject)Previous[i];
|
||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||
|
||||
if (!(osuPrevious.BaseObject is Spinner))
|
||||
if (!(currentObj.BaseObject is Spinner))
|
||||
{
|
||||
double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length;
|
||||
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
|
||||
|
||||
cumulativeStrainTime += osuPrevious.StrainTime;
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||
if (i == 0)
|
||||
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
||||
|
||||
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
||||
double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0);
|
||||
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
||||
|
||||
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
result += stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||
}
|
||||
|
||||
lastObj = currentObj;
|
||||
}
|
||||
|
||||
result = Math.Pow(smallDistNerf * result, 2.0);
|
||||
|
@ -55,73 +55,75 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
|
||||
bool firstDeltaSwitch = false;
|
||||
|
||||
for (int i = Previous.Count - 2; i > 0; i--)
|
||||
int rhythmStart = 0;
|
||||
|
||||
while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max)
|
||||
rhythmStart++;
|
||||
|
||||
for (int i = rhythmStart; i > 0; i--)
|
||||
{
|
||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
|
||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
|
||||
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
|
||||
|
||||
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
|
||||
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
||||
|
||||
if (currHistoricalDecay != 0)
|
||||
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
|
||||
|
||||
double currDelta = currObj.StrainTime;
|
||||
double prevDelta = prevObj.StrainTime;
|
||||
double lastDelta = lastObj.StrainTime;
|
||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
||||
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
||||
|
||||
windowPenalty = Math.Min(1, windowPenalty);
|
||||
|
||||
double effectiveRatio = windowPenalty * currRatio;
|
||||
|
||||
if (firstDeltaSwitch)
|
||||
{
|
||||
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
|
||||
|
||||
double currDelta = currObj.StrainTime;
|
||||
double prevDelta = prevObj.StrainTime;
|
||||
double lastDelta = lastObj.StrainTime;
|
||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
||||
|
||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
||||
|
||||
windowPenalty = Math.Min(1, windowPenalty);
|
||||
|
||||
double effectiveRatio = windowPenalty * currRatio;
|
||||
|
||||
if (firstDeltaSwitch)
|
||||
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
||||
{
|
||||
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
||||
{
|
||||
if (islandSize < 7)
|
||||
islandSize++; // island is still progressing, count size.
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||
effectiveRatio *= 0.25;
|
||||
|
||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||
effectiveRatio *= 0.25;
|
||||
|
||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
||||
effectiveRatio *= 0.50;
|
||||
|
||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
||||
|
||||
startRatio = effectiveRatio;
|
||||
|
||||
previousIslandSize = islandSize; // log the last island size.
|
||||
|
||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||
|
||||
islandSize = 1;
|
||||
}
|
||||
if (islandSize < 7)
|
||||
islandSize++; // island is still progressing, count size.
|
||||
}
|
||||
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
||||
else
|
||||
{
|
||||
// Begin counting island until we change speed again.
|
||||
firstDeltaSwitch = true;
|
||||
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||
effectiveRatio *= 0.25;
|
||||
|
||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||
effectiveRatio *= 0.25;
|
||||
|
||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
||||
effectiveRatio *= 0.50;
|
||||
|
||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||
effectiveRatio *= 0.125;
|
||||
|
||||
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
||||
|
||||
startRatio = effectiveRatio;
|
||||
|
||||
previousIslandSize = islandSize; // log the last island size.
|
||||
|
||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||
|
||||
islandSize = 1;
|
||||
}
|
||||
}
|
||||
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
||||
{
|
||||
// Begin counting island until we change speed again.
|
||||
firstDeltaSwitch = true;
|
||||
startRatio = effectiveRatio;
|
||||
islandSize = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||
@ -154,7 +156,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
if (strainTime < min_speed_bonus)
|
||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||
|
||||
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
|
||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
|
||||
|
||||
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// 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;
|
||||
@ -26,12 +26,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public BindableFloat Scale { get; } = new BindableFloat(4)
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 2,
|
||||
MinValue = 1.5f,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
[SettingSource("Style", "Change the animation style of the approach circles.", 1)]
|
||||
public Bindable<AnimationStyle> Style { get; } = new Bindable<AnimationStyle>();
|
||||
public Bindable<AnimationStyle> Style { get; } = new Bindable<AnimationStyle>(AnimationStyle.Gravity);
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
@ -52,9 +52,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
switch (style)
|
||||
{
|
||||
default:
|
||||
case AnimationStyle.Linear:
|
||||
return Easing.None;
|
||||
|
||||
case AnimationStyle.Gravity:
|
||||
return Easing.InBack;
|
||||
|
||||
case AnimationStyle.InOut1:
|
||||
return Easing.InOutCubic;
|
||||
|
||||
case AnimationStyle.InOut2:
|
||||
return Easing.InOutQuint;
|
||||
|
||||
case AnimationStyle.Accelerate1:
|
||||
return Easing.In;
|
||||
|
||||
@ -64,9 +73,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case AnimationStyle.Accelerate3:
|
||||
return Easing.InQuint;
|
||||
|
||||
case AnimationStyle.Gravity:
|
||||
return Easing.InBack;
|
||||
|
||||
case AnimationStyle.Decelerate1:
|
||||
return Easing.Out;
|
||||
|
||||
@ -76,16 +82,14 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
case AnimationStyle.Decelerate3:
|
||||
return Easing.OutQuint;
|
||||
|
||||
case AnimationStyle.InOut1:
|
||||
return Easing.InOutCubic;
|
||||
|
||||
case AnimationStyle.InOut2:
|
||||
return Easing.InOutQuint;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(style), style, @"Unsupported animation style");
|
||||
}
|
||||
}
|
||||
|
||||
public enum AnimationStyle
|
||||
{
|
||||
Linear,
|
||||
Gravity,
|
||||
InOut1,
|
||||
InOut2,
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges.Events;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
@ -39,6 +41,19 @@ namespace osu.Game.Rulesets.Osu
|
||||
return base.Handle(e);
|
||||
}
|
||||
|
||||
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
|
||||
{
|
||||
if (!AllowUserCursorMovement)
|
||||
{
|
||||
// Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
|
||||
// Primarily relied upon by the "autopilot" osu! mod.
|
||||
var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
|
||||
e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
|
||||
}
|
||||
|
||||
return base.HandleMouseTouchStateChange(e);
|
||||
}
|
||||
|
||||
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
|
||||
{
|
||||
public bool AllowUserPresses = true;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Rulesets.Taiko.Tests.iOS
|
||||
@ -9,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
UIApplication.Main(args, "GameUIApplication", "AppDelegate");
|
||||
UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,5 +32,7 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
|
@ -9,14 +9,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
[JsonProperty("stamina_strain")]
|
||||
public double StaminaStrain { get; set; }
|
||||
[JsonProperty("stamina_difficulty")]
|
||||
public double StaminaDifficulty { get; set; }
|
||||
|
||||
[JsonProperty("rhythm_strain")]
|
||||
public double RhythmStrain { get; set; }
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
|
||||
[JsonProperty("colour_strain")]
|
||||
public double ColourStrain { get; set; }
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_STRAIN, StarRating);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
|
||||
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
base.FromDatabaseAttributes(values);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_STRAIN];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
{
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
|
||||
return new TaikoDifficultyAttributes { Mods = mods };
|
||||
|
||||
var colour = (Colour)skills[0];
|
||||
var rhythm = (Rhythm)skills[1];
|
||||
@ -91,12 +91,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
StaminaStrain = staminaRating,
|
||||
RhythmStrain = rhythmRating,
|
||||
ColourStrain = colourRating,
|
||||
StaminaDifficulty = staminaRating,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ColourDifficulty = colourRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||
Skills = skills
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoPerformanceAttributes : PerformanceAttributes
|
||||
{
|
||||
[JsonProperty("difficulty")]
|
||||
public double Difficulty { get; set; }
|
||||
|
||||
[JsonProperty("accuracy")]
|
||||
public double Accuracy { get; set; }
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
}
|
||||
|
||||
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||
public override PerformanceAttributes Calculate()
|
||||
{
|
||||
mods = Score.Mods;
|
||||
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
|
||||
@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
|
||||
// Custom multipliers for NoFail and SpunOut.
|
||||
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
||||
|
||||
if (mods.Any(m => m is ModNoFail))
|
||||
@ -44,43 +43,38 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (mods.Any(m => m is ModHidden))
|
||||
multiplier *= 1.10;
|
||||
|
||||
double strainValue = computeStrainValue();
|
||||
double difficultyValue = computeDifficultyValue();
|
||||
double accuracyValue = computeAccuracyValue();
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(strainValue, 1.1) +
|
||||
Math.Pow(difficultyValue, 1.1) +
|
||||
Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
|
||||
) * multiplier;
|
||||
|
||||
if (categoryDifficulty != null)
|
||||
return new TaikoPerformanceAttributes
|
||||
{
|
||||
categoryDifficulty["Strain"] = strainValue;
|
||||
categoryDifficulty["Accuracy"] = accuracyValue;
|
||||
}
|
||||
|
||||
return totalValue;
|
||||
Difficulty = difficultyValue,
|
||||
Accuracy = accuracyValue,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeStrainValue()
|
||||
private double computeDifficultyValue()
|
||||
{
|
||||
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
|
||||
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
|
||||
|
||||
// Longer maps are worth more
|
||||
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
strainValue *= lengthBonus;
|
||||
difficultyValue *= lengthBonus;
|
||||
|
||||
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
||||
strainValue *= Math.Pow(0.985, countMiss);
|
||||
difficultyValue *= Math.Pow(0.985, countMiss);
|
||||
|
||||
if (mods.Any(m => m is ModHidden))
|
||||
strainValue *= 1.025;
|
||||
difficultyValue *= 1.025;
|
||||
|
||||
if (mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
|
||||
strainValue *= 1.05 * lengthBonus;
|
||||
difficultyValue *= 1.05 * lengthBonus;
|
||||
|
||||
// Scale the speed value with accuracy _slightly_
|
||||
return strainValue * Score.Accuracy;
|
||||
return difficultyValue * Score.Accuracy;
|
||||
}
|
||||
|
||||
private double computeAccuracyValue()
|
||||
@ -88,11 +82,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (Attributes.GreatHitWindow <= 0)
|
||||
return 0;
|
||||
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
||||
double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0;
|
||||
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
||||
// Bonus for many objects - it's harder to keep good accuracy up for longer
|
||||
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -149,9 +148,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
centreHit.Colour = colours.Pink;
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayClock gameplayClock { get; set; }
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
{
|
||||
Drawable target = null;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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
|
||||
@ -9,7 +10,7 @@ namespace osu.Game.Tests.iOS
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
UIApplication.Main(args, "GameUIApplication", "AppDelegate");
|
||||
UIApplication.Main(args, typeof(GameUIApplication), typeof(AppDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,5 +32,7 @@
|
||||
</array>
|
||||
<key>XSAppIconAssets</key>
|
||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -2,14 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Scoring;
|
||||
@ -21,6 +27,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
[TestFixture]
|
||||
public class LegacyScoreDecoderTest
|
||||
{
|
||||
private CultureInfo originalCulture;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
originalCulture = CultureInfo.CurrentCulture;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeManiaReplay()
|
||||
{
|
||||
@ -30,7 +44,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
var score = decoder.Parse(resourceStream);
|
||||
|
||||
Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID);
|
||||
Assert.AreEqual(3, score.ScoreInfo.Ruleset.OnlineID);
|
||||
|
||||
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
|
||||
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
|
||||
@ -44,6 +58,59 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCultureInvariance()
|
||||
{
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = scoreInfo,
|
||||
Replay = new Replay
|
||||
{
|
||||
Frames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// the "se" culture is used here, as it encodes the negative number sign as U+2212 MINUS SIGN,
|
||||
// rather than the classic ASCII U+002D HYPHEN-MINUS.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("se");
|
||||
|
||||
var encodeStream = new MemoryStream();
|
||||
|
||||
var encoder = new LegacyScoreEncoder(score, beatmap);
|
||||
encoder.Encode(encodeStream);
|
||||
|
||||
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
||||
|
||||
var decoder = new TestLegacyScoreDecoder();
|
||||
var decodedAfterEncode = decoder.Parse(decodeStream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode, Is.Not.Null);
|
||||
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.User.Username, Is.EqualTo(scoreInfo.User.Username));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.BeatmapInfoID, Is.EqualTo(scoreInfo.BeatmapInfoID));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Ruleset, Is.EqualTo(scoreInfo.Ruleset));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(scoreInfo.TotalScore));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.MaxCombo, Is.EqualTo(scoreInfo.MaxCombo));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Date, Is.EqualTo(scoreInfo.Date));
|
||||
|
||||
Assert.That(decodedAfterEncode.Replay.Frames.Count, Is.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
CultureInfo.CurrentCulture = originalCulture;
|
||||
}
|
||||
|
||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||
{
|
||||
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
|
||||
|
@ -16,6 +16,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -363,15 +364,15 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
var files = osu.Dependencies.Get<FileStore>();
|
||||
|
||||
long originalLength;
|
||||
using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath))
|
||||
using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath()))
|
||||
originalLength = stream.Length;
|
||||
|
||||
using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath, FileAccess.Write, FileMode.Create))
|
||||
using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath(), FileAccess.Write, FileMode.Create))
|
||||
stream.WriteByte(0);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoOsu(osu);
|
||||
|
||||
using (var stream = files.Storage.GetStream(firstFile.FileInfo.StoragePath))
|
||||
using (var stream = files.Storage.GetStream(firstFile.FileInfo.GetStoragePath()))
|
||||
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
@ -583,7 +584,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
OnlineID = 1,
|
||||
Metadata = metadata,
|
||||
Beatmaps = new List<BeatmapInfo>
|
||||
Beatmaps =
|
||||
{
|
||||
new BeatmapInfo
|
||||
{
|
||||
@ -595,7 +596,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
OnlineID = 2,
|
||||
Metadata = metadata,
|
||||
Status = BeatmapSetOnlineStatus.Loved,
|
||||
Status = BeatmapOnlineStatus.Loved,
|
||||
BaseDifficulty = difficulty
|
||||
}
|
||||
}
|
||||
@ -1021,7 +1022,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
{
|
||||
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
||||
{
|
||||
OnlineScoreID = 2,
|
||||
OnlineID = 2,
|
||||
BeatmapInfo = beatmapInfo,
|
||||
BeatmapInfoID = beatmapInfo.ID
|
||||
}, new ImportScoreTest.TestArchiveReader());
|
||||
@ -1049,7 +1050,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
|
||||
{
|
||||
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
|
||||
Assert.AreEqual(expected, osu.Dependencies.Get<DatabaseContextFactory>().Get().FileInfo.Count(f => f.ReferenceCount == 1));
|
||||
}
|
||||
|
||||
private static void ensureLoaded(OsuGameBase osu, int timeout = 60000)
|
||||
|
@ -25,9 +25,6 @@ namespace osu.Game.Tests.Beatmaps
|
||||
|
||||
private BeatmapSetInfo importedSet;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
|
||||
private TestBeatmapDifficultyCache difficultyCache;
|
||||
|
||||
private IBindable<StarDifficulty?> starDifficultyBindable;
|
||||
@ -100,8 +97,8 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestKeyEqualsWithDifferentModInstances()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
|
||||
@ -110,8 +107,8 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestKeyEqualsWithDifferentModOrder()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
|
||||
@ -120,8 +117,8 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestKeyDoesntEqualWithDifferentModSettings()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
|
||||
|
||||
Assert.That(key1, Is.Not.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
|
||||
@ -130,8 +127,8 @@ namespace osu.Game.Tests.Beatmaps
|
||||
[Test]
|
||||
public void TestKeyEqualWithMatchingModSettings()
|
||||
{
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
|
||||
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
|
||||
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
|
||||
|
||||
Assert.That(key1, Is.EqualTo(key2));
|
||||
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
|
||||
@ -164,9 +161,9 @@ namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
public Func<DifficultyCacheLookup, StarDifficulty> ComputeDifficulty { get; set; }
|
||||
|
||||
protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
|
||||
protected override Task<StarDifficulty?> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(ComputeDifficulty?.Invoke(lookup) ?? new StarDifficulty(BASE_STARS, 0));
|
||||
return Task.FromResult<StarDifficulty?>(ComputeDifficulty?.Invoke(lookup) ?? new StarDifficulty(BASE_STARS, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
115
osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs
Normal file
115
osu.Game.Tests/Beatmaps/WorkingBeatmapTest.cs
Normal file
@ -0,0 +1,115 @@
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
[TestFixture]
|
||||
public class WorkingBeatmapTest
|
||||
{
|
||||
[Test]
|
||||
public void TestGetPlayableSuccess()
|
||||
{
|
||||
var working = new TestNeverLoadsWorkingBeatmap();
|
||||
|
||||
working.ResetEvent.Set();
|
||||
|
||||
Assert.NotNull(working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetPlayableCancellationToken()
|
||||
{
|
||||
var working = new TestNeverLoadsWorkingBeatmap();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var loadStarted = new ManualResetEventSlim();
|
||||
var loadCompleted = new ManualResetEventSlim();
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
loadStarted.Set();
|
||||
Assert.Throws<OperationCanceledException>(() => working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>(), cts.Token));
|
||||
loadCompleted.Set();
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
|
||||
Assert.IsTrue(loadStarted.Wait(10000));
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
Assert.IsTrue(loadCompleted.Wait(10000));
|
||||
|
||||
working.ResetEvent.Set();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetPlayableDefaultTimeout()
|
||||
{
|
||||
var working = new TestNeverLoadsWorkingBeatmap();
|
||||
|
||||
Assert.Throws(Is.InstanceOf<TimeoutException>(), () => working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
|
||||
|
||||
working.ResetEvent.Set();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetPlayableRulesetLoadFailure()
|
||||
{
|
||||
var working = new TestWorkingBeatmap(new Beatmap());
|
||||
|
||||
// by default mocks return nulls if not set up, which is actually desired here to simulate a ruleset load failure scenario.
|
||||
var ruleset = new Mock<IRulesetInfo>();
|
||||
|
||||
Assert.Throws<RulesetLoadException>(() => working.GetPlayableBeatmap(ruleset.Object));
|
||||
}
|
||||
|
||||
public class TestNeverLoadsWorkingBeatmap : TestWorkingBeatmap
|
||||
{
|
||||
public ManualResetEventSlim ResetEvent = new ManualResetEventSlim();
|
||||
|
||||
public TestNeverLoadsWorkingBeatmap()
|
||||
: base(new Beatmap())
|
||||
{
|
||||
}
|
||||
|
||||
protected override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => new TestConverter(beatmap, ResetEvent);
|
||||
|
||||
public class TestConverter : IBeatmapConverter
|
||||
{
|
||||
private readonly ManualResetEventSlim resetEvent;
|
||||
|
||||
public TestConverter(IBeatmap beatmap, ManualResetEventSlim resetEvent)
|
||||
{
|
||||
this.resetEvent = resetEvent;
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
|
||||
|
||||
protected virtual void OnObjectConverted(HitObject arg1, IEnumerable<HitObject> arg2) => ObjectConverted?.Invoke(arg1, arg2);
|
||||
|
||||
public IBeatmap Beatmap { get; }
|
||||
|
||||
public bool CanConvert() => true;
|
||||
|
||||
public IBeatmap Convert(CancellationToken cancellationToken = default)
|
||||
{
|
||||
resetEvent.Wait(cancellationToken);
|
||||
return new OsuBeatmap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -128,7 +128,7 @@ namespace osu.Game.Tests.Collections.IO
|
||||
[Test]
|
||||
public async Task TestSaveAndReload()
|
||||
{
|
||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload", bypassCleanup: true))
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(bypassCleanup: true))
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -149,7 +149,8 @@ namespace osu.Game.Tests.Collections.IO
|
||||
}
|
||||
}
|
||||
|
||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload"))
|
||||
// Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location.
|
||||
using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestSaveAndReload)))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -12,8 +12,10 @@ using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -73,6 +75,24 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAccessFileAfterImport()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new BeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
var beatmap = imported.Beatmaps.First();
|
||||
var file = beatmap.File;
|
||||
|
||||
Assert.NotNull(file);
|
||||
Assert.AreEqual(beatmap.Hash, file!.File.Hash);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDelete()
|
||||
{
|
||||
@ -348,15 +368,15 @@ namespace osu.Game.Tests.Database
|
||||
var firstFile = imported.Files.First();
|
||||
|
||||
long originalLength;
|
||||
using (var stream = storage.GetStream(firstFile.File.StoragePath))
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||
originalLength = stream.Length;
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.StoragePath, FileAccess.Write, FileMode.Create))
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath(), FileAccess.Write, FileMode.Create))
|
||||
stream.WriteByte(0);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
using (var stream = storage.GetStream(firstFile.File.StoragePath))
|
||||
using (var stream = storage.GetStream(firstFile.File.GetStoragePath()))
|
||||
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
@ -455,7 +475,7 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDeleteThenImport()
|
||||
public void TestImportThenDeleteThenImportOptimisedPath()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
@ -466,11 +486,39 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
deleteBeatmapSet(imported, realmFactory.Context);
|
||||
|
||||
Assert.IsTrue(imported.DeletePending);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsFalse(imported.DeletePending);
|
||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportThenDeleteThenImportNonOptimisedPath()
|
||||
{
|
||||
RunTestWithRealmAsync(async (realmFactory, storage) =>
|
||||
{
|
||||
using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage);
|
||||
using var store = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
deleteBeatmapSet(imported, realmFactory.Context);
|
||||
|
||||
Assert.IsTrue(imported.DeletePending);
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
Assert.IsTrue(imported.ID == importedSecondTime.ID);
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsFalse(imported.DeletePending);
|
||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||
});
|
||||
}
|
||||
|
||||
@ -531,7 +579,7 @@ namespace osu.Game.Tests.Database
|
||||
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
|
||||
{
|
||||
OnlineID = 2,
|
||||
Status = BeatmapSetOnlineStatus.Loved,
|
||||
Status = BeatmapOnlineStatus.Loved,
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -761,7 +809,7 @@ namespace osu.Game.Tests.Database
|
||||
// TODO: reimplement when we have score support in realm.
|
||||
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
|
||||
// {
|
||||
// OnlineScoreID = 2,
|
||||
// OnlineID = 2,
|
||||
// Beatmap = beatmap,
|
||||
// BeatmapInfoID = beatmap.ID
|
||||
// }, new ImportScoreTest.TestArchiveReader());
|
||||
@ -804,7 +852,11 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
IQueryable<RealmBeatmapSet>? resultSets = null;
|
||||
|
||||
waitForOrAssert(() => (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
|
||||
waitForOrAssert(() =>
|
||||
{
|
||||
realm.Refresh();
|
||||
return (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any();
|
||||
},
|
||||
@"BeatmapSet did not import to the database in allocated time.", timeout);
|
||||
|
||||
// ensure we were stored to beatmap database backing...
|
||||
@ -817,16 +869,16 @@ namespace osu.Game.Tests.Database
|
||||
// ReSharper disable once PossibleUnintendedReferenceComparison
|
||||
IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
|
||||
|
||||
waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
|
||||
waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
|
||||
Assert.AreEqual(12, queryBeatmaps().Count(), @"Beatmap count was not correct");
|
||||
Assert.AreEqual(1, queryBeatmapSets().Count(), @"Beatmapset count was not correct");
|
||||
|
||||
int countBeatmapSetBeatmaps = 0;
|
||||
int countBeatmaps = 0;
|
||||
int countBeatmapSetBeatmaps;
|
||||
int countBeatmaps;
|
||||
|
||||
waitForOrAssert(() =>
|
||||
(countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
|
||||
(countBeatmaps = queryBeatmaps().Count()),
|
||||
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
|
||||
Assert.AreEqual(
|
||||
countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count,
|
||||
countBeatmaps = queryBeatmaps().Count(),
|
||||
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).");
|
||||
|
||||
foreach (RealmBeatmap b in set.Beatmaps)
|
||||
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
|
||||
@ -848,5 +900,15 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.Fail(failureMessage);
|
||||
}
|
||||
|
||||
public class NonOptimisedBeatmapImporter : BeatmapImporter
|
||||
{
|
||||
public NonOptimisedBeatmapImporter(RealmContextFactory realmFactory, Storage storage)
|
||||
: base(realmFactory, storage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool HasCustomHashFunction => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Stores;
|
||||
|
||||
@ -28,7 +29,7 @@ namespace osu.Game.Tests.Database
|
||||
realm.Write(() => files.Add(testData, realm));
|
||||
|
||||
Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
|
||||
Assert.True(files.Storage.Exists(realm.All<RealmFile>().First().StoragePath));
|
||||
Assert.True(files.Storage.Exists(realm.All<RealmFile>().First().GetStoragePath()));
|
||||
});
|
||||
}
|
||||
|
||||
@ -74,7 +75,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
|
||||
|
||||
string path = file.StoragePath;
|
||||
string path = file.GetStoragePath();
|
||||
|
||||
Assert.True(realm.All<RealmFile>().Any());
|
||||
Assert.True(files.Storage.Exists(path));
|
||||
@ -98,7 +99,7 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
|
||||
|
||||
string path = file.StoragePath;
|
||||
string path = file.GetStoragePath();
|
||||
|
||||
Assert.True(realm.All<RealmFile>().Any());
|
||||
Assert.True(files.Storage.Exists(path));
|
||||
|
@ -5,6 +5,8 @@ using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -33,6 +35,39 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
|
||||
/// due to context fetching semaphores.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestNestedContextCreationWithSubscription()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
bool callbackRan = false;
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
{
|
||||
var subscription = context.All<RealmBeatmap>().QueryAsyncWithNotifications((sender, changes, error) =>
|
||||
{
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
callbackRan = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Force the callback above to run.
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
}
|
||||
|
||||
subscription?.Dispose();
|
||||
}
|
||||
|
||||
Assert.IsTrue(callbackRan);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockOperationsWithContention()
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using Realms;
|
||||
@ -18,68 +18,91 @@ namespace osu.Game.Tests.Database
|
||||
public class RealmLiveTests : RealmTest
|
||||
{
|
||||
[Test]
|
||||
public void TestLiveCastability()
|
||||
public void TestLiveEquality()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
|
||||
ILive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(realmFactory);
|
||||
|
||||
ILive<IBeatmapInfo> iBeatmap = beatmap;
|
||||
ILive<RealmBeatmap> beatmap2 = realmFactory.CreateContext().All<RealmBeatmap>().First().ToLive(realmFactory);
|
||||
|
||||
Assert.AreEqual(0, iBeatmap.Value.Length);
|
||||
Assert.AreEqual(beatmap, beatmap2);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueAccessWithOpenContext()
|
||||
public void TestAccessAfterStorageMigrate()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, storage) =>
|
||||
{
|
||||
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
|
||||
|
||||
ILive<RealmBeatmap> liveBeatmap;
|
||||
|
||||
using (var context = realmFactory.CreateContext())
|
||||
{
|
||||
context.Write(r => r.Add(beatmap));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
|
||||
using (var migratedStorage = new TemporaryNativeStorage("realm-test-migration-target"))
|
||||
{
|
||||
migratedStorage.DeleteDirectory(string.Empty);
|
||||
|
||||
storage.Migrate(migratedStorage);
|
||||
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAccessAfterAttach()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
|
||||
|
||||
liveBeatmap = beatmap.ToLive();
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
var liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
using (var context = realmFactory.CreateContext())
|
||||
context.Write(r => r.Add(beatmap));
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
var resolved = liveBeatmap.Value;
|
||||
|
||||
Assert.IsTrue(resolved.Realm.IsClosed);
|
||||
Assert.IsTrue(resolved.IsValid);
|
||||
|
||||
// can access properties without a crash.
|
||||
Assert.IsFalse(resolved.Hidden);
|
||||
}
|
||||
});
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAccessNonManaged()
|
||||
{
|
||||
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
|
||||
var liveBeatmap = beatmap.ToLiveUnmanaged();
|
||||
|
||||
Assert.IsFalse(beatmap.Hidden);
|
||||
Assert.IsFalse(liveBeatmap.Value.Hidden);
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => liveBeatmap.PerformWrite(l => l.Hidden = true));
|
||||
|
||||
Assert.IsFalse(beatmap.Hidden);
|
||||
Assert.IsFalse(liveBeatmap.Value.Hidden);
|
||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScopedReadWithoutContext()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||
ILive<RealmBeatmap>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive();
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
|
||||
@ -101,14 +124,14 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||
ILive<RealmBeatmap>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive();
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
|
||||
@ -123,18 +146,72 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueAccessWithoutOpenContextFails()
|
||||
public void TestValueAccessNonManaged()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
|
||||
var liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
|
||||
Assert.DoesNotThrow(() =>
|
||||
{
|
||||
var __ = liveBeatmap.Value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueAccessWithOpenContextFails()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
ILive<RealmBeatmap>? liveBeatmap = null;
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive();
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
|
||||
Debug.Assert(liveBeatmap != null);
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
// Can't be used, without a valid context.
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var __ = liveBeatmap.Value;
|
||||
});
|
||||
|
||||
// Can't be used, even from within a valid context.
|
||||
using (realmFactory.CreateContext())
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
var __ = liveBeatmap.Value;
|
||||
});
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestValueAccessWithoutOpenContextFails()
|
||||
{
|
||||
RunTestWithRealm((realmFactory, _) =>
|
||||
{
|
||||
ILive<RealmBeatmap>? liveBeatmap = null;
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
using (var threadContext = realmFactory.CreateContext())
|
||||
{
|
||||
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
|
||||
@ -159,8 +236,8 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
using (var updateThreadContext = realmFactory.CreateContext())
|
||||
{
|
||||
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
|
||||
RealmLive<RealmBeatmap>? liveBeatmap = null;
|
||||
updateThreadContext.All<RealmBeatmap>().QueryAsyncWithNotifications(gotChange);
|
||||
ILive<RealmBeatmap>? liveBeatmap = null;
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
@ -173,7 +250,7 @@ namespace osu.Game.Tests.Database
|
||||
// not just a refresh from the resolved Live.
|
||||
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
|
||||
|
||||
liveBeatmap = beatmap.ToLive();
|
||||
liveBeatmap = beatmap.ToLive(realmFactory);
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||
|
||||
@ -183,23 +260,22 @@ namespace osu.Game.Tests.Database
|
||||
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
|
||||
Assert.AreEqual(0, changesTriggered);
|
||||
|
||||
var resolved = liveBeatmap.Value;
|
||||
|
||||
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
|
||||
Assert.AreEqual(1, changesTriggered);
|
||||
|
||||
// even though the realm that this instance was resolved for was closed, it's still valid.
|
||||
Assert.IsTrue(resolved.Realm.IsClosed);
|
||||
Assert.IsTrue(resolved.IsValid);
|
||||
|
||||
// can access properties without a crash.
|
||||
Assert.IsFalse(resolved.Hidden);
|
||||
|
||||
updateThreadContext.Write(r =>
|
||||
liveBeatmap.PerformRead(resolved =>
|
||||
{
|
||||
// can use with the main context.
|
||||
r.Remove(resolved);
|
||||
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
|
||||
Assert.AreEqual(1, changesTriggered);
|
||||
|
||||
// can access properties without a crash.
|
||||
Assert.IsFalse(resolved.Hidden);
|
||||
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
updateThreadContext.Write(r =>
|
||||
{
|
||||
// can use with the main context.
|
||||
r.Remove(resolved);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Models;
|
||||
|
||||
#nullable enable
|
||||
@ -27,15 +28,16 @@ namespace osu.Game.Tests.Database
|
||||
storage.DeleteDirectory(string.Empty);
|
||||
}
|
||||
|
||||
protected void RunTestWithRealm(Action<RealmContextFactory, Storage> testAction, [CallerMemberName] string caller = "")
|
||||
protected void RunTestWithRealm(Action<RealmContextFactory, OsuStorage> testAction, [CallerMemberName] string caller = "")
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
|
||||
{
|
||||
host.Run(new RealmTestGame(() =>
|
||||
{
|
||||
var testStorage = storage.GetStorageForDirectory(caller);
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
var testStorage = new OsuStorage(host, storage.GetStorageForDirectory(caller));
|
||||
|
||||
using (var realmFactory = new RealmContextFactory(testStorage, caller))
|
||||
using (var realmFactory = new RealmContextFactory(testStorage, "client"))
|
||||
{
|
||||
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
|
||||
testAction(realmFactory, testStorage);
|
||||
@ -58,7 +60,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
var testStorage = storage.GetStorageForDirectory(caller);
|
||||
|
||||
using (var realmFactory = new RealmContextFactory(testStorage, caller))
|
||||
using (var realmFactory = new RealmContextFactory(testStorage, "client"))
|
||||
{
|
||||
Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
|
||||
await testAction(realmFactory, testStorage);
|
||||
|
@ -45,9 +45,9 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
var rulesets = new RealmRulesetStore(realmFactory, storage);
|
||||
|
||||
Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
|
||||
Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
|
||||
Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
|
||||
Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
|
||||
Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
|
||||
Assert.IsFalse(rulesets.GetRuleset("mania")?.IsManaged);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,45 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultsPopulationRemovesExcess()
|
||||
{
|
||||
Assert.That(queryCount(), Is.EqualTo(0));
|
||||
|
||||
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||
|
||||
// Add some excess bindings for an action which only supports 1.
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
realm.Add(new RealmKeyBinding
|
||||
{
|
||||
Action = GlobalAction.Back,
|
||||
KeyCombination = new KeyCombination(InputKey.A)
|
||||
});
|
||||
|
||||
realm.Add(new RealmKeyBinding
|
||||
{
|
||||
Action = GlobalAction.Back,
|
||||
KeyCombination = new KeyCombination(InputKey.S)
|
||||
});
|
||||
|
||||
realm.Add(new RealmKeyBinding
|
||||
{
|
||||
Action = GlobalAction.Back,
|
||||
KeyCombination = new KeyCombination(InputKey.D)
|
||||
});
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3));
|
||||
|
||||
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
|
||||
|
||||
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
private int queryCount(GlobalAction? match = null)
|
||||
{
|
||||
using (var realm = realmContextFactory.CreateContext())
|
||||
|
@ -1,7 +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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using FileInfo = osu.Game.IO.FileInfo;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
@ -33,14 +31,10 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files = new List<BeatmapSetFileInfo>(new[]
|
||||
Files =
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = "abc123.mp4",
|
||||
FileInfo = new FileInfo { Hash = "abcdef" }
|
||||
}
|
||||
})
|
||||
CheckTestHelpers.CreateMockFile("mp4"),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
// 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 System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@ -12,7 +12,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using FileInfo = osu.Game.IO.FileInfo;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
@ -25,25 +24,17 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var file = CheckTestHelpers.CreateMockFile("jpg");
|
||||
|
||||
check = new CheckBackgroundQuality();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = file.Filename },
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files = new List<BeatmapSetFileInfo>(new[]
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = "abc123.jpg",
|
||||
FileInfo = new FileInfo
|
||||
{
|
||||
Hash = "abcdef"
|
||||
}
|
||||
}
|
||||
})
|
||||
Files = { file }
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -54,7 +45,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
// While this is a problem, it is out of scope for this check and is caught by a different one.
|
||||
beatmap.Metadata.BackgroundFile = string.Empty;
|
||||
var context = getContext(null, new MemoryStream(System.Array.Empty<byte>()));
|
||||
var context = getContext(null, new MemoryStream(Array.Empty<byte>()));
|
||||
|
||||
Assert.That(check.Run(context), Is.Empty);
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
// 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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -22,22 +20,17 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var file = CheckTestHelpers.CreateMockFile("jpg");
|
||||
|
||||
check = new CheckBackgroundPresence();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = file.Filename },
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files = new List<BeatmapSetFileInfo>(new[]
|
||||
{
|
||||
new BeatmapSetFileInfo
|
||||
{
|
||||
Filename = "abc123.jpg",
|
||||
FileInfo = new FileInfo { Hash = "abcdef" }
|
||||
}
|
||||
})
|
||||
Files = { file }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user