Compare commits
133 Commits
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1006.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1012.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@@ -2,10 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -137,5 +140,53 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case CatchModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case CatchModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case CatchModHalfTime:
|
||||
case CatchModDaycore:
|
||||
multiplier *= 0.3;
|
||||
break;
|
||||
|
||||
case CatchModHidden:
|
||||
multiplier *= scoreV2 ? 1.0 : 1.06;
|
||||
break;
|
||||
|
||||
case CatchModHardRock:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case CatchModDoubleTime:
|
||||
case CatchModNightcore:
|
||||
multiplier *= 1.06;
|
||||
break;
|
||||
|
||||
case CatchModFlashlight:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case CatchModRelax:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
{
|
||||
@@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
|
||||
public DefaultCatcher()
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
InternalChild = sprite = new Sprite
|
||||
{
|
||||
@@ -32,6 +34,15 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// matches stable's origin position since we're using the same catcher sprite.
|
||||
// see LegacyCatcher for more information.
|
||||
OriginPosition = new Vector2(DrawWidth / 2, 16f);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore store, Bindable<CatcherAnimationState> currentState)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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 osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public abstract partial class LegacyCatcher : CompositeDrawable
|
||||
{
|
||||
protected LegacyCatcher()
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
// in stable, catcher sprites are displayed in their raw size. stable also has catcher sprites displayed with the following scale factors applied:
|
||||
// 1. 0.5x, affecting all sprites in the playfield, computed here based on lazer's catch playfield dimensions (see WIDTH/HEIGHT constants in CatchPlayfield),
|
||||
// source: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L483-L494
|
||||
// 2. 0.7x, a constant scale applied to all catcher sprites on construction.
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Scale = new Vector2(0.5f * 0.7f);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// stable sets the Y origin position of the catcher to 16px in order for the catching range and OD scaling to align with the top of the catcher's plate in the default skin.
|
||||
OriginPosition = new Vector2(DrawWidth / 2, 16f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,12 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCatcherNew : CompositeDrawable
|
||||
public partial class LegacyCatcherNew : LegacyCatcher
|
||||
{
|
||||
[Resolved]
|
||||
private Bindable<CatcherAnimationState> currentState { get; set; } = null!;
|
||||
@@ -23,25 +21,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
private Drawable currentDrawable = null!;
|
||||
|
||||
public LegacyCatcherNew()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
foreach (var state in Enum.GetValues<CatcherAnimationState>())
|
||||
{
|
||||
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
|
||||
{
|
||||
d.Anchor = Anchor.TopCentre;
|
||||
d.Origin = Anchor.TopCentre;
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
d.Size = Vector2.One;
|
||||
d.FillMode = FillMode.Fit;
|
||||
d.Alpha = 0;
|
||||
}));
|
||||
AddInternal(drawables[state] = getDrawableFor(state).With(d => d.Alpha = 0));
|
||||
}
|
||||
|
||||
currentDrawable = drawables[CatcherAnimationState.Idle];
|
||||
|
||||
@@ -3,30 +3,21 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCatcherOld : CompositeDrawable
|
||||
public partial class LegacyCatcherOld : LegacyCatcher
|
||||
{
|
||||
public LegacyCatcherOld()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
InternalChild = (skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty()).With(d =>
|
||||
{
|
||||
d.Anchor = Anchor.TopCentre;
|
||||
d.Origin = Anchor.TopCentre;
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
d.Size = Vector2.One;
|
||||
d.FillMode = FillMode.Fit;
|
||||
});
|
||||
InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatchPlayfieldAdjustmentContainer()
|
||||
{
|
||||
// because we are using centre anchor/origin, we will need to limit visibility in the future
|
||||
// to ensure tall windows do not get a readability advantage.
|
||||
// it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values
|
||||
// which are compatible with TopCentre alignment.
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
// we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8.
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Y = (1 - playfield_size_adjust) / 4 * 3;
|
||||
|
||||
Size = new Vector2(playfield_size_adjust);
|
||||
|
||||
@@ -42,18 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private partial class ScalingContainer : Container
|
||||
{
|
||||
public ScalingContainer()
|
||||
{
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// in stable, fruit fall vertically from -100 to 340.
|
||||
// to emulate this, we want to make our playfield 440 gameplay pixels high.
|
||||
// we then offset it -100 vertically in the position set below.
|
||||
const float stable_v_offset_ratio = 440 / 384f;
|
||||
// in stable, fruit fall vertically from 100 pixels above the playfield top down to the catcher's Y position (i.e. -100 to 340),
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjects/Fruits/HitCircleFruits.cs#L65
|
||||
// we already have the playfield positioned similar to stable (see CatchPlayfieldAdjustmentContainer constructor),
|
||||
// so we only need to increase this container's height 100 pixels above the playfield, and offset it to have the bottom at 340 rather than 384.
|
||||
const float stable_fruit_start_position = -100;
|
||||
const float stable_catcher_y_position = 340;
|
||||
const float playfield_v_size_adjustment = (stable_catcher_y_position - stable_fruit_start_position) / CatchPlayfield.HEIGHT;
|
||||
const float playfield_v_catcher_offset = stable_catcher_y_position - CatchPlayfield.HEIGHT;
|
||||
|
||||
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
|
||||
Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X);
|
||||
Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale);
|
||||
Scale = new Vector2(Parent!.ChildSize.X / CatchPlayfield.WIDTH);
|
||||
Position = new Vector2(0f, playfield_v_catcher_offset * Scale.Y);
|
||||
Size = Vector2.Divide(new Vector2(1, playfield_v_size_adjustment), Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// The size of the catcher at 1x scale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is mainly used to compute catching range, the actual catcher size may differ based on skin implementation and sprite textures.
|
||||
/// This is also equivalent to the "catcherWidth" property in osu-stable when the game field and beatmap difficulty are set to default values.
|
||||
/// </remarks>
|
||||
/// <seealso cref="CatchPlayfield.WIDTH"/>
|
||||
/// <seealso cref="CatchPlayfield.HEIGHT"/>
|
||||
/// <seealso cref="IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY"/>
|
||||
public const float BASE_SIZE = 106.75f;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -6,7 +6,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
@@ -26,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
: base(new CatchSkinComponentLookup(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
|
||||
OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE;
|
||||
Origin = Anchor.TopCentre;
|
||||
CentreComponent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
c.Add(hitExplosionPools[poolIndex].Get(e =>
|
||||
{
|
||||
e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
|
||||
e.Apply(new JudgementResult(new HitObject(), new ManiaJudgement()));
|
||||
|
||||
e.Anchor = Anchor.Centre;
|
||||
e.Origin = Anchor.Centre;
|
||||
|
||||
@@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
@@ -73,7 +72,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
@@ -92,7 +90,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
@@ -111,7 +108,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -129,7 +125,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -149,7 +144,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -169,7 +163,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
}
|
||||
|
||||
@@ -188,10 +181,31 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xox o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + 1),
|
||||
new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertComboAtJudgement(0, 1);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
assertComboAtJudgement(1, 0);
|
||||
assertComboAtJudgement(2, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo x o
|
||||
@@ -208,7 +222,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -228,7 +241,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@@ -246,7 +258,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@@ -264,7 +275,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@@ -358,7 +368,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Good);
|
||||
@@ -405,7 +414,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Great);
|
||||
@@ -425,7 +433,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@@ -476,42 +483,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitTailBeforeLastTick()
|
||||
{
|
||||
const int tick_rate = 8;
|
||||
const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
|
||||
const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_last_tick - 5)
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertLastTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Ok);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroLength()
|
||||
{
|
||||
@@ -551,11 +522,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private void assertNoteJudgement(HitResult result)
|
||||
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
|
||||
|
||||
private void assertTickJudgement(HitResult result)
|
||||
=> AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result));
|
||||
|
||||
private void assertLastTickJudgement(HitResult result)
|
||||
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
|
||||
private void assertComboAtJudgement(int judgementIndex, int combo)
|
||||
=> AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
@@ -43,39 +44,41 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
: base(beatmap, ruleset)
|
||||
{
|
||||
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||
TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap));
|
||||
|
||||
double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
|
||||
double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
if (IsForCurrentRuleset)
|
||||
if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo);
|
||||
|
||||
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
TargetColumns /= 2;
|
||||
Dual = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count;
|
||||
if (percentSliderOrSpinner < 0.2)
|
||||
TargetColumns = 7;
|
||||
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
|
||||
TargetColumns = roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
else if (percentSliderOrSpinner > 0.6)
|
||||
TargetColumns = roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
else
|
||||
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
TargetColumns /= 2;
|
||||
Dual = true;
|
||||
}
|
||||
|
||||
originalTargetColumns = TargetColumns;
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(beatmapInfo.Difficulty.CircleSize);
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
return GetColumnCountForNonConvert(difficulty);
|
||||
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
|
||||
float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
@@ -12,5 +17,50 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
return new LegacyScoreAttributes { ComboScore = 1000000 };
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case ManiaModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case ManiaModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case ManiaModHalfTime:
|
||||
case ManiaModDaycore:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
return multiplier;
|
||||
|
||||
// Apply key mod multipliers.
|
||||
|
||||
int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty);
|
||||
int actualColumns = originalColumns;
|
||||
|
||||
actualColumns = mods.OfType<ManiaKeyMod>().SingleOrDefault()?.KeyCount ?? actualColumns;
|
||||
if (mods.Any(m => m is ManiaModDualStages))
|
||||
actualColumns *= 2;
|
||||
|
||||
if (actualColumns > originalColumns)
|
||||
multiplier *= 0.9;
|
||||
else if (actualColumns < originalColumns)
|
||||
multiplier *= 0.9 - 0.04 * (originalColumns - actualColumns);
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@@ -23,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
/// <summary>
|
||||
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
|
||||
/// </summary>
|
||||
public partial class ManiaBeatSnapGrid : Component
|
||||
public partial class ManiaBeatSnapGrid : CompositeComponent
|
||||
{
|
||||
private const double visible_range = 750;
|
||||
|
||||
@@ -53,6 +55,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
private readonly List<ScrollingHitObjectContainer> grids = new List<ScrollingHitObjectContainer>();
|
||||
|
||||
private readonly DrawablePool<DrawableGridLine> linesPool = new DrawablePool<DrawableGridLine>(50);
|
||||
|
||||
private readonly Cached lineCache = new Cached();
|
||||
|
||||
private (double start, double end)? selectionTimeRange;
|
||||
@@ -60,6 +64,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(HitObjectComposer composer)
|
||||
{
|
||||
AddInternal(linesPool);
|
||||
|
||||
foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages)
|
||||
{
|
||||
foreach (var column in stage.Columns)
|
||||
@@ -85,17 +91,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
|
||||
|
||||
private void createLines()
|
||||
{
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
|
||||
availableLines.Push(line);
|
||||
|
||||
grid.Clear();
|
||||
}
|
||||
|
||||
if (selectionTimeRange == null)
|
||||
return;
|
||||
@@ -131,10 +130,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
if (!availableLines.TryPop(out var line))
|
||||
line = new DrawableGridLine();
|
||||
var line = linesPool.Get();
|
||||
|
||||
line.Apply(new HitObject
|
||||
{
|
||||
StartTime = time
|
||||
});
|
||||
|
||||
line.HitObject.StartTime = time;
|
||||
line.Colour = colour;
|
||||
|
||||
grid.Add(line);
|
||||
|
||||
@@ -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.
|
||||
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class HoldNoteBodyJudgement : ManiaJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.IgnoreHit;
|
||||
public override HitResult MinResult => HitResult.ComboBreak;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class HoldNoteTickJudgement : ManiaJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.LargeTickHit;
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.Meh:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo)));
|
||||
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty)));
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
|
||||
@@ -385,22 +385,9 @@ namespace osu.Game.Rulesets.Mania
|
||||
HitResult.Good,
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
};
|
||||
}
|
||||
|
||||
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
return "hold tick";
|
||||
}
|
||||
|
||||
return base.GetDisplayNameForHitResult(result);
|
||||
}
|
||||
|
||||
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||
{
|
||||
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||
|
||||
@@ -35,10 +35,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
public DrawableHoldNoteHead Head => headContainer.Child;
|
||||
public DrawableHoldNoteTail Tail => tailContainer.Child;
|
||||
public DrawableHoldNoteBody Body => bodyContainer.Child;
|
||||
|
||||
private Container<DrawableHoldNoteHead> headContainer;
|
||||
private Container<DrawableHoldNoteTail> tailContainer;
|
||||
private Container<DrawableHoldNoteTick> tickContainer;
|
||||
private Container<DrawableHoldNoteBody> bodyContainer;
|
||||
|
||||
private PausableSkinnableSound slidingSample;
|
||||
|
||||
@@ -60,12 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
public double? HoldStartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
|
||||
/// </summary>
|
||||
public double? HoldBrokenTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hold note has been released potentially without having caused a break.
|
||||
/// Used to decide whether to visually clamp the hold note to the judgement line.
|
||||
/// </summary>
|
||||
private double? releaseTime;
|
||||
|
||||
@@ -103,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
|
||||
}
|
||||
},
|
||||
bodyContainer = new Container<DrawableHoldNoteBody> { RelativeSizeAxes = Axes.Both },
|
||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@@ -110,7 +107,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
},
|
||||
tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
|
||||
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||
});
|
||||
@@ -118,7 +114,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
maskedContents.AddRange(new[]
|
||||
{
|
||||
bodyPiece.CreateProxy(),
|
||||
tickContainer.CreateProxy(),
|
||||
tailContainer.CreateProxy(),
|
||||
});
|
||||
}
|
||||
@@ -136,7 +131,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
sizingContainer.Size = Vector2.One;
|
||||
HoldStartTime = null;
|
||||
HoldBrokenTime = null;
|
||||
releaseTime = null;
|
||||
}
|
||||
|
||||
@@ -154,8 +148,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
tailContainer.Child = tail;
|
||||
break;
|
||||
|
||||
case DrawableHoldNoteTick tick:
|
||||
tickContainer.Add(tick);
|
||||
case DrawableHoldNoteBody body:
|
||||
bodyContainer.Child = body;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -165,7 +159,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
base.ClearNestedHitObjects();
|
||||
headContainer.Clear(false);
|
||||
tailContainer.Clear(false);
|
||||
tickContainer.Clear(false);
|
||||
bodyContainer.Clear(false);
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
@@ -178,8 +172,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
case HeadNote head:
|
||||
return new DrawableHoldNoteHead(head);
|
||||
|
||||
case HoldNoteTick tick:
|
||||
return new DrawableHoldNoteTick(tick);
|
||||
case HoldNoteBody body:
|
||||
return new DrawableHoldNoteBody(body);
|
||||
}
|
||||
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
@@ -266,20 +260,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
if (Tail.AllJudged)
|
||||
{
|
||||
foreach (var tick in tickContainer)
|
||||
{
|
||||
if (!tick.Judged)
|
||||
tick.MissForcefully();
|
||||
}
|
||||
|
||||
if (Tail.IsHit)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
else
|
||||
MissForcefully();
|
||||
}
|
||||
|
||||
if (Tail.Judged && !Tail.IsHit)
|
||||
HoldBrokenTime = Time.Current;
|
||||
// Make sure that the hold note is fully judged by giving the body a judgement.
|
||||
if (Tail.AllJudged && !Body.AllJudged)
|
||||
Body.TriggerResult(Tail.IsHit);
|
||||
}
|
||||
|
||||
public override void MissForcefully()
|
||||
@@ -333,22 +322,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (e.Action != Action.Value)
|
||||
return;
|
||||
|
||||
// Make sure a hold was started
|
||||
if (HoldStartTime == null)
|
||||
return;
|
||||
|
||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
return;
|
||||
|
||||
Tail.UpdateResult();
|
||||
endHold();
|
||||
// When our action is released and we are in the middle of a hold, there's a chance that
|
||||
// the user has released too early (before the tail).
|
||||
//
|
||||
// In such a case, we want to record this against the DrawableHoldNoteBody.
|
||||
if (HoldStartTime != null)
|
||||
{
|
||||
Tail.UpdateResult();
|
||||
Body.TriggerResult(Tail.IsHit);
|
||||
|
||||
// If the key has been released too early, the user should not receive full score for the release
|
||||
if (!Tail.IsHit)
|
||||
HoldBrokenTime = Time.Current;
|
||||
|
||||
releaseTime = Time.Current;
|
||||
endHold();
|
||||
releaseTime = Time.Current;
|
||||
}
|
||||
}
|
||||
|
||||
private void endHold()
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableHoldNoteBody : DrawableManiaHitObject<HoldNoteBody>
|
||||
{
|
||||
public bool HasHoldBreak => AllJudged && !IsHit;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableHoldNoteBody()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableHoldNoteBody(HoldNoteBody hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
internal void TriggerResult(bool hit)
|
||||
{
|
||||
if (AllJudged) return;
|
||||
|
||||
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
ApplyResult(r =>
|
||||
{
|
||||
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
||||
if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
|
||||
bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak;
|
||||
|
||||
if (result > HitResult.Meh && hasComboBreak)
|
||||
result = HitResult.Meh;
|
||||
|
||||
r.Type = result;
|
||||
|
||||
@@ -1,110 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualises a <see cref="HoldNoteTick"/> hit object.
|
||||
/// </summary>
|
||||
public partial class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
|
||||
{
|
||||
/// <summary>
|
||||
/// References the time at which the user started holding the hold note.
|
||||
/// </summary>
|
||||
private Func<double?> holdStartTime;
|
||||
|
||||
private Container glowContainer;
|
||||
|
||||
public DrawableHoldNoteTick()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableHoldNoteTick(HoldNoteTick hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(glowContainer = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AccentColour.BindValueChanged(colour =>
|
||||
{
|
||||
glowContainer.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 2f,
|
||||
Roundness = 15f,
|
||||
Colour = colour.NewValue.Opacity(0.3f)
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
|
||||
Debug.Assert(ParentHitObject != null);
|
||||
|
||||
var holdNote = (DrawableHoldNote)ParentHitObject;
|
||||
holdStartTime = () => holdNote.HoldStartTime;
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
{
|
||||
base.OnFree();
|
||||
|
||||
holdStartTime = null;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (Time.Current < HitObject.StartTime)
|
||||
return;
|
||||
|
||||
double? startTime = holdStartTime?.Invoke();
|
||||
|
||||
if (startTime == null || startTime > HitObject.StartTime)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
else
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The head note of a <see cref="HoldNote"/>.
|
||||
/// </summary>
|
||||
public class HeadNote : Note
|
||||
{
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -81,27 +79,18 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
/// </summary>
|
||||
public TailNote Tail { get; private set; }
|
||||
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The time between ticks of this hold.
|
||||
/// The body of the hold.
|
||||
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
|
||||
/// </summary>
|
||||
private double tickSpacing = 50;
|
||||
public HoldNoteBody Body { get; private set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
|
||||
}
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
createTicks(cancellationToken);
|
||||
|
||||
AddNested(Head = new HeadNote
|
||||
{
|
||||
StartTime = StartTime,
|
||||
@@ -115,23 +104,12 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
Column = Column,
|
||||
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
||||
});
|
||||
}
|
||||
|
||||
private void createTicks(CancellationToken cancellationToken)
|
||||
{
|
||||
if (tickSpacing == 0)
|
||||
return;
|
||||
|
||||
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
|
||||
AddNested(Body = new HoldNoteBody
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AddNested(new HoldNoteTick
|
||||
{
|
||||
StartTime = t,
|
||||
Column = Column
|
||||
});
|
||||
}
|
||||
StartTime = StartTime,
|
||||
Column = Column
|
||||
});
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
|
||||
@@ -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 osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The body of a <see cref="HoldNote"/>.
|
||||
/// Mostly a dummy hitobject that provides the judgement for the "holding" state.<br />
|
||||
/// On hit - the hold note was held correctly for the full duration.<br />
|
||||
/// On miss - the hold note was released at some point during its judgement period.
|
||||
/// </summary>
|
||||
public class HoldNoteBody : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +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.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// A scoring tick of a hold note.
|
||||
/// </summary>
|
||||
public class HoldNoteTick : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ using osu.Game.Rulesets.Mania.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The tail note of a <see cref="HoldNote"/>.
|
||||
/// </summary>
|
||||
public class TailNote : Note
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -209,7 +209,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
missFadeTime.Value ??= holdNote.HoldBrokenTime;
|
||||
|
||||
if (holdNote.Body.HasHoldBreak)
|
||||
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
|
||||
|
||||
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
@@ -69,9 +68,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
public void Animate(JudgementResult result)
|
||||
{
|
||||
if (result.Judgement is HoldNoteTickJudgement)
|
||||
return;
|
||||
|
||||
(explosion as IFramedAnimation)?.GotoFrame(0);
|
||||
|
||||
explosion?.FadeInFromZero(FADE_IN_DURATION)
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
RegisterPool<HoldNote, DrawableHoldNote>(10, 50);
|
||||
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
|
||||
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
|
||||
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250);
|
||||
RegisterPool<HoldNoteBody, DrawableHoldNoteBody>(10, 50);
|
||||
}
|
||||
|
||||
private void onSourceChanged()
|
||||
|
||||
@@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osuTK;
|
||||
@@ -150,9 +149,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
// scale roughly in-line with visual appearance of notes
|
||||
Vector2 scale = new Vector2(1, 0.6f);
|
||||
|
||||
if (result.Judgement is HoldNoteTickJudgement)
|
||||
scale *= 0.5f;
|
||||
|
||||
this.ScaleTo(scale);
|
||||
|
||||
largeFaint
|
||||
|
||||
@@ -195,10 +195,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
// Tick judgements should not display text.
|
||||
if (judgedObject is DrawableHoldNoteTick)
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPool.Get(j =>
|
||||
{
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -174,5 +177,58 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case OsuModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case OsuModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case OsuModHalfTime:
|
||||
case OsuModDaycore:
|
||||
multiplier *= 0.3;
|
||||
break;
|
||||
|
||||
case OsuModHidden:
|
||||
multiplier *= 1.06;
|
||||
break;
|
||||
|
||||
case OsuModHardRock:
|
||||
multiplier *= scoreV2 ? 1.10 : 1.06;
|
||||
break;
|
||||
|
||||
case OsuModDoubleTime:
|
||||
case OsuModNightcore:
|
||||
multiplier *= scoreV2 ? 1.20 : 1.12;
|
||||
break;
|
||||
|
||||
case OsuModFlashlight:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case OsuModSpunOut:
|
||||
multiplier *= 0.9;
|
||||
break;
|
||||
|
||||
case OsuModRelax:
|
||||
case OsuModAutopilot:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||
|
||||
public LegacyApproachCircle()
|
||||
: base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS)
|
||||
: base("Gameplay/osu/approachcircle", OsuHitObject.OBJECT_DIMENSIONS * 2)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
@@ -194,5 +197,47 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case TaikoModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case TaikoModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case TaikoModHalfTime:
|
||||
case TaikoModDaycore:
|
||||
multiplier *= 0.3;
|
||||
break;
|
||||
|
||||
case TaikoModHidden:
|
||||
case TaikoModHardRock:
|
||||
multiplier *= 1.06;
|
||||
break;
|
||||
|
||||
case TaikoModDoubleTime:
|
||||
case TaikoModNightcore:
|
||||
case TaikoModFlashlight:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case TaikoModRelax:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
[Test]
|
||||
public void TestDecodeBeatmapVersion()
|
||||
{
|
||||
using (var resStream = TestResources.OpenResource("beatmap-version.osu"))
|
||||
using (var resStream = TestResources.OpenResource("beatmap-version-6.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoder = Decoder.GetDecoder<Beatmap>(stream);
|
||||
@@ -45,6 +45,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestPreviewPointWithOffsets(bool applyOffsets)
|
||||
{
|
||||
using (var resStream = TestResources.OpenResource("beatmap-version-4.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoder = Decoder.GetDecoder<Beatmap>(stream);
|
||||
((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets;
|
||||
var working = new TestWorkingBeatmap(decoder.Decode(stream));
|
||||
|
||||
Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion);
|
||||
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
|
||||
|
||||
Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeBeatmapGeneral()
|
||||
{
|
||||
@@ -915,10 +934,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLegacyDefaultsPreserved()
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestLegacyDefaultsPreserved(bool applyOffsets)
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = applyOffsets };
|
||||
|
||||
using (var memoryStream = new MemoryStream())
|
||||
using (var stream = new LineBufferedReader(memoryStream))
|
||||
|
||||
@@ -127,8 +127,9 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreUpgradeSuccess()
|
||||
[TestCase(30000002)]
|
||||
[TestCase(30000003)]
|
||||
public void TestScoreUpgradeSuccess(int scoreVersion)
|
||||
{
|
||||
ScoreInfo scoreInfo = null!;
|
||||
|
||||
@@ -138,7 +139,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
|
||||
{
|
||||
TotalScoreVersion = 30000002,
|
||||
TotalScoreVersion = scoreVersion,
|
||||
LegacyTotalScore = 123456,
|
||||
IsLegacyScore = true,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Input
|
||||
{
|
||||
[HeadlessTest]
|
||||
public partial class RealmKeyBindingTest : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestUnmapGlobalAction()
|
||||
{
|
||||
var keyBinding = new RealmKeyBinding(GlobalAction.ToggleReplaySettings, KeyCombination.FromKey(Key.Z));
|
||||
|
||||
AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)GlobalAction.ToggleReplaySettings));
|
||||
AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(GlobalAction.ToggleReplaySettings));
|
||||
}
|
||||
|
||||
[TestCase(typeof(OsuRuleset), OsuAction.Smoke, null)]
|
||||
[TestCase(typeof(TaikoRuleset), TaikoAction.LeftCentre, null)]
|
||||
[TestCase(typeof(CatchRuleset), CatchAction.MoveRight, null)]
|
||||
[TestCase(typeof(ManiaRuleset), ManiaAction.Key7, 7)]
|
||||
public void TestUnmapRulesetActions(Type rulesetType, object action, int? variant)
|
||||
{
|
||||
string rulesetName = ((Ruleset)Activator.CreateInstance(rulesetType)!).ShortName;
|
||||
var keyBinding = new RealmKeyBinding(action, KeyCombination.FromKey(Key.Z), rulesetName, variant);
|
||||
|
||||
AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)action));
|
||||
AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(action));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
osu file format v4
|
||||
|
||||
[General]
|
||||
PreviewTime: -1
|
||||
@@ -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;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
[TestFixture]
|
||||
public class HitResultTest
|
||||
{
|
||||
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })]
|
||||
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })]
|
||||
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })]
|
||||
[TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })]
|
||||
[TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })]
|
||||
public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults)
|
||||
{
|
||||
HitResult[] unsupportedResults = HitResultExtensions.ALL_TYPES.Where(t => !minResults.Contains(t)).ToArray();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var max in maxResults)
|
||||
{
|
||||
foreach (var min in minResults)
|
||||
Assert.DoesNotThrow(() => HitResultExtensions.ValidateHitResultPair(max, min), $"{max} + {min} should be supported.");
|
||||
|
||||
foreach (var unsupported in unsupportedResults)
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => HitResultExtensions.ValidateHitResultPair(max, unsupported), $"{max} + {unsupported} should not be supported.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new TestJudgement(maxResult))
|
||||
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], fourObjectBeatmap.HitObjects[i].CreateJudgement())
|
||||
{
|
||||
Type = i == 2 ? minResult : hitResult
|
||||
};
|
||||
@@ -259,6 +259,41 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
[Test]
|
||||
public void TestComboBreak()
|
||||
{
|
||||
Assert.That(HitResult.ComboBreak.IncreasesCombo(), Is.False);
|
||||
Assert.That(HitResult.ComboBreak.BreaksCombo(), Is.True);
|
||||
Assert.That(HitResult.ComboBreak.AffectsCombo(), Is.True);
|
||||
Assert.That(HitResult.ComboBreak.AffectsAccuracy(), Is.False);
|
||||
Assert.That(HitResult.ComboBreak.IsBasic(), Is.False);
|
||||
Assert.That(HitResult.ComboBreak.IsTick(), Is.False);
|
||||
Assert.That(HitResult.ComboBreak.IsBonus(), Is.False);
|
||||
Assert.That(HitResult.ComboBreak.IsHit(), Is.False);
|
||||
Assert.That(HitResult.ComboBreak.IsScorable(), Is.True);
|
||||
Assert.That(HitResultExtensions.ALL_TYPES, Does.Contain(HitResult.ComboBreak));
|
||||
|
||||
beatmap = new TestBeatmap(new RulesetInfo())
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new TestHitObject(HitResult.Great),
|
||||
new TestHitObject(HitResult.IgnoreHit, HitResult.ComboBreak),
|
||||
}
|
||||
};
|
||||
|
||||
scoreProcessor = new TestScoreProcessor();
|
||||
scoreProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Great });
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
||||
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.ComboBreak });
|
||||
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAccuracyWhenNearPerfect()
|
||||
{
|
||||
@@ -275,7 +310,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
for (int i = 0; i < beatmap.HitObjects.Count; i++)
|
||||
{
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], new TestJudgement(HitResult.Great))
|
||||
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[i], beatmap.HitObjects[i].CreateJudgement())
|
||||
{
|
||||
Type = i == 0 ? HitResult.Miss : HitResult.Great
|
||||
});
|
||||
@@ -293,24 +328,31 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
public TestJudgement(HitResult maxResult)
|
||||
public override HitResult MinResult => minResult ?? base.MinResult;
|
||||
|
||||
private readonly HitResult? minResult;
|
||||
|
||||
public TestJudgement(HitResult maxResult, HitResult? minResult = null)
|
||||
{
|
||||
MaxResult = maxResult;
|
||||
this.minResult = minResult;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHitObject : HitObject
|
||||
{
|
||||
private readonly HitResult maxResult;
|
||||
private readonly HitResult? minResult;
|
||||
|
||||
public override Judgement CreateJudgement()
|
||||
{
|
||||
return new TestJudgement(maxResult);
|
||||
return new TestJudgement(maxResult, minResult);
|
||||
}
|
||||
|
||||
public TestHitObject(HitResult maxResult)
|
||||
public TestHitObject(HitResult maxResult, HitResult? minResult = null)
|
||||
{
|
||||
this.maxResult = maxResult;
|
||||
this.minResult = minResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
};
|
||||
});
|
||||
|
||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
private void applyOneJudgement(HitResult result)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,9 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -26,6 +29,7 @@ using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@@ -98,6 +102,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
|
||||
|
||||
AddStep("reset last played", () => Realm.Write(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)!.LastPlayed = null));
|
||||
AddAssert("last played is null", () => getLastPlayed() == null);
|
||||
|
||||
CreateTest();
|
||||
@@ -150,6 +155,40 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGuestScoreIsStoredAsGuest()
|
||||
{
|
||||
AddStep("set up API", () => ((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetUserRequest userRequest:
|
||||
userRequest.TriggerSuccess(new APIUser
|
||||
{
|
||||
Username = "Guest",
|
||||
CountryCode = CountryCode.JP,
|
||||
Id = 1234
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("log out", () => API.Logout());
|
||||
CreateTest();
|
||||
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
AddStep("log back in", () => API.Login("username", "password"));
|
||||
|
||||
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
|
||||
|
||||
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
AddAssert("score is not associated with online user", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID))!.UserID == APIUser.SYSTEM_USER_ID);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReplayExport()
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@@ -28,6 +29,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
});
|
||||
|
||||
AddStep("Start track playing", () =>
|
||||
{
|
||||
Beatmap.Value.Track.Start();
|
||||
});
|
||||
|
||||
AddStep("initialise gameplay", () =>
|
||||
{
|
||||
Stack.Push(player = new MultiplayerPlayer(MultiplayerClient.ServerAPIRoom, new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
@@ -37,7 +43,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
|
||||
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);
|
||||
|
||||
AddAssert("gameplay clock is paused", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
|
||||
AddAssert("gameplay clock is not running", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
|
||||
|
||||
AddStep("start gameplay", () => ((IMultiplayerClient)MultiplayerClient).GameplayStarted());
|
||||
|
||||
AddUntilStep("gameplay clock is not paused", () => !player.ChildrenOfType<GameplayClockContainer>().Single().IsPaused.Value);
|
||||
AddAssert("gameplay clock is running", () => player.ChildrenOfType<GameplayClockContainer>().Single().IsRunning);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
|
||||
.ChildrenOfType<VariantBindingsSubsection>()
|
||||
.FirstOrDefault(s => s.Ruleset.ShortName == "osu");
|
||||
.FirstOrDefault(s => s.Ruleset!.ShortName == "osu");
|
||||
|
||||
private OsuButton configureBindingsButton => Game.Settings
|
||||
.ChildrenOfType<BindingSettings>().SingleOrDefault()?
|
||||
|
||||
@@ -2,16 +2,19 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -34,6 +37,49 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Test]
|
||||
public void TestMania() => createSwitchTestFor(new ManiaRuleset());
|
||||
|
||||
[Test]
|
||||
public void TestShowRateAdjusts()
|
||||
{
|
||||
AddStep("create mod icons", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods()
|
||||
.OfType<ModRateAdjust>()
|
||||
.SelectMany(m =>
|
||||
{
|
||||
List<TestModSwitchTiny> icons = new List<TestModSwitchTiny> { new TestModSwitchTiny(m) };
|
||||
|
||||
for (double i = m.SpeedChange.MinValue; i < m.SpeedChange.MaxValue; i += m.SpeedChange.Precision * 10)
|
||||
{
|
||||
m = (ModRateAdjust)m.DeepClone();
|
||||
m.SpeedChange.Value = i;
|
||||
icons.Add(new TestModSwitchTiny(m, true));
|
||||
}
|
||||
|
||||
return icons;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("adjust rates", () =>
|
||||
{
|
||||
foreach (var icon in this.ChildrenOfType<TestModSwitchTiny>())
|
||||
{
|
||||
if (icon.Mod is ModRateAdjust rateAdjust)
|
||||
{
|
||||
rateAdjust.SpeedChange.Value = RNG.NextDouble() > 0.9
|
||||
? rateAdjust.SpeedChange.Default
|
||||
: RNG.NextDouble(rateAdjust.SpeedChange.MinValue, rateAdjust.SpeedChange.MaxValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddToggleStep("toggle active", active => this.ChildrenOfType<TestModSwitchTiny>().ForEach(s => s.Active.Value = active));
|
||||
}
|
||||
|
||||
private void createSwitchTestFor(Ruleset ruleset)
|
||||
{
|
||||
AddStep("no colour scheme", () => Child = createContent(ruleset, null));
|
||||
@@ -43,7 +89,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep($"{scheme} colour scheme", () => Child = createContent(ruleset, scheme));
|
||||
}
|
||||
|
||||
AddToggleStep("toggle active", active => this.ChildrenOfType<ModSwitchTiny>().ForEach(s => s.Active.Value = active));
|
||||
AddToggleStep("toggle active", active => this.ChildrenOfType<TestModSwitchTiny>().ForEach(s => s.Active.Value = active));
|
||||
}
|
||||
|
||||
private static Drawable createContent(Ruleset ruleset, OverlayColourScheme? colourScheme)
|
||||
@@ -62,7 +108,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Full,
|
||||
Spacing = new Vector2(5),
|
||||
ChildrenEnumerable = group.Select(mod => new ModSwitchTiny(mod))
|
||||
ChildrenEnumerable = group.Select(mod => new TestModSwitchTiny(mod))
|
||||
})
|
||||
};
|
||||
|
||||
@@ -81,5 +127,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
return switchFlow;
|
||||
}
|
||||
|
||||
private partial class TestModSwitchTiny : ModSwitchTiny
|
||||
{
|
||||
public new IMod Mod => base.Mod;
|
||||
|
||||
public TestModSwitchTiny(IMod mod, bool showExtendedInformation = false)
|
||||
: base(mod, showExtendedInformation)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays;
|
||||
@@ -31,6 +33,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
public double TimeToCompleteProgress { get; set; } = 2000;
|
||||
|
||||
private readonly UserLookupCache userLookupCache = new TestUserLookupCache();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@@ -60,6 +64,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep(@"simple #2", sendAmazingNotification);
|
||||
AddStep(@"progress #1", sendUploadProgress);
|
||||
AddStep(@"progress #2", sendDownloadProgress);
|
||||
AddStep(@"User notification", sendUserNotification);
|
||||
|
||||
checkProgressingCount(2);
|
||||
|
||||
@@ -537,6 +542,16 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
progressingNotifications.Add(n);
|
||||
}
|
||||
|
||||
private void sendUserNotification()
|
||||
{
|
||||
var user = userLookupCache.GetUserAsync(0).GetResultSafely();
|
||||
if (user == null) return;
|
||||
|
||||
var n = new UserAvatarNotification(user, $"{user.Username} invited you to a multiplayer match!");
|
||||
|
||||
notificationOverlay.Post(n);
|
||||
}
|
||||
|
||||
private void sendUploadProgress()
|
||||
{
|
||||
var n = new ProgressNotification
|
||||
|
||||
@@ -84,6 +84,7 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
Colour = colours.Gray3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.4f,
|
||||
},
|
||||
flow = new FillFlowContainer
|
||||
{
|
||||
|
||||
@@ -235,7 +235,9 @@ namespace osu.Game
|
||||
Logger.Log("Querying for scores that need total score conversion...");
|
||||
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
||||
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
|
||||
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
|
||||
&& (s.TotalScoreVersion == 30000002
|
||||
|| s.TotalScoreVersion == 30000003))
|
||||
.AsEnumerable().Select(s => s.ID)));
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||
|
||||
@@ -196,7 +196,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"PreviewTime":
|
||||
metadata.PreviewTime = getOffsetTime(Parsing.ParseInt(pair.Value));
|
||||
int time = Parsing.ParseInt(pair.Value);
|
||||
metadata.PreviewTime = time == -1 ? time : getOffsetTime(time);
|
||||
break;
|
||||
|
||||
case @"SampleSet":
|
||||
|
||||
@@ -10,7 +10,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -225,28 +224,30 @@ namespace osu.Game.Database
|
||||
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
|
||||
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
|
||||
|
||||
return ConvertFromLegacyTotalScore(score, attributes);
|
||||
return ConvertFromLegacyTotalScore(score, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
|
||||
/// </summary>
|
||||
/// <param name="score">The score to convert the total score of.</param>
|
||||
/// <param name="attributes">Difficulty attributes providing the legacy scoring values
|
||||
/// (<see cref="DifficultyAttributes.LegacyAccuracyScore"/>, <see cref="DifficultyAttributes.LegacyComboScore"/>, and <see cref="DifficultyAttributes.LegacyBonusScoreRatio"/>)
|
||||
/// for the beatmap which the score was set on.</param>
|
||||
/// <param name="difficulty">The beatmap difficulty.</param>
|
||||
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
|
||||
/// <returns>The standardised total score.</returns>
|
||||
public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyScoreAttributes attributes)
|
||||
public static long ConvertFromLegacyTotalScore(ScoreInfo score, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
|
||||
{
|
||||
if (!score.IsLegacyScore)
|
||||
return score.TotalScore;
|
||||
|
||||
Debug.Assert(score.LegacyTotalScore != null);
|
||||
|
||||
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||
Ruleset ruleset = score.Ruleset.CreateInstance();
|
||||
if (ruleset is not ILegacyRuleset legacyRuleset)
|
||||
return score.TotalScore;
|
||||
|
||||
double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty);
|
||||
int maximumLegacyAccuracyScore = attributes.AccuracyScore;
|
||||
long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * modMultiplier);
|
||||
long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier);
|
||||
double maximumLegacyBonusRatio = attributes.BonusScoreRatio;
|
||||
|
||||
// The part of total score that doesn't include bonus.
|
||||
@@ -258,6 +259,8 @@ namespace osu.Game.Database
|
||||
// The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
|
||||
|
||||
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||
|
||||
switch (score.Ruleset.OnlineID)
|
||||
{
|
||||
case 0:
|
||||
|
||||
@@ -78,6 +78,7 @@ namespace osu.Game.Graphics
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Miss:
|
||||
case HitResult.ComboBreak:
|
||||
return Red;
|
||||
|
||||
case HitResult.Meh:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input;
|
||||
@@ -13,6 +14,8 @@ namespace osu.Game.Input.Bindings
|
||||
{
|
||||
public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected override bool Prioritised => true;
|
||||
|
||||
private readonly IKeyBindingHandler<GlobalAction>? handler;
|
||||
|
||||
public GlobalActionContainer(OsuGameBase? game)
|
||||
@@ -22,22 +25,62 @@ namespace osu.Game.Input.Bindings
|
||||
handler = h;
|
||||
}
|
||||
|
||||
protected override bool Prioritised => true;
|
||||
|
||||
// IMPORTANT: Take care when changing order of the items in the enumerable.
|
||||
// It is used to decide the order of precedence, with the earlier items having higher precedence.
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
|
||||
.Concat(EditorKeyBindings)
|
||||
.Concat(InGameKeyBindings)
|
||||
.Concat(ReplayKeyBindings)
|
||||
.Concat(SongSelectKeyBindings)
|
||||
.Concat(AudioControlKeyBindings)
|
||||
/// <summary>
|
||||
/// All default key bindings across all categories, ordered with highest priority first.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// IMPORTANT: Take care when changing order of the items in the enumerable.
|
||||
/// It is used to decide the order of precedence, with the earlier items having higher precedence.
|
||||
/// </remarks>
|
||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => globalKeyBindings
|
||||
.Concat(editorKeyBindings)
|
||||
.Concat(inGameKeyBindings)
|
||||
.Concat(replayKeyBindings)
|
||||
.Concat(songSelectKeyBindings)
|
||||
.Concat(audioControlKeyBindings)
|
||||
// Overlay bindings may conflict with more local cases like the editor so they are checked last.
|
||||
// It has generally been agreed on that local screens like the editor should have priority,
|
||||
// based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
|
||||
.Concat(OverlayKeyBindings);
|
||||
.Concat(overlayKeyBindings);
|
||||
|
||||
public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
|
||||
public static IEnumerable<KeyBinding> GetDefaultBindingsFor(GlobalActionCategory category)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case GlobalActionCategory.General:
|
||||
return globalKeyBindings;
|
||||
|
||||
case GlobalActionCategory.Editor:
|
||||
return editorKeyBindings;
|
||||
|
||||
case GlobalActionCategory.InGame:
|
||||
return inGameKeyBindings;
|
||||
|
||||
case GlobalActionCategory.Replay:
|
||||
return replayKeyBindings;
|
||||
|
||||
case GlobalActionCategory.SongSelect:
|
||||
return songSelectKeyBindings;
|
||||
|
||||
case GlobalActionCategory.AudioControl:
|
||||
return audioControlKeyBindings;
|
||||
|
||||
case GlobalActionCategory.Overlays:
|
||||
return overlayKeyBindings;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}");
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<GlobalAction> GetGlobalActionsFor(GlobalActionCategory category)
|
||||
=> GetDefaultBindingsFor(category).Select(binding => binding.Action).Cast<GlobalAction>().Distinct();
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
|
||||
|
||||
private static IEnumerable<KeyBinding> globalKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
|
||||
new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
|
||||
@@ -67,7 +110,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> OverlayKeyBindings => new[]
|
||||
private static IEnumerable<KeyBinding> overlayKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
|
||||
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
|
||||
@@ -77,7 +120,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> EditorKeyBindings => new[]
|
||||
private static IEnumerable<KeyBinding> editorKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode),
|
||||
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
|
||||
@@ -101,7 +144,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
||||
private static IEnumerable<KeyBinding> inGameKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
|
||||
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
|
||||
@@ -118,7 +161,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> ReplayKeyBindings => new[]
|
||||
private static IEnumerable<KeyBinding> replayKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay),
|
||||
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
|
||||
@@ -127,7 +170,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> SongSelectKeyBindings => new[]
|
||||
private static IEnumerable<KeyBinding> songSelectKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
|
||||
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
|
||||
@@ -136,7 +179,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
|
||||
private static IEnumerable<KeyBinding> audioControlKeyBindings => new[]
|
||||
{
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
|
||||
@@ -153,10 +196,6 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(InputKey.PlayPause, GlobalAction.MusicPlay),
|
||||
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
|
||||
};
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
|
||||
}
|
||||
|
||||
public enum GlobalAction
|
||||
@@ -365,4 +404,15 @@ namespace osu.Game.Input.Bindings
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
|
||||
EditorToggleRotateControl,
|
||||
}
|
||||
|
||||
public enum GlobalActionCategory
|
||||
{
|
||||
General,
|
||||
Editor,
|
||||
InGame,
|
||||
Replay,
|
||||
SongSelect,
|
||||
AudioControl,
|
||||
Overlays
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Input.Bindings
|
||||
@@ -26,6 +28,13 @@ namespace osu.Game.Input.Bindings
|
||||
set => KeyCombinationString = value.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The resultant action which is triggered by this binding.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implementation always returns an integer.
|
||||
/// If wanting to get the actual enum-typed value, use <see cref="GetAction"/>.
|
||||
/// </remarks>
|
||||
[Ignored]
|
||||
public object Action
|
||||
{
|
||||
@@ -53,5 +62,20 @@ namespace osu.Game.Input.Bindings
|
||||
private RealmKeyBinding()
|
||||
{
|
||||
}
|
||||
|
||||
public object GetAction(RulesetStore rulesets)
|
||||
{
|
||||
if (string.IsNullOrEmpty(RulesetName))
|
||||
return (GlobalAction)ActionInt;
|
||||
|
||||
var ruleset = rulesets.GetRuleset(RulesetName);
|
||||
var actionType = ruleset!.CreateInstance()
|
||||
.GetDefaultKeyBindings(Variant ?? 0)
|
||||
.First() // let's just assume nobody does something stupid like mix multiple types...
|
||||
.Action
|
||||
.GetType();
|
||||
|
||||
return Enum.ToObject(actionType, ActionInt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
|
||||
|
||||
/// <summary>
|
||||
/// "Invite player"
|
||||
/// </summary>
|
||||
public static LocalisableString InvitePlayer => new TranslatableString(getKey(@"invite_player"), @"Invite player");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ Please try changing your audio device to a working setting.");
|
||||
/// </summary>
|
||||
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
|
||||
|
||||
/// <summary>
|
||||
/// "{0} invited you to the multiplayer match "{1}"! Click to join."
|
||||
/// </summary>
|
||||
public static LocalisableString InvitedYouToTheMultiplayer(string username, string roomName) => new TranslatableString(getKey(@"invited_you_to_the_multiplayer"), @"{0} invited you to the multiplayer match ""{1}""! Click to join.", username, roomName);
|
||||
|
||||
/// <summary>
|
||||
/// "You do not have the beatmap for this replay."
|
||||
/// </summary>
|
||||
|
||||
@@ -14,6 +14,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
|
||||
|
||||
/// <summary>
|
||||
/// "Can't invite this user as you have blocked them or they have blocked you."
|
||||
/// </summary>
|
||||
public static LocalisableString InviteFailedUserBlocked => new TranslatableString(getKey(@"cant_invite_this_user_as"), @"Can't invite this user as you have blocked them or they have blocked you.");
|
||||
|
||||
/// <summary>
|
||||
/// "Can't invite this user as they have opted out of non-friend communications."
|
||||
/// </summary>
|
||||
public static LocalisableString InviteFailedUserOptOut => new TranslatableString(getKey(@"cant_invite_this_user_as1"), @"Can't invite this user as they have opted out of non-friend communications.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,14 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <param name="user">The user.</param>
|
||||
Task UserKicked(MultiplayerRoomUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Signals that the local user has been invited into a multiplayer room.
|
||||
/// </summary>
|
||||
/// <param name="invitedBy">Id of user that invited the player.</param>
|
||||
/// <param name="roomID">Id of the room the user got invited to.</param>
|
||||
/// <param name="password">Password to join the room.</param>
|
||||
Task Invited(int invitedBy, long roomID, string password);
|
||||
|
||||
/// <summary>
|
||||
/// Signal that the host of the room has changed.
|
||||
/// </summary>
|
||||
|
||||
@@ -99,5 +99,13 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The item to remove.</param>
|
||||
Task RemovePlaylistItem(long playlistItemId);
|
||||
|
||||
/// <summary>
|
||||
/// Invites a player to the current room.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to invite.</param>
|
||||
/// <exception cref="UserBlockedException">The user has blocked or has been blocked by the invited user.</exception>
|
||||
/// <exception cref="UserBlocksPMsException">The invited user does not accept private messages.</exception>
|
||||
Task InvitePlayer(int userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
@@ -30,6 +31,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public Action<Notification>? PostNotification { protected get; set; }
|
||||
|
||||
public Action<Room, string>? PresentMatch { protected get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when any change occurs to the multiplayer room.
|
||||
/// </summary>
|
||||
@@ -260,6 +263,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
protected abstract Task LeaveRoomInternal();
|
||||
|
||||
public abstract Task InvitePlayer(int userId);
|
||||
|
||||
/// <summary>
|
||||
/// Change the current <see cref="MultiplayerRoom"/> settings.
|
||||
/// </summary>
|
||||
@@ -440,6 +445,38 @@ namespace osu.Game.Online.Multiplayer
|
||||
return handleUserLeft(user, UserKicked);
|
||||
}
|
||||
|
||||
async Task IMultiplayerClient.Invited(int invitedBy, long roomID, string password)
|
||||
{
|
||||
APIUser? apiUser = await userLookupCache.GetUserAsync(invitedBy).ConfigureAwait(false);
|
||||
Room? apiRoom = await getRoomAsync(roomID).ConfigureAwait(false);
|
||||
|
||||
if (apiUser == null || apiRoom == null) return;
|
||||
|
||||
PostNotification?.Invoke(
|
||||
new UserAvatarNotification(apiUser, NotificationsStrings.InvitedYouToTheMultiplayer(apiUser.Username, apiRoom.Name.Value))
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
PresentMatch?.Invoke(apiRoom, password);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Task<Room?> getRoomAsync(long id)
|
||||
{
|
||||
TaskCompletionSource<Room?> taskCompletionSource = new TaskCompletionSource<Room?>();
|
||||
|
||||
var request = new GetRoomRequest(id);
|
||||
request.Success += room => taskCompletionSource.TrySetResult(room);
|
||||
request.Failure += _ => taskCompletionSource.TrySetResult(null);
|
||||
|
||||
API.Queue(request);
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
}
|
||||
|
||||
private void addUserToAPIRoom(MultiplayerRoomUser user)
|
||||
{
|
||||
Debug.Assert(APIRoom != null);
|
||||
|
||||
@@ -12,6 +12,8 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
@@ -50,6 +52,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
|
||||
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
|
||||
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked);
|
||||
connection.On<int, long, string>(nameof(IMultiplayerClient.Invited), ((IMultiplayerClient)this).Invited);
|
||||
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
|
||||
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
|
||||
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
|
||||
@@ -106,6 +109,32 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom));
|
||||
}
|
||||
|
||||
public override async Task InvitePlayer(int userId)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
return;
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
|
||||
try
|
||||
{
|
||||
await connection.InvokeAsync(nameof(IMultiplayerServer.InvitePlayer), userId).ConfigureAwait(false);
|
||||
}
|
||||
catch (HubException exception)
|
||||
{
|
||||
switch (exception.GetHubExceptionMessage())
|
||||
{
|
||||
case UserBlockedException.MESSAGE:
|
||||
PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserBlocked });
|
||||
break;
|
||||
|
||||
case UserBlocksPMsException.MESSAGE:
|
||||
PostNotification?.Invoke(new SimpleErrorNotification { Text = OnlinePlayStrings.InviteFailedUserOptOut });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override Task TransferHost(int userId)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class UserBlockedException : HubException
|
||||
{
|
||||
public const string MESSAGE = @"Cannot perform action due to user being blocked.";
|
||||
|
||||
public UserBlockedException()
|
||||
: base(MESSAGE)
|
||||
{
|
||||
}
|
||||
|
||||
protected UserBlockedException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
[Serializable]
|
||||
public class UserBlocksPMsException : HubException
|
||||
{
|
||||
public const string MESSAGE = "Cannot perform action because user has disabled non-friend communications.";
|
||||
|
||||
public UserBlocksPMsException()
|
||||
: base(MESSAGE)
|
||||
{
|
||||
}
|
||||
|
||||
protected UserBlocksPMsException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.Music;
|
||||
@@ -58,6 +59,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
@@ -643,6 +645,24 @@ namespace osu.Game
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Join a multiplayer match immediately.
|
||||
/// </summary>
|
||||
/// <param name="room">The room to join.</param>
|
||||
/// <param name="password">The password to join the room, if any is given.</param>
|
||||
public void PresentMultiplayerMatch(Room room, string password)
|
||||
{
|
||||
PerformFromScreen(screen =>
|
||||
{
|
||||
if (!(screen is Multiplayer multiplayer))
|
||||
screen.Push(multiplayer = new Multiplayer());
|
||||
|
||||
multiplayer.Join(room, password);
|
||||
});
|
||||
// TODO: We should really be able to use `validScreens: new[] { typeof(Multiplayer) }` here
|
||||
// but `PerformFromScreen` doesn't understand nested stacks.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Present a score's replay immediately.
|
||||
/// The user should have already requested this interactively.
|
||||
@@ -853,6 +873,7 @@ namespace osu.Game
|
||||
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
|
||||
|
||||
MultiplayerClient.PostNotification = n => Notifications.Post(n);
|
||||
MultiplayerClient.PresentMatch = PresentMultiplayerMatch;
|
||||
|
||||
// make config aware of how to lookup skins for on-screen display purposes.
|
||||
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
users.GetUserAsync(userId).ContinueWith(task =>
|
||||
{
|
||||
var user = task.GetResultSafely();
|
||||
APIUser user = task.GetResultSafely();
|
||||
|
||||
if (user == null)
|
||||
return;
|
||||
@@ -130,6 +130,9 @@ namespace osu.Game.Overlays.Dashboard
|
||||
if (!playingUsers.Contains(user.Id))
|
||||
return;
|
||||
|
||||
// TODO: remove this once online state is being updated more correctly.
|
||||
user.IsOnline = true;
|
||||
|
||||
userFlow.Add(createUserPanel(user));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,7 +113,8 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Children = new[]
|
||||
{
|
||||
new NotificationSection(AccountsStrings.NotificationsTitle, new[] { typeof(SimpleNotification) }),
|
||||
// The main section adds as a catch-all for notifications which don't group into other sections.
|
||||
new NotificationSection(AccountsStrings.NotificationsTitle),
|
||||
new NotificationSection(NotificationsStrings.RunningTasks, new[] { typeof(ProgressNotification) }),
|
||||
}
|
||||
}
|
||||
@@ -205,7 +206,8 @@ namespace osu.Game.Overlays
|
||||
var ourType = notification.GetType();
|
||||
int depth = notification.DisplayOnTop ? -runningDepth : runningDepth;
|
||||
|
||||
var section = sections.Children.First(s => s.AcceptedNotificationTypes.Any(accept => accept.IsAssignableFrom(ourType)));
|
||||
var section = sections.Children.FirstOrDefault(s => s.AcceptedNotificationTypes?.Any(accept => accept.IsAssignableFrom(ourType)) == true)
|
||||
?? sections.First();
|
||||
|
||||
section.Add(notification, depth);
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ namespace osu.Game.Overlays.Notifications
|
||||
public virtual string PopInSampleName => "UI/notification-default";
|
||||
public virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||
|
||||
protected const float CORNER_RADIUS = 6;
|
||||
|
||||
protected NotificationLight Light;
|
||||
|
||||
protected Container IconContent;
|
||||
@@ -128,7 +130,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
AutoSizeAxes = Axes.Y,
|
||||
}.WithChild(MainContent = new Container
|
||||
{
|
||||
CornerRadius = 6,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@@ -473,10 +475,9 @@ namespace osu.Game.Overlays.Notifications
|
||||
base.Colour = value;
|
||||
pulsateLayer.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = ((Color4)value).Opacity(0.5f), //todo: avoid cast
|
||||
Colour = ((Color4)value).Opacity(0.18f),
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 12,
|
||||
Roundness = 12,
|
||||
Radius = 14,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,17 @@ namespace osu.Game.Overlays.Notifications
|
||||
notifications.Insert((int)position, notification);
|
||||
}
|
||||
|
||||
public IEnumerable<Type> AcceptedNotificationTypes { get; }
|
||||
/// <summary>
|
||||
/// Enumerable of notification types accepted in this section.
|
||||
/// If <see langword="null"/>, the section accepts any and all notifications.
|
||||
/// </summary>
|
||||
public IEnumerable<Type>? AcceptedNotificationTypes { get; }
|
||||
|
||||
private readonly LocalisableString titleText;
|
||||
|
||||
public NotificationSection(LocalisableString title, IEnumerable<Type> acceptedNotificationTypes)
|
||||
public NotificationSection(LocalisableString title, IEnumerable<Type>? acceptedNotificationTypes = null)
|
||||
{
|
||||
AcceptedNotificationTypes = acceptedNotificationTypes.ToArray();
|
||||
AcceptedNotificationTypes = acceptedNotificationTypes?.ToArray();
|
||||
|
||||
titleText = title;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users.Drawables;
|
||||
|
||||
namespace osu.Game.Overlays.Notifications
|
||||
{
|
||||
public partial class UserAvatarNotification : Notification
|
||||
{
|
||||
private LocalisableString text;
|
||||
|
||||
public override LocalisableString Text
|
||||
{
|
||||
get => text;
|
||||
set
|
||||
{
|
||||
text = value;
|
||||
if (textDrawable != null)
|
||||
textDrawable.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
private TextFlowContainer? textDrawable;
|
||||
|
||||
private readonly APIUser user;
|
||||
|
||||
public UserAvatarNotification(APIUser user, LocalisableString text)
|
||||
{
|
||||
this.user = user;
|
||||
Text = text;
|
||||
}
|
||||
|
||||
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, OverlayColourProvider colourProvider)
|
||||
{
|
||||
Light.Colour = colours.Orange2;
|
||||
|
||||
Content.Add(textDrawable = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 14, weight: FontWeight.Medium))
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = text
|
||||
});
|
||||
|
||||
IconContent.Masking = true;
|
||||
IconContent.CornerRadius = CORNER_RADIUS;
|
||||
|
||||
IconContent.AddRange(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
});
|
||||
|
||||
LoadComponentAsync(new DrawableAvatar(user)
|
||||
{
|
||||
FillMode = FillMode.Fill,
|
||||
}, IconContent.Add);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -18,92 +19,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
|
||||
public override LocalisableString Header => InputSettingsStrings.GlobalKeyBindingHeader;
|
||||
|
||||
public GlobalKeyBindingsSection(GlobalActionContainer manager)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(new DefaultBindingsSubsection(manager));
|
||||
Add(new OverlayBindingsSubsection(manager));
|
||||
Add(new AudioControlKeyBindingsSubsection(manager));
|
||||
Add(new SongSelectKeyBindingSubsection(manager));
|
||||
Add(new InGameKeyBindingsSubsection(manager));
|
||||
Add(new ReplayKeyBindingsSubsection(manager));
|
||||
Add(new EditorKeyBindingsSubsection(manager));
|
||||
}
|
||||
|
||||
private partial class DefaultBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => string.Empty;
|
||||
|
||||
public DefaultBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
AddRange(new[]
|
||||
{
|
||||
Defaults = manager.GlobalKeyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class OverlayBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => InputSettingsStrings.OverlaysSection;
|
||||
|
||||
public OverlayBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
{
|
||||
Defaults = manager.OverlayKeyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class SongSelectKeyBindingSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => InputSettingsStrings.SongSelectSection;
|
||||
|
||||
public SongSelectKeyBindingSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
{
|
||||
Defaults = manager.SongSelectKeyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class InGameKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => InputSettingsStrings.InGameSection;
|
||||
|
||||
public InGameKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
{
|
||||
Defaults = manager.InGameKeyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ReplayKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => InputSettingsStrings.ReplaySection;
|
||||
|
||||
public ReplayKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
{
|
||||
Defaults = manager.ReplayKeyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class AudioControlKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => InputSettingsStrings.AudioSection;
|
||||
|
||||
public AudioControlKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
{
|
||||
Defaults = manager.AudioControlKeyBindings;
|
||||
}
|
||||
}
|
||||
|
||||
private partial class EditorKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => InputSettingsStrings.EditorSection;
|
||||
|
||||
public EditorKeyBindingsSubsection(GlobalActionContainer manager)
|
||||
: base(null)
|
||||
{
|
||||
Defaults = manager.EditorKeyBindings;
|
||||
}
|
||||
new GlobalKeyBindingsSubsection(string.Empty, GlobalActionCategory.General),
|
||||
new GlobalKeyBindingsSubsection(InputSettingsStrings.OverlaysSection, GlobalActionCategory.Overlays),
|
||||
new GlobalKeyBindingsSubsection(InputSettingsStrings.AudioSection, GlobalActionCategory.AudioControl),
|
||||
new GlobalKeyBindingsSubsection(InputSettingsStrings.SongSelectSection, GlobalActionCategory.SongSelect),
|
||||
new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame),
|
||||
new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay),
|
||||
new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
public partial class GlobalKeyBindingsSubsection : KeyBindingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header { get; }
|
||||
|
||||
private readonly GlobalActionCategory category;
|
||||
|
||||
public GlobalKeyBindingsSubsection(LocalisableString header, GlobalActionCategory category)
|
||||
{
|
||||
Header = header;
|
||||
this.category = category;
|
||||
Defaults = GlobalActionContainer.GetDefaultBindingsFor(category);
|
||||
}
|
||||
|
||||
protected override IEnumerable<RealmKeyBinding> GetKeyBindings(Realm realm)
|
||||
{
|
||||
var bindings = realm.All<RealmKeyBinding>()
|
||||
.Where(b => b.RulesetName == null && b.Variant == null)
|
||||
.Detach();
|
||||
|
||||
var actionsInSection = GlobalActionContainer.GetGlobalActionsFor(category).Cast<int>().ToHashSet();
|
||||
return bindings.Where(kb => actionsInSection.Contains(kb.ActionInt));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
@@ -14,9 +13,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
protected override Drawable CreateHeader() => new SettingsHeader(InputSettingsStrings.KeyBindingPanelHeader, InputSettingsStrings.KeyBindingPanelDescription);
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(RulesetStore rulesets, GlobalActionContainer global)
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
AddSection(new GlobalKeyBindingsSection(global));
|
||||
AddSection(new GlobalKeyBindingsSection());
|
||||
|
||||
foreach (var ruleset in rulesets.AvailableRulesets)
|
||||
AddSection(new RulesetBindingsSection(ruleset));
|
||||
|
||||
@@ -1,15 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
@@ -37,16 +37,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
/// <summary>
|
||||
/// Invoked when the binding of this row is updated with a change being written.
|
||||
/// </summary>
|
||||
public Action<KeyBindingRow> BindingUpdated { get; set; }
|
||||
public Action<KeyBindingRow>? BindingUpdated { get; set; }
|
||||
|
||||
private readonly object action;
|
||||
private readonly IEnumerable<RealmKeyBinding> bindings;
|
||||
/// <summary>
|
||||
/// Whether left and right mouse button clicks should be included in the edited bindings.
|
||||
/// </summary>
|
||||
public bool AllowMainMouseButtons { get; init; }
|
||||
|
||||
private const float transition_time = 150;
|
||||
/// <summary>
|
||||
/// The default key bindings for this row.
|
||||
/// </summary>
|
||||
public IEnumerable<KeyCombination> Defaults { get; init; } = Array.Empty<KeyCombination>();
|
||||
|
||||
private const float height = 20;
|
||||
|
||||
private const float padding = 5;
|
||||
#region IFilterable
|
||||
|
||||
private bool matchingFilter;
|
||||
|
||||
@@ -60,24 +63,45 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
}
|
||||
}
|
||||
|
||||
private Container content;
|
||||
public bool FilteringActive { get; set; }
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text);
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly object action;
|
||||
private readonly IEnumerable<RealmKeyBinding> bindings;
|
||||
|
||||
private Bindable<bool> isDefault { get; } = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private Container content = null!;
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
private FillFlowContainer cancelAndClearButtons = null!;
|
||||
private FillFlowContainer<KeyButton> buttons = null!;
|
||||
|
||||
private KeyButton? bindTarget;
|
||||
|
||||
private const float transition_time = 150;
|
||||
private const float height = 20;
|
||||
private const float padding = 5;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
content.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public bool FilteringActive { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
|
||||
|
||||
private OsuSpriteText text;
|
||||
private FillFlowContainer cancelAndClearButtons;
|
||||
private FillFlowContainer<KeyButton> buttons;
|
||||
|
||||
private Bindable<bool> isDefault { get; } = new BindableBool(true);
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms => bindings.Select(b => (LocalisableString)keyCombinationProvider.GetReadableString(b.KeyCombination)).Prepend(text.Text);
|
||||
public override bool AcceptsFocus => bindTarget == null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="KeyBindingRow"/>.
|
||||
/// </summary>
|
||||
/// <param name="action">The action that this row contains bindings for.</param>
|
||||
/// <param name="bindings">The keybindings to display in this row.</param>
|
||||
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
||||
{
|
||||
this.action = action;
|
||||
@@ -87,9 +111,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
@@ -205,21 +226,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => bindTarget == null;
|
||||
|
||||
private KeyButton bindTarget;
|
||||
|
||||
public bool AllowMainMouseButtons;
|
||||
|
||||
public IEnumerable<KeyCombination> Defaults;
|
||||
|
||||
private bool isModifier(Key k) => k < Key.F1;
|
||||
|
||||
protected override bool OnClick(ClickEvent e) => true;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (!HasFocus || !bindTarget.IsHovered)
|
||||
if (!HasFocus)
|
||||
return base.OnMouseDown(e);
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
if (!bindTarget.IsHovered)
|
||||
return base.OnMouseDown(e);
|
||||
|
||||
if (!AllowMainMouseButtons)
|
||||
@@ -245,6 +263,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
if (bindTarget.IsHovered)
|
||||
finalise(false);
|
||||
// prevent updating bind target before clear button's action
|
||||
@@ -256,6 +276,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
if (HasFocus)
|
||||
{
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
if (bindTarget.IsHovered)
|
||||
{
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState, e.ScrollDelta), KeyCombination.FromScrollDelta(e.ScrollDelta).First());
|
||||
@@ -272,6 +294,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (!HasFocus || e.Repeat)
|
||||
return false;
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromKey(e.Key));
|
||||
if (!isModifier(e.Key)) finalise();
|
||||
|
||||
@@ -294,6 +318,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (!HasFocus)
|
||||
return false;
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromJoystickButton(e.Button));
|
||||
finalise();
|
||||
|
||||
@@ -316,6 +342,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (!HasFocus)
|
||||
return false;
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromMidiKey(e.Key));
|
||||
finalise();
|
||||
|
||||
@@ -338,6 +366,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (!HasFocus)
|
||||
return false;
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletAuxiliaryButton(e.Button));
|
||||
finalise();
|
||||
|
||||
@@ -360,6 +390,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
if (!HasFocus)
|
||||
return false;
|
||||
|
||||
Debug.Assert(bindTarget != null);
|
||||
|
||||
bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState), KeyCombination.FromTabletPenButton(e.Button));
|
||||
finalise();
|
||||
|
||||
@@ -473,10 +505,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
public readonly OsuSpriteText Text;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; }
|
||||
private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;
|
||||
|
||||
private bool isBinding;
|
||||
|
||||
@@ -599,7 +631,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (keyCombinationProvider != null)
|
||||
if (keyCombinationProvider.IsNotNull())
|
||||
keyCombinationProvider.KeymapChanged -= updateKeyCombinationText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Localisation;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
@@ -25,39 +25,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
/// </summary>
|
||||
protected virtual bool AutoAdvanceTarget => false;
|
||||
|
||||
protected IEnumerable<Framework.Input.Bindings.KeyBinding> Defaults;
|
||||
protected IEnumerable<KeyBinding> Defaults { get; init; } = Array.Empty<KeyBinding>();
|
||||
|
||||
public RulesetInfo Ruleset { get; protected set; }
|
||||
|
||||
private readonly int? variant;
|
||||
|
||||
protected KeyBindingsSubsection(int? variant)
|
||||
protected KeyBindingsSubsection()
|
||||
{
|
||||
this.variant = variant;
|
||||
|
||||
FlowContent.Spacing = new Vector2(0, 3);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RealmAccess realm)
|
||||
{
|
||||
string rulesetName = Ruleset?.ShortName;
|
||||
|
||||
var bindings = realm.Run(r => r.All<RealmKeyBinding>()
|
||||
.Where(b => b.RulesetName == rulesetName && b.Variant == variant)
|
||||
.Detach());
|
||||
var bindings = realm.Run(r => GetKeyBindings(r).Detach());
|
||||
|
||||
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
||||
{
|
||||
int intKey = (int)defaultGroup.Key;
|
||||
|
||||
// one row per valid action.
|
||||
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList())
|
||||
{
|
||||
AllowMainMouseButtons = Ruleset != null,
|
||||
Defaults = defaultGroup.Select(d => d.KeyCombination),
|
||||
BindingUpdated = onBindingUpdated
|
||||
});
|
||||
Add(CreateKeyBindingRow(
|
||||
defaultGroup.Key,
|
||||
bindings.Where(b => b.ActionInt.Equals(intKey)).ToList(),
|
||||
defaultGroup)
|
||||
.With(row => row.BindingUpdated = onBindingUpdated));
|
||||
}
|
||||
|
||||
Add(new ResetButton
|
||||
@@ -66,6 +55,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract IEnumerable<RealmKeyBinding> GetKeyBindings(Realm realm);
|
||||
|
||||
protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable<RealmKeyBinding> keyBindings, IEnumerable<KeyBinding> defaults)
|
||||
=> new KeyBindingRow(action, keyBindings.ToList())
|
||||
{
|
||||
AllowMainMouseButtons = false,
|
||||
Defaults = defaults.Select(d => d.KeyCombination),
|
||||
};
|
||||
|
||||
private void onBindingUpdated(KeyBindingRow sender)
|
||||
{
|
||||
if (AutoAdvanceTarget)
|
||||
|
||||
@@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
@@ -18,7 +19,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
public RulesetBindingsSection(RulesetInfo ruleset)
|
||||
{
|
||||
this.ruleset = ruleset;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var r = ruleset.CreateInstance();
|
||||
|
||||
foreach (int variant in r.AvailableVariants)
|
||||
|
||||
@@ -1,8 +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.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
@@ -12,15 +17,33 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
|
||||
protected override LocalisableString Header { get; }
|
||||
|
||||
public RulesetInfo Ruleset { get; }
|
||||
private readonly int variant;
|
||||
|
||||
public VariantBindingsSubsection(RulesetInfo ruleset, int variant)
|
||||
: base(variant)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
this.variant = variant;
|
||||
|
||||
var rulesetInstance = ruleset.CreateInstance();
|
||||
|
||||
Header = rulesetInstance.GetVariantName(variant);
|
||||
Defaults = rulesetInstance.GetDefaultKeyBindings(variant);
|
||||
}
|
||||
|
||||
protected override IEnumerable<RealmKeyBinding> GetKeyBindings(Realm realm)
|
||||
{
|
||||
string rulesetName = Ruleset.ShortName;
|
||||
|
||||
return realm.All<RealmKeyBinding>()
|
||||
.Where(b => b.RulesetName == rulesetName && b.Variant == variant);
|
||||
}
|
||||
|
||||
protected override KeyBindingRow CreateKeyBindingRow(object action, IEnumerable<RealmKeyBinding> keyBindings, IEnumerable<KeyBinding> defaults)
|
||||
=> new KeyBindingRow(action, keyBindings.ToList())
|
||||
{
|
||||
AllowMainMouseButtons = true,
|
||||
Defaults = defaults.Select(d => d.KeyCombination),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,40 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// <summary>
|
||||
/// The minimum <see cref="HitResult"/> that can be achieved - the inverse of <see cref="MaxResult"/>.
|
||||
/// </summary>
|
||||
public HitResult MinResult
|
||||
/// <remarks>
|
||||
/// Defaults to a sane value for the given <see cref="MaxResult"/>. May be overridden to provide a supported custom value:
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term><see cref="MaxResult"/>s</term>
|
||||
/// <description>Valid <see cref="MinResult"/>s</description>
|
||||
/// </listheader>
|
||||
/// <item>
|
||||
/// <term><see cref="HitResult.Perfect"/>, <see cref="HitResult.Great"/>, <see cref="HitResult.Good"/>, <see cref="HitResult.Ok"/>, <see cref="HitResult.Meh"/></term>
|
||||
/// <description><see cref="HitResult.Miss"/></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="HitResult.LargeBonus"/></term>
|
||||
/// <description><see cref="HitResult.IgnoreMiss"/></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="HitResult.SmallBonus"/></term>
|
||||
/// <description><see cref="HitResult.IgnoreMiss"/></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="HitResult.SmallTickHit"/></term>
|
||||
/// <description><see cref="HitResult.SmallTickMiss"/></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="HitResult.LargeTickHit"/></term>
|
||||
/// <description><see cref="HitResult.LargeTickMiss"/></description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term><see cref="HitResult.IgnoreHit"/></term>
|
||||
/// <description><see cref="HitResult.IgnoreMiss"/>, <see cref="HitResult.ComboBreak"/></description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public virtual HitResult MinResult
|
||||
{
|
||||
get
|
||||
{
|
||||
|
||||
@@ -672,6 +672,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
if (!Result.HasResult)
|
||||
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
|
||||
|
||||
HitResultExtensions.ValidateHitResultPair(Result.Judgement.MaxResult, Result.Judgement.MinResult);
|
||||
|
||||
if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
|
||||
@@ -120,6 +120,16 @@ namespace osu.Game.Rulesets.Scoring
|
||||
[Order(12)]
|
||||
IgnoreHit,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a combo break should occur, but does not otherwise affect score.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// May be paired with <see cref="IgnoreHit"/>.
|
||||
/// </remarks>
|
||||
[EnumMember(Value = "combo_break")]
|
||||
[Order(15)]
|
||||
ComboBreak,
|
||||
|
||||
/// <summary>
|
||||
/// A special result used as a padding value for legacy rulesets. It is a hit type and affects combo, but does not affect the base score (does not affect accuracy).
|
||||
/// </summary>
|
||||
@@ -165,6 +175,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.LegacyComboIncrease:
|
||||
case HitResult.ComboBreak:
|
||||
return true;
|
||||
|
||||
default:
|
||||
@@ -177,11 +188,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public static bool AffectsAccuracy(this HitResult result)
|
||||
{
|
||||
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
|
||||
if (result == HitResult.LegacyComboIncrease)
|
||||
return false;
|
||||
switch (result)
|
||||
{
|
||||
// LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result.
|
||||
case HitResult.LegacyComboIncrease:
|
||||
return false;
|
||||
|
||||
return IsScorable(result) && !IsBonus(result);
|
||||
// ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting.
|
||||
case HitResult.ComboBreak:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return IsScorable(result) && !IsBonus(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -189,11 +208,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public static bool IsBasic(this HitResult result)
|
||||
{
|
||||
// LegacyComboIncrease is a special type which is neither a basic, tick, bonus, or accuracy-affecting result.
|
||||
if (result == HitResult.LegacyComboIncrease)
|
||||
return false;
|
||||
switch (result)
|
||||
{
|
||||
// LegacyComboIncrease is a special non-gameplay type which is neither a basic, tick, bonus, or accuracy-affecting result.
|
||||
case HitResult.LegacyComboIncrease:
|
||||
return false;
|
||||
|
||||
return IsScorable(result) && !IsTick(result) && !IsBonus(result);
|
||||
// ComboBreak is a special type that only affects combo. It cannot be considered as basic, tick, bonus, or accuracy-affecting.
|
||||
case HitResult.ComboBreak:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return IsScorable(result) && !IsTick(result) && !IsBonus(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -242,6 +269,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
case HitResult.Miss:
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.ComboBreak:
|
||||
return false;
|
||||
|
||||
default:
|
||||
@@ -254,11 +282,20 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public static bool IsScorable(this HitResult result)
|
||||
{
|
||||
// LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
|
||||
if (result == HitResult.LegacyComboIncrease)
|
||||
return true;
|
||||
switch (result)
|
||||
{
|
||||
// LegacyComboIncrease is not actually scorable (in terms of usable by rulesets for that purpose), but needs to be defined as such to be correctly included in statistics output.
|
||||
case HitResult.LegacyComboIncrease:
|
||||
return true;
|
||||
|
||||
return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
|
||||
// ComboBreak is its own type that affects score via combo.
|
||||
case HitResult.ComboBreak:
|
||||
return true;
|
||||
|
||||
default:
|
||||
// Note that IgnoreHit and IgnoreMiss are excluded as they do not affect score.
|
||||
return result >= HitResult.Miss && result < HitResult.IgnoreMiss;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -291,6 +328,30 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="result">The <see cref="HitResult"/> to get the index of.</param>
|
||||
/// <returns>The index of <paramref name="result"/>.</returns>
|
||||
public static int GetIndexForOrderedDisplay(this HitResult result) => order.IndexOf(result);
|
||||
|
||||
public static void ValidateHitResultPair(HitResult maxResult, HitResult minResult)
|
||||
{
|
||||
if (maxResult == HitResult.None || !IsHit(maxResult))
|
||||
throw new ArgumentOutOfRangeException(nameof(maxResult), $"{maxResult} is not a valid maximum judgement result.");
|
||||
|
||||
if (minResult == HitResult.None || IsHit(minResult))
|
||||
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum judgement result.");
|
||||
|
||||
if (maxResult == HitResult.IgnoreHit && minResult is not (HitResult.IgnoreMiss or HitResult.ComboBreak))
|
||||
throw new ArgumentOutOfRangeException(nameof(minResult), $"{minResult} is not a valid minimum result for a {maxResult} judgement.");
|
||||
|
||||
if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss)
|
||||
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement.");
|
||||
|
||||
if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss)
|
||||
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement.");
|
||||
|
||||
if (maxResult == HitResult.SmallTickHit && minResult != HitResult.SmallTickMiss)
|
||||
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.SmallTickMiss} is the only valid minimum result for a {maxResult} judgement.");
|
||||
|
||||
if (maxResult.IsBasic() && minResult != HitResult.Miss)
|
||||
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.Miss} is the only valid minimum result for a {maxResult} judgement.");
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
{
|
||||
@@ -16,5 +18,13 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
/// <param name="workingBeatmap">The working beatmap.</param>
|
||||
/// <param name="playableBeatmap">A playable version of the beatmap for the ruleset.</param>
|
||||
LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the legacy score multiplier for the mods. This is only used during legacy score conversion.
|
||||
/// </summary>
|
||||
/// <param name="mods">The mods.</param>
|
||||
/// <param name="difficulty">Extra difficulty parameters.</param>
|
||||
/// <returns>The legacy multiplier.</returns>
|
||||
double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
{
|
||||
/// <summary>
|
||||
/// A set of properties that are required to facilitate beatmap conversion between legacy rulesets.
|
||||
/// </summary>
|
||||
public class LegacyBeatmapConversionDifficultyInfo : IBeatmapDifficultyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The beatmap's ruleset.
|
||||
/// </summary>
|
||||
public IRulesetInfo SourceRuleset { get; set; } = new RulesetInfo();
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap circle size.
|
||||
/// </summary>
|
||||
public float CircleSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap overall difficulty.
|
||||
/// </summary>
|
||||
public float OverallDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The count of hitcircles in the beatmap.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When converting from osu! ruleset beatmaps, this is equivalent to the sum of sliders and spinners in the beatmap.
|
||||
/// </remarks>
|
||||
public int CircleCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total count of hitobjects in the beatmap.
|
||||
/// </summary>
|
||||
public int TotalObjectCount { get; set; }
|
||||
|
||||
float IBeatmapDifficultyInfo.DrainRate => 0;
|
||||
float IBeatmapDifficultyInfo.ApproachRate => 0;
|
||||
double IBeatmapDifficultyInfo.SliderMultiplier => 0;
|
||||
double IBeatmapDifficultyInfo.SliderTickRate => 0;
|
||||
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromAPIBeatmap(APIBeatmap apiBeatmap) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = apiBeatmap.Ruleset,
|
||||
CircleSize = apiBeatmap.CircleSize,
|
||||
OverallDifficulty = apiBeatmap.OverallDifficulty,
|
||||
CircleCount = apiBeatmap.CircleCount,
|
||||
TotalObjectCount = apiBeatmap.SliderCount + apiBeatmap.SpinnerCount + apiBeatmap.CircleCount
|
||||
};
|
||||
|
||||
public static LegacyBeatmapConversionDifficultyInfo FromBeatmap(IBeatmap beatmap) => new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = beatmap.BeatmapInfo.Ruleset,
|
||||
CircleSize = beatmap.Difficulty.CircleSize,
|
||||
OverallDifficulty = beatmap.Difficulty.OverallDifficulty,
|
||||
CircleCount = beatmap.HitObjects.Count(h => h is not IHasDuration),
|
||||
TotalObjectCount = beatmap.HitObjects.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,16 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
@@ -21,8 +22,10 @@ namespace osu.Game.Rulesets.UI
|
||||
public BindableBool Active { get; } = new BindableBool();
|
||||
|
||||
public const float DEFAULT_HEIGHT = 30;
|
||||
private const float width = 73;
|
||||
|
||||
private readonly IMod mod;
|
||||
protected readonly IMod Mod;
|
||||
private readonly bool showExtendedInformation;
|
||||
|
||||
private readonly Box background;
|
||||
private readonly OsuSpriteText acronymText;
|
||||
@@ -33,33 +36,69 @@ namespace osu.Game.Rulesets.UI
|
||||
private Color4 activeBackgroundColour;
|
||||
private Color4 inactiveBackgroundColour;
|
||||
|
||||
public ModSwitchTiny(IMod mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
Size = new Vector2(73, DEFAULT_HEIGHT);
|
||||
private readonly CircularContainer extendedContent;
|
||||
private readonly Box extendedBackground;
|
||||
private readonly OsuSpriteText extendedText;
|
||||
private ModSettingChangeTracker? modSettingsChangeTracker;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
public ModSwitchTiny(IMod mod, bool showExtendedInformation = false)
|
||||
{
|
||||
Mod = mod;
|
||||
this.showExtendedInformation = showExtendedInformation;
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = DEFAULT_HEIGHT;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
extendedContent = new CircularContainer
|
||||
{
|
||||
background = new Box
|
||||
Name = "extended content",
|
||||
Width = 100 + DEFAULT_HEIGHT / 2,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Masking = true,
|
||||
X = width,
|
||||
Margin = new MarginPadding { Left = -DEFAULT_HEIGHT },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
acronymText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black),
|
||||
Text = mod.Acronym,
|
||||
Margin = new MarginPadding
|
||||
extendedBackground = new Box
|
||||
{
|
||||
Top = 4
|
||||
}
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
extendedText = new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding { Left = 3 * DEFAULT_HEIGHT / 4 },
|
||||
Font = OsuFont.Default.With(size: 30f, weight: FontWeight.Bold),
|
||||
UseFullGlyphHeight = false,
|
||||
Text = mod.ExtendedIconInformation,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
}
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Width = width,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
acronymText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
Font = OsuFont.Numeric.With(size: 24, weight: FontWeight.Black),
|
||||
Text = mod.Acronym,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Top = 4
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -68,7 +107,7 @@ namespace osu.Game.Rulesets.UI
|
||||
private void load(OsuColour colours, OverlayColourProvider? colourProvider)
|
||||
{
|
||||
inactiveBackgroundColour = colourProvider?.Background5 ?? colours.Gray3;
|
||||
activeBackgroundColour = colours.ForModType(mod.Type);
|
||||
activeBackgroundColour = colours.ForModType(Mod.Type);
|
||||
|
||||
inactiveForegroundColour = colourProvider?.Background2 ?? colours.Gray5;
|
||||
activeForegroundColour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, activeForegroundColour, 0, 1);
|
||||
@@ -80,12 +119,37 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
Active.BindValueChanged(_ => updateState(), true);
|
||||
FinishTransforms(true);
|
||||
|
||||
if (Mod is Mod actualMod)
|
||||
{
|
||||
modSettingsChangeTracker = new ModSettingChangeTracker(new[] { actualMod });
|
||||
modSettingsChangeTracker.SettingChanged = _ => updateExtendedInformation();
|
||||
}
|
||||
|
||||
updateExtendedInformation();
|
||||
}
|
||||
|
||||
private void updateExtendedInformation()
|
||||
{
|
||||
bool showExtended = showExtendedInformation && !string.IsNullOrEmpty(Mod.ExtendedIconInformation);
|
||||
|
||||
extendedContent.Alpha = showExtended ? 1 : 0;
|
||||
extendedText.Text = Mod.ExtendedIconInformation;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
acronymText.FadeColour(Active.Value ? activeForegroundColour : inactiveForegroundColour, 200, Easing.OutQuint);
|
||||
background.FadeColour(Active.Value ? activeBackgroundColour : inactiveBackgroundColour, 200, Easing.OutQuint);
|
||||
|
||||
extendedText.Colour = Active.Value ? activeBackgroundColour.Lighten(0.2f) : inactiveBackgroundColour;
|
||||
extendedBackground.Colour = Active.Value ? activeBackgroundColour.Darken(2.4f) : inactiveBackgroundColour.Darken(2.8f);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
modSettingsChangeTracker?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
|
||||
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion.</description></item>
|
||||
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
|
||||
/// <item><description>30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000003;
|
||||
public const int LATEST_VERSION = 30000004;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
||||
@@ -190,6 +190,9 @@ namespace osu.Game.Scoring
|
||||
/// </summary>
|
||||
private void populateUserDetails(ScoreInfo model)
|
||||
{
|
||||
if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID)
|
||||
return;
|
||||
|
||||
string username = model.RealmUser.Username;
|
||||
|
||||
if (usernameLookupCache.TryGetValue(username, out var existing))
|
||||
|
||||
@@ -346,7 +346,7 @@ namespace osu.Game.Scoring
|
||||
case HitResult.LargeBonus:
|
||||
case HitResult.SmallBonus:
|
||||
if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0)
|
||||
yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
|
||||
yield return new HitResultDisplayStatistic(r.result, value, count, r.displayName);
|
||||
|
||||
break;
|
||||
|
||||
|
||||
@@ -456,6 +456,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
|
||||
private IEnumerable<Drawable> createButtons() => new[]
|
||||
{
|
||||
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
|
||||
showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
|
||||
{
|
||||
Size = new Vector2(30, 30),
|
||||
@@ -463,7 +464,6 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
Alpha = AllowShowingResults ? 1 : 0,
|
||||
TooltipText = "View results"
|
||||
},
|
||||
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
|
||||
editButton = new PlaylistEditButton
|
||||
{
|
||||
Size = new Vector2(30, 30),
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
|
||||
@@ -90,6 +91,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen();
|
||||
|
||||
public void Join(Room room, string? password) => Schedule(() => Lounge.Join(room, password));
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -148,6 +148,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
loadingDisplay.Show();
|
||||
client.ChangeState(MultiplayerUserState.ReadyForGameplay);
|
||||
}
|
||||
|
||||
// This will pause the clock, pending the gameplay started callback from the server.
|
||||
GameplayClockContainer.Reset();
|
||||
}
|
||||
|
||||
private void failAndBail(string message = null)
|
||||
|
||||