维护代码

This commit is contained in:
LA
2025-09-19 18:33:40 +08:00
parent a566704bf2
commit 6bc9867f64
22 changed files with 494 additions and 411 deletions

View File

@@ -1,34 +0,0 @@
namespace Example
{
// 主文件
public partial class FilterControl
{
private string privateField = "只有嵌套类能访问";
private void privateMethod() { }
}
// 情况1正确的嵌套类写法
public partial class FilterControl
{
public partial class DifficultyRangeSlider
{
public void AccessParent()
{
// ✅ 可以访问外层类的私有成员
var field = privateField; // 编译成功
privateMethod(); // 编译成功
}
}
}
// 情况2错误的独立类写法
public partial class DifficultyRangeSlider // 这是独立的类,不是嵌套类
{
public void AccessParent()
{
// ❌ 无法访问 FilterControl 的私有成员
// var field = privateField; // 编译错误!
// privateMethod(); // 编译错误!
}
}
}

View File

@@ -1,56 +0,0 @@
// 示例:嵌套类 vs 顶级类的区别
namespace Example
{
// ========== 情况1嵌套类设计osu! 当前使用) ==========
public partial class FilterControl : OverlayContainer
{
private readonly Dictionary<string, object> controlState = new();
private bool isInitialized = false;
// 嵌套类 - 逻辑上是 FilterControl 的一部分
public partial class KeyModeFilterTabControl : CompositeDrawable
{
public void AccessParentState()
{
// ✅ 可以访问外层类的私有成员
// 注意:需要通过外层类实例访问非静态成员
}
public void UpdateParentControl(FilterControl parent)
{
// ✅ 可以访问私有成员
parent.controlState["keyMode"] = "updated";
if (parent.isInitialized) { /* ... */ }
}
}
// 其他相关的嵌套类
public partial class DifficultyRangeSlider : ShearedRangeSlider { }
public partial class SongSelectSearchTextBox : ShearedFilterTextBox { }
}
// ========== 情况2顶级类设计替代方案 ==========
public class FilterControl : OverlayContainer
{
internal readonly Dictionary<string, object> controlState = new(); // 必须改为 internal
internal bool isInitialized = false; // 必须改为 internal
}
// 独立的顶级类
public class KeyModeFilterTabControl : CompositeDrawable
{
public void UpdateParentControl(FilterControl parent)
{
// ✅ 只能访问 internal/public 成员
parent.controlState["keyMode"] = "updated";
if (parent.isInitialized) { /* ... */ }
// ❌ 无法访问 private 成员
// parent.somePrivateField = value; // 编译错误
}
}
public class DifficultyRangeSlider : ShearedRangeSlider { }
public class SongSelectSearchTextBox : ShearedFilterTextBox { }
}

View File

