1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 02:57:25 +08:00

Merge branch 'master' into ruleset-shaders

This commit is contained in:
Dan Balasescu 2021-07-07 15:44:30 +09:00 committed by GitHub
commit d2722f1523
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
351 changed files with 6955 additions and 2797 deletions

View File

@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.608.0",
"version": "2021.705.0",
"commands": [
"localisation"
]

View File

@ -157,7 +157,7 @@ csharp_style_unused_value_assignment_preference = discard_variable:warning
#Style - variable declaration
csharp_style_inlined_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_deconstructed_variable_declaration = false:silent
#Style - other C# 7.x features
dotnet_style_prefer_inferred_tuple_names = true:warning
@ -168,8 +168,8 @@ dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
#Style - C# 8 features
csharp_prefer_static_local_function = true:warning
csharp_prefer_simple_using_statement = true:silent
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning
csharp_style_prefer_index_operator = false:silent
csharp_style_prefer_range_operator = false:silent
csharp_style_prefer_switch_expression = false:none
#Supressing roslyn built-in analyzers

View File

@ -15,6 +15,7 @@ jobs:
- { prettyname: macOS, fullname: macos-latest }
- { prettyname: Linux, fullname: ubuntu-latest }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2

View File

@ -21,6 +21,7 @@ jobs:
- { prettyname: macOS }
- { prettyname: Linux }
threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 5
steps:
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.4.2

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="osu-client-sqlite" uuid="1aa4b9be-cd8d-47ae-8186-30a13cd724a5">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source>
</component>
</project>

View File

@ -1,3 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Realm />
<Realm DisableAnalytics="true" />
</Weavers>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -12,7 +12,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<ItemGroup>

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.622.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.707.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.2.0" />
<PackageReference Include="Realm" Version="10.2.1" />
</ItemGroup>
</Project>

View File

@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
if (scheduleRecheck)
{
// check again in 30 minutes.
Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
}
}
@ -141,7 +141,7 @@ namespace osu.Desktop.Updater
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
.ContinueWith(_ => updateManager.Schedule(() => game?.GracefullyExit()));
return true;
};
}

View File

@ -5,8 +5,8 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Description>A free-to-win rhythm game. Rhythm is just a *click* away!</Description>
<AssemblyName>osu!</AssemblyName>
<Title>osu!lazer</Title>
<Product>osu!lazer</Product>
<Title>osu!</Title>
<Product>osu!</Product>
<ApplicationIcon>lazer.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Version>0.0.0</Version>

View File

@ -3,7 +3,7 @@
<metadata>
<id>osulazer</id>
<version>0.0.0</version>
<title>osu!lazer</title>
<title>osu!</title>
<authors>ppy Pty Ltd</authors>
<owners>Dean Herbert</owners>
<projectUrl>https://osu.ppy.sh/</projectUrl>
@ -20,4 +20,3 @@
<file src="**.config" target="lib\net45\"/>
</files>
</package>

View File

@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new CatchRuleset();
}
}

View File

@ -0,0 +1,114 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using Direction = osu.Game.Rulesets.Catch.UI.Direction;
namespace osu.Game.Rulesets.Catch.Tests
{
public class TestSceneCatchSkinConfiguration : OsuTestScene
{
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
private Catcher catcher;
private readonly Container container;
public TestSceneCatchSkinConfiguration()
{
Add(droppedObjectContainer = new DroppedObjectContainer());
Add(container = new Container { RelativeSizeAxes = Axes.Both });
}
[TestCase(false)]
[TestCase(true)]
public void TestCatcherPlateFlipping(bool flip)
{
AddStep("setup catcher", () =>
{
var skin = new TestSkin { FlipCatcherPlate = flip };
container.Child = new SkinProvidingContainer(skin)
{
Child = catcher = new Catcher(new Container())
{
Anchor = Anchor.Centre
}
};
});
Fruit fruit = new Fruit();
AddStep("catch fruit", () => catchFruit(fruit, 20));
float position = 0;
AddStep("record fruit position", () => position = getCaughtObjectPosition(fruit));
AddStep("face left", () => catcher.VisualDirection = Direction.Left);
if (flip)
AddAssert("fruit position changed", () => !Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
else
AddAssert("fruit position unchanged", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
AddStep("face right", () => catcher.VisualDirection = Direction.Right);
AddAssert("fruit position restored", () => Precision.AlmostEquals(getCaughtObjectPosition(fruit), position));
}
private float getCaughtObjectPosition(Fruit fruit)
{
var caughtObject = catcher.ChildrenOfType<CaughtObject>().Single(c => c.HitObject == fruit);
return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
}
private void catchFruit(Fruit fruit, float x)
{
fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableFruit = new DrawableFruit(fruit) { X = x };
var judgement = fruit.CreateJudgement();
catcher.OnNewResult(drawableFruit, new CatchJudgementResult(fruit, judgement)
{
Type = judgement.MaxResult
});
}
private class TestSkin : DefaultSkin
{
public bool FlipCatcherPlate { get; set; }
public TestSkin()
: base(null)
{
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is CatchSkinConfiguration config)
{
if (config == CatchSkinConfiguration.FlipCatcherPlate)
return SkinUtils.As<TValue>(new Bindable<bool>(FlipCatcherPlate));
}
return base.GetConfig<TLookup, TValue>(lookup);
}
}
}
}

View File

@ -6,8 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.UI;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
@ -31,10 +31,23 @@ namespace osu.Game.Rulesets.Catch.Tests
[Resolved]
private OsuConfigManager config { get; set; }
private Container<CaughtObject> droppedObjectContainer;
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
private readonly Container trailContainer;
private TestCatcher catcher;
public TestSceneCatcher()
{
Add(trailContainer = new Container
{
Anchor = Anchor.Centre,
Depth = -1
});
Add(droppedObjectContainer = new DroppedObjectContainer());
}
[SetUp]
public void SetUp() => Schedule(() =>
{
@ -43,20 +56,13 @@ namespace osu.Game.Rulesets.Catch.Tests
CircleSize = 0,
};
var trailContainer = new Container();
droppedObjectContainer = new Container<CaughtObject>();
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
if (catcher != null)
Remove(catcher);
Child = new Container
Add(catcher = new TestCatcher(trailContainer, difficulty)
{
Anchor = Anchor.Centre,
Children = new Drawable[]
{
trailContainer,
droppedObjectContainer,
catcher
}
};
Anchor = Anchor.Centre
});
});
[Test]
@ -188,9 +194,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10);
AddAssert("caught objects are stacked", () =>
catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) &&
catcher.CaughtObjects.Any(obj => obj.Y < -25));
catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
catcher.CaughtObjects.Any(obj => obj.Y < 0));
}
[Test]
@ -293,8 +299,8 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
public TestCatcher(Container trailsTarget, Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, droppedObjectTarget, difficulty)
public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
: base(trailsTarget, difficulty)
{
}
}

View File

