1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 06:03:08 +08:00

Merge branch 'master' into gameplay-hud-redesign/counters

This commit is contained in:
Salman Ahmed 2023-11-05 02:02:36 +03:00
commit 1d4f4cf4c3
83 changed files with 1130 additions and 291 deletions

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -4,6 +4,7 @@ Version: 2.5
[Mania]
Keys: 4
ColumnLineWidth: 3,1,3,1,1
LightFramePerSecond: 15
// some skins found in the wild had configuration keys where the @2x suffix was included in the values.
// the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything
// if @2x assets are present.
@ -15,5 +16,6 @@ Hit300: mania/hit300@2x
Hit300g: mania/hit300g@2x
StageLeft: mania/stage-left
StageRight: mania/stage-right
StageLight: mania/stage-light
NoteImage0L: LongNoteTailWang
NoteImage1L: LongNoteTailWang

View File

@ -255,16 +255,6 @@ namespace osu.Game.Rulesets.Mania
case ModType.Conversion:
return new Mod[]
{
new MultiMod(new ManiaModKey4(),
new ManiaModKey5(),
new ManiaModKey6(),
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
new ManiaModKey10(),
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3()),
new ManiaModRandom(),
new ManiaModDualStages(),
new ManiaModMirror(),
@ -272,7 +262,19 @@ namespace osu.Game.Rulesets.Mania
new ManiaModClassic(),
new ManiaModInvert(),
new ManiaModConstantSpeed(),
new ManiaModHoldOff()
new ManiaModHoldOff(),
new MultiMod(
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
new ManiaModKey6(),
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
new ManiaModKey10()
),
};
case ModType.Automation:

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
@ -99,9 +100,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return SkinUtils.As<TValue>(new Bindable<float>(30));
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(
stage.IsSpecialColumn(columnIndex) ? 120 : 60
));
float width;
bool isSpecialColumn = stage.IsSpecialColumn(columnIndex);
// Best effort until we have better mobile support.
if (RuntimeInfo.IsMobile)
width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1);
else
width = 60 * (isSpecialColumn ? 2 : 1);
return SkinUtils.As<TValue>(new Bindable<float>(width));
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d =>
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d =>
{
if (d == null)
return;

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI.Scrolling;
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Container lightContainer = null!;
private Sprite light = null!;
private Drawable light = null!;
public LegacyColumnBackground()
{
@ -39,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
Color4 lightColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
?? Color4.White;
int lightFramePerSecond = skin.GetManiaSkinConfig<int>(LegacyManiaSkinConfigurationLookups.LightFramePerSecond)?.Value ?? 60;
InternalChildren = new[]
{
lightContainer = new Container
@ -46,16 +47,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = lightPosition },
Child = light = new Sprite
Child = light = skin.GetAnimation(lightImage, true, true, frameLength: 1000d / lightFramePerSecond)?.With(l =>
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
Texture = skin.GetTexture(lightImage),
RelativeSizeAxes = Axes.X,
Width = 1,
Alpha = 0
}
l.Anchor = Anchor.BottomCentre;
l.Origin = Anchor.BottomCentre;
l.Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour);
l.RelativeSizeAxes = Axes.X;
l.Width = 1;
l.Alpha = 0;
}) ?? Empty(),
}
};

View File

@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value
?? default_hit_result_skin_filenames[result];
var animation = this.GetAnimation(filename, true, true);
var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d);
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
}

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
@ -24,15 +21,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderVelocityAdjust : OsuGameTestScene
{
private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor;
private Screens.Edit.Editor? editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor;
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().FirstOrDefault();
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().FirstOrDefault()!;
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().FirstOrDefault();
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().FirstOrDefault()!;
private Slider slider => editorBeatmap.HitObjects.OfType<Slider>().FirstOrDefault();
private Slider? slider => editorBeatmap.HitObjects.OfType<Slider>().FirstOrDefault();
private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType<TimelineHitObjectBlueprint>().FirstOrDefault();
private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType<TimelineHitObjectBlueprint>().FirstOrDefault()!;
private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType<DifficultyPointPiece>().First();
@ -46,6 +43,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
double? velocity = null;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True);
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.Centre));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
AddStep("exit placement mode", () => InputManager.Key(Key.Number1));
AddAssert("slider placed", () => slider, () => Is.Not.Null);
AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider));
AddAssert("ensure one slider placed", () => slider, () => Is.Not.Null);
AddStep("store velocity", () => velocity = slider!.Velocity);
if (adjustVelocity)
{
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
AddAssert("velocity adjusted", () => slider!.Velocity,
() => Is.EqualTo(velocity!.Value * 2).Within(Precision.DOUBLE_EPSILON));
AddStep("store velocity", () => velocity = slider!.Velocity);
}
AddStep("save", () => InputManager.Keys(PlatformAction.Save));
AddStep("exit", () => InputManager.Key(Key.Escape));
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True);
AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime));
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity));
}
[Test]
public void TestVelocityUndo()
{
double? velocityBefore = null;
double? durationBefore = null;
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
@ -60,36 +106,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("exit placement mode", () => InputManager.Key(Key.Number1));
AddAssert("slider placed", () => slider != null);
AddAssert("slider placed", () => slider, () => Is.Not.Null);
AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider));
AddAssert("ensure one slider placed", () => slider != null);
AddStep("store velocity", () => velocity = slider.Velocity);
if (adjustVelocity)
AddStep("store velocity", () =>
{
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
velocityBefore = slider!.Velocity;
durationBefore = slider.Duration;
});
AddAssert("velocity adjusted", () =>
{
Debug.Assert(velocity != null);
return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity);
});
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
AddStep("store velocity", () => velocity = slider.Velocity);
}
AddAssert("velocity adjusted", () => slider!.Velocity, () => Is.EqualTo(velocityBefore!.Value * 2).Within(Precision.DOUBLE_EPSILON));
AddStep("save", () => InputManager.Keys(PlatformAction.Save));
AddStep("exit", () => InputManager.Key(Key.Escape));
AddStep("undo", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Z);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
AddAssert("slider has correct velocity", () => slider.Velocity == velocity);
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore));
AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore));
}
}
}

View File

