From 978c6c0762441b58beef909f8990c7b662c0c52e Mon Sep 17 00:00:00 2001 From: LonelyWindG Date: Thu, 16 Mar 2023 19:24:35 +0800 Subject: [PATCH] Added "Save Data" to json function menu; Replace the menu button image to keep the style consistent; --- OctopathTraveler/DataContext.cs | 5 + .../GvasConverter/CustomFormatData.cs | 8 + .../GvasConverter/CustomFormatDataEntry.cs | 10 + .../GvasConverter/EngineVersion.cs | 11 + OctopathTraveler/GvasConverter/Gvas.cs | 32 ++ .../GvasConverter/GvasConverter.cs | 49 ++ .../Serialization/BinaryReaderEx.cs | 53 ++ .../GvasConverter/Serialization/UEProperty.cs | 460 ++++++++++++++++++ .../Serialization/UESerializer.UETypes.cs | 79 +++ .../Serialization/UESerializer.cs | 71 +++ .../GvasConverter/Utils/StringEx.cs | 40 ++ OctopathTraveler/MainWindow.xaml | 15 +- OctopathTraveler/MainWindow.xaml.cs | 159 ++++-- OctopathTraveler/OctopathTraveler.csproj | 21 +- .../Properties/Resources.Designer.cs | 31 +- OctopathTraveler/Properties/Resources.resx | 13 +- .../Properties/Resources.zh-CN.resx | 9 + OctopathTraveler/Resource/Convert.png | Bin 0 -> 378 bytes OctopathTraveler/Resource/Open.png | Bin 334 -> 590 bytes OctopathTraveler/Resource/Save.png | Bin 383 -> 308 bytes OctopathTraveler/Resource/SaveAsJson.png | Bin 0 -> 600 bytes OctopathTraveler/SaveData.cs | 16 +- 22 files changed, 1004 insertions(+), 78 deletions(-) create mode 100644 OctopathTraveler/GvasConverter/CustomFormatData.cs create mode 100644 OctopathTraveler/GvasConverter/CustomFormatDataEntry.cs create mode 100644 OctopathTraveler/GvasConverter/EngineVersion.cs create mode 100644 OctopathTraveler/GvasConverter/Gvas.cs create mode 100644 OctopathTraveler/GvasConverter/GvasConverter.cs create mode 100644 OctopathTraveler/GvasConverter/Serialization/BinaryReaderEx.cs create mode 100644 OctopathTraveler/GvasConverter/Serialization/UEProperty.cs create mode 100644 OctopathTraveler/GvasConverter/Serialization/UESerializer.UETypes.cs create mode 100644 OctopathTraveler/GvasConverter/Serialization/UESerializer.cs create mode 100644 OctopathTraveler/GvasConverter/Utils/StringEx.cs create mode 100644 OctopathTraveler/Resource/Convert.png create mode 100644 OctopathTraveler/Resource/SaveAsJson.png diff --git a/OctopathTraveler/DataContext.cs b/OctopathTraveler/DataContext.cs index 6fb83bc..df1263c 100644 --- a/OctopathTraveler/DataContext.cs +++ b/OctopathTraveler/DataContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; namespace OctopathTraveler @@ -120,6 +121,10 @@ public DataContext() continue; uint treausreAddress = treasures[i] + 100; + if (tid == 1277013) + { + Trace.Write($"{i + 1}/{treasures.Count}"); + } //var data = gvas.Key("TreasureStateArray_" + i); var treasure = new TreasureState(treausreAddress, info); diff --git a/OctopathTraveler/GvasConverter/CustomFormatData.cs b/OctopathTraveler/GvasConverter/CustomFormatData.cs new file mode 100644 index 0000000..85c3a96 --- /dev/null +++ b/OctopathTraveler/GvasConverter/CustomFormatData.cs @@ -0,0 +1,8 @@ +namespace GvasFormat +{ + public class CustomFormatData + { + public int Count; + public CustomFormatDataEntry[] Entries; + } +} \ No newline at end of file diff --git a/OctopathTraveler/GvasConverter/CustomFormatDataEntry.cs b/OctopathTraveler/GvasConverter/CustomFormatDataEntry.cs new file mode 100644 index 0000000..9a56313 --- /dev/null +++ b/OctopathTraveler/GvasConverter/CustomFormatDataEntry.cs @@ -0,0 +1,10 @@ +using System; + +namespace GvasFormat +{ + public class CustomFormatDataEntry + { + public Guid Id; + public int Value; + } +} \ No newline at end of file diff --git a/OctopathTraveler/GvasConverter/EngineVersion.cs b/OctopathTraveler/GvasConverter/EngineVersion.cs new file mode 100644 index 0000000..d21f282 --- /dev/null +++ b/OctopathTraveler/GvasConverter/EngineVersion.cs @@ -0,0 +1,11 @@ +namespace GvasFormat +{ + public struct EngineVersion + { + public short Major; + public short Minor; + public short Patch; + public int Build; + public string BuildId; + } +} \ No newline at end of file diff --git a/OctopathTraveler/GvasConverter/Gvas.cs b/OctopathTraveler/GvasConverter/Gvas.cs new file mode 100644 index 0000000..743b144 --- /dev/null +++ b/OctopathTraveler/GvasConverter/Gvas.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; +using GvasFormat.Serialization.UETypes; + +namespace GvasFormat +{ + /* + * General format notes: + * Strings are 4-byte length + value + \0, length includes \0 + * + */ + [DataContract] + public class Gvas + { + public static readonly byte[] Header = Encoding.ASCII.GetBytes("GVAS"); + [DataMember(Order = 0)] + public int SaveGameVersion; + [DataMember(Order = 1)] + public int PackageVersion; + [DataMember(Order = 2)] + public EngineVersion EngineVersion = new EngineVersion(); + [DataMember(Order = 3)] + public int CustomFormatVersion; + [DataMember(Order = 4)] + public CustomFormatData CustomFormatData = new CustomFormatData(); + [DataMember(Order = 5)] + public string SaveGameType; + [DataMember(Order = 6)] + public List Properties = new List(); + } +} diff --git a/OctopathTraveler/GvasConverter/GvasConverter.cs b/OctopathTraveler/GvasConverter/GvasConverter.cs new file mode 100644 index 0000000..c48046d --- /dev/null +++ b/OctopathTraveler/GvasConverter/GvasConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using GvasFormat; +using GvasFormat.Serialization; +using GvasFormat.Serialization.UETypes; + +namespace OctopathTraveler.GvasFormat +{ + class GvasConverter + { + public static (bool, Exception) Convert2JsonFile(string outputPath, Stream gvasStream) + { + (Gvas save, Exception e) = UESerializer.Read(gvasStream); + if (save == null) return (false, e); + + var jsonNode = JsonSerializer.SerializeToNode(save, new JsonSerializerOptions + { + IncludeFields = true, + MaxDepth = 64, + Converters = { new UEPropJsonConvert() } + }); + File.WriteAllText(outputPath, jsonNode.ToJsonString(new JsonSerializerOptions + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + })); + return (true, e); + } + } + + /// + /// Optimize json output + /// + public class UEPropJsonConvert : JsonConverter + { + public override UEProperty? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + public override void Write(Utf8JsonWriter writer, UEProperty value, JsonSerializerOptions options) + { + writer.WriteRawValue(JsonSerializer.SerializeToUtf8Bytes(value.ToObject(), options)); + } + } +} diff --git a/OctopathTraveler/GvasConverter/Serialization/BinaryReaderEx.cs b/OctopathTraveler/GvasConverter/Serialization/BinaryReaderEx.cs new file mode 100644 index 0000000..71d67ca --- /dev/null +++ b/OctopathTraveler/GvasConverter/Serialization/BinaryReaderEx.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Text; + +namespace GvasFormat.Serialization +{ + public static class BinaryReaderEx + { + private static readonly Encoding Utf8 = new UTF8Encoding(false); + + public static void Terminator(this BinaryReader reader) + { + var terminator = reader.ReadByte(); + if (terminator != 0) + { + throw new FormatException($"Offset: 0x{reader.BaseStream.Position - 1:x8}. Expected terminator (0x00), but was (0x{terminator:x2})"); + } + } + + public static string ReadUEString(this BinaryReader reader) + { + if (reader.PeekChar() < 0) + return null; + + // ue字符串通常以0结尾,length包含null + var lengthOffset = reader.BaseStream.Position; + var length = reader.ReadInt32(); + if (length == 0) + return null; + + if (length == 1) + return ""; + + var valueBytes = new byte[length]; + + int i = 0; + for (; i < length; i++) + { + var b = reader.ReadByte(); + if (b == 0) break; + valueBytes[i] = b; + } + + var str = Utf8.GetString(valueBytes, 0, length - 1); + + // 如果读出来和length不一样,那么肯定是哪里分析错了 + if (length != str.Length + 1) + throw new FormatException($"Offset: 0x{lengthOffset:x8} read string error."); + + return str; + } + } +} diff --git a/OctopathTraveler/GvasConverter/Serialization/UEProperty.cs b/OctopathTraveler/GvasConverter/Serialization/UEProperty.cs new file mode 100644 index 0000000..5f2a8fb --- /dev/null +++ b/OctopathTraveler/GvasConverter/Serialization/UEProperty.cs @@ -0,0 +1,460 @@ +using System; +using System.IO; +using System.Diagnostics; +using System.Linq; +using GvasFormat.Utils; +using System.Collections.Generic; + +namespace GvasFormat.Serialization.UETypes +{ + public abstract class UEProperty + { + public string Name; + public string Type; + public long Offset; + public virtual object ToObject() => this; + public static UEProperty Read(BinaryReader br) + { + if (br.PeekChar() < 0) + return null; + + var name = br.ReadUEString(); + if (name == null || name == "None") return null; + + var type = br.ReadUEString(); + var valLen = br.ReadInt64(); + return UESerializer.Deserialize(name, type, valLen, br); + } + + public static UEProperty[] Read(BinaryReader br, int count) + { + if (br.PeekChar() < 0) + return null; + + var name = br.ReadUEString(); + var type = br.ReadUEString(); + var valLen = br.ReadInt64(); + return UESerializer.Deserialize(name, type, valLen, count, br); + } + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEBoolProperty : UEProperty + { + public bool Value; + public UEBoolProperty(BinaryReader br, long valLen) + { + Offset = br.BaseStream.Position; + var val = valLen == -1 ? br.ReadByte() : br.ReadInt16(); + if (val == 0) + Value = false; + else if (val == 1) + Value = true; + else + throw new InvalidOperationException($"Offset: 0x{br.BaseStream.Position - 1:x8}. Expected bool value, but was {val}"); + } + + public override object ToObject() + { + if (Name == null) + return Value; + return new { Offset, Name, Value }; + } + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEFloatProperty : UEProperty + { + public UEFloatProperty() { } + public UEFloatProperty(BinaryReader br, long valLen) + { + br.Terminator(); + Offset = br.BaseStream.Position; + Value = br.ReadSingle(); + } + + public float Value; + public override object ToObject() + { + if (Name == null) + return Value; + + return new { Offset, Name, Value }; + } + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEIntProperty : UEProperty + { + public UEIntProperty() { } + public UEIntProperty(BinaryReader br, long valLen) + { + // -1 来自array + if (valLen > -1) + br.Terminator(); + + Offset = br.BaseStream.Position; + Value = br.ReadInt32(); + } + + public int Value; + public override object ToObject() + { + if (Name == null) + return Value; + + return new { Offset, Name, Value }; + } + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEUInt64Property : UEProperty + { + public UEUInt64Property() { } + public UEUInt64Property(BinaryReader br, long valLen) + { + // valLen = -1 来自 array + if (valLen > -1) + br.Terminator(); + + Offset = br.BaseStream.Position; + Value = br.ReadUInt64(); + } + + public ulong Value; + public override object ToObject() + { + if (Name == null) + return Value; + + return new { Offset, Name, Value }; + } + } + + [DebuggerDisplay("Count = {Items.Length}", Name = "{Name}")] + public sealed class UEArrayProperty : UEProperty + { + public string ItemType; + public UEProperty[] Items; + public UEArrayProperty(BinaryReader br, long valLen) + { + ItemType = br.ReadUEString(); + br.Terminator(); + + // valLen 从这里开始 + var count = br.ReadInt32(); + Items = new UEProperty[count]; + + // 定位到第一个元素位置 + Offset = br.BaseStream.Position; + + switch (ItemType) + { + case "StructProperty": + Items = Read(br, count); + break; + case "ByteProperty": + Items = UEByteProperty.Read(br, valLen, count); + break; + default: + { + for (var i = 0; i < count; i++) + Items[i] = UESerializer.Deserialize(null, ItemType, -1, br); + break; + } + } + } + public override object ToObject() + { + // 优化json输出,如果觉得数据有用也可以输出 + var items = Items.Length >= 50 ? new string[] { "..." } : Items.Select(it => it.ToObject()); + return new { Offset, Name, ItemType, Items.Length, Items = items }; + } + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEByteProperty : UEProperty + { + public UEByteProperty() { } + public static UEByteProperty Read(BinaryReader br, long valLen) + { + if (valLen == 1) + { + br.ReadUEString(); // None + br.ReadInt16();// 不清楚 + return new UEByteProperty { Value = "" }; + } + + if (br.PeekChar() == 0) + { + br.Terminator(); + // valLen starts here + var arrayLength = br.ReadInt32(); + var bytes = br.ReadBytes(arrayLength); + return new UEByteProperty { Value = bytes.AsHex() }; + } + else + { + var str = ""; + while (true) + { + str += br.ReadUEString(); + if (br.PeekChar() != 0) + break; + br.ReadByte(); // 0 + } + return new UEByteProperty { Value = str }; + } + + } + + public static UEProperty[] Read(BinaryReader br, long valLen, int count) + { + var Offset = br.BaseStream.Position; + if (sizeof(int) + count == valLen) + { + // 纯字节 + var bytes = br.ReadBytes(count); + return new UEProperty[] { new UEByteProperty { Offset = Offset, Value = bytes.AsHex() } }; + } + else + { + // 虽然itemtype是ByteProperty,单数数据是字符串 + var r = new UEByteProperty[count]; + for (int i = 0; i < count; i++) + r[i] = new UEByteProperty { Offset = Offset, Value = br.ReadUEString() }; + return r; + } + } + + public string Value; + + public override object ToObject() + { + if (Name == null) + return Value; + return new { Offset, Name, Value }; + } + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEDateTimeStructProperty : UEStructProperty + { + public DateTime Value; + public UEDateTimeStructProperty(BinaryReader br) + { + Offset = br.BaseStream.Position; + Value = DateTime.FromBinary(br.ReadInt64()); + } + public override object ToObject() => Value; + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEEnumProperty : UEProperty + { + public string EnumType; + public string Value; + public UEEnumProperty(BinaryReader br, long valLen) + { + EnumType = br.ReadUEString(); + br.Terminator(); + // valLen 从这里开始 + Offset = br.BaseStream.Position; + Value = br.ReadUEString(); + } + } + + [DebuggerDisplay("Count = {Properties.Count}", Name = "{Name}")] + public sealed class UEGenericStructProperty : UEStructProperty + { + public List Properties = new List(); + + public override object ToObject() + { + var _Properties = Properties.Select(it => it.ToObject()); + if (Name == null) + return _Properties; + + return new { Offset, Name, StructType, Properties = _Properties }; + } + } + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEGuidStructProperty : UEStructProperty + { + public Guid Value; + public UEGuidStructProperty(BinaryReader br) + { + Offset = br.BaseStream.Position; + Value = new Guid(br.ReadBytes(16)); + } + } + + [DebuggerDisplay("R = {R}, G = {G}, B = {B}, A = {A}", Name = "{Name}")] + public sealed class UELinearColorStructProperty : UEStructProperty + { + public float R, G, B, A; + public UELinearColorStructProperty(BinaryReader br) + { + Offset = br.BaseStream.Position; + R = br.ReadSingle(); + G = br.ReadSingle(); + B = br.ReadSingle(); + A = br.ReadSingle(); + } + } + + [DebuggerDisplay("Count = {Map.Count}", Name = "{Name}")] + public sealed class UEMapProperty : UEProperty + { + public UEMapProperty(BinaryReader br, long valLen) + { + var keyType = br.ReadUEString(); + var valueType = br.ReadUEString(); + var unknown = br.ReadBytes(5); + if (unknown.Any(b => b != 0)) + throw new InvalidOperationException($"Offset: 0x{br.BaseStream.Position - 5:x8}. Expected ??? to be 0, but was 0x{unknown.AsHex()}"); + + var count = br.ReadInt32(); + + Offset = br.BaseStream.Position; + for (var i = 0; i < count; i++) + { + UEProperty key, value; + if (keyType == "StructProperty") + key = Read(br); + else + key = UESerializer.Deserialize(null, keyType, -1, br); + var values = new List(); + do + { + if (valueType == "StructProperty") + value = Read(br); + else + value = UESerializer.Deserialize(null, valueType, -1, br); + values.Add(value); + } while (!(value is UENoneProperty)); + Map.Add(new UEKeyValuePair { Key = key, Values = values }); + } + } + public List Map = new List(); + + public class UEKeyValuePair + { + public UEProperty Key; + public List Values; + } + } + + [DebuggerDisplay("", Name = "{Name}")] + public sealed class UENoneProperty : UEProperty + { + public override object ToObject() => null; + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UEStringProperty : UEProperty + { + public string Value; + public UEStringProperty(BinaryReader br, long valLen) + { + if (valLen > -1) br.Terminator(); + Offset = br.BaseStream.Position; + Value = br.ReadUEString(); + } + + } + public abstract class UEStructProperty : UEProperty + { + public static UEStructProperty Read(BinaryReader br, long valLen) + { + var type = br.ReadUEString(); + // new Guid(br.ReadBytes(16)); + br.ReadBytes(16); + br.Terminator(); + return ReadStructValue(type, br); + } + + public static UEStructProperty[] Read(BinaryReader br, long valLen, int count) + { + var type = br.ReadUEString(); + br.ReadBytes(16); // uuid + br.Terminator(); + var result = new UEStructProperty[count]; + for (var i = 0; i < count; i++) + result[i] = ReadStructValue(type, br); + return result; + } + + protected static UEStructProperty ReadStructValue(string type, BinaryReader br) + { + UEStructProperty result; + var Offset = br.BaseStream.Position; + switch (type) + { + case "DateTime": + result = new UEDateTimeStructProperty(br); + break; + case "Guid": + result = new UEGuidStructProperty(br); + break; + case "Vector": + case "Rotator": + result = new UEVectorStructProperty(br); + break; + case "LinearColor": + result = new UELinearColorStructProperty(br); + break; + default: + var tmp = new UEGenericStructProperty(); + while (Read(br) is UEProperty prop) + { + tmp.Properties.Add(prop); + if (prop is UENoneProperty) + break; + } + result = tmp; + break; + } + result.StructType = type; + result.Type = type; + result.Offset = Offset; + return result; + } + + public string StructType; + } + + [DebuggerDisplay("{Value}", Name = "{Name}")] + public sealed class UETextProperty : UEProperty + { + public UETextProperty(BinaryReader br, long valLen) + { + br.Terminator(); + // valLen starts here + Flags = br.ReadInt64(); + br.Terminator(); + Id = br.ReadUEString(); + Offset = br.BaseStream.Position; + Value = br.ReadUEString(); + } + + public long Flags; + public string Id; + public string Value; + } + + [DebuggerDisplay("X = {X}, Y = {Y}, Z = {Z}", Name = "{Name}")] + public sealed class UEVectorStructProperty : UEStructProperty + { + public UEVectorStructProperty(BinaryReader br) + { + Offset = br.BaseStream.Position; + X = br.ReadSingle(); + Y = br.ReadSingle(); + Z = br.ReadSingle(); + } + + public float X, Y, Z; + } + +} diff --git a/OctopathTraveler/GvasConverter/Serialization/UESerializer.UETypes.cs b/OctopathTraveler/GvasConverter/Serialization/UESerializer.UETypes.cs new file mode 100644 index 0000000..f3db4fa --- /dev/null +++ b/OctopathTraveler/GvasConverter/Serialization/UESerializer.UETypes.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using GvasFormat.Serialization.UETypes; + +namespace GvasFormat.Serialization +{ + public static partial class UESerializer + { + internal static UEProperty Deserialize(string name, string type, long valLen, BinaryReader reader) + { + UEProperty result; + var itemOffset = reader.BaseStream.Position; + switch (type) + { + case "BoolProperty": + result = new UEBoolProperty(reader, valLen); + break; + case "IntProperty": + result = new UEIntProperty(reader, valLen); + break; + case "FloatProperty": + result = new UEFloatProperty(reader, valLen); + break; + case "NameProperty": + case "StrProperty": + result = new UEStringProperty(reader, valLen); + break; + case "TextProperty": + result = new UETextProperty(reader, valLen); + break; + case "EnumProperty": + result = new UEEnumProperty(reader, valLen); + break; + case "StructProperty": + result = UEStructProperty.Read(reader, valLen); + break; + case "ArrayProperty": + result = new UEArrayProperty(reader, valLen); + break; + case "MapProperty": + result = new UEMapProperty(reader, valLen); + break; + case "ByteProperty": + result = UEByteProperty.Read(reader, valLen); + break; + case "UInt64Property": + result = new UEUInt64Property(reader, valLen); + break; + default: + throw new FormatException($"Offset: 0x{itemOffset:x8}. Unknown value type '{type}' of item '{name}'"); + } + result.Name = name; + result.Type = type; + return result; + } + + internal static UEProperty[] Deserialize(string name, string type, long valLen, int count, BinaryReader reader) + { + UEProperty[] result; + switch (type) + { + case "StructProperty": + result = UEStructProperty.Read(reader, valLen, count); + break; + case "ByteProperty": + result = UEByteProperty.Read(reader, valLen, count); + break; + default: + throw new FormatException($"Unknown value type '{type}' of item '{name}'"); + } + foreach (var item in result) + { + item.Name = name; + item.Type = type; + } + return result; + } + } +} \ No newline at end of file diff --git a/OctopathTraveler/GvasConverter/Serialization/UESerializer.cs b/OctopathTraveler/GvasConverter/Serialization/UESerializer.cs new file mode 100644 index 0000000..d928060 --- /dev/null +++ b/OctopathTraveler/GvasConverter/Serialization/UESerializer.cs @@ -0,0 +1,71 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using GvasFormat.Serialization.UETypes; +using GvasFormat.Utils; + +namespace GvasFormat.Serialization +{ + public static partial class UESerializer + { + public static (Gvas, Exception) Read(Stream stream) + { + using (var reader = new BinaryReader(stream, Encoding.ASCII, true)) + { + var header = reader.ReadBytes(Gvas.Header.Length); + if (!Gvas.Header.SequenceEqual(header)) + { + return (null, new FormatException($"Invalid header, expected {Gvas.Header.AsHex()}")); + } + + var result = new Gvas + { + SaveGameVersion = reader.ReadInt32(), + PackageVersion = reader.ReadInt32() + }; + result.EngineVersion.Major = reader.ReadInt16(); + result.EngineVersion.Minor = reader.ReadInt16(); + result.EngineVersion.Patch = reader.ReadInt16(); + result.EngineVersion.Build = reader.ReadInt32(); + result.EngineVersion.BuildId = reader.ReadUEString(); + + result.CustomFormatVersion = reader.ReadInt32(); + result.CustomFormatData.Count = reader.ReadInt32(); + + // Entries 好像没什么用,不输出json + result.CustomFormatData.Entries = null; + reader.BaseStream.Position += (16 + 4) * result.CustomFormatData.Count; + /* + result.CustomFormatData.Entries = new CustomFormatDataEntry[result.CustomFormatData.Count]; + for (var i = 0; i < result.CustomFormatData.Count; i++) + { + var entry = new CustomFormatDataEntry(); + entry.Id = new Guid(reader.ReadBytes(16)); + entry.Value = reader.ReadInt32(); + result.CustomFormatData.Entries[i] = entry; + } + */ + + result.SaveGameType = reader.ReadUEString(); + + try + { + // 这个过程很容易出问题,将当前的数据,输出到json + while (UEProperty.Read(reader) is UEProperty prop) + result.Properties.Add(prop); + } + catch (Exception e) + { + var exception = new Exception($"Bad Result Json: {e.Message}", e); + Console.WriteLine(exception); + Trace.Write(exception); + return (result, exception); + } + + return (result, null); + } + } + } +} diff --git a/OctopathTraveler/GvasConverter/Utils/StringEx.cs b/OctopathTraveler/GvasConverter/Utils/StringEx.cs new file mode 100644 index 0000000..87bb8e8 --- /dev/null +++ b/OctopathTraveler/GvasConverter/Utils/StringEx.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Text; + +namespace GvasFormat.Utils +{ + public static class StringEx + { + public static string AsHex(this byte[] bytes) + { + if (bytes == null) + return null; + + if (bytes.Length == 0) + return ""; + + var result = new StringBuilder(bytes.Length * 2); + foreach (var b in bytes) + result.Append(b.ToString("x2")); + return result.ToString(); + } + + public static byte[] AsBytes(this string hex) + { + if (hex == null) + return null; + + if (hex.Length == 0) + return new byte[0]; + + if (hex.Length % 2 == 1) + throw new InvalidOperationException($"Odd hex string length of {hex.Length}"); + + var result = new byte[hex.Length % 2]; + for (int i = 0, j = 0; i < hex.Length; i += 2, j++) + result[j] = byte.Parse(hex.Substring(i, 2), NumberStyles.HexNumber); + return result; + } + } +} diff --git a/OctopathTraveler/MainWindow.xaml b/OctopathTraveler/MainWindow.xaml index 4472d36..acca5e0 100644 --- a/OctopathTraveler/MainWindow.xaml +++ b/OctopathTraveler/MainWindow.xaml @@ -9,7 +9,7 @@ Title="{x:Static properties:Resources.MainWindowTitle}" Height="650" Width="800" WindowStartupLocation="CenterScreen" AllowDrop="True" Drop="Window_Drop" PreviewDragOver="Window_PreviewDragOver"> - + @@ -17,6 +17,7 @@ + @@ -25,10 +26,16 @@ - + + diff --git a/OctopathTraveler/MainWindow.xaml.cs b/OctopathTraveler/MainWindow.xaml.cs index 86820df..91b6cfe 100644 --- a/OctopathTraveler/MainWindow.xaml.cs +++ b/OctopathTraveler/MainWindow.xaml.cs @@ -5,6 +5,7 @@ using System.Windows.Controls; using System.Windows.Data; using Microsoft.Win32; +using static OctopathTraveler.Properties.Resources; namespace OctopathTraveler { @@ -57,13 +58,12 @@ private void Window_PreviewDragOver(object sender, DragEventArgs e) private void Window_Drop(object sender, DragEventArgs e) { - string[] files = e.Data.GetData(DataFormats.FileDrop) as string[]; - if (files == null) return; - if (!System.IO.File.Exists(files[0])) return; + if (e.Data.GetData(DataFormats.FileDrop) is not string[] files) return; + if (!File.Exists(files[0])) return; SaveData.Instance().Open(files[0]); Init(); - MessageBox.Show(Properties.Resources.MessageLoadSuccess); + MessageBox.Show(MessageLoadSuccess); } private void MenuItemFileOpen_Click(object sender, RoutedEventArgs e) @@ -75,7 +75,7 @@ private void MenuItemFileSave_Click(object sender, RoutedEventArgs e) { if (SaveData.IsReadonlyMode) { - MessageBox.Show("ReadonlyMode"); + MessageBox.Show(MeaageSaveFail, "ReadonlyMode"); return; } Save(); @@ -85,44 +85,49 @@ private void MenuItemFileSaveAs_Click(object sender, RoutedEventArgs e) { if (SaveData.IsReadonlyMode) { - MessageBox.Show("ReadonlyMode"); + MessageBox.Show(MeaageSaveFail, "ReadonlyMode"); return; } - SaveFileDialog dlg = new SaveFileDialog(); + var dlg = new SaveFileDialog(); if (dlg.ShowDialog() == false) return; - if (SaveData.Instance().SaveAs(dlg.FileName) == true) MessageBox.Show(Properties.Resources.MessageSaveSuccess); - else MessageBox.Show(Properties.Resources.MeaageSaveFail, SaveData.IsReadonlyMode ? "ReadonlyMode" : ""); + if (SaveData.Instance().SaveAs(dlg.FileName) == true) MessageBox.Show(MessageSaveSuccess); + else MessageBox.Show(MeaageSaveFail, SaveData.IsReadonlyMode ? "ReadonlyMode" : ""); + } + + private void MenuItemFileSaveAsJson_Click(object sender, RoutedEventArgs e) + { + ConvertSaveDataToJson(false); } private void MenuItemExportInfoExcel_Click(object sender, RoutedEventArgs e) { if (!Info.TryGetEmbeddedInfoExcel(out var excels)) { - MessageBox.Show(Properties.Resources.MeaageSaveFail); + MessageBox.Show(MeaageSaveFail); return; } bool isSaveAny = false; - foreach (var excel in excels) + foreach ((var fileName, var bytes) in excels) { - string ext = Path.GetExtension(excel.Item1); + string ext = Path.GetExtension(fileName); string v = $"Excel files (*{ext})|*{ext}|All files (*.*)|*.*"; var dialog = new SaveFileDialog { InitialDirectory = Directory.GetCurrentDirectory(), - FileName = excel.Item1, + FileName = fileName, Filter = v }; if (dialog.ShowDialog() == false) continue; - File.WriteAllBytes(Path.Combine(dialog.InitialDirectory, dialog.FileName), excel.Item2); + File.WriteAllBytes(dialog.FileName, bytes); isSaveAny = true; } - MessageBox.Show(isSaveAny ? Properties.Resources.MessageSaveSuccess : Properties.Resources.MeaageSaveFail); + MessageBox.Show(isSaveAny ? MessageSaveSuccess : MeaageSaveFail); } private void MenuItemExit_Click(object sender, RoutedEventArgs e) @@ -150,6 +155,11 @@ private void ToolBarFileSave_Click(object sender, RoutedEventArgs e) Save(); } + private void ToolBarConvertToJson_Click(object sender, RoutedEventArgs e) + { + ConvertSaveDataToJson(true); + } + private void Init() { SetWeakFilter(0); @@ -160,24 +170,74 @@ private void Init() private void Load(bool force) { - OpenFileDialog dlg = new OpenFileDialog(); + var dlg = new OpenFileDialog(); if (dlg.ShowDialog() == false) return; SaveData.Instance().Open(dlg.FileName); Init(); - MessageBox.Show(Properties.Resources.MessageLoadSuccess); + MessageBox.Show(MessageLoadSuccess); } private void Save() { - if (SaveData.Instance().Save() == true) MessageBox.Show(Properties.Resources.MessageSaveSuccess); - else MessageBox.Show(Properties.Resources.MeaageSaveFail, SaveData.IsReadonlyMode ? "ReadonlyMode" : ""); + if (SaveData.Instance().Save() == true) MessageBox.Show(MessageSaveSuccess); + else MessageBox.Show(MeaageSaveFail, SaveData.IsReadonlyMode ? "ReadonlyMode" : ""); + } + + private static void ConvertSaveDataToJson(bool convertOtherFile) + { + string fileName; + if (convertOtherFile) + { + var openDialog = new OpenFileDialog(); + if (openDialog.ShowDialog() == false) return; + fileName = openDialog.FileName; + } + else + { + fileName = SaveData.Instance().FileName; + if (string.IsNullOrWhiteSpace(fileName)) + { + MessageBox.Show(MeaageSaveFail); + return; + } + } + + string folder = Path.GetDirectoryName(fileName); + var saveDialog = new SaveFileDialog + { + InitialDirectory = string.IsNullOrEmpty(folder) ? Directory.GetCurrentDirectory() : folder, + FileName = Path.GetFileName(fileName) + ".json", + Filter = "Json files (*.json)|*.json|All files (*.*)|*.*" + }; + if (saveDialog.ShowDialog() == false) + return; + + bool saved; + Exception ex; + + if (convertOtherFile) + (saved, ex) = GvasFormat.GvasConverter.Convert2JsonFile(saveDialog.FileName, File.OpenRead(fileName)); + else + (saved, ex) = SaveData.Instance().SaveAsJson(saveDialog.FileName); + + if (ex != null) + { + if (saved) + MessageBox.Show("Only part of the data is saved as json because of the exception:\n" + ex.ToString(), MessageSaveSuccess); + else + MessageBox.Show(ex.ToString(), MeaageSaveFail); + + } + else + { + MessageBox.Show(saved ? MessageSaveSuccess : MeaageSaveFail); + } } private void ButtonSword_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if(chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Sword = ChoiceEquipment(chara.Sword); } @@ -185,8 +245,7 @@ private void ButtonSword_Click(object sender, RoutedEventArgs e) private void ButtonLance_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Lance = ChoiceEquipment(chara.Lance); } @@ -194,8 +253,7 @@ private void ButtonLance_Click(object sender, RoutedEventArgs e) private void ButtonDagger_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Dagger = ChoiceEquipment(chara.Dagger); } @@ -203,8 +261,7 @@ private void ButtonDagger_Click(object sender, RoutedEventArgs e) private void ButtonAxe_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Axe = ChoiceEquipment(chara.Axe); } @@ -212,8 +269,7 @@ private void ButtonAxe_Click(object sender, RoutedEventArgs e) private void ButtonBow_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Bow = ChoiceEquipment(chara.Bow); } @@ -221,8 +277,7 @@ private void ButtonBow_Click(object sender, RoutedEventArgs e) private void ButtonRod_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Rod = ChoiceEquipment(chara.Rod); } @@ -230,8 +285,7 @@ private void ButtonRod_Click(object sender, RoutedEventArgs e) private void ButtonShield_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Shield = ChoiceEquipment(chara.Shield); } @@ -239,8 +293,7 @@ private void ButtonShield_Click(object sender, RoutedEventArgs e) private void ButtonHead_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Head = ChoiceEquipment(chara.Head); } @@ -248,8 +301,7 @@ private void ButtonHead_Click(object sender, RoutedEventArgs e) private void ButtonBody_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Body = ChoiceEquipment(chara.Body); } @@ -257,8 +309,7 @@ private void ButtonBody_Click(object sender, RoutedEventArgs e) private void ButtonAccessory1_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Accessory1 = ChoiceEquipment(chara.Accessory1); } @@ -266,8 +317,7 @@ private void ButtonAccessory1_Click(object sender, RoutedEventArgs e) private void ButtonAccessory2_Click(object sender, RoutedEventArgs e) { - Charactor chara = CharactorList.SelectedItem as Charactor; - if (chara != null) + if (CharactorList.SelectedItem is Charactor chara) { chara.Accessory2 = ChoiceEquipment(chara.Accessory2); } @@ -275,12 +325,13 @@ private void ButtonAccessory2_Click(object sender, RoutedEventArgs e) private void ButtonItem_Click(object sender, RoutedEventArgs e) { - Item item = (sender as Button)?.DataContext as Item; - if(item != null) + if (sender is Button { DataContext: Item item }) { - ItemChoiceWindow window = new ItemChoiceWindow(); - window.Type = ItemChoiceWindow.eType.item; - window.ID = item.ID; + var window = new ItemChoiceWindow + { + Type = ItemChoiceWindow.eType.item, + ID = item.ID + }; window.ShowDialog(); item.ID = window.ID; } @@ -288,8 +339,10 @@ private void ButtonItem_Click(object sender, RoutedEventArgs e) private uint ChoiceEquipment(uint id) { - ItemChoiceWindow window = new ItemChoiceWindow(); - window.ID = id; + var window = new ItemChoiceWindow + { + ID = id + }; window.ShowDialog(); return window.ID; } @@ -316,7 +369,7 @@ static void AppendNum(StringBuilder builder, string des, int completed, int tota builder.Append(des); builder.Append(" : "); builder.Append(completed); - builder.Append("/"); + builder.Append('/'); builder.Append(total); builder.AppendLine(); } @@ -394,16 +447,16 @@ static void AppendNum(StringBuilder builder, string des, uint completed, uint to builder.Append(des); builder.Append(" : "); builder.Append(completed); - builder.Append("/"); + builder.Append('/'); builder.Append(total); builder.AppendLine(); } AppendNum(builder, "Completed", completedCount, (uint)treasureStates.Count); AppendNum(builder, "Uncompleted", (uint)treasureStates.Count - completedCount, (uint)treasureStates.Count); - AppendNum(builder, Properties.Resources.TreasureStatesSummation, completedChest + completedHiddenItem, totalChest + totalHiddenItem); - AppendNum(builder, Properties.Resources.TreasureStatesChest, completedChest, totalChest); - AppendNum(builder, Properties.Resources.TreasureStatesHiddenItem, completedHiddenItem, totalHiddenItem); + AppendNum(builder, TreasureStatesSummation, completedChest + completedHiddenItem, totalChest + totalHiddenItem); + AppendNum(builder, TreasureStatesChest, completedChest, totalChest); + AppendNum(builder, TreasureStatesHiddenItem, completedHiddenItem, totalHiddenItem); text = builder.ToString(); } } diff --git a/OctopathTraveler/OctopathTraveler.csproj b/OctopathTraveler/OctopathTraveler.csproj index 715ffa6..3effd40 100644 --- a/OctopathTraveler/OctopathTraveler.csproj +++ b/OctopathTraveler/OctopathTraveler.csproj @@ -13,11 +13,9 @@ True - embedded True - embedded $(DefineConstants);DEBUG @@ -28,19 +26,12 @@ - - Never - - - Never - - - + + - @@ -59,4 +50,12 @@ Resources.Designer.cs + + + Never + + + Never + + \ No newline at end of file diff --git a/OctopathTraveler/Properties/Resources.Designer.cs b/OctopathTraveler/Properties/Resources.Designer.cs index 1cd07c9..7f79548 100644 --- a/OctopathTraveler/Properties/Resources.Designer.cs +++ b/OctopathTraveler/Properties/Resources.Designer.cs @@ -278,7 +278,7 @@ public static string ItemWind { } /// - /// 查找类似 OCTOPATH TRAVELER SaveDataEditor(Nintendo Switch) 的本地化字符串。 + /// 查找类似 OCTOPATH TRAVELER SaveDataEditor 的本地化字符串。 /// public static string MainWindowTitle { get { @@ -350,7 +350,7 @@ public static string MenuFileSave { } /// - /// 查找类似 Save(_A)s... 的本地化字符串。 + /// 查找类似 Save (_A)s... 的本地化字符串。 /// public static string MenuFileSaveAs { get { @@ -358,6 +358,15 @@ public static string MenuFileSaveAs { } } + /// + /// 查找类似 Save As (_J)son... 的本地化字符串。 + /// + public static string MenuFileSaveAsJson { + get { + return ResourceManager.GetString("MenuFileSaveAsJson", resourceCulture); + } + } + /// /// 查找类似 Load Success 的本地化字符串。 /// @@ -484,6 +493,15 @@ public static string TabItemWeak { } } + /// + /// 查找类似 Convert SaveData to Json 的本地化字符串。 + /// + public static string ToolTipConvert { + get { + return ResourceManager.GetString("ToolTipConvert", resourceCulture); + } + } + /// /// 查找类似 Open 的本地化字符串。 /// @@ -502,6 +520,15 @@ public static string ToolTipSave { } } + /// + /// 查找类似 Save As Json 的本地化字符串。 + /// + public static string ToolTipSaveAsJson { + get { + return ResourceManager.GetString("ToolTipSaveAsJson", resourceCulture); + } + } + /// /// 查找类似 Chest 的本地化字符串。 /// diff --git a/OctopathTraveler/Properties/Resources.resx b/OctopathTraveler/Properties/Resources.resx index 7dea00f..e27fbf5 100644 --- a/OctopathTraveler/Properties/Resources.resx +++ b/OctopathTraveler/Properties/Resources.resx @@ -191,7 +191,7 @@ Wind - OCTOPATH TRAVELER SaveDataEditor(Nintendo Switch) + OCTOPATH TRAVELER SaveDataEditor Save Fail @@ -215,7 +215,10 @@ (_S)ave - Save(_A)s... + Save (_A)s... + + + Save As (_J)son... Load Success @@ -259,12 +262,18 @@ Weakness + + Convert SaveData to Json + Open Save + + Save As Json + Chest diff --git a/OctopathTraveler/Properties/Resources.zh-CN.resx b/OctopathTraveler/Properties/Resources.zh-CN.resx index f64dc54..e8579f4 100644 --- a/OctopathTraveler/Properties/Resources.zh-CN.resx +++ b/OctopathTraveler/Properties/Resources.zh-CN.resx @@ -274,4 +274,13 @@ 该页面中的地点可能遗漏部分名称, 而且有些地点ID找不到对应名称, 可能是游戏开发时的弃用地点未移除 + + 另存为Json(_J)... + + + 将存档转换为Json + + + 另存为Json + \ No newline at end of file diff --git a/OctopathTraveler/Resource/Convert.png b/OctopathTraveler/Resource/Convert.png new file mode 100644 index 0000000000000000000000000000000000000000..c69c40963f37e791536e827d869cf41706410510 GIT binary patch literal 378 zcmV-=0fqjFP)Px$G)Y83R9HvFm%$CfFbqXs7hn*W0SQLnz&ya2P2j=?a9{!?SOo4MqXva4PMgG4 zd~6)|KfCNi1 z@izrXX-twfbAaj!q{#1lO;S+33UW>!+EZKwIUpt}sNM&1PHvil>X)#33aTqm?GB{A z3u2&C;NCh69!RWC-i^}>&^K6?fL(y4Hj>GLI9OS9ydUy3vjPdu5&p-%QMv@=CwVu> Y2eW{JA+zmRX8-^I07*qoM6N<$g0feXZU6uP literal 0 HcmV?d00001 diff --git a/OctopathTraveler/Resource/Open.png b/OctopathTraveler/Resource/Open.png index 568966e38b5b7c5e93d938fdc63543bac106acdc..402bb88340dd38f50580002d19d6c5ffb25d6c79 100644 GIT binary patch delta 566 zcmV-60?GZ(0?q`GBYyw^b5ch_0Itp)=>Px%2uVaiR9HvN*H5TTQ547V&np=)P-ZCq zN{T|rpOn0Mk}yz6VP;@}QbJKk2on({Ge!ARu@0xz*Ee@d^{-wM30aCAA;!nA2I+;##?;C zn9wf+tilxp;k*pyA-NG}aS>~)0MVmY;~@5A0yu{a*nf;uRe-`XdcfUG08xJ9FdZ)& z0sO)+{LBE*iTj9Sd`54_adg2Y8O%fSK-$`q*xDPwP8`hy@CXZ1o<*{EkLrbF7M^DS zD9Y~##^XyFAogTh2`GcVBiMm!SXnlR2t?^!N)I0(W}Ra$mZrp>S-p7|SEJl_fN!by zpRRsc`7bKog^3td=jW%gyMynIbo=@*P%}$UxwZrR0W)l9ftT4>RUkmUSd{*#?Kr^bh)=ID2T{vSM7U<>FQB`m-VJ)nz1@mae96b-GYBsBgA3IyR096RTXxssoguLivU0000Px#?ny*JR9Hvtm%$CfFbqXsra)o@U#ayhm1Ob4TsN`-VgW5ss;pDr1G#mf z0er=QE}!Lp0B*oI@uJsW)Y-FSI8p7{flQvgO@fLVXL6ek_vUnt-q6bIF;#m)}7y#M4q(4u%K z-2u7+U4gDZS6*P20&$&M@*S}6@UlDkTd=fnt^eNw@O~D?p=3;2tC0JQb;+8wTK5`w zz8|i_oO-9%9q%$8^`!w|VNI&`e9VY0g>gk diff --git a/OctopathTraveler/Resource/SaveAsJson.png b/OctopathTraveler/Resource/SaveAsJson.png new file mode 100644 index 0000000000000000000000000000000000000000..568f5c4cb61438f58af6e9786c17028aa7618192 GIT binary patch literal 600 zcmV-e0;m0nP)Px%5=lfsR9HvtmN7B}Q4of|JpoRDfE1YwC%_4C0#wOy0EIw|RDyBvZRPW-njkP=Qu^q)XS1=cdvCUv$| zTICYx!>1VkNhWpOfPL`zaQ#yRo`KuA@rJW>U4H~7zEgr;6|iGn0hhpfOgi~-64+jU z7K?*156s6zFW8Ak*7G=Q#w=>QSP$Es-h{D^@4TkzumHMmt+$gW#6m6TheR`hWmki55` zzI|0-5fE4nBsk)(0vHE&fQ_h>fJr}Yi+V6tB?5k67CIYfDKeXl;AVFd0q1}h zU<|m+M3?JF^7^H1DHWjr5)jZwlZYkORKDfc%*uEEwZw{16HI@7lMwaGzkPr4ry5Zv zh5CM4fwB>*KD5t~o{IxXBIC&9@478Dy>|Q}0mWx4RYcD9NqBV;DV>N^x2K@jR9gp` mt|OVjC1I1X5=~ggSpEhAkeGBuL*yO+0000 mFileName; private SaveData() { } public static SaveData Instance() { - if (mThis == null) mThis = new SaveData(); - return mThis; + return mThis ??= new SaveData(); } public bool Open(string filename) @@ -50,6 +50,12 @@ public bool SaveAs(string filenname) return Save(); } + public (bool, Exception) SaveAsJson(string filePath) + { + if (mBuffer == null) return (false, null); + return GvasFormat.GvasConverter.Convert2JsonFile(filePath, new MemoryStream(mBuffer, false)); + } + public uint ReadNumber(uint address, uint size) { if (mBuffer == null) return 0; @@ -200,14 +206,12 @@ private void Backup() if (IsReadonlyMode) return; - DateTime now = DateTime.Now; - string path = Path.Combine(Directory.GetCurrentDirectory(), "backup"); + string path = Path.Combine(Directory.GetCurrentDirectory(), "OctopathTraveler Backup"); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } - path = Path.Combine(path, - string.Format("{0:0000}-{1:00}-{2:00} {3:00}-{4:00}", now.Year, now.Month, now.Day, now.Hour, now.Minute)); + path = Path.Combine(path, $"{Path.GetFileName(mFileName)}-{DateTime.Now:yyyy-MM-dd-HH-mm}"); File.WriteAllBytes(path, mBuffer); } }