mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
更新,增加皮肤判定组件
This commit is contained in:
187
osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs
Normal file
187
osu.Game.Rulesets.Mania.Tests/ManiaFilterCriteriaTest.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaFilterCriteriaTest
|
||||
{
|
||||
[TestCase]
|
||||
public void TestKeysEqualSingleValue()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysEqualMultipleValues()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysNotEqualSingleValue()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysNotEqualMultipleValues()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysGreaterOrEqualThan()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey7()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestFilterIntersection()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }),
|
||||
new FilterCriteria()));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestInvalidFilters()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
@@ -17,20 +18,72 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private FilterCriteria.OptionalRange<float> keys;
|
||||
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
|
||||
{
|
||||
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
|
||||
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
|
||||
|
||||
return includedKeyCounts.Contains(keyCount);
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "key":
|
||||
case "keys":
|
||||
return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
|
||||
{
|
||||
var keyCounts = new HashSet<int>();
|
||||
|
||||
foreach (string strValue in strValues.Split(','))
|
||||
{
|
||||
if (!int.TryParse(strValue, out int keyCount))
|
||||
return false;
|
||||
|
||||
keyCounts.Add(keyCount);
|
||||
}
|
||||
|
||||
int? singleKeyCount = keyCounts.Count == 1 ? keyCounts.Single() : null;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case Operator.Equal:
|
||||
includedKeyCounts.IntersectWith(keyCounts);
|
||||
return true;
|
||||
|
||||
case Operator.NotEqual:
|
||||
includedKeyCounts.ExceptWith(keyCounts);
|
||||
return true;
|
||||
|
||||
case Operator.Less:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k >= singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.LessOrEqual:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k > singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.Greater:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k <= singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.GreaterOrEqual:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k < singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -38,7 +91,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
if (keys.HasFilter)
|
||||
if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT)
|
||||
{
|
||||
// Interpreting as the Mod type is required for equality comparison.
|
||||
HashSet<Mod> oldSet = mods.OldValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
private partial class ManiaScrollBaseSpeedSlider : RoundedSliderBar<double>
|
||||
{
|
||||
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(
|
||||
Current.Value, 200);
|
||||
(int)Current.Value, 200);
|
||||
}
|
||||
|
||||
private partial class ManiaScrollTimePerSpeedSlider : RoundedSliderBar<double>
|
||||
|
||||
@@ -1,106 +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 System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class Ez2ManiaComboCounter : ComboCounter
|
||||
{
|
||||
protected EzComCounterText Text = null!;
|
||||
|
||||
// public IBindable<float> WireframeOpacity { get; } = new BindableFloat();
|
||||
protected override double RollingDuration => 250;
|
||||
protected virtual bool DisplayXSymbol => true;
|
||||
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
private IBindable<ScrollingDirection> direction = null!;
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
|
||||
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
Current.BindValueChanged(combo =>
|
||||
{
|
||||
bool wasIncrease = combo.NewValue > combo.OldValue;
|
||||
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;
|
||||
|
||||
float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 3f : 1f), 0.6f, 3f);
|
||||
|
||||
float duration = wasMiss ? 2000 : 500;
|
||||
|
||||
Text.NumberContainer
|
||||
.ScaleTo(new Vector2(newScale))
|
||||
.ScaleTo(Vector2.One, duration, Easing.OutQuint);
|
||||
|
||||
if (wasMiss)
|
||||
Text.FlashColour(Color4.Red, duration, Easing.OutQuint);
|
||||
});
|
||||
}
|
||||
|
||||
private int getDigitsRequiredForDisplayCount()
|
||||
{
|
||||
// one for the single presumed starting digit, one for the "x" at the end (unless disabled).
|
||||
int digitsRequired = DisplayXSymbol ? 2 : 1;
|
||||
long c = DisplayedCount;
|
||||
while ((c /= 10) > 0)
|
||||
digitsRequired++;
|
||||
return digitsRequired;
|
||||
}
|
||||
|
||||
protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? $@"{count}" : count.ToString();
|
||||
|
||||
protected override IHasText CreateText() => Text = new EzComCounterText(Anchor.TopCentre, MatchesStrings.MatchScoreStatsCombo.ToUpper())
|
||||
{
|
||||
// WireframeOpacity = { BindTarget = WireframeOpacity },
|
||||
ShowLabel = { BindTarget = ShowLabel },
|
||||
};
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
UsesFixedAnchor = true;
|
||||
|
||||
direction = scrollingInfo.Direction.GetBoundCopy();
|
||||
direction.BindValueChanged(_ => updateAnchor());
|
||||
|
||||
// 需要两个调度,以便在下一帧执行 updateAnchor,
|
||||
// 这是 combo 计数器通过 Ez2ManiaSkinTransformer 的默认布局接收其 Y 位置的时间。
|
||||
Schedule(() => Schedule(updateAnchor));
|
||||
}
|
||||
|
||||
private void updateAnchor()
|
||||
{
|
||||
// 如果锚点不是垂直中心,则根据滚动方向设置顶部或底部锚点
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
|
||||
// 根据滚动方向更改 Y 坐标的符号。
|
||||
// 即,如果用户将方向从下更改为上,锚点从顶部更改为底部,Y 从正数翻转为负数。
|
||||
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
@@ -122,17 +120,38 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
|
||||
private void applyBounceAnimation(bool wasIncrease, bool wasMiss)
|
||||
{
|
||||
float factor = Math.Clamp(wasIncrease ? -10 * IncreaseScale.Value : 10 * DecreaseScale.Value, -100f, 100f);
|
||||
float factor = Math.Clamp(wasIncrease ? 10 * IncreaseScale.Value : -10 * DecreaseScale.Value, -100f, 100f);
|
||||
|
||||
Anchor originAnchor = Enum.Parse<Anchor>(AnimationOrigin.Value.ToString());
|
||||
|
||||
Text.NumberContainer.Anchor = originAnchor;
|
||||
Text.NumberContainer.Origin = originAnchor;
|
||||
|
||||
float moveToYStart = 0;
|
||||
float moveToYEnd = 0;
|
||||
|
||||
switch (AnimationOrigin.Value)
|
||||
{
|
||||
case OriginOptions.TopCentre:
|
||||
moveToYStart = factor;
|
||||
moveToYEnd = -5;
|
||||
break;
|
||||
|
||||
case OriginOptions.BottomCentre:
|
||||
moveToYStart = -factor;
|
||||
moveToYEnd = 5;
|
||||
break;
|
||||
|
||||
case OriginOptions.Centre:
|
||||
moveToYStart = factor;
|
||||
moveToYEnd = -5;
|
||||
break;
|
||||
}
|
||||
|
||||
Text.NumberContainer
|
||||
.MoveToY(factor, IncreaseDuration.Value / 2, Easing.OutBounce)
|
||||
.MoveToY(moveToYStart, IncreaseDuration.Value / 2, Easing.OutBounce)
|
||||
.Then()
|
||||
.MoveToY(0, DecreaseDuration.Value, Easing.OutBounce);
|
||||
.MoveToY(moveToYEnd, DecreaseDuration.Value, Easing.OutBounce);
|
||||
|
||||
if (wasMiss)
|
||||
Text.FlashColour(Color4.Red, DecreaseDuration.Value, Easing.OutQuint);
|
||||
@@ -145,11 +164,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
ShowLabel = { BindTarget = ShowLabel },
|
||||
};
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Stat.With(size: 40f, family: "Ez"),
|
||||
};
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@@ -162,7 +176,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
public enum AnimationType
|
||||
{
|
||||
Scale,
|
||||
Bounce
|
||||
Bounce,
|
||||
None
|
||||
}
|
||||
|
||||
public enum OriginOptions
|
||||
|
||||
251
osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs
Normal file
251
osu.Game.Rulesets.Mania/Skinning/Ez2/Ez2HUD/EzComComboSprite.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence comboSprite.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Texture = osu.Framework.Graphics.Textures.Texture;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComComboSprite : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.SpriteName), nameof(SkinnableComponentStrings.SpriteNameDescription), SettingControlType = typeof(SpriteSelectorControl))]
|
||||
public Bindable<string> SpriteName { get; } = new Bindable<string>("default");
|
||||
|
||||
[SettingSource("Animation Type", "The type of animation to apply")]
|
||||
public Bindable<AnimationType> Animation { get; } = new Bindable<AnimationType>(AnimationType.Scale);
|
||||
|
||||
[SettingSource("Increase Scale", "The scale factor when the combo increases")]
|
||||
public BindableNumber<float> IncreaseScale { get; } = new BindableNumber<float>(0.5f)
|
||||
{
|
||||
MinValue = 0.1f,
|
||||
MaxValue = 5f,
|
||||
Precision = 0.05f,
|
||||
};
|
||||
|
||||
[SettingSource("Increase Duration", "The scale duration time when the combo increases")]
|
||||
public BindableNumber<float> IncreaseDuration { get; } = new BindableNumber<float>(10)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 300,
|
||||
Precision = 1f,
|
||||
};
|
||||
|
||||
[SettingSource("Decrease Duration", "The scale duration time when the combo decrease")]
|
||||
public BindableNumber<float> DecreaseDuration { get; } = new BindableNumber<float>(200)
|
||||
{
|
||||
MinValue = 10,
|
||||
MaxValue = 500,
|
||||
Precision = 10f,
|
||||
};
|
||||
|
||||
[SettingSource("Animation Origin", "The origin point for the animation")]
|
||||
public Bindable<OriginOptions> AnimationOrigin { get; } = new Bindable<OriginOptions>(OriginOptions.TopCentre);
|
||||
|
||||
[SettingSource("Alpha", "The alpha value of this box")]
|
||||
public BindableNumber<float> BoxAlpha { get; } = new BindableNumber<float>(1)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
public Bindable<int> Current { get; } = new Bindable<int>();
|
||||
private Sprite comboSprite = null!;
|
||||
private readonly Dictionary<string, Texture> textureMap = new Dictionary<string, Texture>();
|
||||
|
||||
[Resolved]
|
||||
private TextureStore textures { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IRenderer renderer { get; set; } = null!;
|
||||
|
||||
public EzComComboSprite(string textureName)
|
||||
{
|
||||
SpriteName.Value = textureName;
|
||||
}
|
||||
|
||||
public EzComComboSprite()
|
||||
{
|
||||
RelativeSizeAxes = Axes.None;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
SpriteName.BindValueChanged(name =>
|
||||
{
|
||||
if (IsLoaded)
|
||||
updateTexture();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void updateTexture()
|
||||
{
|
||||
string lookupName = SpriteName.Value;
|
||||
string localPath = Path.Combine("Skins/Ez2/combo", $"{lookupName}.png");
|
||||
|
||||
Texture? texture = null;
|
||||
|
||||
Logger.Log($"Found {localPath}");
|
||||
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
using (var stream = File.OpenRead(localPath))
|
||||
{
|
||||
texture = Texture.FromStream(renderer, stream);
|
||||
Logger.Log($"Loaded texture from {localPath}");
|
||||
}
|
||||
}
|
||||
|
||||
texture ??= textures.Get(@"Gameplay/Ez2/combo/default_combo.png");
|
||||
|
||||
comboSprite.Texture = texture;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
InternalChild = comboSprite = new Sprite
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre
|
||||
};
|
||||
|
||||
foreach (var item in SpriteSelectorControl.SPRITE_PATH_MAP)
|
||||
{
|
||||
var texture = textures.Get(item.Value);
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
textureMap[item.Key] = texture;
|
||||
}
|
||||
}
|
||||
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
Current.BindValueChanged(combo =>
|
||||
{
|
||||
bool wasIncrease = combo.NewValue > combo.OldValue;
|
||||
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;
|
||||
|
||||
switch (Animation.Value)
|
||||
{
|
||||
case AnimationType.Scale:
|
||||
applyScaleAnimation(wasIncrease, wasMiss);
|
||||
break;
|
||||
|
||||
case AnimationType.Bounce:
|
||||
applyBounceAnimation(wasIncrease, wasMiss);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
updateTexture();
|
||||
}
|
||||
|
||||
private void applyScaleAnimation(bool wasIncrease, bool wasMiss)
|
||||
{
|
||||
float newScaleValue = Math.Clamp(comboSprite.Scale.X * (wasIncrease ? IncreaseScale.Value : 0.8f), 0.5f, 3f);
|
||||
Vector2 newScale = new Vector2(newScaleValue);
|
||||
|
||||
setSpriteAnchorAndOrigin();
|
||||
|
||||
comboSprite
|
||||
.ScaleTo(newScale, IncreaseDuration.Value, Easing.OutQuint)
|
||||
.Then()
|
||||
.ScaleTo(Vector2.One, DecreaseDuration.Value, Easing.OutQuint);
|
||||
|
||||
if (wasMiss)
|
||||
comboSprite.FlashColour(Color4.Red, DecreaseDuration.Value, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void applyBounceAnimation(bool wasIncrease, bool wasMiss)
|
||||
{
|
||||
float factor = Math.Clamp(wasIncrease ? -10 * IncreaseScale.Value : 50, -100f, 100f);
|
||||
|
||||
setSpriteAnchorAndOrigin();
|
||||
|
||||
comboSprite
|
||||
.MoveToY(factor, IncreaseDuration.Value / 2, Easing.OutBounce)
|
||||
.Then()
|
||||
.MoveToY(0, DecreaseDuration.Value, Easing.OutBounce);
|
||||
|
||||
if (wasMiss)
|
||||
comboSprite.FlashColour(Color4.Red, DecreaseDuration.Value, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void setSpriteAnchorAndOrigin()
|
||||
{
|
||||
Anchor originAnchor = Enum.Parse<Anchor>(AnimationOrigin.Value.ToString());
|
||||
comboSprite.Anchor = originAnchor;
|
||||
comboSprite.Origin = originAnchor;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
BoxAlpha.BindValueChanged(alpha => comboSprite.Alpha = alpha.NewValue, true);
|
||||
}
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public partial class SpriteSelectorControl : SettingsDropdown<string>
|
||||
{
|
||||
[Resolved]
|
||||
private Storage storage { get; set; } = null!;
|
||||
|
||||
internal static readonly Dictionary<string, string> SPRITE_PATH_MAP = new Dictionary<string, string>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
const string skin_path = @"Skins\Ez2\combo";
|
||||
SPRITE_PATH_MAP.Clear();
|
||||
SPRITE_PATH_MAP["default"] = @"Gameplay/Ez2/combo/default_combo.png";
|
||||
|
||||
string fullPath = storage.GetFullPath(skin_path);
|
||||
|
||||
if (!Directory.Exists(fullPath))
|
||||
Directory.CreateDirectory(fullPath);
|
||||
|
||||
string[] files = Directory.GetFiles(fullPath, "*.png", SearchOption.TopDirectoryOnly);
|
||||
|
||||
foreach (string file in files)
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(file);
|
||||
SPRITE_PATH_MAP[fileName] = Path.Combine(skin_path, Path.GetFileName(file));
|
||||
}
|
||||
|
||||
Items = SPRITE_PATH_MAP.Keys.ToList();
|
||||
|
||||
Current.ValueChanged += e =>
|
||||
{
|
||||
if (SPRITE_PATH_MAP.TryGetValue(e.NewValue, out string? path))
|
||||
{
|
||||
if (Current.Value != path)
|
||||
Current.Value = path;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComComboSprite2 : ComboCounter
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Container directionContainer = null!;
|
||||
|
||||
private Drawable noteAnimation = null!;
|
||||
|
||||
public EzComComboSprite2()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
|
||||
{
|
||||
InternalChild = directionContainer = new Container
|
||||
{
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = noteAnimation = GetAnimation(skin) ?? Empty()
|
||||
};
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(OnDirectionChanged, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Texture? texture = null;
|
||||
|
||||
if (noteAnimation is Sprite sprite)
|
||||
texture = sprite.Texture;
|
||||
else if (noteAnimation is TextureAnimation textureAnimation && textureAnimation.FrameCount > 0)
|
||||
texture = textureAnimation.CurrentFrame;
|
||||
|
||||
if (texture != null)
|
||||
{
|
||||
// The height is scaled to the minimum column width, if provided.
|
||||
float minimumWidth = minimumColumnWidth ?? DrawWidth;
|
||||
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
if (direction.NewValue == ScrollingDirection.Up)
|
||||
{
|
||||
directionContainer.Anchor = Anchor.TopCentre;
|
||||
directionContainer.Scale = new Vector2(1, -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
directionContainer.Anchor = Anchor.BottomCentre;
|
||||
directionContainer.Scale = Vector2.One;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Drawable? GetAnimation(ISkinSource skin) => GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
|
||||
|
||||
protected Drawable? GetAnimationFromLookup(ISkin skin, LegacyManiaSkinConfigurationLookups lookup)
|
||||
{
|
||||
string[] videoFiles = System.IO.Directory.GetFiles(@"Textures/Webm", "*.webm");
|
||||
|
||||
if (videoFiles.Length == 0)
|
||||
{
|
||||
// 没有找到视频文件,返回一个默认背景
|
||||
return new Background(getBackgroundTextureName());
|
||||
}
|
||||
|
||||
string videoPath = videoFiles[RNG.Next(videoFiles.Length)];
|
||||
return new VideoBackgroundScreen(videoPath);
|
||||
|
||||
return skin.GetAnimation(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,61 +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 System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComComboText : ComboCounter
|
||||
{
|
||||
public EzComCounterText Text = null!;
|
||||
|
||||
protected override double RollingDuration => 250;
|
||||
|
||||
protected virtual bool DisplayXSymbol => true;
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
|
||||
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
Current.BindTo(scoreProcessor.Combo);
|
||||
Current.BindValueChanged(combo =>
|
||||
{
|
||||
bool wasIncrease = combo.NewValue > combo.OldValue;
|
||||
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;
|
||||
|
||||
float newScale = Math.Clamp(Text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f);
|
||||
|
||||
float duration = wasMiss ? 1000 : 100;
|
||||
|
||||
Text.NumberContainer
|
||||
.ScaleTo(new Vector2(newScale))
|
||||
.ScaleTo(Vector2.One, duration, Easing.OutQuint);
|
||||
|
||||
if (wasMiss)
|
||||
Text.FlashColour(Color4.Red, duration, Easing.OutQuint);
|
||||
});
|
||||
}
|
||||
|
||||
protected override LocalisableString FormatCount(int count) => DisplayXSymbol ? @"Combo" : count.ToString();
|
||||
|
||||
protected override IHasText CreateText() => Text = new EzComCounterText(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper())
|
||||
{
|
||||
ShowLabel = { BindTarget = ShowLabel },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Text = label.GetValueOrDefault(),
|
||||
Font = OsuFont.Stat.With(size: 12, weight: FontWeight.Bold),
|
||||
// Margin = new MarginPadding { Bottom = 1 },
|
||||
},
|
||||
textPart = new Ez2CounterSpriteText(textLookup)
|
||||
@@ -138,7 +137,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
|
||||
private class GlyphStore : ITexturedGlyphLookupStore
|
||||
{
|
||||
private readonly string fontName;
|
||||
// private readonly string fontName;
|
||||
private readonly TextureStore textures;
|
||||
private readonly Func<char, string> getLookup;
|
||||
|
||||
@@ -146,17 +145,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
|
||||
public GlyphStore(string fontName, TextureStore textures, Func<char, string> getLookup)
|
||||
{
|
||||
this.fontName = fontName;
|
||||
// this.fontName = fontName;
|
||||
this.textures = textures;
|
||||
this.getLookup = getLookup;
|
||||
}
|
||||
|
||||
public ITexturedCharacterGlyph? Get(string? fontName, char character)
|
||||
{
|
||||
// We only service one font.
|
||||
if (fontName != this.fontName)
|
||||
return null;
|
||||
|
||||
if (cache.TryGetValue(character, out var cached))
|
||||
return cached;
|
||||
|
||||
|
||||
@@ -21,22 +21,22 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
private readonly Bindable<HitResult> hitResult = new Bindable<HitResult>();
|
||||
|
||||
[Resolved]
|
||||
protected HitWindows HitWindows { get; private set; }
|
||||
protected HitWindows HitWindows { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor processor { get; set; }
|
||||
private ScoreProcessor processor { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayClockContainer gameplayClockContainer { get; set; }
|
||||
private GameplayClockContainer gameplayClockContainer { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private DrawableRuleset drawableRuleset { get; set; }
|
||||
private DrawableRuleset drawableRuleset { get; set; } = null!;
|
||||
|
||||
private OsuSpriteText offsetText;
|
||||
private Box backgroundBox;
|
||||
private OsuSpriteText offsetText = null!;
|
||||
private Box backgroundBox = null!;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComJudgementArea : CompositeDrawable, ISerialisableDrawable, IHitExplosion
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
private Container largeFaint = null!;
|
||||
private Container background = null!;
|
||||
private double bpm;
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Bindable<Color4> accentColour = null!;
|
||||
|
||||
[Resolved]
|
||||
private Column column { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IGameplayClock gameplayClock { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private StageDefinition stageDefinition { get; set; } = null!;
|
||||
|
||||
public EzComJudgementArea()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
if (stageDefinition.Columns == 14 && column.Index == 13)
|
||||
Alpha = 0;
|
||||
|
||||
Size = new Vector2(2);
|
||||
Alpha = 1;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
background = new Container
|
||||
{
|
||||
Height = NOTE_HEIGHT,
|
||||
Blending = BlendingParameters.Mixture,
|
||||
Colour = Color4.Gray,
|
||||
Alpha = 0.3f,
|
||||
},
|
||||
largeFaint = new Container
|
||||
{
|
||||
Height = NOTE_HEIGHT,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0.8f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Masking = true,
|
||||
Scale = new Vector2(0.5f),
|
||||
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = new Color4(1f, 1f, 1f, 0.5f),
|
||||
Radius = 2f, // 调整光晕半径
|
||||
Roundness = 0f,
|
||||
},
|
||||
},
|
||||
};
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
|
||||
accentColour = column.AccentColour.GetBoundCopy();
|
||||
accentColour.BindValueChanged(colour =>
|
||||
{
|
||||
largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1);
|
||||
|
||||
largeFaint.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = colour.NewValue,
|
||||
Roundness = Ez2NotePiece.NOTE_HEIGHT,
|
||||
Radius = 50,
|
||||
};
|
||||
}, true);
|
||||
bpm = beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.GetTrueGameplayRate();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
Height = DrawWidth;
|
||||
|
||||
if (stageDefinition.Columns == 14 && column.Index == 13)
|
||||
Alpha = 0;
|
||||
|
||||
double interval = 60000 / bpm;
|
||||
const double amplitude = 6.0;
|
||||
double progress = (gameplayClock.CurrentTime % interval) / interval;
|
||||
|
||||
double smoothValue = smoothSineWave(progress);
|
||||
Y = (float)(smoothValue * amplitude);
|
||||
}
|
||||
|
||||
private double smoothSineWave(double t)
|
||||
{
|
||||
const double frequency = 1;
|
||||
const double amplitude = 0.3;
|
||||
return amplitude * Math.Sin(frequency * t * 2 * Math.PI);
|
||||
}
|
||||
|
||||
private const float NOTE_HEIGHT = 40;
|
||||
|
||||
public void Animate(JudgementResult result)
|
||||
{
|
||||
this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
Anchor = Origin = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
|
||||
{
|
||||
if (e.Action != column.Action.Value) return false;
|
||||
|
||||
const double lighting_fade_in_duration = 70;
|
||||
Color4 lightingColour = getLightingColour();
|
||||
|
||||
background.ScaleTo(0.9f, lighting_fade_in_duration, Easing.OutQuint);
|
||||
background.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour.Opacity(0.1f),
|
||||
Radius = 20,
|
||||
}, lighting_fade_in_duration, Easing.OutQuint);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<ManiaAction> e)
|
||||
{
|
||||
if (e.Action != column.Action.Value) return;
|
||||
|
||||
const double lighting_fade_out_duration = 800;
|
||||
|
||||
Color4 lightingColour = getLightingColour().Opacity(0);
|
||||
|
||||
background.ScaleTo(1f, 200, Easing.OutQuint);
|
||||
background.TransformTo(nameof(EdgeEffect), new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = lightingColour,
|
||||
Radius = 20,
|
||||
}, lighting_fade_out_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Color4 getLightingColour() => Interpolation.ValueAt(0.2f, accentColour.Value, Color4.White, 0, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class EzComJudgementTexture : CompositeDrawable, ISerialisableDrawable //, IAnimatableJudgement
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[SettingSource("Playback FPS", "The FPS value of this Animation")]
|
||||
public BindableNumber<float> PlaybackFps { get; } = new BindableNumber<float>(60)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 240,
|
||||
Precision = 1f,
|
||||
};
|
||||
|
||||
private Vector2 dragStartPosition;
|
||||
private bool isDragging;
|
||||
|
||||
[Resolved]
|
||||
private TextureStore textures { get; set; } = null!;
|
||||
|
||||
protected HitWindows HitWindows { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreProcessor processor { get; set; } = null!;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private GameplayClockContainer gameplayClockContainer { get; set; } = null!;
|
||||
|
||||
public EzComJudgementTexture()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
// private (HitResult result, double length)[] hitWindows = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableRuleset drawableRuleset)
|
||||
{
|
||||
HitWindows = drawableRuleset.FirstAvailableHitWindows ?? HitWindows.Empty;
|
||||
// hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
||||
// This is to allow the visual state to be correct after HUD comes visible after being hidden.
|
||||
AlwaysPresent = true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
gameplayClockContainer.OnSeek += Clear;
|
||||
|
||||
processor.NewJudgement += processorNewJudgement;
|
||||
}
|
||||
|
||||
private void processorNewJudgement(JudgementResult j) => Schedule(() => OnNewJudgement(j));
|
||||
|
||||
protected void OnNewJudgement(JudgementResult judgement)
|
||||
{
|
||||
if (!judgement.IsHit || judgement.HitObject.HitWindows?.WindowFor(HitResult.Miss) == 0)
|
||||
return;
|
||||
|
||||
if (!judgement.Type.IsScorable() || judgement.Type.IsBonus())
|
||||
return;
|
||||
|
||||
ClearInternal(true);
|
||||
|
||||
var judgementText = CreateJudgementTexture(judgement.Type);
|
||||
AddInternal(judgementText);
|
||||
}
|
||||
|
||||
protected virtual void Clear()
|
||||
{
|
||||
FinishTransforms(true);
|
||||
|
||||
ClearInternal();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
PlaybackFps.UnbindAll();
|
||||
processor.NewJudgement -= processorNewJudgement;
|
||||
gameplayClockContainer.OnSeek -= Clear;
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
protected TextureAnimation CreateJudgementTexture(HitResult result)
|
||||
{
|
||||
var judgementTexture = new TextureAnimation
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
// DefaultFrameLength = 1000 / PlaybackFps.Value,
|
||||
Loop = false
|
||||
};
|
||||
|
||||
string gifPath = getGifPath(result);
|
||||
|
||||
for (int i = 0;; i++)
|
||||
{
|
||||
var texture = textures.Get($@"{gifPath}/frame_{i}");
|
||||
if (texture == null)
|
||||
break;
|
||||
|
||||
judgementTexture.AddFrame(texture);
|
||||
}
|
||||
|
||||
PlaybackFps.BindValueChanged(fps =>
|
||||
{
|
||||
judgementTexture.DefaultFrameLength = 1000 / fps.NewValue;
|
||||
}, true);
|
||||
|
||||
PlayAnimation(result, judgementTexture);
|
||||
|
||||
judgementTexture.OnUpdate += _ =>
|
||||
{
|
||||
if (judgementTexture.CurrentFrameIndex == judgementTexture.FrameCount - 1)
|
||||
judgementTexture.Expire();
|
||||
};
|
||||
return judgementTexture;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
dragStartPosition = e.ScreenSpaceMousePosition;
|
||||
isDragging = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
if (isDragging)
|
||||
{
|
||||
var delta = e.ScreenSpaceMousePosition - dragStartPosition;
|
||||
Position += delta;
|
||||
dragStartPosition = e.ScreenSpaceMousePosition;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
private string getGifPath(HitResult hitResult)
|
||||
{
|
||||
return hitResult switch
|
||||
{
|
||||
HitResult.Miss => @"Gameplay/Ez2/Miss",
|
||||
HitResult.Meh => @"Gameplay/Ez2/Fail1",
|
||||
HitResult.Ok => @"Gameplay/Ez2/Fail",
|
||||
HitResult.Good => @"Gameplay/Ez2/Good",
|
||||
HitResult.Great => @"Gameplay/Ez2/Cool",
|
||||
HitResult.Perfect => @"Gameplay/Ez2/Kool",
|
||||
_ => @"Gameplay/Ez2",
|
||||
};
|
||||
}
|
||||
|
||||
public virtual void PlayAnimation(HitResult hitResult, Drawable drawable)
|
||||
{
|
||||
const float flash_speed = 60f;
|
||||
|
||||
switch (hitResult)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
applyFadeEffect(drawable, new[] { Color4.Red, Color4.IndianRed }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
applyFadeEffect(drawable, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Ok:
|
||||
applyFadeEffect(drawable, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Good:
|
||||
applyFadeEffect(drawable, new[] { Color4.Green, Color4.LightGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
applyFadeEffect(drawable, new[] { Color4.AliceBlue, Color4.LightSkyBlue }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Perfect:
|
||||
applyFadeEffect(drawable, new[] { Color4.LightBlue, Color4.LightGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFadeEffect(Drawable drawable, Color4[] colors, double flashSpeed)
|
||||
{
|
||||
if (!drawable.IsLoaded)
|
||||
return;
|
||||
|
||||
drawable.FadeColour(colors[0], 0);
|
||||
var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint);
|
||||
|
||||
for (int i = 1; i < colors.Length; i++)
|
||||
{
|
||||
sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint);
|
||||
}
|
||||
|
||||
// sequence.Loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,12 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD
|
||||
{
|
||||
public partial class Ez2ScoreCounter : GameplayScoreCounter, ISerialisableDrawable
|
||||
public partial class EzComScoreCounter : GameplayScoreCounter, ISerialisableDrawable
|
||||
{
|
||||
protected override double RollingDuration => 250;
|
||||
|
||||
[SettingSource("Wireframe opacity", "Controls the opacity of the wireframes behind the digits.")]
|
||||
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0)
|
||||
{
|
||||
Precision = 0.01f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
};
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.ShowLabel), nameof(SkinnableComponentStrings.ShowLabelDescription))]
|
||||
public Bindable<bool> ShowLabel { get; } = new BindableBool(true);
|
||||
public Bindable<bool> ShowLabel { get; } = new BindableBool(false);
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
@@ -19,9 +19,9 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
{
|
||||
public partial class Ez2JudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||
public partial class Ez2JudgementPiece : LAsJudgementText, IAnimatableJudgement
|
||||
{
|
||||
private const float judgement_y_position = 140;
|
||||
internal const float JUDGEMENT_Y_POSITION = 140;
|
||||
|
||||
private RingExplosion? ringExplosion;
|
||||
|
||||
@@ -33,7 +33,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
Y = judgement_y_position;
|
||||
Y = JUDGEMENT_Y_POSITION;
|
||||
}
|
||||
|
||||
public Ez2JudgementPiece()
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -78,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
JudgementText
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint);
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToY(JUDGEMENT_Y_POSITION);
|
||||
|
||||
applyFadeEffect(this, new[] { Color4.Red, Color4.IndianRed }, flash_speed);
|
||||
applyScaleAndFadeOutEffect(this, new Vector2(1.5f), 300, new Vector2(1.5f, 0.1f), 300, 300);
|
||||
@@ -88,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
JudgementText
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint);
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToY(JUDGEMENT_Y_POSITION);
|
||||
|
||||
applyFadeEffect(this, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed);
|
||||
applyScaleAndFadeOutEffect(this, new Vector2(1.5f), 300, new Vector2(1.5f, 0.1f), 300, 300);
|
||||
@@ -98,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
JudgementText
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint);
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToY(JUDGEMENT_Y_POSITION);
|
||||
|
||||
applyFadeEffect(this, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed);
|
||||
applyScaleAndFadeOutEffect(this, new Vector2(1.3f), 200, new Vector2(1.3f, 0.1f), 400, 400);
|
||||
break;
|
||||
|
||||
case HitResult.Good:
|
||||
this.MoveToY(judgement_y_position);
|
||||
this.MoveToY(JUDGEMENT_Y_POSITION);
|
||||
JudgementText
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.3f), 1800, Easing.OutQuint);
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
{
|
||||
public partial class GifJudgementPiece : TextJudgementPiece, IAnimatableJudgement, ISerialisableDrawable
|
||||
{
|
||||
private TextureAnimation? gifAnimation;
|
||||
private Vector2 dragStartPosition;
|
||||
private bool isDragging;
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
public GifJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
if (Result.IsHit())
|
||||
{
|
||||
gifAnimation = new TextureAnimation
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
DefaultFrameLength = 100,
|
||||
// AutoSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
string gifPath = getGifPath(Result);
|
||||
|
||||
for (int i = 0; i < 26; i++) // 假设每个GIF有10帧
|
||||
{
|
||||
gifAnimation.AddFrame(textures.Get($"{gifPath}/frame_{i}"));
|
||||
}
|
||||
|
||||
AddInternal(gifAnimation);
|
||||
PlayAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
dragStartPosition = e.ScreenSpaceMousePosition;
|
||||
isDragging = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
if (isDragging)
|
||||
{
|
||||
var delta = e.ScreenSpaceMousePosition - dragStartPosition;
|
||||
Position += delta;
|
||||
dragStartPosition = e.ScreenSpaceMousePosition;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
private string getGifPath(HitResult result)
|
||||
{
|
||||
return result switch
|
||||
{
|
||||
HitResult.Miss => "osu.Game.Resources/Skins/Ez2/Miss",
|
||||
HitResult.Meh => "osu.Game.Resources/Skins/Ez2/Fail1",
|
||||
HitResult.Ok => "osu.Game.Resources/Skins/Ez2/Fail",
|
||||
HitResult.Good => "osu.Game.Resources/Skins/Ez2/Good",
|
||||
HitResult.Great => "osu.Game.Resources/Skins/Ez2/Cool",
|
||||
HitResult.Perfect => "osu.Game.Resources/Skins/Ez2/Kool",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
protected override OsuSpriteText CreateJudgementText() => new OsuSpriteText();
|
||||
|
||||
public virtual void PlayAnimation()
|
||||
{
|
||||
const float flash_speed = 60f;
|
||||
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
applyFadeEffect(this, new[] { Color4.Red, Color4.IndianRed }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Meh:
|
||||
|
||||
applyFadeEffect(this, new[] { Color4.Purple, Color4.MediumPurple }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Ok:
|
||||
applyFadeEffect(this, new[] { Color4.ForestGreen, Color4.SeaGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Good:
|
||||
applyFadeEffect(this, new[] { Color4.Green, Color4.LightGreen }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
applyFadeEffect(this, new[] { Color4.AliceBlue, Color4.LightSkyBlue }, flash_speed);
|
||||
break;
|
||||
|
||||
case HitResult.Perfect:
|
||||
applyFadeEffect(this, new[] { Color4.LightBlue, Color4.LightGreen }, flash_speed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFadeEffect(Drawable drawable, Color4[] colors, double flashSpeed)
|
||||
{
|
||||
var sequence = drawable.FadeColour(colors[0], flashSpeed, Easing.OutQuint);
|
||||
|
||||
for (int i = 1; i < colors.Length; i++)
|
||||
{
|
||||
sequence = sequence.Then().FadeColour(colors[i], flashSpeed, Easing.OutQuint);
|
||||
}
|
||||
|
||||
sequence.Loop();
|
||||
}
|
||||
|
||||
public Drawable? GetAboveHitObjectsProxiedContent()
|
||||
{
|
||||
return null; // 根据需要返回适当的 Drawable
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Schedule(() => Schedule(updateAnchor));
|
||||
}
|
||||
|
||||
private void updateAnchor()
|
||||
{
|
||||
// 如果锚点不是垂直中心,则根据滚动方向设置顶部或底部锚点
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
hitTiming.Y = 500;
|
||||
}
|
||||
|
||||
var comboSprite = container.ChildrenOfType<EzComComboSprite>().FirstOrDefault();
|
||||
|
||||
if (comboSprite != null)
|
||||
{
|
||||
comboSprite.Anchor = Anchor.TopCentre;
|
||||
comboSprite.Origin = Anchor.Centre;
|
||||
comboSprite.Scale = new Vector2(0.8f);
|
||||
comboSprite.Y = 190;
|
||||
}
|
||||
|
||||
var combos = container.ChildrenOfType<EzComComboCounter>().ToArray();
|
||||
|
||||
if (combos.Length >= 2)
|
||||
@@ -78,7 +88,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
combo1.Origin = Anchor.Centre;
|
||||
combo1.Colour = Colour4.White;
|
||||
combo1.Y = 200;
|
||||
combo1.ShowLabel.Value = true;
|
||||
combo1.ShowLabel.Value = false;
|
||||
combo1.BoxAlpha.Value = 0.8f;
|
||||
combo1.IncreaseScale.Value = 1.5f;
|
||||
combo1.DecreaseScale.Value = 1f;
|
||||
@@ -88,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
combo2.Anchor = Anchor.TopCentre;
|
||||
combo2.Origin = Anchor.Centre;
|
||||
combo2.Colour = Colour4.White;
|
||||
combo2.Y = 208;
|
||||
combo2.Y = 200;
|
||||
combo2.ShowLabel.Value = false;
|
||||
combo2.BoxAlpha.Value = 0.2f;
|
||||
combo2.IncreaseScale.Value = 3f;
|
||||
@@ -121,15 +131,25 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
hitErrorMeter.CentreMarkerStyle.Value = BarHitErrorMeter.CentreMarkerStyles.Circle;
|
||||
hitErrorMeter.LabelStyle.Value = BarHitErrorMeter.LabelStyles.None;
|
||||
}
|
||||
|
||||
var judgementPiece = container.OfType<EzComJudgementTexture>().FirstOrDefault();
|
||||
|
||||
if (judgementPiece != null)
|
||||
{
|
||||
judgementPiece.Anchor = Anchor.Centre;
|
||||
judgementPiece.Origin = Anchor.Centre;
|
||||
judgementPiece.Y = 50;
|
||||
}
|
||||
})
|
||||
{
|
||||
// new EzComComboText(),
|
||||
new EzComComboSprite(),
|
||||
new EzComComboCounter(),
|
||||
new EzComComboCounter(),
|
||||
new Ez2KeyCounterDisplay(),
|
||||
// new ArgonKeyCounterDisplay(),
|
||||
new BarHitErrorMeter(),
|
||||
// new EzComHitTiming(),
|
||||
new EzComJudgementTexture(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,8 +159,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
|
||||
// if (Skin is Ez2Skin && resultComponent.Component > HitResult.Great)
|
||||
// return Drawable.Empty();
|
||||
|
||||
return new Ez2JudgementPiece(resultComponent.Component);
|
||||
// return new GifJudgementPiece(resultComponent.Component);
|
||||
// return new Ez2JudgementPiece(resultComponent.Component);
|
||||
return Drawable.Empty();
|
||||
// return new DefaultSkinComponentsContainer(container =>
|
||||
// {
|
||||
// });
|
||||
|
||||
@@ -6,7 +6,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Ez2;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Ez2.Ez2HUD;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
@@ -95,8 +94,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.SbI
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
// if (Skin is SbISkin && resultComponent.Component >= HitResult.Great)
|
||||
// return Drawable.Empty();
|
||||
return new GifJudgementPiece(resultComponent.Component);
|
||||
// return new SbIJudgementPiece(resultComponent.Component);
|
||||
// return new EzComJudgementTexture(resultComponent.Component);
|
||||
return new SbIJudgementPiece(resultComponent.Component);
|
||||
|
||||
case ManiaSkinComponentLookup maniaComponent:
|
||||
switch (maniaComponent.Component)
|
||||
|
||||
@@ -86,9 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSpinningSamplePitchShift()
|
||||
{
|
||||
PausableSkinnableSound spinSample = null;
|
||||
|
||||
AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000)));
|
||||
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
|
||||
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
|
||||
AddUntilStep("wait for spin sample", () => (spinSample = getSpinningSample()) != null);
|
||||
AddUntilStep("Pitch starts low", () => spinSample.Frequency.Value < 0.8);
|
||||
AddUntilStep("Pitch increases", () => spinSample.Frequency.Value > 0.8);
|
||||
|
||||
PausableSkinnableSound getSpinningSample() =>
|
||||
drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
|
||||
|
||||
@@ -85,9 +85,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
|
||||
Scale = new Vector2(Interpolation.ValueAt(Time.Current, 1, 1.5f, drawableRepeat.HitStateUpdateTime, drawableRepeat.HitStateUpdateTime + animDuration, Easing.Out));
|
||||
|
||||
// When hit, don't animate further. This avoids a scale being applied on a scale and looking very weird.
|
||||
return;
|
||||
}
|
||||
else
|
||||
Scale = Vector2.One;
|
||||
|
||||
Scale = Vector2.One;
|
||||
|
||||
const float move_distance = -12;
|
||||
const float scale_amount = 1.3f;
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.17.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -135,6 +135,24 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoopFadeTransformIsIgnoredForLifetime()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("noop-fade-transform-is-ignored-for-lifetime.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
|
||||
Assert.AreEqual(2, background.Elements.Count);
|
||||
|
||||
Assert.AreEqual(1500, background.Elements[0].StartTime);
|
||||
Assert.AreEqual(1500, background.Elements[1].StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOutOfOrderStartTimes()
|
||||
{
|
||||
@@ -288,6 +306,29 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoWithCustomFadeIn()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using var resStream = TestResources.OpenResource("video-custom-alpha-transform.osb");
|
||||
using var stream = new LineBufferedReader(resStream);
|
||||
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(storyboard.GetLayer(@"Video").Elements, Has.Count.EqualTo(1));
|
||||
Assert.That(storyboard.GetLayer(@"Video").Elements.Single(), Is.InstanceOf<StoryboardVideo>());
|
||||
Assert.That(storyboard.GetLayer(@"Video").Elements.Single().StartTime, Is.EqualTo(-5678));
|
||||
Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().StartTime, Is.EqualTo(1500));
|
||||
Assert.That(((StoryboardVideo)storyboard.GetLayer(@"Video").Elements.Single()).Commands.Alpha.Single().EndTime, Is.EqualTo(1600));
|
||||
|
||||
Assert.That(storyboard.EarliestEventTime, Is.Null);
|
||||
Assert.That(storyboard.LatestEventTime, Is.Null);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVideoAndBackgroundEventsDoNotAffectStoryboardBounds()
|
||||
{
|
||||
|
||||
73
osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs
Normal file
73
osu.Game.Tests/OnlinePlay/MultiplayerPlaylistItemTest.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using Bogus;
|
||||
using MessagePack;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[TestFixture]
|
||||
public class MultiplayerPlaylistItemTest
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Randomizer.Seed = new Random(1337);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCloneMultiplayerPlaylistItem()
|
||||
{
|
||||
var faker = new Faker<MultiplayerPlaylistItem>()
|
||||
.StrictMode(true)
|
||||
.RuleFor(o => o.ID, f => f.Random.Long())
|
||||
.RuleFor(o => o.OwnerID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
|
||||
.RuleFor(o => o.RulesetID, f => f.Random.Int())
|
||||
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.Expired, f => f.Random.Bool())
|
||||
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
|
||||
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
|
||||
.RuleFor(o => o.StarRating, f => f.Random.Double())
|
||||
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
MultiplayerPlaylistItem item = faker.Generate();
|
||||
Assert.That(MessagePackSerializer.SerializeToJson(item.Clone()), Is.EqualTo(MessagePackSerializer.SerializeToJson(item)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConstructFromAPIModel()
|
||||
{
|
||||
var faker = new Faker<MultiplayerPlaylistItem>()
|
||||
.StrictMode(true)
|
||||
.RuleFor(o => o.ID, f => f.Random.Long())
|
||||
.RuleFor(o => o.OwnerID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapID, f => f.Random.Int())
|
||||
.RuleFor(o => o.BeatmapChecksum, f => f.Random.Hash())
|
||||
.RuleFor(o => o.RulesetID, f => f.Random.Int())
|
||||
.RuleFor(o => o.RequiredMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.AllowedMods, f => f.Make(5, _ => new APIMod { Acronym = f.Random.String2(3) }))
|
||||
.RuleFor(o => o.Expired, f => f.Random.Bool())
|
||||
.RuleFor(o => o.PlaylistOrder, f => f.Random.UShort())
|
||||
.RuleFor(o => o.PlayedAt, f => f.Date.RecentOffset())
|
||||
.RuleFor(o => o.StarRating, f => f.Random.Double())
|
||||
.RuleFor(o => o.Freestyle, f => f.Random.Bool());
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
MultiplayerPlaylistItem initialItem = faker.Generate();
|
||||
MultiplayerPlaylistItem copiedItem = new MultiplayerPlaylistItem(new PlaylistItem(initialItem));
|
||||
Assert.That(MessagePackSerializer.SerializeToJson(copiedItem), Is.EqualTo(MessagePackSerializer.SerializeToJson(initialItem)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
[Events]
|
||||
//Storyboard Layer 0 (Background)
|
||||
Sprite,Background,TopCentre,"img.jpg",320,240
|
||||
F,0,1000,1000,0,0 // should be ignored
|
||||
F,0,1500,1600,0,1
|
||||
Sprite,Background,TopCentre,"img.jpg",320,240
|
||||
F,0,1000,1000,0,0 // should be ignored
|
||||
F,0,1500,1600,1,1
|
||||
@@ -0,0 +1,5 @@
|
||||
osu file format v14
|
||||
|
||||
[Events]
|
||||
Video,-5678,"Video.avi",0,0
|
||||
F,0,1500,1600,0,1
|
||||
@@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
@@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
|
||||
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
[Test]
|
||||
public void TestDailyChallenge()
|
||||
{
|
||||
startChallenge(1234);
|
||||
startChallenge();
|
||||
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayIntroOnceFlag()
|
||||
{
|
||||
startChallenge(1234);
|
||||
startChallenge();
|
||||
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
|
||||
|
||||
startChallenge(1235);
|
||||
startChallenge();
|
||||
|
||||
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
|
||||
|
||||
@@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
|
||||
}
|
||||
|
||||
private void startChallenge(int roomId)
|
||||
private void startChallenge()
|
||||
{
|
||||
AddStep("add room", () =>
|
||||
{
|
||||
API.Perform(new CreateRoomRequest(room = new Room
|
||||
{
|
||||
RoomID = roomId,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
@@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
Category = RoomCategory.DailyChallenge
|
||||
}));
|
||||
});
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
PoolableSkinnableSample[] loopingSamples = null;
|
||||
PoolableSkinnableSample[] onceOffSamples = null;
|
||||
|
||||
AddStep("get first slider", () =>
|
||||
{
|
||||
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
||||
onceOffSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => !s.Looping).ToArray();
|
||||
loopingSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => s.Looping).ToArray();
|
||||
});
|
||||
AddStep("get first slider", () => slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First());
|
||||
|
||||
AddStep("start playback", () => EditorClock.Start());
|
||||
|
||||
@@ -38,6 +33,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
if (!slider.Tracking.Value)
|
||||
return false;
|
||||
|
||||
onceOffSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => !s.Looping).ToArray();
|
||||
loopingSamples = slider.ChildrenOfType<PoolableSkinnableSample>().Where(s => s.Looping).ToArray();
|
||||
|
||||
if (!loopingSamples.Any(s => s.Playing))
|
||||
return false;
|
||||
|
||||
|
||||
@@ -50,21 +50,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
// 50 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
List<HitEvent> hitEvents =
|
||||
[
|
||||
// 10 events total. one of them (head circle) being timed / having hitwindows, rest having no hitwindows
|
||||
new HitEvent(30, 1, HitResult.LargeTickHit, new SliderHeadCircle { ClassicSliderBehaviour = true }, null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null),
|
||||
];
|
||||
|
||||
for (int i = 0; i < 49; i++)
|
||||
{
|
||||
hitEvents.Add(new HitEvent(0, 1, HitResult.LargeTickHit, new SliderTick(), null, null));
|
||||
}
|
||||
|
||||
foreach (var ev in hitEvents)
|
||||
ev.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
@@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnchorRadioButtonBehavior()
|
||||
{
|
||||
ISerialisableDrawable? selectedComponent = null;
|
||||
|
||||
AddStep("Select first component", () =>
|
||||
{
|
||||
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First();
|
||||
skinEditor.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Add(blueprint.Item);
|
||||
selectedComponent = blueprint.Item;
|
||||
});
|
||||
|
||||
AddStep("Right-click to open context menu", () =>
|
||||
{
|
||||
if (selectedComponent != null)
|
||||
InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddStep("Click on Anchor menu", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Anchor"));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("Right-click TopLeft anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
|
||||
AddStep("Right-click Centre anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
|
||||
|
||||
AddStep("Right-click Closest anchor", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
|
||||
AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
|
||||
|
||||
Menu.DrawableMenuItem getMenuItemByText(string text)
|
||||
=> this.ChildrenOfType<Menu.DrawableMenuItem>().First(m => m.Item.Text.ToString() == text);
|
||||
}
|
||||
|
||||
private Skin importSkinFromArchives(string filename)
|
||||
{
|
||||
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -336,6 +337,61 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserModSelectUpdatesWhenNotVisible()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = [new APIMod(new OsuModFlashlight())]
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
// 1. Open the mod select overlay and enable flashlight
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddUntilStep("mod select contents loaded", () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
|
||||
AddStep("click flashlight panel", () =>
|
||||
{
|
||||
ModPanel panel = this.ChildrenOfType<ModPanel>().Single(p => p.Mod is OsuModFlashlight);
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("flashlight mod enabled", () => MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
|
||||
// 2. Close the mod select overlay, edit the playlist to disable allowed mods, and then edit it again to re-enable allowed mods.
|
||||
|
||||
AddStep("close mod select overlay", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().Hide());
|
||||
AddUntilStep("mod select overlay not present", () => !this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().IsPresent);
|
||||
AddStep("disable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0])
|
||||
{
|
||||
AllowedMods = []
|
||||
})));
|
||||
// This would normally be done as part of the above operation with an actual server.
|
||||
AddStep("disable user mods", () => MultiplayerClient.ChangeUserMods(API.LocalUser.Value.OnlineID, Array.Empty<APIMod>()));
|
||||
AddUntilStep("flashlight mod disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
AddStep("re-enable allowed mods", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(MultiplayerClient.ServerRoom!.Playlist[0])
|
||||
{
|
||||
AllowedMods = [new APIMod(new OsuModFlashlight())]
|
||||
})));
|
||||
AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
|
||||
// 3. Open the mod select overlay, check that the flashlight mod panel is deactivated.
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddUntilStep("mod select contents loaded", () => this.ChildrenOfType<ModColumn>().Any() && this.ChildrenOfType<ModColumn>().All(col => col.IsLoaded && col.ItemsLoaded));
|
||||
AddAssert("flashlight mod still disabled", () => !MultiplayerClient.ClientRoom!.Users[0].Mods.Any());
|
||||
AddAssert("flashlight mod panel not activated", () => !this.ChildrenOfType<ModPanel>().Single(p => p.Mod is OsuModFlashlight).Active.Value);
|
||||
}
|
||||
|
||||
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
@@ -32,7 +31,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -47,19 +45,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create list", () =>
|
||||
{
|
||||
Child = list = new MultiplayerPlaylist(room)
|
||||
Child = list = new MultiplayerPlaylist
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.4f, 0.8f),
|
||||
SelectedItem = new Bindable<PlaylistItem?>()
|
||||
Size = new Vector2(0.4f, 0.8f)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -158,37 +154,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertQueueTabCount(0);
|
||||
}
|
||||
|
||||
[Ignore("Expired items are initially removed from the room.")]
|
||||
[Test]
|
||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
||||
{
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
|
||||
AddStep("join room with items", () =>
|
||||
AddStep("join room with expired items", () =>
|
||||
{
|
||||
API.Queue(new CreateRoomRequest(new Room
|
||||
{
|
||||
Name = "test name",
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID
|
||||
},
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID,
|
||||
Expired = true
|
||||
}
|
||||
]
|
||||
}));
|
||||
Room room = CreateDefaultRoom();
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID
|
||||
},
|
||||
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
{
|
||||
RulesetID = Ruleset.Value.OnlineID,
|
||||
Expired = true
|
||||
}
|
||||
];
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
WaitForJoined();
|
||||
|
||||
assertItemInQueueListStep(1, 0);
|
||||
assertItemInHistoryListStep(2, 0);
|
||||
// IDs are offset by 1 because we've joined two rooms in this test.
|
||||
assertItemInQueueListStep(2, 0);
|
||||
assertItemInHistoryListStep(3, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -49,13 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create playlist", () =>
|
||||
{
|
||||
Child = playlist = new MultiplayerQueueList(room)
|
||||
Child = playlist = new MultiplayerQueueList
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(500, 300),
|
||||
Size = new Vector2(500, 300)
|
||||
};
|
||||
|
||||
playlist.Items.ReplaceRange(0, playlist.Items.Count, MultiplayerClient.ClientAPIRoom!.Playlist);
|
||||
|
||||
MultiplayerClient.ClientAPIRoom!.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Room.Playlist))
|
||||
@@ -132,6 +134,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
assertDeleteButtonVisibility(1, false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeExistingItem()
|
||||
{
|
||||
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = playlist.Items[0].ID,
|
||||
BeatmapID = 1337
|
||||
}).WaitSafely());
|
||||
|
||||
AddUntilStep("first playlist item has new beatmap", () => playlist.Items[0].Beatmap.OnlineID, () => Is.EqualTo(1337));
|
||||
}
|
||||
|
||||
private void addPlaylistItem(Func<int> userId)
|
||||
{
|
||||
long itemId = -1;
|
||||
|
||||
@@ -9,6 +9,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
@@ -20,10 +22,6 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set up working beatmap", () =>
|
||||
{
|
||||
Beatmap.Value.BeatmapInfo.OnlineID = 42;
|
||||
});
|
||||
AddStep("set up network requests", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
@@ -40,6 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
|
||||
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
|
||||
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
|
||||
new APITag { Id = 5, Name = "mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, },
|
||||
]
|
||||
}), 500);
|
||||
return true;
|
||||
@@ -67,19 +66,34 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("create control", () =>
|
||||
AddStep("show for osu! beatmap", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
Width = 500,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||
working.BeatmapInfo.OnlineID = 42;
|
||||
Beatmap.Value = working;
|
||||
recreateControl();
|
||||
});
|
||||
AddStep("show for taiko beatmap", () =>
|
||||
{
|
||||
var working = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo);
|
||||
working.BeatmapInfo.OnlineID = 44;
|
||||
Beatmap.Value = working;
|
||||
recreateControl();
|
||||
});
|
||||
}
|
||||
|
||||
private void recreateControl()
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new UserTagControl(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
Width = 500,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Bogus" Version="35.6.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="DeepEqual" Version="4.2.1" />
|
||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace osu.Game.Tournament.Models
|
||||
{
|
||||
public int ID;
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> Acronyms
|
||||
{
|
||||
get
|
||||
|
||||
@@ -36,6 +36,11 @@ namespace osu.Game.Beatmaps.Formats
|
||||
/// </remarks>
|
||||
public const double CONTROL_POINT_LENIENCY = 5;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum allowed number of keys in mania beatmaps.
|
||||
/// </summary>
|
||||
public const int MAX_MANIA_KEY_COUNT = 18;
|
||||
|
||||
internal static RulesetStore? RulesetStore;
|
||||
|
||||
private Beatmap beatmap = null!;
|
||||
@@ -116,7 +121,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
// mania uses "circle size" for key count, thus different allowable range
|
||||
difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3
|
||||
? Math.Clamp(difficulty.CircleSize, 0, 10)
|
||||
: Math.Clamp(difficulty.CircleSize, 1, 18);
|
||||
: Math.Clamp(difficulty.CircleSize, 1, MAX_MANIA_KEY_COUNT);
|
||||
|
||||
difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10);
|
||||
difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10);
|
||||
|
||||
@@ -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.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
@@ -16,5 +17,8 @@ namespace osu.Game.Configuration
|
||||
|
||||
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
|
||||
BeatmapWithStoryboard,
|
||||
|
||||
[Description("Use 'Textures/Webm/*.webm' file from local folders")]
|
||||
WebmSource,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@ namespace osu.Game.Configuration
|
||||
if (newScore.Mods.Any(m => !m.UserPlayable || m is IHasNoTimedInputs))
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.Count < 10)
|
||||
if (newScore.HitEvents.Count < 50)
|
||||
return;
|
||||
|
||||
if (newScore.HitEvents.CalculateAverageHitError() is not double averageError)
|
||||
if (newScore.HitEvents.CalculateMedianHitError() is not double medianError)
|
||||
return;
|
||||
|
||||
// keep a sane maximum number of entries.
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Configuration
|
||||
averageHitErrorHistory.RemoveAt(0);
|
||||
|
||||
double globalOffset = configManager.Get<double>(OsuSetting.AudioOffset);
|
||||
averageHitErrorHistory.Add(new DataPoint(averageError, globalOffset));
|
||||
averageHitErrorHistory.Add(new DataPoint(medianError, globalOffset));
|
||||
}
|
||||
|
||||
public void ClearHistory() => averageHitErrorHistory.Clear();
|
||||
|
||||
@@ -139,9 +139,14 @@ namespace osu.Game.Database
|
||||
notification.Progress = (float)current / tasks.Length;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException cancelled)
|
||||
{
|
||||
throw;
|
||||
// We don't want to abort the full import process based off difficulty calculator's internal cancellation
|
||||
// see https://github.com/ppy/osu/blob/91f3be5feaab0c73c17e1a8c270516aa9bee1e14/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs#L65.
|
||||
if (cancelled.CancellationToken == notification.CancellationToken)
|
||||
throw;
|
||||
|
||||
Logger.Error(cancelled, $@"Timed out importing ({task})", LoggingTarget.Database);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -7,11 +7,9 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
@@ -211,9 +209,9 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
if (targetMode == ScalingMode.Gameplay)
|
||||
{
|
||||
if ((scalingGameMode.Value == ScalingGameMode.Std && ruleset.Value.OnlineID == 0) ||
|
||||
if ((scalingGameMode.Value == ScalingGameMode.Standard && ruleset.Value.OnlineID == 0) ||
|
||||
(scalingGameMode.Value == ScalingGameMode.Taiko && ruleset.Value.OnlineID == 1) ||
|
||||
(scalingGameMode.Value == ScalingGameMode.Ctb && ruleset.Value.OnlineID == 2) ||
|
||||
(scalingGameMode.Value == ScalingGameMode.Catch && ruleset.Value.OnlineID == 2) ||
|
||||
(scalingGameMode.Value == ScalingGameMode.Mania && ruleset.Value.OnlineID == 3))
|
||||
{
|
||||
sizableContainer.RelativePositionAxes = Axes.Both;
|
||||
@@ -316,16 +314,12 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
public enum ScalingGameMode
|
||||
{
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), "Standard")]
|
||||
Std,
|
||||
Standard,
|
||||
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), "Taiko")]
|
||||
Taiko,
|
||||
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), "Mania")]
|
||||
Mania,
|
||||
|
||||
[LocalisableDescription(typeof(LayoutSettingsStrings), "CatchTheBeat")]
|
||||
Ctb,
|
||||
Catch,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,6 @@ namespace osu.Game.Graphics
|
||||
|
||||
public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular);
|
||||
|
||||
public static FontUsage Stat => GetFont(Typeface.Stat, weight: FontWeight.Regular);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="FontUsage"/>.
|
||||
/// </summary>
|
||||
@@ -72,9 +70,6 @@ namespace osu.Game.Graphics
|
||||
|
||||
case Typeface.Inter:
|
||||
return @"Inter";
|
||||
|
||||
case Typeface.Stat:
|
||||
return @"Stat";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -121,7 +116,6 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
Venera,
|
||||
Torus,
|
||||
Stat,
|
||||
|
||||
[Description("Torus (alternate)")]
|
||||
TorusAlternate,
|
||||
|
||||
@@ -109,21 +109,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ScoreDisplayMode => new TranslatableString(getKey(@"score_display_mode"), @"Score display mode");
|
||||
|
||||
/// <summary>
|
||||
/// "Hit Mode(No Active)"
|
||||
/// </summary>
|
||||
public static LocalisableString HitMode => new TranslatableString(getKey(@"hit_mode"), @"Hit Mode(No Active)");
|
||||
|
||||
/// <summary>
|
||||
/// "Accuracy Cutoff S"
|
||||
/// </summary>
|
||||
public static LocalisableString AccuracyCutoffS => new TranslatableString(getKey(@"accuracy_cutoff_s"), @"Accuracy Cutoff S");
|
||||
|
||||
/// <summary>
|
||||
/// "Accuracy Cutoff A"
|
||||
/// </summary>
|
||||
public static LocalisableString AccuracyCutoffA => new TranslatableString(getKey(@"accuracy_cutoff_a"), @"Accuracy Cutoff A");
|
||||
|
||||
/// <summary>
|
||||
/// "Disable Windows key during gameplay"
|
||||
/// </summary>
|
||||
|
||||
@@ -9,8 +9,6 @@ namespace osu.Game.Localisation.HUD
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.HUD.BarHitErrorMeter";
|
||||
|
||||
public static LocalisableString JudgementIconFadeOutDuration = new TranslatableString(getKey(@"Judgement_Icon_Fade_Out_Duration"), "Judgement Icon Fade Out Duration");
|
||||
|
||||
/// <summary>
|
||||
/// "Judgement line thickness"
|
||||
/// </summary>
|
||||
|
||||
@@ -34,11 +34,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ScaleEverythingExcludingOverlays => new TranslatableString(getKey(@"scale_everything_excluding_overlays"), @"Excluding overlays");
|
||||
|
||||
public static LocalisableString Standard => "Standard";
|
||||
public static LocalisableString Taiko => "Taiko";
|
||||
public static LocalisableString Mania => "Mania";
|
||||
public static LocalisableString CatchTheBeat => "CatchTheBeat";
|
||||
|
||||
/// <summary>
|
||||
/// "Everything"
|
||||
/// </summary>
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "{0}ms (speed {1:N1})"
|
||||
/// </summary>
|
||||
public static LocalisableString ScrollSpeedTooltip(double scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed);
|
||||
public static LocalisableString ScrollSpeedTooltip(int scrollTime, double scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1:N1})", scrollTime, scrollSpeed);
|
||||
|
||||
/// <summary>
|
||||
/// "Touch control scheme"
|
||||
|
||||
@@ -15,5 +15,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("ruleset_id")]
|
||||
public int? RulesetId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace osu.Game.Online.Chat
|
||||
[Resolved]
|
||||
private ChannelManager channelManager { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
@@ -47,19 +50,19 @@ namespace osu.Game.Online.Chat
|
||||
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, IAPIProvider api)
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
notifyOnUsername = config.GetBindable<bool>(OsuSetting.NotifyOnUsernameMentioned);
|
||||
notifyOnPrivateMessage = config.GetBindable<bool>(OsuSetting.NotifyOnPrivateMessage);
|
||||
|
||||
localUser.BindTo(api.LocalUser);
|
||||
joinedChannels.BindTo(channelManager.JoinedChannels);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
joinedChannels.BindCollectionChanged(channelsChanged, true);
|
||||
|
||||
localUser.BindTo(api.LocalUser);
|
||||
joinedChannels.BindTo(channelManager.JoinedChannels);
|
||||
}
|
||||
|
||||
private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
|
||||
@@ -85,6 +85,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// Retrieves the active <see cref="MultiplayerPlaylistItem"/> as determined by the room's current settings.
|
||||
/// </summary>
|
||||
[IgnoreMember]
|
||||
[JsonIgnore]
|
||||
public MultiplayerPlaylistItem CurrentPlaylistItem => Playlist.Single(item => item.ID == Settings.PlaylistItemId);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -62,11 +62,20 @@ namespace osu.Game.Online.Rooms
|
||||
[Key(11)]
|
||||
public bool Freestyle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiplayerPlaylistItem"/>.
|
||||
/// </summary>
|
||||
[SerializationConstructor]
|
||||
public MultiplayerPlaylistItem()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiplayerPlaylistItem"/> from an API <see cref="PlaylistItem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create unique instances of the <see cref="RequiredMods"/> and <see cref="AllowedMods"/> arrays but NOT unique instances of the contained <see cref="APIMod"/>s.
|
||||
/// </remarks>
|
||||
public MultiplayerPlaylistItem(PlaylistItem item)
|
||||
{
|
||||
ID = item.ID;
|
||||
@@ -82,5 +91,19 @@ namespace osu.Game.Online.Rooms
|
||||
StarRating = item.Beatmap.StarRating;
|
||||
Freestyle = item.Freestyle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of this <see cref="MultiplayerPlaylistItem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create unique instances of the <see cref="RequiredMods"/> and <see cref="AllowedMods"/> arrays but NOT unique instances of the contained <see cref="APIMod"/>s.
|
||||
/// </remarks>
|
||||
public MultiplayerPlaylistItem Clone()
|
||||
{
|
||||
MultiplayerPlaylistItem clone = (MultiplayerPlaylistItem)MemberwiseClone();
|
||||
clone.RequiredMods = RequiredMods.ToArray();
|
||||
clone.AllowedMods = AllowedMods.ToArray();
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +96,14 @@ namespace osu.Game.Online.Rooms
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PlaylistItem"/> from a <see cref="MultiplayerPlaylistItem"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create unique instances of the <see cref="RequiredMods"/> and <see cref="AllowedMods"/> arrays but NOT unique instances of the contained <see cref="APIMod"/>s.
|
||||
/// </remarks>
|
||||
public PlaylistItem(MultiplayerPlaylistItem item)
|
||||
: this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
|
||||
: this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating, Checksum = item.BeatmapChecksum })
|
||||
{
|
||||
ID = item.ID;
|
||||
OwnerID = item.OwnerID;
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Configuration.Tracking;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
@@ -56,8 +59,11 @@ namespace osu.Game.Overlays
|
||||
/// <param name="configManager">The <see cref="ConfigManager{T}"/> to be tracked.</param>
|
||||
/// <exception cref="ArgumentNullException">If <paramref name="configManager"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">If <paramref name="configManager"/> is already being tracked from the same <paramref name="source"/>.</exception>
|
||||
public void BeginTracking(object source, ITrackableConfigManager configManager)
|
||||
/// <returns>An object representing the registration, that may be disposed to stop tracking the <see cref="ConfigManager{T}"/>.</returns>
|
||||
public IDisposable BeginTracking(object source, ITrackableConfigManager configManager)
|
||||
{
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(configManager);
|
||||
|
||||
if (trackedConfigManagers.ContainsKey((source, configManager)))
|
||||
@@ -65,32 +71,18 @@ namespace osu.Game.Overlays
|
||||
|
||||
var trackedSettings = configManager.CreateTrackedSettings();
|
||||
if (trackedSettings == null)
|
||||
return;
|
||||
return new InvokeOnDisposal(() => { });
|
||||
|
||||
configManager.LoadInto(trackedSettings);
|
||||
trackedSettings.SettingChanged += displayTrackedSettingChange;
|
||||
|
||||
trackedConfigManagers.Add((source, configManager), trackedSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a <see cref="ConfigManager{T}"/> from having its settings tracked by this <see cref="OnScreenDisplay"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">The object that registered the <see cref="ConfigManager{T}"/> to be tracked.</param>
|
||||
/// <param name="configManager">The <see cref="ConfigManager{T}"/> that is being tracked.</param>
|
||||
/// <exception cref="ArgumentNullException">If <paramref name="configManager"/> is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">If <paramref name="configManager"/> is not being tracked from the same <paramref name="source"/>.</exception>
|
||||
public void StopTracking(object source, ITrackableConfigManager configManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configManager);
|
||||
|
||||
if (!trackedConfigManagers.TryGetValue((source, configManager), out var existing))
|
||||
return;
|
||||
|
||||
existing.Unload();
|
||||
existing.SettingChanged -= displayTrackedSettingChange;
|
||||
|
||||
trackedConfigManagers.Remove((source, configManager));
|
||||
return new InvokeOnDisposal(() =>
|
||||
{
|
||||
trackedSettings.Unload();
|
||||
trackedSettings.SettingChanged -= displayTrackedSettingChange;
|
||||
trackedConfigManagers.Remove((source, configManager));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -62,8 +62,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IAPIProvider api, INotificationOverlay? notifications)
|
||||
private void load(INotificationOverlay? notifications)
|
||||
{
|
||||
localUser.BindTo(api.LocalUser);
|
||||
|
||||
@@ -73,15 +76,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
updateColor();
|
||||
});
|
||||
|
||||
User.BindValueChanged(u =>
|
||||
{
|
||||
followerCount = u.NewValue?.User.FollowerCount ?? 0;
|
||||
updateStatus();
|
||||
}, true);
|
||||
|
||||
apiFriends.BindTo(api.Friends);
|
||||
apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus));
|
||||
|
||||
Action += () =>
|
||||
{
|
||||
if (User.Value == null)
|
||||
@@ -126,6 +120,20 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
apiFriends.BindTo(api.Friends);
|
||||
apiFriends.BindCollectionChanged((_, _) => Schedule(updateStatus));
|
||||
|
||||
User.BindValueChanged(u =>
|
||||
{
|
||||
followerCount = u.NewValue?.User.FollowerCount ?? 0;
|
||||
updateStatus();
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
if (status.Value > FriendStatus.None)
|
||||
|
||||
@@ -29,20 +29,20 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
||||
new SettingsEnumDropdown<HitWindows.HitMode>
|
||||
{
|
||||
ClassicDefault = HitWindows.HitMode.Ez2AcStyle,
|
||||
LabelText = GameplaySettingsStrings.HitMode,
|
||||
LabelText = "Hit Mode(No Active)",
|
||||
Current = config.GetBindable<HitWindows.HitMode>(OsuSetting.HitMode),
|
||||
Keywords = new[] { "scoring" }
|
||||
},
|
||||
new SettingsSlider<double>
|
||||
{
|
||||
LabelText = GameplaySettingsStrings.AccuracyCutoffS,
|
||||
LabelText = "Accuracy Cutoff S",
|
||||
Current = config.GetBindable<double>(OsuSetting.AccuracyCutoffS),
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
},
|
||||
new SettingsSlider<double>
|
||||
{
|
||||
LabelText = GameplaySettingsStrings.AccuracyCutoffA,
|
||||
LabelText = "Accuracy Cutoff A",
|
||||
Current = config.GetBindable<double>(OsuSetting.AccuracyCutoffA),
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
|
||||
@@ -760,11 +760,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
#region Delegation of IEditorChangeHandler
|
||||
|
||||
public event Action? OnStateChange
|
||||
{
|
||||
add => throw new NotImplementedException();
|
||||
remove => throw new NotImplementedException();
|
||||
}
|
||||
public event Action? OnStateChange;
|
||||
|
||||
private IEditorChangeHandler? beginChangeHandler;
|
||||
|
||||
@@ -773,6 +769,9 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
// Change handler may change between begin and end, which can cause unbalanced operations.
|
||||
// Let's track the one that was used when beginning the change so we can call EndChange on it specifically.
|
||||
(beginChangeHandler = changeHandler)?.BeginChange();
|
||||
|
||||
if (beginChangeHandler != null)
|
||||
beginChangeHandler.OnStateChange += OnStateChange;
|
||||
}
|
||||
|
||||
public void EndChange() => beginChangeHandler?.EndChange();
|
||||
|
||||
@@ -22,7 +22,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinSelectionHandler : SelectionHandler<ISerialisableDrawable>
|
||||
{
|
||||
private OsuMenuItem originMenu = null!;
|
||||
private OsuMenuItem? originMenu;
|
||||
|
||||
private TernaryStateRadioMenuItem? closestAnchor;
|
||||
private AnchorMenuItem[]? fixedAnchors;
|
||||
|
||||
[Resolved]
|
||||
private SkinEditor skinEditor { get; set; } = null!;
|
||||
@@ -44,6 +47,38 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
return scaleHandler;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (ChangeHandler != null)
|
||||
ChangeHandler.OnStateChange += updateTernaryStates;
|
||||
SelectedItems.BindCollectionChanged((_, _) => updateTernaryStates());
|
||||
}
|
||||
|
||||
private void updateTernaryStates()
|
||||
{
|
||||
var usingClosestAnchor = GetStateFromSelection(SelectedBlueprints, c => !c.Item.UsesFixedAnchor);
|
||||
|
||||
if (closestAnchor != null)
|
||||
closestAnchor.State.Value = usingClosestAnchor;
|
||||
|
||||
if (fixedAnchors != null)
|
||||
{
|
||||
foreach (var fixedAnchor in fixedAnchors)
|
||||
fixedAnchor.State.Value = GetStateFromSelection(SelectedBlueprints, c => c.Item.UsesFixedAnchor && ((Drawable)c.Item).Anchor == fixedAnchor.Anchor);
|
||||
}
|
||||
|
||||
if (originMenu != null)
|
||||
{
|
||||
foreach (var origin in originMenu.Items.OfType<AnchorMenuItem>())
|
||||
{
|
||||
origin.State.Value = GetStateFromSelection(SelectedBlueprints, c => ((Drawable)c.Item).Origin == origin.Anchor);
|
||||
origin.Action.Disabled = usingClosestAnchor == TernaryState.True;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
|
||||
{
|
||||
var selectionQuad = getSelectionQuad();
|
||||
@@ -102,27 +137,17 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
|
||||
{
|
||||
var closestItem = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors())
|
||||
{
|
||||
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
|
||||
};
|
||||
closestAnchor = new TernaryStateRadioMenuItem(SkinEditorStrings.Closest, MenuItemType.Standard, _ => applyClosestAnchors());
|
||||
fixedAnchors = createAnchorItems(applyFixedAnchors).ToArray();
|
||||
|
||||
yield return new OsuMenuItem(SkinEditorStrings.Anchor)
|
||||
{
|
||||
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
|
||||
.Prepend(closestItem)
|
||||
.ToArray()
|
||||
Items = fixedAnchors.Prepend(closestAnchor).ToArray()
|
||||
};
|
||||
|
||||
yield return originMenu = new OsuMenuItem(SkinEditorStrings.Origin);
|
||||
|
||||
closestItem.State.BindValueChanged(s =>
|
||||
{
|
||||
// For UX simplicity, origin should only be user-editable when "closest" anchor mode is disabled.
|
||||
originMenu.Items = s.NewValue == TernaryState.True
|
||||
? Array.Empty<MenuItem>()
|
||||
: createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray();
|
||||
}, true);
|
||||
originMenu.Items = createAnchorItems(applyOrigins).ToArray();
|
||||
|
||||
yield return new OsuMenuItemSpacer();
|
||||
|
||||
@@ -163,27 +188,37 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISerialisableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
|
||||
updateTernaryStates();
|
||||
}
|
||||
|
||||
private IEnumerable<AnchorMenuItem> createAnchorItems(Action<Anchor> applyFunction)
|
||||
{
|
||||
var displayableAnchors = new[]
|
||||
{
|
||||
var displayableAnchors = new[]
|
||||
{
|
||||
Anchor.TopLeft,
|
||||
Anchor.TopCentre,
|
||||
Anchor.TopRight,
|
||||
Anchor.CentreLeft,
|
||||
Anchor.Centre,
|
||||
Anchor.CentreRight,
|
||||
Anchor.BottomLeft,
|
||||
Anchor.BottomCentre,
|
||||
Anchor.BottomRight,
|
||||
};
|
||||
return displayableAnchors.Select(a =>
|
||||
{
|
||||
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
|
||||
{
|
||||
State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) }
|
||||
};
|
||||
});
|
||||
Anchor.TopLeft,
|
||||
Anchor.TopCentre,
|
||||
Anchor.TopRight,
|
||||
Anchor.CentreLeft,
|
||||
Anchor.Centre,
|
||||
Anchor.CentreRight,
|
||||
Anchor.BottomLeft,
|
||||
Anchor.BottomCentre,
|
||||
Anchor.BottomRight,
|
||||
};
|
||||
return displayableAnchors.Select(a =>
|
||||
{
|
||||
return new AnchorMenuItem(a, _ => applyFunction(a));
|
||||
});
|
||||
}
|
||||
|
||||
private partial class AnchorMenuItem : TernaryStateRadioMenuItem
|
||||
{
|
||||
public readonly Anchor Anchor;
|
||||
|
||||
public AnchorMenuItem(Anchor anchor, Action<Anchor> applyFunction)
|
||||
: base(anchor.ToString(), MenuItemType.Standard, _ => applyFunction(anchor))
|
||||
{
|
||||
Anchor = anchor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
osu.Game/Rulesets/Judgements/LAsJudgementText.cs
Normal file
45
osu.Game/Rulesets/Judgements/LAsJudgementText.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Judgements
|
||||
{
|
||||
public abstract partial class LAsJudgementText : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
protected readonly HitResult Result;
|
||||
|
||||
protected SpriteText JudgementText { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
protected LAsJudgementText(HitResult result)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
protected LAsJudgementText()
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(JudgementText = CreateJudgementText());
|
||||
|
||||
JudgementText.Colour = colours.ForHitResult(Result);
|
||||
JudgementText.Text = Result.GetDescription().ToUpperInvariant();
|
||||
}
|
||||
|
||||
protected abstract SpriteText CreateJudgementText();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@@ -16,7 +15,6 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
@@ -63,6 +61,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
protected PausableSkinnableSound Samples { get; private set; }
|
||||
|
||||
private bool samplesLoaded;
|
||||
|
||||
public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
|
||||
|
||||
private readonly List<DrawableHitObject> nestedHitObjects = new List<DrawableHitObject>();
|
||||
@@ -227,6 +227,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
comboColourBrightness.BindValueChanged(_ => UpdateComboColour());
|
||||
|
||||
samplesBindable.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
if (samplesLoaded)
|
||||
LoadSamples();
|
||||
});
|
||||
|
||||
// Apply transforms
|
||||
updateStateFromResult();
|
||||
}
|
||||
@@ -293,8 +299,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
|
||||
samplesBindable.BindTo(HitObject.SamplesBindable);
|
||||
samplesBindable.BindCollectionChanged(onSamplesChanged, true);
|
||||
|
||||
HitObject.DefaultsApplied += onDefaultsApplied;
|
||||
|
||||
OnApply();
|
||||
@@ -335,11 +339,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
samplesBindable.UnbindFrom(HitObject.SamplesBindable);
|
||||
|
||||
// When a new hitobject is applied, the samples will be cleared before re-populating.
|
||||
// In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply().
|
||||
samplesBindable.CollectionChanged -= onSamplesChanged;
|
||||
|
||||
// Release the samples for other hitobjects to use.
|
||||
samplesLoaded = false;
|
||||
Samples?.ClearSamples();
|
||||
|
||||
foreach (var obj in nestedHitObjects)
|
||||
@@ -396,8 +397,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
Samples.Samples = samples.Cast<ISampleInfo>().ToArray();
|
||||
}
|
||||
|
||||
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();
|
||||
|
||||
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
|
||||
|
||||
private void onRevertResult()
|
||||
@@ -631,6 +630,33 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
// We use a flag here to load samples only when they are required to be played.
|
||||
// Why in Update and not PlaySamples? Because some hit object implementations may expect LoadSamples to be called to load custom samples
|
||||
// (slider slide sound as an example).
|
||||
//
|
||||
// This is best effort optimisation (over previous method of loading and de-pooling in `OnApply`) due to requiring knowledge of
|
||||
// hitobjects' metadata. For cases like sliders with many repeats, there can be a sudden request to de-pool (ie slider with many repeats)
|
||||
// hundreds of samples, causing a gameplay stutter.
|
||||
//
|
||||
// Note that we already have optimisations in OsuPlayfield for this but it applies to DrawableHitObjects and not samples.
|
||||
//
|
||||
// This is definitely not the end of optimisation of sample loading, but the structure of gameplay samples is going to take some
|
||||
// time to dismantle and optimise. Optimally:
|
||||
//
|
||||
// - we would want to remove as much of the drawable overheads from samples as possible (currently two drawables per sample worst case)
|
||||
// - pool the rawest representation of samples possible (if required at that point).
|
||||
// - infer metadata at beatmap load to asynchronously preload the samples (into memory / bass).
|
||||
if (!samplesLoaded)
|
||||
{
|
||||
samplesLoaded = true;
|
||||
LoadSamples();
|
||||
}
|
||||
|
||||
base.Update();
|
||||
}
|
||||
|
||||
public override bool UpdateSubTreeMasking() => false;
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
@@ -640,14 +666,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
UpdateResult(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules an <see cref="Action"/> to this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only provided temporarily until hitobject pooling is implemented.
|
||||
/// </remarks>
|
||||
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
|
||||
|
||||
/// <summary>
|
||||
/// An offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
|
||||
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
|
||||
|
||||
@@ -55,20 +55,23 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the average hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// Calculates the median hit offset/error for a sequence of <see cref="HitEvent"/>s, where negative numbers mean the user hit too early on average.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A non-null <see langword="double"/> value if unstable rate could be calculated,
|
||||
/// and <see langword="null"/> if unstable rate cannot be calculated due to <paramref name="hitEvents"/> being empty.
|
||||
/// </returns>
|
||||
public static double? CalculateAverageHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
public static double? CalculateMedianHitError(this IEnumerable<HitEvent> hitEvents)
|
||||
{
|
||||
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).ToArray();
|
||||
double[] timeOffsets = hitEvents.Where(AffectsUnstableRate).Select(ev => ev.TimeOffset).OrderBy(x => x).ToArray();
|
||||
|
||||
if (timeOffsets.Length == 0)
|
||||
return null;
|
||||
|
||||
return timeOffsets.Average();
|
||||
int center = timeOffsets.Length / 2;
|
||||
|
||||
// Use average of the 2 central values if length is even
|
||||
return timeOffsets.Length % 2 == 0 ? (timeOffsets[center - 1] + timeOffsets[center]) / 2 : timeOffsets[center];
|
||||
}
|
||||
|
||||
public static bool AffectsUnstableRate(HitEvent e) => AffectsUnstableRate(e.HitObject, e.Result);
|
||||
|
||||
@@ -54,7 +54,12 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <summary>
|
||||
/// The key conversion input manager for this DrawableRuleset.
|
||||
/// </summary>
|
||||
protected PassThroughInputManager KeyBindingInputManager;
|
||||
protected PassThroughInputManager KeyBindingInputManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// This configuration for this DrawableRuleset.
|
||||
/// </summary>
|
||||
protected IRulesetConfigManager Config { get; private set; }
|
||||
|
||||
public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0;
|
||||
|
||||
@@ -77,8 +82,26 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public override IFrameStableClock FrameStableClock => frameStabilityContainer;
|
||||
|
||||
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap.
|
||||
/// </summary>
|
||||
[Cached(typeof(IBeatmap))]
|
||||
public readonly Beatmap<TObject> Beatmap;
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
public sealed override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private OnScreenDisplay onScreenDisplay { get; set; }
|
||||
|
||||
private readonly PlayfieldAdjustmentContainer playfieldAdjustmentContainer;
|
||||
|
||||
private IDisposable configTracker;
|
||||
private FrameStabilityContainer frameStabilityContainer;
|
||||
private DrawableRulesetDependencies dependencies;
|
||||
|
||||
private bool allowBackwardsSeeks;
|
||||
|
||||
public override bool AllowBackwardsSeeks
|
||||
@@ -105,25 +128,6 @@ namespace osu.Game.Rulesets.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap.
|
||||
/// </summary>
|
||||
[Cached(typeof(IBeatmap))]
|
||||
public readonly Beatmap<TObject> Beatmap;
|
||||
|
||||
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;
|
||||
|
||||
protected IRulesetConfigManager Config { get; private set; }
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
public sealed override IReadOnlyList<Mod> Mods { get; }
|
||||
|
||||
private FrameStabilityContainer frameStabilityContainer;
|
||||
|
||||
private OnScreenDisplay onScreenDisplay;
|
||||
|
||||
private DrawableRulesetDependencies dependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ruleset visualisation for the provided ruleset and beatmap.
|
||||
/// </summary>
|
||||
@@ -156,6 +160,9 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (Config != null)
|
||||
configTracker = onScreenDisplay?.BeginTracking(this, Config);
|
||||
|
||||
IsPaused.ValueChanged += paused =>
|
||||
{
|
||||
if (HasReplayLoaded.Value)
|
||||
@@ -168,13 +175,7 @@ namespace osu.Game.Rulesets.UI
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
dependencies = new DrawableRulesetDependencies(Ruleset, base.CreateChildDependencies(parent));
|
||||
|
||||
Config = dependencies.RulesetConfigManager;
|
||||
|
||||
onScreenDisplay = dependencies.Get<OnScreenDisplay>();
|
||||
if (Config != null)
|
||||
onScreenDisplay?.BeginTracking(this, Config);
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@@ -404,11 +405,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (Config != null)
|
||||
{
|
||||
onScreenDisplay?.StopTracking(this, Config);
|
||||
Config = null;
|
||||
}
|
||||
configTracker?.Dispose();
|
||||
|
||||
// Dispose the components created by this dependency container.
|
||||
dependencies?.Dispose();
|
||||
|
||||
@@ -140,6 +140,23 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
private Background createBackground()
|
||||
{
|
||||
switch (source.Value)
|
||||
{
|
||||
case BackgroundSource.WebmSource:
|
||||
{
|
||||
string[] videoFiles = System.IO.Directory.GetFiles(@"Textures/Webm", "*.webm");
|
||||
|
||||
if (videoFiles.Length == 0)
|
||||
{
|
||||
// 没有找到视频文件,返回一个默认背景
|
||||
return new Background(getBackgroundTextureName());
|
||||
}
|
||||
|
||||
string videoPath = videoFiles[RNG.Next(videoFiles.Length)];
|
||||
return new VideoBackgroundScreen(videoPath);
|
||||
}
|
||||
}
|
||||
|
||||
// seasonal background loading gets highest priority.
|
||||
Background newBackground = seasonalBackgroundLoader.LoadNextBackground();
|
||||
|
||||
|
||||
30
osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs
Normal file
30
osu.Game/Screens/Backgrounds/VideoBackgroundScreen.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// VideoBackgroundScreen.cs
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Video;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
|
||||
namespace osu.Game.Screens.Backgrounds
|
||||
{
|
||||
public partial class VideoBackgroundScreen : Background
|
||||
{
|
||||
private readonly string videoPath;
|
||||
|
||||
public VideoBackgroundScreen(string videoPath)
|
||||
{
|
||||
this.videoPath = videoPath;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var video = new Video(videoPath)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Loop = true
|
||||
};
|
||||
AddInternal(video);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -217,7 +216,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
bpmText = new OsuTextFlowContainer(st =>
|
||||
{
|
||||
st.Font = OsuFont.Default.With(fixedWidth: true);
|
||||
st.Spacing = new Vector2(-2.2f, 0);
|
||||
st.Spacing = new Vector2(-1.9f, 0);
|
||||
})
|
||||
{
|
||||
Name = @"BPM display",
|
||||
@@ -233,8 +232,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
}
|
||||
|
||||
private double effectiveBeatLength;
|
||||
|
||||
private double effectiveBpm => 60_000 / effectiveBeatLength;
|
||||
private double effectiveBpm;
|
||||
|
||||
private TimingControlPoint timingPoint = null!;
|
||||
|
||||
@@ -268,19 +266,26 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private void updateBpmText()
|
||||
{
|
||||
int intPart = (int)interpolatedBpm.Value;
|
||||
bool reachedFinalNumber = interpolatedBpm.Value == effectiveBpm;
|
||||
int decimalPlaces = Math.Min(2, FormatUtils.FindPrecision((decimal)effectiveBpm));
|
||||
|
||||
bpmText.Text = intPart.ToLocalisableString();
|
||||
string text = interpolatedBpm.Value.ToString($"N{decimalPlaces}");
|
||||
int? breakPoint = null;
|
||||
|
||||
// While interpolating between two integer values, showing the decimal places would look a bit odd
|
||||
// so rounding is applied until we're close to the final value.
|
||||
int decimalPlaces = FormatUtils.FindPrecision((decimal)effectiveBpm);
|
||||
|
||||
if (decimalPlaces > 0)
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
{
|
||||
bool reachedFinalNumber = intPart == (int)effectiveBpm;
|
||||
if (!char.IsDigit(text[i]))
|
||||
breakPoint = i;
|
||||
}
|
||||
|
||||
bpmText.AddText((effectiveBpm % 1).ToLocalisableString("." + new string('0', decimalPlaces)), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.1f);
|
||||
if (breakPoint != null)
|
||||
{
|
||||
bpmText.Text = text.Substring(0, breakPoint.Value);
|
||||
bpmText.AddText(text.Substring(breakPoint.Value), cp => cp.Alpha = reachedFinalNumber ? 0.5f : 0.2f);
|
||||
}
|
||||
else
|
||||
{
|
||||
bpmText.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +305,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (effectiveBeatLength != timingPoint.BeatLength / Divisor)
|
||||
{
|
||||
effectiveBeatLength = timingPoint.BeatLength / Divisor;
|
||||
effectiveBpm = TimingSection.BeatLengthToBpm(effectiveBeatLength);
|
||||
|
||||
EarlyActivationMilliseconds = timingPoint.BeatLength / 2;
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
try
|
||||
{
|
||||
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
|
||||
beatLengthBindable.Value = beatLengthToBpm(doubleVal);
|
||||
beatLengthBindable.Value = BeatLengthToBpm(doubleVal);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
beatLengthBindable.BindValueChanged(val =>
|
||||
{
|
||||
Current.Value = beatLengthToBpm(val.NewValue).ToString("N2");
|
||||
Current.Value = BeatLengthToBpm(val.NewValue).ToString("N2");
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
}
|
||||
}
|
||||
|
||||
private static double beatLengthToBpm(double beatLength) => 60000 / beatLength;
|
||||
public static double BeatLengthToBpm(double beatLength) => 60000 / beatLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -27,6 +26,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Utils;
|
||||
using Container = osu.Framework.Graphics.Containers.Container;
|
||||
|
||||
@@ -62,11 +62,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
|
||||
private Sample? sampleStart;
|
||||
|
||||
/// <summary>
|
||||
/// Any mods applied by/to the local user.
|
||||
/// </summary>
|
||||
protected readonly Bindable<IReadOnlyList<Mod>> UserMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IOverlayManager? overlayManager { get; set; }
|
||||
|
||||
@@ -245,12 +240,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
}
|
||||
};
|
||||
|
||||
LoadComponent(UserModsSelectOverlay = new RoomModSelectOverlay
|
||||
{
|
||||
SelectedItem = { BindTarget = SelectedItem },
|
||||
SelectedMods = { BindTarget = UserMods },
|
||||
IsValidMod = _ => false
|
||||
});
|
||||
LoadComponent(UserModsSelectOverlay = new MultiplayerUserModSelectOverlay());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -258,7 +248,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
base.LoadComplete();
|
||||
|
||||
SelectedItem.BindValueChanged(_ => updateSpecifics());
|
||||
UserMods.BindValueChanged(_ => updateSpecifics());
|
||||
|
||||
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateSpecifics());
|
||||
|
||||
@@ -441,11 +430,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
? rulesetInstance.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, Room.Type)).ToArray()
|
||||
: item.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
|
||||
// Remove any user mods that are no longer allowed.
|
||||
Mod[] newUserMods = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
if (!newUserMods.SequenceEqual(UserMods.Value))
|
||||
UserMods.Value = newUserMods;
|
||||
|
||||
// Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
|
||||
int beatmapId = GetGameplayBeatmap().OnlineID;
|
||||
var localBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", beatmapId);
|
||||
@@ -456,15 +440,11 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
Ruleset.Value = GetGameplayRuleset();
|
||||
|
||||
if (allowedMods.Length > 0)
|
||||
{
|
||||
UserModsSection.Show();
|
||||
UserModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
|
||||
}
|
||||
else
|
||||
{
|
||||
UserModsSection.Hide();
|
||||
UserModsSelectOverlay.Hide();
|
||||
UserModsSelectOverlay.IsValidMod = _ => false;
|
||||
}
|
||||
|
||||
if (item.Freestyle)
|
||||
@@ -488,7 +468,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
UserStyleSection.Hide();
|
||||
}
|
||||
|
||||
protected virtual APIMod[] GetGameplayMods() => UserMods.Value.Select(m => new APIMod(m)).Concat(SelectedItem.Value!.RequiredMods).ToArray();
|
||||
protected virtual APIMod[] GetGameplayMods() => SelectedItem.Value!.RequiredMods;
|
||||
|
||||
protected virtual RulesetInfo GetGameplayRuleset() => Rulesets.GetRuleset(SelectedItem.Value!.RulesetID)!;
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public partial class MultiplayerUserModSelectOverlay : UserModSelectOverlay
|
||||
{
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
private ScheduledDelegate? debouncedModSettingsUpdate;
|
||||
|
||||
public MultiplayerUserModSelectOverlay()
|
||||
: base(OverlayColourScheme.Plum)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
SelectedMods.BindValueChanged(onSelectedModsChanged);
|
||||
|
||||
updateValidMods();
|
||||
}
|
||||
|
||||
private void onRoomUpdated()
|
||||
{
|
||||
// Importantly, this is not scheduled because the client must not skip intermediate server states to validate the allowed mods.
|
||||
updateValidMods();
|
||||
}
|
||||
|
||||
private void onSelectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(mods.NewValue).FireAndForget();
|
||||
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
||||
modSettingChangeTracker.SettingChanged += _ =>
|
||||
{
|
||||
// Debounce changes to mod settings so as to not thrash the network.
|
||||
debouncedModSettingsUpdate?.Cancel();
|
||||
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(SelectedMods.Value).FireAndForget();
|
||||
}, 500);
|
||||
};
|
||||
}
|
||||
|
||||
private void updateValidMods()
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return;
|
||||
|
||||
MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem;
|
||||
Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance();
|
||||
Mod[] allowedMods = currentItem.Freestyle
|
||||
? ruleset.AllMods.OfType<Mod>().Where(m => ModUtils.IsValidFreeModForMatchType(m, client.Room.Settings.MatchType)).ToArray()
|
||||
: currentItem.AllowedMods.Select(m => m.ToMod(ruleset)).ToArray();
|
||||
|
||||
// Update the mod panels to reflect the ones which are valid for selection.
|
||||
IsValidMod = allowedMods.Length > 0
|
||||
? m => allowedMods.Any(a => a.GetType() == m.GetType())
|
||||
: _ => false;
|
||||
|
||||
// Remove any mods that are no longer allowed.
|
||||
Mod[] newUserMods = SelectedMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
|
||||
if (!newUserMods.SequenceEqual(SelectedMods.Value))
|
||||
SelectedMods.Value = newUserMods;
|
||||
|
||||
// The active mods include the playlist item's required mods which change separately from the selected mods.
|
||||
IReadOnlyList<Mod> newActiveMods = ComputeActiveMods();
|
||||
if (!newActiveMods.SequenceEqual(ActiveMods.Value))
|
||||
ActiveMods.Value = newActiveMods;
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<Mod> ComputeActiveMods()
|
||||
{
|
||||
if (client.Room == null || client.LocalUser == null)
|
||||
return [];
|
||||
|
||||
MultiplayerPlaylistItem currentItem = client.Room.CurrentPlaylistItem;
|
||||
Ruleset ruleset = rulesets.GetRuleset(client.LocalUser.RulesetId ?? currentItem.RulesetID)!.CreateInstance();
|
||||
return currentItem.RequiredMods.Select(m => m.ToMod(ruleset)).Concat(base.ComputeActiveMods()).ToArray();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
|
||||
modSettingChangeTracker?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
@@ -19,12 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
{
|
||||
public readonly Bindable<MultiplayerPlaylistDisplayMode> DisplayMode = new Bindable<MultiplayerPlaylistDisplayMode>();
|
||||
|
||||
public required Bindable<PlaylistItem?> SelectedItem
|
||||
{
|
||||
get => selectedItem;
|
||||
set => selectedItem.Current = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when an item requests to be edited.
|
||||
/// </summary>
|
||||
@@ -33,18 +28,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
private readonly Room room;
|
||||
private readonly BindableWithCurrent<PlaylistItem?> selectedItem = new BindableWithCurrent<PlaylistItem?>();
|
||||
private MultiplayerPlaylistTabControl playlistTabControl = null!;
|
||||
private MultiplayerQueueList queueList = null!;
|
||||
private MultiplayerHistoryList historyList = null!;
|
||||
private bool firstPopulation = true;
|
||||
|
||||
public MultiplayerPlaylist(Room room)
|
||||
{
|
||||
this.room = room;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@@ -65,17 +53,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
queueList = new MultiplayerQueueList(room)
|
||||
queueList = new MultiplayerQueueList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
SelectedItem = { BindTarget = selectedItem },
|
||||
RequestEdit = item => RequestEdit?.Invoke(item)
|
||||
},
|
||||
historyList = new MultiplayerHistoryList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
SelectedItem = { BindTarget = selectedItem }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,10 +75,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
base.LoadComplete();
|
||||
|
||||
DisplayMode.BindValueChanged(onDisplayModeChanged, true);
|
||||
|
||||
client.ItemAdded += playlistItemAdded;
|
||||
client.ItemRemoved += playlistItemRemoved;
|
||||
client.ItemChanged += playlistItemChanged;
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
@@ -121,28 +109,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
|
||||
firstPopulation = false;
|
||||
}
|
||||
|
||||
PlaylistItem? currentItem = client.Room == null ? null : new PlaylistItem(client.Room.CurrentPlaylistItem);
|
||||
queueList.SelectedItem.Value = currentItem;
|
||||
historyList.SelectedItem.Value = currentItem;
|
||||
}
|
||||
|
||||
private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item));
|
||||
private void playlistItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() => addItemToLists(item));
|
||||
|
||||
private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item));
|
||||
private void playlistItemRemoved(long item) => Scheduler.Add(() => removeItemFromLists(item));
|
||||
|
||||
private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() =>
|
||||
private void playlistItemChanged(MultiplayerPlaylistItem item) => Scheduler.Add(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
var newApiItem = new PlaylistItem(item);
|
||||
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
|
||||
var existingItem = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
|
||||
|
||||
// Test if the only change between the two playlist items is the order.
|
||||
if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
|
||||
if (existingItem != null && existingItem.With(playlistOrder: item.PlaylistOrder).Equals(new PlaylistItem(item)))
|
||||
{
|
||||
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
|
||||
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
|
||||
|
||||
// The following isn't really required, but is here for safety and explicitness.
|
||||
// MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation.
|
||||
// Set the new order directly and refresh the flow layout as an optimisation to avoid refreshing the items' visual state.
|
||||
existingItem.PlaylistOrder = item.PlaylistOrder;
|
||||
queueList.Invalidate();
|
||||
}
|
||||
else
|
||||
@@ -154,22 +142,35 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
|
||||
private void addItemToLists(MultiplayerPlaylistItem item)
|
||||
{
|
||||
var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID);
|
||||
|
||||
// Item could have been removed from the playlist while the local player was in gameplay.
|
||||
if (apiItem == null)
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
if (item.Expired)
|
||||
historyList.Items.Add(new PlaylistItem(apiItem));
|
||||
historyList.Items.Add(new PlaylistItem(item));
|
||||
else
|
||||
queueList.Items.Add(new PlaylistItem(apiItem));
|
||||
queueList.Items.Add(new PlaylistItem(item));
|
||||
}
|
||||
|
||||
private void removeItemFromLists(long item)
|
||||
private void removeItemFromLists(long itemId)
|
||||
{
|
||||
queueList.Items.RemoveAll(i => i.ID == item);
|
||||
historyList.Items.RemoveAll(i => i.ID == item);
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
queueList.Items.RemoveAll(i => i.ID == itemId);
|
||||
historyList.Items.RemoveAll(i => i.ID == itemId);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.ItemAdded -= playlistItemAdded;
|
||||
client.ItemRemoved -= playlistItemRemoved;
|
||||
client.ItemChanged -= playlistItemChanged;
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -20,50 +19,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
/// </summary>
|
||||
public partial class MultiplayerQueueList : DrawableRoomPlaylist
|
||||
{
|
||||
private readonly Room room;
|
||||
|
||||
private QueueFillFlowContainer flow = null!;
|
||||
|
||||
public MultiplayerQueueList(Room room)
|
||||
public MultiplayerQueueList()
|
||||
{
|
||||
this.room = room;
|
||||
ShowItemOwners = true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
room.PropertyChanged += onRoomPropertyChanged;
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(Room.Playlist))
|
||||
updateRoomPlaylist();
|
||||
}
|
||||
|
||||
private void updateRoomPlaylist()
|
||||
=> flow.InvalidateLayout();
|
||||
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => flow = new QueueFillFlowContainer
|
||||
protected override FillFlowContainer<RearrangeableListItem<PlaylistItem>> CreateListFillFlowContainer() => new QueueFillFlowContainer
|
||||
{
|
||||
Spacing = new Vector2(0, 2)
|
||||
};
|
||||
|
||||
protected override DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new QueuePlaylistItem(item);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
room.PropertyChanged -= onRoomPropertyChanged;
|
||||
}
|
||||
|
||||
private partial class QueueFillFlowContainer : FillFlowContainer<RearrangeableListItem<PlaylistItem>>
|
||||
{
|
||||
public new void InvalidateLayout() => base.InvalidateLayout();
|
||||
|
||||
public override IEnumerable<Drawable> FlowingChildren => base.FlowingChildren.OfType<RearrangeableListItem<PlaylistItem>>().OrderBy(item => item.Model.PlaylistOrder);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -11,9 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API;
|
||||
@@ -23,7 +20,6 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
@@ -64,7 +60,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
base.LoadComplete();
|
||||
|
||||
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
|
||||
UserMods.BindValueChanged(onUserModsChanged);
|
||||
|
||||
client.LoadRequested += onLoadRequested;
|
||||
client.RoomUpdated += onRoomUpdated;
|
||||
@@ -145,11 +140,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
null,
|
||||
new Drawable[]
|
||||
{
|
||||
new MultiplayerPlaylist(Room)
|
||||
new MultiplayerPlaylist
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RequestEdit = OpenSongSelection,
|
||||
SelectedItem = SelectedItem
|
||||
RequestEdit = OpenSongSelection
|
||||
}
|
||||
},
|
||||
new[]
|
||||
@@ -307,35 +301,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
protected override void PartRoom() => client.LeaveRoom();
|
||||
|
||||
private ModSettingChangeTracker? modSettingChangeTracker;
|
||||
private ScheduledDelegate? debouncedModSettingsUpdate;
|
||||
|
||||
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
modSettingChangeTracker?.Dispose();
|
||||
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(mods.NewValue).FireAndForget();
|
||||
|
||||
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
|
||||
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
|
||||
}
|
||||
|
||||
private void onModSettingsChanged(Mod mod)
|
||||
{
|
||||
// Debounce changes to mod settings so as to not thrash the network.
|
||||
debouncedModSettingsUpdate?.Cancel();
|
||||
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
if (client.Room == null)
|
||||
return;
|
||||
|
||||
client.ChangeUserMods(UserMods.Value).FireAndForget();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
|
||||
{
|
||||
if (client.Room == null)
|
||||
@@ -463,8 +428,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
client.RoomUpdated -= onRoomUpdated;
|
||||
client.LoadRequested -= onLoadRequested;
|
||||
}
|
||||
|
||||
modSettingChangeTracker?.Dispose();
|
||||
}
|
||||
|
||||
public partial class AddItemButton : PurpleRoundedButton
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
Precision = 0.1f,
|
||||
};
|
||||
|
||||
[SettingSource(typeof(BarHitErrorMeterStrings), nameof(BarHitErrorMeterStrings.JudgementIconFadeOutDuration))]
|
||||
[SettingSource("Icon Fade Out Duration", "Icon Fade Out Duration")]
|
||||
public BindableNumber<int> JudgementFadeOutDuration { get; } = new BindableNumber<int>(1200)
|
||||
{
|
||||
MinValue = 100,
|
||||
|
||||
@@ -135,6 +135,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public BreakOverlay BreakOverlay;
|
||||
|
||||
private LetterboxOverlay letterboxOverlay;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the gameplay is currently in a break.
|
||||
/// </summary>
|
||||
@@ -277,6 +279,12 @@ namespace osu.Game.Screens.Play
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
|
||||
GameplayClockContainer.Add(new GameplayScrollWheelHandling());
|
||||
|
||||
// needs to exist in frame stable content, but is used by underlay layers so make sure assigned early.
|
||||
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
|
||||
{
|
||||
Breaks = Beatmap.Value.Beatmap.Breaks
|
||||
};
|
||||
|
||||
// load the skinning hierarchy first.
|
||||
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
|
||||
GameplayClockContainer.Add(rulesetSkinProvider);
|
||||
@@ -292,7 +300,7 @@ namespace osu.Game.Screens.Play
|
||||
Children = new[]
|
||||
{
|
||||
// underlay and gameplay should have access to the skinning sources.
|
||||
createUnderlayComponents(),
|
||||
createUnderlayComponents(Beatmap.Value),
|
||||
createGameplayComponents(Beatmap.Value)
|
||||
}
|
||||
},
|
||||
@@ -335,10 +343,13 @@ namespace osu.Game.Screens.Play
|
||||
dependencies.CacheAs(DrawableRuleset.FrameStableClock);
|
||||
dependencies.CacheAs<IGameplayClock>(DrawableRuleset.FrameStableClock);
|
||||
|
||||
letterboxOverlay.Clock = DrawableRuleset.FrameStableClock;
|
||||
letterboxOverlay.ProcessCustomClock = false;
|
||||
|
||||
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
|
||||
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
|
||||
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
||||
failAnimationContainer.Add(createOverlayComponents(Beatmap.Value));
|
||||
failAnimationContainer.Add(createOverlayComponents());
|
||||
|
||||
if (!DrawableRuleset.AllowGameplayOverlays)
|
||||
{
|
||||
@@ -409,14 +420,22 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
|
||||
private Drawable createUnderlayComponents()
|
||||
private Drawable createUnderlayComponents(WorkingBeatmap working)
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both },
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
letterboxOverlay = new LetterboxOverlay
|
||||
{
|
||||
BreakTracker = breakTracker,
|
||||
Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0,
|
||||
},
|
||||
new KiaiGameplayFountains(),
|
||||
},
|
||||
};
|
||||
@@ -434,15 +453,12 @@ namespace osu.Game.Screens.Play
|
||||
ScoreProcessor,
|
||||
HealthProcessor,
|
||||
new ComboEffects(ScoreProcessor),
|
||||
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
|
||||
{
|
||||
Breaks = working.Beatmap.Breaks
|
||||
}
|
||||
breakTracker,
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
private Drawable createOverlayComponents(IWorkingBeatmap working)
|
||||
private Drawable createOverlayComponents()
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
@@ -450,13 +466,6 @@ namespace osu.Game.Screens.Play
|
||||
Children = new[]
|
||||
{
|
||||
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
|
||||
new LetterboxOverlay
|
||||
{
|
||||
Clock = DrawableRuleset.FrameStableClock,
|
||||
ProcessCustomClock = false,
|
||||
BreakTracker = breakTracker,
|
||||
Alpha = working.Beatmap.LetterboxInBreaks ? 1 : 0,
|
||||
},
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
|
||||
{
|
||||
HoldToQuit =
|
||||
|
||||
@@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
[Resolved]
|
||||
private IGameplayClock? gameplayClock { get; set; }
|
||||
|
||||
private double lastPlayAverage;
|
||||
private double lastPlayMedian;
|
||||
private double lastPlayBeatmapOffset;
|
||||
private HitEventTimingDistributionGraph? lastPlayGraph;
|
||||
|
||||
private SettingsButton? useAverageButton;
|
||||
private SettingsButton? calibrateFromLastPlayButton;
|
||||
|
||||
private IDisposable? beatmapOffsetSubscription;
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
var hitEvents = score.NewValue.HitEvents;
|
||||
|
||||
if (!(hitEvents.CalculateAverageHitError() is double average))
|
||||
if (!(hitEvents.CalculateMedianHitError() is double median))
|
||||
return;
|
||||
|
||||
referenceScoreContainer.Children = new Drawable[]
|
||||
@@ -210,7 +210,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
// affecting unstable rate here is used as a substitute of determining if a hit event represents a *timed* hit event,
|
||||
// i.e. an user input that the user had to *time to the track*,
|
||||
// i.e. one that it *makes sense to use* when doing anything with timing and offsets.
|
||||
if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 10)
|
||||
if (hitEvents.Count(HitEventExtensions.AffectsUnstableRate) < 50)
|
||||
{
|
||||
referenceScoreContainer.AddRange(new Drawable[]
|
||||
{
|
||||
@@ -226,7 +226,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
return;
|
||||
}
|
||||
|
||||
lastPlayAverage = average;
|
||||
lastPlayMedian = median;
|
||||
lastPlayBeatmapOffset = Current.Value;
|
||||
|
||||
LinkFlowContainer globalOffsetText;
|
||||
@@ -239,7 +239,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
Height = 50,
|
||||
},
|
||||
new AverageHitError(hitEvents),
|
||||
useAverageButton = new SettingsButton
|
||||
calibrateFromLastPlayButton = new SettingsButton
|
||||
{
|
||||
Text = BeatmapOffsetControlStrings.CalibrateUsingLastPlay,
|
||||
Action = () =>
|
||||
@@ -247,7 +247,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
if (Current.Disabled)
|
||||
return;
|
||||
|
||||
Current.Value = lastPlayBeatmapOffset - lastPlayAverage;
|
||||
Current.Value = lastPlayBeatmapOffset - lastPlayMedian;
|
||||
lastAppliedScore.Value = ReferenceScore.Value;
|
||||
},
|
||||
},
|
||||
@@ -281,8 +281,8 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
||||
|
||||
bool allow = allowOffsetAdjust;
|
||||
|
||||
if (useAverageButton != null)
|
||||
useAverageButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayAverage, adjustmentSinceLastPlay, Current.Precision / 2);
|
||||
if (calibrateFromLastPlayButton != null)
|
||||
calibrateFromLastPlayButton.Enabled.Value = allow && !Precision.AlmostEquals(lastPlayMedian, adjustmentSinceLastPlay, Current.Precision / 2);
|
||||
|
||||
Current.Disabled = !allow;
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@ using osu.Game.Rulesets.Scoring;
|
||||
namespace osu.Game.Screens.Ranking.Statistics
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays the unstable rate statistic for a given play.
|
||||
/// Displays the average hit error statistic for a given play.
|
||||
/// </summary>
|
||||
public partial class AverageHitError : SimpleStatisticItem<double?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and computes an <see cref="AverageHitError"/> statistic.
|
||||
/// </summary>
|
||||
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
|
||||
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the average hit error based on.</param>
|
||||
public AverageHitError(IEnumerable<HitEvent> hitEvents)
|
||||
: base("Average Hit Error")
|
||||
{
|
||||
Value = hitEvents.CalculateAverageHitError();
|
||||
Value = hitEvents.CalculateMedianHitError();
|
||||
}
|
||||
|
||||
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
|
||||
|
||||
@@ -148,12 +148,14 @@ namespace osu.Game.Screens.Ranking
|
||||
if (allTags.Value == null || apiBeatmap.Value?.TopTags == null)
|
||||
return;
|
||||
|
||||
var allTagsById = allTags.Value.ToDictionary(t => t.Id);
|
||||
var relevantTagsById = allTags.Value
|
||||
.Where(tag => tag.RulesetId == null || tag.RulesetId == beatmapInfo.Ruleset.OnlineID)
|
||||
.ToDictionary(t => t.Id);
|
||||
var ownTagIds = apiBeatmap.Value.OwnTagIds?.ToHashSet() ?? new HashSet<long>();
|
||||
|
||||
foreach (var topTag in apiBeatmap.Value.TopTags)
|
||||
{
|
||||
if (allTagsById.Remove(topTag.TagId, out var tag))
|
||||
if (relevantTagsById.Remove(topTag.TagId, out var tag))
|
||||
{
|
||||
displayedTags.Add(new UserTag(tag)
|
||||
{
|
||||
@@ -163,7 +165,7 @@ namespace osu.Game.Screens.Ranking
|
||||
}
|
||||
}
|
||||
|
||||
extraTags.AddRange(allTagsById.Select(t => new UserTag(t.Value)));
|
||||
extraTags.AddRange(relevantTagsById.Select(t => new UserTag(t.Value)));
|
||||
|
||||
loadingLayer.Hide();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace osu.Game.Screens.Select.Filter
|
||||
Less,
|
||||
LessOrEqual,
|
||||
Equal,
|
||||
NotEqual,
|
||||
GreaterOrEqual,
|
||||
Greater
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select
|
||||
public static class FilterQueryParser
|
||||
{
|
||||
private static readonly Regex query_syntax_regex = new Regex(
|
||||
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*""[!]?)|(\S*))",
|
||||
@"\b(?<key>\w+)(?<op>(!?(:|=)|(>|<)(:|=)?))(?<value>("".*""[!]?)|(\S*))",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
||||
@@ -129,6 +129,10 @@ namespace osu.Game.Screens.Select
|
||||
case ":":
|
||||
return Operator.Equal;
|
||||
|
||||
case "!=":
|
||||
case "!:":
|
||||
return Operator.NotEqual;
|
||||
|
||||
case "<":
|
||||
return Operator.Less;
|
||||
|
||||
|
||||
@@ -24,35 +24,48 @@ namespace osu.Game.Storyboards
|
||||
|
||||
public readonly StoryboardCommandGroup Commands = new StoryboardCommandGroup();
|
||||
|
||||
public double StartTime
|
||||
public virtual double StartTime
|
||||
{
|
||||
get
|
||||
{
|
||||
// To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero.
|
||||
// A StartValue of zero governs, above all else, the first valid display time of a sprite.
|
||||
// Users that are crafting storyboards using raw osb scripting or external tools may create alpha events far before the actual display time
|
||||
// of sprites.
|
||||
//
|
||||
// You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero,
|
||||
// anything before that point can be ignored (the sprite is not visible after all).
|
||||
var alphaCommands = new List<(double startTime, bool isZeroStartValue)>();
|
||||
// To make sure lifetime optimisations work as efficiently as they can, let's locally find the first time a sprite becomes visible.
|
||||
var alphaCommands = new List<StoryboardCommand<float>>();
|
||||
|
||||
var command = Commands.Alpha.FirstOrDefault();
|
||||
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
|
||||
foreach (var command in Commands.Alpha)
|
||||
{
|
||||
alphaCommands.Add(command);
|
||||
if (visibleAtStartOrEnd(command))
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var loop in loopingGroups)
|
||||
{
|
||||
command = loop.Alpha.FirstOrDefault();
|
||||
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
|
||||
foreach (var command in loop.Alpha)
|
||||
{
|
||||
alphaCommands.Add(command);
|
||||
if (visibleAtStartOrEnd(command))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alphaCommands.Count > 0)
|
||||
{
|
||||
var firstAlpha = alphaCommands.MinBy(t => t.startTime);
|
||||
// Special care is given to cases where there's one or more no-op transforms (ie transforming from alpha 0 to alpha 0).
|
||||
// - If a 0->0 transform exists, we still need to check it to ensure the absolute first start value is non-visible.
|
||||
// - After ascertaining this, we then check the first non-noop transform to get the true start lifetime.
|
||||
var firstAlpha = alphaCommands.MinBy(c => c.StartTime);
|
||||
var firstRealAlpha = alphaCommands.Where(visibleAtStartOrEnd).MinBy(c => c.StartTime);
|
||||
|
||||
if (firstAlpha.isZeroStartValue)
|
||||
return firstAlpha.startTime;
|
||||
if (firstAlpha!.StartValue == 0 && firstRealAlpha != null)
|
||||
return firstRealAlpha.StartTime;
|
||||
}
|
||||
|
||||
return EarliestTransformTime;
|
||||
|
||||
bool visibleAtStartOrEnd(StoryboardCommand<float> command) => command.StartValue > 0 || command.EndValue > 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,11 @@ namespace osu.Game.Storyboards
|
||||
{
|
||||
// This is just required to get a valid StartTime based on the incoming offset.
|
||||
// Actual fades are handled inside DrawableStoryboardVideo for now.
|
||||
Commands.AddAlpha(Easing.None, offset, offset, 0, 0);
|
||||
StartTime = offset;
|
||||
}
|
||||
|
||||
public override double StartTime { get; }
|
||||
|
||||
public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
/// <param name="host">The room host.</param>
|
||||
public void AddServerSideRoom(Room room, APIUser host)
|
||||
{
|
||||
room.RoomID ??= currentRoomId++;
|
||||
room.RoomID = currentRoomId++;
|
||||
room.Host = host;
|
||||
|
||||
for (int i = 0; i < room.Playlist.Count; i++)
|
||||
|
||||
Reference in New Issue
Block a user