同步更新,并增加一些TODO

This commit is contained in:
LA
2026-01-01 13:17:20 +08:00
parent b3fe92a483
commit 53f9f2b717
45 changed files with 552 additions and 90 deletions

View File

@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1209.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1229.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@@ -50,7 +50,6 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Rulesets.Mania.Objects.EzCurrentHitObject;
namespace osu.Game.Rulesets.Mania
{

View File

@@ -175,6 +175,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
switch (hitMode)
{
// TODO: 判定值应该调整,太松了
case EzMUGHitMode.O2Jam:
if (bpm == 0) bpm = 200;
double coolRange = 7500.0 / bpm;

View File

@@ -13,7 +13,6 @@ using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Screens;
using osu.Game.Skinning;
using osuTK;

View File

@@ -322,7 +322,10 @@ namespace osu.Game.Rulesets.Mania.UI
column.RegisterPool<NoComboBreakLNTail, MalodyDrawableLNTail>(10, 50);
break;
// TODO: 暂时先用 EZ2AC 的物件池,以后根据使用反馈单独实现
case EzMUGHitMode.IIDX:
column.RegisterPool<NoMissLNBody, DrawableHoldNoteBody>(10, 50);
column.RegisterPool<Ez2AcLNHead, DrawableHoldNoteHead>(10, 50);
column.RegisterPool<NoMissLNBody, DrawableHoldNoteBody>(10, 50);
column.RegisterPool<Ez2AcLNTail, Ez2AcDrawableLNTail>(10, 50);
break;

View File

@@ -379,6 +379,25 @@ namespace osu.Game.Tests.Visual.SongSelectV2
checkMatchedBeatmaps(6);
}
[Test]
public void TestScopeToBeatmapWhenDifficultiesGroupedBySet()
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(SortMode.Artist);
checkMatchedBeatmaps(6);
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
WaitForFiltering();
checkMatchedBeatmaps(6);
}
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));

View File

@@ -23,6 +23,8 @@ namespace osu.Game.Tournament.Tests.Screens
{
FullName = { Value = @"Japan" },
Acronym = { Value = "JPN" },
Seed = { Value = "#28" },
LastYearPlacing = { Value = "#17-24" },
SeedingResults =
{
new SeedingResult
@@ -36,20 +38,38 @@ namespace osu.Game.Tournament.Tests.Screens
Seed = { Value = 8 }
}
}
},
new TournamentTeam
{
Acronym = { Value = "USA" },
FlagName = { Value = "US" },
FullName = { Value = "United States" },
}
}
};
[Test]
public void TestBasic()
[BackgroundDependencyLoader]
private void load()
{
AddStep("create seeding screen", () => Add(new SeedingScreen
Add(new SeedingScreen
{
FillMode = FillMode.Fit,
FillAspectRatio = 16 / 9f
}));
});
}
AddStep("set team to Japan", () => this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single());
[Test]
public void TestBasic()
{
AddStep("set team to Japan", () =>
this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single(t => t.FullName.Value == "Japan"));
}
[Test]
public void TestNoSeed()
{
AddStep("set team to USA", () =>
this.ChildrenOfType<SettingsTeamDropdown>().Single().Current.Value = ladder.Teams.Single(t => t.FullName.Value == "United States"));
}
}
}

View File

@@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Tests
Acronym = { Value = "JPN" },
FlagName = { Value = "JP" },
FullName = { Value = "Japan" },
LastYearPlacing = { Value = 10 },
LastYearPlacing = { Value = "#10" },
Seed = { Value = "#12" },
SeedingResults =
{

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -49,11 +50,9 @@ namespace osu.Game.Tournament.Models
public Bindable<string> Seed = new Bindable<string>(string.Empty);
public Bindable<int> LastYearPlacing = new BindableInt
{
MinValue = 0,
MaxValue = 256
};
[JsonProperty]
[JsonConverter(typeof(LastYearPlacingConverter))]
public Bindable<string> LastYearPlacing = new Bindable<string>(@"N/A");
[JsonProperty]
public BindableList<TournamentUser> Players { get; } = new BindableList<TournamentUser>();
@@ -76,5 +75,37 @@ namespace osu.Game.Tournament.Models
}
public override string ToString() => FullName.Value ?? Acronym.Value;
public class LastYearPlacingConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => objectType == typeof(Bindable<string>);
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
=> serializer.Serialize(writer, ((Bindable<string>)value!).Value);
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
var lastYearPlacing = existingValue as Bindable<string>;
Debug.Assert(lastYearPlacing != null);
switch (reader.TokenType)
{
case JsonToken.String:
lastYearPlacing.Value = (string?)reader.Value ?? lastYearPlacing.Default;
break;
case JsonToken.Integer:
long value = (long)reader.Value!;
lastYearPlacing.Value = value > 0 ? $@"#{value}" : lastYearPlacing.Default;
break;
default:
reader.Read();
break;
}
return lastYearPlacing;
}
}
}
}

