1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-19 17:03:02 +08:00

Merge branch 'master' into osu-target-mod

This commit is contained in:
Henry Lin 2021-07-06 12:02:51 +08:00 committed by GitHub
commit 50e316fca4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 703 additions and 226 deletions

View File

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

View File

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

View File

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

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

@ -194,9 +194,9 @@ namespace osu.Game.Rulesets.Catch.Tests
AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9)); AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9));
checkPlate(10); checkPlate(10);
AddAssert("caught objects are stacked", () => AddAssert("caught objects are stacked", () =>
catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) && catcher.CaughtObjects.All(obj => obj.Y <= 0) &&
catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) && catcher.CaughtObjects.Any(obj => obj.Y == 0) &&
catcher.CaughtObjects.Any(obj => obj.Y < -25)); catcher.CaughtObjects.Any(obj => obj.Y < 0));
} }
[Test] [Test]

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

@ -103,6 +103,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value); result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable<TValue>)result; 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 base.GetConfig<TLookup, TValue>(lookup); return base.GetConfig<TLookup, TValue>(lookup);

View File

@ -56,11 +56,6 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier; 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> /// <summary>
/// The amount by which caught fruit should be scaled down to fit on the plate. /// The amount by which caught fruit should be scaled down to fit on the plate.
/// </summary> /// </summary>
@ -84,8 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherAnimationState CurrentState public CatcherAnimationState CurrentState
{ {
get => body.AnimationState.Value; get => Body.AnimationState.Value;
private set => body.AnimationState.Value = value; private set => Body.AnimationState.Value = value;
} }
/// <summary> /// <summary>
@ -108,18 +103,22 @@ namespace osu.Game.Rulesets.Catch.UI
} }
} }
public Direction VisualDirection /// <summary>
{ /// The currently facing direction.
get => Scale.X > 0 ? Direction.Right : Direction.Left; /// </summary>
set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y); 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> /// <summary>
/// Width of the area that can be used to attempt catches during gameplay. /// Width of the area that can be used to attempt catches during gameplay.
/// </summary> /// </summary>
private readonly float catchWidth; private readonly float catchWidth;
private readonly SkinnableCatcher body; internal readonly SkinnableCatcher Body;
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR; private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
@ -157,8 +156,10 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
// offset fruit vertically to better place "above" the plate.
Y = -5
}, },
body = new SkinnableCatcher(), Body = new SkinnableCatcher(),
hitExplosionContainer = new HitExplosionContainer hitExplosionContainer = new HitExplosionContainer
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -347,6 +348,8 @@ namespace osu.Game.Rulesets.Catch.UI
trails.HyperDashTrailsColour = hyperDashColour; trails.HyperDashTrailsColour = hyperDashColour;
trails.EndGlowSpritesColour = hyperDashEndGlowColour; trails.EndGlowSpritesColour = hyperDashEndGlowColour;
flipCatcherPlate = skin.GetConfig<CatchSkinConfiguration, bool>(CatchSkinConfiguration.FlipCatcherPlate)?.Value ?? true;
runHyperDashStateTransition(HyperDashing); runHyperDashStateTransition(HyperDashing);
} }
@ -354,6 +357,10 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
base.Update(); base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
Body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting. // Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) || if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X)) (hyperDashDirection < 0 && hyperDashTargetPosition > X))
@ -388,9 +395,6 @@ namespace osu.Game.Rulesets.Catch.UI
float adjustedRadius = displayRadius * lenience_adjust; float adjustedRadius = displayRadius * lenience_adjust;
float checkDistance = MathF.Pow(adjustedRadius, 2); 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)) while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance))
{ {
position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius); position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius);
@ -465,7 +469,7 @@ namespace osu.Game.Rulesets.Catch.UI
break; break;
case DroppedObjectAnimation.Explode: 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.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine);
d.MoveToX(d.X + originalX * 6, 1000); d.MoveToX(d.X + originalX * 6, 1000);
d.FadeOut(750); d.FadeOut(750);

View File

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

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
/// <remarks> /// <remarks>
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards. /// 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) /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
/// </remarks> /// </remarks>
protected const float SPINNER_TOP_OFFSET = 45f - 16f; protected const float SPINNER_TOP_OFFSET = 45f - 16f;

View File

