75 Commits

Author SHA1 Message Date
c37f72f567 jvnkosu initial bringup
Some checks failed
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Test (map[fullname:ubuntu-latest prettyname:Linux], MultiThreaded) (push) Has been cancelled
Continuous Integration / Test (map[fullname:ubuntu-latest prettyname:Linux], SingleThread) (push) Has been cancelled
Continuous Integration / Test (map[fullname:windows-latest prettyname:Windows], MultiThreaded) (push) Has been cancelled
Continuous Integration / Test (map[fullname:windows-latest prettyname:Windows], SingleThread) (push) Has been cancelled
Continuous Integration / Build only (Android) (push) Has been cancelled
Continuous Integration / Build only (iOS) (push) Has been cancelled
2025-08-21 22:12:07 +03:00
Bartłomiej Dach
47fecfb669 Merge pull request #34606 from cl8n/sliderpoint
Add skin support for sliderpoint10 and sliderpoint30
2025-08-19 15:06:20 +02:00
Bartłomiej Dach
3b49673e83 Merge pull request #34667 from Hiviexd/verify/exclude-audio-from-hs-check
Exclude all beatmap audios from the hitsounds format check
2025-08-19 14:33:35 +02:00
Bartłomiej Dach
b1296b0c83 Merge pull request #34666 from Hiviexd/verify/check-inconsistent-audio
Add verify check for inconsistent audio usage
2025-08-19 14:33:13 +02:00
Bartłomiej Dach
33df7dc5e5 Add test coverage 2025-08-19 13:53:18 +02:00
Dean Herbert
7c3249c24c Update resources 2025-08-19 20:01:06 +09:00
Dean Herbert
35ab30e83f Merge pull request #34710 from LumpBloom7/SSV2-beatmap-panel-missing-ruleset-icon
Use fallback icon in `PanelBeatmap` if ruleset is not found
2025-08-19 17:48:17 +09:00
Dean Herbert
aba160fb62 Merge pull request #25716 from cdwcgt/hitposition
Add `AimErrorMeter`
2025-08-19 17:30:47 +09:00
Dean Herbert
ceb8a621ff Adjust marker style description to look more correct in dropdown 2025-08-19 16:43:55 +09:00
Dean Herbert
cf38bdfb04 Merge pull request #34721 from bdach/branch-2
Fix even more issues with replay fail indicator
2025-08-18 23:34:11 +09:00
Bartłomiej Dach
a337c8bb99 Adjust weighted average to 90/10 to match bar error meter 2025-08-18 14:43:24 +02:00
Bartłomiej Dach
807ba111fd Remove unnecessary condition 2025-08-18 14:40:50 +02:00
Bartłomiej Dach
fde2887068 Fix average marker not moving to first hit position 2025-08-18 14:40:07 +02:00
Bartłomiej Dach
49bb157fb8 Remove undesirable switch syntax 2025-08-18 14:14:05 +02:00
Bartłomiej Dach
b2dbd4a9dc Make extracted helper more comprehensible 2025-08-18 14:12:56 +02:00
Bartłomiej Dach
8dd349fd17 Rewrite incomprehensible comments 2025-08-18 13:54:18 +02:00
Bartłomiej Dach
df210241fc Fix manual click test being broken 2025-08-18 13:46:06 +02:00
Bartłomiej Dach
d696ac99d4 Improve test coverage somewhat 2025-08-18 13:20:38 +02:00
Bartłomiej Dach
777ab61143 Rename everything to start with 2025-08-18 12:46:53 +02:00
Bartłomiej Dach
fe612d465b Merge branch 'master' into hitposition 2025-08-18 12:20:51 +02:00
Bartłomiej Dach
d26f31b71d Unapply replay playback speed when going to results
closes https://github.com/ppy/osu/issues/34700
2025-08-18 09:09:34 +02:00
Bartłomiej Dach
59ec6ed2eb Stop fail sample when rewinding to before it in replay
closes https://github.com/ppy/osu/issues/34688

