From 65c549840f85613791e79ee2a085f466f38d5203 Mon Sep 17 00:00:00 2001 From: dexyfex Date: Wed, 10 Jan 2018 14:17:30 +1100 Subject: [PATCH 1/6] 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))}; + } + + + + } } From d9797bd04dc386121dd020164592026cf55d674b Mon Sep 17 00:00:00 2001 From: dexyfex Date: Wed, 10 Jan 2018 15:55:19 +1100 Subject: [PATCH 2/6] RPF Explorer Import XML --- ExploreForm.cs | 102 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/ExploreForm.cs b/ExploreForm.cs index 46d5698..9d6217c 100644 --- a/ExploreForm.cs +++ b/ExploreForm.cs @@ -14,6 +14,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using System.Xml; namespace CodeWalker { @@ -2034,7 +2035,98 @@ namespace CodeWalker { if (!EditMode) return; if (CurrentFolder?.IsSearchResults ?? false) return; - MessageBox.Show("Import XML TODO..."); + + 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; + } + + OpenFileDialog.Filter = "XML Files|*.xml"; + 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 (!fnamel.EndsWith(".xml")) + { + MessageBox.Show(fname + ": Not an XML file!", "Cannot import XML"); + continue; + } + if (fnamel.EndsWith(".pso.xml")) + { + MessageBox.Show(fname + ": PSO XML import not yet supported.", "Cannot import XML"); + continue; + } + if (fnamel.EndsWith(".rbf.xml")) + { + MessageBox.Show(fname + ": RBF XML import not yet supported.", "Cannot import XML"); + continue; + } + + fname = fname.Substring(0, fname.Length - 4); + fnamel = fnamel.Substring(0, fnamel.Length - 4); + + var doc = new XmlDocument(); + string text = File.ReadAllText(fpath); + if (!string.IsNullOrEmpty(text)) + { + doc.LoadXml(text); + } + + var meta = XmlMeta.GetMeta(doc); + + + if ((meta.DataBlocks?.Data == null) || (meta.DataBlocks.Count == 0)) + { + MessageBox.Show(fname + ": Schema not supported.", "Cannot import XML"); + continue; + } + + + byte[] data = ResourceBuilder.Build(meta, 2); //meta is RSC V:2 + + + 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; + } + } + + RpfFile.CreateFile(parentrpffldr, fname, data); + + } + } + catch (Exception ex) + { + MessageBox.Show(ex.Message, "Unable to import file"); + return; + } + + CurrentFolder.ListItems = null; + RefreshMainListView(); + } private void ImportRaw() { @@ -2048,6 +2140,7 @@ namespace CodeWalker return; } + OpenFileDialog.Filter = string.Empty; if (OpenFileDialog.ShowDialog(this) != DialogResult.OK) { return;//canceled @@ -2073,6 +2166,9 @@ namespace CodeWalker continue; } + byte[] data = File.ReadAllBytes(fpath); + + foreach (var exfile in parentrpffldr.Files) { if (exfile.NameLower == fnamel) @@ -2085,10 +2181,6 @@ namespace CodeWalker } } - - byte[] data = File.ReadAllBytes(fpath); - - RpfFile.CreateFile(parentrpffldr, fname, data); } From 262b50303d2cbfe96959bb220a4068ab6520dd64 Mon Sep 17 00:00:00 2001 From: dexyfex Date: Wed, 10 Jan 2018 18:09:46 +1100 Subject: [PATCH 3/6] R29_dev1 - Updated todo --- Todo.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Todo.txt b/Todo.txt index 09d0048..aa5edfc 100644 --- a/Todo.txt +++ b/Todo.txt @@ -102,7 +102,9 @@ done: [v.29] entitySets - like mapDataGroups but for interiors..... in Mlo data -delete key - delete item! +World view - delete key - delete item! +RPF Explorer Edit mode +AWC audio player (dav90 WIP) [v.28] From f9bda5835a3bb98d8c0c2a4fc59c1bf2c7f27ace Mon Sep 17 00:00:00 2001 From: dexyfex Date: Thu, 11 Jan 2018 12:10:03 +1100 Subject: [PATCH 4/6] RPF Explorer Edit mode - auto change archive encryption type when editing, import/export raw child RPFs --- ExploreForm.cs | 128 +++++++++++++++++++++++++++++---- GameFiles/Resources/RpfFile.cs | 74 ++++++++++++++----- Todo.txt | 1 + 3 files changed, 174 insertions(+), 29 deletions(-) diff --git a/ExploreForm.cs b/ExploreForm.cs index 9d6217c..cd2aec6 100644 --- a/ExploreForm.cs +++ b/ExploreForm.cs @@ -1132,11 +1132,21 @@ namespace CodeWalker private byte[] GetFileData(MainListItem file) { byte[] data = null; - if (file.File != null) + if (file.Folder != null) + { + var entry = file.Folder.RpfFile?.ParentFileEntry; + if (entry != null) + { + data = entry.File.ExtractFile(entry);//extract an RPF from another. + } + } + else if (file.File != null) { //load file from RPF - if (file.File.File == null) return null; //no RPF file? go no further - data = file.File.File.ExtractFile(file.File); + if (file.File.File != null) //need the reference to the RPF archive + { + data = file.File.File.ExtractFile(file.File); + } } else if (!string.IsNullOrEmpty(file.FullPath)) { @@ -1515,22 +1525,27 @@ namespace CodeWalker bool isitem = false; bool isfile = false; bool isfolder = false; + bool isarchive = false; bool isfilesys = false; bool issearch = CurrentFolder?.IsSearchResults ?? false; bool canview = false; bool canexportxml = false; + bool canextract = false; bool canimport = EditMode && (CurrentFolder?.RpfFolder != null) && !issearch; bool canedit = false; if (item != null) { + var entry = item.GetRpfEntry(); + isitem = true; + isfilesys = (entry == null); + isarchive = (item.Folder?.RpfFile != null); isfolder = (item.Folder != null); - isfilesys = (item.File == null) && (item.Folder == null); + isfile = !isfolder; canview = CanViewFile(item); canexportxml = CanExportXml(item); - isitem = true; - isfile = !isfolder; canedit = EditMode && !issearch; + canextract = isfile || (isarchive && !isfilesys); } @@ -1538,7 +1553,7 @@ namespace CodeWalker ListContextViewHexMenu.Enabled = isfile; ListContextExportXmlMenu.Enabled = canexportxml; - ListContextExtractRawMenu.Enabled = isfile; + ListContextExtractRawMenu.Enabled = canextract; ListContextExtractUncompressedMenu.Enabled = isfile; ListContextNewMenu.Visible = EditMode; @@ -1605,6 +1620,50 @@ namespace CodeWalker + + private bool EnsureRpfValidEncryption() + { + if (CurrentFolder.RpfFolder == null) return false; + + var rpf = CurrentFolder.RpfFolder.File; + + if (rpf == null) return false; + + bool needsupd = false; + var f = rpf; + List files = new List(); + while (f != null) + { + if (f.Encryption != RpfEncryption.OPEN) + { + var msg = "Archive " + f.Name + " is currently set to " + f.Encryption.ToString() + " encryption.\nAre you sure you want to change this archive to OPEN encryption?\nLoading by the game will require OpenIV.asi."; + if (MessageBox.Show(msg, "Change RPF encryption type", MessageBoxButtons.YesNo) != DialogResult.Yes) + { + return false; + } + needsupd = true; + } + if (needsupd) + { + files.Add(f); + } + f = f.Parent; + } + + //change encryption types, starting from the root rpf. + files.Reverse(); + foreach (var file in files) + { + RpfFile.SetEncryptionType(file, RpfEncryption.OPEN); + } + + return true; + } + + + + + private void ViewSelected() { for (int i = 0; i < MainListView.SelectedIndices.Count; i++) @@ -1731,7 +1790,8 @@ namespace CodeWalker var idx = MainListView.SelectedIndices[0]; if ((idx < 0) || (idx >= CurrentFiles.Count)) return; var file = CurrentFiles[idx]; - if (file.Folder == null) + + if ((file.Folder == null) || (file.Folder.RpfFile != null)) { byte[] data = GetFileData(file); if (data == null) @@ -1744,7 +1804,7 @@ namespace CodeWalker RpfResourceFileEntry rrfe = file.File as RpfResourceFileEntry; if (rrfe != null) //add resource header if this is a resource file. { - data = ResourceBuilder.Compress(data); + data = ResourceBuilder.Compress(data); //not completely ideal to recompress it... data = ResourceBuilder.AddResourceHeader(rrfe, data); } @@ -1777,7 +1837,7 @@ namespace CodeWalker var idx = MainListView.SelectedIndices[i]; if ((idx < 0) || (idx >= CurrentFiles.Count)) continue; var file = CurrentFiles[idx]; - if (file.Folder == null) + if ((file.Folder == null) || (file.Folder.RpfFile != null)) { var path = folderpath + file.Name; var data = GetFileData(file); @@ -1791,7 +1851,7 @@ namespace CodeWalker RpfResourceFileEntry rrfe = file.File as RpfResourceFileEntry; if (rrfe != null) //add resource header if this is a resource file. { - data = ResourceBuilder.Compress(data); + data = ResourceBuilder.Compress(data); //not completely ideal to recompress it... data = ResourceBuilder.AddResourceHeader(rrfe, data); } @@ -1893,7 +1953,7 @@ namespace CodeWalker foreach (var file in CurrentFiles) { - if (file.Folder == null) + if ((file.Folder == null) || (file.Folder.RpfFile != null)) { var path = folderpath + file.Name; var data = GetFileData(file); @@ -1950,6 +2010,8 @@ namespace CodeWalker { if (CurrentFolder.RpfFolder != null) { + if (!EnsureRpfValidEncryption()) return; + //create new directory entry in the RPF. newdir = RpfFile.CreateDirectory(CurrentFolder.RpfFolder, fname); @@ -2007,6 +2069,8 @@ namespace CodeWalker { if (CurrentFolder.RpfFolder != null) { + if (!EnsureRpfValidEncryption()) return; + //adding a new RPF as a child of another newrpf = RpfFile.CreateNew(CurrentFolder.RpfFolder, fname, encryption); } @@ -2043,6 +2107,9 @@ namespace CodeWalker return; } + if (!EnsureRpfValidEncryption()) return; + + OpenFileDialog.Filter = "XML Files|*.xml"; if (OpenFileDialog.ShowDialog(this) != DialogResult.OK) { @@ -2140,6 +2207,9 @@ namespace CodeWalker return; } + if (!EnsureRpfValidEncryption()) return; + + OpenFileDialog.Filter = string.Empty; if (OpenFileDialog.ShowDialog(this) != DialogResult.OK) { @@ -2181,7 +2251,31 @@ namespace CodeWalker } } - RpfFile.CreateFile(parentrpffldr, fname, data); + var entry = RpfFile.CreateFile(parentrpffldr, fname, data); + + + var newrpf = parentrpffldr.File?.FindChildArchive(entry); + if (newrpf != null) + { + //an RPF file was imported. add its structure to the UI! + var rootpath = GetRootPath(); + var tnf = CreateRpfTreeFolder(newrpf, newrpf.Path, rootpath + newrpf.Path); + if (CurrentFolder.Children != null) //make sure any existing (replaced!) one is removed first! + { + foreach (var child in CurrentFolder.Children) + { + if (child.Path == tnf.Path) + { + CurrentFolder.Children.Remove(child); + child.TreeNode.Remove(); + break; + } + } + } + CurrentFolder.AddChildToHierarchy(tnf); + RecurseMainTreeViewRPF(tnf, AllRpfs); + RecurseAddMainTreeViewNodes(tnf, CurrentFolder.TreeNode); + } } } @@ -2281,6 +2375,8 @@ namespace CodeWalker } if (entry != null) { + if (!EnsureRpfValidEncryption()) return; + //renaming an entry in an RPF RpfFile.RenameEntry(entry, newname); } @@ -2327,6 +2423,9 @@ namespace CodeWalker if (MainListView.SelectedIndices.Count != 1) return; MessageBox.Show("ReplaceSelected TODO..."); //delete the selected items, and replace with... choose + + //if (!EnsureRpfEncryptionType()) return; + } private void DeleteSelected() { @@ -2368,6 +2467,8 @@ namespace CodeWalker if (parent.RpfFolder != null) { //delete an item in an RPF. + if (!EnsureRpfValidEncryption()) return; + RpfEntry entry = item.GetRpfEntry(); RpfFile.DeleteEntry(entry); @@ -3251,6 +3352,7 @@ namespace CodeWalker { FileSize = fld.RpfFile.FileSize; FileSizeText = TextUtil.GetBytesReadable(FileSize); + Attributes += fld.RpfFile.Encryption.ToString() + " encryption"; } else { diff --git a/GameFiles/Resources/RpfFile.cs b/GameFiles/Resources/RpfFile.cs index 287a4d0..fc2138a 100644 --- a/GameFiles/Resources/RpfFile.cs +++ b/GameFiles/Resources/RpfFile.cs @@ -885,15 +885,6 @@ namespace CodeWalker.GameFiles //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) { @@ -916,6 +907,13 @@ namespace CodeWalker.GameFiles break; } + //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); bw.Write(entriesdata); bw.Write(namesdata); @@ -1337,7 +1335,7 @@ namespace CodeWalker.GameFiles } } - private RpfFile FindChildArchive(RpfFileEntry f) + public RpfFile FindChildArchive(RpfFileEntry f) { RpfFile c = null; if (Children != null) @@ -1442,8 +1440,8 @@ namespace CodeWalker.GameFiles { //create a new directory inside the given parent dir - string namel = name.ToLowerInvariant(); RpfFile parent = dir.File; + string namel = name.ToLowerInvariant(); string fpath = parent.GetPhysicalFilePath(); string rpath = dir.Path + "\\" + namel; @@ -1486,8 +1484,9 @@ namespace CodeWalker.GameFiles public static RpfFileEntry CreateFile(RpfDirectoryEntry dir, string name, byte[] data) { RpfFile parent = dir.File; + string namel = name.ToLowerInvariant(); string fpath = parent.GetPhysicalFilePath(); - string rpath = dir.Path + "\\" + name; + string rpath = dir.Path + "\\" + namel; if (!File.Exists(fpath)) { throw new Exception("Root RPF file " + fpath + " does not exist!"); @@ -1497,8 +1496,15 @@ namespace CodeWalker.GameFiles 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)) + + bool isrpf = 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(); @@ -1519,16 +1525,20 @@ namespace CodeWalker.GameFiles entry = rentry; } + if (namel.EndsWith(".rpf") && (hdr == 0x52504637)) //'RPF7' + { + isrpf = true; + } if (entry == null) { //no RSC7 header present, import as a binary file. - var compressed = CompressBytes(data); + var compressed = isrpf ? data : 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; + bentry.FileSize = isrpf ? 0 : (uint)compressed.Length; if (bentry.FileSize > 0xFFFFFF) { bentry.FileSize = 0; @@ -1577,6 +1587,25 @@ namespace CodeWalker.GameFiles } + 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; } @@ -1690,6 +1719,19 @@ namespace CodeWalker.GameFiles } + public static void SetEncryptionType(RpfFile file, RpfEncryption encryption) + { + file.Encryption = encryption; + string fpath = file.GetPhysicalFilePath(); + using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite)) + { + using (var bw = new BinaryWriter(fstream)) + { + file.WriteHeader(bw); + } + } + } + private static string GetParentPath(string path) diff --git a/Todo.txt b/Todo.txt index aa5edfc..e7a34db 100644 --- a/Todo.txt +++ b/Todo.txt @@ -105,6 +105,7 @@ entitySets - like mapDataGroups but for interiors..... in Mlo data World view - delete key - delete item! RPF Explorer Edit mode AWC audio player (dav90 WIP) +Invert mouse option [v.28] From 54a3c1fc71299ff3362fc84a429a946535a082a8 Mon Sep 17 00:00:00 2001 From: dexyfex Date: Thu, 11 Jan 2018 14:06:48 +1100 Subject: [PATCH 5/6] RPF Explorer export XML for .cut files, list context menu bug fix --- ExploreForm.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ExploreForm.cs b/ExploreForm.cs index cd2aec6..2844744 100644 --- a/ExploreForm.cs +++ b/ExploreForm.cs @@ -1199,6 +1199,7 @@ namespace CodeWalker case FileTypeAction.ViewYmap: case FileTypeAction.ViewYtyp: case FileTypeAction.ViewJPso: + case FileTypeAction.ViewCut: return true; } return false; @@ -1532,6 +1533,7 @@ namespace CodeWalker bool canexportxml = false; bool canextract = false; bool canimport = EditMode && (CurrentFolder?.RpfFolder != null) && !issearch; + bool cancreate = EditMode && !issearch; bool canedit = false; if (item != null) @@ -1556,10 +1558,10 @@ namespace CodeWalker ListContextExtractRawMenu.Enabled = canextract; ListContextExtractUncompressedMenu.Enabled = isfile; - ListContextNewMenu.Visible = EditMode; + ListContextNewMenu.Visible = cancreate; ListContextImportRawMenu.Visible = canimport; ListContextImportXmlMenu.Visible = canimport; - ListContextImportSeparator.Visible = EditMode; + ListContextImportSeparator.Visible = cancreate; ListContextCopyMenu.Enabled = isfile; ListContextCopyPathMenu.Enabled = isitem; @@ -2260,8 +2262,9 @@ namespace CodeWalker //an RPF file was imported. add its structure to the UI! var rootpath = GetRootPath(); var tnf = CreateRpfTreeFolder(newrpf, newrpf.Path, rootpath + newrpf.Path); - if (CurrentFolder.Children != null) //make sure any existing (replaced!) one is removed first! + if (CurrentFolder.Children != null) { + //make sure any existing (replaced!) one is removed first! foreach (var child in CurrentFolder.Children) { if (child.Path == tnf.Path) From 8c7415e8a88759a222689776a400f191131b7793 Mon Sep 17 00:00:00 2001 From: dexyfex Date: Thu, 11 Jan 2018 15:03:58 +1100 Subject: [PATCH 6/6] RPF Explorer Edit mode warning message --- ExploreForm.Designer.cs | 92 ++++++++++++++++++++++++++++++++++++++++- ExploreForm.cs | 14 +++++++ ExploreForm.resx | 40 +++++++++++++----- 3 files changed, 134 insertions(+), 12 deletions(-) diff --git a/ExploreForm.Designer.cs b/ExploreForm.Designer.cs index 5a57795..85fc40d 100644 --- a/ExploreForm.Designer.cs +++ b/ExploreForm.Designer.cs @@ -127,6 +127,12 @@ this.SaveFileDialog = new System.Windows.Forms.SaveFileDialog(); this.OpenFileDialog = new System.Windows.Forms.OpenFileDialog(); this.FolderBrowserDialog = new System.Windows.Forms.FolderBrowserDialog(); + this.EditModeBaseWarningPanel = new System.Windows.Forms.Panel(); + this.label1 = new System.Windows.Forms.Label(); + this.pictureBox1 = new System.Windows.Forms.PictureBox(); + this.EditModeModsWarningPanel = new System.Windows.Forms.Panel(); + this.pictureBox2 = new System.Windows.Forms.PictureBox(); + this.label2 = new System.Windows.Forms.Label(); this.MainMenu.SuspendLayout(); this.MainToolbar.SuspendLayout(); this.StatusBar.SuspendLayout(); @@ -136,6 +142,10 @@ this.MainSplitContainer.SuspendLayout(); this.ListContextMenu.SuspendLayout(); this.TreeContextMenu.SuspendLayout(); + this.EditModeBaseWarningPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit(); + this.EditModeModsWarningPanel.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).BeginInit(); this.SuspendLayout(); // // MainMenu @@ -658,6 +668,8 @@ // MainSplitContainer.Panel2 // this.MainSplitContainer.Panel2.Controls.Add(this.MainListView); + this.MainSplitContainer.Panel2.Controls.Add(this.EditModeBaseWarningPanel); + this.MainSplitContainer.Panel2.Controls.Add(this.EditModeModsWarningPanel); this.MainSplitContainer.Size = new System.Drawing.Size(876, 516); this.MainSplitContainer.SplitterDistance = 309; this.MainSplitContainer.TabIndex = 3; @@ -717,7 +729,7 @@ // MainNameColumnHeader // this.MainNameColumnHeader.Text = "Name"; - this.MainNameColumnHeader.Width = 225; + this.MainNameColumnHeader.Width = 219; // // MainTypeColumnHeader // @@ -1017,6 +1029,72 @@ // this.OpenFileDialog.Multiselect = true; // + // EditModeBaseWarningPanel + // + this.EditModeBaseWarningPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.EditModeBaseWarningPanel.BackColor = System.Drawing.Color.DarkRed; + this.EditModeBaseWarningPanel.Controls.Add(this.pictureBox1); + this.EditModeBaseWarningPanel.Controls.Add(this.label1); + this.EditModeBaseWarningPanel.Location = new System.Drawing.Point(1, 3); + this.EditModeBaseWarningPanel.Name = "EditModeBaseWarningPanel"; + this.EditModeBaseWarningPanel.Size = new System.Drawing.Size(559, 24); + this.EditModeBaseWarningPanel.TabIndex = 1; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.BackColor = System.Drawing.Color.Transparent; + this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.ForeColor = System.Drawing.Color.White; + this.label1.Location = new System.Drawing.Point(25, 5); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(285, 13); + this.label1.TabIndex = 0; + this.label1.Text = "Warning: You are directly editing base game files"; + // + // pictureBox1 + // + this.pictureBox1.Image = ((System.Drawing.Image)(resources.GetObject("pictureBox1.Image"))); + this.pictureBox1.Location = new System.Drawing.Point(6, 3); + this.pictureBox1.Name = "pictureBox1"; + this.pictureBox1.Size = new System.Drawing.Size(16, 16); + this.pictureBox1.TabIndex = 1; + this.pictureBox1.TabStop = false; + // + // EditModeModsWarningPanel + // + this.EditModeModsWarningPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.EditModeModsWarningPanel.BackColor = System.Drawing.Color.DarkGreen; + this.EditModeModsWarningPanel.Controls.Add(this.pictureBox2); + this.EditModeModsWarningPanel.Controls.Add(this.label2); + this.EditModeModsWarningPanel.Location = new System.Drawing.Point(1, 28); + this.EditModeModsWarningPanel.Name = "EditModeModsWarningPanel"; + this.EditModeModsWarningPanel.Size = new System.Drawing.Size(559, 24); + this.EditModeModsWarningPanel.TabIndex = 2; + // + // pictureBox2 + // + this.pictureBox2.Image = ((System.Drawing.Image)(resources.GetObject("pictureBox2.Image"))); + this.pictureBox2.Location = new System.Drawing.Point(6, 3); + this.pictureBox2.Name = "pictureBox2"; + this.pictureBox2.Size = new System.Drawing.Size(16, 16); + this.pictureBox2.TabIndex = 1; + this.pictureBox2.TabStop = false; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.BackColor = System.Drawing.Color.Transparent; + this.label2.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label2.ForeColor = System.Drawing.Color.White; + this.label2.Location = new System.Drawing.Point(25, 5); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(225, 13); + this.label2.TabIndex = 0; + this.label2.Text = "You are editing files in the mods folder"; + // // ExploreForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); @@ -1043,6 +1121,12 @@ this.MainSplitContainer.ResumeLayout(false); this.ListContextMenu.ResumeLayout(false); this.TreeContextMenu.ResumeLayout(false); + this.EditModeBaseWarningPanel.ResumeLayout(false); + this.EditModeBaseWarningPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit(); + this.EditModeModsWarningPanel.ResumeLayout(false); + this.EditModeModsWarningPanel.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.pictureBox2)).EndInit(); this.ResumeLayout(false); this.PerformLayout(); @@ -1147,5 +1231,11 @@ private System.Windows.Forms.ToolStripMenuItem ListContextNewRpfArchiveMenu; private System.Windows.Forms.ToolStripSeparator toolStripSeparator10; private System.Windows.Forms.ToolStripButton EditModeButton; + private System.Windows.Forms.Panel EditModeBaseWarningPanel; + private System.Windows.Forms.PictureBox pictureBox1; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Panel EditModeModsWarningPanel; + private System.Windows.Forms.PictureBox pictureBox2; + private System.Windows.Forms.Label label2; } } \ No newline at end of file diff --git a/ExploreForm.cs b/ExploreForm.cs index 2844744..c73e040 100644 --- a/ExploreForm.cs +++ b/ExploreForm.cs @@ -405,6 +405,7 @@ namespace CodeWalker //called after the CurrentFolder and CurrentFiles have changed. UpdateNavigateUI(); + EnsureEditModeWarning(); if (!HistoryNavigating) //only do this if not currently navigating forward or back { @@ -1600,9 +1601,22 @@ namespace CodeWalker EditModeButton.Checked = enable; MainListView.LabelEdit = enable; + EnsureEditModeWarning(); } + private void EnsureEditModeWarning() + { + bool show = EditMode && !CurrentFolder.Path.ToLowerInvariant().StartsWith("mods"); + int gap = 3; + int bot = MainListView.Bottom; + EditModeBaseWarningPanel.Top = gap; + EditModeModsWarningPanel.Top = gap; + EditModeModsWarningPanel.Visible = false; + + MainListView.Top = show ? EditModeBaseWarningPanel.Bottom + gap : gap; + MainListView.Height = bot - MainListView.Top; + } diff --git a/ExploreForm.resx b/ExploreForm.resx index 3fa8922..f35d760 100644 --- a/ExploreForm.resx +++ b/ExploreForm.resx @@ -271,6 +271,16 @@ e2d2SZBn3BDEyPiVG5X23Ap3LtwYqxPDuBkxUEmGNWnPzTCEn1GZjBBcYawROqpiopbZ8v/CtN9mmB+9 1vZY1yV7KT9+37JAwB1LBeyfTv8N11OX0LGtniroCF2hd2L+f3A9qqp2iWbL30hjPP3/CJi+jvVtWwLw A4Rmgl76+inbAAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADVSURBVDhPpVIxDoMwDORJfQN7pI5hz1pmytSJ7/EFhrYZ + MqSVgDXlIhslaoga9aQTsn1nOyFVDsuynNd1HTe6gCPyJDnGJrpN0+T6vnd1Xe9EjDzqJP0GJkAkhIjM + TOSpSXoTrMmTu+7qHvene9m3/7aX1udRh44sMXBWnsZmJmKuQUeWGFiPRaGZKaXMN/hrg3meTygopbyo + 6A7IbIdh2KekmPwLR+amaaI4+Q6OzDiGMcafNWD8EnNmrbVFnaRppBr8bGaETYrNDG4CFpsZMJaZq+oD + NQar60zqQI0AAAAASUVORK5CYII= @@ -290,16 +300,6 @@ lVY67e7NJiI/2QxXrEetVVZsAY5938U5NzUbthbgknMW7735iOnYsB0AqBXXlJL5jOnYsDUBqA1uMcbh mYyuz6aAU/M9hKDP3GR0ffYegNrwXEpRADdZr5+aAlB7UAB3j1V/Anh1j1UD4Fub4YrN8HPL9gAVE1vf J6IiRgAAAABJRU5ErkJggg== - - - - - iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADVSURBVDhPpVIxDoMwDORJfQN7pI5hz1pmytSJ7/EFhrYZ - MqSVgDXlIhslaoga9aQTsn1nOyFVDsuynNd1HTe6gCPyJDnGJrpN0+T6vnd1Xe9EjDzqJP0GJkAkhIjM - TOSpSXoTrMmTu+7qHvene9m3/7aX1udRh44sMXBWnsZmJmKuQUeWGFiPRaGZKaXMN/hrg3meTygopbyo - 6A7IbIdh2KekmPwLR+amaaI4+Q6OzDiGMcafNWD8EnNmrbVFnaRppBr8bGaETYrNDG4CFpsZMJaZq+oD - NQar60zqQI0AAAAASUVORK5CYII= @@ -313,7 +313,7 @@ AAEAAAD/////AQAAAAAAAAAMAgAAAFdTeXN0ZW0uV2luZG93cy5Gb3JtcywgVmVyc2lvbj00LjAuMC4w LCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI3N2E1YzU2MTkzNGUwODkFAQAAACZTeXN0 ZW0uV2luZG93cy5Gb3Jtcy5JbWFnZUxpc3RTdHJlYW1lcgEAAAAERGF0YQcCAgAAAAkDAAAADwMAAADo - HwAAAk1TRnQBSQFMAgEBGAEAAdABAAHQAQABEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo + HwAAAk1TRnQBSQFMAgEBGAEAAdgBAAHYAQABEAEAARABAAT/AQkBAAj/AUIBTQE2AQQGAAE2AQQCAAEo AwABQAMAAXADAAEBAQABCAYAARwYAAGAAgABgAMAAoABAAGAAwABgAEAAYABAAKAAgADwAEAAcAB3AHA AQAB8AHKAaYBAAEzBQABMwEAATMBAAEzAQACMwIAAxYBAAMcAQADIgEAAykBAANVAQADTQEAA0IBAAM5 AQABgAF8Af8BAAJQAf8BAAGTAQAB1gEAAf8B7AHMAQABxgHWAe8BAAHWAucBAAGQAakBrQIAAf8BMwMA @@ -450,6 +450,24 @@ AQABAQHAAQEBAAEDAcABAQEAAQEBwAEBAQABAwHAAQEBAAEBAQABAQEAAQcBwAEDAQABAQEAAQEBAAEP AcABAwEAAQEBAAEBAQABDwHAAQMBAAEBAQABAQEAAQ8BwAEDAQABAQHAAQcBAAEPAcABAwEAAQMB4AEP AQABDwHAAQMBAAEHCw== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAACbSURBVDhPlY/BDYAgDEXZxNWcywsLeXECb45A4kX8n1CC + UBCaPFNq6H+YVjnnVuAjaxyPFy6dz7F4wj6Ox4qJ97V5v5sA+ykLSZcFUxZlujBskafjGGA/ZFGmY5QW + kF+LPF1b0LUo07UFpGlRphOMqwWqhZbeo7LQ0gl+VQbkY9FLby0gyaKV/keywMfOvF+IBlaeYQEHM+Cy + MS8nLPK4Pr55DAAAAABJRU5ErkJggg== + + + + + iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 + YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAC1SURBVDhP3ZO9DUIxDISzBw2bkQWYgGnYA+mJhjYzQIWg + eKJJG/yBLb34GSGg46Qr4vOd8uMkj1prFg7C5kgta9scIi6FZTje2mp3bovtqSM1NHroVdsTah7X+8vM + 6Lk5XAkZuxBZFATfbPB1DSlmzmzNN0FDpOlxMgFDdOZ3xIOXgLABGiIN4v2TgJeXaIi06SX+9ow6CyWa + QoOvd4MEZPEY5WgaPcNRBhry3WeaQsQPvnNKd1hjo1V/TdpFAAAAAElFTkSuQmCC