@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
@ -97,18 +96,12 @@ namespace osu.Game.Rulesets.Catch.Tests
SetContents(_ =>
{
var droppedObjectContainer = new Container<CaughtObject>
{
RelativeSizeAxes = Axes.Both
};
return new CatchInputManager(catchRuleset)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
droppedObjectContainer,
new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
new TestCatcherArea(beatmapDifficulty)
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
@ -126,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests
private class TestCatcherArea : CatcherArea
{
public TestCatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
: base(droppedObjectContainer, beatmapDifficulty)
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
: base(beatmapDifficulty)
{
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
}
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);

View File

@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("create hyper-dashing catcher", () =>
{
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container<CaughtObject>())
Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(4f),
Origin = Anchor.Centre
}, skin);
});
@ -139,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("finish hyper-dashing", () =>
{
catcherArea.MovableCatcher.SetHyperDashState(1);
catcherArea.MovableCatcher.SetHyperDashState();
catcherArea.MovableCatcher.FinishTransforms();
});
@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
}
private class TestCatcherArea : CatcherArea
{
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
public TestCatcherArea()
{
Scale = new Vector2(4f);
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
}
}
}
}

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Beatmaps
@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
public const int RNG_SEED = 1337;
public bool HardRockOffsets { get; set; }
public CatchBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
}
}
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
float? lastPosition = null;
double lastStartTime = 0;
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
case Fruit fruit:
if (shouldApplyHardRockOffset)
if (HardRockOffsets)
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
break;

View File

@ -22,7 +22,9 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Rulesets.Edit;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
@ -175,12 +177,14 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
}
}

View File

@ -0,0 +1,24 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class BananaShowerCompositionTool : HitObjectCompositionTool
{
public BananaShowerCompositionTool()
: base(nameof(BananaShower))
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new BananaShowerPlacementBlueprint();
}
}

View File

@ -0,0 +1,73 @@
// 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.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint<BananaShower>
{
private readonly TimeSpanOutline outline;
public BananaShowerPlacementBlueprint()
{
InternalChild = outline = new TimeSpanOutline();
}
protected override void Update()
{
base.Update();
outline.UpdateFrom(HitObjectContainer, HitObject);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
if (e.Button != MouseButton.Left) break;
BeginPlacement(true);
return true;
case PlacementState.Active:
if (e.Button != MouseButton.Right) break;
// If the duration is negative, swap the start and the end time to make the duration positive.
if (HitObject.Duration < 0)
{
HitObject.StartTime = HitObject.EndTime;
HitObject.Duration = -HitObject.Duration;
}
EndPlacement(HitObject.Duration > 0);
return true;
}
return base.OnMouseDown(e);
}
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
if (!(result.Time is double time)) return;
switch (PlacementActive)
{
case PlacementState.Waiting:
HitObject.StartTime = time;
break;
case PlacementState.Active:
HitObject.EndTime = time;
break;
}
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class BananaShowerSelectionBlueprint : CatchSelectionBlueprint<BananaShower>
{
public BananaShowerSelectionBlueprint(BananaShower hitObject)
: base(hitObject)
{
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class CatchPlacementBlueprint<THitObject> : PlacementBlueprint
where THitObject : CatchHitObject, new()
{
protected new THitObject HitObject => (THitObject)base.HitObject;
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
private Playfield playfield { get; set; }
public CatchPlacementBlueprint()
: base(new THitObject())
{
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject>
where THitObject : CatchHitObject
{
public override Vector2 ScreenSpaceSelectionPoint
{
get
{
float x = HitObject.OriginalX;
float y = HitObjectContainer.PositionAtTime(HitObject.StartTime);
return HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
}
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SelectionQuad.Contains(screenSpacePos);
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
private Playfield playfield { get; set; }
protected CatchSelectionBlueprint(THitObject hitObject)
: base(hitObject)
{
}
}
}

View File

@ -0,0 +1,38 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class FruitOutline : CompositeDrawable
{
public FruitOutline()
{
Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre;
InternalChild = new BorderPiece();
}
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
Colour = osuColour.Yellow;
}
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
{
X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
Scale = new Vector2(hitObject.Scale);
}
}
}

View File

@ -0,0 +1,53 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class NestedOutlineContainer : CompositeDrawable
{
private readonly List<CatchHitObject> nestedHitObjects = new List<CatchHitObject>();
public NestedOutlineContainer()
{
Anchor = Anchor.BottomLeft;
}
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
X = parentHitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
}
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
nestedHitObjects.Clear();
nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
.OfType<CatchHitObject>()
.Where(h => !(h is TinyDroplet)));
while (nestedHitObjects.Count < InternalChildren.Count)
RemoveInternal(InternalChildren[^1]);
while (InternalChildren.Count < nestedHitObjects.Count)
AddInternal(new FruitOutline());
for (int i = 0; i < nestedHitObjects.Count; i++)
{
var hitObject = nestedHitObjects[i];
var outline = (FruitOutline)InternalChildren[i];
outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}

View File

@ -0,0 +1,83 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class ScrollingPath : CompositeDrawable
{
private readonly Path drawablePath;
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
public ScrollingPath()
{
Anchor = Anchor.BottomLeft;
InternalChildren = new Drawable[]
{
drawablePath = new SmoothPath
{
PathRadius = 2,
Alpha = 0.5f
},
};
}
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
{
X = hitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
}
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
computeDistanceXs(hitObject);
drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
.ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
}
private void computeDistanceXs(JuiceStream hitObject)
{
vertices.Clear();
var sliderVertices = new List<Vector2>();
hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
if (sliderVertices.Count == 0)
return;
double distance = 0;
Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{
foreach (var position in sliderVertices)
{
distance += Vector2.Distance(lastPosition, position);
lastPosition = position;
vertices.Add((distance, position.X));
}
sliderVertices.Reverse();
}
}
// Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}

View File

@ -0,0 +1,63 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class TimeSpanOutline : CompositeDrawable
{
private const float border_width = 4;
private const float opacity_when_empty = 0.5f;
private bool isEmpty = true;
public TimeSpanOutline()
{
Anchor = Origin = Anchor.BottomLeft;
RelativeSizeAxes = Axes.X;
Masking = true;
BorderThickness = border_width;
Alpha = opacity_when_empty;
// A box is needed to make the border visible.
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Transparent
};
}
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
BorderColour = osuColour.Yellow;
}
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, BananaShower hitObject)
{
float startY = hitObjectContainer.PositionAtTime(hitObject.StartTime);
float endY = hitObjectContainer.PositionAtTime(hitObject.EndTime);
Y = Math.Max(startY, endY);
float height = Math.Abs(startY - endY);
bool wasEmpty = isEmpty;
isEmpty = height == 0;
if (wasEmpty != isEmpty)
this.FadeTo(isEmpty ? opacity_when_empty : 1f, 150);
Height = Math.Max(height, border_width);
}
}
}

View File

@ -0,0 +1,50 @@
// 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.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class FruitPlacementBlueprint : CatchPlacementBlueprint<Fruit>
{
private readonly FruitOutline outline;
public FruitPlacementBlueprint()
{
InternalChild = outline = new FruitOutline();
}
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update()
{
base.Update();
outline.UpdateFrom(HitObjectContainer, HitObject);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left) return base.OnMouseDown(e);
EndPlacement(true);
return true;
}
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class FruitSelectionBlueprint : CatchSelectionBlueprint<Fruit>
{
private readonly FruitOutline outline;
public FruitSelectionBlueprint(Fruit hitObject)
: base(hitObject)
{
InternalChild = outline = new FruitOutline();
}
protected override void Update()
{
base.Update();
if (IsSelected)
outline.UpdateFrom(HitObjectContainer, HitObject);
}
}
}

View File