@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
};
var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2);
});
@ -103,8 +103,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private partial class TestLegacyMainCirclePiece : LegacyMainCirclePiece
{
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
: base(priorityLookupPrefix, false)

View File

@ -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 NUnit.Framework;
@ -10,6 +11,8 @@ using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays;
@ -47,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void Setup() => Schedule(() =>
{
manualClock = null;
SelectedMods.Value = Array.Empty<Mod>();
});
/// <summary>
@ -102,6 +106,33 @@ namespace osu.Game.Rulesets.Osu.Tests
assertSpinnerHit(false);
}
[Test]
public void TestVibrateWithoutSpinningOnCentreWithDoubleTime()
{
List<ReplayFrame> frames = new List<ReplayFrame>();
const int rate = 2;
// the track clock is going to be playing twice as fast,
// so the vibration time in clock time needs to be twice as long
// to keep constant speed in real time.
const int vibrate_time = 50 * rate;
int direction = -1;
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
{
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton));
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton));
direction *= -1;
}
AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } });
performTest(frames);
assertSpinnerHit(false);
}
/// <summary>
/// Spins in a single direction.
/// </summary>

View File

@ -2,11 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -90,21 +89,18 @@ namespace osu.Game.Rulesets.Osu.Mods
break;
default:
addBubble();
BubbleDrawable bubble = bubblePool.Get();
bubble.WasHit = drawable.IsHit;
bubble.Position = getPosition(drawableOsuHitObject);
bubble.AccentColour = drawable.AccentColour.Value;
bubble.InitialSize = new Vector2(bubbleSize);
bubble.FadeTime = bubbleFade;
bubble.MaxSize = maxSize;
bubbleContainer.Add(bubble);
break;
}
void addBubble()
{
BubbleDrawable bubble = bubblePool.Get();
bubble.DrawableOsuHitObject = drawableOsuHitObject;
bubble.InitialSize = new Vector2(bubbleSize);
bubble.FadeTime = bubbleFade;
bubble.MaxSize = maxSize;
bubbleContainer.Add(bubble);
}
};
drawableObject.OnRevertResult += (drawable, _) =>
@ -118,18 +114,38 @@ namespace osu.Game.Rulesets.Osu.Mods
};
}
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
{
switch (drawableObject)
{
// SliderHeads are derived from HitCircles,
// so we must handle them before to avoid them using the wrong positioning logic
case DrawableSliderHead:
return drawableObject.HitObject.Position;
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
case DrawableHitCircle:
return drawableObject.Position;
default:
return drawableObject.HitObject.Position;
}
}
#region Pooled Bubble drawable
private partial class BubbleDrawable : PoolableDrawable
{
public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
public Vector2 InitialSize { get; set; }
public float MaxSize { get; set; }
public double FadeTime { get; set; }
public bool WasHit { get; set; }
public Color4 AccentColour { get; set; }
private readonly Box colourBox;
private readonly CircularContainer content;
@ -157,15 +173,12 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void PrepareForUse()
{
Debug.Assert(DrawableOsuHitObject.IsNotNull());
Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
Colour = WasHit ? Colour4.White : Colour4.Black;
Scale = new Vector2(1);
Position = getPosition(DrawableOsuHitObject);
Size = InitialSize;
//We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
ColourInfo colourDarker = AccentColour.Darken(0.1f);
// The absolute length of the bubble's animation, can be used in fractions for animations of partial length
double duration = 1700 + Math.Pow(FadeTime, 1.07f);
@ -178,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Mods
.ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
.FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
if (!DrawableOsuHitObject.IsHit) return;
if (!WasHit) return;
content.BorderThickness = InitialSize.X / 3.5f;
content.BorderColour = Colour4.White;
@ -192,24 +205,6 @@ namespace osu.Game.Rulesets.Osu.Mods
// Avoids transparency overlap issues during the bubble "pop"
.TransformTo(nameof(BorderThickness), 0f);
}
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
{
switch (drawableObject)
{
// SliderHeads are derived from HitCircles,
// so we must handle them before to avoid them using the wrong positioning logic
case DrawableSliderHead:
return drawableObject.HitObject.Position;
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
case DrawableHitCircle:
return drawableObject.Position;
default:
return drawableObject.HitObject.Position;
}
}
}
#endregion

View File

@ -41,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (hitObject)
{
case Slider slider:
slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value;
foreach (var head in slider.NestedHitObjects.OfType<SliderHeadCircle>())
head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value;
slider.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value;
break;
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public Container OverlayElementContainer { get; private set; }
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
public override bool DisplayResult => HitObject.ClassicSliderBehaviour;
[CanBeNull]
public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody;
@ -272,30 +272,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || !TailCircle.Judged || Time.Current < HitObject.EndTime)
return;
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
if (HitObject.OnlyJudgeNestedObjects)
if (HitObject.ClassicSliderBehaviour)
{
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
return;
}
// Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
ApplyResult(r =>
{
int totalTicks = NestedHitObjects.Count;
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
if (hitTicks == totalTicks)
r.Type = HitResult.Great;
else if (hitTicks == 0)
r.Type = HitResult.Miss;
else
// Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
ApplyResult(r =>
{
double hitFraction = (double)hitTicks / totalTicks;
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
}
});
int totalTicks = NestedHitObjects.Count;
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
if (hitTicks == totalTicks)
r.Type = HitResult.Great;
else if (hitTicks == 0)
r.Type = HitResult.Miss;
else
{
double hitFraction = (double)hitTicks / totalTicks;
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
}
});
}
else
{
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
public override void PlaySamples()

View File

@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
public override bool DisplayResult
{
get
{
if (HitObject?.ClassicSliderBehaviour == true)
return false;
return base.DisplayResult;
}
}
private readonly IBindable<int> pathVersion = new Bindable<int>();
@ -56,12 +65,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Debug.Assert(HitObject != null);
if (HitObject.JudgeAsNormalHitCircle)
return base.ResultFor(timeOffset);
if (HitObject.ClassicSliderBehaviour)
{
// With classic slider behaviour, heads are considered fully hit if in the largest hit window.
// We can't award a full Great because the true Great judgement is awarded on the Slider itself,
// reduced based on number of ticks hit,
// so we use the most suitable LargeTick judgement here instead.
return base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
// If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring.
var result = base.ResultFor(timeOffset);
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
return base.ResultFor(timeOffset);
}
public override void Shake()

View File

@ -275,6 +275,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (spinningSample != null && spinnerFrequencyModulate)
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
// Ticks can theoretically be judged at any point in the spinner's duration.
// A tick must be alive to correctly play back samples,
// but for performance reasons, we only want to keep the next tick alive.
var next = NestedHitObjects.FirstOrDefault(h => !h.Judged);
// See default `LifetimeStart` as set in `DrawableSpinnerTick`.
if (next?.LifetimeStart == double.MaxValue)
next.LifetimeStart = HitObject.StartTime;
}
protected override void UpdateAfterChildren()