I originally wrote it this way semi-intentionally because I thought
cutting out the sample was worse than letting it play out, but I also
forgot that people use like seventy hour long fail samples.
2025-08-18 09:02:49 +02:00
Bartłomiej Dach
5b1b22cb66 Fix replay fail indicator "go to results" button being clickable while invisible
closes https://github.com/ppy/osu/issues/34685
2025-08-18 08:54:45 +02:00
Derrick Timmermans
a1fb7acef3 Use fallback icon if ruleset is not found 2025-08-17 16:11:42 +02:00
clayton
e77fb987a9 Don't switch on array index in slidertick test 2025-08-16 09:32:31 -07:00
clayton
c4163e33e5 Don't use switch expression 2025-08-16 05:08:30 -07:00
clayton
a96d00a55f Fix Slider case of test 2025-08-16 04:57:05 -07:00
clayton
6a16200314 Dedupe switch returns 2025-08-16 04:55:49 -07:00
clayton
4d1ecab4e3 Map sliderpoint textures directly to HitResult types 2025-08-16 04:54:34 -07:00
clayton
f2839c7b65 Rename class to be more consistent with other skins' judgement pieces 2025-08-16 04:09:40 -07:00
Hivie
0bcf29304b more correct hashset usage 2025-08-15 14:41:59 +01:00
Hivie
7b455efe34 exclude all beatmap audios from the check
- prevents false positives on maps with multiple audios
2025-08-15 00:11:16 +01:00
Hivie
14530fe894 add tests 2025-08-15 00:09:03 +01:00
Hivie
148bc4ac34 add check for inconsistent audio usage 2025-08-15 00:08:55 +01:00
clayton
375da52a34 Fix judgement position when not supplied a drawable hit object 2025-08-10 07:16:57 -07:00
clayton
cd7a304640 Add tick judgement gallery test scene 2025-08-10 07:16:18 -07:00
clayton
bcc9bc4498 Remove hit lighting for tick hits 2025-08-10 07:15:37 -07:00
clayton
5fc5d0bd5f Fix tick hits in non-legacy skins 2025-08-10 07:15:20 -07:00
clayton
18803fbec0 Always show slider head judgement
Because it may display sliderpoints in classic behaviour
2025-08-10 07:13:47 -07:00
clayton
9542e77d16 Add sliderpoint10 and sliderpoint30 support 2025-08-10 07:12:59 -07:00
cdwcgt
0fa0568f13 Merge remote-tracking branch 'upstream/master' into hitposition 2025-08-03 00:35:54 +08:00
cdwcgt
771081b9a7 Merge remote-tracking branch 'upstream/master' into hitposition 2024-08-10 18:33:15 +08:00
cdwcgt
20921577d7 handle IApplicableToDifficulty for CS change. 2024-08-10 18:14:45 +08:00
cdwcgt
84c0bd0052 Merge branch 'master' into hitposition 2024-06-07 20:07:20 +09:00
cdwcgt
398ac1b98d improve testing
can change aim meter style in test
2024-06-07 20:05:27 +09:00
Dean Herbert
00fd22841d Merge branch 'master' into hitposition 2024-05-28 10:35:46 +09:00
cdwcgt
e487f20f1b Merge branch 'master' into hitposition 2024-04-06 11:23:28 +09:00
cdwcgt
33ab00ecd8 code format 2024-03-01 13:15:29 +09:00
cdwcgt
098ade2cfa Merge branch 'master' into hitposition 2024-02-20 19:36:32 +09:00
cdwcgt
0b5251fcf4 Remove dependencies on ScoreProcesser 2024-02-20 19:34:45 +09:00
cdwcgt
55bc043719 Merge branch 'master' into hitposition 2024-01-26 15:27:17 +09:00
cdwcgt
bf70552186 add comment, fix some 2024-01-26 14:32:52 +09:00
cdwcgt
9e3c7e2ca9 Rewrite some text and names 2024-01-26 00:43:37 +09:00
cdwcgt
d22435b55f rename param 2024-01-25 22:57:43 +09:00
cdwcgt
4e0dca69ed move AimErrorMeter to HUD 2024-01-25 17:45:56 +09:00
cdwcgt
5ffb92b638 Add new hit position style, change appearance
1. round the hit position that it will not beyond meter range.
2. add relative position style.
- in relative style, rotation can be apply that adjust the relative direction, in Absolute will use `UprightAspectMaintainingContai`ner to prevent rotate because it will cause confusing and meaningless.
3. use the cross-style in https://github.com/ppy/osu/pull/25716#issuecomment-1848974233
2024-01-25 17:45:02 +09:00
cdwcgt
1cd2331d28 expose FindRelativeHitPosition method 2024-01-25 17:27:22 +09:00
cdwcgt
814f39058e fix prefix in AimErrorMeterStrings not corrently 2023-12-13 19:07:39 +08:00
cdwcgt
f61cb3caa7 clear transforms and returned to pool after Clear()
from #25747
2023-12-13 19:06:35 +08:00
cdwcgt
07d81c0824 move AimErrorMeterStrings to HUD 2023-12-13 19:00:24 +08:00
cdwcgt
3195681805 fix getkey isn't match the name 2023-12-13 18:59:03 +08:00
cdwcgt
77a2ac8f42 remove dot at the end of label string 2023-12-11 07:58:41 +09:00
cdwcgt
6eda09aff4 Judgment -> Judgement 2023-12-11 07:57:32 +09:00
cdwcgt
380c3d0444 Zoom animation 2023-12-10 21:18:52 +09:00
cdwcgt
ad1fdc631d use FadeInFromZero to avoid sudden transition 2023-12-10 21:05:05 +09:00
cdwcgt
c16ef5eac3 cleanup 2023-12-10 20:55:05 +09:00
cdwcgt
f9076183d0 rename Hit position to Aim error
used in danser and I think this is better
2023-12-10 20:54:30 +09:00
cdwcgt
6fa6de7c27 remove empty line 2023-12-10 14:29:43 +09:00
cdwcgt
daff00300a add settings for hit position meter 2023-12-10 14:16:22 +09:00
cdwcgt
72d97f4ad6 + to x 2023-12-10 12:56:31 +09:00
cdwcgt
3e56633882 adjust object scale 2023-12-10 12:56:23 +09:00
cdwcgt
18d3e9154f add color for miss
but miss HitEvent have no position?
2023-12-10 12:47:21 +09:00
cdwcgt
eae4227c5a add gameObject to test 2023-12-10 12:47:02 +09:00
cdwcgt
de65e90abf typo, add border, remove usage of scoreprocessor 2023-12-10 10:26:07 +08:00
cdwcgt
01815de675 add HitPositionMeter and basic test 2023-12-10 10:25:37 +09:00
31 changed files with 1323 additions and 73 deletions

View File

@@ -21,9 +21,9 @@ namespace osu.Desktop
public static class Program public static class Program
{ {
#if DEBUG #if DEBUG
private const string base_game_name = @"osu-development"; private const string base_game_name = @"jvnkosu-development";
#else #else
private const string base_game_name = @"osu"; private const string base_game_name = @"jvnkosu";
#endif #endif
private static LegacyTcpIpcProvider? legacyIpc; private static LegacyTcpIpcProvider? legacyIpc;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,162 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Scoring;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Framework.Threading;
using osu.Game.Rulesets.Osu.HUD;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneAimErrorMeter : OsuManualInputManagerTestScene
{
private DependencyProvidingContainer dependencyContainer = null!;
private ScoreProcessor scoreProcessor = null!;
private TestAimErrorMeter aimErrorMeter = null!;
private CircularContainer gameObject = null!;
private ScheduledDelegate? automaticAdditionDelegate;
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("Hit marker size", 0f, 12f, 7f, t =>
{
if (aimErrorMeter.IsNotNull())
aimErrorMeter.HitMarkerSize.Value = t;
});
AddSliderStep("Average position marker size", 1f, 25f, 7f, t =>
{
if (aimErrorMeter.IsNotNull())
aimErrorMeter.AverageMarkerSize.Value = t;
});
}
[SetUpSteps]
public void SetupSteps() => AddStep("Create components", () =>
{
automaticAdditionDelegate?.Cancel();
automaticAdditionDelegate = null;
var ruleset = new OsuRuleset();
scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ScoreProcessor), scoreProcessor)
}
};
dependencyContainer.Children = new Drawable[]
{
aimErrorMeter = new TestAimErrorMeter
{
Margin = new MarginPadding
{
Top = 100
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Scale = new Vector2(2),
},
gameObject = new CircularContainer
{
Size = new Vector2(2 * OsuHitObject.OBJECT_RADIUS),
Position = new Vector2(256, 192),
Colour = Color4.Yellow,
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
}
}
}
};
});
protected override bool OnMouseDown(MouseDownEvent e)
{
// the division by 2 is because CS=5 applies a 0.5x (plus fudge) multiplier to `OBJECT_RADIUS`
aimErrorMeter.AddPoint((gameObject.ToLocalSpace(e.ScreenSpaceMouseDownPosition) - new Vector2(OsuHitObject.OBJECT_RADIUS)) / 2);
return true;
}
[Test]
public void TestManyHitPointsAutomatic()
{
AddStep("add scheduled delegate", () =>
{
automaticAdditionDelegate = Scheduler.AddDelayed(() =>
{
var randomPos = new Vector2(
RNG.NextSingle(0, 2 * OsuHitObject.OBJECT_RADIUS),
RNG.NextSingle(0, 2 * OsuHitObject.OBJECT_RADIUS));
aimErrorMeter.AddPoint(randomPos - new Vector2(OsuHitObject.OBJECT_RADIUS));
InputManager.MoveMouseTo(gameObject.ToScreenSpace(randomPos));
}, 1, true);
});
AddWaitStep("wait for some hit points", 10);
}
[Test]
public void TestDisplayStyles()
{
AddStep("Switch hit position marker style to +", () => aimErrorMeter.HitMarkerStyle.Value = AimErrorMeter.MarkerStyle.Plus);
AddStep("Switch hit position marker style to x", () => aimErrorMeter.HitMarkerStyle.Value = AimErrorMeter.MarkerStyle.X);
AddStep("Switch average position marker style to +", () => aimErrorMeter.AverageMarkerStyle.Value = AimErrorMeter.MarkerStyle.Plus);
AddStep("Switch average position marker style to x", () => aimErrorMeter.AverageMarkerStyle.Value = AimErrorMeter.MarkerStyle.X);
AddStep("Switch position display to absolute", () => aimErrorMeter.PositionDisplayStyle.Value = AimErrorMeter.PositionDisplay.Absolute);
AddStep("Switch position display to relative", () => aimErrorMeter.PositionDisplayStyle.Value = AimErrorMeter.PositionDisplay.Normalised);
}
[Test]
public void TestManualPlacement()
{
AddStep("return user input", () => InputManager.UseParentInput = true);
}
private partial class TestAimErrorMeter : AimErrorMeter
{
public void AddPoint(Vector2 position)
{
OnNewJudgement(new OsuHitCircleJudgementResult(new HitCircle(), new OsuJudgement())
{
CursorPositionAtHit = position
});
}
}
}
}