@ -0,0 +1,92 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
public class JuiceStreamSelectionBlueprint : CatchSelectionBlueprint<JuiceStream>
{
public override Quad SelectionQuad => HitObjectContainer.ToScreenSpace(getBoundingBox().Offset(new Vector2(0, HitObjectContainer.DrawHeight)));
private float minNestedX;
private float maxNestedX;
private readonly ScrollingPath scrollingPath;
private readonly NestedOutlineContainer nestedOutlineContainer;
private readonly Cached pathCache = new Cached();
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject)
{
InternalChildren = new Drawable[]
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer()
};
}
[BackgroundDependencyLoader]
private void load()
{
HitObject.DefaultsApplied += onDefaultsApplied;
computeObjectBounds();
}
protected override void Update()
{
base.Update();
if (!IsSelected) return;
scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
if (pathCache.IsValid) return;
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
pathCache.Validate();
}
private void onDefaultsApplied(HitObject _)
{
computeObjectBounds();
pathCache.Invalidate();
}
private void computeObjectBounds()
{
minNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Min(nested => nested.OriginalX) - HitObject.OriginalX;
maxNestedX = HitObject.NestedHitObjects.OfType<CatchHitObject>().Max(nested => nested.OriginalX) - HitObject.OriginalX;
}
private RectangleF getBoundingBox()
{
float left = HitObject.OriginalX + minNestedX;
float right = HitObject.OriginalX + maxNestedX;
float top = HitObjectContainer.PositionAtTime(HitObject.EndTime);
float bottom = HitObjectContainer.PositionAtTime(HitObject.StartTime);
float objectRadius = CatchHitObject.OBJECT_RADIUS * HitObject.Scale;
return new RectangleF(left, top, right - left, bottom - top).Inflate(objectRadius);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
HitObject.DefaultsApplied -= onDefaultsApplied;
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchBlueprintContainer : ComposeBlueprintContainer
{
public CatchBlueprintContainer(CatchHitObjectComposer composer)
: base(composer)
{
}
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
{
case Fruit fruit:
return new FruitSelectionBlueprint(fruit);
case JuiceStream juiceStream:
return new JuiceStreamSelectionBlueprint(juiceStream);
case BananaShower bananaShower:
return new BananaShowerSelectionBlueprint(bananaShower);
}
return base.CreateHitObjectBlueprintFor(hitObject);
}
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchEditorPlayfield : CatchPlayfield
{
// TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen.
public CatchEditorPlayfield(BeatmapDifficulty difficulty)
: base(difficulty)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
// TODO: honor "hit animation" setting?
CatcherArea.MovableCatcher.CatchFruitOnPlate = false;
// TODO: disable hit lighting as well
}
}
}

View File

@ -0,0 +1,42 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
{
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{
new FruitCompositionTool(),
new BananaShowerCompositionTool()
};
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
// TODO: implement position snap
result.ScreenSpacePosition.X = screenSpacePosition.X;
return result;
}
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
}
}

View File

@ -0,0 +1,46 @@
// 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 osu.Framework.Allocation;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchSelectionHandler : EditorSelectionHandler
{
protected ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
[Resolved]
private Playfield playfield { get; set; }
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var blueprint = moveEvent.Blueprint;
Vector2 originalPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint);
Vector2 targetPosition = HitObjectContainer.ToLocalSpace(blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta);
float deltaX = targetPosition.X - originalPosition.X;
EditorBeatmap.PerformOnSelection(h =>
{
if (!(h is CatchHitObject hitObject)) return;
if (hitObject is BananaShower) return;
// TODO: confine in bounds
hitObject.OriginalX += deltaX;
// Move the nested hit objects to give an instant result before nested objects are recreated.
foreach (var nested in hitObject.NestedHitObjects.OfType<CatchHitObject>())
nested.OriginalX += deltaX;
});
return true;
}
}
}

View File

@ -0,0 +1,21 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Edit
{
public class DrawableCatchEditorRuleset : DrawableCatchRuleset
{
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods)
{
}
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
}
}

View File

@ -0,0 +1,24 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
namespace osu.Game.Rulesets.Catch.Edit
{
public class FruitCompositionTool : HitObjectCompositionTool
{
public FruitCompositionTool()
: base(nameof(Fruit))
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new FruitPlacementBlueprint();
}
}

View File

@ -5,11 +5,12 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModDifficultyAdjust : ModDifficultyAdjust
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
approachRate,
spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
}
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
{
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
catchProcessor.HardRockOffsets = HardRockOffsets.Value;
}
}
}

View File

