1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 14:42:56 +08:00

Merge branch 'master' into footer-v2-button-animation

This commit is contained in:
Dean Herbert 2024-05-11 21:41:50 +08:00 committed by GitHub
commit 69d699a218
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 461 additions and 368 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.423.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.509.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -1,76 +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 osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
namespace osu.Android
{
public partial class AndroidJoystickSettings : SettingsSubsection
{
protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
private readonly AndroidJoystickHandler joystickHandler;
private readonly Bindable<bool> enabled = new BindableBool(true);
private SettingsSlider<float> deadzoneSlider = null!;
private Bindable<float> handlerDeadzone = null!;
private Bindable<float> localDeadzone = null!;
public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
{
this.joystickHandler = joystickHandler;
}
[BackgroundDependencyLoader]
private void load()
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
localDeadzone = handlerDeadzone.GetUnboundCopy();
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = enabled
},
deadzoneSlider = new SettingsSlider<float>
{
LabelText = JoystickSettingsStrings.DeadzoneThreshold,
KeyboardStep = 0.01f,
DisplayAsPercentage = true,
Current = localDeadzone,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
enabled.BindTo(joystickHandler.Enabled);
enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
handlerDeadzone.BindValueChanged(val =>
{
bool disabled = localDeadzone.Disabled;
localDeadzone.Disabled = false;
localDeadzone.Value = val.NewValue;
localDeadzone.Disabled = disabled;
}, true);
localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
}
}
}

View File

@ -1,97 +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 Android.OS;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
namespace osu.Android
{
public partial class AndroidMouseSettings : SettingsSubsection
{
private readonly AndroidMouseHandler mouseHandler;
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
private Bindable<double> handlerSensitivity = null!;
private Bindable<double> localSensitivity = null!;
private Bindable<bool> relativeMode = null!;
public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
{
this.mouseHandler = mouseHandler;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig)
{
// use local bindable to avoid changing enabled state of game host's bindable.
handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
localSensitivity = handlerSensitivity.GetUnboundCopy();
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
// High precision/pointer capture is only available on Android 8.0 and up
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.HighPrecisionMouse,
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
Current = relativeMode,
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
},
new MouseSettings.SensitivitySetting
{
LabelText = MouseSettingsStrings.CursorSensitivity,
Current = localSensitivity,
},
});
}
AddRange(new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
});
}
protected override void LoadComplete()
{
base.LoadComplete();
relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
handlerSensitivity.BindValueChanged(val =>
{
bool disabled = localSensitivity.Disabled;
localSensitivity.Disabled = false;
localSensitivity.Value = val.NewValue;
localSensitivity.Disabled = disabled;
}, true);
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
}
}
}

View File

