From ba6fd6116951e134798eac0c7c549e99f1aae53b Mon Sep 17 00:00:00 2001 From: dexyfex Date: Wed, 10 Jan 2018 14:17:30 +1100 Subject: [PATCH 1/4] 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 79a0fecde71b6e196d843d561b9084d10a7aaefb Mon Sep 17 00:00:00 2001 From: dexyfex Date: Wed, 10 Jan 2018 15:55:19 +1100 Subject: [PATCH 2/4] 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 31b49d2df39c5ddd4fa69a85adb60153f7c23963 Mon Sep 17 00:00:00 2001 From: kolardavid Date: Wed, 10 Jan 2018 06:49:43 +0100 Subject: [PATCH 3/4] Fixed generating of audio for stream for AwcAudio --- GameFiles/FileTypes/AwcFile.cs | 110 +++++++++++++-------------------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/GameFiles/FileTypes/AwcFile.cs b/GameFiles/FileTypes/AwcFile.cs index c39649b..aeb64d6 100644 --- a/GameFiles/FileTypes/AwcFile.cs +++ b/GameFiles/FileTypes/AwcFile.cs @@ -414,9 +414,9 @@ namespace CodeWalker.GameFiles [TC(typeof(EXP))] public class AwcFormatChunk { public uint Samples { get; set; } - public int UnkMinusOne { get; set; } + public int LoopPoint { get; set; } public ushort SamplesPerSecond { get; set; } - public ushort Unk1 { get; set; } + public short Headroom { get; set; } public ushort Unk2 { get; set; } public ushort Unk3 { get; set; } public ushort Unk4 { get; set; } @@ -426,9 +426,9 @@ namespace CodeWalker.GameFiles public AwcFormatChunk(DataReader r) { Samples = r.ReadUInt32(); - UnkMinusOne = r.ReadInt32(); + LoopPoint = r.ReadInt32(); SamplesPerSecond = r.ReadUInt16(); - Unk1 = r.ReadUInt16(); + Headroom = r.ReadInt16(); Unk2 = r.ReadUInt16(); Unk3 = r.ReadUInt16(); Unk4 = r.ReadUInt16(); @@ -443,7 +443,7 @@ namespace CodeWalker.GameFiles public override string ToString() { - return Unk1.ToString() + ", " + Unk6.ToString() + ": " + Samples.ToString() + " samples, " + SamplesPerSecond.ToString() + " samples/sec"; + return Headroom.ToString() + ", " + Unk6.ToString() + ": " + Samples.ToString() + " samples, " + SamplesPerSecond.ToString() + " samples/sec"; } } @@ -503,14 +503,20 @@ namespace CodeWalker.GameFiles return fmt + ((hz > 0) ? (", " + hz.ToString() + " Hz") : ""); } } + + public float Length + { + get + { + return Format == null ? 0 : (float)Format.Samples / Format.SamplesPerSecond; + } + } + public string LengthStr { get { - if (Format == null) return "0:00"; - float sec = (float)Format.Samples / Format.SamplesPerSecond; - TimeSpan ts = TimeSpan.FromSeconds(sec); - return ts.ToString("m\\:ss"); + return TimeSpan.FromSeconds(Length).ToString("m\\:ss"); } } @@ -534,8 +540,8 @@ namespace CodeWalker.GameFiles public Stream GetWavStream() { - MemoryStream ms = new MemoryStream(); - BinaryWriter w = new BinaryWriter(ms); + MemoryStream stream = new MemoryStream(); + BinaryWriter w = new BinaryWriter(stream); //see http://icculus.org/SDL_sound/downloads/external_documentation/wavecomp.htm @@ -543,70 +549,38 @@ namespace CodeWalker.GameFiles //see https://msdn.microsoft.com/en-us/library/windows/desktop/ff538799(v=vs.85).aspx - int sampleCount = SampleCount; int samplesPerSec = SamplesPerSecond; - //short sampleSize = (short)((BitsPerSample / 8) * Channels);//2 - //int avgBytesPerSec = sampleSize * samplesPerSec; - short blockAlign = 512; - short samplesPerBlock = (short)((((blockAlign - (7 * Channels)) * 8) / (BitsPerSample * Channels)) + 2); - int avgBytesPerSec = ((samplesPerSec / samplesPerBlock) * blockAlign); - - w.Write("RIFF".ToCharArray()); - w.Write(0); //file size written later... - w.Write("WAVE".ToCharArray()); - w.Write("fmt ".ToCharArray()); - w.Write(50); //(PCM:16) //header size - w.Write((short)2); //pcm format tag 1=PCM, 2=ADPCM - w.Write(Channels); - w.Write(samplesPerSec); - w.Write(avgBytesPerSec); - w.Write(blockAlign);// sampleSize); - w.Write(BitsPerSample); - w.Write((short)32);//extra byte count for WAVEFORMATEX - - w.Write(samplesPerBlock); - w.Write((short)7);//num coefficients - w.Write((short)256); //coeff 0 - w.Write((short)0); - w.Write((short)512); //coeff 1 - w.Write((short)-256); - w.Write((short)0); //coeff 2 - w.Write((short)0); - w.Write((short)192); //coeff 3 - w.Write((short)64); - w.Write((short)240); //coeff 4 - w.Write((short)0); - w.Write((short)460); //coeff 5 - w.Write((short)-208); - w.Write((short)392); //coeff 6 - w.Write((short)-232); + Channels = 1; + BitsPerSample = 16; + int byteRate = samplesPerSec * Channels * BitsPerSample / 8; + short blockAlign = (short)(Channels * BitsPerSample / 8); + + // RIFF chunk + var fileSize = 4 + 24 + 8 + Data.Length; + w.Write("RIFF".ToCharArray()); // 0x00 - "RIFF" magic + w.Write(fileSize); // 0x04 - file size + w.Write("WAVE".ToCharArray()); // 0x08 - "WAVE" magic + // fmt sub-chunk + w.Write("fmt ".ToCharArray()); // 0x0C - "fmt " magic + w.Write(16); // 0x10 - header size (16 bytes) + w.Write((short)1); // 0x14 - audio format (1=PCM) + w.Write(Channels); // 0x16 - number of channels + w.Write(samplesPerSec); // 0x18 + w.Write(byteRate); // 0x1C + w.Write(blockAlign);// sampleSize); // 0x20 + w.Write(BitsPerSample); // 0x22 + + // data sub-chunk w.Write("data".ToCharArray()); - w.Write(0); //data size written later... - - if (sampleCount != 0) - { - - //var sc = sampleCount * sampleSize; - var datalen = Data.Length; - w.Write(Data); - } - else - { - w.Write(Data); - } - - ms.Position = 4; - w.Write((int)ms.Length - 8); - - ms.Position = 74;// 40; - w.Write((int)ms.Length - 78);// 44); + w.Write(Data.Length); + w.Write(Data); w.Flush(); - ms.Position = 0; - return ms; + stream.Position = 0; + return stream; } From 7dbaa8dd825f595dc8683824df80eb2dbd34123e Mon Sep 17 00:00:00 2001 From: kolardavid Date: Wed, 10 Jan 2018 06:50:35 +0100 Subject: [PATCH 4/4] Implemented audio player base --- Forms/AwcForm.Designer.cs | 63 +++++---- Forms/AwcForm.cs | 270 ++++++++++++++++++++++++-------------- Forms/AwcForm.resx | 3 + 3 files changed, 211 insertions(+), 125 deletions(-) diff --git a/Forms/AwcForm.Designer.cs b/Forms/AwcForm.Designer.cs index cfee778..f78d5fa 100644 --- a/Forms/AwcForm.Designer.cs +++ b/Forms/AwcForm.Designer.cs @@ -28,12 +28,14 @@ /// private void InitializeComponent() { + this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AwcForm)); this.MainTabControl = new System.Windows.Forms.TabControl(); this.PlayerTabPage = new System.Windows.Forms.TabPage(); + this.VolumeLabel = new System.Windows.Forms.Label(); + this.chbAutoJump = new System.Windows.Forms.CheckBox(); this.PrevButton = new System.Windows.Forms.Button(); this.NextButton = new System.Windows.Forms.Button(); - this.VolumeButton = new System.Windows.Forms.Button(); this.PlayButton = new System.Windows.Forms.Button(); this.PlayListView = new System.Windows.Forms.ListView(); this.PlaylistNameHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); @@ -43,7 +45,7 @@ this.PositionTrackBar = new System.Windows.Forms.TrackBar(); this.DetailsTabPage = new System.Windows.Forms.TabPage(); this.DetailsPropertyGrid = new CodeWalker.WinForms.PropertyGridFix(); - this.label1 = new System.Windows.Forms.Label(); + this.Timer = new System.Windows.Forms.Timer(this.components); this.MainTabControl.SuspendLayout(); this.PlayerTabPage.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.VolumeTrackBar)).BeginInit(); @@ -64,10 +66,10 @@ // // PlayerTabPage // - this.PlayerTabPage.Controls.Add(this.label1); + this.PlayerTabPage.Controls.Add(this.VolumeLabel); + this.PlayerTabPage.Controls.Add(this.chbAutoJump); this.PlayerTabPage.Controls.Add(this.PrevButton); this.PlayerTabPage.Controls.Add(this.NextButton); - this.PlayerTabPage.Controls.Add(this.VolumeButton); this.PlayerTabPage.Controls.Add(this.PlayButton); this.PlayerTabPage.Controls.Add(this.PlayListView); this.PlayerTabPage.Controls.Add(this.VolumeTrackBar); @@ -80,6 +82,26 @@ this.PlayerTabPage.Text = "Player"; this.PlayerTabPage.UseVisualStyleBackColor = true; // + // VolumeLabel + // + this.VolumeLabel.AutoSize = true; + this.VolumeLabel.Location = new System.Drawing.Point(414, 305); + this.VolumeLabel.Name = "VolumeLabel"; + this.VolumeLabel.Size = new System.Drawing.Size(42, 13); + this.VolumeLabel.TabIndex = 9; + this.VolumeLabel.Text = "Volume"; + // + // chbAutoJump + // + this.chbAutoJump.AutoSize = true; + this.chbAutoJump.Enabled = false; + this.chbAutoJump.Location = new System.Drawing.Point(17, 305); + this.chbAutoJump.Name = "chbAutoJump"; + this.chbAutoJump.Size = new System.Drawing.Size(108, 17); + this.chbAutoJump.TabIndex = 8; + this.chbAutoJump.Text = "Auto-jump to next"; + this.chbAutoJump.UseVisualStyleBackColor = true; + // // PrevButton // this.PrevButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom; @@ -102,17 +124,6 @@ this.NextButton.UseVisualStyleBackColor = true; this.NextButton.Click += new System.EventHandler(this.NextButton_Click); // - // VolumeButton - // - this.VolumeButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); - this.VolumeButton.Location = new System.Drawing.Point(426, 301); - this.VolumeButton.Name = "VolumeButton"; - this.VolumeButton.Size = new System.Drawing.Size(33, 23); - this.VolumeButton.TabIndex = 5; - this.VolumeButton.Text = "Vol"; - this.VolumeButton.UseVisualStyleBackColor = true; - this.VolumeButton.Click += new System.EventHandler(this.VolumeButton_Click); - // // PlayButton // this.PlayButton.Anchor = System.Windows.Forms.AnchorStyles.Bottom; @@ -136,11 +147,13 @@ this.PlayListView.FullRowSelect = true; this.PlayListView.HideSelection = false; this.PlayListView.Location = new System.Drawing.Point(6, 6); + this.PlayListView.MultiSelect = false; this.PlayListView.Name = "PlayListView"; - this.PlayListView.Size = new System.Drawing.Size(556, 216); + this.PlayListView.Size = new System.Drawing.Size(556, 235); this.PlayListView.TabIndex = 0; this.PlayListView.UseCompatibleStateImageBehavior = false; this.PlayListView.View = System.Windows.Forms.View.Details; + this.PlayListView.DoubleClick += new System.EventHandler(this.PlayListView_DoubleClick); // // PlaylistNameHeader // @@ -176,7 +189,7 @@ this.PositionTrackBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.PositionTrackBar.BackColor = System.Drawing.SystemColors.ControlLightLight; - this.PositionTrackBar.LargeChange = 50; + this.PositionTrackBar.LargeChange = 1000; this.PositionTrackBar.Location = new System.Drawing.Point(6, 263); this.PositionTrackBar.Maximum = 1000; this.PositionTrackBar.Name = "PositionTrackBar"; @@ -205,14 +218,10 @@ this.DetailsPropertyGrid.Size = new System.Drawing.Size(562, 326); this.DetailsPropertyGrid.TabIndex = 0; // - // label1 + // Timer // - this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(44, 238); - this.label1.Name = "label1"; - this.label1.Size = new System.Drawing.Size(248, 13); - this.label1.TabIndex = 7; - this.label1.Text = "NOTE: Work in progress... Audio does not play yet!"; + this.Timer.Enabled = true; + this.Timer.Tick += new System.EventHandler(this.Timer_Tick); // // AwcForm // @@ -223,6 +232,7 @@ this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Name = "AwcForm"; this.Text = "AWC Player - CodeWalker by dexyfex"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.AwcForm_FormClosing); this.MainTabControl.ResumeLayout(false); this.PlayerTabPage.ResumeLayout(false); this.PlayerTabPage.PerformLayout(); @@ -245,10 +255,11 @@ private System.Windows.Forms.ColumnHeader PlaylistLengthHeader; private System.Windows.Forms.Button PrevButton; private System.Windows.Forms.Button NextButton; - private System.Windows.Forms.Button VolumeButton; private System.Windows.Forms.Button PlayButton; private System.Windows.Forms.TrackBar VolumeTrackBar; private System.Windows.Forms.TrackBar PositionTrackBar; - private System.Windows.Forms.Label label1; + private System.Windows.Forms.Timer Timer; + private System.Windows.Forms.CheckBox chbAutoJump; + private System.Windows.Forms.Label VolumeLabel; } } \ No newline at end of file diff --git a/Forms/AwcForm.cs b/Forms/AwcForm.cs index 3b754bf..6f144f5 100644 --- a/Forms/AwcForm.cs +++ b/Forms/AwcForm.cs @@ -2,15 +2,8 @@ using SharpDX.Multimedia; using SharpDX.XAudio2; using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; using System.Windows.Forms; namespace CodeWalker.Forms @@ -19,6 +12,11 @@ namespace CodeWalker.Forms { public AwcFile Awc { get; set; } + private XAudio2 xAudio2; + private MasteringVoice masteringVoice; + private AudioBuffer audioBuffer; + private SourceVoice sourceVoice; + private string fileName; public string FileName { @@ -31,36 +29,35 @@ namespace CodeWalker.Forms } public string FilePath { get; set; } - private bool Playing = false; + private enum PlayerState { Stopped, Playing, Paused }; + private PlayerState playerState = PlayerState.Stopped; + private Stopwatch playtime; + private float trackLength; public AwcForm() { InitializeComponent(); + + playtime = new Stopwatch(); } - - private void UpdateFormTitle() { Text = fileName + " - AWC Player - CodeWalker by dexyfex"; } - public void LoadAwc(AwcFile awc) { Awc = awc; DetailsPropertyGrid.SelectedObject = awc; - //MainTabControl.SelectedTab = DetailsTabPage;//remove this - fileName = awc?.Name; if (string.IsNullOrEmpty(fileName)) { fileName = awc?.FileEntry?.Name; } - PlayListView.Items.Clear(); if (awc.Audios != null) { @@ -76,79 +73,129 @@ namespace CodeWalker.Forms UpdateFormTitle(); } + private void Stop() + { + if (playerState != PlayerState.Stopped) + { + sourceVoice.DestroyVoice(); + sourceVoice.Dispose(); + audioBuffer.Stream.Dispose(); + SetPlayerState(PlayerState.Stopped); + } + } + + private void SetPlayerState(PlayerState newState) + { + if (playerState != newState) + { + switch (newState) + { + case PlayerState.Playing: + if (playerState == PlayerState.Stopped) + playtime.Reset(); + playtime.Start(); + break; + default: + playtime.Stop(); + break; + } + + playerState = newState; + } + } + + private void InitializeAudio(AwcAudio audio) + { + trackLength = audio.Length; + + if (xAudio2 == null) + { + xAudio2 = new XAudio2(); + masteringVoice = new MasteringVoice(xAudio2); + } + + Stream wavStream = audio.GetWavStream(); + SoundStream soundStream = new SoundStream(wavStream); + audioBuffer = new AudioBuffer + { + Stream = soundStream.ToDataStream(), + AudioBytes = (int)soundStream.Length, + Flags = BufferFlags.EndOfStream + }; + soundStream.Close(); + wavStream.Close(); + + sourceVoice = new SourceVoice(xAudio2, soundStream.Format, true); + sourceVoice.SubmitSourceBuffer(audioBuffer, soundStream.DecodedPacketsInfo); + sourceVoice.SetVolume((float)VolumeTrackBar.Value / 100); + } private void Play() { - if (PlayListView.SelectedItems.Count != 1) return; + Stop(); - var item = PlayListView.SelectedItems[0]; - var audio = item.Tag as AwcAudio; + if (PlayListView.SelectedItems.Count == 1) + { + var item = PlayListView.SelectedItems[0]; + var audio = item.Tag as AwcAudio; - if (audio == null) return; - - - - //see https://github.com/sharpdx/SharpDX-Samples/blob/master/Desktop/XAudio2/PlaySound/Program.cs - //see https://github.com/sharpdx/SharpDX-Samples/blob/master/Desktop/XAudio2/AudioPlayerApp/AudioPlayer.cs - - //var mstrm = new MemoryStream(audio.Data); - //var sstrm = new SoundStream(mstrm); - //SourceVoice sv=new SourceVoice() - var mstrm = audio.GetWavStream(); - - ////var mdata = ((MemoryStream)mstrm).GetBuffer(); - ////File.WriteAllBytes("C:\\test2.wav", mdata); - ////return; - - //var sstrm = new SoundStream(mstrm); - //var waveFormat = sstrm.Format; - //var buffer = new AudioBuffer - //{ - // Stream = sstrm.ToDataStream(), - // AudioBytes = (int)sstrm.Length, - // Flags = BufferFlags.EndOfStream - //}; - //sstrm.Close(); - - - //var xaudio2 = new XAudio2();//cache this... - //var masteringVoice = new MasteringVoice(xaudio2);//cache this... - //var sourceVoice = new SourceVoice(xaudio2, waveFormat, true); - ////sourceVoice.BufferEnd += (context) => Console.WriteLine(" => event received: end of buffer"); - //sourceVoice.SubmitSourceBuffer(buffer, sstrm.DecodedPacketsInfo); - //sourceVoice.Start(); - //while (sourceVoice.State.BuffersQueued > 0) // && !IsKeyPressed(ConsoleKey.Escape)) - //{ - // Thread.Sleep(10); - //} - //sourceVoice.DestroyVoice(); - //sourceVoice.Dispose(); - //buffer.Stream.Dispose(); - - //masteringVoice.Dispose();//on form exit? - //xaudio2.Dispose();//on form exit? - - - - Playing = true; + if (audio != null) + { + InitializeAudio(audio); + sourceVoice.Start(); + SetPlayerState(PlayerState.Playing); + } + } } + private void PlayPrevious() + { + Stop(); + if (PlayListView.SelectedIndices.Count > 0) + { + var nextIndex = PlayListView.SelectedIndices[0] - 1; + if (nextIndex >= 0) + { + PlayListView.Items[nextIndex].Selected = true; + PlayListView.Items[nextIndex].Focused = true; + Play(); + } + } + } + + private void PlayNext() + { + Stop(); + if (PlayListView.SelectedIndices.Count > 0) + { + var nextIndex = PlayListView.SelectedIndices[0] + 1; + if (nextIndex < PlayListView.Items.Count) + { + PlayListView.Items[nextIndex].Selected = true; + PlayListView.Items[nextIndex].Focused = true; + Play(); + } + } + } + private void Pause() { - - Playing = false; + if (playerState == PlayerState.Playing) + { + sourceVoice.Stop(); + SetPlayerState(PlayerState.Paused); + } } - private void Prev() - { + private void Resume() + { + if (playerState == PlayerState.Paused) + { + sourceVoice.Start(); + SetPlayerState(PlayerState.Playing); + } } - private void Next() - { - } - - - private void PositionTrackBar_Scroll(object sender, EventArgs e) { @@ -156,41 +203,66 @@ namespace CodeWalker.Forms private void PlayButton_Click(object sender, EventArgs e) { - if (Playing) Pause(); - else Play(); + switch (playerState) + { + case PlayerState.Stopped: + Play(); + break; + case PlayerState.Playing: + Pause(); + break; + case PlayerState.Paused: + Resume(); + break; + } } private void PrevButton_Click(object sender, EventArgs e) { - Prev(); + PlayPrevious(); } private void NextButton_Click(object sender, EventArgs e) { - Next(); - } - - private void VolumeButton_Click(object sender, EventArgs e) - { - + PlayNext(); } private void VolumeTrackBar_Scroll(object sender, EventArgs e) { - - } + if (playerState == PlayerState.Playing) + sourceVoice.SetVolume((float)VolumeTrackBar.Value / 100); + } + + private void Timer_Tick(object sender, EventArgs e) + { + if (playerState != PlayerState.Stopped) + { + int playedMs = (int)playtime.Elapsed.TotalMilliseconds; + int totalMs = (int)(trackLength * 1000); + PositionTrackBar.Maximum = totalMs; + PositionTrackBar.Value = playedMs < totalMs ? playedMs : totalMs; + } + else + { + PositionTrackBar.Value = 0; + } + } + + private void PlayListView_DoubleClick(object sender, EventArgs e) + { + if (playerState == PlayerState.Playing) + Stop(); + Play(); + } + + private void AwcForm_FormClosing(object sender, FormClosingEventArgs e) + { + Stop(); + if (xAudio2 != null) + { + masteringVoice.Dispose(); + xAudio2.Dispose(); + } + } } - - - - - - - public class AudioPlayer - { - - } - - - } diff --git a/Forms/AwcForm.resx b/Forms/AwcForm.resx index 1431f6b..3badf1f 100644 --- a/Forms/AwcForm.resx +++ b/Forms/AwcForm.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 17, 17 +