@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
{
public override double ScoreMultiplier => 1.12;
public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
{
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
catchProcessor.HardRockOffsets = true;
}
}
}

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 Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -20,6 +21,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary>
/// The horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary>
/// <remarks>
/// Only setter is exposed.
/// Use <see cref="OriginalX"/> or <see cref="EffectiveX"/> to get the horizontal position.
/// </remarks>
[JsonIgnore]
public float X
{
set => OriginalXBindable.Value = value;
@ -34,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public float XOffset
{
get => XOffsetBindable.Value;
set => XOffsetBindable.Value = value;
}
@ -44,7 +51,11 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original <see cref="X"/> value specified in the beatmap, not affected by the beatmap processing.
/// Use <see cref="EffectiveX"/> for a gameplay.
/// </remarks>
public float OriginalX => OriginalXBindable.Value;
public float OriginalX
{
get => OriginalXBindable.Value;
set => OriginalXBindable.Value = value;
}
/// <summary>
/// The effective horizontal position of the hit object between 0 and <see cref="CatchPlayfield.WIDTH"/>.
@ -53,9 +64,9 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original <see cref="X"/> value plus the offset applied by the beatmap processing.
/// Use <see cref="OriginalX"/> if a value not affected by the offset is desired.
/// </remarks>
public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value;
public float EffectiveX => OriginalX + XOffset;
public double TimePreempt = 1000;
public double TimePreempt { get; set; } = 1000;
public readonly Bindable<int> IndexInBeatmapBindable = new Bindable<int>();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -25,7 +26,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; }
[JsonIgnore]
public double Velocity { get; private set; }
[JsonIgnore]
public double TickDistance { get; private set; }
/// <summary>
@ -113,6 +117,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public float EndX => OriginalX + this.CurvePositionAt(1).X;
[JsonIgnore]
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Graphics;
@ -33,6 +34,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// <summary>
/// The target fruit if we are to initiate a hyperdash.
/// </summary>
[JsonIgnore]
public CatchHitObject HyperDashTarget
{
get => hyperDashTarget;

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Rulesets.Catch.Skinning
{
public enum CatchSkinConfiguration
{
/// <summary>
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
/// </summary>
FlipCatcherPlate
}
}

View File

@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
/// </summary>
private bool providesComboCounter => this.HasFont(LegacyFont.Combo);
public CatchLegacySkinTransformer(ISkinSource source)
: base(source)
public CatchLegacySkinTransformer(ISkin skin)
: base(skin)
{
}
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (targetComponent.Target)
{
case SkinnableTarget.MainHUDComponents:
var components = Source.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
var components = base.GetDrawableComponent(component) as SkinnableTargetComponentsContainer;
if (providesComboCounter && components != null)
{
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return null;
case CatchSkinComponents.Catcher:
var version = Source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
var version = GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value ?? 1;
if (version < 2.3m)
{
@ -83,13 +83,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter(Source);
return new LegacyCatchComboCounter(Skin);
return null;
}
}
return Source.GetDrawableComponent(component);
return base.GetDrawableComponent(component);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
@ -97,15 +97,28 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (lookup)
{
case CatchSkinColour colour:
var result = (Bindable<Color4>)Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
var result = (Bindable<Color4>)base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
if (result == null)
return null;
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable<TValue>)result;
case CatchSkinConfiguration config:
switch (config)
{
case CatchSkinConfiguration.FlipCatcherPlate:
// Don't flip catcher plate contents if the catcher is provided by this legacy skin.
if (GetDrawableComponent(new CatchSkinComponent(CatchSkinComponents.Catcher)) != null)
return (IBindable<TValue>)new Bindable<bool>();
break;
}
break;
}
return Source.GetConfig<TLookup, TValue>(lookup);
return base.GetConfig<TLookup, TValue>(lookup);
}
}
}

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -27,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public const float CENTER_X = WIDTH / 2;
[Cached]
private readonly DroppedObjectContainer droppedObjectContainer;
internal readonly CatcherArea CatcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
@ -35,12 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty)
{
var droppedObjectContainer = new Container<CaughtObject>
{
RelativeSizeAxes = Axes.Both,
};
CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
CatcherArea = new CatcherArea(difficulty)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
@ -48,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI
InternalChildren = new[]
{
droppedObjectContainer,
droppedObjectContainer = new DroppedObjectContainer(),
CatcherArea.MovableCatcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to

View File

@ -56,11 +56,6 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
/// <summary>
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
/// </summary>
public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5;
/// <summary>
/// The amount by which caught fruit should be scaled down to fit on the plate.
/// </summary>
@ -79,12 +74,13 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary>
/// Contains objects dropped from the plate.
/// </summary>
private readonly Container<CaughtObject> droppedObjectTarget;
[Resolved]
private DroppedObjectContainer droppedObjectTarget { get; set; }
public CatcherAnimationState CurrentState
{
get => body.AnimationState.Value;
private set => body.AnimationState.Value = value;
get => Body.AnimationState.Value;
private set => Body.AnimationState.Value = value;
}
/// <summary>
@ -107,18 +103,22 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
public Direction VisualDirection
{
get => Scale.X > 0 ? Direction.Right : Direction.Left;
set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y);
}
/// <summary>
/// The currently facing direction.
/// </summary>
public Direction VisualDirection { get; set; } = Direction.Right;
/// <summary>
/// Whether the contents of the catcher plate should be visually flipped when the catcher direction is changed.
/// </summary>
private bool flipCatcherPlate;
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
private readonly float catchWidth;
private readonly SkinnableCatcher body;
internal readonly SkinnableCatcher Body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@ -134,10 +134,9 @@ namespace osu.Game.Rulesets.Catch.UI
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
public Catcher([NotNull] Container trailsTarget, [NotNull] Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty = null)
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{
this.trailsTarget = trailsTarget;
this.droppedObjectTarget = droppedObjectTarget;
Origin = Anchor.TopCentre;
@ -157,8 +156,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
// offset fruit vertically to better place "above" the plate.
Y = -5
},
body = new SkinnableCatcher(),
Body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer
{
Anchor = Anchor.TopCentre,
@ -347,6 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI
trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour;
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
runHyperDashStateTransition(HyperDashing);
}
@ -354,6 +357,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
Body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
@ -388,9 +395,6 @@ namespace osu.Game.Rulesets.Catch.UI
float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2);
// offset fruit vertically to better place "above" the plate.
position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET;
while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
@ -465,7 +469,7 @@ namespace osu.Game.Rulesets.Catch.UI
break;
case DroppedObjectAnimation.Explode:
var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X;
float originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * caughtObjectContainer.Scale.X;
d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750);

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
private int currentDirection;
public CatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty difficulty = null)
public CatcherArea(BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Children = new Drawable[]
@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI
Margin = new MarginPadding { Bottom = 350f },
X = CatchPlayfield.CENTER_X
},
MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
};
}

View File

@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
protected override void FreeAfterUse()
{
ClearTransforms();
Alpha = 1;
base.FreeAfterUse();
}
}

View File

@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI
CatcherTrail sprite = trailPool.Get();
sprite.AnimationState = catcher.CurrentState;
sprite.Scale = catcher.Scale;
sprite.Scale = catcher.Scale * catcher.Body.Scale;
sprite.Position = catcher.Position;
target.Add(sprite);

View File

@ -0,0 +1,17 @@
// 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.Game.Rulesets.Catch.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.UI
{
public class DroppedObjectContainer : Container<CaughtObject>
{
public DroppedObjectContainer()
{
RelativeSizeAxes = Axes.Both;
}
}
}

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new ManiaLegacySkinTransformer(skin, beatmap);
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
private class TimeSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString("N0") + "ms";
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}

View File

@ -50,29 +50,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ HitResult.Miss, "mania-hit0" }
};
private Lazy<bool> isLegacySkin;
private readonly Lazy<bool> isLegacySkin;
/// <summary>
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
/// </summary>
private Lazy<bool> hasKeyTexture;
private readonly Lazy<bool> hasKeyTexture;
public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
: base(source)
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
: base(skin)
{
this.beatmap = (ManiaBeatmap)beatmap;
Source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
isLegacySkin = new Lazy<bool>(() => FindProvider(s => s.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null) != null);
hasKeyTexture = new Lazy<bool>(() => FindProvider(s => s.GetAnimation(
s.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value
?? "mania-key1", true, true) != null) != null);
isLegacySkin = new Lazy<bool>(() => GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() =>
{
var keyImage = this.GetManiaSkinConfig<string>(LegacyManiaSkinConfigurationLookups.KeyImage, 0)?.Value ?? "mania-key1";
return this.GetAnimation(keyImage, true, true) != null;
});
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@ -125,7 +121,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
break;
}
return Source.GetDrawableComponent(component);
return base.GetDrawableComponent(component);
}
private Drawable getResult(HitResult result)
@ -146,15 +142,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
return new SampleVirtual();
return Source.GetSample(sampleInfo);
return base.GetSample(sampleInfo);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
return Source.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
return base.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
return Source.GetConfig<TLookup, TValue>(lookup);
return base.GetConfig<TLookup, TValue>(lookup);
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@ -112,7 +113,9 @@ namespace osu.Game.Rulesets.Osu.Tests
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
public IEnumerable<ISkin> AllSources => new[] { this };
public event Action SourceChanged
{

View File

@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
case OsuSkinConfiguration osuLookup:
if (osuLookup == OsuSkinConfiguration.CursorCentre)
return SkinUtils.As<TValue>(new BindableBool(false));
return SkinUtils.As<TValue>(new BindableBool());
break;
}

View File

@ -2,6 +2,7 @@
// 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.Allocation;
@ -164,9 +165,11 @@ namespace osu.Game.Rulesets.Osu.Tests
public ISample GetSample(ISampleInfo sampleInfo) => null;
public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration => default;
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => null;
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
public IEnumerable<ISkin> AllSources => new[] { this };
public event Action SourceChanged;

View File