View File

@@ -0,0 +1,160 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneDrawableJudgementSliderTicks : OsuSkinnableTestScene
{
private bool classic;
private readonly JudgementPooler<DrawableOsuJudgement>[] judgementPools;
public TestSceneDrawableJudgementSliderTicks()
{
judgementPools = new JudgementPooler<DrawableOsuJudgement>[Rows * Cols];
}
protected override void LoadComplete()
{
base.LoadComplete();
int cellIndex = 0;
SetContents(_ =>
{
var container = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
judgementPools[cellIndex] = new JudgementPooler<DrawableOsuJudgement>(new[]
{
HitResult.Great,
HitResult.Miss,
HitResult.LargeTickHit,
HitResult.SliderTailHit,
HitResult.LargeTickMiss,
HitResult.IgnoreMiss,
}),
new GridContainer
{
Padding = new MarginPadding { Top = 26f },
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content =
new[]
{
new[]
{
Empty(),
new OsuSpriteText
{
Text = "hit",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
new OsuSpriteText
{
Text = "miss",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
},
}.Concat(new[]
{
"head",
"tick",
"repeat",
"tail",
"slider",
}.Select(label => new Drawable[]
{
new OsuSpriteText
{
Text = label,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
new Container<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
new Container<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
})).ToArray(),
},
},
};
cellIndex++;
return container;
});
AddToggleStep("Toggle classic behaviour", c => classic = c);
AddStep("Show judgements", createAllJudgements);
}
private void createAllJudgements()
{
for (int cellIndex = 0; cellIndex < Rows * Cols; cellIndex++)
{
var slider = new Slider { StartTime = Time.Current, ClassicSliderBehaviour = classic };
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableHitObjects = new DrawableOsuHitObject[]
{
new DrawableSliderHead(new SliderHeadCircle { StartTime = Time.Current, ClassicSliderBehaviour = classic }),
new DrawableSliderTick(new SliderTick { StartTime = Time.Current }),
new DrawableSliderRepeat(new SliderRepeat(slider) { StartTime = Time.Current }),
new DrawableSliderTail(new SliderTailCircle(slider) { StartTime = Time.Current, ClassicSliderBehaviour = classic }),
new DrawableSlider(slider),
};
var containers = Cell(cellIndex).ChildrenOfType<Container<DrawableOsuJudgement>>().ToArray();
for (int i = 0; i < drawableHitObjects.Length; i++)
{
createJudgement(judgementPools[cellIndex], containers[i * 2], drawableHitObjects[i], true);
createJudgement(judgementPools[cellIndex], containers[i * 2 + 1], drawableHitObjects[i], false);
}
}
}
private void createJudgement(JudgementPooler<DrawableOsuJudgement> pool, Container<DrawableOsuJudgement> container, DrawableOsuHitObject drawableHitObject, bool hit)
{
container.Clear(false);
if (!drawableHitObject.DisplayResult)
return;
var hitObject = drawableHitObject.HitObject;
var result = new OsuJudgementResult(hitObject, hitObject.Judgement)
{
Type = hit ? hitObject.Judgement.MaxResult : hitObject.Judgement.MinResult,
};
var judgement = pool.Get(result.Type, d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
d.Scale = new Vector2(0.7f);
d.Apply(result, null);
});
if (judgement != null)
container.Add(judgement);
}
}
}

View File

