From 65c549840f85613791e79ee2a085f466f38d5203 Mon Sep 17 00:00:00 2001 From: dexyfex Date: Wed, 10 Jan 2018 14:17:30 +1100 Subject: [PATCH] RPF Explorer Edit mode --- ExploreForm.Designer.cs | 73 ++- ExploreForm.cs | 603 +++++++++++++++++-- ExploreForm.resx | 15 +- GameFiles/GameFile.cs | 2 +- GameFiles/Resources/RpfFile.cs | 1001 +++++++++++++++++++++++++++++--- GameFiles/Utils/GTACrypto.cs | 178 ++++++ 6 files changed, 1750 insertions(+), 122 deletions(-) diff --git a/ExploreForm.Designer.cs b/ExploreForm.Designer.cs index b5a3e1c..5a57795 100644 --- a/ExploreForm.Designer.cs +++ b/ExploreForm.Designer.cs @@ -59,6 +59,7 @@ this.ViewListMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ViewDetailsMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ToolsMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.ToolsBinSearchMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ToolsRpfBrowserMenu = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator7 = new System.Windows.Forms.ToolStripSeparator(); this.ToolsOptionsMenu = new System.Windows.Forms.ToolStripMenuItem(); @@ -72,6 +73,8 @@ this.LocationTextBox = new CodeWalker.WinForms.ToolStripSpringTextBox(); this.GoButton = new System.Windows.Forms.ToolStripButton(); this.RefreshButton = new System.Windows.Forms.ToolStripButton(); + this.toolStripSeparator10 = new System.Windows.Forms.ToolStripSeparator(); + this.EditModeButton = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.SearchTextBox = new System.Windows.Forms.ToolStripTextBox(); this.SearchButton = new System.Windows.Forms.ToolStripSplitButton(); @@ -97,6 +100,9 @@ this.ListContextExtractUncompressedMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextExtractAllMenu = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator(); + this.ListContextNewMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.ListContextNewFolderMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.ListContextNewRpfArchiveMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextImportXmlMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextImportRawMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ListContextImportSeparator = new System.Windows.Forms.ToolStripSeparator(); @@ -121,7 +127,6 @@ this.SaveFileDialog = new System.Windows.Forms.SaveFileDialog(); this.OpenFileDialog = new System.Windows.Forms.OpenFileDialog(); this.FolderBrowserDialog = new System.Windows.Forms.FolderBrowserDialog(); - this.ToolsBinSearchMenu = new System.Windows.Forms.ToolStripMenuItem(); this.MainMenu.SuspendLayout(); this.MainToolbar.SuspendLayout(); this.StatusBar.SuspendLayout(); @@ -395,6 +400,13 @@ this.ToolsMenu.Size = new System.Drawing.Size(47, 20); this.ToolsMenu.Text = "Tools"; // + // ToolsBinSearchMenu + // + this.ToolsBinSearchMenu.Name = "ToolsBinSearchMenu"; + this.ToolsBinSearchMenu.Size = new System.Drawing.Size(161, 22); + this.ToolsBinSearchMenu.Text = "Binary Search..."; + this.ToolsBinSearchMenu.Click += new System.EventHandler(this.ToolsBinSearchMenu_Click); + // // ToolsRpfBrowserMenu // this.ToolsRpfBrowserMenu.Name = "ToolsRpfBrowserMenu"; @@ -424,6 +436,8 @@ this.LocationTextBox, this.GoButton, this.RefreshButton, + this.toolStripSeparator10, + this.EditModeButton, this.toolStripSeparator1, this.SearchTextBox, this.SearchButton}); @@ -496,7 +510,7 @@ // LocationTextBox // this.LocationTextBox.Name = "LocationTextBox"; - this.LocationTextBox.Size = new System.Drawing.Size(510, 25); + this.LocationTextBox.Size = new System.Drawing.Size(423, 25); this.LocationTextBox.Enter += new System.EventHandler(this.LocationTextBox_Enter); this.LocationTextBox.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.LocationTextBox_KeyPress); // @@ -522,6 +536,22 @@ this.RefreshButton.Text = "Refresh All"; this.RefreshButton.Click += new System.EventHandler(this.RefreshButton_Click); // + // toolStripSeparator10 + // + this.toolStripSeparator10.Name = "toolStripSeparator10"; + this.toolStripSeparator10.Size = new System.Drawing.Size(6, 25); + // + // EditModeButton + // + this.EditModeButton.Enabled = false; + this.EditModeButton.Image = ((System.Drawing.Image)(resources.GetObject("EditModeButton.Image"))); + this.EditModeButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; + this.EditModeButton.ImageTransparentColor = System.Drawing.Color.Magenta; + this.EditModeButton.Name = "EditModeButton"; + this.EditModeButton.Size = new System.Drawing.Size(81, 22); + this.EditModeButton.Text = "Edit mode"; + this.EditModeButton.Click += new System.EventHandler(this.EditModeButton_Click); + // // toolStripSeparator1 // this.toolStripSeparator1.Name = "toolStripSeparator1"; @@ -676,6 +706,7 @@ this.MainListView.UseCompatibleStateImageBehavior = false; this.MainListView.View = System.Windows.Forms.View.Details; this.MainListView.VirtualMode = true; + this.MainListView.AfterLabelEdit += new System.Windows.Forms.LabelEditEventHandler(this.MainListView_AfterLabelEdit); this.MainListView.ColumnClick += new System.Windows.Forms.ColumnClickEventHandler(this.MainListView_ColumnClick); this.MainListView.ItemActivate += new System.EventHandler(this.MainListView_ItemActivate); this.MainListView.RetrieveVirtualItem += new System.Windows.Forms.RetrieveVirtualItemEventHandler(this.MainListView_RetrieveVirtualItem); @@ -720,6 +751,7 @@ this.ListContextExtractUncompressedMenu, this.ListContextExtractAllMenu, this.toolStripSeparator5, + this.ListContextNewMenu, this.ListContextImportXmlMenu, this.ListContextImportRawMenu, this.ListContextImportSeparator, @@ -735,7 +767,7 @@ this.ListContextEditSeparator, this.ListContextSelectAllMenu}); this.ListContextMenu.Name = "MainContextMenu"; - this.ListContextMenu.Size = new System.Drawing.Size(208, 392); + this.ListContextMenu.Size = new System.Drawing.Size(208, 414); // // ListContextViewMenu // @@ -799,6 +831,29 @@ this.toolStripSeparator5.Name = "toolStripSeparator5"; this.toolStripSeparator5.Size = new System.Drawing.Size(204, 6); // + // ListContextNewMenu + // + this.ListContextNewMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.ListContextNewFolderMenu, + this.ListContextNewRpfArchiveMenu}); + this.ListContextNewMenu.Name = "ListContextNewMenu"; + this.ListContextNewMenu.Size = new System.Drawing.Size(207, 22); + this.ListContextNewMenu.Text = "New"; + // + // ListContextNewFolderMenu + // + this.ListContextNewFolderMenu.Name = "ListContextNewFolderMenu"; + this.ListContextNewFolderMenu.Size = new System.Drawing.Size(146, 22); + this.ListContextNewFolderMenu.Text = "Folder..."; + this.ListContextNewFolderMenu.Click += new System.EventHandler(this.ListContextNewFolderMenu_Click); + // + // ListContextNewRpfArchiveMenu + // + this.ListContextNewRpfArchiveMenu.Name = "ListContextNewRpfArchiveMenu"; + this.ListContextNewRpfArchiveMenu.Size = new System.Drawing.Size(146, 22); + this.ListContextNewRpfArchiveMenu.Text = "RPF Archive..."; + this.ListContextNewRpfArchiveMenu.Click += new System.EventHandler(this.ListContextNewRpfArchiveMenu_Click); + // // ListContextImportXmlMenu // this.ListContextImportXmlMenu.Image = ((System.Drawing.Image)(resources.GetObject("ListContextImportXmlMenu.Image"))); @@ -958,12 +1013,9 @@ this.TreeContextCollapseAllMenu.Text = "Collapse All"; this.TreeContextCollapseAllMenu.Click += new System.EventHandler(this.TreeContextCollapseAllMenu_Click); // - // ToolsBinSearchMenu + // OpenFileDialog // - this.ToolsBinSearchMenu.Name = "ToolsBinSearchMenu"; - this.ToolsBinSearchMenu.Size = new System.Drawing.Size(161, 22); - this.ToolsBinSearchMenu.Text = "Binary Search..."; - this.ToolsBinSearchMenu.Click += new System.EventHandler(this.ToolsBinSearchMenu_Click); + this.OpenFileDialog.Multiselect = true; // // ExploreForm // @@ -1090,5 +1142,10 @@ private System.Windows.Forms.ToolStripSeparator ListContextOpenFileLocationSeparator; private System.Windows.Forms.ToolStripMenuItem ListContextExtractUncompressedMenu; private System.Windows.Forms.ToolStripMenuItem ToolsBinSearchMenu; + private System.Windows.Forms.ToolStripMenuItem ListContextNewMenu; + private System.Windows.Forms.ToolStripMenuItem ListContextNewFolderMenu; + private System.Windows.Forms.ToolStripMenuItem ListContextNewRpfArchiveMenu; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator10; + private System.Windows.Forms.ToolStripButton EditModeButton; } } \ No newline at end of file diff --git a/ExploreForm.cs b/ExploreForm.cs index 881a937..46d5698 100644 --- a/ExploreForm.cs +++ b/ExploreForm.cs @@ -418,6 +418,7 @@ namespace CodeWalker GoButton.Enabled = true; RefreshButton.Enabled = true; SearchButton.Enabled = true; + EditModeButton.Enabled = true; } public void GoUp(MainTreeFolder toFolder = null) @@ -626,10 +627,7 @@ namespace CodeWalker var exists = nodes.TryGetValue(parentpath, out node); if (!exists) { - node = new MainTreeFolder(); - node.Name = parentname; - node.Path = parentpath; - node.FullPath = replpath + parentpath; + node = CreateRootDirTreeFolder(parentname, parentpath, replpath + parentpath); nodes[parentpath] = node; } if (parentnode == null) @@ -656,12 +654,7 @@ namespace CodeWalker rpf.ScanStructure(UpdateStatus, UpdateErrorLog); - node = new MainTreeFolder(); - node.RpfFile = rpf; - node.RpfFolder = rpf.Root; - node.Name = rpf.Name; - node.Path = relpath; - node.FullPath = filepath; + node = CreateRpfTreeFolder(rpf, relpath, filepath); RecurseMainTreeViewRPF(node, allRpfs); @@ -717,11 +710,7 @@ namespace CodeWalker { foreach (var dir in fld.Directories) { - MainTreeFolder dtnf = new MainTreeFolder(); - dtnf.RpfFolder = dir; - dtnf.Name = dir.Name; - dtnf.Path = dir.Path; - dtnf.FullPath = rootpath + dir.Path; + var dtnf = CreateRpfDirTreeFolder(dir, dir.Path, rootpath + dir.Path); f.AddChild(dtnf); RecurseMainTreeViewRPF(dtnf, allRpfs); } @@ -737,35 +726,22 @@ namespace CodeWalker { foreach (var child in rpf.Children) { - MainTreeFolder ctnf = new MainTreeFolder(); - ctnf.RpfFile = child; - ctnf.RpfFolder = child.Root; - ctnf.Name = child.Name; - ctnf.Path = child.Path; - ctnf.FullPath = rootpath + child.Path; + var ctnf = CreateRpfTreeFolder(child, child.Path, rootpath + child.Path); f.AddChildToHierarchy(ctnf); RecurseMainTreeViewRPF(ctnf, allRpfs); } } - JenkIndex.Ensure(rpf.Name); + //JenkIndex.Ensure(rpf.Name); if (rpf.AllEntries != null) { foreach (RpfEntry entry in rpf.AllEntries) { if (string.IsNullOrEmpty(entry.NameLower)) continue; - JenkIndex.Ensure(entry.Name); - JenkIndex.Ensure(entry.NameLower); + var shortnamel = entry.GetShortNameLower(); + JenkIndex.Ensure(shortnamel); + entry.ShortNameHash = JenkHash.GenHash(shortnamel); entry.NameHash = JenkHash.GenHash(entry.NameLower); - int ind = entry.Name.LastIndexOf('.'); - if (ind > 0) - { - var shortname = entry.Name.Substring(0, ind); - var shortnamel = entry.NameLower.Substring(0, ind); - JenkIndex.Ensure(shortname); - JenkIndex.Ensure(shortnamel); - entry.ShortNameHash = JenkHash.GenHash(shortnamel); - } } } } @@ -786,6 +762,7 @@ namespace CodeWalker ForwardButton.Enabled = false; RefreshButton.Enabled = false; SearchButton.Enabled = false; + EditModeButton.Enabled = false; MainTreeView.Nodes.Clear(); MainListView.VirtualListSize = 0; //also clear the list view... } @@ -841,6 +818,8 @@ namespace CodeWalker tn.ToolTipText = f.Path; tn.Tag = f; + f.TreeNode = tn; + if (f.Children != null) { f.Children.Sort((n1, n2) => n1.Name.CompareTo(n2.Name)); @@ -882,6 +861,112 @@ namespace CodeWalker } catch { } } + private void AddNewFolderTreeNode(MainTreeFolder f) + { + if (CurrentFolder == null) return; + + RecurseAddMainTreeViewNodes(f, CurrentFolder.TreeNode); + + CurrentFolder.AddChild(f); + CurrentFolder.ListItems = null; + + RefreshMainListView(); + } + private MainTreeFolder CreateRpfTreeFolder(RpfFile rpf, string relpath, string fullpath) + { + var node = new MainTreeFolder(); + node.RpfFile = rpf; + node.RpfFolder = rpf.Root; + node.Name = rpf.Name; + node.Path = relpath; + node.FullPath = fullpath; + return node; + } + private MainTreeFolder CreateRpfDirTreeFolder(RpfDirectoryEntry dir, string relpath, string fullpath) + { + var node = new MainTreeFolder(); + node.RpfFolder = dir; + node.Name = dir.Name; + node.Path = relpath; + node.FullPath = fullpath; + return node; + } + private MainTreeFolder CreateRootDirTreeFolder(string name, string path, string fullpath) + { + var node = new MainTreeFolder(); + node.Name = name; + node.Path = path; + node.FullPath = fullpath; + return node; + } + private void RenameTreeFolder(MainTreeFolder f, string newname) + { + if (f.Parent == null) return; + f.Name = newname; + f.Path = f.Parent.Path + "\\" + newname.ToLowerInvariant(); + f.FullPath = f.Parent.FullPath + "\\" + newname; + if (f.TreeNode != null) + { + f.TreeNode.Text = newname; + } + if (f.Children != null) + { + foreach (var item in f.Children) + { + RenameTreeFolder(item, item.Name);//just to make sure the all the paths are correct... + } + } + if (f.ListItems != null) + { + foreach (var item in f.ListItems) + { + RenameListItem(item, item.Name); + } + } + } + private void RenameListItem(MainListItem i, string newname) + { + if (i.Parent == null) return; + i.Name = newname; + i.Path = i.Parent.Path + "\\" + newname.ToLowerInvariant(); + i.FullPath = i.Parent.FullPath + "\\" + newname; + + if (i.Parent == CurrentFolder) + { + int idx = CurrentFiles.IndexOf(i); + if (idx >= 0) + { + MainListView.RedrawItems(idx, idx, false); + } + } + } + private void RemoveTreeFolder(MainTreeFolder f) + { + if (f.Parent == null) return; + + f.Parent.Children?.Remove(f); + + if (f.TreeNode != null) + { + f.TreeNode.Remove(); + } + } + private void RemoveListItem(MainListItem i) + { + if (i.Parent == null) return; + + MainListView.VirtualListSize = 0; + + i.Parent.ListItems?.Remove(i); + + if (i.Parent == CurrentFolder) + { + CurrentFiles.Remove(i);//should really be the same list as above, but just in case... + } + + MainListView.VirtualListSize = CurrentFiles.Count; + } + private void RefreshMainListView() { @@ -1430,12 +1515,11 @@ namespace CodeWalker bool isfile = false; bool isfolder = false; bool isfilesys = false; + bool issearch = CurrentFolder?.IsSearchResults ?? false; bool canview = false; bool canexportxml = false; - bool editmode = false;//todo: set this for edit mode - bool canimport = editmode; + bool canimport = EditMode && (CurrentFolder?.RpfFolder != null) && !issearch; bool canedit = false; - bool issearch = CurrentFolder?.IsSearchResults ?? false; if (item != null) { @@ -1445,7 +1529,7 @@ namespace CodeWalker canexportxml = CanExportXml(item); isitem = true; isfile = !isfolder; - canedit = editmode && !canimport; + canedit = EditMode && !issearch; } @@ -1456,9 +1540,10 @@ namespace CodeWalker ListContextExtractRawMenu.Enabled = isfile; ListContextExtractUncompressedMenu.Enabled = isfile; + ListContextNewMenu.Visible = EditMode; ListContextImportRawMenu.Visible = canimport; ListContextImportXmlMenu.Visible = canimport; - ListContextImportSeparator.Visible = canimport; + ListContextImportSeparator.Visible = EditMode; ListContextCopyMenu.Enabled = isfile; ListContextCopyPathMenu.Enabled = isitem; @@ -1477,6 +1562,48 @@ namespace CodeWalker + + private void EnableEditMode(bool enable) + { + if (EditMode == enable) + { + return; + } + + if (enable) + { + if (MessageBox.Show(this, "While in edit mode, all changes are automatically saved.\nDo you want to continue?", "Warning - Entering edit mode", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) + { + return; + } + } + + EditMode = enable; + EditModeButton.Checked = enable; + MainListView.LabelEdit = enable; + + } + + + + + + + private bool IsFilenameOk(string name) + { + foreach (var ic in Path.GetInvalidFileNameChars()) + { + if (name.Contains(ic)) + { + return false; + } + } + return true; + } + + + + private void ViewSelected() { for (int i = 0; i < MainListView.SelectedIndices.Count; i++) @@ -1798,15 +1925,182 @@ namespace CodeWalker MessageBox.Show("Errors were encountered:\n" + errstr); } } + private void NewFolder() + { + if (CurrentFolder == null) return;//shouldn't happen + if (CurrentFolder?.IsSearchResults ?? false) return; + + string fname = Prompt.ShowDialog(this, "Enter a name for the new folder:", "Create folder", "folder"); + if (string.IsNullOrEmpty(fname)) + { + return;//no name was provided. + } + if (!IsFilenameOk(fname)) return; //new name contains invalid char(s). don't do anything + + + string relpath = (CurrentFolder.Path ?? "") + "\\" + fname; + var rootpath = GetRootPath(); + string fullpath = rootpath + relpath; + + RpfDirectoryEntry newdir = null; + MainTreeFolder node = null; + + try + { + if (CurrentFolder.RpfFolder != null) + { + //create new directory entry in the RPF. + + newdir = RpfFile.CreateDirectory(CurrentFolder.RpfFolder, fname); + + node = CreateRpfDirTreeFolder(newdir, relpath, fullpath); + } + else + { + //create a folder in the filesystem. + if (Directory.Exists(fullpath)) + { + throw new Exception("Folder " + fullpath + " already exists!"); + } + Directory.CreateDirectory(fullpath); + + node = CreateRootDirTreeFolder(fname, relpath, fullpath); + } + } + catch (Exception ex) + { + MessageBox.Show("Error creating new folder: " + ex.Message, "Unable to create new folder"); + return; + } + + if (node != null) + { + AddNewFolderTreeNode(node); + } + + } + private void NewRpfArchive() + { + if (CurrentFolder == null) return;//shouldn't happen + if (CurrentFolder?.IsSearchResults ?? false) return; + + string fname = Prompt.ShowDialog(this, "Enter a name for the new archive:", "Create RPF7 archive", "new"); + if (string.IsNullOrEmpty(fname)) + { + return;//no name was provided. + } + if (!IsFilenameOk(fname)) return; //new name contains invalid char(s). don't do anything + + if (!fname.ToLowerInvariant().EndsWith(".rpf")) + { + fname = fname + ".rpf";//make sure it ends with .rpf + } + string relpath = (CurrentFolder.Path ?? "") + "\\" + fname.ToLowerInvariant(); + + + RpfEncryption encryption = RpfEncryption.OPEN;//TODO: select encryption mode + + RpfFile newrpf = null; + + try + { + if (CurrentFolder.RpfFolder != null) + { + //adding a new RPF as a child of another + newrpf = RpfFile.CreateNew(CurrentFolder.RpfFolder, fname, encryption); + } + else + { + //adding a new RPF in the filesystem + newrpf = RpfFile.CreateNew(Settings.Default.GTAFolder, relpath, encryption); + } + } + catch (Exception ex) + { + MessageBox.Show("Error creating archive: " + ex.Message, "Unable to create new archive"); + return; + } + + + if (newrpf != null) + { + var node = CreateRpfTreeFolder(newrpf, newrpf.Path, GetRootPath() + newrpf.Path); + RecurseMainTreeViewRPF(node, AllRpfs); + AddNewFolderTreeNode(node); + } + + } private void ImportXml() { if (!EditMode) return; - MessageBox.Show("Edit mode functions not yet implemented..."); + if (CurrentFolder?.IsSearchResults ?? false) return; + MessageBox.Show("Import XML TODO..."); } private void ImportRaw() { if (!EditMode) return; - MessageBox.Show("Edit mode functions not yet implemented..."); + if (CurrentFolder?.IsSearchResults ?? false) return; + + RpfDirectoryEntry parentrpffldr = CurrentFolder.RpfFolder; + if (parentrpffldr == null) + { + MessageBox.Show("No parent RPF folder selected! This shouldn't happen. Refresh the view and try again."); + return; + } + + if (OpenFileDialog.ShowDialog(this) != DialogResult.OK) + { + return;//canceled + } + + try + { + var fpaths = OpenFileDialog.FileNames; + foreach (var fpath in fpaths) + { + if (!File.Exists(fpath)) + { + continue;//this shouldn't happen... + } + + var fi = new FileInfo(fpath); + var fname = fi.Name; + var fnamel = fname.ToLowerInvariant(); + + if (fi.Length > 0x3FFFFFFF) + { + MessageBox.Show("File " + fname + " is too big! Max 1GB supported.", "Unable to import file"); + continue; + } + + foreach (var exfile in parentrpffldr.Files) + { + if (exfile.NameLower == fnamel) + { + //file already exists. delete the existing one first! + //this should probably be optimised to just replace the existing one... + //TODO: investigate along with ReplaceSelected() + RpfFile.DeleteEntry(exfile); + break; + } + } + + + byte[] data = File.ReadAllBytes(fpath); + + + RpfFile.CreateFile(parentrpffldr, fname, data); + + } + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Unable to import file"); + return; + } + + CurrentFolder.ListItems = null; + RefreshMainListView(); } private void CopySelected() { @@ -1863,19 +2157,156 @@ namespace CodeWalker private void RenameSelected() { if (!EditMode) return; - if (MainListView.SelectedItems.Count != 1) return; - MessageBox.Show("Edit mode functions not yet implemented..."); + if (MainListView.SelectedIndices.Count != 1) return; + var idx = MainListView.SelectedIndices[0]; + if ((CurrentFiles != null) && (CurrentFiles.Count > idx)) + { + var item = CurrentFiles[idx]; + string newname = Prompt.ShowDialog(this, "Enter the new name for this item:", "Rename item", item.Name); + if (!string.IsNullOrEmpty(newname)) + { + RenameItem(item, newname); + } + } + } + private void RenameItem(MainListItem item, string newname) + { + if (!EditMode) return; + if (item.Name == newname) return; + if (CurrentFolder?.IsSearchResults ?? false) return; + if (!IsFilenameOk(newname)) return; //new name contains invalid char(s). don't do anything + + + RpfFile file = item.Folder?.RpfFile; + RpfEntry entry = item.GetRpfEntry(); + + try + { + if (file != null) + { + //updates all items in the RPF with the new path - no actual file changes made here + RpfFile.RenameArchive(file, newname); + } + if (entry != null) + { + //renaming an entry in an RPF + RpfFile.RenameEntry(entry, newname); + } + else + { + //renaming a filesystem item... + var dirinfo = new DirectoryInfo(item.FullPath); + var newpath = Path.Combine(dirinfo.Parent.FullName, newname); + if (item.FullPath.ToLowerInvariant() == newpath.ToLowerInvariant()) + { + return;//filesystem tends to be case-insensitive... paths are the same + } + if ((item.Folder != null) && (item.Folder.RpfFile == null)) + { + //renaming a filesystem folder... + Directory.Move(item.FullPath, newpath); + } + else + { + //renaming a filesystem file... + File.Move(item.FullPath, newpath); + } + } + + if (item.Folder != null) + { + RenameTreeFolder(item.Folder, newname); + } + + RenameListItem(item, newname); + + } + catch (Exception ex) + { + MessageBox.Show("Error renaming " + item.Path + ": " + ex.Message, "Unable to rename item"); + return; + } + } private void ReplaceSelected() { if (!EditMode) return; - if (MainListView.SelectedItems.Count != 1) return; - MessageBox.Show("Edit mode functions not yet implemented..."); + if (CurrentFolder?.IsSearchResults ?? false) return; + if (MainListView.SelectedIndices.Count != 1) return; + MessageBox.Show("ReplaceSelected TODO..."); + //delete the selected items, and replace with... choose } private void DeleteSelected() { if (!EditMode) return; - MessageBox.Show("Edit mode functions not yet implemented..."); + if (CurrentFolder?.IsSearchResults ?? false) return; + if (MainListView.SelectedIndices.Count <= 0) return; + //if (MainListView.SelectedIndices.Count == 1) //is confirmation always really necessary? + //{ + // var item = CurrentFiles[MainListView.SelectedIndices[0]]; + // if (MessageBox.Show("Are you sure you want to permantly delete " + item.Name + "?\nThis cannot be undone.", "Confirm delete", MessageBoxButtons.YesNo) != DialogResult.Yes) + // { + // return; + // } + //} + //else + //{ + // if (MessageBox.Show("Are you sure you want to permantly delete " + MainListView.SelectedIndices.Count.ToString() + " items?\nThis cannot be undone.", "Confirm delete", MessageBoxButtons.YesNo) != DialogResult.Yes) + // { + // return; + // } + //} + var delitems = new List(); + foreach (int idx in MainListView.SelectedIndices) + { + if ((idx < 0) || (idx >= CurrentFiles.Count)) return; + var f = CurrentFiles[idx];//this could change when deleting.. so need to use the temp list + delitems.Add(f); + } + foreach (var f in delitems) + { + DeleteItem(f); + } + } + private void DeleteItem(MainListItem item) + { + try + { + var parent = item.Parent; + if (parent.RpfFolder != null) + { + //delete an item in an RPF. + RpfEntry entry = item.GetRpfEntry(); + + RpfFile.DeleteEntry(entry); + } + else + { + //delete an item in the filesystem. + if ((item.Folder != null) && (item.Folder.RpfFile == null)) + { + Directory.Delete(item.FullPath); + } + else + { + File.Delete(item.FullPath); + } + } + + + if (item.Folder != null) + { + RemoveTreeFolder(item.Folder); + } + + RemoveListItem(item); + + } + catch (Exception ex) + { + MessageBox.Show("Error deleting " + item.Path + ": " + ex.Message, "Unable to delete " + item.Name); + return; + } } private void SelectAll() { @@ -2116,6 +2547,14 @@ namespace CodeWalker } } + private void MainListView_AfterLabelEdit(object sender, LabelEditEventArgs e) + { + if ((CurrentFiles != null) && (CurrentFiles.Count > e.Item) && (!string.IsNullOrEmpty(e.Label))) + { + RenameItem(CurrentFiles[e.Item], e.Label); + } + } + private void BackButton_ButtonClick(object sender, EventArgs e) { GoBack(); @@ -2183,6 +2622,11 @@ namespace CodeWalker }); } + private void EditModeButton_Click(object sender, EventArgs e) + { + EnableEditMode(!EditMode); + } + private void SearchTextBox_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == 13) @@ -2298,6 +2742,16 @@ namespace CodeWalker ExtractAll(); } + private void ListContextNewFolderMenu_Click(object sender, EventArgs e) + { + NewFolder(); + } + + private void ListContextNewRpfArchiveMenu_Click(object sender, EventArgs e) + { + NewRpfArchive(); + } + private void ListContextImportXmlMenu_Click(object sender, EventArgs e) { ImportXml(); @@ -2474,6 +2928,7 @@ namespace CodeWalker public MainTreeFolder Parent { get; set; } public List Children { get; set; } public List ListItems { get; set; } + public TreeNode TreeNode { get; set; } public bool IsSearchResults { get; set; } public string SearchTerm { get; set; } @@ -2768,6 +3223,27 @@ namespace CodeWalker //return i1.Name.CompareTo(i2.Name); } + public RpfEntry GetRpfEntry() + { + RpfFile file = Folder?.RpfFile; + RpfDirectoryEntry fldr = Folder?.RpfFolder; + RpfEntry entry = File; + if (entry == null) + { + if (file != null) + { + //for an RPF file, get its entry in the parent (if any). + entry = file.ParentFileEntry; + } + else if (fldr != null) + { + //RPF folders are referenced in the item.Folder + entry = fldr; + } + } + return entry; + } + } @@ -2817,4 +3293,41 @@ namespace CodeWalker ViewCacheDat = 18, } + + + + + + + + + public static class Prompt + { + public static string ShowDialog(IWin32Window owner, string text, string caption, string defaultvalue = "") + { + Form prompt = new Form() + { + Width = 450, + Height = 150, + FormBorderStyle = FormBorderStyle.FixedDialog, + Text = caption, + StartPosition = FormStartPosition.CenterParent, + MaximizeBox = false, + MinimizeBox = false + }; + Label textLabel = new Label() { Left = 30, Top = 20, Width = 370, Height = 20, Text = text, }; + TextBox textBox = new TextBox() { Left = 30, Top = 40, Width = 370, Text = defaultvalue }; + Button cancel = new Button() { Text = "Cancel", Left = 230, Width = 80, Top = 70, DialogResult = DialogResult.Cancel }; + Button confirmation = new Button() { Text = "Ok", Left = 320, Width = 80, Top = 70, DialogResult = DialogResult.OK }; + cancel.Click += (sender, e) => { prompt.Close(); }; + confirmation.Click += (sender, e) => { prompt.Close(); }; + prompt.Controls.Add(textBox); + prompt.Controls.Add(confirmation); + prompt.Controls.Add(cancel); + prompt.Controls.Add(textLabel); + prompt.AcceptButton = confirmation; + + return prompt.ShowDialog(owner) == DialogResult.OK ? textBox.Text : ""; + } + } } diff --git a/ExploreForm.resx b/ExploreForm.resx index d945c3d..3fa8922 100644 --- a/ExploreForm.resx +++ b/ExploreForm.resx @@ -258,6 +258,19 @@ GvZP7/3NqMHOyEACQlVVHxGjBiOpbdu1KIokGRDjZCqQj/3WzsPozi3L8oNTCXEyAqDv+93PykyM7hyb OEgy37QlAVUSCxs9tKCADeqLDGjzT4Gu676IErtUgaDbuTwDCUhYccAhyS2wicsjWZXIJ9R1vYsbPdjm ePJI4uQjAWAz8UYNxvNEhJNVrq7ziMzzN2pq/DDMhNNPcE6+bs69AUIIBqGH+5QgAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGKSURBVDhPjZM7T8JQFMfP4GpiGFhcGBwddPBVkBK0kKhR + J8UoD4OoRGTxMRs3EyYHE2N8EF0wJiwuBhUTwecAH8DvwODe4z3tQSi3GP/JSZt7z+93e9tbsEswGPRp + mlYUV6Sie1F9PN0+otkVCATOCRqYXUdnLIvlEx/uJMYNEc+5uL0RXq1CTd6pEHZHjxFSHwgb76g/eUS5 + 8e5QxcicZoiolxjGjZWxZ/EAHSvXCJsCZBiSJHCjXlTMehzB6oUHL/dVQ8S4KYD0pwRD8s0C6w/DZt0P + 2QhsYFhjQQusFwZlgSOek2BYfbWFq2eKLHBGsxIMCSFoganKR25JkM9sj0kvjECIvyAslxFioqIlhEgJ + e2d2SZBn3BDEyPiVG5X23Ap3LtwYqxPDuBkxUEmGNWnPzTCEn1GZjBBcYawROqpiopbZ8v/CtN9mmB+9 + 1vZY1yV7KT9+37JAwB1LBeyfTv8N11OX0LGtniroCF2hd2L+f3A9qqp2iWbL30hjPP3/CJi+jvVtWwLw + A4Rmgl76+inbAAAAAElFTkSuQmCC @@ -300,7 +313,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAADo - HwAAAk1TRnQBSQFMAgEBGAEAAaABAAGgAQABEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo + HwAAAk1TRnQBSQFMAgEBGAEAAdABAAHQAQABEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo AwABQAMAAXADAAEBAQABCAYAARwYAAGAAgABgAMAAoABAAGAAwABgAEAAYABAAKAAgADwAEAAcAB3AHA AQAB8AHKAaYBAAEzBQABMwEAATMBAAEzAQACMwIAAxYBAAMcAQADIgEAAykBAANVAQADTQEAA0IBAAM5 AQABgAF8Af8BAAJQAf8BAAGTAQAB1gEAAf8B7AHMAQABxgHWAe8BAAHWAucBAAGQAakBrQIAAf8BMwMA diff --git a/GameFiles/GameFile.cs b/GameFiles/GameFile.cs index d03b626..8120afa 100644 --- a/GameFiles/GameFile.cs +++ b/GameFiles/GameFile.cs @@ -21,7 +21,7 @@ namespace CodeWalker.GameFiles { RpfFileEntry = entry; Type = type; - MemoryUsage = (entry != null) ? entry.FileSize : 0; + MemoryUsage = (entry != null) ? entry.GetFileSize() : 0; if (entry is RpfResourceFileEntry) { var resent = entry as RpfResourceFileEntry; diff --git a/GameFiles/Resources/RpfFile.cs b/GameFiles/Resources/RpfFile.cs index 3d986ec..287a4d0 100644 --- a/GameFiles/Resources/RpfFile.cs +++ b/GameFiles/Resources/RpfFile.cs @@ -22,13 +22,8 @@ namespace CodeWalker.GameFiles public RpfDirectoryEntry Root { get; set; } - public bool IsCompressed { get; set; } public bool IsAESEncrypted { get; set; } public bool IsNGEncrypted { get; set; } - public long UncompressedSize { get; set; } - - public string RootFileName { get; set; } - public long RootFileSize { get; set; } //offset in the current file @@ -44,6 +39,7 @@ namespace CodeWalker.GameFiles public List AllEntries { get; set; } public List Children { get; set; } public RpfFile Parent { get; set; } + public RpfBinaryFileEntry ParentFileEntry { get; set; } public BinaryReader CurrentFileReader { get; set; } //for temporary use while reading header @@ -61,7 +57,7 @@ namespace CodeWalker.GameFiles public long ExtractedByteCount { get; set; } - public RpfFile(string fpath, string relpath) + public RpfFile(string fpath, string relpath) //for a ROOT filesystem RPF { FileInfo fi = new FileInfo(fpath); Name = fi.Name; @@ -69,22 +65,14 @@ namespace CodeWalker.GameFiles Path = relpath.ToLowerInvariant(); FilePath = fpath; FileSize = fi.Length; - IsCompressed = false; - IsAESEncrypted = false; - RootFileName = Name; - RootFileSize = FileSize; } - public RpfFile(string name, string path, string filepath, long filesize, bool compressed, bool encrypted, string rootfn, long rootfs) + public RpfFile(string name, string path, long filesize) //for a child RPF { Name = name; NameLower = Name.ToLowerInvariant(); Path = path.ToLowerInvariant(); - FilePath = filepath; + FilePath = path; FileSize = filesize; - IsCompressed = compressed; - IsAESEncrypted = encrypted; - RootFileName = rootfn; - RootFileSize = rootfs; } @@ -112,29 +100,18 @@ namespace CodeWalker.GameFiles NamesLength = br.ReadUInt32(); Encryption = (RpfEncryption)br.ReadUInt32(); //0x04E45504F (1313165391): none; 0x0ffffff9 (268435449): AES - if (Version == 0xF00) - { - throw new Exception("Invalid Resource."); - } if (Version != 0x52504637) { throw new Exception("Invalid Resource - not GTAV!"); } - - uint entriestotalbytes = EntryCount * 16; //4x uints each - uint entriesptr = (uint)br.BaseStream.Position; - uint namesptr = entriesptr + entriestotalbytes; - - byte[] entriesdata = new byte[entriestotalbytes]; - int entread = br.Read(entriesdata, 0, (int)entriestotalbytes); - - byte[] namesdata = new byte[NamesLength]; - int namread = br.Read(namesdata, 0, (int)NamesLength); + byte[] entriesdata = br.ReadBytes((int)EntryCount * 16); //4x uints each + byte[] namesdata = br.ReadBytes((int)NamesLength); switch (Encryption) { - case RpfEncryption.OPEN: //nothing to do.. OpenIV modified RPF with unencrypted TOC + case RpfEncryption.NONE: //no encryption + case RpfEncryption.OPEN: //OpenIV style RPF with unencrypted TOC break; case RpfEncryption.AES: entriesdata = GTACrypto.DecryptAES(entriesdata); @@ -153,7 +130,6 @@ namespace CodeWalker.GameFiles } - var entriesrdr = new DataReader(new MemoryStream(entriesdata)); var namesrdr = new DataReader(new MemoryStream(namesdata)); AllEntries = new List(); @@ -221,9 +197,13 @@ namespace CodeWalker.GameFiles { var item = stack.Pop(); - for (int i = (int)item.EntriesIndex; i < (item.EntriesIndex + item.EntriesCount); i++) + int starti = (int)item.EntriesIndex; + int endi = (int)(item.EntriesIndex + item.EntriesCount); + + for (int i = starti; i < endi; i++) { RpfEntry e = AllEntries[i]; + e.Parent = item; if (e is RpfDirectoryEntry) { RpfDirectoryEntry rde = e as RpfDirectoryEntry; @@ -249,6 +229,7 @@ namespace CodeWalker.GameFiles + public void ScanStructure(Action updateStatus, Action errorLog) { using (BinaryReader br = new BinaryReader(File.OpenRead(FilePath))) @@ -269,14 +250,13 @@ namespace CodeWalker.GameFiles { ReadHeader(br); - Children = new List(); - GrandTotalRpfCount = 1; //count this file.. GrandTotalFileCount = 1; //start with this one. GrandTotalFolderCount = 0; GrandTotalResourceCount = 0; GrandTotalBinaryFileCount = 0; + Children = new List(); updateStatus?.Invoke("Scanning " + Path + "..."); @@ -287,18 +267,18 @@ namespace CodeWalker.GameFiles if (entry is RpfBinaryFileEntry) { RpfBinaryFileEntry binentry = entry as RpfBinaryFileEntry; - long l = binentry.FileSize; - if (l == 0) l = binentry.FileUncompressedSize; //search all the sub resources for YSC files. (recurse!) string lname = binentry.NameLower; if (lname.EndsWith(".rpf")) { - br.BaseStream.Position = StartPos + (binentry.FileOffset * 512); + br.BaseStream.Position = StartPos + ((long)binentry.FileOffset * 512); - RpfFile subfile = new RpfFile(binentry.Name, binentry.Path, binentry.Path, l, binentry.FileSize != 0, binentry.IsEncrypted, RootFileName, RootFileSize); - subfile.UncompressedSize = binentry.FileUncompressedSize; + long l = binentry.GetFileSize(); + + RpfFile subfile = new RpfFile(binentry.Name, binentry.Path, l); subfile.Parent = this; + subfile.ParentFileEntry = binentry; subfile.ScanStructure(br, updateStatus, errorLog); @@ -367,18 +347,17 @@ namespace CodeWalker.GameFiles if (entry is RpfBinaryFileEntry) { RpfBinaryFileEntry binentry = entry as RpfBinaryFileEntry; - long l = binentry.FileSize; - if (l == 0) l = binentry.FileUncompressedSize; + long l = binentry.GetFileSize(); //search all the sub resources for YSC files. (recurse!) string lname = binentry.NameLower; if (lname.EndsWith(".rpf")) { - br.BaseStream.Position = StartPos + (binentry.FileOffset * 512); + br.BaseStream.Position = StartPos + ((long)binentry.FileOffset * 512); - RpfFile subfile = new RpfFile(binentry.Name, binentry.Path, binentry.Path, l, binentry.FileSize != 0, binentry.IsEncrypted, RootFileName, RootFileSize); - subfile.UncompressedSize = binentry.FileUncompressedSize; + RpfFile subfile = new RpfFile(binentry.Name, binentry.Path, l); subfile.Parent = this; + subfile.ParentFileEntry = binentry; subfile.ExtractScripts(br, outputfolder, updateStatus); } @@ -398,7 +377,7 @@ namespace CodeWalker.GameFiles //found a YSC file. extract it! string ofpath = outputfolder + "\\" + resentry.Name; - br.BaseStream.Position = StartPos + (resentry.FileOffset * 512); + br.BaseStream.Position = StartPos + ((long)resentry.FileOffset * 512); if (resentry.FileSize > 0) { @@ -505,8 +484,7 @@ namespace CodeWalker.GameFiles { br.BaseStream.Position = StartPos + ((long)entry.FileOffset * 512); - long l = entry.FileSize; - if (l == 0) l = entry.FileUncompressedSize; + long l = entry.GetFileSize(); if (l > 0) { @@ -563,14 +541,11 @@ namespace CodeWalker.GameFiles br.BaseStream.Position += offset; - //byte[] hbytes = new byte[16]; //what are these 16 bytes actually used for? - //br.Read(hbytes, 0, 16); - //MetaHash h1 = br.ReadUInt32(); - //MetaHash h2 = br.ReadUInt32(); - //MetaHash h3 = br.ReadUInt32(); - //MetaHash h4 = br.ReadUInt32(); - //long l1 = br.ReadInt64(); - //long l2 = br.ReadInt64(); + //byte[] hbytes = br.ReadBytes(16); //what are these 16 bytes actually used for? + //if (entry.FileSize > 0xFFFFFF) + //{ //(for huge files, the full file size is packed in 4 of these bytes... seriously wtf) + // var filesize = (hbytes[7] << 0) | (hbytes[14] << 8) | (hbytes[5] << 16) | (hbytes[2] << 24); + //} br.Read(tbytes, 0, (int)totlen); @@ -857,8 +832,8 @@ namespace CodeWalker.GameFiles if (outbuf.Length <= bytes.Length) { - LastError = "Decompressed data was smaller than compressed data..."; - return null; + LastError = "Warning: Decompressed data was smaller than compressed data..."; + //return null; //could still be OK for tiny things! } return outbuf; @@ -871,6 +846,866 @@ namespace CodeWalker.GameFiles return null; } } + public static byte[] CompressBytes(byte[] data) //TODO: refactor this with ResourceBuilder.Compress/Decompress + { + using (MemoryStream ms = new MemoryStream()) + { + DeflateStream ds = new DeflateStream(ms, CompressionMode.Compress, true); + ds.Write(data, 0, data.Length); + ds.Close(); + byte[] deflated = ms.GetBuffer(); + byte[] outbuf = new byte[ms.Length]; //need to copy to the right size buffer... + Array.Copy(deflated, outbuf, outbuf.Length); + return outbuf; + } + } + + + + + + + + + + + + + + + private void WriteHeader(BinaryWriter bw) + { + var namesdata = GetHeaderNamesData(); + NamesLength = (uint)namesdata.Length; + + //ensure there's enough space for the new header, move things if necessary + var headersize = GetHeaderBlockCount() * 512; + EnsureSpace(bw, null, headersize); + + //entries may have been updated, so need to do this after ensuring header space + var entriesdata = GetHeaderEntriesData(); + + //now there's enough space, it's safe to write the header data... + bw.BaseStream.Position = StartPos; + + bw.Write(Version); + bw.Write(EntryCount); + bw.Write(NamesLength); + bw.Write((uint)Encryption); + + + //FileSize = ... //need to make sure this is updated for NG encryption... + switch (Encryption) + { + case RpfEncryption.NONE: //no encryption + case RpfEncryption.OPEN: //OpenIV style RPF with unencrypted TOC + break; + case RpfEncryption.AES: + entriesdata = GTACrypto.EncryptAES(entriesdata); + namesdata = GTACrypto.EncryptAES(namesdata); + IsAESEncrypted = true; + break; + case RpfEncryption.NG: + entriesdata = GTACrypto.EncryptNG(entriesdata, Name, (uint)FileSize); + namesdata = GTACrypto.EncryptNG(namesdata, Name, (uint)FileSize); + IsNGEncrypted = true; + break; + default: //unknown encryption type? assume NG.. should never get here! + entriesdata = GTACrypto.EncryptNG(entriesdata, Name, (uint)FileSize); + namesdata = GTACrypto.EncryptNG(namesdata, Name, (uint)FileSize); + break; + } + + bw.Write(entriesdata); + bw.Write(namesdata); + + WritePadding(bw.BaseStream, StartPos + headersize); //makes sure the actual file can grow... + } + + + private static void WritePadding(Stream s, long upto) + { + int diff = (int)(upto - s.Position); + if (diff > 0) + { + s.Write(new byte[diff], 0, diff); + } + } + + + private void EnsureAllEntries() + { + if (AllEntries == null) + { + //assume this is a new RPF, create the root directory entry + AllEntries = new List(); + Root = new RpfDirectoryEntry(); + Root.File = this; + Root.Name = string.Empty; + Root.NameLower = string.Empty; + Root.Path = Path.ToLowerInvariant(); + } + if (Children == null) + { + Children = new List(); + } + + + + //re-build the AllEntries list from the root node. + List temp = new List(); //for sorting + AllEntries.Clear(); + AllEntries.Add(Root); + Stack stack = new Stack(); + stack.Push(Root); + while (stack.Count > 0) + { + var item = stack.Pop(); + + item.EntriesCount = (uint)(item.Directories.Count + item.Files.Count); + item.EntriesIndex = (uint)AllEntries.Count; + + //having items sorted by name is important for the game for some reason. (it crashes otherwise!) + temp.Clear(); + temp.AddRange(item.Directories); + temp.AddRange(item.Files); + temp.Sort((a, b) => String.CompareOrdinal(a.Name, b.Name)); + + foreach (var entry in temp) + { + AllEntries.Add(entry); + RpfDirectoryEntry dir = entry as RpfDirectoryEntry; + if (dir != null) + { + stack.Push(dir); + } + } + } + + EntryCount = (uint)AllEntries.Count; + + } + private byte[] GetHeaderNamesData() + { + MemoryStream namesstream = new MemoryStream(); + DataWriter nameswriter = new DataWriter(namesstream); + var namedict = new Dictionary(); + foreach (var entry in AllEntries) + { + uint nameoffset; + string name = entry.Name ?? ""; + if (namedict.TryGetValue(name, out nameoffset)) + { + entry.NameOffset = nameoffset; + } + else + { + entry.NameOffset = (uint)namesstream.Length; + namedict.Add(name, entry.NameOffset); + nameswriter.Write(name); + } + } + var buf = new byte[namesstream.Length]; + namesstream.Position = 0; + namesstream.Read(buf, 0, buf.Length); + return PadBuffer(buf, 16); + } + private byte[] GetHeaderEntriesData() + { + MemoryStream entriesstream = new MemoryStream(); + DataWriter entrieswriter = new DataWriter(entriesstream); + foreach (var entry in AllEntries) + { + entry.Write(entrieswriter); + } + var buf = new byte[entriesstream.Length]; + entriesstream.Position = 0; + entriesstream.Read(buf, 0, buf.Length); + return buf; + } + private uint GetHeaderBlockCount()//make sure EntryCount and NamesLength are updated before calling this... + { + uint headerusedbytes = 16 + (EntryCount * 16) + NamesLength; + uint headerblockcount = GetBlockCount(headerusedbytes); + return headerblockcount; + } + private static byte[] PadBuffer(byte[] buf, uint n)//add extra bytes as necessary to nearest n + { + uint buflen = (uint)buf.Length; + uint newlen = PadLength(buflen, n); + if (newlen != buflen) + { + byte[] buf2 = new byte[newlen]; + Buffer.BlockCopy(buf, 0, buf2, 0, buf.Length); + return buf2; + } + return buf; + } + private static uint PadLength(uint l, uint n)//round up to nearest n bytes + { + uint rem = l % n; + return l + ((rem > 0) ? (n - rem) : 0); + } + private static uint GetBlockCount(long bytecount) + { + uint b0 = (uint)(bytecount & 0x1FF); //511; + uint b1 = (uint)(bytecount >> 9); + if (b0 == 0) return b1; + return b1 + 1; + } + private RpfFileEntry FindFirstFileAfter(uint block) + { + RpfFileEntry nextentry = null; + foreach (var entry in AllEntries) + { + RpfFileEntry fe = entry as RpfFileEntry; + if ((fe != null) && (fe.FileOffset > block)) + { + if ((nextentry == null) || (fe.FileOffset < nextentry.FileOffset)) + { + nextentry = fe; + } + } + } + return nextentry; + } + private uint FindHole(uint reqblocks, uint ignorestart, uint ignoreend) + { + //find the block index of a hole that can fit the required number of blocks. + //return 0 if no hole found (0 is the header block, it can't be used for files!) + //make sure any found hole is not within the ignore range + //(i.e. area where space is currently being made) + + //gather and sort the list of files to allow searching for holes + List allfiles = new List(); + foreach (var entry in AllEntries) + { + RpfFileEntry rfe = entry as RpfFileEntry; + if (rfe != null) + { + allfiles.Add(rfe); + } + } + allfiles.Sort((e1, e2) => e1.FileOffset.CompareTo(e2.FileOffset)); + + //find the smallest available hole from the list. + uint found = 0; + uint foundsize = 0xFFFFFFFF; + + for (int i = 1; i < allfiles.Count(); i++) + { + RpfFileEntry e1 = allfiles[i - 1]; + RpfFileEntry e2 = allfiles[i]; + + uint e1cnt = GetBlockCount(e1.GetFileSize()); + uint e1end = e1.FileOffset + e1cnt; + uint e2beg = e2.FileOffset; + if ((e2beg > ignorestart) && (e1end < ignoreend)) + { + continue; //this space is in the ignore area. + } + if (e1end < e2beg) + { + uint space = e2beg - e1end; + if ((space >= reqblocks) && (space < foundsize)) + { + found = e1end; + foundsize = space; + } + } + } + + return found; + } + private uint FindEndBlock() + { + //find the next available block after all other files (or after header if there's no files) + uint endblock = 0; + foreach (var entry in AllEntries) + { + RpfFileEntry e = entry as RpfFileEntry; + if (e != null) + { + uint ecnt = GetBlockCount(e.GetFileSize()); + uint eend = e.FileOffset + ecnt; + if (eend > endblock) + { + endblock = eend; + } + } + } + + if (endblock == 0) + { + //must be no files present, end block comes directly after the header. + endblock = GetHeaderBlockCount(); + } + + return endblock; + } + private void GrowArchive(BinaryWriter bw, uint newblockcount) + { + uint newsize = newblockcount * 512; + if (newsize < FileSize) + { + return;//already bigger than it needs to be, can happen if last file got moved into a hole... + } + if (FileSize == newsize) + { + return;//nothing to do... correct size already + } + + FileSize = newsize; + + + //ensure enough space in the parent if there is one... + if (Parent != null) + { + if (ParentFileEntry == null) + { + throw new Exception("Can't grow archive " + Path + ": ParentFileEntry was null!"); + } + + + //parent's header will be updated with these new values. + ParentFileEntry.FileUncompressedSize = newsize; + ParentFileEntry.FileSize = 0; //archives have FileSize==0 in parent... + + Parent.EnsureSpace(bw, ParentFileEntry, newsize); + } + } + private void RelocateFile(BinaryWriter bw, RpfFileEntry f, uint newblock) + { + //directly move this file. does NOT update the header! + //enough space should already be allocated for this move. + + uint flen = GetBlockCount(f.GetFileSize()); + uint fbeg = f.FileOffset; + uint fend = fbeg + flen; + uint nend = newblock + flen; + if ((nend > fbeg) && (newblock < fend))//can't move to somewhere within itself! + { + throw new Exception("Unable to relocate file " + f.Path + ": new position was inside the original!"); + } + + var stream = bw.BaseStream; + long origpos = stream.Position; + long source = StartPos + ((long)fbeg * 512); + long dest = StartPos + ((long)newblock * 512); + long newstart = dest; + long length = (long)flen * 512; + long destend = dest + length; + const int BUFFER_SIZE = 16384;//what buffer size is best for HDD copy? + var buffer = new byte[BUFFER_SIZE]; + while (length > 0) + { + stream.Position = source; + int i = stream.Read(buffer, 0, (int)Math.Min(length, BUFFER_SIZE)); + stream.Position = dest; + stream.Write(buffer, 0, i); + source += i; + dest += i; + length -= i; + } + + WritePadding(stream, destend);//makes sure the stream can grow if necessary + + stream.Position = origpos;//reset this just to be nice + + f.FileOffset = newblock; + + //if this is a child RPF archive, need to update its StartPos... + var child = FindChildArchive(f); + if (child != null) + { + child.StartPos = newstart; + } + + } + private void EnsureSpace(BinaryWriter bw, RpfFileEntry e, long bytecount) + { + //(called with null entry for ensuring header space) + + uint blockcount = GetBlockCount(bytecount); + uint startblock = e?.FileOffset ?? 0; //0 is always header block + uint endblock = startblock + blockcount; + + RpfFileEntry nextentry = FindFirstFileAfter(startblock); + + while (nextentry != null) //just deal with relocating one entry at a time. + { + //move this nextentry to somewhere else... preferably into a hole otherwise at the end + //if the RPF needs to grow, space needs to be ensured in the parent rpf (if there is one)... + //keep moving further entries until enough space is gained. + + if (nextentry.FileOffset >= endblock) + { + break; //already enough space for this entry, don't go further. + } + + uint entryblocks = GetBlockCount(nextentry.GetFileSize()); + uint newblock = FindHole(entryblocks, startblock, endblock); + if (newblock == 0) + { + //no hole was found, move this entry to the end of the file. + newblock = FindEndBlock(); + GrowArchive(bw, newblock + entryblocks); + } + + //now move the file contents and update the entry's position. + RelocateFile(bw, nextentry, newblock); + + //move on to the next file... + nextentry = FindFirstFileAfter(startblock); + } + + if (nextentry == null) + { + //last entry in the RPF, so just need to grow the RPF enough to fit. + //this could be the header (for an empty RPF)... + uint newblock = FindEndBlock(); + GrowArchive(bw, newblock + ((e != null) ? blockcount : 0)); + } + + //changing a file's size (not the header size!) - need to update the header..! + //also, files could have been moved. so always update the header if we aren't already + if (e != null) + { + WriteHeader(bw); + } + + } + private void InsertFileSpace(BinaryWriter bw, RpfFileEntry entry) + { + //to insert a new entry. find space in the archive for it and assign the FileOffset. + + uint blockcount = GetBlockCount(entry.GetFileSize()); + entry.FileOffset = FindHole(blockcount, 0, 0); + if (entry.FileOffset == 0) + { + entry.FileOffset = FindEndBlock(); + GrowArchive(bw, entry.FileOffset + blockcount); + } + EnsureAllEntries(); + WriteHeader(bw); + } + + private void WriteNewArchive(BinaryWriter bw, RpfEncryption encryption) + { + var stream = bw.BaseStream; + Encryption = encryption; + Version = 0x52504637; //'RPF7' + IsAESEncrypted = (encryption == RpfEncryption.AES); + IsNGEncrypted = (encryption == RpfEncryption.NG); + StartPos = stream.Position; + EnsureAllEntries(); + WriteHeader(bw); + FileSize = stream.Position - StartPos; + } + + private void UpdatePaths(RpfDirectoryEntry dir = null) + { + //recursively update paths, including in child RPFs. + if (dir == null) + { + Root.Path = Path.ToLowerInvariant(); + dir = Root; + } + foreach (var file in dir.Files) + { + file.Path = dir.Path + "\\" + file.NameLower; + + RpfBinaryFileEntry binf = file as RpfBinaryFileEntry; + if ((binf != null) && file.NameLower.EndsWith(".rpf")) + { + RpfFile childrpf = FindChildArchive(binf); + if (childrpf != null) + { + childrpf.Path = binf.Path; + childrpf.FilePath = binf.Path; + childrpf.UpdatePaths(); + } + else + { }//couldn't find child RPF! problem..! + } + + } + foreach (var subdir in dir.Directories) + { + subdir.Path = dir.Path + "\\" + subdir.NameLower; + UpdatePaths(subdir); + } + } + + private RpfFile FindChildArchive(RpfFileEntry f) + { + RpfFile c = null; + if (Children != null) + { + foreach (var child in Children)//kinda messy, but no other option really... + { + if (child.ParentFileEntry == f) + { + c = child; + break; + } + } + } + return c; + } + + + + public static RpfFile CreateNew(string gtafolder, string relpath, RpfEncryption encryption = RpfEncryption.OPEN) + { + //create a new, empty RPF file in the filesystem + //this will assume that the folder the file is going into already exists! + + string fpath = gtafolder; + fpath = fpath.EndsWith("\\") ? fpath : fpath + "\\"; + fpath = fpath + relpath; + + if (File.Exists(fpath)) + { + throw new Exception("File " + fpath + " already exists!"); + } + + File.Create(fpath).Dispose(); //just write a placeholder, will fill it out later + + RpfFile file = new RpfFile(fpath, relpath); + + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + file.WriteNewArchive(bw, encryption); + } + } + + return file; + } + + public static RpfFile CreateNew(RpfDirectoryEntry dir, string name, RpfEncryption encryption = RpfEncryption.OPEN) + { + //create a new empty RPF inside the given parent RPF directory. + + string namel = name.ToLowerInvariant(); + 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!"); + } + + + RpfFile file = new RpfFile(name, rpath, 512);//empty RPF is 512 bytes... + file.Parent = parent; + file.ParentFileEntry = new RpfBinaryFileEntry(); + + RpfBinaryFileEntry entry = file.ParentFileEntry; + entry.Parent = dir; + entry.FileOffset = 0;//InsertFileSpace will update this + entry.FileSize = 0; + entry.FileUncompressedSize = (uint)file.FileSize; + entry.EncryptionType = 0; + entry.IsEncrypted = false; + entry.File = parent; + entry.Path = rpath; + entry.Name = name; + entry.NameLower = namel; + entry.NameHash = JenkHash.GenHash(name); + entry.ShortNameHash = JenkHash.GenHash(entry.GetShortNameLower()); + + dir.Files.Add(entry); + + parent.Children.Add(file); + + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + parent.InsertFileSpace(bw, entry); + + fstream.Position = parent.StartPos + entry.FileOffset * 512; + + file.WriteNewArchive(bw, encryption); + } + } + + + return file; + } + + public static RpfDirectoryEntry CreateDirectory(RpfDirectoryEntry dir, string name) + { + //create a new directory inside the given parent dir + + string namel = name.ToLowerInvariant(); + 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!"); + } + + RpfDirectoryEntry entry = new RpfDirectoryEntry(); + entry.Parent = dir; + entry.File = parent; + entry.Path = rpath; + entry.Name = name; + entry.NameLower = namel; + entry.NameHash = JenkHash.GenHash(name); + entry.ShortNameHash = JenkHash.GenHash(entry.GetShortNameLower()); + + foreach (var exdir in dir.Directories) + { + if (exdir.NameLower == entry.NameLower) + { + throw new Exception("RPF Directory \"" + entry.Name + "\" already exists!"); + } + } + + dir.Directories.Add(entry); + + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + parent.EnsureAllEntries(); + parent.WriteHeader(bw); + } + } + + return entry; + } + + public static RpfFileEntry CreateFile(RpfDirectoryEntry dir, string name, byte[] data) + { + RpfFile parent = dir.File; + string fpath = parent.GetPhysicalFilePath(); + string rpath = dir.Path + "\\" + name; + if (!File.Exists(fpath)) + { + throw new Exception("Root RPF file " + fpath + " does not exist!"); + } + + + RpfFileEntry entry = null; + uint len = (uint)data.Length; + + //check if this is RSC7 data, import as a resource if it is... + if ((len >= 16) && (BitConverter.ToUInt32(data, 0) == 0x37435352)) + { + //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 (entry == null) + { + //no RSC7 header present, import as a binary file. + var compressed = CompressBytes(data); + var bentry = new RpfBinaryFileEntry(); + bentry.EncryptionType = 0;//TODO: binary encryption + bentry.IsEncrypted = false; + bentry.FileUncompressedSize = (uint)data.Length; + bentry.FileSize = (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. + } + } + + + return entry; + } + + + public static void RenameArchive(RpfFile file, string newname) + { + //updates all items in the RPF with the new path - no actual file changes made here + //(since all the paths are generated at runtime and not stored) + + file.Name = newname; + file.NameLower = newname.ToLowerInvariant(); + file.Path = GetParentPath(file.Path) + newname; + file.FilePath = GetParentPath(file.FilePath) + newname; + + file.UpdatePaths(); + + } + + public static void RenameEntry(RpfEntry entry, string newname) + { + //rename the entry in the RPF header... + //also make sure any relevant child paths are updated... + + string dirpath = GetParentPath(entry.Path); + + entry.Name = newname; + entry.NameLower = newname.ToLowerInvariant(); + entry.Path = dirpath + newname; + + string sname = entry.GetShortNameLower(); + JenkIndex.Ensure(sname);//could be anything... but it needs to be there + entry.NameHash = JenkHash.GenHash(newname); + entry.ShortNameHash = JenkHash.GenHash(sname); + + RpfFile parent = entry.File; + string fpath = parent.GetPhysicalFilePath(); + + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + parent.EnsureAllEntries(); + parent.WriteHeader(bw); + } + } + + if (entry is RpfDirectoryEntry) + { + //a folder was renamed, make sure all its children's paths get updated + parent.UpdatePaths(entry as RpfDirectoryEntry); + } + + } + + + public static void DeleteEntry(RpfEntry entry) + { + //delete this entry from the RPF header. + //also remove any references to this item in its parent directory... + //if this is a directory entry, make sure it is empty first + + RpfFile parent = entry.File; + string fpath = parent.GetPhysicalFilePath(); + if (!File.Exists(fpath)) + { + throw new Exception("Root RPF file " + fpath + " does not exist!"); + } + + RpfDirectoryEntry entryasdir = entry as RpfDirectoryEntry; + RpfFileEntry entryasfile = entry as RpfFileEntry;//it has to be one or the other... + + if (entryasdir != null) + { + var dircount = entryasdir.Directories?.Count ?? 0; + var filecount = entryasdir.Files?.Count ?? 0; + if ((dircount + filecount) > 0) + { + throw new Exception("RPF directory is not empty! Please delete its contents first."); + } + } + + if (entry.Parent == null) + { + throw new Exception("Parent directory is null! This shouldn't happen - please refresh the folder!"); + } + + if (entryasdir != null) + { + entry.Parent.Directories.Remove(entryasdir); + } + if (entryasfile != null) + { + entry.Parent.Files.Remove(entryasfile); + + var child = parent.FindChildArchive(entryasfile); + if (child != null) + { + parent.Children.Remove(child); //RPF file being deleted... + } + } + + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + parent.EnsureAllEntries(); + parent.WriteHeader(bw); + } + } + + } + + + + + private static string GetParentPath(string path) + { + string dirpath = path.Replace('/', '\\');//just to make sure.. + int lidx = dirpath.LastIndexOf('\\'); + if (lidx > 0) + { + dirpath = dirpath.Substring(0, lidx + 1); + } + if (!dirpath.EndsWith("\\")) + { + dirpath = dirpath + "\\"; + } + return dirpath; + } public override string ToString() @@ -882,7 +1717,8 @@ namespace CodeWalker.GameFiles public enum RpfEncryption : uint { - OPEN = 0x4E45504F, //1313165391 "OPEN", ie. "no encryption?" + NONE = 0, //some modded RPF's may use this + OPEN = 0x4E45504F, //1313165391 "OPEN", ie. "no encryption" AES = 0x0FFFFFF9, //268435449 NG = 0x0FEFFFFF, //267386879 } @@ -891,6 +1727,7 @@ namespace CodeWalker.GameFiles [TypeConverter(typeof(ExpandableObjectConverter))] public abstract class RpfEntry { public RpfFile File { get; set; } + public RpfDirectoryEntry Parent { get; set; } public uint NameHash { get; set; } public uint ShortNameHash { get; set; } @@ -910,6 +1747,25 @@ namespace CodeWalker.GameFiles { return Path; } + + public string GetShortName() + { + int ind = Name.LastIndexOf('.'); + if (ind > 0) + { + return Name.Substring(0, ind); + } + return Name; + } + public string GetShortNameLower() + { + int ind = NameLower.LastIndexOf('.'); + if (ind > 0) + { + return NameLower.Substring(0, ind); + } + return NameLower; + } } [TypeConverter(typeof(ExpandableObjectConverter))] public class RpfDirectoryEntry : RpfEntry @@ -950,10 +1806,8 @@ namespace CodeWalker.GameFiles public uint FileSize { get; set; } public bool IsEncrypted { get; set; } - public virtual long GetFileSize() - { - return FileSize; - } + public abstract long GetFileSize(); + public abstract void SetFileSize(uint s); } [TypeConverter(typeof(ExpandableObjectConverter))] public class RpfBinaryFileEntry : RpfFileEntry @@ -979,6 +1833,7 @@ namespace CodeWalker.GameFiles default: throw new Exception("Error in RPF7 file entry."); } + } public override void Write(DataWriter writer) { @@ -1012,7 +1867,12 @@ namespace CodeWalker.GameFiles public override long GetFileSize() { - return FileUncompressedSize; + return (FileSize == 0) ? FileUncompressedSize : FileSize; + } + public override void SetFileSize(uint s) + { + //FileUncompressedSize = s; + FileSize = s; } } @@ -1396,10 +2256,13 @@ namespace CodeWalker.GameFiles { writer.Write((ushort)NameOffset); + var fs = FileSize; + if (fs > 0xFFFFFF) fs = 0xFFFFFF;//will also need to make sure the RSC header is updated... + var buf1 = new byte[] { - (byte)((FileSize >> 0) & 0xFF), - (byte)((FileSize >> 8) & 0xFF), - (byte)((FileSize >> 16) & 0xFF) + (byte)((fs >> 0) & 0xFF), + (byte)((fs >> 8) & 0xFF), + (byte)((fs >> 16) & 0xFF) }; writer.Write(buf1); @@ -1422,6 +2285,10 @@ namespace CodeWalker.GameFiles { return (FileSize == 0) ? (long)(SystemSize + GraphicsSize) : FileSize; } + public override void SetFileSize(uint s) + { + FileSize = s; + } } diff --git a/GameFiles/Utils/GTACrypto.cs b/GameFiles/Utils/GTACrypto.cs index fce4e1d..d6ccd47 100644 --- a/GameFiles/Utils/GTACrypto.cs +++ b/GameFiles/Utils/GTACrypto.cs @@ -39,6 +39,10 @@ namespace CodeWalker.GameFiles { return DecryptAESData(data, GTA5Keys.PC_AES_KEY); } + public static byte[] EncryptAES(byte[] data) + { + return EncryptAESData(data, GTA5Keys.PC_AES_KEY); + } public static byte[] DecryptAESData(byte[] data, byte[] key, int rounds = 1) { @@ -247,5 +251,179 @@ namespace CodeWalker.GameFiles + + + + + + + + + + + + + + + public static byte[] EncryptNG(byte[] data, string name, uint length) + { + byte[] key = GetNGKey(name, length); + return EncryptNG(data, key); + } + + public static byte[] EncryptNG(byte[] data, byte[] key) + { + if ((GTA5Keys.PC_NG_ENCRYPT_TABLES == null) || (GTA5Keys.PC_NG_ENCRYPT_LUTs == null)) + { + throw new Exception("Unable to encrypt - tables not loaded."); + } + + var encryptedData = new byte[data.Length]; + + var keyuints = new uint[key.Length / 4]; + Buffer.BlockCopy(key, 0, keyuints, 0, key.Length); + + for (int blockIndex = 0; blockIndex < data.Length / 16; blockIndex++) + { + byte[] decryptedBlock = new byte[16]; + Array.Copy(data, 16 * blockIndex, decryptedBlock, 0, 16); + byte[] encryptedBlock = EncryptBlock(decryptedBlock, keyuints); + Array.Copy(encryptedBlock, 0, encryptedData, 16 * blockIndex, 16); + } + + if (data.Length % 16 != 0) + { + var left = data.Length % 16; + Buffer.BlockCopy(data, data.Length - left, encryptedData, data.Length - left, left); + } + + return encryptedData; + } + + public static byte[] EncryptBlock(byte[] data, uint[] key) + { + var buffer = data; + + // prepare key... + var subKeys = new uint[17][]; + for (int i = 0; i < 17; i++) + { + subKeys[i] = new uint[4]; + subKeys[i][0] = key[4 * i + 0]; + subKeys[i][1] = key[4 * i + 1]; + subKeys[i][2] = key[4 * i + 2]; + subKeys[i][3] = key[4 * i + 3]; + } + + buffer = EncryptRoundA(buffer, subKeys[16], GTA5Keys.PC_NG_ENCRYPT_TABLES[16]); + for (int k = 15; k >= 2; k--) + buffer = EncryptRoundB_LUT(buffer, subKeys[k], GTA5Keys.PC_NG_ENCRYPT_LUTs[k]); + buffer = EncryptRoundA(buffer, subKeys[1], GTA5Keys.PC_NG_ENCRYPT_TABLES[1]); + buffer = EncryptRoundA(buffer, subKeys[0], GTA5Keys.PC_NG_ENCRYPT_TABLES[0]); + + return buffer; + } + + public static byte[] EncryptRoundA(byte[] data, uint[] key, uint[][] table) + { + // apply xor to data first... + var xorbuf = new byte[16]; + Buffer.BlockCopy(key, 0, xorbuf, 0, 16); + + var x1 = + table[0][data[0] ^ xorbuf[0]] ^ + table[1][data[1] ^ xorbuf[1]] ^ + table[2][data[2] ^ xorbuf[2]] ^ + table[3][data[3] ^ xorbuf[3]]; + var x2 = + table[4][data[4] ^ xorbuf[4]] ^ + table[5][data[5] ^ xorbuf[5]] ^ + table[6][data[6] ^ xorbuf[6]] ^ + table[7][data[7] ^ xorbuf[7]]; + var x3 = + table[8][data[8] ^ xorbuf[8]] ^ + table[9][data[9] ^ xorbuf[9]] ^ + table[10][data[10] ^ xorbuf[10]] ^ + table[11][data[11] ^ xorbuf[11]]; + var x4 = + table[12][data[12] ^ xorbuf[12]] ^ + table[13][data[13] ^ xorbuf[13]] ^ + table[14][data[14] ^ xorbuf[14]] ^ + table[15][data[15] ^ xorbuf[15]]; + + var buf = new byte[16]; + Array.Copy(BitConverter.GetBytes(x1), 0, buf, 0, 4); + Array.Copy(BitConverter.GetBytes(x2), 0, buf, 4, 4); + Array.Copy(BitConverter.GetBytes(x3), 0, buf, 8, 4); + Array.Copy(BitConverter.GetBytes(x4), 0, buf, 12, 4); + return buf; + } + + public static byte[] EncryptRoundA_LUT(byte[] dataOld, uint[] key, GTA5NGLUT[] lut) + { + var data = (byte[])dataOld.Clone(); + + // apply xor to data first... + var xorbuf = new byte[16]; + Buffer.BlockCopy(key, 0, xorbuf, 0, 16); + for (int y = 0; y < 16; y++) + { + data[y] ^= xorbuf[y]; + } + + return new byte[] { + lut[0].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[1].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[2].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[3].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[4].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[5].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[6].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[7].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[8].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[9].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[10].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[11].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[12].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[13].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[14].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[15].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)) + }; + } + + public static byte[] EncryptRoundB_LUT(byte[] dataOld, uint[] key, GTA5NGLUT[] lut) + { + var data = (byte[])dataOld.Clone(); + + // apply xor to data first... + var xorbuf = new byte[16]; + Buffer.BlockCopy(key, 0, xorbuf, 0, 16); + for (int y = 0; y < 16; y++) + { + data[y] ^= xorbuf[y]; + } + + return new byte[] { + lut[0].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[1].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[2].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[3].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[4].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[5].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[6].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[7].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[8].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0)), + lut[9].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[10].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[11].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[12].LookUp(BitConverter.ToUInt32( new byte[] { data[12], data[13], data[14], data[15] }, 0)), + lut[13].LookUp(BitConverter.ToUInt32( new byte[] { data[0], data[1], data[2], data[3] }, 0)), + lut[14].LookUp(BitConverter.ToUInt32( new byte[] { data[4], data[5], data[6], data[7] }, 0)), + lut[15].LookUp(BitConverter.ToUInt32( new byte[] { data[8], data[9], data[10], data[11] }, 0))}; + } + + + + } }