diff --git a/AssetStudio/AssetStudio.csproj b/AssetStudio/AssetStudio.csproj index d67e90b..c5a850f 100644 --- a/AssetStudio/AssetStudio.csproj +++ b/AssetStudio/AssetStudio.csproj @@ -15,5 +15,11 @@ + + + + PreserveNewest + + diff --git a/AssetStudio/BundleFile.cs b/AssetStudio/BundleFile.cs index be2aa57..35b5d6d 100644 --- a/AssetStudio/BundleFile.cs +++ b/AssetStudio/BundleFile.cs @@ -16,6 +16,7 @@ namespace AssetStudio BlocksInfoAtTheEnd = 0x80, OldWebPluginCompatibility = 0x100, BlockInfoNeedPaddingAtStart = 0x200, + UnityCNEncryption = 0x400 } [Flags] @@ -66,6 +67,7 @@ namespace AssetStudio } private Game Game; + private UnityCN UnityCN; public Header m_Header; private Node[] m_DirectoryInfo; @@ -75,6 +77,7 @@ namespace AssetStudio private bool HasUncompressedDataHash = true; + private bool HasBlockInfoNeedPaddingAtStart = true; public BundleFile(FileReader reader, Game game) { @@ -100,6 +103,10 @@ namespace AssetStudio case "UnityFS": case "ENCR": ReadHeader(reader); + if (game.Type.IsUnityCN()) + { + ReadUnityCN(reader); + } ReadBlocksInfoAndDirectory(reader); using (var blocksStream = CreateBlocksStream(reader.FullPath)) { @@ -312,6 +319,32 @@ namespace AssetStudio } } + private void ReadUnityCN(EndianBinaryReader reader) + { + ArchiveFlags mask; + + var version = ParseVersion(); + //Flag changed it in these versions + if (version[0] < 2020 || //2020 and earlier + (version[0] == 2020 && version[1] == 3 && version[2] <= 34) || //2020.3.34 and earlier + (version[0] == 2021 && version[1] == 3 && version[2] <= 2) || //2021.3.2 and earlier + (version[0] == 2022 && version[1] == 3 && version[2] <= 1)) //2022.3.1 and earlier + { + mask = ArchiveFlags.BlockInfoNeedPaddingAtStart; + HasBlockInfoNeedPaddingAtStart = false; + } + else + { + mask = ArchiveFlags.UnityCNEncryption; + HasBlockInfoNeedPaddingAtStart = true; + } + + if ((m_Header.flags & mask) != 0) + { + UnityCN = new UnityCN(reader); + } + } + private void ReadBlocksInfoAndDirectory(FileReader reader) { byte[] blocksInfoBytes; @@ -403,7 +436,7 @@ namespace AssetStudio }; } } - if ((m_Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0) + if (HasBlockInfoNeedPaddingAtStart && (m_Header.flags & ArchiveFlags.BlockInfoNeedPaddingAtStart) != 0) { reader.AlignStream(16); } @@ -439,14 +472,18 @@ namespace AssetStudio { compressedBytesSpan = Mr0kUtils.Decrypt(compressedBytesSpan, (Mr0k)Game); } - if (Game.Type.IsOPFP()) + if (Game.Type.IsUnityCN() && ((int)blockInfo.flags & 0x100) != 0) { - OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath); + UnityCN.DecryptBlock(compressedBytes, compressedSize, i); } if (Game.Type.IsNetEase() && i == 0) { NetEaseUtils.Decrypt(compressedBytesSpan); } + if (Game.Type.IsOPFP()) + { + OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath); + } var uncompressedSize = (int)blockInfo.uncompressedSize; var uncompressedBytes = BigArrayPool.Shared.Rent(uncompressedSize); var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize); diff --git a/AssetStudio/Crypto/UnityCN.cs b/AssetStudio/Crypto/UnityCN.cs new file mode 100644 index 0000000..7f4df11 --- /dev/null +++ b/AssetStudio/Crypto/UnityCN.cs @@ -0,0 +1,158 @@ +using System; +using System.Text; +using System.Security.Cryptography; + +namespace AssetStudio +{ + public class UnityCN + { + private const string Signature = "#$unity3dchina!@"; + + private static ICryptoTransform Encryptor; + + public byte[] Index = new byte[0x10]; + public byte[] Sub = new byte[0x10]; + + public UnityCN(EndianBinaryReader reader) + { + reader.ReadUInt32(); + + var infoBytes = reader.ReadBytes(0x10); + var infoKey = reader.ReadBytes(0x10); + reader.Position += 1; + + var signatureBytes = reader.ReadBytes(0x10); + var signatureKey = reader.ReadBytes(0x10); + reader.Position += 1; + + DecryptKey(signatureKey, signatureBytes); + + var str = Encoding.UTF8.GetString(signatureBytes); + if (str != Signature) + throw new Exception("Invalid Signature !!"); + + DecryptKey(infoKey, infoBytes); + + infoBytes = infoBytes.ToUInt4Array(); + infoBytes.AsSpan(0, 0x10).CopyTo(Index); + var subBytes = infoBytes.AsSpan(0x10, 0x10); + for (var i = 0; i < subBytes.Length; i++) + { + var idx = (i % 4 * 4) + (i / 4); + Sub[idx] = subBytes[i]; + } + } + + public static bool SetKey(Entry entry) + { + try + { + using (var aes = Aes.Create()) + { + aes.Mode = CipherMode.ECB; + aes.Key = Convert.FromHexString(entry.Key); + + Encryptor = aes.CreateEncryptor(); + } + } + catch (Exception e) + { + Logger.Error($"[UnityCN] Invalid key !!\n{e.Message}"); + return false; + } + return true; + } + + public void DecryptBlock(Span bytes, int size, int index) + { + var offset = 0; + while (offset < size) + { + offset += Decrypt(bytes.Slice(offset), index++, size - offset); + } + } + + private void DecryptKey(byte[] key, byte[] data) + { + if (Encryptor != null) + { + key = Encryptor.TransformFinalBlock(key, 0, key.Length); + for (int i = 0; i < 0x10; i++) + data[i] ^= key[i]; + } + } + + private int DecryptByte(Span bytes, ref int offset, ref int index) + { + var b = Sub[((index >> 2) & 3) + 4] + Sub[index & 3] + Sub[((index >> 4) & 3) + 8] + Sub[((byte)index >> 6) + 12]; + bytes[offset] = (byte)((Index[bytes[offset] & 0xF] - b) & 0xF | 0x10 * (Index[bytes[offset] >> 4] - b)); + b = bytes[offset]; + offset++; + index++; + return b; + } + + private int Decrypt(Span bytes, int index, int remaining) + { + var offset = 0; + + var curByte = DecryptByte(bytes, ref offset, ref index); + var byteHigh = curByte >> 4; + var byteLow = curByte & 0xF; + + if (byteHigh == 0xF) + { + int b; + do + { + b = DecryptByte(bytes, ref offset, ref index); + byteHigh += b; + } while (b == 0xFF); + } + + offset += byteHigh; + + if (offset < remaining) + { + DecryptByte(bytes, ref offset, ref index); + DecryptByte(bytes, ref offset, ref index); + if (byteLow == 0xF) + { + int b; + do + { + b = DecryptByte(bytes, ref offset, ref index); + } while (b == 0xFF); + } + } + + return offset; + } + + public class Entry + { + public string Name { get; private set; } + public string Key { get; private set; } + + public Entry(string name, string key) + { + Name = name; + Key = key; + } + + public bool Validate() + { + var bytes = Convert.FromHexString(Key); + if (bytes.Length != 0x10) + { + Logger.Warning($"[UnityCN] {this} has invalid key, size should be 16 bytes, skipping..."); + return false; + } + + return true; + } + + public override string ToString() => $"{Name} ({Key})"; + } + } +} \ No newline at end of file diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index 82a396c..4c695f3 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -12,6 +12,7 @@ namespace AssetStudio { int index = 0; Games.Add(index++, new(GameType.Normal)); + Games.Add(index++, new(GameType.UnityCN)); Games.Add(index++, new Mhy0(GameType.GI, GIMhy0ShiftRow, GIMhy0Key, GIMhy0Mul, GIExpansionKey, GISBox, GIInitVector, GIInitSeed)); Games.Add(index++, new Mr0k(GameType.GI_Pack, PackExpansionKey, blockKey: PackBlockKey)); Games.Add(index++, new Mr0k(GameType.GI_CB1)); @@ -116,6 +117,7 @@ namespace AssetStudio public enum GameType { Normal, + UnityCN, GI, GI_Pack, GI_CB1, @@ -142,6 +144,7 @@ namespace AssetStudio public static class GameTypes { public static bool IsNormal(this GameType type) => type == GameType.Normal; + public static bool IsUnityCN(this GameType type) => type == GameType.UnityCN; public static bool IsGI(this GameType type) => type == GameType.GI; public static bool IsGIPack(this GameType type) => type == GameType.GI_Pack; public static bool IsGICB1(this GameType type) => type == GameType.GI_CB1; diff --git a/AssetStudio/Keys.json b/AssetStudio/Keys.json new file mode 100644 index 0000000..5f72d4a --- /dev/null +++ b/AssetStudio/Keys.json @@ -0,0 +1,86 @@ +[ + { + "Name": "PGR GLB/KR", + "Key": "6B75726F6B75726F6B75726F6B75726F" + }, + { + "Name": "PGR CN/JP/TW", + "Key": "7935585076714C4F72436F6B57524961" + }, + { + "Name": "Archeland/Kalpa of Universe", + "Key": "426C61636B4A61636B50726F6A656374" + }, + { + "Name": "Archeland 1.1.14", + "Key": "50726F6A65637441726368654C616E64" + }, + { + "Name": "Neural Cloud", + "Key": "31636162383436663532393031633965" + }, + { + "Name": "Higan: Eruthyll", + "Key": "45317832633361346C35693662377572" + }, + { + "Name": "White Chord", + "Key": "79756C6F6E6731383638676E6F6C7579" + }, + { + "Name": "Mecharashi", + "Key": "33384338334631333245374637413041" + }, + { + "Name": "Castlevania: Moon Night Fantasy", + "Key": "31323334353637383132333435363738" + }, + { + "Name": "Huā Yì Shān Xīn Zhī Yuè", + "Key": "494E484A6E68647970716B3534377864" + }, + { + "Name": "Doula Continent", + "Key": "52346366773339474644326661785756" + }, + { + "Name": "Bless Global", + "Key": "6C6F6E67747567616D652E796A66623F" + }, + { + "Name": "Starside", + "Key": "41394A3542384D4A50554D3539464B57" + }, + { + "Name": "Resonance Soltice", + "Key": "5265736F6E616E63655265626F726E52" + }, + { + "Name": "Oblivion Override", + "Key": "7179666D6F6F6E323331323433343532" + }, + { + "Name": "Dawnlands", + "Key": "636F6465737339353237636F64657373" + }, + { + "Name": "BB", + "Key": "5F6C4E3F3A3F233F3F3F3F663F1A3F3F" + }, + { + "Name": "Dynasty Legends 2", + "Key": "746169686567616D6573323032323032" + }, + { + "Name": "Evernight CN", + "Key": "68687878747478736868787874747873" + }, + { + "Name": "Xintianlong Babu", + "Key": "61323562623133346363326464333265" + }, + { + "Name": "Frostpunk: Beyond the Ice", + "Key": "7368756978696E673838383838383838" + } +] diff --git a/AssetStudio/UnityCNManager.cs b/AssetStudio/UnityCNManager.cs new file mode 100644 index 0000000..ccfb420 --- /dev/null +++ b/AssetStudio/UnityCNManager.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace AssetStudio +{ + public static class UnityCNManager + { + public const string KeysFileName = "Keys.json"; + + private static List Entries = new List(); + + static UnityCNManager() + { + var str = File.ReadAllText(KeysFileName); + Entries = JsonConvert.DeserializeObject>(str); + } + + public static void SaveEntries(List entries) + { + Entries.Clear(); + Entries.AddRange(entries); + + var str = JsonConvert.SerializeObject(Entries); + File.WriteAllText(KeysFileName, str); + } + + public static void SetKey(int index) + { + if (TryGetEntry(index, out var unityCN)) + { + if (UnityCN.SetKey(unityCN)) + { + Logger.Info($"[UnityCN] Selected Key is {unityCN}"); + } + else + { + Logger.Info($"[UnityCN] No Key is selected !!"); + } + } + else + { + Logger.Error("Invalid Key !!"); + Logger.Warning(GetEntries().Select(x => x.ToString()).ToString()); + } + } + + public static bool TryGetEntry(int index, out UnityCN.Entry key) + { + try + { + if (index < 0 || index > Entries.Count) + { + throw new ArgumentOutOfRangeException(); + } + + key = Entries[index]; + } + catch(Exception e) + { + Logger.Error($"[UnityCN] Invalid Index, check if list is not empty !!\n{e.Message}"); + key = null; + return false; + } + + return true; + } + public static UnityCN.Entry[] GetEntries() => Entries.ToArray(); + + public new static string ToString() => string.Join("\n", GetEntries().Select((x, i) => $"{i}: {x.Name}")); + } +} diff --git a/AssetStudioCLI/Components/CommandLine.cs b/AssetStudioCLI/Components/CommandLine.cs index f82f3a1..ec05cba 100644 --- a/AssetStudioCLI/Components/CommandLine.cs +++ b/AssetStudioCLI/Components/CommandLine.cs @@ -26,6 +26,7 @@ namespace AssetStudioCLI optionsBinder.NameFilter, optionsBinder.ContainerFilter, optionsBinder.GameName, + optionsBinder.KeyIndex, optionsBinder.MapOp, optionsBinder.MapType, optionsBinder.MapName, @@ -50,6 +51,7 @@ namespace AssetStudioCLI public Regex[] NameFilter { get; set; } public Regex[] ContainerFilter { get; set; } public string GameName { get; set; } + public int KeyIndex { get; set; } public MapOpType MapOp { get; set; } public ExportListType MapType { get; set; } public string MapName { get; set; } @@ -69,6 +71,7 @@ namespace AssetStudioCLI public readonly Option NameFilter; public readonly Option ContainerFilter; public readonly Option GameName; + public readonly Option KeyIndex; public readonly Option MapOp; public readonly Option MapType; public readonly Option MapName; @@ -88,6 +91,7 @@ namespace AssetStudioCLI NameFilter = new Option("--names", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify name regex filter(s).") { AllowMultipleArgumentsPerToken = true }; ContainerFilter = new Option("--containers", result => result.Tokens.Select(x => new Regex(x.Value, RegexOptions.IgnoreCase)).ToArray(), false, "Specify container regex filter(s).") { AllowMultipleArgumentsPerToken = true }; GameName = new Option("--game", $"Specify Game.") { IsRequired = true }; + KeyIndex = new Option("--key_index", "Specify key index.") { ArgumentHelpName = UnityCNManager.ToString() }; MapOp = new Option("--map_op", "Specify which map to build."); MapType = new Option("--map_type", "AssetMap output type."); MapName = new Option("--map_name", () => "assets_map", "Specify AssetMap file name."); @@ -141,6 +145,7 @@ namespace AssetStudioCLI GroupAssetsType.SetDefaultValue(AssetGroupOption.ByType); MapOp.SetDefaultValue(MapOpType.None); MapType.SetDefaultValue(ExportListType.XML); + KeyIndex.SetDefaultValue(0); } public void FilterValidator(OptionResult result) @@ -174,6 +179,7 @@ namespace AssetStudioCLI NameFilter = bindingContext.ParseResult.GetValueForOption(NameFilter), ContainerFilter = bindingContext.ParseResult.GetValueForOption(ContainerFilter), GameName = bindingContext.ParseResult.GetValueForOption(GameName), + KeyIndex = bindingContext.ParseResult.GetValueForOption(KeyIndex), MapOp = bindingContext.ParseResult.GetValueForOption(MapOp), MapType = bindingContext.ParseResult.GetValueForOption(MapType), MapName = bindingContext.ParseResult.GetValueForOption(MapName), diff --git a/AssetStudioCLI/Program.cs b/AssetStudioCLI/Program.cs index 5ef8360..5cf517f 100644 --- a/AssetStudioCLI/Program.cs +++ b/AssetStudioCLI/Program.cs @@ -25,6 +25,19 @@ namespace AssetStudioCLI return; } + if (game.Type.IsUnityCN()) + { + if (!UnityCNManager.TryGetEntry(o.KeyIndex, out var unityCN)) + { + Console.WriteLine("Invalid key index !!"); + Console.WriteLine($"Available Options: \n{UnityCNManager.ToString()}"); + return; + } + + UnityCN.SetKey(unityCN); + Logger.Info($"[UnityCN] Selected Key is {unityCN}"); + } + Studio.Game = game; Logger.Default = new ConsoleLogger(); AssetsHelper.Minimal = Settings.Default.minimalAssetMap; diff --git a/AssetStudioGUI/App.config b/AssetStudioGUI/App.config index 1f170a6..b123ea2 100644 --- a/AssetStudioGUI/App.config +++ b/AssetStudioGUI/App.config @@ -109,6 +109,9 @@ False + + 0 + \ No newline at end of file diff --git a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs index 477543a..a2cf5ea 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.Designer.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.Designer.cs @@ -44,12 +44,15 @@ namespace AssetStudioGUI abortStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); optionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); displayAll = new System.Windows.Forms.ToolStripMenuItem(); + toolStripSeparator10 = new System.Windows.Forms.ToolStripSeparator(); enablePreview = new System.Windows.Forms.ToolStripMenuItem(); enableModelPreview = new System.Windows.Forms.ToolStripMenuItem(); modelsOnly = new System.Windows.Forms.ToolStripMenuItem(); + toolStripSeparator11 = new System.Windows.Forms.ToolStripSeparator(); + displayInfo = new System.Windows.Forms.ToolStripMenuItem(); enableResolveDependencies = new System.Windows.Forms.ToolStripMenuItem(); skipContainer = new System.Windows.Forms.ToolStripMenuItem(); - displayInfo = new System.Windows.Forms.ToolStripMenuItem(); + toolStripSeparator12 = new System.Windows.Forms.ToolStripSeparator(); toolStripMenuItem14 = new System.Windows.Forms.ToolStripMenuItem(); specifyUnityVersion = new System.Windows.Forms.ToolStripTextBox(); toolStripMenuItem18 = new System.Windows.Forms.ToolStripMenuItem(); @@ -164,9 +167,8 @@ namespace AssetStudioGUI exportAnimatorwithselectedAnimationClipMenuItem = new System.Windows.Forms.ToolStripMenuItem(); goToSceneHierarchyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); showOriginalFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - toolStripSeparator10 = new System.Windows.Forms.ToolStripSeparator(); - toolStripSeparator11 = new System.Windows.Forms.ToolStripSeparator(); - toolStripSeparator12 = new System.Windows.Forms.ToolStripSeparator(); + specifyUnityCNKey = new System.Windows.Forms.ToolStripMenuItem(); + toolStripSeparator13 = new System.Windows.Forms.ToolStripSeparator(); menuStrip1.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); splitContainer1.Panel1.SuspendLayout(); @@ -258,7 +260,7 @@ namespace AssetStudioGUI // // optionsToolStripMenuItem // - optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { displayAll, toolStripSeparator10, enablePreview, enableModelPreview, modelsOnly, toolStripSeparator11, displayInfo, enableResolveDependencies, skipContainer, toolStripSeparator12, toolStripMenuItem14, toolStripMenuItem18, toolStripMenuItem19, showExpOpt }); + optionsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { displayAll, toolStripSeparator10, enablePreview, enableModelPreview, modelsOnly, toolStripSeparator11, displayInfo, enableResolveDependencies, skipContainer, toolStripSeparator12, toolStripMenuItem14, specifyUnityCNKey, toolStripSeparator13, toolStripMenuItem18, toolStripMenuItem19, showExpOpt }); optionsToolStripMenuItem.Name = "optionsToolStripMenuItem"; optionsToolStripMenuItem.Size = new System.Drawing.Size(61, 20); optionsToolStripMenuItem.Text = "Options"; @@ -272,6 +274,11 @@ namespace AssetStudioGUI displayAll.ToolTipText = "Check this option will display all types assets. Not extractable assets can export the RAW file."; displayAll.CheckedChanged += displayAll_CheckedChanged; // + // toolStripSeparator10 + // + toolStripSeparator10.Name = "toolStripSeparator10"; + toolStripSeparator10.Size = new System.Drawing.Size(222, 6); + // // enablePreview // enablePreview.Checked = true; @@ -299,6 +306,22 @@ namespace AssetStudioGUI modelsOnly.Text = "Filter models only"; modelsOnly.CheckedChanged += modelsOnly_CheckedChanged; // + // toolStripSeparator11 + // + toolStripSeparator11.Name = "toolStripSeparator11"; + toolStripSeparator11.Size = new System.Drawing.Size(222, 6); + // + // displayInfo + // + displayInfo.Checked = true; + displayInfo.CheckOnClick = true; + displayInfo.CheckState = System.Windows.Forms.CheckState.Checked; + displayInfo.Name = "displayInfo"; + displayInfo.Size = new System.Drawing.Size(225, 22); + displayInfo.Text = "Display asset information"; + displayInfo.ToolTipText = "Toggle the overlay that shows information about each asset, eg. image size, format, audio bitrate, etc."; + displayInfo.CheckedChanged += displayAssetInfo_Check; + // // enableResolveDependencies // enableResolveDependencies.Checked = true; @@ -316,20 +339,13 @@ namespace AssetStudioGUI skipContainer.Name = "skipContainer"; skipContainer.Size = new System.Drawing.Size(225, 22); skipContainer.Text = "Skip container recovery"; - skipContainer.ToolTipText = "Skips the container recovery step.\nImproves loading when dealing with a large num" + - "ber of files."; + skipContainer.ToolTipText = "Skips the container recovery step.\nImproves loading when dealing with a large number of files."; skipContainer.CheckedChanged += skipContainer_CheckedChanged; // - // displayInfo + // toolStripSeparator12 // - displayInfo.Checked = true; - displayInfo.CheckOnClick = true; - displayInfo.CheckState = System.Windows.Forms.CheckState.Checked; - displayInfo.Name = "displayInfo"; - displayInfo.Size = new System.Drawing.Size(225, 22); - displayInfo.Text = "Display asset information"; - displayInfo.ToolTipText = "Toggle the overlay that shows information about each asset, eg. image size, format, audio bitrate, etc."; - displayInfo.CheckedChanged += displayAssetInfo_Check; + toolStripSeparator12.Name = "toolStripSeparator12"; + toolStripSeparator12.Size = new System.Drawing.Size(222, 6); // // toolStripMenuItem14 // @@ -1292,20 +1308,17 @@ namespace AssetStudioGUI showOriginalFileToolStripMenuItem.Visible = false; showOriginalFileToolStripMenuItem.Click += showOriginalFileToolStripMenuItem_Click; // - // toolStripSeparator10 + // specifyUnityCNKey // - toolStripSeparator10.Name = "toolStripSeparator10"; - toolStripSeparator10.Size = new System.Drawing.Size(222, 6); + specifyUnityCNKey.Name = "specifyUnityCNKey"; + specifyUnityCNKey.Size = new System.Drawing.Size(225, 22); + specifyUnityCNKey.Text = "Specify UnityCN Key"; + specifyUnityCNKey.Click += specifyUnityCNKey_Click; // - // toolStripSeparator11 + // toolStripSeparator13 // - toolStripSeparator11.Name = "toolStripSeparator11"; - toolStripSeparator11.Size = new System.Drawing.Size(222, 6); - // - // toolStripSeparator12 - // - toolStripSeparator12.Name = "toolStripSeparator12"; - toolStripSeparator12.Size = new System.Drawing.Size(222, 6); + toolStripSeparator13.Name = "toolStripSeparator13"; + toolStripSeparator13.Size = new System.Drawing.Size(222, 6); // // AssetStudioGUIForm // @@ -1491,6 +1504,8 @@ namespace AssetStudioGUI private System.Windows.Forms.ToolStripSeparator toolStripSeparator10; private System.Windows.Forms.ToolStripSeparator toolStripSeparator11; private System.Windows.Forms.ToolStripSeparator toolStripSeparator12; + private System.Windows.Forms.ToolStripMenuItem specifyUnityCNKey; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator13; } } diff --git a/AssetStudioGUI/AssetStudioGUIForm.cs b/AssetStudioGUI/AssetStudioGUIForm.cs index b1db9ed..9ad095a 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.cs +++ b/AssetStudioGUI/AssetStudioGUIForm.cs @@ -157,6 +157,7 @@ namespace AssetStudioGUI Logger.Info($"Target Game type is {Studio.Game.Type}"); MapNameComboBox.SelectedIndexChanged += new EventHandler(specifyNameComboBox_SelectedIndexChanged); + UnityCNManager.SetKey(Properties.Settings.Default.selectedUnityCNKey); } private void AssetStudioGUIForm_DragEnter(object sender, DragEventArgs e) { @@ -274,6 +275,10 @@ namespace AssetStudioGUI { productName = Studio.Game.Name; } + else if (Studio.Game.Type.IsUnityCN() && UnityCNManager.TryGetEntry(Properties.Settings.Default.selectedUnityCNKey, out var unityCN)) + { + productName = unityCN.Name; + } else { productName = "no productName"; @@ -2300,6 +2305,12 @@ namespace AssetStudioGUI assetBrowser.Show(); } + private void specifyUnityCNKey_Click(object sender, EventArgs e) + { + var unitycn = new UnityCNForm(); + unitycn.Show(); + } + #region FMOD private void FMODinit() { diff --git a/AssetStudioGUI/AssetStudioGUIForm.resx b/AssetStudioGUI/AssetStudioGUIForm.resx index de2aa35..500ca4f 100644 --- a/AssetStudioGUI/AssetStudioGUIForm.resx +++ b/AssetStudioGUI/AssetStudioGUIForm.resx @@ -120,9 +120,6 @@ 312, 17 - - 432, 17 - abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWYZ 1234567890.:,;'\"(!?)+-*/= @@ -141,6 +138,9 @@ The quick brown fox jumps over the lazy dog. 1234567890 The quick brown fox jumps over the lazy dog. 1234567890 + + 432, 17 + 553, 17 diff --git a/AssetStudioGUI/ExportOptions.resx b/AssetStudioGUI/ExportOptions.resx index 5cb967e..ea96e60 100644 --- a/AssetStudioGUI/ExportOptions.resx +++ b/AssetStudioGUI/ExportOptions.resx @@ -120,6 +120,12 @@ 17, 17 + + 17, 17 + + + 162, 17 + 162, 17 @@ -129,6 +135,9 @@ 273, 17 + + 404, 17 + 57 diff --git a/AssetStudioGUI/Properties/Settings.Designer.cs b/AssetStudioGUI/Properties/Settings.Designer.cs index 94084cb..cec55b5 100644 --- a/AssetStudioGUI/Properties/Settings.Designer.cs +++ b/AssetStudioGUI/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace AssetStudioGUI.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.5.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.6.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -442,5 +442,16 @@ namespace AssetStudioGUI.Properties { this["enableModelPreview"] = value; } } + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public int selectedUnityCNKey { + get { + return ((int)(this["selectedUnityCNKey"])); + } + set { + this["selectedUnityCNKey"] = value; + } + } } } diff --git a/AssetStudioGUI/Properties/Settings.settings b/AssetStudioGUI/Properties/Settings.settings index a26d555..e473a34 100644 --- a/AssetStudioGUI/Properties/Settings.settings +++ b/AssetStudioGUI/Properties/Settings.settings @@ -107,5 +107,8 @@ False + + 0 + \ No newline at end of file diff --git a/AssetStudioGUI/UnityCNForm.Designer.cs b/AssetStudioGUI/UnityCNForm.Designer.cs new file mode 100644 index 0000000..00816e8 --- /dev/null +++ b/AssetStudioGUI/UnityCNForm.Designer.cs @@ -0,0 +1,94 @@ +namespace AssetStudioGUI +{ + partial class UnityCNForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.specifyUnityCNList = new System.Windows.Forms.DataGridView(); + this.NameField = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.KeyField = new System.Windows.Forms.DataGridViewTextBoxColumn(); + ((System.ComponentModel.ISupportInitialize)(this.specifyUnityCNList)).BeginInit(); + this.SuspendLayout(); + // + // specifyUnityCNList + // + this.specifyUnityCNList.AllowUserToResizeColumns = false; + this.specifyUnityCNList.AllowUserToResizeRows = false; + this.specifyUnityCNList.ClipboardCopyMode = System.Windows.Forms.DataGridViewClipboardCopyMode.EnableWithoutHeaderText; + this.specifyUnityCNList.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.specifyUnityCNList.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.NameField, + this.KeyField}); + this.specifyUnityCNList.Dock = System.Windows.Forms.DockStyle.Fill; + this.specifyUnityCNList.Location = new System.Drawing.Point(0, 0); + this.specifyUnityCNList.MultiSelect = false; + this.specifyUnityCNList.Name = "specifyUnityCNList"; + this.specifyUnityCNList.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.DisableResizing; + this.specifyUnityCNList.RowTemplate.Height = 25; + this.specifyUnityCNList.Size = new System.Drawing.Size(408, 204); + this.specifyUnityCNList.TabIndex = 0; + this.specifyUnityCNList.RowHeaderMouseDoubleClick += new System.Windows.Forms.DataGridViewCellMouseEventHandler(this.specifyUnityCNList_RowHeaderMouseDoubleClick); + // + // NameField + // + this.NameField.HeaderText = "Name"; + this.NameField.Name = "NameField"; + this.NameField.Width = 140; + // + // KeyField + // + this.KeyField.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill; + this.KeyField.HeaderText = "Key"; + this.KeyField.Name = "KeyField"; + // + // UnityCNForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(408, 204); + this.Controls.Add(this.specifyUnityCNList); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Fixed3D; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "UnityCNForm"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "UnityCNForm"; + this.TopMost = true; + ((System.ComponentModel.ISupportInitialize)(this.specifyUnityCNList)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.DataGridView specifyUnityCNList; + private System.Windows.Forms.DataGridViewTextBoxColumn NameField; + private System.Windows.Forms.DataGridViewTextBoxColumn KeyField; + } +} \ No newline at end of file diff --git a/AssetStudioGUI/UnityCNForm.cs b/AssetStudioGUI/UnityCNForm.cs new file mode 100644 index 0000000..d8956fa --- /dev/null +++ b/AssetStudioGUI/UnityCNForm.cs @@ -0,0 +1,77 @@ +using System; +using System.Windows.Forms; +using System.Collections.Generic; +using AssetStudio; +using System.Linq; + +namespace AssetStudioGUI +{ + public partial class UnityCNForm : Form + { + public UnityCNForm() + { + InitializeComponent(); + + var keys = UnityCNManager.GetEntries(); + + for (int i = 0; i < keys.Length; i++) + { + var key = keys[i]; + var rowIdx = specifyUnityCNList.Rows.Add(); + + specifyUnityCNList.Rows[rowIdx].Cells["NameField"].Value = key.Name; + specifyUnityCNList.Rows[rowIdx].Cells["KeyField"].Value = key.Key; + } + + var index = Properties.Settings.Default.selectedUnityCNKey; + if (index >= specifyUnityCNList.RowCount) + { + index = 0; + } + specifyUnityCNList.CurrentCell = specifyUnityCNList.Rows[index].Cells[0]; + } + + private void specifyUnityCNList_RowHeaderMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e) + { + var keys = new List(); + for (int i = specifyUnityCNList.Rows.Count - 1; i >= 0; i--) + { + var row = specifyUnityCNList.Rows[i]; + var name = row.Cells["NameField"].Value as string; + var key = row.Cells["KeyField"].Value as string; + + if (!(string.IsNullOrEmpty(name) || string.IsNullOrEmpty(key))) + { + var unityCN = new UnityCN.Entry(name, key); + + if (unityCN.Validate()) + { + keys.Add(unityCN); + continue; + } + } + + if (specifyUnityCNList.CurrentCell.RowIndex == row.Index) + { + var previousRow = specifyUnityCNList.Rows.Cast().ElementAtOrDefault(i - 1); + if (previousRow != null) + { + specifyUnityCNList.CurrentCell = previousRow.Cells[0]; + } + } + if (i != specifyUnityCNList.RowCount - 1) + { + specifyUnityCNList.Rows.RemoveAt(i); + } + } + UnityCNManager.SaveEntries(keys.Reverse().ToList()); + UnityCNManager.SetKey(specifyUnityCNList.CurrentRow.Index); + + Properties.Settings.Default.selectedUnityCNKey = specifyUnityCNList.CurrentRow.Index; + Properties.Settings.Default.Save(); + + DialogResult = DialogResult.OK; + Close(); + } + } +} diff --git a/AssetStudioGUI/UnityCNForm.resx b/AssetStudioGUI/UnityCNForm.resx new file mode 100644 index 0000000..622ff10 --- /dev/null +++ b/AssetStudioGUI/UnityCNForm.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + True + + + True + + \ No newline at end of file