1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 07:07:45 +08:00

Merge branch 'ppy:master' into speed-changes

This commit is contained in:
KermitNuggies 2024-08-24 23:22:25 +12:00 committed by GitHub
commit 794ac6f9e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
365 changed files with 9100 additions and 3258 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2024.517.0",
"version": "2024.802.0",
"commands": [
"localisation"
]

View File

@ -64,10 +64,11 @@ jobs:
matrix:
os:
- { prettyname: Windows, fullname: windows-latest }
- { prettyname: macOS, fullname: macos-latest }
# macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
# - { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
@ -120,9 +121,7 @@ jobs:
build-only-ios:
name: Build only (iOS)
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
runs-on: macos-13
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
@ -136,8 +135,5 @@ jobs:
- name: Install .NET Workloads
run: dotnet workload install maui-ios
- name: Select Xcode 15.2
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
- name: Build
run: dotnet build -c Debug osu.iOS

View File

@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.

View File

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

View File

@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(-100, 100);
addVertexCheckStep(3, 1, times[0], positions[0]);
addDragEndStep();
}
[Test]
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMouseMoveStep(times[2] - 50, positions[2] - 50);
addVertexCheckStep(4, 1, times[1] - 50, positions[1] - 50);
addVertexCheckStep(4, 2, times[2] - 50, positions[2] - 50);
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
addDragEndStep();
}
[Test]
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.SliderVelocityMultiplierBindable.IsDefault);
addDragEndStep();
}
[Test]
@ -129,6 +134,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("scroll playfield", () => manualClock.CurrentTime += 200);
AddMouseMoveStep(times[1] + 200, positions[1] + 100);
addVertexCheckStep(2, 1, times[1] + 200, positions[1] + 100);
addDragEndStep();
}
[Test]
@ -161,18 +167,18 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addAddVertexSteps(500, 150);
addVertexCheckStep(3, 1, 500, 150);
addAddVertexSteps(90, 200);
addVertexCheckStep(4, 1, times[0], positions[0]);
addAddVertexSteps(160, 200);
addVertexCheckStep(4, 1, 160, 200);
addAddVertexSteps(750, 180);
addVertexCheckStep(5, 4, 750, 180);
addVertexCheckStep(5, 4, 800, 160);
AddAssert("duration is changed", () => Precision.AlmostEquals(hitObject.Duration, 800 - times[0], 1e-3));
}
[Test]
public void TestDeleteVertex()
{
double[] times = { 100, 300, 500 };
double[] times = { 100, 300, 400 };
float[] positions = { 100, 200, 150 };
addBlueprintStep(times, positions);
@ -265,7 +271,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddStep("delete vertex", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
}

View File

@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Skinning;
@ -19,20 +18,17 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin)
public void TestLegacyHUDComboCounterNotExistent([Values] bool withModifiedSkin)
{
if (withModifiedSkin)
{
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("update target", () => Player.ChildrenOfType<SkinnableContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit());
CreateTest();
}
AddAssert("legacy HUD combo counter hidden", () =>
{
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
});
AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyDefaultComboCounter>().Any());
}
}
}

View File

@ -248,7 +248,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
AddAssert("correct hit lighting colour",
() => catcher.ChildrenOfType<HitExplosion>().First()?.Entry?.ObjectColour == this.ChildrenOfType<DrawableCatchHitObject>().First().AccentColour.Value);
}
[Test]
@ -259,6 +260,16 @@ namespace osu.Game.Rulesets.Catch.Tests
AddAssert("no hit lighting", () => !catcher.ChildrenOfType<HitExplosion>().Any());
}
[Test]
public void TestAllExplodedObjectsAtUniquePositions()
{
AddStep("catch normal fruit", () => attemptCatch(new Fruit()));
AddStep("catch normal fruit", () => attemptCatch(new Fruit { IndexInBeatmap = 2, LastInCombo = true }));
AddAssert("two fruit at distinct x coordinates",
() => this.ChildrenOfType<CaughtFruit>().Select(f => f.DrawPosition.X).Distinct(),
() => Has.Exactly(2).Items);
}
private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count);
private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state);

View File

@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
public class CatchSkinComponentLookup : GameplaySkinComponentLookup<CatchSkinComponents>
public class CatchSkinComponentLookup : SkinComponentLookup<CatchSkinComponents>
{
public CatchSkinComponentLookup(CatchSkinComponents component)
: base(component)

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
@ -42,6 +43,9 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
[Resolved]
private IBeatSnapProvider? beatSnapProvider { get; set; }
[Resolved]
protected EditorBeatmap? EditorBeatmap { get; private set; }
protected EditablePath(Func<float, double> positionToTime)
{
PositionToTime = positionToTime;
@ -103,15 +107,23 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
//
// The value is clamped here by the bindable min and max values.
// In case the required velocity is too large, the path is not preserved.
double previousVelocity = svBindable.Value;
svBindable.Value = Math.Ceiling(requiredVelocity / svToVelocityFactor);
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, hitObject.Velocity);
// adjust velocity locally, so that once the SV change is applied by applying defaults
// (triggered by `EditorBeatmap.Update()` call at end of method),
// it results in the outcome desired by the user.
double relativeChange = svBindable.Value / previousVelocity;
double localVelocity = hitObject.Velocity * relativeChange;
path.ConvertToSliderPath(hitObject.Path, hitObject.LegacyConvertedY, localVelocity);
if (beatSnapProvider == null) return;
double endTime = hitObject.StartTime + path.Duration;
double snappedEndTime = beatSnapProvider.SnapTime(endTime, hitObject.StartTime);
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * hitObject.Velocity;
hitObject.Path.ExpectedDistance.Value = (snappedEndTime - hitObject.StartTime) * localVelocity;
EditorBeatmap?.Update(hitObject);
}
public Vector2 ToRelativePosition(Vector2 screenSpacePosition)

View File

@ -4,12 +4,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Edit;
using osu.Game.Rulesets.Catch.Objects;
using osuTK;
using osuTK.Input;
@ -19,22 +18,27 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public MenuItem[] ContextMenuItems => getContextMenuItems().ToArray();
private readonly JuiceStream juiceStream;
// To handle when the editor is scrolled while dragging.
private Vector2 dragStartPosition;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
public SelectionEditablePath(Func<float, double> positionToTime)
public SelectionEditablePath(JuiceStream juiceStream, Func<float, double> positionToTime)
: base(positionToTime)
{
this.juiceStream = juiceStream;
}
public void AddVertex(Vector2 relativePosition)
{
EditorBeatmap?.BeginChange();
double time = Math.Max(0, PositionToTime(relativePosition.Y));
int index = AddVertex(time, relativePosition.X);
UpdateHitObjectFromPath(juiceStream);
selectOnly(index);
EditorBeatmap?.EndChange();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => InternalChildren.Any(d => d.ReceivePositionalInputAt(screenSpacePos));
@ -45,9 +49,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
if (index == -1 || VertexStates[index].IsFixed)
return false;
if (e.Button == MouseButton.Left && e.ShiftPressed)
if (e.Button == MouseButton.Right && e.ShiftPressed)
{
EditorBeatmap?.BeginChange();
RemoveVertex(index);
UpdateHitObjectFromPath(juiceStream);
EditorBeatmap?.EndChange();
return true;
}
@ -74,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
for (int i = 0; i < VertexCount; i++)
VertexStates[i].VertexBeforeChange = Vertices[i];
changeHandler?.BeginChange();
EditorBeatmap?.BeginChange();
return true;
}
@ -88,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
protected override void OnDragEnd(DragEndEvent e)
{
changeHandler?.EndChange();
EditorBeatmap?.EndChange();
}
private int getMouseTargetVertex(Vector2 screenSpacePosition)
@ -118,11 +126,17 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
private void deleteSelectedVertices()
{
EditorBeatmap?.BeginChange();
for (int i = VertexCount - 1; i >= 0; i--)
{
if (VertexStates[i].IsSelected)
RemoveVertex(i);
}
UpdateHitObjectFromPath(juiceStream);
EditorBeatmap?.EndChange();
}
}
}

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public partial class VertexPiece : Circle
{
private VertexState state = new VertexState();
[Resolved]
private OsuColour osuColour { get; set; } = null!;
@ -24,7 +27,32 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateFrom(VertexState state)
{
Colour = state.IsSelected ? osuColour.Yellow.Lighten(1) : osuColour.Yellow;
this.state = state;
updateMarkerDisplay();
}
protected override bool OnHover(HoverEvent e)
{
updateMarkerDisplay();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateMarkerDisplay();
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>
private void updateMarkerDisplay()
{
var colour = osuColour.Yellow;
if (IsHovered || state.IsSelected)
colour = colour.Lighten(1);
Colour = colour;
Alpha = state.IsFixed ? 0.5f : 1;
}
}

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer(),
editablePath = new SelectionEditablePath(positionToTime)
editablePath = new SelectionEditablePath(hitObject, positionToTime)
};
}

