Compare commits
435 Commits
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace osu.Android
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.DisableMouseButtons,
|
||||
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
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
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||