@@ -0,0 +1,475 @@
// 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.ComponentModel;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Localisation.HUD;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
using osuTK.Graphics;
using Container = osu.Framework.Graphics.Containers.Container;
namespace osu.Game.Rulesets.Osu.HUD
{
[Cached]
public partial class AimErrorMeter : HitErrorMeter
{
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.HitMarkerSize), nameof(AimErrorMeterStrings.HitMarkerSizeDescription))]
public BindableNumber<float> HitMarkerSize { get; } = new BindableNumber<float>(7f)
{
MinValue = 0f,
MaxValue = 12f,
Precision = 1f
};
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.HitMarkerStyle), nameof(AimErrorMeterStrings.HitMarkerStyleDescription))]
public Bindable<MarkerStyle> HitMarkerStyle { get; } = new Bindable<MarkerStyle>();
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageMarkerSize), nameof(AimErrorMeterStrings.AverageMarkerSizeDescription))]
public BindableNumber<float> AverageMarkerSize { get; } = new BindableNumber<float>(12f)
{
MinValue = 7f,
MaxValue = 25f,
Precision = 1f
};
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.AverageMarkerStyle), nameof(AimErrorMeterStrings.AverageMarkerStyleDescription))]
public Bindable<MarkerStyle> AverageMarkerStyle { get; } = new Bindable<MarkerStyle>(MarkerStyle.Plus);
[SettingSource(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.PositionDisplayStyle), nameof(AimErrorMeterStrings.PositionDisplayStyleDescription))]
public Bindable<PositionDisplay> PositionDisplayStyle { get; } = new Bindable<PositionDisplay>();
// used for calculate relative position.
private Vector2? lastObjectPosition;
private Container averagePositionMarker = null!;
private Container averagePositionMarkerRotationContainer = null!;
private Vector2? averagePosition;
private readonly DrawablePool<HitPositionMarker> hitPositionPool = new DrawablePool<HitPositionMarker>(30);
private Container hitPositionMarkerContainer = null!;
private Container arrowBackgroundContainer = null!;
private UprightAspectMaintainingContainer rotateFixedContainer = null!;
private Container mainContainer = null!;
private float objectRadius;
private const int max_concurrent_judgements = 30;
private const float line_thickness = 2;
private const float inner_portion = 0.85f;
[Resolved]
private OsuColour colours { get; set; } = null!;
public AimErrorMeter()
{
AutoSizeAxes = Axes.Both;
AlwaysPresent = true;
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, ScoreProcessor processor)
{
InternalChild = new Container
{
Height = 100,
Width = 100,
Children = new Drawable[]
{
hitPositionPool,
rotateFixedContainer = new UprightAspectMaintainingContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
}
};
mainContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Colour4.White,
Masking = true,
BorderThickness = 2,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Child = new Box
{
Colour = Colour4.Gray,
Alpha = 0.3f,
RelativeSizeAxes = Axes.Both
},
},
arrowBackgroundContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = "Arrow Background",
RelativeSizeAxes = Axes.Both,
Rotation = 45,
Alpha = 0f,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = inner_portion + 0.2f,
Width = line_thickness / 2,
},
new Circle
{
Height = 5f,
Width = line_thickness / 2,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(-line_thickness / 4),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + 0.2f) / 2,
Rotation = -45
},
new Circle
{
Height = 5f,
Width = line_thickness / 2,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding(-line_thickness / 4),
RelativePositionAxes = Axes.Both,
Y = -(inner_portion + 0.2f) / 2,
Rotation = 45
}
}
},
new Container
{
Name = "Cross Background",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.5f,
Width = line_thickness,
Height = inner_portion * 0.9f
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.5f,
Width = line_thickness,
Height = inner_portion * 0.9f,
Rotation = 90
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Width = line_thickness / 2,
Height = inner_portion * 0.9f,
Rotation = 45
},
new Circle
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0.2f,
Width = line_thickness / 2,
Height = inner_portion * 0.9f,
Rotation = 135
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
hitPositionMarkerContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
averagePositionMarker = new UprightAspectMaintainingContainer
{
RelativePositionAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = averagePositionMarkerRotationContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
},
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = 90
}
}
}
}
}
}
}
};
// handle IApplicableToDifficulty for CS change.
BeatmapDifficulty newDifficulty = new BeatmapDifficulty();
beatmap.Value.Beatmap.Difficulty.CopyTo(newDifficulty);
var mods = processor.Mods.Value;
foreach (var mod in mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(newDifficulty);
objectRadius = OsuHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(newDifficulty.CircleSize, true);
AverageMarkerSize.BindValueChanged(size => averagePositionMarker.Size = new Vector2(size.NewValue), true);
AverageMarkerStyle.BindValueChanged(style => averagePositionMarkerRotationContainer.Rotation = style.NewValue == MarkerStyle.Plus ? 0 : 45, true);
PositionDisplayStyle.BindValueChanged(s =>
{
Clear();
if (s.NewValue == PositionDisplay.Normalised)
{
arrowBackgroundContainer.FadeIn(100);
rotateFixedContainer.Remove(mainContainer, false);
AddInternal(mainContainer);
}
else
{
arrowBackgroundContainer.FadeOut(100);
// when in absolute mode, rotation of the aim error meter as a whole should not affect how the component is displayed
RemoveInternal(mainContainer, false);
rotateFixedContainer.Add(mainContainer);
}
}, true);
}
protected override void OnNewJudgement(JudgementResult judgement)
{
if (judgement is not OsuHitCircleJudgementResult circleJudgement) return;
if (circleJudgement.CursorPositionAtHit == null) return;
if (hitPositionMarkerContainer.Count > max_concurrent_judgements)
{
const double quick_fade_time = 300;
// check with a bit of lenience to avoid precision error in comparison.
var old = hitPositionMarkerContainer.FirstOrDefault(j => j.LifetimeEnd > Clock.CurrentTime + quick_fade_time * 1.1);
if (old != null)
{
old.ClearTransforms();
old.FadeOut(quick_fade_time).Expire();
}
}
Vector2 hitPosition;
if (PositionDisplayStyle.Value == PositionDisplay.Normalised && lastObjectPosition != null)
{
hitPosition = AccuracyHeatmap.FindRelativeHitPosition(lastObjectPosition.Value, ((OsuHitObject)circleJudgement.HitObject).StackedEndPosition,
circleJudgement.CursorPositionAtHit.Value, objectRadius, 45) * 0.5f;
}
else
{
// get relative position between mouse position and current object.
hitPosition = (circleJudgement.CursorPositionAtHit.Value - ((OsuHitObject)circleJudgement.HitObject).StackedPosition) / objectRadius / 2 * inner_portion;
}
hitPosition = Vector2.Clamp(hitPosition, new Vector2(-0.5f), new Vector2(0.5f));
hitPositionPool.Get(drawableHit =>
{
drawableHit.X = hitPosition.X;
drawableHit.Y = hitPosition.Y;
drawableHit.Colour = getColourForPosition(hitPosition);
hitPositionMarkerContainer.Add(drawableHit);
});
var newAveragePosition = 0.1f * hitPosition + 0.9f * (averagePosition ?? hitPosition);
averagePositionMarker.MoveTo(newAveragePosition, 800, Easing.OutQuint);
averagePosition = newAveragePosition;
lastObjectPosition = ((OsuHitObject)circleJudgement.HitObject).StackedPosition;
}
private Color4 getColourForPosition(Vector2 position)
{
float distance = Vector2.Distance(position, Vector2.Zero);
if (distance >= 0.5f * inner_portion)
return colours.Red;
if (distance >= 0.35f * inner_portion)
return colours.Yellow;
if (distance >= 0.2f * inner_portion)
return colours.Green;
return colours.Blue;
}
public override void Clear()
{
averagePosition = null;
averagePositionMarker.MoveTo(Vector2.Zero, 800, Easing.OutQuint);
lastObjectPosition = null;
foreach (var h in hitPositionMarkerContainer)
{
h.ClearTransforms();
h.Expire();
}
}
private partial class HitPositionMarker : PoolableDrawable
{
[Resolved]
private AimErrorMeter aimErrorMeter { get; set; } = null!;
public readonly BindableNumber<float> MarkerSize = new BindableFloat();
public readonly Bindable<MarkerStyle> Style = new Bindable<MarkerStyle>();
private readonly Container content;
public HitPositionMarker()
{
RelativePositionAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = new UprightAspectMaintainingContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = content = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = -45
},
new Circle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.25f,
Rotation = 45
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
MarkerSize.BindTo(aimErrorMeter.HitMarkerSize);
MarkerSize.BindValueChanged(size => Size = new Vector2(size.NewValue), true);
Style.BindTo(aimErrorMeter.HitMarkerStyle);
Style.BindValueChanged(style => content.Rotation = style.NewValue == MarkerStyle.X ? 0 : 45, true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
const int judgement_fade_in_duration = 100;
const int judgement_fade_out_duration = 5000;
this
.ResizeTo(new Vector2(0))
.FadeInFromZero(judgement_fade_in_duration, Easing.OutQuint)
.ResizeTo(new Vector2(MarkerSize.Value), judgement_fade_in_duration, Easing.OutQuint)
.Then()
.FadeOut(judgement_fade_out_duration)
.Expire();
}
}
public enum MarkerStyle
{
[Description("x")]
X,
[Description("+")]
Plus,
}
public enum PositionDisplay
{
[LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.Absolute))]
Absolute,
[LocalisableDescription(typeof(AimErrorMeterStrings), nameof(AimErrorMeterStrings.Normalised))]
Normalised,
}
}
}

View File