View File

@ -15,7 +15,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Objects
{
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation
public abstract class CatchHitObject : HitObject, IHasPosition, IHasComboInformation, IHasTimePreempt
{
public const float OBJECT_RADIUS = 64;

View File

@ -21,11 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public Bindable<Color4> AccentColour { get; } = new Bindable<Color4>();
public Bindable<bool> HyperDash { get; } = new Bindable<bool>();
public Bindable<int> IndexInBeatmap { get; } = new Bindable<int>();
public Vector2 DisplayPosition => DrawPosition;
public Vector2 DisplaySize => Size * Scale;
public float DisplayRotation => Rotation;
public double DisplayStartTime => HitObject.StartTime;
/// <summary>
@ -44,19 +42,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2);
}
/// <summary>
/// Copies the hit object visual state from another <see cref="IHasCatchObjectState"/> object.
/// </summary>
public virtual void CopyStateFrom(IHasCatchObjectState objectState)
{
HitObject = objectState.HitObject;
Scale = Vector2.Divide(objectState.DisplaySize, Size);
Rotation = objectState.DisplayRotation;
AccentColour.Value = objectState.AccentColour.Value;
HyperDash.Value = objectState.HyperDash.Value;
IndexInBeatmap.Value = objectState.IndexInBeatmap.Value;
}
protected override void FreeAfterUse()
{
ClearTransforms();
@ -64,5 +49,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
base.FreeAfterUse();
}
public void RestoreState(CatchObjectState state)
{
HitObject = state.HitObject;
AccentColour.Value = state.AccentColour;
HyperDash.Value = state.HyperDash;
IndexInBeatmap.Value = state.IndexInBeatmap;
Position = state.DisplayPosition;
Scale = Vector2.Divide(state.DisplaySize, Size);
Rotation = state.DisplayRotation;
}
}
}

View File

@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
/// </summary>
protected readonly Container ScalingContainer;
public Vector2 DisplayPosition => DrawPosition;
public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale;
public float DisplayRotation => ScalingContainer.Rotation;
@ -95,5 +97,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
base.OnFree();
}
public void RestoreState(CatchObjectState state) => throw new NotSupportedException("Cannot restore state into a drawable catch hitobject.");
}
}

View File

@ -13,17 +13,35 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public interface IHasCatchObjectState
{
PalpableCatchHitObject HitObject { get; }
double DisplayStartTime { get; }
Bindable<Color4> AccentColour { get; }
Bindable<bool> HyperDash { get; }
Bindable<int> IndexInBeatmap { get; }
double DisplayStartTime { get; }
Vector2 DisplayPosition { get; }
Vector2 DisplaySize { get; }
float DisplayRotation { get; }
void RestoreState(CatchObjectState state);
}
public static class HasCatchObjectStateExtensions
{
public static CatchObjectState SaveState(this IHasCatchObjectState target) => new CatchObjectState(
target.HitObject,
target.AccentColour.Value,
target.HyperDash.Value,
target.IndexInBeatmap.Value,
target.DisplayPosition,
target.DisplaySize,
target.DisplayRotation);
}
public readonly record struct CatchObjectState(
PalpableCatchHitObject HitObject,
Color4 AccentColour,
bool HyperDash,
int IndexInBeatmap,
Vector2 DisplayPosition,
Vector2 DisplaySize,
float DisplayRotation);
}

View File

@ -4,8 +4,8 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
@ -28,76 +28,94 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is SkinComponentsContainerLookup containerLookup)
switch (lookup)
{
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container;
if (providesComboCounter && components != null)
{
// catch may provide its own combo counter; hide the default.
// todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>())
legacyComboCounter.HiddenByRulesetImplementation = false;
}
return components;
}
}
if (lookup is CatchSkinComponentLookup catchSkinComponent)
{
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
case GlobalSkinnableContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
// Our own ruleset components default.
switch (containerLookup.Lookup)
{
case GlobalSkinnableContainers.MainHUDComponents:
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
return null;
if (keyCounter != null)
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
})
{
Children = new Drawable[]
{
new LegacyKeyCounterDisplay(),
}
};
}
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece();
return null;
return null;
case CatchSkinComponentLookup catchSkinComponent:
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
case CatchSkinComponents.Catcher:
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
return null;
if (version < 2.3m)
{
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
return null;
return null;
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece();
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter();
return null;
return null;
case CatchSkinComponents.Catcher:
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
if (version < 2.3m)
{
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
return null;
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
default:
throw new UnsupportedSkinComponentException(lookup);
}
return null;
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter();
return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
}
return base.GetDrawableComponent(lookup);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Buffers;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
@ -362,7 +363,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (caughtObject == null) return;
caughtObject.CopyStateFrom(drawableObject);
caughtObject.RestoreState(drawableObject.SaveState());
caughtObject.Anchor = Anchor.TopCentre;
caughtObject.Position = position;
caughtObject.Scale *= caught_fruit_scale_adjust;
@ -411,41 +412,50 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
private CaughtObject getDroppedObject(CaughtObject caughtObject)
private CaughtObject getDroppedObject(CatchObjectState state)
{
var droppedObject = getCaughtObject(caughtObject.HitObject);
var droppedObject = getCaughtObject(state.HitObject);
Debug.Assert(droppedObject != null);
droppedObject.CopyStateFrom(caughtObject);
droppedObject.RestoreState(state);
droppedObject.Anchor = Anchor.TopLeft;
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget);
droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(state.DisplayPosition, droppedObjectTarget);
return droppedObject;
}
private void clearPlate(DroppedObjectAnimation animation)
{
var caughtObjects = caughtObjectContainer.Children.ToArray();
int caughtCount = caughtObjectContainer.Children.Count;
CatchObjectState[] states = ArrayPool<CatchObjectState>.Shared.Rent(caughtCount);
caughtObjectContainer.Clear(false);
try
{
for (int i = 0; i < caughtCount; i++)
states[i] = caughtObjectContainer.Children[i].SaveState();
// Use the already returned PoolableDrawables for new objects
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
caughtObjectContainer.Clear(false);
droppedObjectTarget.AddRange(droppedObjects);
foreach (var droppedObject in droppedObjects)
applyDropAnimation(droppedObject, animation);
for (int i = 0; i < caughtCount; i++)
{
CaughtObject obj = getDroppedObject(states[i]);
droppedObjectTarget.Add(obj);
applyDropAnimation(obj, animation);
}
}
finally
{
ArrayPool<CatchObjectState>.Shared.Return(states);
}
}
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
{
CatchObjectState state = caughtObject.SaveState();
caughtObjectContainer.Remove(caughtObject, false);
var droppedObject = getDroppedObject(caughtObject);
var droppedObject = getDroppedObject(state);
droppedObjectTarget.Add(droppedObject);
applyDropAnimation(droppedObject, animation);
}

View File

@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI
if (Catcher.Dashing || Catcher.HyperDashing)
{
double generationInterval = Catcher.HyperDashing ? 25 : 50;
const double trail_generation_interval = 16;
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
}

View File