View File

@@ -10,9 +10,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
@@ -111,43 +109,43 @@ namespace osu.Game.Tournament.Screens.Editors
new SettingsTextBox
{
LabelText = "Name",
Width = 0.2f,
Width = 0.33f,
Current = Model.FullName
},
acronymTextBox = new SettingsTextBox
{
LabelText = "Acronym",
Width = 0.2f,
Width = 0.25f,
Current = Model.Acronym
},
new SettingsTextBox
{
LabelText = "Flag",
Width = 0.2f,
Width = 0.25f,
Current = Model.FlagName
},
new SettingsTextBox
{
LabelText = "Seed",
Width = 0.2f,
Current = Model.Seed
},
new SettingsSlider<int, LastYearPlacementSlider>
{
LabelText = "Last Year Placement",
Width = 0.33f,
Current = Model.LastYearPlacing
},
new SettingsButton
{
Width = 0.2f,
Margin = new MarginPadding(10),
Width = 0.33f,
Margin = new MarginPadding { Top = 20 },
Text = "Edit seeding results",
Action = () =>
{
sceneManager?.SetScreen(new SeedingEditorScreen(team, parent));
}
},
new SettingsTextBox
{
LabelText = "Seed",
Width = 0.25f,
Current = Model.Seed
},
new SettingsTextBox
{
LabelText = "Last Year Placement",
Width = 0.25f,
Current = Model.LastYearPlacing
},
playerEditor,
new SettingsButton
{
@@ -200,11 +198,6 @@ namespace osu.Game.Tournament.Screens.Editors
}, true);
}
private partial class LastYearPlacementSlider : RoundedSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;
}
public partial class PlayerEditor : CompositeDrawable
{
private readonly TournamentTeam team;

View File

@@ -274,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
new RowDisplay("Seed:", team.Seed.Value),
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"),
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value),
new Container { Margin = new MarginPadding { Bottom = 30 } },
}
},

View File

@@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration
{
public enum IntroSequence
@@ -8,6 +11,8 @@ namespace osu.Game.Configuration
Circles,
Welcome,
Triangles,
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.IntroRandom))]
Random
}
}

View File

@@ -92,11 +92,12 @@ namespace osu.Game.Graphics.UserInterface
{
base.LoadComplete();
Colour = dimColour;
Enabled.BindValueChanged(_ => this.FadeColour(dimColour, 200, Easing.OutQuint));
Colour = Enabled.Value ? Colour4.White : DimColour;
Enabled.BindValueChanged(_ => this.FadeColour(DimColour, 200, Easing.OutQuint), true);
FinishTransforms(true);
}
private Color4 dimColour => Enabled.Value ? Color4.White : colours.Gray9;
protected virtual Colour4 DimColour => colours.Gray9;
protected override bool OnHover(HoverEvent e)
{

View File

@@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
@@ -137,7 +138,7 @@ namespace osu.Game.LAsEzExtensions.Analysis
Colour = colour ?? Colour4.White;
}
protected override string DisplayValue(string? value)
protected override LocalisableString DisplayValue(string? value)
{
return value ?? "N/A";
}

View File

