1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-23 02:39:52 +08:00

Compare commits

..

133 Commits

113 changed files with 1694 additions and 704 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1006.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1012.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup>
@@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -137,5 +140,53 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (increaseCombo)
combo++;
}
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
{
bool scoreV2 = mods.Any(m => m is ModScoreV2);
double multiplier = 1.0;
foreach (var mod in mods)
{
switch (mod)
{
case CatchModNoFail:
multiplier *= scoreV2 ? 1.0 : 0.5;
break;
case CatchModEasy:
multiplier *= 0.5;
break;
case CatchModHalfTime:
case CatchModDaycore:
multiplier *= 0.3;
break;
case CatchModHidden:
multiplier *= scoreV2 ? 1.0 : 1.06;
break;
case CatchModHardRock:
multiplier *= 1.12;
break;
case CatchModDoubleTime:
case CatchModNightcore:
multiplier *= 1.06;
break;
case CatchModFlashlight:
multiplier *= 1.12;
break;
case CatchModRelax:
return 0;
}
}
return multiplier;
}
}
}
@@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.TopCentre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Catch.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
@@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
public DefaultCatcher()
{
Anchor = Anchor.TopCentre;
RelativeSizeAxes = Axes.Both;
InternalChild = sprite = new Sprite
{
@@ -32,6 +34,15 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
};
}
protected override void Update()
{
base.Update();
// matches stable's origin position since we're using the same catcher sprite.
// see LegacyCatcher for more information.
OriginPosition = new Vector2(DrawWidth / 2, 16f);
}
[BackgroundDependencyLoader]
private void load(TextureStore store, Bindable<CatcherAnimationState> currentState)
{
@@ -0,0 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public abstract partial class LegacyCatcher : CompositeDrawable
{
protected LegacyCatcher()
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
// in stable, catcher sprites are displayed in their raw size. stable also has catcher sprites displayed with the following scale factors applied:
// 1. 0.5x, affecting all sprites in the playfield, computed here based on lazer's catch playfield dimensions (see WIDTH/HEIGHT constants in CatchPlayfield),
// source: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L483-L494
// 2. 0.7x, a constant scale applied to all catcher sprites on construction.
AutoSizeAxes = Axes.Both;
Scale = new Vector2(0.5f * 0.7f);
}
protected override void Update()
{
base.Update();
// stable sets the Y origin position of the catcher to 16px in order for the catching range and OD scaling to align with the top of the catcher's plate in the default skin.
OriginPosition = new Vector2(DrawWidth / 2, 16f);
}
}
}
@@ -7,14 +7,12 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public partial class LegacyCatcherNew : CompositeDrawable
public partial class LegacyCatcherNew : LegacyCatcher
{
[Resolved]
private Bindable<CatcherAnimationState> currentState { get; set; } = null!;
@@ -23,25 +21,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
private Drawable currentDrawable = null!;
public LegacyCatcherNew()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
foreach (var state in Enum.GetValues<CatcherAnimationState>())
{
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
d.FillMode = FillMode.Fit;
d.Alpha = 0;
}));
AddInternal(drawables[state] = getDrawableFor(state).With(d => d.Alpha = 0));
}
currentDrawable = drawables[CatcherAnimationState.Idle];
@@ -3,30 +3,21 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public partial class LegacyCatcherOld : CompositeDrawable
public partial class LegacyCatcherOld : LegacyCatcher
{
public LegacyCatcherOld()
{
RelativeSizeAxes = Axes.Both;
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
InternalChild = (skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty()).With(d =>
{
d.Anchor = Anchor.TopCentre;
d.Origin = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
d.FillMode = FillMode.Fit;
});
InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty();
}
}
}
@@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfieldAdjustmentContainer()
{
// because we are using centre anchor/origin, we will need to limit visibility in the future
// to ensure tall windows do not get a readability advantage.
// it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values
// which are compatible with TopCentre alignment.
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
// we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8.
RelativePositionAxes = Axes.Y;
Y = (1 - playfield_size_adjust) / 4 * 3;
Size = new Vector2(playfield_size_adjust);
@@ -42,18 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
private partial class ScalingContainer : Container
{
public ScalingContainer()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
}
protected override void Update()
{
base.Update();
// in stable, fruit fall vertically from -100 to 340.
// to emulate this, we want to make our playfield 440 gameplay pixels high.
// we then offset it -100 vertically in the position set below.
const float stable_v_offset_ratio = 440 / 384f;
// in stable, fruit fall vertically from 100 pixels above the playfield top down to the catcher's Y position (i.e. -100 to 340),
// see: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjects/Fruits/HitCircleFruits.cs#L65
// we already have the playfield positioned similar to stable (see CatchPlayfieldAdjustmentContainer constructor),
// so we only need to increase this container's height 100 pixels above the playfield, and offset it to have the bottom at 340 rather than 384.
const float stable_fruit_start_position = -100;
const float stable_catcher_y_position = 340;
const float playfield_v_size_adjustment = (stable_catcher_y_position - stable_fruit_start_position) / CatchPlayfield.HEIGHT;
const float playfield_v_catcher_offset = stable_catcher_y_position - CatchPlayfield.HEIGHT;
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X);
Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale);
Scale = new Vector2(Parent!.ChildSize.X / CatchPlayfield.WIDTH);
Position = new Vector2(0f, playfield_v_catcher_offset * Scale.Y);
Size = Vector2.Divide(new Vector2(1, playfield_v_size_adjustment), Scale);
}
}
}
+7
View File
@@ -29,6 +29,13 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary>
/// The size of the catcher at 1x scale.
/// </summary>
/// <remarks>
/// This is mainly used to compute catching range, the actual catcher size may differ based on skin implementation and sprite textures.
/// This is also equivalent to the "catcherWidth" property in osu-stable when the game field and beatmap difficulty are set to default values.
/// </remarks>
/// <seealso cref="CatchPlayfield.WIDTH"/>
/// <seealso cref="CatchPlayfield.HEIGHT"/>
/// <seealso cref="IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY"/>
public const float BASE_SIZE = 106.75f;
/// <summary>
@@ -6,7 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
@@ -26,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.UI
: base(new CatchSkinComponentLookup(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
{
Anchor = Anchor.TopCentre;
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE;
Origin = Anchor.TopCentre;
CentreComponent = false;
}
}
}
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
c.Add(hitExplosionPools[poolIndex].Get(e =>
{
e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
e.Apply(new JudgementResult(new HitObject(), new ManiaJudgement()));
e.Anchor = Anchor.Centre;
e.Origin = Anchor.Centre;
@@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
}
@@ -73,7 +72,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect);
assertNoteJudgement(HitResult.IgnoreHit);
}
@@ -92,7 +90,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
}
@@ -111,7 +108,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -129,7 +125,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
@@ -149,7 +144,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -169,7 +163,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Perfect);
}
@@ -188,10 +181,31 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
}
/// <summary>
/// -----[ ]-----
/// xox o
/// </summary>
[Test]
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
{
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_head + 1),
new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
new ManiaReplayFrame(time_tail),
});
assertHeadJudgement(HitResult.Perfect);
assertComboAtJudgement(0, 1);
assertTailJudgement(HitResult.Meh);
assertComboAtJudgement(1, 0);
assertComboAtJudgement(2, 1);
}
/// <summary>
/// -----[ ]-----
/// xo x o
@@ -208,7 +222,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -228,7 +241,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Perfect);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -246,7 +258,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Miss);
}
@@ -264,7 +275,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickHit);
assertTailJudgement(HitResult.Meh);
}
@@ -358,7 +368,6 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Good);
@@ -405,7 +414,6 @@ namespace osu.Game.Rulesets.Mania.Tests
}, beatmap);
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
assertHitObjectJudgement(note, HitResult.Great);
@@ -425,7 +433,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Meh);
}
@@ -476,42 +483,6 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
[Test]
public void TestHitTailBeforeLastTick()
{
const int tick_rate = 8;
const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = time_head,
Duration = time_tail - time_head,
Column = 0,
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_last_tick - 5)
}, beatmap);
assertHeadJudgement(HitResult.Perfect);
assertLastTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Ok);
}
[Test]
public void TestZeroLength()
{
@@ -551,11 +522,8 @@ namespace osu.Game.Rulesets.Mania.Tests
private void assertNoteJudgement(HitResult result)
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
private void assertTickJudgement(HitResult result)
=> AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result));
private void assertLastTickJudgement(HitResult result)
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
private void assertComboAtJudgement(int judgementIndex, int combo)
=> AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
private ScoreAccessibleReplayPlayer currentPlayer = null!;
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Utils;
using osuTK;
@@ -43,39 +44,41 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
: base(beatmap, ruleset)
{
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap));
double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
if (IsForCurrentRuleset)
if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo);
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
{
TargetColumns /= 2;
Dual = true;
}
}
else
{
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
TargetColumns = roundedOverallDifficulty > 5 ? 7 : 6;
else if (percentSliderOrSpinner > 0.6)
TargetColumns = roundedOverallDifficulty > 4 ? 5 : 4;
else
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
TargetColumns /= 2;
Dual = true;
}
originalTargetColumns = TargetColumns;
}
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
{
double roundedCircleSize = Math.Round(beatmapInfo.Difficulty.CircleSize);
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
return GetColumnCountForNonConvert(difficulty);
double roundedCircleSize = Math.Round(difficulty.CircleSize);
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount;
if (percentSpecialObjects < 0.2)
return 7;
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
return roundedOverallDifficulty > 5 ? 7 : 6;
if (percentSpecialObjects > 0.6)
return roundedOverallDifficulty > 4 ? 5 : 4;
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty)
{
double roundedCircleSize = Math.Round(difficulty.CircleSize);
return (int)Math.Max(1, roundedCircleSize);
}
@@ -1,7 +1,12 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
namespace osu.Game.Rulesets.Mania.Difficulty
@@ -12,5 +17,50 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
return new LegacyScoreAttributes { ComboScore = 1000000 };
}
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
{
bool scoreV2 = mods.Any(m => m is ModScoreV2);
double multiplier = 1.0;
foreach (var mod in mods)
{
switch (mod)
{
case ManiaModNoFail:
multiplier *= scoreV2 ? 1.0 : 0.5;
break;
case ManiaModEasy:
multiplier *= 0.5;
break;
case ManiaModHalfTime:
case ManiaModDaycore:
multiplier *= 0.5;
break;
}
}
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
return multiplier;
// Apply key mod multipliers.
int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty);
int actualColumns = originalColumns;
actualColumns = mods.OfType<ManiaKeyMod>().SingleOrDefault()?.KeyCount ?? actualColumns;
if (mods.Any(m => m is ManiaModDualStages))
actualColumns *= 2;
if (actualColumns > originalColumns)
multiplier *= 0.9;
else if (actualColumns < originalColumns)
multiplier *= 0.9 - 0.04 * (originalColumns - actualColumns);
return multiplier;
}
}
}
@@ -8,6 +8,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@@ -23,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Edit
/// <summary>
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
/// </summary>
public partial class ManiaBeatSnapGrid : Component
public partial class ManiaBeatSnapGrid : CompositeComponent
{
private const double visible_range = 750;
@@ -53,6 +55,8 @@ namespace osu.Game.Rulesets.Mania.Edit
private readonly List<ScrollingHitObjectContainer> grids = new List<ScrollingHitObjectContainer>();
private readonly DrawablePool<DrawableGridLine> linesPool = new DrawablePool<DrawableGridLine>(50);
private readonly Cached lineCache = new Cached();
private (double start, double end)? selectionTimeRange;
@@ -60,6 +64,8 @@ namespace osu.Game.Rulesets.Mania.Edit
[BackgroundDependencyLoader]
private void load(HitObjectComposer composer)
{
AddInternal(linesPool);
foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages)
{
foreach (var column in stage.Columns)
@@ -85,17 +91,10 @@ namespace osu.Game.Rulesets.Mania.Edit
}
}
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
private void createLines()
{
foreach (var grid in grids)
{
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
availableLines.Push(line);
grid.Clear();
}
if (selectionTimeRange == null)
return;
@@ -131,10 +130,13 @@ namespace osu.Game.Rulesets.Mania.Edit
foreach (var grid in grids)
{
if (!availableLines.TryPop(out var line))
line = new DrawableGridLine();
var line = linesPool.Get();
line.Apply(new HitObject
{
StartTime = time
});
line.HitObject.StartTime = time;
line.Colour = colour;
grid.Add(line);
@@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteBodyJudgement : ManiaJudgement
{
public override HitResult MaxResult => HitResult.IgnoreHit;
public override HitResult MinResult => HitResult.ComboBreak;
}
}
@@ -1,12 +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.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
@@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
switch (result)
{
case HitResult.LargeTickHit:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.LargeTickMiss:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Meh:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania
public bool Matches(BeatmapInfo beatmapInfo)
{
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo)));
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty)));
}
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
-13
View File
@@ -385,22 +385,9 @@ namespace osu.Game.Rulesets.Mania
HitResult.Good,
HitResult.Ok,
HitResult.Meh,
HitResult.LargeTickHit,
};
}
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "hold tick";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
@@ -35,10 +35,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
public DrawableHoldNoteBody Body => bodyContainer.Child;
private Container<DrawableHoldNoteHead> headContainer;
private Container<DrawableHoldNoteTail> tailContainer;
private Container<DrawableHoldNoteTick> tickContainer;
private Container<DrawableHoldNoteBody> bodyContainer;
private PausableSkinnableSound slidingSample;
@@ -60,12 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; }
/// <summary>
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
/// </summary>
public double? HoldBrokenTime { get; private set; }
/// <summary>
/// Whether the hold note has been released potentially without having caused a break.
/// Used to decide whether to visually clamp the hold note to the judgement line.
/// </summary>
private double? releaseTime;
@@ -103,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
}
},
bodyContainer = new Container<DrawableHoldNoteBody> { RelativeSizeAxes = Axes.Both },
bodyPiece = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
{
RelativeSizeAxes = Axes.Both,
@@ -110,7 +107,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
RelativeSizeAxes = Axes.X
},
tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
slidingSample = new PausableSkinnableSound { Looping = true }
});
@@ -118,7 +114,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
maskedContents.AddRange(new[]
{
bodyPiece.CreateProxy(),
tickContainer.CreateProxy(),
tailContainer.CreateProxy(),
});
}
@@ -136,7 +131,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
sizingContainer.Size = Vector2.One;
HoldStartTime = null;
HoldBrokenTime = null;
releaseTime = null;
}
@@ -154,8 +148,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tailContainer.Child = tail;
break;
case DrawableHoldNoteTick tick:
tickContainer.Add(tick);
case DrawableHoldNoteBody body:
bodyContainer.Child = body;
break;
}
}
@@ -165,7 +159,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
base.ClearNestedHitObjects();
headContainer.Clear(false);
tailContainer.Clear(false);
tickContainer.Clear(false);
bodyContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
@@ -178,8 +172,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
case HeadNote head:
return new DrawableHoldNoteHead(head);
case HoldNoteTick tick:
return new DrawableHoldNoteTick(tick);
case HoldNoteBody body:
return new DrawableHoldNoteBody(body);
}
return base.CreateNestedHitObject(hitObject);
@@ -266,20 +260,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
if (Tail.AllJudged)
{
foreach (var tick in tickContainer)
{
if (!tick.Judged)
tick.MissForcefully();
}
if (Tail.IsHit)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else
MissForcefully();
}
if (Tail.Judged && !Tail.IsHit)
HoldBrokenTime = Time.Current;
// Make sure that the hold note is fully judged by giving the body a judgement.
if (Tail.AllJudged && !Body.AllJudged)
Body.TriggerResult(Tail.IsHit);
}
public override void MissForcefully()
@@ -333,22 +322,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (e.Action != Action.Value)
return;
// Make sure a hold was started
if (HoldStartTime == null)
return;
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
if ((Clock as IGameplayClock)?.IsRewinding == true)
return;
Tail.UpdateResult();
endHold();
// When our action is released and we are in the middle of a hold, there's a chance that
// the user has released too early (before the tail).
//
// In such a case, we want to record this against the DrawableHoldNoteBody.
if (HoldStartTime != null)
{
Tail.UpdateResult();
Body.TriggerResult(Tail.IsHit);
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
HoldBrokenTime = Time.Current;
releaseTime = Time.Current;
endHold();
releaseTime = Time.Current;
}
}
private void endHold()
@@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public partial class DrawableHoldNoteBody : DrawableManiaHitObject<HoldNoteBody>
{
public bool HasHoldBreak => AllJudged && !IsHit;
public override bool DisplayResult => false;
public DrawableHoldNoteBody()
: this(null)
{
}
public DrawableHoldNoteBody(HoldNoteBody hitObject)
: base(hitObject)
{
}
internal void TriggerResult(bool hit)
{
if (AllJudged) return;
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
}
@@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak;
if (result > HitResult.Meh && hasComboBreak)
result = HitResult.Meh;
r.Type = result;
@@ -1,110 +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.
#nullable disable
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="HoldNoteTick"/> hit object.
/// </summary>
public partial class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
{
/// <summary>
/// References the time at which the user started holding the hold note.
/// </summary>
private Func<double?> holdStartTime;
private Container glowContainer;
public DrawableHoldNoteTick()
: this(null)
{
}
public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(glowContainer = new CircularContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
AccentColour.BindValueChanged(colour =>
{
glowContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 2f,
Roundness = 15f,
Colour = colour.NewValue.Opacity(0.3f)
};
}, true);
}
protected override void OnApply()
{
base.OnApply();
Debug.Assert(ParentHitObject != null);
var holdNote = (DrawableHoldNote)ParentHitObject;
holdStartTime = () => holdNote.HoldStartTime;
}
protected override void OnFree()
{
base.OnFree();
holdStartTime = null;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime)
return;
double? startTime = holdStartTime?.Invoke();
if (startTime == null || startTime > HitObject.StartTime)
ApplyResult(r => r.Type = r.Judgement.MinResult);
else
ApplyResult(r => r.Type = r.Judgement.MaxResult);
}
}
}
@@ -3,6 +3,9 @@
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// The head note of a <see cref="HoldNote"/>.
/// </summary>
public class HeadNote : Note
{
}
+8 -30
View File
@@ -6,8 +6,6 @@
using System.Collections.Generic;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -81,27 +79,18 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary>
public TailNote Tail { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
/// <summary>
/// The time between ticks of this hold.
/// The body of the hold.
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
/// </summary>
private double tickSpacing = 50;
public HoldNoteBody Body { get; private set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
}
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
createTicks(cancellationToken);
AddNested(Head = new HeadNote
{
StartTime = StartTime,
@@ -115,23 +104,12 @@ namespace osu.Game.Rulesets.Mania.Objects
Column = Column,
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
}
private void createTicks(CancellationToken cancellationToken)
{
if (tickSpacing == 0)
return;
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
AddNested(Body = new HoldNoteBody
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new HoldNoteTick
{
StartTime = t,
Column = Column
});
}
StartTime = StartTime,
Column = Column
});
}
public override Judgement CreateJudgement() => new IgnoreJudgement();
@@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// The body of a <see cref="HoldNote"/>.
/// Mostly a dummy hitobject that provides the judgement for the "holding" state.<br />
/// On hit - the hold note was held correctly for the full duration.<br />
/// On miss - the hold note was released at some point during its judgement period.
/// </summary>
public class HoldNoteBody : ManiaHitObject
{
public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
@@ -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.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// A scoring tick of a hold note.
/// </summary>
public class HoldNoteTick : ManiaHitObject
{
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}
@@ -6,6 +6,9 @@ using osu.Game.Rulesets.Mania.Judgements;
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// The tail note of a <see cref="HoldNote"/>.
/// </summary>
public class TailNote : Note
{
/// <summary>
@@ -209,7 +209,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
protected override void Update()
{
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
if (holdNote.Body.HasHoldBreak)
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
@@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -69,9 +68,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public void Animate(JudgementResult result)
{
if (result.Judgement is HoldNoteTickJudgement)
return;
(explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(FADE_IN_DURATION)
+1 -1
View File
@@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
}
private void onSourceChanged()
@@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -150,9 +149,6 @@ namespace osu.Game.Rulesets.Mania.UI
// scale roughly in-line with visual appearance of notes
Vector2 scale = new Vector2(1, 0.6f);
if (result.Judgement is HoldNoteTickJudgement)
scale *= 0.5f;
this.ScaleTo(scale);
largeFaint
-4
View File
@@ -195,10 +195,6 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
// Tick judgements should not display text.
if (judgedObject is DrawableHoldNoteTick)
return;
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{
@@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@@ -174,5 +177,58 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (increaseCombo)
combo++;
}
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
{
bool scoreV2 = mods.Any(m => m is ModScoreV2);
double multiplier = 1.0;
foreach (var mod in mods)
{
switch (mod)
{
case OsuModNoFail:
multiplier *= scoreV2 ? 1.0 : 0.5;
break;
case OsuModEasy:
multiplier *= 0.5;
break;
case OsuModHalfTime:
case OsuModDaycore:
multiplier *= 0.3;
break;
case OsuModHidden:
multiplier *= 1.06;
break;
case OsuModHardRock:
multiplier *= scoreV2 ? 1.10 : 1.06;
break;
case OsuModDoubleTime:
case OsuModNightcore:
multiplier *= scoreV2 ? 1.20 : 1.12;
break;
case OsuModFlashlight:
multiplier *= 1.12;
break;
case OsuModSpunOut:
multiplier *= 0.9;
break;
case OsuModRelax:
case OsuModAutopilot:
return 0;
}
}
return multiplier;
}
}
}
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private DrawableHitObject drawableObject { get; set; } = null!;
public LegacyApproachCircle()
: base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS)
: base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2)
{
}
@@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
@@ -194,5 +197,47 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (increaseCombo)
combo++;
}
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
{
bool scoreV2 = mods.Any(m => m is ModScoreV2);
double multiplier = 1.0;
foreach (var mod in mods)
{
switch (mod)
{
case TaikoModNoFail:
multiplier *= scoreV2 ? 1.0 : 0.5;
break;
case TaikoModEasy:
multiplier *= 0.5;
break;
case TaikoModHalfTime:
case TaikoModDaycore:
multiplier *= 0.3;
break;
case TaikoModHidden:
case TaikoModHardRock:
multiplier *= 1.06;
break;
case TaikoModDoubleTime:
case TaikoModNightcore:
case TaikoModFlashlight:
multiplier *= 1.12;
break;
case TaikoModRelax:
return 0;
}
}
return multiplier;
}
}
}
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test]
public void TestDecodeBeatmapVersion()
{
using (var resStream = TestResources.OpenResource("beatmap-version.osu"))
using (var resStream = TestResources.OpenResource("beatmap-version-6.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoder = Decoder.GetDecoder<Beatmap>(stream);
@@ -45,6 +45,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[TestCase(false)]
[TestCase(true)]
public void TestPreviewPointWithOffsets(bool applyOffsets)
{
using (var resStream = TestResources.OpenResource("beatmap-version-4.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoder = Decoder.GetDecoder<Beatmap>(stream);
((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(-1, working.BeatmapInfo.Metadata.PreviewTime);
}
}
[Test]
public void TestDecodeBeatmapGeneral()
{
@@ -915,10 +934,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestLegacyDefaultsPreserved()
[TestCase(false)]
[TestCase(true)]
public void TestLegacyDefaultsPreserved(bool applyOffsets)
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = applyOffsets };
using (var memoryStream = new MemoryStream())
using (var stream = new LineBufferedReader(memoryStream))
@@ -127,8 +127,9 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestScoreUpgradeSuccess()
[TestCase(30000002)]
[TestCase(30000003)]
public void TestScoreUpgradeSuccess(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@@ -138,7 +139,7 @@ namespace osu.Game.Tests.Database
{
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
{
TotalScoreVersion = 30000002,
TotalScoreVersion = scoreVersion,
LegacyTotalScore = 123456,
IsLegacyScore = true,
});
@@ -0,0 +1,48 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Testing;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Tests.Input
{
[HeadlessTest]
public partial class RealmKeyBindingTest : OsuTestScene
{
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Test]
public void TestUnmapGlobalAction()
{
var keyBinding = new RealmKeyBinding(GlobalAction.ToggleReplaySettings, KeyCombination.FromKey(Key.Z));
AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)GlobalAction.ToggleReplaySettings));
AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(GlobalAction.ToggleReplaySettings));
}
[TestCase(typeof(OsuRuleset), OsuAction.Smoke, null)]
[TestCase(typeof(TaikoRuleset), TaikoAction.LeftCentre, null)]
[TestCase(typeof(CatchRuleset), CatchAction.MoveRight, null)]
[TestCase(typeof(ManiaRuleset), ManiaAction.Key7, 7)]
public void TestUnmapRulesetActions(Type rulesetType, object action, int? variant)
{
string rulesetName = ((Ruleset)Activator.CreateInstance(rulesetType)!).ShortName;
var keyBinding = new RealmKeyBinding(action, KeyCombination.FromKey(Key.Z), rulesetName, variant);
AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)action));
AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(action));
}
}
}
@@ -0,0 +1,4 @@
osu file format v4
[General]
PreviewTime: -1
@@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Tests.Rulesets.Scoring
{
[TestFixture]
public class HitResultTest
{
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })]
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })]
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })]
[TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })]
public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults)
{
HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray();
Assert.Multiple(() =>
{
foreach (var max in maxResults)
{
foreach (var min in minResults)
Assert.DoesNotThrow(() => HitResultExtensions.ValidateHitResultPair(max, min), $"{max} + {min} should be supported.");
foreach (var unsupported in unsupportedResults)
Assert.Throws<ArgumentOutOfRangeException>(() => HitResultExtensions.ValidateHitResultPair(max, unsupported), $"{max} + {unsupported} should not be supported.");
}
});
}
}
}
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
for (int i = 0; i < 4; i++)
{
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult))
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].CreateJudgement())
{
Type = i == 2 ? minResult : hitResult
};
@@ -259,6 +259,41 @@ namespace osu.Game.Tests.Rulesets.Scoring
}
#pragma warning restore CS0618
[Test]
public void TestComboBreak()
{
Assert.That(HitResult.ComboBreak.IncreasesCombo(), Is.False);
Assert.That(HitResult.ComboBreak.BreaksCombo(), Is.True);
Assert.That(HitResult.ComboBreak.AffectsCombo(), Is.True);
Assert.That(HitResult.ComboBreak.AffectsAccuracy(), Is.False);
Assert.That(HitResult.ComboBreak.IsBasic(), Is.False);
Assert.That(HitResult.ComboBreak.IsTick(), Is.False);
Assert.That(HitResult.ComboBreak.IsBonus(), Is.False);
Assert.That(HitResult.ComboBreak.IsHit(), Is.False);
Assert.That(HitResult.ComboBreak.IsScorable(), Is.True);
Assert.That(HitResultExtensions.ALL_TYPES, Does.Contain(HitResult.ComboBreak));
beatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List<HitObject>
{
new TestHitObject(HitResult.Great),
new TestHitObject(HitResult.IgnoreHit, HitResult.ComboBreak),
}
};
scoreProcessor = new TestScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Great });
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.ComboBreak });
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
}
[Test]
public void TestAccuracyWhenNearPerfect()
{
@@ -275,7 +310,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
for (int i = 0; i < beatmap.HitObjects.Count; i++)
{
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement())
{
Type = i == 0 ? HitResult.Miss : HitResult.Great
});
@@ -293,24 +328,31 @@ namespace osu.Game.Tests.Rulesets.Scoring
{
public override HitResult MaxResult { get; }
public TestJudgement(HitResult maxResult)
public override HitResult MinResult => minResult ?? base.MinResult;
private readonly HitResult? minResult;
public TestJudgement(HitResult maxResult, HitResult? minResult = null)
{
MaxResult = maxResult;
this.minResult = minResult;
}
}
private class TestHitObject : HitObject
{
private readonly HitResult maxResult;
private readonly HitResult? minResult;
public override Judgement CreateJudgement()
{
return new TestJudgement(maxResult);
return new TestJudgement(maxResult, minResult);
}
public TestHitObject(HitResult maxResult)
public TestHitObject(HitResult maxResult, HitResult? minResult = null)
{
this.maxResult = maxResult;
this.minResult = minResult;
}
}
@@ -11,8 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.JudgementCounter;
@@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay
};
});
protected override Ruleset CreateRuleset() => new ManiaRuleset();
protected override Ruleset CreateRuleset() => new OsuRuleset();
private void applyOneJudgement(HitResult result)
{
@@ -15,6 +15,9 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
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.Mods;
using osu.Game.Rulesets.Objects;
@@ -26,6 +29,7 @@ using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@@ -98,6 +102,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
AddStep("reset last played", () => Realm.Write(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)!.LastPlayed = null));
AddAssert("last played is null", () => getLastPlayed() == null);
CreateTest();
@@ -150,6 +155,40 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
}
[Test]
public void TestGuestScoreIsStoredAsGuest()
{
AddStep("set up API", () => ((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(new APIUser
{
Username = "Guest",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
});
AddStep("log out", () => API.Logout());
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("log back in", () => API.Login("username", "password"));
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
AddAssert("score is not associated with online user", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID))!.UserID == APIUser.SYSTEM_USER_ID);
}
[Test]
public void TestReplayExport()
{
@@ -11,6 +11,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -28,6 +29,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
});
AddStep("Start track playing", () =>
{
Beatmap.Value.Track.Start();
});
AddStep("initialise gameplay", () =>
{
Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.ServerAPIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo)
@@ -37,7 +43,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);
AddAssert("gameplay clock is paused", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
AddAssert("gameplay clock is not running", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted());
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
AddAssert("gameplay clock is running", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
}
[Test]
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Navigation
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
.ChildrenOfType<VariantBindingsSubsection>()
.FirstOrDefault(s => s.Ruleset.ShortName == "osu");
.FirstOrDefault(s => s.Ruleset!.ShortName == "osu");
private OsuButton configureBindingsButton => Game.Settings
.ChildrenOfType<BindingSettings>().SingleOrDefault()?
@@ -2,16 +2,19 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI;
@@ -34,6 +37,49 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestMania() => createSwitchTestFor(new ManiaRuleset());
[Test]
public void TestShowRateAdjusts()
{
AddStep("create mod icons", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Full,
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods()
.OfType<ModRateAdjust>()
.SelectMany(m =>
{
List<TestModSwitchTiny> icons = new List<TestModSwitchTiny> { new TestModSwitchTiny(m) };
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
{
m = (ModRateAdjust)m.DeepClone();
m.SpeedChange.Value = i;
icons.Add(new TestModSwitchTiny(m, true));
}
return icons;
}),
};
});
AddStep("adjust rates", () =>
{
foreach (var icon in this.ChildrenOfType<TestModSwitchTiny>())
{
if (icon.Mod is ModRateAdjust rateAdjust)
{
rateAdjust.SpeedChange.Value = RNG.NextDouble() > 0.9
? rateAdjust.SpeedChange.Default
: RNG.NextDouble(rateAdjust.SpeedChange.MinValue, rateAdjust.SpeedChange.MaxValue);
}
}
});
AddToggleStep("toggle active", active => this.ChildrenOfType<TestModSwitchTiny>().ForEach(s => s.Active.Value = active));
}
private void createSwitchTestFor(Ruleset ruleset)
{
AddStep("no colour scheme", () => Child = createContent(ruleset, null));
@@ -43,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme));
}
AddToggleStep("toggle active", active => this.ChildrenOfType<ModSwitchTiny>().ForEach(s => s.Active.Value = active));
AddToggleStep("toggle active", active => this.ChildrenOfType<TestModSwitchTiny>().ForEach(s => s.Active.Value = active));
}
private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme)
@@ -62,7 +108,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(5),
ChildrenEnumerable = group.Select(mod => new ModSwitchTiny(mod))
ChildrenEnumerable = group.Select(mod => new TestModSwitchTiny(mod))
})
};
@@ -81,5 +127,15 @@ namespace osu.Game.Tests.Visual.UserInterface
return switchFlow;
}
private partial class TestModSwitchTiny : ModSwitchTiny
{
public new IMod Mod => base.Mod;
public TestModSwitchTiny(IMod mod, bool showExtendedInformation = false)
: base(mod, showExtendedInformation)
{
}
}
}
}
@@ -5,11 +5,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
@@ -31,6 +33,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public double TimeToCompleteProgress { get; set; } = 2000;
private readonly UserLookupCache userLookupCache = new TestUserLookupCache();
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -60,6 +64,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep(@"simple #2", sendAmazingNotification);
AddStep(@"progress #1", sendUploadProgress);
AddStep(@"progress #2", sendDownloadProgress);
AddStep(@"User notification", sendUserNotification);
checkProgressingCount(2);
@@ -537,6 +542,16 @@ namespace osu.Game.Tests.Visual.UserInterface
progressingNotifications.Add(n);
}
private void sendUserNotification()
{
var user = userLookupCache.GetUserAsync(0).GetResultSafely();
if (user == null) return;
var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!");
notificationOverlay.Post(n);
}
private void sendUploadProgress()
{
var n = new ProgressNotification
@@ -84,6 +84,7 @@ namespace osu.Game.Tournament.Components
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
flow = new FillFlowContainer
{
+3 -1
View File
@@ -235,7 +235,9 @@ namespace osu.Game
Logger.Log("Querying for scores that need total score conversion...");
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
&& (s.TotalScoreVersion == 30000002
|| s.TotalScoreVersion == 30000003))
.AsEnumerable().Select(s => s.ID)));
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
@@ -196,7 +196,8 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"PreviewTime":
metadata.PreviewTime = getOffsetTime(Parsing.ParseInt(pair.Value));
int time = Parsing.ParseInt(pair.Value);
metadata.PreviewTime = time == -1 ? time : getOffsetTime(time);
break;
case @"SampleSet":
@@ -10,7 +10,6 @@ using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.IO.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -225,28 +224,30 @@ namespace osu.Game.Database
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
return ConvertFromLegacyTotalScore(score, attributes);
return ConvertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
}
/// <summary>
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
/// </summary>
/// <param name="score">The score to convert the total score of.</param>
/// <param name="attributes">Difficulty attributes providing the legacy scoring values
/// (<see cref="DifficultyAttributes.LegacyAccuracyScore"/>, <see cref="DifficultyAttributes.LegacyComboScore"/>, and <see cref="DifficultyAttributes.LegacyBonusScoreRatio"/>)
/// for the beatmap which the score was set on.</param>
/// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns>
public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyScoreAttributes attributes)
public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
{
if (!score.IsLegacyScore)
return score.TotalScore;
Debug.Assert(score.LegacyTotalScore != null);
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
Ruleset ruleset = score.Ruleset.CreateInstance();
if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore;
double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty);
int maximumLegacyAccuracyScore = attributes.AccuracyScore;
long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * modMultiplier);
long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier);
double maximumLegacyBonusRatio = attributes.BonusScoreRatio;
// The part of total score that doesn't include bonus.
@@ -258,6 +259,8 @@ namespace osu.Game.Database
// The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore.
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
switch (score.Ruleset.OnlineID)
{
case 0:
+1
View File
@@ -78,6 +78,7 @@ namespace osu.Game.Graphics
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.Miss:
case HitResult.ComboBreak:
return Red;
case HitResult.Meh:
@@ -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.Input;
@@ -13,6 +14,8 @@ namespace osu.Game.Input.Bindings
{
public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput, IKeyBindingHandler<GlobalAction>
{
protected override bool Prioritised => true;
private readonly IKeyBindingHandler<GlobalAction>? handler;
public GlobalActionContainer(OsuGameBase? game)
@@ -22,22 +25,62 @@ namespace osu.Game.Input.Bindings
handler = h;
}
protected override bool Prioritised => true;
// IMPORTANT: Take care when changing order of the items in the enumerable.
// It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(EditorKeyBindings)
.Concat(InGameKeyBindings)
.Concat(ReplayKeyBindings)
.Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings)
/// <summary>
/// All default key bindings across all categories, ordered with highest priority first.
/// </summary>
/// <remarks>
/// IMPORTANT: Take care when changing order of the items in the enumerable.
/// It is used to decide the order of precedence, with the earlier items having higher precedence.
/// </remarks>
public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings
.Concat(editorKeyBindings)
.Concat(inGameKeyBindings)
.Concat(replayKeyBindings)
.Concat(songSelectKeyBindings)
.Concat(audioControlKeyBindings)
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
// It has generally been agreed on that local screens like the editor should have priority,
// based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
.Concat(OverlayKeyBindings);
.Concat(overlayKeyBindings);
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
public static IEnumerable<KeyBinding> GetDefaultBindingsFor(GlobalActionCategory category)
{
switch (category)
{
case GlobalActionCategory.General:
return globalKeyBindings;
case GlobalActionCategory.Editor:
return editorKeyBindings;
case GlobalActionCategory.InGame:
return inGameKeyBindings;
case GlobalActionCategory.Replay:
return replayKeyBindings;
case GlobalActionCategory.SongSelect:
return songSelectKeyBindings;
case GlobalActionCategory.AudioControl:
return audioControlKeyBindings;
case GlobalActionCategory.Overlays:
return overlayKeyBindings;
default:
throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}");
}
}
public static IEnumerable<GlobalAction> GetGlobalActionsFor(GlobalActionCategory category)
=> GetDefaultBindingsFor(category).Select(binding => binding.Action).Cast<GlobalAction>().Distinct();
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
private static IEnumerable<KeyBinding> globalKeyBindings => new[]
{
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
@@ -67,7 +110,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
};
public IEnumerable<KeyBinding> OverlayKeyBindings => new[]
private static IEnumerable<KeyBinding> overlayKeyBindings => new[]
{
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
@@ -77,7 +120,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
};
public IEnumerable<KeyBinding> EditorKeyBindings => new[]
private static IEnumerable<KeyBinding> editorKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode),
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
@@ -101,7 +144,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
};
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
@@ -118,7 +161,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
};
public IEnumerable<KeyBinding> ReplayKeyBindings => new[]
private static IEnumerable<KeyBinding> replayKeyBindings => new[]
{
new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
@@ -127,7 +170,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings),
};
public IEnumerable<KeyBinding> SongSelectKeyBindings => new[]
private static IEnumerable<KeyBinding> songSelectKeyBindings => new[]
{
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
@@ -136,7 +179,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods),
};
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
private static IEnumerable<KeyBinding> audioControlKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
@@ -153,10 +196,6 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.PlayPause, GlobalAction.MusicPlay),
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
};
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
}
public enum GlobalAction
@@ -365,4 +404,15 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
EditorToggleRotateControl,
}
public enum GlobalActionCategory
{
General,
Editor,
InGame,
Replay,
SongSelect,
AudioControl,
Overlays
}
}
@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Input.Bindings;
using osu.Game.Database;
using osu.Game.Rulesets;
using Realms;
namespace osu.Game.Input.Bindings
@@ -26,6 +28,13 @@ namespace osu.Game.Input.Bindings
set => KeyCombinationString = value.ToString();
}
/// <summary>
/// The resultant action which is triggered by this binding.
/// </summary>
/// <remarks>
/// This implementation always returns an integer.
/// If wanting to get the actual enum-typed value, use <see cref="GetAction"/>.
/// </remarks>
[Ignored]
public object Action
{
@@ -53,5 +62,20 @@ namespace osu.Game.Input.Bindings
private RealmKeyBinding()
{
}
public object GetAction(RulesetStore rulesets)
{
if (string.IsNullOrEmpty(RulesetName))
return (GlobalAction)ActionInt;
var ruleset = rulesets.GetRuleset(RulesetName);
var actionType = ruleset!.CreateInstance()
.GetDefaultKeyBindings(Variant ?? 0)
.First() // let's just assume nobody does something stupid like mix multiple types...
.Action
.GetType();
return Enum.ToObject(actionType, ActionInt);
}
}
}
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
/// <summary>
/// "Invite player"
/// </summary>
public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -93,6 +93,11 @@ Please try changing your audio device to a working setting.");
/// </summary>
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
/// <summary>
/// "{0} invited you to the multiplayer match &quot;{1}&quot;! Click to join."
/// </summary>
public static LocalisableString InvitedYouToTheMultiplayer(string username, string roomName) => new TranslatableString(getKey(@"invited_you_to_the_multiplayer"), @"{0} invited you to the multiplayer match ""{1}""! Click to join.", username, roomName);
/// <summary>
/// "You do not have the beatmap for this replay."
/// </summary>
@@ -14,6 +14,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
/// <summary>
/// "Can&#39;t invite this user as you have blocked them or they have blocked you."
/// </summary>
public static LocalisableString InviteFailedUserBlocked => new TranslatableString(getKey(@"cant_invite_this_user_as"), @"Can't invite this user as you have blocked them or they have blocked you.");
/// <summary>
/// "Can&#39;t invite this user as they have opted out of non-friend communications."
/// </summary>
public static LocalisableString InviteFailedUserOptOut => new TranslatableString(getKey(@"cant_invite_this_user_as1"), @"Can't invite this user as they have opted out of non-friend communications.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -42,6 +42,14 @@ namespace osu.Game.Online.Multiplayer
/// <param name="user">The user.</param>
Task UserKicked(MultiplayerRoomUser user);
/// <summary>
/// Signals that the local user has been invited into a multiplayer room.
/// </summary>
/// <param name="invitedBy">Id of user that invited the player.</param>
/// <param name="roomID">Id of the room the user got invited to.</param>
/// <param name="password">Password to join the room.</param>
Task Invited(int invitedBy, long roomID, string password);
/// <summary>
/// Signal that the host of the room has changed.
/// </summary>
@@ -99,5 +99,13 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
/// <param name="playlistItemId">The item to remove.</param>
Task RemovePlaylistItem(long playlistItemId);
/// <summary>
/// Invites a player to the current room.
/// </summary>
/// <param name="userId">The user to invite.</param>
/// <exception cref="UserBlockedException">The user has blocked or has been blocked by the invited user.</exception>
/// <exception cref="UserBlocksPMsException">The invited user does not accept private messages.</exception>
Task InvitePlayer(int userId);
}
}
@@ -23,6 +23,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osu.Game.Localisation;
namespace osu.Game.Online.Multiplayer
{
@@ -30,6 +31,8 @@ namespace osu.Game.Online.Multiplayer
{
public Action<Notification>? PostNotification { protected get; set; }
public Action<Room, string>? PresentMatch { protected get; set; }
/// <summary>
/// Invoked when any change occurs to the multiplayer room.
/// </summary>
@@ -260,6 +263,8 @@ namespace osu.Game.Online.Multiplayer
protected abstract Task LeaveRoomInternal();
public abstract Task InvitePlayer(int userId);
/// <summary>
/// Change the current <see cref="MultiplayerRoom"/> settings.
/// </summary>
@@ -440,6 +445,38 @@ namespace osu.Game.Online.Multiplayer
return handleUserLeft(user, UserKicked);
}
async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password)
{
APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false);
Room? apiRoom = await getRoomAsync(roomID).ConfigureAwait(false);
if (apiUser == null || apiRoom == null) return;
PostNotification?.Invoke(
new UserAvatarNotification(apiUser, NotificationsStrings.InvitedYouToTheMultiplayer(apiUser.Username, apiRoom.Name.Value))
{
Activated = () =>
{
PresentMatch?.Invoke(apiRoom, password);
return true;
}
}
);
Task<Room?> getRoomAsync(long id)
{
TaskCompletionSource<Room?> taskCompletionSource = new TaskCompletionSource<Room?>();
var request = new GetRoomRequest(id);
request.Success += room => taskCompletionSource.TrySetResult(room);
request.Failure += _ => taskCompletionSource.TrySetResult(null);
API.Queue(request);
return taskCompletionSource.Task;
}
}
private void addUserToAPIRoom(MultiplayerRoomUser user)
{
Debug.Assert(APIRoom != null);
@@ -12,6 +12,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Notifications;
using osu.Game.Localisation;
namespace osu.Game.Online.Multiplayer
{
@@ -50,6 +52,7 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked);
connection.On<int, long, string>(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited);
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
@@ -106,6 +109,32 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
}
public override async Task InvitePlayer(int userId)
{
if (!IsConnected.Value)
return;
Debug.Assert(connection != null);
try
{
await connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId).ConfigureAwait(false);
}
catch (HubException exception)
{
switch (exception.GetHubExceptionMessage())
{
case UserBlockedException.MESSAGE:
PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserBlocked });
break;
case UserBlocksPMsException.MESSAGE:
PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserOptOut });
break;
}
}
}
public override Task TransferHost(int userId)
{
if (!IsConnected.Value)
@@ -0,0 +1,25 @@
// 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.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
{
[Serializable]
public class UserBlockedException : HubException
{
public const string MESSAGE = @"Cannot perform action due to user being blocked.";
public UserBlockedException()
: base(MESSAGE)
{
}
protected UserBlockedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
@@ -0,0 +1,25 @@
// 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.Runtime.Serialization;
using Microsoft.AspNetCore.SignalR;
namespace osu.Game.Online.Multiplayer
{
[Serializable]
public class UserBlocksPMsException : HubException
{
public const string MESSAGE = "Cannot perform action because user has disabled non-friend communications.";
public UserBlocksPMsException()
: base(MESSAGE)
{
}
protected UserBlocksPMsException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
+21
View File
@@ -46,6 +46,7 @@ using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Music;
@@ -58,6 +59,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
@@ -643,6 +645,24 @@ namespace osu.Game
});
}
/// <summary>
/// Join a multiplayer match immediately.
/// </summary>
/// <param name="room">The room to join.</param>
/// <param name="password">The password to join the room, if any is given.</param>
public void PresentMultiplayerMatch(Room room, string password)
{
PerformFromScreen(screen =>
{
if (!(screen is Multiplayer multiplayer))
screen.Push(multiplayer = new Multiplayer());
multiplayer.Join(room, password);
});
// TODO: We should really be able to use `validScreens: new[] { typeof(Multiplayer) }` here
// but `PerformFromScreen` doesn't understand nested stacks.
}
/// <summary>
/// Present a score's replay immediately.
/// The user should have already requested this interactively.
@@ -853,6 +873,7 @@ namespace osu.Game
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
MultiplayerClient.PostNotification = n => Notifications.Post(n);
MultiplayerClient.PresentMatch = PresentMultiplayerMatch;
// make config aware of how to lookup skins for on-screen display purposes.
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
@@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Dashboard
{
users.GetUserAsync(userId).ContinueWith(task =>
{
var user = task.GetResultSafely();
APIUser user = task.GetResultSafely();
if (user == null)
return;
@@ -130,6 +130,9 @@ namespace osu.Game.Overlays.Dashboard
if (!playingUsers.Contains(user.Id))
return;
// TODO: remove this once online state is being updated more correctly.
user.IsOnline = true;
userFlow.Add(createUserPanel(user));
});
});
+4 -2
View File
@@ -113,7 +113,8 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.X,
Children = new[]
{
new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }),
// The main section adds as a catch-all for notifications which don't group into other sections.
new NotificationSection(AccountsStrings.NotificationsTitle),
new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }),
}
}
@@ -205,7 +206,8 @@ namespace osu.Game.Overlays
var ourType = notification.GetType();
int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType)));
var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes?.Any(accept => accept.IsAssignableFrom(ourType)) == true)
?? sections.First();
section.Add(notification, depth);
@@ -53,6 +53,8 @@ namespace osu.Game.Overlays.Notifications
public virtual string PopInSampleName => "UI/notification-default";
public virtual string PopOutSampleName => "UI/overlay-pop-out";
protected const float CORNER_RADIUS = 6;
protected NotificationLight Light;
protected Container IconContent;
@@ -128,7 +130,7 @@ namespace osu.Game.Overlays.Notifications
AutoSizeAxes = Axes.Y,
}.WithChild(MainContent = new Container
{
CornerRadius = 6,
CornerRadius = CORNER_RADIUS,
Masking = true,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -473,10 +475,9 @@ namespace osu.Game.Overlays.Notifications
base.Colour = value;
pulsateLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = ((Color4)value).Opacity(0.5f), //todo: avoid cast
Colour = ((Color4)value).Opacity(0.18f),
Type = EdgeEffectType.Glow,
Radius = 12,
Roundness = 12,
Radius = 14,
};
}
}
@@ -37,13 +37,17 @@ namespace osu.Game.Overlays.Notifications
notifications.Insert((int)position, notification);
}
public IEnumerable<Type> AcceptedNotificationTypes { get; }
/// <summary>
/// Enumerable of notification types accepted in this section.
/// If <see langword="null"/>, the section accepts any and all notifications.
/// </summary>
public IEnumerable<Type>? AcceptedNotificationTypes { get; }
private readonly LocalisableString titleText;
public NotificationSection(LocalisableString title, IEnumerable<Type> acceptedNotificationTypes)
public NotificationSection(LocalisableString title, IEnumerable<Type>? acceptedNotificationTypes = null)
{
AcceptedNotificationTypes = acceptedNotificationTypes.ToArray();
AcceptedNotificationTypes = acceptedNotificationTypes?.ToArray();
titleText = title;
}
@@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users.Drawables;
namespace osu.Game.Overlays.Notifications
{
public partial class UserAvatarNotification : Notification
{
private LocalisableString text;
public override LocalisableString Text
{
get => text;
set
{
text = value;
if (textDrawable != null)
textDrawable.Text = text;
}
}
private TextFlowContainer? textDrawable;
private readonly APIUser user;
public UserAvatarNotification(APIUser user, LocalisableString text)
{
this.user = user;
Text = text;
}
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
Light.Colour = colours.Orange2;
Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium))
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Text = text
});
IconContent.Masking = true;
IconContent.CornerRadius = CORNER_RADIUS;
IconContent.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
});
LoadComponentAsync(new DrawableAvatar(user)
{
FillMode = FillMode.Fill,
}, IconContent.Add);
}
}
}
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@@ -18,92 +19,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
public override LocalisableString Header => InputSettingsStrings.GlobalKeyBindingHeader;
public GlobalKeyBindingsSection(GlobalActionContainer manager)
[BackgroundDependencyLoader]
private void load()
{
Add(new DefaultBindingsSubsection(manager));
Add(new OverlayBindingsSubsection(manager));
Add(new AudioControlKeyBindingsSubsection(manager));
Add(new SongSelectKeyBindingSubsection(manager));
Add(new InGameKeyBindingsSubsection(manager));
Add(new ReplayKeyBindingsSubsection(manager));
Add(new EditorKeyBindingsSubsection(manager));
}
private partial class DefaultBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => string.Empty;
public DefaultBindingsSubsection(GlobalActionContainer manager)
: base(null)
AddRange(new[]
{
Defaults = manager.GlobalKeyBindings;
}
}
private partial class OverlayBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.OverlaysSection;
public OverlayBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.OverlayKeyBindings;
}
}
private partial class SongSelectKeyBindingSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.SongSelectSection;
public SongSelectKeyBindingSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.SongSelectKeyBindings;
}
}
private partial class InGameKeyBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.InGameSection;
public InGameKeyBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.InGameKeyBindings;
}
}
private partial class ReplayKeyBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.ReplaySection;
public ReplayKeyBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.ReplayKeyBindings;
}
}
private partial class AudioControlKeyBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.AudioSection;
public AudioControlKeyBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.AudioControlKeyBindings;
}
}
private partial class EditorKeyBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header => InputSettingsStrings.EditorSection;
public EditorKeyBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.EditorKeyBindings;
}
new GlobalKeyBindingsSubsection(string.Empty, GlobalActionCategory.General),
new GlobalKeyBindingsSubsection(InputSettingsStrings.OverlaysSection, GlobalActionCategory.Overlays),
new GlobalKeyBindingsSubsection(InputSettingsStrings.AudioSection, GlobalActionCategory.AudioControl),
new GlobalKeyBindingsSubsection(InputSettingsStrings.SongSelectSection, GlobalActionCategory.SongSelect),
new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame),
new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay),
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor),
});
}
}
}
@@ -0,0 +1,36 @@
// 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.Localisation;
using osu.Game.Database;
using osu.Game.Input.Bindings;
using Realms;
namespace osu.Game.Overlays.Settings.Sections.Input
{
public partial class GlobalKeyBindingsSubsection : KeyBindingsSubsection
{
protected override LocalisableString Header { get; }
private readonly GlobalActionCategory category;
public GlobalKeyBindingsSubsection(LocalisableString header, GlobalActionCategory category)
{
Header = header;
this.category = category;
Defaults = GlobalActionContainer.GetDefaultBindingsFor(category);
}
protected override IEnumerable<RealmKeyBinding> GetKeyBindings(Realm realm)
{
var bindings = realm.All<RealmKeyBinding>()
.Where(b => b.RulesetName == null && b.Variant == null)
.Detach();
var actionsInSection = GlobalActionContainer.GetGlobalActionsFor(category).Cast<int>().ToHashSet();
return bindings.Where(kb => actionsInSection.Contains(kb.ActionInt));
}
}
}
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets;
@@ -14,9 +13,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
protected override Drawable CreateHeader() => new SettingsHeader(InputSettingsStrings.KeyBindingPanelHeader, InputSettingsStrings.KeyBindingPanelDescription);
[BackgroundDependencyLoader(permitNulls: true)]
private void load(RulesetStore rulesets, GlobalActionContainer global)
private void load(RulesetStore rulesets)
{
AddSection(new GlobalKeyBindingsSection(global));
AddSection(new GlobalKeyBindingsSection());
foreach (var ruleset in rulesets.AvailableRulesets)
AddSection(new RulesetBindingsSection(ruleset));
@@ -1,15 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -37,16 +37,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
/// <summary>
/// Invoked when the binding of this row is updated with a change being written.
/// </summary>
public Action<KeyBindingRow> BindingUpdated { get; set; }
public Action<KeyBindingRow>? BindingUpdated { get; set; }
private readonly object action;
private readonly IEnumerable<RealmKeyBinding> bindings;
/// <summary>
/// Whether left and right mouse button clicks should be included in the edited bindings.
/// </summary>
public bool AllowMainMouseButtons { get; init; }
private const float transition_time = 150;
/// <summary>
/// The default key bindings for this row.
/// </summary>
public IEnumerable<KeyCombination> Defaults { get; init; } = Array.Empty<KeyCombination>();
private const float height = 20;
private const float padding = 5;
#region IFilterable
private bool matchingFilter;
@@ -60,24 +63,45 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
private Container content;
public bool FilteringActive { get; set; }
public IEnumerable<LocalisableString> FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text);
#endregion
private readonly object action;
private readonly IEnumerable<RealmKeyBinding> bindings;
private Bindable<bool> isDefault { get; } = new BindableBool(true);
[Resolved]
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Container content = null!;
private OsuSpriteText text = null!;
private FillFlowContainer cancelAndClearButtons = null!;
private FillFlowContainer<KeyButton> buttons = null!;
private KeyButton? bindTarget;
private const float transition_time = 150;
private const float height = 20;
private const float padding = 5;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
content.ReceivePositionalInputAt(screenSpacePos);
public bool FilteringActive { get; set; }
[Resolved]
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
private OsuSpriteText text;
private FillFlowContainer cancelAndClearButtons;
private FillFlowContainer<KeyButton> buttons;
private Bindable<bool> isDefault { get; } = new BindableBool(true);
public IEnumerable<LocalisableString> FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text);
public override bool AcceptsFocus => bindTarget == null;
/// <summary>
/// Creates a new <see cref="KeyBindingRow"/>.
/// </summary>
/// <param name="action">The action that this row contains bindings for.</param>
/// <param name="bindings">The keybindings to display in this row.</param>
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
{
this.action = action;
@@ -87,9 +111,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
AutoSizeAxes = Axes.Y;
}
[Resolved]
private RealmAccess realm { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@@ -205,21 +226,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input
base.OnHoverLost(e);
}
public override bool AcceptsFocus => bindTarget == null;
private KeyButton bindTarget;
public bool AllowMainMouseButtons;
public IEnumerable<KeyCombination> Defaults;
private bool isModifier(Key k) => k < Key.F1;
protected override bool OnClick(ClickEvent e) => true;
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!HasFocus || !bindTarget.IsHovered)
if (!HasFocus)
return base.OnMouseDown(e);
Debug.Assert(bindTarget != null);
if (!bindTarget.IsHovered)
return base.OnMouseDown(e);
if (!AllowMainMouseButtons)
@@ -245,6 +263,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
return;
}
Debug.Assert(bindTarget != null);
if (bindTarget.IsHovered)
finalise(false);
// prevent updating bind target before clear button's action
@@ -256,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
if (HasFocus)
{
Debug.Assert(bindTarget != null);
if (bindTarget.IsHovered)
{
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta), KeyCombination.FromScrollDelta(e.ScrollDelta).First());
@@ -272,6 +294,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus || e.Repeat)
return false;
Debug.Assert(bindTarget != null);
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromKey(e.Key));
if (!isModifier(e.Key)) finalise();
@@ -294,6 +318,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
Debug.Assert(bindTarget != null);
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromJoystickButton(e.Button));
finalise();
@@ -316,6 +342,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
Debug.Assert(bindTarget != null);
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMidiKey(e.Key));
finalise();
@@ -338,6 +366,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
Debug.Assert(bindTarget != null);
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletAuxiliaryButton(e.Button));
finalise();
@@ -360,6 +390,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (!HasFocus)
return false;
Debug.Assert(bindTarget != null);
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletPenButton(e.Button));
finalise();
@@ -473,10 +505,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
public readonly OsuSpriteText Text;
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;
private bool isBinding;
@@ -599,7 +631,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
base.Dispose(isDisposing);
if (keyCombinationProvider != null)
if (keyCombinationProvider.IsNotNull())
keyCombinationProvider.KeymapChanged -= updateKeyCombinationText;
}
}
@@ -1,19 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Localisation;
using osuTK;
using Realms;
namespace osu.Game.Overlays.Settings.Sections.Input
{
@@ -25,39 +25,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
/// </summary>
protected virtual bool AutoAdvanceTarget => false;
protected IEnumerable<Framework.Input.Bindings.KeyBinding> Defaults;
protected IEnumerable<KeyBinding> Defaults { get; init; } = Array.Empty<KeyBinding>();
public RulesetInfo Ruleset { get; protected set; }
private readonly int? variant;
protected KeyBindingsSubsection(int? variant)
protected KeyBindingsSubsection()
{
this.variant = variant;
FlowContent.Spacing = new Vector2(0, 3);
}
[BackgroundDependencyLoader]
private void load(RealmAccess realm)
{
string rulesetName = Ruleset?.ShortName;
var bindings = realm.Run(r => r.All<RealmKeyBinding>()
.Where(b => b.RulesetName == rulesetName && b.Variant == variant)
.Detach());
var bindings = realm.Run(r => GetKeyBindings(r).Detach());
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
{
int intKey = (int)defaultGroup.Key;
// one row per valid action.
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList())
{
AllowMainMouseButtons = Ruleset != null,
Defaults = defaultGroup.Select(d => d.KeyCombination),
BindingUpdated = onBindingUpdated
});
Add(CreateKeyBindingRow(
defaultGroup.Key,
bindings.Where(b => b.ActionInt.Equals(intKey)).ToList(),
defaultGroup)
.With(row => row.BindingUpdated = onBindingUpdated));
}
Add(new ResetButton
@@ -66,6 +55,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
});
}
protected abstract IEnumerable<RealmKeyBinding> GetKeyBindings(Realm realm);
protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable<RealmKeyBinding> keyBindings, IEnumerable<KeyBinding> defaults)
=> new KeyBindingRow(action, keyBindings.ToList())
{
AllowMainMouseButtons = false,
Defaults = defaults.Select(d => d.KeyCombination),
};
private void onBindingUpdated(KeyBindingRow sender)
{
if (AutoAdvanceTarget)
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets;
@@ -18,7 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
public RulesetBindingsSection(RulesetInfo ruleset)
{
this.ruleset = ruleset;
}
[BackgroundDependencyLoader]
private void load()
{
var r = ruleset.CreateInstance();
foreach (int variant in r.AvailableVariants)
@@ -1,8 +1,13 @@
// 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.Input.Bindings;
using osu.Framework.Localisation;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using Realms;
namespace osu.Game.Overlays.Settings.Sections.Input
{
@@ -12,15 +17,33 @@ namespace osu.Game.Overlays.Settings.Sections.Input
protected override LocalisableString Header { get; }
public RulesetInfo Ruleset { get; }
private readonly int variant;
public VariantBindingsSubsection(RulesetInfo ruleset, int variant)
: base(variant)
{
Ruleset = ruleset;
this.variant = variant;
var rulesetInstance = ruleset.CreateInstance();
Header = rulesetInstance.GetVariantName(variant);
Defaults = rulesetInstance.GetDefaultKeyBindings(variant);
}
protected override IEnumerable<RealmKeyBinding> GetKeyBindings(Realm realm)
{
string rulesetName = Ruleset.ShortName;
return realm.All<RealmKeyBinding>()
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
}
protected override KeyBindingRow CreateKeyBindingRow(object action, IEnumerable<RealmKeyBinding> keyBindings, IEnumerable<KeyBinding> defaults)
=> new KeyBindingRow(action, keyBindings.ToList())
{
AllowMainMouseButtons = true,
Defaults = defaults.Select(d => d.KeyCombination),
};
}
}
+34 -1
View File
@@ -35,7 +35,40 @@ namespace osu.Game.Rulesets.Judgements
/// <summary>
/// The minimum <see cref="HitResult"/> that can be achieved - the inverse of <see cref="MaxResult"/>.
/// </summary>
public HitResult MinResult
/// <remarks>
/// Defaults to a sane value for the given <see cref="MaxResult"/>. May be overridden to provide a supported custom value:
/// <list type="table">
/// <listheader>
/// <term><see cref="MaxResult"/>s</term>
/// <description>Valid <see cref="MinResult"/>s</description>
/// </listheader>
/// <item>
/// <term><see cref="HitResult.Perfect"/>, <see cref="HitResult.Great"/>, <see cref="HitResult.Good"/>, <see cref="HitResult.Ok"/>, <see cref="HitResult.Meh"/></term>
/// <description><see cref="HitResult.Miss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.LargeBonus"/></term>
/// <description><see cref="HitResult.IgnoreMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.SmallBonus"/></term>
/// <description><see cref="HitResult.IgnoreMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.SmallTickHit"/></term>
/// <description><see cref="HitResult.SmallTickMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.LargeTickHit"/></term>
/// <description><see cref="HitResult.LargeTickMiss"/></description>
/// </item>
/// <item>
/// <term><see cref="HitResult.IgnoreHit"/></term>
/// <description><see cref="HitResult.IgnoreMiss"/>, <see cref="HitResult.ComboBreak"/></description>
/// </item>
/// </list>
/// </remarks>
public virtual HitResult MinResult
{
get
{
@@ -672,6 +672,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (!Result.HasResult)
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
HitResultExtensions.ValidateHitResultPair(Result.Judgement.MaxResult, Result.Judgement.MinResult);
if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
{
throw new InvalidOperationException(
+73 -12
View File
@@ -120,6 +120,16 @@ namespace osu.Game.Rulesets.Scoring
[Order(12)]
IgnoreHit,
/// <summary>
/// Indicates that a combo break should occur, but does not otherwise affect score.
/// </summary>
/// <remarks>
/// May be paired with <see cref="IgnoreHit"/>.
/// </remarks>
[EnumMember(Value = "combo_break")]
[Order(15)]
ComboBreak,
/// <summary>
/// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy).
/// </summary>
@@ -165,6 +175,7 @@ namespace osu.Game.Rulesets.Scoring
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
case HitResult.LegacyComboIncrease:
case HitResult.ComboBreak:
return true;
default:
@@ -177,11 +188,19 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public static bool AffectsAccuracy(this HitResult result)
{
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
if (result == HitResult.LegacyComboIncrease)
return false;
switch (result)
{
// LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result.
case HitResult.LegacyComboIncrease:
return false;
return IsScorable(result) && !IsBonus(result);
// ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting.
case HitResult.ComboBreak:
return false;
default:
return IsScorable(result) && !IsBonus(result);
}
}
/// <summary>
@@ -189,11 +208,19 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public static bool IsBasic(this HitResult result)
{
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
if (result == HitResult.LegacyComboIncrease)
return false;
switch (result)
{
// LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result.
case HitResult.LegacyComboIncrease:
return false;
return IsScorable(result) && !IsTick(result) && !IsBonus(result);
// ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting.
case HitResult.ComboBreak:
return false;
default:
return IsScorable(result) && !IsTick(result) && !IsBonus(result);
}
}
/// <summary>
@@ -242,6 +269,7 @@ namespace osu.Game.Rulesets.Scoring
case HitResult.Miss:
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
case HitResult.ComboBreak:
return false;
default:
@@ -254,11 +282,20 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public static bool IsScorable(this HitResult result)
{
// LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
if (result == HitResult.LegacyComboIncrease)
return true;
switch (result)
{
// LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
case HitResult.LegacyComboIncrease:
return true;
return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
// ComboBreak is its own type that affects score via combo.
case HitResult.ComboBreak:
return true;
default:
// Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score.
return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
}
}
/// <summary>
@@ -291,6 +328,30 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="result">The <see cref="HitResult"/> to get the index of.</param>
/// <returns>The index of <paramref name="result"/>.</returns>
public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result);
public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult)
{
if (maxResult == HitResult.None || !IsHit(maxResult))
throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result.");
if (minResult == HitResult.None || IsHit(minResult))
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result.");
if (maxResult == HitResult.IgnoreHit && minResult is not (HitResult.IgnoreMiss or HitResult.ComboBreak))
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement.");
if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement.");
if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement.");
if (maxResult == HitResult.SmallTickHit && minResult != HitResult.SmallTickMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.SmallTickMiss} is the only valid minimum result for a {maxResult} judgement.");
if (maxResult.IsBasic() && minResult != HitResult.Miss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.Miss} is the only valid minimum result for a {maxResult} judgement.");
}
}
#pragma warning restore CS0618
}
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Scoring.Legacy
{
@@ -16,5 +18,13 @@ namespace osu.Game.Rulesets.Scoring.Legacy
/// <param name="workingBeatmap">The working beatmap.</param>
/// <param name="playableBeatmap">A playable version of the beatmap for the ruleset.</param>
LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap);
/// <summary>
/// Returns the legacy score multiplier for the mods. This is only used during legacy score conversion.
/// </summary>
/// <param name="mods">The mods.</param>
/// <param name="difficulty">Extra difficulty parameters.</param>
/// <returns>The legacy multiplier.</returns>
double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty);
}
}
@@ -0,0 +1,67 @@
// 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.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Scoring.Legacy
{
/// <summary>
/// A set of properties that are required to facilitate beatmap conversion between legacy rulesets.
/// </summary>
public class LegacyBeatmapConversionDifficultyInfo : IBeatmapDifficultyInfo
{
/// <summary>
/// The beatmap's ruleset.
/// </summary>
public IRulesetInfo SourceRuleset { get; set; } = new RulesetInfo();
/// <summary>
/// The beatmap circle size.
/// </summary>
public float CircleSize { get; set; }
/// <summary>
/// The beatmap overall difficulty.
/// </summary>
public float OverallDifficulty { get; set; }
/// <summary>
/// The count of hitcircles in the beatmap.
/// </summary>
/// <remarks>
/// When converting from osu! ruleset beatmaps, this is equivalent to the sum of sliders and spinners in the beatmap.
/// </remarks>
public int CircleCount { get; set; }
/// <summary>
/// The total count of hitobjects in the beatmap.
/// </summary>
public int TotalObjectCount { get; set; }
float IBeatmapDifficultyInfo.DrainRate => 0;
float IBeatmapDifficultyInfo.ApproachRate => 0;
double IBeatmapDifficultyInfo.SliderMultiplier => 0;
double IBeatmapDifficultyInfo.SliderTickRate => 0;
public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => new LegacyBeatmapConversionDifficultyInfo
{
SourceRuleset = apiBeatmap.Ruleset,
CircleSize = apiBeatmap.CircleSize,
OverallDifficulty = apiBeatmap.OverallDifficulty,
CircleCount = apiBeatmap.CircleCount,
TotalObjectCount = apiBeatmap.SliderCount + apiBeatmap.SpinnerCount + apiBeatmap.CircleCount
};
public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo
{
SourceRuleset = beatmap.BeatmapInfo.Ruleset,
CircleSize = beatmap.Difficulty.CircleSize,
OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
CircleCount = beatmap.HitObjects.Count(h => h is not IHasDuration),
TotalObjectCount = beatmap.HitObjects.Count
};
}
}
+88 -24
View File
@@ -3,15 +3,16 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.UI
@@ -21,8 +22,10 @@ namespace osu.Game.Rulesets.UI
public BindableBool Active { get; } = new BindableBool();
public const float DEFAULT_HEIGHT = 30;
private const float width = 73;
private readonly IMod mod;
protected readonly IMod Mod;
private readonly bool showExtendedInformation;
private readonly Box background;
private readonly OsuSpriteText acronymText;
@@ -33,33 +36,69 @@ namespace osu.Game.Rulesets.UI
private Color4 activeBackgroundColour;
private Color4 inactiveBackgroundColour;
public ModSwitchTiny(IMod mod)
{
this.mod = mod;
Size = new Vector2(73, DEFAULT_HEIGHT);
private readonly CircularContainer extendedContent;
private readonly Box extendedBackground;
private readonly OsuSpriteText extendedText;
private ModSettingChangeTracker? modSettingsChangeTracker;
InternalChild = new CircularContainer
public ModSwitchTiny(IMod mod, bool showExtendedInformation = false)
{
Mod = mod;
this.showExtendedInformation = showExtendedInformation;
AutoSizeAxes = Axes.X;
Height = DEFAULT_HEIGHT;
InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
extendedContent = new CircularContainer
{
background = new Box
Name = "extended content",
Width = 100 + DEFAULT_HEIGHT / 2,
RelativeSizeAxes = Axes.Y,
Masking = true,
X = width,
Margin = new MarginPadding { Left = -DEFAULT_HEIGHT },
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both
},
acronymText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black),
Text = mod.Acronym,
Margin = new MarginPadding
extendedBackground = new Box
{
Top = 4
}
RelativeSizeAxes = Axes.Both,
},
extendedText = new OsuSpriteText
{
Margin = new MarginPadding { Left = 3 * DEFAULT_HEIGHT / 4 },
Font = OsuFont.Default.With(size: 30f, weight: FontWeight.Bold),
UseFullGlyphHeight = false,
Text = mod.ExtendedIconInformation,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
},
new CircularContainer
{
Width = width,
RelativeSizeAxes = Axes.Y,
Masking = true,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
acronymText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black),
Text = mod.Acronym,
Margin = new MarginPadding
{
Top = 4
}
},
},
}
};
}
@@ -68,7 +107,7 @@ namespace osu.Game.Rulesets.UI
private void load(OsuColour colours, OverlayColourProvider? colourProvider)
{
inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3;
activeBackgroundColour = colours.ForModType(mod.Type);
activeBackgroundColour = colours.ForModType(Mod.Type);
inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5;
activeForegroundColour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, activeForegroundColour, 0, 1);
@@ -80,12 +119,37 @@ namespace osu.Game.Rulesets.UI
Active.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
if (Mod is Mod actualMod)
{
modSettingsChangeTracker = new ModSettingChangeTracker(new[] { actualMod });
modSettingsChangeTracker.SettingChanged = _ => updateExtendedInformation();
}
updateExtendedInformation();
}
private void updateExtendedInformation()
{
bool showExtended = showExtendedInformation && !string.IsNullOrEmpty(Mod.ExtendedIconInformation);
extendedContent.Alpha = showExtended ? 1 : 0;
extendedText.Text = Mod.ExtendedIconInformation;
}
private void updateState()
{
acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint);
background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint);
extendedText.Colour = Active.Value ? activeBackgroundColour.Lighten(0.2f) : inactiveBackgroundColour;
extendedBackground.Colour = Active.Value ? activeBackgroundColour.Darken(2.4f) : inactiveBackgroundColour.Darken(2.8f);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
modSettingsChangeTracker?.Dispose();
}
}
}
@@ -30,9 +30,10 @@ namespace osu.Game.Scoring.Legacy
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion.</description></item>
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
/// <item><description>30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores.</description></item>
/// </list>
/// </remarks>
public const int LATEST_VERSION = 30000003;
public const int LATEST_VERSION = 30000004;
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
+3
View File
@@ -190,6 +190,9 @@ namespace osu.Game.Scoring
/// </summary>
private void populateUserDetails(ScoreInfo model)
{
if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID)
return;
string username = model.RealmUser.Username;
if (usernameLookupCache.TryGetValue(username, out var existing))
+1 -1
View File
@@ -346,7 +346,7 @@ namespace osu.Game.Scoring
case HitResult.LargeBonus:
case HitResult.SmallBonus:
if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0)
yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
yield return new HitResultDisplayStatistic(r.result, value, count, r.displayName);
break;
@@ -456,6 +456,7 @@ namespace osu.Game.Screens.OnlinePlay
private IEnumerable<Drawable> createButtons() => new[]
{
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
{
Size = new Vector2(30, 30),
@@ -463,7 +464,6 @@ namespace osu.Game.Screens.OnlinePlay
Alpha = AllowShowingResults ? 1 : 0,
TooltipText = "View results"
},
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
editButton = new PlaylistEditButton
{
Size = new Vector2(30, 30),
@@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
@@ -90,6 +91,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen();
public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password));
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -148,6 +148,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
loadingDisplay.Show();
client.ChangeState(MultiplayerUserState.ReadyForGameplay);
}
// This will pause the clock, pending the gameplay started callback from the server.
GameplayClockContainer.Reset();
}
private void failAndBail(string message = null)

Some files were not shown because too many files have changed in this diff Show More