@ -12,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Tests
{
[TestCase(ManiaAction.Key1)]
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
[TestCase(ManiaAction.Special1)]
[TestCase(ManiaAction.Key8)]
[TestCase(ManiaAction.Key5)]
[TestCase(ManiaAction.Key9)]
public void TestEncodeDecodeSingleStage(params ManiaAction[] actions)
{
var beatmap = new ManiaBeatmap(new StageDefinition(9));
@ -29,11 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(ManiaAction.Key1)]
[TestCase(ManiaAction.Key1, ManiaAction.Key2)]
[TestCase(ManiaAction.Special1)]
[TestCase(ManiaAction.Special2)]
[TestCase(ManiaAction.Special1, ManiaAction.Special2)]
[TestCase(ManiaAction.Special1, ManiaAction.Key5)]
[TestCase(ManiaAction.Key3)]
[TestCase(ManiaAction.Key8)]
[TestCase(ManiaAction.Key3, ManiaAction.Key8)]
[TestCase(ManiaAction.Key3, ManiaAction.Key6)]
[TestCase(ManiaAction.Key10)]
public void TestEncodeDecodeDualStage(params ManiaAction[] actions)
{
var beatmap = new ManiaBeatmap(new StageDefinition(5));

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
{
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo();
[Cached]
private readonly StageDefinition stage = new StageDefinition(4);
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
protected ManiaSkinnableTestScene()
{
scrollingInfo.Direction.Value = ScrollingDirection.Down;
ScrollingInfo.Direction.Value = ScrollingDirection.Down;
Add(new Box
{
@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Test]
public void TestScrollingDown()
{
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
}
[Test]
public void TestScrollingUp()
{
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
}
private class TestScrollingInfo : IScrollingInfo
protected class TestScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();

View File

@ -0,0 +1,95 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Skinning.Argon;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public partial class TestSceneComboCounter : ManiaSkinnableTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
[Test]
public void TestDisplay()
{
setup(Anchor.Centre);
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
}
[Test]
public void TestAnchorOrigin()
{
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
setup(Anchor.TopCentre, 20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
check(Anchor.BottomCentre, -20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
setup(Anchor.BottomCentre, -20);
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
check(Anchor.TopCentre, 20);
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
setup(Anchor.Centre, 20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
check(Anchor.Centre, 20);
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
setup(Anchor.Centre, -20);
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
check(Anchor.Centre, -20);
}
private void setup(Anchor anchor, float y = 0)
{
AddStep($"setup {anchor} {y}", () => SetContents(s =>
{
var container = new Container
{
RelativeSizeAxes = Axes.Both,
};
if (s is ArgonSkin)
container.Add(new ArgonManiaComboCounter());
else if (s is LegacySkin)
container.Add(new LegacyManiaComboCounter());
else
container.Add(new LegacyManiaComboCounter());
container.Child.Anchor = anchor;
container.Child.Origin = Anchor.Centre;
container.Child.Y = y;
return container;
}));
}
private void check(Anchor anchor, float y)
{
AddAssert($"check {anchor} {y}", () =>
{
foreach (var combo in this.ChildrenOfType<ISerialisableDrawable>())
{
var drawableCombo = (Drawable)combo;
if (drawableCombo.Anchor != anchor || drawableCombo.Y != y)
return false;
}
return true;
});
}
}
}

View File

@ -28,14 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
AddStep("Show " + result.GetDescription(), () =>
{
SetContents(_ =>
new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
}, null)
{
var drawableManiaJudgement = new DrawableManiaJudgement
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
};
drawableManiaJudgement.Apply(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())
{
Type = result
}, null);
return drawableManiaJudgement;
});
// for test purposes, undo the Y adjustment related to the `ScorePosition` legacy positioning config value
// (see `LegacyManiaJudgementPiece.load()`).

View File

@ -3,15 +3,22 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public partial class TestScenePlayfield : ManiaSkinnableTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
[Test]
@ -29,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Child = new ManiaPlayfield(stageDefinitions)
});
});
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
}
[TestCase(2)]
@ -54,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
}
});
});
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Perfect }), 20);
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
}
protected override IBeatmap CreateBeatmapForSkinProvider()

View File

@ -14,12 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
SetContents(_ =>
{
ManiaAction normalAction = ManiaAction.Key1;
ManiaAction specialAction = ManiaAction.Special1;
ManiaAction action = ManiaAction.Key1;
return new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
Child = new Stage(0, new StageDefinition(4), ref normalAction, ref specialAction)
Child = new Stage(0, new StageDefinition(4), ref action)
};
});
}

View File

@ -36,8 +36,8 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
}
[Test]
@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
}
[Test]

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaPlayerLegacySkin : LegacySkinPlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
// play with a converted beatmap to allow dual stages mod to work.
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(new RulesetInfo());
protected override bool HasCustomSteps => true;
[Test]
public void TestSingleStage()
{
AddStep("Load single stage", LoadPlayer);
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
[Test]
public void TestDualStage()
{
AddStep("Load dual stage", () => LoadPlayer(new Mod[] { new ManiaModDualStages() }));
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
}
}
}

View File

@ -131,9 +131,7 @@ namespace osu.Game.Rulesets.Mania.Tests
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)
{
var specialAction = ManiaAction.Special1;
var stage = new Stage(0, new StageDefinition(2), ref action, ref specialAction);
var stage = new Stage(0, new StageDefinition(2), ref action);
stages.Add(stage);
return new ScrollingTestContainer(direction)

View File

@ -45,18 +45,15 @@ namespace osu.Game.Rulesets.Mania
LeftKeys = stage1LeftKeys,
RightKeys = stage1RightKeys,
SpecialKey = InputKey.V,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1
}.GenerateKeyBindingsFor(singleStageVariant, out var nextNormal);
}.GenerateKeyBindingsFor(singleStageVariant);
var stage2Bindings = new VariantMappingGenerator
{
LeftKeys = stage2LeftKeys,
RightKeys = stage2RightKeys,
SpecialKey = InputKey.B,
SpecialAction = ManiaAction.Special2,
NormalActionStart = nextNormal
}.GenerateKeyBindingsFor(singleStageVariant, out _);
ActionStart = (ManiaAction)singleStageVariant,
}.GenerateKeyBindingsFor(singleStageVariant);
return stage1Bindings.Concat(stage2Bindings);
}

View File

@ -19,16 +19,8 @@ namespace osu.Game.Rulesets.Mania
public enum ManiaAction
{
[Description("Special 1")]
Special1 = 1,
[Description("Special 2")]
Special2,
// This offsets the start value of normal keys in-case we add more special keys
// above at a later time, without breaking replays/configs.
[Description("Key 1")]
Key1 = 10,
Key1,
[Description("Key 2")]
Key2,

View File

@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponentLookup : GameplaySkinComponentLookup<ManiaSkinComponents>
public class ManiaSkinComponentLookup : SkinComponentLookup<ManiaSkinComponents>
{
/// <summary>
/// Creates a new <see cref="ManiaSkinComponentLookup"/>.

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.Conversion;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert), typeof(ManiaModNoRelease) };
public void ApplyToBeatmap(IBeatmap beatmap)
{

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Localisation;
@ -27,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Mods
public override ModType Type => ModType.DifficultyReduction;
public override Type[] IncompatibleMods => new[] { typeof(ManiaModHoldOff) };
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;

View File

@ -268,11 +268,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyMaxResult();
else
MissForcefully();
}
// Make sure that the hold note is fully judged by giving the body a judgement.
if (Tail.AllJudged && !Body.AllJudged)
Body.TriggerResult(Tail.IsHit);
// Make sure that the hold note is fully judged by giving the body a judgement.
if (!Body.AllJudged)
Body.TriggerResult(Tail.IsHit);
// Important that this is always called when a result is applied.
endHold();
}
}
public override void MissForcefully()

View File