@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
private Vector2 screenSpacePosition; private Vector2? screenSpacePosition;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@@ -65,7 +65,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Lighting.ResetAnimation(); Lighting.ResetAnimation();
Lighting.SetColourFrom(this, Result); Lighting.SetColourFrom(this, Result);
Position = Parent!.ToLocalSpace(screenSpacePosition);
if (screenSpacePosition != null)
Position = Parent!.ToLocalSpace(screenSpacePosition.Value);
} }
protected override void ApplyHitAnimations() protected override void ApplyHitAnimations()
@@ -87,7 +89,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplyHitAnimations(); base.ApplyHitAnimations();
} }
protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result); protected override Drawable CreateDefaultJudgement(HitResult result) =>
// Tick hits don't show a judgement by default
result.IsHit() && result.IsTick() ? Empty() : new OsuJudgementPiece(result);
private partial class OsuJudgementPiece : DefaultJudgementPiece private partial class OsuJudgementPiece : DefaultJudgementPiece
{ {

View File

@@ -16,17 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult
{
get
{
if (HitObject?.ClassicSliderBehaviour == true)
return false;
return base.DisplayResult;
}
}
private readonly IBindable<int> pathVersion = new Bindable<int>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;

View File

@@ -3,6 +3,7 @@
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (targetJudgement == null || targetResult == null) if (targetJudgement == null || targetResult == null)
Colour = Color4.White; Colour = Color4.White;
else else
Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent; Colour = targetResult.IsHit && !targetResult.Type.IsTick() ? targetJudgement.AccentColour : Color4.Transparent;
} }
} }
} }

View File

@@ -29,6 +29,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
switch (result) switch (result)
{ {
case HitResult.LargeTickHit:
case HitResult.SliderTailHit:
return null;
case HitResult.IgnoreMiss: case HitResult.IgnoreMiss:
case HitResult.LargeTickMiss: case HitResult.LargeTickMiss:
return new ArgonJudgementPieceSliderTickMiss(result); return new ArgonJudgementPieceSliderTickMiss(result);

View File

@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public partial class LegacyJudgementPieceSliderTickHit : Sprite, IAnimatableJudgement
{
public void PlayAnimation()
{
// https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L804-L806
this.MoveToOffset(new Vector2(0, -10), 300, Easing.Out)
.Then()
.FadeOut(60);
}
public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy();
}
}

View File

@@ -5,7 +5,9 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@@ -115,6 +117,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null; return null;
case SkinComponentLookup<HitResult> resultComponent:
switch (resultComponent.Component)
{
case HitResult.LargeTickHit:
case HitResult.SliderTailHit:
if (getSliderPointTexture(resultComponent.Component) is Texture texture)
return new LegacyJudgementPieceSliderTickHit { Texture = texture };
break;
// If the corresponding hit result displays a judgement and the miss texture isn't provided by this skin, don't look up the miss texture from any further skins.
case HitResult.LargeTickMiss:
case HitResult.IgnoreMiss:
if (getSliderPointTexture(resultComponent.Component == HitResult.LargeTickMiss
? HitResult.LargeTickHit
: HitResult.SliderTailHit) != null)
return base.GetDrawableComponent(lookup) ?? Drawable.Empty();
break;
}
return base.GetDrawableComponent(lookup);
Texture? getSliderPointTexture(HitResult result)
{
// https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L799
if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2m)
// Note that osu!stable used sliderpoint30 for heads and repeats, and sliderpoint10 for ticks, but the mapping is intentionally changed here so that each texture represents one type of HitResult.
return GetTexture(result == HitResult.LargeTickHit ? "sliderpoint30" : "sliderpoint10");
return null;
}
case OsuSkinComponentLookup osuComponent: case OsuSkinComponentLookup osuComponent:
switch (osuComponent.Component) switch (osuComponent.Component)
{ {

View File

@@ -232,10 +232,47 @@ namespace osu.Game.Rulesets.Osu.Statistics
if (pointGrid.Content.Count == 0) if (pointGrid.Content.Count == 0)
return; return;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. Vector2 relativePosition = FindRelativeHitPosition(start, end, hitPoint, radius, rotation);
double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
var localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion;
var localPoint = localCentre + localRadius * relativePosition;
// Find the most relevant hit point.
int r = (int)Math.Round(localPoint.Y);
int c = (int)Math.Round(localPoint.X);
if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
}
/// <summary>
/// Normalises the position of a hit on a circle such that it is relative to the movement that was performed to arrive at said circle.
/// </summary>
/// <param name="previousObjectPosition">The position of the object prior to the one getting hit.</param>
/// <param name="nextObjectPosition">The position of the object which is getting hit.</param>
/// <param name="hitPoint">The point at which the user hit.</param>
/// <param name="objectRadius">The radius of <paramref name="previousObjectPosition"/> and <paramref name="nextObjectPosition"/>.</param>
/// <param name="rotation">
/// The rotation of the axis which is to be considered in the same direction as the vector
/// leading from <paramref name="previousObjectPosition"/> to <paramref name="nextObjectPosition"/>.
/// </param>
/// <returns>
/// A 2D vector representing the <paramref name="hitPoint"/> as relative to the movement between <paramref name="previousObjectPosition"/> and <paramref name="nextObjectPosition"/>
/// and relative to the <paramref name="objectRadius"/>.
/// If the object was hit perfectly in the middle, the return value will be <see cref="Vector2.Zero"/>.
/// If the object was hit perfectly at its edge, the returned vector will have a magnitude of 1.
/// </returns>
public static Vector2 FindRelativeHitPosition(Vector2 previousObjectPosition, Vector2 nextObjectPosition, Vector2 hitPoint, float objectRadius, float rotation)
{
double angle1 = Math.Atan2(nextObjectPosition.Y - hitPoint.Y, hitPoint.X - nextObjectPosition.X); // Angle between the end point and the hit point.
double angle2 = Math.Atan2(nextObjectPosition.Y - previousObjectPosition.Y, previousObjectPosition.X - nextObjectPosition.X); // Angle between the end point and the start point.
double finalAngle = angle2 - angle1; // Angle between start, end, and hit points. double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
float normalisedDistance = Vector2.Distance(hitPoint, end) / radius; float normalisedDistance = Vector2.Distance(hitPoint, nextObjectPosition) / objectRadius; // Distance between the hit point and the end point.
// Consider two objects placed horizontally, with the start on the left and the end on the right. // Consider two objects placed horizontally, with the start on the left and the end on the right.
// The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form: // The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
@@ -254,22 +291,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
// //
// We also need to apply the anti-clockwise rotation. // We also need to apply the anti-clockwise rotation.
double rotatedAngle = finalAngle - float.DegreesToRadians(rotation); double rotatedAngle = finalAngle - float.DegreesToRadians(rotation);
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle)); return -normalisedDistance * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance;
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
// Find the most relevant hit point.
int r = (int)Math.Round(localPoint.Y);
int c = (int)Math.Round(localPoint.X);
if (r < 0 || r >= points_per_dimension || c < 0 || c >= points_per_dimension)
return;
PeakValue = Math.Max(PeakValue, ((GridPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
} }
private abstract partial class GridPoint : CompositeDrawable private abstract partial class GridPoint : CompositeDrawable

View File

@@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Osu.UI
HitResult.Ok, HitResult.Ok,
HitResult.Meh, HitResult.Meh,
HitResult.Miss, HitResult.Miss,
HitResult.LargeTickHit,
HitResult.SliderTailHit,
HitResult.LargeTickMiss, HitResult.LargeTickMiss,
HitResult.IgnoreMiss, HitResult.IgnoreMiss,
}, onJudgementLoaded)); }, onJudgementLoaded));