@@ -39,6 +39,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString HitObjectsAppearEarlier => new TranslatableString(getKey(@"hit_objects_appear_earlier"), @"(hit objects appear earlier)");
/// <summary>
/// "Beatmap offset was adjusted to {0} ms."
/// </summary>
public static LocalisableString BeatmapOffsetWasAdjustedTo(string offset) => new TranslatableString(getKey(@"beatmap_offset_was_adjusted_to"), @"Beatmap offset was adjusted to {0} ms.", offset);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -0,0 +1,39 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class RankingStatisticsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.RankingStatisticsStrings";
/// <summary>
/// "Average Hit Error"
/// </summary>
public static LocalisableString AverageHitErrorTitle => new TranslatableString(getKey(@"average_hit_error_title"), @"Average Hit Error");
/// <summary>
/// "Unstable Rate"
/// </summary>
public static LocalisableString UnstableRateTitle => new TranslatableString(getKey(@"unstable_rate_title"), @"Unstable Rate");
/// <summary>
/// "{0:N2} ms early"
/// </summary>
public static LocalisableString Early(double offset) => new TranslatableString(getKey(@"early"), @"{0:N2} ms early", offset);
/// <summary>
/// "{0:N2} ms late"
/// </summary>
public static LocalisableString Late(double offset) => new TranslatableString(getKey(@"late"), @"{0:N2} ms late", offset);
/// <summary>
/// "(not available)"
/// </summary>
public static LocalisableString NotAvailable => new TranslatableString(getKey(@"not_available"), @"(not available)");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"change the way osu! behaves");
/// <summary>
/// "Copy version"
/// </summary>
public static LocalisableString CopyVersion => new TranslatableString(getKey(@"copy_version"), @"Copy version");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@@ -59,6 +59,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString IntroSequence => new TranslatableString(getKey(@"intro_sequence"), @"Intro sequence");
/// <summary>
/// "Random"
/// </summary>
public static LocalisableString IntroRandom => new TranslatableString(getKey(@"intro_random"), @"Random");
/// <summary>
/// "Background source"
/// </summary>

View File

@@ -224,21 +224,27 @@ namespace osu.Game.Overlays
};
}
private double? lastSeekTime;
private double? lastSeekGameTime;
private double? lastSeekAudioTargetTime;
private void onSeek(double progress)
{
if (!musicController.IsPlaying || lastSeekTime == null || Time.Current - lastSeekTime > TRACK_DRAG_SEEK_DEBOUNCE)
if (!musicController.IsPlaying || lastSeekGameTime == null || Time.Current - lastSeekGameTime > TRACK_DRAG_SEEK_DEBOUNCE)
{
musicController.SeekTo(progress);
lastSeekTime = Time.Current;
lastSeekGameTime = Time.Current;
lastSeekAudioTargetTime = progress;
}
}
private void onCommit(double progress)
{
musicController.SeekTo(progress);
lastSeekTime = null;
// Avoid a second seek to the same location, which could occur when using keyboard navigation from `OnSeek` and subsequent `OnCommit` calls.
if (progress != lastSeekAudioTargetTime)
musicController.SeekTo(progress);
lastSeekGameTime = null;
lastSeekAudioTargetTime = null;
}
private void togglePlaylist()
@@ -518,6 +524,8 @@ namespace osu.Game.Overlays
private partial class HoverableProgressBar : ProgressBar
{
public override bool HandleNonPositionalInput => IsHovered;
public HoverableProgressBar()
: base(true)
{

View File

@@ -4,7 +4,6 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework;

View File

@@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
LabelText = GraphicsSettingsStrings.FrameLimiter,
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync),
Keywords = new[] { @"fps" },
Keywords = new[] { @"fps", @"framerate" },
},
new SettingsEnumDropdown<ExecutionMode>
{
@@ -54,7 +54,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.ShowFPS,
Current = osuConfig.GetBindable<bool>(OsuSetting.ShowFpsDisplay)
Current = osuConfig.GetBindable<bool>(OsuSetting.ShowFpsDisplay),
Keywords = new[] { @"framerate", @"counter" },
},
};

View File

@@ -297,7 +297,7 @@ namespace osu.Game.Overlays.Settings.Sections
Height = 40,
RelativeSizeAxes = Axes.X,
MatchingFilter = true,
Text = "Save",
Text = WebCommonStrings.ButtonsSave,
}
}
};

View File