@ -5,13 +5,9 @@ using System;
using Android.App;
using Microsoft.Maui.Devices;
using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input.Handlers;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater;
using osu.Game.Utils;
@ -88,24 +84,6 @@ namespace osu.Android
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
switch (handler)
{
case AndroidMouseHandler mh:
return new AndroidMouseSettings(mh);
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
case AndroidTouchHandler th:
return new TouchSettings(th);
default:
return base.CreateSettingsSubsectionFor(handler);
}
}
private class AndroidBatteryInfo : BatteryInfo
{
public override double? ChargeLevel => Battery.ChargeLevel;

View File

@ -1,24 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M73.92,44.43C74.82,44.43 75.43,45.1 75.43,46.02V54.54C75.43,55.46 74.82,56.13 73.92,56.13C73,56.13 72.41,55.46 72.41,54.54V46.02C72.41,45.1 73,44.43 73.92,44.43ZM73.92,61.55C72.82,61.55 71.95,60.68 71.95,59.58C71.95,58.51 72.82,57.64 73.92,57.64C75.02,57.64 75.89,58.51 75.89,59.58C75.89,60.68 75.02,61.55 73.92,61.55Z"
android:fillColor="#000000"/>
<path
android:pathData="M68.41,48.55C69.33,48.55 69.92,49.22 69.92,50.11V55.77C69.92,59.94 67.35,61.55 64.22,61.55C61.08,61.55 58.5,59.94 58.5,55.77V50.11C58.5,49.22 59.09,48.55 60.01,48.55C60.91,48.55 61.52,49.22 61.52,50.11V55.56C61.52,57.84 62.48,58.74 64.22,58.74C65.94,58.74 66.9,57.84 66.9,55.56V50.11C66.9,49.22 67.51,48.55 68.41,48.55Z"
android:fillColor="#000000"/>
<path
android:pathData="M49.94,52.01C49.94,52.85 50.81,53.16 52.47,53.54C54.78,54.1 56.98,54.69 56.98,57.53C56.98,60.3 54.93,61.55 51.99,61.55C49.56,61.55 47.79,60.71 46.97,59.73C46.33,58.97 46.41,58.3 47.02,57.71C47.79,56.97 48.46,57.28 48.89,57.66C49.58,58.3 50.43,58.97 52.07,58.97C53.29,58.97 54.06,58.56 54.06,57.74C54.06,56.92 53.24,56.64 51.09,56.05C48.97,55.46 47.08,54.9 47.08,52.36C47.08,49.52 49.38,48.35 51.94,48.35C53.4,48.35 55.06,48.73 56.08,49.83C56.52,50.27 56.85,50.88 56.08,51.72C55.32,52.52 54.75,52.29 54.22,51.88C53.73,51.52 52.88,50.91 51.6,50.91C50.73,50.91 49.94,51.19 49.94,52.01Z"
android:fillColor="#000000"/>
<path
android:pathData="M38.79,61.55C34.9,61.55 32.11,58.74 32.11,54.95C32.11,51.14 34.9,48.35 38.79,48.35C42.68,48.35 45.47,51.14 45.47,54.95C45.47,58.74 42.68,61.55 38.79,61.55ZM38.79,58.74C41.04,58.74 42.45,57.1 42.45,54.95C42.45,52.8 41.04,51.14 38.79,51.14C36.54,51.14 35.13,52.8 35.13,54.95C35.13,57.1 36.54,58.74 38.79,58.74Z"
android:fillColor="#000000"/>
<path
android:pathData="M86,54C86,71.67 71.67,86 54,86C36.33,86 22,71.67 22,54C22,36.33 36.33,22 54,22C71.67,22 86,36.33 86,54ZM25.2,54C25.2,69.91 38.09,82.8 54,82.8C69.91,82.8 82.8,69.91 82.8,54C82.8,38.09 69.91,25.2 54,25.2C38.09,25.2 25.2,38.09 25.2,54Z"
android:fillColor="#000000"/>
<path
android:pathData="M36.78,54.99C36.78,56.09 37.65,56.96 38.75,56.96C39.85,56.96 40.72,56.09 40.72,54.99C40.72,53.91 39.85,53.04 38.75,53.04C37.65,53.04 36.78,53.91 36.78,54.99Z"
android:fillColor="#000000"/>
<vector android:height="108dp" android:viewportHeight="434"
android:viewportWidth="434" android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M299.36,178.05C303.08,178.05 305.62,180.81 305.62,184.62V219.92C305.62,223.74 303.08,226.5 299.36,226.5C295.55,226.5 293.11,223.74 293.11,219.92V184.62C293.11,180.81 295.55,178.05 299.36,178.05ZM299.36,248.97C294.81,248.97 291.2,245.37 291.2,240.81C291.2,236.35 294.81,232.75 299.36,232.75C303.92,232.75 307.53,236.35 307.53,240.81C307.53,245.37 303.92,248.97 299.36,248.97Z"/>
<path android:fillColor="#000000" android:pathData="M276.52,195.12C280.34,195.12 282.77,197.87 282.77,201.58V225.01C282.77,242.29 272.12,248.97 259.19,248.97C246.15,248.97 235.49,242.29 235.49,225.01V201.58C235.49,197.87 237.93,195.12 241.75,195.12C245.46,195.12 248,197.87 248,201.58V224.16C248,233.6 251.98,237.31 259.19,237.31C266.29,237.31 270.27,233.6 270.27,224.16V201.58C270.27,197.87 272.81,195.12 276.52,195.12Z"/>
<path android:fillColor="#000000" android:pathData="M200.02,209.43C200.02,212.93 203.63,214.2 210.52,215.79C220.06,218.12 229.18,220.56 229.18,232.33C229.18,243.78 220.7,248.97 208.51,248.97C198.43,248.97 191.12,245.47 187.73,241.44C185.08,238.26 185.4,235.51 187.94,233.07C191.12,229.99 193.88,231.27 195.68,232.86C198.54,235.51 202.04,238.26 208.82,238.26C213.91,238.26 217.09,236.57 217.09,233.18C217.09,229.78 213.7,228.62 204.8,226.18C196,223.74 188.15,221.41 188.15,210.91C188.15,199.15 197.69,194.27 208.29,194.27C214.34,194.27 221.23,195.86 225.47,200.42C227.27,202.22 228.65,204.76 225.47,208.26C222.29,211.55 219.96,210.6 217.73,208.9C215.71,207.41 212.22,204.87 206.92,204.87C203.31,204.87 200.02,206.04 200.02,209.43Z"/>
<path android:fillColor="#000000" android:pathData="M153.74,248.97C138.46,248.97 127.5,237.27 127.5,221.53C127.5,205.68 138.46,194.09 153.74,194.09C169.03,194.09 179.99,205.68 179.99,221.53C179.99,237.27 169.03,248.97 153.74,248.97ZM153.74,237.27C162.59,237.27 168.12,230.46 168.12,221.53C168.12,212.6 162.59,205.68 153.74,205.68C144.89,205.68 139.36,212.6 139.36,221.53C139.36,230.46 144.89,237.27 153.74,237.27Z"/>
<path android:fillColor="#000000" android:pathData="M349,217.5C349,290.13 290.13,349 217.5,349C144.88,349 86,290.13 86,217.5C86,144.88 144.88,86 217.5,86C290.13,86 349,144.88 349,217.5ZM99.15,217.5C99.15,282.86 152.14,335.85 217.5,335.85C282.86,335.85 335.85,282.86 335.85,217.5C335.85,152.14 282.86,99.15 217.5,99.15C152.14,99.15 99.15,152.14 99.15,217.5Z"/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -107,7 +107,13 @@ namespace osu.Desktop
}
}
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null }))
var hostOptions = new HostOptions
{
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
FriendlyGameName = OsuGameBase.GAME_NAME,
};
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions))
{
if (!host.IsPrimaryInstance)
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
// Generally all the control points are within the visible area all the time.
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => true;
public override bool UpdateSubTreeMasking() => true;
/// <summary>
/// Handles correction of invalid path types.

View File

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
// For osu! gameplay, everything is always on screen.
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking() => false;
public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; }