View File

@@ -117,6 +117,52 @@ namespace osu.Game.Tests.Editing.Checks
} }
} }
[Test]
public void TestBeatmapAudioTracksExemptedFromCheck()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var beatmapSet = new BeatmapSetInfo
{
Files =
{
CheckTestHelpers.CreateMockFile("wav"),
CheckTestHelpers.CreateMockFile("mp3")
}
};
var firstPlayable = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = beatmapSet,
Metadata = new BeatmapMetadata { AudioFile = beatmapSet.Files[0].Filename }
}
};
var firstWorking = new Mock<TestWorkingBeatmap>(firstPlayable, null, null);
firstWorking.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
var secondPlayable = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = beatmapSet,
Metadata = new BeatmapMetadata { AudioFile = beatmapSet.Files[1].Filename }
}
};
var secondWorking = new Mock<TestWorkingBeatmap>(secondPlayable, null, null);
secondWorking.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
var context = new BeatmapVerifierContext(
new BeatmapVerifierContext.VerifiedBeatmap(firstWorking.Object, firstPlayable),
[new BeatmapVerifierContext.VerifiedBeatmap(secondWorking.Object, secondPlayable)],
DifficultyRating.ExpertPlus);
var issues = check.Run(context).ToList();
Assert.That(issues, Is.Empty);
}
}
private BeatmapVerifierContext getContext(Stream? resourceStream) private BeatmapVerifierContext getContext(Stream? resourceStream)
{ {
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null); var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);

View File

@@ -0,0 +1,151 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Models;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckInconsistentAudioTest
{
private CheckInconsistentAudio check = null!;
[SetUp]
public void Setup()
{
check = new CheckInconsistentAudio();
}
[Test]
public void TestConsistentAudio()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3", "audio.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestInconsistentAudio()
{
var beatmaps = createBeatmapSetWithAudio("audio1.mp3", "audio2.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio);
Assert.That(issues.Single().ToString(), Contains.Substring("audio1.mp3"));
Assert.That(issues.Single().ToString(), Contains.Substring("audio2.mp3"));
}
[Test]
public void TestInconsistentAudioWithNull()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3", null);
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio);
Assert.That(issues.Single().ToString(), Contains.Substring("audio.mp3"));
Assert.That(issues.Single().ToString(), Contains.Substring("not set"));
}
[Test]
public void TestInconsistentAudioWithEmptyString()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3", "");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio);
Assert.That(issues.Single().ToString(), Contains.Substring("audio.mp3"));
Assert.That(issues.Single().ToString(), Contains.Substring("not set"));
}
[Test]
public void TestBothAudioNotSet()
{
var beatmaps = createBeatmapSetWithAudio("", "");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestMultipleInconsistencies()
{
var beatmaps = createBeatmapSetWithAudio("audio1.mp3", "audio2.mp3", "audio3.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.All(issue => issue.Template is CheckInconsistentAudio.IssueTemplateInconsistentAudio));
}
[Test]
public void TestSingleDifficulty()
{
var beatmaps = createBeatmapSetWithAudio("audio.mp3");
var context = createContextWithMultipleDifficulties(beatmaps.First(), beatmaps);
Assert.That(check.Run(context), Is.Empty);
}
private IBeatmap createBeatmapWithAudio(string audioFile, RealmNamedFileUsage? file)
{
var beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { AudioFile = audioFile },
BeatmapSet = new BeatmapSetInfo()
}
};
if (file != null)
beatmap.BeatmapInfo.BeatmapSet!.Files.Add(file);
return beatmap;
}
private IBeatmap[] createBeatmapSetWithAudio(params string?[] audioFiles)
{
var beatmapSet = new BeatmapSetInfo();
var beatmaps = new IBeatmap[audioFiles.Length];
for (int i = 0; i < audioFiles.Length; i++)
{
string? audioFile = audioFiles[i];
var file = !string.IsNullOrEmpty(audioFile) ? CheckTestHelpers.CreateMockFile("mp3") : null;
beatmaps[i] = createBeatmapWithAudio(audioFile ?? "", file);
beatmaps[i].BeatmapInfo.BeatmapSet = beatmapSet;
beatmaps[i].BeatmapInfo.DifficultyName = $"Difficulty {i + 1}";
beatmapSet.Beatmaps.Add(beatmaps[i].BeatmapInfo);
}
return beatmaps;
}
private BeatmapVerifierContext createContextWithMultipleDifficulties(IBeatmap currentBeatmap, IBeatmap[] allDifficulties)
{
var verifiedCurrentBeatmap = new BeatmapVerifierContext.VerifiedBeatmap(new TestWorkingBeatmap(currentBeatmap), currentBeatmap);
var verifiedOtherBeatmaps = allDifficulties.Select(b => new BeatmapVerifierContext.VerifiedBeatmap(new TestWorkingBeatmap(b), b)).ToList();
return new BeatmapVerifierContext(verifiedCurrentBeatmap, verifiedOtherBeatmaps, DifficultyRating.ExpertPlus);
}
}
}

View File

@@ -204,10 +204,11 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension; Filename += realm_extension;
#if DEBUG // TODO: fix
// #if DEBUG
if (!DebugUtils.IsNUnitRunning) if (!DebugUtils.IsNUnitRunning)
applyFilenameSchemaSuffix(ref Filename); applyFilenameSchemaSuffix(ref Filename);
#endif // #endif
// `prepareFirstRealmAccess()` triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date. // `prepareFirstRealmAccess()` triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
using (var realm = prepareFirstRealmAccess()) using (var realm = prepareFirstRealmAccess())

View File