@ -38,19 +38,28 @@ namespace osu.Game.Tests.Database
[Test] [Test]
public void TestDefaultsPopulationAndQuery() public void TestDefaultsPopulationAndQuery()
{ {
Assert.That(query().Count, Is.EqualTo(0)); Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer); 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(queryCount(GlobalAction.Back), Is.EqualTo(1));
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2)); 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] [Test]
public void TestUpdateViaQueriedReference() public void TestUpdateViaQueriedReference()
@ -59,7 +68,9 @@ namespace osu.Game.Tests.Database
keyBindingStore.Register(testContainer); keyBindingStore.Register(testContainer);
var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back); using (var primaryUsage = realmContextFactory.GetForRead())
{
var backBinding = primaryUsage.Realm.All<RealmKeyBinding>().Single(k => k.ActionInt == (int)GlobalAction.Back);
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
@ -76,9 +87,10 @@ namespace osu.Game.Tests.Database
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. // check still correct after re-query.
backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back); 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 }));
} }
}
[TearDown] [TearDown]
public void TearDown() public void TearDown()

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

@ -0,0 +1,123 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
namespace osu.Game.Tests.NonVisual
{
public class FirstAvailableHitWindowsTest
{
private TestDrawableRuleset testDrawableRuleset;
[SetUp]
public void Setup()
{
testDrawableRuleset = new TestDrawableRuleset();
}
[Test]
public void TestResultIfOnlyParentHitWindowIsEmpty()
{
var testObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(new HitWindows());
testObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, nested.HitWindows);
}
[Test]
public void TestResultIfParentHitWindowsIsNotEmpty()
{
var testObject = new TestHitObject(new HitWindows());
HitObject nested = new TestHitObject(new HitWindows());
testObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { testObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, testObject.HitWindows);
}
[Test]
public void TestResultIfParentAndChildHitWindowsAreEmpty()
{
var firstObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(HitWindows.Empty);
firstObject.AddNested(nested);
var secondObject = new TestHitObject(new HitWindows());
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject, secondObject };
Assert.AreSame(testDrawableRuleset.FirstAvailableHitWindows, secondObject.HitWindows);
}
[Test]
public void TestResultIfAllHitWindowsAreEmpty()
{
var firstObject = new TestHitObject(HitWindows.Empty);
HitObject nested = new TestHitObject(HitWindows.Empty);
firstObject.AddNested(nested);
testDrawableRuleset.HitObjects = new List<HitObject> { firstObject };
Assert.IsNull(testDrawableRuleset.FirstAvailableHitWindows);
}
[SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")]
private class TestDrawableRuleset : DrawableRuleset
{
public List<HitObject> HitObjects;
public override IEnumerable<HitObject> Objects => HitObjects;
public override event Action<JudgementResult> NewResult;
public override event Action<JudgementResult> RevertResult;
public override Playfield Playfield { get; }
public override Container Overlays { get; }
public override Container FrameStableComponents { get; }
public override IFrameStableClock FrameStableClock { get; }
internal override bool FrameStablePlayback { get; set; }
public override IReadOnlyList<Mod> Mods { get; }
public override double GameplayStartTime { get; }
public override GameplayCursorContainer Cursor { get; }
public TestDrawableRuleset()
: base(new OsuRuleset())
{
// won't compile without this.
NewResult?.Invoke(null);
RevertResult?.Invoke(null);
}
public override void SetReplayScore(Score replayScore) => throw new NotImplementedException();
public override void SetRecordTarget(Score score) => throw new NotImplementedException();
public override void RequestResume(Action continueResume) => throw new NotImplementedException();
public override void CancelResume() => throw new NotImplementedException();
}
public class TestHitObject : HitObject
{
public TestHitObject(HitWindows hitWindows)
{
HitWindows = hitWindows;
HitWindows.SetDifficulty(0.5f);
}
public new void AddNested(HitObject nested) => base.AddNested(nested);
}
}
}

View File