@ -17,28 +17,9 @@ namespace osu.Game.Rulesets.Mania.Replays
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
private readonly ManiaAction[] columnActions;
public ManiaAutoGenerator(ManiaBeatmap beatmap)
: base(beatmap)
{
columnActions = new ManiaAction[Beatmap.TotalColumns];
var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1;
int totalCounter = 0;
foreach (var stage in Beatmap.Stages)
{
for (int i = 0; i < stage.Columns; i++)
{
if (stage.IsSpecialColumn(i))
columnActions[totalCounter] = specialAction++;
else
columnActions[totalCounter] = normalAction++;
totalCounter++;
}
}
}
protected override void GenerateFrames()
@ -57,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Replays
switch (point)
{
case HitPoint:
actions.Add(columnActions[point.Column]);
actions.Add(ManiaAction.Key1 + point.Column);
break;
case ReleasePoint:
actions.Remove(columnActions[point.Column]);
actions.Remove(ManiaAction.Key1 + point.Column);
break;
}
}

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
@ -27,118 +25,27 @@ namespace osu.Game.Rulesets.Mania.Replays
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1;
var action = ManiaAction.Key1;
int activeColumns = (int)(legacyFrame.MouseX ?? 0);
int counter = 0;
while (activeColumns > 0)
{
bool isSpecial = isColumnAtIndexSpecial(maniaBeatmap, counter);
if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction);
Actions.Add(action);
if (isSpecial)
specialAction++;
else
normalAction++;
counter++;
action++;
activeColumns >>= 1;
}
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
int keys = 0;
foreach (var action in Actions)
{
switch (action)
{
case ManiaAction.Special1:
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 0);
break;
case ManiaAction.Special2:
keys |= 1 << getSpecialColumnIndex(maniaBeatmap, 1);
break;
default:
// the index in lazer, which doesn't include special keys.
int nonSpecialKeyIndex = action - ManiaAction.Key1;
// the index inclusive of special keys.
int overallIndex = 0;
// iterate to find the index including special keys.
for (; overallIndex < maniaBeatmap.TotalColumns; overallIndex++)
{
// skip over special columns.
if (isColumnAtIndexSpecial(maniaBeatmap, overallIndex))
continue;
// found a non-special column to use.
if (nonSpecialKeyIndex == 0)
break;
// found a non-special column but not ours.
nonSpecialKeyIndex--;
}
keys |= 1 << overallIndex;
break;
}
}
keys |= 1 << (int)action;
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
/// <summary>
/// Find the overall index (across all stages) for a specified special key.
/// </summary>
/// <param name="maniaBeatmap">The beatmap.</param>
/// <param name="specialOffset">The special key offset (0 is S1).</param>
/// <returns>The overall index for the special column.</returns>
private int getSpecialColumnIndex(ManiaBeatmap maniaBeatmap, int specialOffset)
{
for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
{
if (isColumnAtIndexSpecial(maniaBeatmap, i))
{
if (specialOffset == 0)
return i;
specialOffset--;
}
}
throw new ArgumentException("Special key index is too high.", nameof(specialOffset));
}
/// <summary>
/// Check whether the column at an overall index (across all stages) is a special column.
/// </summary>
/// <param name="beatmap">The beatmap.</param>
/// <param name="index">The overall index to check.</param>
private bool isColumnAtIndexSpecial(ManiaBeatmap beatmap, int index)
{
foreach (var stage in beatmap.Stages)
{
if (index >= stage.Columns)
{
index -= stage.Columns;
continue;
}
return stage.IsSpecialColumn(index);
}
throw new ArgumentException("Column index is too high.", nameof(index));
}
}
}

View File

@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Mania
LeftKeys = leftKeys,
RightKeys = rightKeys,
SpecialKey = InputKey.Space,
SpecialAction = ManiaAction.Special1,
NormalActionStart = ManiaAction.Key1,
}.GenerateKeyBindingsFor(variant, out _);
}.GenerateKeyBindingsFor(variant);
}
}

View File

@ -0,0 +1,52 @@
// 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.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
public partial class ArgonManiaComboCounter : ArgonComboCounter
{
protected override bool DisplayXSymbol => false;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
protected override void LoadComplete()
{
base.LoadComplete();
// the logic of flipping the position of the combo counter w.r.t. the direction does not work with "Closest" anchor,
// because it always forces the anchor to be top or bottom based on scrolling direction.
UsesFixedAnchor = true;
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => updateAnchor());
// two schedules are required so that updateAnchor is executed in the next frame,
// which is when the combo counter receives its Y position by the default layout in ArgonManiaSkinTransformer.
Schedule(() => Schedule(updateAnchor));
}
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (Anchor.HasFlag(Anchor.y1))
return;
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
// change the sign of the Y coordinate in line with the scrolling direction.
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
}
}
}

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring;
@ -26,7 +28,34 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case GlobalSkinnableContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
switch (containerLookup.Lookup)
{
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.ShowLabel.Value = false;
combo.Anchor = Anchor.TopCentre;
combo.Origin = Anchor.Centre;
combo.Y = 200;
}
})
{
new ArgonManiaComboCounter(),
};
}
return null;
case SkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();

View File

@ -0,0 +1,91 @@
// 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.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyManiaComboCounter : LegacyComboCounter
{
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
DisplayedCountText.Anchor = Anchor.Centre;
DisplayedCountText.Origin = Anchor.Centre;
PopOutCountText.Anchor = Anchor.Centre;
PopOutCountText.Origin = Anchor.Centre;
PopOutCountText.Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ComboBreakColour)?.Value ?? Color4.Red;
}
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
private IBindable<ScrollingDirection> direction = null!;
protected override void LoadComplete()
{
base.LoadComplete();
direction = scrollingInfo.Direction.GetBoundCopy();
direction.BindValueChanged(_ => updateAnchor());
// two schedules are required so that updateAnchor is executed in the next frame,
// which is when the combo counter receives its Y position by the default layout in LegacyManiaSkinTransformer.
Schedule(() => Schedule(updateAnchor));
}
private void updateAnchor()
{
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
if (Anchor.HasFlag(Anchor.y1))
return;
Anchor &= ~(Anchor.y0 | Anchor.y2);
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
// change the sign of the Y coordinate in line with the scrolling direction.
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
}
protected override void OnCountIncrement()
{
base.OnCountIncrement();
PopOutCountText.Hide();
DisplayedCountText.ScaleTo(new Vector2(1f, 1.4f))
.ScaleTo(new Vector2(1f), 300, Easing.Out)
.FadeIn(120);
}
protected override void OnCountChange()
{
base.OnCountChange();
PopOutCountText.Hide();
DisplayedCountText.ScaleTo(1f);
}
protected override void OnCountRolling()
{
if (DisplayedCount > 0)
{
PopOutCountText.Text = FormatCount(DisplayedCount);
PopOutCountText.FadeTo(0.8f).FadeOut(200)
.ScaleTo(1f).ScaleTo(4f, 200);
DisplayedCountText.FadeTo(0.5f, 300);
}
base.OnCountRolling();
}
}
}

View File

@ -5,9 +5,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -78,7 +80,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case GlobalSkinnableContainerLookup containerLookup:
// Modifications for global components.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
switch (containerLookup.Lookup)
{
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
if (combo != null)
{
combo.Anchor = Anchor.TopCentre;
combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
}
})
{
new LegacyManiaComboCounter(),
};
}
return null;
case SkinComponentLookup<HitResult> resultComponent:
return getResult(resultComponent.Component);
case ManiaSkinComponentLookup maniaComponent:

View File

@ -5,22 +5,12 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.UI
{
public partial class DrawableManiaJudgement : DrawableJudgement
{
public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject)
{
}
public DrawableManiaJudgement()
{
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result);
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece

View File

@ -66,13 +66,12 @@ namespace osu.Game.Rulesets.Mania.UI
Content = new[] { new Drawable[stageDefinitions.Count] }
});
var normalColumnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1;
var columnAction = ManiaAction.Key1;
int firstColumnIndex = 0;
for (int i = 0; i < stageDefinitions.Count; i++)
{
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
var newStage = new Stage(firstColumnIndex, stageDefinitions[i], ref columnAction);
playfieldGrid.Content[0][i] = newStage;

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.UI
private ISkinSource currentSkin = null!;
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction columnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
Definition = definition;
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
RelativeSizeAxes = Axes.Both,
Width = 1,
Action = { Value = isSpecial ? specialColumnStartAction++ : normalColumnStartAction++ }
Action = { Value = columnStartAction++ }
};
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());

View File