@@ -1,29 +1,23 @@
// 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.
// #pragma warning disable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Storyboards;
using osu.Game.Screens.Play;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Mods.LAsMods
{
public class ManiaModEz2Settings : Mod, IApplicableToDifficulty, IApplicableToBeatmap, IApplicableToPlayer
public class ManiaModEz2Settings : Mod, IApplicableToDifficulty, IApplicableToBeatmap, IApplicableToDrawableHitObject
{
public override string Name => "Ez2 Settings";
public override string Acronym => "ES";
@@ -51,25 +45,22 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
Precision = 1
};
private IBeatmap beatmap = null!;
private readonly List<(double time, List<HitSampleInfo> samples)> removedNoteSamples = new List<(double, List<HitSampleInfo>)>();
private readonly List<HitObject> objectsToMakeAuto = new List<HitObject>();
public ManiaModEz2Settings()
{
NoScratch.ValueChanged += OnSettingChanged;
HealthScratch.ValueChanged += OnHealthScratchChanged;
NoScratch.ValueChanged += onSettingChanged;
HealthScratch.ValueChanged += onHealthScratchChanged;
}
private void OnSettingChanged(ValueChangedEvent<bool> e)
private void onSettingChanged(ValueChangedEvent<bool> e)
{
if (e.NewValue)
HealthScratch.Value = false;
if (e.NewValue) HealthScratch.Value = false;
}
private void OnHealthScratchChanged(ValueChangedEvent<bool> e)
private void onHealthScratchChanged(ValueChangedEvent<bool> e)
{
if (e.NewValue)
NoScratch.Value = false;
if (e.NewValue) NoScratch.Value = false;
}
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
@@ -93,20 +84,18 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
public void ApplyToBeatmap(IBeatmap beatmap)
{
this.beatmap = beatmap;
objectsToMakeAuto.Clear();
var maniaBeatmap = (ManiaBeatmap)beatmap;
int keys = (int)maniaBeatmap.Difficulty.CircleSize;
if (HealthScratch.Value)
NoScratch.Value = false;
if (HealthScratch.Value) NoScratch.Value = false;
if (HealthScratch.Value && HealthTemplate.TryGetValue(keys, out var moveTargets))
{
var notesToMove = maniaBeatmap.HitObjects
.Where(h => h is ManiaHitObject maniaHitObject && moveTargets.Contains(maniaHitObject.Column))
.Where(h => moveTargets.Contains(h.Column))
.OrderBy(h => h.StartTime)
.ToList();
ManiaHitObject? previousNote = null;
foreach (var note in notesToMove)
@@ -115,42 +104,38 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
{
bool moved = false;
if (MoveTemplate.TryGetValue(keys, out var targetColumns))
foreach (int targetColumn in MoveTemplate[keys])
{
foreach (int targetColumn in targetColumns)
int newColumn = targetColumn;
note.Column = newColumn % keys;
var targetColumnNotes = maniaBeatmap.HitObjects
.Where(h => h.Column == newColumn)
.OrderBy(h => h.StartTime)
.ToList();
bool isValid = true;
for (int i = 0; i < targetColumnNotes.Count - 1; i++)
{
int newColumn = targetColumn % keys;
note.Column = newColumn;
var currentNote = targetColumnNotes[i];
var nextNote = targetColumnNotes[i + 1];
var targetColumnNotes = maniaBeatmap.HitObjects
.Where(h => h is ManiaHitObject maniaHitObject && maniaHitObject.Column == newColumn)
.OrderBy(h => h.StartTime)
.ToList();
bool isValid = true;
for (int i = 0; i < targetColumnNotes.Count - 1; i++)
if (nextNote.StartTime - currentNote.StartTime <= beatmap.ControlPointInfo.TimingPointAt(nextNote.StartTime).BeatLength / 4)
{
var currentNote = targetColumnNotes[i];
var nextNote = targetColumnNotes[i + 1];
if (nextNote.StartTime - currentNote.StartTime <= beatmap.ControlPointInfo.TimingPointAt(nextNote.StartTime).BeatLength / 4)
{
isValid = false;
break;
}
if (currentNote is HoldNote holdNote && nextNote.StartTime <= holdNote.EndTime)
{
isValid = false;
break;
}
}
if (isValid)
{
moved = true;
isValid = false;
break;
}
if (currentNote is HoldNote holdNote && nextNote.StartTime <= holdNote.EndTime)
{
isValid = false;
break;
}
}
if (isValid)
{
moved = true;
break;
}
}
@@ -164,92 +149,23 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
if (NoScratch.Value && NoScratchTemplate.TryGetValue(keys, out var scratchToRemove))
{
var scratchNotesToRemove = maniaBeatmap.HitObjects
.Where(h => h is ManiaHitObject maniaHitObject && scratchToRemove.Contains(maniaHitObject.Column))
.OrderBy(h => h.StartTime)
.ToList();
foreach (var note in scratchNotesToRemove)
{
// 在移除前收集样本信息
captureRemovedSamples(note);
maniaBeatmap.HitObjects.Remove(note);
applySamples(note);
}
objectsToMakeAuto.AddRange(maniaBeatmap.HitObjects.Where(h => h is ManiaHitObject maniaHitObject && scratchToRemove.Contains(maniaHitObject.Column)));
}
if (NoPanel.Value && NoPanelTemplate.TryGetValue(keys, out var panelToRemove))
{
var panelNotesToRemove = maniaBeatmap.HitObjects
.Where(h => h is ManiaHitObject maniaHitObject && panelToRemove.Contains(maniaHitObject.Column))
.OrderBy(h => h.StartTime)
.ToList();
foreach (var note in panelNotesToRemove)
{
// 在移除前收集样本信息
captureRemovedSamples(note);
maniaBeatmap.HitObjects.Remove(note);
applySamples(note);
}
objectsToMakeAuto.AddRange(maniaBeatmap.HitObjects.Where(h => h is ManiaHitObject maniaHitObject && panelToRemove.Contains(maniaHitObject.Column)));
}
}
public void ApplyToPlayer(Player player)
public void ApplyToDrawableHitObject(DrawableHitObject drawableHitObject)
{
// 在游戏开始时添加自动激活的样本
addStoryboardSamplesForRemovedNotes(player);
}
private void captureRemovedSamples(HitObject hitObject)
{
if (hitObject.Samples == null || hitObject.Samples.Count == 0)
return;
var hitSamples = new List<HitSampleInfo>();
foreach (var sample in hitObject.Samples)
if (objectsToMakeAuto.Contains(drawableHitObject.HitObject))
{
if (sample is HitSampleInfo hitSample)
hitSamples.Add(hitSample);
// 隐藏note但保留音效参考ModCinema的实现
drawableHitObject.AlwaysPresent = true;
drawableHitObject.Hide();
}
if (hitSamples.Count > 0)
removedNoteSamples.Add((hitObject.StartTime, hitSamples));
}
private void addStoryboardSamplesForRemovedNotes(Player player)
{
if (removedNoteSamples.Count == 0)
return;
// 获取storyboard并添加自动播放的样本
var storyboard = player.GameplayState.Storyboard;
var backgroundLayer = storyboard.GetLayer("Background");
foreach (var (time, samples) in removedNoteSamples)
{
foreach (var sample in samples)
{
string? samplePath = sample.LookupNames.FirstOrDefault();
if (!string.IsNullOrEmpty(samplePath))
{
var storyboardSample = new StoryboardSampleInfo(samplePath, time, sample.Volume);
backgroundLayer.Add(storyboardSample);
}
}
}
// 清空列表以避免重复添加
removedNoteSamples.Clear();
}
private void applySamples(HitObject hitObject)
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + 5)
?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
}
public void ApplyToDifficulty(BeatmapDifficulty difficulty) { }
@@ -264,7 +180,7 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
{ 9, new List<int> { 0 } },
{ 8, new List<int> { 0 } },
{ 7, new List<int> { 0 } },
{ 6, new List<int> { 0 } },
{ 6, new List<int> { 0 } }
};
public Dictionary<int, List<int>> NoPanelTemplate { get; set; } = new Dictionary<int, List<int>>
@@ -272,7 +188,7 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
{ 18, new List<int> { 6, 11 } },
{ 14, new List<int> { 6 } },
{ 9, new List<int> { 8 } },
{ 7, new List<int> { 6 } },
{ 7, new List<int> { 6 } }
};
public Dictionary<int, List<int>> HealthTemplate { get; set; } = new Dictionary<int, List<int>>
@@ -283,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
{ 9, new List<int> { 0, 8 } },
{ 8, new List<int> { 0, 8 } },
{ 7, new List<int> { 0 } },
{ 6, new List<int> { 0 } },
{ 6, new List<int> { 0 } }
};
public Dictionary<int, List<int>> MoveTemplate { get; set; } = new Dictionary<int, List<int>>
@@ -294,43 +210,7 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
{ 9, new List<int> { 8, 0, 4, 2, 3, 1, 5, 7, 6 } },
{ 8, new List<int> { 7, 0, 6, 4, 2, 5, 3, 1 } },
{ 7, new List<int> { 6, 4, 2, 5, 3, 1 } },
{ 6, new List<int> { 4, 2, 5, 3, 1 } },
{ 6, new List<int> { 4, 2, 5, 3, 1 } }
};
}
}
// #pragma warning restore
// public void SetTrackBackgroundColor(List<int> trackIndices, Color4 color, List<Ez2ColumnBackground> columnBackgrounds)
// {
// foreach (int trackIndex in trackIndices)
// {
// if (trackIndex >= 0 && trackIndex < columnBackgrounds.Count)
// {
// var columnBackground = columnBackgrounds[trackIndex];
// columnBackground.background.Colour = color;
// }
// }
// }
// public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty)
// {
// }
// string lines = note;
// beatmap.UnhandledEventLines.Add(lines);
// string path = note.Samples.GetHashCode().ToString();
// double time = note.StartTime;
// storyboard.GetLayer("Background").Add(new StoryboardSampleInfo(path, time, 100));
// storyboard.GetLayer("Background").Add();
// applySamples(note);
// private Storyboard storyboard = null!;
// public void ApplyToSample(IAdjustableAudioComponent sample)
// {
// foreach (var noteSample in removedSamples)
// {
// string path = noteSample.Sample.ToString() ?? string.Empty;
// double time = noteSample.StartTime;
// storyboard.GetLayer("Background").Add(new StoryboardSampleInfo(path, time, 100));
// }
// }
// processedTracks.AddRange(panelToRemove);
// setTrackBackgroundColor(panelToRemove, new Color4(0, 0, 0, 0));