@ -9,6 +9,8 @@ using osu.Game.Online.Rooms;
using osu.Game.Online.Solo; using osu.Game.Online.Solo;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -35,6 +37,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for token request", () => Player.TokenCreationRequested); AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
@ -52,6 +57,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for token request", () => Player.TokenCreationRequested); AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen); AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
@ -67,10 +75,29 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for token request", () => Player.TokenCreationRequested); AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("exit", () => Player.Exit()); AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null); AddAssert("ensure no submission", () => Player.SubmittedScore == null);
} }
[Test]
public void TestNoSubmissionOnEmptyFail()
{
prepareTokenResponse(true);
CreateTest(() => allowFail = true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test] [Test]
public void TestSubmissionOnFail() public void TestSubmissionOnFail()
{ {
@ -79,12 +106,28 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() => allowFail = true); CreateTest(() => allowFail = true);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested); AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit()); AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
} }
[Test]
public void TestNoSubmissionOnEmptyExit()
{
prepareTokenResponse(true);
CreateTest(() => allowFail = false);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
[Test] [Test]
public void TestSubmissionOnExit() public void TestSubmissionOnExit()
{ {
@ -93,10 +136,27 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() => allowFail = false); CreateTest(() => allowFail = false);
AddUntilStep("wait for token request", () => Player.TokenCreationRequested); AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddStep("exit", () => Player.Exit()); AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
} }
private void addFakeHit()
{
AddUntilStep("wait for first result", () => Player.Results.Count > 0);
AddStep("force successfuly hit", () =>
{
Player.ScoreProcessor.RevertResult(Player.Results.First());
Player.ScoreProcessor.ApplyResult(new OsuJudgementResult(Beatmap.Value.Beatmap.HitObjects.First(), new OsuJudgement())
{
Type = HitResult.Great,
});
});
}
private void prepareTokenResponse(bool validToken) private void prepareTokenResponse(bool validToken)
{ {
AddStep("Prepare test API", () => AddStep("Prepare test API", () =>

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "All Metrics", Version = "All Metrics",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Source = "osu!lazer", Source = "osu!",
Tags = "this beatmap has all the metrics", Tags = "this beatmap has all the metrics",
}, },
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "Only Ratings", Version = "Only Ratings",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Source = "osu!lazer", Source = "osu!",
Tags = "this beatmap has ratings metrics but not retries or fails", Tags = "this beatmap has ratings metrics but not retries or fails",
}, },
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "Only Retries and Fails", Version = "Only Retries and Fails",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Source = "osu!lazer", Source = "osu!",
Tags = "this beatmap has retries and fails but no ratings", Tags = "this beatmap has retries and fails but no ratings",
}, },
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
@ -149,7 +149,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Version = "No Metrics", Version = "No Metrics",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
Source = "osu!lazer", Source = "osu!",
Tags = "this beatmap has no metrics", Tags = "this beatmap has no metrics",
}, },
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty

View File

@ -103,14 +103,11 @@ namespace osu.Game.Tests.Visual.UserInterface
var easierMods = osu.GetModsFor(ModType.DifficultyReduction); var easierMods = osu.GetModsFor(ModType.DifficultyReduction);
var harderMods = osu.GetModsFor(ModType.DifficultyIncrease); var harderMods = osu.GetModsFor(ModType.DifficultyIncrease);
var conversionMods = osu.GetModsFor(ModType.Conversion);
var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
var targetMod = conversionMods.FirstOrDefault(m => m is OsuModTarget);
var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
@ -118,8 +115,6 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiMod(doubleTimeMod); testMultiMod(doubleTimeMod);
testIncompatibleMods(easy, hardRock); testIncompatibleMods(easy, hardRock);
testDeselectAll(easierMods.Where(m => !(m is MultiMod))); testDeselectAll(easierMods.Where(m => !(m is MultiMod)));
testUnimplementedMod(targetMod);
} }
[Test] [Test]
@ -249,6 +244,19 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2); AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType<ModButton>().Count(b => b.Selected) == 2);
} }
[Test]
public void TestUnimplementedModIsUnselectable()
{
var testRuleset = new TestUnimplementedModOsuRuleset();
changeTestRuleset(testRuleset.RulesetInfo);
var conversionMods = testRuleset.GetModsFor(ModType.Conversion);
var unimplementedMod = conversionMods.FirstOrDefault(m => m is TestUnimplementedMod);
testUnimplementedMod(unimplementedMod);
}
private void testSingleMod(Mod mod) private void testSingleMod(Mod mod)
{ {
selectNext(mod); selectNext(mod);
@ -343,6 +351,12 @@ namespace osu.Game.Tests.Visual.UserInterface
waitForLoad(); waitForLoad();
} }
private void changeTestRuleset(RulesetInfo rulesetInfo)
{
AddStep($"change ruleset to {rulesetInfo.Name}", () => { Ruleset.Value = rulesetInfo; });
waitForLoad();
}
private void waitForLoad() => private void waitForLoad() =>
AddUntilStep("wait for icons to load", () => modSelect.AllLoaded); AddUntilStep("wait for icons to load", () => modSelect.AllLoaded);
@ -401,5 +415,24 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
protected override bool Stacked => false; protected override bool Stacked => false;
} }
private class TestUnimplementedMod : Mod
{
public override string Name => "Unimplemented mod";
public override string Acronym => "UM";
public override string Description => "A mod that is not implemented.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.Conversion;
}
private class TestUnimplementedModOsuRuleset : OsuRuleset
{
public override IEnumerable<Mod> GetModsFor(ModType type)
{
if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
return base.GetModsFor(type);
}
}
} }
} }