@ -26,37 +26,30 @@ namespace osu.Game.Rulesets.Mania
public InputKey SpecialKey;
/// <summary>
/// The <see cref="ManiaAction"/> at which the normal columns should begin.
/// The <see cref="ManiaAction"/> at which the columns should begin.
/// </summary>
public ManiaAction NormalActionStart;
/// <summary>
/// The <see cref="ManiaAction"/> for the special column.
/// </summary>
public ManiaAction SpecialAction;
public ManiaAction ActionStart;
/// <summary>
/// Generates a list of <see cref="KeyBinding"/>s for a specific number of columns.
/// </summary>
/// <param name="columns">The number of columns that need to be bound.</param>
/// <param name="nextNormalAction">The next <see cref="ManiaAction"/> to use for normal columns.</param>
/// <returns>The keybindings.</returns>
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns, out ManiaAction nextNormalAction)
public IEnumerable<KeyBinding> GenerateKeyBindingsFor(int columns)
{
ManiaAction currentNormalAction = NormalActionStart;
ManiaAction currentAction = ActionStart;
var bindings = new List<KeyBinding>();
for (int i = LeftKeys.Length - columns / 2; i < LeftKeys.Length; i++)
bindings.Add(new KeyBinding(LeftKeys[i], currentNormalAction++));
bindings.Add(new KeyBinding(LeftKeys[i], currentAction++));
if (columns % 2 == 1)
bindings.Add(new KeyBinding(SpecialKey, SpecialAction));
bindings.Add(new KeyBinding(SpecialKey, currentAction++));
for (int i = 0; i < columns / 2; i++)
bindings.Add(new KeyBinding(RightKeys[i], currentNormalAction++));
bindings.Add(new KeyBinding(RightKeys[i], currentAction++));
nextNormalAction = currentNormalAction;
return bindings;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection();
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
mergeSelection();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection();

View File

@ -24,24 +24,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestGridToggles()
{
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(true);
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
AddStep("disable distance snap grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
gridActive<RectangularPositionSnapGrid>(true);
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("disable rectangular grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
gridActive<RectangularPositionSnapGrid>(false);
}
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
double distanceSnap = double.PositiveInfinity;
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestGridSizeToggling()
{
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
gridSizeIs(4);

View File

@ -299,6 +299,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
assertControlPointTypeDuringPlacement(0, PathType.BSpline(4));
AddStep("press alt-2", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.Key(Key.Number2);
InputManager.ReleaseKey(Key.AltLeft);
});
assertControlPointTypeDuringPlacement(0, PathType.BEZIER);
AddStep("start new segment via S", () => InputManager.Key(Key.S));
assertControlPointTypeDuringPlacement(2, PathType.LINEAR);
@ -309,7 +317,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addClickStep(MouseButton.Right);
assertPlaced(true);
assertFinalControlPointType(0, PathType.BSpline(4));
assertFinalControlPointType(0, PathType.BEZIER);
assertFinalControlPointType(2, PathType.PERFECT_CURVE);
}

View File

@ -88,6 +88,21 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
}
[Test]
public void TestClickExpand()
{
createTest(() => new Container
{
RelativeSizeAxes = Axes.Both,
Scale = new Vector2(10),
Child = new CursorTrail(),
});
AddStep("expand", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = new Vector2(3));
AddWaitStep("let the cursor trail draw a bit", 5);
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
}
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
{
Clear();

View File

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
base.PostProcess();
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList();
ApplyStacking(Beatmap);
}
internal static void ApplyStacking(IBeatmap beatmap)
{
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
if (hitObjects.Count > 0)
{
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects)
h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else
applyStackingOld(Beatmap.BeatmapInfo, hitObjects);
applyStackingOld(beatmap.BeatmapInfo, hitObjects);
}
}
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
}
private void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{
for (int i = 0; i < hitObjects.Count; i++)
{

View File

@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightRating *= 0.7;
}
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
double baseFlashlightPerformance = 0.0;
if (mods.Any(h => h is OsuModFlashlight))
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
double basePerformance =
Math.Pow(

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)

View File

@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
}
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
}
}

View File

@ -67,5 +67,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return difficulty * DifficultyMultiplier;
}
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
}
}

View File

@ -309,8 +309,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (!e.AltPressed)
return false;
// If no pieces are selected, we can't change the path type.
if (Pieces.All(p => !p.IsSelected.Value))
return false;
var type = path_types[e.Key - Key.Number1];
// The first control point can never be inherit type
if (Pieces[0].IsSelected.Value && type == null)
return false;

View File

@ -359,8 +359,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
cursor.Position = getCursorPosition();
}
else if (cursor != null)
{
@ -374,6 +373,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
private Vector2 getCursorPosition()
{
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All);
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
/// <summary>
/// Whether a new control point can be placed at the current mouse position.
/// </summary>
@ -386,7 +391,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var lastPiece = controlPointVisualiser.Pieces.Single(p => p.ControlPoint == last);
lastPoint = last;
return lastPiece.IsHovered != true;
// We may only place a new control point if the cursor is not overlapping with the last control point.
// If snapping is enabled, the cursor may not hover the last piece while still placing the control point at the same position.
return !lastPiece.IsHovered && (last is null || Vector2.DistanceSquared(last.Position, getCursorPosition()) > 1f);
}
private void placeNewControlPoint()

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class GenerateToolboxGroup : EditorToolboxGroup
{
private readonly EditorToolButton polygonButton;
public GenerateToolboxGroup()
: base("Generate")
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
polygonButton = new EditorToolButton("Polygon",
() => new SpriteIcon { Icon = FontAwesome.Solid.Spinner },
() => new PolygonGenerationPopover()),
}
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat) return false;
switch (e.Key)
{
case Key.D:
if (!e.ControlPressed || !e.ShiftPressed)
return false;
polygonButton.TriggerClick();
return true;
default:
return false;
}
}
}
}

View File

@ -54,24 +54,21 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
.Concat(new[]
{
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
});
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
.Concat(DistanceSnapProvider.CreateTernaryButtons());
private BindableList<HitObject> selectedHitObjects;
private Bindable<HitObject> placementObject;
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[Cached]
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
[Cached]
protected readonly FreehandSliderToolboxGroup FreehandlSliderToolboxGroup = new FreehandSliderToolboxGroup();
protected readonly FreehandSliderToolboxGroup FreehandSliderToolboxGroup = new FreehandSliderToolboxGroup();
[BackgroundDependencyLoader]
private void load()
@ -110,7 +107,8 @@ namespace osu.Game.Rulesets.Osu.Edit
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
},
FreehandlSliderToolboxGroup
new GenerateToolboxGroup(),
FreehandSliderToolboxGroup
}
);
}
@ -295,6 +293,12 @@ namespace osu.Game.Rulesets.Osu.Edit
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{
// if the snap target is a stacked object, snap to its unstacked position rather than its stacked position.
// this is intended to make working with stacks easier (because thanks to this, you can drag an object to any
// of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times).
if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero)
closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset);
// only return distance portion, since time is not really valid
snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true;

View File