@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
private IBeatmap beatmap;
private const double duration_of_span = 3605;
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
private Slider slider;
private DrawableSlider drawableSlider;
[SetUp]
public void Setup() => Schedule(() =>
{
slider = null;
drawableSlider = null;
});
[SetUpSteps]
public override void SetUpSteps()
{
@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
retrieveSlider(sliderIndex);
setSnaking(true);
ensureSnakingIn(startTime + fade_in_modifier);
addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i < sliderIndex; i++)
{
// non-final repeats should not snake out
ensureNoSnakingOut(startTime, i);
addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
// final repeat should snake out
ensureSnakingOut(startTime, sliderIndex);
addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex);
}
[TestCase(0)]
@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
retrieveSlider(sliderIndex);
setSnaking(false);
ensureNoSnakingIn(startTime + fade_in_modifier);
addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i <= sliderIndex; i++)
{
// no snaking out ever, including final repeat
ensureNoSnakingOut(startTime, i);
addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
}
@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality
checkPositionChange(16600, sliderRepeat, positionAlmostSame);
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
}
[Test]
@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests
setSnaking(true);
base.SetUpSteps();
checkPositionChange(16600, sliderRepeat, positionDecreased);
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
private void ensureSnakingOut(double startTime, int repeatIndex)
private void retrieveSlider(int index)
{
var repeatTime = timeAtRepeat(startTime, repeatIndex);
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
addSeekStep(() => slider);
AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
}
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
private void addEnsureNoSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame);
private void addEnsureSnakingOutSteps(Func<double> startTime, int repeatIndex)
{
if (repeatIndex % 2 == 0)
checkPositionChange(repeatTime, sliderStart, positionIncreased);
addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased);
else
checkPositionChange(repeatTime, sliderEnd, positionDecreased);
addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased);
}
private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private void addEnsureNoSnakingOutStep(Func<double> startTime, int repeatIndex)
=> addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)sliderStart : sliderEnd;
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)getSliderStart : getSliderEnd;
private List<Vector2> sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 sliderStart() => sliderCurve.First();
private Vector2 sliderEnd() => sliderCurve.Last();
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 getSliderStart() => getSliderCurve().First();
private Vector2 getSliderEnd() => getSliderCurve().Last();
private Vector2 sliderRepeat()
private Vector2 getSliderRepeat()
{
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]);
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]);
var repeat = drawable.ChildrenOfType<Container<DrawableSliderRepeat>>().First().Children.First();
return repeat.Position;
}
@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
private void checkPositionChange(double startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
private void addCheckPositionChangeSteps(Func<double> startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
{
Vector2 previousPosition = Vector2.Zero;
@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(startTime);
AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
addSeekStep(startTime + 100);
addSeekStep(() => startTime() + 100);
AddAssert($"{positionDescription} {assertionDescription}", () =>
{
var currentPosition = positionToCheck.Invoke();
@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
private void addSeekStep(double time)
private void addSeekStep(Func<Slider> slider)
{
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
private void addSeekStep(Func<double> time)
{
HitObjects = hitObjects
};
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
private readonly List<HitObject> hitObjects = new List<HitObject>
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
private static List<HitObject> createHitObjects() => new List<HitObject>
{
new Slider
{

View File

@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}

View File

@ -5,7 +5,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}

View File

@ -0,0 +1,16 @@
// 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.
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Marker interface for any mod which completely hides the approach circles.
/// Used for incompatibility with <see cref="IRequiresApproachCircles"/>.
/// </summary>
/// <remarks>
/// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
/// </remarks>
public interface IHidesApproachCircles
{
}
}

View File

@ -1,12 +0,0 @@
// 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.
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
/// </summary>
public interface IMutateApproachCircles
{
}
}

View File

@ -0,0 +1,16 @@
// 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.
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Marker interface for any mod which requires the approach circles to be visible.
/// Used for incompatibility with <see cref="IHidesApproachCircles"/>.
/// </summary>
/// <remarks>
/// Note that this is only a marker interface for incompatibility purposes, it does not change any gameplay behaviour.
/// </remarks>
public interface IRequiresApproachCircles
{
}
}

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IRequiresApproachCircles
{
public override string Name => "Approach Different";
public override string Acronym => "AD";
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)

View File

@ -158,17 +158,17 @@ namespace osu.Game.Rulesets.Osu.Mods
var firstObj = beatmap.HitObjects[0];
var startDelay = firstObj.StartTime - firstObj.TimePreempt;
using (BeginAbsoluteSequence(startDelay + break_close_late, true))
using (BeginAbsoluteSequence(startDelay + break_close_late))
leaveBreak();
foreach (var breakInfo in beatmap.Breaks)
{
if (breakInfo.HasEffect)
{
using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early, true))
using (BeginAbsoluteSequence(breakInfo.StartTime - break_open_early))
{
enterBreak();
using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late, true))
using (BeginDelayedSequence(breakInfo.Duration + break_open_early + break_close_late))
leaveBreak();
}
}

View File

@ -15,12 +15,12 @@ using osu.Game.Rulesets.Osu.Skinning;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHidden : ModHidden, IMutateApproachCircles
public class OsuModHidden : ModHidden, IHidesApproachCircles
{
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// <summary>
/// Adjusts the size of hit objects during their fade in animation.
/// </summary>
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override ModType Type => ModType.Fun;
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{

View File

@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
@ -23,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Description => "It never gets boring!";
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
private const float playfield_edge_ratio = 0.375f;
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random rng;
@ -113,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods
distanceToPrev * (float)Math.Sin(current.AngleRad)
);
posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
@ -185,73 +177,6 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
/// <summary>
/// Determines the position of the current hit object relative to the previous one.
/// </summary>
/// <returns>The position of the current hit object relative to the previous one</returns>
private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
{
var relativeRotationDistance = 0f;
if (prevPosChanged.X < playfield_middle.X)
{
relativeRotationDistance = Math.Max(
(border_distance_x - prevPosChanged.X) / border_distance_x,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
relativeRotationDistance
);
}
if (prevPosChanged.Y < playfield_middle.Y)
{
relativeRotationDistance = Math.Max(
(border_distance_y - prevPosChanged.Y) / border_distance_y,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
relativeRotationDistance
);
}
return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
}
/// <summary>
/// Rotates vector "initial" towards vector "destinantion"
/// </summary>
/// <param name="initial">Vector to rotate to "destination"</param>
/// <param name="destination">Vector "initial" should be rotated to</param>
/// <param name="relativeDistance">The angle the vector should be rotated relative to the difference between the angles of the the two vectors.</param>
/// <returns>Resulting vector</returns>
private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
{
var initialAngleRad = Math.Atan2(initial.Y, initial.X);
var destAngleRad = Math.Atan2(destination.Y, destination.X);
var diff = destAngleRad - initialAngleRad;
while (diff < -Math.PI) diff += 2 * Math.PI;
while (diff > Math.PI) diff -= 2 * Math.PI;
var finalAngleRad = initialAngleRad + relativeDistance * diff;
return new Vector2(
initial.Length * (float)Math.Cos(finalAngleRad),
initial.Length * (float)Math.Sin(finalAngleRad)
);
}
private class RandomObjectInfo
{
public float AngleRad { get; set; }

View File

@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
public class OsuModSpinIn : ModWithVisibilityAdjustment, IHidesApproachCircles
{
public override string Name => "Spin In";
public override string Acronym => "SI";
@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Circles spin in. No approach circles.";
public override double ScoreMultiplier => 1;
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
// todo: this mod needs to be incompatible with "hidden" due to forcing the circle to remain opaque,
// further implementation will be required for supporting that.
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModObjectScaleTween), typeof(OsuModHidden) };
private const int rotate_offset = 360;
private const float rotate_starting_width = 2;
@ -43,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableHitCircle circle:
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
{
circle.ApproachCircle.Hide();

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles
{
public override string Name => "Traceable";
public override string Acronym => "TC";
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
{
var h = hitObject.HitObject;
using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
(hitCircle ?? hitObject).Hide();
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
double moveDuration = hitObject.TimePreempt + 1;
using (drawable.BeginAbsoluteSequence(appearTime, true))
using (drawable.BeginAbsoluteSequence(appearTime))
{
drawable
.MoveToOffset(appearOffset)

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration, true))
using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration))
wiggle();
}
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Mods
for (int i = 0; i < amountWiggles; i++)
{
using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration, true))
using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration))
wiggle();
}
}