View File

@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
{ {
if (!base.CanReuseExisting(existing, import)) if (!base.CanSkipImport(existing, import))
return false; return false;
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);

View File

@ -94,7 +94,10 @@ namespace osu.Game.Beatmaps
public RomanisableString ToRomanisableString() public RomanisableString ToRomanisableString()
{ {
string author = Author == null ? string.Empty : $"({Author})"; string author = Author == null ? string.Empty : $"({Author})";
return new RomanisableString($"{ArtistUnicode} - {TitleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode;
var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode;
return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim());
} }
[JsonIgnore] [JsonIgnore]

View File

@ -353,8 +353,6 @@ namespace osu.Game.Database
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
delayEvents();
bool checkedExisting = false; bool checkedExisting = false;
TModel existing = null; TModel existing = null;
@ -394,6 +392,8 @@ namespace osu.Game.Database
} }
} }
delayEvents();
try try
{ {
LogForModel(item, @"Beginning import..."); LogForModel(item, @"Beginning import...");

View File

@ -9,6 +9,7 @@ namespace osu.Game.Database
{ {
/// <summary> /// <summary>
/// The main realm context, bound to the update thread. /// The main realm context, bound to the update thread.
/// If querying from a non-update thread is needed, use <see cref="GetForRead"/> or <see cref="GetForWrite"/> to receive a context instead.
/// </summary> /// </summary>
Realm Context { get; } Realm Context { get; }

View File

@ -77,6 +77,9 @@ namespace osu.Game.Database
{ {
cmd.CommandText = "PRAGMA journal_mode=WAL;"; cmd.CommandText = "PRAGMA journal_mode=WAL;";
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
cmd.CommandText = "PRAGMA foreign_keys=OFF;";
cmd.ExecuteNonQuery();
} }
} }
catch catch

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -38,11 +40,18 @@ namespace osu.Game.Database
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes"); private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages"); private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
private readonly object updateContextLock = new object();
private Realm context; private Realm context;
public Realm Context public Realm Context
{ {
get get
{
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException($"Use {nameof(GetForRead)} or {nameof(GetForWrite)} when performing realm operations from a non-update thread");
lock (updateContextLock)
{ {
if (context == null) if (context == null)
{ {
@ -55,6 +64,7 @@ namespace osu.Game.Database
return context; return context;
} }
} }
}
public RealmContextFactory(Storage storage) public RealmContextFactory(Storage storage)
{ {
@ -107,9 +117,12 @@ namespace osu.Game.Database
{ {
base.Update(); base.Update();
lock (updateContextLock)
{
if (context?.Refresh() == true) if (context?.Refresh() == true)
refreshes.Value++; refreshes.Value++;
} }
}
private Realm createContext() private Realm createContext()
{ {
@ -154,9 +167,15 @@ namespace osu.Game.Database
private void flushContexts() private void flushContexts()
{ {
Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database); Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database);
Debug.Assert(blockingLock.CurrentCount == 0);
var previousContext = context; Realm previousContext;
lock (updateContextLock)
{
previousContext = context;
context = null; context = null;
}
// wait for all threaded usages to finish // wait for all threaded usages to finish
while (active_usages.Value > 0) while (active_usages.Value > 0)

View File

@ -320,6 +320,7 @@ namespace osu.Game.Online.Chat
JoinMultiplayerMatch, JoinMultiplayerMatch,
Spectate, Spectate,
OpenUserProfile, OpenUserProfile,
SearchBeatmapSet,
OpenWiki, OpenWiki,
Custom, Custom,
} }

View File

@ -305,6 +305,10 @@ namespace osu.Game
ShowChannel(link.Argument); ShowChannel(link.Argument);
break; break;
case LinkAction.SearchBeatmapSet:
SearchBeatmapSet(link.Argument);
break;
case LinkAction.OpenEditorTimestamp: case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch: case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate: case LinkAction.Spectate:
@ -375,6 +379,12 @@ namespace osu.Game
/// <param name="beatmapId">The beatmap to show.</param> /// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId)); public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId));
/// <summary>
/// Shows the beatmap listing overlay, with the given <paramref name="query"/> in the search box.
/// </summary>
/// <param name="query">The query to search for.</param>
public void SearchBeatmapSet(string query) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithSearch(query));
/// <summary> /// <summary>
/// Show a wiki's page as an overlay /// Show a wiki's page as an overlay
/// </summary> /// </summary>

View File

@ -80,7 +80,7 @@ namespace osu.Game
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release"); return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
var version = AssemblyVersion; var version = AssemblyVersion;
return $@"{version.Major}.{version.Minor}.{version.Build}"; return $@"{version.Major}.{version.Minor}.{version.Build}-lazer";
} }
} }
@ -162,7 +162,7 @@ namespace osu.Game
public OsuGameBase() public OsuGameBase()
{ {
UseDevelopmentServer = DebugUtils.IsDebugBuild; UseDevelopmentServer = DebugUtils.IsDebugBuild;
Name = @"osu!lazer"; Name = @"osu!";
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -122,6 +122,9 @@ namespace osu.Game.Overlays.BeatmapListing
sortControlBackground.Colour = colourProvider.Background5; sortControlBackground.Colour = colourProvider.Background5;
} }
public void Search(string query)
=> searchControl.Query.Value = query;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();