View File

@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public override bool DisplayResult => false;
protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject;
public DrawableSpinnerTick()
: this(null)
{
@ -29,10 +27,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.OnApply();
// the tick can be theoretically judged at any point in the spinner's duration,
// so it must be alive throughout the spinner's entire lifetime.
// this mostly matters for correct sample playback.
LifetimeStart = DrawableSpinner.HitObject.StartTime;
// Lifetime will be managed by `DrawableSpinner`.
LifetimeStart = double.MaxValue;
}
/// <summary>

View File

@ -49,13 +49,9 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
path.ControlPoints.Clear();
path.ExpectedDistance.Value = null;
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
if (value != null)
{
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
}
}
@ -128,10 +124,21 @@ namespace osu.Game.Rulesets.Osu.Objects
public double TickDistanceMultiplier = 1;
/// <summary>
/// Whether this <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
/// If <c>false</c>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
/// If <see langword="false"/>, <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
/// If <see langword="true"/>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
/// </summary>
public bool OnlyJudgeNestedObjects = true;
public bool ClassicSliderBehaviour
{
get => classicSliderBehaviour;
set
{
classicSliderBehaviour = value;
if (HeadCircle != null)
HeadCircle.ClassicSliderBehaviour = value;
}
}
private bool classicSliderBehaviour;
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
{
@ -191,7 +198,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
@ -201,6 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
ClassicSliderBehaviour = ClassicSliderBehaviour,
});
break;
@ -210,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Objects
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
StackHeight = StackHeight,
});
break;
@ -221,7 +228,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
}
@ -266,7 +272,11 @@ namespace osu.Game.Rulesets.Osu.Objects
TailSamples = this.GetNodeSamples(repeatCount + 1);
}
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
public override Judgement CreateJudgement() => ClassicSliderBehaviour
// See logic in `DrawableSlider.CheckForResult()`
? new OsuJudgement()
// Of note, this creates a combo discrepancy for non-classic-mod sliders (there is no combo increase for tail or slider judgement).
: new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}

View File

@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Osu.Objects
public class SliderHeadCircle : HitCircle
{
/// <summary>
/// Whether to treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
/// If <c>false</c>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
/// If <see langword="false"/>, treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
/// If <see langword="true"/>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
/// </summary>
public bool JudgeAsNormalHitCircle = true;
public bool ClassicSliderBehaviour;
public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new SliderTickJudgement() : base.CreateJudgement();
}
}

View File

@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
rotationTransferred = true;
}
Debug.Assert(Math.Abs(delta) <= 180);
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
delta = (float)(delta * Math.Abs(rate));
Debug.Assert(Math.Abs(delta) <= 180);
currentRotation += delta;
drawableSpinner.Result.History.ReportDelta(Time.Current, delta);
}

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@ -62,12 +63,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// otherwise fall back to the default prefix "hitcircle".
string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2;
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[]
{
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2) })
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -76,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2))
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -41,11 +42,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2) ?? Empty()).With(d =>
InternalChild = arrow = new Sprite
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
});
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin?.GetTexture(lookupName)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2),
};
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;

View File

