1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 14:10:33 +08:00

Compare commits

...

435 Commits

260 changed files with 4582 additions and 1437 deletions
+3 -1
View File
@@ -185,9 +185,11 @@ jobs:
- name: Add comment environment
if: ${{ github.event_name == 'issue_comment' }}
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
# Add comment environment
echo '${{ github.event.comment.body }}' | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
echo $COMMENT_BODY | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
opt=$(echo ${line} | cut -d '=' -f1)
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
done
+1 -1
View File
@@ -7,7 +7,7 @@ Templates for use when creating osu! dependent projects. Create a fully-testable
```bash
# install (or update) templates package.
# this only needs to be done once
dotnet new -i ppy.osu.Game.Templates
dotnet new install ppy.osu.Game.Templates
# create an empty freeform ruleset
dotnet new ruleset -n MyCoolRuleset
+1 -1
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.1111.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+1 -1
View File
@@ -70,7 +70,7 @@ namespace osu.Android
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseButtons,
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
});
+4
View File
@@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater;
using osu.Game.Utils;
@@ -97,6 +98,9 @@ namespace osu.Android
case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh);
case AndroidTouchHandler th:
return new TouchSettings(th);
default:
return base.CreateSettingsSubsectionFor(handler);
}
+2 -2
View File
@@ -10,8 +10,8 @@ using osu.Game;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using Squirrel;
using Squirrel.SimpleSplat;
using Squirrel.Sources;
using LogLevel = Squirrel.SimpleSplat.LogLevel;
using UpdateManager = osu.Game.Updater.UpdateManager;
@@ -63,7 +63,7 @@ namespace osu.Desktop.Updater
if (localUserInfo?.IsPlaying.Value == true)
return false;
updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
+1 -1
View File
@@ -23,7 +23,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Clowd.Squirrel" Version="2.10.2" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@@ -179,5 +180,33 @@ namespace osu.Game.Rulesets.Catch.Edit
return null;
}
}
protected override void Update()
{
base.Update();
updateDistanceSnapGrid();
}
private void updateDistanceSnapGrid()
{
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
{
distanceSnapGrid.Hide();
return;
}
var sourceHitObject = getDistanceSnapGridSourceHitObject();
if (sourceHitObject == null)
{
distanceSnapGrid.Hide();
return;
}
distanceSnapGrid.Show();
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
}
}
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

@@ -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
+13 -11
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:
@@ -108,7 +108,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
RelativeSizeAxes = Axes.X
},
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
slidingSample = new PausableSkinnableSound { Looping = true }
slidingSample = new PausableSkinnableSound
{
Looping = true,
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
}
});
maskedContents.AddRange(new[]
@@ -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:
@@ -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;
@@ -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(),
}
};
@@ -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);
}
@@ -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));
}
}
}
@@ -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)
@@ -0,0 +1,204 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Input;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModTouchDevice : RateAdjustedBeatmapTestScene
{
[Resolved]
private SessionStatics statics { get; set; } = null!;
private ScoreAccessibleSoloPlayer currentPlayer = null!;
private readonly ManualClock manualClock = new ManualClock { Rate = 0 };
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);
[BackgroundDependencyLoader]
private void load()
{
Add(new TouchInputInterceptor());
}
public override void SetUpSteps()
{
AddStep("reset static", () => statics.SetValue(Static.TouchInputActive, false));
base.SetUpSteps();
}
[Test]
public void TestUserAlreadyHasTouchDeviceActive()
{
loadPlayer();
// it is presumed that a previous screen (i.e. song select) will set this up
AddStep("set up touchscreen user", () =>
{
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
statics.SetValue(Static.TouchInputActive, true);
});
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("touch circle", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchDuringBreak()
{
loadPlayer();
AddStep("seek to 2000", () => currentPlayer.GameplayClockContainer.Seek(2000));
AddUntilStep("wait until 2000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000));
AddUntilStep("wait until break entered", () => currentPlayer.IsBreakTime.Value);
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchMiss()
{
loadPlayer();
// ensure mouse is active (and that it's not suppressed due to touches in previous tests)
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddStep("seek to 200", () => currentPlayer.GameplayClockContainer.Seek(200));
AddUntilStep("wait until 200", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestIncompatibleModActive()
{
loadPlayer();
// this is only a veneer of enabling autopilot as having it actually active from the start is annoying to make happen
// given the tests' structure.
AddStep("enable autopilot", () => currentPlayer.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() });
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestSecondObjectTouched()
{
loadPlayer();
// ensure mouse is active (and that it's not suppressed due to touches in previous tests)
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("click circle", () =>
{
InputManager.MoveMouseTo(currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
AddStep("seek to 5000", () => currentPlayer.GameplayClockContainer.Seek(5000));
AddUntilStep("wait until 5000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
private void loadPlayer()
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuBeatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
},
},
Breaks =
{
new BreakPeriod(2000, 3000)
}
});
var p = new ScoreAccessibleSoloPlayer();
LoadScreen(currentPlayer = p);
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
}
private partial class ScoreAccessibleSoloPlayer : SoloPlayer
{
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleSoloPlayer()
: base(new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.710442985146793d, 206, "diffcalc-test")]
[TestCase(1.4386882251130073d, 45, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 2, "very-fast-slider")]
[TestCase(0.14102693012101306d, 1, "nan-slider")]
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
[TestCase(0.14102693012101306d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9742952703071666d, 206, "diffcalc-test")]
[TestCase(0.55071082800473514d, 2, "very-fast-slider")]
[TestCase(1.743180218215227d, 45, "zero-length-sliders")]
[TestCase(8.9742952703071666d, 239, "diffcalc-test")]
[TestCase(1.743180218215227d, 54, "zero-length-sliders")]
[TestCase(0.55071082800473514d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.710442985146793d, 239, "diffcalc-test")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -1,3 +1,4 @@
[General]
Version: latest
HitCircleOverlayAboveNumber: 0
HitCirclePrefix: display
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
Child = new MovingCursorInputManager { Child = createContent?.Invoke() }
Child = new MovingCursorInputManager { Child = createContent() }
});
});
@@ -94,16 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == OsuCursor.GetScaleForCircleSize(circleSize) * userScale);
AddStep("set user scale to 1", () => config.SetValue(OsuSetting.GameplayCursorSize, 1f));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == OsuCursor.GetScaleForCircleSize(circleSize));
AddStep("turn off autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, false));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == 1);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == 1);
AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale));
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == userScale);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.CursorScale.Value == userScale);
}
[Test]
@@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Hit Big Stream", () => SetContents(_ => testStream(2, true)));
AddStep("Hit Medium Stream", () => SetContents(_ => testStream(5, true)));
AddStep("Hit Small Stream", () => SetContents(_ => testStream(7, true)));
AddStep("High combo index", () => SetContents(_ => testSingle(2, true, comboIndex: 15)));
}
[Test]
@@ -66,12 +67,12 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
}
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null, int comboIndex = 0)
{
var playfield = new TestOsuPlayfield();
for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
playfield.Add(createSingle(circleSize, auto, t, positionOffset));
playfield.Add(createSingle(circleSize, auto, t, positionOffset, comboIndex: comboIndex));
return playfield;
}
@@ -84,14 +85,14 @@ namespace osu.Game.Rulesets.Osu.Tests
for (int i = 0; i <= 1000; i += 100)
{
playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset));
playfield.Add(createSingle(circleSize, auto, i, pos, hitOffset, i / 100 - 1));
pos.X += 50;
}
return playfield;
}
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0)
private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset, double hitOffset = 0, int comboIndex = 0)
{
positionOffset ??= Vector2.Zero;
@@ -99,6 +100,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000 + timeOffset,
Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value,
IndexInCurrentCombo = comboIndex,
};
circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
@@ -133,8 +133,11 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
public void TestSimpleInput()
public void TestSimpleInput([Values] bool disableMouseButtons)
{
// OsuSetting.MouseDisableButtons should not affect touch taps
AddStep($"{(disableMouseButtons ? "disable" : "enable")} mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, disableMouseButtons));
beginTouch(TouchSource.Touch1);
assertKeyCounter(1, 0);
@@ -468,7 +471,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestInputWhileMouseButtonsDisabled()
{
AddStep("Disable mouse buttons", () => config.SetValue(OsuSetting.MouseDisableButtons, true));
AddStep("Disable gameplay taps", () => config.SetValue(OsuSetting.TouchDisableGameplayTaps, true));
beginTouch(TouchSource.Touch1);
@@ -620,6 +623,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Release all touches", () =>
{
config.SetValue(OsuSetting.MouseDisableButtons, false);
config.SetValue(OsuSetting.TouchDisableGameplayTaps, false);
foreach (TouchSource source in InputManager.CurrentState.Touch.ActiveSources)
InputManager.EndTouch(new Touch(source, osuInputManager.ScreenSpaceDrawQuad.Centre));
});
@@ -1,38 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osu.Game.Tests.Gameplay;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneResumeOverlay : OsuManualInputManagerTestScene
{
private ManualOsuInputManager osuInputManager = null!;
private CursorContainer cursor = null!;
private ResumeOverlay resume = null!;
private bool resumeFired;
private OsuConfigManager localConfig = null!;
[Cached]
private GameplayState gameplayState;
public TestSceneResumeOverlay()
{
ManualOsuInputManager osuInputManager;
CursorContainer cursor;
ResumeOverlay resume;
gameplayState = TestGameplayState.Create(new OsuRuleset());
}
bool resumeFired = false;
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo)
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("cursor size", 0.1f, 2f, 1f, v => localConfig.SetValue(OsuSetting.GameplayCursorSize, v));
AddSliderStep("circle size", 0f, 10f, 0f, val =>
{
Children = new Drawable[]
{
cursor = new CursorContainer(),
resume = new OsuResumeOverlay
{
GameplayCursor = cursor
},
}
};
gameplayState.Beatmap.Difficulty.CircleSize = val;
SetUp();
});
resume.ResumeAction = () => resumeFired = true;
AddToggleStep("auto size", v => localConfig.SetValue(OsuSetting.AutoCursorSize, v));
}
[SetUp]
public void SetUp() => Schedule(loadContent);
[TestCase(1)]
[TestCase(0.5f)]
[TestCase(2)]
public void TestResume(float cursorSize)
{
AddStep($"set cursor size to {cursorSize}", () => localConfig.SetValue(OsuSetting.GameplayCursorSize, cursorSize));
AddStep("move mouse to center", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre));
AddStep("show", () => resume.Show());
@@ -41,11 +72,39 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("click", () => osuInputManager.GameClick());
AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible);
AddStep("move mouse back", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre));
AddStep("move mouse just out of range", () =>
{
var resumeOverlay = this.ChildrenOfType<OsuResumeOverlay>().Single();
var resumeOverlayCursor = resumeOverlay.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single();
Vector2 offset = resumeOverlay.ToScreenSpace(new Vector2(OsuCursor.SIZE / 2)) - resumeOverlay.ToScreenSpace(Vector2.Zero);
InputManager.MoveMouseTo(resumeOverlayCursor.ScreenSpaceDrawQuad.Centre - offset - new Vector2(1));
});
AddStep("click", () => osuInputManager.GameClick());
AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible);
AddStep("move mouse just within range", () =>
{
var resumeOverlay = this.ChildrenOfType<OsuResumeOverlay>().Single();
var resumeOverlayCursor = resumeOverlay.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single();
Vector2 offset = resumeOverlay.ToScreenSpace(new Vector2(OsuCursor.SIZE / 2)) - resumeOverlay.ToScreenSpace(Vector2.Zero);
InputManager.MoveMouseTo(resumeOverlayCursor.ScreenSpaceDrawQuad.Centre - offset + new Vector2(1));
});
AddStep("click", () => osuInputManager.GameClick());
AddAssert("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden);
}
private void loadContent()
{
Child = osuInputManager = new ManualOsuInputManager(new OsuRuleset().RulesetInfo) { Children = new Drawable[] { cursor = new CursorContainer(), resume = new OsuResumeOverlay { GameplayCursor = cursor }, } };
resumeFired = false;
resume.ResumeAction = () => resumeFired = true;
}
private partial class ManualOsuInputManager : OsuInputManager
{
public ManualOsuInputManager(RulesetInfo ruleset)
@@ -38,6 +38,42 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
[TestCase(30, 0)]
[TestCase(30, 1)]
[TestCase(40, 0)]
[TestCase(40, 1)]
[TestCase(50, 1)]
[TestCase(60, 1)]
[TestCase(70, 1)]
[TestCase(80, 1)]
[TestCase(80, 0)]
[TestCase(80, 10)]
[TestCase(90, 1)]
[Ignore("headless test doesn't run at high enough precision for this to always enter a tracking state in time.")]
public void TestVeryShortSliderMissHead(float sliderLength, int repeatCount)
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start - 10 },
new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 2000 },
}, new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
RepeatCount = repeatCount,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(sliderLength, 0),
}),
}, 240, 1);
AddAssert("Head judgement is first", () => judgementResults[0].HitObject is SliderHeadCircle);
AddAssert("Tail judgement is second last", () => judgementResults[^2].HitObject is SliderTailCircle);
AddAssert("Slider judgement is last", () => judgementResults[^1].HitObject is Slider);
}
// Making these too short causes breakage from frames not being processed fast enough.
// To keep things simple, these tests are crafted to always be >16ms length.
// If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit.
@@ -76,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Tests
assertAllMaxJudgements();
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
// If not, hitsounds will not play on time.
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
@@ -119,7 +157,9 @@ namespace osu.Game.Rulesets.Osu.Tests
if (hit)
assertAllMaxJudgements();
else
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
// If not, hitsounds will not play on time.
@@ -157,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
});
AddAssert("Tracking lost", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
}
/// <summary>
@@ -238,7 +278,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
});
AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailTracked);
assertHeadMissTailTracked();
}
/// <summary>
@@ -262,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking re-acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@@ -288,7 +328,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking lost", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
}
/// <summary>
@@ -310,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@@ -333,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
[Test]
@@ -347,7 +387,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@@ -372,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
});
AddAssert("Tracking acquired", assertMidSliderJudgements);
assertMidSliderJudgements();
}
/// <summary>
@@ -414,7 +454,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
});
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
assertMidSliderJudgementFail();
}
private void assertAllMaxJudgements()
@@ -425,11 +465,21 @@ namespace osu.Game.Rulesets.Osu.Tests
}, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult))));
}
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
private void assertHeadMissTailTracked()
{
AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False);
}
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
private void assertMidSliderJudgements()
{
AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
}
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
private void assertMidSliderJudgementFail()
{
AddAssert("Tracking lost", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.IgnoreMiss));
}
private void performTest(List<ReplayFrame> frames, Slider? slider = null, double? bpm = null, int? tickRate = null)
{
@@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -36,6 +37,12 @@ namespace osu.Game.Rulesets.Osu.Tests
AddSliderStep("Spin rate", 0.5, 5, 1, val => spinRate.Value = val);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Reset rate", () => spinRate.Value = 1);
}
[TestCase(true)]
[TestCase(false)]
public void TestVariousSpinners(bool autoplay)
@@ -46,6 +53,36 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"{term} Small", () => SetContents(_ => testSingle(7, autoplay)));
}
[Test]
public void TestSpinnerNoBonus()
{
AddStep("Set high spin rate", () => spinRate.Value = 5);
Spinner spinner;
AddStep("add spinner", () => SetContents(_ =>
{
spinner = new Spinner
{
StartTime = Time.Current,
EndTime = Time.Current + 750,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
}
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { OverallDifficulty = 0 });
return drawableSpinner = new TestDrawableSpinner(spinner, true, spinRate)
{
Anchor = Anchor.Centre,
Depth = depthIndex++,
Scale = new Vector2(0.75f)
};
}));
}
[Test]
public void TestSpinningSamplePitchShift()
{
@@ -153,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
base.Update();
if (auto)
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * spinRate.Value));
RotationTracker.AddRotation((float)Math.Min(180, Clock.ElapsedFrameTime * spinRate.Value));
}
}
}
@@ -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>
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
}
@@ -33,7 +33,8 @@ namespace osu.Game.Rulesets.Osu.Mods
typeof(ModNoFail),
typeof(ModAutoplay),
typeof(OsuModMagnetised),
typeof(OsuModRepel)
typeof(OsuModRepel),
typeof(ModTouchDevice)
};
public bool PerformFail() => false;
+36 -41
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
+1 -5
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;
}
}
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Burn the notes into your memory.";
//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
public override ModType Type => ModType.Fun;
@@ -1,18 +1,14 @@
// 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;
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTouchDevice : Mod
public class OsuModTouchDevice : ModTouchDevice
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray();
private float theta;
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
HeadCircle,
TailCircle,
repeatContainer,
Body,
};
@@ -49,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;
@@ -107,7 +108,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball,
slidingSample = new PausableSkinnableSound { Looping = true }
slidingSample = new PausableSkinnableSound
{
Looping = true,
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
}
});
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@@ -267,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()
@@ -4,7 +4,6 @@
#nullable disable
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
@@ -15,12 +14,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
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>();
@@ -60,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()
@@ -17,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
@@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => false;
public bool Tracking { get; set; }
public DrawableSliderRepeat()
: base(null)
{
@@ -85,8 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (HitObject.StartTime <= Time.Current)
ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
// shared implementation with DrawableSliderTick.
if (timeOffset >= 0)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void UpdateInitialTransforms()
@@ -4,6 +4,7 @@
#nullable disable
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -129,16 +130,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered)
return;
// Ensure the tail can only activate after all previous ticks already have.
// Ensure the tail can only activate after all previous ticks/repeats already have.
//
// This covers the edge case where the lenience may allow the tail to activate before
// the last tick, changing ordering of score/combo awarding.
if (DrawableSlider.NestedHitObjects.Count > 1 && !DrawableSlider.NestedHitObjects[^2].Judged)
var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
if (lastTick?.Judged == false)
return;
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
return;
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
// An actual tick miss should only occur if reaching the tick itself.
if (timeOffset >= SliderEventGenerator.TAIL_LENIENCY && Tracking)
if (Tracking)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else if (timeOffset > 0)
ApplyResult(r => r.Type = r.Judgement.MinResult);
@@ -75,8 +75,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// shared implementation with DrawableSliderRepeat.
if (timeOffset >= 0)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void UpdateInitialTransforms()
@@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@@ -45,6 +46,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
private SkinnableSound maxBonusSample;
/// <summary>
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
/// </summary>
@@ -106,8 +109,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
spinningSample = new PausableSkinnableSound
{
Volume = { Value = 0 },
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
Looping = true,
Frequency = { Value = spinning_sample_initial_frequency }
},
maxBonusSample = new SkinnableSound
{
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
}
});
@@ -127,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.OnFree();
spinningSample.ClearSamples();
maxBonusSample.ClearSamples();
}
protected override void LoadSamples()
@@ -135,6 +144,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
spinningSample.Samples = HitObject.CreateSpinningSamples().Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
maxBonusSample.Samples = new ISampleInfo[] { new SpinnerBonusMaxSampleInfo(HitObject.CreateHitSampleInfo()) };
}
private void updateSpinningSample(ValueChangedEvent<bool> tracking)
@@ -156,6 +167,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.StopAllSamples();
spinningSample?.Stop();
maxBonusSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@@ -274,6 +286,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()
@@ -312,10 +333,38 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit.
tick?.TriggerResult(true);
if (tick == null)
{
// we still want to play a sound. this will probably be a new sound in the future, but for now let's continue playing the bonus sound.
// TODO: this doesn't concurrency. i can't figure out how to make it concurrency. samples are bad and need a refactor.
maxBonusSample.Play();
}
else
tick.TriggerResult(true);
completedFullSpins.Value++;
}
}
public class SpinnerBonusMaxSampleInfo : HitSampleInfo
{
public override IEnumerable<string> LookupNames
{
get
{
foreach (string name in base.LookupNames)
yield return name;
foreach (string name in base.LookupNames)
yield return name.Replace("-max", string.Empty);
}
}
public SpinnerBonusMaxSampleInfo(HitSampleInfo sampleInfo)
: base("spinnerbonus-max", sampleInfo.Bank, sampleInfo.Suffix, sampleInfo.Volume)
{
}
}
}
}
@@ -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>
+26 -13
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,23 @@ 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;
if (TailCircle != null)
TailCircle.ClassicSliderBehaviour = value;
}
}
private bool classicSliderBehaviour;
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
{
@@ -191,7 +200,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
@@ -201,6 +209,7 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
ClassicSliderBehaviour = ClassicSliderBehaviour,
});
break;
@@ -210,7 +219,8 @@ namespace osu.Game.Rulesets.Osu.Objects
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
StackHeight = StackHeight,
ClassicSliderBehaviour = ClassicSliderBehaviour,
});
break;
@@ -221,7 +231,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 +275,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
// Final combo is provided by the slider itself - see logic in `DrawableSlider.CheckForResult()`
? new OsuJudgement()
// Final combo is provided by the tail circle - see `SliderTailCircle`
: new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
@@ -3,6 +3,8 @@
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
@@ -43,5 +45,12 @@ namespace osu.Game.Rulesets.Osu.Objects
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderEndJudgement();
public class SliderEndJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}
@@ -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();
}
}
@@ -1,10 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderRepeat : SliderEndCircle
@@ -13,12 +9,5 @@ namespace osu.Game.Rulesets.Osu.Objects
: base(slider)
{
}
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}
@@ -9,16 +9,28 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderTailCircle : SliderEndCircle
{
/// <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.
/// </summary>
public bool ClassicSliderBehaviour;
public SliderTailCircle(Slider slider)
: base(slider)
{
}
public override Judgement CreateJudgement() => new SliderTailJudgement();
public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new LegacyTailJudgement() : new TailJudgement();
public class SliderTailJudgement : OsuJudgement
public class LegacyTailJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.SmallTickHit;
}
public class TailJudgement : SliderEndJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
public override HitResult MinResult => HitResult.IgnoreMiss;
}
}
}
@@ -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);
}
@@ -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,
@@ -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;
@@ -1,35 +1,39 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public partial class LegacySliderBall : CompositeDrawable
{
private readonly Drawable animationContent;
private readonly ISkin skin;
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
public Color4 BallColour => animationContent.Colour;
private Sprite layerNd = null!;
private Sprite layerSpec = null!;
public LegacySliderBall(Drawable animationContent, ISkin skin)
private TextureAnimation ballAnimation = null!;
private Texture[] ballTextures = null!;
public Color4 BallColour => ballAnimation.Colour;
public LegacySliderBall(ISkin skin)
{
this.animationContent = animationContent;
this.skin = skin;
AutoSizeAxes = Axes.Both;
@@ -38,30 +42,39 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[BackgroundDependencyLoader]
private void load()
{
var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
Vector2 maxSize = OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE;
InternalChildren = new[]
var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
ballTextures = skin.GetTextures("sliderb", default, default, true, "", maxSize, out _);
InternalChildren = new Drawable[]
{
layerNd = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE),
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(maxSize),
Colour = new Color4(5, 5, 5, 255),
},
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
ballAnimation = new LegacySkinExtensions.SkinnableTextureAnimation
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}), ballColour),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = ballColour,
},
layerSpec = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE),
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(maxSize),
Blending = BlendingParameters.Additive,
},
};
if (parentObject != null)
parentObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(parentObject);
}
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
@@ -78,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (skin.GetConfig<SkinConfiguration.LegacySetting, bool>(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
{
accentColour.BindTo(parentObject.AccentColour);
accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
accentColour.BindValueChanged(a => ballAnimation.Colour = a.NewValue, true);
}
}
}
@@ -94,6 +107,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
layerSpec.Rotation = -appliedRotation;
}
private void onHitObjectApplied(DrawableHitObject? drawableObject = null)
{
double frameDelay;
if (drawableObject?.HitObject != null)
{
DrawableSlider drawableSlider = (DrawableSlider)drawableObject;
frameDelay = Math.Max(
0.15 / drawableSlider.HitObject.Velocity * LegacySkinExtensions.SIXTY_FRAME_TIME,
LegacySkinExtensions.SIXTY_FRAME_TIME);
}
else
frameDelay = LegacySkinExtensions.SIXTY_FRAME_TIME;
ballAnimation.ClearFrames();
foreach (var texture in ballTextures)
ballAnimation.AddFrame(texture, frameDelay);
}
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
@@ -114,7 +147,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
base.Dispose(isDisposing);
if (parentObject != null)
{
parentObject.HitObjectApplied -= onHitObjectApplied;
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}
}
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.Centre,
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 299,
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
},
spmBackground = new Sprite
{
Anchor = Anchor.TopCentre,
@@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.TopRight,
Scale = new Vector2(SPRITE_SCALE * 0.9f),
Position = new Vector2(80, 448 + spm_hide_offset),
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
},
}
});
}
@@ -59,13 +59,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case OsuSkinComponents.SliderBall:
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
// todo: slider ball has a custom frame delay based on velocity
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null)
return new LegacySliderBall(sliderBallContent, this);
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
return new LegacySliderBall(this);
return null;
@@ -150,10 +145,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
const float hitcircle_text_scale = 0.8f;
return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale)
return new LegacySpriteText(LegacyFont.HitCircle)
{
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(hitcircle_text_scale),
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
};
case OsuSkinComponents.SpinnerBody:
+63 -12
View File
@@ -4,12 +4,16 @@
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -18,30 +22,42 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public partial class OsuCursor : SkinReloadableDrawable
{
private const float size = 28;
public const float SIZE = 28;
private const float pressed_scale = 1.2f;
private const float released_scale = 1f;
private bool cursorExpand;
private SkinnableDrawable cursorSprite;
private Container cursorScaleContainer = null!;
private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite;
public IBindable<float> CursorScale => cursorScale;
private readonly Bindable<float> cursorScale = new BindableFloat(1);
private Bindable<float> userCursorScale = null!;
private Bindable<bool> autoCursorScale = null!;
[Resolved(canBeNull: true)]
private GameplayState state { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
public OsuCursor()
{
Origin = Anchor.Centre;
Size = new Vector2(size);
}
protected override void SkinChanged(ISkinSource skin)
{
cursorExpand = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorExpand)?.Value ?? true;
Size = new Vector2(SIZE);
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Container
InternalChild = cursorScaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
@@ -52,10 +68,39 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Anchor = Anchor.Centre,
}
};
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
userCursorScale.ValueChanged += _ => calculateCursorScale();
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => calculateCursorScale();
cursorScale.BindValueChanged(e => cursorScaleContainer.Scale = new Vector2(e.NewValue), true);
}
private const float pressed_scale = 1.2f;
private const float released_scale = 1f;
protected override void LoadComplete()
{
base.LoadComplete();
calculateCursorScale();
}
private void calculateCursorScale()
{
float scale = userCursorScale.Value;
if (autoCursorScale.Value && state != null)
{
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
}
cursorScale.Value = scale;
}
protected override void SkinChanged(ISkinSource skin)
{
cursorExpand = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorExpand)?.Value ?? true;
}
public void Expand()
{
@@ -66,6 +111,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad);
/// <summary>
/// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
/// </summary>
public static float GetScaleForCircleSize(float circleSize) =>
1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
private partial class DefaultCursor : OsuCursorSprite
{
public DefaultCursor()
@@ -83,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = size / 6,
BorderThickness = SIZE / 6,
BorderColour = Color4.White,
EdgeEffect = new EdgeEffectParameters
{
@@ -105,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = size / 3,
BorderThickness = SIZE / 3,
BorderColour = Color4.White.Opacity(0.5f),
Children = new Drawable[]
{
@@ -11,11 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
@@ -23,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public partial class OsuCursorContainer : GameplayCursorContainer, IKeyBindingHandler<OsuAction>
{
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
protected override Drawable CreateCursor() => new OsuCursor();
protected override Container<Drawable> Content => fadeContainer;
@@ -33,13 +32,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly Drawable cursorTrail;
public IBindable<float> CursorScale => cursorScale;
private readonly Bindable<float> cursorScale = new BindableFloat(1);
private Bindable<float> userCursorScale;
private Bindable<bool> autoCursorScale;
private readonly CursorRippleVisualiser rippleVisualiser;
public OsuCursorContainer()
@@ -56,12 +48,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
};
}
[Resolved(canBeNull: true)]
private GameplayState state { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager rulesetConfig)
{
@@ -74,46 +60,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
showTrail.BindValueChanged(v => cursorTrail.FadeTo(v.NewValue ? 1 : 0, 200), true);
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
userCursorScale.ValueChanged += _ => calculateScale();
autoCursorScale = config.GetBindable<bool>(OsuSetting.AutoCursorSize);
autoCursorScale.ValueChanged += _ => calculateScale();
CursorScale.BindValueChanged(e =>
ActiveCursor.CursorScale.BindValueChanged(e =>
{
var newScale = new Vector2(e.NewValue);
ActiveCursor.Scale = newScale;
rippleVisualiser.CursorScale = newScale;
cursorTrail.Scale = newScale;
}, true);
calculateScale();
}
/// <summary>
/// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
/// </summary>
public static float GetScaleForCircleSize(float circleSize) =>
1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
private void calculateScale()
{
float scale = userCursorScale.Value;
if (autoCursorScale.Value && state != null)
{
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize);
}
cursorScale.Value = scale;
var newScale = new Vector2(scale);
ActiveCursor.ScaleTo(newScale, 400, Easing.OutQuint);
cursorTrail.Scale = newScale;
}
private int downCount;
@@ -121,9 +74,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private void updateExpandedState()
{
if (downCount > 0)
(ActiveCursor as OsuCursor)?.Expand();
ActiveCursor.Expand();
else
(ActiveCursor as OsuCursor)?.Contract();
ActiveCursor.Contract();
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
@@ -160,13 +113,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override void PopIn()
{
fadeContainer.FadeTo(1, 300, Easing.OutQuint);
ActiveCursor.ScaleTo(CursorScale.Value, 400, Easing.OutQuint);
ActiveCursor.ScaleTo(1f, 400, Easing.OutQuint);
}
protected override void PopOut()
{
fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint);
ActiveCursor.ScaleTo(CursorScale.Value * 0.8f, 450, Easing.OutQuint);
ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint);
}
private partial class DefaultCursorTrail : CursorTrail
+2 -10
View File
@@ -5,7 +5,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -14,7 +13,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.UI
@@ -25,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.UI
private OsuClickToResumeCursor clickToResumeCursor;
private OsuCursorContainer localCursorContainer;
private IBindable<float> localCursorScale;
public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
@@ -49,13 +46,7 @@ namespace osu.Game.Rulesets.Osu.UI
clickToResumeCursor.Appear();
if (localCursorContainer == null)
{
Add(localCursorContainer = new OsuCursorContainer());
localCursorScale = new BindableFloat();
localCursorScale.BindTo(localCursorContainer.CursorScale);
localCursorScale.BindValueChanged(scale => cursorScaleContainer.Scale = new Vector2(scale.NewValue), true);
}
}
protected override void PopOut()
@@ -98,7 +89,8 @@ namespace osu.Game.Rulesets.Osu.UI
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
if (!IsHovered) return false;
if (!IsHovered)
return false;
this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly OsuInputManager osuInputManager;
private Bindable<bool> mouseDisabled = null!;
private Bindable<bool> tapsDisabled = null!;
public OsuTouchInputMapper(OsuInputManager inputManager)
{
@@ -43,9 +43,7 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
// The mouse button disable setting affects touch. It's a bit weird.
// This is mostly just doing the same as what is done in RulesetInputManager to match behaviour.
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
tapsDisabled = config.GetBindable<bool>(OsuSetting.TouchDisableGameplayTaps);
}
// Required to handle touches outside of the playfield when screen scaling is enabled.
@@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.UI
: OsuAction.LeftButton;
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !tapsDisabled.Value && trackedTouches.All(t => t.Action != action);
// If we can actually accept as an action, check whether this tap was on a circle's receptor.
// This case gets special handling to allow for empty-space stream tapping.
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects.Drawables;
@@ -14,53 +15,54 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public partial class ArgonBarLine : CompositeDrawable
{
private Container majorEdgeContainer = null!;
private Bindable<bool> major = null!;
private Box mainLine = null!;
private Drawable topAnchor = null!;
private Drawable bottomAnchor = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
RelativeSizeAxes = Axes.Both;
const float line_offset = 8;
var majorPieceSize = new Vector2(6, 20);
// Avoid flickering due to no anti-aliasing of boxes by default.
var edgeSmoothness = new Vector2(0.3f);
InternalChildren = new Drawable[]
AddInternal(mainLine = new Box
{
line = new Box
{
RelativeSizeAxes = Axes.Both,
EdgeSmoothness = new Vector2(0.5f, 0),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
majorEdgeContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new Circle
{
Name = "Top line",
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
Size = majorPieceSize,
Y = -line_offset,
},
new Circle
{
Name = "Bottom line",
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Size = majorPieceSize,
Y = line_offset,
},
}
}
};
Name = "Bar line",
EdgeSmoothness = edgeSmoothness,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
});
const float major_extension = 10;
AddInternal(topAnchor = new Box
{
Name = "Top anchor",
EdgeSmoothness = edgeSmoothness,
Blending = BlendingParameters.Additive,
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
Height = major_extension,
RelativeSizeAxes = Axes.X,
Colour = ColourInfo.GradientVertical(Colour4.Transparent, Colour4.White),
});
AddInternal(bottomAnchor = new Box
{
Name = "Bottom anchor",
EdgeSmoothness = edgeSmoothness,
Blending = BlendingParameters.Additive,
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Height = major_extension,
RelativeSizeAxes = Axes.X,
Colour = ColourInfo.GradientVertical(Colour4.White, Colour4.Transparent),
});
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
}
@@ -71,13 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
major.BindValueChanged(updateMajor, true);
}
private Box line = null!;
private void updateMajor(ValueChangedEvent<bool> major)
{
line.Alpha = major.NewValue ? 1f : 0.5f;
line.Width = major.NewValue ? 1 : 0.5f;
majorEdgeContainer.Alpha = major.NewValue ? 1 : 0;
mainLine.Alpha = major.NewValue ? 1f : 0.5f;
topAnchor.Alpha = bottomAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0;
}
}
}
@@ -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;
@@ -87,6 +87,34 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestDecodeLegacyOnlineID()
{
var decoder = new TestLegacyScoreDecoder();
using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-legacy-online-id.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(-1));
Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(255));
}
}
[Test]
public void TestDecodeNewOnlineID()
{
var decoder = new TestLegacyScoreDecoder();
using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258));
Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1));
}
}
[TestCase(3, true)]
[TestCase(6, false)]
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
@@ -287,5 +287,26 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration));
}
}
[Test]
public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds()
{
var decoder = new LegacyStoryboardDecoder();
using var resStream = TestResources.OpenResource("video-background-events-ignored.osb");
using var stream = new LineBufferedReader(resStream);
var storyboard = decoder.Decode(stream);
Assert.Multiple(() =>
{
Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1));
Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf<StoryboardVideo>());
Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678));
Assert.That(storyboard.EarliestEventTime, Is.Null);
Assert.That(storyboard.LatestEventTime, Is.Null);
});
}
}
}
@@ -108,6 +108,28 @@ namespace osu.Game.Tests.Gameplay
AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f));
}
[Test]
public void TestStopUsingBeatmapClock()
{
ClockBackedTestWorkingBeatmap working = null;
MasterGameplayClockContainer gameplayClockContainer = null;
BindableDouble frequencyAdjustment = new BindableDouble(2);
AddStep("create container", () =>
{
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
gameplayClockContainer.Reset(startClock: true);
});
AddStep("apply frequency adjustment", () => gameplayClockContainer.AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment));
AddAssert("track frequency changed", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(2));
AddStep("stop using beatmap clock", () => gameplayClockContainer.StopUsingBeatmapClock());
AddAssert("frequency adjustment unapplied", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(1));
}
protected override void Dispose(bool isDisposing)
{
localConfig?.Dispose();
+4
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]
+3 -3
View File
@@ -147,11 +147,11 @@ namespace osu.Game.Tests.Mods
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
// system mod not applicable in lazer.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
new Mod[] { new OsuModHidden(), new ModScoreV2() },
new[] { typeof(ModScoreV2) }
},
// multi mod.
new object[]
@@ -0,0 +1,5 @@
osu file format v14
[Events]
0,-1234,"BG.jpg",0,0
Video,-5678,"Video.avi",0,0
@@ -11,9 +11,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestFixture]
public class HitResultTest
{
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })]
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })]
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })]
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })]
public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults)
+113
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()
{
@@ -54,7 +54,11 @@ namespace osu.Game.Tests.Skins
// Covers key counters
"Archives/modified-argon-pro-20230618.osk",
// Covers "Argon" health display
"Archives/modified-argon-pro-20231001.osk"
"Archives/modified-argon-pro-20231001.osk",
// Covers player name text component.
"Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk",
};
/// <summary>
@@ -47,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
CanScaleX = true,
CanScaleY = true,
CanScaleDiagonally = true,
CanFlipX = true,
CanFlipY = true,
@@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
AddStep("dismiss prompt", () =>
{
@@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
@@ -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);
});
@@ -47,12 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay
};
});
AddSliderStep("Width", 0, 1f, 1f, val =>
{
if (healthDisplay.IsNotNull())
healthDisplay.BarLength.Value = val;
});
AddSliderStep("Height", 0, 64, 0, val =>
{
if (healthDisplay.IsNotNull())
@@ -31,19 +31,66 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(72.7f),
Children = new KeyCounterDisplay[]
Spacing = new Vector2(20),
Children = new Drawable[]
{
new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Scale = new Vector2(1, -1)
},
new ArgonKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
new ArgonKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
Scale = new Vector2(1, -1)
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Spacing = new Vector2(20),
Children = new Drawable[]
{
new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Rotation = -90,
},
new DefaultKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Rotation = 90,
},
new ArgonKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Rotation = -90,
},
new ArgonKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Rotation = 90,
},
}
},
}
}
};
@@ -77,8 +124,15 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Disable counting", () => controller.IsCounting.Value = false);
addPressKeyStep();
AddAssert($"Check {testKey} count has not changed", () => testTrigger.ActivationCount.Value == 2);
AddStep("Enable counting", () => controller.IsCounting.Value = true);
addPressKeyStep(100);
addPressKeyStep(1000);
void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
void addPressKeyStep(int repeat = 1) => AddStep($"Press {testKey} key {repeat} times", () =>
{
for (int i = 0; i < repeat; i++)
InputManager.Key(testKey);
});
}
}
}
@@ -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;
};
});
@@ -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);
}
@@ -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.
@@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
OnlineID = hasOnlineId ? online_score_id : 0,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(),
Hash = replayAvailable ? "online" : string.Empty,
HasOnlineReplay = replayAvailable,
User = new APIUser
{
Id = 39828,
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -96,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Begin drag top left", () =>
{
InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4));
InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4, box1.ScreenSpaceDrawQuad.Height / 8));
InputManager.PressButton(MouseButton.Left);
});
@@ -146,8 +147,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("Add big black box", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left);
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First(b => b.ChildrenOfType<BigBlackBox>().FirstOrDefault() != null).TriggerClick();
});
AddStep("store box", () =>
@@ -243,7 +243,9 @@ namespace osu.Game.Tests.Visual.Gameplay
void revertAndCheckUnchanged()
{
AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue));
AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
AddAssert("Current state is same as default",
() => Encoding.UTF8.GetString(defaultState),
() => Is.EqualTo(Encoding.UTF8.GetString(changeHandler.GetCurrentState())));
}
}
@@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateArgonImplementation() => new ArgonAccuracyCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter();
@@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateArgonImplementation() => new ArgonComboCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter();
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 1f };
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };
@@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;
protected override Drawable CreateArgonImplementation() => new ArgonScoreCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

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