View File

@ -89,6 +89,12 @@ namespace osu.Game.Overlays
}; };
} }
public void ShowWithSearch(string query)
{
filterControl.Search(query);
Show();
}
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader(); protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6; protected override Color4 BackgroundColour => ColourProvider.Background6;

View File

@ -8,15 +8,12 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
{ {
public class Info : Container public class Info : Container
{ {
private const float transition_duration = 250;
private const float metadata_width = 175; private const float metadata_width = 175;
private const float spacing = 20; private const float spacing = 20;
private const float base_height = 220; private const float base_height = 220;
@ -60,7 +57,7 @@ namespace osu.Game.Overlays.BeatmapSet
Child = new Container Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new MetadataSection("Description"), Child = new MetadataSection(MetadataType.Description),
}, },
}, },
new Container new Container
@ -78,10 +75,10 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Full, Direction = FillDirection.Full,
Children = new[] Children = new[]
{ {
source = new MetadataSection("Source"), source = new MetadataSection(MetadataType.Source),
genre = new MetadataSection("Genre") { Width = 0.5f }, genre = new MetadataSection(MetadataType.Genre) { Width = 0.5f },
language = new MetadataSection("Language") { Width = 0.5f }, language = new MetadataSection(MetadataType.Language) { Width = 0.5f },
tags = new MetadataSection("Tags"), tags = new MetadataSection(MetadataType.Tags),
}, },
}, },
}, },
@ -135,48 +132,5 @@ namespace osu.Game.Overlays.BeatmapSet
successRateBackground.Colour = colourProvider.Background4; successRateBackground.Colour = colourProvider.Background4;
background.Colour = colourProvider.Background5; background.Colour = colourProvider.Background5;
} }
private class MetadataSection : FillFlowContainer
{
private readonly TextFlowContainer textFlow;
public string Text
{
set
{
if (string.IsNullOrEmpty(value))
{
Hide();
return;
}
this.FadeIn(transition_duration);
textFlow.Clear();
textFlow.AddText(value, s => s.Font = s.Font.With(size: 12));
}
}
public MetadataSection(string title)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Spacing = new Vector2(5f);
InternalChildren = new Drawable[]
{
new OsuSpriteText
{
Text = title,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Margin = new MarginPadding { Top = 15 },
},
textFlow = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
};
}
}
} }
} }

View File