@ -48,40 +48,45 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider)
{
Drawable? getDrawableFor(string lookup)
Drawable? getDrawableFor(string lookup, bool animatable)
{
const string normal_hit = "taikohit";
const string big_hit = "taikobig";
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: max_circle_sprite_size) ??
return skin.GetAnimation($"{prefix}{lookup}", animatable, false, maxSize: max_circle_sprite_size) ??
// fallback to regular size if "big" version doesn't exist.
skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: max_circle_sprite_size);
skin.GetAnimation($"{normal_hit}{lookup}", animatable, false, maxSize: max_circle_sprite_size);
}
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle", false))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
foregroundLayer = getDrawableFor("circleoverlay", true);
foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
{
foregroundLayer.Anchor = Anchor.Centre;
foregroundLayer.Origin = Anchor.Centre;
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
// For now just stop at first frame for sanity.
if (foregroundLayer is IFramedAnimation animatedForegroundLayer)
animatedForegroundLayer.Stop();
AddInternal(foregroundLayer);
}
drawableHitObject.StartTimeBindable.BindValueChanged(startTime =>
{
timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT;
}, true);
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
// For now just stop at first frame for sanity.
foreach (var c in InternalChildren)
{
(c as IFramedAnimation)?.Stop();
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
}
if (gameplayState != null)
currentCombo.BindTo(gameplayState.ScoreProcessor.Combo);
}
@ -101,11 +106,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
foreach (var c in InternalChildren)
c.Scale = new Vector2(DrawHeight / circle_piece_size.Y);
if (foregroundLayer is IFramedAnimation animatableForegroundLayer)
animateForegroundLayer(animatableForegroundLayer);
if (foregroundLayer is IFramedAnimation animatedForegroundLayer)
animateForegroundLayer(animatedForegroundLayer);
}
private void animateForegroundLayer(IFramedAnimation animatableForegroundLayer)
private void animateForegroundLayer(IFramedAnimation animation)
{
int multiplier;
@ -119,12 +124,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
else
{
animatableForegroundLayer.GotoFrame(0);
animation.GotoFrame(0);
return;
}
animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1;
animatableForegroundLayer.GotoFrame(animationFrame);
animation.GotoFrame(animationFrame);
}
private Color4 accentColour;

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests
@ -46,12 +47,15 @@ namespace osu.Game.Tests
public partial class TestOsuGameBase : OsuGameBase
{
public RealmAccess Realm => Dependencies.Get<RealmAccess>();
public new IAPIProvider API => base.API;
private readonly bool withBeatmap;
public TestOsuGameBase(bool withBeatmap)
{
this.withBeatmap = withBeatmap;
base.API = new DummyAPIAccess();
}
[BackgroundDependencyLoader]

View File

@ -11,7 +11,10 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -67,6 +70,116 @@ namespace osu.Game.Tests.Scores.IO
}
}
[TestCase(false)]
[TestCase(true)]
public void TestLastPlayedUpdate(bool isLocalUser)
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
if (!isLocalUser)
osu.API.Logout();
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var beatmapInfo = beatmap.Beatmaps.First();
DateTimeOffset replayDate = DateTimeOffset.Now;
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser
{
Username = "Test user",
Id = DummyAPIAccess.DUMMY_USER_ID,
},
Date = replayDate,
OnlineID = 12345,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
if (isLocalUser)
Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(replayDate));
else
Assert.That(imported.BeatmapInfo!.LastPlayed, Is.Null);
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestLastPlayedNotUpdatedDueToNewerPlays()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var beatmapInfo = beatmap.Beatmaps.First();
var realmAccess = osu.Dependencies.Get<RealmAccess>();
realmAccess.Write(r => r.Find<BeatmapInfo>(beatmapInfo.ID)!.LastPlayed = new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero));
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser
{
Username = "Test user",
Id = DummyAPIAccess.DUMMY_USER_ID,
},
Date = new DateTimeOffset(2023, 10, 27, 0, 0, 0, TimeSpan.Zero),
OnlineID = 12345,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero)));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestImportMods()
{

View File

@ -7,8 +7,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
@ -44,6 +46,47 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestContextMenuWithObjectBehind()
{
TimelineHitObjectBlueprint blueprint;
AddStep("add object", () =>
{
EditorBeatmap.Add(new HitCircle { StartTime = 3000 });
});
AddStep("enter slider placement", () =>
{
InputManager.Key(Key.Number3);
InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre);
});
AddStep("start conflicting slider", () =>
{
InputManager.Click(MouseButton.Left);
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().First();
InputManager.MoveMouseTo(blueprint.ScreenSpaceDrawQuad.TopLeft - new Vector2(10, 0));
});
AddStep("end conflicting slider", () =>
{
InputManager.Click(MouseButton.Right);
});
AddStep("click object", () =>
{
InputManager.Key(Key.Number1);
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().First();
InputManager.MoveMouseTo(blueprint);
InputManager.Click(MouseButton.Left);
});
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("context menu open", () => this.ChildrenOfType<OsuContextMenu>().SingleOrDefault()?.State == MenuState.Open);
}
[Test]
public void TestNudgeSelection()
{
@ -139,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("click away", () =>
{
InputManager.MoveMouseTo(Editor.ChildrenOfType<TimelineArea>().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One);
InputManager.MoveMouseTo(Editor.ChildrenOfType<Timeline>().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(5));
InputManager.Click(MouseButton.Left);
});

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestPauseWithLargeOffset()
{
double lastTime;
double lastStopTime;
bool alwaysGoingForward = true;
AddStep("force large offset", () =>
@ -84,20 +84,24 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add time forward check hook", () =>
{
lastTime = double.MinValue;
lastStopTime = double.MinValue;
alwaysGoingForward = true;
Player.OnUpdate += _ =>
{
double currentTime = Player.GameplayClockContainer.CurrentTime;
bool goingForward = currentTime >= lastTime - 500;
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
double currentTime = masterClock.CurrentTime;
bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime);
alwaysGoingForward &= goingForward;
if (!goingForward)
Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})");
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
lastTime = currentTime;
if (masterClock.LastStopTime != null)
lastStopTime = masterClock.LastStopTime.Value;
};
});

View File

@ -214,10 +214,18 @@ namespace osu.Game.Tests.Visual.Gameplay
// Files starting with _ are temporary, created by CreateFileSafely call.
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
AddAssert("filesize is non-zero", () =>
AddUntilStep("filesize is non-zero", () =>
{
using (var stream = LocalStorage.GetStream(filePath))
return stream.Length;
try
{
using (var stream = LocalStorage.GetStream(filePath))
return stream.Length;
}
catch (IOException)
{
// file move may still be in progress.
return 0;
}
}, () => Is.Not.Zero);
}

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
@ -27,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
/// <remarks>
/// The HUD is hidden as it does't really affect game balance if HUD elements are larger than they should be.
/// </remarks>
[Ignore("This test is for visual testing, and has no value in being run in standard CI runs.")]
public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers
{
// scale textures to 4 times their size.

View File

@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20));
}
[SetUp]

View File

@ -693,7 +693,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
[FlakyTest] // See above
[Ignore("Failing too often, needs revisiting in some future.")]
// This test is failing even after 10 retries (see https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419)
// Something is stopping the ready button from changing states, over multiple runs.
public void TestGameplayExitFlow()
{
Bindable<double>? holdDelay = null;

View File

@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Navigation
double scrollPosition = 0;
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden));
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());

View File