View File

@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
@ -345,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public void Add(Drawable proxy) => AddInternal(proxy);
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
public override bool UpdateSubTreeMasking()
{
// DrawableHitObject disables masking.
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@ -31,6 +32,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Beatmaps.Formats
{
@ -224,6 +226,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.User = new APIUser
{
Username = "spaceman_atlas",
Id = 3035836,
CountryCode = CountryCode.PL
};
scoreInfo.ClientVersion = "2023.1221.0";
var beatmap = new TestBeatmap(ruleset);
@ -248,6 +256,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836));
});
}
@ -413,6 +422,80 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
[Test]
public void TestTotalScoreWithoutModsReadIfPresent()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
scoreInfo.TotalScoreWithoutMods = 1_000_000;
scoreInfo.TotalScore = 1_020_000;
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
});
}
[Test]
public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
scoreInfo.TotalScoreWithoutMods = 0;
scoreInfo.TotalScore = 1_020_000;
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
});
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();

View File

@ -287,7 +287,7 @@ namespace osu.Game.Tests.Scores.IO
}
[Test]
public void TestUserLookedUpForOnlineScore()
public void TestUserLookedUpByUsernameForOnlineScoreIfUserIDMissing()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
@ -301,6 +301,9 @@ namespace osu.Game.Tests.Scores.IO
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
@ -350,7 +353,7 @@ namespace osu.Game.Tests.Scores.IO
}
[Test]
public void TestUserLookedUpForLegacyOnlineScore()
public void TestUserLookedUpByUsernameForLegacyOnlineScore()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
@ -364,6 +367,9 @@ namespace osu.Game.Tests.Scores.IO
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
@ -413,7 +419,7 @@ namespace osu.Game.Tests.Scores.IO
}
[Test]
public void TestUserNotLookedUpForOfflineScore()
public void TestUserNotLookedUpForOfflineScoreIfUserIDMissing()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
@ -427,6 +433,9 @@ namespace osu.Game.Tests.Scores.IO
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
@ -476,6 +485,73 @@ namespace osu.Game.Tests.Scores.IO
}
}
[Test]
public void TestUserLookedUpByOnlineIDIfPresent([Values] bool isOnlineScore)
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "5555")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Some other guy",
CountryCode = CountryCode.DE,
Id = 5555
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Id = 5555 },
Date = DateTimeOffset.Now,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
if (isOnlineScore)
toImport.OnlineID = 12345;
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual("Some other guy", imported.RealmUser.Username);
Assert.AreEqual(5555, imported.RealmUser.OnlineID);
Assert.AreEqual(CountryCode.DE, imported.RealmUser.CountryCode);
}
finally
{
host.Exit();
}
}
}
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
// clone to avoid attaching the input score to realm.