@ -0,0 +1,115 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
public class MetadataSection : Container
{
private readonly FillFlowContainer textContainer;
private readonly MetadataType type;
private TextFlowContainer textFlow;
private const float transition_duration = 250;
public MetadataSection(MetadataType type)
{
this.type = type;
Alpha = 0;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = textContainer = new FillFlowContainer
{
Alpha = 0,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 15 },
Spacing = new Vector2(5),
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
Text = this.type.ToString(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
},
},
};
}
public string Text
{
set
{
if (string.IsNullOrEmpty(value))
{
this.FadeOut(transition_duration);
return;
}
this.FadeIn(transition_duration);
setTextAsync(value);
}
}
private void setTextAsync(string text)
{
LoadComponentAsync(new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Colour = Color4.White.Opacity(0.75f),
}, loaded =>
{
textFlow?.Expire();
switch (type)
{
case MetadataType.Tags:
string[] tags = text.Split(" ");
for (int i = 0; i <= tags.Length - 1; i++)
{
loaded.AddLink(tags[i], LinkAction.SearchBeatmapSet, tags[i]);
if (i != tags.Length - 1)
loaded.AddText(" ");
}
break;
case MetadataType.Source:
loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
break;
default:
loaded.AddText(text);
break;
}
textContainer.Add(textFlow = loaded);
// fade in if we haven't yet.
textContainer.FadeIn(transition_duration);
});
}
}
}

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.
namespace osu.Game.Overlays.BeatmapSet
{
public enum MetadataType
{
Tags,
Source,
Description,
Genre,
Language
}
}

View File