@ -5,8 +5,10 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
@ -28,7 +30,14 @@ namespace osu.Game.Tests.Visual.Online
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create header", () => Child = header = new ProfileHeader());
AddStep("create header", () =>
{
Child = new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
Child = header = new ProfileHeader()
};
});
}
[Test]
@ -136,5 +145,260 @@ namespace osu.Game.Tests.Visual.Online
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
}, new OsuRuleset().RulesetInfo));
}
[Test]
public void TestManyTournamentBanners()
{
AddStep("Show user w/ many tournament banners", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 728,
Username = "Certain Guy",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
Statistics = new UserStatistics
{
IsRanked = false,
// web will sometimes return non-empty rank history even for unranked users.
RankHistory = new APIRankHistory
{
Mode = @"osu",
Data = Enumerable.Range(2345, 85).ToArray()
},
},
TournamentBanners = new[]
{
new TournamentBanner
{
Id = 15329,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg"
},
new TournamentBanner
{
Id = 15588,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg"
},
new TournamentBanner
{
Id = 15589,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg"
},
new TournamentBanner
{
Id = 15590,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg"
},
new TournamentBanner
{
Id = 15591,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP@2x.jpg"
},
new TournamentBanner
{
Id = 15592,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU@2x.jpg"
},
new TournamentBanner
{
Id = 15593,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR@2x.jpg"
},
new TournamentBanner
{
Id = 15594,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ@2x.jpg"
},
new TournamentBanner
{
Id = 15595,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH@2x.jpg"
},
new TournamentBanner
{
Id = 15596,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW@2x.jpg"
},
new TournamentBanner
{
Id = 15603,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID@2x.jpg"
},
new TournamentBanner
{
Id = 15604,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ@2x.jpg"
},
new TournamentBanner
{
Id = 15605,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR@2x.jpg"
},
new TournamentBanner
{
Id = 15606,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR@2x.jpg"
},
new TournamentBanner
{
Id = 15607,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg"
},
new TournamentBanner
{
Id = 15639,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX@2x.jpg"
},
new TournamentBanner
{
Id = 15640,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU@2x.jpg"
},
new TournamentBanner
{
Id = 15641,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT@2x.jpg"
},
new TournamentBanner
{
Id = 15642,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA@2x.jpg"
},
new TournamentBanner
{
Id = 15643,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL@2x.jpg"
},
new TournamentBanner
{
Id = 15644,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI@2x.jpg"
},
new TournamentBanner
{
Id = 15645,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO@2x.jpg"
},
new TournamentBanner
{
Id = 15646,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG@2x.jpg"
},
new TournamentBanner
{
Id = 15647,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE@2x.jpg"
},
new TournamentBanner
{
Id = 15648,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES@2x.jpg"
},
new TournamentBanner
{
Id = 15649,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE@2x.jpg"
},
new TournamentBanner
{
Id = 15650,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA@2x.jpg"
},
new TournamentBanner
{
Id = 15651,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO@2x.jpg"
},
new TournamentBanner
{
Id = 15652,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB@2x.jpg"
},
new TournamentBanner
{
Id = 15653,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US@2x.jpg"
},
new TournamentBanner
{
Id = 15654,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg"
},
new TournamentBanner
{
Id = 15655,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR@2x.jpg"
},
new TournamentBanner
{
Id = 15686,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg"
}
}
}, new OsuRuleset().RulesetInfo));
}
}
}

View File

@ -121,12 +121,29 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
},
TournamentBanner = new TournamentBanner
TournamentBanners = new[]
{
Id = 13926,
TournamentId = 35,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg",
new TournamentBanner
{
Id = 15588,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg"
},
new TournamentBanner
{
Id = 15589,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg"
},
new TournamentBanner
{
Id = 15590,
TournamentId = 41,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg"
}
},
Badges = new[]
{

View File

@ -140,6 +140,17 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
}
[Test]
public void TestSearchTextBoxSelectedOnShow()
{
SearchTextBox searchTextBox = null!;
AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First()).Current.Value = "some text");
AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty);
AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2);
AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value);
}
[BackgroundDependencyLoader]
private void load()
{

View File

@ -13,6 +13,7 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@ -1111,6 +1112,23 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("0 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "0 matches");
}
[Test]
public void TestCutInFilterTextBox()
{
createSongSelect();
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
AddStep("press ctrl-x", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.X);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text, () => Is.Empty);
}
private void waitForInitialSelection()
{
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);

View File

@ -799,8 +799,11 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectOverlay.ChildrenOfType<ModColumn>().Any() && modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
&& modSelectOverlay.ChildrenOfType<ModPresetColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModPresetColumn>().All(column => column.IsLoaded));
private void changeRuleset(int id)
{

View File

@ -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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Tests.Visual;
@ -14,13 +15,19 @@ namespace osu.Game.Tournament.Tests.Components
{
public partial class TestSceneDrawableTournamentTeam : OsuGridTestScene
{
[Cached]
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
public TestSceneDrawableTournamentTeam()
: base(4, 3)
{
AddToggleStep("toggle seed view", v => Ladder.DisplayTeamSeeds.Value = v);
var team = new TournamentTeam
{
FlagName = { Value = "AU" },
FullName = { Value = "Australia" },
Seed = { Value = "#5" },
Players =
{
new TournamentUser { Username = "ASecretBox" },
@ -30,7 +37,7 @@ namespace osu.Game.Tournament.Tests.Components
new TournamentUser { Username = "Parkes" },
new TournamentUser { Username = "Shiroha" },
new TournamentUser { Username = "Jordan The Bear" },
}
},
};
var match = new TournamentMatch { Team1 = { Value = team } };

View File

@ -67,7 +67,7 @@ namespace osu.Game.Tournament.Tests
FlagName = { Value = "JP" },
FullName = { Value = "Japan" },
LastYearPlacing = { Value = 10 },
Seed = { Value = "Low" },
Seed = { Value = "#12" },
SeedingResults =
{
new SeedingResult
@ -140,6 +140,7 @@ namespace osu.Game.Tournament.Tests
Acronym = { Value = "USA" },
FlagName = { Value = "US" },
FullName = { Value = "United States" },
Seed = { Value = "#3" },
Players =
{
new TournamentUser { Username = "Hello" },

View File

@ -0,0 +1,45 @@
// 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.Bindables;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Components
{
public partial class DrawableTeamSeed : TournamentSpriteTextWithBackground
{
private readonly TournamentTeam? team;
private IBindable<string> seed = null!;
private Bindable<bool> displaySeed = null!;
public DrawableTeamSeed(TournamentTeam? team)
{
this.team = team;
}
[Resolved]
private LadderInfo ladder { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Text.Font = Text.Font.With(size: 36);
}
protected override void LoadComplete()
{
base.LoadComplete();
if (team == null)
return;
seed = team.Seed.GetBoundCopy();
seed.BindValueChanged(s => Text.Text = s.NewValue, true);
displaySeed = ladder.DisplayTeamSeeds.GetBoundCopy();
displaySeed.BindValueChanged(v => Alpha = v.NewValue ? 1 : 0, true);
}
}
}

View File

@ -18,11 +18,12 @@ namespace osu.Game.Tournament.Components
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
new DrawableTeamHeader(colour),
new DrawableTeamTitle(team),
new DrawableTeamSeed(team),
}
};
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components
Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR,
Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50),
Padding = new MarginPadding { Left = 10, Right = 20 },
Text = text
Text = text,
}
};
}

View File

