From c5b95c352d0d71600d20ae204fde58f3fa0ea17c Mon Sep 17 00:00:00 2001 From: laolarou Date: Wed, 18 Jan 2023 00:13:51 -0800 Subject: [PATCH] add helpers for game resources resolve now ProjBobcat can resolve following resources: - Game Mods - Game Resource Packs - Game Shader Packs See GameResourcesResolveHelper for more information --- .../Class/Helper/DirectoryHelper.cs | 17 +- .../Helper/GameResourcesResolveHelper.cs | 314 +++ .../Class/Helper/TOMLParser/TomlParser.cs | 2191 +++++++++++++++++ .../Helper/TOMLParser/TommyExtensions.cs | 228 ++ .../Class/Model/Fabric/FabricModInfoModel.cs | 52 + .../Model/GameResource/GameModInfoModel.cs | 30 + .../GameResource/GameResourcePackModel.cs | 15 + .../ResolvedInfo/GameModResolvedInfo.cs | 12 + .../GameResourcePackResolvedInfo.cs | 9 + .../GameShaderPackResolvedInfo.cs | 5 + ProjBobcat/ProjBobcat/ProjBobcat.csproj | 4 + 11 files changed, 2876 insertions(+), 1 deletion(-) create mode 100644 ProjBobcat/ProjBobcat/Class/Helper/GameResourcesResolveHelper.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TomlParser.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TommyExtensions.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Model/Fabric/FabricModInfoModel.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Model/GameResource/GameModInfoModel.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Model/GameResource/GameResourcePackModel.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameModResolvedInfo.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameResourcePackResolvedInfo.cs create mode 100644 ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameShaderPackResolvedInfo.cs diff --git a/ProjBobcat/ProjBobcat/Class/Helper/DirectoryHelper.cs b/ProjBobcat/ProjBobcat/Class/Helper/DirectoryHelper.cs index edfd5af6..206c473c 100644 --- a/ProjBobcat/ProjBobcat/Class/Helper/DirectoryHelper.cs +++ b/ProjBobcat/ProjBobcat/Class/Helper/DirectoryHelper.cs @@ -1,4 +1,6 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; namespace ProjBobcat.Class.Helper; @@ -25,4 +27,17 @@ public static void CleanDirectory(string path, bool deleteDirectory = false) foreach (var subDirectory in directory.GetDirectories()) subDirectory.Delete(true); } } + + /// + /// 获取一个目录下所有文件和文件夹 + /// + /// 需要获取的路径 + /// (路径,是否是文件夹) + public static IEnumerable<(string, bool)> EnumerateFilesAndDirectories(string path) + { + var files = Directory.EnumerateFiles(path).Select(p => (p, false)); + var dirs = Directory.EnumerateDirectories(path).Select(p => (p, true)); + + return files.Concat(dirs); + } } \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Helper/GameResourcesResolveHelper.cs b/ProjBobcat/ProjBobcat/Class/Helper/GameResourcesResolveHelper.cs new file mode 100644 index 00000000..3a9d1ac5 --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Helper/GameResourcesResolveHelper.cs @@ -0,0 +1,314 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using System.Threading; +using System; +using System.Collections.Immutable; +using ProjBobcat.Class.Model.GameResource; +using ProjBobcat.Class.Model.GameResource.ResolvedInfo; +using Newtonsoft.Json.Linq; +using ProjBobcat.Class.Helper.TOMLParser; +using ProjBobcat.Class.Model.Fabric; +using SharpCompress.Archives; + +namespace ProjBobcat.Class.Helper; + +public static class GameResourcesResolveHelper +{ + public static async IAsyncEnumerable ResolveModListAsync(IEnumerable files, [EnumeratorCancellation] CancellationToken ct) + { + foreach (var file in files) + { + if (ct.IsCancellationRequested) yield break; + + var ext = Path.GetExtension(file); + if (string.IsNullOrEmpty(ext) || + !(ext.Equals(".jar", StringComparison.OrdinalIgnoreCase) || + ext.Equals(".disabled", StringComparison.OrdinalIgnoreCase))) + continue; + + if (!ArchiveHelper.TryOpen(file, out var archive)) continue; + if (archive == null) continue; + + var modInfoEntry = + archive.Entries.FirstOrDefault(e => + e.Key.Equals("mcmod.info", StringComparison.OrdinalIgnoreCase)); + var fabricModInfoEntry = + archive.Entries.FirstOrDefault(e => + e.Key.Equals("fabric.mod.json", StringComparison.OrdinalIgnoreCase)); + var tomlInfoEntry = + archive.Entries.FirstOrDefault(e => + e.Key.Equals("META-INF/mods.toml", StringComparison.OrdinalIgnoreCase)); + + var isEnabled = ext.Equals(".jar", StringComparison.OrdinalIgnoreCase); + + async Task GetLegacyModInfo(IArchiveEntry entry) + { + await using var stream = entry.OpenEntryStream(); + using var sR = new StreamReader(stream); + using var parser = new TOMLParser.TOMLParser(sR); + var pResult = parser.TryParse(out var table, out _); + + if (!pResult) return null; + if (!table.HasKey("mods")) return null; + + var innerTable = table["mods"]; + + if (innerTable is not TomlArray arr) return null; + if (arr.ChildrenCount == 0) return null; + + var infoTable = arr.Children.First(); + + var title = infoTable.HasKey("modId") + ? infoTable["modId"]?.AsString + : Path.GetFileName(file); + var author = infoTable.HasKey("authors") + ? infoTable["authors"]?.AsString + : null; + var version = infoTable.HasKey("version") + ? infoTable["version"]?.AsString + : null; + + return new GameModResolvedInfo(author?.Value, file, null, title, version?.Value, "Forge", isEnabled); + } + + async Task GetNewModInfo(IArchiveEntry entry) + { + await using var stream = entry.OpenEntryStream(); + using var sR = new StreamReader(stream); + var content = await sR.ReadToEndAsync(); + var tempModel = JsonConvert.DeserializeObject(content); + + var model = new List(); + switch (tempModel) + { + case JObject jObj: + var obj = jObj.ToObject(); + + if (obj == null) break; + + model.Add(obj); + break; + case JArray jArr: + model = jArr.ToObject>() ?? new List(); + break; + } + + var authors = new HashSet(); + foreach (var author in model.Where(m => m.AuthorList != null).SelectMany(m => m.AuthorList!)) + authors.Add(author); + + var baseMod = model.FirstOrDefault(m => string.IsNullOrEmpty(m.Parent)); + + if (baseMod == null) + { + baseMod = model.First(); + model.RemoveAt(0); + } + else + { + model.Remove(baseMod); + } + + var authorStr = string.Join(',', authors); + var authorResult = string.IsNullOrEmpty(authorStr) ? null : authorStr; + var modList = model.Where(m => !string.IsNullOrEmpty(m.Name)).Select(m => m.Name!).ToImmutableList(); + var titleResult = string.IsNullOrEmpty(baseMod.Name) ? Path.GetFileName(file) : baseMod.Name; + + var displayModel = new GameModResolvedInfo(authorResult, file, modList, titleResult, baseMod.Version, + "Forge *", isEnabled); + + return displayModel; + } + + async Task GetFabricModInfo(IArchiveEntry entry) + { + await using var stream = entry.OpenEntryStream(); + using var sR = new StreamReader(stream); + var content = await sR.ReadToEndAsync(); + var tempModel = JsonConvert.DeserializeObject(content); + + var author = tempModel?.Authors?.Any() ?? false + ? string.Join(',', tempModel.Authors) + : null; + var modList = tempModel?.Depends?.Select(d => d.Key)?.ToImmutableList(); + var titleResult = string.IsNullOrEmpty(tempModel?.Id) ? Path.GetFileName(file) : tempModel.Id; + var versionResult = string.IsNullOrEmpty(tempModel?.Version) ? null : tempModel.Version; + + return new GameModResolvedInfo(author, file, modList, titleResult, versionResult, "Fabric", isEnabled); + } + + GameModResolvedInfo? result = null; + + if (modInfoEntry != null) + { + result = await GetNewModInfo(modInfoEntry); + goto ReturnResult; + } + + if (tomlInfoEntry != null) + { + var info = await GetLegacyModInfo(tomlInfoEntry); + + if (info == null) continue; + + result = info; + + goto ReturnResult; + } + + if (fabricModInfoEntry != null) + { + result = await GetFabricModInfo(fabricModInfoEntry); + } + + ReturnResult: + if (result != null) + yield return result; + } + } + + public static async IAsyncEnumerable ResolveResourcePackAsync(IEnumerable<(string, bool)> files, + [EnumeratorCancellation] CancellationToken ct) + { + async Task ResolveResPackFile(string file) + { + var ext = Path.GetExtension(file); + + if (!ext.Equals(".zip", StringComparison.OrdinalIgnoreCase)) return null; + if (!ArchiveHelper.TryOpen(file, out var archive)) return null; + if (archive == null) return null; + + var packIconEntry = + archive.Entries.FirstOrDefault(e => e.Key.Equals("pack.png", StringComparison.OrdinalIgnoreCase)); + var packInfoEntry = archive.Entries.FirstOrDefault(e => + e.Key.Equals("pack.mcmeta", StringComparison.OrdinalIgnoreCase)); + + var fileName = Path.GetFileName(file); + byte[]? imageBytes; + string? description = null; + var version = -1; + + if (packIconEntry != null) + { + await using var stream = packIconEntry.OpenEntryStream(); + await using var ms = new MemoryStream(); + await stream.CopyToAsync(ms, ct); + + imageBytes = ms.ToArray(); + } + else + { + return null; + } + + if (packInfoEntry != null) + { + await using var stream = packInfoEntry.OpenEntryStream(); + using var sR = new StreamReader(stream); + var content = await sR.ReadToEndAsync(); + var model = JsonConvert.DeserializeObject(content); + + description = model?.Pack?.Description; + version = model?.Pack?.PackFormat ?? -1; + } + + return new GameResourcePackResolvedInfo(fileName, description, version, imageBytes); + } + + async Task ResolveResPackDir(string dir) + { + var iconPath = Path.Combine(dir, "pack.png"); + var infoPath = Path.Combine(dir, "pack.mcmeta"); + + if (!File.Exists(iconPath)) return null; + + var fileName = dir.Split('\\').Last(); + var imageBytes = await File.ReadAllBytesAsync(iconPath, ct); + string? description = null; + var version = -1; + + if (File.Exists(infoPath)) + { + var content = await File.ReadAllTextAsync(infoPath, ct); + var model = JsonConvert.DeserializeObject(content); + + description = model?.Pack?.Description; + version = model?.Pack?.PackFormat ?? -1; + } + + return new GameResourcePackResolvedInfo(fileName, description, version, imageBytes); + } + + foreach (var (path, isDir) in files) + { + if (ct.IsCancellationRequested) yield break; + + if (!isDir) + { + var result = await ResolveResPackFile(path); + + if (result == null) continue; + + yield return result; + } + else + { + var result = await ResolveResPackDir(path); + + if (result == null) continue; + + yield return result; + } + } + } + + public static IEnumerable ResolveShaderPack(IEnumerable<(string, bool)> paths, CancellationToken ct) + { + GameShaderPackResolvedInfo? ResolveShaderPackFile(string file) + { + if (!ArchiveHelper.TryOpen(file, out var archive)) return null; + if (archive == null) return null; + if (!archive.Entries.Any(e => e.Key.StartsWith("shaders/", StringComparison.OrdinalIgnoreCase))) + return null; + + var model = new GameShaderPackResolvedInfo(Path.GetFileName(file), false); + + return model; + } + + GameShaderPackResolvedInfo? ResolveShaderPackDir(string dir) + { + var shaderPath = Path.Combine(dir, "shaders"); + + if (!Directory.Exists(shaderPath)) return null; + + return new GameShaderPackResolvedInfo(dir.Split('\\').Last(), true); + } + + foreach (var (path, isDir) in paths) + { + if (ct.IsCancellationRequested) yield break; + + if (!isDir) + { + var result = ResolveShaderPackFile(path); + + if (result == null) continue; + + yield return result; + } + else + { + var result = ResolveShaderPackDir(path); + + if (result == null) continue; + + yield return result; + } + } + } +} \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TomlParser.cs b/ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TomlParser.cs new file mode 100644 index 00000000..fb1f92f4 --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TomlParser.cs @@ -0,0 +1,2191 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +#pragma warning disable + +namespace ProjBobcat.Class.Helper.TOMLParser; + +#region TOML Nodes + +public abstract class TomlNode : IEnumerable +{ + public virtual bool HasValue { get; } = false; + public virtual bool IsArray { get; } = false; + public virtual bool IsTable { get; } = false; + public virtual bool IsString { get; } = false; + public virtual bool IsInteger { get; } = false; + public virtual bool IsFloat { get; } = false; + public virtual bool IsDateTime { get; } = false; + public virtual bool IsBoolean { get; } = false; + public virtual string Comment { get; set; } + public virtual int CollapseLevel { get; set; } + + public virtual TomlTable? AsTable => this as TomlTable; + public virtual TomlString? AsString => this as TomlString; + public virtual TomlInteger? AsInteger => this as TomlInteger; + public virtual TomlFloat? AsFloat => this as TomlFloat; + public virtual TomlBoolean? AsBoolean => this as TomlBoolean; + public virtual TomlDateTime? AsDateTime => this as TomlDateTime; + public virtual TomlArray? AsArray => this as TomlArray; + + public virtual int ChildrenCount => 0; + + public virtual TomlNode? this[string key] + { + get => null; + set { } + } + + public virtual TomlNode? this[int index] + { + get => null; + set { } + } + + public virtual IEnumerable Children + { + get { yield break; } + } + + public virtual IEnumerable Keys + { + get { yield break; } + } + + public IEnumerator GetEnumerator() + { + return Children.GetEnumerator(); + } + + public virtual bool TryGetNode(string key, out TomlNode? node) + { + node = null; + return false; + } + + public virtual bool HasKey(string key) + { + return false; + } + + public virtual bool HasItemAt(int index) + { + return false; + } + + public virtual void Add(string key, TomlNode node) + { + } + + public virtual void Add(TomlNode node) + { + } + + public virtual void Delete(TomlNode node) + { + } + + public virtual void Delete(string key) + { + } + + public virtual void Delete(int index) + { + } + + public virtual void AddRange(IEnumerable nodes) + { + foreach (var tomlNode in nodes) Add(tomlNode); + } + + public virtual void WriteTo(TextWriter tw, string name = null) + { + tw.WriteLine(ToInlineToml()); + } + + public virtual string ToInlineToml() + { + return ToString(); + } + + #region Native type to TOML cast + + public static implicit operator TomlNode(string value) + { + return new TomlString { Value = value }; + } + + public static implicit operator TomlNode(bool value) + { + return new TomlBoolean { Value = value }; + } + + public static implicit operator TomlNode(long value) + { + return new TomlInteger { Value = value }; + } + + public static implicit operator TomlNode(float value) + { + return new TomlFloat { Value = value }; + } + + public static implicit operator TomlNode(double value) + { + return new TomlFloat { Value = value }; + } + + public static implicit operator TomlNode(DateTime value) + { + return new TomlDateTime { Value = value }; + } + + public static implicit operator TomlNode(TomlNode[] nodes) + { + var result = new TomlArray(); + result.AddRange(nodes); + return result; + } + + #endregion + + #region TOML to native type cast + + public static implicit operator string(TomlNode value) + { + return value.ToString(); + } + + public static implicit operator int(TomlNode value) + { + return (int)value.AsInteger.Value; + } + + public static implicit operator long(TomlNode value) + { + return value.AsInteger.Value; + } + + public static implicit operator float(TomlNode value) + { + return (float)value.AsFloat.Value; + } + + public static implicit operator double(TomlNode value) + { + return value.AsFloat.Value; + } + + public static implicit operator bool(TomlNode value) + { + return value.AsBoolean.Value; + } + + public static implicit operator DateTime(TomlNode value) + { + return value.AsDateTime.Value; + } + + #endregion +} + +public class TomlString : TomlNode +{ + public override bool HasValue { get; } = true; + public override bool IsString { get; } = true; + public bool IsMultiline { get; set; } + public bool PreferLiteral { get; set; } + + public string Value { get; set; } + + public override string ToString() + { + return Value; + } + + public override string ToInlineToml() + { + if (Value.IndexOf(TomlSyntax.LITERAL_STRING_SYMBOL) != -1 && PreferLiteral) PreferLiteral = false; + var quotes = new string(PreferLiteral ? TomlSyntax.LITERAL_STRING_SYMBOL : TomlSyntax.BASIC_STRING_SYMBOL, + IsMultiline ? 3 : 1); + var result = PreferLiteral ? Value : Value.Escape(!IsMultiline); + return $"{quotes}{result}{quotes}"; + } +} + +public class TomlInteger : TomlNode +{ + public enum Base + { + Binary = 2, + Octal = 8, + Decimal = 10, + Hexadecimal = 16 + } + + public override bool IsInteger { get; } = true; + public override bool HasValue { get; } = true; + public Base IntegerBase { get; set; } = Base.Decimal; + + public long Value { get; set; } + + public override string ToString() + { + return Value.ToString(); + } + + public override string ToInlineToml() + { + return IntegerBase != Base.Decimal + ? $"0{TomlSyntax.BaseIdentifiers[(int)IntegerBase]}{Convert.ToString(Value, (int)IntegerBase)}" + : Value.ToString(CultureInfo.InvariantCulture); + } +} + +public class TomlFloat : TomlNode, IFormattable +{ + public override bool IsFloat { get; } = true; + public override bool HasValue { get; } = true; + + public double Value { get; set; } + + public string ToString(string? format, IFormatProvider? formatProvider) + { + return Value.ToString(format, formatProvider); + } + + public override string ToString() + { + return Value.ToString(CultureInfo.CurrentCulture); + } + + public string ToString(IFormatProvider formatProvider) + { + return Value.ToString(formatProvider); + } + + public override string ToInlineToml() + { + return Value switch + { + var v when double.IsNaN(v) => TomlSyntax.NAN_VALUE, + var v when double.IsPositiveInfinity(v) => TomlSyntax.INF_VALUE, + var v when double.IsPositiveInfinity(v) => TomlSyntax.NEG_INF_VALUE, + var v => v.ToString("G", CultureInfo.InvariantCulture) + }; + } +} + +public class TomlBoolean : TomlNode +{ + public override bool IsBoolean { get; } = true; + public override bool HasValue { get; } = true; + + public bool Value { get; set; } + + public override string ToString() + { + return Value.ToString(); + } + + public override string ToInlineToml() + { + return Value ? TomlSyntax.TRUE_VALUE : TomlSyntax.FALSE_VALUE; + } +} + +public class TomlDateTime : TomlNode, IFormattable +{ + public override bool IsDateTime { get; } = true; + public override bool HasValue { get; } = true; + public bool OnlyDate { get; set; } + public bool OnlyTime { get; set; } + public int SecondsPrecision { get; set; } + + public DateTime Value { get; set; } + + public string ToString(string? format, IFormatProvider? formatProvider) + { + return Value.ToString(format, formatProvider); + } + + public override string ToString() + { + return Value.ToString(CultureInfo.CurrentCulture); + } + + public string ToString(IFormatProvider formatProvider) + { + return Value.ToString(formatProvider); + } + + public override string ToInlineToml() + { + return Value switch + { + var v when OnlyDate => v.ToString(TomlSyntax.LocalDateFormat), + var v when OnlyTime => v.ToString(TomlSyntax.RFC3339LocalTimeFormats[SecondsPrecision]), + var v when v.Kind is DateTimeKind.Local => + v.ToString(TomlSyntax.RFC3339LocalDateTimeFormats[SecondsPrecision]), + var v => v.ToString(TomlSyntax.RFC3339Formats[SecondsPrecision]) + }; + } +} + +public class TomlArray : TomlNode +{ + private List values; + + public override bool HasValue { get; } = true; + public override bool IsArray { get; } = true; + public bool IsTableArray { get; set; } + public List RawArray => values ??= new List(); + + public override TomlNode this[int index] + { + get + { + if (index < RawArray.Count) return RawArray[index]; + var lazy = new TomlLazy(this); + this[index] = lazy; + return lazy; + } + set + { + if (index == RawArray.Count) + RawArray.Add(value); + else + RawArray[index] = value; + } + } + + public override int ChildrenCount => RawArray.Count; + + public override IEnumerable Children => RawArray.AsEnumerable(); + + public override void Add(TomlNode node) + { + RawArray.Add(node); + } + + public override void AddRange(IEnumerable nodes) + { + RawArray.AddRange(nodes); + } + + public override void Delete(TomlNode node) + { + RawArray.Remove(node); + } + + public override void Delete(int index) + { + RawArray.RemoveAt(index); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(TomlSyntax.ARRAY_START_SYMBOL); + + if (ChildrenCount != 0) + sb.Append(' ') + .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(RawArray.Select(n => n.ToInlineToml()))) + .Append(' '); + + sb.Append(TomlSyntax.ARRAY_END_SYMBOL); + return sb.ToString(); + } + + public override void WriteTo(TextWriter tw, string name = null) + { + // If it's a normal array, write it as usual + if (!IsTableArray) + { + tw.Write(ToInlineToml()); + return; + } + + tw.WriteLine(); + Comment?.AsComment(tw); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(name); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.WriteLine(); + + var first = true; + + foreach (var tomlNode in RawArray) + { + if (tomlNode is not TomlTable tbl) + throw new TomlFormatException("The array is marked as array table but contains non-table nodes!"); + + // Ensure it's parsed as a section + tbl.IsInline = false; + + if (!first) + { + tw.WriteLine(); + + Comment?.AsComment(tw); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(name); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.WriteLine(); + } + + first = false; + + // Don't pass section name because we already specified it + tbl.WriteTo(tw); + + tw.WriteLine(); + } + } +} + +public class TomlTable : TomlNode +{ + private Dictionary children; + + public override bool HasValue { get; } = false; + public override bool IsTable { get; } = true; + public bool IsInline { get; set; } + public Dictionary RawTable => children ??= new Dictionary(); + + public override TomlNode this[string key] + { + get + { + if (RawTable.TryGetValue(key, out var result)) return result; + var lazy = new TomlLazy(this); + RawTable[key] = lazy; + return lazy; + } + set => RawTable[key] = value; + } + + public override int ChildrenCount => RawTable.Count; + public override IEnumerable Children => RawTable.Select(kv => kv.Value); + public override IEnumerable Keys => RawTable.Select(kv => kv.Key); + + public override bool HasKey(string key) + { + return RawTable.ContainsKey(key); + } + + public override void Add(string key, TomlNode node) + { + RawTable.Add(key, node); + } + + public override bool TryGetNode(string key, out TomlNode node) + { + return RawTable.TryGetValue(key, out node); + } + + public override void Delete(TomlNode node) + { + RawTable.Remove(RawTable.First(kv => kv.Value == node).Key); + } + + public override void Delete(string key) + { + RawTable.Remove(key); + } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(TomlSyntax.INLINE_TABLE_START_SYMBOL); + + if (ChildrenCount != 0) + { + var collapsed = CollectCollapsedItems(out var nonCollapsible); + + sb.Append(' '); + sb.Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(RawTable.Where(n => nonCollapsible.Contains(n.Key)) + .Select(n => + $"{n.Key.AsKey()} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}"))); + + if (collapsed.Count != 0) + sb.Append(TomlSyntax.ITEM_SEPARATOR) + .Append(' ') + .Append($"{TomlSyntax.ITEM_SEPARATOR} ".Join(collapsed.Select(n => + $"{n.Key} {TomlSyntax.KEY_VALUE_SEPARATOR} {n.Value.ToInlineToml()}"))); + sb.Append(' '); + } + + sb.Append(TomlSyntax.INLINE_TABLE_END_SYMBOL); + return sb.ToString(); + } + + private Dictionary CollectCollapsedItems(out HashSet nonCollapsibleItems, + string prefix = "", + Dictionary nodes = null, + int level = 0) + { + nonCollapsibleItems = new HashSet(); + if (nodes == null) + { + nodes = new Dictionary(); + foreach (var keyValuePair in RawTable) + { + var node = keyValuePair.Value; + var key = keyValuePair.Key.AsKey(); + if (node is TomlTable tbl) + { + tbl.CollectCollapsedItems(out var nonCollapsible, $"{prefix}{key}.", nodes, level + 1); + if (nonCollapsible.Count != 0) + nonCollapsibleItems.Add(key); + } + else + { + nonCollapsibleItems.Add(key); + } + } + + return nodes; + } + + foreach (var keyValuePair in RawTable) + { + var node = keyValuePair.Value; + var key = keyValuePair.Key.AsKey(); + + if (node.CollapseLevel == level) + { + nodes.Add($"{prefix}{key}", node); + } + else if (node is TomlTable tbl) + { + tbl.CollectCollapsedItems(out var nonCollapsible, $"{prefix}{key}.", nodes, level + 1); + if (nonCollapsible.Count != 0) + nonCollapsibleItems.Add(key); + } + else + { + nonCollapsibleItems.Add(key); + } + } + + return nodes; + } + + public override void WriteTo(TextWriter tw, string name = null) + { + // The table is inline table + if (IsInline && name != null) + { + tw.Write(ToInlineToml()); + return; + } + + if (RawTable.All(n => n.Value.CollapseLevel != 0)) + return; + + var hasRealValues = !RawTable.All(n => n.Value is TomlTable { IsInline: false }); + + var collapsedItems = CollectCollapsedItems(out _); + + Comment?.AsComment(tw); + + if (name != null && (hasRealValues || collapsedItems.Count > 0)) + { + tw.Write(TomlSyntax.ARRAY_START_SYMBOL); + tw.Write(name); + tw.Write(TomlSyntax.ARRAY_END_SYMBOL); + tw.WriteLine(); + } + else if (Comment != null) // Add some spacing between the first node and the comment + { + tw.WriteLine(); + } + + var namePrefix = name == null ? "" : $"{name}."; + var first = true; + + var sectionableItems = new Dictionary(); + + foreach (var child in RawTable) + { + // If value should be parsed as section, separate if from the bunch + if (child.Value is TomlArray { IsTableArray: true } || child.Value is TomlTable { IsInline: false }) + { + sectionableItems.Add(child.Key, child.Value); + continue; + } + + // If the value is collapsed, it belongs to the parent + if (child.Value.CollapseLevel != 0) + continue; + + if (!first) tw.WriteLine(); + first = false; + + var key = child.Key.AsKey(); + child.Value.Comment?.AsComment(tw); + tw.Write(key); + tw.Write(' '); + tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR); + tw.Write(' '); + + child.Value.WriteTo(tw, $"{namePrefix}{key}"); + } + + foreach (var collapsedItem in collapsedItems) + { + if (collapsedItem.Value is TomlArray { IsTableArray: true } || + collapsedItem.Value is TomlTable { IsInline: false }) + throw new + TomlFormatException( + $"Value {collapsedItem.Key} cannot be defined as collapsed, because it is not an inline value!"); + + tw.WriteLine(); + var key = collapsedItem.Key; + collapsedItem.Value.Comment?.AsComment(tw); + tw.Write(key); + tw.Write(' '); + tw.Write(TomlSyntax.KEY_VALUE_SEPARATOR); + tw.Write(' '); + + collapsedItem.Value.WriteTo(tw, $"{namePrefix}{key}"); + } + + if (sectionableItems.Count == 0) + return; + + tw.WriteLine(); + tw.WriteLine(); + first = true; + foreach (var child in sectionableItems) + { + if (!first) tw.WriteLine(); + first = false; + + child.Value.WriteTo(tw, $"{namePrefix}{child.Key}"); + } + } +} + +internal class TomlLazy : TomlNode +{ + private readonly TomlNode parent; + private TomlNode replacement; + + public TomlLazy(TomlNode parent) + { + this.parent = parent; + } + + public override TomlNode this[int index] + { + get => Set()[index]; + set => Set()[index] = value; + } + + public override TomlNode this[string key] + { + get => Set()[key]; + set => Set()[key] = value; + } + + public override void Add(TomlNode node) + { + Set().Add(node); + } + + public override void Add(string key, TomlNode node) + { + Set().Add(key, node); + } + + public override void AddRange(IEnumerable nodes) + { + Set().AddRange(nodes); + } + + private TomlNode Set() where T : TomlNode, new() + { + if (replacement != null) return replacement; + + var newNode = new T + { + Comment = Comment + }; + + if (parent.IsTable) + { + var key = parent.Keys.FirstOrDefault(s => parent.TryGetNode(s, out var node) && node.Equals(this)); + if (key == null) return default(T); + + parent[key] = newNode; + } + else if (parent.IsArray) + { + var index = parent.Children.TakeWhile(child => child != this).Count(); + if (index == parent.ChildrenCount) return default(T); + parent[index] = newNode; + } + else + { + return default(T); + } + + replacement = newNode; + return newNode; + } +} + +#endregion + +#region Parser + +public class TOMLParser : IDisposable +{ + public enum ParseState + { + None, + KeyValuePair, + SkipToNextLine, + Table + } + + private readonly TextReader reader; + private ParseState currentState; + private int line, col; + private List syntaxErrors; + + public TOMLParser(TextReader reader) + { + this.reader = reader; + line = col = 0; + } + + public bool ForceASCII { get; set; } + + public void Dispose() + { + reader?.Dispose(); + } + + public TomlTable Parse() + { + syntaxErrors = new List(); + line = col = 0; + var rootNode = new TomlTable(); + var currentNode = rootNode; + currentState = ParseState.None; + var keyParts = new List(); + var arrayTable = false; + var latestComment = new StringBuilder(); + var firstComment = true; + + int currentChar; + while ((currentChar = reader.Peek()) >= 0) + { + var c = (char)currentChar; + + if (currentState == ParseState.None) + { + // Skip white space + if (TomlSyntax.IsWhiteSpace(c)) goto consume_character; + + if (TomlSyntax.IsNewLine(c)) + { + // Check if there are any comments and so far no items being declared + if (latestComment.Length != 0 && firstComment) + { + rootNode.Comment = latestComment.ToString().TrimEnd(); + latestComment.Length = 0; + firstComment = false; + } + + if (TomlSyntax.IsLineBreak(c)) + AdvanceLine(); + + goto consume_character; + } + + // Start of a comment; ignore until newline + if (c == TomlSyntax.COMMENT_SYMBOL) + { + // Consume the comment symbol and buffer the whole comment line + reader.Read(); + latestComment.AppendLine(reader.ReadLine()?.Trim()); + AdvanceLine(0); + continue; + } + + // Encountered a non-comment value. The comment must belong to it (ignore possible newlines)! + firstComment = false; + + if (c == TomlSyntax.TABLE_START_SYMBOL) + { + currentState = ParseState.Table; + goto consume_character; + } + + if (TomlSyntax.IsBareKey(c) || TomlSyntax.IsQuoted(c)) + { + currentState = ParseState.KeyValuePair; + } + else + { + AddError($"Unexpected character \"{c}\""); + continue; + } + } + + if (currentState == ParseState.KeyValuePair) + { + var keyValuePair = ReadKeyValuePair(keyParts); + + if (keyValuePair == null) + { + latestComment.Length = 0; + keyParts.Clear(); + + if (currentState != ParseState.None) + AddError("Failed to parse key-value pair!"); + continue; + } + + keyValuePair.Comment = latestComment.ToString().TrimEnd(); + var inserted = InsertNode(keyValuePair, currentNode, keyParts); + latestComment.Length = 0; + keyParts.Clear(); + if (inserted) + currentState = ParseState.SkipToNextLine; + continue; + } + + if (currentState == ParseState.Table) + { + if (keyParts.Count == 0) + { + // We have array table + if (c == TomlSyntax.TABLE_START_SYMBOL) + { + // Consume the character + ConsumeChar(); + arrayTable = true; + } + + if (!ReadKeyName(ref keyParts, TomlSyntax.TABLE_END_SYMBOL, true)) + { + keyParts.Clear(); + continue; + } + + if (keyParts.Count == 0) + { + AddError("Table name is emtpy."); + arrayTable = false; + latestComment.Length = 0; + keyParts.Clear(); + } + + continue; + } + + if (c == TomlSyntax.TABLE_END_SYMBOL) + { + if (arrayTable) + { + // Consume the ending bracket so we can peek the next character + ConsumeChar(); + var nextChar = reader.Peek(); + if (nextChar < 0 || (char)nextChar != TomlSyntax.TABLE_END_SYMBOL) + { + AddError($"Array table {".".Join(keyParts)} has only one closing bracket."); + keyParts.Clear(); + arrayTable = false; + latestComment.Length = 0; + continue; + } + } + + currentNode = CreateTable(rootNode, keyParts, arrayTable); + if (currentNode != null) + { + currentNode.IsInline = false; + currentNode.Comment = latestComment.ToString().TrimEnd(); + } + + keyParts.Clear(); + arrayTable = false; + latestComment.Length = 0; + + if (currentNode == null) + { + if (currentState != ParseState.None) + AddError("Error creating table array!"); + continue; + } + + currentState = ParseState.SkipToNextLine; + goto consume_character; + } + + if (keyParts.Count != 0) + { + AddError($"Unexpected character \"{c}\""); + keyParts.Clear(); + arrayTable = false; + latestComment.Length = 0; + } + } + + if (currentState == ParseState.SkipToNextLine) + { + if (TomlSyntax.IsWhiteSpace(c) || c == TomlSyntax.NEWLINE_CARRIAGE_RETURN_CHARACTER) + goto consume_character; + + if (c == TomlSyntax.COMMENT_SYMBOL || c == TomlSyntax.NEWLINE_CHARACTER) + { + currentState = ParseState.None; + AdvanceLine(); + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + col++; + reader.ReadLine(); + continue; + } + + goto consume_character; + } + + AddError($"Unexpected character \"{c}\" at the end of the line."); + } + + consume_character: + reader.Read(); + col++; + } + + if (currentState != ParseState.None && currentState != ParseState.SkipToNextLine) + AddError("Unexpected end of file!"); + + if (syntaxErrors.Count > 0) + throw new TomlParseException(rootNode, syntaxErrors); + + return rootNode; + } + + private bool AddError(string message) + { + syntaxErrors.Add(new TomlSyntaxException(message, currentState, line, col)); + // Skip the whole line in hope that it was only a single faulty value (and non-multiline one at that) + reader.ReadLine(); + AdvanceLine(0); + currentState = ParseState.None; + return false; + } + + private void AdvanceLine(int startCol = -1) + { + line++; + col = startCol; + } + + private int ConsumeChar() + { + col++; + return reader.Read(); + } + + #region Key-Value pair parsing + + /** + * Reads a single key-value pair. + * Assumes the cursor is at the first character that belong to the pair (including possible whitespace). + * Consumes all characters that belong to the key and the value (ignoring possible trailing whitespace at the end). + * + * Example: + * foo = "bar" ==> foo = "bar" + * ^ ^ + */ + private TomlNode ReadKeyValuePair(List keyParts) + { + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (TomlSyntax.IsQuoted(c) || TomlSyntax.IsBareKey(c)) + { + if (keyParts.Count != 0) + { + AddError("Encountered extra characters in key definition!"); + return null; + } + + if (!ReadKeyName(ref keyParts, TomlSyntax.KEY_VALUE_SEPARATOR)) + return null; + + continue; + } + + if (TomlSyntax.IsWhiteSpace(c)) + { + ConsumeChar(); + continue; + } + + if (c == TomlSyntax.KEY_VALUE_SEPARATOR) + { + ConsumeChar(); + return ReadValue(); + } + + AddError($"Unexpected character \"{c}\" in key name."); + return null; + } + + return null; + } + + /** + * Reads a single value. + * Assumes the cursor is at the first character that belongs to the value (including possible starting whitespace). + * Consumes all characters belonging to the value (ignoring possible trailing whitespace at the end). + * + * Example: + * "test" ==> "test" + * ^ ^ + */ + private TomlNode ReadValue(bool skipNewlines = false) + { + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (TomlSyntax.IsWhiteSpace(c)) + { + ConsumeChar(); + continue; + } + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + AddError("No value found!"); + return null; + } + + if (TomlSyntax.IsNewLine(c)) + { + if (skipNewlines) + { + reader.Read(); + AdvanceLine(0); + continue; + } + + AddError("Encountered a newline when expecting a value!"); + return null; + } + + if (TomlSyntax.IsQuoted(c)) + { + var isMultiline = IsTripleQuote(c, out var excess); + + // Error occurred in triple quote parsing + if (currentState == ParseState.None) + return null; + + var value = isMultiline + ? ReadQuotedValueMultiLine(c) + : ReadQuotedValueSingleLine(c, excess); + + return new TomlString + { + Value = value, + IsMultiline = isMultiline, + PreferLiteral = c == TomlSyntax.LITERAL_STRING_SYMBOL + }; + } + + return c switch + { + TomlSyntax.INLINE_TABLE_START_SYMBOL => ReadInlineTable(), + TomlSyntax.ARRAY_START_SYMBOL => ReadArray(), + _ => ReadTomlValue() + }; + } + + return null; + } + + /** + * Reads a single key name. + * Assumes the cursor is at the first character belonging to the key (with possible trailing whitespace if `skipWhitespace = true`). + * Consumes all the characters until the `until` character is met (but does not consume the character itself). + * + * Example 1: + * foo.bar ==> foo.bar (`skipWhitespace = false`, `until = ' '`) + * ^ ^ + * + * Example 2: + * [ foo . bar ] ==> [ foo . bar ] (`skipWhitespace = true`, `until = ']'`) + * ^ ^ + */ + private bool ReadKeyName(ref List parts, char until, bool skipWhitespace = false) + { + var buffer = new StringBuilder(); + var quoted = false; + var prevWasSpace = false; + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + // Reached the final character + if (c == until) break; + + if (TomlSyntax.IsWhiteSpace(c)) + if (skipWhitespace) + { + prevWasSpace = true; + goto consume_character; + } + else + { + break; + } + + if (buffer.Length == 0) prevWasSpace = false; + + if (c == TomlSyntax.SUBKEY_SEPARATOR) + { + if (buffer.Length == 0) + return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); + + parts.Add(buffer.ToString()); + buffer.Length = 0; + quoted = false; + prevWasSpace = false; + goto consume_character; + } + + if (prevWasSpace) + return AddError("Invalid spacing in key name"); + + if (TomlSyntax.IsQuoted(c)) + { + if (quoted) + + return AddError("Expected a subkey separator but got extra data instead!"); + + if (buffer.Length != 0) + return AddError("Encountered a quote in the middle of subkey name!"); + + // Consume the quote character and read the key name + col++; + buffer.Append(ReadQuotedValueSingleLine((char)reader.Read())); + quoted = true; + continue; + } + + if (TomlSyntax.IsBareKey(c)) + { + buffer.Append(c); + goto consume_character; + } + + // If we see an invalid symbol, let the next parser handle it + break; + + consume_character: + reader.Read(); + col++; + } + + if (buffer.Length == 0) + return AddError($"Found an extra subkey separator in {".".Join(parts)}..."); + + parts.Add(buffer.ToString()); + + return true; + } + + #endregion + + #region Non-string value parsing + + /** + * Reads the whole raw value until the first non-value character is encountered. + * Assumes the cursor start position at the first value character and consumes all characters that may be related to the value. + * Example: + * + * 1_0_0_0 ==> 1_0_0_0 + * ^ ^ + */ + private string ReadRawValue() + { + var result = new StringBuilder(); + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + if (c == TomlSyntax.COMMENT_SYMBOL || TomlSyntax.IsNewLine(c) || TomlSyntax.IsValueSeparator(c)) break; + result.Append(c); + ConsumeChar(); + } + + // Replace trim with manual space counting? + return result.ToString().Trim(); + } + + /** + * Reads and parses a non-string, non-composite TOML value. + * Assumes the cursor at the first character that is related to the value (with possible spaces). + * Consumes all the characters that are related to the value. + * + * Example + * 1_0_0_0 # This is a comment + * + * ==> 1_0_0_0 # This is a comment + * ^ ^ + */ + private TomlNode ReadTomlValue() + { + var value = ReadRawValue(); + TomlNode node = value switch + { + var v when TomlSyntax.IsBoolean(v) => bool.Parse(v), + var v when TomlSyntax.IsNaN(v) => double.NaN, + var v when TomlSyntax.IsPosInf(v) => double.PositiveInfinity, + var v when TomlSyntax.IsNegInf(v) => double.NegativeInfinity, + var v when TomlSyntax.IsInteger(v) => long.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), + CultureInfo.InvariantCulture), + var v when TomlSyntax.IsFloat(v) => double.Parse(value.RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), + CultureInfo.InvariantCulture), + var v when TomlSyntax.IsIntegerWithBase(v, out var numberBase) => new TomlInteger + { + Value = Convert.ToInt64(value[2..].RemoveAll(TomlSyntax.INT_NUMBER_SEPARATOR), numberBase), + IntegerBase = (TomlInteger.Base)numberBase + }, + _ => null + }; + if (node != null) return node; + + value = value.Replace("T", " "); + if (StringUtils.TryParseDateTime(value, + TomlSyntax.RFC3339LocalDateTimeFormats, + DateTimeStyles.AssumeLocal, + out var dateTimeResult, + out var precision)) + return new TomlDateTime + { + Value = dateTimeResult, + SecondsPrecision = precision + }; + + if (StringUtils.TryParseDateTime(value, + TomlSyntax.RFC3339Formats, + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, + out dateTimeResult, + out precision)) + return new TomlDateTime + { + Value = dateTimeResult, + SecondsPrecision = precision + }; + + + if (DateTime.TryParseExact(value, + TomlSyntax.LocalDateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out dateTimeResult)) + return new TomlDateTime + { + Value = dateTimeResult, + OnlyDate = true + }; + + if (StringUtils.TryParseDateTime(value, + TomlSyntax.RFC3339LocalTimeFormats, + DateTimeStyles.AssumeLocal, + out dateTimeResult, + out precision)) + return new TomlDateTime + { + Value = dateTimeResult, + OnlyTime = true, + SecondsPrecision = precision + }; + + AddError($"Value \"{value}\" is not a valid TOML 0.5.0 value!"); + return null; + } + + /** + * Reads an array value. + * Assumes the cursor is at the start of the array definition. Reads all character until the array closing bracket. + * + * Example: + * [1, 2, 3] ==> [1, 2, 3] + * ^ ^ + */ + private TomlArray ReadArray() + { + // Consume the start of array character + ConsumeChar(); + var result = new TomlArray(); + TomlNode currentValue = null; + + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (c == TomlSyntax.ARRAY_END_SYMBOL) + { + ConsumeChar(); + break; + } + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + reader.ReadLine(); + AdvanceLine(0); + continue; + } + + if (TomlSyntax.IsWhiteSpace(c) || TomlSyntax.IsNewLine(c)) + { + if (TomlSyntax.IsLineBreak(c)) + AdvanceLine(); + goto consume_character; + } + + if (c == TomlSyntax.ITEM_SEPARATOR) + { + if (currentValue == null) + { + AddError("Encountered multiple value separators in an array!"); + return null; + } + + result.Add(currentValue); + currentValue = null; + goto consume_character; + } + + currentValue = ReadValue(true); + if (currentValue == null) + { + if (currentState != ParseState.None) + AddError("Failed to determine and parse a value!"); + return null; + } + + if (result.ChildrenCount != 0 && result[0].GetType() != currentValue.GetType()) + { + AddError( + $"Arrays cannot have mixed types! Inferred type: {result[0].GetType().FullName}. Element type: {currentValue.GetType().FullName}"); + return null; + } + + continue; + consume_character: + ConsumeChar(); + } + + if (currentValue != null) result.Add(currentValue); + return result; + } + + /** + * Reads an inline table. + * Assumes the cursor is at the start of the table definition. Reads all character until the table closing bracket. + * + * Example: + * { test = "foo", value = 1 } ==> { test = "foo", value = 1 } + * ^ ^ + */ + private TomlNode ReadInlineTable() + { + ConsumeChar(); + var result = new TomlTable { IsInline = true }; + TomlNode currentValue = null; + var keyParts = new List(); + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (c == TomlSyntax.INLINE_TABLE_END_SYMBOL) + { + ConsumeChar(); + break; + } + + if (c == TomlSyntax.COMMENT_SYMBOL) + { + AddError("Incomplete inline table definition!"); + return null; + } + + if (TomlSyntax.IsNewLine(c)) + { + AddError("Inline tables are only allowed to be on single line"); + return null; + } + + if (TomlSyntax.IsWhiteSpace(c)) + goto consume_character; + + if (c == TomlSyntax.ITEM_SEPARATOR) + { + if (currentValue == null) + { + AddError("Encountered multiple value separators in inline table!"); + return null; + } + + if (!InsertNode(currentValue, result, keyParts)) + return null; + keyParts.Clear(); + currentValue = null; + goto consume_character; + } + + currentValue = ReadKeyValuePair(keyParts); + continue; + + consume_character: + ConsumeChar(); + } + + if (currentValue != null && !InsertNode(currentValue, result, keyParts)) + return null; + + return result; + } + + #endregion + + #region String parsing + + /** + * Checks if the string value a multiline string (i.e. a triple quoted string). + * Assumes the cursor is at the first quote character. Consumes the least amount of characters needed to determine if the string is multiline. + * + * If the result is false, returns the consumed character through the `excess` variable. + * + * Example 1: + * """test""" ==> """test""" + * ^ ^ + * + * Example 2: + * "test" ==> "test" (doesn't return the first quote) + * ^ ^ + * + * Example 3: + * "" ==> "" (returns the extra `"` through the `excess` variable) + * ^ ^ + */ + private bool IsTripleQuote(char quote, out char excess) + { + // Copypasta, but it's faster... + + int cur; + // Consume the first quote + ConsumeChar(); + if ((cur = reader.Peek()) < 0) + { + excess = '\0'; + return AddError("Unexpected end of file!"); + } + + if ((char)cur != quote) + { + excess = '\0'; + return false; + } + + // Consume the second quote + excess = (char)ConsumeChar(); + if ((cur = reader.Peek()) < 0 || (char)cur != quote) return false; + + // Consume the final quote + ConsumeChar(); + excess = '\0'; + return true; + } + + /** + * A convenience method to process a single character within a quote. + */ + private bool ProcessQuotedValueCharacter(char quote, + bool isNonLiteral, + char c, + StringBuilder sb, + ref bool escaped) + { + if (TomlSyntax.ShouldBeEscaped(c)) + return AddError($"The character U+{c:X8} must be escaped in a string!"); + + if (escaped) + { + sb.Append(c); + escaped = false; + return false; + } + + if (c == quote) return true; + if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL) + escaped = true; + if (c == TomlSyntax.NEWLINE_CHARACTER) + return AddError("Encountered newline in single line string!"); + + sb.Append(c); + return false; + } + + /** + * Reads a single-line string. + * Assumes the cursor is at the first character that belongs to the string. + * Consumes all characters that belong to the string (including the closing quote). + * + * Example: + * "test" ==> "test" + * ^ ^ + */ + private string ReadQuotedValueSingleLine(char quote, char initialData = '\0') + { + var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL; + var sb = new StringBuilder(); + var escaped = false; + + if (initialData != '\0') + { + var shouldReturn = + ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, sb, ref escaped); + if (currentState == ParseState.None) return null; + if (shouldReturn) return isNonLiteral ? sb.ToString().Unescape() : sb.ToString(); + } + + int cur; + while ((cur = reader.Read()) >= 0) + { + // Consume the character + col++; + var c = (char)cur; + if (ProcessQuotedValueCharacter(quote, isNonLiteral, c, sb, ref escaped)) + { + if (currentState == ParseState.None) return null; + break; + } + } + + return isNonLiteral ? sb.ToString().Unescape() : sb.ToString(); + } + + /** + * Reads a multiline string. + * Assumes the cursor is at the first character that belongs to the string. + * Consumes all characters that belong to the string and the three closing quotes. + * + * Example: + * """test""" ==> """test""" + * ^ ^ + */ + private string ReadQuotedValueMultiLine(char quote) + { + var isBasic = quote == TomlSyntax.BASIC_STRING_SYMBOL; + var sb = new StringBuilder(); + var escaped = false; + var skipWhitespace = false; + var quotesEncountered = 0; + var first = true; + int cur; + while ((cur = ConsumeChar()) >= 0) + { + var c = (char)cur; + if (TomlSyntax.ShouldBeEscaped(c)) + throw new Exception($"The character U+{c:X8} must be escaped!"); + // Trim the first newline + if (first && TomlSyntax.IsNewLine(c)) + { + if (TomlSyntax.IsLineBreak(c)) + first = false; + else + AdvanceLine(); + continue; + } + + first = false; + //TODO: Reuse ProcessQuotedValueCharacter + // Skip the current character if it is going to be escaped later + if (escaped) + { + sb.Append(c); + escaped = false; + continue; + } + + // If we are currently skipping empty spaces, skip + if (skipWhitespace) + { + if (TomlSyntax.IsEmptySpace(c)) + { + if (TomlSyntax.IsLineBreak(c)) + AdvanceLine(); + continue; + } + + skipWhitespace = false; + } + + // If we encounter an escape sequence... + if (isBasic && c == TomlSyntax.ESCAPE_SYMBOL) + { + var next = reader.Peek(); + if (next >= 0) + { + // ...and the next char is empty space, we must skip all whitespaces + if (TomlSyntax.IsEmptySpace((char)next)) + { + skipWhitespace = true; + continue; + } + + // ...and we have \", skip the character + if ((char)next == quote) escaped = true; + } + } + + // Count the consecutive quotes + if (c == quote) + quotesEncountered++; + else + quotesEncountered = 0; + + // If the are three quotes, count them as closing quotes + if (quotesEncountered == 3) break; + + sb.Append(c); + } + + // Remove last two quotes (third one wasn't included by default + sb.Length -= 2; + return isBasic ? sb.ToString().Unescape() : sb.ToString(); + } + + #endregion + + #region Node creation + + private bool InsertNode(TomlNode node, TomlNode root, IList path) + { + var latestNode = root; + if (path.Count > 1) + for (var index = 0; index < path.Count - 1; index++) + { + var subkey = path[index]; + if (latestNode.TryGetNode(subkey, out var currentNode)) + { + if (currentNode.HasValue) + return AddError($"The key {".".Join(path)} already has a value assigned to it!"); + } + else + { + currentNode = new TomlTable(); + latestNode[subkey] = currentNode; + } + + latestNode = currentNode; + } + + if (latestNode.HasKey(path[^1])) + return AddError($"The key {".".Join(path)} is already defined!"); + latestNode[path[^1]] = node; + node.CollapseLevel = path.Count - 1; + return true; + } + + private TomlTable CreateTable(TomlNode root, IList path, bool arrayTable) + { + if (path.Count == 0) return null; + var latestNode = root; + for (var index = 0; index < path.Count; index++) + { + var subkey = path[index]; + + if (latestNode.TryGetNode(subkey, out var node)) + { + if (node.IsArray && arrayTable) + { + var arr = (TomlArray)node; + + if (!arr.IsTableArray) + { + AddError($"The array {".".Join(path)} cannot be redefined as an array table!"); + return null; + } + + if (index == path.Count - 1) + { + latestNode = new TomlTable(); + arr.Add(latestNode); + break; + } + + latestNode = arr[arr.ChildrenCount - 1]; + continue; + } + + if (node.HasValue) + { + if (node is not TomlArray { IsTableArray: true } array) + { + AddError($"The key {".".Join(path)} has a value assigned to it!"); + return null; + } + + latestNode = array[array.ChildrenCount - 1]; + continue; + } + + if (index == path.Count - 1) + { + if (arrayTable && !node.IsArray) + { + AddError($"The table {".".Join(path)} cannot be redefined as an array table!"); + return null; + } + + if (node is TomlTable { IsInline: false }) + { + AddError($"The table {".".Join(path)} is defined multiple times!"); + return null; + } + } + } + else + { + if (index == path.Count - 1 && arrayTable) + { + var table = new TomlTable(); + var arr = new TomlArray + { + IsTableArray = true + }; + arr.Add(table); + latestNode[subkey] = arr; + latestNode = table; + break; + } + + node = new TomlTable + { + IsInline = true + }; + latestNode[subkey] = node; + } + + latestNode = node; + } + + var result = (TomlTable)latestNode; + return result; + } + + #endregion +} + +#endregion + +public static class TOML +{ + public static bool ForceASCII { get; set; } = false; + + public static TomlTable Parse(TextReader reader) + { + using var parser = new TOMLParser(reader) { ForceASCII = ForceASCII }; + return parser.Parse(); + } +} + +#region Exception Types + +public class TomlFormatException : Exception +{ + public TomlFormatException(string message) : base(message) + { + } +} + +public class TomlParseException : Exception +{ + public TomlParseException(TomlTable parsed, IEnumerable exceptions) : + base("TOML file contains format errors") + { + ParsedTable = parsed; + SyntaxErrors = exceptions; + } + + public TomlTable ParsedTable { get; } + + public IEnumerable SyntaxErrors { get; } +} + +public class TomlSyntaxException : Exception +{ + public TomlSyntaxException(string message, TOMLParser.ParseState state, int line, int col) : base(message) + { + ParseState = state; + Line = line; + Column = col; + } + + public TOMLParser.ParseState ParseState { get; } + + public int Line { get; } + + public int Column { get; } +} + +#endregion + +#region Parse utilities + +internal static partial class TomlSyntax +{ + #region Type Patterns + + public const string TRUE_VALUE = "true"; + public const string FALSE_VALUE = "false"; + public const string NAN_VALUE = "nan"; + public const string POS_NAN_VALUE = "+nan"; + public const string NEG_NAN_VALUE = "-nan"; + public const string INF_VALUE = "inf"; + public const string POS_INF_VALUE = "+inf"; + public const string NEG_INF_VALUE = "-inf"; + + public static bool IsBoolean(string s) + { + return s is TRUE_VALUE or FALSE_VALUE; + } + + public static bool IsPosInf(string s) + { + return s is INF_VALUE or POS_INF_VALUE; + } + + public static bool IsNegInf(string s) + { + return s == NEG_INF_VALUE; + } + + public static bool IsNaN(string s) + { + return s is NAN_VALUE or POS_NAN_VALUE or NEG_NAN_VALUE; + } + + public static bool IsInteger(string s) + { + return IntegerPattern.IsMatch(s); + } + + public static bool IsFloat(string s) + { + return FloatPattern.IsMatch(s); + } + + public static bool IsIntegerWithBase(string s, out int numberBase) + { + numberBase = 10; + var match = BasedIntegerPattern.Match(s); + if (!match.Success) return false; + IntegerBases.TryGetValue(match.Groups["base"].Value, out numberBase); + return true; + } + + + /** + * A helper dictionary to map TOML base codes into the radii. + */ + public static readonly Dictionary IntegerBases = new() + { + ["x"] = 16, + ["o"] = 8, + ["b"] = 2 + }; + + /** + * A helper dictionary to map non-decimal bases to their TOML identifiers + */ + public static readonly Dictionary BaseIdentifiers = new() + { + [2] = "b", + [8] = "o", + [16] = "x" + }; + + /** + * Valid date formats with timezone as per RFC3339. + */ + public static readonly string[] RFC3339Formats = + { + "yyyy'-'MM-dd HH':'mm':'ssK", "yyyy'-'MM-dd HH':'mm':'ss'.'fK", "yyyy'-'MM-dd HH':'mm':'ss'.'ffK", + "yyyy'-'MM-dd HH':'mm':'ss'.'fffK", "yyyy'-'MM-dd HH':'mm':'ss'.'ffffK", + "yyyy'-'MM-dd HH':'mm':'ss'.'fffffK", "yyyy'-'MM-dd HH':'mm':'ss'.'ffffffK", + "yyyy'-'MM-dd HH':'mm':'ss'.'fffffffK" + }; + + /** + * Valid date formats without timezone (assumes local) as per RFC3339. + */ + public static readonly string[] RFC3339LocalDateTimeFormats = + { + "yyyy'-'MM-dd HH':'mm':'ss", "yyyy'-'MM-dd HH':'mm':'ss'.'f", "yyyy'-'MM-dd HH':'mm':'ss'.'ff", + "yyyy'-'MM-dd HH':'mm':'ss'.'fff", "yyyy'-'MM-dd HH':'mm':'ss'.'ffff", + "yyyy'-'MM-dd HH':'mm':'ss'.'fffff", "yyyy'-'MM-dd HH':'mm':'ss'.'ffffff", + "yyyy'-'MM-dd HH':'mm':'ss'.'fffffff" + }; + + /** + * Valid full date format as per TOML spec. + */ + public const string LocalDateFormat = "yyyy'-'MM'-'dd"; + + /** + * Valid time formats as per TOML spec. + */ + public static readonly string[] RFC3339LocalTimeFormats = + { + "HH':'mm':'ss", "HH':'mm':'ss'.'f", "HH':'mm':'ss'.'ff", "HH':'mm':'ss'.'fff", "HH':'mm':'ss'.'ffff", + "HH':'mm':'ss'.'fffff", "HH':'mm':'ss'.'ffffff", "HH':'mm':'ss'.'fffffff" + }; + + #endregion + + #region Character definitions + + public const char ARRAY_END_SYMBOL = ']'; + public const char ITEM_SEPARATOR = ','; + public const char ARRAY_START_SYMBOL = '['; + public const char BASIC_STRING_SYMBOL = '\"'; + public const char COMMENT_SYMBOL = '#'; + public const char ESCAPE_SYMBOL = '\\'; + public const char KEY_VALUE_SEPARATOR = '='; + public const char NEWLINE_CARRIAGE_RETURN_CHARACTER = '\r'; + public const char NEWLINE_CHARACTER = '\n'; + public const char SUBKEY_SEPARATOR = '.'; + public const char TABLE_END_SYMBOL = ']'; + public const char TABLE_START_SYMBOL = '['; + public const char INLINE_TABLE_START_SYMBOL = '{'; + public const char INLINE_TABLE_END_SYMBOL = '}'; + public const char LITERAL_STRING_SYMBOL = '\''; + public const char INT_NUMBER_SEPARATOR = '_'; + + public static readonly char[] NewLineCharacters = { NEWLINE_CHARACTER, NEWLINE_CARRIAGE_RETURN_CHARACTER }; + + public static bool IsQuoted(char c) + { + return c is BASIC_STRING_SYMBOL or LITERAL_STRING_SYMBOL; + } + + public static bool IsWhiteSpace(char c) + { + return c is ' ' or '\t'; + } + + public static bool IsNewLine(char c) + { + return c is NEWLINE_CHARACTER or NEWLINE_CARRIAGE_RETURN_CHARACTER; + } + + public static bool IsLineBreak(char c) + { + return c == NEWLINE_CHARACTER; + } + + public static bool IsEmptySpace(char c) + { + return IsWhiteSpace(c) || IsNewLine(c); + } + + public static bool IsBareKey(char c) + { + return c is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9' or '_' or '-'; + } + + public static bool ShouldBeEscaped(char c) + { + return c is <= '\u001f' or '\u007f' && !IsNewLine(c); + } + + public static bool IsValueSeparator(char c) + { + return c is ITEM_SEPARATOR or ARRAY_END_SYMBOL or INLINE_TABLE_END_SYMBOL; + } + + /** + * A pattern to verify the integer value according to the TOML specification. + */ + static readonly Regex IntegerPattern = new ("^(\\+|-)?(?!_)(0|(?!0)(_?\\d)*)$", RegexOptions.Compiled); + + /** + * A pattern to verify the float value according to the TOML specification. + */ + static readonly Regex FloatPattern = new ("^(\\+|-)?(?!_)(0|(?!0)(_?\\d)+)(((e(\\+|-)?(?!_)(_?\\d)+)?)|(\\.(?!_)(_?\\d)+(e(\\+|-)?(?!_)(_?\\d)+)?))$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + + /** + * A pattern to verify a special 0x, 0o and 0b forms of an integer according to the TOML specification. + */ + static readonly Regex BasedIntegerPattern = new("^(\\+|-)?0(?x|b|o)(?!_)(_?[0-9A-F])*$", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + #endregion +} + +internal static class StringUtils +{ + public static string AsKey(this string key) + { + var quote = key.Any(c => !TomlSyntax.IsBareKey(c)); + return !quote ? key : $"{TomlSyntax.BASIC_STRING_SYMBOL}{key.Escape()}{TomlSyntax.BASIC_STRING_SYMBOL}"; + } + + public static string Join(this string self, IEnumerable subItems) + { + var sb = new StringBuilder(); + var first = true; + + foreach (var subItem in subItems) + { + if (!first) sb.Append(self); + first = false; + sb.Append(subItem); + } + + return sb.ToString(); + } + + public static bool TryParseDateTime(string s, + string[] formats, + DateTimeStyles styles, + out DateTime dateTime, + out int parsedFormat) + { + parsedFormat = 0; + dateTime = new DateTime(); + + for (var i = 0; i < formats.Length; i++) + { + var format = formats[i]; + if (!DateTime.TryParseExact(s, format, CultureInfo.InvariantCulture, styles, out dateTime)) continue; + parsedFormat = i; + return true; + } + + return false; + } + + public static void AsComment(this string self, TextWriter tw) + { + foreach (var line in self.Split(TomlSyntax.NEWLINE_CHARACTER)) + tw.WriteLine($"{TomlSyntax.COMMENT_SYMBOL} {line.Trim()}"); + } + + public static string RemoveAll(this string txt, char toRemove) + { + var sb = new StringBuilder(txt.Length); + foreach (var c in txt.Where(c => c != toRemove)) + sb.Append(c); + return sb.ToString(); + } + + public static string Escape(this string txt, bool escapeNewlines = true) + { + var stringBuilder = new StringBuilder(txt.Length + 2); + for (var i = 0; i < txt.Length; i++) + { + var c = txt[i]; + + static string CodePoint(string txt, ref int i, char c) + { + return char.IsSurrogatePair(txt, i) + ? $"\\U{char.ConvertToUtf32(txt, i++):X8}" + : $"\\u{c:X4}"; + } + + stringBuilder.Append(c switch + { + '\b' => @"\b", + '\t' => @"\t", + '\n' when escapeNewlines => @"\n", + '\f' => @"\f", + '\r' when escapeNewlines => @"\r", + '\\' => @"\\", + '\"' => @"\""", + _ when TomlSyntax.ShouldBeEscaped(c) || (TOML.ForceASCII && c > sbyte.MaxValue) => + CodePoint(txt, ref i, c), + _ => c + }); + } + + return stringBuilder.ToString(); + } + + public static string Unescape(this string txt) + { + if (string.IsNullOrEmpty(txt)) return txt; + var stringBuilder = new StringBuilder(txt.Length); + for (var i = 0; i < txt.Length;) + { + var num = txt.IndexOf('\\', i); + var next = num + 1; + if (num < 0 || num == txt.Length - 1) num = txt.Length; + stringBuilder.Append(txt, i, num - i); + if (num >= txt.Length) break; + var c = txt[next]; + + static string CodePoint(int next, string txt, ref int num, int size) + { + if (next + size >= txt.Length) throw new Exception("Undefined escape sequence!"); + num += size; + return char.ConvertFromUtf32(Convert.ToInt32(txt.Substring(next + 1, size), 16)); + } + + stringBuilder.Append(c switch + { + 'b' => "\b", + 't' => "\t", + 'n' => "\n", + 'f' => "\f", + 'r' => "\r", + '\'' => "\'", + '\"' => "\"", + '\\' => "\\", + 'u' => CodePoint(next, txt, ref num, 4), + 'U' => CodePoint(next, txt, ref num, 8), + _ => throw new Exception("Undefined escape sequence!") + }); + i = num + 2; + } + + return stringBuilder.ToString(); + } +} + +#endregion \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TommyExtensions.cs b/ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TommyExtensions.cs new file mode 100644 index 00000000..bbab6b1c --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Helper/TOMLParser/TommyExtensions.cs @@ -0,0 +1,228 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using System; + +namespace ProjBobcat.Class.Helper.TOMLParser; + +/// +/// Class of various extension methods for Tommy +/// +public static class TommyExtensions +{ + /// + /// Tries to parse TOML file. + /// + /// TOML parser to use. + /// Parsed root node. If parsing fails, the parsed document might not contain all values. + /// Parse errors, if any occur. + /// True, if parsing succeeded without errors. Otherwise false. + public static bool TryParse(this TOMLParser self, + out TomlNode rootNode, + out IEnumerable errors) + { + try + { + rootNode = self.Parse(); + errors = new List(); + return true; + } + catch (TomlParseException ex) + { + rootNode = ex.ParsedTable; + errors = ex.SyntaxErrors; + return false; + } + } + + + /// + /// Gets node given a fully-keyed path to it. + /// + /// Node to start search from. + /// Full path to the target node. The path must follow the TOML format. + /// Found node. If no matching node is found, returns null. + public static TomlNode FindNode(this TomlNode self, string path) + { + static bool ProcessQuotedValueCharacter(char quote, + bool isNonLiteral, + char c, + int next, + StringBuilder sb, + ref bool escaped) + { + if (TomlSyntax.ShouldBeEscaped(c)) + throw new Exception($"The character U+{c:X8} must be escaped in a string!"); + + if (escaped) + { + sb.Append(c); + escaped = false; + return false; + } + + if (c == quote) return true; + + if (isNonLiteral && c == TomlSyntax.ESCAPE_SYMBOL) + if (next >= 0 && (char)next == quote) + escaped = true; + + if (c == TomlSyntax.NEWLINE_CHARACTER) + throw new Exception("Encountered newline in single line string!"); + + sb.Append(c); + return false; + } + + string ReadQuotedValueSingleLine(char quote, TextReader reader, char initialData = '\0') + { + var isNonLiteral = quote == TomlSyntax.BASIC_STRING_SYMBOL; + var sb = new StringBuilder(); + + var escaped = false; + + if (initialData != '\0' && + ProcessQuotedValueCharacter(quote, isNonLiteral, initialData, reader.Peek(), sb, ref escaped)) + return isNonLiteral ? sb.ToString().Unescape() : sb.ToString(); + + int cur; + while ((cur = reader.Read()) >= 0) + { + var c = (char)cur; + if (ProcessQuotedValueCharacter(quote, isNonLiteral, c, reader.Peek(), sb, ref escaped)) break; + } + + return isNonLiteral ? sb.ToString().Unescape() : sb.ToString(); + } + + void ReadKeyName(TextReader reader, List parts) + { + var buffer = new StringBuilder(); + var quoted = false; + int cur; + while ((cur = reader.Peek()) >= 0) + { + var c = (char)cur; + + if (TomlSyntax.IsWhiteSpace(c)) + break; + + if (c == TomlSyntax.SUBKEY_SEPARATOR) + { + if (buffer.Length == 0) + throw new Exception($"Found an extra subkey separator in {".".Join(parts)}..."); + + parts.Add(buffer.ToString()); + buffer.Length = 0; + quoted = false; + goto consume_character; + } + + if (TomlSyntax.IsQuoted(c)) + { + if (quoted) + throw new Exception("Expected a subkey separator but got extra data instead!"); + if (buffer.Length != 0) + throw new Exception("Encountered a quote in the middle of subkey name!"); + + // Consume the quote character and read the key name + buffer.Append(ReadQuotedValueSingleLine((char)reader.Read(), reader)); + quoted = true; + continue; + } + + if (TomlSyntax.IsBareKey(c)) + { + buffer.Append(c); + goto consume_character; + } + + // If we see an invalid symbol, let the next parser handle it + throw new Exception($"Unexpected symbol {c}"); + + consume_character: + reader.Read(); + } + + if (buffer.Length == 0) + throw new Exception($"Found an extra subkey separator in {".".Join(parts)}..."); + + parts.Add(buffer.ToString()); + } + + var pathParts = new List(); + + using (var sr = new StringReader(path)) + { + ReadKeyName(sr, pathParts); + } + + var curNode = self; + + foreach (var pathPart in pathParts) + { + if (!curNode.TryGetNode(pathPart, out var node)) + return null; + curNode = node; + } + + return curNode; + } + + /// + /// Merges the current TOML node with another node. Useful for default values. + /// + /// Node to merge into. + /// Node to merge. + /// + /// If true, will also merge values present in the other node that are not present in this + /// node. + /// + /// The node that the other node was merged into. + public static TomlNode MergeWith(this TomlNode self, TomlNode with, bool mergeNewValues = false) + { + switch (self) + { + case TomlTable tbl when with is TomlTable withTbl: + { + foreach (var keyValuePair in withTbl.RawTable) + if (tbl.TryGetNode(keyValuePair.Key, out var node)) + node.MergeWith(keyValuePair.Value, mergeNewValues); + else if (mergeNewValues) + tbl[keyValuePair.Key] = node; + } + break; + case TomlArray arr when with is TomlArray withArr: + { + if (arr.ChildrenCount != 0 && + withArr.ChildrenCount != 0 && + arr[0].GetType() != withArr[0].GetType()) + return self; + + for (var i = 0; i < withArr.RawArray.Count; i++) + if (i < arr.RawArray.Count) + arr.RawArray[i].MergeWith(withArr.RawArray[i], mergeNewValues); + else + arr.RawArray.Add(withArr.RawArray[i]); + } + break; + case TomlBoolean bl when with is TomlBoolean withBl: + bl.Value = withBl.Value; + break; + case TomlDateTime dt when with is TomlDateTime withDt: + dt.Value = withDt.Value; + break; + case TomlFloat fl when with is TomlFloat withFl: + fl.Value = withFl.Value; + break; + case TomlInteger tint when with is TomlInteger withTint: + tint.Value = withTint.Value; + break; + case TomlString tstr when with is TomlString withTStr: + tstr.Value = withTStr.Value; + break; + } + + return self; + } +} \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Model/Fabric/FabricModInfoModel.cs b/ProjBobcat/ProjBobcat/Class/Model/Fabric/FabricModInfoModel.cs new file mode 100644 index 00000000..62f89391 --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Model/Fabric/FabricModInfoModel.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace ProjBobcat.Class.Model.Fabric; + +public class FabricFileInfo +{ + [JsonProperty("file")] public string File { get; set; } +} + +public class ModUpdater +{ + [JsonProperty("strategy")] public string Strategy { get; set; } + + [JsonProperty("url")] public string Url { get; set; } +} + +public class Custom +{ + [JsonProperty("modUpdater")] public ModUpdater ModUpdater { get; set; } +} + +public class FabricModInfoModel +{ + [JsonProperty("schemaVersion")] public int SchemaVersion { get; set; } + + [JsonProperty("id")] public string Id { get; set; } + + [JsonProperty("version")] public string Version { get; set; } + + [JsonProperty("environment")] public string Environment { get; set; } + + [JsonProperty("entrypoints")] public Dictionary> Entrypoints { get; set; } + + [JsonProperty("custom")] public Custom Custom { get; set; } + + [JsonProperty("depends")] public Dictionary Depends { get; set; } + + [JsonProperty("recommends")] public Dictionary Recommends { get; set; } + + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("description")] public string Description { get; set; } + + [JsonProperty("icon")] public string Icon { get; set; } + + [JsonProperty("authors")] public List Authors { get; set; } + + [JsonProperty("contacts")] public Dictionary Contacts { get; set; } + + [JsonProperty("jars")] public List Jars { get; set; } +} \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Model/GameResource/GameModInfoModel.cs b/ProjBobcat/ProjBobcat/Class/Model/GameResource/GameModInfoModel.cs new file mode 100644 index 00000000..2b8dca8a --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Model/GameResource/GameModInfoModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace ProjBobcat.Class.Model.GameResource; + +public class GameModInfoModel +{ + public string? ModId { get; set; } + + public string? Name { get; set; } + + public string? Version { get; set; } + + public string? McVersion { get; set; } + + public string? Description { get; set; } + + public string? Credits { get; set; } + + public string? Url { get; set; } + + public string? UpdateUrl { get; set; } + + public List? AuthorList { get; set; } + + public string? Parent { get; set; } + + public List? Screenshots { get; set; } + + public List? Dependencies { get; set; } +} \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Model/GameResource/GameResourcePackModel.cs b/ProjBobcat/ProjBobcat/Class/Model/GameResource/GameResourcePackModel.cs new file mode 100644 index 00000000..00c28f36 --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Model/GameResource/GameResourcePackModel.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; + +namespace ProjBobcat.Class.Model.GameResource; + +public class Pack +{ + [JsonProperty("pack_format")] public int PackFormat { get; set; } + + [JsonProperty("description")] public string? Description { get; set; } +} + +public class GameResourcePackModel +{ + [JsonProperty("pack")] public Pack? Pack { get; set; } +} \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameModResolvedInfo.cs b/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameModResolvedInfo.cs new file mode 100644 index 00000000..1629a24e --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameModResolvedInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace ProjBobcat.Class.Model.GameResource.ResolvedInfo; + +public record GameModResolvedInfo( + string? Author, + string FilePath, + IImmutableList? ModList, + string? Title, + string? Version, + string? ModType, + bool IsEnabled); \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameResourcePackResolvedInfo.cs b/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameResourcePackResolvedInfo.cs new file mode 100644 index 00000000..37dc1a10 --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameResourcePackResolvedInfo.cs @@ -0,0 +1,9 @@ +using System.Collections.Immutable; + +namespace ProjBobcat.Class.Model.GameResource.ResolvedInfo; + +public record GameResourcePackResolvedInfo( + string FileName, + string? Description, + int Version, + byte[]? IconBytes); \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameShaderPackResolvedInfo.cs b/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameShaderPackResolvedInfo.cs new file mode 100644 index 00000000..2a376ebb --- /dev/null +++ b/ProjBobcat/ProjBobcat/Class/Model/GameResource/ResolvedInfo/GameShaderPackResolvedInfo.cs @@ -0,0 +1,5 @@ +namespace ProjBobcat.Class.Model.GameResource.ResolvedInfo; + +public record GameShaderPackResolvedInfo( + string FileName, + bool IsDirectory); \ No newline at end of file diff --git a/ProjBobcat/ProjBobcat/ProjBobcat.csproj b/ProjBobcat/ProjBobcat/ProjBobcat.csproj index c924901c..6c0a9a8e 100644 --- a/ProjBobcat/ProjBobcat/ProjBobcat.csproj +++ b/ProjBobcat/ProjBobcat/ProjBobcat.csproj @@ -64,6 +64,7 @@ add support for Quilt installation https://github.com/Corona-Studio/ProjBobcat $(AssemblyName) README.md + enable @@ -120,5 +121,8 @@ add support for Quilt installation + + + \ No newline at end of file