From e3aa58f1358b3673ee903e4d5ce311399f7a857b Mon Sep 17 00:00:00 2001 From: Skyth <19259897+blueskythlikesclouds@users.noreply.github.com> Date: Sat, 12 Nov 2016 19:02:48 +0300 Subject: [PATCH] Initial commit --- .gitattributes | 63 ++ .gitignore | 244 ++++++ LICENSE | 21 + Release/AcbEditor.exe | Bin 0 -> 11776 bytes Release/CsbEditor.exe | Bin 0 -> 11776 bytes Release/SonicAudioLib.dll | Bin 0 -> 49152 bytes SonicAudioTools.sln | 39 + Source/AcbEditor/AcbEditor.csproj | 78 ++ Source/AcbEditor/App.config | 6 + Source/AcbEditor/Program.cs | 272 +++++++ Source/AcbEditor/Properties/AssemblyInfo.cs | 36 + .../Properties/Resources.Designer.cs | 86 +++ Source/AcbEditor/Properties/Resources.resx | 142 ++++ Source/CsbEditor/App.config | 6 + Source/CsbEditor/CsbEditor.csproj | 78 ++ Source/CsbEditor/Program.cs | 169 +++++ Source/CsbEditor/Properties/AssemblyInfo.cs | 36 + .../Properties/Resources.Designer.cs | 86 +++ Source/CsbEditor/Properties/Resources.resx | 142 ++++ Source/SonicAudioCmd/App.config | 6 + Source/SonicAudioCmd/Program.cs | 43 ++ .../SonicAudioCmd/Properties/AssemblyInfo.cs | 36 + .../Properties/Resources.Designer.cs | 85 +++ .../SonicAudioCmd/Properties/Resources.resx | 139 ++++ Source/SonicAudioCmd/SonicAudioCmd.csproj | 77 ++ Source/SonicAudioCmd/acb_extractor.txt | 43 ++ Source/SonicAudioCmd/csb_file_replacer.txt | 171 +++++ .../csb_frequence_subdivision.txt | 45 ++ Source/SonicAudioCmd/file_replacer_acb.txt | 116 +++ Source/SonicAudioCmd/utf_to_cs.txt | 134 ++++ Source/SonicAudioCmd/utf_to_cs_memory.txt | 139 ++++ Source/SonicAudioLib/Archive/ArchiveBase.cs | 110 +++ .../SonicAudioLib/Archive/CriAfs2Archive.cs | 194 +++++ Source/SonicAudioLib/Archive/CriCpkArchive.cs | 136 ++++ .../SonicAudioLib/Archive/HeroesPacArchive.cs | 98 +++ .../Collections/OrderedDictionary.cs | 175 +++++ Source/SonicAudioLib/CriMw/CriField.cs | 134 ++++ .../SonicAudioLib/CriMw/CriFieldCollection.cs | 121 +++ Source/SonicAudioLib/CriMw/CriRow.cs | 107 +++ .../SonicAudioLib/CriMw/CriRowCollection.cs | 83 ++ .../SonicAudioLib/CriMw/CriTable.Internal.cs | 52 ++ Source/SonicAudioLib/CriMw/CriTable.cs | 143 ++++ Source/SonicAudioLib/CriMw/CriTableReader.cs | 702 +++++++++++++++++ Source/SonicAudioLib/CriMw/CriTableWriter.cs | 713 ++++++++++++++++++ .../CriMw/Serialization/CriFieldAttribute.cs | 49 ++ .../CriMw/Serialization/CriIgnoreAttribute.cs | 13 + .../Serialization/CriSerializableAttribute.cs | 29 + .../CriMw/Serialization/CriTableSerializer.cs | 201 +++++ Source/SonicAudioLib/IO/EndianStream.cs | 395 ++++++++++ Source/SonicAudioLib/IO/StringPool.cs | 88 +++ Source/SonicAudioLib/IO/Substream.cs | 199 +++++ Source/SonicAudioLib/IO/VldPool.cs | 172 +++++ Source/SonicAudioLib/Module/ModuleBase.cs | 52 ++ .../SonicAudioLib/Properties/AssemblyInfo.cs | 36 + Source/SonicAudioLib/SonicAudioLib.csproj | 76 ++ 55 files changed, 6616 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Release/AcbEditor.exe create mode 100644 Release/CsbEditor.exe create mode 100644 Release/SonicAudioLib.dll create mode 100644 SonicAudioTools.sln create mode 100644 Source/AcbEditor/AcbEditor.csproj create mode 100644 Source/AcbEditor/App.config create mode 100644 Source/AcbEditor/Program.cs create mode 100644 Source/AcbEditor/Properties/AssemblyInfo.cs create mode 100644 Source/AcbEditor/Properties/Resources.Designer.cs create mode 100644 Source/AcbEditor/Properties/Resources.resx create mode 100644 Source/CsbEditor/App.config create mode 100644 Source/CsbEditor/CsbEditor.csproj create mode 100644 Source/CsbEditor/Program.cs create mode 100644 Source/CsbEditor/Properties/AssemblyInfo.cs create mode 100644 Source/CsbEditor/Properties/Resources.Designer.cs create mode 100644 Source/CsbEditor/Properties/Resources.resx create mode 100644 Source/SonicAudioCmd/App.config create mode 100644 Source/SonicAudioCmd/Program.cs create mode 100644 Source/SonicAudioCmd/Properties/AssemblyInfo.cs create mode 100644 Source/SonicAudioCmd/Properties/Resources.Designer.cs create mode 100644 Source/SonicAudioCmd/Properties/Resources.resx create mode 100644 Source/SonicAudioCmd/SonicAudioCmd.csproj create mode 100644 Source/SonicAudioCmd/acb_extractor.txt create mode 100644 Source/SonicAudioCmd/csb_file_replacer.txt create mode 100644 Source/SonicAudioCmd/csb_frequence_subdivision.txt create mode 100644 Source/SonicAudioCmd/file_replacer_acb.txt create mode 100644 Source/SonicAudioCmd/utf_to_cs.txt create mode 100644 Source/SonicAudioCmd/utf_to_cs_memory.txt create mode 100644 Source/SonicAudioLib/Archive/ArchiveBase.cs create mode 100644 Source/SonicAudioLib/Archive/CriAfs2Archive.cs create mode 100644 Source/SonicAudioLib/Archive/CriCpkArchive.cs create mode 100644 Source/SonicAudioLib/Archive/HeroesPacArchive.cs create mode 100644 Source/SonicAudioLib/Collections/OrderedDictionary.cs create mode 100644 Source/SonicAudioLib/CriMw/CriField.cs create mode 100644 Source/SonicAudioLib/CriMw/CriFieldCollection.cs create mode 100644 Source/SonicAudioLib/CriMw/CriRow.cs create mode 100644 Source/SonicAudioLib/CriMw/CriRowCollection.cs create mode 100644 Source/SonicAudioLib/CriMw/CriTable.Internal.cs create mode 100644 Source/SonicAudioLib/CriMw/CriTable.cs create mode 100644 Source/SonicAudioLib/CriMw/CriTableReader.cs create mode 100644 Source/SonicAudioLib/CriMw/CriTableWriter.cs create mode 100644 Source/SonicAudioLib/CriMw/Serialization/CriFieldAttribute.cs create mode 100644 Source/SonicAudioLib/CriMw/Serialization/CriIgnoreAttribute.cs create mode 100644 Source/SonicAudioLib/CriMw/Serialization/CriSerializableAttribute.cs create mode 100644 Source/SonicAudioLib/CriMw/Serialization/CriTableSerializer.cs create mode 100644 Source/SonicAudioLib/IO/EndianStream.cs create mode 100644 Source/SonicAudioLib/IO/StringPool.cs create mode 100644 Source/SonicAudioLib/IO/Substream.cs create mode 100644 Source/SonicAudioLib/IO/VldPool.cs create mode 100644 Source/SonicAudioLib/Module/ModuleBase.cs create mode 100644 Source/SonicAudioLib/Properties/AssemblyInfo.cs create mode 100644 Source/SonicAudioLib/SonicAudioLib.csproj diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdc6f31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,244 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]eleases/ +[Xx]64/ +[Xx]86/ +[Bb]uild/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml + +# TODO: Un-comment the next line if you do not want to checkin +# your web deploy settings because they may include unencrypted +# passwords +#*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# LightSwitch generated files +GeneratedArtifacts/ +ModelManifest.xml + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ac8e60 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Skyth + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Release/AcbEditor.exe b/Release/AcbEditor.exe new file mode 100644 index 0000000000000000000000000000000000000000..bea929a844734d74bda9d40a974fdcf1f9c86439 GIT binary patch literal 11776 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 C&=mdv literal 0 HcmV?d00001 diff --git a/Release/CsbEditor.exe b/Release/CsbEditor.exe new file mode 100644 index 0000000000000000000000000000000000000000..c0cd99c9b8cecba67a453bc168fc683a0dd678d8 GIT binary patch literal 11776 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 + + + + Debug + AnyCPU + {45A72DAF-370A-42D1-833E-CEE27EA5E311} + Exe + Properties + AcbEditor + AcbEditor + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + ..\..\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\..\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + {63138773-1f47-474c-9345-15eb6183ecc6} + SonicAudioLib + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + \ No newline at end of file diff --git a/Source/AcbEditor/App.config b/Source/AcbEditor/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/Source/AcbEditor/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/AcbEditor/Program.cs b/Source/AcbEditor/Program.cs new file mode 100644 index 0000000..86ae447 --- /dev/null +++ b/Source/AcbEditor/Program.cs @@ -0,0 +1,272 @@ +using System; +using System.IO; +using System.Windows.Forms; + +using SonicAudioLib.CriMw; +using SonicAudioLib.IO; +using SonicAudioLib.Archive; + +namespace AcbEditor +{ + class Program + { + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine(Properties.Resources.Description); + Console.ReadLine(); + return; + } + + try + { + if (args[0].EndsWith(".acb")) + { + string baseDirectory = Path.GetDirectoryName(args[0]); + string outputDirectoryPath = Path.Combine(baseDirectory, Path.GetFileNameWithoutExtension(args[0])); + string extAfs2ArchivePath = string.Empty; + + Directory.CreateDirectory(outputDirectoryPath); + + using (CriTableReader acbReader = CriTableReader.Create(args[0])) + { + acbReader.Read(); + + CriAfs2Archive afs2Archive = new CriAfs2Archive(); + CriAfs2Archive extAfs2Archive = new CriAfs2Archive(); + + if (acbReader.GetLength("AwbFile") > 0) + { + using (Substream afs2Stream = acbReader.GetSubstream("AwbFile")) + { + afs2Archive.Read(afs2Stream); + } + } + + if (acbReader.GetLength("StreamAwbAfs2Header") > 0) + { + using (Substream extAfs2Stream = acbReader.GetSubstream("StreamAwbAfs2Header")) + { + extAfs2Archive.Read(extAfs2Stream); + } + + // cheatingggggg + extAfs2ArchivePath = outputDirectoryPath + ".awb"; + bool found = File.Exists(extAfs2ArchivePath); + + if (!found) + { + extAfs2ArchivePath = outputDirectoryPath + "_streamfiles.awb"; + found = File.Exists(extAfs2ArchivePath); + } + + if (!found) + { + extAfs2ArchivePath = outputDirectoryPath + "_STR.awb"; + found = File.Exists(extAfs2ArchivePath); + } + + if (!found) + { + throw new Exception("Cannot find the external .AWB file for this .ACB file. Please ensure that the external .AWB file is stored in the directory where the .ACB file is."); + } + } + + using (Substream waveformTableStream = acbReader.GetSubstream("WaveformTable")) + using (CriTableReader waveformReader = CriTableReader.Create(waveformTableStream)) + { + while (waveformReader.Read()) + { + ushort index = waveformReader.GetUInt16("Id"); + byte encodeType = waveformReader.GetByte("EncodeType"); + bool streaming = waveformReader.GetBoolean("Streaming"); + + string outputName = index.ToString("D5"); + if (streaming) + { + outputName += "_streaming"; + } + + outputName += GetExtension(encodeType); + outputName = Path.Combine(outputDirectoryPath, outputName); + + Console.WriteLine("Extracting {0} file with index {1}...", GetExtension(encodeType).ToUpper(), index); + + if (streaming) + { + CriAfs2Entry afs2Entry = extAfs2Archive.GetByCueIndex(index); + + using (Stream extAfs2Stream = File.OpenRead(extAfs2ArchivePath)) + using (Stream afs2EntryStream = afs2Entry.Open(extAfs2Stream)) + using (Stream afs2EntryDestination = File.Create(outputName)) + { + afs2EntryStream.CopyTo(afs2EntryDestination); + } + } + + else + { + CriAfs2Entry entry = afs2Archive.GetByCueIndex(index); + + using (Substream afs2Stream = acbReader.GetSubstream("AwbFile")) + using (Stream afs2EntryStream = entry.Open(afs2Stream)) + using (Stream afs2EntryDestination = File.Create(outputName)) + { + afs2EntryStream.CopyTo(afs2EntryDestination); + } + } + } + } + } + } + + else if (File.GetAttributes(args[0]).HasFlag(FileAttributes.Directory)) + { + string baseDirectory = Path.GetDirectoryName(args[0]); + string acbPath = args[0] + ".acb"; + + string awbPath = args[0] + "_streamfiles.awb"; + bool found = File.Exists(awbPath); + + if (!found) + { + awbPath = args[0] + "_STR.awb"; + found = File.Exists(awbPath); + } + + if (!found) + { + awbPath = args[0] + ".awb"; + } + + if (!File.Exists(acbPath)) + { + throw new Exception("Cannot find the .ACB file for this directory. Please ensure that the .ACB file is stored in the directory where this directory is."); + } + + CriTable acbFile = new CriTable(); + acbFile.Load(acbPath); + + CriAfs2Archive afs2Archive = new CriAfs2Archive(); + CriAfs2Archive extAfs2Archive = new CriAfs2Archive(); + + using (CriTableReader reader = CriTableReader.Create((byte[])acbFile.Rows[0]["WaveformTable"])) + { + while (reader.Read()) + { + ushort index = reader.GetUInt16("Id"); + byte encodeType = reader.GetByte("EncodeType"); + bool streaming = reader.GetBoolean("Streaming"); + + string inputName = index.ToString("D5"); + if (streaming) + { + inputName += "_streaming"; + } + + inputName += GetExtension(encodeType); + inputName = Path.Combine(args[0], inputName); + + if (!File.Exists(inputName)) + { + throw new Exception($"Cannot find audio file with index {index} for replacement.\nPath attempt: {inputName}"); + } + + CriAfs2Entry entry = new CriAfs2Entry(); + entry.CueIndex = index; + entry.FilePath = new FileInfo(inputName); + + Console.WriteLine("Adding {0}...", Path.GetFileName(inputName)); + + if (streaming) + { + extAfs2Archive.Add(entry); + } + + else + { + afs2Archive.Add(entry); + } + } + } + + acbFile.Rows[0]["AwbFile"] = null; + acbFile.Rows[0]["StreamAwbAfs2Header"] = null; + + if (afs2Archive.Count > 0) + { + afs2Archive.Order(); + + Console.WriteLine("Saving internal AWB..."); + acbFile.Rows[0]["AwbFile"] = afs2Archive.Save(); + } + + if (extAfs2Archive.Count > 0) + { + extAfs2Archive.Order(); + + Console.WriteLine("Saving external AWB..."); + extAfs2Archive.Save(awbPath); + + byte[] afs2Header = new byte[16 + + (extAfs2Archive.Count * extAfs2Archive.CueIndexFieldLength) + + (extAfs2Archive.Count * extAfs2Archive.PositionFieldLength) + + extAfs2Archive.PositionFieldLength]; + + using (FileStream fileStream = File.OpenRead(awbPath)) + { + fileStream.Read(afs2Header, 0, afs2Header.Length); + } + + acbFile.Rows[0]["StreamAwbAfs2Header"] = afs2Header; + } + + acbFile.WriterSettings = CriTableWriterSettings.Adx2Settings; + acbFile.Save(acbPath); + } + } + + catch (Exception exception) + { + MessageBox.Show($"{exception.Message}", "ACB Editor", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + static string GetExtension(byte encodeType) + { + switch (encodeType) + { + case 0: + case 3: + return ".adx"; + case 1: + return ".ahx"; + case 2: + return ".hca"; + case 4: + return ".wiiadpcm"; + case 5: + return ".dsadpcm"; + case 6: + return ".hcamx"; + case 10: + case 7: + return ".vag"; + case 8: + return ".at3"; + case 9: + return ".3dsadpcm"; + case 18: + case 11: + return ".at9"; + case 12: + return ".xma"; + case 13: + return ".wiiuadpcm"; + default: + return ".bin"; + } + } + } +} diff --git a/Source/AcbEditor/Properties/AssemblyInfo.cs b/Source/AcbEditor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c031983 --- /dev/null +++ b/Source/AcbEditor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AcbEditor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AcbEditor")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("45a72daf-370a-42d1-833e-cee27ea5e311")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/AcbEditor/Properties/Resources.Designer.cs b/Source/AcbEditor/Properties/Resources.Designer.cs new file mode 100644 index 0000000..446764f --- /dev/null +++ b/Source/AcbEditor/Properties/Resources.Designer.cs @@ -0,0 +1,86 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AcbEditor.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AcbEditor.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to ACB Editor + ///========== + /// + ///Usage: + ///Drag and drop an .ACB file to unpack its contents to a directory. + ///The directory will have the same name as the .ACB file, but without + ///its extension. + /// + ///In the directory, there will be the audio files, extracted straight + ///from the .ACB file (and the external .AWB file if it is present.) + ///Files with "_streaming" suffix means that it was extracted from the + ///external .AWB file. + /// + ///You can edit those files as you want, but you shouldn't rename the + ///files, or remove any of them [rest of string was truncated]";. + /// + internal static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + } +} diff --git a/Source/AcbEditor/Properties/Resources.resx b/Source/AcbEditor/Properties/Resources.resx new file mode 100644 index 0000000..10b1e80 --- /dev/null +++ b/Source/AcbEditor/Properties/Resources.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ACB Editor +========== + +Usage: +Drag and drop an .ACB file to unpack its contents to a directory. +The directory will have the same name as the .ACB file, but without +its extension. + +In the directory, there will be the audio files, extracted straight +from the .ACB file (and the external .AWB file if it is present.) +Files with "_streaming" suffix means that it was extracted from the +external .AWB file. + +You can edit those files as you want, but you shouldn't rename the +files, or remove any of them. + +To pack the .ACB file back, you gotta have the extracted directory +and the .ACB file in the same directory, and also the external .AWB +file if it exists. Drag and drop the directory to the .EXE file, +it will collect all the files inside directory and pack them back. + + \ No newline at end of file diff --git a/Source/CsbEditor/App.config b/Source/CsbEditor/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/Source/CsbEditor/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/CsbEditor/CsbEditor.csproj b/Source/CsbEditor/CsbEditor.csproj new file mode 100644 index 0000000..6a2c3ba --- /dev/null +++ b/Source/CsbEditor/CsbEditor.csproj @@ -0,0 +1,78 @@ + + + + + Debug + AnyCPU + {91F6B6A6-5D95-4C7A-B22E-A35BD32DB67A} + Exe + Properties + CsbEditor + CsbEditor + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + ..\..\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\..\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + {63138773-1f47-474c-9345-15eb6183ecc6} + SonicAudioLib + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + \ No newline at end of file diff --git a/Source/CsbEditor/Program.cs b/Source/CsbEditor/Program.cs new file mode 100644 index 0000000..44444c6 --- /dev/null +++ b/Source/CsbEditor/Program.cs @@ -0,0 +1,169 @@ +using System; +using System.Linq; +using System.IO; +using System.Windows.Forms; + +using SonicAudioLib.CriMw; +using SonicAudioLib.IO; +using SonicAudioLib.Archive; + +using System.Globalization; + +namespace CsbEditor +{ + class Program + { + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine(Properties.Resources.Description); + Console.ReadLine(); + return; + } + + try + { + if (args[0].EndsWith(".csb")) + { + string baseDirectory = Path.GetDirectoryName(args[0]); + string outputDirectoryName = Path.Combine(baseDirectory, Path.GetFileNameWithoutExtension(args[0])); + + using (CriTableReader reader = CriTableReader.Create(args[0])) + { + while (reader.Read()) + { + if (reader.GetString("name") == "SOUND_ELEMENT") + { + using (CriTableReader sdlReader = CriTableReader.Create(reader.GetSubstream("utf"))) + { + while (sdlReader.Read()) + { + if (sdlReader.GetByte("stmflg") != 0) + { + throw new Exception("The given CSB file contains external audio data. Those kind of CSB files are not supported yet."); + } + + if (sdlReader.GetByte("fmt") != 0) + { + throw new Exception("The given CSB file contains an audio file which is not an ADX. Only CSB files with ADXs are supported."); + } + + string sdlName = sdlReader.GetString("name"); + DirectoryInfo destinationPath = new DirectoryInfo(Path.Combine(outputDirectoryName, sdlName)); + destinationPath.Create(); + + Console.WriteLine("Extracting {0}...", sdlName); + using (CriTableReader aaxReader = CriTableReader.Create(sdlReader.GetSubstream("data"))) + { + while (aaxReader.Read()) + { + string outputName = Path.Combine(destinationPath.FullName, aaxReader.GetBoolean("lpflg") ? "Loop.adx" : "Intro.adx"); + + using (Stream source = aaxReader.GetSubstream("data")) + using (Stream destination = File.Create(outputName)) + { + source.CopyTo(destination); + } + } + } + } + } + + break; + } + } + } + } + + else if (File.GetAttributes(args[0]).HasFlag(FileAttributes.Directory)) + { + string baseDirectory = Path.GetDirectoryName(args[0]); + string csbPath = args[0] + ".csb"; + + if (!File.Exists(csbPath)) + { + throw new Exception("Cannot find the .CSB file for this directory. Please ensure that the .CSB file is stored in the directory where this directory is."); + } + + CriTable csbFile = new CriTable(); + csbFile.Load(csbPath); + + CriRow soundElementRow = csbFile.Rows.Single(row => (string)row["name"] == "SOUND_ELEMENT"); + + CriTable soundElementTable = new CriTable(); + soundElementTable.Load((byte[])soundElementRow["utf"]); + + foreach (CriRow sdlRow in soundElementTable.Rows) + { + string sdlName = (string)sdlRow["name"]; + + DirectoryInfo sdlDirectory = new DirectoryInfo(Path.Combine(args[0], sdlName)); + + if (!sdlDirectory.Exists) + { + throw new Exception($"Cannot find sound element directory for replacement.\nPath attempt: {sdlDirectory.FullName}"); + } + + uint sampleRate = (uint)sdlRow["sfreq"]; + byte numberChannels = (byte)sdlRow["nch"]; + + Console.WriteLine("Adding {0}...", sdlName); + + using (MemoryStream memoryStream = new MemoryStream()) + using (CriTableWriter writer = CriTableWriter.Create(memoryStream, CriTableWriterSettings.AdxSettings)) + { + writer.WriteStartTable("AAX"); + + writer.WriteField("data", typeof(byte[])); + writer.WriteField("lpflg", typeof(byte)); + + foreach (FileInfo audioFile in sdlDirectory.GetFiles("*.adx")) + { + // In Turkish, lowercase I is ı so you get the idea + if (audioFile.Name.ToLower(CultureInfo.GetCultureInfo("en-US")) == "intro.adx") + { + ReadAdx(audioFile, out sampleRate, out numberChannels); + writer.WriteRow(true, audioFile, (byte)0); + } + + else if (audioFile.Name.ToLower() == "loop.adx") + { + ReadAdx(audioFile, out sampleRate, out numberChannels); + writer.WriteRow(true, audioFile, (byte)1); + } + } + + writer.WriteEndTable(); + sdlRow["data"] = memoryStream.ToArray(); + } + + sdlRow["sfreq"] = sampleRate; + sdlRow["nch"] = numberChannels; + } + + soundElementTable.WriterSettings = CriTableWriterSettings.AdxSettings; + soundElementRow["utf"] = soundElementTable.Save(); + + csbFile.WriterSettings = CriTableWriterSettings.AdxSettings; + csbFile.Save(args[0] + ".csb"); + } + } + + catch (Exception exception) + { + MessageBox.Show($"{exception.Message}", "CSB Editor", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + static void ReadAdx(FileInfo fileInfo, out uint sampleRate, out byte numberChannels) + { + using (Stream source = fileInfo.OpenRead()) + { + source.Seek(7, SeekOrigin.Begin); + numberChannels = EndianStream.ReadByte(source); + sampleRate = EndianStream.ReadUInt32BE(source); + } + } + } +} diff --git a/Source/CsbEditor/Properties/AssemblyInfo.cs b/Source/CsbEditor/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ba36765 --- /dev/null +++ b/Source/CsbEditor/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CsbEditor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CsbEditor")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("91f6b6a6-5d95-4c7a-b22e-a35bd32db67a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/CsbEditor/Properties/Resources.Designer.cs b/Source/CsbEditor/Properties/Resources.Designer.cs new file mode 100644 index 0000000..4f25845 --- /dev/null +++ b/Source/CsbEditor/Properties/Resources.Designer.cs @@ -0,0 +1,86 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace CsbEditor.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CsbEditor.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to CSB Editor + ///========== + /// + ///Usage: + ///Drag and drop a .CSB file to unpack its contents to a directory. + ///The directory will have the same name as the .CSB file, but without + ///its extension. + /// + ///In the deepest directories, you will see .ADX files, named "Intro.adx" + ///or "Loop.adx". The ADX files are literally what the names say. You can + ///add/delete/modify them freely. + /// + ///The sample rate and channel count information will be automatically + ///updated in the CSB file if you use an ADX file with different sample + ///rate or [rest of string was truncated]";. + /// + internal static string Description { + get { + return ResourceManager.GetString("Description", resourceCulture); + } + } + } +} diff --git a/Source/CsbEditor/Properties/Resources.resx b/Source/CsbEditor/Properties/Resources.resx new file mode 100644 index 0000000..0a0879c --- /dev/null +++ b/Source/CsbEditor/Properties/Resources.resx @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + CSB Editor +========== + +Usage: +Drag and drop a .CSB file to unpack its contents to a directory. +The directory will have the same name as the .CSB file, but without +its extension. + +In the deepest directories, you will see .ADX files, named "Intro.adx" +or "Loop.adx". The ADX files are literally what the names say. You can +add/delete/modify them freely. + +The sample rate and channel count information will be automatically +updated in the CSB file if you use an ADX file with different sample +rate or channel count than original. + +To pack the .CSB file back, you gotta have the extracted directory +and the .CSB file in the same directory. Drag and drop the directory +to the .EXE file, it will collect all the files inside directory +and pack them back. + + \ No newline at end of file diff --git a/Source/SonicAudioCmd/App.config b/Source/SonicAudioCmd/App.config new file mode 100644 index 0000000..88fa402 --- /dev/null +++ b/Source/SonicAudioCmd/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/SonicAudioCmd/Program.cs b/Source/SonicAudioCmd/Program.cs new file mode 100644 index 0000000..a749ec9 --- /dev/null +++ b/Source/SonicAudioCmd/Program.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; + +namespace SonicAudioCmd +{ + class Program + { + static void Main(string[] args) + { + CriCpkArchive archive = new CriCpkArchive(); + archive.Load(args[0]); + + foreach (CriCpkEntry entry in archive) + { + using (Stream source = File.OpenRead(args[0]), substream = entry.Open(source)) + { + FileInfo fileInfo = new FileInfo(Path.Combine(Path.GetFileNameWithoutExtension(args[0]), entry.DirectoryName, entry.Name)); + fileInfo.Directory.Create(); + using (Stream destination = fileInfo.Create()) + { + substream.CopyTo(destination); + } + } + } + + Console.ReadLine(); + } + } +} diff --git a/Source/SonicAudioCmd/Properties/AssemblyInfo.cs b/Source/SonicAudioCmd/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a8563c3 --- /dev/null +++ b/Source/SonicAudioCmd/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SonicAudioCmd")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SonicAudioCmd")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c412fc07-0c07-4361-88ba-c9c182cf8d70")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/SonicAudioCmd/Properties/Resources.Designer.cs b/Source/SonicAudioCmd/Properties/Resources.Designer.cs new file mode 100644 index 0000000..b3dc50a --- /dev/null +++ b/Source/SonicAudioCmd/Properties/Resources.Designer.cs @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace SonicAudioCmd.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SonicAudioCmd.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to CSB Compiler/Extractor + /// + ///Usage: [fileName] + /// + ///This tool will extract the contents of a CSB file into a folder, + ///resulting also an XML file. Just simply drag and drop your CSB file. + ///You can modify the .ADX files inside as you want! + /// + ///In the XML file, do not touch the paths, but modify the SampleRate + ///or NumberChannels elements if your custom ADX has a different + ///sample rate or channel count than the original. Otherwise, the + ///modifications in game will result pitched/faster/slower. + /// + ///To compile the CSB ba [rest of string was truncated]";. + /// + internal static string HelpText { + get { + return ResourceManager.GetString("HelpText", resourceCulture); + } + } + } +} diff --git a/Source/SonicAudioCmd/Properties/Resources.resx b/Source/SonicAudioCmd/Properties/Resources.resx new file mode 100644 index 0000000..c608162 --- /dev/null +++ b/Source/SonicAudioCmd/Properties/Resources.resx @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + CSB Compiler/Extractor + +Usage: [fileName] + +This tool will extract the contents of a CSB file into a folder, +resulting also an XML file. Just simply drag and drop your CSB file. +You can modify the .ADX files inside as you want! + +In the XML file, do not touch the paths, but modify the SampleRate +or NumberChannels elements if your custom ADX has a different +sample rate or channel count than the original. Otherwise, the +modifications in game will result pitched/faster/slower. + +To compile the CSB back, you gotta have your extracted folder, +CSB file and XML file in the same folder. Drag and drop the XML +to the tool, it will load all the ADXs and compile them back to +the CSB file. (The existing CSB file will be overwritten!) + + \ No newline at end of file diff --git a/Source/SonicAudioCmd/SonicAudioCmd.csproj b/Source/SonicAudioCmd/SonicAudioCmd.csproj new file mode 100644 index 0000000..60a62ec --- /dev/null +++ b/Source/SonicAudioCmd/SonicAudioCmd.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {C412FC07-0C07-4361-88BA-C9C182CF8D70} + Exe + Properties + SonicAudioCmd + SonicAudioCmd + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + ..\..\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + ..\..\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + + + {63138773-1f47-474c-9345-15eb6183ecc6} + SonicAudioLib + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + \ No newline at end of file diff --git a/Source/SonicAudioCmd/acb_extractor.txt b/Source/SonicAudioCmd/acb_extractor.txt new file mode 100644 index 0000000..4c9c9b6 --- /dev/null +++ b/Source/SonicAudioCmd/acb_extractor.txt @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; + +namespace SonicAudioCmd +{ + class Program + { + static void Main(string[] args) + { + using (CriTableReader reader = new CriTableReader(args[0])) + { + reader.Read(); + + using (Stream substream = reader.GetSubstream("AwbFile")) + { + Afs2Archive archive = new Afs2Archive(substream); + + foreach (Afs2Entry entry in archive.Entries) + { + using (Stream entryStream = entry.Open(substream), outStream = File.Create(entry.CueIndex.ToString() + ".hca")) + { + entryStream.CopyTo(outStream); + } + } + } + } + } + } +} diff --git a/Source/SonicAudioCmd/csb_file_replacer.txt b/Source/SonicAudioCmd/csb_file_replacer.txt new file mode 100644 index 0000000..89ce9aa --- /dev/null +++ b/Source/SonicAudioCmd/csb_file_replacer.txt @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; +using System.Xml.Serialization; + +namespace SonicAudioCmd +{ + [Serializable] + public class SoundElement + { + public string Path { get; set; } + public uint SampleRate { get; set; } + public byte NumberChannels { get; set; } + public List Sounds = new List(); + } + + class Program + { + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine(Properties.Resources.HelpText); + Console.ReadLine(); + return; + } + + if (args[0].EndsWith(".csb")) + { + string directoryName = Path.Combine(Path.GetDirectoryName(args[0]), Path.GetFileNameWithoutExtension(args[0])); + + List soundElements = new List(); + using (CriTableReader reader = CriTableReader.Create(args[0])) + { + while (reader.Read()) + { + if (reader.GetString("name") == "SOUND_ELEMENT") + { + using (CriTableReader sdlReader = CriTableReader.Create(reader.GetSubstream("utf"))) + { + while (sdlReader.Read()) + { + SoundElement soundElement = new SoundElement(); + soundElement.Path = sdlReader.GetString("name"); + soundElement.SampleRate = sdlReader.GetUInt32("sfreq"); + soundElement.NumberChannels = sdlReader.GetByte("nch"); + using (CriTableReader aaxReader = CriTableReader.Create(sdlReader.GetSubstream("data"))) + { + while (aaxReader.Read()) + { + string fileName = Path.Combine(directoryName, soundElement.Path, + $"{soundElement.Path.Replace('/', '_')}_{aaxReader.GetByte("lpflg")}.adx"); + soundElement.Sounds.Add(fileName); + + FileInfo fileInfo = new FileInfo(fileName); + fileInfo.Directory.Create(); + + using (Stream inStream = aaxReader.GetSubstream("data"), outStream = fileInfo.Create()) + { + inStream.CopyTo(outStream); + } + } + } + soundElements.Add(soundElement); + } + } + + XmlSerializer xmlSerializer = new XmlSerializer(typeof(List)); + + using (XmlWriter xmlWriter = XmlWriter.Create(directoryName + ".xml", new XmlWriterSettings() { Indent = true })) + { + xmlSerializer.Serialize(xmlWriter, soundElements); + } + + break; + } + } + } + } + + else if (args[0].EndsWith(".xml")) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(List)); + List soundElements; + + using (XmlReader xmlReader = XmlReader.Create(args[0])) + { + soundElements = (List)xmlSerializer.Deserialize(xmlReader); + } + + CriTable criTable = new CriTable(); + string directoryName = Path.Combine(Path.GetDirectoryName(args[0]), Path.GetFileNameWithoutExtension(args[0])); + + if (!File.Exists(directoryName + ".csb")) + { + throw new FileNotFoundException("No valid CSB file found for this XML!"); + } + + criTable.Load(directoryName + ".csb"); + + Directory.CreateDirectory(directoryName + "-temp"); + + CriTable soundElementTable = new CriTable(); + CriRow soundElementRow = criTable.Rows.Single(row => (string)row["name"] == "SOUND_ELEMENT"); + + soundElementTable.Load((byte[])soundElementRow["utf"]); + + foreach (SoundElement soundElement in soundElements) + { + CriRow elementRow = soundElementTable.Rows.Single(row => (string)row["name"] == soundElement.Path); + elementRow["nch"] = soundElement.NumberChannels; + elementRow["sfreq"] = soundElement.SampleRate; + + using (CriTableWriter aaxWriter = CriTableWriter.Create($"{directoryName}-temp/{soundElement.Path.Replace('/', '_')}")) + { + aaxWriter.WriteStartTable("AAX"); + aaxWriter.WriteStartFieldCollection(); + aaxWriter.WriteField("data", typeof(byte[])); + aaxWriter.WriteField("lpflg", typeof(byte)); + aaxWriter.WriteEndFieldCollection(); + + foreach (string sound in soundElement.Sounds) + { + if (!File.Exists(sound)) + { + Directory.Delete(directoryName + "-temp", true); + throw new FileNotFoundException($"Can't find file! {sound}"); + } + + string baseName = Path.GetFileNameWithoutExtension(sound); + int lastUnderscore = baseName.LastIndexOf('_'); + baseName = baseName.Substring(lastUnderscore + 1); + + byte loopFlag = byte.Parse(baseName); + + aaxWriter.WriteRow(true, new FileInfo(sound), loopFlag); + } + + aaxWriter.WriteEndTable(); + } + + elementRow["data"] = File.ReadAllBytes($"{directoryName}-temp/{soundElement.Path.Replace('/', '_')}"); + } + + soundElementTable.Save($"{directoryName}-temp/SOUND_ELEMENT"); + soundElementRow["utf"] = File.ReadAllBytes($"{directoryName}-temp/SOUND_ELEMENT"); + criTable.Save(directoryName + ".csb"); + + Directory.Delete(directoryName + "-temp", true); + } + + else + { + throw new FileNotFoundException("No valid file found!"); + } + } + } +} diff --git a/Source/SonicAudioCmd/csb_frequence_subdivision.txt b/Source/SonicAudioCmd/csb_frequence_subdivision.txt new file mode 100644 index 0000000..476ca7c --- /dev/null +++ b/Source/SonicAudioCmd/csb_frequence_subdivision.txt @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; +using System.Xml.Serialization; + +namespace SonicAudioCmd +{ + class Program + { + static void Main(string[] args) + { + CriTable table = new CriTable(); + table.Load(args[0]); + + CriRow soundElementTableRow = table.Rows.Single(row => (string)row["name"] == "SOUND_ELEMENT"); + + CriTable sdlTable = new CriTable(); + sdlTable.Load(soundElementTableRow["utf"] as byte[]); + + foreach (CriRow criRow in sdlTable.Rows) + { + criRow["sfreq"] = (uint)criRow["sfreq"] / 2; + } + + sdlTable.WriterSettings = CriTableWriterSettings.AdxSettings; + soundElementTableRow["utf"] = sdlTable.Save(); + + table.WriterSettings = CriTableWriterSettings.AdxSettings; + table.Save(args[0]); + } + } +} diff --git a/Source/SonicAudioCmd/file_replacer_acb.txt b/Source/SonicAudioCmd/file_replacer_acb.txt new file mode 100644 index 0000000..54c86da --- /dev/null +++ b/Source/SonicAudioCmd/file_replacer_acb.txt @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; + +namespace SonicAudioCmd +{ + class Program + { + static void Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine("Usage: [path]"); + Console.WriteLine("Drag and drop an ACB file to unpack its contents."); + Console.WriteLine("Drag and drop a folder to pack its contents back into the ACB. (The ACB file must be in the same directory.)"); + Console.ReadLine(); + return; + } + + if (args[0].EndsWith(".acb")) + { + string baseDirectory = Path.Combine( + Path.GetDirectoryName(args[0]), Path.GetFileNameWithoutExtension(args[0])); + + Directory.CreateDirectory(baseDirectory); + + using (CriTableReader reader = CriTableReader.Create(args[0])) + { + reader.Read(); + + using (Stream afs2Stream = reader.GetSubstream("AwbFile")) + { + Afs2Archive archive = new Afs2Archive(afs2Stream); + + using (CriTableReader waveformReader = CriTableReader.Create(reader.GetSubstream("WaveformTable"))) + { + while (waveformReader.Read()) + { + if (!waveformReader.GetBoolean("Streaming")) + { + ushort cueIndex = waveformReader.GetUInt16("Id"); + Afs2Entry entry = archive.GetEntryByCueIndex(cueIndex); + + string fileName = entry.CueIndex.ToString(); + switch (waveformReader.GetByte("EncodeType")) + { + case 0: + fileName += ".adx"; + break; + case 2: + fileName += ".hca"; + break; + case 13: + fileName += ".dsp"; + break; + default: + fileName += ".bin"; + break; + } + + using (Stream entryIn = entry.Open(), entryOut = File.Create(Path.Combine(baseDirectory, fileName))) + { + entryIn.CopyTo(entryOut); + } + } + } + } + } + } + } + + else + { + CriTable criTable = new CriTable(); + criTable.Load(args[0] + ".acb"); + + Afs2Archive afs2Archive = new Afs2Archive(); + + List files = new List(); + files.AddRange(Directory.GetFiles(args[0], "*.adx")); + files.AddRange(Directory.GetFiles(args[0], "*.hca")); + files.AddRange(Directory.GetFiles(args[0], "*.dsp")); + files.AddRange(Directory.GetFiles(args[0], "*.bin")); + + foreach (string file in Directory.GetFiles(args[0], "*.*")) + { + string baseName = Path.GetFileNameWithoutExtension(file); + + Afs2Entry afs2Entry = new Afs2Entry(); + afs2Entry.CueIndex = int.Parse(baseName); + afs2Entry.LocalFilePath = new FileInfo(file); + afs2Archive.Entries.Add(afs2Entry); + } + + MemoryStream memoryStream = new MemoryStream(); + afs2Archive.Write(memoryStream); + + criTable.Rows[0]["AwbFile"] = memoryStream.ToArray(); + criTable.Save(args[0] + ".acb", CriTableFileMode.Adx2); + } + } + } +} diff --git a/Source/SonicAudioCmd/utf_to_cs.txt b/Source/SonicAudioCmd/utf_to_cs.txt new file mode 100644 index 0000000..cd28cfd --- /dev/null +++ b/Source/SonicAudioCmd/utf_to_cs.txt @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; + +namespace SonicAudioCmd +{ + class Program + { + static void Main(string[] args) + { + CreateCSharpSource(File.OpenRead(args[0])); + } + + static void CreateCSharpSource(Stream stream) + { + using (CriTableReader reader = new CriTableReader(stream)) + using (TextWriter textWriter = File.CreateText(reader.TableName + ".cs")) + { + textWriter.WriteLine("using System.IO;"); + textWriter.WriteLine("using SonicAudioLib.CriMw;"); + textWriter.WriteLine(); + textWriter.WriteLine("namespace {0} {{", "SonicAudioLib.CriMw"); + textWriter.WriteLine(" [CriSerializable(\"{0}\")]", reader.TableName); + textWriter.WriteLine(" public class {0} {{", reader.TableName); + + for (int i = 0; i < reader.NumberOfFields; i++) + { + textWriter.Write(" private {0} _{1}", GetSimplifiedName(reader.GetFieldType(i)), reader.GetFieldName(i)); + + object defaultValue = reader.GetFieldValue(i); + if (defaultValue != null) + { + textWriter.Write(" = {0}", defaultValue); + } + + textWriter.WriteLine(";"); + } + + for (int i = 0; i < reader.NumberOfFields; i++) + { + string name = reader.GetFieldName(i); + Type type = reader.GetFieldType(i); + object defaultValue = reader.GetFieldValue(i); + + textWriter.WriteLine(); + if (defaultValue != null) + { + textWriter.WriteLine(" [CriField(\"{0}\", {1}, {2})]", name, defaultValue, i); + } + + else + { + textWriter.WriteLine(" [CriField(\"{0}\", {1})]", name, i); + } + + textWriter.WriteLine(" public {0} {1} {{", GetSimplifiedName(reader.GetFieldType(i)), name); + textWriter.WriteLine(" get {"); + textWriter.WriteLine(" return _{0};", name); + textWriter.WriteLine(" }"); + textWriter.WriteLine(" set {"); + textWriter.WriteLine(" _{0} = value;", name); + textWriter.WriteLine(" }"); + textWriter.WriteLine(" }"); + } + textWriter.WriteLine(" }"); + textWriter.WriteLine("}"); + + while (reader.Read()) + { + for (int i = 0; i < reader.NumberOfFields; i++) + { + if (reader.GetFieldType(i) == typeof(byte[])) + { + stream.Position = reader.GetPosition(i); + if (EndianStream.ReadInt32(stream) == 0x46545540) + { + CreateCSharpSource(reader.GetSubstream(i)); + } + } + } + } + } + } + + static string GetSimplifiedName(Type type) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Byte: + return "byte"; + case TypeCode.SByte: + return "sbyte"; + case TypeCode.Int16: + return "short"; + case TypeCode.UInt16: + return "ushort"; + case TypeCode.Int32: + return "int"; + case TypeCode.UInt32: + return "uint"; + case TypeCode.Int64: + return "long"; + case TypeCode.UInt64: + return "ulong"; + case TypeCode.Single: + return "float"; + case TypeCode.Double: + return "double"; + case TypeCode.String: + return "string"; + } + + if (type == typeof(byte[])) + { + return "FileInfo"; + } + + return "DBNull"; + } + } +} diff --git a/Source/SonicAudioCmd/utf_to_cs_memory.txt b/Source/SonicAudioCmd/utf_to_cs_memory.txt new file mode 100644 index 0000000..9f3b37f --- /dev/null +++ b/Source/SonicAudioCmd/utf_to_cs_memory.txt @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.IO.Compression; +using System.ComponentModel; +using System.Collections; + +using SonicAudioLib; +using SonicAudioLib.Archive; +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +using System.Xml; + +namespace SonicAudioCmd +{ + class Program + { + static void Main(string[] args) + { + byte[] bytes = File.ReadAllBytes(args[0]); + + for (int i = 0; i < bytes.Length; i++) + { + if (bytes[i] == '@' && bytes[i + 1] == 'U' && bytes[i + 2] == 'T' && bytes[i + 3] == 'F') + { + using (MemoryStream memoryStream = new MemoryStream(bytes)) + { + memoryStream.Position = i; + CreateCSharpSource(memoryStream); + } + } + } + } + + static void CreateCSharpSource(Stream stream) + { + using (CriTableReader reader = CriTableReader.Create(stream)) + using (TextWriter textWriter = File.CreateText(reader.TableName + ".cs")) + { + textWriter.WriteLine("using System.IO;"); + textWriter.WriteLine("using SonicAudioLib.CriMw;"); + textWriter.WriteLine(); + textWriter.WriteLine("namespace {0} {{", "SonicAudioLib.CriMw"); + textWriter.WriteLine(" [CriSerializable(\"{0}\")]", reader.TableName); + textWriter.WriteLine(" public class {0} {{", reader.TableName); + + for (int i = 0; i < reader.NumberOfFields; i++) + { + textWriter.Write(" private {0} _{1}", GetSimplifiedName(reader.GetFieldType(i)), reader.GetFieldName(i)); + + object defaultValue = reader.GetFieldValue(i); + if (defaultValue != null) + { + textWriter.Write(" = {0}", defaultValue); + } + + textWriter.WriteLine(";"); + } + + for (int i = 0; i < reader.NumberOfFields; i++) + { + string name = reader.GetFieldName(i); + Type type = reader.GetFieldType(i); + object defaultValue = reader.GetFieldValue(i); + string fieldFlag = reader.GetFieldFlag(i).ToString("X2"); + + textWriter.WriteLine(); + textWriter.WriteLine(" // Field flag: {0}", fieldFlag); + textWriter.WriteLine(" [CriField(\"{0}\", {1})]", name, i); + textWriter.WriteLine(" public {0} {1} {{", GetSimplifiedName(reader.GetFieldType(i)), name); + textWriter.WriteLine(" get {"); + textWriter.WriteLine(" return _{0};", name); + textWriter.WriteLine(" }"); + textWriter.WriteLine(" set {"); + textWriter.WriteLine(" _{0} = value;", name); + textWriter.WriteLine(" }"); + textWriter.WriteLine(" }"); + } + textWriter.WriteLine(" }"); + textWriter.WriteLine("}"); + + while (reader.Read()) + { + for (int i = 0; i < reader.NumberOfFields; i++) + { + if (reader.GetFieldType(i) == typeof(byte[])) + { + stream.Position = reader.GetPosition(i); + if (EndianStream.ReadInt32(stream) == 0x46545540) + { + CreateCSharpSource(reader.GetSubstream(i)); + } + } + } + } + } + } + + static string GetSimplifiedName(Type type) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Byte: + return "byte"; + case TypeCode.SByte: + return "sbyte"; + case TypeCode.Int16: + return "short"; + case TypeCode.UInt16: + return "ushort"; + case TypeCode.Int32: + return "int"; + case TypeCode.UInt32: + return "uint"; + case TypeCode.Int64: + return "long"; + case TypeCode.UInt64: + return "ulong"; + case TypeCode.Single: + return "float"; + case TypeCode.Double: + return "double"; + case TypeCode.String: + return "string"; + } + + if (type == typeof(byte[])) + { + return "FileInfo"; + } + + return "DBNull"; + } + } +} diff --git a/Source/SonicAudioLib/Archive/ArchiveBase.cs b/Source/SonicAudioLib/Archive/ArchiveBase.cs new file mode 100644 index 0000000..9fb16a2 --- /dev/null +++ b/Source/SonicAudioLib/Archive/ArchiveBase.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using SonicAudioLib.IO; +using SonicAudioLib.Module; +using System.Collections; + +namespace SonicAudioLib.Archive +{ + public abstract class EntryBase + { + protected long length; + + public virtual long Position { get; set; } + + public virtual long Length + { + get + { + if (FilePath != null) + { + return FilePath.Length; + } + + return length; + } + + set + { + length = value; + } + } + + public virtual FileInfo FilePath { get; set; } + + public virtual Stream Open(Stream source) + { + return new Substream(source, Position, length); + } + + public virtual Stream Open() + { + return FilePath.OpenRead(); + } + } + + public abstract class ArchiveBase : ModuleBase, IEnumerable + { + protected List entries = new List(); + + public virtual T this[int index] + { + get + { + return entries[index]; + } + } + + public virtual int Count + { + get + { + return entries.Count; + } + } + + public virtual void Add(T item) + { + entries.Add(item); + } + + public virtual T Get(int index) + { + return entries[index]; + } + + public virtual void Clear() + { + entries.Clear(); + } + + public virtual bool Contains(T item) + { + return entries.Contains(item); + } + + public virtual void CopyTo(T[] array, int arrayIndex) + { + entries.CopyTo(array, arrayIndex); + } + + public virtual IEnumerator GetEnumerator() + { + return entries.GetEnumerator(); + } + + public virtual bool Remove(T item) + { + return entries.Remove(item); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return entries.GetEnumerator(); + } + } +} diff --git a/Source/SonicAudioLib/Archive/CriAfs2Archive.cs b/Source/SonicAudioLib/Archive/CriAfs2Archive.cs new file mode 100644 index 0000000..9e53087 --- /dev/null +++ b/Source/SonicAudioLib/Archive/CriAfs2Archive.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using SonicAudioLib.IO; +using SonicAudioLib.Module; + +namespace SonicAudioLib.Archive +{ + public class CriAfs2Entry : EntryBase + { + public ushort CueIndex { get; set; } + } + + public class CriAfs2Archive : ArchiveBase + { + public uint Align { get; set; } + public uint CueIndexFieldLength { get; set; } + public uint PositionFieldLength { get; set; } + + public override void Read(Stream source) + { + if (EndianStream.ReadCString(source, 4) != "AFS2") + { + throw new Exception("No AFS2 signature found."); + } + + uint information = EndianStream.ReadUInt32(source); + + uint type = information & 0xFF; + if (type != 1) + { + throw new Exception($"Invalid AFS2 type ({type}). Please report the error with the AWB file."); + } + + CueIndexFieldLength = (information & 0x00FF0000) >> 16; + PositionFieldLength = (information & 0x0000FF00) >> 8; + + ushort entryCount = (ushort)EndianStream.ReadUInt32(source); + Align = EndianStream.ReadUInt32(source); + + CriAfs2Entry previousEntry = null; + for (uint i = 0; i < entryCount; i++) + { + CriAfs2Entry afs2Entry = new CriAfs2Entry(); + + long cueIndexPosition = 16 + (i * CueIndexFieldLength); + source.Seek(cueIndexPosition, SeekOrigin.Begin); + + switch (CueIndexFieldLength) + { + case 2: + afs2Entry.CueIndex = EndianStream.ReadUInt16(source); + break; + + default: + throw new Exception($"Unknown CueIndexFieldLength ({CueIndexFieldLength}). Please report the error with the AWB file."); + } + + long positionPosition = 16 + (entryCount * CueIndexFieldLength) + (i * PositionFieldLength); + source.Seek(positionPosition, SeekOrigin.Begin); + + switch (PositionFieldLength) + { + case 2: + afs2Entry.Position = EndianStream.ReadUInt16(source); + break; + + case 4: + afs2Entry.Position = EndianStream.ReadUInt32(source); + break; + + default: + throw new Exception($"Unknown PositionFieldLength ({PositionFieldLength}). Please report the error with the AWB file."); + } + + if (previousEntry != null) + { + previousEntry.Length = afs2Entry.Position - previousEntry.Position; + } + + while ((afs2Entry.Position % Align) != 0) + { + afs2Entry.Position++; + } + + if (i == entryCount - 1) + { + switch (PositionFieldLength) + { + case 2: + afs2Entry.Length = EndianStream.ReadUInt16(source) - afs2Entry.Position; + break; + + case 4: + afs2Entry.Length = EndianStream.ReadUInt32(source) - afs2Entry.Position; + break; + } + } + + entries.Add(afs2Entry); + previousEntry = afs2Entry; + } + } + + public override void Write(Stream destination) + { + uint headerLength = (uint)(16 + (entries.Count * CueIndexFieldLength) + (entries.Count * PositionFieldLength) + PositionFieldLength); + + EndianStream.WriteCString(destination, "AFS2", 4); + EndianStream.WriteUInt32(destination, 1 | (CueIndexFieldLength << 16) | (PositionFieldLength << 8)); + EndianStream.WriteUInt32(destination, (ushort)entries.Count); + EndianStream.WriteUInt32(destination, 1); + + // FIXME: Alignment support + VldPool vldPool = new VldPool(1); + + foreach (CriAfs2Entry afs2Entry in entries) + { + switch (CueIndexFieldLength) + { + case 2: + EndianStream.WriteUInt16(destination, (ushort)afs2Entry.CueIndex); + break; + + default: + throw new Exception($"Unknown CueIndexFieldLength ({CueIndexFieldLength}). Please set a valid length."); + } + } + + foreach (CriAfs2Entry afs2Entry in entries) + { + uint entryPosition = (uint)(headerLength + vldPool.Put(afs2Entry.FilePath)); + + switch (PositionFieldLength) + { + case 2: + EndianStream.WriteUInt16(destination, (ushort)entryPosition); + break; + + case 4: + EndianStream.WriteUInt32(destination, entryPosition); + break; + + default: + throw new Exception($"Unknown PositionFieldLength ({PositionFieldLength}). Please set a valid length."); + } + + afs2Entry.Position = entryPosition; + } + + EndianStream.WriteUInt32(destination, (uint)(headerLength + vldPool.Length)); + + vldPool.Write(destination); + vldPool.Clear(); + } + + public CriAfs2Entry GetByCueIndex(uint cueIndex) + { + return entries.Single(e => e.CueIndex == cueIndex); + } + + public override long CalculateLength() + { + long length = 16 + (entries.Count * CueIndexFieldLength) + (entries.Count * PositionFieldLength) + PositionFieldLength; + + foreach (CriAfs2Entry afs2Entry in entries) + { + while ((length % Align) != 0) + { + length++; + } + + length += afs2Entry.Length; + } + + return length; + } + + public void Order() + { + entries = entries.OrderBy(entry => entry.CueIndex).ToList(); + } + + public CriAfs2Archive() + { + Align = 32; + CueIndexFieldLength = 2; + PositionFieldLength = 4; + } + } +} diff --git a/Source/SonicAudioLib/Archive/CriCpkArchive.cs b/Source/SonicAudioLib/Archive/CriCpkArchive.cs new file mode 100644 index 0000000..31aa296 --- /dev/null +++ b/Source/SonicAudioLib/Archive/CriCpkArchive.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +using SonicAudioLib.IO; +using SonicAudioLib.CriMw; + +namespace SonicAudioLib.Archive +{ + public class CriCpkEntry : EntryBase + { + public string DirectoryName { get; set; } + public string Name { get; set; } + public uint Index { get; set; } + public string Comment { get; set; } + public bool IsCompressed { get; set; } + } + + public class CriCpkArchive : ArchiveBase + { + public override void Read(Stream source) + { + using (CriTableReader reader = CriCpkSection.Open(source, source.Position)) + { + reader.Read(); + + if (reader.GetUInt32("CpkMode") != 1) + { + throw new Exception("Unsupported CPK type! Only TOC CPKs are supported for now."); + } + + long tocPosition = (long)reader.GetUInt64("TocOffset"); + long contentPosition = (long)reader.GetUInt64("ContentOffset"); + ushort align = reader.GetUInt16("Align"); + + using (CriTableReader tocReader = CriCpkSection.Open(source, tocPosition)) + { + while (tocReader.Read()) + { + CriCpkEntry entry = new CriCpkEntry(); + entry.DirectoryName = tocReader.GetString("DirName"); + entry.Name = tocReader.GetString("FileName"); + entry.Length = tocReader.GetUInt32("FileSize"); + entry.Position = (long)tocReader.GetUInt64("FileOffset"); + entry.Index = tocReader.GetUInt32("ID"); + entry.Comment = tocReader.GetString("UserString"); + + if (entry.Length != tocReader.GetUInt32("ExtractSize")) + { + entry.IsCompressed = true; + } + + if (contentPosition < tocPosition) + { + entry.Position += contentPosition; + } + + else + { + entry.Position += tocPosition; + } + + while ((entry.Position % align) != 0) + { + entry.Position++; + } + + entries.Add(entry); + + Console.WriteLine(Path.Combine(entry.DirectoryName, entry.Name)); + } + } + } + } + + public override void Write(Stream destination) + { + throw new NotImplementedException(); + } + + private class CriCpkSection : IDisposable + { + private Stream destination; + private long headerPosition; + + private CriTableWriter writer; + + public CriTableWriter Writer + { + get + { + return writer; + } + } + + public void Dispose() + { + writer.Dispose(); + + long position = destination.Position; + uint length = (uint)(position - (headerPosition - 8)); + + destination.Seek(headerPosition + 8, SeekOrigin.Begin); + EndianStream.WriteUInt32(destination, length); + + destination.Seek(position, SeekOrigin.Begin); + } + + public static CriTableReader Open(Stream source, long position) + { + source.Seek(position, SeekOrigin.Begin); + + string signature = EndianStream.ReadCString(source, 4); + uint flag = EndianStream.ReadUInt32(source); + uint tableLength = EndianStream.ReadUInt32(source); + uint unknown = EndianStream.ReadUInt32(source); + + return CriTableReader.Create(new Substream(source, source.Position, tableLength)); + } + + public CriCpkSection(Stream destination, string signature) + { + this.destination = destination; + headerPosition = destination.Position; + + EndianStream.WriteCString(destination, signature, 4); + EndianStream.WriteUInt32(destination, byte.MaxValue); + destination.Seek(8, SeekOrigin.Begin); + + writer = CriTableWriter.Create(destination); + } + } + } +} diff --git a/Source/SonicAudioLib/Archive/HeroesPacArchive.cs b/Source/SonicAudioLib/Archive/HeroesPacArchive.cs new file mode 100644 index 0000000..75ecd8d --- /dev/null +++ b/Source/SonicAudioLib/Archive/HeroesPacArchive.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +using SonicAudioLib.IO; +using SonicAudioLib.Module; + +namespace SonicAudioLib.Archive +{ + public class HeroesPacEntry : EntryBase + { + public uint GlobalIndex { get; set; } + } + + public class HeroesPacArchive : ArchiveBase + { + public override void Read(Stream source) + { + uint entryCount = EndianStream.ReadUInt32(source); + uint tablePosition = EndianStream.ReadUInt32(source); + uint vldPoolLength = EndianStream.ReadUInt32(source); + uint vldPoolPosition = EndianStream.ReadUInt32(source); + + source.Seek(tablePosition, SeekOrigin.Begin); + + HeroesPacEntry previousEntry = null; + for (uint i = 0; i < entryCount; i++) + { + HeroesPacEntry pacEntry = new HeroesPacEntry(); + + pacEntry.GlobalIndex = EndianStream.ReadUInt32(source); + pacEntry.Position = vldPoolPosition + EndianStream.ReadUInt32(source); + + if (previousEntry != null) + { + previousEntry.Length = pacEntry.Position - previousEntry.Position; + } + + if (i == entryCount - 1) + { + pacEntry.Length = (uint)source.Length - pacEntry.Position; + } + + entries.Add(pacEntry); + previousEntry = pacEntry; + + source.Seek(8, SeekOrigin.Current); + } + } + + public override void Write(Stream destination) + { + long headerPosition = destination.Position; + + uint headerLength = 16; + for (uint i = 0; i < headerLength; i++) + { + destination.WriteByte(0); + } + + while ((destination.Position % 32) != 0) + { + destination.WriteByte(0); + } + + uint tableLength = (uint)(entries.Count * 16); + uint tablePosition = (uint)destination.Position; + + VldPool vldPool = new VldPool(); + + foreach (HeroesPacEntry pacEntry in entries) + { + uint entryPosition = (uint)vldPool.Put(pacEntry.FilePath); + + EndianStream.WriteUInt32(destination, pacEntry.GlobalIndex); + EndianStream.WriteUInt32(destination, entryPosition); + + while ((destination.Position % 16) != 0) + { + destination.WriteByte(0); + } + + pacEntry.Position = tablePosition + tableLength + entryPosition; + } + + vldPool.Write(destination); + vldPool.Clear(); + + destination.Seek(0, SeekOrigin.Begin); + EndianStream.WriteUInt32(destination, (uint)entries.Count); + EndianStream.WriteUInt32(destination, tablePosition); + EndianStream.WriteUInt32(destination, (uint)vldPool.Length); + EndianStream.WriteUInt32(destination, (uint)vldPool.Position); + } + } +} diff --git a/Source/SonicAudioLib/Collections/OrderedDictionary.cs b/Source/SonicAudioLib/Collections/OrderedDictionary.cs new file mode 100644 index 0000000..1d06bc0 --- /dev/null +++ b/Source/SonicAudioLib/Collections/OrderedDictionary.cs @@ -0,0 +1,175 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace SonicAudioLib.Collections +{ + /// + /// Represents a key/value pair for an . + /// + /// + /// + public class KeyValuePair + { + public TKey Key { get; set; } + public TValue Value { get; set; } + + public KeyValuePair(TKey key, TValue value) + { + Key = key; + Value = value; + } + } + + /// + /// Represents a key/value pair collection that is accessable by its key or index. + /// + public class OrderedDictionary : IEnumerable> + { + private List> items = new List>(); + + /// + /// Gets the count of key/value pairs. + /// + public int Count + { + get + { + return items.Count; + } + } + + /// + /// Gets the value at the specified index. + /// + public TValue this[int index] + { + get + { + return items[index].Value; + } + + set + { + items[index].Value = value; + } + } + + /// + /// Gets the value by the specified key. + /// + public TValue this[TKey key] + { + get + { + return items.Single(k => (k.Key).Equals(key)).Value; + } + + set + { + items.Single(k => (k.Key).Equals(key)).Value = value; + } + } + + /// + /// Determines whether the collection contains the specified key. + /// + public bool ContainsKey(TKey key) + { + return items.Any(k => (k.Key).Equals(key)); + } + + /// + /// Adds a key/value pair to end of the collection. + /// + public void Add(TKey key, TValue value) + { + items.Add(new KeyValuePair(key, value)); + } + + /// + /// Removes the key/value pair by its key. + /// + public bool Remove(TKey key) + { + return items.Remove(items.Single(k => (k.Key).Equals(key))); + } + + /// + /// Clears all the key/value pairs. + /// + public void Clear() + { + items.Clear(); + } + + /// + /// Gets the index of the specified key. + /// + public int IndexOf(TKey key) + { + return items.IndexOf(items.Single(k => (k.Key).Equals(key))); + } + + /// + /// Inserts a key/value pair to the specified index. + /// + public void Insert(int index, TKey key, TValue value) + { + items.Insert(index, new KeyValuePair(key, value)); + } + + /// + /// Removes key/value pair at the specified index. + /// + public void RemoveAt(int index) + { + items.RemoveAt(index); + } + + /// + /// Gets key at the specified index. + /// + public TKey GetKeyByIndex(int index) + { + return items[index].Key; + } + + /// + /// Gets value at the specified index. + /// + public TValue GetValueByIndex(int index) + { + return items[index].Value; + } + + /// + /// Sets key at the specified index. + /// + public void SetKeyByIndex(int index, TKey key) + { + items[index].Key = key; + } + + /// + /// Sets value at the specified index. + /// + public void SetValueByIndex(int index, TValue value) + { + items[index].Value = value; + } + + /// + /// Returns an enumerator of key/value pairs. + /// + public IEnumerator> GetEnumerator() + { + return items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return items.GetEnumerator(); + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriField.cs b/Source/SonicAudioLib/CriMw/CriField.cs new file mode 100644 index 0000000..24a5a9e --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriField.cs @@ -0,0 +1,134 @@ +using System; +using System.ComponentModel; + +namespace SonicAudioLib.CriMw +{ + public class CriField + { + public static readonly Type[] FieldTypes = + { + typeof(byte), + typeof(sbyte), + typeof(ushort), + typeof(short), + typeof(uint), + typeof(int), + typeof(ulong), + typeof(long), + typeof(float), + typeof(double), + typeof(string), + typeof(byte[]), + typeof(Guid), + }; + + public static object[] NullValues = + { + (byte)0, + (sbyte)0, + (ushort)0, + (short)0, + (uint)0, + (int)0, + (ulong)0, + (long)0, + (float)0.0f, + (double)0.0f, + (string)string.Empty, + (byte[])new byte[0], + (Guid)Guid.Empty, + }; + + private Type fieldType; + private string fieldName; + private object defaultValue; + private CriTable parent; + + public int FieldTypeIndex + { + get + { + return Array.IndexOf(FieldTypes, fieldType); + } + } + + public Type FieldType + { + get + { + return fieldType; + } + } + + public object DefaultValue + { + get + { + return defaultValue; + } + + set + { + defaultValue = ConvertObject(value); + } + } + + public string FieldName + { + get + { + return fieldName; + } + } + + public object ConvertObject(object obj) + { + if (obj == null) + { + return NullValues[FieldTypeIndex]; + } + + Type typ = obj.GetType(); + + if (typ == fieldType) + { + return obj; + } + + TypeConverter typeConverter = TypeDescriptor.GetConverter(fieldType); + + if (typeConverter.CanConvertFrom(typ)) + { + return typeConverter.ConvertFrom(obj); + } + + return DefaultValue; + } + + public CriTable Parent + { + get + { + return parent; + } + + internal set + { + parent = value; + } + } + + public CriField(string name, Type type) + { + fieldName = name; + fieldType = type; + } + + public CriField(string name, Type type, object defaultValue) + { + fieldName = name; + fieldType = type; + this.defaultValue = ConvertObject(defaultValue); + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriFieldCollection.cs b/Source/SonicAudioLib/CriMw/CriFieldCollection.cs new file mode 100644 index 0000000..c0f9bc1 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriFieldCollection.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace SonicAudioLib.CriMw +{ + public class CriFieldCollection : IEnumerable + { + private CriTable parent; + private List fields = new List(); + + public CriField this[int index] + { + get + { + return fields[index]; + } + } + + public CriField this[string name] + { + get + { + return fields.Single(f => f.FieldName == name); + } + } + + public int Count + { + get + { + return fields.Count; + } + } + + public CriTable Parent + { + get + { + return parent; + } + + internal set + { + parent = value; + } + } + + public void Add(CriField criField) + { + criField.Parent = parent; + fields.Add(criField); + } + + public CriField Add(string name, Type type) + { + CriField criField = new CriField(name, type); + Add(criField); + + return criField; + } + + public CriField Add(string name, Type type, object defaultValue) + { + CriField criField = new CriField(name, type, defaultValue); + Add(criField); + + return criField; + } + + public void Insert(int index, CriField criField) + { + if (index >= fields.Count || index < 0) + { + fields.Add(criField); + } + + else + { + fields.Insert(index, criField); + } + } + + public void Remove(CriField criField) + { + fields.Remove(criField); + + // Update the objects + foreach (CriRow criRow in parent.Rows) + { + criRow.Records.Remove(criField); + } + } + + public void RemoveAt(int index) + { + Remove(fields[index]); + } + + internal void Clear() + { + fields.Clear(); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)fields).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)fields).GetEnumerator(); + } + + public CriFieldCollection(CriTable parent) + { + this.parent = parent; + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriRow.cs b/Source/SonicAudioLib/CriMw/CriRow.cs new file mode 100644 index 0000000..c95f91f --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriRow.cs @@ -0,0 +1,107 @@ +using SonicAudioLib.Collections; +using System.Collections; +using System.Linq; + +namespace SonicAudioLib.CriMw +{ + public class CriRow : IEnumerable + { + private OrderedDictionary records = new OrderedDictionary(); + private CriTable parent; + + public object this[CriField criField] + { + get + { + return records[criField]; + } + + set + { + records[criField] = value; + } + } + + public object this[int index] + { + get + { + return records[index]; + } + + set + { + records[index] = value; + } + } + + public object this[string name] + { + get + { + return this[records.Single(k => (k.Key).FieldName == name).Key]; + } + + set + { + this[records.Single(k => (k.Key).FieldName == name).Key] = value; + } + } + + public CriTable Parent + { + get + { + return parent; + } + + internal set + { + parent = value; + } + } + + internal OrderedDictionary Records + { + get + { + return records; + } + } + + public int FieldCount + { + get + { + return records.Count; + } + } + + public object[] GetValueArray() + { + object[] values = new object[records.Count]; + + for (int i = 0; i < records.Count; i++) + { + values[i] = records[i]; + } + + return values; + } + + public IEnumerator GetEnumerator() + { + foreach (var keyValPair in records) + { + yield return keyValPair.Value; + } + + yield break; + } + + internal CriRow(CriTable parent) + { + this.parent = parent; + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriRowCollection.cs b/Source/SonicAudioLib/CriMw/CriRowCollection.cs new file mode 100644 index 0000000..569b384 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriRowCollection.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace SonicAudioLib.CriMw +{ + public class CriRowCollection : IEnumerable + { + private CriTable parent; + private List rows = new List(); + + public CriRow this[int index] + { + get + { + return rows[index]; + } + } + + public int Count + { + get + { + return rows.Count; + } + } + + public CriTable Parent + { + get + { + return parent; + } + + internal set + { + parent = value; + } + } + + public void Add(CriRow criRow) + { + criRow.Parent = parent; + rows.Add(criRow); + } + + public CriRow Add(params object[] objs) + { + CriRow criRow = parent.NewRow(); + + object[] objects = new object[criRow.FieldCount]; + Array.Copy(objs, objects, Math.Min(objs.Length, objects.Length)); + + for (int i = 0; i < criRow.FieldCount; i++) + { + criRow[i] = objects[i]; + } + + Add(criRow); + return criRow; + } + + internal void Clear() + { + rows.Clear(); + } + + public IEnumerator GetEnumerator() + { + return ((IEnumerable)rows).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)rows).GetEnumerator(); + } + + public CriRowCollection(CriTable parent) + { + this.parent = parent; + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriTable.Internal.cs b/Source/SonicAudioLib/CriMw/CriTable.Internal.cs new file mode 100644 index 0000000..3121250 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriTable.Internal.cs @@ -0,0 +1,52 @@ +using System; + +namespace SonicAudioLib.CriMw +{ + struct CriTableHeader + { + public static readonly string Signature = "@UTF"; + public uint Length { get; set; } + public bool FirstBoolean { get; set; } + public bool SecondBoolean { get; set; } + public ushort RowsPosition { get; set; } + public uint StringPoolPosition { get; set; } + public uint DataPoolPosition { get; set; } + public string TableName { get; set; } + public ushort NumberOfFields { get; set; } + public ushort RowLength { get; set; } + public uint NumberOfRows { get; set; } + } + + [Flags] + enum CriFieldFlag + { + Name = 16, + DefaultValue = 32, + RowStorage = 64, + + Byte = 0, + SByte = 1, + UInt16 = 2, + Int16 = 3, + UInt32 = 4, + Int32 = 5, + UInt64 = 6, + Int64 = 7, + Float = 8, + Double = 9, + String = 10, + Data = 11, + Guid = 12, + + TypeMask = 15, + }; + + struct CriTableField + { + public CriFieldFlag Flag { get; set; } + public string Name { get; set; } + public uint Position { get; set; } + public uint Length { get; set; } + public object Value { get; set; } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriTable.cs b/Source/SonicAudioLib/CriMw/CriTable.cs new file mode 100644 index 0000000..d7471e9 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriTable.cs @@ -0,0 +1,143 @@ +using System; +using System.IO; +using System.Linq; + +using SonicAudioLib.IO; +using SonicAudioLib.Module; + +namespace SonicAudioLib.CriMw +{ + public class CriTable : ModuleBase + { + private CriFieldCollection fields; + private CriRowCollection rows; + private string tableName = "(no name)"; + private CriTableWriterSettings writerSettings; + + public CriFieldCollection Fields + { + get + { + return fields; + } + } + + public CriRowCollection Rows + { + get + { + return rows; + } + } + + public string TableName + { + get + { + return tableName; + } + + set + { + tableName = value; + } + } + + public CriTableWriterSettings WriterSettings + { + get + { + return writerSettings; + } + + set + { + writerSettings = value; + } + } + + public void Clear() + { + rows.Clear(); + fields.Clear(); + } + + public CriRow NewRow() + { + CriRow criRow = new CriRow(this); + + foreach (CriField criField in fields) + { + criRow.Records.Add(criField, criField.DefaultValue); + } + + return criRow; + } + + public override void Read(Stream source) + { + using (CriTableReader reader = CriTableReader.Create(source)) + { + tableName = reader.TableName; + + for (int i = 0; i < reader.NumberOfFields; i++) + { + fields.Add(reader.GetFieldName(i), reader.GetFieldType(i), reader.GetFieldValue(i)); + } + + while (reader.Read()) + { + rows.Add(reader.GetValueArray()); + } + } + } + + public override void Write(Stream destination) + { + using (CriTableWriter writer = CriTableWriter.Create(destination, writerSettings)) + { + writer.WriteStartTable(tableName); + + writer.WriteStartFieldCollection(); + foreach (CriField criField in fields) + { + if (!rows.Any(row => row[criField] != criField.DefaultValue)) + { + writer.WriteField(criField.FieldName, criField.FieldType, criField.DefaultValue); + } + + else + { + writer.WriteField(criField.FieldName, criField.FieldType); + } + } + writer.WriteEndFieldCollection(); + + foreach (CriRow criRow in rows) + { + writer.WriteRow(true, criRow.GetValueArray()); + } + + writer.WriteEndTable(); + } + } + + public override long CalculateLength() + { + // TODO + return base.CalculateLength(); + } + + public CriTable() + { + fields = new CriFieldCollection(this); + rows = new CriRowCollection(this); + writerSettings = new CriTableWriterSettings(); + } + + public CriTable(string tableName) : this() + { + this.tableName = tableName; + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriTableReader.cs b/Source/SonicAudioLib/CriMw/CriTableReader.cs new file mode 100644 index 0000000..fb6bd59 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriTableReader.cs @@ -0,0 +1,702 @@ +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace SonicAudioLib.CriMw +{ + public class CriTableReader : IDisposable + { + private OrderedDictionary fields; + private Stream source; + private CriTableHeader header; + private int rowIndex = -1; + private uint headerPosition; + private bool leaveOpen; + + public object this[int fieldIndex] + { + get + { + return GetValue(fieldIndex); + } + } + + public object this[string fieldName] + { + get + { + return GetValue(fieldName); + } + } + + public ushort NumberOfFields + { + get + { + return header.NumberOfFields; + } + } + + public uint NumberOfRows + { + get + { + return header.NumberOfRows; + } + } + + public string TableName + { + get + { + return header.TableName; + } + } + + public int CurrentRow + { + get + { + return rowIndex; + } + } + + public Stream SourceStream + { + get + { + return source; + } + } + + private void ReadTable() + { + headerPosition = (uint)source.Position; + + if (EndianStream.ReadCString(source, 4) != CriTableHeader.Signature) + { + throw new Exception("No @UTF signature found."); + } + + header.Length = ReadUInt32() + 0x8; + header.FirstBoolean = ReadBoolean(); + header.SecondBoolean = ReadBoolean(); + header.RowsPosition = (ushort)(ReadUInt16() + 0x8); + header.StringPoolPosition = ReadUInt32() + 0x8; + header.DataPoolPosition = ReadUInt32() + 0x8; + header.TableName = ReadString(); + header.NumberOfFields = ReadUInt16(); + header.RowLength = ReadUInt16(); + header.NumberOfRows = ReadUInt32(); + + if (header.FirstBoolean) + { + throw new Exception($"Invalid boolean ({header.FirstBoolean}. Please report the error with the file."); + } + + for (ushort i = 0; i < header.NumberOfFields; i++) + { + CriTableField field = new CriTableField(); + + field.Flag = (CriFieldFlag)ReadByte(); + + if (field.Flag.HasFlag(CriFieldFlag.Name)) + { + field.Name = ReadString(); + } + + if (field.Flag.HasFlag(CriFieldFlag.DefaultValue)) + { + if (field.Flag.HasFlag(CriFieldFlag.Data)) + { + uint vldPosition; + uint vldLength; + + ReadData(out vldPosition, out vldLength); + + field.Position = vldPosition; + field.Length = vldLength; + } + + else + { + field.Value = ReadValue(field.Flag); + } + } + + // Not even per row, and not even constant value? Then there's no storage. + else if (!field.Flag.HasFlag(CriFieldFlag.RowStorage) && !field.Flag.HasFlag(CriFieldFlag.DefaultValue)) + { + if (field.Flag.HasFlag(CriFieldFlag.Data)) + { + field.Position = 0; + field.Length = 0; + } + + else + { + field.Value = CriField.NullValues[(byte)field.Flag & 0x0F]; + } + } + + fields.Add(field.Name, field); + } + } + + public string GetFieldName(int fieldIndex) + { + return fields[fieldIndex].Name; + } + + public Type GetFieldType(int fieldIndex) + { + return CriField.FieldTypes[(byte)fields[fieldIndex].Flag & 0x0F]; + } + + public Type GetFieldType(string fieldName) + { + return CriField.FieldTypes[(byte)fields[fieldName].Flag & 0x0F]; + } + + public object GetFieldValue(int fieldIndex) + { + return fields[fieldIndex].Value; + } + + public byte GetFieldFlag(string fieldName) + { + return (byte)fields[fieldName].Flag; + } + + public byte GetFieldFlag(int fieldIndex) + { + return (byte)fields[fieldIndex].Flag; + } + + public object GetFieldValue(string fieldName) + { + return fields[fieldName].Value; + } + + public CriField GetField(int fieldIndex) + { + return new CriField(GetFieldName(fieldIndex), GetFieldType(fieldIndex), GetFieldValue(fieldIndex)); + } + + public CriField GetField(string fieldName) + { + return new CriField(fieldName, GetFieldType(fieldName), GetFieldValue(fieldName)); + } + + private void GoToValue(int fieldIndex) + { + long position = headerPosition + header.RowsPosition + (header.RowLength * rowIndex); + + for (int i = 0; i < fieldIndex; i++) + { + if (!fields[i].Flag.HasFlag(CriFieldFlag.RowStorage)) + { + continue; + } + + switch (fields[i].Flag & CriFieldFlag.TypeMask) + { + case CriFieldFlag.Byte: + case CriFieldFlag.SByte: + position += 1; + break; + case CriFieldFlag.Int16: + case CriFieldFlag.UInt16: + position += 2; + break; + case CriFieldFlag.Int32: + case CriFieldFlag.UInt32: + case CriFieldFlag.Float: + case CriFieldFlag.String: + position += 4; + break; + case CriFieldFlag.Int64: + case CriFieldFlag.UInt64: + case CriFieldFlag.Double: + case CriFieldFlag.Data: + position += 8; + break; + } + } + + source.Position = position; + } + + public bool Read() + { + if (rowIndex + 1 >= header.NumberOfRows) + { + return false; + } + + rowIndex++; + return true; + } + + public bool MoveToRow(int rowIndex) + { + if (rowIndex >= header.NumberOfRows) + { + return false; + } + + this.rowIndex = rowIndex; + return true; + } + + public object[] GetValueArray() + { + object[] values = new object[header.NumberOfFields]; + + for (int i = 0; i < header.NumberOfFields; i++) + { + if (fields[i].Flag.HasFlag(CriFieldFlag.Data)) + { + values[i] = GetData(i); + } + + else + { + values[i] = GetValue(i); + } + } + + return values; + } + + public object GetValue(int fieldIndex) + { + if (fieldIndex < 0 || fieldIndex >= fields.Count) + { + return null; + } + + if (!fields[fieldIndex].Flag.HasFlag(CriFieldFlag.RowStorage)) + { + if (fields[fieldIndex].Flag.HasFlag(CriFieldFlag.Data)) + { + return new Substream(source, 0, 0); + } + + return fields[fieldIndex].Value; + } + + GoToValue(fieldIndex); + return ReadValue(fields[fieldIndex].Flag); + } + + public object GetValue(string fieldName) + { + return GetValue(fields.IndexOf(fieldName)); + } + + public T GetValue(int fieldIndex) + { + return (T)GetValue(fieldIndex); + } + + public T GetValue(string fieldName) + { + return (T)GetValue(fieldName); + } + + public byte GetByte(int fieldIndex) + { + return (byte)GetValue(fieldIndex); + } + + public byte GetByte(string fieldName) + { + return (byte)GetValue(fieldName); + } + + public sbyte GetSByte(int fieldIndex) + { + return (sbyte)GetValue(fieldIndex); + } + + public sbyte GetSByte(string fieldName) + { + return (sbyte)GetValue(fieldName); + } + + public ushort GetUInt16(int fieldIndex) + { + return (ushort)GetValue(fieldIndex); + } + + public ushort GetUInt16(string fieldName) + { + return (ushort)GetValue(fieldName); + } + + public short GetInt16(int fieldIndex) + { + return (short)GetValue(fieldIndex); + } + + public short GetInt16(string fieldName) + { + return (short)GetValue(fieldName); + } + + public uint GetUInt32(int fieldIndex) + { + return (uint)GetValue(fieldIndex); + } + + public uint GetUInt32(string fieldName) + { + return (uint)GetValue(fieldName); + } + + public int GetInt32(int fieldIndex) + { + return (int)GetValue(fieldIndex); + } + + public int GetInt32(string fieldName) + { + return (int)GetValue(fieldName); + } + + public ulong GetUInt64(int fieldIndex) + { + return (ulong)GetValue(fieldIndex); + } + + public ulong GetUInt64(string fieldName) + { + return (ulong)GetValue(fieldName); + } + + public long GetInt64(int fieldIndex) + { + return (long)GetValue(fieldIndex); + } + + public long GetInt64(string fieldName) + { + return (long)GetValue(fieldName); + } + + public float GetFloat(int fieldIndex) + { + return (float)GetValue(fieldIndex); + } + + public float GetFloat(string fieldName) + { + return (float)GetValue(fieldName); + } + + public double GetDouble(int fieldIndex) + { + return (double)GetValue(fieldIndex); + } + + public double GetDouble(string fieldName) + { + return (double)GetValue(fieldName); + } + + public string GetString(int fieldIndex) + { + return (string)GetValue(fieldIndex); + } + + public string GetString(string fieldName) + { + return (string)GetValue(fieldName); + } + + public Substream GetSubstream(int fieldIndex) + { + return (Substream)GetValue(fieldIndex); + } + + public Substream GetSubstream(string fieldName) + { + return (Substream)GetValue(fieldName); + } + + public byte[] GetData(int fieldIndex) + { + return GetSubstream(fieldIndex).ToArray(); + } + + public byte[] GetData(string fieldName) + { + return GetData(fields.IndexOf(fieldName)); + } + + public uint GetLength(int fieldIndex) + { + if (fieldIndex < 0 || fieldIndex >= fields.Count) + { + return 0; + } + + if (!fields[fieldIndex].Flag.HasFlag(CriFieldFlag.RowStorage)) + { + return fields[fieldIndex].Length; + } + + uint vldPosition; + uint vldLength; + + GoToValue(fieldIndex); + ReadData(out vldPosition, out vldLength); + return vldLength; + } + + public uint GetLength(string fieldName) + { + return GetLength(fields.IndexOf(fieldName)); + } + + public uint GetPosition(int fieldIndex) + { + if (fieldIndex < 0 || fieldIndex >= fields.Count) + { + return 0; + } + + if (!fields[fieldIndex].Flag.HasFlag(CriFieldFlag.RowStorage)) + { + return fields[fieldIndex].Position; + } + + uint vldPosition; + uint vldLength; + + GoToValue(fieldIndex); + ReadData(out vldPosition, out vldLength); + return (uint)(headerPosition + header.DataPoolPosition + vldPosition); + } + + public uint GetPosition(string fieldName) + { + return GetPosition(fields.IndexOf(fieldName)); + } + + public bool GetBoolean(int fieldIndex) + { + return (byte)GetValue(fieldIndex) > 0; + } + + public bool GetBoolean(string fieldName) + { + return (byte)GetValue(fieldName) > 0; + } + + public Guid GetGuid(int fieldIndex) + { + return (Guid)GetValue(fieldIndex); + } + + public Guid GetGuid(string fieldName) + { + return (Guid)GetValue(fieldName); + } + + private byte[] ReadBytes(int length) + { + byte[] buff = new byte[length]; + source.Read(buff, 0, length); + return buff; + } + + private byte ReadByte() + { + return EndianStream.ReadByte(source); + } + + private bool ReadBoolean() + { + return EndianStream.ReadBoolean(source); + } + + private sbyte ReadSByte() + { + return EndianStream.ReadSByte(source); + } + + private ushort ReadUInt16() + { + return EndianStream.ReadUInt16BE(source); + } + + private short ReadInt16() + { + return EndianStream.ReadInt16BE(source); + } + + private uint ReadUInt32() + { + return EndianStream.ReadUInt32BE(source); + } + + private int ReadInt32() + { + return EndianStream.ReadInt32BE(source); + } + + private ulong ReadUInt64() + { + return EndianStream.ReadUInt64BE(source); + } + + private long ReadInt64() + { + return EndianStream.ReadInt64BE(source); + } + + private float ReadFloat() + { + return EndianStream.ReadFloatBE(source); + } + + private double ReadDouble() + { + return EndianStream.ReadDoubleBE(source); + } + + private string ReadString() + { + int stringPosition = ReadInt32(); + + long previousPosition = source.Position; + + source.Position = headerPosition + header.StringPoolPosition + stringPosition; + string strResult = EndianStream.ReadCString(source, Encoding.Default); + source.Position = previousPosition; + + if (strResult == "" || + (strResult == header.TableName && stringPosition == 0)) + { + return null; + } + + return strResult; + } + + private void ReadData(out uint vldPosition, out uint vldLength) + { + vldPosition = ReadUInt32(); + vldLength = ReadUInt32(); + } + + private Guid ReadGuid() + { + byte[] buffer = new byte[16]; + source.Read(buffer, 0, buffer.Length); + return new Guid(buffer); + } + + private object ReadValue(CriFieldFlag fieldFlag) + { + switch (fieldFlag & CriFieldFlag.TypeMask) + { + case CriFieldFlag.Byte: + return ReadByte(); + case CriFieldFlag.SByte: + return ReadSByte(); + case CriFieldFlag.UInt16: + return ReadUInt16(); + case CriFieldFlag.Int16: + return ReadInt16(); + case CriFieldFlag.UInt32: + return ReadUInt32(); + case CriFieldFlag.Int32: + return ReadInt32(); + case CriFieldFlag.UInt64: + return ReadUInt64(); + case CriFieldFlag.Int64: + return ReadInt64(); + case CriFieldFlag.Float: + return ReadFloat(); + case CriFieldFlag.Double: + return ReadDouble(); + case CriFieldFlag.String: + return ReadString(); + case CriFieldFlag.Data: + { + uint vldPosition; + uint vldLength; + + ReadData(out vldPosition, out vldLength); + + // SecondBoolean being true, check if utf table + if (vldPosition > 0 && vldLength == 0) + { + source.Position = headerPosition + header.DataPoolPosition + vldPosition; + + if (Encoding.ASCII.GetString(ReadBytes(4)) == "@UTF") + { + vldLength = ReadUInt32() + 8; + } + } + + return new Substream(source, headerPosition + header.DataPoolPosition + vldPosition, vldLength); + } + case CriFieldFlag.Guid: + return ReadGuid(); + } + + return null; + } + + public void Dispose() + { + fields.Clear(); + + if (!leaveOpen) + { + source.Close(); + } + + GC.SuppressFinalize(this); + } + + public static CriTableReader Create(byte[] sourceByteArray) + { + Stream source = new MemoryStream(sourceByteArray); + return Create(source); + } + + public static CriTableReader Create(string sourceFileName) + { + Stream source = File.OpenRead(sourceFileName); + return Create(source); + } + + public static CriTableReader Create(Stream source) + { + return Create(source, false); + } + + public static CriTableReader Create(Stream source, bool leaveOpen) + { + return new CriTableReader(source, leaveOpen); + } + + private CriTableReader(Stream source, bool leaveOpen) + { + this.source = source; + header = new CriTableHeader(); + fields = new OrderedDictionary(); + this.leaveOpen = leaveOpen; + + ReadTable(); + } + } +} diff --git a/Source/SonicAudioLib/CriMw/CriTableWriter.cs b/Source/SonicAudioLib/CriMw/CriTableWriter.cs new file mode 100644 index 0000000..4a7b9c5 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/CriTableWriter.cs @@ -0,0 +1,713 @@ +using System; +using System.IO; +using System.Text; +using System.ComponentModel; + +using SonicAudioLib.Collections; +using SonicAudioLib.IO; +using SonicAudioLib.Module; + +namespace SonicAudioLib.CriMw +{ + public class CriTableWriter : IDisposable + { + public enum Status + { + Begin, + Start, + FieldCollection, + Row, + Idle, + End, + } + + private CriTableWriterSettings settings; + private OrderedDictionary fields; + private Stream destination; + private CriTableHeader header; + private VldPool vldPool; + private StringPool stringPool; + private uint headerPosition; + private uint endPosition; + + private Status status = Status.Begin; + + public Status CurrentStatus + { + get + { + return status; + } + } + + public Stream DestinationStream + { + get + { + return destination; + } + } + + public void WriteStartTable() + { + WriteStartTable("(no name)"); + } + + public void WriteStartTable(string tableName) + { + if (status != Status.Begin) + { + throw new InvalidOperationException("Attempted to start table when the status wasn't Begin"); + } + + status = Status.Start; + + headerPosition = (uint)destination.Position; + header.TableName = tableName; + + if (settings.PutBlankString) + { + stringPool.Put(StringPool.AdxBlankString); + } + + EndianStream.WriteCString(destination, CriTableHeader.Signature, 4); + WriteUInt32(uint.MinValue); + WriteBoolean(false); + WriteBoolean(false); + WriteUInt16(ushort.MinValue); + WriteUInt32(uint.MinValue); + WriteUInt32(uint.MinValue); + WriteString(tableName); + WriteUInt16(ushort.MinValue); + WriteUInt16(ushort.MinValue); + WriteUInt32(uint.MinValue); + } + + public void WriteEndTable() + { + if (status == Status.FieldCollection) + { + WriteEndFieldCollection(); + } + + if (status == Status.Row) + { + WriteEndRow(); + } + + status = Status.End; + + destination.Seek(headerPosition + header.RowsPosition + (header.RowLength * header.NumberOfRows), SeekOrigin.Begin); + + stringPool.Write(destination); + header.StringPoolPosition = (uint)stringPool.Position - headerPosition; + + while ((destination.Position % vldPool.Align) != 0) + { + destination.WriteByte(0); + } + + vldPool.Write(destination); + header.DataPoolPosition = (uint)vldPool.Position - headerPosition; + + while ((destination.Position % vldPool.Align) != 0) + { + destination.WriteByte(0); + } + + header.Length = (uint)destination.Position - headerPosition; + + header.FirstBoolean = false; + header.SecondBoolean = false; + + destination.Position = headerPosition + 4; + WriteUInt32(header.Length - 8); + WriteBoolean(header.FirstBoolean); + WriteBoolean(header.SecondBoolean); + WriteUInt16((ushort)(header.RowsPosition - 8)); + WriteUInt32(header.StringPoolPosition - 8); + WriteUInt32(header.DataPoolPosition - 8); + destination.Seek(4, SeekOrigin.Current); + WriteUInt16(header.NumberOfFields); + WriteUInt16(header.RowLength); + WriteUInt32(header.NumberOfRows); + destination.Seek(0, SeekOrigin.End); + } + + public void WriteStartFieldCollection() + { + if (status != Status.Start) + { + throw new InvalidOperationException("Attempted to start field collection when the status wasn't Start"); + } + + status = Status.FieldCollection; + } + + public void WriteField(string fieldName, Type fieldType, object defaultValue) + { + if (status != Status.FieldCollection) + { + WriteStartFieldCollection(); + } + + CriFieldFlag fieldFlag = (CriFieldFlag)Array.IndexOf(CriField.FieldTypes, fieldType); + + if (!string.IsNullOrEmpty(fieldName)) + { + fieldFlag |= CriFieldFlag.Name; + } + + if (defaultValue != null) + { + fieldFlag |= CriFieldFlag.DefaultValue; + } + + CriTableField field = new CriTableField + { + Flag = fieldFlag, + Name = fieldName, + Value = defaultValue + }; + + WriteByte((byte)field.Flag); + + if (!string.IsNullOrEmpty(fieldName)) + { + WriteString(field.Name); + } + + if (defaultValue != null) + { + WriteValue(defaultValue); + } + + fields.Add(fieldName, field); + header.NumberOfFields++; + } + + public void WriteField(string fieldName, Type fieldType) + { + if (status != Status.FieldCollection) + { + WriteStartFieldCollection(); + } + + CriFieldFlag fieldFlag = (CriFieldFlag)Array.IndexOf(CriField.FieldTypes, fieldType) | CriFieldFlag.RowStorage; + + if (!string.IsNullOrEmpty(fieldName)) + { + fieldFlag |= CriFieldFlag.Name; + } + + CriTableField field = new CriTableField + { + Flag = fieldFlag, + Name = fieldName + }; + + WriteByte((byte)field.Flag); + + if (!string.IsNullOrEmpty(fieldName)) + { + WriteString(field.Name); + } + + fields.Add(fieldName, field); + header.NumberOfFields++; + } + + public void WriteField(CriField criField) + { + WriteField(criField.FieldName, criField.FieldType); + } + + public void WriteEndFieldCollection() + { + if (status != Status.FieldCollection) + { + throw new InvalidOperationException("Attempted to end field collection when the status wasn't FieldCollection"); + } + + status = Status.Idle; + + header.RowsPosition = (ushort)(destination.Position - headerPosition); + header.RowLength = CalculateRowLength(); + } + + public void WriteStartRow() + { + if (status == Status.FieldCollection) + { + WriteEndFieldCollection(); + } + + if (status != Status.Idle) + { + throw new InvalidOperationException("Attempted to start row when the status wasn't Idle"); + } + + status = Status.Row; + + header.NumberOfRows++; + + destination.Position = headerPosition + header.RowsPosition + (header.NumberOfRows * header.RowLength); + byte[] buffer = new byte[header.RowLength]; + destination.Write(buffer, 0, buffer.Length); + } + + public void WriteValue(int fieldIndex, object rowValue) + { + if (!fields[fieldIndex].Flag.HasFlag(CriFieldFlag.RowStorage) || rowValue == null) + { + return; + } + + GoToValue(fieldIndex); + WriteValue(rowValue); + } + + public void WriteValue(string fieldName, object rowValue) + { + WriteValue(fields.IndexOf(fieldName)); + } + + private void GoToValue(int fieldIndex) + { + long position = headerPosition + header.RowsPosition + (header.RowLength * (header.NumberOfRows - 1)); + + for (int i = 0; i < fieldIndex; i++) + { + if (!fields[i].Flag.HasFlag(CriFieldFlag.RowStorage)) + { + continue; + } + + switch (fields[i].Flag & CriFieldFlag.TypeMask) + { + case CriFieldFlag.Byte: + case CriFieldFlag.SByte: + position += 1; + break; + case CriFieldFlag.Int16: + case CriFieldFlag.UInt16: + position += 2; + break; + case CriFieldFlag.Int32: + case CriFieldFlag.UInt32: + case CriFieldFlag.Float: + case CriFieldFlag.String: + position += 4; + break; + case CriFieldFlag.Int64: + case CriFieldFlag.UInt64: + case CriFieldFlag.Double: + case CriFieldFlag.Data: + position += 8; + break; + } + } + + destination.Position = position; + } + + private ushort CalculateRowLength() + { + ushort length = 0; + + for (int i = 0; i < fields.Count; i++) + { + if (!fields[i].Flag.HasFlag(CriFieldFlag.RowStorage)) + { + continue; + } + + switch (fields[i].Flag & CriFieldFlag.TypeMask) + { + case CriFieldFlag.Byte: + case CriFieldFlag.SByte: + length += 1; + break; + case CriFieldFlag.Int16: + case CriFieldFlag.UInt16: + length += 2; + break; + case CriFieldFlag.Int32: + case CriFieldFlag.UInt32: + case CriFieldFlag.Float: + case CriFieldFlag.String: + length += 4; + break; + case CriFieldFlag.Int64: + case CriFieldFlag.UInt64: + case CriFieldFlag.Double: + case CriFieldFlag.Data: + length += 8; + break; + } + } + + return length; + } + + public void WriteEndRow() + { + if (status != Status.Row) + { + throw new InvalidOperationException("Attempted to end row when the status wasn't Row"); + } + + status = Status.Idle; + } + + public void WriteRow(bool close, params object[] rowValues) + { + WriteStartRow(); + + for (int i = 0; i < Math.Min(rowValues.Length, fields.Count); i++) + { + WriteValue(i, rowValues[i]); + } + + if (close) + { + WriteEndRow(); + } + } + + private void WriteByte(byte value) + { + EndianStream.WriteByte(destination, value); + } + + private void WriteBoolean(bool value) + { + EndianStream.WriteBoolean(destination, value); + } + + private void WriteSByte(sbyte value) + { + EndianStream.WriteSByte(destination, value); + } + + private void WriteUInt16(ushort value) + { + EndianStream.WriteUInt16BE(destination, value); + } + + private void WriteInt16(short value) + { + EndianStream.WriteInt16BE(destination, value); + } + + private void WriteUInt32(uint value) + { + EndianStream.WriteUInt32BE(destination, value); + } + + private void WriteInt32(int value) + { + EndianStream.WriteInt32BE(destination, value); + } + + private void WriteUInt64(ulong value) + { + EndianStream.WriteUInt64BE(destination, value); + } + + private void WriteInt64(long value) + { + EndianStream.WriteInt64BE(destination, value); + } + + private void WriteFloat(float value) + { + EndianStream.WriteFloatBE(destination, value); + } + + private void WriteDouble(double value) + { + EndianStream.WriteDoubleBE(destination, value); + } + + private void WriteString(string value) + { + if (settings.RemoveDuplicateStrings && stringPool.ContainsString(value)) + { + WriteUInt32((uint)stringPool.GetStringPosition(value)); + } + + else + { + WriteUInt32((uint)stringPool.Put(value)); + } + } + + private void WriteData(byte[] data) + { + WriteUInt32((uint)vldPool.Put(data)); + WriteUInt32((uint)data.Length); + } + + private void WriteStream(Stream stream) + { + WriteUInt32((uint)vldPool.Put(stream)); + WriteUInt32((uint)stream.Length); + } + + private void WriteFile(FileInfo fileInfo) + { + WriteUInt32((uint)vldPool.Put(fileInfo)); + WriteUInt32((uint)fileInfo.Length); + } + + private void WriteModule(ModuleBase module) + { + WriteUInt32((uint)vldPool.Put(module)); + WriteUInt32((uint)module.CalculateLength()); + } + + private void WriteGuid(Guid guid) + { + byte[] buffer = guid.ToByteArray(); + destination.Write(buffer, 0, buffer.Length); + } + + private void WriteValue(object value) + { + if (value == null) + { + return; + } + + if (value is byte) + { + WriteByte((byte)value); + } + + else if (value is sbyte) + { + WriteSByte((sbyte)value); + } + + else if (value is ushort) + { + WriteUInt16((ushort)value); + } + + else if (value is short) + { + WriteInt16((short)value); + } + + else if (value is uint) + { + WriteUInt32((uint)value); + } + + else if (value is int) + { + WriteInt32((int)value); + } + + else if (value is ulong) + { + WriteUInt64((ulong)value); + } + + else if (value is long) + { + WriteInt64((long)value); + } + + else if (value is float) + { + WriteFloat((float)value); + } + + else if (value is double) + { + WriteDouble((double)value); + } + + else if (value is string) + { + WriteString((string)value); + } + + else if (value is byte[]) + { + WriteData((byte[])value); + } + + else if (value is Stream) + { + WriteStream((Stream)value); + } + + else if (value is FileInfo) + { + WriteFile((FileInfo)value); + } + + else if (value is ModuleBase) + { + WriteModule((ModuleBase)value); + } + + else if (value is Guid) + { + WriteGuid((Guid)value); + } + } + + public void Dispose() + { + fields.Clear(); + stringPool.Clear(); + vldPool.Clear(); + + if (!settings.LeaveOpen) + { + destination.Close(); + } + } + + public static CriTableWriter Create(string destinationFileName) + { + return Create(destinationFileName, new CriTableWriterSettings()); + } + + public static CriTableWriter Create(string destinationFileName, CriTableWriterSettings settings) + { + Stream destination = File.Create(destinationFileName); + return new CriTableWriter(destination, settings); + } + + public static CriTableWriter Create(Stream destination) + { + return new CriTableWriter(destination, new CriTableWriterSettings()); + } + + public static CriTableWriter Create(Stream destination, CriTableWriterSettings settings) + { + return new CriTableWriter(destination, settings); + } + + private CriTableWriter(Stream destination, CriTableWriterSettings settings) + { + this.destination = destination; + this.settings = settings; + + header = new CriTableHeader(); + fields = new OrderedDictionary(); + stringPool = new StringPool(settings.EncodingType); + vldPool = new VldPool(settings.Align); + } + } + + public class CriTableWriterSettings + { + private uint align = 1; + private bool putBlankString = true; + private bool leaveOpen = false; + private Encoding encodingType = Encoding.GetEncoding("shift-jis"); + private bool removeDuplicateStrings = true; + + public uint Align + { + get + { + return align; + } + + set + { + if (value <= 0) + { + value = 1; + } + + align = value; + } + } + + public bool PutBlankString + { + get + { + return putBlankString; + } + + set + { + putBlankString = value; + } + } + + public bool LeaveOpen + { + get + { + return leaveOpen; + } + + set + { + leaveOpen = true; + } + } + + public Encoding EncodingType + { + get + { + return encodingType; + } + + set + { + encodingType = value; + } + } + + public bool RemoveDuplicateStrings + { + get + { + return removeDuplicateStrings; + } + + set + { + removeDuplicateStrings = value; + } + } + + public static CriTableWriterSettings AdxSettings + { + get + { + return new CriTableWriterSettings() + { + Align = 4, + PutBlankString = true, + RemoveDuplicateStrings = true, + }; + } + } + + public static CriTableWriterSettings Adx2Settings + { + get + { + return new CriTableWriterSettings() + { + Align = 32, + PutBlankString = false, + RemoveDuplicateStrings = false, + }; + } + } + } +} diff --git a/Source/SonicAudioLib/CriMw/Serialization/CriFieldAttribute.cs b/Source/SonicAudioLib/CriMw/Serialization/CriFieldAttribute.cs new file mode 100644 index 0000000..f6c1747 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/Serialization/CriFieldAttribute.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SonicAudioLib.CriMw.Serialization +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class CriFieldAttribute : Attribute + { + private string fieldName; + private ushort order = ushort.MaxValue; + + public string FieldName + { + get + { + return fieldName; + } + } + + public ushort Order + { + get + { + return order; + } + } + + public CriFieldAttribute(ushort order) + { + this.order = order; + } + + public CriFieldAttribute(string fieldName) + { + this.fieldName = fieldName; + } + + public CriFieldAttribute(string fieldName, ushort order) + { + this.fieldName = fieldName; + this.order = order; + } + + public CriFieldAttribute() { } + } +} diff --git a/Source/SonicAudioLib/CriMw/Serialization/CriIgnoreAttribute.cs b/Source/SonicAudioLib/CriMw/Serialization/CriIgnoreAttribute.cs new file mode 100644 index 0000000..3429104 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/Serialization/CriIgnoreAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SonicAudioLib.CriMw.Serialization +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class CriIgnoreAttribute : Attribute + { + } +} diff --git a/Source/SonicAudioLib/CriMw/Serialization/CriSerializableAttribute.cs b/Source/SonicAudioLib/CriMw/Serialization/CriSerializableAttribute.cs new file mode 100644 index 0000000..31f62a0 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/Serialization/CriSerializableAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SonicAudioLib.CriMw.Serialization +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)] + public class CriSerializableAttribute : Attribute + { + private string tableName; + + public string TableName + { + get + { + return tableName; + } + } + + public CriSerializableAttribute(string tableName) + { + this.tableName = tableName; + } + + public CriSerializableAttribute() { } + } +} diff --git a/Source/SonicAudioLib/CriMw/Serialization/CriTableSerializer.cs b/Source/SonicAudioLib/CriMw/Serialization/CriTableSerializer.cs new file mode 100644 index 0000000..79122b7 --- /dev/null +++ b/Source/SonicAudioLib/CriMw/Serialization/CriTableSerializer.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.ComponentModel; +using System.Reflection; +using System.Diagnostics; + +using SonicAudioLib.IO; + +namespace SonicAudioLib.CriMw.Serialization +{ + public static class CriTableSerializer + { + public static void Serialize(Stream destination, Type type, ICollection objects, CriTableWriterSettings settings) + { + ArrayList arrayList = null; + + if (objects != null) + { + arrayList = new ArrayList(objects); + } + + CriTableWriter tableWriter = CriTableWriter.Create(destination, settings); + + string tableName = type.Name; + CriSerializableAttribute serAttribute = type.GetCustomAttribute(); + if (serAttribute != null && !string.IsNullOrEmpty(serAttribute.TableName)) + { + tableName = serAttribute.TableName; + } + + tableWriter.WriteStartTable(tableName); + + SortedList sortedProperties = new SortedList(); + + foreach (PropertyInfo propertyInfo in type.GetProperties()) + { + // Add the properties in order + CriIgnoreAttribute ignoreAttribute = propertyInfo.GetCustomAttribute(); + if (ignoreAttribute != null) + { + continue; + } + + // Also ignore the properties that are not supportable (except FileInfo, Stream and ICollection<>) + if (propertyInfo.PropertyType != typeof(FileInfo) && + propertyInfo.PropertyType != typeof(Stream) && + !CriField.FieldTypes.Contains(propertyInfo.PropertyType)) + { + continue; + } + + CriFieldAttribute fieldAttribute = propertyInfo.GetCustomAttribute(); + + int order = ushort.MaxValue; + if (fieldAttribute != null) + { + order = fieldAttribute.Order; + } + + while (sortedProperties.ContainsKey(order)) + { + order++; + } + + sortedProperties.Add(order, propertyInfo); + } + + tableWriter.WriteStartFieldCollection(); + foreach (var keyValuePair in sortedProperties) + { + PropertyInfo propertyInfo = keyValuePair.Value; + CriFieldAttribute fieldAttribute = propertyInfo.GetCustomAttribute(); + + string fieldName = propertyInfo.Name; + Type fieldType = propertyInfo.PropertyType; + object defaultValue = null; + + // Since the invalid types were cleaned, we can assume that those can be FileInfo or Stream + // so directly change the type to byte[] + if (!CriField.FieldTypes.Contains(fieldType)) + { + fieldType = typeof(byte[]); + } + + if (fieldAttribute != null) + { + if (!string.IsNullOrEmpty(fieldAttribute.FieldName)) + { + fieldName = fieldAttribute.FieldName; + } + } + + // Checks if all the rows have the same value for this field + bool useDefaultValue = true; + + if (arrayList != null && arrayList.Count > 0) + { + useDefaultValue = true; + + foreach (object obj in arrayList) + { + if (propertyInfo.GetValue(obj) != propertyInfo.GetValue(arrayList[0])) + { + useDefaultValue = false; + break; + } + } + + if (useDefaultValue) + { + defaultValue = propertyInfo.GetValue(arrayList[0]); + } + } + + if (useDefaultValue) + { + tableWriter.WriteField(fieldName, fieldType, defaultValue); + } + + else + { + tableWriter.WriteField(fieldName, fieldType); + } + } + + tableWriter.WriteEndFieldCollection(); + + // Time for objects. + if (arrayList != null) + { + foreach (object obj in arrayList) + { + tableWriter.WriteStartRow(); + + int index = 0; + foreach (PropertyInfo propertyInfo in sortedProperties.Values) + { + object value = propertyInfo.GetValue(obj); + + Type propertyType = propertyInfo.PropertyType; + + tableWriter.WriteValue(index, value); + index++; + } + + tableWriter.WriteEndRow(); + } + } + + tableWriter.WriteEndTable(); + tableWriter.Dispose(); + } + + public static ArrayList Deserialize(Stream source, Type type) + { + ArrayList arrayList = new ArrayList(); + + using (CriTableReader tableReader = CriTableReader.Create(source, true)) + { + PropertyInfo[] propertyInfos = type.GetProperties(); + + while (tableReader.Read()) + { + object obj = Activator.CreateInstance(type); + + for (int i = 0; i < tableReader.NumberOfFields; i++) + { + string fieldName = tableReader.GetFieldName(i); + + foreach (PropertyInfo propertyInfo in propertyInfos) + { + string fieldNameMatch = propertyInfo.Name; + + CriFieldAttribute fieldAttribute = propertyInfo.GetCustomAttribute(); + + if (fieldAttribute != null && !string.IsNullOrEmpty(fieldAttribute.FieldName)) + { + fieldNameMatch = fieldAttribute.FieldName; + } + + if (fieldName == fieldNameMatch) + { + object value = tableReader.GetValue(i); + propertyInfo.SetValue(obj, value); + break; + } + } + } + + arrayList.Add(obj); + } + } + + return arrayList; + } + } +} diff --git a/Source/SonicAudioLib/IO/EndianStream.cs b/Source/SonicAudioLib/IO/EndianStream.cs new file mode 100644 index 0000000..985ed16 --- /dev/null +++ b/Source/SonicAudioLib/IO/EndianStream.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SonicAudioLib.IO +{ + /// + /// Represents a static class for reading various data types in any endian format from a . + /// + public static class EndianStream + { + private static byte[] buffer; + + /// + /// Fills in the given length. + /// + private static void FillBuffer(Stream source, int length) + { + buffer = new byte[length]; + source.Read(buffer, 0, length); + } + + public static void CopyTo(Stream source, Stream destination) + { + CopyTo(source, destination, 4096); + } + + public static void CopyTo(Stream source, Stream destination, int bufferSize) + { + int read; + byte[] buffer = new byte[bufferSize]; + + while ((read = source.Read(buffer, 0, buffer.Length)) != 0) + { + destination.Write(buffer, 0, read); + } + } + + public static byte[] ReadBytes(Stream source, int length) + { + FillBuffer(source, length); + return buffer; + } + + public static void WriteBytes(Stream destination, byte[] value) + { + destination.Write(value, 0, value.Length); + } + + public static void WriteBytes(Stream destination, byte[] value, int length) + { + destination.Write(value, 0, length); + } + + public static byte ReadByte(Stream source) + { + int value = source.ReadByte(); + if (value == -1) + { + throw new EndOfStreamException(); + } + + return (byte)value; + } + + public static byte ReadByteAt(Stream source, long position) + { + long oldPosition = source.Position; + source.Position = position; + + byte value = ReadByte(source); + source.Position = oldPosition; + + return value; + } + + public static void WriteByte(Stream destination, byte value) + { + destination.WriteByte(value); + } + + public static void WriteByteAt(Stream destination, byte value, long position) + { + long oldPosition = destination.Position; + destination.Position = position; + + WriteByte(destination, value); + destination.Position = oldPosition; + } + + public static bool ReadBoolean(Stream source) + { + return ReadByte(source) > 0; + } + + public static void WriteBoolean(Stream destination, bool value) + { + WriteByte(destination, (byte)(value == true ? 1 : 0)); + } + + public static sbyte ReadSByte(Stream source) + { + return (sbyte)ReadByte(source); + } + + public static void WriteSByte(Stream source, sbyte value) + { + WriteByte(source, (byte)value); + } + + public static ushort ReadUInt16(Stream source) + { + FillBuffer(source, 2); + return BitConverter.ToUInt16(buffer, 0); + } + + public static ushort ReadUInt16BE(Stream source) + { + FillBuffer(source, 2); + + Array.Reverse(buffer); + return BitConverter.ToUInt16(buffer, 0); + } + + public static void WriteUInt16(Stream destination, ushort value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 2); + } + + public static void WriteUInt16BE(Stream destination, ushort value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 2); + } + + public static short ReadInt16(Stream source) + { + FillBuffer(source, 2); + return BitConverter.ToInt16(buffer, 0); + } + + public static short ReadInt16BE(Stream source) + { + FillBuffer(source, 2); + + Array.Reverse(buffer); + return BitConverter.ToInt16(buffer, 0); + } + + public static void WriteInt16(Stream destination, short value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 2); + } + + public static void WriteInt16BE(Stream destination, short value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 2); + } + + public static uint ReadUInt32(Stream source) + { + FillBuffer(source, 4); + return BitConverter.ToUInt32(buffer, 0); + } + + public static uint ReadUInt32BE(Stream source) + { + FillBuffer(source, 4); + + Array.Reverse(buffer); + return BitConverter.ToUInt32(buffer, 0); + } + + public static void WriteUInt32(Stream destination, uint value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 4); + } + + public static void WriteUInt32BE(Stream destination, uint value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 4); + } + + public static int ReadInt32(Stream source) + { + FillBuffer(source, 4); + return BitConverter.ToInt32(buffer, 0); + } + + public static int ReadInt32BE(Stream source) + { + FillBuffer(source, 4); + + Array.Reverse(buffer); + return BitConverter.ToInt32(buffer, 0); + } + + public static void WriteInt32(Stream destination, int value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 4); + } + + public static void WriteInt32BE(Stream destination, int value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 4); + } + + public static ulong ReadUInt64(Stream source) + { + FillBuffer(source, 8); + return BitConverter.ToUInt64(buffer, 0); + } + + public static ulong ReadUInt64BE(Stream source) + { + FillBuffer(source, 8); + + Array.Reverse(buffer); + return BitConverter.ToUInt64(buffer, 0); + } + + public static void WriteUInt64(Stream destination, ulong value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 8); + } + + public static void WriteUInt64BE(Stream destination, ulong value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 8); + } + + public static long ReadInt64(Stream source) + { + FillBuffer(source, 8); + return BitConverter.ToInt64(buffer, 0); + } + + public static long ReadInt64BE(Stream source) + { + FillBuffer(source, 8); + + Array.Reverse(buffer); + return BitConverter.ToInt64(buffer, 0); + } + + public static void WriteInt64(Stream destination, long value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 8); + } + + public static void WriteInt64BE(Stream destination, long value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 8); + } + + public static float ReadFloat(Stream source) + { + FillBuffer(source, 4); + return BitConverter.ToSingle(buffer, 0); + } + + public static float ReadFloatBE(Stream source) + { + FillBuffer(source, 4); + + Array.Reverse(buffer); + return BitConverter.ToSingle(buffer, 0); + } + + public static void WriteFloat(Stream destination, float value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 4); + } + + public static void WriteFloatBE(Stream destination, float value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 4); + } + + public static double ReadDouble(Stream source) + { + FillBuffer(source, 8); + return BitConverter.ToDouble(buffer, 0); + } + + public static double ReadDoubleBE(Stream source) + { + FillBuffer(source, 8); + + Array.Reverse(buffer); + return BitConverter.ToDouble(buffer, 0); + } + + public static void WriteDouble(Stream destination, double value) + { + buffer = BitConverter.GetBytes(value); + destination.Write(buffer, 0, 8); + } + + public static void WriteDoubleBE(Stream destination, double value) + { + buffer = BitConverter.GetBytes(value); + + Array.Reverse(buffer); + destination.Write(buffer, 0, 8); + } + + public static string ReadCString(Stream source) + { + return ReadCString(source, Encoding.ASCII); + } + + public static string ReadCString(Stream source, Encoding encoding) + { + var list = new List(); + + byte buff; + while ((buff = ReadByte(source)) != 0) + { + list.Add(buff); + } + + return encoding.GetString(list.ToArray()); + } + + public static void WriteCString(Stream destination, string value) + { + WriteCString(destination, value, Encoding.ASCII); + } + + public static void WriteCString(Stream destination, string value, Encoding encoding) + { + var buff = encoding.GetBytes(value); + + foreach (byte _buff in buff) + { + WriteByte(destination, _buff); + } + + WriteByte(destination, 0); + } + + public static string ReadCString(Stream source, int length) + { + return ReadCString(source, length, Encoding.ASCII); + } + + public static string ReadCString(Stream source, int length, Encoding encoding) + { + byte[] buffer = new byte[length]; + source.Read(buffer, 0, length); + + return encoding.GetString(buffer); + } + + public static void WriteCString(Stream destination, string value, int length) + { + WriteCString(destination, value, length, Encoding.ASCII); + } + + public static void WriteCString(Stream destination, string value, int length, Encoding encoding) + { + byte[] buffer = encoding.GetBytes(value.ToCharArray(), 0, length); + destination.Write(buffer, 0, length); + } + } +} diff --git a/Source/SonicAudioLib/IO/StringPool.cs b/Source/SonicAudioLib/IO/StringPool.cs new file mode 100644 index 0000000..3d89019 --- /dev/null +++ b/Source/SonicAudioLib/IO/StringPool.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Linq; + +namespace SonicAudioLib.IO +{ + public class StringPool + { + private List items = new List(); + + private long startPosition = 0; + private long length = 0; + private Encoding encoding = Encoding.Default; + + public static readonly string AdxBlankString = ""; + + public long Position + { + get + { + return startPosition; + } + } + + public long Length + { + get + { + return length; + } + } + + public long Put(string value) + { + if (string.IsNullOrEmpty(value)) + { + return 0; + } + + long position = length; + items.Add(new StringItem() { Value = value, Position = position }); + + length += value.Length + 1; + return position; + } + + public void Write(Stream destination) + { + startPosition = (uint)destination.Position; + + foreach (StringItem item in items) + { + EndianStream.WriteCString(destination, item.Value, encoding); + } + } + + public bool ContainsString(string value) + { + return items.Any(item => item.Value == value); + } + + public long GetStringPosition(string value) + { + return items.First(item => item.Value == value).Position; + } + + public void Clear() + { + items.Clear(); + } + + public StringPool(Encoding encoding) + { + this.encoding = encoding; + } + + public StringPool() + { + } + + private class StringItem + { + public string Value { get; set; } + public long Position { get; set; } + } + } +} diff --git a/Source/SonicAudioLib/IO/Substream.cs b/Source/SonicAudioLib/IO/Substream.cs new file mode 100644 index 0000000..0ff8c92 --- /dev/null +++ b/Source/SonicAudioLib/IO/Substream.cs @@ -0,0 +1,199 @@ +using System; +using System.IO; + +namespace SonicAudioLib.IO +{ + /// + /// Represents a based substream for viewing a portion of a Stream. + /// + public class Substream : Stream + { + private Stream baseStream; + private long streamPosition; + private long streamLength; + + /// + /// Determines whether the base Stream supports reading. + /// + public override bool CanRead + { + get + { + return baseStream.CanRead; + } + } + + /// + /// Determines whether the base Stream supports seeking. + /// + public override bool CanSeek + { + get + { + return baseStream.CanSeek; + } + } + + /// + /// Always returns false. + /// + public override bool CanWrite + { + get + { + return false; + } + } + + /// + /// Gets the length of the substream. + /// + public override long Length + { + get + { + return streamLength; + } + } + + /// + /// Gets or sets the position of the substream. + /// + public override long Position + { + get + { + return baseStream.Position - streamPosition; + } + + set + { + baseStream.Position = value + streamPosition; + } + } + + /// + /// Gets or sets the position of the base Stream. + /// + public long AbsolutePosition + { + get + { + return baseStream.Position; + } + + set + { + baseStream.Position = value; + } + } + + /// + /// Closes the substream. + /// + public override void Close() + { + base.Close(); + } + + /// + /// Does nothing. + /// + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (baseStream.Position >= streamPosition + streamLength) + { + count = 0; + } + else if (baseStream.Position + count > streamPosition + streamLength) + { + count = (int)(streamPosition + streamLength - baseStream.Position); + } + + return baseStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + offset += streamPosition; + } + + else if (origin == SeekOrigin.End) + { + offset = streamPosition + streamLength - offset; + origin = SeekOrigin.Begin; + } + + return baseStream.Seek(offset, origin); + } + + /// + /// Seeks to the start of the substream. + /// + public void SeekToStart() + { + baseStream.Position = streamPosition; + } + + /// + /// Throws . + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Throws . + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// Throws . + /// + public override void WriteByte(byte value) + { + throw new NotSupportedException(); + } + + /// + /// Gets an array of the data that the substream covers. + /// + public byte[] ToArray() + { + using (MemoryStream memoryStream = new MemoryStream()) + { + CopyTo(memoryStream); + return memoryStream.ToArray(); + } + } + + /// + /// Creates a substream by the specified base Stream at the specified offset. + /// + public Substream(Stream baseStream, long streamPosition) : this(baseStream, streamPosition, baseStream.Length - streamPosition) + { + } + + /// + /// Creates a substream by the specified base Stream at the specified offset and with the specified length. + /// + public Substream(Stream baseStream, long streamPosition, long streamLength) + { + this.baseStream = baseStream; + this.streamPosition = streamPosition; + this.streamLength = streamLength; + + SeekToStart(); + } + } +} diff --git a/Source/SonicAudioLib/IO/VldPool.cs b/Source/SonicAudioLib/IO/VldPool.cs new file mode 100644 index 0000000..33fdd1c --- /dev/null +++ b/Source/SonicAudioLib/IO/VldPool.cs @@ -0,0 +1,172 @@ +using System.Collections; +using System.IO; + +using SonicAudioLib.Module; + +namespace SonicAudioLib.IO +{ + public class VldPool + { + private ArrayList items = new ArrayList(); + + private long startPosition = 0; + private uint align = 1; + private long length = 0; + + public long Position + { + get + { + return startPosition; + } + } + + public long Length + { + get + { + return length; + } + } + + public long Align + { + get + { + return align; + } + } + + public long Put(byte[] data) + { + if (data == null || data.Length <= 0) + { + return 0; + } + + while ((length % align) != 0) + { + length++; + } + + long position = length; + length += data.Length; + items.Add(data); + + return position; + } + + public long Put(Stream stream) + { + if (stream == null || stream.Length <= 0) + { + return 0; + } + + while ((length % align) != 0) + { + length++; + } + + long position = length; + length += stream.Length; + items.Add(stream); + + return position; + } + + public long Put(FileInfo fileInfo) + { + if (fileInfo == null || fileInfo.Length <= 0) + { + return 0; + } + + while ((length % align) != 0) + { + length++; + } + + long position = length; + length += fileInfo.Length; + items.Add(fileInfo); + + return position; + } + + public long Put(ModuleBase module) + { + if (module == null) + { + return 0; + } + + while ((length % align) != 0) + { + length++; + } + + long position = length; + length += module.CalculateLength(); + items.Add(module); + + return position; + } + + public void Write(Stream destination) + { + startPosition = destination.Position; + + foreach (object item in items) + { + while ((destination.Position % align) != 0) + { + destination.WriteByte(0); + } + + if (item is byte[]) + { + byte[] output = (byte[])item; + destination.Write(output, 0, output.Length); + } + + else if (item is Stream) + { + Stream output = (Stream)item; + output.Seek(0, SeekOrigin.Begin); + + output.CopyTo(destination); + } + + else if (item is FileInfo) + { + FileInfo fileInfo = (FileInfo)item; + + Stream output = fileInfo.OpenRead(); + output.CopyTo(destination); + output.Close(); + } + + else if (item is ModuleBase) + { + ModuleBase module = (ModuleBase)item; + module.Write(destination); + } + } + } + + public void Clear() + { + items.Clear(); + } + + public VldPool(uint align) + { + this.align = align; + } + + public VldPool() + { + } + } +} diff --git a/Source/SonicAudioLib/Module/ModuleBase.cs b/Source/SonicAudioLib/Module/ModuleBase.cs new file mode 100644 index 0000000..7029627 --- /dev/null +++ b/Source/SonicAudioLib/Module/ModuleBase.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace SonicAudioLib.Module +{ + public abstract class ModuleBase + { + public abstract void Read(Stream source); + public abstract void Write(Stream destination); + + public virtual void Load(string sourceFileName) + { + using (Stream source = File.OpenRead(sourceFileName)) + { + Read(source); + } + } + + public virtual void Load(byte[] sourceByteArray) + { + using (Stream source = new MemoryStream(sourceByteArray)) + { + Read(source); + } + } + + public virtual void Save(string destinationFileName) + { + using (Stream destination = File.Create(destinationFileName)) + { + Write(destination); + } + } + + public virtual byte[] Save() + { + using (MemoryStream destination = new MemoryStream()) + { + Write(destination); + return destination.ToArray(); + } + } + + public virtual long CalculateLength() + { + return -1; + } + } +} diff --git a/Source/SonicAudioLib/Properties/AssemblyInfo.cs b/Source/SonicAudioLib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..efb864a --- /dev/null +++ b/Source/SonicAudioLib/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Sonic Audio Library")] +[assembly: AssemblyDescription("Class library for various audio formats for Sonic the Hedgehog games.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SonicAudioLib")] +[assembly: AssemblyCopyright("Copyright © blueskythlikesclouds 2014-2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("63138773-1f47-474c-9345-15eb6183ecc6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/SonicAudioLib/SonicAudioLib.csproj b/Source/SonicAudioLib/SonicAudioLib.csproj new file mode 100644 index 0000000..cd2b557 --- /dev/null +++ b/Source/SonicAudioLib/SonicAudioLib.csproj @@ -0,0 +1,76 @@ + + + + + Debug + AnyCPU + {63138773-1F47-474C-9345-15EB6183ECC6} + Library + Properties + SonicAudioLib + SonicAudioLib + v4.5.2 + 512 + + + true + full + false + ..\..\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\..\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file