View File

@@ -18,23 +18,20 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
/// <summary>
/// 用于显示列背景的组件,支持按键高亮和暗化效果。
/// 背景虚化功能由 Stage 级别处理。
/// </summary>
public partial class EzColumnBackground : CompositeDrawable, IKeyBindingHandler<ManiaAction>
{
private Bindable<double> columnDim = new BindableDouble();
private Bindable<double> hitPosition = new Bindable<double>();
private Color4 brightColour;
private Color4 dimColour;
private Box backgroundOverlay = null!;
private readonly Box separator = new Box
{
Name = "Separator",
Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre,
Width = 2,
Colour = Color4.White.Opacity(0.5f),
Alpha = 0,
};
private Box dimOverlay = null!;
private Box hitOverlay = null!;
private Box separator = null!;
private Bindable<Color4> accentColour = null!;
@@ -52,49 +49,49 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
RelativeSizeAxes = Axes.Both;
// Masking = true;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Container
dimOverlay = new Box
{
Name = "Dim Overlay",
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Name = "Background",
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.8f).Darken(3),
},
backgroundOverlay = new Box
{
Name = "Background Gradient Overlay",
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Height = 0.5f,
Blending = BlendingParameters.Additive,
Alpha = 0
},
}
Colour = Color4.Black,
};
hitOverlay = new Box
{
Name = "Hit Overlay",
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Height = 0.5f,
Blending = BlendingParameters.Additive,
Alpha = 0
};
separator = new Box
{
Name = "Separator",
Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre,
Width = 2,
Colour = Color4.White.Opacity(0.5f),
Alpha = 0,
};
accentColour = new Bindable<Color4>(ezSkinConfig.GetColumnColor(stageDefinition.Columns, Column.Index));
accentColour.BindValueChanged(colour =>
{
var newColour = colour.NewValue.Darken(3);
var baseCol = colour.NewValue;
var newColour = baseCol.Darken(3);
if (newColour.A != 0)
{
newColour = newColour.Opacity(0.8f);
}
backgroundOverlay.Colour = newColour;
brightColour = colour.NewValue.Opacity(0.6f);
dimColour = colour.NewValue.Opacity(0);
hitOverlay.Colour = newColour;
brightColour = baseCol.Opacity(0.6f);
dimColour = baseCol.Opacity(0);
}, true);
}
@@ -102,37 +99,37 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
base.LoadComplete();
bool hasSeparator = Column.TopLevelContainer.Children
.OfType<Box>()
.Any(b => b.Name == "Separator");
if (!hasSeparator)
if (Column.TopLevelContainer.Children.OfType<Box>().All(b => b.Name != "Separator"))
Column.TopLevelContainer.Add(separator);
if (!Column.BackgroundContainer.Children.Contains(dimOverlay))
Column.BackgroundContainer.Add(dimOverlay);
if (!Column.BackgroundContainer.Children.Contains(hitOverlay))
Column.BackgroundContainer.Add(hitOverlay);
hitPosition = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
hitPosition.BindValueChanged(_ => OnConfigChanged(), true);
hitPosition.BindValueChanged(_ => updateSeparator(), true);
columnDim = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnDim);
columnDim.BindValueChanged(_ => applyDim(), true);
}
protected virtual Color4 NoteColor
private float dimTarget => (float)columnDim.Value;
private void applyDim()
{
get
{
int keyMode = stageDefinition.Columns;
int columnIndex = Column.Index;
return ezSkinConfig.GetColumnColor(keyMode, columnIndex);
}
dimOverlay.Colour = Colour4.Black.Opacity(dimTarget);
}
private void OnConfigChanged()
private void updateSeparator()
{
separator.Height = DrawHeight - (float)hitPosition.Value;
if (drawSeparator(Column.Index, stageDefinition))
separator.Alpha = 0.25f;
else
separator.Alpha = 0;
separator.Alpha = drawSeparator(Column.Index, stageDefinition) ? 0.25f : 0;
}
protected virtual Color4 NoteColor => ezSkinConfig.GetColumnColor(stageDefinition.Columns, Column.Index);
public bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
{
if (e.Action == Column.Action.Value)
@@ -140,8 +137,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
var noteColour = NoteColor;
brightColour = noteColour.Opacity(0.9f);
dimColour = noteColour.Opacity(0);
backgroundOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour);
backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
hitOverlay.Colour = ColourInfo.GradientVertical(dimColour, brightColour);
hitOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint);
}
return false;
@@ -150,19 +147,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
public void OnReleased(KeyBindingReleaseEvent<ManiaAction> e)
{
if (e.Action == Column.Action.Value)
backgroundOverlay.FadeTo(0, 250, Easing.OutQuint);
hitOverlay.FadeTo(0, 250, Easing.OutQuint);
}
//TODO: 这里的逻辑可以优化,避免重复计算
private bool drawSeparator(int columnIndex, StageDefinition stage)
private bool drawSeparator(int columnIndex, StageDefinition stage) => stage.Columns switch
{
return stage.Columns switch
{
12 => columnIndex is 0 or 10,
14 => columnIndex is 0 or 5 or 6 or 11,
16 => columnIndex is 0 or 5 or 9 or 14,
_ => false
};
}
12 => columnIndex is 0 or 10,
14 => columnIndex is 0 or 5 or 6 or 11,
16 => columnIndex is 0 or 5 or 9 or 14,
_ => false
};
}
}