@@ -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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class AimErrorMeterStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.AimErrorMeterStrings";
/// <summary>
/// "Hit marker size"
/// </summary>
public static LocalisableString HitMarkerSize => new TranslatableString(getKey(@"hit_marker_size"), @"Hit marker size");
/// <summary>
/// "Controls the size of the markers displayed after every hit."
/// </summary>
public static LocalisableString HitMarkerSizeDescription => new TranslatableString(getKey(@"hit_marker_size_description"), @"Controls the size of the markers displayed after every hit.");
/// <summary>
/// "Hit marker style"
/// </summary>
public static LocalisableString HitMarkerStyle => new TranslatableString(getKey(@"hit_marker_style"), @"Hit marker style");
/// <summary>
/// "The visual style of the hit markers."
/// </summary>
public static LocalisableString HitMarkerStyleDescription => new TranslatableString(getKey(@"hit_marker_style_description"), @"The visual style of the hit markers.");
/// <summary>
/// "Average position marker size"
/// </summary>
public static LocalisableString AverageMarkerSize => new TranslatableString(getKey(@"average_marker_size"), @"Average position marker size");
/// <summary>
/// "Controls the size of the marker showing average hit position."
/// </summary>
public static LocalisableString AverageMarkerSizeDescription => new TranslatableString(getKey(@"average_marker_size_description"), @"Controls the size of the marker showing average hit position.");
/// <summary>
/// "Average position marker style"
/// </summary>
public static LocalisableString AverageMarkerStyle => new TranslatableString(getKey(@"average_marker_style"), @"Average position marker style");
/// <summary>
/// "The visual style of the average position marker."
/// </summary>
public static LocalisableString AverageMarkerStyleDescription => new TranslatableString(getKey(@"average_marker_style_description"), @"The visual style of the average position marker.");
/// <summary>
/// "Position display style"
/// </summary>
public static LocalisableString PositionDisplayStyle => new TranslatableString(getKey(@"position_style"), @"Position display style");
/// <summary>
/// "Controls whether positions displayed on the meter are absolute (as seen on screen) or normalised (relative to the direction of movement from previous object)."
/// </summary>
public static LocalisableString PositionDisplayStyleDescription => new TranslatableString(getKey(@"position_style_description"), @"Controls whether positions displayed on the meter are absolute (as seen on screen) or normalised (relative to the direction of movement from previous object).");
/// <summary>
/// "Absolute"
/// </summary>
public static LocalisableString Absolute => new TranslatableString(getKey(@"absolute"), @"Absolute");
/// <summary>
/// "Normalised"
/// </summary>
public static LocalisableString Normalised => new TranslatableString(getKey(@"normalised"), @"Normalised");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -7,13 +7,13 @@ namespace osu.Game.Online
{ {
public DevelopmentEndpointConfiguration() public DevelopmentEndpointConfiguration()
{ {
WebsiteUrl = APIUrl = @"https://dev.ppy.sh"; WebsiteUrl = APIUrl = @"https://osu.jvnko.boats";
APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; APIClientSecret = @"ijBg9O6aULCYGnvEELYD3IdW7fqrYiFaoMdkzQNA";
APIClientID = "5"; APIClientID = "1";
SpectatorUrl = $@"{APIUrl}/signalr/spectator"; SpectatorUrl = $@"https://osu-spec.jvnko.boats/spectator";
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; MultiplayerUrl = $@"https://osu-spec.jvnko.boats/multiplayer";
MetadataUrl = $@"{APIUrl}/signalr/metadata"; MetadataUrl = $@"https://osu-spec.jvnko.boats/metadata";
BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission"; BeatmapSubmissionServiceUrl = $@"https://osu-bss.jvnko.boats";
} }
} }
} }

View File

@@ -7,13 +7,13 @@ namespace osu.Game.Online
{ {
public ProductionEndpointConfiguration() public ProductionEndpointConfiguration()
{ {
WebsiteUrl = APIUrl = @"https://osu.ppy.sh"; WebsiteUrl = APIUrl = @"https://osu.jvnko.boats";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientSecret = @"ijBg9O6aULCYGnvEELYD3IdW7fqrYiFaoMdkzQNA";
APIClientID = "5"; APIClientID = "1";
SpectatorUrl = "https://spectator.ppy.sh/spectator"; SpectatorUrl = $@"https://osu-spec.jvnko.boats/spectator";
MultiplayerUrl = "https://spectator.ppy.sh/multiplayer"; MultiplayerUrl = $@"https://osu-spec.jvnko.boats/multiplayer";
MetadataUrl = "https://spectator.ppy.sh/metadata"; MetadataUrl = $@"https://osu-spec.jvnko.boats/metadata";
BeatmapSubmissionServiceUrl = "https://bss.ppy.sh"; BeatmapSubmissionServiceUrl = $@"https://osu-bss.jvnko.boats";
} }
} }
} }

View File

@@ -11,11 +11,11 @@ namespace osu.Game.Online
{ {
protected override string GetLookupUrl(string url) protected override string GetLookupUrl(string url)
{ {
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase)) /* if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase))
{ {
Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important); Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important);
return string.Empty; return string.Empty;
} }*/
return url; return url;
} }

View File