View File

@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new OsuLegacySkinTransformer(skin);
public int LegacyID => 0;

View File

@ -233,35 +233,43 @@ namespace osu.Game.Rulesets.Osu.Replays
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
bool hasWaited = false;
if (waitTime > lastFrame.Time)
{
lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions };
hasWaited = true;
AddFrameToReplay(lastFrame);
}
Vector2 lastPosition = lastFrame.Position;
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
OsuReplayFrame lastLastFrame = Frames.Count >= 2 ? (OsuReplayFrame)Frames[^2] : null;
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
((lastPosition - targetPos).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
if (timeDifference > 0)
{
// Perform eased movement
// If the last frame is a key-up frame and there has been no wait period, adjust the last frame's position such that it begins eased movement instantaneously.
if (lastLastFrame != null && lastFrame is OsuKeyUpReplayFrame && !hasWaited)
{
// [lastLastFrame] ... [lastFrame] ... [current frame]
// We want to find the cursor position at lastFrame, so interpolate between lastLastFrame and the new target position.
lastFrame.Position = Interpolation.ValueAt(lastFrame.Time, lastFrame.Position, targetPos, lastLastFrame.Time, h.StartTime, easing);
}
Vector2 lastPosition = lastFrame.Position;
// Perform the rest of the eased movement until the target position is reached.
for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
}
}
buttonIndex = 0;
}
else
{
// Start alternating once the time separation is too small (faster than ~225BPM).
if (timeDifference > 0 && timeDifference < 266)
buttonIndex++;
}
else
buttonIndex = 0;
}
/// <summary>
@ -284,7 +292,7 @@ namespace osu.Game.Rulesets.Osu.Replays
// TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime.
double hEndTime = h.GetEndTime() + KEY_UP_DELAY;
int endDelay = h is Spinner ? 1 : 0;
var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
var endFrame = new OsuKeyUpReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y));
// Decrement because we want the previous frame, not the next one
int index = FindInsertionIndex(startFrame) - 1;
@ -381,5 +389,13 @@ namespace osu.Game.Rulesets.Osu.Replays
}
#endregion
private class OsuKeyUpReplayFrame : OsuReplayFrame
{
public OsuKeyUpReplayFrame(double time, Vector2 position)
: base(time, position)
{
}
}
}
}

View File

@ -130,18 +130,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Spinner spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
{
switch (state)
{
@ -157,17 +157,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
updateComplete(state == ArmedState.Hit, 0);
}

View File

@ -86,6 +86,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
{
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
// ReSharper disable once RedundantArgumentDefaultValue - removing the "redundant" default value triggers BaseMethodCallWithDefaultParameter
base.ApplyTransformsAt(time, false);
}

View File

@ -100,17 +100,17 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case DrawableSpinner d:
Spinner spinner = d.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
fixedMiddle.FadeColour(Color4.White);
using (BeginDelayedSequence(spinner.TimePreempt, true))
using (BeginDelayedSequence(spinner.TimePreempt))
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
}

View File

@ -89,10 +89,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Spinner spinner = d.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
/// <remarks>
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
/// This offset is negated to bring all constants into window-space.
/// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
/// </remarks>
protected const float SPINNER_TOP_OFFSET = 45f - 16f;
@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
double startTime = Math.Min(Time.Current, DrawableSpinner.HitStateUpdateTime - 400);
using (BeginAbsoluteSequence(startTime, true))
using (BeginAbsoluteSequence(startTime))
{
clear.FadeInFromZero(400, Easing.Out);
@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
const double fade_out_duration = 50;
using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true))
using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration))
clear.FadeOut(fade_out_duration);
}
else
@ -182,14 +182,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength))
spin.FadeOutFromOne(spinFadeOutLength);
break;
case DrawableSpinnerTick d:
if (state == ArmedState.Hit)
{
using (BeginAbsoluteSequence(d.HitStateUpdateTime, true))
using (BeginAbsoluteSequence(d.HitStateUpdateTime))
spin.FadeOut(300);
}

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class OsuLegacySkinTransformer : LegacySkinTransformer
{
private Lazy<bool> hasHitCircle;
private readonly Lazy<bool> hasHitCircle;
/// <summary>
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
@ -20,16 +20,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
/// </summary>
public const float LEGACY_CIRCLE_RADIUS = 64 - 5;
public OsuLegacySkinTransformer(ISkinSource source)
: base(source)
public OsuLegacySkinTransformer(ISkin skin)
: base(skin)
{
Source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
hasHitCircle = new Lazy<bool>(() => FindProvider(s => s.GetTexture("hitcircle") != null) != null);
hasHitCircle = new Lazy<bool>(() => GetTexture("hitcircle") != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@ -49,16 +43,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return followCircle;
case OsuSkinComponents.SliderBall:
// specular and nd layers must come from the same source as the ball texure.
var ballProvider = Source.FindProvider(s => s.GetTexture("sliderb") != null || s.GetTexture("sliderb0") != null);
var sliderBallContent = ballProvider.GetAnimation("sliderb", true, true, animationSeparator: "");
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "");
// todo: slider ball has a custom frame delay based on velocity
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
if (sliderBallContent != null)
return new LegacySliderBall(sliderBallContent, ballProvider);
return new LegacySliderBall(sliderBallContent, this);
return null;
@ -87,18 +78,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null;
case OsuSkinComponents.Cursor:
var cursorProvider = Source.FindProvider(s => s.GetTexture("cursor") != null);
if (cursorProvider != null)
return new LegacyCursor(cursorProvider);
if (GetTexture("cursor") != null)
return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
var trailProvider = Source.FindProvider(s => s.GetTexture("cursortrail") != null);
if (trailProvider != null)
return new LegacyCursorTrail(trailProvider);
if (GetTexture("cursortrail") != null)
return new LegacyCursorTrail(this);
return null;
@ -113,9 +100,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
case OsuSkinComponents.SpinnerBody:
bool hasBackground = Source.GetTexture("spinner-background") != null;
bool hasBackground = GetTexture("spinner-background") != null;
if (Source.GetTexture("spinner-top") != null && !hasBackground)
if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
@ -124,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
}
return Source.GetDrawableComponent(component);
return base.GetDrawableComponent(component);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
@ -132,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (lookup)
{
case OsuSkinColour colour:
return Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
return base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
case OsuSkinConfiguration osuLookup:
switch (osuLookup)
@ -146,14 +133,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
// See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
// HitCircleOverlayAboveNumer (with typo) should still be supported for now.
return Source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
Source.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
return base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
}
break;
}
return Source.GetConfig<TLookup, TValue>(lookup);
return base.GetConfig<TLookup, TValue>(lookup);
}
}
}

View File