View File

@@ -38,11 +38,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
public EzHoldNoteMiddle()
{
RelativeSizeAxes = Axes.Both;
// FillMode = FillMode.Stretch;
// Anchor = Anchor.BottomCentre;
// Origin = Anchor.BottomCentre;
// Masking = true;
}
[BackgroundDependencyLoader(true)]
@@ -59,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
base.LoadComplete();
isHitting.BindValueChanged(onIsHittingChanged, true);
// 确保光效层被正确初始化
if (lightContainer == null)
OnLightChanged();
}
@@ -120,7 +114,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
protected override void OnDrawableChanged()
{
// 清理之前的光效层和容器
if (lightContainer != null)
{
if (lightContainer.Parent != null)

View File

@@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
return new EzColumnBackground();
case ManiaSkinComponents.KeyArea:
return new Ez2KeyArea();
return columnWidth == 0 ? Drawable.Empty() : new EzKeyArea();
case ManiaSkinComponents.Note:
return new EzNote();
@@ -221,6 +221,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
#region GetConfig
private float columnWidth;
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
@@ -228,16 +230,16 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
int columnIndex = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(columnIndex);
bool isSpecialColumn = ezSkinConfig.GetColumnType(stage.Columns, columnIndex) == "S";
float width = (float)columnWidthBindable.Value * (isSpecialColumn ? (float)specialFactorBindable.Value : 1f);
columnWidth = (float)columnWidthBindable.Value * (isSpecialColumn ? (float)specialFactorBindable.Value : 1f);
// float hitPositionValue = (float)hitPosition.Value; // + (float)virtualHitPosition.Value - 110f;
if (stage.Columns == 14 && columnIndex == 13)
width = 0f;
columnWidth = 0f;
switch (maniaLookup.Lookup)
{
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
return SkinUtils.As<TValue>(new Bindable<float>(width));
return SkinUtils.As<TValue>(new Bindable<float>(columnWidth));
// case LegacyManiaSkinConfigurationLookups.HitPosition:
// return SkinUtils.As<TValue>(new Bindable<float>(hitPositionValue));

View File

@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable noteAnimation = null!;
private float? minimumColumnWidth;
private float? widthForNoteHeightScale;
public LegacyNotePiece()
{
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
minimumColumnWidth = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
widthForNoteHeightScale = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale))?.Value;
InternalChild = directionContainer = new Container
{
@@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
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);
float noteHeight = widthForNoteHeightScale ?? DrawWidth;
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, noteHeight), texture.DisplayWidth);
}
}

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 System.Net;
using NUnit.Framework;
@@ -9,10 +10,12 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings;
@@ -54,7 +57,7 @@ namespace osu.Game.Tests.Visual.Menus
}
[Test]
public void TestLoginSuccess()
public void TestLoginSuccess_EmailVerification()
{
AddStep("logout", () => API.Logout());
assertAPIState(APIState.Offline);
@@ -94,6 +97,152 @@ namespace osu.Game.Tests.Visual.Menus
assertDropdownState(UserAction.DoNotDisturb);
}
[Test]
public void TestLoginSuccess_TOTPVerification()
{
AddStep("logout", () => API.Logout());
assertAPIState(APIState.Offline);
AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword);
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
assertAPIState(APIState.RequiresSecondFactorAuth);
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case VerifySessionRequest verifySessionRequest:
if (verifySessionRequest.VerificationKey == "012345")
verifySessionRequest.TriggerSuccess();
else
verifySessionRequest.TriggerFailure(new WebException());
return true;
}
return false;
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "012345");
assertAPIState(APIState.Online);
assertDropdownState(UserAction.Online);
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
assertDropdownState(UserAction.Online);
AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb));
assertDropdownState(UserAction.DoNotDisturb);
}
[Test]
public void TestLoginSuccess_TOTPVerification_FallbackToEmail()
{
AddStep("logout", () => API.Logout());
assertAPIState(APIState.Offline);
AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword);
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
assertAPIState(APIState.RequiresSecondFactorAuth);
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case VerifySessionRequest verifySessionRequest:
if (verifySessionRequest.VerificationKey == "deadbeef")
verifySessionRequest.TriggerSuccess();
else
verifySessionRequest.TriggerFailure(new WebException());
return true;
case VerificationMailFallbackRequest verificationMailFallbackRequest:
verificationMailFallbackRequest.TriggerSuccess();
return true;
}
return false;
});
AddStep("request fallback to email", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<OsuSpriteText>().Single(t => t.Text.ToString().Contains("email", StringComparison.InvariantCultureIgnoreCase)));
InputManager.Click(MouseButton.Left);
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "deadbeef");
assertAPIState(APIState.Online);
assertDropdownState(UserAction.Online);
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
assertDropdownState(UserAction.Online);
AddStep("change user state", () => localConfig.SetValue(OsuSetting.UserOnlineStatus, UserStatus.DoNotDisturb));
assertDropdownState(UserAction.DoNotDisturb);
}
[Test]
public void TestLoginSuccess_TOTPVerification_TurnedOffMidwayThrough()
{
bool firstAttemptHandled = false;
AddStep("logout", () => API.Logout());
assertAPIState(APIState.Offline);
AddStep("expect totp verification", () => dummyAPI.SessionVerificationMethod = SessionVerificationMethod.TimedOneTimePassword);
AddStep("enter password", () => loginOverlay.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
AddStep("submit", () => loginOverlay.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
assertAPIState(APIState.RequiresSecondFactorAuth);
AddUntilStep("wait for second factor auth form", () => loginOverlay.ChildrenOfType<SecondFactorAuthForm>().SingleOrDefault(), () => Is.Not.Null);
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case VerifySessionRequest verifySessionRequest:
verifySessionRequest.RequiredVerificationMethod = SessionVerificationMethod.EmailMessage;
verifySessionRequest.TriggerFailure(new WebException());
firstAttemptHandled = true;
return true;
}
return false;
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "123456");
AddUntilStep("first verification attempt handled", () => firstAttemptHandled);
assertAPIState(APIState.RequiresSecondFactorAuth);
AddStep("set up verification handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case VerifySessionRequest verifySessionRequest:
if (verifySessionRequest.VerificationKey == "deadbeef")
verifySessionRequest.TriggerSuccess();
else
verifySessionRequest.TriggerFailure(new WebException());
return true;
}
return false;
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "deadbeef");
assertAPIState(APIState.Online);
assertDropdownState(UserAction.Online);
}
private void assertDropdownState(UserAction state)
{
AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType<UserDropdown>().First().Current.Value, () => Is.EqualTo(state));