@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
public Bindable<bool> AutoProgressScreens = new BindableBool(true);
public Bindable<bool> SplitMapPoolByMods = new BindableBool(true);
public Bindable<bool> DisplayTeamSeeds = new BindableBool();
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{
private readonly TeamScore score;
private readonly TournamentSpriteTextWithBackground teamText;
private readonly TournamentSpriteTextWithBackground teamNameText;
private readonly Bindable<string> teamName = new Bindable<string>("???");
@ -95,7 +95,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
}
}
},
teamText = new TournamentSpriteTextWithBackground
teamNameText = new TournamentSpriteTextWithBackground
{
Scale = new Vector2(0.5f),
Origin = anchor,
Anchor = anchor,
},
new DrawableTeamSeed(Team)
{
Scale = new Vector2(0.5f),
Origin = anchor,
@ -119,7 +125,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
if (Team != null)
teamName.BindTo(Team.FullName);
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true);
}
private void updateDisplay()

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
private Drawable chroma = null!;
[BackgroundDependencyLoader]
private void load(LadderInfo ladder, MatchIPCInfo ipc)
private void load(MatchIPCInfo ipc)
{
this.ipc = ipc;
@ -49,7 +49,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
},
header = new MatchHeader
{
ShowLogo = false
ShowLogo = false,
},
new Container
{
@ -118,12 +118,12 @@ namespace osu.Game.Tournament.Screens.Gameplay
LabelText = "Players per team",
Current = LadderInfo.PlayersPerTeam,
KeyboardStep = 1,
}
},
}
}
});
ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
warmup.BindValueChanged(w =>
{

View File

@ -140,6 +140,12 @@ namespace osu.Game.Tournament.Screens.Setup
Description = "Screens will progress automatically from gameplay -> results -> map pool",
Current = LadderInfo.AutoProgressScreens,
},
new LabelledSwitchButton
{
Label = "Display team seeds",
Description = "Team seeds will display alongside each team at the top in gameplay/map pool screens.",
Current = LadderInfo.DisplayTeamSeeds,
},
};
}

View File

@ -7,6 +7,7 @@ using System;
using System.Globalization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
using osu.Game.Extensions;
@ -46,7 +47,7 @@ namespace osu.Game.Online.API
if (WebRequest != null)
{
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
}
}

View File

@ -112,7 +112,7 @@ namespace osu.Game.Online.API
LocalUser.Value = new APIUser
{
Username = username,
Id = 1001,
Id = DUMMY_USER_ID,
};
state.Value = APIState.Online;

View File

@ -234,9 +234,8 @@ namespace osu.Game.Online.API.Requests.Responses
set => Statistics.RankHistory = value;
}
[JsonProperty(@"active_tournament_banner")]
[CanBeNull]
public TournamentBanner TournamentBanner;
[JsonProperty(@"active_tournament_banners")]
public TournamentBanner[] TournamentBanners;
[JsonProperty("badges")]
public Badge[] Badges;

View File

@ -11,7 +11,7 @@ using osu.Game.Overlays.Profile.Header.Components;
namespace osu.Game.Overlays.Profile.Header
{
public partial class BannerHeaderContainer : CompositeDrawable
public partial class BannerHeaderContainer : FillFlowContainer
{
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Header
private void load()
{
Alpha = 0;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
FillAspectRatio = 1000 / 60f;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
}
protected override void LoadComplete()
@ -40,13 +40,21 @@ namespace osu.Game.Overlays.Profile.Header
ClearInternal();
var banner = user?.TournamentBanner;
var banners = user?.TournamentBanners;
if (banner != null)
if (banners?.Length > 0)
{
Show();
LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token);
for (int index = 0; index < banners.Length; index++)
{
int displayIndex = index;
LoadComponentAsync(new DrawableTournamentBanner(banners[index]), asyncBanner =>
{
// load in stable order regardless of async load order.
Insert(displayIndex, asyncBanner);
}, cancellationTokenSource.Token);
}
}
else
{

View File

@ -15,12 +15,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
[LongRunningLoad]
public partial class DrawableTournamentBanner : OsuClickableContainer
{
private const float banner_aspect_ratio = 60 / 1000f;
private readonly TournamentBanner banner;
public DrawableTournamentBanner(TournamentBanner banner)
{
this.banner = banner;
RelativeSizeAxes = Axes.Both;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
this.FadeInFromZero(200);
}
protected override void Update()
{
base.Update();
Height = DrawWidth * banner_aspect_ratio;
}
public override LocalisableString TooltipText => "view in browser";
}
}

View File

@ -498,7 +498,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (existingBinding == null)
{
realm.WriteAsync(r => r.Find<RealmKeyBinding>(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString());
realm.Write(r => r.Find<RealmKeyBinding>(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString());
BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: false, advanceToNextBinding));
return;
}

View File

@ -135,7 +135,7 @@ namespace osu.Game.Overlays
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Child = searchTextBox = new SeekLimitedSearchTextBox
Child = searchTextBox = new SettingsSearchTextBox
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,

View File

@ -0,0 +1,22 @@
// 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 osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays
{
public partial class SettingsSearchTextBox : SeekLimitedSearchTextBox
{
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
// on mobile platforms, focus is not held by the search text box, and the select all feature
// will not make sense on it, and might annoy the user when they try to focus manually.
if (HoldFocus)
SelectAll();
}
}
}

View File

@ -5,6 +5,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Edit
private SelectionState state;
[CanBeNull]
public event Action<SelectionState> StateChanged;
public SelectionState State

View File

@ -107,12 +107,52 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool HasImplementation => this is IApplicableMod;
/// <summary>
/// Whether this mod can be played by a real human user.
/// Non-user-playable mods are not viable for single-player score submission.
/// </summary>
/// <example>
/// <list type="bullet">
/// <item><see cref="ModDoubleTime"/> is user-playable.</item>
/// <item><see cref="ModAutoplay"/> is not user-playable.</item>
/// </list>
/// </example>
[JsonIgnore]
public virtual bool UserPlayable => true;
/// <summary>
/// Whether this mod can be specified as a "required" mod in a multiplayer context.
/// </summary>
/// <example>
/// <list type="bullet">
/// <item><see cref="ModHardRock"/> is valid for multiplayer.</item>
/// <item>
/// <see cref="ModDoubleTime"/> is valid for multiplayer as long as it is a <b>required</b> mod,
/// as that ensures the same duration of gameplay for all users in the room.
/// </item>
/// <item>
/// <see cref="ModAdaptiveSpeed"/> is not valid for multiplayer, as it leads to varying
/// gameplay duration depending on how the users in the room play.
/// </item>
/// <item><see cref="ModAutoplay"/> is not valid for multiplayer.</item>
/// </list>
/// </example>
[JsonIgnore]
public virtual bool ValidForMultiplayer => true;
/// <summary>
/// Whether this mod can be specified as a "free" or "allowed" mod in a multiplayer context.
/// </summary>
/// <example>
/// <list type="bullet">
/// <item><see cref="ModHardRock"/> is valid for multiplayer as a free mod.</item>
/// <item>
/// <see cref="ModDoubleTime"/> is <b>not</b> valid for multiplayer as a free mod,
/// as it could to varying gameplay duration between users in the room depending on whether they picked it.
/// </item>
/// <item><see cref="ModAutoplay"/> is not valid for multiplayer as a free mod.</item>
/// </list>
/// </example>
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;