@@ -12,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
@@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Settings
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Copy version", MenuItemType.Standard, () => game?.CopyToClipboard(version))
new OsuMenuItem(SettingsStrings.CopyVersion, MenuItemType.Standard, () => game?.CopyToClipboard(version))
};
}
}

View File

@@ -95,7 +95,7 @@ namespace osu.Game.Overlays.SkinEditor
},
new SceneButton
{
Text = "Skin.ini(Null)",
Text = "Mania Note Editor(Testing)",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Action = () =>

View File

@@ -4,13 +4,13 @@
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Exposes a mod time range that can be updated from editor timeline UI.
/// All times are in milliseconds.
/// 用于链接循环时间范围的Mod接口。
/// 单位是ms。
/// </summary>
public interface ILoopTimeRangeMod
{
/// <summary>
/// Updates the time range represented by this mod.
/// 更新循环时间范围。
/// </summary>
/// <param name="startTime">Start time in milliseconds.</param>
/// <param name="endTime">End time in milliseconds.</param>

View File

@@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mods
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
// 供override的速率调整使用
public virtual double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp), typeof(ModAdaptiveSpeed), typeof(ModRateAdjust) };

View File

@@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Scoring
/// <summary>
/// mania特殊专用按键事件的未命中结果。
/// 禁止用在Judgement覆写上这不属于note返回的判定结果
/// TODO: 这是一个错误拼写为了突出这是一个临时解决方案。未来应当通过更好的方式实现可切换的空判机制并改为正确的Poor。
/// </summary>
[Description(@"Pool")]
[EnumMember(Value = "pool")]

View File

@@ -17,7 +17,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -186,13 +185,13 @@ namespace osu.Game.Screens.Edit.Components
}
else
{
// 默认范围:以当前活动光标为 A 起点,向后 8 个 1/4 节拍为 B 终点。
// 8 * (1/4 beat) = 2 beats.
// 默认范围:以当前活动光标为 A 起点,向后 设置 B 终点。
double currentTime = Math.Clamp(editorClock.CurrentTime, 0, editorClock.TrackLength);
double startTime = editorClock.GetSnappedTime(currentTime);
var timingPoint = editorClock.ControlPointInfo.TimingPointAt(startTime);
// 8 * (4/4 beat) = 8 beats.也就是8根白线。
double endTime = startTime + timingPoint.BeatLength * 8;
endTime = Math.Min(endTime, editorClock.TrackLength);
endTime = editorClock.GetSnappedTime(endTime);

View File

@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
if (!base.ReceivePositionalInputAt(screenSpacePos))
return false;
// Only allow interaction in the upper half of the timeline.
// 改为只在上半部分响应交互为了配合A-B Loop光标仅下半区的UI交互
var localPos = ToLocalSpace(screenSpacePos);
return localPos.Y < DrawHeight / 2;
}

View File

@@ -10,7 +10,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.LAsEzExtensions.Screens.Edit;
using osu.Game.Overlays;
@@ -227,8 +226,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
applied = true;
}
// If the user returns to song select without restarting, the mod overlay may already be alive.
// It only reacts to SelectedMods bindable value changes, so force a value update to propagate the changed settings.
// 如果用户在不重启的情况下返回歌曲选择Mod覆盖可能已经存在。
// 它只对 SelectedMods 绑定值的更改做出反应,因此强制值更新以传播更改的设置。
if (applied && mods is Bindable<IReadOnlyList<Mod>> writableMods)
writableMods.Value = writableMods.Value.ToArray();
}

View File

