1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-22 07:09:53 +08:00
Files
osu-lazer/osu.Game/Extensions/ModelExtensions.cs
T
Dean Herbert 81aaddca34 Change lazer's valid filename method to match stable
On revisiting the issue at hand, this honestly seems like the best way
forward. It also addresses my concerns that with the method we were
using, filenames could end up being half underscores.

The main reason for choosing to change the lazer end is that stable's
difficulty update process is based on sending the beatmap's filename to
the server. This means that if we use a proposed fix of checking online
ID, it will still mean beatmaps cannot be updated on stable (for all
users which have downloaded the beatmap) if a mapper updates once on
lazer.

Implementation inspired by:

- https://referencesource.microsoft.com/#mscorlib/system/io/path.cs,1144ad3c4eff3f24
- https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/Beatmaps/Beatmap.cs#L1575-L1590

Closes #33060.
2025-06-09 19:44:12 +09:00

195 lines
8.4 KiB
C#

// 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.IO;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
namespace osu.Game.Extensions
{
public static class ModelExtensions
{
/// <summary>
/// Get the relative path in osu! storage for this file.
/// </summary>
/// <param name="fileInfo">The file info.</param>
/// <returns>A relative file path.</returns>
public static string GetStoragePath(this IFileInfo fileInfo) => Path.Combine(fileInfo.Hash.Remove(1), fileInfo.Hash.Remove(2), fileInfo.Hash);
/// <summary>
/// Returns a user-facing string representing the <paramref name="model"/>.
/// </summary>
/// <remarks>
/// <para>
/// Non-interface types without special handling will fall back to <see cref="object.ToString()"/>.
/// </para>
/// <para>
/// Warning: This method is _purposefully_ not called <c>GetDisplayTitle()</c> like the others, because otherwise
/// extension method type inference rules cause this method to call itself and cause a stack overflow.
/// </para>
/// </remarks>
public static string GetDisplayString(this object? model)
{
string? result = null;
switch (model)
{
case IBeatmapSetInfo beatmapSetInfo:
result = beatmapSetInfo.Metadata.GetDisplayTitle();
break;
case IBeatmapInfo beatmapInfo:
result = beatmapInfo.GetDisplayTitle();
break;
case IBeatmapMetadataInfo metadataInfo:
result = metadataInfo.GetDisplayTitle();
break;
case IScoreInfo scoreInfo:
result = scoreInfo.GetDisplayTitle();
break;
case IRulesetInfo rulesetInfo:
result = rulesetInfo.Name;
break;
case IUser user:
result = user.Username;
break;
}
// fallback in case none of the above happens to match.
result ??= model?.ToString() ?? @"null";
return result;
}
/// <summary>
/// Check whether this <see cref="IRulesetInfo"/>'s online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania).
/// </summary>
public static bool IsLegacyRuleset(this IRulesetInfo ruleset) => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
/// <summary>
/// Check whether the online ID of two <see cref="IBeatmapSetInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IBeatmapSetInfo? instance, IBeatmapSetInfo? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="IBeatmapInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IBeatmapInfo? instance, IBeatmapInfo? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="IRulesetInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IRulesetInfo? instance, IRulesetInfo? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="APIUser"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this APIUser? instance, APIUser? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="IScoreInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>
/// Whether online IDs match.
/// Both <see cref="IHasOnlineID{T}.OnlineID"/> and <see cref="IScoreInfo.LegacyOnlineID"/> are checked, in that order.
/// If either instance is missing an online ID, this will return false.
/// </returns>
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other)
{
if (matchesOnlineID(instance, other))
return true;
if (instance == null || other == null)
return false;
if (instance.LegacyOnlineID < 0 || other.LegacyOnlineID < 0)
return false;
return instance.LegacyOnlineID.Equals(other.LegacyOnlineID);
}
private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
{
if (instance == null || other == null)
return false;
if (instance.OnlineID < 0 || other.OnlineID < 0)
return false;
return instance.OnlineID.Equals(other.OnlineID);
}
private static bool matchesOnlineID(this IHasOnlineID<int>? instance, IHasOnlineID<int>? other)
{
if (instance == null || other == null)
return false;
if (instance.OnlineID < 0 || other.OnlineID < 0)
return false;
return instance.OnlineID.Equals(other.OnlineID);
}
// intentionally chosen to match stable.
// see https://referencesource.microsoft.com/#mscorlib/system/io/path.cs,88
private static readonly char[] invalid_filename_chars =
{
'\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17,
(char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/'
};
/// <summary>
/// Create a valid filename which should work across all platforms.
/// </summary>
/// <remarks>
/// This function replaces all characters not included in a very pessimistic list which should be compatible
/// across all operating systems. We are using this in place of <see cref="Path.GetInvalidFileNameChars"/> as
/// that function does not have per-platform considerations (and is only made to work on windows).
/// </remarks>
public static string GetValidFilename(this string filename)
{
foreach (char c in invalid_filename_chars)
filename = filename.Replace(c.ToString(), string.Empty);
return filename;
}
public static bool RequiresSupporter(this BeatmapLeaderboardScope scope, bool filterMods)
{
switch (scope)
{
case BeatmapLeaderboardScope.Local:
return false;
case BeatmapLeaderboardScope.Country:
case BeatmapLeaderboardScope.Friend:
return true;
}
return filterMods;
}
}
}