@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuHitObjectInspector : HitObjectInspector
{
protected override void AddInspectorValues()
protected override void AddInspectorValues(HitObject[] objects)
{
base.AddInspectorValues();
base.AddInspectorValues(objects);
if (EditorBeatmap.SelectedHitObjects.Count > 0)
if (objects.Length > 0)
{
var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!;
var firstInSelection = (OsuHitObject)objects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)objects.MaxBy(ho => ho.GetEndTime())!;
Debug.Assert(firstInSelection != null && lastInSelection != null);

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{
var hitObjects = selectedMovableObjects;
var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// this conditional is a rather ugly special case for stacks.
// as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around
// (they would stack and then unstack every frame).
// the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position
// which is unique to osu! due to stacking being applied as a post-processing step.
// therefore, the following loop would occur:
// - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully,
// the blueprint moves up the stack from its original drag position.
// - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack*
// to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position).
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
return true;
// this will potentially move the selection out of bounds...
foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
h.Position += localDelta;
// but this will be corrected.
moveSelectionInBounds();
// manually update stacking.
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
// as the entire flow is too expensive to run on every movement.
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
return true;
}

View File

@ -0,0 +1,193 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PolygonGenerationPopover : OsuPopover
{
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
private SliderWithTextBoxInput<int> repeatCountInput = null!;
private SliderWithTextBoxInput<int> pointInput = null!;
private RoundedButton commitButton = null!;
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
private bool began;
private bool committed;
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private HitObjectComposer composer { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
{
Current = new BindableNumber<double>(1)
{
MinValue = 0.1,
MaxValue = 6,
Precision = 0.1,
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
},
Instantaneous = true
},
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
{
Current = new BindableNumber<int>
{
MinValue = 0,
MaxValue = 180,
Precision = 1
},
Instantaneous = true
},
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
{
Current = new BindableNumber<int>(1)
{
MinValue = 1,
MaxValue = 10,
Precision = 1
},
Instantaneous = true
},
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
{
Current = new BindableNumber<int>(3)
{
MinValue = 3,
MaxValue = 10,
Precision = 1,
},
Instantaneous = true
},
commitButton = new RoundedButton
{
RelativeSizeAxes = Axes.X,
Text = "Create",
Action = commit
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
changeHandler?.BeginChange();
began = true;
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
tryCreatePolygon();
}
private void tryCreatePolygon()
{
double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime);
TimingControlPoint timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(startTime);
double timeSpacing = timingPoint.BeatLength / editorBeatmap.BeatDivisor;
IHasSliderVelocity lastWithSliderVelocity = editorBeatmap.HitObjects.Where(ho => ho.GetEndTime() <= startTime).OfType<IHasSliderVelocity>().LastOrDefault() ?? new Slider();
double velocity = OsuHitObject.BASE_SCORING_DISTANCE * editorBeatmap.Difficulty.SliderMultiplier
/ LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(lastWithSliderVelocity, timingPoint, OsuRuleset.SHORT_NAME);
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
editorBeatmap.RemoveRange(insertedCircles);
insertedCircles.Clear();
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
bool first = true;
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
{
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
var circle = new HitCircle
{
Position = position,
StartTime = startTime,
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
};
// TODO: probably ensure samples also follow current ternary status (not trivial)
circle.Samples.Add(circle.CreateHitSampleInfo());
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
{
commitButton.Enabled.Value = false;
return;
}
insertedCircles.Add(circle);
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
first = false;
}
editorBeatmap.AddRange(insertedCircles);
commitButton.Enabled.Value = true;
}
private void commit()
{
changeHandler?.EndChange();
committed = true;
Hide();
}
protected override void PopOut()
{
base.PopOut();
if (began && !committed)
{
editorBeatmap.RemoveRange(insertedCircles);
changeHandler?.EndChange();
}
}
}
}

View File

@ -5,19 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableOsuJudgement : DrawableJudgement
{
internal Color4 AccentColour { get; private set; }
internal SkinnableLighting Lighting { get; private set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
private bool positionTransferred;
private Vector2 screenSpacePosition;
[BackgroundDependencyLoader]
private void load()
@ -32,37 +36,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
public override void Apply(JudgementResult result, DrawableHitObject? judgedObject)
{
base.Apply(result, judgedObject);
if (judgedObject is not DrawableOsuHitObject osuObject)
return;
AccentColour = osuObject.AccentColour.Value;
switch (osuObject)
{
case DrawableSlider slider:
screenSpacePosition = slider.TailCircle.ToScreenSpace(slider.TailCircle.OriginPosition);
break;
default:
screenSpacePosition = osuObject.ToScreenSpace(osuObject.OriginPosition);
break;
}
Scale = new Vector2(osuObject.HitObject.Scale);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
Lighting.ResetAnimation();
Lighting.SetColourFrom(JudgedObject, Result);
positionTransferred = false;
}
protected override void Update()
{
base.Update();
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{
switch (osuObject)
{
case DrawableSlider slider:
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
break;
default:
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
break;
}
positionTransferred = true;
Scale = new Vector2(osuObject.HitObject.Scale);
}
Lighting.SetColourFrom(this, Result);
Position = Parent!.ToLocalSpace(screenSpacePosition);
}
protected override void ApplyHitAnimations()

View File

@ -1,8 +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 osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
@ -12,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
internal partial class SkinnableLighting : SkinnableSprite
{
private DrawableHitObject targetObject;
private JudgementResult targetResult;
private DrawableOsuJudgement? targetJudgement;
private JudgementResult? targetResult;
public SkinnableLighting()
: base("lighting")
@ -29,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <summary>
/// Updates the lighting colour from a given hitobject and result.
/// </summary>
/// <param name="targetObject">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetObject"/> was judged with.</param>
public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult)
/// <param name="targetJudgement">The <see cref="DrawableHitObject"/> that's been judged.</param>
/// <param name="targetResult">The <see cref="JudgementResult"/> that <paramref name="targetJudgement"/> was judged with.</param>
public void SetColourFrom(DrawableOsuJudgement targetJudgement, JudgementResult? targetResult)
{
this.targetObject = targetObject;
this.targetJudgement = targetJudgement;
this.targetResult = targetResult;
updateColour();
@ -41,10 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void updateColour()
{
if (targetObject == null || targetResult == null)
if (targetJudgement == null || targetResult == null)
Colour = Color4.White;
else
Colour = targetResult.IsHit ? targetObject.AccentColour.Value : Color4.Transparent;
Colour = targetResult.IsHit ? targetJudgement.AccentColour : Color4.Transparent;
}
}
}

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects
{
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition, IHasTimePreempt
{
/// <summary>
/// The radius of hit objects (ie. the radius of a <see cref="HitCircle"/>).
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public const double PREEMPT_MAX = 1800;
public double TimePreempt = 600;
public double TimePreempt { get; set; } = 600;
public double TimeFadeIn = 400;
private HitObjectProperty<Vector2> position;

View File

@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu
{
public class OsuSkinComponentLookup : GameplaySkinComponentLookup<OsuSkinComponents>
public class OsuSkinComponentLookup : SkinComponentLookup<OsuSkinComponents>
{
public OsuSkinComponentLookup(OsuSkinComponents component)
: base(component)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
HitResult result = resultComponent.Component;
// This should eventually be moved to a skin setting, when supported.

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
HitResult result = resultComponent.Component;
switch (result)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects;
@ -41,139 +42,187 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is OsuSkinComponentLookup osuComponent)
switch (lookup)
{
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));
case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
case OsuSkinComponents.SliderFollowCircle:
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
if (followCircleContent != null)
return new LegacyFollowCircle(followCircleContent);
case GlobalSkinnableContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
if (!IsProvidingLegacyResources)
return null;
case OsuSkinComponents.SliderBall:
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
return new LegacySliderBall(this);
// Our own ruleset components default.
switch (containerLookup.Lookup)
{
case GlobalSkinnableContainers.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
return null;
if (keyCounter != null)
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f;
}
case OsuSkinComponents.SliderBody:
if (hasHitCircle.Value)
return new LegacySliderBody();
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
return null;
if (combo != null)
{
combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f);
}
})
{
Children = new Drawable[]
{
new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(),
}
};
}
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
return null;
case OsuSkinComponentLookup osuComponent:
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false,
maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacySliderHeadHitCircle();
case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
return null;
case OsuSkinComponents.SliderFollowCircle:
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
if (followCircleContent != null)
return new LegacyFollowCircle(followCircleContent);
case OsuSkinComponents.ReverseArrow:
if (hasHitCircle.Value)
return new LegacyReverseArrow();
return null;
case OsuSkinComponents.HitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
case OsuSkinComponents.Cursor:
if (GetTexture("cursor") != null)
return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
if (GetTexture("cursortrail") != null)
return new LegacyCursorTrail(this);
return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
return null;
case OsuSkinComponents.CursorSmoke:
if (GetTexture("cursor-smoke") != null)
return new LegacySmokeSegment();
return null;
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
const float hitcircle_text_scale = 0.8f;
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.SliderBall:
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
return new LegacySliderBall(this);
case OsuSkinComponents.SpinnerBody:
bool hasBackground = GetTexture("spinner-background") != null;
return null;
if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
case OsuSkinComponents.SliderBody:
if (hasHitCircle.Value)
return new LegacySliderBody();
return null;
return null;
case OsuSkinComponents.ApproachCircle:
if (GetTexture(@"approachcircle") != null)
return new LegacyApproachCircle();
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacySliderHeadHitCircle();
return null;
case OsuSkinComponents.ReverseArrow:
if (hasHitCircle.Value)
return new LegacyReverseArrow();
return null;
case OsuSkinComponents.HitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
case OsuSkinComponents.Cursor:
if (GetTexture("cursor") != null)
return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
if (GetTexture("cursortrail") != null)
return new LegacyCursorTrail(this);
return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
return null;
case OsuSkinComponents.CursorSmoke:
if (GetTexture("cursor-smoke") != null)
return new LegacySmokeSegment();
return null;
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
const float hitcircle_text_scale = 0.8f;
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:
bool hasBackground = GetTexture("spinner-background") != null;
if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
return null;
case OsuSkinComponents.ApproachCircle:
if (GetTexture(@"approachcircle") != null)
return new LegacyApproachCircle();
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
default:
return base.GetDrawableComponent(lookup);
}
return base.GetDrawableComponent(lookup);
}
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing;
using osuTK;
using osuTK.Graphics;
@ -39,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private double timeOffset;
private float time;
/// <summary>
/// The scale used on creation of a new trail part.
/// </summary>
public Vector2 NewPartScale = Vector2.One;
private Anchor trailOrigin = Anchor.Centre;
protected Anchor TrailOrigin
@ -63,8 +67,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
// -1 signals that the part is unusable, and should not be drawn
parts[i].InvalidationID = -1;
}
AddLayout(partSizeCache);
}
[BackgroundDependencyLoader]
@ -95,12 +97,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
}
private readonly LayoutValue<Vector2> partSizeCache = new LayoutValue<Vector2>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
private Vector2 partSize => partSizeCache.IsValid
? partSizeCache.Value
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
/// <summary>
/// The amount of time to fade the cursor trail pieces.
/// </summary>
@ -156,6 +152,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position)
{
position = ToLocalSpace(position);
if (InterpolateMovements)
{
if (!lastPosition.HasValue)
@ -174,7 +172,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier;
float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < stopAt; d += interval)
@ -191,10 +189,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
}
private void addPart(Vector2 screenSpacePosition)
private void addPart(Vector2 localSpacePosition)
{
parts[currentIndex].Position = screenSpacePosition;
parts[currentIndex].Position = localSpacePosition;
parts[currentIndex].Time = time + 1;
parts[currentIndex].Scale = NewPartScale;
++parts[currentIndex].InvalidationID;
currentIndex = (currentIndex + 1) % max_sprites;
@ -206,6 +205,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
public Vector2 Position;
public float Time;
public Vector2 Scale;
public long InvalidationID;
}
@ -220,7 +220,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
private Vector2 originPosition;
private IVertexBatch<TexturedTrailVertex> vertexBatch;
@ -236,7 +235,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader = Source.shader;
texture = Source.texture;
size = Source.partSize;
time = Source.time;
fadeExponent = Source.FadeExponent;
@ -277,6 +275,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
RectangleF textureRect = texture.GetTextureRect();
renderer.PushLocalMatrix(DrawInfo.Matrix);
foreach (var part in parts)
{
if (part.InvalidationID == -1)
@ -287,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@ -305,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@ -314,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
@ -322,6 +322,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
});
}
renderer.PopLocalMatrix();
vertexBatch.Draw();
shader.Unbind();
}

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
/// <summary>
/// The current expanded scale of the cursor.
/// </summary>
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
public IBindable<float> CursorScale => cursorScale;
private readonly Bindable<float> cursorScale = new BindableFloat(1);

