From d875f0882d80ba451c4ba4ff5846ffa27595cd55 Mon Sep 17 00:00:00 2001 From: grzybeek Date: Mon, 1 Apr 2024 00:58:30 +0200 Subject: [PATCH] implement singleplayer resource fix drawables with skin generating ydd as _u --- .../GameFiles/MetaTypes/Rbf.cs | 2 + .../CodeWalker.Core/GameFiles/RpfFile.cs | 169 +++++- .../ModernLabel/ModernLabelRadioButton.xaml | 56 ++ .../ModernLabelRadioButton.xaml.cs | 63 +++ grzyClothTool/Helpers/BuildResourceHelper.cs | 512 ++++++++++++++---- grzyClothTool/Helpers/ImgHelper.cs | 4 +- grzyClothTool/Models/Addon.cs | 4 +- grzyClothTool/Models/GDrawable.cs | 2 + grzyClothTool/Views/BuildWindow.xaml | 21 +- grzyClothTool/Views/BuildWindow.xaml.cs | 79 ++- 10 files changed, 786 insertions(+), 126 deletions(-) create mode 100644 grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml create mode 100644 grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml.cs diff --git a/CodeWalker/CodeWalker.Core/GameFiles/MetaTypes/Rbf.cs b/CodeWalker/CodeWalker.Core/GameFiles/MetaTypes/Rbf.cs index f2f2e55..4ab95bf 100644 --- a/CodeWalker/CodeWalker.Core/GameFiles/MetaTypes/Rbf.cs +++ b/CodeWalker/CodeWalker.Core/GameFiles/MetaTypes/Rbf.cs @@ -44,6 +44,8 @@ namespace CodeWalker.GameFiles public List descriptors { get; set; } public Dictionary outDescriptors { get; private set; } = new Dictionary(); + public string SingleplayerFileName { get; set; } + public void Load(byte[] data) { diff --git a/CodeWalker/CodeWalker.Core/GameFiles/RpfFile.cs b/CodeWalker/CodeWalker.Core/GameFiles/RpfFile.cs index 9746bf9..8effcac 100644 --- a/CodeWalker/CodeWalker.Core/GameFiles/RpfFile.cs +++ b/CodeWalker/CodeWalker.Core/GameFiles/RpfFile.cs @@ -967,18 +967,154 @@ public static byte[] CompressBytes(byte[] data) //TODO: refactor this with Resou } } + public static RpfFileEntry CreateFile(RpfDirectoryEntry dir, string name, byte[] data, bool overwrite = true) + { + string namel = name.ToLowerInvariant(); + if (overwrite) + { + foreach (var exfile in dir.Files) + { + if (exfile.NameLower == namel) + { + //file already exists. delete the existing one first! + //this should probably be optimised to just replace the existing one... + //TODO: investigate along with ExploreForm.ReplaceSelected() + DeleteEntry(exfile); + break; + } + } + } + //else fail if already exists..? items with the same name allowed? + RpfFile parent = dir.File; + string fpath = parent.GetPhysicalFilePath(); + string rpath = dir.Path + "\\" + namel; + if (!File.Exists(fpath)) + { + throw new Exception("Root RPF file " + fpath + " does not exist!"); + } + RpfFileEntry entry = null; + uint len = (uint)data.Length; + bool isrpf = false; + bool isawc = false; + uint hdr = 0; + if (len >= 16) + { + hdr = BitConverter.ToUInt32(data, 0); + } + + if (hdr == 0x37435352) //'RSC7' + { + //RSC header is present... import as resource + var rentry = new RpfResourceFileEntry(); + var version = BitConverter.ToUInt32(data, 4); + rentry.SystemFlags = BitConverter.ToUInt32(data, 8); + rentry.GraphicsFlags = BitConverter.ToUInt32(data, 12); + rentry.FileSize = len; + if (len >= 0xFFFFFF) + { + //just....why + //FileSize = (buf[7] << 0) | (buf[14] << 8) | (buf[5] << 16) | (buf[2] << 24); + data[7] = (byte)((len >> 0) & 0xFF); + data[14] = (byte)((len >> 8) & 0xFF); + data[5] = (byte)((len >> 16) & 0xFF); + data[2] = (byte)((len >> 24) & 0xFF); + } + + entry = rentry; + } + if (namel.EndsWith(".rpf") && (hdr == 0x52504637)) //'RPF7' + { + isrpf = true; + } + if (namel.EndsWith(".awc")) + { + isawc = true; + } + if (entry == null) + { + //no RSC7 header present, import as a binary file. + var compressed = (isrpf || isawc) ? data : CompressBytes(data); + var bentry = new RpfBinaryFileEntry(); + bentry.EncryptionType = 0;//TODO: binary encryption + bentry.IsEncrypted = false; + bentry.FileUncompressedSize = (uint)data.Length; + bentry.FileSize = (isrpf || isawc) ? 0 : (uint)compressed.Length; + if (bentry.FileSize > 0xFFFFFF) + { + bentry.FileSize = 0; + compressed = data; + //can't compress?? since apparently FileSize>0 means compressed... + } + data = compressed; + entry = bentry; + } + entry.Parent = dir; + entry.File = parent; + entry.Path = rpath; + entry.Name = name; + entry.NameLower = name.ToLowerInvariant(); + entry.NameHash = JenkHash.GenHash(name); + entry.ShortNameHash = JenkHash.GenHash(entry.GetShortNameLower()); + foreach (var exfile in dir.Files) + { + if (exfile.NameLower == entry.NameLower) + { + throw new Exception("File \"" + entry.Name + "\" already exists!"); + } + } + + + + dir.Files.Add(entry); + + + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + parent.InsertFileSpace(bw, entry); + long bbeg = parent.StartPos + (entry.FileOffset * 512); + long bend = bbeg + (GetBlockCount(entry.GetFileSize()) * 512); + fstream.Position = bbeg; + fstream.Write(data, 0, data.Length); + WritePadding(fstream, bend); //write 0's until the end of the block. + } + } + + + if (isrpf) + { + //importing a raw RPF archive. create the new RpfFile object, and read its headers etc. + RpfFile file = new RpfFile(name, rpath, data.LongLength); + file.Parent = parent; + file.ParentFileEntry = entry as RpfBinaryFileEntry; + file.StartPos = parent.StartPos + (entry.FileOffset * 512); + parent.Children.Add(file); + + using (var fstream = File.OpenRead(fpath)) + { + using (var br = new BinaryReader(fstream)) + { + fstream.Position = file.StartPos; + file.ScanStructure(br, null, null); + } + } + } + + return entry; + } private void WriteHeader(BinaryWriter bw) { @@ -1506,10 +1642,11 @@ public static RpfFile CreateNew(string gtafolder, string relpath, RpfEncryption fpath = fpath.EndsWith("\\") ? fpath : fpath + "\\"; fpath = fpath + relpath; - if (File.Exists(fpath)) - { - throw new Exception("File " + fpath + " already exists!"); - } + //if (File.Exists(fpath)) + //{ + + // throw new Exception("File " + fpath + " already exists!"); + //} File.Create(fpath).Dispose(); //just write a placeholder, will fill it out later @@ -1526,6 +1663,30 @@ public static RpfFile CreateNew(string gtafolder, string relpath, RpfEncryption return file; } + public static RpfDirectoryEntry CreateDirectory(RpfDirectoryEntry rpfRoot, string directoryName) + { + RpfDirectoryEntry newDirectory = new RpfDirectoryEntry + { + Name = directoryName, + NameLower = directoryName.ToLowerInvariant(), + Path = rpfRoot.Path + "\\" + directoryName.ToLowerInvariant(), + File = rpfRoot.File, + Parent = rpfRoot + }; + + rpfRoot.Directories.Add(newDirectory); + + using (var fstream = File.Open(rpfRoot.File.GetPhysicalFilePath(), FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + rpfRoot.File.EnsureAllEntries(); + rpfRoot.File.WriteHeader(bw); + } + } + return newDirectory; + } + public static RpfFile CreateNew(RpfDirectoryEntry dir, string name, RpfEncryption encryption = RpfEncryption.OPEN) { //create a new empty RPF inside the given parent RPF directory. diff --git a/grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml b/grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml new file mode 100644 index 0000000..f283514 --- /dev/null +++ b/grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml.cs b/grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml.cs new file mode 100644 index 0000000..9b47084 --- /dev/null +++ b/grzyClothTool/Controls/ModernLabel/ModernLabelRadioButton.xaml.cs @@ -0,0 +1,63 @@ +using System.Windows; +using System.Windows.Controls; + +namespace grzyClothTool.Controls +{ + public partial class ModernLabelRadioButton : UserControl + { + public ModernLabelRadioButton() + { + InitializeComponent(); + } + + public static readonly DependencyProperty GroupNameProperty = + DependencyProperty.Register("GroupName", typeof(string), typeof(ModernLabelRadioButton), new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty LabelProperty = + DependencyProperty.Register("Label", typeof(string), typeof(ModernLabelRadioButton), new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty IsCheckedProperty = + DependencyProperty.Register("IsChecked", typeof(bool?), typeof(ModernLabelRadioButton), new PropertyMetadata(false)); + + public static readonly RoutedEvent RadioBtnSelectEvent = EventManager.RegisterRoutedEvent( + "BtnSelectEvent", + RoutingStrategy.Bubble, + typeof(RoutedEventHandler), + typeof(ModernLabelRadioButton) + ); + + public event RoutedEventHandler MyBtnSelectEvent + { + add { AddHandler(RadioBtnSelectEvent, value); } + remove { RemoveHandler(RadioBtnSelectEvent, value); } + } + + public string GroupName + { + get { return (string)GetValue(GroupNameProperty); } + set { SetValue(GroupNameProperty, value); } + } + + public bool? IsChecked + { + get { return (bool?)GetValue(IsCheckedProperty); } + set { SetValue(IsCheckedProperty, value); } + } + + public string Label + { + get { return (string)GetValue(LabelProperty); } + set { SetValue(LabelProperty, value); } + } + + private void RadioButton_Change(object sender, RoutedEventArgs e) + { + BtnSelectEventArgs args = new(RadioBtnSelectEvent); + RaiseEvent(args); + } + + private class BtnSelectEventArgs(RoutedEvent routedEvent) : RoutedEventArgs(routedEvent) + { + } + } +} \ No newline at end of file diff --git a/grzyClothTool/Helpers/BuildResourceHelper.cs b/grzyClothTool/Helpers/BuildResourceHelper.cs index 9eddb24..c9f97ab 100644 --- a/grzyClothTool/Helpers/BuildResourceHelper.cs +++ b/grzyClothTool/Helpers/BuildResourceHelper.cs @@ -26,8 +26,6 @@ public BuildResourceHelper(string name, string path, int totalCount) _buildPath = path; shouldUseNumber = totalCount > 1; - - Directory.CreateDirectory(Path.Combine(_buildPath, "stream")); } public void SetAddon(Addon addon) @@ -46,6 +44,403 @@ public string GetProjectName(int? number = null) return shouldUseNumber ? $"{_projectName}_{number:D2}" : _projectName; } + + #region FiveM + + + public async Task BuildFiveMFilesAsync(bool isMale, byte[] ymtBytes, int counter) + { + var pedName = GetPedName(isMale); + var projectName = GetProjectName(counter); + + var drawables = _addon.Drawables.Where(x => x.Sex == isMale).ToList(); + var drawableGroups = drawables.Select((x, i) => new { Index = i, Value = x }) + .GroupBy(x => x.Value.Number / 128) + .Select(x => x.Select(v => v.Value).ToList()) + .ToList(); + + // Prepare all directory paths first to minimize file system access + var genderFolderName = isMale ? "[male]" : "[female]"; + var directoriesToEnsure = drawableGroups.SelectMany(g => g.Select(d => Path.Combine(_buildPath, "stream", genderFolderName, d.TypeName))).Distinct(); + foreach (var dir in directoriesToEnsure) + { + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + } + + var fileOperations = new List(); + + foreach (var group in drawableGroups) + { + var ymtPath = Path.Combine(_buildPath, "stream", $"{pedName}_{projectName}.ymt"); + fileOperations.Add(File.WriteAllBytesAsync(ymtPath, ymtBytes)); + + foreach (var d in group) + { + var drawablePedName = d.IsProp ? $"{pedName}_p" : pedName; + var folderPath = Path.Combine(_buildPath, "stream", genderFolderName, d.TypeName); + var prefix = RemoveInvalidChars($"{drawablePedName}_{projectName}^"); + var finalPath = Path.Combine(folderPath, $"{prefix}{d.Name}{Path.GetExtension(d.FilePath)}"); + fileOperations.Add(FileHelper.CopyAsync(d.FilePath, finalPath)); + + foreach (var t in d.Textures) + { + var displayName = RemoveInvalidChars(t.DisplayName); + var finalTexPath = Path.Combine(folderPath, $"{prefix}{displayName}{Path.GetExtension(t.FilePath)}"); + if (t.IsOptimizedDuringBuild) + { + var optimizedBytes = await ImgHelper.Optimize(t); + fileOperations.Add(File.WriteAllBytesAsync(finalTexPath, optimizedBytes)); + } + else + { + fileOperations.Add(FileHelper.CopyAsync(t.FilePath, finalTexPath)); + } + } + } + } + + // Run all file operations + await Task.WhenAll(fileOperations); + await Task.Run(() => + { + var generated = GenerateCreatureMetadata(drawables); + generated?.Save(_buildPath + "/stream/mp_creaturemetadata_" + GetGenderLetter(isMale) + "_" + projectName + ".ymt"); + } + ); + } + + public void BuildFxManifest(List metaFiles) + { + StringBuilder contentBuilder = new(); + contentBuilder.AppendLine("-- This resource was generated by grzyClothTool :)"); + contentBuilder.AppendLine("-- discord.gg/HCQutNhxWt"); + contentBuilder.AppendLine(); + contentBuilder.AppendLine("fx_version 'cerulean'"); + contentBuilder.AppendLine("game 'gta5'"); + contentBuilder.AppendLine("author 'grzyClothTool'"); + contentBuilder.AppendLine(); + contentBuilder.AppendLine("files {"); + + string filesSection = string.Join(",\n ", metaFiles.Select(f => $"'{f}'")); + contentBuilder.AppendLine($" {filesSection}"); + + contentBuilder.AppendLine("}"); + contentBuilder.AppendLine(); + + foreach (var file in metaFiles) + { + contentBuilder.AppendLine($"data_file 'SHOP_PED_APPAREL_META_FILE' '{file}'"); + } + + var finalPath = Path.Combine(_buildPath, "fxmanifest.lua"); + File.WriteAllText(finalPath, contentBuilder.ToString()); + } + + #endregion + + + #region Singleplayer + + public async Task BuildSingleplayer() + { + var dlcRpf = RpfFile.CreateNew(_buildPath, "dlc.rpf", RpfEncryption.OPEN); + var creatureMetadatas = BuildContentXml(dlcRpf.Root); + BuildSetupXml(dlcRpf.Root); + + var x64 = RpfFile.CreateDirectory(dlcRpf.Root, "x64"); + var common = RpfFile.CreateDirectory(dlcRpf.Root, "common"); + var dataFolder = RpfFile.CreateDirectory(common, "data"); + + var models = RpfFile.CreateDirectory(x64, "models"); + var cdimages = RpfFile.CreateDirectory(models, "cdimages"); + + if(creatureMetadatas.Count > 0) + { + var animFolder = RpfFile.CreateDirectory(x64, "anim"); + var creature = RpfFile.CreateNew(animFolder, "creaturemetadata.rpf"); + + foreach (var meta in creatureMetadatas) + { + RpfFile.CreateFile(creature.Root, meta.SingleplayerFileName + ".ymt", meta.Save()); + } + } + + int counter = 1; + foreach (var selectedAddon in MainWindow.AddonManager.Addons) + { + SetAddon(selectedAddon); + SetNumber(counter); + + if (selectedAddon.HasMale) + { + var (name, bytes) = BuildMeta(true); + RpfFile.CreateFile(dataFolder, name, bytes); + + var ymtBytes = BuildYMT(true); + await BuildSingleplayerFilesAsync(true, ymtBytes, counter, cdimages); + } + + if (selectedAddon.HasFemale) + { + var (name, bytes) = BuildMeta(false); + RpfFile.CreateFile(dataFolder, name, bytes); + + var ymtBytes = BuildYMT(false); + await BuildSingleplayerFilesAsync(false, ymtBytes, counter, cdimages); + } + + counter++; + } + } + + private List BuildContentXml(RpfDirectoryEntry dir) + { + StringBuilder sb = new(); + + sb.AppendLine($""); + sb.AppendLine($""); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + var generatedCreatureMetadatas = new List(); + var filesToEnable = new List(); + foreach (var addon in MainWindow.AddonManager.Addons) + { + if (addon.HasMale) + { + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/common/data/mp_m_freemode_01_{_projectName}.meta"); + sb.AppendLine($" SHOP_PED_APPAREL_META_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_male.rpf"); + sb.AppendLine($" RPF_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + filesToEnable.Add($"dlc_{_projectName}:/common/data/mp_m_freemode_01_{_projectName}.meta"); + filesToEnable.Add($"dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_male.rpf"); + + if (addon.HasProps) + { + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_male_p.rpf"); + sb.AppendLine($" RPF_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + filesToEnable.Add($"dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_male_p.rpf"); + } + + var drawables = addon.Drawables.Where(x => x.Sex == true).ToList(); + var generated = GenerateCreatureMetadata(drawables); + if (generated != null) + { + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/%PLATFORM%/anim/mp_creaturemetadata_m_{_projectName}.rpf"); + sb.AppendLine($" RPF_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + filesToEnable.Add($"dlc_{_projectName}:/%PLATFORM%/anim/mp_creaturemetadata_m_{_projectName}.rpf"); + generated.SingleplayerFileName = $"mp_creaturemetadata_m_{_projectName}"; + generatedCreatureMetadatas.Add(generated); + } + } + + if (addon.HasFemale) + { + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/common/data/mp_f_freemode_01_{_projectName}.meta"); + sb.AppendLine($" SHOP_PED_APPAREL_META_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_female.rpf"); + sb.AppendLine($" RPF_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + filesToEnable.Add($"dlc_{_projectName}:/common/data/mp_f_freemode_01_{_projectName}.meta"); + filesToEnable.Add($"dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_female.rpf"); + + if (addon.HasProps) + { + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_female_p.rpf"); + sb.AppendLine($" RPF_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + + filesToEnable.Add($"dlc_{_projectName}:/%PLATFORM%/models/cdimages/{_projectName}_female_p.rpf"); + } + + var drawables = addon.Drawables.Where(x => x.Sex == false).ToList(); + var generated = GenerateCreatureMetadata(drawables); + if (generated != null) + { + sb.AppendLine($" "); + sb.AppendLine($" dlc_{_projectName}:/%PLATFORM%/anim/mp_creaturemetadata_f_{_projectName}.rpf"); + sb.AppendLine($" RPF_FILE"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + filesToEnable.Add($"dlc_{_projectName}:/%PLATFORM%/anim/mp_creaturemetadata_f_{_projectName}.rpf"); + generated.SingleplayerFileName = $"mp_creaturemetadata_f_{_projectName}"; + generatedCreatureMetadatas.Add(generated); + } + } + } + + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" {_projectName}_AUTOGEN"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + + foreach (var file in filesToEnable) + { + sb.AppendLine($" {file}"); + } + + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($""); + + RpfFile.CreateFile(dir, "content.xml", Encoding.UTF8.GetBytes(sb.ToString())); + + return generatedCreatureMetadatas; + } + + private void BuildSetupXml(RpfDirectoryEntry dir) + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + StringBuilder sb = new(); + sb.AppendLine($""); + sb.AppendLine($""); + sb.AppendLine($" dlc_{_projectName}"); + sb.AppendLine($" content.xml"); + sb.AppendLine($" {timestamp}"); + sb.AppendLine($" {_projectName}"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" GROUP_STARTUP"); + sb.AppendLine($" "); + sb.AppendLine($" {_projectName.ToUpper()}_GEN"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" EXTRACONTENT_COMPAT_PACK"); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($""); + + RpfFile.CreateFile(dir, "setup2.xml", Encoding.UTF8.GetBytes(sb.ToString())); + } + + private async Task BuildSingleplayerFilesAsync(bool isMale, byte[] ymtBytes, int counter, RpfDirectoryEntry cdimages) + { + var pedName = GetPedName(isMale); + var projectName = GetProjectName(counter); + + var drawables = _addon.Drawables.Where(x => x.Sex == isMale).ToList(); + var drawableGroups = drawables.Select((x, i) => new { Index = i, Value = x }) + .GroupBy(x => x.Value.Number / 128) + .Select(x => x.Select(v => v.Value).ToList()) + .ToList(); + + var fileOperations = new List(); + + var hasProps = drawables.Any(x => x.IsProp); + + var genderRpfName = isMale ? "_male.rpf" : "_female.rpf"; + var componentsRpf = RpfFile.CreateNew(cdimages, projectName + genderRpfName); + var componentsFolder = RpfFile.CreateDirectory(componentsRpf.Root, pedName + "_" + projectName); + RpfFile propsRpf = null; + RpfDirectoryEntry propsFolder = null; + if (hasProps) + { + propsRpf = RpfFile.CreateNew(cdimages, projectName + genderRpfName + "_p"); + propsFolder = RpfFile.CreateDirectory(propsRpf.Root, pedName + "_p_" + projectName); + } + + RpfFile.CreateFile(componentsRpf.Root, $"{pedName}_{projectName}.ymt", ymtBytes); + + foreach (var group in drawableGroups) + { + foreach (var d in group) + { + var drawableBytes = File.ReadAllBytes(d.FilePath); + RpfFile.CreateFile(componentsFolder, $"{d.Name}{Path.GetExtension(d.FilePath)}", drawableBytes); + + foreach (var t in d.Textures) + { + var displayName = RemoveInvalidChars(t.DisplayName); + + if (t.IsOptimizedDuringBuild) + { + var optimizedBytes = await ImgHelper.Optimize(t); + RpfFile.CreateFile(componentsFolder, $"{displayName}{Path.GetExtension(t.FilePath)}", optimizedBytes); + } + else + { + var texBytes = File.ReadAllBytes(t.FilePath); + RpfFile.CreateFile(componentsFolder, $"{displayName}{Path.GetExtension(t.FilePath)}", texBytes); + } + } + } + } + } + + + #endregion + + + #region Generic + public byte[] BuildYMT(bool isMale) { var mb = new MetaBuilder(); @@ -198,8 +593,7 @@ public byte[] BuildYMT(bool isMale) return data; } - - public FileInfo BuildMeta(bool isMale) + public (string, byte[]) BuildMeta(bool isMale) { var eCharacter = isMale ? "SCR_CHAR_MULTIPLAYER" : "SCR_CHAR_MULTIPLAYER_F"; var genderLetter = GetGenderLetter(isMale); @@ -228,109 +622,18 @@ public FileInfo BuildMeta(bool isMale) var xml = sb.ToString(); - var finalPath = Path.Combine(_buildPath, pedName + "_" + projectName + ".meta"); - File.WriteAllText(finalPath, xml); - - return new FileInfo(finalPath); - } - - public async Task BuildFilesAsync(bool isMale, byte[] ymtBytes, int counter) - { - var pedName = GetPedName(isMale); - var projectName = GetProjectName(counter); - - var drawables = _addon.Drawables.Where(x => x.Sex == isMale).ToList(); - var drawableGroups = drawables.Select((x, i) => new { Index = i, Value = x }) - .GroupBy(x => x.Value.Number / 128) - .Select(x => x.Select(v => v.Value).ToList()) - .ToList(); - - // Prepare all directory paths first to minimize file system access - var genderFolderName = isMale ? "[male]" : "[female]"; - var directoriesToEnsure = drawableGroups.SelectMany(g => g.Select(d => Path.Combine(_buildPath, "stream", genderFolderName, d.TypeName))).Distinct(); - foreach (var dir in directoriesToEnsure) - { - if (!Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - } - - var fileOperations = new List(); - - foreach (var group in drawableGroups) - { - var ymtPath = Path.Combine(_buildPath, "stream", $"{pedName}_{projectName}.ymt"); - fileOperations.Add(File.WriteAllBytesAsync(ymtPath, ymtBytes)); - - foreach (var d in group) - { - var drawablePedName = d.IsProp ? $"{pedName}_p" : pedName; - var folderPath = Path.Combine(_buildPath, "stream", genderFolderName, d.TypeName); - var prefix = RemoveInvalidChars($"{drawablePedName}_{projectName}^"); - var finalPath = Path.Combine(folderPath, $"{prefix}{d.Name}{Path.GetExtension(d.FilePath)}"); - fileOperations.Add(FileHelper.CopyAsync(d.FilePath, finalPath)); - - foreach (var t in d.Textures) - { - var displayName = RemoveInvalidChars(t.DisplayName); - var finalTexPath = Path.Combine(folderPath, $"{prefix}{displayName}{Path.GetExtension(t.FilePath)}"); - if (t.IsOptimizedDuringBuild) - { - fileOperations.Add(ImgHelper.OptimizeAndSave(t, finalTexPath)); - } - else - { - fileOperations.Add(FileHelper.CopyAsync(t.FilePath, finalTexPath)); - } - } - } - } - - // Run all file operations - await Task.WhenAll(fileOperations); - await Task.Run(() => GenerateCreatureMetadata(drawables, isMale)); - } - - - private static string RemoveInvalidChars(string input) - { - return string.Concat(input.Split(Path.GetInvalidFileNameChars())); - } - - public void BuildFxManifest(List metaFiles) - { - StringBuilder contentBuilder = new(); - contentBuilder.AppendLine("-- This resource was generated by grzyClothTool :)"); - contentBuilder.AppendLine(); - contentBuilder.AppendLine("fx_version 'cerulean'"); - contentBuilder.AppendLine("game { 'gta5' }"); - contentBuilder.AppendLine(); - contentBuilder.AppendLine("files {"); - - string filesSection = string.Join(",\n ", metaFiles.Select(f => $"'{f}'")); - contentBuilder.AppendLine($" {filesSection}"); + var name = pedName + "_" + projectName + ".meta"; - contentBuilder.AppendLine("}"); - contentBuilder.AppendLine(); - - foreach (var file in metaFiles) - { - contentBuilder.AppendLine($"data_file 'SHOP_PED_APPAREL_META_FILE' '{file}'"); - } - - var finalPath = Path.Combine(_buildPath, "fxmanifest.lua"); - File.WriteAllText(finalPath, contentBuilder.ToString()); + var bytes = Encoding.UTF8.GetBytes(xml); + return (name, bytes); } - private void GenerateCreatureMetadata(List drawables, bool isMale) + private static RbfFile GenerateCreatureMetadata(List drawables) { //taken from ymteditor because it works fine xd - var projectName = GetProjectName(); - var shouldGenCreatureHeels = drawables.Any(x => x.EnableHighHeels); var shouldGenCreatureHats = drawables.Any(x => x.EnableHairScale); - if (!shouldGenCreatureHeels && !shouldGenCreatureHats) return; + if (!shouldGenCreatureHeels && !shouldGenCreatureHats) return null; XElement xml = new("CCreatureMetaData"); XElement pedCompExpressions = new("pedCompExpressions"); @@ -386,7 +689,12 @@ private void GenerateCreatureMetadata(List drawables, bool isMale) xmldoc.Load(xml.CreateReader()); RbfFile rbf = XmlRbf.GetRbf(xmldoc); - rbf.Save(_buildPath + "/stream/mp_creaturemetadata_" + GetGenderLetter(isMale) + "_" + projectName + ".ymt"); + return rbf; + } + + private static string RemoveInvalidChars(string input) + { + return string.Concat(input.Split(Path.GetInvalidFileNameChars())); } private static string GetGenderLetter(bool isMale) @@ -398,4 +706,6 @@ private static string GetPedName(bool isMale) { return isMale ? "mp_m_freemode_01" : "mp_f_freemode_01"; } + + #endregion } diff --git a/grzyClothTool/Helpers/ImgHelper.cs b/grzyClothTool/Helpers/ImgHelper.cs index 008783a..f45c547 100644 --- a/grzyClothTool/Helpers/ImgHelper.cs +++ b/grzyClothTool/Helpers/ImgHelper.cs @@ -31,7 +31,7 @@ public static (int, int) CheckPowerOfTwo(int width, int height) return (width, height); } - public static async Task OptimizeAndSave(GTexture gtxt, string savePath) + public static async Task Optimize(GTexture gtxt) { var ytd = CWHelper.GetYtdFile(gtxt.FilePath); var txt = ytd.TextureDict.Textures[0]; @@ -55,7 +55,7 @@ public static async Task OptimizeAndSave(GTexture gtxt, string savePath) ytd.TextureDict.BuildFromTextureList([newTxt]); var bytes = ytd.Save(); - await File.WriteAllBytesAsync(savePath, bytes); + return bytes; } private static string GetCompressionString(string cwCompression) diff --git a/grzyClothTool/Models/Addon.cs b/grzyClothTool/Models/Addon.cs index 5325587..6deaba7 100644 --- a/grzyClothTool/Models/Addon.cs +++ b/grzyClothTool/Models/Addon.cs @@ -20,6 +20,7 @@ public class Addon : INotifyPropertyChanged public bool HasFemale { get; set; } public bool HasMale { get; set; } + public bool HasProps { get; set; } private bool _isPreviewEnabled; public bool IsPreviewEnabled { @@ -150,9 +151,10 @@ public async Task AddDrawables(string[] filePaths, bool isMale) // Add the drawable to the current Addon currentAddon.Drawables.Add(drawable); - //set HasMale/HasFemale only once adding first drawable of this gender + //set HasMale/HasFemale/HasProps only once adding first drawable if (isMale && !currentAddon.HasMale) currentAddon.HasMale = true; if (!isMale && !currentAddon.HasFemale) currentAddon.HasFemale = true; + if (isProp && !currentAddon.HasProps) currentAddon.HasProps = true; } // Sort the ObservableCollection in place, in all existing addons diff --git a/grzyClothTool/Models/GDrawable.cs b/grzyClothTool/Models/GDrawable.cs index f94155b..b6bbaa4 100644 --- a/grzyClothTool/Models/GDrawable.cs +++ b/grzyClothTool/Models/GDrawable.cs @@ -76,6 +76,8 @@ public bool HasSkin { txt.HasSkin = value; } + SetDrawableName(); + OnPropertyChanged(); } } } diff --git a/grzyClothTool/Views/BuildWindow.xaml b/grzyClothTool/Views/BuildWindow.xaml index 8ebfc46..cdf48fd 100644 --- a/grzyClothTool/Views/BuildWindow.xaml +++ b/grzyClothTool/Views/BuildWindow.xaml @@ -9,7 +9,7 @@ ResizeMode="NoResize" WindowStartupLocation="CenterOwner" MouseDown="Window_MouseDown" - Title="(WIP) grzyClothTool - Build FiveM resource" + Title="(WIP) grzyClothTool - Build resource" Height="400" Width="500"> @@ -18,6 +18,25 @@ + + + + + + + + public partial class BuildWindow : Window { + public enum ResourceType + { + FiveM, + Singleplayer + } + public string ProjectName { get; set; } public string BuildPath { get; set; } + private ResourceType _resourceType; + public BuildWindow() { InitializeComponent(); @@ -39,42 +49,77 @@ private async void build_MyBtnClickEvent(object sender, RoutedEventArgs e) } var timer = new Stopwatch(); timer.Start(); + var buildHelper = new BuildResourceHelper(ProjectName, BuildPath, MainWindow.AddonManager.Addons.Count); + + switch (_resourceType) + { + case ResourceType.FiveM: + await BuildFiveMResource(buildHelper); + break; + case ResourceType.Singleplayer: + await buildHelper.BuildSingleplayer(); + break; + default: + throw new NotImplementedException(); + } + + Close(); + timer.Stop(); + + CustomMessageBox.Show($"Build done, elapsed time: {timer.Elapsed}", "Build done", CustomMessageBoxButtons.OpenFolder, BuildPath); + } + + private async Task BuildFiveMResource(BuildResourceHelper bHelper) + { int counter = 1; var metaFiles = new List(); var tasks = new List(); - var buildHelper = new BuildResourceHelper(ProjectName, BuildPath, MainWindow.AddonManager.Addons.Count); + foreach (var selectedAddon in MainWindow.AddonManager.Addons) { - buildHelper.SetAddon(selectedAddon); - buildHelper.SetNumber(counter); + bHelper.SetAddon(selectedAddon); + bHelper.SetNumber(counter); if (selectedAddon.HasMale) { - var bytes = buildHelper.BuildYMT(true); - tasks.Add(buildHelper.BuildFilesAsync(true, bytes, counter)); + var bytes = bHelper.BuildYMT(true); + tasks.Add(bHelper.BuildFiveMFilesAsync(true, bytes, counter)); + + var (name, b) = bHelper.BuildMeta(true); + metaFiles.Add(name); - var meta = buildHelper.BuildMeta(true); - metaFiles.Add(meta.Name); + var path = Path.Combine(BuildPath, name); + tasks.Add(File.WriteAllBytesAsync(path, b)); } if (selectedAddon.HasFemale) { - var bytes = buildHelper.BuildYMT(false); - tasks.Add(buildHelper.BuildFilesAsync(false, bytes, counter)); + var bytes = bHelper.BuildYMT(false); + tasks.Add(bHelper.BuildFiveMFilesAsync(false, bytes, counter)); - var meta = buildHelper.BuildMeta(false); - metaFiles.Add(meta.Name); + var (name, b) = bHelper.BuildMeta(false); + metaFiles.Add(name); + + var path = Path.Combine(BuildPath, name); + tasks.Add(File.WriteAllBytesAsync(path, b)); } counter++; } await Task.WhenAll(tasks); - buildHelper.BuildFxManifest(metaFiles); - - Close(); - timer.Stop(); + bHelper.BuildFxManifest(metaFiles); + } - //MessageBox.Show($"Build done, elapsed time: {timer.Elapsed}"); - CustomMessageBox.Show($"Build done, elapsed time: {timer.Elapsed}", "Build done", CustomMessageBoxButtons.OpenFolder, BuildPath); + private void RadioButton_ChangedEvent(object sender, RoutedEventArgs e) + { + if (sender is ModernLabelRadioButton radioButton && radioButton.IsChecked == true) + { + _resourceType = radioButton.Label switch + { + "FiveM" => ResourceType.FiveM, + "Singleplayer" => ResourceType.Singleplayer, + _ => throw new NotImplementedException() + }; + } } } }