View File

@ -7,7 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
@ -97,15 +97,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestUnrankedBadge()
{
AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
AddUntilStep("Unranked badge shown", () => footerButtonMods.UnrankedBadge.Alpha == 1);
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<FooterButtonModsV2.UnrankedBadge>().Single().Alpha == 1);
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
AddUntilStep("Unranked badge not shown", () => footerButtonMods.UnrankedBadge.Alpha == 0);
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<FooterButtonModsV2.UnrankedBadge>().Single().Alpha == 0);
}
private void changeMods(IReadOnlyList<Mod> mods)
{
footerButtonMods.Current.Value = mods;
}
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
private bool assertModsMultiplier(IEnumerable<Mod> mods)
{
@ -117,7 +114,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class TestFooterButtonModsV2 : FooterButtonModsV2
{
public new Container UnrankedBadge => base.UnrankedBadge;
public new OsuSpriteText MultiplierText => base.MultiplierText;
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Screens.Ladder
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking() => false;
protected override void OnDrag(DragEvent e)
{

View File

@ -92,8 +92,9 @@ namespace osu.Game.Database
/// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo.
/// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values.
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
/// </summary>
private const int schema_version = 40;
private const int schema_version = 41;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -1130,6 +1131,12 @@ namespace osu.Game.Database
}
break;
case 41:
foreach (var score in migration.NewRealm.All<ScoreInfo>())
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
break;
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Database
{
@ -248,6 +249,7 @@ namespace osu.Game.Database
score.Accuracy = computeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor);
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap);
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
}
/// <summary>

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DeleteConfirmationContentStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationContent";
/// <summary>
/// "Are you sure you want to delete all beatmaps?"
/// </summary>
public static LocalisableString Beatmaps => new TranslatableString(getKey(@"beatmaps"), @"Are you sure you want to delete all beatmaps?");
/// <summary>
/// "Are you sure you want to delete all beatmaps videos? This cannot be undone!"
/// </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 delete all skins? This cannot be undone!"
/// </summary>
public static LocalisableString Skins => new TranslatableString(getKey(@"skins"), @"Are you sure you want to delete all skins? This cannot be undone!");
/// <summary>
/// "Are you sure you want to delete all collections? This cannot be undone!"
/// </summary>
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Are you sure you want to delete all collections? This cannot be undone!");
/// <summary>
/// "Are you sure you want to delete all scores? This cannot be undone!"
/// </summary>
public static LocalisableString Scores => new TranslatableString(getKey(@"collections"), @"Are you sure you want to delete all scores? This cannot be undone!");
/// <summary>
/// "Are you sure you want to delete all mod presets?"
/// </summary>
public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"Are you sure you want to delete all mod presets?");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -10,9 +10,9 @@ namespace osu.Game.Localisation
private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog";
/// <summary>
/// "Confirm deletion of"
/// "Caution"
/// </summary>
public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Confirm deletion of");
public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Caution");
/// <summary>
/// "Yes. Go for it."

View File