View File

@ -23,14 +23,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
protected override Drawable CreateCursor() => new OsuCursor();
protected override Container<Drawable> Content => fadeContainer;
private readonly Container<Drawable> fadeContainer;
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
private readonly Drawable cursorTrail;
private readonly SkinnableDrawable cursorTrail;
private readonly CursorRippleVisualiser rippleVisualiser;
@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
InternalChild = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new[]
Children = new CompositeDrawable[]
{
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
rippleVisualiser = new CursorRippleVisualiser(),
@ -79,6 +78,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
ActiveCursor.Contract();
}
protected override void Update()
{
base.Update();
if (cursorTrail.Drawable is CursorTrail trail)
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
switch (e.Action)

View File

@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
private OsuResumeOverlay.OsuResumeOverlayInputBlocker? resumeInputBlocker;
public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker)
{
Debug.Assert(this.resumeInputBlocker == null);
this.resumeInputBlocker = resumeInputBlocker;
AddInternal(resumeInputBlocker);
}
private partial class ProxyContainer : LifetimeManagementContainer
{
public void Add(Drawable proxy) => AddInternal(proxy);

View File

@ -33,9 +33,30 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load()
{
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
Add(cursorScaleContainer = new Container
{
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
Child = clickToResumeCursor = new OsuClickToResumeCursor
{
ResumeRequested = () =>
{
// since the user had to press a button to tap the resume cursor,
// block that press event from potentially reaching a hit circle that's behind the cursor.
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
if (inputBlocker != null)
inputBlocker.BlockNextPress = true;
Resume();
}
}
});
}
@ -115,7 +136,6 @@ namespace osu.Game.Rulesets.Osu.UI
return false;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
return true;
}
@ -141,5 +161,27 @@ namespace osu.Game.Rulesets.Osu.UI
this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint);
}
}
public partial class OsuResumeOverlayInputBlocker : Drawable, IKeyBindingHandler<OsuAction>
{
public bool BlockNextPress;
public OsuResumeOverlayInputBlocker()
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
bool block = BlockNextPress;
BlockNextPress = false;
return block;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,70 @@
// 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.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public partial class TestSceneTaikoEditorTestGameplay : EditorTestScene
{
protected override bool IsolateSavingFromDatabase => false;
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
[Resolved]
private OsuGameBase game { get; set; } = null!;
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
private BeatmapSetInfo importedBeatmapSet = null!;
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
base.SetUpSteps();
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
[Test]
public void TestBasicGameplayTest()
{
AddStep("add objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.Add(new Swell { StartTime = 500, EndTime = 1500 });
EditorBeatmap.Add(new Hit { StartTime = 3000 });
});
AddStep("seek to 250", () => EditorClock.Seek(250));
AddUntilStep("wait for seek", () => EditorClock.CurrentTime, () => Is.EqualTo(250));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Screens.Edit.Editor);
}
}
}

View File

@ -315,10 +315,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
hitObjectContainer.Add(drawableSwell);
});
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
@ -352,10 +349,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
hitObjectContainer.Add(drawableSwell);
});
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);

View File

@ -11,5 +11,6 @@
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
</ItemGroup>
</Project>

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
cancellationToken.ThrowIfCancellationRequested();
AddNested(new SwellTick
{
StartTime = StartTime,
Samples = Samples
});
}

View File

@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
}
public override Drawable? GetDrawableComponent(ISkinComponentLookup component)
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
switch (component)
switch (lookup)
{
case GameplaySkinComponentLookup<HitResult> resultComponent:
case SkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported.
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
return Drawable.Empty();
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
break;
}
return base.GetDrawableComponent(component);
return base.GetDrawableComponent(lookup);
}
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is GameplaySkinComponentLookup<HitResult>)
if (lookup is SkinComponentLookup<HitResult>)
{
// if a taiko skin is providing explosion sprites, hide the judgements completely
if (hasExplosion.Value)

View File

@ -5,7 +5,7 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
public class TaikoSkinComponentLookup : GameplaySkinComponentLookup<TaikoSkinComponents>
public class TaikoSkinComponentLookup : SkinComponentLookup<TaikoSkinComponents>
{
public TaikoSkinComponentLookup(TaikoSkinComponents component)
: base(component)

View File

@ -468,6 +468,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestDecodeBeatmapHitObjectCoordinatesLegacy()
{
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("hitobject-coordinates-legacy.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
var positionData = hitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(256, 256), positionData!.Position);
}
}
[Test]
public void TestDecodeBeatmapHitObjectCoordinatesLazer()
{
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
using (var resStream = TestResources.OpenResource("hitobject-coordinates-lazer.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
var positionData = hitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(256.99853f, 256.001f), positionData!.Position);
}
}
[Test]
public void TestDecodeBeatmapHitObjects()
{

View File

@ -19,7 +19,7 @@ using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@ -65,14 +65,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore);
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic));
Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL"));
Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL"));
Assert.That(score.ScoreInfo.APIMods.Select(m => m.Acronym), Is.EquivalentTo(new[] { "CL", "9K", "DS" }));
Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001));
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
Assert.That(score.Replay.Frames, Is.Not.Empty);
Assert.That(score.Replay.Frames, Has.One.Matches<ManiaReplayFrame>(frame =>
frame.Time == 414 && frame.Actions.SequenceEqual(new[] { ManiaAction.Key1, ManiaAction.Key18 })));
}
}