View File

@@ -15,6 +15,7 @@ using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@@ -52,6 +53,8 @@ namespace osu.Game.Online.API
public string ProvidedUsername { get; private set; }
public SessionVerificationMethod? SessionVerificationMethod { get; set; }
public string SecondFactorCode { get; private set; }
private string password;
@@ -292,7 +295,17 @@ namespace osu.Game.Online.API
verificationRequest.Failure += ex =>
{
state.Value = APIState.RequiresSecondFactorAuth;
LastLoginError = ex;
if (verificationRequest.RequiredVerificationMethod != null)
{
SessionVerificationMethod = verificationRequest.RequiredVerificationMethod;
LastLoginError = new APIException($"Must use {SessionVerificationMethod.GetDescription().ToLowerInvariant()} to complete verification.", ex);
}
else
{
LastLoginError = ex;
}
SecondFactorCode = null;
};
@@ -337,7 +350,8 @@ namespace osu.Game.Online.API
localUser.Value = me;
configSupporter.Value = me.IsSupporter;
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
SessionVerificationMethod = me.SessionVerificationMethod;
state.Value = SessionVerificationMethod == null ? APIState.Online : APIState.RequiresSecondFactorAuth;
failureCount = 0;
};

View File

@@ -5,6 +5,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API.Requests;
@@ -62,7 +63,8 @@ namespace osu.Game.Online.API
private bool shouldFailNextLogin;
private bool stayConnectingNextLogin;
private bool requiredSecondFactorAuth = true;
public SessionVerificationMethod? SessionVerificationMethod { get; set; } = Requests.Responses.SessionVerificationMethod.EmailMessage;
/// <summary>
/// The current connectivity state of the API.
@@ -130,14 +132,14 @@ namespace osu.Game.Online.API
Id = DUMMY_USER_ID,
};
if (requiredSecondFactorAuth)
if (SessionVerificationMethod != null)
{
state.Value = APIState.RequiresSecondFactorAuth;
}
else
{
onSuccessfulLogin();
requiredSecondFactorAuth = true;
SessionVerificationMethod = null;
}
}
@@ -147,7 +149,16 @@ namespace osu.Game.Online.API
request.Failure += e =>
{
state.Value = APIState.RequiresSecondFactorAuth;
LastLoginError = e;
if (request.RequiredVerificationMethod != null)
{
SessionVerificationMethod = request.RequiredVerificationMethod;
LastLoginError = new APIException($"Must use {SessionVerificationMethod.GetDescription().ToLowerInvariant()} to complete verification.", e);
}
else
{
LastLoginError = e;
}
};
state.Value = APIState.Connecting;
@@ -204,7 +215,7 @@ namespace osu.Game.Online.API
/// <summary>
/// Skip 2FA requirement for next login.
/// </summary>
public void SkipSecondFactor() => requiredSecondFactorAuth = false;
public void SkipSecondFactor() => SessionVerificationMethod = null;
/// <summary>
/// During the next simulated login, the process will fail immediately.