@@ -66,8 +66,8 @@ namespace osu.Game.Screens.Edit
this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
// The editor operates in raw beatmap time. Applying offsets here breaks seek/loop logic
// (FramedBeatmapClock seeks in raw time but reports CurrentTime with offsets applied).
// 编辑器使用谱面原始时间。若在此应用偏移会破坏寻迹/循环逻辑
// FramedBeatmapClock 在原始时间上进行寻迹,但报告的 CurrentTime 却应用了偏移)。
underlyingClock = new FramedBeatmapClock(applyOffsets: false, requireDecoupling: true);
AddInternal(underlyingClock);
@@ -85,8 +85,9 @@ namespace osu.Game.Screens.Edit
}
/// <summary>
/// Returns <paramref name="time"/> snapped to the currently configured editor beat divisor.
/// This matches the displayed editor grid granularity.
/// 这个方法返回根据当前编辑器节拍除数配置进行捕捉的<paramref name="time"/>。
/// 对其到显示的编辑器网格粒度。
/// TODO: 尽管如此,视觉上也依然难以发觉捕捉效果。最好在编辑器顶部的时间轴上同步显示光标位置。
/// </summary>
public double GetSnappedTime(double time)
{

View File

@@ -102,6 +102,7 @@ namespace osu.Game.Screens.Play
private bool isRestarting;
private bool skipExitTransition;
// 公开以供外部检查当前状态。
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
public IBindable<bool> LocalUserPlaying => localUserPlaying;

View File

@@ -330,7 +330,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
if (offsetChanged)
{
offsetText.AddText($"Beatmap offset was adjusted to {Current.Value.ToStandardFormattedString(1)} ms.", t => t.Font = OsuFont.Style.Caption1);
offsetText.AddText(BeatmapOffsetControlStrings.BeatmapOffsetWasAdjustedTo(Current.Value.ToStandardFormattedString(1)), t => t.Font = OsuFont.Style.Caption1);
offsetText.NewParagraph();
}
}

View File

@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Localisation;
namespace osu.Game.Screens.Ranking.Statistics
{
@@ -17,11 +19,19 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public AverageHitError(IEnumerable<HitEvent> hitEvents)
: base("Average Hit Error")
: base(RankingStatisticsStrings.AverageHitErrorTitle)
{
Value = hitEvents.CalculateAverageHitError();
}
protected override string DisplayValue(double? value) => value == null ? "(not available)" : $"{Math.Abs(value.Value):N2} ms {(value.Value < 0 ? "early" : "late")}";
protected override LocalisableString DisplayValue(double? value)
{
return value == null ? RankingStatisticsStrings.NotAvailable : getEarlyLateText(value.Value);
LocalisableString getEarlyLateText(double offset) =>
offset < 0
? RankingStatisticsStrings.Early(Math.Abs(offset))
: RankingStatisticsStrings.Late(Math.Abs(offset));
}
}
}

View File

@@ -1,8 +1,11 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -17,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <summary>
/// The text to display as the statistic's value.
/// </summary>
protected string Value
protected LocalisableString Value
{
set => valueText.Text = value;
}
@@ -41,9 +44,9 @@ namespace osu.Game.Screens.Ranking.Statistics
/// Creates a new simple statistic item.
/// </summary>
/// <param name="name">The name of the statistic.</param>
protected SimpleStatisticItem(string name)
protected SimpleStatisticItem(LocalisableString name)
{
Name = name;
Name = name.ToString();
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@@ -52,7 +55,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
nameText = new OsuSpriteText
{
Text = Name,
Text = name,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: StatisticItem.FONT_SIZE)
@@ -91,9 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics
/// Used to convert <see cref="Value"/> to a text representation.
/// Defaults to using <see cref="object.ToString"/>.
/// </summary>
protected virtual string DisplayValue(TValue value) => value!.ToString() ?? string.Empty;
protected virtual LocalisableString DisplayValue(TValue value)
{
if (value is IFormattable formattable)
return formattable.ToLocalisableString();
public SimpleStatisticItem(string name)
return value!.ToString() ?? string.Empty;
}
public SimpleStatisticItem(LocalisableString name)
: base(name)
{
}

View File

@@ -2,6 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Ranking.Statistics
@@ -16,11 +19,11 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public UnstableRate(IReadOnlyList<HitEvent> hitEvents)
: base("Unstable Rate")
: base(RankingStatisticsStrings.UnstableRateTitle)
{
Value = hitEvents.CalculateUnstableRate()?.Result;
}
protected override string DisplayValue(double? value) => value?.ToString(@"N2") ?? "(not available)";
protected override LocalisableString DisplayValue(double? value) => value?.ToLocalisableString(@"N2") ?? RankingStatisticsStrings.NotAvailable;
}
}

View File

