1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-04 09:03:54 +08:00

Merge branch 'master' into playlist-item-copy

This commit is contained in:
Dan Balasescu
2025-03-20 16:35:55 +09:00
Unverified
92 changed files with 830 additions and 465 deletions
+1 -1
View File
@@ -136,4 +136,4 @@ jobs:
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
- name: Build
run: dotnet build -c Debug osu.iOS
run: dotnet build -c Debug osu.iOS.slnf
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.313.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.318.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield();
@@ -759,9 +759,9 @@ namespace osu.Game.Rulesets.Osu.Tests
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
},
ControlPointInfo = cpi
ControlPointInfo = cpi,
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
});
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
});
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
ComboOffset = comboData?.ComboOffset ?? 0,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
}.Yield();
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects)
h.StackHeight = 0;
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
if (beatmap.BeatmapVersion >= 6)
applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
else
applyStackingOld(beatmap, hitObjects);
@@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double osuVelocity = taikoVelocity * (1000f / beatLength);
// osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
if (beatmap.BeatmapVersion >= 8)
beatLength = timingPoint.BeatLength;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
+10 -1
View File
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.iOS.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-ios</TargetFramework>
@@ -6,11 +7,19 @@
<RootNamespace>osu.Game.Tests</RootNamespace>
<AssemblyName>osu.Game.Tests.iOS</AssemblyName>
</PropertyGroup>
<Import Project="..\osu.iOS.props" />
<PropertyGroup>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\osu.Game.Tests\**\*.cs" Exclude="**\obj\**">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<!-- TargetPath is relative to RootNamespace,
and DllResourceStore is relative to AssemblyName. -->
<EmbeddedResource Include="..\osu.Game.Tests\**\Resources\**\*">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
<TargetPath>iOS\%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</EmbeddedResource>
</ItemGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
@@ -42,9 +42,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = Decoder.GetDecoder<Beatmap>(stream);
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.Beatmap.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.Beatmap.BeatmapVersion);
Assert.That(working.Beatmap.BeatmapInfo.Ruleset.Name, Is.Not.EqualTo("null placeholder ruleset"));
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion);
}
}
@@ -59,9 +59,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets;
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
Assert.AreEqual(4, working.Beatmap.BeatmapVersion);
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion);
Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime);
}
@@ -155,10 +155,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset)
{
BeatmapInfo =
{
BeatmapVersion = beatmapVersion
}
BeatmapVersion = beatmapVersion
};
var score = new Score
@@ -633,14 +630,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
},
// needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die
// needs to have at least one object so that `StandardisedScoreMigrationTools` doesn't die
// when trying to recompute total score.
HitObjects =
{
new HitCircle()
}
},
BeatmapVersion = beatmapVersion,
});
}
}
@@ -208,5 +208,11 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7));
}
[Test]
public void TestBeatmapVersionPopulatedCorrectly()
{
AddAssert("beatmap version is populated", () => EditorBeatmap.BeatmapVersion > 0);
}
}
}
@@ -50,30 +50,17 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestGameplay()
{
KiaiGameplayFountains fountains = null!;
AddStep("make fountains", () =>
{
Children = new[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
fountains = new KiaiGameplayFountains(),
};
});
AddStep("activate fountains", () =>
{
((StarFountain)Children[0]).Shoot(1);
((StarFountain)Children[1]).Shoot(-1);
});
AddStep("activate fountains", () => fountains.Shoot());
}
[Test]
@@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
multiplayerRoom = new MultiplayerRoom(0)
{
Playlist = { TestMultiplayerClient.CreateMultiplayerPlaylistItem(item) },
Playlist = { new MultiplayerPlaylistItem(item) },
Users = { localUser },
Host = localUser,
};
@@ -924,7 +924,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -956,7 +956,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary>
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{
Expired = expired,
PlayedAt = DateTimeOffset.Now
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add playlist item", () =>
{
MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();
@@ -289,12 +289,37 @@ namespace osu.Game.Tests.Visual.Online
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(0));
});
AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().All(s => s.Text != "BanchoBot"));
AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().All(s => s.Text != "BanchoBot0"));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddAssert("guest mapper information shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().Any(s => s.Text == "BanchoBot0"));
}
[Test]
public void TestBeatmapsetWithALotGuestOwner()
{
AddStep("show map with 2 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(2)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddStep("show map with 3 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(3)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddStep("show map with 10 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(10)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddStep("show map with 20 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(20)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddAssert("guest mapper information shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().Any(s => s.Text == "BanchoBot"));
}
private APIBeatmapSet createManyDifficultiesBeatmapSet()
@@ -336,22 +361,31 @@ namespace osu.Game.Tests.Visual.Online
return beatmapSet;
}
private APIBeatmapSet createBeatmapSetWithGuestDifficulty()
private APIBeatmapSet createBeatmapSetWithGuestDifficulty(int guestCount = 1)
{
var set = getBeatmapSet();
var beatmaps = new List<APIBeatmap>();
var beatmapOwners = new List<APIBeatmap.BeatmapOwner>();
var ownersAPIUser = new List<APIUser>();
var guestUser = new APIUser
for (int i = 0; i < guestCount; i++)
{
Username = @"BanchoBot",
Id = 3,
};
var guestUser = new APIUser
{
Username = @$"BanchoBot{i}",
Id = i + 3,
};
set.RelatedUsers = new[]
{
set.Author, guestUser
};
beatmapOwners.Add(new APIBeatmap.BeatmapOwner
{
Username = @$"BanchoBot{i}",
Id = i + 3,
});
ownersAPIUser.Add(guestUser);
}
set.RelatedUsers = new[] { set.Author }.Concat(ownersAPIUser).ToArray();
beatmaps.Add(new APIBeatmap
{
@@ -366,7 +400,7 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
Status = BeatmapOnlineStatus.Graveyard
Status = BeatmapOnlineStatus.Graveyard,
});
beatmaps.Add(new APIBeatmap
@@ -382,7 +416,8 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
Status = BeatmapOnlineStatus.Graveyard
Status = BeatmapOnlineStatus.Graveyard,
BeatmapOwners = beatmapOwners.ToArray(),
});
set.Beatmaps = beatmaps.ToArray();
@@ -72,10 +72,10 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0));
AddStep("User began playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() }));
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id));
AddStep("User finished playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
@@ -88,13 +88,12 @@ namespace osu.Game.Tests.Visual.Online
{
IDisposable token = null!;
AddStep("User began playing", () => spectatorClient.SendStartPlay(streamingUser.Id, 0));
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == streamingUser.Id);
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
AddStep("User finished playing", () => spectatorClient.SendEndPlay(streamingUser.Id));
AddStep("User finished playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
AddStep("End watching user presence", () => token.Dispose());
@@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "flyte",
Id = 3103765,
IsOnline = true,
WasRecentlyOnline = true,
Statistics = new UserStatistics { GlobalRank = 1111 },
CountryCode = CountryCode.JP,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
@@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "peppy",
Id = 2,
IsOnline = false,
WasRecentlyOnline = false,
Statistics = new UserStatistics { GlobalRank = 2222 },
CountryCode = CountryCode.AU,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
@@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 8195163,
CountryCode = CountryCode.BY,
CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
WasRecentlyOnline = false,
LastVisit = DateTimeOffset.Now
}
};
@@ -0,0 +1,38 @@
// 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.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers.Markdown;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneImageProxying : OsuTestScene
{
[Test]
public void TestExternalImageLink()
{
MarkdownContainer markdown = null!;
// use base MarkdownContainer as a method of directly attempting to load an image without proxying logic.
AddStep("load external without proxying", () => Child = markdown = new MarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddWaitStep("wait", 5);
AddAssert("image not loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture == null);
AddStep("load external with proxying", () => Child = markdown = new OsuMarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddUntilStep("image loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture != null);
}
}
}
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = countryCode,
CoverUrl = cover,
Colour = color ?? "000000",
IsOnline = true
WasRecentlyOnline = true
};
return new ClickableAvatar(user, showPanel)
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
IsOnline = true
WasRecentlyOnline = true
}) { Width = 300 },
new UserGridPanel(new APIUser
{
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 1001,
Username = "IAmOnline",
LastVisit = DateTimeOffset.Now,
IsOnline = true,
WasRecentlyOnline = true,
}, new OsuRuleset().RulesetInfo));
AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 1002,
Username = "IAmOffline",
LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false,
WasRecentlyOnline = false,
}, new OsuRuleset().RulesetInfo));
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online;
using osu.Game.Scoring;
@@ -12,7 +13,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneOverallRanking : OsuTestScene
{
private OverallRanking overallRanking = null!;
private readonly Bindable<ScoreBasedUserStatisticsUpdate?> statisticsUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
[Test]
public void TestUpdatePending()
@@ -104,14 +105,19 @@ namespace osu.Game.Tests.Visual.Ranking
displayUpdate(statistics, statistics);
}
private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking
private void createDisplay() => AddStep("create display", () =>
{
Width = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
statisticsUpdate.Value = null;
Child = new OverallRanking(new ScoreInfo())
{
Width = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
DisplayedUpdate = { BindTarget = statisticsUpdate }
};
});
private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after));
AddStep("display update", () => statisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after));
}
}
@@ -404,7 +404,7 @@ namespace osu.Game.Tests.Visual.Ranking
: base(score)
{
AllowRetry = true;
ShowUserStatistics = true;
IsLocalPlay = true;
}
protected override void LoadComplete()
@@ -11,6 +11,7 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -18,6 +19,9 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@@ -36,6 +40,8 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneStatisticsPanel : OsuTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Test]
public void TestScoreWithPositionStatistics()
{
@@ -137,62 +143,114 @@ namespace osu.Game.Tests.Visual.Ranking
{
CachedDependencies = [(typeof(UserStatisticsWatcher), userStatisticsWatcher)],
RelativeSizeAxes = Axes.Both,
Child = new UserStatisticsPanel(score)
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score, }
Score = { Value = score, },
AchievedScore = score,
}
});
AddUntilStep("overall ranking present", () => this.ChildrenOfType<OverallRanking>().Any());
AddUntilStep("loading spinner not visible", () => this.ChildrenOfType<LoadingLayer>().All(l => l.State.Value == Visibility.Hidden));
AddUntilStep("loading spinner not visible",
() => this.ChildrenOfType<OverallRanking>().Single()
.ChildrenOfType<LoadingLayer>().All(l => l.State.Value == Visibility.Hidden));
}
[Test]
public void TestTagging()
{
var score = TestResources.CreateTestScoreInfo();
AddStep("set up network requests", () =>
{
dummyAPI.HandleRequest = request =>
{
switch (request)
{
case ListTagsRequest listTagsRequest:
{
Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection
{
Tags =
[
new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
]
}), 500);
return true;
}
case GetBeatmapSetRequest getBeatmapSetRequest:
{
var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo);
beatmapSet.Beatmaps.Single().TopTags =
[
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
];
Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500);
return true;
}
case AddBeatmapTagRequest:
case RemoveBeatmapTagRequest:
{
Scheduler.AddDelayed(request.TriggerSuccess, 500);
return true;
}
}
return false;
};
});
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
[Test]
public void TestTaggingWhenRankTooLow()
{
var score = TestResources.CreateTestScoreInfo();
score.Rank = ScoreRank.D;
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new UserStatisticsPanel(score)
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
DisplayedUserStatisticsUpdate =
{
Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics
{
Level = new UserStatistics.LevelInfo
{
Current = 5,
Progress = 20,
},
GlobalRank = 38000,
CountryRank = 12006,
PP = 2134,
RankedScore = 21123849,
Accuracy = 0.985,
PlayCount = 13375,
PlayTime = 354490,
TotalScore = 128749597,
TotalHits = 0,
MaxCombo = 1233,
}, new UserStatistics
{
Level = new UserStatistics.LevelInfo
{
Current = 5,
Progress = 30,
},
GlobalRank = 36000,
CountryRank = 12000,
PP = (decimal)2134.5,
RankedScore = 23897015,
Accuracy = 0.984,
PlayCount = 13376,
PlayTime = 35789,
TotalScore = 132218497,
TotalHits = 0,
MaxCombo = 1233,
})
}
AchievedScore = score,
};
});
@@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Ranking
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new UserTagControl
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
{
Width = 500,
Anchor = Anchor.Centre,
@@ -993,7 +993,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().IsScrolledToStart());
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("customisation panel closed",
AddUntilStep("customisation panel closed",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
@@ -1018,7 +1018,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void assertCustomisationToggleState(bool disabled, bool active)
{
AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled);
AddAssert($"customisation panel is {(active ? "" : "not ")}active",
AddUntilStep($"customisation panel is {(active ? "" : "not ")}active",
() => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
}
+2 -2
View File
@@ -6,9 +6,9 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
namespace osu.Game.Audio
{
@@ -30,7 +30,7 @@ namespace osu.Game.Audio
[BackgroundDependencyLoader]
private void load(AudioManager audioManager)
{
trackStore = audioManager.GetTrackStore(new OnlineStore());
trackStore = audioManager.GetTrackStore(new TrustedDomainOnlineStore());
}
/// <summary>
+3
View File
@@ -9,6 +9,7 @@ using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using Newtonsoft.Json;
using osu.Framework.Lists;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps
@@ -141,6 +142,8 @@ namespace osu.Game.Beatmaps
public int[] Bookmarks { get; set; } = Array.Empty<int>();
public int BeatmapVersion { get; set; } = LegacyBeatmapEncoder.FIRST_LAZER_VERSION;
IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
+1
View File
@@ -86,6 +86,7 @@ namespace osu.Game.Beatmaps
beatmap.Countdown = original.Countdown;
beatmap.CountdownOffset = original.CountdownOffset;
beatmap.Bookmarks = original.Bookmarks;
beatmap.BeatmapVersion = original.BeatmapVersion;
return beatmap;
}
+3 -4
View File
@@ -125,9 +125,10 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>
public void ResetOnlineInfo()
public void ResetOnlineInfo(bool resetOnlineId = true)
{
OnlineID = -1;
if (resetOnlineId)
OnlineID = -1;
LastOnlineUpdate = null;
OnlineMD5Hash = string.Empty;
if (Status != BeatmapOnlineStatus.LocallyModified)
@@ -231,8 +232,6 @@ namespace osu.Game.Beatmaps
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
public int? MaxCombo { get; set; }
public int BeatmapVersion;
public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone();
public override string ToString() => this.GetDisplayTitle();
@@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps.Formats
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
{
this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
this.beatmap.BeatmapVersion = FormatVersion;
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
ApplyLegacyDefaults(this.beatmap);
@@ -193,6 +193,10 @@ namespace osu.Game.Beatmaps.Formats
internal static void ApplyLegacyDefaults(Beatmap beatmap)
{
beatmap.WidescreenStoryboard = false;
// in a perfect world this would throw if osu! ruleset couldn't be found,
// but unfortunately there are "legitimate" cases where it's not there (i.e. ruleset test projects),
// so attempt to trudge on with whatever it is that's in `BeatmapInfo` if the lookup fails.
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? beatmap.BeatmapInfo.Ruleset;
}
protected override void ParseLine(Beatmap beatmap, Section section, string line)
+2
View File
@@ -109,6 +109,8 @@ namespace osu.Game.Beatmaps
int[] Bookmarks { get; internal set; }
int BeatmapVersion { get; }
/// <summary>
/// Creates a shallow-clone of this beatmap and returns it.
/// </summary>
+11 -1
View File
@@ -97,8 +97,9 @@ namespace osu.Game.Database
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions.
/// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues).
/// </summary>
private const int schema_version = 47;
private const int schema_version = 48;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@@ -1245,6 +1246,15 @@ namespace osu.Game.Database
break;
}
case 48:
const int qualified = (int)BeatmapOnlineStatus.Qualified;
var beatmaps = migration.NewRealm.All<BeatmapInfo>().Where(b => b.StatusInt == qualified);
foreach (var beatmap in beatmaps)
beatmap.ResetOnlineInfo(resetOnlineId: false);
break;
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
@@ -17,5 +17,8 @@ namespace osu.Game.Graphics.Containers.Markdown
{
TooltipText = linkInline.Title;
}
protected override ImageContainer CreateImageContainer(string url)
=> base.CreateImageContainer($@"https://osu.ppy.sh/media-url?url={url}");
}
}
@@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Containers
private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight
{
public float LineBaseHeight => DrawHeight;
public float LineBaseHeight => (Child as IHasLineBaseHeight)?.LineBaseHeight ?? DrawHeight;
public ArbitraryDrawableWrapper()
{
@@ -89,7 +89,7 @@ namespace osu.Game.Graphics.UserInterface
{
if (Link == null) return;
game?.CopyUrlToClipboard(Link);
game?.CopyToClipboard(Link);
}
}
}
@@ -17,7 +17,13 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary>
/// "Whether to show extended information for each mod."
/// </summary>
public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
public static LocalisableString ShowExtendedInformationDescription =>
new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
/// <summary>
/// "Display direction"
/// </summary>
public static LocalisableString DisplayDirection => new TranslatableString(getKey(@"display_direction"), "Display direction");
/// <summary>
/// "Expansion mode"
+2 -2
View File
@@ -45,9 +45,9 @@ namespace osu.Game.Localisation
public static LocalisableString SkinSaved => new TranslatableString(getKey(@"skin_saved"), @"Skin saved");
/// <summary>
/// "Link copied to clipboard"
/// "Copied to clipboard"
/// </summary>
public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"Link copied to clipboard");
public static LocalisableString CopiedToClipboard => new TranslatableString(getKey(@"copied_to_clipboard"), @"Copied to clipboard");
/// <summary>
/// "Speed changed to {0:N2}x"
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Globalization;
using System.Net.Http;
using osu.Framework.IO.Network;
@@ -21,11 +20,10 @@ namespace osu.Game.Online.API.Requests
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter(@"tag_id", TagID.ToString(CultureInfo.InvariantCulture), RequestParameterType.Query);
req.Method = HttpMethod.Put;
return req;
}
protected override string Target => $@"beatmaps/{BeatmapID}/tags";
protected override string Target => $@"beatmaps/{BeatmapID}/tags/{TagID}";
}
}
@@ -109,6 +109,9 @@ namespace osu.Game.Online.API.Requests.Responses
public double BPM { get; set; }
[JsonProperty(@"owners")]
public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty<BeatmapOwner>();
#region Implementation of IBeatmapInfo
public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata();
@@ -177,5 +180,14 @@ namespace osu.Game.Online.API.Requests.Responses
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
public class BeatmapOwner
{
[JsonProperty(@"id")]
public int Id { get; set; }
[JsonProperty(@"username")]
public string Username { get; set; } = string.Empty;
}
}
}
@@ -9,6 +9,7 @@ using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Game.Extensions;
using osu.Game.Online.Metadata;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
@@ -111,8 +112,13 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"is_active")]
public bool Active;
/// <summary>
/// From osu-web's perspective, whether a user was recently online.
/// This doesn't imply the user is online in a lazer client (may be updated from stable or web browser).
/// Use <see cref="MetadataClient.GetPresence"/> for real-time lazer online status checks.
/// </summary>
[JsonProperty(@"is_online")]
public bool IsOnline;
public bool WasRecentlyOnline;
[JsonProperty(@"pm_friends_only")]
public bool PMFriendsOnly;
@@ -57,6 +57,9 @@ namespace osu.Game.Online.Metadata
/// <summary>
/// Attempts to retrieve the presence of a user.
/// </summary>
/// <remarks>
/// This will return data if the client is currently receiving presence data. See <see cref="BeginWatchingUserPresence"/>.
/// </remarks>
/// <param name="userId">The user ID.</param>
/// <returns>The user presence, or null if not available or the user's offline.</returns>
public UserPresence? GetPresence(int userId)
@@ -47,11 +47,6 @@ namespace osu.Game.Online.Spectator
/// </summary>
public IBindableList<SpectatorUser> WatchingUsers => watchingUsers;
/// <summary>
/// A global list of all players currently playing.
/// </summary>
public IBindableList<int> PlayingUsers => playingUsers;
/// <summary>
/// Whether the local user is playing.
/// </summary>
@@ -91,7 +86,6 @@ namespace osu.Game.Online.Spectator
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private readonly BindableList<SpectatorUser> watchingUsers = new BindableList<SpectatorUser>();
private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly SpectatorState currentState = new SpectatorState();
private IBeatmap? currentBeatmap;
@@ -134,7 +128,6 @@ namespace osu.Game.Online.Spectator
}
else
{
playingUsers.Clear();
watchedUserStates.Clear();
watchingUsers.Clear();
}
@@ -145,9 +138,6 @@ namespace osu.Game.Online.Spectator
{
Schedule(() =>
{
if (!playingUsers.Contains(userId))
playingUsers.Add(userId);
if (watchedUsersRefCounts.ContainsKey(userId))
watchedUserStates[userId] = state;
@@ -161,8 +151,6 @@ namespace osu.Game.Online.Spectator
{
Schedule(() =>
{
playingUsers.Remove(userId);
if (watchedUsersRefCounts.ContainsKey(userId))
watchedUserStates[userId] = state;
@@ -0,0 +1,23 @@
// 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 osu.Framework.IO.Stores;
using osu.Framework.Logging;
namespace osu.Game.Online
{
public sealed class TrustedDomainOnlineStore : OnlineStore
{
protected override string GetLookupUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase))
{
Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important);
return string.Empty;
}
return url;
}
}
}
+3 -3
View File
@@ -519,10 +519,10 @@ namespace osu.Game
}
});
public void CopyUrlToClipboard(string url) => waitForReady(() => onScreenDisplay, _ =>
public void CopyToClipboard(string value) => waitForReady(() => onScreenDisplay, _ =>
{
dependencies.Get<Clipboard>().SetText(url);
onScreenDisplay.Display(new CopyUrlToast());
dependencies.Get<Clipboard>().SetText(value);
onScreenDisplay.Display(new CopiedToClipboardToast());
});
public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode));
+3 -1
View File
@@ -108,6 +108,8 @@ namespace osu.Game
public virtual EndpointConfiguration CreateEndpoints() =>
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
protected override OnlineStore CreateOnlineStore() => new TrustedDomainOnlineStore();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
/// <summary>
@@ -278,7 +280,7 @@ namespace osu.Game
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(CreateOnlineStore()));
dependencies.Cache(largeStore);
dependencies.CacheAs(LocalConfig);
+67 -71
View File
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Extensions;
@@ -31,9 +32,7 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_icon_padding = 7;
private const float tile_spacing = 2;
private readonly OsuSpriteText version, starRating, starRatingText;
private readonly LinkFlowContainer guestMapperContainer;
private readonly FillFlowContainer starRatingContainer;
private readonly LinkFlowContainer infoContainer;
private readonly Statistic plays, favourites;
public readonly DifficultiesContainer Difficulties;
@@ -53,6 +52,9 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
[Resolved]
private OsuColour colours { get; set; } = null!;
public BeatmapPicker()
{
RelativeSizeAxes = Axes.X;
@@ -72,59 +74,13 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2), Bottom = 10 },
OnLostHover = () =>
{
showBeatmap(Beatmap.Value);
starRatingContainer.FadeOut(100);
},
OnLostHover = () => showBeatmap(Beatmap.Value, withStarRating: false),
},
new FillFlowContainer
infoContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5f),
Children = new Drawable[]
{
version = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)
},
guestMapperContainer = new LinkFlowContainer(s =>
s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Bottom = 1 },
},
starRatingContainer = new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Alpha = 0,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0),
Margin = new MarginPadding { Bottom = 1 },
Children = new[]
{
starRatingText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold),
Text = BeatmapsetsStrings.ShowStatsStars,
},
starRating = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold),
Text = string.Empty,
},
}
},
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.BottomLeft,
},
new FillFlowContainer
{
@@ -144,7 +100,7 @@ namespace osu.Game.Overlays.BeatmapSet
Beatmap.ValueChanged += b =>
{
showBeatmap(b.NewValue);
showBeatmap(b.NewValue, withStarRating: Difficulties.Any(d => d.IsHovered));
updateDifficultyButtons();
};
}
@@ -153,10 +109,8 @@ namespace osu.Game.Overlays.BeatmapSet
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
starRating.Colour = colours.Yellow;
starRatingText.Colour = colours.Yellow;
updateDisplay();
}
@@ -185,16 +139,12 @@ namespace osu.Game.Overlays.BeatmapSet
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarRating.FormatStarRating();
starRatingContainer.FadeIn(100);
showBeatmap(beatmap, withStarRating: true);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRatingContainer.FadeOut(100);
// If a selection is already made, try and maintain it.
if (Beatmap.Value != null)
Beatmap.Value = Difficulties.FirstOrDefault(b => b.Beatmap.OnlineID == Beatmap.Value.OnlineID)?.Beatmap;
@@ -208,22 +158,68 @@ namespace osu.Game.Overlays.BeatmapSet
updateDifficultyButtons();
}
private void showBeatmap(APIBeatmap? beatmapInfo)
private void showBeatmap(APIBeatmap? beatmapInfo, bool withStarRating)
{
guestMapperContainer.Clear();
infoContainer.Clear();
if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID)
infoContainer.AddText(beatmapInfo?.DifficultyName ?? string.Empty, s => s.Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold));
infoContainer.AddArbitraryDrawable(Empty().With(e => e.Width = 5));
var beatmapOwners = beatmapInfo?.BeatmapOwners;
bool isHostDifficulty = beatmapOwners?.Length == 1 && beatmapOwners.First().Id == beatmapSet?.AuthorID;
if (beatmapOwners != null && !isHostDifficulty)
{
APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID);
APIUser[] users = BeatmapSet?.RelatedUsers?.Where(u => beatmapOwners.Any(o => o.Id == u.OnlineID)).ToArray() ?? [];
int count = users.Length;
if (user != null)
switch (count)
{
guestMapperContainer.AddText("mapped by ");
guestMapperContainer.AddUserLink(user);
case 0:
break;
case 1:
infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty));
infoContainer.AddUserLink(users[0]);
break;
case 2:
infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty));
infoContainer.AddUserLink(users[0]);
infoContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector);
infoContainer.AddUserLink(users[1]);
break;
default:
{
infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty));
for (int i = 0; i < count; i++)
{
infoContainer.AddUserLink(users[i]);
if (i < count - 2)
infoContainer.AddText(CommonStrings.ArrayAndWordsConnector);
else if (i == count - 2)
infoContainer.AddText(CommonStrings.ArrayAndLastWordConnector);
}
break;
}
}
}
version.Text = beatmapInfo?.DifficultyName ?? string.Empty;
if (withStarRating)
{
infoContainer.AddArbitraryDrawable(Empty().With(e => e.Width = 5));
infoContainer.AddText(
LocalisableString.Interpolate($"{BeatmapsetsStrings.ShowStatsStars} {beatmapInfo?.StarRating.FormatStarRating()}"),
t =>
{
t.Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold);
t.Colour = colours.Yellow;
});
}
}
private void updateDifficultyButtons()
@@ -98,7 +98,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
Vertical = BeatmapSetOverlay.Y_PADDING,
Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH + 10,
},
Children = new Drawable[]
{
+37 -8
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -22,7 +24,10 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
using ChatStrings = osu.Game.Localisation.ChatStrings;
@@ -69,6 +74,12 @@ namespace osu.Game.Overlays.Chat
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient? multiplayerClient { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved(canBeNull: true)]
private ChannelManager? chatManager { get; set; }
@@ -161,13 +172,10 @@ namespace osu.Game.Overlays.Chat
if (user.Equals(APIUser.SYSTEM_USER))
return Array.Empty<MenuItem>();
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)
};
if (user.Equals(api.LocalUser.Value))
return Array.Empty<MenuItem>();
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
List<MenuItem> items = new List<MenuItem>();
if (currentChannel?.Value != null)
{
@@ -177,8 +185,29 @@ namespace osu.Game.Overlays.Chat
}));
}
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
items.Add(new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile));
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
// We should probably be checking against an online state here.
// But we can't use MetadataClient.GetPresence because we may not be requesting/receiving presences.
// This isn't really too bad worst case scenario the client will open spectator view and show the user as "offline".
{
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
{
performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user)));
}));
if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true)
{
items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id)));
}
}
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
return items.ToArray();
}
@@ -1,13 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -19,7 +22,7 @@ using osuTK;
namespace osu.Game.Overlays.Comments
{
public partial class CommentReportButton : CompositeDrawable, IHasPopover
public partial class CommentReportButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight
{
private readonly Comment comment;
@@ -88,5 +91,7 @@ namespace osu.Game.Overlays.Comments
api.Queue(request);
}
public float LineBaseHeight => link.ChildrenOfType<IHasLineBaseHeight>().FirstOrDefault()?.LineBaseHeight ?? DrawHeight;
}
}
@@ -420,7 +420,7 @@ namespace osu.Game.Overlays.Comments
private void copyUrl()
{
clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}");
onScreenDisplay?.Display(new CopyUrlToast());
onScreenDisplay?.Display(new CopiedToClipboardToast());
}
private void toggleReply()
@@ -2,9 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@@ -16,10 +14,8 @@ using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Spectator;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Match.Components;
@@ -34,19 +30,12 @@ namespace osu.Game.Overlays.Dashboard
private const float search_textbox_height = 40;
private const float padding = 10;
private readonly IBindableList<int> playingUsers = new BindableList<int>();
private readonly IBindableDictionary<int, UserPresence> onlineUserPresences = new BindableDictionary<int, UserPresence>();
private readonly Dictionary<int, OnlineUserPanel> userPanels = new Dictionary<int, OnlineUserPanel>();
private SearchContainer<OnlineUserPanel> userFlow = null!;
private BasicSearchTextBox searchTextBox = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
private MetadataClient metadataClient { get; set; } = null!;
@@ -106,9 +95,6 @@ namespace osu.Game.Overlays.Dashboard
onlineUserPresences.BindTo(metadataClient.UserPresences);
onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true);
playingUsers.BindTo(spectatorClient.PlayingUsers);
playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
}
protected override void OnFocus(FocusEvent e)
@@ -152,53 +138,27 @@ namespace osu.Game.Overlays.Dashboard
}
});
private void onPlayingUsersChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null);
foreach (int userId in e.NewItems)
{
if (userPanels.TryGetValue(userId, out var panel))
panel.CanSpectate.Value = userId != api.LocalUser.Value.Id;
}
break;
case NotifyCollectionChangedAction.Remove:
Debug.Assert(e.OldItems != null);
foreach (int userId in e.OldItems)
{
if (userPanels.TryGetValue(userId, out var panel))
panel.CanSpectate.Value = false;
}
break;
}
}
private OnlineUserPanel createUserPanel(APIUser user) =>
new OnlineUserPanel(user).With(panel =>
{
panel.Anchor = Anchor.TopCentre;
panel.Origin = Anchor.TopCentre;
panel.CanSpectate.Value = playingUsers.Contains(user.Id);
});
public partial class OnlineUserPanel : CompositeDrawable, IFilterable
{
public readonly APIUser User;
public BindableBool CanSpectate { get; } = new BindableBool();
private PurpleRoundedButton spectateButton = null!;
public IEnumerable<LocalisableString> FilterTerms { get; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
private MetadataClient? metadataClient { get; set; }
public bool FilteringActive { set; get; }
public bool MatchingFilter
@@ -221,6 +181,27 @@ namespace osu.Game.Overlays.Dashboard
AutoSizeAxes = Axes.Both;
}
protected override void Update()
{
base.Update();
// TODO: we probably don't want to do this every frame.
var activity = metadataClient?.GetPresence(User.Id)?.Activity;
switch (activity)
{
default:
spectateButton.Enabled.Value = false;
break;
case UserActivity.InSoloGame:
case UserActivity.InMultiplayerGame:
case UserActivity.InPlaylistGame:
spectateButton.Enabled.Value = true;
break;
}
}
[BackgroundDependencyLoader]
private void load()
{
@@ -240,14 +221,13 @@ namespace osu.Game.Overlays.Dashboard
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
new PurpleRoundedButton
spectateButton = new PurpleRoundedButton
{
RelativeSizeAxes = Axes.X,
Text = "Spectate",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))),
Enabled = { BindTarget = CanSpectate }
}
}
},
@@ -107,12 +107,7 @@ namespace osu.Game.Overlays.MedalSplash
},
};
description.AddText(medal.Description, s =>
{
s.Anchor = Anchor.TopCentre;
s.Origin = Anchor.TopCentre;
s.Font = s.Font.With(size: 16);
});
description.AddText(medal.Description, s => s.Font = s.Font.With(size: 16));
medalContainer.OnLoadComplete += _ =>
{
@@ -177,15 +177,18 @@ namespace osu.Game.Overlays.Mods
bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(originalDifficulty);
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(originalDifficulty);
mod.ApplyToDifficulty(adjustedDifficulty);
Ruleset ruleset = GameRuleset.Value.CreateInstance();
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate);
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);
circleSizeDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.CircleSize, adjustedDifficulty.CircleSize);
drainRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.DrainRate, adjustedDifficulty.DrainRate);
approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate);
overallDifficultyDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty);
@@ -223,15 +223,28 @@ namespace osu.Game.Overlays.Mods
inputManager = GetContainingInputManager()!;
}
private double timeUntilCollapse;
private const double collapse_grace_time = 180;
private const float collapse_grace_position = 40;
protected override void Update()
{
base.Update();
if (ExpandedState.Value == ModCustomisationPanelState.Expanded
&& !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position)
&& inputManager.DraggedDrawable == null)
if (ExpandedState.Value == ModCustomisationPanelState.Expanded)
{
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
bool canCollapse = !DrawRectangle.Inflate(new Vector2(collapse_grace_position)).Contains(ToLocalSpace(inputManager.CurrentState.Mouse.Position))
&& inputManager.DraggedDrawable == null;
if (canCollapse)
{
if (timeUntilCollapse <= 0)
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
timeUntilCollapse -= Time.Elapsed;
}
else
timeUntilCollapse = collapse_grace_time;
}
}
}
@@ -5,10 +5,10 @@ using osu.Game.Localisation;
namespace osu.Game.Overlays.OSD
{
public partial class CopyUrlToast : Toast
public partial class CopiedToClipboardToast : Toast
{
public CopyUrlToast()
: base(CommonStrings.General, ToastStrings.UrlCopied, "")
public CopiedToClipboardToast()
: base(CommonStrings.General, ToastStrings.CopiedToClipboard, "")
{
}
}
@@ -93,7 +93,7 @@ namespace osu.Game.Overlays.Profile.Header
addSpacer(topLinkContainer);
if (user.IsOnline)
if (user.WasRecentlyOnline)
{
topLinkContainer.AddText(UsersStrings.ShowLastvisitOnline);
addSpacer(topLinkContainer);
+11 -1
View File
@@ -5,6 +5,8 @@ using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Logging;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -76,13 +78,16 @@ namespace osu.Game.Overlays.Settings
}
}
private partial class BuildDisplay : OsuAnimatedButton
private partial class BuildDisplay : OsuAnimatedButton, IHasContextMenu
{
private readonly string version;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
public BuildDisplay(string version)
{
this.version = version;
@@ -108,6 +113,11 @@ namespace osu.Game.Overlays.Settings
Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
});
}
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyToClipboard(version))
};
}
}
}
+8 -1
View File
@@ -14,6 +14,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
@@ -56,7 +57,13 @@ namespace osu.Game.Overlays
private SettingsSubPanel lastOpenedSubPanel;
protected override Drawable CreateHeader() => new SettingsHeader(Title, Description);
protected override Drawable CreateFooter() => new SettingsFooter();
protected override Drawable CreateFooter() => new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new SettingsFooter()
};
public SettingsOverlay()
: base(false)
@@ -344,6 +344,7 @@ namespace osu.Game.Rulesets.Difficulty
public double TotalBreakTime => baseBeatmap.TotalBreakTime;
public IEnumerable<BeatmapStatistic> GetStatistics() => baseBeatmap.GetStatistics();
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
public int BeatmapVersion => baseBeatmap.BeatmapVersion;
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
public double AudioLeadIn
@@ -96,7 +96,7 @@ namespace osu.Game.Scoring.Legacy
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
beatmapOffset = currentBeatmap.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
/* score.HpGraphString = */
sr.ReadString();
@@ -142,7 +142,7 @@ namespace osu.Game.Scoring.Legacy
StringBuilder replayData = new StringBuilder();
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
double offset = beatmap?.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
int lastTime = 0;
+9
View File
@@ -13,6 +13,7 @@ using osu.Framework.Bindables;
using osu.Framework.Lists;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
@@ -133,6 +134,8 @@ namespace osu.Game.Screens.Edit
BeatmapInfo.Metadata.PreviewTime = s.NewValue;
EndChange();
});
BeatmapVersion = PlayableBeatmap.BeatmapVersion;
}
/// <summary>
@@ -286,6 +289,8 @@ namespace osu.Game.Screens.Edit
set => PlayableBeatmap.Bookmarks = value;
}
public int BeatmapVersion { get; set; }
public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
@@ -456,6 +461,10 @@ namespace osu.Game.Screens.Edit
if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0)
return;
// if the user is doing edits to this beatmaps via this flow, we better bump the beatmap version
// because the beatmap encoder can only output this specific beatmap version anyway,
// so *not* bumping it could lead to results that look misleading at best.
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION;
beatmapProcessor.PreProcess();
foreach (var h in batchPendingDeletes) processHitObject(h);
@@ -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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -13,7 +14,9 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest
@@ -228,5 +231,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
editor.RestoreState(editorState);
return base.OnExiting(e);
}
protected override ResultsScreen CreateResults(ScoreInfo score) => throw new NotSupportedException();
}
}
+4 -6
View File
@@ -6,9 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
namespace osu.Game.Screens.Menu
{
@@ -20,7 +18,7 @@ namespace osu.Game.Screens.Menu
[Resolved]
private GameHost host { get; set; } = null!;
private SkinnableSound? sample;
private StarFountainSounds sounds = null!;
[BackgroundDependencyLoader]
private void load()
@@ -41,7 +39,7 @@ namespace osu.Game.Screens.Menu
Origin = Anchor.BottomRight,
X = -250,
},
sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot"))
sounds = new StarFountainSounds()
};
}
@@ -83,9 +81,9 @@ namespace osu.Game.Screens.Menu
break;
}
// Don't play SFX when game is in background as it can be a bit noisy.
// Don't play SFX when game is in background, as it can be a bit noisy.
if (host.IsActive.Value)
sample?.Play();
sounds.Play();
}
}
}
@@ -0,0 +1,71 @@
// 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.Threading;
using osu.Game.Audio;
using osu.Game.Skinning;
namespace osu.Game.Screens.Menu
{
public partial class StarFountainSounds : CompositeComponent
{
private const int shoot_retrigger_delay = 500;
private const int loop_fade_duration = 500;
private double? lastPlayback;
private SkinnableSound shootSample = null!;
private PausableSkinnableSound loopSample = null!;
private ScheduledDelegate? loopFadeDelegate;
private ScheduledDelegate? loopStopDelegate;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
shootSample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot")),
loopSample = new PausableSkinnableSound(new SampleInfo("Gameplay/fountain-loop")) { Looping = true },
};
}
public void Play()
{
loopFadeDelegate?.Cancel();
loopStopDelegate?.Cancel();
try
{
// Only play 'shootSample' if enough time has passed since last `Play()` call.
if (lastPlayback == null || Time.Current - lastPlayback > shoot_retrigger_delay)
{
loopSample.Stop();
shootSample.Play();
return;
}
// Only call `Play()` if `loopSample` is not already playing, to prevent restarting the sample each time.
if (!loopSample.RequestedPlaying)
{
this.TransformBindableTo(loopSample.Volume, 1);
loopSample.Play();
}
// Schedule a volume fadeout, followed by a `Stop()`.
loopFadeDelegate = Scheduler.AddDelayed(() =>
{
this.TransformBindableTo(loopSample.Volume, 0, loop_fade_duration);
loopStopDelegate = Scheduler.AddDelayed(() => loopSample.Stop(), loop_fade_duration);
}, shoot_retrigger_delay);
}
finally
{
lastPlayback = Time.Current;
}
}
}
}
@@ -100,7 +100,6 @@ namespace osu.Game.Screens.Menu
t.Padding = new MarginPadding { Left = 5, Top = 1 };
t.Font = t.Font.With(size: font_size);
t.Origin = Anchor.Centre;
t.Colour = colours.Pink;
Schedule(() =>
@@ -355,7 +355,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
items.AddRange([
new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))),
new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)))
new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyToClipboard(formatRoomUrl(Room.RoomID.Value)))
]);
}
@@ -351,7 +351,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
joiningRoomOperation?.Dispose();
joiningRoomOperation = null;
onFailure?.Invoke(error);
if (onFailure != null)
onFailure(error);
else
Logger.Log(error, level: LogLevel.Error);
});
});
@@ -88,12 +88,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (exception?.GetHubExceptionMessage() is string message)
onFailure(message);
else
{
const string generic_failure_message = "Failed to join multiplayer room.";
if (result.Exception != null)
Logger.Error(result.Exception, generic_failure_message);
onFailure(generic_failure_message);
}
onFailure($"Failed to join multiplayer room: {exception?.Message}");
}
});
}
@@ -198,11 +198,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return multiplayerLeaderboard.TeamScores.Count == 2
? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
{
ShowUserStatistics = true,
IsLocalPlay = true,
}
: new MultiplayerResultsScreen(score, Room.RoomID.Value, PlaylistItem)
{
ShowUserStatistics = true
IsLocalPlay = true,
};
}
@@ -59,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistItemScoreResultsScreen(score, Room.RoomID.Value, PlaylistItem)
{
AllowRetry = true,
ShowUserStatistics = true,
IsLocalPlay = true,
};
}
+8 -2
View File
@@ -67,6 +67,12 @@ namespace osu.Game.Screens.Play.HUD
}
}
public FillDirection FillDirection
{
get => iconsContainer.Direction;
set => iconsContainer.Direction = value;
}
private readonly FillFlowContainer<ModIcon> iconsContainer;
public ModDisplay(bool showExtendedInformation = true)
@@ -122,13 +128,13 @@ namespace osu.Game.Screens.Play.HUD
private void expand(double duration = 500)
{
if (ExpansionMode != ExpansionMode.AlwaysContracted)
iconsContainer.TransformSpacingTo(new Vector2(5, 0), duration, Easing.OutQuint);
iconsContainer.TransformSpacingTo(new Vector2(5, -10), duration, Easing.OutQuint);
}
private void contract(double duration = 500)
{
if (ExpansionMode != ExpansionMode.AlwaysExpanded)
iconsContainer.TransformSpacingTo(new Vector2(-25, 0), duration, Easing.OutQuint);
iconsContainer.TransformSpacingTo(new Vector2(-25), duration, Easing.OutQuint);
}
protected override bool OnHover(HoverEvent e)
@@ -30,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD
[SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.ExpansionMode), nameof(SkinnableModDisplayStrings.ExpansionModeDescription))]
public Bindable<ExpansionMode> ExpansionModeSetting { get; } = new Bindable<ExpansionMode>();
[SettingSource(typeof(SkinnableModDisplayStrings), nameof(SkinnableModDisplayStrings.DisplayDirection))]
public Bindable<Direction> Direction { get; } = new Bindable<Direction>();
[BackgroundDependencyLoader]
private void load()
{
@@ -50,6 +53,7 @@ namespace osu.Game.Screens.Play.HUD
ShowExtendedInformation.BindValueChanged(_ => modDisplay.ShowExtendedInformation = ShowExtendedInformation.Value, true);
ExpansionModeSetting.BindValueChanged(_ => modDisplay.ExpansionMode = ExpansionModeSetting.Value, true);
Direction.BindValueChanged(_ => modDisplay.FillDirection = Direction.Value == Framework.Graphics.Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical, true);
FinishTransforms(true);
}
+3
View File
@@ -147,6 +147,9 @@ namespace osu.Game.Screens.Play
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
// This display is potentially a duplicate of users with a local ModDisplay in their skins.
// It would be very nice to remove this, but the version here has special logic with regards to replays
// and initial states, so needs a bit of thought before doing so.
ModDisplay = CreateModsContainer(),
}
},
@@ -6,11 +6,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play
{
@@ -21,7 +19,7 @@ namespace osu.Game.Screens.Play
private Bindable<bool> kiaiStarFountains = null!;
private SkinnableSound? sample;
private StarFountainSounds sounds = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
@@ -44,7 +42,7 @@ namespace osu.Game.Screens.Play
Origin = Anchor.BottomRight,
X = -75,
},
sample = new SkinnableSound(new SampleInfo("Gameplay/fountain-shoot"))
sounds = new StarFountainSounds(),
};
}
@@ -72,7 +70,7 @@ namespace osu.Game.Screens.Play
leftFountain.Shoot(1);
rightFountain.Shoot(-1);
sample?.Play();
sounds.Play();
}
public partial class GameplayStarFountain : StarFountain
+21 -3
View File
@@ -5,9 +5,12 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Audio;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
@@ -31,14 +34,29 @@ namespace osu.Game.Screens.Play
OnResume?.Invoke();
};
private readonly IBindable<bool> windowActive = new Bindable<bool>(true);
private float targetVolume => windowActive.Value && State.Value == Visibility.Visible ? 1.0f : 0;
[BackgroundDependencyLoader]
private void load()
private void load(GameHost? host)
{
AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("Gameplay/pause-loop"))
{
Looping = true,
Volume = { Value = 0 }
});
if (host != null)
windowActive.BindTo(host.IsActive);
}
protected override void LoadComplete()
{
base.LoadComplete();
// Schedule required because host.IsActive doesn't seem to always run on the update thread.
windowActive.BindValueChanged(_ => Schedule(() => pauseLoop.VolumeTo(targetVolume, 1000, Easing.Out)));
}
public void StopAllSamples()
@@ -53,7 +71,7 @@ namespace osu.Game.Screens.Play
{
base.PopIn();
pauseLoop.VolumeTo(1.0f, TRANSITION_DURATION, Easing.InQuint);
pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.InQuint);
pauseLoop.Play();
}
@@ -61,7 +79,7 @@ namespace osu.Game.Screens.Play
{
base.PopOut();
pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop());
pauseLoop.VolumeTo(targetVolume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop());
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
+1 -5
View File
@@ -1276,11 +1276,7 @@ namespace osu.Game.Screens.Play
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to be displayed in the results screen.</param>
/// <returns>The <see cref="ResultsScreen"/>.</returns>
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
{
AllowRetry = true,
ShowUserStatistics = true,
};
protected abstract ResultsScreen CreateResults(ScoreInfo score);
private void fadeOut()
{
@@ -21,6 +21,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
{
@@ -323,5 +324,11 @@ namespace osu.Game.Screens.Play
api.Queue(request);
return scoreSubmissionSource.Task;
}
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score)
{
AllowRetry = true,
IsLocalPlay = true,
};
}
}
+8 -18
View File
@@ -79,11 +79,10 @@ namespace osu.Game.Screens.Ranking
public bool AllowWatchingReplay { get; init; } = true;
/// <summary>
/// Whether the user's personal statistics should be shown on the extended statistics panel
/// after clicking the score panel associated with the <see cref="Score"/> being presented.
/// Requires <see cref="Score"/> to be present.
/// Whether the provided score is for a local user's play.
/// This will trigger elements like the user's ranking to display.
/// </summary>
public bool ShowUserStatistics { get; init; }
public bool IsLocalPlay { get; init; }
private Sample? popInSample;
@@ -121,11 +120,12 @@ namespace osu.Game.Screens.Ranking
Children = new Drawable[]
{
new GlobalScrollAdjustsVolume(),
StatisticsPanel = createStatisticsPanel().With(panel =>
StatisticsPanel = new StatisticsPanel
{
panel.RelativeSizeAxes = Axes.Both;
panel.Score.BindTarget = SelectedScore;
}),
RelativeSizeAxes = Axes.Both,
Score = { BindTarget = SelectedScore },
AchievedScore = IsLocalPlay && Score != null ? Score : null,
},
ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
@@ -353,16 +353,6 @@ namespace osu.Game.Screens.Ranking
/// <param name="direction">The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list.</param>
protected virtual Task<ScoreInfo[]> FetchNextPage(int direction) => Task.FromResult<ScoreInfo[]>([]);
/// <summary>
/// Creates the <see cref="Statistics.StatisticsPanel"/> to be used to display extended information about scores.
/// </summary>
private StatisticsPanel createStatisticsPanel()
{
return ShowUserStatistics && Score != null
? new UserStatisticsPanel(Score)
: new StatisticsPanel();
}
private Task addScores(ScoreInfo[] scores)
{
var tcs = new TaskCompletionSource();
@@ -14,10 +14,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Placeholders;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
using osuTK;
namespace osu.Game.Screens.Ranking.Statistics
@@ -28,11 +31,21 @@ namespace osu.Game.Screens.Ranking.Statistics
public readonly Bindable<ScoreInfo?> Score = new Bindable<ScoreInfo?>();
/// <summary>
/// The score which was achieved by the local user.
/// If this is set to a non-null score, an <see cref="OverallRanking"/> component will be displayed showing changes to the local user's ranking and statistics
/// when a statistics update related to this score is received from spectator server.
/// </summary>
public ScoreInfo? AchievedScore { get; init; }
protected override bool StartHidden => true;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Container content;
private readonly LoadingSpinner spinner;
@@ -97,7 +110,7 @@ namespace osu.Game.Screens.Ranking.Statistics
bool hitEventsAvailable = newScore.HitEvents.Count != 0;
Container<Drawable> container;
var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely());
var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely()).ToArray();
if (!hitEventsAvailable && statisticItems.All(c => c.RequiresHitEvents))
{
@@ -199,8 +212,53 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary>
/// <param name="newScore">The score to create the rows for.</param>
/// <param name="playableBeatmap">The beatmap on which the score was set.</param>
protected virtual ICollection<StatisticItem> CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap)
=> newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap);
protected virtual IEnumerable<StatisticItem> CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap)
{
foreach (var statistic in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap))
yield return statistic;
if (AchievedScore != null
&& newScore.UserID > 1
&& newScore.UserID == AchievedScore.UserID
&& newScore.OnlineID > 0
&& newScore.OnlineID == AchievedScore.OnlineID)
{
yield return new StatisticItem("Overall Ranking", () => new OverallRanking(newScore)
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
if (AchievedScore != null
&& newScore.BeatmapInfo!.OnlineID > 0
&& api.IsLoggedIn)
{
if (
// We may want to iterate on this condition
AchievedScore.Rank >= ScoreRank.C
)
{
yield return new StatisticItem("Tag the beatmap!", () => new UserTagControl(newScore.BeatmapInfo)
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
else
{
yield return new StatisticItem("Tag the beatmap!", () => new OsuTextFlowContainer(cp => cp.Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE, weight: FontWeight.SemiBold))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.Centre,
Text = "Set a better score to contribute to beatmap tags!",
});
}
}
}
protected override bool OnClick(ClickEvent e)
{
@@ -5,8 +5,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Scoring;
namespace osu.Game.Screens.Ranking.Statistics.User
{
@@ -14,13 +16,21 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{
private const float transition_duration = 300;
public Bindable<ScoreBasedUserStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
public Bindable<ScoreBasedUserStatisticsUpdate?> DisplayedUpdate { get; } = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private readonly IBindable<ScoreBasedUserStatisticsUpdate?> latestGlobalStatisticsUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private readonly ScoreInfo scoreInfo;
private LoadingLayer loadingLayer = null!;
private GridContainer content = null!;
public OverallRanking(ScoreInfo scoreInfo)
{
this.scoreInfo = scoreInfo;
}
[BackgroundDependencyLoader]
private void load()
private void load(UserStatisticsWatcher? userStatisticsWatcher)
{
AutoSizeAxes = Axes.Y;
AutoSizeEasing = Easing.OutQuint;
@@ -55,34 +65,44 @@ namespace osu.Game.Screens.Ranking.Statistics.User
{
new Drawable[]
{
new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new GlobalRankChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } },
new SimpleStatisticTable.Spacer(),
new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new PerformancePointsChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } },
},
[],
new Drawable[]
{
new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new MaximumComboChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } },
new SimpleStatisticTable.Spacer(),
new AccuracyChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new AccuracyChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } },
},
[],
new Drawable[]
{
new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new RankedScoreChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } },
new SimpleStatisticTable.Spacer(),
new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = StatisticsUpdate } },
new TotalScoreChangeRow { StatisticsUpdate = { BindTarget = DisplayedUpdate } },
}
}
}
};
if (userStatisticsWatcher != null)
{
latestGlobalStatisticsUpdate.BindTo(userStatisticsWatcher.LatestUpdate);
latestGlobalStatisticsUpdate.BindValueChanged(update =>
{
if (update.NewValue?.Score.MatchesOnlineID(scoreInfo) == true)
DisplayedUpdate.Value = update.NewValue;
}, true);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
StatisticsUpdate.BindValueChanged(onUpdateReceived, true);
DisplayedUpdate.BindValueChanged(onUpdateReceived, true);
FinishTransforms(true);
}
@@ -1,65 +0,0 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
namespace osu.Game.Screens.Ranking.Statistics
{
public partial class UserStatisticsPanel : StatisticsPanel
{
private readonly ScoreInfo achievedScore;
internal readonly Bindable<ScoreBasedUserStatisticsUpdate?> DisplayedUserStatisticsUpdate = new Bindable<ScoreBasedUserStatisticsUpdate?>();
private IBindable<ScoreBasedUserStatisticsUpdate?> latestGlobalStatisticsUpdate = null!;
public UserStatisticsPanel(ScoreInfo achievedScore)
{
this.achievedScore = achievedScore;
}
[BackgroundDependencyLoader]
private void load(UserStatisticsWatcher? userStatisticsWatcher)
{
if (userStatisticsWatcher != null)
{
latestGlobalStatisticsUpdate = userStatisticsWatcher.LatestUpdate.GetBoundCopy();
latestGlobalStatisticsUpdate.BindValueChanged(update =>
{
if (update.NewValue?.Score.MatchesOnlineID(achievedScore) == true)
DisplayedUserStatisticsUpdate.Value = update.NewValue;
}, true);
}
}
protected override ICollection<StatisticItem> CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap)
{
var items = base.CreateStatisticItems(newScore, playableBeatmap);
if (newScore.UserID > 1
&& newScore.UserID == achievedScore.UserID
&& newScore.OnlineID > 0
&& newScore.OnlineID == achievedScore.OnlineID)
{
items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
StatisticsUpdate = { BindTarget = DisplayedUserStatisticsUpdate }
})).ToArray();
}
return items;
}
}
}
+42 -12
View File
@@ -27,9 +27,11 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
using osuTK.Input;
@@ -37,6 +39,8 @@ namespace osu.Game.Screens.Ranking
{
public partial class UserTagControl : CompositeDrawable
{
private readonly BeatmapInfo beatmapInfo;
public override bool HandlePositionalInput => true;
private readonly Cached layout = new Cached();
@@ -53,8 +57,10 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private Bindable<WorkingBeatmap> beatmap { get; set; } = null!;
public UserTagControl(BeatmapInfo beatmapInfo)
{
this.beatmapInfo = beatmapInfo;
}
[BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics)
@@ -104,8 +110,8 @@ namespace osu.Game.Screens.Ranking
api.Queue(listTagsRequest);
}
var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmap.Value.BeatmapInfo.BeatmapSet!.OnlineID);
getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmap.Value.BeatmapInfo));
var getBeatmapSetRequest = new GetBeatmapSetRequest(beatmapInfo.BeatmapSet!.OnlineID);
getBeatmapSetRequest.Success += set => apiBeatmap.Value = set.Beatmaps.SingleOrDefault(b => b.MatchesOnlineID(beatmapInfo));
api.Queue(getBeatmapSetRequest);
}
@@ -114,7 +120,7 @@ namespace osu.Game.Screens.Ranking
loadingLayer.Show();
extraTags.Remove(tag);
var req = new AddBeatmapTagRequest(beatmap.Value.BeatmapInfo.OnlineID, tag.Id);
var req = new AddBeatmapTagRequest(beatmapInfo.OnlineID, tag.Id);
req.Success += () =>
{
tag.Voted.Value = true;
@@ -495,21 +501,45 @@ namespace osu.Game.Screens.Ranking
searchBox.Current.BindValueChanged(_ => searchContainer.SearchTerm = searchBox.Current.Value, true);
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (base.OnPressed(e))
return true;
if (e.Repeat)
return false;
if (State.Value == Visibility.Hidden)
return false;
if (e.Action == GlobalAction.Select)
{
attemptSelect();
return true;
}
return false;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
var visibleItems = searchContainer.OfType<DrawableAddableTag>().Where(d => d.IsPresent).ToArray();
if (e.Key == Key.Enter)
{
if (visibleItems.Length == 1)
select(visibleItems.Single().Tag);
attemptSelect();
return true;
}
return base.OnKeyDown(e);
}
private void attemptSelect()
{
var visibleItems = searchContainer.OfType<DrawableAddableTag>().Where(d => d.IsPresent).ToArray();
if (visibleItems.Length == 1)
select(visibleItems.Single().Tag);
}
private void select(UserTag tag)
{
OnSelected?.Invoke(tag);
@@ -530,14 +560,14 @@ namespace osu.Game.Screens.Ranking
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load(OsuColour colours, OverlayColourProvider? colourProvider)
{
Content.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoamDark,
Colour = colourProvider?.Background3 ?? colours.GreySeaFoamDark,
Depth = float.MaxValue,
},
new FillFlowContainer
@@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select.Carousel
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
if (beatmapInfo.GetOnlineURL(api, ruleset.Value) is string url)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url)));
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url)));
if (manager != null)
items.Add(new OsuMenuItem("Mark as played", MenuItemType.Standard, () => manager.MarkPlayed(beatmapInfo)));
@@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select.Carousel
items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet)));
if (beatmapSet.GetOnlineURL(api, ruleset.Value) is string url)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyUrlToClipboard(url)));
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard(url)));
if (dialogOverlay != null)
items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet))));
@@ -238,7 +238,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
QueueMode = ServerAPIRoom.QueueMode,
AutoStartDuration = ServerAPIRoom.AutoStartDuration
},
Playlist = ServerAPIRoom.Playlist.Select(CreateMultiplayerPlaylistItem).ToList(),
Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(),
Users = { localUser },
Host = localUser
};
@@ -687,21 +687,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
return MessagePackSerializer.Deserialize<T>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
}
public static MultiplayerPlaylistItem CreateMultiplayerPlaylistItem(PlaylistItem item) => new MultiplayerPlaylistItem
{
ID = item.ID,
OwnerID = item.OwnerID,
BeatmapID = item.Beatmap.OnlineID,
BeatmapChecksum = item.Beatmap.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.ToArray(),
AllowedMods = item.AllowedMods.ToArray(),
Expired = item.Expired,
PlaylistOrder = item.PlaylistOrder ?? 0,
PlayedAt = item.PlayedAt,
StarRating = item.Beatmap.StarRating,
};
public override Task DisconnectInternal()
{
isConnected.Value = false;
+1
View File
@@ -90,6 +90,7 @@ namespace osu.Game.Users
private void updatePresence()
{
// TODO: we probably don't want to do this every frame.
UserPresence? presence = metadata?.GetPresence(User.OnlineID);
UserStatus status = presence?.Status ?? UserStatus.Offline;
UserActivity? activity = presence?.Activity;
+2 -2
View File
@@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.313.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.313.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.318.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.318.0" />
<PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" />
+1 -1
View File
@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.313.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.318.1" />
</ItemGroup>
</Project>