同步更新

This commit is contained in:
LA
2025-11-15 22:54:10 +08:00
parent f98858b9c6
commit 3aab70ea97
23 changed files with 226 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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