@@ -532,6 +532,9 @@ namespace osu.Game.Screens.SelectV2
// If a group was selected that is not the one containing the selection, attempt to reselect it.
if (groupForReselection != null && grouping.GroupItems.TryGetValue(groupForReselection, out _))
setExpandedGroup(groupForReselection);
foreach (var item in Scroll.Panels.OfType<PanelBeatmapSet>().Where(p => p.Item != null))
updateVisibleBeatmaps((GroupedBeatmapSet)item.Item!.Model, item);
}
private void selectRecommendedDifficultyForBeatmapSet(GroupedBeatmapSet set)
@@ -959,13 +962,24 @@ namespace osu.Game.Screens.SelectV2
return beatmapPanelPool.Get();
case GroupedBeatmapSet:
return setPanelPool.Get();
case GroupedBeatmapSet groupedBeatmapSet:
var setPanel = setPanelPool.Get();
updateVisibleBeatmaps(groupedBeatmapSet, setPanel);
return setPanel;
}
throw new InvalidOperationException();
}
private void updateVisibleBeatmaps(GroupedBeatmapSet groupedBeatmapSet, PanelBeatmapSet setPanel)
{
HashSet<BeatmapInfo> visibleBeatmaps = [];
if (grouping.SetItems.TryGetValue(groupedBeatmapSet, out var visibleItems))
visibleBeatmaps = visibleItems.Where(i => i.Model is GroupedBeatmap).Select(i => ((GroupedBeatmap)i.Model).Beatmap).ToHashSet();
setPanel.VisibleBeatmaps.Value = visibleBeatmaps;
}
#endregion
#region Random selection handling

View File

@@ -33,6 +33,7 @@ namespace osu.Game.Screens.SelectV2
{
private const float corner_radius = 10;
// 这里存储所选的工作谱面以供全局访问例如在MOD中无法注入可以用这个来获取工作谱面
public static WorkingBeatmap? SelectedWorkingBeatmap;
[Resolved]

View File

@@ -255,6 +255,7 @@ namespace osu.Game.Screens.SelectV2
localUser.BindValueChanged(_ => updateCriteria());
localUserFavouriteBeatmapSets.BindCollectionChanged((_, _) => updateCriteria());
ScopedBeatmapSet.BindValueChanged(_ => updateCriteria(clearScopedSet: false));
csSelector.Current.BindValueChanged(_ => updateCriteria());
csSelector.EzKeyModeFilter.SelectionChanged += updateCriteria;
@@ -262,6 +263,12 @@ namespace osu.Game.Screens.SelectV2
updateCriteria();
}
private void updateCriteria()
{
currentCriteria = CreateCriteria();
CriteriaChanged?.Invoke(currentCriteria);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -278,6 +285,7 @@ namespace osu.Game.Screens.SelectV2
var criteria = new FilterCriteria
{
SelectedBeatmapSet = ScopedBeatmapSet.Value,
Sort = sortDropdown.Current.Value,
Group = groupDropdown.Current.Value,
AllowConvertedBeatmaps = showConvertedBeatmapsButton.Active.Value,
@@ -332,8 +340,16 @@ namespace osu.Game.Screens.SelectV2
}
}
private void updateCriteria()
private void updateCriteria(bool clearScopedSet = true)
{
if (clearScopedSet && ScopedBeatmapSet.Value != null)
{
ScopedBeatmapSet.Value = null;
// because `ScopedBeatmapSet` has a value change callback bound to it that calls `updateCriteria()` again,
// we can just do nothing other than clear it to avoid extra work and duplicated `CriteriaChanged` invocations
return;
}
currentCriteria = CreateCriteria();
CriteriaChanged?.Invoke(currentCriteria);
}

View File

