Skip to content

Commit

Permalink
Merge branch 'master' into taikostatacc
Browse files Browse the repository at this point in the history
  • Loading branch information
smoogipoo authored Aug 5, 2023
2 parents 5f0020b + e13eb75 commit 3a609c9
Show file tree
Hide file tree
Showing 64 changed files with 1,311 additions and 502 deletions.
2 changes: 1 addition & 1 deletion osu.Android.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.724.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.801.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
Expand Down
17 changes: 17 additions & 0 deletions osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
Expand All @@ -25,6 +26,22 @@ public partial class ManiaPlayfield : ScrollingPlayfield

private readonly List<Stage> stages = new List<Stage>();

public override Quad SkinnableComponentScreenSpaceDrawQuad
{
get
{
if (Stages.Count == 1)
return Stages.First().ScreenSpaceDrawQuad;

RectangleF area = RectangleF.Empty;

foreach (var stage in Stages)
area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);

return area;
}
}

public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));

public ManiaPlayfield(List<StageDefinition> stageDefinitions)
Expand Down
147 changes: 147 additions & 0 deletions osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;

namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene
{
private const double time_spinner_start = 2000;
private const double time_spinner_end = 4000;

private List<JudgementResult> judgementResults = new List<JudgementResult>();
private ScoreAccessibleReplayPlayer currentPlayer = null!;

[Test]
public void TestHitNothing()
{
performTest(new List<ReplayFrame>());

AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult));
}

[TestCase(1)]
[TestCase(2)]
[TestCase(5)]
public void TestNumberOfSpins(int spins)
{
performTest(generateReplay(spins));

for (int i = 0; i < spins; ++i)
assertResult<SpinnerTick>(i, HitResult.SmallBonus);

assertResult<SpinnerTick>(spins, HitResult.IgnoreMiss);
}

[Test]
public void TestHitEverything()
{
performTest(generateReplay(20));

AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
}

private static List<ReplayFrame> generateReplay(int spins)
{
var replayFrames = new List<ReplayFrame>();

const int frames_per_spin = 30;

for (int i = 0; i < spins * frames_per_spin; ++i)
{
float totalProgress = i / (float)(spins * frames_per_spin);
float spinProgress = (i % frames_per_spin) / (float)frames_per_spin;
double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress;
float posX = MathF.Cos(2 * MathF.PI * spinProgress);
float posY = MathF.Sin(2 * MathF.PI * spinProgress);
Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50;

replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton));
}

return replayFrames;
}

private void performTest(List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects =
{
new Spinner
{
StartTime = time_spinner_start,
EndTime = time_spinner_end,
Position = OsuPlayfield.BASE_SIZE / 2
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty(),
Ruleset = new OsuRuleset().RulesetInfo
},
});
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});

AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}

private void assertResult<T>(int index, HitResult expectedResult)
{
AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}",
() => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type,
() => Is.EqualTo(expectedResult));
}

private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;

protected override bool PauseOnFocusLost => false;

public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}
69 changes: 11 additions & 58 deletions osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
using osuTK.Input;

Expand All @@ -27,11 +28,6 @@ public partial class OsuSelectionHandler : EditorSelectionHandler
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }

/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;

/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
Expand All @@ -42,9 +38,8 @@ protected override void OnSelectionChanged()
{
base.OnSelectionChanged();

Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();

SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
Expand All @@ -53,7 +48,6 @@ protected override void OnSelectionChanged()
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referenceOrigin = null;
referencePathTypes = null;
}

Expand Down Expand Up @@ -109,13 +103,13 @@ public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{
var hitObjects = selectedMovableObjects;

var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);

bool didFlip = false;

foreach (var h in hitObjects)
{
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);

if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
Expand Down Expand Up @@ -169,34 +163,13 @@ private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}

public override bool HandleRotation(float delta)
{
var hitObjects = selectedMovableObjects;

Quad quad = getSurroundingQuad(hitObjects);

referenceOrigin ??= quad.Centre;

foreach (var h in hitObjects)
{
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);

if (h is IHasPath path)
{
foreach (PathControlPoint cp in path.Path.ControlPoints)
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
}
}

// this isn't always the case but let's be lenient for now.
return true;
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();

private void scaleSlider(Slider slider, Vector2 scale)
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();

Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));

// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
Expand All @@ -222,7 +195,7 @@ private void scaleSlider(Slider slider, Vector2 scale)
slider.SnapTo(snapProvider);

//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);

if (xInBounds && yInBounds && slider.Path.HasValidLength)
Expand All @@ -238,10 +211,10 @@ private void scaleSlider(Slider slider, Vector2 scale)
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
Quad selectionQuad = getSurroundingQuad(hitObjects);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);

foreach (var h in hitObjects)
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
}

private (bool X, bool Y) isQuadInBounds(Quad quad)
Expand All @@ -256,7 +229,7 @@ private void moveSelectionInBounds()
{
var hitObjects = selectedMovableObjects;

Quad quad = getSurroundingQuad(hitObjects);
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);

Vector2 delta = Vector2.Zero;

Expand Down Expand Up @@ -286,7 +259,7 @@ private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vec
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;

Quad selectionQuad = getSurroundingQuad(hitObjects);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);

//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
Expand All @@ -311,26 +284,6 @@ private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vec
return scale;
}

/// <summary>
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
GetSurroundingQuad(hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
return new[]
{
h.Position,
// can't use EndPosition for reverse slider cases.
h.Position + path.Path.PositionAt(1)
};
}
return new[] { h.Position };
}));

/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
Expand Down
Loading

0 comments on commit 3a609c9

Please sign in to comment.