2021-09-30 14:40:41 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-04-13 17:19:50 +08:00
using System ;
2021-04-17 23:47:13 +08:00
using System.IO ;
2021-10-04 16:00:22 +08:00
using System.Linq ;
2021-09-30 14:40:41 +08:00
using JetBrains.Annotations ;
using osu.Framework.Audio ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Audio.Track ;
2022-08-02 18:50:57 +08:00
using osu.Framework.Graphics.Rendering ;
using osu.Framework.Graphics.Rendering.Dummy ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.Textures ;
2021-09-30 14:40:41 +08:00
using osu.Framework.IO.Stores ;
using osu.Framework.Lists ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Logging ;
2021-09-30 14:40:41 +08:00
using osu.Framework.Platform ;
2022-01-11 13:40:47 +08:00
using osu.Framework.Statistics ;
2020-10-16 13:39:02 +08:00
using osu.Framework.Testing ;
2018-04-13 17:19:50 +08:00
using osu.Game.Beatmaps.Formats ;
2021-12-14 13:19:43 +08:00
using osu.Game.Database ;
2019-09-10 06:43:30 +08:00
using osu.Game.IO ;
2018-04-13 17:19:50 +08:00
using osu.Game.Skinning ;
using osu.Game.Storyboards ;
namespace osu.Game.Beatmaps
{
2021-09-30 15:45:32 +08:00
public class WorkingBeatmapCache : IBeatmapResourceProvider , IWorkingBeatmapCache
2018-04-13 17:19:50 +08:00
{
2021-09-30 14:40:41 +08:00
private readonly WeakList < BeatmapManagerWorkingBeatmap > workingCache = new WeakList < BeatmapManagerWorkingBeatmap > ( ) ;
2022-04-14 16:32:47 +08:00
/// <summary>
/// Beatmap files may specify this filename to denote that they don't have an audio track.
/// </summary>
private const string virtual_track_filename = @"virtual" ;
2021-09-30 14:40:41 +08:00
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
/// </summary>
public readonly WorkingBeatmap DefaultBeatmap ;
private readonly AudioManager audioManager ;
private readonly IResourceStore < byte [ ] > resources ;
private readonly LargeTextureStore largeTextureStore ;
private readonly ITrackStore trackStore ;
private readonly IResourceStore < byte [ ] > files ;
[CanBeNull]
private readonly GameHost host ;
2022-04-14 16:32:47 +08:00
public WorkingBeatmapCache ( ITrackStore trackStore , AudioManager audioManager , IResourceStore < byte [ ] > resources , IResourceStore < byte [ ] > files , WorkingBeatmap defaultBeatmap = null ,
GameHost host = null )
2021-09-30 14:40:41 +08:00
{
DefaultBeatmap = defaultBeatmap ;
this . audioManager = audioManager ;
this . resources = resources ;
this . host = host ;
this . files = files ;
2022-08-02 18:50:57 +08:00
largeTextureStore = new LargeTextureStore ( host ? . Renderer ? ? new DummyRenderer ( ) , host ? . CreateTextureLoaderStore ( files ) ) ;
2021-11-09 16:27:07 +08:00
this . trackStore = trackStore ;
2021-09-30 14:40:41 +08:00
}
public void Invalidate ( BeatmapSetInfo info )
{
foreach ( var b in info . Beatmaps )
Invalidate ( b ) ;
}
public void Invalidate ( BeatmapInfo info )
{
lock ( workingCache )
{
2021-11-24 11:49:57 +08:00
var working = workingCache . FirstOrDefault ( w = > info . Equals ( w . BeatmapInfo ) ) ;
2021-10-14 12:58:36 +08:00
2021-09-30 14:40:41 +08:00
if ( working ! = null )
2021-10-14 12:58:36 +08:00
{
Logger . Log ( $"Invalidating working beatmap cache for {info}" ) ;
2021-09-30 14:40:41 +08:00
workingCache . Remove ( working ) ;
2022-06-20 18:48:46 +08:00
OnInvalidated ? . Invoke ( working ) ;
2021-10-14 12:58:36 +08:00
}
2021-09-30 14:40:41 +08:00
}
}
2022-06-20 18:48:46 +08:00
public event Action < WorkingBeatmap > OnInvalidated ;
2021-09-30 14:40:41 +08:00
public virtual WorkingBeatmap GetWorkingBeatmap ( BeatmapInfo beatmapInfo )
{
2022-01-11 13:40:47 +08:00
if ( beatmapInfo ? . BeatmapSet = = null )
return DefaultBeatmap ;
lock ( workingCache )
2021-09-30 14:40:41 +08:00
{
2022-01-11 13:40:47 +08:00
var working = workingCache . FirstOrDefault ( w = > beatmapInfo . Equals ( w . BeatmapInfo ) ) ;
2021-09-30 14:40:41 +08:00
2022-01-11 13:40:47 +08:00
if ( working ! = null )
return working ;
2022-01-07 13:17:22 +08:00
2022-01-11 20:36:34 +08:00
beatmapInfo = beatmapInfo . Detach ( ) ;
2022-01-11 13:40:47 +08:00
workingCache . Add ( working = new BeatmapManagerWorkingBeatmap ( beatmapInfo , this ) ) ;
2021-09-30 14:40:41 +08:00
2022-01-11 13:40:47 +08:00
// best effort; may be higher than expected.
GlobalStatistics . Get < int > ( "Beatmaps" , $"Cached {nameof(WorkingBeatmap)}s" ) . Value = workingCache . Count ( ) ;
return working ;
}
2021-09-30 14:40:41 +08:00
}
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider . LargeTextureStore = > largeTextureStore ;
ITrackStore IBeatmapResourceProvider . Tracks = > trackStore ;
2022-08-02 18:50:57 +08:00
IRenderer IStorageResourceProvider . Renderer = > host ? . Renderer ? ? new DummyRenderer ( ) ;
2021-09-30 14:40:41 +08:00
AudioManager IStorageResourceProvider . AudioManager = > audioManager ;
2022-01-24 18:59:58 +08:00
RealmAccess IStorageResourceProvider . RealmAccess = > null ;
2021-09-30 14:40:41 +08:00
IResourceStore < byte [ ] > IStorageResourceProvider . Files = > files ;
IResourceStore < byte [ ] > IStorageResourceProvider . Resources = > resources ;
IResourceStore < TextureUpload > IStorageResourceProvider . CreateTextureLoaderStore ( IResourceStore < byte [ ] > underlyingStore ) = > host ? . CreateTextureLoaderStore ( underlyingStore ) ;
#endregion
2020-10-16 13:39:02 +08:00
[ExcludeFromDynamicCompile]
2020-08-11 12:48:57 +08:00
private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
2018-04-13 17:19:50 +08:00
{
2020-12-22 11:06:10 +08:00
[NotNull]
2020-12-21 13:06:50 +08:00
private readonly IBeatmapResourceProvider resources ;
2018-04-13 17:19:50 +08:00
2020-12-22 11:06:10 +08:00
public BeatmapManagerWorkingBeatmap ( BeatmapInfo beatmapInfo , [ NotNull ] IBeatmapResourceProvider resources )
: base ( beatmapInfo , resources . AudioManager )
2018-04-13 17:19:50 +08:00
{
2020-12-21 13:06:50 +08:00
this . resources = resources ;
2018-04-13 17:19:50 +08:00
}
2018-04-19 19:44:38 +08:00
protected override IBeatmap GetBeatmap ( )
2018-04-13 17:19:50 +08:00
{
2020-08-24 18:38:05 +08:00
if ( BeatmapInfo . Path = = null )
2020-09-04 12:13:53 +08:00
return new Beatmap { BeatmapInfo = BeatmapInfo } ;
2020-08-24 18:38:05 +08:00
2018-04-13 17:19:50 +08:00
try
{
2022-07-07 13:29:15 +08:00
string fileStorePath = BeatmapSetInfo . GetPathForFile ( BeatmapInfo . Path ) ;
2022-10-13 17:20:49 +08:00
// TODO: check validity of file
2022-07-07 13:29:15 +08:00
var stream = GetStream ( fileStorePath ) ;
if ( stream = = null )
{
Logger . Log ( $"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})." , level : LogLevel . Error ) ;
return null ;
}
using ( var reader = new LineBufferedReader ( stream ) )
return Decoder . GetDecoder < Beatmap > ( reader ) . Decode ( reader ) ;
2018-04-13 17:19:50 +08:00
}
2020-02-10 16:25:11 +08:00
catch ( Exception e )
2018-04-13 17:19:50 +08:00
{
2020-02-10 16:25:11 +08:00
Logger . Error ( e , "Beatmap failed to load" ) ;
2018-04-13 17:19:50 +08:00
return null ;
}
}
protected override Texture GetBackground ( )
{
2021-11-04 13:11:21 +08:00
if ( string . IsNullOrEmpty ( Metadata ? . BackgroundFile ) )
2018-04-13 17:19:50 +08:00
return null ;
try
{
2022-07-07 13:29:15 +08:00
string fileStorePath = BeatmapSetInfo . GetPathForFile ( Metadata . BackgroundFile ) ;
var texture = resources . LargeTextureStore . Get ( fileStorePath ) ;
if ( texture = = null )
{
2022-07-17 20:20:50 +08:00
Logger . Log ( $"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath})." ) ;
2022-07-07 13:29:15 +08:00
return null ;
}
return texture ;
2018-04-13 17:19:50 +08:00
}
2020-02-10 16:25:11 +08:00
catch ( Exception e )
2018-04-13 17:19:50 +08:00
{
2020-02-10 16:25:11 +08:00
Logger . Error ( e , "Background failed to load" ) ;
2018-04-13 17:19:50 +08:00
return null ;
}
}
2019-08-31 04:19:34 +08:00
2020-08-07 21:31:41 +08:00
protected override Track GetBeatmapTrack ( )
2018-04-13 17:19:50 +08:00
{
2021-11-04 13:01:01 +08:00
if ( string . IsNullOrEmpty ( Metadata ? . AudioFile ) )
2020-09-01 14:48:13 +08:00
return null ;
2022-04-14 16:32:47 +08:00
if ( Metadata . AudioFile = = virtual_track_filename )
return null ;
2018-04-13 17:19:50 +08:00
try
{
2022-07-07 13:29:15 +08:00
string fileStorePath = BeatmapSetInfo . GetPathForFile ( Metadata . AudioFile ) ;
var track = resources . Tracks . Get ( fileStorePath ) ;
if ( track = = null )
{
Logger . Log ( $"Beatmap failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath})." , level : LogLevel . Error ) ;
return null ;
}
return track ;
2018-04-13 17:19:50 +08:00
}
2020-02-10 16:25:11 +08:00
catch ( Exception e )
2018-04-13 17:19:50 +08:00
{
2020-02-10 16:25:11 +08:00
Logger . Error ( e , "Track failed to load" ) ;
2018-06-27 15:02:49 +08:00
return null ;
2018-04-13 17:19:50 +08:00
}
}
2018-06-27 15:07:18 +08:00
protected override Waveform GetWaveform ( )
{
2021-11-04 13:01:01 +08:00
if ( string . IsNullOrEmpty ( Metadata ? . AudioFile ) )
2021-12-22 18:14:18 +08:00
return null ;
2020-09-01 14:48:13 +08:00
2022-04-14 16:32:47 +08:00
if ( Metadata . AudioFile = = virtual_track_filename )
return null ;
2018-06-27 15:07:18 +08:00
try
{
2022-07-07 13:29:15 +08:00
string fileStorePath = BeatmapSetInfo . GetPathForFile ( Metadata . AudioFile ) ;
var trackData = GetStream ( fileStorePath ) ;
if ( trackData = = null )
{
Logger . Log ( $"Beatmap waveform failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath})." , level : LogLevel . Error ) ;
return null ;
}
return new Waveform ( trackData ) ;
2018-06-27 15:07:18 +08:00
}
2020-02-10 16:25:11 +08:00
catch ( Exception e )
2018-06-27 15:07:18 +08:00
{
2020-02-10 16:25:11 +08:00
Logger . Error ( e , "Waveform failed to load" ) ;
2021-12-22 18:14:18 +08:00
return null ;
2018-06-27 15:07:18 +08:00
}
}
2018-04-13 17:19:50 +08:00
protected override Storyboard GetStoryboard ( )
{
Storyboard storyboard ;
2019-04-01 11:16:05 +08:00
2022-01-11 20:36:34 +08:00
if ( BeatmapInfo . Path = = null )
return new Storyboard ( ) ;
2018-04-13 17:19:50 +08:00
try
{
2022-07-07 13:29:15 +08:00
string fileStorePath = BeatmapSetInfo . GetPathForFile ( BeatmapInfo . Path ) ;
2022-07-07 13:33:17 +08:00
var beatmapFileStream = GetStream ( fileStorePath ) ;
2022-07-07 13:29:15 +08:00
2022-07-07 13:33:17 +08:00
if ( beatmapFileStream = = null )
2018-04-13 17:19:50 +08:00
{
2022-07-07 13:29:15 +08:00
Logger . Log ( $"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})" , level : LogLevel . Error ) ;
return null ;
}
2022-07-07 13:33:17 +08:00
using ( var reader = new LineBufferedReader ( beatmapFileStream ) )
2022-07-07 13:29:15 +08:00
{
var decoder = Decoder . GetDecoder < Storyboard > ( reader ) ;
2018-04-13 17:19:50 +08:00
2022-07-07 13:33:17 +08:00
Stream storyboardFileStream = null ;
2021-10-04 15:50:29 +08:00
2023-01-26 14:09:36 +08:00
string mainStoryboardFilename = getMainStoryboardFilename ( BeatmapSetInfo . Metadata ) ;
if ( BeatmapSetInfo ? . Files . FirstOrDefault ( f = > f . Filename . Equals ( mainStoryboardFilename , StringComparison . OrdinalIgnoreCase ) ) ? . Filename is string
storyboardFilename )
2018-04-13 17:19:50 +08:00
{
2022-07-07 13:33:17 +08:00
string storyboardFileStorePath = BeatmapSetInfo ? . GetPathForFile ( storyboardFilename ) ;
storyboardFileStream = GetStream ( storyboardFileStorePath ) ;
2022-07-07 13:29:15 +08:00
2022-07-07 13:33:17 +08:00
if ( storyboardFileStream = = null )
Logger . Log ( $"Storyboard failed to load (file {storyboardFilename} not found on disk at expected location {storyboardFileStorePath})" , level : LogLevel . Error ) ;
}
2022-07-07 13:29:15 +08:00
2022-07-07 13:33:17 +08:00
if ( storyboardFileStream ! = null )
{
// Stand-alone storyboard was found, so parse in addition to the beatmap's local storyboard.
using ( var secondaryReader = new LineBufferedReader ( storyboardFileStream ) )
2022-07-07 13:29:15 +08:00
storyboard = decoder . Decode ( reader , secondaryReader ) ;
2018-04-13 17:19:50 +08:00
}
2022-07-07 13:33:17 +08:00
else
storyboard = decoder . Decode ( reader ) ;
2018-04-13 17:19:50 +08:00
}
}
catch ( Exception e )
{
Logger . Error ( e , "Storyboard failed to load" ) ;
storyboard = new Storyboard ( ) ;
}
storyboard . BeatmapInfo = BeatmapInfo ;
return storyboard ;
}
2021-08-16 00:38:01 +08:00
protected internal override ISkin GetSkin ( )
2018-04-13 17:19:50 +08:00
{
try
{
2022-03-22 18:23:22 +08:00
return new LegacyBeatmapSkin ( BeatmapInfo , resources ) ;
2018-04-13 17:19:50 +08:00
}
catch ( Exception e )
{
Logger . Error ( e , "Skin failed to load" ) ;
2019-08-26 13:25:35 +08:00
return null ;
2018-04-13 17:19:50 +08:00
}
}
2021-04-17 23:47:13 +08:00
public override Stream GetStream ( string storagePath ) = > resources . Files . GetStream ( storagePath ) ;
2023-01-26 14:09:36 +08:00
private string getMainStoryboardFilename ( IBeatmapMetadataInfo metadata )
{
// Matches stable implementation, because it's probably simpler than trying to do anything else.
// This may need to be reconsidered after we begin storing storyboards in the new editor.
return windowsFilenameStrip (
( metadata . Artist . Length > 0 ? metadata . Artist + @" - " + metadata . Title : Path . GetFileNameWithoutExtension ( metadata . AudioFile ) )
+ ( metadata . Author . Username . Length > 0 ? @" (" + metadata . Author . Username + @")" : string . Empty )
+ @".osb" ) ;
string windowsFilenameStrip ( string entry )
{
// Inlined from Path.GetInvalidFilenameChars() to ensure the windows characters are used (to match stable).
char [ ] invalidCharacters =
{
' \ x00 ' , ' \ x01 ' , ' \ x02 ' , ' \ x03 ' , ' \ x04 ' , ' \ x05 ' , ' \ x06 ' , ' \ x07 ' ,
' \ x08 ' , ' \ x09 ' , ' \ x0A ' , ' \ x0B ' , ' \ x0C ' , ' \ x0D ' , ' \ x0E ' , ' \ x0F ' , ' \ x10 ' , ' \ x11 ' , ' \ x12 ' ,
' \ x13 ' , ' \ x14 ' , ' \ x15 ' , ' \ x16 ' , ' \ x17 ' , ' \ x18 ' , ' \ x19 ' , ' \ x1A ' , ' \ x1B ' , ' \ x1C ' , ' \ x1D ' ,
' \ x1E ' , ' \ x1F ' , ' \ x22 ' , ' \ x3C ' , ' \ x3E ' , ' \ x7C ' , ':' , '*' , '?' , '\\' , '/'
} ;
foreach ( char c in invalidCharacters )
entry = entry . Replace ( c . ToString ( ) , string . Empty ) ;
return entry ;
}
}
2018-04-13 17:19:50 +08:00
}
}
}