@@ -212,6 +212,7 @@ namespace osu.Game.Screens.SelectV2
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.4f)
},
// TODO: 在此处增加XXY_SR
new OsuSpriteText
{
Text = "[Notes] ",
@@ -413,6 +414,7 @@ namespace osu.Game.Screens.SelectV2
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
// TODO: 当前机制不健全,显示内容可能不准确,怀疑被缓存影响而未能及时更新
if (iBeatmap != null)
{
string keyCountTextValue = EzBeatmapCalculator.GetScratch(iBeatmap, keyCount);

View File

@@ -0,0 +1,264 @@
// 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.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Screens.SelectV2
{
public partial class PanelBeatmapSet
{
public partial class SpreadDisplay : OsuAnimatedButton
{
public Bindable<BeatmapSetInfo?> BeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
public Bindable<HashSet<BeatmapInfo>?> VisibleBeatmaps { get; } = new Bindable<HashSet<BeatmapInfo>?>();
public BindableBool Expanded { get; } = new BindableBool();
protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200;
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private FillFlowContainer flow = null!;
private SpriteIcon icon = null!;
public SpreadDisplay()
{
AutoSizeAxes = Axes.X;
Height = 14;
Content.CornerRadius = 5;
}
[BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager)
{
Add(new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Padding = new MarginPadding { Horizontal = 5 },
Children = new Drawable[]
{
flow = new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1),
},
icon = new SpriteIcon
{
Size = new Vector2(12),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Eye,
Alpha = 0,
}
}
});
if (songSelect != null)
scopedBeatmapSet.BindTo(songSelect.ScopedBeatmapSet);
configManager.BindWith(OsuSetting.ShowConvertedBeatmaps, showConvertedBeatmaps);
}
protected override void LoadComplete()
{
base.LoadComplete();
BeatmapSet.BindValueChanged(_ => updateBeatmapSet());
VisibleBeatmaps.BindValueChanged(_ => updateBeatmapSet());
showConvertedBeatmaps.BindValueChanged(_ => updateBeatmapSet(), true);
Expanded.BindValueChanged(_ => updateEnabled());
scopedBeatmapSet.BindValueChanged(_ => updateEnabled(), true);
Enabled.BindValueChanged(_ => updateAppearance(), true);
FinishTransforms(true);
}
private void updateBeatmapSet()
{
if (BeatmapSet.Value == null)
{
this.FadeOut(transition_duration, Easing.OutQuint);
return;
}
flow.Clear();
const int max_difficulties_before_collapsing = 12;
var beatmaps = BeatmapSet.Value.Beatmaps
.Where(b => b.AllowGameplayWithRuleset(ruleset.Value, showConvertedBeatmaps.Value))
.ToList();
this.FadeTo(beatmaps.Count > 0 ? 1 : 0, transition_duration, Easing.OutQuint);
if (beatmaps.Count == 0)
return;
bool showVisible = VisibleBeatmaps.Value == null || VisibleBeatmaps.Value?.Count <= max_difficulties_before_collapsing;
bool showHidden = beatmaps.Count <= max_difficulties_before_collapsing;
var beatmapsByRuleset = beatmaps.GroupBy(beatmap => beatmap.Ruleset.OnlineID).OrderBy(group => group.Key);
foreach (var rulesetGrouping in beatmapsByRuleset)
{
int rulesetId = rulesetGrouping.Key;
var rulesetIcon = rulesets.GetRuleset(rulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
flow.Add(rulesetIcon.With(i =>
{
i.Size = new Vector2(14);
i.Anchor = i.Origin = Anchor.CentreLeft;
i.Margin = new MarginPadding { Left = flow.Count > 0 ? 9 : 0 };
}));
int overflowVisible = 0;
int overflowHidden = 0;
bool? lastBeatmapVisible = null;
foreach (var beatmap in rulesetGrouping.OrderBy(beatmap => beatmap.StarRating))
{
bool visible = VisibleBeatmaps.Value?.Contains(beatmap) != false;
if ((visible && showVisible) || (!visible && showHidden))
{
var circle = new Circle
{
Size = visible ? new Vector2(7, 12) : new Vector2(5, 10),
Alpha = visible ? 1 : 0.5f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = colours.ForStarDifficulty(beatmap.StarRating),
Margin = new MarginPadding { Left = lastBeatmapVisible != null && lastBeatmapVisible != visible ? 1 : 0 }
};
flow.Add(circle);
lastBeatmapVisible = visible;
}
else
{
if (visible)
overflowVisible++;
else
overflowHidden++;
}
}
if (overflowVisible > 0)
{
flow.Add(new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Style.Caption2,
Text = overflowVisible.ToLocalisableString(),
});
}
if (overflowHidden > 0)
{
flow.Add(new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Style.Caption2,
Text = LocalisableString.Interpolate($@"+{overflowHidden}"),
Alpha = 0.7f,
});
}
}
Action = () => scopedBeatmapSet.Value = BeatmapSet.Value;
updateEnabled();
}
private void updateEnabled()
{
Enabled.Value = Expanded.Value && scopedBeatmapSet.Value == null;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!Enabled.Value)
return false;
base.OnMouseDown(e);
return true;
}
protected override bool OnClick(ClickEvent e)
{
if (!Enabled.Value)
return false;
// this is a crude workaround with an issue with `OsuAnimatedButton` that isn't easily fixable.
// the issue is that when wanting to turn off the hover layer upon click, `HoverColour` can be set to a transparent colour,
// *but* this has to happen *before* `base.OnClick()`.
// this is because `base.OnClick()` uses `FlashColour()` to flash the button on click,
// but that `FlashColour()` call implicitly copies `hoverColour` *at the point of call* into the transform that ends the flash.
updateAppearance(false);
return base.OnClick(e);
}
protected override bool OnHover(HoverEvent e)
{
updateAppearance();
if (!Enabled.Value)
return false;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateAppearance();
if (!Enabled.Value)
return;
base.OnHoverLost(e);
}
private void updateAppearance(bool? isInteractable = null)
{
isInteractable ??= Enabled.Value && IsHovered;
HoverColour = isInteractable.Value ? Colour4.White.Opacity(0.1f) : Colour4.Transparent;
icon.FadeTo(isInteractable.Value ? 1 : 0, transition_duration, Easing.OutQuint);
}
}
}
}