View File

@@ -107,10 +107,15 @@ namespace osu.Game.Online.API
/// <param name="password">The user's password.</param>
void Login(string username, string password);
/// <summary>
/// The <see cref="SessionVerificationMethod"/> requested by the server to complete verification.
/// </summary>
SessionVerificationMethod? SessionVerificationMethod { get; }
/// <summary>
/// Provide a second-factor authentication code for authentication.
/// </summary>
/// <param name="code">The 2FA code.</param>
/// <paramref name="code">The 2FA code.</paramref>
void AuthenticateSecondFactor(string code);
/// <summary>

View File

@@ -1,13 +1,26 @@
// 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 System.Runtime.Serialization;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIMe : APIUser
{
[JsonProperty("session_verified")]
public bool SessionVerified { get; set; }
[JsonProperty("session_verification_method")]
public SessionVerificationMethod? SessionVerificationMethod { get; set; }
}
public enum SessionVerificationMethod
{
[Description("Timed one-time password")]
[EnumMember(Value = "totp")]
TimedOneTimePassword,
[Description("E-mail")]
[EnumMember(Value = "mail")]
EmailMessage,
}
}

View File

@@ -0,0 +1,20 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class VerificationMailFallbackRequest : APIRequest
{
protected override string Target => @"session/verify/mail-fallback";
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
return req;
}
}
}