@ -0,0 +1,104 @@
// 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.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
{
public static class OsuHitObjectGenerationUtils
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
private const float playfield_edge_ratio = 0.375f;
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Rotate a hit object away from the playfield edge, while keeping a constant distance
/// from the previous object.
/// </summary>
/// <remarks>
/// The extent of rotation depends on the position of the hit object. Hit objects
/// closer to the playfield edge will be rotated to a larger extent.
/// </remarks>
/// <param name="prevObjectPos">Position of the previous hit object.</param>
/// <param name="posRelativeToPrev">Position of the hit object to be rotated, relative to the previous hit object.</param>
/// <param name="rotationRatio">
/// The extent of rotation.
/// 0 means the hit object is never rotated.
/// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
/// </param>
/// <returns>The new position of the hit object, relative to the previous one.</returns>
public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f)
{
var relativeRotationDistance = 0f;
if (prevObjectPos.X < playfield_middle.X)
{
relativeRotationDistance = Math.Max(
(border_distance_x - prevObjectPos.X) / border_distance_x,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevObjectPos.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
relativeRotationDistance
);
}
if (prevObjectPos.Y < playfield_middle.Y)
{
relativeRotationDistance = Math.Max(
(border_distance_y - prevObjectPos.Y) / border_distance_y,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevObjectPos.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
relativeRotationDistance
);
}
return RotateVectorTowardsVector(
posRelativeToPrev,
playfield_middle - prevObjectPos,
Math.Min(1, relativeRotationDistance * rotationRatio)
);
}
/// <summary>
/// Rotates vector "initial" towards vector "destination".
/// </summary>
/// <param name="initial">The vector to be rotated.</param>
/// <param name="destination">The vector that "initial" should be rotated towards.</param>
/// <param name="rotationRatio">How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".</param>
/// <returns>The rotated vector.</returns>
public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio)
{
var initialAngleRad = MathF.Atan2(initial.Y, initial.X);
var destAngleRad = MathF.Atan2(destination.Y, destination.X);
var diff = destAngleRad - initialAngleRad;
while (diff < -MathF.PI) diff += 2 * MathF.PI;
while (diff > MathF.PI) diff -= 2 * MathF.PI;
var finalAngleRad = initialAngleRad + rotationRatio * diff;
return new Vector2(
initial.Length * MathF.Cos(finalAngleRad),
initial.Length * MathF.Sin(finalAngleRad)
);
}
}
}

View File

@ -4,7 +4,7 @@
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">

View File

@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.UpdateStartTimeStateTransforms();
using (BeginDelayedSequence(-ring_appear_offset, true))
using (BeginDelayedSequence(-ring_appear_offset))
targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint);
}

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Scoring;
@ -15,18 +14,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class TaikoLegacySkinTransformer : LegacySkinTransformer
{
private Lazy<bool> hasExplosion;
private readonly Lazy<bool> hasExplosion;
public TaikoLegacySkinTransformer(ISkinSource source)
: base(source)
public TaikoLegacySkinTransformer(ISkin skin)
: base(skin)
{
Source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
hasExplosion = new Lazy<bool>(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
hasExplosion = new Lazy<bool>(() => GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
@ -56,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit:
if (GetTexture("taikohitcircle") != null)
return new LegacyHit(taikoComponent.Component);
@ -91,7 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null;
case TaikoSkinComponents.TaikoExplosionMiss:
var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false);
if (missSprite != null)
return new LegacyHitExplosion(missSprite);
@ -100,7 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.TaikoExplosionOk:
case TaikoSkinComponents.TaikoExplosionGreat:
var hitName = getHitName(taikoComponent.Component);
var hitSprite = this.GetAnimation(hitName, true, false);
@ -132,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
}
return Source.GetDrawableComponent(component);
return base.GetDrawableComponent(component);
}
private string getHitName(TaikoSkinComponents component)
@ -155,13 +145,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override ISample GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is HitSampleInfo hitSampleInfo)
return Source.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
return base.GetSample(new LegacyTaikoSampleInfo(hitSampleInfo));
return base.GetSample(sampleInfo);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Source.GetConfig<TLookup, TValue>(lookup);
private class LegacyTaikoSampleInfo : HitSampleInfo
{
public LegacyTaikoSampleInfo(HitSampleInfo sampleInfo)

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new TaikoLegacySkinTransformer(skin);
public const string SHORT_NAME = "taiko";

View File

@ -19,7 +19,9 @@ using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Scores.IO;
using osu.Game.Users;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
@ -185,13 +187,62 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
private string hashFile(string filename)
[Test]
public async Task TestImportThenImportWithChangedHashedFile()
{
using (var s = File.OpenRead(filename))
return s.ComputeMD5Hash();
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
var imported = await LoadOszIntoOsu(osu);
await createScoreForBeatmap(osu, imported.Beatmaps.First());
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
// arbitrary write to hashed file
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
await sw.WriteLineAsync("// changed");
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
// check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
}
finally
{
Directory.Delete(extractedFolder, true);
}
}
finally
{
host.Exit();
}
}
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public async Task TestImportThenImportWithChangedFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
@ -294,6 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public async Task TestImportCorruptThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
@ -439,12 +491,11 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[TestCase(true)]
[TestCase(false)]
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
[Test]
public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}"))
{
try
{
@ -452,10 +503,8 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = await LoadOszIntoOsu(osu);
if (set)
imported.OnlineBeatmapSetID = 1234;
else
imported.Beatmaps.First().OnlineBeatmapID = 1234;
foreach (var b in imported.Beatmaps)
b.OnlineBeatmapID = null;
osu.Dependencies.Get<BeatmapManager>().Update(imported);
@ -895,7 +944,17 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
}
private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap)
{
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
{
OnlineScoreID = 2,
Beatmap = beatmap,
BeatmapInfoID = beatmap.ID
}, new ImportScoreTest.TestArchiveReader());
}
private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
{
var manager = osu.Dependencies.Get<BeatmapManager>();
@ -904,12 +963,18 @@ namespace osu.Game.Tests.Beatmaps.IO
: manager.GetAllUsableBeatmapSets().Count);
}
private void checkBeatmapCount(OsuGameBase osu, int expected)
private static string hashFile(string filename)
{
using (var s = File.OpenRead(filename))
return s.ComputeMD5Hash();
}
private static void checkBeatmapCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get<BeatmapManager>().QueryBeatmaps(_ => true).ToList().Count);
}
private void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestSingleSpan()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestRepeat()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestNonEvenTicks()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
Assert.That(events[0].Time, Is.EqualTo(start_time));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestLegacyLastTickOffset()
{
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
Assert.That(events[2].Time, Is.EqualTo(900));
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
const double velocity = 5;
const double min_distance = velocity * 10;
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0, default).ToArray();
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
Assert.Multiple(() =>
{

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Chat;
using osu.Game.Tests.Visual;
using osu.Game.Users;
namespace osu.Game.Tests.Chat
{
[HeadlessTest]
public class TestSceneChannelManager : OsuTestScene
{
private ChannelManager channelManager;
private int currentMessageId;
[SetUp]
public void Setup() => Schedule(() =>
{
var container = new ChannelManagerContainer();
Child = container;
channelManager = container.ChannelManager;
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("register request handling", () =>
{
currentMessageId = 0;
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(++currentMessageId)
{
IsAction = postMessage.Message.IsAction,
ChannelId = postMessage.Message.ChannelId,
Content = postMessage.Message.Content,
Links = postMessage.Message.Links,
Timestamp = postMessage.Message.Timestamp,
Sender = postMessage.Message.Sender
});
return true;
}
return false;
};
});
}
[Test]
public void TestCommandsPostedToCorrectChannelWhenNotCurrent()
{
Channel channel1 = null;
Channel channel2 = null;
AddStep("join 2 rooms", () =>
{
channelManager.JoinChannel(channel1 = createChannel(1, ChannelType.Public));
channelManager.JoinChannel(channel2 = createChannel(2, ChannelType.Public));
});
AddStep("select channel 1", () => channelManager.CurrentChannel.Value = channel1);
AddStep("post /me command to channel 2", () => channelManager.PostCommand("me dances", channel2));
AddAssert("/me command received by channel 2", () => channel2.Messages.Last().Content == "dances");
AddStep("post /np command to channel 2", () => channelManager.PostCommand("np", channel2));
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
}
private Channel createChannel(int id, ChannelType type) => new Channel(new User())
{
Id = id,
Name = $"Channel {id}",
Topic = $"Topic of channel {id} with type {type}",
Type = type,
};
private class ChannelManagerContainer : CompositeDrawable
{
[Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager();
public ChannelManagerContainer()
{
InternalChild = ChannelManager;
}
}
}
}

View File

@ -38,19 +38,28 @@ namespace osu.Game.Tests.Database
[Test]
public void TestDefaultsPopulationAndQuery()
{
Assert.That(query().Count, Is.EqualTo(0));
Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
Assert.That(query().Count, Is.EqualTo(3));
Assert.That(queryCount(), Is.EqualTo(3));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
private IQueryable<RealmKeyBinding> query() => realmContextFactory.Context.All<RealmKeyBinding>();
private int queryCount(GlobalAction? match = null)
{
using (var usage = realmContextFactory.GetForRead())
{
var results = usage.Realm.All<RealmKeyBinding>();
if (match.HasValue)
results = results.Where(k => k.ActionInt == (int)match.Value);
return results.Count();
}
}
[Test]
public void TestUpdateViaQueriedReference()
@ -59,25 +68,28 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer);
var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding);
using (var usage = realmContextFactory.GetForWrite())
using (var primaryUsage = realmContextFactory.GetForRead())
{
var binding = usage.Realm.ResolveReference(tsr);
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
usage.Commit();
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
var tsr = ThreadSafeReference.Create(backBinding);
using (var usage = realmContextFactory.GetForWrite())
{
var binding = usage.Realm.ResolveReference(tsr);
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
usage.Commit();
}
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
// check still correct after re-query.
backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
}
[TearDown]