View File

@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 0.5;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public sealed override bool ValidForMultiplayer => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) };

View File

@ -20,9 +20,9 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 1;
public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public sealed override bool UserPlayable => false;
public sealed override bool ValidForMultiplayer => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) };

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods
{
public abstract class ModRateAdjust : Mod, IApplicableToRate
{
public override bool ValidForMultiplayerAsFreeMod => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public abstract BindableNumber<double> SpeedChange { get; }

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods
/// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active.
/// It should not be used in any real capacity going forward.
/// </remarks>
public class ModScoreV2 : Mod
public sealed class ModScoreV2 : Mod
{
public override string Name => "Score V2";
public override string Acronym => @"SV2";
@ -17,5 +17,7 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
public override double ScoreMultiplier => 1;
public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public abstract BindableBool AdjustPitch { get; }
public override bool ValidForMultiplayerAsFreeMod => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };

View File

@ -5,7 +5,7 @@ using osu.Framework.Localisation;
namespace osu.Game.Rulesets.Mods
{
public class UnknownMod : Mod
public sealed class UnknownMod : Mod
{
/// <summary>
/// The acronym of the mod which could not be resolved.

View File

@ -182,6 +182,12 @@ namespace osu.Game.Scoring
base.PostImport(model, realm, parameters);
populateUserDetails(model);
Debug.Assert(model.BeatmapInfo != null);
// This needs to be run after user detail population to ensure we have a valid user id.
if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed))
model.BeatmapInfo.LastPlayed = model.Date;
}
/// <summary>

View File

@ -166,6 +166,8 @@ namespace osu.Game.Screens.Backgrounds
public override void Add(Drawable drawable)
{
ArgumentNullException.ThrowIfNull(drawable);
if (drawable is Background)
throw new InvalidOperationException($"Use {nameof(Background)} to set a background.");

View File

@ -321,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Spacing = new Vector2(10),
Children = new Drawable[]
{
divisorTextBox = new OsuNumberBox
divisorTextBox = new AutoSelectTextBox
{
RelativeSizeAxes = Axes.X,
PlaceholderText = "Beat divisor"
@ -341,8 +341,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.LoadComplete();
BeatDivisor.BindValueChanged(_ => updateState(), true);
divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry();
Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox));
}
private void setPresetsFromTextBoxEntry()
@ -590,5 +588,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
}
private partial class AutoSelectTextBox : OsuNumberBox
{
protected override void LoadComplete()
{
base.LoadComplete();
GetContainingInputManager().ChangeFocus(this);
SelectAll();
}
}
}
}

View File

@ -40,11 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
public PlacementBlueprint CurrentPlacement { get; private set; }
[Resolved(canBeNull: true)]
private EditorScreenWithTimeline editorScreen { get; set; }
/// <remarks>
/// Positional input must be received outside the container's bounds,
/// in order to handle composer blueprints which are partially offscreen.
/// </remarks>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
public ComposeBlueprintContainer(HitObjectComposer composer)
: base(composer)

View File

@ -11,13 +11,14 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit
{
[Cached]
public abstract partial class EditorScreenWithTimeline : EditorScreen
{
public const float PADDING = 10;
private Container timelineContainer = null!;
public Container TimelineContent { get; private set; } = null!;
private Container mainContent = null!;
public Container MainContent { get; private set; } = null!;
private LoadingSpinner spinner = null!;
@ -70,7 +71,7 @@ namespace osu.Game.Screens.Edit
{
new Drawable[]
{
timelineContainer = new Container
TimelineContent = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit
},
new Drawable[]
{
mainContent = new Container
MainContent = new Container
{
Name = "Main content",
RelativeSizeAxes = Axes.Both,
@ -116,10 +117,10 @@ namespace osu.Game.Screens.Edit
{
spinner.State.Value = Visibility.Hidden;
mainContent.Add(content);
MainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint);
LoadComponentAsync(new TimelineArea(CreateTimelineContent()), timelineContainer.Add);
LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add);
});
}

View File

@ -123,6 +123,8 @@ namespace osu.Game.Screens.Edit
oldWithRepeats.NodeSamples.Clear();
oldWithRepeats.NodeSamples.AddRange(newWithRepeats.NodeSamples);
}
editorBeatmap.Update(oldObject);
}
}

View File

@ -34,16 +34,18 @@ namespace osu.Game.Screens.Edit.Timing
private IAdjustableClock metronomeClock = null!;
private Sample? sampleTick;
private Sample? sampleTickDownbeat;
private Sample? sampleLatch;
private ScheduledDelegate? tickPlaybackDelegate;
private readonly MetronomeTick metronomeTick = new MetronomeTick();
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
public bool EnableClicking { get; set; } = true;
public bool EnableClicking
{
get => metronomeTick.EnableClicking;
set => metronomeTick.EnableClicking = value;
}
public MetronomeDisplay()
{
@ -53,8 +55,6 @@ namespace osu.Game.Screens.Edit.Timing
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleTick = audio.Samples.Get(@"UI/metronome-tick");
sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat");
sampleLatch = audio.Samples.Get(@"UI/metronome-latch");
const float taper = 25;
@ -67,8 +67,11 @@ namespace osu.Game.Screens.Edit.Timing
AutoSizeAxes = Axes.Both;
metronomeTick.Ticked = onTickPlayed;
InternalChildren = new Drawable[]
{
metronomeTick,
new Container
{
Name = @"Taper adjust",
@ -265,9 +268,6 @@ namespace osu.Game.Screens.Edit.Timing
isSwinging = false;
tickPlaybackDelegate?.Cancel();
tickPlaybackDelegate = null;
// instantly latch if pendulum arm is close enough to center (to prevent awkward delayed playback of latch sound)
if (Precision.AlmostEquals(swing.Rotation, 0, 1))
{
@ -306,27 +306,53 @@ namespace osu.Game.Screens.Edit.Timing
float targetAngle = currentAngle > 0 ? -angle : angle;
swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
}
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
private void onTickPlayed()
{
// Originally, this flash only occurred when the pendulum correctly passess the centre.
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
// so now this matches the actual tick sample.
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
}
private partial class MetronomeTick : BeatSyncedContainer
{
public bool EnableClicking;
private Sample? sampleTick;
private Sample? sampleTickDownbeat;
public Action? Ticked;
public MetronomeTick()
{
using (BeginDelayedSequence(beatLength / 2))
{
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
AllowMistimedEventFiring = false;
}
tickPlaybackDelegate = Schedule(() =>
{
if (!EnableClicking)
return;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleTick = audio.Samples.Get(@"UI/metronome-tick");
sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat");
}
var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel();
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
if (channel == null)
return;
if (!IsBeatSyncedWithTrack || !EnableClicking)
return;
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
channel.Play();
});
}
var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel();
if (channel == null)
return;
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
channel.Play();
Ticked?.Invoke();
}
}
}

