mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
同步更新
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1028.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1111.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[TestCase("mania-samples")]
|
||||
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
||||
[TestCase("slider-convert-samples")]
|
||||
[TestCase("spinner-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 1000.0,
|
||||
"Objects": [{
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 8000.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:5
|
||||
OverallDifficulty:5
|
||||
ApproachRate:5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,2,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,1000,8,4,8000,0:2:0:0:
|
||||
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConvertHitObjectCustomSampleBank()
|
||||
{
|
||||
const string beatmap_sample = "normal-hitwhistle2";
|
||||
const string user_skin_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(beatmap_sample, user_skin_sample);
|
||||
|
||||
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(beatmap_sample);
|
||||
AssertUserLookup(user_skin_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
NodeSamples =
|
||||
[
|
||||
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
|
||||
HitObject.Samples
|
||||
]
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private readonly Lazy<bool> hasKeyTexture;
|
||||
|
||||
private readonly ManiaBeatmap beatmap;
|
||||
private readonly bool isBeatmapConverted;
|
||||
|
||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
@@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
// layered hit sounds never play in mania
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
|
||||
return new SampleVirtual();
|
||||
|
||||
return base.GetSample(sampleInfo);
|
||||
|
||||
@@ -126,6 +126,22 @@ namespace osu.Game.Tests.Gameplay
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which specifies a specific sample file which doesn't exist (or isn't allowed to be looked up)
|
||||
/// falls back to a normal sample.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestFileSampleFallsBackToNormal()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(null, expected_sample);
|
||||
|
||||
CreateTestWithBeatmap("file-beatmap-sample.osu");
|
||||
|
||||
AssertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -157,6 +157,12 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
return ID.GetHashCode();
|
||||
}
|
||||
|
||||
public bool AudioEquals(BeatmapInfo? other) => other != null
|
||||
&& BeatmapSet != null
|
||||
&& other.BeatmapSet != null
|
||||
|
||||
@@ -544,7 +544,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (!banksOnly)
|
||||
{
|
||||
int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
|
||||
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
|
||||
string sampleFilename = samples.FirstOrDefault(s => s is ConvertHitObjectParser.FileHitSampleInfo)?.LookupNames.First() ?? string.Empty;
|
||||
int volume = samples.FirstOrDefault()?.Volume ?? 100;
|
||||
|
||||
// We want to ignore custom sample banks and volume when not encoding to the mania game mode,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -263,11 +264,11 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
Task.Run(() => collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
@@ -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.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -10,7 +11,7 @@ namespace osu.Game.Collections
|
||||
public class CollectionToggleMenuItem : ToggleMenuItem
|
||||
{
|
||||
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => Task.Run(() =>
|
||||
{
|
||||
collection.PerformWrite(c =>
|
||||
{
|
||||
@@ -19,7 +20,7 @@ namespace osu.Game.Collections
|
||||
else
|
||||
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
|
||||
});
|
||||
})
|
||||
}))
|
||||
{
|
||||
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
|
||||
}
|
||||
|
||||
@@ -785,32 +785,20 @@ namespace osu.Game.Graphics.Carousel
|
||||
// We are performing two important operations here:
|
||||
// - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions.
|
||||
// - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use.
|
||||
FindCarouselItemsForSelection(ref currentKeyboardSelection, ref currentSelection, carouselItems);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var item = carouselItems[i];
|
||||
|
||||
bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!);
|
||||
bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!);
|
||||
|
||||
// while we don't know the Y position of the item yet, as it's about to be updated,
|
||||
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
|
||||
// at the correct item to avoid redundant local equality checks.
|
||||
// the Y positions will be filled in after they're computed.
|
||||
if (isKeyboardSelection)
|
||||
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i);
|
||||
|
||||
if (isSelection)
|
||||
currentSelection = new Selection(currentSelection.Model, item, null, i);
|
||||
|
||||
updateItemYPosition(item, ref lastVisible, ref yPos);
|
||||
|
||||
if (isKeyboardSelection)
|
||||
currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
|
||||
|
||||
if (isSelection)
|
||||
currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
|
||||
}
|
||||
|
||||
if (currentKeyboardSelection.CarouselItem is CarouselItem currentKeyboardSelectionItem)
|
||||
currentKeyboardSelection = currentKeyboardSelection with { YPosition = currentKeyboardSelectionItem.CarouselYPosition + currentKeyboardSelectionItem.DrawHeight / 2 };
|
||||
|
||||
if (currentSelection.CarouselItem is CarouselItem currentSelectionItem)
|
||||
currentSelection = currentSelection with { YPosition = currentSelectionItem.CarouselYPosition + currentSelectionItem.DrawHeight / 2 };
|
||||
|
||||
// Update the total height of all items (to make the scroll container scrollable through the full height even though
|
||||
// most items are not displayed / loaded).
|
||||
Scroll.SetLayoutHeight(yPos + visibleHalfHeight);
|
||||
@@ -821,6 +809,27 @@ namespace osu.Game.Graphics.Carousel
|
||||
Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
|
||||
}
|
||||
|
||||
protected virtual void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
bool isKeyboardSelection = CheckModelEquality(item.Model, keyboardSelection.Model!);
|
||||
bool isSelection = CheckModelEquality(item.Model, selection.Model!);
|
||||
|
||||
// while we don't know the Y position of the item yet, as it's about to be updated,
|
||||
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
|
||||
// at the correct item to avoid redundant local equality checks.
|
||||
// the Y positions will be filled in after they're computed.
|
||||
if (isKeyboardSelection)
|
||||
keyboardSelection = new Selection(keyboardSelection.Model, item, null, i);
|
||||
|
||||
if (isSelection)
|
||||
selection = new Selection(selection.Model, item, null, i);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Display handling
|
||||
@@ -1081,7 +1090,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="CarouselItem">A related carousel item representation for the model. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
/// <param name="YPosition">The Y position of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
protected record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
|
||||
private record DisplayRange(int First, int Last)
|
||||
{
|
||||
|
||||
@@ -550,7 +550,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
}
|
||||
else
|
||||
{
|
||||
// Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
|
||||
soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
|
||||
}
|
||||
|
||||
@@ -680,14 +679,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
|
||||
}
|
||||
|
||||
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
|
||||
public class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
|
||||
{
|
||||
public readonly string Filename;
|
||||
|
||||
public FileHitSampleInfo(string filename, int volume)
|
||||
// Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin.
|
||||
// Note that this does not change the lookup names, as they are overridden locally.
|
||||
: base(string.Empty, customSampleBank: 1, volume: volume)
|
||||
: base(HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, customSampleBank: 1, volume: volume)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
@@ -696,7 +694,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
Filename,
|
||||
Path.ChangeExtension(Filename, null)
|
||||
};
|
||||
}.Concat(base.LookupNames);
|
||||
|
||||
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
|
||||
|
||||
@@ -328,7 +328,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||
{
|
||||
liveCollection.PerformWrite(c =>
|
||||
Task.Run(() => liveCollection.PerformWrite(c =>
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
{
|
||||
@@ -346,7 +346,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
|
||||
@@ -459,6 +459,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
// - Background user tag population runs and causes a realm update.
|
||||
// We don't display user tags so want to ignore this.
|
||||
bool equalForDisplayPurposes =
|
||||
// covers import-as-update flows, such as updating the beatmap with the latest online versions, or external editing inside editor
|
||||
oldBeatmap.ID == newBeatmap.ID &&
|
||||
// covers metadata changes
|
||||
oldBeatmap.Hash == newBeatmap.Hash &&
|
||||
// sanity check
|
||||
@@ -483,6 +485,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
|
||||
{
|
||||
if (keyboardSelection.Model != null && grouping.ItemMap.TryGetValue(keyboardSelection.Model, out var keyboardSelectionItem))
|
||||
keyboardSelection = keyboardSelection with { CarouselItem = keyboardSelectionItem.item, Index = keyboardSelectionItem.index };
|
||||
|
||||
if (selection.Model != null && grouping.ItemMap.TryGetValue(selection.Model, out var selectionItem))
|
||||
selection = selection with { CarouselItem = selectionItem.item, Index = selectionItem.index };
|
||||
}
|
||||
|
||||
protected override void HandleFilterCompleted()
|
||||
{
|
||||
base.HandleFilterCompleted();
|
||||
@@ -497,14 +508,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
// The filter might have changed the set of available groups, which means that the current selection may point to a stale group.
|
||||
// Check whether that is the case.
|
||||
bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0;
|
||||
|
||||
bool groupStillValid = false;
|
||||
|
||||
if (currentGroupedBeatmap?.Group != null)
|
||||
{
|
||||
groupStillValid = grouping.GroupItems.TryGetValue(currentGroupedBeatmap.Group, out var items)
|
||||
&& items.Any(i => CheckModelEquality(i.Model, currentGroupedBeatmap));
|
||||
}
|
||||
bool groupStillValid = currentGroupedBeatmap?.Group != null && grouping.ItemMap.ContainsKey(currentGroupedBeatmap);
|
||||
|
||||
if (groupingRemainsOff || groupStillValid)
|
||||
{
|
||||
@@ -515,6 +519,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
// If the group no longer exists (or the item no longer exists in the previous group), grab an arbitrary other instance of the beatmap under the first group encountered.
|
||||
var newSelection = GetCarouselItems()?.Select(i => i.Model).OfType<GroupedBeatmap>().FirstOrDefault(gb => gb.Beatmap.Equals(currentGroupedBeatmap.Beatmap));
|
||||
|
||||
// Only change the selection if we actually got a positive hit.
|
||||
// This is necessary so that selection isn't lost if the panel reappears later due to e.g. unapplying some filter criteria that made it disappear in the first place.
|
||||
if (newSelection != null)
|
||||
@@ -696,9 +701,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group))
|
||||
return;
|
||||
|
||||
var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group));
|
||||
if (groupItem != null)
|
||||
Activate(groupItem);
|
||||
if (grouping.ItemMap.TryGetValue(CurrentGroupedBeatmap.Group, out var groupItem))
|
||||
Activate(groupItem.item);
|
||||
}
|
||||
|
||||
protected override double? GetScrollTarget()
|
||||
@@ -709,9 +713,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
// attempt a fallback to other possibly expanded panels (set first, then group)
|
||||
if (target == null)
|
||||
{
|
||||
var items = GetCarouselItems();
|
||||
var targetItem = items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedBeatmapSet))
|
||||
?? items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedGroup));
|
||||
CarouselItem? targetItem = null;
|
||||
|
||||
if (ExpandedBeatmapSet != null && grouping.ItemMap.TryGetValue(ExpandedBeatmapSet, out var setItem))
|
||||
targetItem = setItem.item;
|
||||
|
||||
if (targetItem == null && ExpandedGroup != null && grouping.ItemMap.TryGetValue(ExpandedGroup, out var groupItem))
|
||||
targetItem = groupItem.item;
|
||||
|
||||
target = targetItem?.CarouselYPosition;
|
||||
}
|
||||
@@ -921,9 +929,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (x is BeatmapInfo beatmapInfoX && y is BeatmapInfo beatmapInfoY)
|
||||
return beatmapInfoX.Equals(beatmapInfoY);
|
||||
|
||||
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
|
||||
return groupX.Equals(groupY);
|
||||
|
||||
if (x is StarDifficultyGroupDefinition starX && y is StarDifficultyGroupDefinition starY)
|
||||
return starX.Equals(starY);
|
||||
|
||||
@@ -933,6 +938,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (x is RankedStatusGroupDefinition statusX && y is RankedStatusGroupDefinition statusY)
|
||||
return statusX.Equals(statusY);
|
||||
|
||||
// NOTE: this branch must be AFTER all branches that compare `GroupDefinition` subtypes!
|
||||
// this is an optimisation measure. any subclass of `GroupDefinition` will pass the `is GroupDefinition` check,
|
||||
// and testing a subclass of `GroupDefinition` against any other `GroupDefinition` (or subclass thereof)
|
||||
// will result in a casting cascade of `Equals(GroupDefinition) -> Equals(object) -> Equals(GroupDefinitionSubClass)`
|
||||
// (that last one only if the type check passes)
|
||||
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
|
||||
return groupX.Equals(groupY);
|
||||
|
||||
return base.CheckModelEquality(x, y);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public int BeatmapItemsCount { get; private set; }
|
||||
|
||||
public IDictionary<object, (CarouselItem item, int index)> ItemMap => itemMap;
|
||||
|
||||
/// <summary>
|
||||
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
|
||||
/// </summary>
|
||||
@@ -36,6 +38,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap;
|
||||
|
||||
private Dictionary<object, (CarouselItem, int)> itemMap = new Dictionary<object, (CarouselItem, int)>();
|
||||
private Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>> setMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>();
|
||||
private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
|
||||
|
||||
@@ -49,6 +52,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
// preallocate space for the new mappings using last known estimates
|
||||
var newItemMap = new Dictionary<object, (CarouselItem, int)>(itemMap.Count);
|
||||
var newSetMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(setMap.Count);
|
||||
var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count);
|
||||
|
||||
@@ -127,6 +131,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
newItems.Add(i);
|
||||
|
||||
newItemMap[i.Model] = (i, newItems.Count - 1);
|
||||
currentGroupItems?.Add(i);
|
||||
currentSetItems?.Add(i);
|
||||
|
||||
@@ -136,6 +141,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Interlocked.Exchange(ref itemMap, newItemMap);
|
||||
Interlocked.Exchange(ref setMap, newSetMap);
|
||||
Interlocked.Exchange(ref groupMap, newGroupMap);
|
||||
BeatmapItemsCount = displayedBeatmapsCount;
|
||||
@@ -209,7 +215,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupMode.Collections:
|
||||
{
|
||||
var collections = GetCollections();
|
||||
return getGroupsBy(b => defineGroupByCollection(b, collections), items);
|
||||
return defineGroupsByCollection(items, collections);
|
||||
}
|
||||
|
||||
case GroupMode.MyMaps:
|
||||
@@ -396,29 +402,56 @@ namespace osu.Game.Screens.SelectV2
|
||||
return new GroupDefinition(0, source).Yield();
|
||||
}
|
||||
|
||||
private IEnumerable<GroupDefinition> defineGroupByCollection(BeatmapInfo beatmap, List<BeatmapCollection> collections)
|
||||
private List<GroupMapping> defineGroupsByCollection(List<CarouselItem> carouselItems, List<BeatmapCollection> allCollections)
|
||||
{
|
||||
bool anyCollections = false;
|
||||
Dictionary<GroupDefinition, GroupMapping> groupMappings = new Dictionary<GroupDefinition, GroupMapping>();
|
||||
// this is a pre-built mapping of MD5s to a list of collections in which this MD5 is found in.
|
||||
// the reason to pre-build this is that `BeatmapCollection.BeatmapMD5Hashes` is a list and therefore a naive implementation would be slow,
|
||||
// particularly in edge cases where most beatmaps are in more than one collection.
|
||||
Dictionary<string, List<GroupDefinition>> md5ToCollectionsMap = new Dictionary<string, List<GroupDefinition>>();
|
||||
|
||||
for (int i = 0; i < collections.Count; i++)
|
||||
for (int i = 0; i < allCollections.Count; i++)
|
||||
{
|
||||
var collection = collections[i];
|
||||
var collection = allCollections[i];
|
||||
// NOTE: the ordering of the incoming collection list is significant and needs to be preserved.
|
||||
// the fallback to ordering by name cannot be relied on.
|
||||
// see xmldoc of `BeatmapCarousel.GetAllCollections()`.
|
||||
var groupDefinition = new GroupDefinition(i, collection.Name);
|
||||
groupMappings[groupDefinition] = new GroupMapping(groupDefinition, []);
|
||||
|
||||
if (collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash))
|
||||
foreach (string md5 in collection.BeatmapMD5Hashes)
|
||||
{
|
||||
// NOTE: the ordering of the incoming collection list is significant and needs to be preserved.
|
||||
// the fallback to ordering by name cannot be relied on.
|
||||
// see xmldoc of `BeatmapCarousel.GetAllCollections()`.
|
||||
yield return new GroupDefinition(i, collection.Name);
|
||||
if (!md5ToCollectionsMap.TryGetValue(md5, out var collections))
|
||||
md5ToCollectionsMap[md5] = collections = new List<GroupDefinition>();
|
||||
|
||||
anyCollections = true;
|
||||
collections.Add(groupDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
if (anyCollections)
|
||||
yield break;
|
||||
var notInCollection = new GroupDefinition(int.MaxValue, "Not in collection");
|
||||
groupMappings[notInCollection] = new GroupMapping(notInCollection, []);
|
||||
|
||||
yield return new GroupDefinition(int.MaxValue, "Not in collection");
|
||||
foreach (var item in carouselItems)
|
||||
{
|
||||
var beatmap = (BeatmapInfo)item.Model;
|
||||
|
||||
// as a side note, even reading the `MD5Hash` off a realm model is slow if done enough times,
|
||||
// so it definitely helps that thanks to the mapping it needs to only be retrieved once
|
||||
if (md5ToCollectionsMap.TryGetValue(beatmap.MD5Hash, out var collections))
|
||||
{
|
||||
foreach (var collection in collections)
|
||||
groupMappings[collection].ItemsInGroup.Add(item);
|
||||
}
|
||||
else
|
||||
groupMappings[notInCollection].ItemsInGroup.Add(item);
|
||||
}
|
||||
|
||||
return groupMappings.Values
|
||||
// safety against potentially empty eagerly-initialised groups
|
||||
// (could happen if user has a collection with MD5s of maps that aren't locally available)
|
||||
.Where(mapping => mapping.ItemsInGroup.Count > 0)
|
||||
.OrderBy(mapping => mapping.Group!.Order)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<GroupDefinition> defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -237,11 +238,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
Task.Run(() => collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@@ -296,7 +297,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||
{
|
||||
liveCollection.PerformWrite(c =>
|
||||
Task.Run(() => liveCollection.PerformWrite(c =>
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
{
|
||||
@@ -314,7 +315,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
|
||||
@@ -225,47 +225,26 @@ namespace osu.Game.Skinning
|
||||
|
||||
try
|
||||
{
|
||||
// 检查是否包含版本信息
|
||||
dynamic? tempObject = JsonConvert.DeserializeObject<dynamic>(jsonContent);
|
||||
string? versionString = tempObject?.Version?.ToString();
|
||||
|
||||
// 如果版本是 0.0.0.0 或者没有版本信息,按旧格式处理
|
||||
if (string.IsNullOrEmpty(versionString) || versionString == "0.0.0.0")
|
||||
{
|
||||
// 尝试按数组格式反序列化(旧格式)
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(jsonContent);
|
||||
|
||||
if (deserializedContent != null)
|
||||
{
|
||||
layout = new SkinLayoutInfo { Version = 0 };
|
||||
layout.Update(null, deserializedContent.ToArray());
|
||||
Logger.Log($"Loaded legacy format skin layout for {target} (version 0.0.0.0)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 新格式,直接按 SkinLayoutInfo 反序列化
|
||||
layout = JsonConvert.DeserializeObject<SkinLayoutInfo>(jsonContent);
|
||||
}
|
||||
// First attempt to deserialise using the new SkinLayoutInfo format
|
||||
layout = JsonConvert.DeserializeObject<SkinLayoutInfo>(jsonContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Failed to deserialize skin layout using new format for {target}: {ex.Message}", LoggingTarget.Runtime, LogLevel.Debug);
|
||||
layout = null; // 确保 layout 为 null,以便进入后续的fallback逻辑
|
||||
Logger.Log($"Deserialising skin layout to {nameof(SkinLayoutInfo)} failed. Falling back to {nameof(SerialisedDrawableInfo)}[].\nDetails: {ex}");
|
||||
}
|
||||
|
||||
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.
|
||||
if (layout == null)
|
||||
{
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(jsonContent);
|
||||
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(jsonContent);
|
||||
if (deserializedContent == null)
|
||||
return null;
|
||||
|
||||
layout = new SkinLayoutInfo { Version = 0 };
|
||||
layout.Update(null, deserializedContent.ToArray());
|
||||
layout = new SkinLayoutInfo { Version = 0 };
|
||||
layout.Update(null, deserializedContent.ToArray());
|
||||
|
||||
Logger.Log($"Ferrying {deserializedContent.Count()} components in {target} to global section of new {nameof(SkinLayoutInfo)} format");
|
||||
}
|
||||
Logger.Log($"Ferrying {deserializedContent.Count()} components in {target} to global section of new {nameof(SkinLayoutInfo)} format");
|
||||
}
|
||||
|
||||
for (int i = layout.Version + 1; i <= SkinLayoutInfo.LATEST_VERSION; i++)
|
||||
applyMigration(layout, target, i);
|
||||
|
||||
@@ -86,9 +86,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
|
||||
// populate ruleset for beatmap converters that require it to be present.
|
||||
var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID);
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID) ?? new RulesetInfo { OnlineID = currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID };
|
||||
|
||||
currentTestBeatmap.BeatmapInfo.Ruleset = ruleset;
|
||||
});
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1028.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1111.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user