View File

@ -0,0 +1,241 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckFewHitsoundsTest
{
private CheckFewHitsounds check;
private List<HitSampleInfo> notHitsounded;
private List<HitSampleInfo> hitsounded;
[SetUp]
public void Setup()
{
check = new CheckFewHitsounds();
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
hitsounded = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
new HitSampleInfo(HitSampleInfo.HIT_FINISH)
};
}
[Test]
public void TestHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 16; ++i)
{
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
if ((i + 1) % 2 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if ((i + 1) % 3 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
if ((i + 1) % 4 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertOk(hitObjects);
}
[Test]
public void TestHitsoundedWithBreak()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 32; ++i)
{
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
if ((i + 1) % 2 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if ((i + 1) % 3 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
if ((i + 1) % 4 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
// Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
if (i > 8 && i < 24)
continue;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertOk(hitObjects);
}
[Test]
public void TestLightlyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 30; ++i)
{
var samples = i % 8 == 0 ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertLongPeriodNegligible(hitObjects, count: 3);
}
[Test]
public void TestRarelyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 30; ++i)
{
var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
// Should prompt one warning between 1st and 16th, and another between 16th and 31st.
assertLongPeriodWarning(hitObjects, count: 2);
}
[Test]
public void TestExtremelyRarelyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 80; ++i)
{
var samples = i == 40 ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
// Should prompt one problem between 1st and 41st, and another between 41st and 81st.
assertLongPeriodProblem(hitObjects, count: 2);
}
[Test]
public void TestNotHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 20; ++i)
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
assertNoHitsounds(hitObjects);
}
[Test]
public void TestNestedObjectsHitsounded()
{
var ticks = new List<HitObject>();
for (int i = 1; i < 16; ++i)
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
{
Samples = hitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { nested });
}
[Test]
public void TestNestedObjectsRarelyHitsounded()
{
var ticks = new List<HitObject>();
for (int i = 1; i < 16; ++i)
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
{
Samples = hitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertLongPeriodWarning(new List<HitObject> { nested });
}
[Test]
public void TestConcurrentObjects()
{
var hitObjects = new List<HitObject>();
var ticks = new List<HitObject>();
for (int i = 1; i < 10; ++i)
ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
{
Samples = notHitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
hitObjects.Add(nested);
for (int i = 1; i <= 6; ++i)
hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
assertOk(hitObjects);
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
}
private void assertLongPeriodWarning(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
}
private void assertLongPeriodNegligible(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
}
private void assertNoHitsounds(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,289 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckMutedObjectsTest
{
private CheckMutedObjects check;
private ControlPointInfo cpi;
private const int volume_regular = 50;
private const int volume_low = 15;
private const int volume_muted = 5;
[SetUp]
public void Setup()
{
check = new CheckMutedObjects();
cpi = new ControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
}
[Test]
public void TestNormalControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitcircle });
}
[Test]
public void TestLowControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 1000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitcircle });
}
[Test]
public void TestMutedControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitcircle });
}
[Test]
public void TestNormalSampleVolume()
{
// The sample volume should take precedence over the control point volume.
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitcircle });
}
[Test]
public void TestLowSampleVolume()
{
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitcircle });
}
[Test]
public void TestMutedSampleVolume()
{
var hitcircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitcircle });
}
[Test]
public void TestNormalSampleVolumeSlider()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestMutedSampleVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedSampleVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 2250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
// Ends after the 5% control point.
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertLowVolume(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
}
private void assertMuted(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
}
private void assertMutedPassive(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject>
{
ControlPointInfo = cpi,
HitObjects = hitObjects
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

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 System.Collections.Generic;
using System.Threading;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Tests.Editing.Checks
{
public sealed class MockNestableHitObject : HitObject, IHasDuration
{
private readonly IEnumerable<HitObject> toBeNested;
public MockNestableHitObject(IEnumerable<HitObject> toBeNested, double startTime, double endTime)
{
this.toBeNested = toBeNested;
StartTime = startTime;
EndTime = endTime;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
foreach (var hitObject in toBeNested)
AddNested(hitObject);
}
public double EndTime { get; }
public double Duration
{
get => EndTime - StartTime;
set => throw new System.NotImplementedException();
}
}
}

View File

@ -17,7 +17,8 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
Task.Run(() => host.Run(osu));
Task.Run(() => host.Run(osu))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");

View File

@ -0,0 +1,41 @@
// 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;
namespace osu.Game.Tests.Localisation
{
[TestFixture]
public class BeatmapMetadataRomanisationTest
{
[Test]
public void TestRomanisation()
{
var metadata = new BeatmapMetadata
{
Artist = "Romanised Artist",
ArtistUnicode = "Unicode Artist",
Title = "Romanised title",
TitleUnicode = "Unicode Title"
};
var romanisableString = metadata.ToRomanisableString();
Assert.AreEqual(metadata.ToString(), romanisableString.Romanised);
Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original);
}
[Test]
public void TestRomanisationNoUnicode()
{
var metadata = new BeatmapMetadata
{
Artist = "Romanised Artist",
Title = "Romanised title"
};
var romanisableString = metadata.ToRomanisableString();
Assert.AreEqual(romanisableString.Romanised, romanisableString.Original);
}
}
}

View File

@ -14,6 +14,14 @@ namespace osu.Game.Tests.Mods
[TestFixture]
public class ModUtilsTest
{
[Test]
public void TestModIsNotCompatibleWithItself()
{
var mod = new Mock<CustomMod1>();
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object, mod.Object }, out var invalid), Is.False);
Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
}
[Test]
public void TestModIsCompatibleByItself()
{
@ -147,7 +155,7 @@ namespace osu.Game.Tests.Mods
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() },
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
new[] { typeof(MultiMod) }
},
// valid pair.

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