View File

@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play
///
/// In the future I want to change this.
/// </summary>
private double? actualStopTime;
internal double? LastStopTime;
[Resolved]
private MusicController musicController { get; set; } = null!;
@ -100,7 +100,7 @@ namespace osu.Game.Screens.Play
protected override void StopGameplayClock()
{
actualStopTime = GameplayClock.CurrentTime;
LastStopTime = GameplayClock.CurrentTime;
if (IsLoaded)
{
@ -127,17 +127,17 @@ namespace osu.Game.Screens.Play
public override void Seek(double time)
{
// Safety in case the clock is seeked while stopped.
actualStopTime = null;
LastStopTime = null;
base.Seek(time);
}
protected override void PrepareStart()
{
if (actualStopTime != null)
if (LastStopTime != null)
{
Seek(actualStopTime.Value);
actualStopTime = null;
Seek(LastStopTime.Value);
LastStopTime = null;
}
else
base.PrepareStart();

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play
private const float padding = 2;
public const float WIDTH = cube_size + padding;
[CanBeNull]
public event Action<ColumnState> StateChanged;
private readonly List<Box> drawableRows = new List<Box>();

View File

@ -188,7 +188,10 @@ namespace osu.Game.Screens.Play
{
// token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
if (token == null)
{
Logger.Log("No token, skipping score submission");
return Task.CompletedTask;
}
if (scoreSubmissionSource != null)
return scoreSubmissionSource.Task;
@ -197,6 +200,8 @@ namespace osu.Game.Screens.Play
if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0))
return Task.CompletedTask;
Logger.Log($"Beginning score submission (token:{token.Value})...");
scoreSubmissionSource = new TaskCompletionSource<bool>();
var request = CreateSubmissionRequest(score, token.Value);
@ -206,11 +211,12 @@ namespace osu.Game.Screens.Play
score.ScoreInfo.Position = s.Position;
scoreSubmissionSource.SetResult(true);
Logger.Log($"Score submission completed! (token:{token.Value} id:{s.ID})");
};
request.Failure += e =>
{
Logger.Error(e, $"Failed to submit score ({e.Message})");
Logger.Error(e, $"Failed to submit score (token:{token.Value}): {e.Message}");
scoreSubmissionSource.SetResult(false);
};

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Collections;
@ -23,6 +24,7 @@ using osu.Game.Rulesets;
using osu.Game.Screens.Select.Filter;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Select
{
@ -254,9 +256,6 @@ namespace osu.Game.Screens.Select
public OsuSpriteText FilterText { get; private set; }
// clipboard is disabled because one of the "cut" platform key bindings (shift-delete) conflicts with the beatmap deletion action.
protected override bool AllowClipboardExport => false;
public FilterControlTextBox()
{
Height += filter_text_size;
@ -277,6 +276,15 @@ namespace osu.Game.Screens.Select
Colour = colours.Yellow
});
}
public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
// the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action.
if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete))
return false;
return base.OnPressed(e);
}
}
}
}

View File

@ -40,6 +40,7 @@ namespace osu.Game.Skinning
public float ScorePosition = 300 * POSITION_SCALE_FACTOR;
public bool ShowJudgementLine = true;
public bool KeysUnderNotes;
public int LightFramePerSecond = 60;
public LegacyNoteBodyStyle? NoteBodyStyle;

View File

@ -74,6 +74,7 @@ namespace osu.Game.Skinning
Hit50,
Hit0,
KeysUnderNotes,
NoteBodyStyle
NoteBodyStyle,
LightFramePerSecond
}
}

View File

@ -123,6 +123,11 @@ namespace osu.Game.Skinning
currentConfig.WidthForNoteHeightScale = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
break;
case "LightFramePerSecond":
int lightFramePerSecond = int.Parse(pair.Value, CultureInfo.InvariantCulture);
currentConfig.LightFramePerSecond = lightFramePerSecond > 0 ? lightFramePerSecond : 24;
break;
case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal):
HandleColours(currentConfig, line, true);
break;

View File

@ -273,6 +273,9 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.KeysUnderNotes:
return SkinUtils.As<TValue>(new Bindable<bool>(existing.KeysUnderNotes));
case LegacyManiaSkinConfigurationLookups.LightFramePerSecond:
return SkinUtils.As<TValue>(new Bindable<int>(existing.LightFramePerSecond));
}
return null;

View File

@ -115,7 +115,18 @@ namespace osu.Game.Skinning
maxSize *= texture.ScaleAdjust;
var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y));
// Importantly, check per-axis for the minimum dimension to avoid accidentally inflating
// textures with weird aspect ratios.
float newWidth = Math.Min(texture.Width, maxSize.X);
float newHeight = Math.Min(texture.Height, maxSize.Y);
var croppedTexture = texture.Crop(new RectangleF(
texture.Width / 2f - newWidth / 2f,
texture.Height / 2f - newHeight / 2f,
newWidth,
newHeight
));
croppedTexture.ScaleAdjust = texture.ScaleAdjust;
return croppedTexture;
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.1030.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1030.0" />
<PackageReference Include="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

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