mirror of
https://github.com/ppy/osu.git
synced 2024-12-13 08:32:57 +08:00
Merge https://github.com/ppy/osu into std_mod_bloom
This commit is contained in:
commit
28879529db
14
.github/workflows/diffcalc.yml
vendored
14
.github/workflows/diffcalc.yml
vendored
@ -273,22 +273,23 @@ jobs:
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||
|
||||
- name: Extract
|
||||
run: |
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
|
||||
@ -304,22 +305,23 @@ jobs:
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
path: ${{ steps.query.outputs.DATA_PKG }}
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
|
||||
|
||||
- name: Extract
|
||||
run: |
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
|
||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
|
||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -1,5 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csharp"
|
||||
"editorconfig.editorconfig",
|
||||
"ms-dotnettools.csdevkit"
|
||||
]
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ Please make sure you have the following prerequisites:
|
||||
|
||||
- A desktop platform with the [.NET 8.0 SDK](https://dotnet.microsoft.com/download) installed.
|
||||
|
||||
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed.
|
||||
When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) plugin installed.
|
||||
|
||||
### Downloading the source code
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1007.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -279,10 +279,12 @@ namespace osu.Desktop
|
||||
|
||||
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
|
||||
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
|
||||
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
|
||||
if (str.Length < 2)
|
||||
return str.PadRight(2, '\u200B');
|
||||
// Also, spaces don't count. Because reasons, clearly.
|
||||
// That all seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
|
||||
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end. After making sure to trim whitespace.
|
||||
string trimmed = str.Trim();
|
||||
if (trimmed.Length < 2)
|
||||
return trimmed.PadRight(2, '\u200B');
|
||||
|
||||
if (Encoding.UTF8.GetByteCount(str) <= 128)
|
||||
return str;
|
||||
|
@ -24,7 +24,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.1" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||
</ItemGroup>
|
||||
|
@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private bool isSplittable(PathControlPointPiece<T> p) =>
|
||||
// A hit object can only be split on control points which connect two different path segments.
|
||||
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
|
||||
p.ControlPoint.Type.HasValue && p.ControlPoint != controlPoints.FirstOrDefault() && p.ControlPoint != controlPoints.LastOrDefault();
|
||||
|
||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
|
@ -240,39 +240,74 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
points = originalConvexHull!;
|
||||
|
||||
foreach (var point in points)
|
||||
{
|
||||
scale = clampToBound(scale, point, Vector2.Zero);
|
||||
scale = clampToBound(scale, point, OsuPlayfield.BASE_SIZE);
|
||||
}
|
||||
scale = clampToBounds(scale, point, Vector2.Zero, OsuPlayfield.BASE_SIZE);
|
||||
|
||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
return scale;
|
||||
|
||||
float minPositiveComponent(Vector2 v) => MathF.Min(v.X < 0 ? float.PositiveInfinity : v.X, v.Y < 0 ? float.PositiveInfinity : v.Y);
|
||||
|
||||
Vector2 clampToBound(Vector2 s, Vector2 p, Vector2 bound)
|
||||
// Clamps the scale vector s such that the point p scaled by s is within the rectangle defined by lowerBounds and upperBounds
|
||||
Vector2 clampToBounds(Vector2 s, Vector2 p, Vector2 lowerBounds, Vector2 upperBounds)
|
||||
{
|
||||
p -= actualOrigin;
|
||||
bound -= actualOrigin;
|
||||
lowerBounds -= actualOrigin;
|
||||
upperBounds -= actualOrigin;
|
||||
// a.X is the rotated X component of p with respect to the X bounds
|
||||
// a.Y is the rotated X component of p with respect to the Y bounds
|
||||
// b.X is the rotated Y component of p with respect to the X bounds
|
||||
// b.Y is the rotated Y component of p with respect to the Y bounds
|
||||
var a = new Vector2(cos * cos * p.X - sin * cos * p.Y, -sin * cos * p.X + sin * sin * p.Y);
|
||||
var b = new Vector2(sin * sin * p.X + sin * cos * p.Y, sin * cos * p.X + cos * cos * p.Y);
|
||||
|
||||
float sLowerBound, sUpperBound;
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
s.X = MathF.Min(scale.X, minPositiveComponent(Vector2.Divide(bound - b, a)));
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
|
||||
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
s.Y = MathF.Min(scale.Y, minPositiveComponent(Vector2.Divide(bound - a, b)));
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
|
||||
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
s = Vector2.ComponentMin(s, s * minPositiveComponent(Vector2.Divide(bound, a * s.X + b * s.Y)));
|
||||
// Here we compute the bounds for the magnitude multiplier of the scale vector
|
||||
// Therefore the ratio s.X / s.Y will be maintained
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
|
||||
s.X = s.X < 0
|
||||
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||
s.Y = s.Y < 0
|
||||
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||
break;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// Computes the bounds for the magnitude of the scaled point p with respect to the bounds lowerBounds and upperBounds
|
||||
(float, float) computeBounds(Vector2 lowerBounds, Vector2 upperBounds, Vector2 p)
|
||||
{
|
||||
var sLowerBounds = Vector2.Divide(lowerBounds, p);
|
||||
var sUpperBounds = Vector2.Divide(upperBounds, p);
|
||||
|
||||
// If the point is negative, then the bounds are flipped
|
||||
if (p.X < 0)
|
||||
(sLowerBounds.X, sUpperBounds.X) = (sUpperBounds.X, sLowerBounds.X);
|
||||
if (p.Y < 0)
|
||||
(sLowerBounds.Y, sUpperBounds.Y) = (sUpperBounds.Y, sLowerBounds.Y);
|
||||
|
||||
// If the point is at zero, then any scale will have no effect on the point so the bounds are infinite
|
||||
// The float division would already give us infinity for the bounds, but the sign is not consistent so we have to manually set it
|
||||
if (Precision.AlmostEquals(p.X, 0))
|
||||
(sLowerBounds.X, sUpperBounds.X) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||
if (Precision.AlmostEquals(p.Y, 0))
|
||||
(sLowerBounds.Y, sUpperBounds.Y) = (float.NegativeInfinity, float.PositiveInfinity);
|
||||
|
||||
return (MathF.Max(sLowerBounds.X, sLowerBounds.Y), MathF.Min(sUpperBounds.X, sUpperBounds.Y));
|
||||
}
|
||||
}
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
|
@ -175,12 +175,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
axisBindable.Disabled = !available;
|
||||
}
|
||||
|
||||
private void updateMaxScale()
|
||||
private void updateMinMaxScale()
|
||||
{
|
||||
if (!scaleHandler.OriginalSurroundingQuad.HasValue)
|
||||
return;
|
||||
|
||||
const float min_scale = 0.5f;
|
||||
const float max_scale = 10;
|
||||
|
||||
var scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(max_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||
|
||||
if (!scaleInfo.Value.XAxis)
|
||||
@ -189,12 +191,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
scale.Y = max_scale;
|
||||
|
||||
scaleInputBindable.MaxValue = MathF.Max(1, MathF.Min(scale.X, scale.Y));
|
||||
|
||||
scale = scaleHandler.ClampScaleToPlayfieldBounds(new Vector2(min_scale), getOriginPosition(scaleInfo.Value), getAdjustAxis(scaleInfo.Value), getRotation(scaleInfo.Value));
|
||||
|
||||
if (!scaleInfo.Value.XAxis)
|
||||
scale.X = min_scale;
|
||||
if (!scaleInfo.Value.YAxis)
|
||||
scale.Y = min_scale;
|
||||
|
||||
scaleInputBindable.MinValue = MathF.Min(1, MathF.Max(scale.X, scale.Y));
|
||||
}
|
||||
|
||||
private void setOrigin(ScaleOrigin origin)
|
||||
{
|
||||
scaleInfo.Value = scaleInfo.Value with { Origin = origin };
|
||||
updateMaxScale();
|
||||
updateMinMaxScale();
|
||||
updateAxisCheckBoxesEnabled();
|
||||
}
|
||||
|
||||
@ -226,14 +237,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void setAxis(bool x, bool y)
|
||||
{
|
||||
scaleInfo.Value = scaleInfo.Value with { XAxis = x, YAxis = y };
|
||||
updateMaxScale();
|
||||
updateMinMaxScale();
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
scaleHandler.Begin();
|
||||
updateMaxScale();
|
||||
updateMinMaxScale();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -6,10 +6,12 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
@ -81,6 +83,38 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
||||
AddAssert("notification posted", () => notificationOverlay.AllNotifications.OfType<SimpleNotification>().Any(n => n.Text == DailyChallengeStrings.ChallengeEndedNotification));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConclusionNotificationDoesNotFireOnDisconnect()
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = { Value = 1234 },
|
||||
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||
{
|
||||
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
}
|
||||
},
|
||||
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
|
||||
Category = { Value = RoomCategory.DailyChallenge }
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||
AddStep("disconnect from metadata server", () => metadataClient.Disconnect());
|
||||
AddUntilStep("wait for disconnection", () => metadataClient.DailyChallengeInfo.Value, () => Is.Null);
|
||||
AddAssert("no notification posted", () => notificationOverlay.AllNotifications, () => Is.Empty);
|
||||
AddStep("reconnect to metadata server", () => metadataClient.Reconnect());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,5 +79,114 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
trackChangeQueue.Peek().changeDirection == TrackChangeDirection.Next);
|
||||
AddAssert("track actually changed", () => !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShuffleBackwards()
|
||||
{
|
||||
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||
|
||||
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||
|
||||
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||
{
|
||||
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||
beatmap.Length = 60_000;
|
||||
}));
|
||||
|
||||
AddStep("bind to track change", () =>
|
||||
{
|
||||
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||
});
|
||||
|
||||
AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000));
|
||||
AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000);
|
||||
|
||||
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||
AddAssert("no track change", () => trackChangeQueue.Count == 0);
|
||||
AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000);
|
||||
|
||||
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||
|
||||
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||
AddUntilStep("track changed", () => trackChangeQueue.Count == 2);
|
||||
|
||||
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||
AddUntilStep("track changed", () =>
|
||||
trackChangeQueue.Count == 3 && !trackChangeQueue.ElementAt(1).working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShuffleForwards()
|
||||
{
|
||||
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||
|
||||
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||
|
||||
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||
{
|
||||
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||
beatmap.Length = 60_000;
|
||||
}));
|
||||
|
||||
AddStep("bind to track change", () =>
|
||||
{
|
||||
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||
});
|
||||
|
||||
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||
|
||||
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||
AddUntilStep("track changed", () => trackChangeQueue.Count == 2);
|
||||
|
||||
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||
AddUntilStep("track changed", () =>
|
||||
trackChangeQueue.Count == 3 && !trackChangeQueue.ElementAt(1).working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShuffleBackAndForth()
|
||||
{
|
||||
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||
|
||||
AddStep("enable shuffle", () => Game.MusicController.Shuffle.Value = true);
|
||||
|
||||
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||
AddStep("ensure nonzero track duration", () => Game.Realm.Write(r =>
|
||||
{
|
||||
// this was already supposed to be non-zero (see innards of `TestResources.CreateTestBeatmapSetInfo()`),
|
||||
// but the non-zero value is being overwritten *to* zero by `BeatmapUpdater`.
|
||||
// do a bit of a hack to change it back again - otherwise tracks are going to switch instantly and we won't be able to assert anything sane anymore.
|
||||
foreach (var beatmap in r.All<BeatmapInfo>().Where(b => b.Length == 0))
|
||||
beatmap.Length = 60_000;
|
||||
}));
|
||||
|
||||
AddStep("bind to track change", () =>
|
||||
{
|
||||
trackChangeQueue = new Queue<(IWorkingBeatmap, TrackChangeDirection)>();
|
||||
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
|
||||
});
|
||||
|
||||
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
|
||||
AddUntilStep("track changed", () => trackChangeQueue.Count == 1);
|
||||
|
||||
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
|
||||
AddUntilStep("track changed", () =>
|
||||
trackChangeQueue.Count == 2 && !trackChangeQueue.First().working.BeatmapInfo.Equals(trackChangeQueue.Last().working.BeatmapInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("select EZ mod", () =>
|
||||
{
|
||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||
SelectedMods.Value = new[] { ruleset.CreateMod<ModEasy>() };
|
||||
advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModEasy>() };
|
||||
});
|
||||
|
||||
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
|
||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("select HR mod", () =>
|
||||
{
|
||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||
SelectedMods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
|
||||
advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
|
||||
});
|
||||
|
||||
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
|
||||
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
|
||||
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
|
||||
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
|
||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
||||
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||
});
|
||||
|
||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
|
||||
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
|
||||
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
|
||||
SelectedMods.Value = new[] { difficultyAdjustMod };
|
||||
advancedStats.Mods.Value = new[] { difficultyAdjustMod };
|
||||
});
|
||||
|
||||
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
|
||||
|
@ -285,7 +285,8 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r => r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
||||
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r =>
|
||||
r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
|
||||
|
||||
/// <summary>
|
||||
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
|
||||
@ -313,6 +314,23 @@ namespace osu.Game.Beatmaps
|
||||
});
|
||||
}
|
||||
|
||||
public void ResetAllOffsets()
|
||||
{
|
||||
const string reset_complete_message = "All offsets have been reset!";
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var items = r.All<BeatmapInfo>();
|
||||
|
||||
foreach (var beatmap in items)
|
||||
{
|
||||
if (beatmap.UserSettings.Offset != 0)
|
||||
beatmap.UserSettings.Offset = 0;
|
||||
}
|
||||
|
||||
PostNotification?.Invoke(new ProgressCompletionNotification { Text = reset_complete_message });
|
||||
});
|
||||
}
|
||||
|
||||
public void Delete(Expression<Func<BeatmapSetInfo, bool>>? filter = null, bool silent = false)
|
||||
{
|
||||
Realm.Run(r =>
|
||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
if (detachedBeatmapSets.Count > 0 && sender.Count == 0)
|
||||
if (sender is RealmResetEmptySet<BeatmapSetInfo>)
|
||||
{
|
||||
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
|
||||
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
|
||||
|
@ -568,7 +568,7 @@ namespace osu.Game.Database
|
||||
lock (notificationsResetMap)
|
||||
{
|
||||
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
|
||||
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null));
|
||||
notificationsResetMap.Add(action, () => callback(new RealmResetEmptySet<T>(), null));
|
||||
}
|
||||
|
||||
return RegisterCustomSubscription(action);
|
||||
|
@ -12,7 +12,13 @@ using Realms.Schema;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public class EmptyRealmSet<T> : IRealmCollection<T>
|
||||
/// <summary>
|
||||
/// This can arrive in <see cref="RealmAccess.RegisterForNotifications{T}"/> callbacks to imply that realm access has been reset.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usually implies that the original database may return soon and the callback can usually be silently ignored.
|
||||
///</remarks>
|
||||
public class RealmResetEmptySet<T> : IRealmCollection<T>
|
||||
{
|
||||
private IList<T> emptySet => Array.Empty<T>();
|
||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapVideos => new TranslatableString(getKey(@"beatmap_videos"), @"Are you sure you want to delete all beatmaps videos? This cannot be undone!");
|
||||
|
||||
/// <summary>
|
||||
/// "Are you sure you want to reset all local beatmap offsets? This cannot be undone!"
|
||||
/// </summary>
|
||||
public static LocalisableString Offsets => new TranslatableString(getKey(@"offsets"), @"Are you sure you want to reset all local beatmap offsets? This cannot be undone!");
|
||||
|
||||
/// <summary>
|
||||
/// "Are you sure you want to delete all skins? This cannot be undone!"
|
||||
/// </summary>
|
||||
|
@ -59,6 +59,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
|
||||
|
||||
/// <summary>
|
||||
/// "Reset ALL beatmap offsets"
|
||||
/// </summary>
|
||||
public static LocalisableString ResetAllOffsets => new TranslatableString(getKey(@"reset_all_offsets"), @"Reset ALL beatmap offsets");
|
||||
|
||||
/// <summary>
|
||||
/// "Delete ALL scores"
|
||||
/// </summary>
|
||||
|
@ -541,7 +541,10 @@ namespace osu.Game
|
||||
realmBlocker = realm.BlockAllOperations("migration");
|
||||
success = true;
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Attempting to block all operations failed: {ex}", LoggingTarget.Database);
|
||||
}
|
||||
|
||||
readyToRun.Set();
|
||||
}, false);
|
||||
|
@ -3,8 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -16,8 +14,6 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet;
|
||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select.Details;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -37,14 +33,6 @@ namespace osu.Game.Overlays
|
||||
|
||||
private (BeatmapSetLookupType type, int id)? lastLookup;
|
||||
|
||||
/// <remarks>
|
||||
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
|
||||
/// to avoid affecting the beatmap details section (i.e. <see cref="AdvancedStats.StatisticRow"/>).
|
||||
/// </remarks>
|
||||
[Cached]
|
||||
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
|
||||
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
public BeatmapSetOverlay()
|
||||
: base(OverlayColourScheme.Blue)
|
||||
{
|
||||
|
@ -72,8 +72,9 @@ namespace osu.Game.Overlays
|
||||
private AudioFilter audioDuckFilter = null!;
|
||||
|
||||
private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
|
||||
private readonly List<BeatmapSetInfo> previousRandomSets = new List<BeatmapSetInfo>();
|
||||
private readonly List<Live<BeatmapSetInfo>> previousRandomSets = new List<Live<BeatmapSetInfo>>();
|
||||
private int randomHistoryDirection;
|
||||
private int lastRandomTrackDirection;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, OsuConfigManager configManager)
|
||||
@ -249,19 +250,19 @@ namespace osu.Game.Overlays
|
||||
|
||||
queuedDirection = TrackChangeDirection.Prev;
|
||||
|
||||
BeatmapSetInfo? playableSet;
|
||||
Live<BeatmapSetInfo>? playableSet;
|
||||
|
||||
if (Shuffle.Value)
|
||||
playableSet = getNextRandom(-1, allowProtectedTracks);
|
||||
else
|
||||
{
|
||||
playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
|
||||
?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks);
|
||||
playableSet = getBeatmapSets().TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks)
|
||||
?? getBeatmapSets().LastOrDefault(s => !s.Value.Protected || allowProtectedTracks);
|
||||
}
|
||||
|
||||
if (playableSet != null)
|
||||
{
|
||||
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Beatmaps.First()));
|
||||
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Value.Beatmaps.First()));
|
||||
restartTrack();
|
||||
return PreviousTrackResult.Previous;
|
||||
}
|
||||
@ -345,19 +346,19 @@ namespace osu.Game.Overlays
|
||||
|
||||
queuedDirection = TrackChangeDirection.Next;
|
||||
|
||||
BeatmapSetInfo? playableSet;
|
||||
Live<BeatmapSetInfo>? playableSet;
|
||||
|
||||
if (Shuffle.Value)
|
||||
playableSet = getNextRandom(1, allowProtectedTracks);
|
||||
else
|
||||
{
|
||||
playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo))
|
||||
.Where(i => !i.Protected || allowProtectedTracks)
|
||||
playableSet = getBeatmapSets().SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo))
|
||||
.Where(i => !i.Value.Protected || allowProtectedTracks)
|
||||
.ElementAtOrDefault(1)
|
||||
?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks);
|
||||
?? getBeatmapSets().FirstOrDefault(i => !i.Value.Protected || allowProtectedTracks);
|
||||
}
|
||||
|
||||
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
|
||||
var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault();
|
||||
|
||||
if (playableBeatmap != null)
|
||||
{
|
||||
@ -369,56 +370,82 @@ namespace osu.Game.Overlays
|
||||
return false;
|
||||
}
|
||||
|
||||
private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks)
|
||||
private Live<BeatmapSetInfo>? getNextRandom(int direction, bool allowProtectedTracks)
|
||||
{
|
||||
BeatmapSetInfo result;
|
||||
|
||||
var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray();
|
||||
|
||||
if (possibleSets.Length == 0)
|
||||
return null;
|
||||
|
||||
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
|
||||
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
|
||||
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
|
||||
// in both cases, it means that we have a history of previous random selections that we can rewind.
|
||||
if (randomHistoryDirection * direction < 0)
|
||||
try
|
||||
{
|
||||
Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count);
|
||||
result = previousRandomSets[^1];
|
||||
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
||||
randomHistoryDirection += direction;
|
||||
return result;
|
||||
}
|
||||
Live<BeatmapSetInfo> result;
|
||||
|
||||
// if the early-return above didn't cover it, it means that we have no history to fall back on
|
||||
// and need to actually choose something random.
|
||||
switch (randomSelectAlgorithm.Value)
|
||||
{
|
||||
case RandomSelectAlgorithm.Random:
|
||||
result = possibleSets[RNG.Next(possibleSets.Length)];
|
||||
break;
|
||||
var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToList();
|
||||
|
||||
case RandomSelectAlgorithm.RandomPermutation:
|
||||
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray();
|
||||
if (possibleSets.Count == 0)
|
||||
return null;
|
||||
|
||||
if (notYetPlayedSets.Length == 0)
|
||||
// if there is only one possible set left, play it, even if it is the same as the current track.
|
||||
// looping is preferable over playing nothing.
|
||||
if (possibleSets.Count == 1)
|
||||
return possibleSets.Single();
|
||||
|
||||
// now that we actually know there is a choice, do not allow the current track to be played again.
|
||||
possibleSets.RemoveAll(s => s.Value.Equals(current?.BeatmapSetInfo));
|
||||
|
||||
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
|
||||
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
|
||||
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
|
||||
// in both cases, it means that we have a history of previous random selections that we can rewind.
|
||||
if (randomHistoryDirection * direction < 0)
|
||||
{
|
||||
Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count);
|
||||
|
||||
// if the user has been shuffling backwards and now going forwards (or vice versa),
|
||||
// the topmost item from history needs to be discarded because it's the *current* track.
|
||||
if (direction * lastRandomTrackDirection < 0)
|
||||
{
|
||||
notYetPlayedSets = possibleSets;
|
||||
previousRandomSets.Clear();
|
||||
randomHistoryDirection = 0;
|
||||
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
||||
randomHistoryDirection += direction;
|
||||
}
|
||||
|
||||
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)];
|
||||
break;
|
||||
if (previousRandomSets.Count > 0)
|
||||
{
|
||||
result = previousRandomSets[^1];
|
||||
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
|
||||
// if the early-return above didn't cover it, it means that we have no history to fall back on
|
||||
// and need to actually choose something random.
|
||||
switch (randomSelectAlgorithm.Value)
|
||||
{
|
||||
case RandomSelectAlgorithm.Random:
|
||||
result = possibleSets[RNG.Next(possibleSets.Count)];
|
||||
break;
|
||||
|
||||
case RandomSelectAlgorithm.RandomPermutation:
|
||||
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToList();
|
||||
|
||||
if (notYetPlayedSets.Count == 0)
|
||||
{
|
||||
notYetPlayedSets = possibleSets;
|
||||
previousRandomSets.Clear();
|
||||
randomHistoryDirection = 0;
|
||||
}
|
||||
|
||||
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Count)];
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
|
||||
}
|
||||
|
||||
previousRandomSets.Add(result);
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
randomHistoryDirection += direction;
|
||||
lastRandomTrackDirection = direction;
|
||||
}
|
||||
|
||||
previousRandomSets.Add(result);
|
||||
randomHistoryDirection += direction;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void restartTrack()
|
||||
@ -432,7 +459,9 @@ namespace osu.Game.Overlays
|
||||
|
||||
private TrackChangeDirection? queuedDirection;
|
||||
|
||||
private IQueryable<BeatmapSetInfo> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending);
|
||||
private IEnumerable<Live<BeatmapSetInfo>> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending)
|
||||
.AsEnumerable()
|
||||
.Select(s => new RealmLive<BeatmapSetInfo>(s, realm));
|
||||
|
||||
private void changeBeatmap(WorkingBeatmap newWorking)
|
||||
{
|
||||
@ -459,8 +488,8 @@ namespace osu.Game.Overlays
|
||||
else
|
||||
{
|
||||
// figure out the best direction based on order in playlist.
|
||||
int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count();
|
||||
int next = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count();
|
||||
int last = getBeatmapSets().TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count();
|
||||
int next = getBeatmapSets().TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count();
|
||||
|
||||
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
|
||||
private SettingsButton deleteBeatmapsButton = null!;
|
||||
private SettingsButton deleteBeatmapVideosButton = null!;
|
||||
private SettingsButton resetOffsetsButton = null!;
|
||||
private SettingsButton restoreButton = null!;
|
||||
private SettingsButton undeleteButton = null!;
|
||||
|
||||
@ -47,6 +48,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
}, DeleteConfirmationContentStrings.BeatmapVideos));
|
||||
}
|
||||
});
|
||||
|
||||
Add(resetOffsetsButton = new DangerousSettingsButton
|
||||
{
|
||||
Text = MaintenanceSettingsStrings.ResetAllOffsets,
|
||||
Action = () =>
|
||||
{
|
||||
dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
|
||||
{
|
||||
resetOffsetsButton.Enabled.Value = false;
|
||||
Task.Run(beatmaps.ResetAllOffsets).ContinueWith(_ => Schedule(() => resetOffsetsButton.Enabled.Value = true));
|
||||
}, DeleteConfirmationContentStrings.Offsets));
|
||||
}
|
||||
});
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
restoreButton = new SettingsButton
|
||||
|
@ -150,13 +150,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.G:
|
||||
return CanReverse && reverseButton?.TriggerClick() == true;
|
||||
if (!CanReverse || reverseButton == null)
|
||||
return false;
|
||||
|
||||
reverseButton.TriggerAction();
|
||||
return true;
|
||||
|
||||
case Key.Comma:
|
||||
return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true;
|
||||
if (!canRotate.Value || rotateCounterClockwiseButton == null)
|
||||
return false;
|
||||
|
||||
rotateCounterClockwiseButton.TriggerAction();
|
||||
return true;
|
||||
|
||||
case Key.Period:
|
||||
return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true;
|
||||
if (!canRotate.Value || rotateClockwiseButton == null)
|
||||
return false;
|
||||
|
||||
rotateClockwiseButton.TriggerAction();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
@ -285,7 +297,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Action = action
|
||||
};
|
||||
|
||||
button.OperationStarted += freezeButtonPosition;
|
||||
button.Clicked += freezeButtonPosition;
|
||||
button.HoverLost += unfreezeButtonPosition;
|
||||
|
||||
button.OperationStarted += operationStarted;
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public Action? Action;
|
||||
|
||||
public event Action? Clicked;
|
||||
|
||||
public event Action? HoverLost;
|
||||
|
||||
public SelectionBoxButton(IconUsage iconUsage, string tooltip)
|
||||
@ -49,11 +51,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
Circle.FlashColour(Colours.GrayF, 300);
|
||||
Clicked?.Invoke();
|
||||
|
||||
TriggerAction();
|
||||
|
||||
TriggerOperationStarted();
|
||||
Action?.Invoke();
|
||||
TriggerOperationEnded();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -71,5 +72,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
public void TriggerAction()
|
||||
{
|
||||
Circle.FlashColour(Colours.GrayF, 300);
|
||||
|
||||
TriggerOperationStarted();
|
||||
Action?.Invoke();
|
||||
TriggerOperationEnded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -410,7 +410,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
private void dailyChallengeChanged(ValueChangedEvent<DailyChallengeInfo?> change)
|
||||
{
|
||||
if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null)
|
||||
if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null && metadataClient.IsConnected.Value)
|
||||
{
|
||||
notificationOverlay?.Post(new SimpleNotification { Text = DailyChallengeStrings.ChallengeEndedNotification });
|
||||
}
|
||||
|
@ -41,6 +41,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Interactive)
|
||||
return default;
|
||||
|
||||
double progress = Math.Clamp(lastMouseX, 0, DrawWidth) / DrawWidth;
|
||||
|
||||
TimeSpan currentSpan = TimeSpan.FromMilliseconds(Math.Round((EndTime - StartTime) * progress));
|
||||
|
@ -455,7 +455,19 @@ namespace osu.Game.Screens.Play
|
||||
MetadataInfo.Loading = true;
|
||||
|
||||
content.FadeInFromZero(500, Easing.OutQuint);
|
||||
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
|
||||
|
||||
if (quickRestart)
|
||||
{
|
||||
prepareNewPlayer();
|
||||
content.ScaleTo(1, 650, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
content
|
||||
.ScaleTo(1, 650, Easing.OutQuint)
|
||||
.Then()
|
||||
.Schedule(prepareNewPlayer);
|
||||
}
|
||||
|
||||
using (BeginDelayedSequence(delayBeforeSideDisplays))
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Screens.Select
|
||||
public BeatmapDeleteDialog(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
this.beatmapSet = beatmapSet;
|
||||
BodyText = $@"{beatmapSet.Metadata.Artist} - {beatmapSet.Metadata.Title}";
|
||||
BodyText = beatmapSet.Metadata.GetDisplayTitleRomanisable(false);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -36,9 +37,6 @@ namespace osu.Game.Screens.Select.Details
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
|
||||
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
||||
private readonly StatisticRow starDifficulty;
|
||||
|
||||
@ -69,6 +67,14 @@ namespace osu.Game.Screens.Select.Details
|
||||
/// </remarks>
|
||||
public Bindable<RulesetInfo> Ruleset { get; } = new Bindable<RulesetInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Mods to be used for certain elements of display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// No checks are done as to whether the mods specified are valid for the current <see cref="Ruleset"/>.
|
||||
/// </remarks>
|
||||
public Bindable<IReadOnlyList<Mod>> Mods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
public AdvancedStats(int columns = 1)
|
||||
{
|
||||
switch (columns)
|
||||
@ -143,8 +149,7 @@ namespace osu.Game.Screens.Select.Details
|
||||
base.LoadComplete();
|
||||
|
||||
Ruleset.BindValueChanged(_ => updateStatistics());
|
||||
|
||||
mods.BindValueChanged(modsChanged, true);
|
||||
Mods.BindValueChanged(modsChanged, true);
|
||||
}
|
||||
|
||||
private ModSettingChangeTracker modSettingChangeTracker;
|
||||
@ -173,14 +178,14 @@ namespace osu.Game.Screens.Select.Details
|
||||
{
|
||||
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(baseDifficulty);
|
||||
|
||||
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
|
||||
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
|
||||
mod.ApplyToDifficulty(originalDifficulty);
|
||||
|
||||
adjustedDifficulty = originalDifficulty;
|
||||
|
||||
if (Ruleset.Value != null)
|
||||
{
|
||||
double rate = ModUtils.CalculateRateWithMods(mods.Value);
|
||||
double rate = ModUtils.CalculateRateWithMods(Mods.Value);
|
||||
|
||||
adjustedDifficulty = Ruleset.Value.CreateInstance().GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
|
||||
|
||||
@ -198,7 +203,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, mods.Value);
|
||||
int keyCount = baseDifficulty == null ? 0 : legacyRuleset.GetKeyCount(BeatmapInfo, Mods.Value);
|
||||
|
||||
FirstValue.Title = BeatmapsetsStrings.ShowStatsCsMania;
|
||||
FirstValue.Value = (keyCount, keyCount);
|
||||
@ -236,7 +241,7 @@ namespace osu.Game.Screens.Select.Details
|
||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||
|
||||
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, null, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, Ruleset.Value, Mods.Value, starDifficultyCancellationSource.Token);
|
||||
|
||||
Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() =>
|
||||
{
|
||||
|
@ -610,11 +610,6 @@ namespace osu.Game.Screens.Select
|
||||
beatmapInfoPrevious = beatmap;
|
||||
}
|
||||
|
||||
// we can't run this in the debounced run due to the selected mods bindable not being debounced,
|
||||
// since mods could be updated to the new ruleset instances while the decoupled bindable is held behind,
|
||||
// therefore resulting in performing difficulty calculation with invalid states.
|
||||
advancedStats.Ruleset.Value = ruleset;
|
||||
|
||||
void run()
|
||||
{
|
||||
// clear pending task immediately to track any potential nested debounce operation.
|
||||
@ -878,6 +873,8 @@ namespace osu.Game.Screens.Select
|
||||
ModSelect.Beatmap.Value = beatmap;
|
||||
|
||||
advancedStats.BeatmapInfo = beatmap.BeatmapInfo;
|
||||
advancedStats.Mods.Value = selectedMods.Value;
|
||||
advancedStats.Ruleset.Value = Ruleset.Value;
|
||||
|
||||
bool beatmapSelected = beatmap is not DummyWorkingBeatmap;
|
||||
|
||||
@ -990,6 +987,12 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
Beatmap.BindValueChanged(updateCarouselSelection);
|
||||
|
||||
selectedMods.BindValueChanged(_ =>
|
||||
{
|
||||
if (decoupledRuleset.Value.Equals(rulesetNoDebounce))
|
||||
advancedStats.Mods.Value = selectedMods.Value;
|
||||
}, true);
|
||||
|
||||
boundLocalBindables = true;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,8 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
{
|
||||
public partial class TestMetadataClient : MetadataClient
|
||||
{
|
||||
public override IBindable<bool> IsConnected => new BindableBool(true);
|
||||
public override IBindable<bool> IsConnected => isConnected;
|
||||
private readonly BindableBool isConnected = new BindableBool(true);
|
||||
|
||||
public override IBindable<bool> IsWatchingUserPresence => isWatchingUserPresence;
|
||||
private readonly BindableBool isWatchingUserPresence = new BindableBool();
|
||||
@ -98,5 +99,16 @@ namespace osu.Game.Tests.Visual.Metadata
|
||||
}
|
||||
|
||||
public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask;
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
isConnected.Value = false;
|
||||
dailyChallengeInfo.Value = null;
|
||||
}
|
||||
|
||||
public void Reconnect()
|
||||
{
|
||||
isConnected.Value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1007.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1009.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1003.0" />
|
||||
<PackageReference Include="Sentry" Version="4.3.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1007.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1009.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user