@@ -92,9 +92,9 @@ namespace osu.Game
{ {
#if DEBUG #if DEBUG
// Different port allows running release and debug builds alongside each other. // Different port allows running release and debug builds alongside each other.
public const string IPC_PIPE_NAME = "osu-lazer-debug"; public const string IPC_PIPE_NAME = "jvnkosu-debug";
#else #else
public const string IPC_PIPE_NAME = "osu-lazer"; public const string IPC_PIPE_NAME = "jvnkosu";
#endif #endif
/// <summary> /// <summary>

View File

@@ -76,12 +76,12 @@ namespace osu.Game
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{ {
#if DEBUG #if DEBUG
public const string GAME_NAME = "osu! (development)"; public const string GAME_NAME = "jvnkosu! (development)";
#else #else
public const string GAME_NAME = "osu!"; public const string GAME_NAME = "jvnkosu!";
#endif #endif
public const string OSU_PROTOCOL = "osu://"; public const string OSU_PROTOCOL = "jnvkosu://";
/// <summary> /// <summary>
/// The filename of the main client database. /// The filename of the main client database.

View File

@@ -30,9 +30,18 @@ namespace osu.Game.Overlays
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Font = OsuFont.Numeric.With(weight: FontWeight.Bold, size: 12), Font = OsuFont.Torus.With(size: 12),
Colour = colours.YellowDark, Colour = colours.GrayF,
Text = @"DEVELOPER BUILD", Text = "jvnkosu! development build",
Y = -12,
},
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 15),
Colour = colours.Yellow,
Text = "Experimental version",
}, },
new Sprite new Sprite
{ {

View File

@@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Edit
new CheckDelayedHitsounds(), new CheckDelayedHitsounds(),
new CheckSongFormat(), new CheckSongFormat(),
new CheckHitsoundsFormat(), new CheckHitsoundsFormat(),
new CheckInconsistentAudio(),
// Files // Files
new CheckZeroByteFiles(), new CheckZeroByteFiles(),

View File

@@ -7,6 +7,7 @@ using ManagedBass;
using osu.Framework.Audio.Callbacks; using osu.Framework.Audio.Callbacks;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Rulesets.Edit.Checks.Components; using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks namespace osu.Game.Rulesets.Edit.Checks
@@ -24,13 +25,22 @@ namespace osu.Game.Rulesets.Edit.Checks
public IEnumerable<Issue> Run(BeatmapVerifierContext context) public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{ {
var beatmapSet = context.CurrentDifficulty.Playable.BeatmapInfo.BeatmapSet; var beatmapSet = context.CurrentDifficulty.Playable.BeatmapInfo.BeatmapSet;
var audioFile = beatmapSet?.GetFile(context.CurrentDifficulty.Playable.Metadata.AudioFile);
if (beatmapSet == null) yield break; if (beatmapSet == null) yield break;
// Collect all audio files from all difficulties to exclude them from the check, as they aren't hitsounds.
var audioFiles = new HashSet<RealmNamedFileUsage>(ReferenceEqualityComparer.Instance);
foreach (var difficulty in context.AllDifficulties)
{
var audioFile = beatmapSet.GetFile(difficulty.Playable.Metadata.AudioFile);
if (audioFile != null)
audioFiles.Add(audioFile);
}
foreach (var file in beatmapSet.Files) foreach (var file in beatmapSet.Files)
{ {
if (audioFile != null && ReferenceEquals(file.File, audioFile.File)) continue; if (audioFiles.Contains(file)) continue;
using (Stream data = context.CurrentDifficulty.Working.GetStream(file.File.GetStoragePath())) using (Stream data = context.CurrentDifficulty.Working.GetStream(file.File.GetStoragePath()))
{ {

View File

@@ -0,0 +1,53 @@
// 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.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckInconsistentAudio : ICheck
{
public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Inconsistent audio files", CheckScope.BeatmapSet);
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateInconsistentAudio(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (context.AllDifficulties.Count() <= 1)
yield break;
var referenceBeatmap = context.CurrentDifficulty.Playable;
string referenceAudioFile = referenceBeatmap.Metadata.AudioFile;
foreach (var beatmap in context.OtherDifficulties)
{
string currentAudioFile = beatmap.Playable.Metadata.AudioFile;
if (referenceAudioFile != currentAudioFile)
{
yield return new IssueTemplateInconsistentAudio(this).Create(
string.IsNullOrEmpty(referenceAudioFile) ? "not set" : referenceAudioFile,
beatmap.Playable.BeatmapInfo.DifficultyName,
string.IsNullOrEmpty(currentAudioFile) ? "not set" : currentAudioFile
);
}
}
}
public class IssueTemplateInconsistentAudio : IssueTemplate
{
public IssueTemplateInconsistentAudio(ICheck check)
: base(check, IssueType.Problem, "Inconsistent audio file between this difficulty ({0}) and \"{1}\" ({2}).")
{
}
public Issue Create(string referenceAudio, string otherDifficulty, string otherAudio)
=> new Issue(this, referenceAudio, otherDifficulty, otherAudio);
}
}
}

View File

@@ -36,6 +36,7 @@ namespace osu.Game.Screens.Play
private SkinnableSound failSample = null!; private SkinnableSound failSample = null!;
private AudioFilter failLowPassFilter = null!; private AudioFilter failLowPassFilter = null!;
private AudioFilter failHighPassFilter = null!; private AudioFilter failHighPassFilter = null!;
private Container content = null!;
private double? failTime; private double? failTime;
@@ -44,7 +45,6 @@ namespace osu.Game.Screens.Play
public ReplayFailIndicator(GameplayClockContainer gameplayClockContainer) public ReplayFailIndicator(GameplayClockContainer gameplayClockContainer)
{ {
AlwaysPresent = true;
Clock = this.gameplayClockContainer = gameplayClockContainer; Clock = this.gameplayClockContainer = gameplayClockContainer;
} }
@@ -54,7 +54,6 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Alpha = 0;
track = beatmap.Value.Track; track = beatmap.Value.Track;
@@ -65,13 +64,14 @@ namespace osu.Game.Screens.Play
failSample = new SkinnableSound(new SampleInfo(@"Gameplay/failsound")), failSample = new SkinnableSound(new SampleInfo(@"Gameplay/failsound")),
failLowPassFilter = new AudioFilter(audio.TrackMixer), failLowPassFilter = new AudioFilter(audio.TrackMixer),
failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
new Container content = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Masking = true, Masking = true,
CornerRadius = 20, CornerRadius = 20,
Alpha = 0,
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@@ -132,7 +132,7 @@ namespace osu.Game.Screens.Play
// intentionally shorter than the actual fail animation // intentionally shorter than the actual fail animation
const double audio_sweep_duration = 1000; const double audio_sweep_duration = 1000;
this.FadeInFromZero(200, Easing.OutQuint); content.FadeInFromZero(200, Easing.OutQuint);
this.ScaleTo(1.1f, audio_sweep_duration, Easing.OutElasticHalf); this.ScaleTo(1.1f, audio_sweep_duration, Easing.OutElasticHalf);
this.TransformBindableTo(trackFreq, 0, audio_sweep_duration); this.TransformBindableTo(trackFreq, 0, audio_sweep_duration);
this.TransformBindableTo(volumeAdjustment, 0.5); this.TransformBindableTo(volumeAdjustment, 0.5);
@@ -155,8 +155,11 @@ namespace osu.Game.Screens.Play
failSample.Play(); failSample.Play();
} }
if (Time.Current < failTime) if (Time.Current < failTime && failSamplePlaybackInitiated)
{
failSamplePlaybackInitiated = false; failSamplePlaybackInitiated = false;
failSample.Stop();
}
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@@ -197,8 +197,7 @@ namespace osu.Game.Screens.Play
public override void OnSuspending(ScreenTransitionEvent e) public override void OnSuspending(ScreenTransitionEvent e)
{ {
// safety against filters or samples from the indicator playing long after the screen is exited stopAllAudioEffects();
failIndicator.RemoveAndDisposeImmediately();
base.OnSuspending(e); base.OnSuspending(e);
} }
@@ -208,5 +207,17 @@ namespace osu.Game.Screens.Play
failIndicator.RemoveAndDisposeImmediately(); failIndicator.RemoveAndDisposeImmediately();
return base.OnExiting(e); return base.OnExiting(e);
} }
private void stopAllAudioEffects()
{
// safety against filters or samples from the indicator playing long after the screen is exited
failIndicator.RemoveAndDisposeImmediately();
if (GameplayClockContainer is MasterGameplayClockContainer master)
{
playbackSettings.UserPlaybackRate.UnbindFrom(master.UserPlaybackRate);
master.UserPlaybackRate.SetDefault();
}
}
} }
} }

View File

@@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
@@ -50,6 +51,9 @@ namespace osu.Game.Screens.SelectV2
private TrianglesV2 triangles = null!; private TrianglesV2 triangles = null!;
[Resolved]
private IRulesetStore rulesets { get; set; } = null!;
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -215,7 +219,7 @@ namespace osu.Game.Screens.SelectV2
Debug.Assert(Item != null); Debug.Assert(Item != null);
var beatmap = (BeatmapInfo)Item.Model; var beatmap = (BeatmapInfo)Item.Model;
difficultyIcon.Icon = beatmap.Ruleset.CreateInstance().CreateIcon(); difficultyIcon.Icon = getRulesetIcon(beatmap.Ruleset);
localRank.Beatmap = beatmap; localRank.Beatmap = beatmap;
difficultyText.Text = beatmap.DifficultyName; difficultyText.Text = beatmap.DifficultyName;
@@ -225,6 +229,16 @@ namespace osu.Game.Screens.SelectV2
updateKeyCount(); updateKeyCount();
} }
private Drawable getRulesetIcon(RulesetInfo rulesetInfo)
{
var rulesetInstance = rulesets.GetRuleset(rulesetInfo.ShortName)?.CreateInstance();
if (rulesetInstance is null)
return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
return rulesetInstance.CreateIcon();
}
protected override void FreeAfterUse() protected override void FreeAfterUse()
{ {
base.FreeAfterUse(); base.FreeAfterUse();

View File

@@ -36,7 +36,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="20.1.0" /> <PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2025.808.0" /> <PackageReference Include="ppy.osu.Framework" Version="2025.808.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2025.815.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2025.819.0" />
<PackageReference Include="Sentry" Version="5.1.1" /> <PackageReference Include="Sentry" Version="5.1.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.39.0" /> <PackageReference Include="SharpCompress" Version="0.39.0" />