View File

@@ -38,6 +38,8 @@ namespace osu.Game.Screens.SelectV2
{
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
public Bindable<HashSet<BeatmapInfo>?> VisibleBeatmaps { get; } = new Bindable<HashSet<BeatmapInfo>?>();
private Box chevronBackground = null!;
private PanelSetBackground setBackground = null!;
private ScheduledDelegate? scheduledBackgroundRetrieval;
@@ -47,7 +49,7 @@ namespace osu.Game.Screens.SelectV2
private Drawable chevronIcon = null!;
private PanelUpdateBeatmapButton updateButton = null!;
private BeatmapSetOnlineStatusPill statusPill = null!;
private DifficultySpectrumDisplay difficultiesDisplay = null!;
private SpreadDisplay spreadDisplay = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -151,10 +153,12 @@ namespace osu.Game.Screens.SelectV2
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 5f, Top = -2f },
},
difficultiesDisplay = new DifficultySpectrumDisplay
spreadDisplay = new SpreadDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Expanded = { BindTarget = Expanded },
VisibleBeatmaps = { BindTarget = VisibleBeatmaps },
},
},
}
@@ -200,7 +204,7 @@ namespace osu.Game.Screens.SelectV2
artistText.Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist);
updateButton.BeatmapSet = beatmapSet;
statusPill.Status = beatmapSet.Status;
difficultiesDisplay.BeatmapSet = beatmapSet;
spreadDisplay.BeatmapSet.Value = beatmapSet;
}
protected override void FreeAfterUse()
@@ -211,7 +215,7 @@ namespace osu.Game.Screens.SelectV2
scheduledBackgroundRetrieval = null;
setBackground.Beatmap = null;
updateButton.BeatmapSet = null;
difficultiesDisplay.BeatmapSet = null;
spreadDisplay.BeatmapSet.Value = null;
}
[Resolved]

View File

@@ -27,6 +27,8 @@ namespace osu.Game.Screens.SelectV2
public Bindable<BeatmapInfo?> Beatmap { get; } = new Bindable<BeatmapInfo?>();
public Bindable<StarDifficulty> StarDifficulty { get; } = new Bindable<StarDifficulty>();
protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();

View File

@@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1209.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1229.0" />
</ItemGroup>
</Project>

View File

@@ -9,7 +9,7 @@ using UIKit;
namespace osu.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
public class AppDelegate : GameApplicationDelegate, IUIApplicationDelegate
{
private UIInterfaceOrientationMask? defaultOrientationsMask;
private UIInterfaceOrientationMask? orientations;
@@ -41,7 +41,7 @@ namespace osu.iOS
protected override Framework.Game CreateGame() => new OsuGameIOS(this);
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
public UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
{
if (orientations != null)
return orientations.Value;