@ -1,19 +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 osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class FooterButtonModsV2Strings
{
private const string prefix = @"osu.Game.Resources.Localisation.FooterButtonModsV2";
/// <summary>
/// "{0} mods"
/// </summary>
public static LocalisableString Mods(int count) => new TranslatableString(getKey(@"mods"), @"{0} mods", count);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -14,10 +14,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ModSelectTitle => new TranslatableString(getKey(@"mod_select_title"), @"Mod Select");
/// <summary>
/// "{0} mods"
/// </summary>
public static LocalisableString Mods(int count) => new TranslatableString(getKey(@"mods"), @"{0} mods", count);
/// <summary>
/// "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun."
/// </summary>
public static LocalisableString ModSelectDescription => new TranslatableString(getKey(@"mod_select_description"), @"Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun.");
public static LocalisableString ModSelectDescription => new TranslatableString(getKey(@"mod_select_description"),
@"Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun.");
/// <summary>
/// "Mod Customisation"

View File

@ -33,6 +33,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("total_score")]
public long TotalScore { get; set; }
[JsonProperty("total_score_without_mods")]
public long TotalScoreWithoutMods { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
@ -206,6 +209,7 @@ namespace osu.Game.Online.API.Requests.Responses
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed,
TotalScore = TotalScore,
TotalScoreWithoutMods = TotalScoreWithoutMods,
LegacyTotalScore = LegacyTotalScore,
Accuracy = Accuracy,
MaxCombo = MaxCombo,
@ -239,6 +243,7 @@ namespace osu.Game.Online.API.Requests.Responses
{
Rank = score.Rank,
TotalScore = score.TotalScore,
TotalScoreWithoutMods = score.TotalScoreWithoutMods,
Accuracy = score.Accuracy,
PP = score.PP,
MaxCombo = score.MaxCombo,

View File

@ -75,6 +75,12 @@ namespace osu.Game
{
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
#if DEBUG
public const string GAME_NAME = "osu! (development)";
#else
public const string GAME_NAME = "osu!";
#endif
public const string OSU_PROTOCOL = "osu://";
public const string CLIENT_STREAM_NAME = @"lazer";
@ -241,11 +247,7 @@ namespace osu.Game
public OsuGameBase()
{
Name = @"osu!";
#if DEBUG
Name += " (development)";
#endif
Name = GAME_NAME;
allowableExceptions = UnhandledExceptionsBeforeCrash;
}

View File

@ -210,7 +210,7 @@ namespace osu.Game.Overlays.Dialog
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.TopCentre,
Padding = new MarginPadding { Horizontal = 5 },
Padding = new MarginPadding { Horizontal = 15 },
},
body = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 18))
{
@ -219,7 +219,7 @@ namespace osu.Game.Overlays.Dialog
TextAnchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 5 },
Padding = new MarginPadding { Horizontal = 15 },
},
buttonsContainer = new FillFlowContainer<PopupDialogButton>
{

View File

@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
deleteBeatmapsButton.Enabled.Value = false;
Task.Run(() => beatmaps.Delete()).ContinueWith(_ => Schedule(() => deleteBeatmapsButton.Enabled.Value = true));
}));
}, DeleteConfirmationContentStrings.Beatmaps));
}
});
@ -40,11 +40,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos,
Action = () =>
{
dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() =>
dialogOverlay?.Push(new MassDeleteConfirmationDialog(() =>
{
deleteBeatmapVideosButton.Enabled.Value = false;
Task.Run(beatmaps.DeleteAllVideos).ContinueWith(_ => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true));
}));
}, DeleteConfirmationContentStrings.BeatmapVideos));
}
});
AddRange(new Drawable[]

View File

@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
Text = MaintenanceSettingsStrings.DeleteAllCollections,
Action = () =>
{
dialogOverlay?.Push(new MassDeleteConfirmationDialog(deleteAllCollections));
dialogOverlay?.Push(new MassDeleteConfirmationDialog(deleteAllCollections, DeleteConfirmationContentStrings.Collections));
}
});
}

View File

@ -2,15 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Localisation;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public partial class MassDeleteConfirmationDialog : DangerousActionDialog
{
public MassDeleteConfirmationDialog(Action deleteAction)
public MassDeleteConfirmationDialog(Action deleteAction, LocalisableString deleteContent)
{
BodyText = "Everything?";
BodyText = deleteContent;
DangerousAction = deleteAction;
}
}

View File

@ -1,16 +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;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public partial class MassVideoDeleteConfirmationDialog : MassDeleteConfirmationDialog
{
public MassVideoDeleteConfirmationDialog(Action deleteAction)
: base(deleteAction)
{
BodyText = "All beatmap videos? This cannot be undone!";
}
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
deleteAllButton.Enabled.Value = false;
Task.Run(deleteAllModPresets).ContinueWith(t => Schedule(onAllModPresetsDeleted, t));
}));
}, DeleteConfirmationContentStrings.ModPresets));
}
},
undeleteButton = new SettingsButton

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
deleteScoresButton.Enabled.Value = false;
Task.Run(() => scores.Delete()).ContinueWith(_ => Schedule(() => deleteScoresButton.Enabled.Value = true));
}));
}, DeleteConfirmationContentStrings.Scores));
}
});
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
deleteSkinsButton.Enabled.Value = false;
Task.Run(() => skins.Delete()).ContinueWith(_ => Schedule(() => deleteSkinsButton.Enabled.Value = true));
}));
}, DeleteConfirmationContentStrings.Skins));
}
});
}

View File