@ -191,7 +191,7 @@ namespace osu.Game.Overlays.Volume
bgProgress.Current.Value = 0.75f; bgProgress.Current.Value = 0.75f;
} }
private int displayVolumeInt; private int? displayVolumeInt;
private double displayVolume; private double displayVolume;
@ -200,9 +200,6 @@ namespace osu.Game.Overlays.Volume
get => displayVolume; get => displayVolume;
set set
{ {
if (value == displayVolume)
return;
displayVolume = value; displayVolume = value;
int intValue = (int)Math.Round(displayVolume * 100); int intValue = (int)Math.Round(displayVolume * 100);
@ -218,7 +215,7 @@ namespace osu.Game.Overlays.Volume
else else
{ {
maxGlow.EffectColour = Color4.Transparent; maxGlow.EffectColour = Color4.Transparent;
text.Text = displayVolumeInt.ToString(CultureInfo.CurrentCulture); text.Text = intValue.ToString(CultureInfo.CurrentCulture);
} }
volumeCircle.Current.Value = displayVolume * 0.75f; volumeCircle.Current.Value = displayVolume * 0.75f;

View File

@ -489,15 +489,15 @@ namespace osu.Game.Rulesets.UI
{ {
get get
{ {
foreach (var h in Objects) foreach (var hitObject in Objects)
{ {
if (h.HitWindows.WindowFor(HitResult.Miss) > 0) if (hitObject.HitWindows.WindowFor(HitResult.Miss) > 0)
return h.HitWindows; return hitObject.HitWindows;
foreach (var n in h.NestedHitObjects) foreach (var nested in hitObject.NestedHitObjects)
{ {
if (h.HitWindows.WindowFor(HitResult.Miss) > 0) if (nested.HitWindows.WindowFor(HitResult.Miss) > 0)
return n.HitWindows; return nested.HitWindows;
} }
} }

View File

@ -208,7 +208,7 @@ namespace osu.Game.Scoring
} }
else else
{ {
// This score is guaranteed to be an osu!lazer score. // This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values. // The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum(); beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum();
} }

View File

@ -230,7 +230,7 @@ namespace osu.Game.Screens.Menu
"New features are coming online every update. Make sure to stay up-to-date!", "New features are coming online every update. Make sure to stay up-to-date!",
"If you find the UI too large or small, try adjusting UI scale in settings!", "If you find the UI too large or small, try adjusting UI scale in settings!",
"Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!", "Try adjusting the \"Screen Scaling\" mode to change your gameplay or UI area, even in fullscreen!",
"For now, what used to be \"osu!direct\" is available to all users on lazer. You can access it anywhere using Ctrl-D!", "What used to be \"osu!direct\" is available to all users just like on the website. You can access it anywhere using Ctrl-D!",
"Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!", "Seeking in replays is available by dragging on the difficulty bar at the bottom of the screen!",
"Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!", "Multithreading support means that even with low \"FPS\" your input and judgements will be accurate!",
"Try scrolling down in the mod select panel to find a bunch of new fun mods!", "Try scrolling down in the mod select panel to find a bunch of new fun mods!",

View File

@ -130,6 +130,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock. // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock.
if (timeDelta < -SYNC_TARGET) if (timeDelta < -SYNC_TARGET)
{ {
// Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock
// when it is required to be running (ie. if all players are ahead of the master).
clock.IsCatchingUp = false;
clock.Stop(); clock.Stop();
continue; continue;
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
@ -144,6 +145,10 @@ namespace osu.Game.Screens.Play
if (scoreSubmissionSource != null) if (scoreSubmissionSource != null)
return scoreSubmissionSource.Task; return scoreSubmissionSource.Task;
// if the user never hit anything, this score should not be counted in any way.
if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0))
return Task.CompletedTask;
scoreSubmissionSource = new TaskCompletionSource<bool>(); scoreSubmissionSource = new TaskCompletionSource<bool>();
var request = CreateSubmissionRequest(score, token.Value); var request = CreateSubmissionRequest(score, token.Value);

View File

@ -1,24 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osuTK; using System.Linq;
using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using System.Linq;
using osu.Game.Online.API;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Screens.Select.Details;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
using osu.Game.Screens.Select.Details;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
{ {
@ -128,13 +129,11 @@ namespace osu.Game.Screens.Select
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
LayoutDuration = transition_duration, LayoutDuration = transition_duration,
LayoutEasing = Easing.OutQuad, LayoutEasing = Easing.OutQuad,
Spacing = new Vector2(spacing * 2),
Margin = new MarginPadding { Top = spacing * 2 },
Children = new[] Children = new[]
{ {
description = new MetadataSection("Description"), description = new MetadataSection(MetadataType.Description),
source = new MetadataSection("Source"), source = new MetadataSection(MetadataType.Source),
tags = new MetadataSection("Tags"), tags = new MetadataSection(MetadataType.Tags),
}, },
}, },
}, },
@ -290,73 +289,5 @@ namespace osu.Game.Screens.Select
}; };
} }
} }
private class MetadataSection : Container
{
private readonly FillFlowContainer textContainer;
private TextFlowContainer textFlow;
public MetadataSection(string title)
{
Alpha = 0;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = textContainer = new FillFlowContainer
{
Alpha = 0,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(spacing / 2),
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
Text = title,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
},
},
};
}
public string Text
{
set
{
if (string.IsNullOrEmpty(value))
{
this.FadeOut(transition_duration);
return;
}
this.FadeIn(transition_duration);
setTextAsync(value);
}
}
private void setTextAsync(string text)
{
LoadComponentAsync(new OsuTextFlowContainer(s => s.Font = s.Font.With(size: 14))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Colour = Color4.White.Opacity(0.75f),
Text = text
}, loaded =>
{
textFlow?.Expire();
textContainer.Add(textFlow = loaded);
// fade in if we haven't yet.
textContainer.FadeIn(transition_duration);
});
}
}
} }
} }

View File

@ -7,7 +7,7 @@ using osuTK.Graphics;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
/// <summary> /// <summary>
/// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only. /// Compatibility methods to apply osu!stable quirks to colours. Should be used for legacy skins only.
/// </summary> /// </summary>
public static class LegacyColourCompatibility public static class LegacyColourCompatibility
{ {

View File

@ -46,7 +46,7 @@ namespace osu.Game.Skinning
public static SkinInfo Default { get; } = new SkinInfo public static SkinInfo Default { get; } = new SkinInfo
{ {
ID = DEFAULT_SKIN, ID = DEFAULT_SKIN,
Name = "osu!lazer", Name = "osu! (triangles)",
Creator = "team osu!", Creator = "team osu!",
InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo() InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo()
}; };

View File

@ -90,7 +90,7 @@ namespace osu.Game.Updater
public UpdateCompleteNotification(string version) public UpdateCompleteNotification(string version)
{ {
this.version = version; this.version = version;
Text = $"You are now running osu!lazer {version}.\nClick to see what's new!"; Text = $"You are now running osu! {version}.\nClick to see what's new!";
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]