View File

@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
@@ -13,6 +15,16 @@ namespace osu.Game.Online.API.Requests
public VerifySessionRequest(string verificationKey)
{
VerificationKey = verificationKey;
Failure += _ =>
{
string? response = WebRequest?.GetResponseString();
if (string.IsNullOrEmpty(response))
return;
var responseObject = JsonConvert.DeserializeObject<VerificationFailureResponse>(response);
RequiredVerificationMethod = responseObject?.RequiredSessionVerificationMethod;
};
}
protected override WebRequest CreateWebRequest()
@@ -26,5 +38,13 @@ namespace osu.Game.Online.API.Requests
}
protected override string Target => @"session/verify";
public SessionVerificationMethod? RequiredVerificationMethod { get; internal set; }
private class VerificationFailureResponse
{
[JsonProperty("method")]
public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; }
}
}
}

View File

@@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
using osuTK;
@@ -21,11 +22,11 @@ namespace osu.Game.Overlays.Login
{
public partial class SecondFactorAuthForm : Container
{
private OsuTextBox codeTextBox = null!;
private LinkFlowContainer explainText = null!;
private ErrorTextFlowContainer errorText = null!;
private LoadingLayer loading = null!;
private FillFlowContainer contentFlow = null!;
private OsuTextBox codeTextBox = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
@@ -36,6 +37,8 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
Children = new Drawable[]
{
new FillFlowContainer
@@ -46,46 +49,18 @@ namespace osu.Game.Overlays.Login
Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
Children = new Drawable[]
{
new FillFlowContainer
contentFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, SettingsSection.ITEM_SPACING),
Children = new Drawable[]
{
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "An email has been sent to you with a verification code. Enter the code.",
},
codeTextBox = new OsuTextBox
{
InputProperties = new TextInputProperties(TextInputType.Code),
PlaceholderText = "Enter code",
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
errorText = new ErrorTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
},
},
},
new LinkFlowContainer
errorText = new ErrorTextFlowContainer
{
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Alpha = 0,
},
}
},
@@ -95,6 +70,56 @@ namespace osu.Game.Overlays.Login
}
};
if (api.LastLoginError?.Message is string error)
{
errorText.Alpha = 1;
errorText.AddErrors(new[] { error });
}
showContent(api.SessionVerificationMethod!.Value);
}
private void showContent(SessionVerificationMethod sessionVerificationMethod)
{
switch (sessionVerificationMethod)
{
case SessionVerificationMethod.EmailMessage:
showEmailVerification();
break;
case SessionVerificationMethod.TimedOneTimePassword:
showTotpVerification();
break;
}
}
private void showEmailVerification()
{
LinkFlowContainer explainText;
contentFlow.Clear();
contentFlow.AddRange(new Drawable[]
{
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "An email has been sent to you with a verification code. Enter the code.",
},
codeTextBox = new OsuTextBox
{
InputProperties = new TextInputProperties(TextInputType.Code),
PlaceholderText = "Enter code",
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
});
explainText.AddParagraph(UserVerificationStrings.BoxInfoCheckSpam);
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
explainText.AddParagraph("If you can't access your email or have forgotten what you used, please follow the ");
@@ -131,12 +156,58 @@ namespace osu.Game.Overlays.Login
codeTextBox.Current.Disabled = true;
}
});
}
if (api.LastLoginError?.Message is string error)
private void showTotpVerification()
{
LinkFlowContainer explainText;
contentFlow.Clear();
contentFlow.AddRange(new Drawable[]
{
errorText.Alpha = 1;
errorText.AddErrors(new[] { error });
}
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Please enter the code from your authenticator app.",
},
codeTextBox = new OsuNumberBox
{
InputProperties = new TextInputProperties(TextInputType.NumericalPassword),
PlaceholderText = "Enter code",
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},
explainText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Regular))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
});
// We can't support localisable strings with nested links yet. Not sure if we even can (probably need to allow markdown link formatting or something).
explainText.AddParagraph("If you can't access your app, ");
explainText.AddLink("you can verify using email instead", () =>
{
var fallbackRequest = new VerificationMailFallbackRequest();
fallbackRequest.Success += showEmailVerification;
fallbackRequest.Failure += ex => errorText.Text = ex.Message;
Task.Run(() => api.Perform(fallbackRequest));
});
explainText.AddText(". You can also ");
explainText.AddLink(UserVerificationStrings.BoxInfoLogoutLink, () => { api.Logout(); });
explainText.AddText(".");
codeTextBox.Current.BindValueChanged(code =>
{
string trimmedCode = code.NewValue.Trim();
if (trimmedCode.Length == 6)
{
api.AuthenticateSecondFactor(trimmedCode);
codeTextBox.Current.Disabled = true;
}
});
}
public override bool AcceptsFocus => true;