View File

@ -157,8 +157,9 @@ namespace osu.Game.Tests.Database
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
[Test]
public void TestScoreUpgradeFailed()
[TestCase(30000002)]
[TestCase(30000013)]
public void TestScoreUpgradeFailed(int scoreVersion)
{
ScoreInfo scoreInfo = null!;
@ -172,16 +173,18 @@ namespace osu.Game.Tests.Database
Ruleset = r.All<RulesetInfo>().First(),
})
{
TotalScoreVersion = 30000002,
TotalScoreVersion = scoreVersion,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
}
[Test]

View File

@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestNoChangesAfterDelete()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchive(out string pathOriginalSecond);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
importBeforeUpdate!.PerformWrite(s => s.DeletePending = true);
var dateBefore = importBeforeUpdate.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
[Test]
public void TestNoChanges()
{
@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
var dateBefore = importBeforeUpdate!.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
@ -479,6 +521,7 @@ namespace osu.Game.Tests.Database
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchiveWithModifications(out string pathMissingOneBeatmap, directory =>
{
// arbitrary beatmap removal
@ -496,7 +539,7 @@ namespace osu.Game.Tests.Database
Debug.Assert(importAfterUpdate != null);
Assert.That(importBeforeUpdate.ID, Is.Not.EqualTo(importAfterUpdate.ID));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(importAfterUpdate.Value.DateAdded).Within(TimeSpan.FromSeconds(1)));
});
}

View File

@ -25,6 +25,9 @@ namespace osu.Game.Tests.Editing
new object?[] { "1:02:3000", false, null, null },
new object?[] { "1:02:300 ()", false, null, null },
new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - ", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - following mod", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - following mod\nwith newlines", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
};
[TestCaseSource(nameof(test_cases))]

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new Note { StartTime = 1000 },
}
});
@ -67,8 +67,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
}
});
@ -136,8 +136,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
}
});
@ -164,8 +164,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -197,9 +197,9 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -232,8 +232,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1100 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1100 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -264,8 +264,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -299,9 +299,9 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 5000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -334,8 +334,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 9000 },
new Note { StartTime = 1000 },
new Note { StartTime = 9000 },
},
Breaks =
{
@ -366,8 +366,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
},
Breaks =
{
@ -393,8 +393,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
new Note { StartTime = 1000 },
new Note { StartTime = 2000 },
},
Breaks =
{
@ -447,8 +447,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
new Note { StartTime = 10000 },
new Note { StartTime = 11000 },
},
Breaks =
{
@ -474,8 +474,8 @@ namespace osu.Game.Tests.Editing
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
new Note { StartTime = 10000 },
new Note { StartTime = 11000 },
},
Breaks =
{
@ -489,5 +489,55 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestTimePreemptIsRespected()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new EditorBeatmap(new Beatmap
{
ControlPointInfo = controlPoints,
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo },
Difficulty =
{
ApproachRate = 10,
},
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 5000 },
}
});
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MIN));
});
beatmap.Difficulty.ApproachRate = 0;
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.Multiple(() =>
{
Assert.That(beatmap.Breaks, Has.Count.EqualTo(1));
Assert.That(beatmap.Breaks[0].StartTime, Is.EqualTo(1200));
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(5000 - OsuHitObject.PREEMPT_MAX));
});
}
}
}

View File

@ -52,10 +52,7 @@ namespace osu.Game.Tests.Editing
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
composer = new TestHitObjectComposer()
};
Child = composer = new TestHitObjectComposer();
BeatDivisor.Value = 1;

View File

@ -8,6 +8,8 @@ using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
@ -105,9 +107,6 @@ namespace osu.Game.Tests.Mods
testMod.ResetSettingsToDefaults();
Assert.That(testMod.DrainRate.Value, Is.Null);
// ReSharper disable once HeuristicUnreachableCode
// see https://youtrack.jetbrains.com/issue/RIDER-70159.
Assert.That(testMod.OverallDifficulty.Value, Is.Null);
var applied = applyDifficulty(new BeatmapDifficulty
@ -119,6 +118,48 @@ namespace osu.Game.Tests.Mods
Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
}
[Test]
public void TestDeserializeIncorrectRange()
{
var apiMod = new APIMod
{
Acronym = @"DA",
Settings = new Dictionary<string, object>
{
[@"circle_size"] = -727,
[@"approach_rate"] = -727,
}
};
var ruleset = new OsuRuleset();
var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
Assert.Multiple(() =>
{
Assert.That(mod.CircleSize.Value, Is.GreaterThanOrEqualTo(0).And.LessThanOrEqualTo(11));
Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
});
}
[Test]
public void TestDeserializeNegativeApproachRate()
{
var apiMod = new APIMod
{
Acronym = @"DA",
Settings = new Dictionary<string, object>
{
[@"approach_rate"] = -9,
}
};
var ruleset = new OsuRuleset();
var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
Assert.That(mod.ApproachRate.Value, Is.EqualTo(-9));
}
/// <summary>
/// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/>
/// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance.

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
@ -286,5 +287,62 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
}
[Test]
public void TestBinarySearchEmptyList()
{
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.FirstFound), Is.EqualTo(-1));
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Leftmost), Is.EqualTo(-1));
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Rightmost), Is.EqualTo(-1));
}
[TestCase(new[] { 1 }, 0, -1)]
[TestCase(new[] { 1 }, 1, 0)]
[TestCase(new[] { 1 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 0, -1)]
[TestCase(new[] { 1, 3 }, 1, 0)]
[TestCase(new[] { 1, 3 }, 2, -2)]
[TestCase(new[] { 1, 3 }, 3, 1)]
[TestCase(new[] { 1, 3 }, 4, -3)]
public void TestBinarySearchUniqueScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestBinarySearchFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 0)]
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
public void TestBinarySearchLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
}
[TestCase(new[] { 1, 1 }, 1, 1)]
[TestCase(new[] { 1, 2, 2 }, 2, 2)]
[TestCase(new[] { 1, 2, 2, 2 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)]
[TestCase(new[] { 1, 2, 2, 3 }, 2, 2)]
public void TestBinarySearchRightMostDuplicateScenarios(int[] values, int search, int expectedIndex)
{
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
}
}
}

View File

@ -73,7 +73,12 @@ namespace osu.Game.Tests.Resources
private static string getTempFilename() => temp_storage.GetFullPath(Guid.NewGuid() + ".osz");
private static int importId;
private static int testId = 1;
/// <summary>
/// Get a unique int value which is incremented each call.
/// </summary>
public static int GetNextTestID() => Interlocked.Increment(ref testId);
/// <summary>
/// Create a test beatmap set model.
@ -88,7 +93,7 @@ namespace osu.Game.Tests.Resources
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length];
int setId = Interlocked.Increment(ref importId);
int setId = GetNextTestID();
var metadata = new BeatmapMetadata
{

View File

@ -0,0 +1,6 @@
osu file format v128
[HitObjects]
// Coordinates should be preserves in lazer beatmaps.
256.99853,256.001,1000,49,0,0:0:0:0:

View File

@ -0,0 +1,5 @@
osu file format v14
[HitObjects]
// Coordinates should be truncated to int values in legacy beatmaps.
256.99853,256.001,1000,49,0,0:0:0:0:

View File

@ -12,6 +12,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
@ -65,7 +66,9 @@ namespace osu.Game.Tests.Skins
// Covers default rank display
"Archives/modified-default-20230809.osk",
// Covers legacy rank display
"Archives/modified-classic-20230809.osk"
"Archives/modified-classic-20230809.osk",
// Covers legacy key counter
"Archives/modified-classic-20240724.osk"
};
/// <summary>
@ -105,7 +108,7 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
}
}
@ -118,8 +121,20 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
}
}
[Test]
public void TestDeserialiseInvalidDrawables()
{
using (var stream = TestResources.OpenResource("Archives/argon-invalid-drawable.osk"))
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos.Any(kvp => kvp.Value.AllDrawables.Any(d => d.Type == typeof(StarFountain))), Is.False);
}
}
@ -132,10 +147,10 @@ namespace osu.Game.Tests.Skins
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First();
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
@ -146,10 +161,10 @@ namespace osu.Game.Tests.Skins
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
}
}

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