// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Collections.Generic; using System.IO; using System.Linq; using JetBrains.Annotations; using osu.Game.IO; using osu.Game.Rulesets; namespace osu.Game.Beatmaps.Formats { public abstract class Decoder : Decoder where TOutput : new() { protected virtual TOutput CreateTemplateObject() => new TOutput(); public TOutput Decode(LineBufferedReader primaryStream, params LineBufferedReader[] otherStreams) { var output = CreateTemplateObject(); foreach (LineBufferedReader stream in otherStreams.Prepend(primaryStream)) ParseStreamInto(stream, output); return output; } protected abstract void ParseStreamInto(LineBufferedReader stream, TOutput output); } public abstract class Decoder { private static readonly Dictionary>> decoders = new Dictionary>>(); private static readonly Dictionary> fallback_decoders = new Dictionary>(); static Decoder() { LegacyBeatmapDecoder.Register(); JsonBeatmapDecoder.Register(); LegacyStoryboardDecoder.Register(); } /// /// Register dependencies for use with static decoder classes. /// /// A store containing all available rulesets (used by ). public static void RegisterDependencies([NotNull] RulesetStore rulesets) { LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets)); } /// /// Retrieves a to parse a . /// /// A stream pointing to the . public static Decoder GetDecoder(LineBufferedReader stream) where T : new() { ArgumentNullException.ThrowIfNull(stream); if (!decoders.TryGetValue(typeof(T), out var typedDecoders)) throw new IOException(@"Unknown decoder type"); // start off with the first line of the file string line = stream.PeekLine()?.Trim(); while (line != null && line.Length == 0) { // consume the previously peeked empty line and advance to the next one stream.ReadLine(); line = stream.PeekLine()?.Trim(); } if (line == null) throw new IOException("Unknown file format (no content)"); var decoder = typedDecoders.Where(d => line.StartsWith(d.Key, StringComparison.InvariantCulture)).Select(d => d.Value).FirstOrDefault(); // it's important the magic does NOT get consumed here, since sometimes it's part of the structure // (see JsonBeatmapDecoder - the magic string is the opening brace) // decoder implementations should therefore not die on receiving their own magic if (decoder != null) return (Decoder)decoder.Invoke(line); if (!fallback_decoders.TryGetValue(typeof(T), out var fallbackDecoder)) throw new IOException($"Unknown file format ({line})"); return (Decoder)fallbackDecoder.Invoke(); } /// /// Registers an instantiation function for a . /// /// A string in the file which triggers this decoder to be used. /// A function which constructs the given . protected static void AddDecoder(string magic, Func constructor) { if (!decoders.TryGetValue(typeof(T), out var typedDecoders)) decoders.Add(typeof(T), typedDecoders = new Dictionary>()); typedDecoders[magic] = constructor; } /// /// Registers a fallback decoder instantiation function. /// The fallback will be returned if the first non-empty line of the decoded stream does not match any known magic. /// Calling this method will overwrite any existing global fallback registration for type - use with caution. /// /// Type of object being decoded. /// A function that constructs the fallback. protected static void SetFallbackDecoder(Func constructor) { fallback_decoders[typeof(T)] = constructor; } } }