更新,增加皮肤判定组件

This commit is contained in:
LA
2025-04-06 22:32:42 +08:00
parent 0a08e78b52
commit 58e7d2ce98
80 changed files with 2026 additions and 831 deletions

View 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"));
}
}
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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

View 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;
}
};
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 },
};
}
}

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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; }

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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 =>
// {
// });

View File

@@ -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)

View File

@@ -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"))));

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()
{

View File

@@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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)));
}
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,5 @@
osu file format v14
[Events]
Video,-5678,"Video.avi",0,0
F,0,1500,1600,0,1

View File

@@ -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)));

View File

@@ -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 }));
}
}
}

View File

@@ -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;

View File

@@ -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());

View File

@@ -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();

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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)]

View File

@@ -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]

View File

@@ -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;

View File

@@ -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,
}
};
}
}
}

View File

@@ -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" />

View File

@@ -19,6 +19,7 @@ namespace osu.Game.Tournament.Models
{
public int ID;
[JsonIgnore]
public List<string> Acronyms
{
get

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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,
}
}

View File

@@ -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();

View File

@@ -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)
{

View File

@@ -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,
}
}

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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; }
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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)

View File

@@ -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

View File

@@ -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();

View File

@@ -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;
}
}

View 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();
}
}

View File

@@ -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"/>.

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View 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);
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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)!;

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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 =

View File

@@ -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;
}

View File

@@ -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")}";

View File

@@ -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();
}

View File

@@ -11,6 +11,7 @@ namespace osu.Game.Screens.Select.Filter
Less,
LessOrEqual,
Equal,
NotEqual,
GreaterOrEqual,
Greater
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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++)