@ -15,7 +15,6 @@ using osu.Framework.Extensions.ListExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Lists;
using osu.Framework.Threading;
using osu.Framework.Utils;
@ -632,7 +631,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
#endregion
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking() => false;
protected override void UpdateAfterChildren()
{

View File

@ -56,6 +56,14 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 };
/// <summary>
/// The total number of points awarded for the score without including mod multipliers.
/// </summary>
/// <remarks>
/// The purpose of this property is to enable future lossless rebalances of mod multipliers.
/// </remarks>
public readonly BindableLong TotalScoreWithoutMods = new BindableLong { MinValue = 0 };
/// <summary>
/// The current accuracy.
/// </summary>
@ -363,7 +371,8 @@ namespace osu.Game.Rulesets.Scoring
double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1;
double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1;
TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier);
TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion));
TotalScore.Value = (long)Math.Round(TotalScoreWithoutMods.Value * scoreMultiplier);
}
private void updateRank()
@ -446,6 +455,7 @@ namespace osu.Game.Rulesets.Scoring
score.MaximumStatistics[result] = MaximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else.
score.TotalScoreWithoutMods = TotalScoreWithoutMods.Value;
score.TotalScore = TotalScore.Value;
}

View File

@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.UI
break;
base.UpdateSubTree();
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
UpdateSubTreeMasking();
} while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds);
return true;

View File

@ -43,6 +43,12 @@ namespace osu.Game.Scoring.Legacy
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank? Rank;
[JsonProperty("user_id")]
public int UserID = -1;
[JsonProperty("total_score_without_mods")]
public long? TotalScoreWithoutMods { get; set; }
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
{
OnlineID = score.OnlineID,
@ -51,6 +57,8 @@ namespace osu.Game.Scoring.Legacy
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
ClientVersion = score.ClientVersion,
Rank = score.Rank,
UserID = score.User.OnlineID,
TotalScoreWithoutMods = score.TotalScoreWithoutMods > 0 ? score.TotalScoreWithoutMods : null,
};
}
}

View File

@ -131,6 +131,13 @@ namespace osu.Game.Scoring.Legacy
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
score.ScoreInfo.ClientVersion = readScore.ClientVersion;
decodedRank = readScore.Rank;
if (readScore.UserID > 1)
score.ScoreInfo.RealmUser.OnlineID = readScore.UserID;
if (readScore.TotalScoreWithoutMods is long totalScoreWithoutMods)
score.ScoreInfo.TotalScoreWithoutMods = totalScoreWithoutMods;
else
PopulateTotalScoreWithoutMods(score.ScoreInfo);
});
}
}
@ -242,6 +249,16 @@ namespace osu.Game.Scoring.Legacy
#pragma warning restore CS0618
}
public static void PopulateTotalScoreWithoutMods(ScoreInfo score)
{
double modMultiplier = 1;
foreach (var mod in score.Mods)
modMultiplier *= mod.ScoreMultiplier;
score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier);
}
private void readLegacyReplay(Replay replay, StreamReader reader)
{
float lastTime = beatmapOffset;

View File

@ -103,6 +103,14 @@ namespace osu.Game.Scoring
}
// Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores).
// TODO: `UserLookupCache` cannot currently be used here because of async foibles.
// It only supports lookups by user ID (username would require web changes), and even then the ID lookups cannot be used.
// That is because that component provides an async interface, and async functions cannot be consumed safely here due to the rigid structure of `RealmArchiveModelImporter`.
// The importer has two paths, one async and one sync; the async path runs the sync path in a task.
// This means that sometimes `PostImport()` is called from a sync context, and sometimes from an async one, whilst itself being a sync method.
// That in turn makes `.GetResultSafely()` not callable inside `PostImport()`, as it will throw when called from an async context,
private readonly Dictionary<int, APIUser> idLookupCache = new Dictionary<int, APIUser>();
private readonly Dictionary<string, APIUser> usernameLookupCache = new Dictionary<string, APIUser>();
protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters)
@ -127,24 +135,34 @@ namespace osu.Game.Scoring
if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID)
return;
if (model.OnlineID < 0 && model.LegacyOnlineID <= 0)
return;
string username = model.RealmUser.Username;
if (usernameLookupCache.TryGetValue(username, out var existing))
if (model.RealmUser.OnlineID > 1)
{
model.User = existing;
model.User = lookupUserById(model.RealmUser.OnlineID) ?? model.User;
return;
}
var userRequest = new GetUserRequest(username);
if (model.OnlineID < 0 && model.LegacyOnlineID <= 0)
return;
model.User = lookupUserByName(model.RealmUser.Username) ?? model.User;
}
private APIUser? lookupUserById(int id)
{
if (idLookupCache.TryGetValue(id, out var existing))
{
return existing;
}
var userRequest = new GetUserRequest(id);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
{
usernameLookupCache.TryAdd(username, new APIUser
APIUser cachedUser;
idLookupCache.TryAdd(id, cachedUser = new APIUser
{
// Because this is a permanent cache, let's only store the pieces we're interested in,
// rather than the full API response. If we start to store more than these three fields
@ -154,8 +172,41 @@ namespace osu.Game.Scoring
CountryCode = user.CountryCode,
});
model.User = user;
return cachedUser;
}
return null;
}
private APIUser? lookupUserByName(string username)
{
if (usernameLookupCache.TryGetValue(username, out var existing))
{
return existing;
}
var userRequest = new GetUserRequest(username);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
{
APIUser cachedUser;
usernameLookupCache.TryAdd(username, cachedUser = new APIUser
{
// Because this is a permanent cache, let's only store the pieces we're interested in,
// rather than the full API response. If we start to store more than these three fields
// in realm, this should be undone.
Id = user.Id,
Username = user.Username,
CountryCode = user.CountryCode,
});
return cachedUser;
}
return null;
}
}
}

