mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:07:52 +08:00
Merge branch 'master' into fix-catmull-bulbs
This commit is contained in:
commit
518addf323
24
.github/workflows/diffcalc.yml
vendored
24
.github/workflows/diffcalc.yml
vendored
@ -110,10 +110,14 @@ jobs:
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
|
||||
steps:
|
||||
- name: Check permissions
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0
|
||||
with:
|
||||
require: 'write'
|
||||
run: |
|
||||
ALLOWED_USERS=(smoogipoo peppy bdach)
|
||||
for i in "${ALLOWED_USERS[@]}"; do
|
||||
if [[ "${{ github.actor }}" == "$i" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
|
||||
create-comment:
|
||||
name: Create PR comment
|
||||
@ -122,7 +126,7 @@ jobs:
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
message: |
|
||||
@ -249,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
@ -280,7 +284,7 @@ jobs:
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
@ -354,7 +358,7 @@ jobs:
|
||||
steps:
|
||||
- name: Update comment on success
|
||||
if: ${{ needs.generator.result == 'success' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
@ -365,7 +369,7 @@ jobs:
|
||||
|
||||
- name: Update comment on failure
|
||||
if: ${{ needs.generator.result == 'failure' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
@ -375,7 +379,7 @@ jobs:
|
||||
|
||||
- name: Update comment on cancellation
|
||||
if: ${{ needs.generator.result == 'cancelled' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: delete
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.306.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.329.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -6,6 +6,7 @@ using System.Text;
|
||||
using DiscordRPC;
|
||||
using DiscordRPC.Message;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@ -35,8 +36,6 @@ namespace osu.Desktop
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
private IBindable<APIUser> user = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
@ -49,9 +48,11 @@ namespace osu.Desktop
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
|
||||
|
||||
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
|
||||
|
||||
private readonly RichPresence presence = new RichPresence
|
||||
@ -64,8 +65,10 @@ namespace osu.Desktop
|
||||
},
|
||||
};
|
||||
|
||||
private IBindable<APIUser>? user;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
private void load()
|
||||
{
|
||||
client = new DiscordRpcClient(client_id)
|
||||
{
|
||||
@ -78,9 +81,20 @@ namespace osu.Desktop
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
|
||||
|
||||
// A URI scheme is required to support game invitations, as well as informing Discord of the game executable path to support launching the game when a user clicks on join/spectate.
|
||||
client.RegisterUriScheme();
|
||||
client.Subscribe(EventType.Join);
|
||||
client.OnJoin += onJoin;
|
||||
// The library doesn't properly support URI registration when ran from an app bundle on macOS.
|
||||
if (!RuntimeInfo.IsApple)
|
||||
{
|
||||
client.RegisterUriScheme();
|
||||
client.Subscribe(EventType.Join);
|
||||
client.OnJoin += onJoin;
|
||||
}
|
||||
|
||||
client.Initialize();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
|
||||
|
||||
@ -99,8 +113,6 @@ namespace osu.Desktop
|
||||
activity.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
|
||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||
|
||||
client.Initialize();
|
||||
}
|
||||
|
||||
private void onReady(object _, ReadyMessage __)
|
||||
@ -141,6 +153,9 @@ namespace osu.Desktop
|
||||
|
||||
private void updatePresence(bool hideIdentifiableInformation)
|
||||
{
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
// user activity
|
||||
if (activity.Value != null)
|
||||
{
|
||||
|
@ -0,0 +1,253 @@
|
||||
// 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.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneCatchReverseSelection : TestSceneEditor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionTwoFruits()
|
||||
{
|
||||
CatchHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
addObjects([
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 200,
|
||||
X = 0,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 400,
|
||||
X = 20,
|
||||
}
|
||||
]);
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
selectEverything();
|
||||
reverseSelection();
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionThreeFruits()
|
||||
{
|
||||
CatchHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
addObjects([
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 200,
|
||||
X = 0,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 400,
|
||||
X = 20,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 600,
|
||||
X = 40,
|
||||
}
|
||||
]);
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
selectEverything();
|
||||
reverseSelection();
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionFruitAndJuiceStream()
|
||||
{
|
||||
CatchHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
addObjects([
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 200,
|
||||
X = 0,
|
||||
},
|
||||
new JuiceStream
|
||||
{
|
||||
StartTime = 400,
|
||||
X = 20,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(50))
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
selectEverything();
|
||||
reverseSelection();
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionTwoFruitsAndJuiceStream()
|
||||
{
|
||||
CatchHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
addObjects([
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 200,
|
||||
X = 0,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 400,
|
||||
X = 20,
|
||||
},
|
||||
new JuiceStream
|
||||
{
|
||||
StartTime = 600,
|
||||
X = 40,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(50))
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
selectEverything();
|
||||
reverseSelection();
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionTwoCombos()
|
||||
{
|
||||
CatchHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
addObjects([
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 200,
|
||||
X = 0,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 400,
|
||||
X = 20,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 600,
|
||||
X = 40,
|
||||
},
|
||||
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 800,
|
||||
NewCombo = true,
|
||||
X = 60,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 1000,
|
||||
X = 80,
|
||||
},
|
||||
new Fruit
|
||||
{
|
||||
StartTime = 1200,
|
||||
X = 100,
|
||||
}
|
||||
]);
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
selectEverything();
|
||||
reverseSelection();
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
private void addObjects(CatchHitObject[] hitObjects) => AddStep("Add objects", () => EditorBeatmap.AddRange(hitObjects));
|
||||
|
||||
private IEnumerable<CatchHitObject> getObjects() => EditorBeatmap.HitObjects.OfType<CatchHitObject>();
|
||||
|
||||
private IEnumerable<bool> getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
|
||||
|
||||
private void selectEverything()
|
||||
{
|
||||
AddStep("Select everything", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
|
||||
});
|
||||
}
|
||||
|
||||
private void reverseSelection()
|
||||
{
|
||||
AddStep("Reverse selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -76,21 +76,38 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
public override bool HandleReverse()
|
||||
{
|
||||
var hitObjects = EditorBeatmap.SelectedHitObjects
|
||||
.OfType<CatchHitObject>()
|
||||
.OrderBy(obj => obj.StartTime)
|
||||
.ToList();
|
||||
|
||||
double selectionStartTime = SelectedItems.Min(h => h.StartTime);
|
||||
double selectionEndTime = SelectedItems.Max(h => h.GetEndTime());
|
||||
|
||||
EditorBeatmap.PerformOnSelection(hitObject =>
|
||||
{
|
||||
hitObject.StartTime = selectionEndTime - (hitObject.GetEndTime() - selectionStartTime);
|
||||
// the expectation is that even if the objects themselves are reversed temporally,
|
||||
// the position of new combos in the selection should remain the same.
|
||||
// preserve it for later before doing the reversal.
|
||||
var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
|
||||
|
||||
if (hitObject is JuiceStream juiceStream)
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.StartTime = selectionEndTime - (h.GetEndTime() - selectionStartTime);
|
||||
|
||||
if (h is JuiceStream juiceStream)
|
||||
{
|
||||
juiceStream.Path.Reverse(out Vector2 positionalOffset);
|
||||
juiceStream.OriginalX += positionalOffset.X;
|
||||
juiceStream.LegacyConvertedY += positionalOffset.Y;
|
||||
EditorBeatmap.Update(juiceStream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// re-order objects by start time again after reversing, and restore new combo flag positioning
|
||||
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
|
||||
|
||||
for (int i = 0; i < hitObjects.Count; ++i)
|
||||
hitObjects[i].NewCombo = newComboOrder[i];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
/// </summary>
|
||||
public int TotalColumns => Stages.Sum(g => g.Columns);
|
||||
|
||||
/// <summary>
|
||||
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
|
||||
/// </summary>
|
||||
public readonly int OriginalTotalColumns;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ManiaBeatmap"/>.
|
||||
/// </summary>
|
||||
@ -35,7 +30,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
|
||||
{
|
||||
Stages.Add(defaultStage);
|
||||
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
|
||||
}
|
||||
|
||||
public override IEnumerable<BeatmapStatistic> GetStatistics()
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using System;
|
||||
using System.Linq;
|
||||
@ -14,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
@ -27,24 +26,42 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
/// </summary>
|
||||
private const int max_notes_for_density = 7;
|
||||
|
||||
/// <summary>
|
||||
/// The total number of columns.
|
||||
/// </summary>
|
||||
public int TotalColumns => TargetColumns * (Dual ? 2 : 1);
|
||||
|
||||
/// <summary>
|
||||
/// The number of columns per-stage.
|
||||
/// </summary>
|
||||
public int TargetColumns;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to double the number of stages.
|
||||
/// </summary>
|
||||
public bool Dual;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the beatmap instantiated with is for the mania ruleset.
|
||||
/// </summary>
|
||||
public readonly bool IsForCurrentRuleset;
|
||||
|
||||
private readonly int originalTargetColumns;
|
||||
|
||||
// Internal for testing purposes
|
||||
internal LegacyRandom Random { get; private set; }
|
||||
internal readonly LegacyRandom Random;
|
||||
|
||||
private Pattern lastPattern = new Pattern();
|
||||
|
||||
private ManiaBeatmap beatmap;
|
||||
|
||||
public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||
: base(beatmap, ruleset)
|
||||
: this(beatmap, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap), ruleset)
|
||||
{
|
||||
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||
TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap));
|
||||
}
|
||||
|
||||
private ManiaBeatmapConverter(IBeatmap? beatmap, LegacyBeatmapConversionDifficultyInfo difficulty, Ruleset ruleset)
|
||||
: base(beatmap!, ruleset)
|
||||
{
|
||||
IsForCurrentRuleset = difficulty.SourceRuleset.Equals(ruleset.RulesetInfo);
|
||||
Random = new LegacyRandom((int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate));
|
||||
TargetColumns = getColumnCount(difficulty);
|
||||
|
||||
if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
@ -52,52 +69,53 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
Dual = true;
|
||||
}
|
||||
|
||||
originalTargetColumns = TargetColumns;
|
||||
static int getColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
|
||||
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
|
||||
{
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
}
|
||||
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList<Mod>? mods = null)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
|
||||
|
||||
if (difficulty.SourceRuleset.ShortName == ManiaRuleset.SHORT_NAME)
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
if (difficulty.TotalObjectCount > 0 && difficulty.EndTimeObjectCount >= 0)
|
||||
if (mods != null)
|
||||
{
|
||||
int countSliderOrSpinner = difficulty.EndTimeObjectCount;
|
||||
|
||||
// In osu!stable, this division appears as if it happens on floats, but due to release-mode
|
||||
// optimisations, it actually ends up happening on doubles.
|
||||
double percentSpecialObjects = (double)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
foreach (var m in mods.OfType<IApplicableToBeatmapConverter>())
|
||||
m.ApplyToBeatmapConverter(converter);
|
||||
}
|
||||
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
return converter.TotalColumns;
|
||||
}
|
||||
|
||||
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
|
||||
|
||||
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||
{
|
||||
IBeatmapDifficultyInfo difficulty = original.Difficulty;
|
||||
|
||||
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
|
||||
Random = new LegacyRandom(seed);
|
||||
|
||||
return base.ConvertBeatmap(original, cancellationToken);
|
||||
}
|
||||
|
||||
protected override Beatmap<ManiaHitObject> CreateBeatmap()
|
||||
{
|
||||
beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns), originalTargetColumns);
|
||||
ManiaBeatmap beatmap = new ManiaBeatmap(new StageDefinition(TargetColumns));
|
||||
|
||||
if (Dual)
|
||||
beatmap.Stages.Add(new StageDefinition(TargetColumns));
|
||||
@ -115,10 +133,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
}
|
||||
|
||||
var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap);
|
||||
|
||||
if (objects == null)
|
||||
yield break;
|
||||
|
||||
foreach (ManiaHitObject obj in objects)
|
||||
yield return obj;
|
||||
}
|
||||
@ -152,7 +166,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
/// <returns>The hit objects generated.</returns>
|
||||
private IEnumerable<ManiaHitObject> generateSpecific(HitObject original, IBeatmap originalBeatmap)
|
||||
{
|
||||
var generator = new SpecificBeatmapPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||
var generator = new SpecificBeatmapPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
|
||||
|
||||
foreach (var newPattern in generator.Generate())
|
||||
{
|
||||
@ -171,13 +185,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
/// <returns>The hit objects generated.</returns>
|
||||
private IEnumerable<ManiaHitObject> generateConverted(HitObject original, IBeatmap originalBeatmap)
|
||||
{
|
||||
Patterns.PatternGenerator conversion = null;
|
||||
Patterns.PatternGenerator? conversion = null;
|
||||
|
||||
switch (original)
|
||||
{
|
||||
case IHasPath:
|
||||
{
|
||||
var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||
var generator = new PathObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
|
||||
conversion = generator;
|
||||
|
||||
var positionData = original as IHasPosition;
|
||||
@ -195,7 +209,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
case IHasDuration endTimeData:
|
||||
{
|
||||
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||
conversion = new EndTimeObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern);
|
||||
|
||||
recordNote(endTimeData.EndTime, new Vector2(256, 192));
|
||||
computeDensity(endTimeData.EndTime);
|
||||
@ -206,7 +220,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
computeDensity(original.StartTime);
|
||||
|
||||
conversion = new HitObjectPatternGenerator(Random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap);
|
||||
conversion = new HitObjectPatternGenerator(Random, original, originalBeatmap, TotalColumns, lastPattern, lastTime, lastPosition, density, lastStair);
|
||||
|
||||
recordNote(original.StartTime, positionData.Position);
|
||||
break;
|
||||
@ -231,8 +245,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
/// </summary>
|
||||
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
|
||||
{
|
||||
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
private readonly int endTime;
|
||||
private readonly PatternType convertType;
|
||||
|
||||
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
|
||||
|
||||
|
@ -23,9 +23,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
private readonly PatternType convertType;
|
||||
|
||||
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density,
|
||||
PatternType lastStair, IBeatmap originalBeatmap)
|
||||
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition,
|
||||
double density, PatternType lastStair)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
StairType = lastStair;
|
||||
|
||||
|
@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
private PatternType convertType;
|
||||
|
||||
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
: base(random, hitObject, beatmap, previousPattern, totalColumns)
|
||||
{
|
||||
convertType = PatternType.None;
|
||||
if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
|
||||
|
@ -27,20 +27,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// </summary>
|
||||
protected readonly LegacyRandom Random;
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap which <see cref="HitObject"/> is being converted from.
|
||||
/// </summary>
|
||||
protected readonly IBeatmap OriginalBeatmap;
|
||||
|
||||
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||
: base(hitObject, beatmap, previousPattern)
|
||||
protected PatternGenerator(LegacyRandom random, HitObject hitObject, IBeatmap beatmap, Pattern previousPattern, int totalColumns)
|
||||
: base(hitObject, beatmap, totalColumns, previousPattern)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(random);
|
||||
ArgumentNullException.ThrowIfNull(originalBeatmap);
|
||||
|
||||
Random = random;
|
||||
OriginalBeatmap = originalBeatmap;
|
||||
|
||||
RandomStart = TotalColumns == 8 ? 1 : 0;
|
||||
}
|
||||
|
||||
@ -104,17 +96,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
if (conversionDifficulty != null)
|
||||
return conversionDifficulty.Value;
|
||||
|
||||
HitObject lastObject = OriginalBeatmap.HitObjects.LastOrDefault();
|
||||
HitObject firstObject = OriginalBeatmap.HitObjects.FirstOrDefault();
|
||||
HitObject lastObject = Beatmap.HitObjects.LastOrDefault();
|
||||
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
|
||||
|
||||
// Drain time in seconds
|
||||
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - OriginalBeatmap.TotalBreakTime) / 1000);
|
||||
int drainTime = (int)(((lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0) - Beatmap.TotalBreakTime) / 1000);
|
||||
|
||||
if (drainTime == 0)
|
||||
drainTime = 10000;
|
||||
|
||||
IBeatmapDifficultyInfo difficulty = OriginalBeatmap.Difficulty;
|
||||
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
|
||||
IBeatmapDifficultyInfo difficulty = Beatmap.Difficulty;
|
||||
conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
|
||||
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
|
||||
|
||||
return conversionDifficulty.Value;
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
||||
@ -25,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
||||
/// <summary>
|
||||
/// The beatmap which <see cref="HitObject"/> is a part of.
|
||||
/// </summary>
|
||||
protected readonly ManiaBeatmap Beatmap;
|
||||
protected readonly IBeatmap Beatmap;
|
||||
|
||||
protected readonly int TotalColumns;
|
||||
|
||||
protected PatternGenerator(HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern)
|
||||
protected PatternGenerator(HitObject hitObject, IBeatmap beatmap, int totalColumns, Pattern previousPattern)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(hitObject);
|
||||
ArgumentNullException.ThrowIfNull(beatmap);
|
||||
@ -38,8 +39,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
|
||||
HitObject = hitObject;
|
||||
Beatmap = beatmap;
|
||||
PreviousPattern = previousPattern;
|
||||
|
||||
TotalColumns = Beatmap.TotalColumns;
|
||||
TotalColumns = totalColumns;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -51,13 +51,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
return multiplier;
|
||||
|
||||
// Apply key mod multipliers.
|
||||
|
||||
int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty);
|
||||
int actualColumns = originalColumns;
|
||||
|
||||
actualColumns = mods.OfType<ManiaKeyMod>().SingleOrDefault()?.KeyCount ?? actualColumns;
|
||||
if (mods.Any(m => m is ManiaModDualStages))
|
||||
actualColumns *= 2;
|
||||
int actualColumns = ManiaBeatmapConverter.GetColumnCount(difficulty, mods);
|
||||
|
||||
if (actualColumns > originalColumns)
|
||||
multiplier *= 0.9;
|
||||
|
@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
private FilterCriteria.OptionalRange<float> keys;
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo)
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
|
||||
{
|
||||
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo)));
|
||||
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
|
@ -423,8 +423,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public override DifficultySection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||
|
||||
public int GetKeyCount(IBeatmapInfo beatmapInfo)
|
||||
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo));
|
||||
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
||||
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
|
||||
}
|
||||
|
||||
public enum PlayfieldType
|
||||
|
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (circle1 is null || circle2 is null || slider is null)
|
||||
if (circle1 == null || circle2 == null || slider == null)
|
||||
return false;
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints;
|
||||
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 is null || slider2 is null || slider1Path is null)
|
||||
if (slider1 == null || slider2 == null || slider1Path == null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path is null || slider2 is null)
|
||||
if (slider1Path == null || slider2 == null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
@ -231,6 +231,137 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMergeSliderSliderSameStartTime()
|
||||
{
|
||||
Slider? slider1 = null;
|
||||
SliderPath? slider1Path = null;
|
||||
Slider? slider2 = null;
|
||||
|
||||
AddStep("select two sliders", () =>
|
||||
{
|
||||
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
|
||||
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
|
||||
EditorClock.Seek(slider1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
|
||||
});
|
||||
|
||||
AddStep("move sliders to the same start time", () =>
|
||||
{
|
||||
slider2!.StartTime = slider1!.StartTime;
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 == null || slider2 == null || slider1Path == null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
var controlPoints2 = slider2.Path.ControlPoints;
|
||||
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
|
||||
|
||||
for (int i = 0; i < controlPoints1.Count - 1; i++)
|
||||
{
|
||||
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < controlPoints2.Count; i++)
|
||||
{
|
||||
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
|
||||
}
|
||||
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("merged slider matches first slider", () =>
|
||||
{
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
|
||||
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
|
||||
});
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path == null || slider2 == null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMergeSliderSliderSameStartAndEndTime()
|
||||
{
|
||||
Slider? slider1 = null;
|
||||
SliderPath? slider1Path = null;
|
||||
Slider? slider2 = null;
|
||||
|
||||
AddStep("select two sliders", () =>
|
||||
{
|
||||
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
|
||||
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
|
||||
EditorClock.Seek(slider1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
|
||||
});
|
||||
|
||||
AddStep("move sliders to the same start & end time", () =>
|
||||
{
|
||||
slider2!.StartTime = slider1!.StartTime;
|
||||
slider2.Path = slider1.Path;
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 == null || slider2 == null || slider1Path == null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
var controlPoints2 = slider2.Path.ControlPoints;
|
||||
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
|
||||
|
||||
for (int i = 0; i < controlPoints1.Count - 1; i++)
|
||||
{
|
||||
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < controlPoints2.Count; i++)
|
||||
{
|
||||
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
|
||||
}
|
||||
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("merged slider matches first slider", () =>
|
||||
{
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
|
||||
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
|
||||
});
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path == null || slider2 == null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
|
||||
});
|
||||
}
|
||||
|
||||
private void mergeSelection()
|
||||
{
|
||||
AddStep("merge selection", () =>
|
||||
|
@ -0,0 +1,300 @@
|
||||
// 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.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneOsuReverseSelection : TestSceneOsuEditor
|
||||
{
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionTwoCircles()
|
||||
{
|
||||
OsuHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
AddStep("Add circles", () =>
|
||||
{
|
||||
var circle1 = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(208, 240)
|
||||
};
|
||||
var circle2 = new HitCircle
|
||||
{
|
||||
StartTime = 200,
|
||||
Position = new Vector2(256, 144)
|
||||
};
|
||||
|
||||
EditorBeatmap.AddRange([circle1, circle2]);
|
||||
});
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("Reverse selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionThreeCircles()
|
||||
{
|
||||
OsuHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
AddStep("Add circles", () =>
|
||||
{
|
||||
var circle1 = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(208, 240)
|
||||
};
|
||||
var circle2 = new HitCircle
|
||||
{
|
||||
StartTime = 200,
|
||||
Position = new Vector2(256, 144)
|
||||
};
|
||||
var circle3 = new HitCircle
|
||||
{
|
||||
StartTime = 400,
|
||||
Position = new Vector2(304, 240)
|
||||
};
|
||||
|
||||
EditorBeatmap.AddRange([circle1, circle2, circle3]);
|
||||
});
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("Reverse selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionCircleAndSlider()
|
||||
{
|
||||
OsuHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
Vector2 sliderHeadOldPosition = default;
|
||||
Vector2 sliderTailOldPosition = default;
|
||||
|
||||
AddStep("Add objects", () =>
|
||||
{
|
||||
var circle = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(208, 240)
|
||||
};
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 200,
|
||||
Position = sliderHeadOldPosition = new Vector2(257, 144),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sliderTailOldPosition = slider.EndPosition;
|
||||
|
||||
EditorBeatmap.AddRange([circle, slider]);
|
||||
});
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("Reverse selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
|
||||
AddAssert("Slider head is at slider tail", () =>
|
||||
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).Position, sliderTailOldPosition) < 1);
|
||||
|
||||
AddAssert("Slider tail is at slider head", () =>
|
||||
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionTwoCirclesAndSlider()
|
||||
{
|
||||
OsuHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
Vector2 sliderHeadOldPosition = default;
|
||||
Vector2 sliderTailOldPosition = default;
|
||||
|
||||
AddStep("Add objects", () =>
|
||||
{
|
||||
var circle1 = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(208, 240)
|
||||
};
|
||||
var circle2 = new HitCircle
|
||||
{
|
||||
StartTime = 200,
|
||||
Position = new Vector2(256, 144)
|
||||
};
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 200,
|
||||
Position = sliderHeadOldPosition = new Vector2(304, 240),
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sliderTailOldPosition = slider.EndPosition;
|
||||
|
||||
EditorBeatmap.AddRange([circle1, circle2, slider]);
|
||||
});
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
AddStep("Select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("Reverse selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
|
||||
AddAssert("Slider head is at slider tail", () =>
|
||||
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).Position, sliderTailOldPosition) < 1);
|
||||
|
||||
AddAssert("Slider tail is at slider head", () =>
|
||||
Vector2.Distance(EditorBeatmap.HitObjects.OfType<Slider>().ElementAt(0).EndPosition, sliderHeadOldPosition) < 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReverseSelectionTwoCombos()
|
||||
{
|
||||
OsuHitObject[] objects = null!;
|
||||
bool[] newCombos = null!;
|
||||
|
||||
AddStep("Add circles", () =>
|
||||
{
|
||||
var circle1 = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(216, 240)
|
||||
};
|
||||
var circle2 = new HitCircle
|
||||
{
|
||||
StartTime = 200,
|
||||
Position = new Vector2(120, 192)
|
||||
};
|
||||
var circle3 = new HitCircle
|
||||
{
|
||||
StartTime = 400,
|
||||
Position = new Vector2(216, 144)
|
||||
};
|
||||
|
||||
var circle4 = new HitCircle
|
||||
{
|
||||
StartTime = 646,
|
||||
NewCombo = true,
|
||||
Position = new Vector2(296, 240)
|
||||
};
|
||||
var circle5 = new HitCircle
|
||||
{
|
||||
StartTime = 846,
|
||||
Position = new Vector2(392, 162)
|
||||
};
|
||||
var circle6 = new HitCircle
|
||||
{
|
||||
StartTime = 1046,
|
||||
Position = new Vector2(296, 144)
|
||||
};
|
||||
|
||||
EditorBeatmap.AddRange([circle1, circle2, circle3, circle4, circle5, circle6]);
|
||||
});
|
||||
|
||||
AddStep("store objects & new combo data", () =>
|
||||
{
|
||||
objects = getObjects().ToArray();
|
||||
newCombos = getObjectNewCombos().ToArray();
|
||||
});
|
||||
|
||||
AddStep("Select circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("Reverse selection", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LControl);
|
||||
InputManager.Key(Key.G);
|
||||
InputManager.ReleaseKey(Key.LControl);
|
||||
});
|
||||
|
||||
AddAssert("objects reversed", getObjects, () => Is.EqualTo(objects.Reverse()));
|
||||
AddAssert("new combo positions preserved", getObjectNewCombos, () => Is.EqualTo(newCombos));
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> getObjects() => EditorBeatmap.HitObjects.OfType<OsuHitObject>();
|
||||
|
||||
private IEnumerable<bool> getObjectNewCombos() => getObjects().Select(ho => ho.NewCombo);
|
||||
}
|
||||
}
|
@ -172,6 +172,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointPathType(4, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStackingUpdatesPointsPosition()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
Vector2[] points =
|
||||
[
|
||||
new Vector2(200),
|
||||
new Vector2(300),
|
||||
new Vector2(500, 300),
|
||||
new Vector2(700, 200),
|
||||
new Vector2(500, 100)
|
||||
];
|
||||
|
||||
foreach (var point in points) addControlPointStep(point);
|
||||
|
||||
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);
|
||||
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
addAssertPointPositionChanged(points, i);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStackingUpdatesConnectionPosition()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
Vector2 connectionPosition;
|
||||
addControlPointStep(connectionPosition = new Vector2(300));
|
||||
addControlPointStep(new Vector2(600));
|
||||
|
||||
// Apply a big number in stacking so the person running the test can clearly see if it fails
|
||||
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
|
||||
|
||||
AddAssert($"Connection at {connectionPosition} changed",
|
||||
() => visualiser.Connections[0].Position,
|
||||
() => !Is.EqualTo(connectionPosition)
|
||||
);
|
||||
}
|
||||
|
||||
private void addAssertPointPositionChanged(Vector2[] points, int index)
|
||||
{
|
||||
AddAssert($"Point at {points.ElementAt(index)} changed",
|
||||
() => visualiser.Pieces[index].Position,
|
||||
() => !Is.EqualTo(points.ElementAt(index))
|
||||
);
|
||||
}
|
||||
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -24,14 +24,38 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestHotkeyHandling()
|
||||
{
|
||||
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
|
||||
AddStep("deselect everything", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
|
||||
AddUntilStep("no popover present", getPopover, () => Is.Null);
|
||||
|
||||
AddStep("select single circle",
|
||||
() => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
|
||||
AddAssert("only playfield centre origin rotation available", () =>
|
||||
{
|
||||
var popover = getPopover();
|
||||
var buttons = popover.ChildrenOfType<EditorRadioButton>();
|
||||
return buttons.Any(btn => btn.Text == "Selection centre" && !btn.Enabled.Value)
|
||||
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
|
||||
});
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", getPopover, () => Is.Null);
|
||||
|
||||
AddStep("select first three objects", () =>
|
||||
{
|
||||
@ -44,14 +68,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
|
||||
AddAssert("both origin rotation available", () =>
|
||||
{
|
||||
var popover = getPopover();
|
||||
var buttons = popover.ChildrenOfType<EditorRadioButton>();
|
||||
return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value)
|
||||
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
|
||||
});
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
|
||||
AddUntilStep("no popover present", getPopover, () => Is.Null);
|
||||
|
||||
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("add hitsounds", () =>
|
||||
{
|
||||
if (slider is null) return;
|
||||
if (slider == null) return;
|
||||
|
||||
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
|
||||
slider.Samples.Add(sample.With());
|
||||
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
if (slider is null || visualiser is null) return;
|
||||
if (slider == null || visualiser == null) return;
|
||||
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
|
||||
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
|
||||
@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
if (visualiser is null) return;
|
||||
if (visualiser == null) return;
|
||||
|
||||
MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
|
@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<int> pathVersion;
|
||||
private IBindable<int> stackHeight;
|
||||
|
||||
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
|
||||
{
|
||||
@ -56,6 +57,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
pathVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
|
||||
stackHeight.BindValueChanged(_ => updateConnectingPath());
|
||||
|
||||
updateConnectingPath();
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<float> hitObjectScale;
|
||||
private IBindable<int> stackHeight;
|
||||
|
||||
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
|
||||
{
|
||||
@ -105,6 +106,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
|
||||
hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
|
||||
stackHeight.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
updateMarkerDisplay();
|
||||
|
@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
foreach (var splitPoint in controlPointsToSplitAt)
|
||||
{
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null)
|
||||
continue;
|
||||
|
||||
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
|
||||
|
@ -78,13 +78,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
public override bool HandleReverse()
|
||||
{
|
||||
var hitObjects = EditorBeatmap.SelectedHitObjects;
|
||||
var hitObjects = EditorBeatmap.SelectedHitObjects
|
||||
.OfType<OsuHitObject>()
|
||||
.OrderBy(obj => obj.StartTime)
|
||||
.ToList();
|
||||
|
||||
double endTime = hitObjects.Max(h => h.GetEndTime());
|
||||
double startTime = hitObjects.Min(h => h.StartTime);
|
||||
|
||||
bool moreThanOneObject = hitObjects.Count > 1;
|
||||
|
||||
// the expectation is that even if the objects themselves are reversed temporally,
|
||||
// the position of new combos in the selection should remain the same.
|
||||
// preserve it for later before doing the reversal.
|
||||
var newComboOrder = hitObjects.Select(obj => obj.NewCombo).ToList();
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
if (moreThanOneObject)
|
||||
@ -97,6 +105,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
// re-order objects by start time again after reversing, and restore new combo flag positioning
|
||||
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
|
||||
|
||||
for (int i = 0; i < hitObjects.Count; ++i)
|
||||
hitObjects[i].NewCombo = newComboOrder[i];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void updateState()
|
||||
{
|
||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
|
||||
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
||||
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||
}
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
|
||||
{
|
||||
this.rotationHandler = rotationHandler;
|
||||
@ -59,13 +61,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
new RadioButton("Selection centre",
|
||||
selectionCentreButton = new RadioButton("Selection centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based rotation." : string.Empty;
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -76,6 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||
rotationOrigin.Items.First().Select();
|
||||
|
||||
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
|
||||
{
|
||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||
}, true);
|
||||
|
||||
rotationInfo.BindValueChanged(rotation =>
|
||||
{
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
|
||||
|
@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private EditorToolButton rotateButton = null!;
|
||||
|
||||
private Bindable<bool> canRotatePlayfieldOrigin = null!;
|
||||
private Bindable<bool> canRotateSelectionOrigin = null!;
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||
|
||||
public TransformToolboxGroup()
|
||||
@ -51,9 +54,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// aggregate two values into canRotate
|
||||
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
|
||||
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
||||
|
||||
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
|
||||
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
||||
|
||||
void updateCanRotateAggregate()
|
||||
{
|
||||
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
|
||||
}
|
||||
|
||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||
canRotate.BindTo(RotationHandler.CanRotate);
|
||||
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
|
||||
{
|
||||
animation.Origin = animation.Anchor = Anchor.BottomLeft;
|
||||
animation.Scale = new Vector2(0.51f); // close enough to stable
|
||||
// matches stable (https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34)
|
||||
animation.Scale = new Vector2(0.6f);
|
||||
});
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
@ -77,6 +78,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
|
||||
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
|
||||
|
@ -309,7 +309,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
match = shouldMatch;
|
||||
}
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo) => match;
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => match;
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
|
||||
}
|
||||
}
|
||||
|
@ -502,7 +502,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
public string? CustomValue { get; set; }
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo) => true;
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria) => true;
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
{
|
||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
this.getTargetContainer = getTargetContainer;
|
||||
|
||||
CanRotate.Value = true;
|
||||
CanRotateSelectionOrigin.Value = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
|
@ -0,0 +1,89 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneStoryboardWithIntro : PlayerTestScene
|
||||
{
|
||||
protected override bool HasCustomSteps => true;
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
var beatmap = new Beatmap();
|
||||
beatmap.HitObjects.Add(new HitCircle { StartTime = firstObjectStartTime });
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
{
|
||||
return base.CreateWorkingBeatmap(beatmap, createStoryboard(storyboardStartTime));
|
||||
}
|
||||
|
||||
private Storyboard createStoryboard(double startTime)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1);
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
return storyboard;
|
||||
}
|
||||
|
||||
private double firstObjectStartTime;
|
||||
private double storyboardStartTime;
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
|
||||
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
|
||||
AddStep("reset first hitobject time", () => firstObjectStartTime = 0);
|
||||
AddStep("reset storyboard start time", () => storyboardStartTime = 0);
|
||||
}
|
||||
|
||||
[TestCase(-5000, 0)]
|
||||
[TestCase(-5000, 30000)]
|
||||
public void TestStoryboardSingleSkip(double storyboardStart, double firstObject)
|
||||
{
|
||||
AddStep($"set storyboard start time to {storyboardStart}", () => storyboardStartTime = storyboardStart);
|
||||
AddStep($"set first object start time to {firstObject}", () => firstObjectStartTime = firstObject);
|
||||
CreateTest();
|
||||
|
||||
AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space));
|
||||
AddAssert("skip performed", () => Player.ChildrenOfType<SkipOverlay>().Any(s => s.SkipCount == 1));
|
||||
AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(firstObject - 2000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoryboardDoubleSkip()
|
||||
{
|
||||
AddStep("set storyboard start time to -11000", () => storyboardStartTime = -11000);
|
||||
AddStep("set first object start time to 11000", () => firstObjectStartTime = 11000);
|
||||
CreateTest();
|
||||
|
||||
AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space));
|
||||
AddAssert("skip performed", () => Player.ChildrenOfType<SkipOverlay>().Any(s => s.SkipCount == 1));
|
||||
AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
|
||||
|
||||
AddStep("skip", () => InputManager.Key(osuTK.Input.Key.Space));
|
||||
AddAssert("skip performed", () => Player.ChildrenOfType<SkipOverlay>().Any(s => s.SkipCount == 2));
|
||||
AddUntilStep("gameplay clock advanced", () => Player.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(9000));
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Login;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -72,9 +73,24 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
|
||||
assertAPIState(APIState.Online);
|
||||
assertDropdownState(UserAction.Online);
|
||||
|
||||
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
|
||||
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
|
||||
|
||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||
|
||||
assertDropdownState(UserAction.Online);
|
||||
AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb);
|
||||
assertDropdownState(UserAction.DoNotDisturb);
|
||||
}
|
||||
|
||||
private void assertDropdownState(UserAction state)
|
||||
{
|
||||
AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType<UserDropdown>().First().Current.Value, () => Is.EqualTo(state));
|
||||
}
|
||||
|
||||
private void assertAPIState(APIState expected) =>
|
||||
|
@ -14,13 +14,16 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Ranking.Expanded;
|
||||
using osu.Game.Screens.Ranking.Expanded.Statistics;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
@ -67,6 +70,40 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddAssert("play time displayed", () => this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPPShownAsProvisionalWhenBeatmapHasNoLeaderboard()
|
||||
{
|
||||
AddStep("show example score", () =>
|
||||
{
|
||||
var beatmap = createTestBeatmap(new RealmUser());
|
||||
beatmap.Status = BeatmapOnlineStatus.Graveyard;
|
||||
showPanel(TestResources.CreateTestScoreInfo(beatmap));
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () =>
|
||||
{
|
||||
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPPShownAsProvisionalWhenUnrankedModsArePresent()
|
||||
{
|
||||
AddStep("show example score", () =>
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()));
|
||||
score.Mods = score.Mods.Append(new OsuModDifficultyAdjust()).ToArray();
|
||||
showPanel(score);
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () =>
|
||||
{
|
||||
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWithDefaultDate()
|
||||
{
|
||||
|
@ -434,6 +434,9 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
if (!beatmaps.Any())
|
||||
throw new ArgumentException("No valid beatmap files found in the beatmap archive.");
|
||||
|
||||
return beatmaps;
|
||||
}
|
||||
}
|
||||
|
@ -175,6 +175,8 @@ namespace osu.Game.Graphics
|
||||
public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect);
|
||||
public static IconUsage EditorSound => get(OsuIconMapping.EditorSound);
|
||||
public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle);
|
||||
public static IconUsage Tortoise => get(OsuIconMapping.Tortoise);
|
||||
public static IconUsage Hare => get(OsuIconMapping.Hare);
|
||||
|
||||
private static IconUsage get(OsuIconMapping glyph) => new IconUsage((char)glyph, FONT_NAME);
|
||||
|
||||
@ -380,6 +382,12 @@ namespace osu.Game.Graphics
|
||||
|
||||
[Description(@"Editor/whistle")]
|
||||
EditorWhistle,
|
||||
|
||||
[Description(@"tortoise")]
|
||||
Tortoise,
|
||||
|
||||
[Description(@"hare")]
|
||||
Hare,
|
||||
}
|
||||
|
||||
public class OsuIconStore : ITextureStore, ITexturedGlyphLookupStore
|
||||
|
41
osu.Game/Graphics/Sprites/GlowingDrawable.cs
Normal file
41
osu.Game/Graphics/Sprites/GlowingDrawable.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public abstract partial class GlowingDrawable : BufferedContainer
|
||||
{
|
||||
// Inflate draw quad to prevent glow from trimming at the edges.
|
||||
// Padding won't suffice since it will affect drawable position in cases when it's not centered.
|
||||
protected override Quad ComputeScreenSpaceDrawQuad()
|
||||
=> base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(new Vector2(Blur.KernelSize(BlurSigma.X), Blur.KernelSize(BlurSigma.Y)));
|
||||
|
||||
public ColourInfo GlowColour
|
||||
{
|
||||
get => EffectColour;
|
||||
set
|
||||
{
|
||||
EffectColour = value;
|
||||
BackgroundColour = value.MultiplyAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
||||
protected GlowingDrawable()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
RedrawOnScale = false;
|
||||
DrawOriginal = true;
|
||||
Child = CreateDrawable();
|
||||
}
|
||||
|
||||
protected abstract Drawable CreateDrawable();
|
||||
}
|
||||
}
|
@ -4,24 +4,17 @@
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public partial class GlowingSpriteText : BufferedContainer, IHasText
|
||||
public partial class GlowingSpriteText : GlowingDrawable, IHasText
|
||||
{
|
||||
private const float blur_sigma = 3f;
|
||||
|
||||
// Inflate draw quad to prevent glow from trimming at the edges.
|
||||
// Padding won't suffice since it will affect text position in cases when it's not centered.
|
||||
protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma));
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
@ -47,16 +40,6 @@ namespace osu.Game.Graphics.Sprites
|
||||
set => text.Colour = value;
|
||||
}
|
||||
|
||||
public ColourInfo GlowColour
|
||||
{
|
||||
get => EffectColour;
|
||||
set
|
||||
{
|
||||
EffectColour = value;
|
||||
BackgroundColour = value.MultiplyAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 Spacing
|
||||
{
|
||||
get => text.Spacing;
|
||||
@ -76,20 +59,16 @@ namespace osu.Game.Graphics.Sprites
|
||||
}
|
||||
|
||||
public GlowingSpriteText()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
BlurSigma = new Vector2(blur_sigma);
|
||||
RedrawOnScale = false;
|
||||
DrawOriginal = true;
|
||||
EffectBlending = BlendingParameters.Additive;
|
||||
EffectPlacement = EffectPlacement.InFront;
|
||||
Child = text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable() => text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Localisation/ResultsScreenStrings.cs
Normal file
24
osu.Game/Localisation/ResultsScreenStrings.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class ResultsScreenStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.ResultsScreen";
|
||||
|
||||
/// <summary>
|
||||
/// "Performance points are not granted for this score because the beatmap is not ranked."
|
||||
/// </summary>
|
||||
public static LocalisableString NoPPForUnrankedBeatmaps => new TranslatableString(getKey(@"no_pp_for_unranked_beatmaps"), @"Performance points are not granted for this score because the beatmap is not ranked.");
|
||||
|
||||
/// <summary>
|
||||
/// "Performance points are not granted for this score because of unranked mods."
|
||||
/// </summary>
|
||||
public static LocalisableString NoPPForUnrankedMods => new TranslatableString(getKey(@"no_pp_for_unranked_mods"), @"Performance points are not granted for this score because of unranked mods.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -248,6 +248,9 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
isPlaying = false;
|
||||
currentBeatmap = null;
|
||||
currentScore = null;
|
||||
currentScoreProcessor = null;
|
||||
currentScoreToken = null;
|
||||
|
||||
if (state.HasPassed)
|
||||
currentState.State = SpectatedUserState.Passed;
|
||||
|
@ -678,16 +678,21 @@ namespace osu.Game
|
||||
/// <summary>
|
||||
/// Allows a maximum of one unhandled exception, per second of execution.
|
||||
/// </summary>
|
||||
private bool onExceptionThrown(Exception _)
|
||||
/// <returns>Whether to ignore the exception and continue running.</returns>
|
||||
private bool onExceptionThrown(Exception ex)
|
||||
{
|
||||
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
|
||||
|
||||
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
|
||||
if (Interlocked.Decrement(ref allowableExceptions) < 0)
|
||||
{
|
||||
Logger.Log("Too many unhandled exceptions, crashing out.");
|
||||
RulesetStore.TryDisableCustomRulesetsCausing(ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Log($"Unhandled exception has been allowed with {allowableExceptions} more allowable exceptions.");
|
||||
// restore the stock of allowable exceptions after a short delay.
|
||||
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
|
||||
|
||||
return continueExecution;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@ -30,15 +31,17 @@ namespace osu.Game.Overlays.Login
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private UserDropdown dropdown = null!;
|
||||
private UserDropdown? dropdown;
|
||||
|
||||
/// <summary>
|
||||
/// Called to request a hide of a parent displaying this container.
|
||||
/// </summary>
|
||||
public Action? RequestHide;
|
||||
|
||||
private IBindable<APIUser> user = null!;
|
||||
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly Bindable<UserStatus?> userStatus = new Bindable<UserStatus?>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
@ -61,11 +64,21 @@ namespace osu.Game.Overlays.Login
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(onlineStateChanged, true);
|
||||
|
||||
user = api.LocalUser.GetBoundCopy();
|
||||
user.BindValueChanged(u =>
|
||||
{
|
||||
status.UnbindBindings();
|
||||
status.BindTo(u.NewValue.Status);
|
||||
}, true);
|
||||
|
||||
status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
|
||||
}
|
||||
|
||||
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
|
||||
@ -144,9 +157,6 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
};
|
||||
|
||||
userStatus.BindTo(api.LocalUser.Value.Status);
|
||||
userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
|
||||
|
||||
dropdown.Current.BindValueChanged(action =>
|
||||
{
|
||||
switch (action.NewValue)
|
||||
@ -171,6 +181,7 @@ namespace osu.Game.Overlays.Login
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -180,6 +191,9 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
private void updateDropdownCurrent(UserStatus? status)
|
||||
{
|
||||
if (dropdown == null)
|
||||
return;
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case UserStatus.Online:
|
||||
|
@ -781,7 +781,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </remarks>>
|
||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
|
||||
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null)
|
||||
return false;
|
||||
|
||||
SelectAllModsButton.TriggerClick();
|
||||
|
@ -52,6 +52,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
private OsuTextFlowContainer headerText = null!;
|
||||
|
||||
private Bindable<Skin> currentSkin = null!;
|
||||
private Bindable<string> clipboardContent = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
@ -65,9 +66,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClipboard clipboard { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SkinEditorOverlay? skinEditorOverlay { get; set; }
|
||||
|
||||
@ -113,7 +111,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(EditorClipboard clipboard)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -224,6 +222,8 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
clipboardContent = clipboard.Content.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -243,7 +243,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
canCopy.Value = canCut.Value = SelectedComponents.Any();
|
||||
}, true);
|
||||
|
||||
clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true);
|
||||
clipboardContent.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true);
|
||||
|
||||
Show();
|
||||
|
||||
@ -495,7 +495,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
protected void Copy()
|
||||
{
|
||||
clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray());
|
||||
clipboardContent.Value = JsonConvert.SerializeObject(SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray());
|
||||
}
|
||||
|
||||
protected void Clone()
|
||||
@ -515,7 +515,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
var drawableInfo = JsonConvert.DeserializeObject<SerialisedDrawableInfo[]>(clipboard.Content.Value);
|
||||
var drawableInfo = JsonConvert.DeserializeObject<SerialisedDrawableInfo[]>(clipboardContent.Value);
|
||||
|
||||
if (drawableInfo == null)
|
||||
return;
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
CanRotate.Value = selectedItems.Count > 0;
|
||||
CanRotateSelectionOrigin.Value = selectedItems.Count > 0;
|
||||
}
|
||||
|
||||
private Drawable[]? objectsInRotation;
|
||||
|
@ -212,6 +212,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty;
|
||||
};
|
||||
}
|
||||
|
||||
TernaryStates = CreateTernaryButtons().ToArray();
|
||||
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
|
||||
|
||||
@ -244,6 +252,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
if (!timing.NewValue)
|
||||
setSelectTool();
|
||||
});
|
||||
|
||||
EditorBeatmap.HasTiming.BindValueChanged(hasTiming =>
|
||||
{
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.Disabled = !hasTiming.NewValue;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -18,11 +18,12 @@ namespace osu.Game.Rulesets.Filter
|
||||
/// in addition to the ones mandated by song select.
|
||||
/// </summary>
|
||||
/// <param name="beatmapInfo">The beatmap to test the criteria against.</param>
|
||||
/// <param name="criteria">The filter criteria.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the beatmap matches the ruleset-specific custom filtering criteria,
|
||||
/// <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
bool Matches(BeatmapInfo beatmapInfo);
|
||||
bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a single custom keyword criterion, given by the user via the song select search box.
|
||||
|
@ -1,7 +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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
@ -18,8 +20,7 @@ namespace osu.Game.Rulesets
|
||||
/// <summary>
|
||||
/// Retrieves the number of mania keys required to play the beatmap.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
int GetKeyCount(IBeatmapInfo beatmapInfo) => 0;
|
||||
int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null) => 0;
|
||||
|
||||
ILegacyScoreSimulator CreateLegacyScoreSimulator();
|
||||
}
|
||||
|
@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
// Edge-case rules (to match stable).
|
||||
if (type == PathType.PERFECT_CURVE)
|
||||
{
|
||||
int endPointLength = endPoint is null ? 0 : 1;
|
||||
int endPointLength = endPoint == null ? 0 : 1;
|
||||
|
||||
if (vertices.Length + endPointLength != 3)
|
||||
type = PathType.BEZIER;
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
public bool Equals(PathControlPoint other) => Position == other?.Position && Type == other.Type;
|
||||
|
||||
public override string ToString() => type is null
|
||||
public override string ToString() => type == null
|
||||
? $"Position={Position}"
|
||||
: $"Position={Position}, Type={type}";
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
var controlPoints = sliderPath.ControlPoints;
|
||||
|
||||
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList();
|
||||
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type == null).ToList();
|
||||
|
||||
// Inherited points after a linear point, as well as the first control point if it inherited,
|
||||
// should be treated as linear points, so their types are temporarily changed to linear.
|
||||
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
inheritedLinearPoints.ForEach(p => p.Type = null);
|
||||
|
||||
// Recalculate middle perfect curve control points at the end of the slider path.
|
||||
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any())
|
||||
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any())
|
||||
{
|
||||
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
|
||||
double lastSegmentEnd = segmentEnds[^1];
|
||||
|
@ -3,8 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
@ -13,17 +16,20 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
public class RealmRulesetStore : RulesetStore
|
||||
{
|
||||
private readonly RealmAccess realmAccess;
|
||||
public override IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
|
||||
|
||||
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
|
||||
|
||||
public RealmRulesetStore(RealmAccess realm, Storage? storage = null)
|
||||
public RealmRulesetStore(RealmAccess realmAccess, Storage? storage = null)
|
||||
: base(storage)
|
||||
{
|
||||
prepareDetachedRulesets(realm);
|
||||
this.realmAccess = realmAccess;
|
||||
prepareDetachedRulesets();
|
||||
informUserAboutBrokenRulesets();
|
||||
}
|
||||
|
||||
private void prepareDetachedRulesets(RealmAccess realmAccess)
|
||||
private void prepareDetachedRulesets()
|
||||
{
|
||||
realmAccess.Write(realm =>
|
||||
{
|
||||
@ -143,5 +149,48 @@ namespace osu.Game.Rulesets
|
||||
|
||||
instance.CreateBeatmapProcessor(converter.Convert());
|
||||
}
|
||||
|
||||
private void informUserAboutBrokenRulesets()
|
||||
{
|
||||
if (RulesetStorage == null)
|
||||
return;
|
||||
|
||||
foreach (string brokenRulesetDll in RulesetStorage.GetFiles(@".", @"*.dll.broken"))
|
||||
{
|
||||
Logger.Log($"Ruleset '{Path.GetFileNameWithoutExtension(brokenRulesetDll)}' has been disabled due to causing a crash.\n\n"
|
||||
+ "Please update the ruleset or report the issue to the developers of the ruleset if no updates are available.", level: LogLevel.Important);
|
||||
}
|
||||
}
|
||||
|
||||
internal void TryDisableCustomRulesetsCausing(Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stackTrace = new StackTrace(exception);
|
||||
|
||||
foreach (var frame in stackTrace.GetFrames())
|
||||
{
|
||||
var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly;
|
||||
if (declaringAssembly == null)
|
||||
continue;
|
||||
|
||||
if (UserRulesetAssemblies.Contains(declaringAssembly))
|
||||
{
|
||||
string sourceLocation = declaringAssembly.Location;
|
||||
string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken");
|
||||
|
||||
if (File.Exists(sourceLocation))
|
||||
{
|
||||
Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken.");
|
||||
File.Move(sourceLocation, destinationLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Attempt to trace back crash to custom ruleset failed: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ namespace osu.Game.Rulesets
|
||||
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
|
||||
|
||||
protected readonly Dictionary<Assembly, Type> LoadedAssemblies = new Dictionary<Assembly, Type>();
|
||||
protected readonly HashSet<Assembly> UserRulesetAssemblies = new HashSet<Assembly>();
|
||||
protected readonly Storage? RulesetStorage;
|
||||
|
||||
/// <summary>
|
||||
/// All available rulesets.
|
||||
@ -41,9 +43,9 @@ namespace osu.Game.Rulesets
|
||||
// to load as unable to locate the game core assembly.
|
||||
AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
|
||||
|
||||
var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
|
||||
if (rulesetStorage != null)
|
||||
loadUserRulesets(rulesetStorage);
|
||||
RulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
|
||||
if (RulesetStorage != null)
|
||||
loadUserRulesets(RulesetStorage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -105,7 +107,11 @@ namespace osu.Game.Rulesets
|
||||
var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
|
||||
|
||||
foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
|
||||
loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
|
||||
{
|
||||
var assembly = loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
|
||||
if (assembly != null)
|
||||
UserRulesetAssemblies.Add(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromDisk()
|
||||
@ -126,21 +132,25 @@ namespace osu.Game.Rulesets
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRulesetFromFile(string file)
|
||||
private Assembly? loadRulesetFromFile(string file)
|
||||
{
|
||||
string filename = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
|
||||
return;
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
addRuleset(Assembly.LoadFrom(file));
|
||||
var assembly = Assembly.LoadFrom(file);
|
||||
addRuleset(assembly);
|
||||
return assembly;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogFailedLoad(filename, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addRuleset(Assembly assembly)
|
||||
|
@ -18,6 +18,16 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
/// </summary>
|
||||
public IRulesetInfo SourceRuleset { get; set; } = new RulesetInfo();
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap drain rate.
|
||||
/// </summary>
|
||||
public float DrainRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap approach rate.
|
||||
/// </summary>
|
||||
public float ApproachRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap circle size.
|
||||
/// </summary>
|
||||
@ -41,8 +51,6 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
/// </summary>
|
||||
public int TotalObjectCount { get; set; }
|
||||
|
||||
float IBeatmapDifficultyInfo.DrainRate => 0;
|
||||
float IBeatmapDifficultyInfo.ApproachRate => 0;
|
||||
double IBeatmapDifficultyInfo.SliderMultiplier => 0;
|
||||
double IBeatmapDifficultyInfo.SliderTickRate => 0;
|
||||
|
||||
@ -51,6 +59,8 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = beatmap.BeatmapInfo.Ruleset,
|
||||
DrainRate = beatmap.Difficulty.DrainRate,
|
||||
ApproachRate = beatmap.Difficulty.ApproachRate,
|
||||
CircleSize = beatmap.Difficulty.CircleSize,
|
||||
OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
|
||||
EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration),
|
||||
@ -60,6 +70,8 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromBeatmapInfo(IBeatmapInfo beatmapInfo) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = beatmapInfo.Ruleset,
|
||||
DrainRate = beatmapInfo.Difficulty.DrainRate,
|
||||
ApproachRate = beatmapInfo.Difficulty.ApproachRate,
|
||||
CircleSize = beatmapInfo.Difficulty.CircleSize,
|
||||
OverallDifficulty = beatmapInfo.Difficulty.OverallDifficulty,
|
||||
EndTimeObjectCount = beatmapInfo.EndTimeObjectCount,
|
||||
|
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
|
||||
modAcronym.Text = value.Acronym;
|
||||
modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question;
|
||||
|
||||
if (value.Icon is null)
|
||||
if (value.Icon == null)
|
||||
{
|
||||
modIcon.FadeOut();
|
||||
modAcronym.FadeIn();
|
||||
|
@ -33,9 +33,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
|
||||
private Drawable icon = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
public EditorRadioButton(RadioButton button)
|
||||
{
|
||||
Button = button;
|
||||
@ -76,8 +73,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
Selected?.Invoke(Button);
|
||||
};
|
||||
|
||||
editorBeatmap?.HasTiming.BindValueChanged(hasTiming => Button.Selected.Disabled = !hasTiming.NewValue, true);
|
||||
|
||||
Button.Selected.BindDisabledChanged(disabled => Enabled.Value = !disabled, true);
|
||||
updateSelectionState();
|
||||
}
|
||||
@ -99,6 +94,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
X = 40f
|
||||
};
|
||||
|
||||
public LocalisableString TooltipText => Enabled.Value ? string.Empty : "Add at least one timing point first!";
|
||||
public LocalisableString TooltipText => Button.TooltipText;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
{
|
||||
@ -11,6 +12,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this <see cref="RadioButton"/> is selected.
|
||||
/// Disable this bindable to disable the button.
|
||||
/// </summary>
|
||||
public readonly BindableBool Selected;
|
||||
|
||||
@ -50,5 +52,8 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
/// Deselects this <see cref="RadioButton"/>.
|
||||
/// </summary>
|
||||
public void Deselect() => Selected.Value = false;
|
||||
|
||||
// Tooltip text that will be shown when hovered over
|
||||
public LocalisableString TooltipText { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -512,7 +512,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected virtual void OnBlueprintDeselected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
|
||||
if (SelectionBlueprints.Contains(blueprint))
|
||||
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
|
||||
|
||||
SelectionHandler.HandleDeselected(blueprint);
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private void load()
|
||||
{
|
||||
if (rotationHandler != null)
|
||||
canRotate.BindTo(rotationHandler.CanRotate);
|
||||
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate(), true);
|
||||
}
|
||||
|
@ -13,9 +13,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
public partial class SelectionRotationHandler : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the rotation can currently be performed.
|
||||
/// Whether rotation anchored by the selection origin can currently be performed.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanRotate { get; private set; } = new BindableBool();
|
||||
public Bindable<bool> CanRotateSelectionOrigin { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether rotation anchored by the center of the playfield can currently be performed.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanRotatePlayfieldOrigin { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a single, instant, atomic rotation operation.
|
||||
|
@ -1,52 +1,16 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.Break
|
||||
{
|
||||
public partial class BlurredIcon : BufferedContainer
|
||||
public partial class BlurredIcon : GlowIcon
|
||||
{
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
public IconUsage Icon
|
||||
{
|
||||
set => icon.Icon = value;
|
||||
get => icon.Icon;
|
||||
}
|
||||
|
||||
public override Vector2 Size
|
||||
{
|
||||
set
|
||||
{
|
||||
icon.Size = value;
|
||||
base.Size = value + BlurSigma * 5;
|
||||
ForceRedraw();
|
||||
}
|
||||
get => base.Size;
|
||||
}
|
||||
|
||||
public BlurredIcon()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
Child = icon = new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = colours.BlueLighter;
|
||||
EffectBlending = BlendingParameters.Additive;
|
||||
DrawOriginal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.Break
|
||||
|
||||
private const int blurred_icon_blur_sigma = 20;
|
||||
private const int blurred_icon_size = 130;
|
||||
private const float blurred_icon_final_offset = 0.35f;
|
||||
private const float blurred_icon_final_offset = 0.38f;
|
||||
private const float blurred_icon_offscreen_offset = 0.7f;
|
||||
|
||||
private readonly GlowIcon leftGlowIcon;
|
||||
|
@ -3,64 +3,45 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.Break
|
||||
{
|
||||
public partial class GlowIcon : Container
|
||||
public partial class GlowIcon : GlowingDrawable
|
||||
{
|
||||
private readonly SpriteIcon spriteIcon;
|
||||
private readonly BlurredIcon blurredIcon;
|
||||
|
||||
public override Vector2 Size
|
||||
{
|
||||
get => base.Size;
|
||||
set
|
||||
{
|
||||
blurredIcon.Size = spriteIcon.Size = value;
|
||||
blurredIcon.ForceRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 BlurSigma
|
||||
{
|
||||
get => blurredIcon.BlurSigma;
|
||||
set => blurredIcon.BlurSigma = value;
|
||||
}
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
public IconUsage Icon
|
||||
{
|
||||
get => spriteIcon.Icon;
|
||||
set => spriteIcon.Icon = blurredIcon.Icon = value;
|
||||
set => icon.Icon = value;
|
||||
get => icon.Icon;
|
||||
}
|
||||
|
||||
public new Vector2 Size
|
||||
{
|
||||
set => icon.Size = value;
|
||||
get => icon.Size;
|
||||
}
|
||||
|
||||
public GlowIcon()
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
blurredIcon = new BlurredIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
blurredIcon.Colour = colours.Blue;
|
||||
GlowColour = colours.BlueLighter;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable() => icon = new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -303,13 +303,13 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
labelEarly.Child = new SpriteIcon
|
||||
{
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.ShippingFast,
|
||||
Icon = OsuIcon.Hare
|
||||
};
|
||||
|
||||
labelLate.Child = new SpriteIcon
|
||||
{
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.Bicycle,
|
||||
Icon = OsuIcon.Tortoise
|
||||
};
|
||||
|
||||
break;
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME;
|
||||
|
||||
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
|
||||
if (StartTime < -10000 && GameplayClock.CurrentTime < 0 && skipTarget > 6000)
|
||||
// double skip exception for storyboards with very long intros
|
||||
skipTarget = 0;
|
||||
|
||||
|
@ -152,6 +152,10 @@ namespace osu.Game.Screens.Play
|
||||
Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important);
|
||||
break;
|
||||
|
||||
case @"invalid beatmap hash":
|
||||
Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important);
|
||||
break;
|
||||
|
||||
case @"expired token":
|
||||
Logger.Log($"Your system clock is set incorrectly. Please check your system time, date and timezone.\n\n{whatWillHappen}", level: LogLevel.Important);
|
||||
break;
|
||||
|
@ -4,20 +4,26 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
{
|
||||
public partial class PerformanceStatistic : StatisticDisplay
|
||||
public partial class PerformanceStatistic : StatisticDisplay, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText { get; private set; }
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
|
||||
private readonly Bindable<int> performance = new Bindable<int>();
|
||||
@ -37,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
{
|
||||
if (score.PP.HasValue)
|
||||
{
|
||||
setPerformanceValue(score.PP.Value);
|
||||
setPerformanceValue(score, score.PP.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -52,15 +58,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
|
||||
var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false);
|
||||
|
||||
Schedule(() => setPerformanceValue(result.Total));
|
||||
Schedule(() => setPerformanceValue(score, result.Total));
|
||||
}, cancellationToken ?? default);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPerformanceValue(double? pp)
|
||||
private void setPerformanceValue(ScoreInfo scoreInfo, double? pp)
|
||||
{
|
||||
if (pp.HasValue)
|
||||
{
|
||||
performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero);
|
||||
|
||||
if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints())
|
||||
{
|
||||
Alpha = 0.5f;
|
||||
TooltipText = ResultsScreenStrings.NoPPForUnrankedBeatmaps;
|
||||
}
|
||||
else if (scoreInfo.Mods.Any(m => !m.Ranked))
|
||||
{
|
||||
Alpha = 0.5f;
|
||||
TooltipText = ResultsScreenStrings.NoPPForUnrankedMods;
|
||||
}
|
||||
else
|
||||
{
|
||||
Alpha = 1f;
|
||||
TooltipText = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Appear()
|
||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);
|
||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo, criteria);
|
||||
|
||||
return match;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -75,6 +76,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
private IBindable<StarDifficulty?> starDifficultyBindable = null!;
|
||||
private CancellationTokenSource? starDifficultyCancellationSource;
|
||||
|
||||
@ -185,6 +189,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateKeyCount());
|
||||
mods.BindValueChanged(_ => updateKeyCount());
|
||||
}
|
||||
|
||||
protected override void Selected()
|
||||
@ -255,7 +260,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
|
||||
|
||||
keyCountText.Alpha = 1;
|
||||
keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo)}K]";
|
||||
keyCountText.Text = $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods.Value)}K]";
|
||||
}
|
||||
else
|
||||
keyCountText.Alpha = 0;
|
||||
|
@ -199,7 +199,7 @@ namespace osu.Game.Screens.Select.Details
|
||||
// For the time being, the key count is static no matter what, because:
|
||||
// a) The method doesn't have knowledge of the active keymods. Doing so may require considerations for filtering.
|
||||
// b) Using the difficulty adjustment mod to adjust OD doesn't have an effect on conversion.
|
||||
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo);
|
||||
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo, mods.Value);
|
||||
|
||||
FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania;
|
||||
FirstValue.Value = (keyCount, keyCount);
|
||||
|
@ -4,7 +4,9 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -22,6 +24,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -65,6 +68,7 @@ namespace osu.Game.Screens.Select
|
||||
Sort = sortMode.Value,
|
||||
AllowConvertedBeatmaps = showConverted.Value,
|
||||
Ruleset = ruleset.Value,
|
||||
Mods = mods.Value,
|
||||
CollectionBeatmapMD5Hashes = collectionDropdown.Current.Value?.Collection?.PerformRead(c => c.BeatmapMD5Hashes).ToImmutableHashSet()
|
||||
};
|
||||
|
||||
@ -84,7 +88,7 @@ namespace osu.Game.Screens.Select
|
||||
base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config)
|
||||
private void load(OsuColour colours, OsuConfigManager config)
|
||||
{
|
||||
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
|
||||
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
|
||||
@ -214,8 +218,18 @@ namespace osu.Game.Screens.Select
|
||||
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
|
||||
maximumStars.ValueChanged += _ => updateCriteria();
|
||||
|
||||
ruleset.BindTo(parentRuleset);
|
||||
ruleset.BindValueChanged(_ => updateCriteria());
|
||||
mods.BindValueChanged(m =>
|
||||
{
|
||||
// Mods are updated once by the mod select overlay when song select is entered,
|
||||
// regardless of if there are any mods or any changes have taken place.
|
||||
// Updating the criteria here so early triggers a re-ordering of panels on song select, via... some mechanism.
|
||||
// Todo: Investigate/fix and potentially remove this.
|
||||
if (m.NewValue.SequenceEqual(m.OldValue))
|
||||
return;
|
||||
|
||||
updateCriteria();
|
||||
});
|
||||
|
||||
groupMode.BindValueChanged(_ => updateCriteria());
|
||||
sortMode.BindValueChanged(_ => updateCriteria());
|
||||
@ -239,7 +253,11 @@ namespace osu.Game.Screens.Select
|
||||
searchTextBox.HoldFocus = true;
|
||||
}
|
||||
|
||||
private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
|
||||
|
||||
private readonly Bindable<bool> showConverted = new Bindable<bool>();
|
||||
private readonly Bindable<double> minimumStars = new BindableDouble();
|
||||
|
@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@ -50,6 +51,7 @@ namespace osu.Game.Screens.Select
|
||||
public OptionalTextFilter[] SearchTerms = Array.Empty<OptionalTextFilter>();
|
||||
|
||||
public RulesetInfo? Ruleset;
|
||||
public IReadOnlyList<Mod>? Mods;
|
||||
public bool AllowConvertedBeatmaps;
|
||||
|
||||
private string searchText = string.Empty;
|
||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
// skins without a skin.ini are supposed to import using the "latest version" spec.
|
||||
// see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298
|
||||
newLines.Add($"Version: {SkinConfiguration.LATEST_VERSION}");
|
||||
newLines.Add(FormattableString.Invariant($"Version: {SkinConfiguration.LATEST_VERSION}"));
|
||||
|
||||
// In the case a skin doesn't have a skin.ini yet, let's create one.
|
||||
writeNewSkinIni();
|
||||
|
@ -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 System;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics;
|
||||
@ -45,10 +46,32 @@ namespace osu.Game.Storyboards
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsStartTime => timelines.Min(static t => t.StartTime);
|
||||
public double CommandsStartTime
|
||||
{
|
||||
get
|
||||
{
|
||||
double min = double.MaxValue;
|
||||
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
min = Math.Min(min, timelines[i].StartTime);
|
||||
|
||||
return min;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsEndTime => timelines.Max(static t => t.EndTime);
|
||||
public double CommandsEndTime
|
||||
{
|
||||
get
|
||||
{
|
||||
double max = double.MinValue;
|
||||
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
max = Math.Max(max, timelines[i].EndTime);
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsDuration => CommandsEndTime - CommandsStartTime;
|
||||
@ -60,7 +83,19 @@ namespace osu.Game.Storyboards
|
||||
public virtual double EndTime => CommandsEndTime;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasCommands => timelines.Any(static t => t.HasCommands);
|
||||
public bool HasCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
{
|
||||
if (timelines[i].HasCommands)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
|
||||
{
|
||||
|
@ -85,23 +85,12 @@ namespace osu.Game.Storyboards
|
||||
{
|
||||
get
|
||||
{
|
||||
double latestEndTime = double.MaxValue;
|
||||
|
||||
// Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex.
|
||||
if (loops.Count == 0)
|
||||
{
|
||||
// Take the minimum time of all the potential "death" reasons.
|
||||
latestEndTime = calculateOptimisedEndTime(TimelineGroup);
|
||||
}
|
||||
|
||||
// If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue
|
||||
// and thus conservativeEndTime will be used.
|
||||
double conservativeEndTime = TimelineGroup.EndTime;
|
||||
double latestEndTime = TimelineGroup.EndTime;
|
||||
|
||||
foreach (var l in loops)
|
||||
conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
|
||||
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
|
||||
|
||||
return Math.Min(latestEndTime, conservativeEndTime);
|
||||
return latestEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,47 +194,6 @@ namespace osu.Game.Storyboards
|
||||
return commands;
|
||||
}
|
||||
|
||||
private static double calculateOptimisedEndTime(CommandTimelineGroup timelineGroup)
|
||||
{
|
||||
// Here we are starting from maximum value and trying to minimise the end time on each step.
|
||||
// There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0.
|
||||
double[] deathTimes =
|
||||
{
|
||||
double.MaxValue, // alpha
|
||||
double.MaxValue, // colour alpha
|
||||
double.MaxValue, // scale
|
||||
double.MaxValue, // scale x
|
||||
double.MaxValue, // scale y
|
||||
};
|
||||
|
||||
// The loops below are following the same pattern.
|
||||
// We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row
|
||||
// so we are saving the earliest of them.
|
||||
foreach (var alphaCommand in timelineGroup.Alpha.Commands)
|
||||
{
|
||||
if (alphaCommand.EndValue == 0)
|
||||
// commands are ordered by the start time, however end time may vary. Save the earliest.
|
||||
deathTimes[0] = Math.Min(alphaCommand.EndTime, deathTimes[0]);
|
||||
else
|
||||
// If value isn't 0 (sprite becomes visible again), revert the saved state.
|
||||
deathTimes[0] = double.MaxValue;
|
||||
}
|
||||
|
||||
foreach (var colourCommand in timelineGroup.Colour.Commands)
|
||||
deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue;
|
||||
|
||||
foreach (var scaleCommand in timelineGroup.Scale.Commands)
|
||||
deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue;
|
||||
|
||||
foreach (var scaleCommand in timelineGroup.VectorScale.Commands)
|
||||
{
|
||||
deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue;
|
||||
deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue;
|
||||
}
|
||||
|
||||
return deathTimes.Min();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Path}, {Origin}, {InitialPosition}";
|
||||
|
||||
|
@ -35,8 +35,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.306.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.321.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.329.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.410.0" />
|
||||
<PackageReference Include="Sentry" Version="3.41.3" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.36.0" />
|
||||
|
@ -23,6 +23,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.306.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.329.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user