View File

@@ -26,13 +26,6 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25;
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
protected const int ATTRIB_ID_AIM_TOP_WEIGHTED_SLIDER_FACTOR = 33;
protected const int ATTRIB_ID_SPEED_TOP_WEIGHTED_SLIDER_FACTOR = 35;
protected const int ATTRIB_ID_NESTED_SCORE_PER_OBJECT = 37;
protected const int ATTRIB_ID_LEGACY_SCORE_BASE_MULTIPLIER = 39;
protected const int ATTRIB_ID_MAXIMUM_LEGACY_COMBO_SCORE = 41;
protected const int ATTRIB_ID_RHYTHM_DIFFICULTY = 43;
protected const int ATTRIB_ID_CONSISTENCY_FACTOR = 45;
/// <summary>
/// The mods which were applied to the beatmap.

View File

@@ -62,7 +62,7 @@ namespace osu.Game.Screens
SetDefault(EzSkinSetting.HitPosition, LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION, 0, 500, 1.0);
SetDefault(EzSkinSetting.VisualHitPosition, 0.0, -100, 100, 1.0);
SetDefault(EzSkinSetting.HitTargetFloatFixed, 6, 0, 10, 0.1);
SetDefault(EzSkinSetting.HitTargetAlpha, 0.3, 0, 1, 0.1);
SetDefault(EzSkinSetting.HitTargetAlpha, 0.6, 0, 1, 0.01);
SetDefault(EzSkinSetting.NoteSetName, "evolve");
SetDefault(EzSkinSetting.StageName, "JIYU");

View File

@@ -171,7 +171,7 @@ namespace osu.Game.Screens
LabelText = "HitTargetAlpha".Localize(),
TooltipText = "HitTargetAlphaTooltip".Localize(),
Current = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitTargetAlpha),
KeyboardStep = 0.1f,
KeyboardStep = 0.01f,
},
new SettingsSlider<double>
{

View File

@@ -217,6 +217,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking
private void updateGameplayState()
{
MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem;
if (item.Expired)
return;
RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!;
Ruleset rulesetInstance = ruleset.CreateInstance();
@@ -228,9 +232,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking
Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
if (Beatmap.Value is DummyWorkingBeatmap)
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
{
if (client.LocalUser!.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle).FireAndForget();
}
else
client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
{
if (client.LocalUser!.State == MultiplayerUserState.Idle)
client.ChangeState(MultiplayerUserState.Ready).FireAndForget();
}
client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget();
}

View File

@@ -263,6 +263,7 @@ namespace osu.Game.Screens.Select.Carousel
{
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Scale = new Vector2(0.8f),
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{

View File

@@ -145,8 +145,10 @@ namespace osu.Game.Skinning
return SkinUtils.As<TValue>(new Bindable<float>(existing.ColumnWidth[maniaLookup.ColumnIndex.Value]));
case LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale:
Debug.Assert(maniaLookup.ColumnIndex != null);
return SkinUtils.As<TValue>(new Bindable<float>(existing.WidthForNoteHeightScale));
float width = existing.WidthForNoteHeightScale;
if (width <= 0)
width = existing.MinimumColumnWidth;
return SkinUtils.As<TValue>(new Bindable<float>(width));
case LegacyManiaSkinConfigurationLookups.HitPosition:
return SkinUtils.As<TValue>(new Bindable<float>(existing.HitPosition));

View File

@@ -20,10 +20,6 @@
<ItemGroup Label="Package References">
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="asio" Version="1.10.8" />
<PackageReference Include="BASS" Version="2.4.12.1" />
<PackageReference Include="BASS.ASIO" Version="1.3.1.2" />
<PackageReference Include="BASS.Native" Version="2.4.13.9" />
<PackageReference Include="DiffPlex" Version="1.7.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Humanizer" Version="2.14.1" />