View File

@ -65,8 +65,19 @@ namespace osu.Game.Scoring
public bool DeletePending { get; set; }
/// <summary>
/// The total number of points awarded for the score.
/// </summary>
public long TotalScore { get; set; }
/// <summary>
/// The total number of points awarded for the score without including mod multipliers.
/// </summary>
/// <remarks>
/// The purpose of this property is to enable future lossless rebalances of mod multipliers.
/// </remarks>
public long TotalScoreWithoutMods { get; set; }
/// <summary>
/// The version of processing applied to calculate total score as stored in the database.
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -20,7 +19,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public partial class TimelineTickDisplay : TimelinePart<PointVisualisation>
{
// With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away.
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
public override bool UpdateSubTreeMasking() => false;
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;

View File

@ -32,6 +32,11 @@ namespace osu.Game.Screens.Select.FooterV2
{
// todo: see https://github.com/ppy/osu-framework/issues/3271
private const float torus_scale_factor = 1.2f;
private const float bar_shear_width = 7f;
private const float bar_height = 37f;
private const float mod_display_portion = 0.65f;
private static readonly Vector2 bar_shear = new Vector2(bar_shear_width / bar_height, 0);
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
@ -43,7 +48,7 @@ namespace osu.Game.Screens.Select.FooterV2
private Container modDisplayBar = null!;
protected Container UnrankedBadge { get; private set; } = null!;
private Drawable unrankedBadge = null!;
private ModDisplay modDisplay = null!;
private OsuSpriteText modCountText = null!;
@ -59,57 +64,19 @@ namespace osu.Game.Screens.Select.FooterV2
[BackgroundDependencyLoader]
private void load()
{
const float bar_shear_width = 7f;
const float bar_height = 37f;
const float mod_display_portion = 0.65f;
var barShear = new Vector2(bar_shear_width / bar_height, 0);
Text = "Mods";
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colours.Lime1;
AddRange(new[]
{
UnrankedBadge = new ContainerWithTooltip
{
Position = new Vector2(BUTTON_WIDTH + 5f, -5f),
Depth = float.MaxValue,
Origin = Anchor.BottomLeft,
Shear = barShear,
CornerRadius = CORNER_RADIUS,
AutoSizeAxes = Axes.X,
Height = bar_height,
Masking = true,
BorderColour = Color4.White,
BorderThickness = 2f,
TooltipText = ModSelectOverlayStrings.UnrankedExplanation,
Children = new Drawable[]
{
new Box
{
Colour = colours.Orange2,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -barShear,
Text = ModSelectOverlayStrings.Unranked.ToUpper(),
Margin = new MarginPadding { Horizontal = 15 },
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold),
Colour = Color4.Black,
}
}
},
unrankedBadge = new UnrankedBadge(),
modDisplayBar = new Container
{
Y = -5f,
Depth = float.MaxValue,
Origin = Anchor.BottomLeft,
Shear = barShear,
Shear = bar_shear,
CornerRadius = CORNER_RADIUS,
Size = new Vector2(BUTTON_WIDTH, bar_height),
Masking = true,
@ -139,7 +106,7 @@ namespace osu.Game.Screens.Select.FooterV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -barShear,
Shear = -bar_shear,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold)
}
@ -161,7 +128,7 @@ namespace osu.Game.Screens.Select.FooterV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -barShear,
Shear = -bar_shear,
Scale = new Vector2(0.6f),
Current = { BindTarget = Current },
ExpansionMode = ExpansionMode.AlwaysContracted,
@ -170,7 +137,7 @@ namespace osu.Game.Screens.Select.FooterV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -barShear,
Shear = -bar_shear,
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold),
Mods = { BindTarget = Current },
}
@ -215,8 +182,8 @@ namespace osu.Game.Screens.Select.FooterV2
modDisplay.FadeOut(duration, easing);
modCountText.FadeOut(duration, easing);
UnrankedBadge.MoveToY(20, duration, easing);
UnrankedBadge.FadeOut(duration, easing);
unrankedBadge.MoveToY(20, duration, easing);
unrankedBadge.FadeOut(duration, easing);
// add delay to let unranked indicator hide first before resizing the button back to its original width.
this.Delay(duration).ResizeWidthTo(BUTTON_WIDTH, duration, easing);
@ -233,21 +200,21 @@ namespace osu.Game.Screens.Select.FooterV2
if (Current.Value.Any(m => !m.Ranked))
{
UnrankedBadge.MoveToX(BUTTON_WIDTH + 5, duration, easing);
UnrankedBadge.FadeIn(duration, easing);
unrankedBadge.MoveToX(0, duration, easing);
unrankedBadge.FadeIn(duration, easing);
this.ResizeWidthTo(BUTTON_WIDTH + UnrankedBadge.DrawWidth + 10, duration, easing);
this.ResizeWidthTo(BUTTON_WIDTH + 5 + unrankedBadge.DrawWidth, duration, easing);
}
else
{
UnrankedBadge.MoveToX(BUTTON_WIDTH + 5 - UnrankedBadge.DrawWidth, duration, easing);
UnrankedBadge.FadeOut(duration, easing);
unrankedBadge.MoveToX(-unrankedBadge.DrawWidth, duration, easing);
unrankedBadge.FadeOut(duration, easing);
this.ResizeWidthTo(BUTTON_WIDTH, duration, easing);
}
modDisplayBar.MoveToY(-5, duration, Easing.OutQuint);
UnrankedBadge.MoveToY(-5, duration, easing);
unrankedBadge.MoveToY(-5, duration, easing);
modDisplayBar.FadeIn(duration, easing);
}
@ -266,13 +233,16 @@ namespace osu.Game.Screens.Select.FooterV2
{
public readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>();
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
Mods.BindValueChanged(v => Text = FooterButtonModsV2Strings.Mods(v.NewValue.Count).ToUpper(), true);
Mods.BindValueChanged(v => Text = ModSelectOverlayStrings.Mods(v.NewValue.Count).ToUpper(), true);
}
public ITooltip<IReadOnlyList<Mod>> GetCustomTooltip() => new ModTooltip();
public ITooltip<IReadOnlyList<Mod>> GetCustomTooltip() => new ModTooltip(colourProvider);
public IReadOnlyList<Mod>? TooltipContent => Mods.Value;
@ -280,8 +250,16 @@ namespace osu.Game.Screens.Select.FooterV2
{
private ModDisplay extendedModDisplay = null!;
[Cached]
private OverlayColourProvider colourProvider;
public ModTooltip(OverlayColourProvider colourProvider)
{
this.colourProvider = colourProvider;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = CORNER_RADIUS;
@ -315,9 +293,49 @@ namespace osu.Game.Screens.Select.FooterV2
}
}
private partial class ContainerWithTooltip : Container, IHasTooltip
internal partial class UnrankedBadge : CompositeDrawable, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
public LocalisableString TooltipText { get; }
public UnrankedBadge()
{
Margin = new MarginPadding { Left = BUTTON_WIDTH + 5f };
Y = -5f;
Depth = float.MaxValue;
Origin = Anchor.BottomLeft;
Shear = bar_shear;
CornerRadius = CORNER_RADIUS;
AutoSizeAxes = Axes.X;
Height = bar_height;
Masking = true;
BorderColour = Color4.White;
BorderThickness = 2f;
TooltipText = ModSelectOverlayStrings.UnrankedExplanation;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Orange2,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -bar_shear,
Text = ModSelectOverlayStrings.Unranked.ToUpper(),
Margin = new MarginPadding { Horizontal = 15 },
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold),
Colour = Color4.Black,
}
};
}
}
}
}

View File

@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.423.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.410.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.509.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.510.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.36.0" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.423.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.509.0" />
</ItemGroup>
</Project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 KiB

After

Width:  |  Height:  |  Size: 386 KiB