├── .branding └── logo │ ├── ico │ ├── nitropacker_lightblue.ico │ ├── nitropacker_pink.ico │ └── nitropacker_red.ico │ ├── png │ ├── nitropacker_lightblue.png │ ├── nitropacker_pink.png │ └── nitropacker_red.png │ └── svg │ ├── nitropacker_lightblue.svg │ ├── nitropacker_pink.svg │ └── nitropacker_red.svg ├── .gitattributes ├── .gitignore ├── Directory.Build.props ├── HaroohieClub.NitroPacker.Cli ├── HaroohieClub.NitroPacker.Cli.csproj ├── MigrateProjectCommand.cs ├── PackCommand.cs ├── PatchArm9Command.cs ├── PatchOverlaysCommand.cs ├── Program.cs ├── UnpackCommand.cs └── Utilities.cs ├── HaroohieClub.NitroPacker.sln ├── HaroohieClub.NitroPacker ├── HaroohieClub.NitroPacker.csproj ├── IO │ ├── Archive │ │ ├── Archive.cs │ │ ├── DiskArchive.cs │ │ ├── IReadOnlyArchive.cs │ │ └── Tree │ │ │ ├── DirectoryNode.cs │ │ │ ├── FileNode.cs │ │ │ ├── FileTree.cs │ │ │ └── INode.cs │ ├── Compression │ │ ├── Blz.cs │ │ ├── CompressionWindow.cs │ │ ├── Lz77.cs │ │ └── Yaz0.cs │ ├── EndianBinaryReader.cs │ ├── EndianBinaryReaderEx.cs │ ├── EndianBinaryWriter.cs │ ├── EndianBinaryWriterEx.cs │ ├── Endianness.cs │ ├── IOUtil.cs │ ├── Pointer.cs │ ├── Serialization │ │ ├── AlignAttribute.cs │ │ ├── ArraySizeAttribute.cs │ │ ├── ConstantAttribute.cs │ │ ├── Fx16Attribute.cs │ │ ├── Fx32Attribute.cs │ │ ├── IgnoreAttribute.cs │ │ ├── PropertyAlignmentAttribute.cs │ │ ├── ReferenceAttribute.cs │ │ ├── SerializationUtil.cs │ │ └── TypeAttribute.cs │ └── SignatureNotCorrectException.cs ├── NdsProjectFile.cs ├── Nitro │ ├── BinaryBlockHeader.cs │ ├── BinaryFileHeader.cs │ ├── Card │ │ ├── AgeRatings.cs │ │ ├── Banners │ │ │ ├── Banner.cs │ │ │ ├── BannerV1.cs │ │ │ ├── BannerV103.cs │ │ │ ├── BannerV2.cs │ │ │ ├── BannerV3.cs │ │ │ └── Header.cs │ │ ├── Cryptography │ │ │ ├── Blowfish.cs │ │ │ ├── Crc16.cs │ │ │ ├── KeyTransform.cs │ │ │ └── Keys.cs │ │ ├── Headers │ │ │ ├── Cryptography.cs │ │ │ ├── RomHeader.cs │ │ │ └── TwlHeader.cs │ │ ├── NitroFooter.cs │ │ ├── Rom.cs │ │ ├── RomBanner.cs │ │ ├── RomFileNameTable.cs │ │ └── RomOverlayTable.cs │ ├── Fs │ │ ├── DirectoryTableEntry.cs │ │ ├── FatEntry.cs │ │ ├── NameEntryWithFatEntry.cs │ │ ├── NameTableEntry.cs │ │ └── NitroFsArchive.cs │ └── Gx │ │ ├── ColorFormat.cs │ │ ├── Enums.cs │ │ ├── GFXUtil.cs │ │ ├── GxUtil.cs │ │ ├── IconAnimationSequence.cs │ │ └── Rgba8Bitmap.cs ├── NitroFsArchiveExtensions.cs ├── Patcher │ ├── Nitro │ │ ├── ARM9.cs │ │ ├── ARM9AsmHack.cs │ │ └── CRT0.cs │ ├── Overlay │ │ ├── Overlay.cs │ │ └── OverlayAsmHack.cs │ └── Utility.cs └── README.md ├── LICENSE ├── MIGRATE.md ├── NuGet.config ├── README.md ├── asm_sample ├── .clang-format ├── Makefile ├── linker.x ├── overlays │ ├── Makefile │ ├── linker.x │ └── main_000C │ │ ├── replSource │ │ └── 20CB424 │ │ │ └── 20CB424.txt │ │ ├── source │ │ └── placeholder.txt │ │ └── symbols.x ├── replSource │ └── 2035A94 │ │ └── 2035A94.txt ├── source │ └── placeholder.txt └── symbols.x └── azure-pipelines-release.yml /.branding/logo/ico/nitropacker_lightblue.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/.branding/logo/ico/nitropacker_lightblue.ico -------------------------------------------------------------------------------- /.branding/logo/ico/nitropacker_pink.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/.branding/logo/ico/nitropacker_pink.ico -------------------------------------------------------------------------------- /.branding/logo/ico/nitropacker_red.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/.branding/logo/ico/nitropacker_red.ico -------------------------------------------------------------------------------- /.branding/logo/png/nitropacker_lightblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/.branding/logo/png/nitropacker_lightblue.png -------------------------------------------------------------------------------- /.branding/logo/png/nitropacker_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/.branding/logo/png/nitropacker_pink.png -------------------------------------------------------------------------------- /.branding/logo/png/nitropacker_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/.branding/logo/png/nitropacker_red.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | *launchSettings.json 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Mono auto generated files 18 | mono_crash.* 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | [Ww][Ii][Nn]32/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Oo]ut/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # ASP.NET Scaffolding 68 | ScaffoldingReadMe.txt 69 | 70 | # StyleCop 71 | StyleCopReport.xml 72 | 73 | # Files built by Visual Studio 74 | *_i.c 75 | *_p.c 76 | *_h.h 77 | *.ilk 78 | *.meta 79 | *.obj 80 | *.iobj 81 | *.pch 82 | *.pdb 83 | *.ipdb 84 | *.pgc 85 | *.pgd 86 | *.rsp 87 | *.sbr 88 | *.tlb 89 | *.tli 90 | *.tlh 91 | *.tmp 92 | *.tmp_proj 93 | *_wpftmp.csproj 94 | *.log 95 | *.vspscc 96 | *.vssscc 97 | .builds 98 | *.pidb 99 | *.svclog 100 | *.scc 101 | 102 | # Chutzpah Test files 103 | _Chutzpah* 104 | 105 | # Visual C++ cache files 106 | ipch/ 107 | *.aps 108 | *.ncb 109 | *.opendb 110 | *.opensdf 111 | *.sdf 112 | *.cachefile 113 | *.VC.db 114 | *.VC.VC.opendb 115 | 116 | # Visual Studio profiler 117 | *.psess 118 | *.vsp 119 | *.vspx 120 | *.sap 121 | 122 | # Visual Studio Trace Files 123 | *.e2e 124 | 125 | # TFS 2012 Local Workspace 126 | $tf/ 127 | 128 | # Guidance Automation Toolkit 129 | *.gpState 130 | 131 | # ReSharper is a .NET coding add-in 132 | _ReSharper*/ 133 | *.[Rr]e[Ss]harper 134 | *.DotSettings.user 135 | 136 | # TeamCity is a build add-in 137 | _TeamCity* 138 | 139 | # DotCover is a Code Coverage Tool 140 | *.dotCover 141 | 142 | # AxoCover is a Code Coverage Tool 143 | .axoCover/* 144 | !.axoCover/settings.json 145 | 146 | # Coverlet is a free, cross platform Code Coverage Tool 147 | coverage*.json 148 | coverage*.xml 149 | coverage*.info 150 | 151 | # Visual Studio code coverage results 152 | *.coverage 153 | *.coveragexml 154 | 155 | # NCrunch 156 | _NCrunch_* 157 | .*crunch*.local.xml 158 | nCrunchTemp_* 159 | 160 | # MightyMoose 161 | *.mm.* 162 | AutoTest.Net/ 163 | 164 | # Web workbench (sass) 165 | .sass-cache/ 166 | 167 | # Installshield output folder 168 | [Ee]xpress/ 169 | 170 | # DocProject is a documentation generator add-in 171 | DocProject/buildhelp/ 172 | DocProject/Help/*.HxT 173 | DocProject/Help/*.HxC 174 | DocProject/Help/*.hhc 175 | DocProject/Help/*.hhk 176 | DocProject/Help/*.hhp 177 | DocProject/Help/Html2 178 | DocProject/Help/html 179 | 180 | # Click-Once directory 181 | publish/ 182 | 183 | # Publish Web Output 184 | *.[Pp]ublish.xml 185 | *.azurePubxml 186 | # Note: Comment the next line if you want to checkin your web deploy settings, 187 | # but database connection strings (with potential passwords) will be unencrypted 188 | *.pubxml 189 | *.publishproj 190 | 191 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 192 | # checkin your Azure Web App publish settings, but sensitive information contained 193 | # in these scripts will be unencrypted 194 | PublishScripts/ 195 | 196 | # NuGet Packages 197 | *.nupkg 198 | # NuGet Symbol Packages 199 | *.snupkg 200 | # The packages folder can be ignored because of Package Restore 201 | **/[Pp]ackages/* 202 | # except build/, which is used as an MSBuild target. 203 | !**/[Pp]ackages/build/ 204 | # Uncomment if necessary however generally it will be regenerated when needed 205 | #!**/[Pp]ackages/repositories.config 206 | # NuGet v3's project.json files produces more ignorable files 207 | *.nuget.props 208 | *.nuget.targets 209 | 210 | # Microsoft Azure Build Output 211 | csx/ 212 | *.build.csdef 213 | 214 | # Microsoft Azure Emulator 215 | ecf/ 216 | rcf/ 217 | 218 | # Windows Store app package directories and files 219 | AppPackages/ 220 | BundleArtifacts/ 221 | Package.StoreAssociation.xml 222 | _pkginfo.txt 223 | *.appx 224 | *.appxbundle 225 | *.appxupload 226 | 227 | # Visual Studio cache files 228 | # files ending in .cache can be ignored 229 | *.[Cc]ache 230 | # but keep track of directories ending in .cache 231 | !?*.[Cc]ache/ 232 | 233 | # Others 234 | ClientBin/ 235 | ~$* 236 | *~ 237 | *.dbmdl 238 | *.dbproj.schemaview 239 | *.jfm 240 | *.pfx 241 | *.publishsettings 242 | orleans.codegen.cs 243 | 244 | # Including strong name files can present a security risk 245 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 246 | #*.snk 247 | 248 | # Since there are multiple workflows, uncomment next line to ignore bower_components 249 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 250 | #bower_components/ 251 | 252 | # RIA/Silverlight projects 253 | Generated_Code/ 254 | 255 | # Backup & report files from converting an old project file 256 | # to a newer Visual Studio version. Backup files are not needed, 257 | # because we have git ;-) 258 | _UpgradeReport_Files/ 259 | Backup*/ 260 | UpgradeLog*.XML 261 | UpgradeLog*.htm 262 | ServiceFabricBackup/ 263 | *.rptproj.bak 264 | 265 | # SQL Server files 266 | *.mdf 267 | *.ldf 268 | *.ndf 269 | 270 | # Business Intelligence projects 271 | *.rdl.data 272 | *.bim.layout 273 | *.bim_*.settings 274 | *.rptproj.rsuser 275 | *- [Bb]ackup.rdl 276 | *- [Bb]ackup ([0-9]).rdl 277 | *- [Bb]ackup ([0-9][0-9]).rdl 278 | 279 | # Microsoft Fakes 280 | FakesAssemblies/ 281 | 282 | # GhostDoc plugin setting file 283 | *.GhostDoc.xml 284 | 285 | # Node.js Tools for Visual Studio 286 | .ntvs_analysis.dat 287 | node_modules/ 288 | 289 | # Visual Studio 6 build log 290 | *.plg 291 | 292 | # Visual Studio 6 workspace options file 293 | *.opt 294 | 295 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 296 | *.vbw 297 | 298 | # Visual Studio LightSwitch build output 299 | **/*.HTMLClient/GeneratedArtifacts 300 | **/*.DesktopClient/GeneratedArtifacts 301 | **/*.DesktopClient/ModelManifest.xml 302 | **/*.Server/GeneratedArtifacts 303 | **/*.Server/ModelManifest.xml 304 | _Pvt_Extensions 305 | 306 | # Paket dependency manager 307 | .paket/paket.exe 308 | paket-files/ 309 | 310 | # FAKE - F# Make 311 | .fake/ 312 | 313 | # CodeRush personal settings 314 | .cr/personal 315 | 316 | # Python Tools for Visual Studio (PTVS) 317 | __pycache__/ 318 | *.pyc 319 | 320 | # Cake - Uncomment if you are using it 321 | # tools/** 322 | # !tools/packages.config 323 | 324 | # Tabs Studio 325 | *.tss 326 | 327 | # Telerik's JustMock configuration file 328 | *.jmconfig 329 | 330 | # BizTalk build output 331 | *.btp.cs 332 | *.btm.cs 333 | *.odx.cs 334 | *.xsd.cs 335 | 336 | # OpenCover UI analysis results 337 | OpenCover/ 338 | 339 | # Azure Stream Analytics local run output 340 | ASALocalRun/ 341 | 342 | # MSBuild Binary and Structured Log 343 | *.binlog 344 | 345 | # NVidia Nsight GPU debugger configuration file 346 | *.nvuser 347 | 348 | # MFractors (Xamarin productivity tool) working folder 349 | .mfractor/ 350 | 351 | # Local History for Visual Studio 352 | .localhistory/ 353 | 354 | # BeatPulse healthcheck temp database 355 | healthchecksdb 356 | 357 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 358 | MigrationBackup/ 359 | 360 | # Ionide (cross platform F# VS Code tools) working folder 361 | .ionide/ 362 | 363 | # Fody - auto-generated XML schema 364 | FodyWeavers.xsd 365 | 366 | # JetBrains 367 | .idea/ -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 3.1.7 6 | 7 | 8 | -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/HaroohieClub.NitroPacker.Cli.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | disable 7 | disable 8 | NitroPacker 9 | $(NPVersion) 10 | ..\.branding\logo\ico\nitropacker_red.ico 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/MigrateProjectCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using Mono.Options; 4 | 5 | namespace HaroohieClub.NitroPacker.Cli; 6 | 7 | public class MigrateProjectCommand : Command 8 | { 9 | private string _projectXml; 10 | 11 | public MigrateProjectCommand() : base("migrate", "Migrates a project from the 2.x format to the 3.x+ format") 12 | { 13 | Options = new() 14 | { 15 | { "i|p|input|project=", "The project XML to port to the new project format", p => _projectXml = p }, 16 | }; 17 | } 18 | 19 | public override int Invoke(IEnumerable arguments) 20 | { 21 | Options.Parse(arguments); 22 | if (string.IsNullOrEmpty(_projectXml) || !File.Exists(_projectXml)) 23 | { 24 | CommandSet.Error.WriteLine("You must provide an existing project XML file!"); 25 | return 1; 26 | } 27 | 28 | NdsProjectFile.ConvertProjectFile(_projectXml); 29 | 30 | return 0; 31 | } 32 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/PackCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mono.Options; 3 | 4 | namespace HaroohieClub.NitroPacker.Cli; 5 | 6 | public class PackCommand : Command 7 | { 8 | private string _projectJson, _outputRom; 9 | private bool _compressArm9 = false; 10 | public PackCommand() : base("pack", "Packs a ROM given a project JSON file") 11 | { 12 | Options = new() 13 | { 14 | { "p|i|project|input=", "Input project JSON file", p => _projectJson = p }, 15 | { "r|o|rom|output=", "Output ROM path", o => _outputRom = o }, 16 | { "c|compress-arm9", "Indicates ARM9 should be compressed", c => _compressArm9 = true }, 17 | }; 18 | } 19 | 20 | public override int Invoke(IEnumerable arguments) 21 | { 22 | Options.Parse(arguments); 23 | 24 | if (string.IsNullOrEmpty(_projectJson)) 25 | { 26 | CommandSet.Out.WriteLine($"Must provide path to project JSON file."); 27 | Options.WriteOptionDescriptions(CommandSet.Out); 28 | return 1; 29 | } 30 | if (string.IsNullOrEmpty(_outputRom)) 31 | { 32 | CommandSet.Out.WriteLine($"Must provide path to output ROM to."); 33 | Options.WriteOptionDescriptions(CommandSet.Out); 34 | return 1; 35 | } 36 | 37 | NdsProjectFile.Pack(_outputRom, _projectJson, _compressArm9); 38 | return 0; 39 | } 40 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/PatchArm9Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Text.Json; 7 | using HaroohieClub.NitroPacker.Patcher.Nitro; 8 | using Mono.Options; 9 | 10 | namespace HaroohieClub.NitroPacker.Cli; 11 | 12 | public class PatchArm9Command : Command 13 | { 14 | private string _inputDir, _outputDir, _projectFilePath, _dockerTag, _devkitArm, _overrideSuffix; 15 | private uint _arenaLoOffset = 0, _ramAddress = 0; 16 | 17 | public PatchArm9Command() : base("patch-arm9", "Patches the game's arm9.bin") 18 | { 19 | Options = new() 20 | { 21 | { "i|input-dir=", "Input directory containing arm9.bin and source", i => _inputDir = i }, 22 | { "o|output-dir=", "Output directory for writing modified arm9.bin", o => _outputDir = o }, 23 | { "a|arena-lo-offset=", "ArenaLoOffset provided as a hex number", a => _arenaLoOffset = uint.Parse(a.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? a[2..] : a, NumberStyles.HexNumber) }, 24 | { "p|project-file=", "An NDS project file from extracting with NitroPacker (can be provided instead of a RAM address)", p => _projectFilePath = p }, 25 | { "r|ram-address=", "The address at which the ROM is loaded into NDS RAM", r => _ramAddress = uint.Parse(r.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? r[2..] : r, NumberStyles.HexNumber) }, 26 | { "d|docker-tag=", "(Optional) Indicates Docker should be used and provides a docker tag of the devkitpro/devkitarm image to use", d => _dockerTag = d }, 27 | { "devkitarm=", "(Optional) Location of the devkitARM installation; defaults to the DEVKITARM environment variable", dev => _devkitArm = dev }, 28 | { "override-suffix=", "(Optional) A file extension suffix to indicate that a general file should be overridden, good for using with e.g. locales", o => _overrideSuffix = o }, 29 | }; 30 | } 31 | 32 | public override int Invoke(IEnumerable arguments) 33 | { 34 | Options.Parse(arguments); 35 | 36 | if (_arenaLoOffset == 0) 37 | { 38 | CommandSet.Out.WriteLine($"ArenaLoOffset must be provided!\n\n{Help}"); 39 | return 1; 40 | } 41 | 42 | if (string.IsNullOrEmpty(_projectFilePath) && _ramAddress == 0) 43 | { 44 | CommandSet.Out.WriteLine("Neither project file nor RAM address were specified; assuming ARM9 RAM address is 0x2000000..."); 45 | _ramAddress = 0x2000000; 46 | } 47 | else if (_ramAddress == 0) 48 | { 49 | NdsProjectFile project = JsonSerializer.Deserialize(File.ReadAllText(_projectFilePath)); 50 | _ramAddress = project.RomInfo.Header.Arm9RamAddress; 51 | } 52 | 53 | if (string.IsNullOrEmpty(_inputDir)) 54 | { 55 | _inputDir = Path.Combine(Environment.CurrentDirectory, "src"); 56 | } 57 | if (string.IsNullOrEmpty(_outputDir)) 58 | { 59 | _outputDir = Path.Combine(Environment.CurrentDirectory, "rom"); 60 | } 61 | 62 | if (!Directory.Exists(_inputDir)) 63 | { 64 | CommandSet.Out.WriteLine($"Input directory {_inputDir} does not exist!\n\n{Help}"); 65 | return 1; 66 | } 67 | if (!Directory.Exists(_outputDir)) 68 | { 69 | Directory.CreateDirectory(_outputDir); 70 | } 71 | 72 | List<(string, string)> renames = Utilities.RenameOverrideFiles(_inputDir, _overrideSuffix, CommandSet.Out); 73 | 74 | ARM9 arm9 = new(File.ReadAllBytes(Path.Combine(_inputDir, "arm9.bin")), _ramAddress); 75 | if (!ARM9AsmHack.Insert(_inputDir, arm9, _arenaLoOffset, _dockerTag, 76 | (object sender, DataReceivedEventArgs e) => Console.WriteLine(e.Data), 77 | (object sender, DataReceivedEventArgs e) => Console.Error.WriteLine(e.Data), 78 | devkitArmPath: _devkitArm)) 79 | { 80 | Console.WriteLine("ERROR: ASM hack insertion failed!"); 81 | Utilities.RevertOverrideFiles(renames); 82 | return 1; 83 | } 84 | File.WriteAllBytes(Path.Combine(_outputDir, "arm9.bin"), arm9.GetBytes()); 85 | 86 | Utilities.RevertOverrideFiles(renames); 87 | 88 | return 0; 89 | } 90 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/PatchOverlaysCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using HaroohieClub.NitroPacker.Patcher.Overlay; 7 | using Mono.Options; 8 | 9 | namespace HaroohieClub.NitroPacker.Cli; 10 | 11 | public class PatchOverlaysCommand : Command 12 | { 13 | private string _inputOverlaysDirectory, _outputOverlaysDirectory, _overlaySourceDir, _romInfoPath, _dockerTag, _devkitArm, _overrideSuffix; 14 | 15 | public PatchOverlaysCommand() : base("patch-overlays", "Patches the game's overlays") 16 | { 17 | Options = new() 18 | { 19 | "Patches the game's overlays given a Riivolution-style XML file and a rominfo.xml", 20 | "Usage: HaruhiChokuretsuCLI patch-overlays -i [inputOverlaysDirectory] -o [outputOverlaysDirectory] -p [overlayPatch] -r [romInfo]", 21 | "", 22 | { "i|input-overlays=", "Directory containing unpatched overlays", i => _inputOverlaysDirectory = i }, 23 | { "o|output-overlays=", "Directory where patched overlays will be written", o => _outputOverlaysDirectory = o }, 24 | { "s|source-dir=", "Directory where overlay source code lives", s => _overlaySourceDir = s }, 25 | { "r|project=", "Project JSON file containing the overlay table", r => _romInfoPath = r }, 26 | { "d|docker-tag=", "(Optional) Indicates docker should be used and provides a docker tag of the devkitpro/devkitarm image to use", d => _dockerTag = d }, 27 | { "devkitarm=", "(Optional) Location of the devkitARM installation; defaults to the DEVKITARM environment variable", dev => _devkitArm = dev }, 28 | { "override-suffix=", "(Optional) A file extension suffix to indicate that a general file should be overridden, good for using with e.g. locales", o => _overrideSuffix = o }, 29 | }; 30 | } 31 | 32 | public override int Invoke(IEnumerable arguments) 33 | { 34 | Options.Parse(arguments); 35 | 36 | if (string.IsNullOrEmpty(_inputOverlaysDirectory) || string.IsNullOrEmpty(_outputOverlaysDirectory) || string.IsNullOrEmpty(_overlaySourceDir) || string.IsNullOrEmpty(_romInfoPath)) 37 | { 38 | int returnValue = 0; 39 | if (string.IsNullOrEmpty(_inputOverlaysDirectory)) 40 | { 41 | CommandSet.Out.WriteLine("Input overlays directory not provided, please supply -i or --input-overlays"); 42 | returnValue = 1; 43 | } 44 | if (string.IsNullOrEmpty(_outputOverlaysDirectory)) 45 | { 46 | CommandSet.Out.WriteLine("Output overlays directory not provided, please supply -o or --output-overlays"); 47 | returnValue = 1; 48 | } 49 | if (string.IsNullOrEmpty(_overlaySourceDir)) 50 | { 51 | CommandSet.Out.WriteLine("Overlay source directory not provided, please supply -s or --source-dir"); 52 | returnValue = 1; 53 | } 54 | if (string.IsNullOrEmpty(_romInfoPath)) 55 | { 56 | CommandSet.Out.WriteLine("rominfo.xml not provided, please supply -r or --rom-info"); 57 | returnValue = 1; 58 | } 59 | Options.WriteOptionDescriptions(CommandSet.Out); 60 | return returnValue; 61 | } 62 | 63 | if (!Directory.Exists(_outputOverlaysDirectory)) 64 | { 65 | Directory.CreateDirectory(_outputOverlaysDirectory); 66 | } 67 | 68 | List overlays = []; 69 | foreach (string file in Directory.GetFiles(_inputOverlaysDirectory)) 70 | { 71 | overlays.Add(new(file, _romInfoPath)); 72 | } 73 | 74 | List<(string, string)> renames = Utilities.RenameOverrideFiles(_overlaySourceDir, _overrideSuffix, CommandSet.Out); 75 | 76 | foreach (Overlay overlay in overlays) 77 | { 78 | if (Directory.GetDirectories(_overlaySourceDir).Contains(Path.Combine(_overlaySourceDir, overlay.Name))) 79 | { 80 | OverlayAsmHack.Insert(_overlaySourceDir, overlay, _romInfoPath, _dockerTag, 81 | (object sender, DataReceivedEventArgs e) => Console.WriteLine(e.Data), 82 | (object sender, DataReceivedEventArgs e) => Console.Error.WriteLine(e.Data), 83 | devkitArmPath: _devkitArm); 84 | } 85 | } 86 | 87 | Utilities.RevertOverrideFiles(renames); 88 | 89 | foreach (Overlay overlay in overlays) 90 | { 91 | overlay.Save(Path.Combine(_outputOverlaysDirectory, $"{overlay.Name}.bin")); 92 | } 93 | 94 | return 0; 95 | } 96 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using Mono.Options; 2 | 3 | namespace HaroohieClub.NitroPacker.Cli; 4 | 5 | public class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | CommandSet commands = new("NitroPacker") 10 | { 11 | new UnpackCommand(), 12 | new PackCommand(), 13 | new PatchArm9Command(), 14 | new PatchOverlaysCommand(), 15 | new MigrateProjectCommand(), 16 | }; 17 | commands.Run(args); 18 | } 19 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/UnpackCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Mono.Options; 3 | 4 | namespace HaroohieClub.NitroPacker.Cli; 5 | 6 | public class UnpackCommand : Command 7 | { 8 | private string _projectName, _romPath, _unpackPath; 9 | private bool _unpackArchives, _decompressArm9, _includeFnt; 10 | public UnpackCommand() : base("unpack", "Unpacks a ROM to a directory and project XML file") 11 | { 12 | Options = new() 13 | { 14 | { "r|rom=", "Input ROM path", r => _romPath = r }, 15 | { "o|u|output|unpack-path=", "Path to unpack ROM to", u => _unpackPath = u }, 16 | { "p|n|project|name|project-name=", "Name of the project file", p => _projectName = p }, 17 | { "d|decompress-arm9", "If specified, will attempt to decompress the ARM9 binary", d => _decompressArm9 = true }, 18 | { "f|include-file-order", "If specified, will include the order of the files in the project file (some games might require this to repack); " + 19 | "if true, no files can be added or removed after unpacking (though they still can be modified)", f => _includeFnt = true }, 20 | { "a|unpack-archives", "If specified, will unpack NARC archives as well", a => _unpackArchives = true }, 21 | }; 22 | } 23 | 24 | public override int Invoke(IEnumerable arguments) 25 | { 26 | Options.Parse(arguments); 27 | 28 | if (string.IsNullOrEmpty(_projectName)) 29 | { 30 | CommandSet.Out.WriteLine($"Must provide project name."); 31 | Options.WriteOptionDescriptions(CommandSet.Out); 32 | return 1; 33 | } 34 | if (string.IsNullOrEmpty(_romPath)) 35 | { 36 | CommandSet.Out.WriteLine($"Must provide path to ROM."); 37 | Options.WriteOptionDescriptions(CommandSet.Out); 38 | return 1; 39 | } 40 | if (string.IsNullOrEmpty(_unpackPath)) 41 | { 42 | CommandSet.Out.WriteLine($"Must provide path to unpack to."); 43 | Options.WriteOptionDescriptions(CommandSet.Out); 44 | return 1; 45 | } 46 | 47 | NdsProjectFile.Create(_projectName, _romPath, _unpackPath, _decompressArm9, _unpackArchives, _includeFnt); 48 | return 0; 49 | } 50 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.Cli/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace HaroohieClub.NitroPacker.Cli; 6 | 7 | internal static class Utilities 8 | { 9 | public static List<(string, string)> RenameOverrideFiles(string sourceDir, string overrideSuffix, TextWriter log) 10 | { 11 | List<(string, string)> renames = []; 12 | foreach (string file in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories)) 13 | { 14 | if (Path.GetExtension(file).Equals($".{overrideSuffix}", StringComparison.OrdinalIgnoreCase)) 15 | { 16 | log.WriteLine($"Found override file '{file}'"); 17 | string overridableFile = Path.Combine(Path.GetDirectoryName(file), Path.GetFileNameWithoutExtension(file)); 18 | string ignoredFile = $"{overridableFile}.ignore"; 19 | renames.Add((overridableFile, file)); 20 | if (File.Exists(overridableFile)) 21 | { 22 | renames.Add((ignoredFile, overridableFile)); 23 | File.Move(overridableFile, ignoredFile); 24 | log.WriteLine($"Overrode file '{overridableFile}"); 25 | } 26 | File.Move(file, overridableFile); 27 | } 28 | } 29 | return renames; 30 | } 31 | 32 | public static void RevertOverrideFiles(List<(string, string)> renames) 33 | { 34 | foreach ((string renamed, string orig) in renames) 35 | { 36 | File.Move(renamed, orig); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaroohieClub.NitroPacker.Cli", "HaroohieClub.NitroPacker.Cli\HaroohieClub.NitroPacker.Cli.csproj", "{E66F7906-D350-47B9-80AF-730CCFC27D9C}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HaroohieClub.NitroPacker", "HaroohieClub.NitroPacker\HaroohieClub.NitroPacker.csproj", "{2B57DC29-801A-49D0-99E2-662CA96C2C3A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {E66F7906-D350-47B9-80AF-730CCFC27D9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {E66F7906-D350-47B9-80AF-730CCFC27D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {E66F7906-D350-47B9-80AF-730CCFC27D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {E66F7906-D350-47B9-80AF-730CCFC27D9C}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2B57DC29-801A-49D0-99E2-662CA96C2C3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2B57DC29-801A-49D0-99E2-662CA96C2C3A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2B57DC29-801A-49D0-99E2-662CA96C2C3A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2B57DC29-801A-49D0-99E2-662CA96C2C3A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {B82C03BE-1378-4C94-BEFA-090EBCE94DA6} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/HaroohieClub.NitroPacker.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 12.0 6 | disable 7 | disable 8 | true 9 | $(NPVersion) 10 | NitroPacker 11 | HaroohieClub.NitroPacker 12 | Ermelber, Gericom, Jonko 13 | Haroohie Translation Club 14 | haroohie-club 15 | A utility for packing and unpacking Nintendo DS ROMs. Additionally can patch ARM9 and overlays. 16 | README.md 17 | GPL-3.0-or-later 18 | https://github.com/haroohie-club/NitroPacker 19 | https://github.com/haroohie-club/NitroPacker 20 | git 21 | HaroohieClub.NitroPacker 22 | true 23 | nds nintendo ds arm9 overlay asm patching romhacking hacking nitrofs nitro unpack pack extract 24 | nitropacker_red.png 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/Archive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace HaroohieClub.NitroPacker.IO.Archive; 7 | 8 | /// 9 | /// A generic archive 10 | /// 11 | public abstract class Archive : IReadOnlyArchive 12 | { 13 | /// 14 | /// The path separator for the archive (usually '/' but '\' on windows) 15 | /// 16 | public const char PathSeparator = '/'; 17 | /// 18 | /// The root path of the archive 19 | /// 20 | public const string RootPath = "/"; 21 | 22 | /// 23 | /// If true, the archive is read-only 24 | /// 25 | public virtual bool IsReadOnly => true; 26 | 27 | /// 28 | public abstract IEnumerable EnumerateFiles(string path, bool fullPath); 29 | 30 | /// 31 | public abstract IEnumerable EnumerateDirectories(string path, bool fullPath); 32 | 33 | /// 34 | /// Deletes a file from the archive 35 | /// 36 | /// The path to the file to delete 37 | /// Throws if the archive is read-only 38 | public virtual void DeleteFile(string path) => throw new NotSupportedException(); 39 | /// 40 | /// Deletes a directory from the archive 41 | /// 42 | /// The path to the directory to delete 43 | /// Throws if the archive is read-only 44 | public virtual void DeleteDirectory(string path) => throw new NotSupportedException(); 45 | 46 | /// 47 | public abstract bool ExistsFile(string path); 48 | 49 | /// 50 | public abstract bool ExistsDirectory(string path); 51 | 52 | /// 53 | /// Creates a directory in the archive 54 | /// 55 | /// The path to the directory to create 56 | /// Throws if the archive is read-only 57 | public virtual void CreateDirectory(string path) => throw new NotSupportedException(); 58 | 59 | /// 60 | /// Gets a file's contents 61 | /// 62 | /// The path to the file 63 | /// The file's contents as a byte array 64 | public abstract byte[] GetFileData(string path); 65 | 66 | /// 67 | /// Gets a file's contents as a read-only span 68 | /// 69 | /// The path to the file 70 | /// A of bytes of the file's data 71 | public ReadOnlySpan GetFileDataSpan(string path) 72 | => GetFileData(path); 73 | 74 | /// 75 | public abstract Stream OpenFileReadStream(string path); 76 | 77 | /// 78 | /// Sets a file's data 79 | /// 80 | /// The path to the file 81 | /// The data to set 82 | /// Throws in archives which are read-only 83 | public virtual void SetFileData(string path, byte[] data) => throw new NotSupportedException(); 84 | 85 | /// 86 | /// Joins multiple paths together with the path separator 87 | /// 88 | /// The various parts of the path 89 | /// A properly joined path 90 | public static string JoinPath(params string[] parts) 91 | => JoinPath((IEnumerable)parts); 92 | 93 | /// 94 | /// Joins multiple paths together with the path separator 95 | /// 96 | /// The various parts of the path 97 | /// A properly joined path 98 | public static string JoinPath(IEnumerable parts) 99 | => PathSeparator + string.Join(PathSeparator, parts.Select(p => p.Trim(PathSeparator)).Where(p => p != "")); 100 | 101 | /// 102 | /// Normalizes a path for consistency 103 | /// 104 | /// The path to normalize 105 | /// A normalized version of that path 106 | /// Thrown if an invalid path is specified 107 | public static string NormalizePath(string path) 108 | { 109 | path = path.Trim(PathSeparator); 110 | string[] parts = path.Split(PathSeparator); 111 | 112 | var newPath = new Stack(); 113 | foreach (string part in parts) 114 | { 115 | if (part == ".") 116 | continue; 117 | 118 | if (part == "..") 119 | { 120 | if (newPath.Count == 0) 121 | throw new ArgumentException("Invalid path specified"); 122 | newPath.Pop(); 123 | continue; 124 | } 125 | 126 | newPath.Push(part); 127 | } 128 | 129 | return JoinPath(newPath.Reverse()); 130 | } 131 | 132 | /// 133 | /// Checks to see if two paths are equivalent 134 | /// 135 | /// The first path 136 | /// The second path 137 | /// True if they are equal, false if not 138 | public static bool PathEqual(string path1, string path2) 139 | => NormalizePath(path1) == NormalizePath(path2); 140 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/DiskArchive.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace HaroohieClub.NitroPacker.IO.Archive; 6 | 7 | /// 8 | /// An "archive" representing a particular folder on the user's computer 9 | /// 10 | public class DiskArchive : Archive 11 | { 12 | /// 13 | /// The root path of the disk 14 | /// 15 | public string RootDiskPath { get; } 16 | 17 | /// 18 | public DiskArchive(string rootPath) 19 | { 20 | RootDiskPath = rootPath; 21 | if (!Directory.Exists(RootDiskPath)) 22 | throw new DirectoryNotFoundException(); 23 | } 24 | 25 | /// 26 | public override bool IsReadOnly => false; 27 | 28 | private string CreateDiskPath(string path) 29 | { 30 | // remove any root and trailing slashes 31 | path = path.Trim(PathSeparator); 32 | 33 | // convert to the right slash type 34 | path = path.Replace(PathSeparator, Path.DirectorySeparatorChar); 35 | 36 | if (!string.IsNullOrEmpty(Path.GetPathRoot(path))) 37 | throw new("Invalid path specified"); 38 | 39 | path = Path.Combine(RootDiskPath, path); 40 | 41 | // check that the path does not accidentally go up past the root of the archive 42 | string relPath = Path.GetRelativePath(RootDiskPath, path); 43 | if (relPath.StartsWith("..")) 44 | throw new("Invalid path specified"); 45 | 46 | return path; 47 | } 48 | 49 | /// 50 | public override IEnumerable EnumerateFiles(string path, bool fullPath) 51 | { 52 | var files = Directory.EnumerateFiles(CreateDiskPath(path)); 53 | return fullPath ? files.Select(file => 54 | PathSeparator + Path.GetRelativePath(RootDiskPath, file) 55 | .Replace(Path.DirectorySeparatorChar, PathSeparator)) : 56 | files.Select(Path.GetFileName); 57 | } 58 | 59 | /// 60 | public override IEnumerable EnumerateDirectories(string path, bool fullPath) 61 | { 62 | var dirs = Directory.EnumerateDirectories(CreateDiskPath(path)); 63 | return fullPath ? dirs.Select(file => 64 | PathSeparator + Path.GetRelativePath(RootDiskPath, file) 65 | .Replace(Path.DirectorySeparatorChar, PathSeparator)) : 66 | dirs.Select(d => d.Substring(Path.GetDirectoryName(d.TrimEnd(Path.DirectorySeparatorChar)).Length + 1)); 67 | } 68 | 69 | /// 70 | public override void DeleteFile(string path) 71 | => File.Delete(CreateDiskPath(path)); 72 | 73 | /// 74 | public override void DeleteDirectory(string path) 75 | { 76 | string diskPath = CreateDiskPath(path); 77 | if (Path.GetRelativePath(RootDiskPath, diskPath) == ".") 78 | throw new("Can't delete root directory"); 79 | 80 | Directory.Delete(diskPath, true); 81 | } 82 | 83 | /// 84 | public override bool ExistsFile(string path) 85 | => File.Exists(CreateDiskPath(path)); 86 | 87 | /// 88 | public override bool ExistsDirectory(string path) 89 | => Directory.Exists(CreateDiskPath(path)); 90 | 91 | /// 92 | public override void CreateDirectory(string path) 93 | => Directory.CreateDirectory(CreateDiskPath(path)); 94 | 95 | /// 96 | public override byte[] GetFileData(string path) 97 | => File.ReadAllBytes(CreateDiskPath(path)); 98 | 99 | /// 100 | public override Stream OpenFileReadStream(string path) 101 | => File.OpenRead(CreateDiskPath(path)); 102 | 103 | /// 104 | public override void SetFileData(string path, byte[] data) 105 | => File.WriteAllBytes(CreateDiskPath(path), data); 106 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/IReadOnlyArchive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace HaroohieClub.NitroPacker.IO.Archive; 6 | 7 | /// 8 | /// Interface for read-only archives 9 | /// 10 | public interface IReadOnlyArchive 11 | { 12 | /// 13 | /// Enumerates the files in a directory in the archive 14 | /// 15 | /// Path to the directory 16 | /// If true, returns the full path of each file; if false, returns only the file name 17 | /// An enumerable of all the files in the specified directory 18 | IEnumerable EnumerateFiles(string path, bool fullPath); 19 | /// 20 | /// Enumerates the subdirectories in a directory in the archive 21 | /// 22 | /// Path to the directory 23 | /// If true, returns the full path of each directory; if false, returns only the directory name 24 | /// An enumerable of all the subdirectories in the specified directory 25 | IEnumerable EnumerateDirectories(string path, bool fullPath); 26 | 27 | /// 28 | /// Checks if a file exists 29 | /// 30 | /// The path to the file 31 | /// True if the file exists, false if it doesn't 32 | bool ExistsFile(string path); 33 | /// 34 | /// Checks if a subdirectory exists 35 | /// 36 | /// The path to the directory 37 | /// True if the subdirectory exists, false if it doesn't 38 | bool ExistsDirectory(string path); 39 | 40 | /// 41 | /// Gets a file's data as a span of bytes 42 | /// 43 | /// The path to the file 44 | /// A of bytes containing the file data 45 | ReadOnlySpan GetFileDataSpan(string path); 46 | /// 47 | /// Opens a stream to a particular file 48 | /// 49 | /// The path to the file 50 | /// A read-only stream for that file's data 51 | Stream OpenFileReadStream(string path); 52 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/Tree/DirectoryNode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace HaroohieClub.NitroPacker.IO.Archive.Tree; 5 | 6 | internal class DirectoryNode(string name, string path, INode parent, ushort id, ushort entryFileId) : INode 7 | { 8 | public string Name { get; set; } = name; 9 | public string Path { get; set; } = path; 10 | public ushort Id { get; set; } = id; 11 | public INode Parent { get; set; } = parent; 12 | public IList Children { get; set; } = []; 13 | public ushort EntryFileId { get; set; } = entryFileId; 14 | 15 | public DirectoryNode FindNodeFromId(ushort id) 16 | { 17 | foreach (DirectoryNode dir in Children.Where(c => c is DirectoryNode).Cast()) 18 | { 19 | if (dir.Id == id) 20 | { 21 | return dir; 22 | } 23 | DirectoryNode fromChildren = dir.FindNodeFromId(id); 24 | if (fromChildren is not null) 25 | { 26 | return fromChildren; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/Tree/FileNode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Archive.Tree; 4 | 5 | internal class FileNode(string name, INode parent, ushort id, string path) : INode 6 | { 7 | public string Name { get; set; } = name; 8 | public string Path { get; set; } = path; 9 | public ushort Id { get; set; } = id; 10 | public INode Parent { get; set; } = parent; 11 | public IList Children { get; set; } = null; 12 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/Tree/FileTree.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Archive.Tree; 4 | 5 | internal class FileTree 6 | { 7 | public DirectoryNode Root { get; private set; } 8 | 9 | public FileTree() 10 | { 11 | } 12 | 13 | public FileTree(Archive archive, ushort rootId, ushort fileId) 14 | { 15 | Root = CreateDirectoryNode(archive, null, Archive.RootPath, ref rootId, ref fileId); 16 | } 17 | 18 | private DirectoryNode CreateDirectoryNode(Archive archive, DirectoryNode parent, string path, ref ushort dirId, ref ushort fileId) 19 | { 20 | DirectoryNode thisNode = new(Path.GetFileName(path), path, parent, dirId++, fileId); 21 | 22 | foreach (string file in archive.EnumerateFiles(path, false)) 23 | { 24 | thisNode.Children.Add(new FileNode(file, thisNode, fileId++, path)); 25 | } 26 | 27 | foreach (string directory in archive.EnumerateDirectories(path, true)) 28 | { 29 | thisNode.Children.Add(CreateDirectoryNode(archive, thisNode, directory, ref dirId, ref fileId)); 30 | } 31 | 32 | return thisNode; 33 | } 34 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Archive/Tree/INode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Archive.Tree; 4 | 5 | internal interface INode 6 | { 7 | public string Name { get; set; } 8 | public string Path { get; set; } 9 | public ushort Id { get; set; } 10 | public INode Parent { get; set; } 11 | public IList Children { get; set; } 12 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Compression/CompressionWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace HaroohieClub.NitroPacker.IO.Compression; 5 | 6 | internal class CompressionWindow 7 | { 8 | private readonly byte[] _src; 9 | private readonly LinkedList[] _windowDict = new LinkedList[256]; 10 | 11 | public uint Position { get; private set; } 12 | 13 | public int MinRun { get; } 14 | public int MaxRun { get; } 15 | public int WindowSize { get; } 16 | 17 | public CompressionWindow(byte[] src, int minRun, int maxRun, int windowSize) 18 | { 19 | _src = src; 20 | MinRun = minRun; 21 | MaxRun = maxRun; 22 | WindowSize = windowSize; 23 | for (int i = 0; i < 256; i++) 24 | _windowDict[i] = new(); 25 | } 26 | 27 | public unsafe (uint pos, int len) FindRun() 28 | { 29 | fixed (byte* pSrc = &_src[0]) 30 | { 31 | var start = _windowDict[pSrc[Position]].First; 32 | int bestLen = -1; 33 | uint bestPos = 0; 34 | while (start != null) 35 | { 36 | uint pos = start.Value; 37 | 38 | if (Position - pos > WindowSize) 39 | { 40 | var old = start; 41 | start = start.Next; 42 | old.List.Remove(old); 43 | continue; 44 | } 45 | 46 | int len = 1; 47 | int lenMax = Math.Min((int)(_src.Length - Position), MaxRun); 48 | byte* a = &pSrc[pos + len]; 49 | byte* b = &pSrc[Position + len]; 50 | while (*a++ == *b++ && len < lenMax) 51 | len++; 52 | if (len >= MinRun && len > bestLen) 53 | { 54 | bestLen = len; 55 | bestPos = pos; 56 | if (len == MaxRun) 57 | break; 58 | } 59 | 60 | start = start.Next; 61 | } 62 | 63 | return (bestPos, bestLen); 64 | } 65 | } 66 | 67 | public void Slide(int count) 68 | { 69 | for (uint i = Position; i < Position + count; i++) 70 | _windowDict[_src[i]].AddFirst(i); 71 | Position = (uint)(Position + count); 72 | } 73 | 74 | public void Reset() 75 | { 76 | Position = 0; 77 | for (int i = 0; i < 256; i++) 78 | _windowDict[i].Clear(); 79 | } 80 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Compression/Lz77.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace HaroohieClub.NitroPacker.IO.Compression; 5 | 6 | internal static class Lz77 7 | { 8 | public static byte[] Compress(byte[] src) 9 | { 10 | var context = new CompressionWindow(src, 3, 18, 4096); 11 | var dst = new MemoryStream(); 12 | dst.WriteByte(0x10); 13 | dst.WriteByte((byte)(src.Length & 0xFF)); 14 | dst.WriteByte((byte)(src.Length >> 8 & 0xFF)); 15 | dst.WriteByte((byte)(src.Length >> 16 & 0xFF)); 16 | 17 | while (context.Position < src.Length) 18 | { 19 | long blockStart = dst.Position; 20 | dst.WriteByte(0); //to be filled in later 21 | byte header = 0; 22 | for (int i = 0; i < 8; i++) 23 | { 24 | header <<= 1; 25 | (uint pos, int len) = context.FindRun(); 26 | if (len > 0) 27 | { 28 | header |= 1; 29 | uint back = context.Position - pos; 30 | dst.WriteByte((byte)(back - 1 >> 8 & 0xF | ((uint)len - 3 & 0xF) << 4)); 31 | dst.WriteByte((byte)(back - 1 & 0xFF)); 32 | context.Slide(len); 33 | } 34 | else 35 | { 36 | dst.WriteByte(src[context.Position]); 37 | context.Slide(1); 38 | } 39 | 40 | if (context.Position >= src.Length) 41 | { 42 | header <<= 7 - i; 43 | break; 44 | } 45 | } 46 | 47 | long curPos = dst.Position; 48 | dst.Position = blockStart; 49 | dst.WriteByte(header); 50 | dst.Position = curPos; 51 | } 52 | 53 | while (dst.Position % 4 != 0) 54 | dst.WriteByte(0); 55 | return dst.ToArray(); 56 | } 57 | 58 | public static byte[] Decompress(ReadOnlySpan src) 59 | { 60 | uint outSize = IOUtil.ReadU24Le(src[1..]); 61 | var dst = new byte[outSize]; 62 | int srcOffs = 4; 63 | int dstOffs = 0; 64 | while (true) 65 | { 66 | byte header = src[srcOffs++]; 67 | for (int i = 0; i < 8; i++) 68 | { 69 | if ((header & 0x80) == 0) 70 | dst[dstOffs++] = src[srcOffs++]; 71 | else 72 | { 73 | byte a = src[srcOffs++]; 74 | byte b = src[srcOffs++]; 75 | 76 | int offs = ((a & 0xF) << 8 | b) + 1; 77 | int length = (a >> 4) + 3; 78 | 79 | for (int j = 0; j < length; j++) 80 | { 81 | dst[dstOffs] = dst[dstOffs - offs]; 82 | dstOffs++; 83 | } 84 | } 85 | 86 | if (dstOffs >= outSize) 87 | return dst; 88 | header <<= 1; 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Compression/Yaz0.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace HaroohieClub.NitroPacker.IO.Compression; 5 | 6 | internal class Yaz0 7 | { 8 | public static byte[] Compress(byte[] src) 9 | { 10 | var context = new CompressionWindow(src, 3, 273, 4096); 11 | var dst = new MemoryStream(); 12 | dst.WriteByte((byte)'Y'); 13 | dst.WriteByte((byte)'a'); 14 | dst.WriteByte((byte)'z'); 15 | dst.WriteByte((byte)'0'); 16 | dst.WriteByte((byte)(src.Length >> 24 & 0xFF)); 17 | dst.WriteByte((byte)(src.Length >> 16 & 0xFF)); 18 | dst.WriteByte((byte)(src.Length >> 8 & 0xFF)); 19 | dst.WriteByte((byte)(src.Length & 0xFF)); 20 | dst.Write(new byte[8]); 21 | 22 | while (context.Position < src.Length) 23 | { 24 | long blockStart = dst.Position; 25 | dst.WriteByte(0); //to be filled in later 26 | byte header = 0; 27 | for (int i = 0; i < 8; i++) 28 | { 29 | header <<= 1; 30 | (uint pos, int len) = context.FindRun(); 31 | if (len > 0) 32 | { 33 | uint back = context.Position - pos; 34 | 35 | if (len >= 18) 36 | { 37 | dst.WriteByte((byte)(back - 1 >> 8 & 0xF)); 38 | dst.WriteByte((byte)(back - 1 & 0xFF)); 39 | dst.WriteByte((byte)(len - 0x12 & 0xFF)); 40 | } 41 | else 42 | { 43 | dst.WriteByte((byte)(back - 1 >> 8 & 0xF | ((uint)len - 2 & 0xF) << 4)); 44 | dst.WriteByte((byte)(back - 1 & 0xFF)); 45 | } 46 | 47 | context.Slide(len); 48 | } 49 | else 50 | { 51 | header |= 1; 52 | dst.WriteByte(src[context.Position]); 53 | context.Slide(1); 54 | } 55 | 56 | if (context.Position >= src.Length) 57 | { 58 | header <<= 7 - i; 59 | break; 60 | } 61 | } 62 | 63 | long curPos = dst.Position; 64 | dst.Position = blockStart; 65 | dst.WriteByte(header); 66 | dst.Position = curPos; 67 | } 68 | 69 | while (dst.Position % 4 != 0) 70 | dst.WriteByte(0); 71 | return dst.ToArray(); 72 | } 73 | 74 | public static byte[] Decompress(ReadOnlySpan data) 75 | { 76 | uint len = IOUtil.ReadU32Be(data[4..]); 77 | var result = new byte[len]; 78 | int ptr = 16; 79 | int dstOffs = 0; 80 | while (true) 81 | { 82 | byte header = data[ptr++]; 83 | for (int i = 0; i < 8; i++) 84 | { 85 | if ((header & 0x80) != 0) 86 | result[dstOffs++] = data[ptr++]; 87 | else 88 | { 89 | byte b = data[ptr++]; 90 | int offs = ((b & 0xF) << 8 | data[ptr++]) + 1; 91 | int length = (b >> 4) + 2; 92 | if (length == 2) 93 | length = data[ptr++] + 0x12; 94 | for (int j = 0; j < length; j++) 95 | { 96 | result[dstOffs] = result[dstOffs - offs]; 97 | dstOffs++; 98 | } 99 | } 100 | 101 | if (dstOffs >= len) 102 | return result; 103 | header <<= 1; 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/EndianBinaryReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace HaroohieClub.NitroPacker.IO; 7 | 8 | /// 9 | /// Basic binary stream reader class 10 | /// 11 | public class EndianBinaryReader : IDisposable 12 | { 13 | private bool _disposed; 14 | private byte[] _buffer; 15 | 16 | /// 17 | /// The underlying stream the binary reader is reading 18 | /// 19 | public Stream BaseStream { get; } 20 | /// 21 | /// The endianness of the reader (big vs little) 22 | /// 23 | public Endianness Endianness { get; } 24 | 25 | private static Endianness SystemEndianness => 26 | BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; 27 | 28 | private bool Reverse => SystemEndianness != Endianness; 29 | 30 | /// 31 | /// Constructs an endian binary reader from the given properties 32 | /// 33 | /// The stream to read 34 | /// The endianness of the given stream (defaults to little endian) 35 | /// Thrown if the stream is null 36 | /// Thrown if the stream cannot be read 37 | public EndianBinaryReader(Stream baseStream, Endianness endianness = Endianness.LittleEndian) 38 | { 39 | if (baseStream == null) 40 | throw new ArgumentNullException(nameof(baseStream)); 41 | if (!baseStream.CanRead) 42 | throw new ArgumentException(nameof(baseStream)); 43 | 44 | BaseStream = baseStream; 45 | Endianness = endianness; 46 | } 47 | 48 | /// 49 | /// Disposes of the endian binary reader 50 | /// 51 | ~EndianBinaryReader() 52 | { 53 | Dispose(false); 54 | } 55 | 56 | private void FillBuffer(int bytes, int stride) 57 | { 58 | if (_buffer == null || _buffer.Length < bytes) 59 | _buffer = new byte[bytes]; 60 | 61 | BaseStream.Read(_buffer, 0, bytes); 62 | 63 | if (Reverse && stride > 1) 64 | { 65 | for (int i = 0; i < bytes; i += stride) 66 | Array.Reverse(_buffer, i, stride); 67 | } 68 | } 69 | 70 | /// 71 | /// Skips a number of bytes in the stream 72 | /// 73 | /// The number of bytes to skip 74 | public void Skip(long numBytes) 75 | { 76 | BaseStream.Seek(numBytes, SeekOrigin.Current); 77 | } 78 | 79 | /// 80 | /// Reads a single from the stream 81 | /// 82 | /// The encoding to use when reading 83 | /// A char from the stream 84 | public char ReadChar(Encoding encoding) 85 | { 86 | int size; 87 | 88 | size = GetEncodingSize(encoding); 89 | FillBuffer(size, size); 90 | return encoding.GetChars(_buffer, 0, size)[0]; 91 | } 92 | 93 | /// 94 | /// Reads a set of chars from the stream 95 | /// 96 | /// The encoding of the characters in the stream 97 | /// The number of characters to read 98 | /// A char array of characters from the strean 99 | public char[] ReadChars(Encoding encoding, int count) 100 | { 101 | int size; 102 | 103 | size = GetEncodingSize(encoding); 104 | FillBuffer(size * count, size); 105 | return encoding.GetChars(_buffer, 0, size * count); 106 | } 107 | 108 | private static int GetEncodingSize(Encoding encoding) 109 | { 110 | if (encoding == Encoding.UTF8 || encoding == Encoding.ASCII) 111 | return 1; 112 | else if (encoding == Encoding.Unicode || encoding == Encoding.BigEndianUnicode) 113 | return 2; 114 | 115 | return 1; 116 | } 117 | 118 | /// 119 | /// Reads a string from the stream in a specified encoding until a null terminator is reached 120 | /// 121 | /// The encoding of the string 122 | /// The characters found as a string 123 | public string ReadStringNT(Encoding encoding) 124 | { 125 | string text; 126 | 127 | text = ""; 128 | 129 | do 130 | { 131 | text += ReadChar(encoding); 132 | } while (!text.EndsWith("\0", StringComparison.Ordinal)); 133 | 134 | return text.Remove(text.Length - 1); 135 | } 136 | 137 | /// 138 | /// Reads a string in a specified encoding of a specified length from the stream 139 | /// 140 | /// The encoding to use 141 | /// The number of characters to read 142 | /// A string of the specified length 143 | public string ReadString(Encoding encoding, int count) 144 | { 145 | return new(ReadChars(encoding, count)); 146 | } 147 | 148 | /// 149 | /// Reads an unmanaged type T from the stream 150 | /// 151 | /// The type to read 152 | /// An object of type T 153 | public unsafe T Read() where T : unmanaged 154 | { 155 | int size = sizeof(T); 156 | FillBuffer(size, size); 157 | return MemoryMarshal.Read(_buffer); 158 | } 159 | 160 | /// 161 | /// Reads a set of unmanaged types T of a specified length 162 | /// 163 | /// The number of objects to read 164 | /// The specified type to read 165 | /// An array of type T 166 | public unsafe T[] Read(int count) where T : unmanaged 167 | { 168 | int size = sizeof(T); 169 | var result = new T[count]; 170 | var byteResult = MemoryMarshal.Cast(result); 171 | BaseStream.Read(byteResult); 172 | 173 | if (Reverse && size > 1) 174 | { 175 | for (int i = 0; i < size * count; i += size) 176 | byteResult.Slice(i, size).Reverse(); 177 | } 178 | 179 | return result; 180 | } 181 | 182 | /// 183 | /// Reads an fx16 (a 16-bit fixed point number used on the Nintendo DS instead of floating point math) 184 | /// 185 | /// A double representation of the fixed point number 186 | public double ReadFx16() 187 | { 188 | return Read() / 4096d; 189 | } 190 | 191 | /// 192 | /// Reads a set of fx16s (16-bit fixed point numbers used on the Nintendo DS instead of floating point math) 193 | /// 194 | /// The number of fx16s to read 195 | /// An array of doubles corresponding to each fixed point number 196 | public double[] ReadFx16s(int count) 197 | { 198 | var result = new double[count]; 199 | for (int i = 0; i < count; i++) 200 | result[i] = ReadFx16(); 201 | 202 | return result; 203 | } 204 | 205 | /// 206 | /// Reads an fx32 (a 32-bit fixed point number used on the Nintendo DS instead of floating point math) 207 | /// 208 | /// A double representation of the fixed point number 209 | public double ReadFx32() 210 | { 211 | return Read() / 4096d; 212 | } 213 | 214 | /// 215 | /// Reads a set of fx32s (32-bit fixed point numbers used on the Nintendo DS instead of floating point math) 216 | /// 217 | /// The number of fx32s to read 218 | /// An array of doubles corresponding to each fixed point number 219 | public double[] ReadFx32s(int count) 220 | { 221 | var result = new double[count]; 222 | for (int i = 0; i < count; i++) 223 | result[i] = ReadFx32(); 224 | 225 | return result; 226 | } 227 | 228 | /// 229 | /// Closes the stream and disposes of the reader 230 | /// 231 | public void Close() 232 | { 233 | Dispose(); 234 | } 235 | 236 | /// 237 | /// Disposes of the reader and stream without having the GC calling the finalizer of this object 238 | /// 239 | public void Dispose() 240 | { 241 | Dispose(true); 242 | GC.SuppressFinalize(this); 243 | } 244 | 245 | /// 246 | /// Disposes of this object while optionally not disposing of the base stream 247 | /// 248 | /// If true, disposes of the base stream 249 | private void Dispose(bool disposing) 250 | { 251 | if (_disposed) 252 | return; 253 | 254 | if (disposing) 255 | BaseStream?.Close(); 256 | 257 | _buffer = null; 258 | _disposed = true; 259 | } 260 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/EndianBinaryWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace HaroohieClub.NitroPacker.IO; 7 | 8 | /// 9 | /// Basic binary stream writer class 10 | /// 11 | public class EndianBinaryWriter : IDisposable 12 | { 13 | /// 14 | /// Indicates whether the writer has been disposed 15 | /// 16 | protected bool Disposed; 17 | private byte[] _buffer; 18 | 19 | /// 20 | /// The underlying stream to write to 21 | /// 22 | public Stream BaseStream { get; private set; } 23 | /// 24 | /// The endianness of the writer (big or little) 25 | /// 26 | public Endianness Endianness { get; set; } 27 | 28 | private static Endianness SystemEndianness => 29 | BitConverter.IsLittleEndian ? Endianness.LittleEndian : Endianness.BigEndian; 30 | 31 | private bool Reverse => SystemEndianness != Endianness; 32 | 33 | /// 34 | /// Constructs an EndianBinaryWriter from the base stream and with the given endianness 35 | /// 36 | /// The stream to write to 37 | /// The endianness of the stream (defaults to little) 38 | /// Thrown if the stream is null 39 | /// Thrown if the stream cannot be written too 40 | public EndianBinaryWriter(Stream baseStream, Endianness endianness = Endianness.LittleEndian) 41 | { 42 | if (baseStream is null) 43 | throw new ArgumentNullException(nameof(baseStream)); 44 | if (!baseStream.CanWrite) 45 | throw new ArgumentException(nameof(baseStream)); 46 | 47 | BaseStream = baseStream; 48 | Endianness = endianness; 49 | } 50 | 51 | /// 52 | /// Disposes of the endian binary writer 53 | /// 54 | ~EndianBinaryWriter() 55 | { 56 | Dispose(false); 57 | } 58 | 59 | private void WriteBuffer(int bytes, int stride) 60 | { 61 | if (Reverse && stride > 1) 62 | { 63 | for (int i = 0; i < bytes; i += stride) 64 | Array.Reverse(_buffer, i, stride); 65 | } 66 | 67 | BaseStream.Write(_buffer, 0, bytes); 68 | } 69 | 70 | private void CreateBuffer(int size) 71 | { 72 | if (_buffer == null || _buffer.Length < size) 73 | _buffer = new byte[size]; 74 | } 75 | 76 | /// 77 | /// Seeks past a certain number of bytes in the stream 78 | /// 79 | /// The number of bytes to skip 80 | public void Skip(long numBytes) 81 | { 82 | BaseStream.Seek(numBytes, SeekOrigin.Current); 83 | } 84 | 85 | /// 86 | /// Writes a byte to the stream 87 | /// 88 | /// The byte to write 89 | public void Write(byte value) 90 | { 91 | CreateBuffer(1); 92 | _buffer[0] = value; 93 | WriteBuffer(1, 1); 94 | } 95 | 96 | /// 97 | /// Writes a character to the stream in a given encoding 98 | /// 99 | /// The character to write to the stream 100 | /// The encoding to encode the character in 101 | public void Write(char value, Encoding encoding) 102 | { 103 | int size; 104 | 105 | size = GetEncodingSize(encoding); 106 | CreateBuffer(size); 107 | Array.Copy(encoding.GetBytes(new string(value, 1)), 0, _buffer, 0, size); 108 | WriteBuffer(size, size); 109 | } 110 | 111 | /// 112 | /// Writes a char array to the stream in a given encoding 113 | /// 114 | /// The char array to write 115 | /// The offset into the char array to write from 116 | /// The number of characters to write 117 | /// The encoding to write in 118 | public void Write(char[] value, int offset, int count, Encoding encoding) 119 | { 120 | int size; 121 | 122 | size = GetEncodingSize(encoding); 123 | CreateBuffer(size * count); 124 | Array.Copy(encoding.GetBytes(value, offset, count), 0, _buffer, 0, count * size); 125 | WriteBuffer(size * count, size); 126 | } 127 | 128 | private static int GetEncodingSize(Encoding encoding) 129 | { 130 | if (encoding.Equals(Encoding.UTF8) || encoding.Equals(Encoding.ASCII)) 131 | return 1; 132 | if (encoding.Equals(Encoding.Unicode) || encoding.Equals(Encoding.BigEndianUnicode)) 133 | return 2; 134 | 135 | return 1; 136 | } 137 | 138 | /// 139 | /// Writes an optionally null terminated string to the stream in a specified encoding 140 | /// 141 | /// The string to write to the stream 142 | /// The encoding to encode the string in 143 | /// Whether to null-terminate the string or not 144 | public void Write(string value, Encoding encoding, bool nullTerminated) 145 | { 146 | Write(value.ToCharArray(), 0, value.Length, encoding); 147 | if (nullTerminated) 148 | Write('\0', encoding); 149 | } 150 | 151 | /// 152 | /// Writes an unmanaged object of type T to the stream 153 | /// 154 | /// The object to write 155 | /// The type of that object 156 | public unsafe void Write(T value) where T : unmanaged 157 | { 158 | int size = sizeof(T); 159 | CreateBuffer(size); 160 | MemoryMarshal.Write(_buffer, ref value); 161 | WriteBuffer(size, size); 162 | } 163 | 164 | /// 165 | /// Writes an array of unmanaged type T 166 | /// 167 | /// The array of objects to write to the stream 168 | /// The unmanaged type of those objects 169 | public unsafe void Write(T[] value) where T : unmanaged 170 | => Write(value.AsSpan()); 171 | 172 | /// 173 | /// Writes a specified number of objects starting at a particular index in a provided array to the stream 174 | /// 175 | /// The array of objects to write from 176 | /// The index to into the array to start writing from 177 | /// The number of objects to write 178 | /// The unmanaged type of the objects 179 | public unsafe void Write(T[] value, int offset, int count) where T : unmanaged 180 | => Write(value.AsSpan(offset, count)); 181 | 182 | /// 183 | /// Writes a span of objects to the stream 184 | /// 185 | /// The ReadOnlySpan of objects 186 | /// The unamanged type of the objects 187 | public unsafe void Write(ReadOnlySpan value) where T : unmanaged 188 | { 189 | int size = sizeof(T); 190 | 191 | if (!Reverse || size == 1) 192 | { 193 | BaseStream.Write(MemoryMarshal.Cast(value)); 194 | return; 195 | } 196 | 197 | CreateBuffer(size * value.Length); 198 | MemoryMarshal.Cast(value).CopyTo(_buffer); 199 | WriteBuffer(size * value.Length, size); 200 | } 201 | 202 | /// 203 | /// Writes an fx16 (a 16-bit fixed point number used on the Nintendo DS instead of floating point math) 204 | /// 205 | /// A double value to be reinterpreted as an fx16 and written to the stream 206 | public void WriteFx16(double value) 207 | { 208 | Write((short)Math.Round(value * 4096d)); 209 | } 210 | 211 | /// 212 | /// Writes an array of fx16s (a 16-bit fixed point number used on the Nintendo DS instead of floating point math) 213 | /// 214 | /// An array of double values to be reinterpreted as fx16s and written to the stream 215 | public void WriteFx16s(ReadOnlySpan values) 216 | { 217 | for (int i = 0; i < values.Length; i++) 218 | WriteFx16(values[i]); 219 | } 220 | 221 | /// 222 | /// Writes an fx32 (a 32-bit fixed point number used on the Nintendo DS instead of floating point math) 223 | /// 224 | /// A double value to be reinterpreted as an fx32 and written to the stream 225 | public void WriteFx32(double value) 226 | { 227 | Write((int)Math.Round(value * 4096d)); 228 | } 229 | 230 | /// 231 | /// Writes an array of fx32s (a 32-bit fixed point number used on the Nintendo DS instead of floating point math) 232 | /// 233 | /// An array of double values to be reinterpreted as fx32s and written to the stream 234 | public void WriteFx32s(ReadOnlySpan values) 235 | { 236 | for (int i = 0; i < values.Length; i++) 237 | WriteFx32(values[i]); 238 | } 239 | 240 | /// 241 | /// Closes the stream and disposes of the writer 242 | /// 243 | public void Close() 244 | { 245 | Dispose(); 246 | } 247 | 248 | /// 249 | /// Closes the stream and disposes of the writer 250 | /// 251 | public void Dispose() 252 | { 253 | Dispose(true); 254 | GC.SuppressFinalize(this); 255 | } 256 | 257 | /// 258 | /// Disposes of the writer optionally not closing the base stream 259 | /// 260 | /// If true, closes the stream 261 | protected virtual void Dispose(bool disposing) 262 | { 263 | if (Disposed) 264 | return; 265 | 266 | if (disposing) 267 | BaseStream?.Close(); 268 | 269 | _buffer = null; 270 | Disposed = true; 271 | } 272 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Endianness.cs: -------------------------------------------------------------------------------- 1 | namespace HaroohieClub.NitroPacker.IO; 2 | 3 | /// 4 | /// Denotes endianness 5 | /// 6 | public enum Endianness 7 | { 8 | /// 9 | /// Big endian 10 | /// 11 | BigEndian, 12 | /// 13 | /// Little endian 14 | /// 15 | LittleEndian 16 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/IOUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace HaroohieClub.NitroPacker.IO; 7 | 8 | internal static class IOUtil 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static short ReadS16Le(byte[] data, int offset) 12 | => BinaryPrimitives.ReadInt16LittleEndian(data.AsSpan(offset)); 13 | 14 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 15 | public static short ReadS16Le(ReadOnlySpan span) 16 | => BinaryPrimitives.ReadInt16LittleEndian(span); 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public static short[] ReadS16Le(byte[] data, int offset, int count) 20 | => ReadS16Le(data.AsSpan(offset), count); 21 | 22 | public static short[] ReadS16Le(ReadOnlySpan data, int count) 23 | { 24 | short[] res = MemoryMarshal.Cast(data.Slice(0, count * 2)).ToArray(); 25 | if (!BitConverter.IsLittleEndian) 26 | { 27 | for (int i = 0; i < count; i++) 28 | res[i] = BinaryPrimitives.ReverseEndianness(res[i]); 29 | } 30 | 31 | return res; 32 | } 33 | 34 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 35 | public static void WriteS16Le(byte[] data, int offset, short value) 36 | => BinaryPrimitives.WriteInt16LittleEndian(data.AsSpan(offset), value); 37 | 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public static void WriteS16Le(Span span, short value) 40 | => BinaryPrimitives.WriteInt16LittleEndian(span, value); 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public static void WriteS16Le(byte[] data, int offset, ReadOnlySpan values) 44 | => WriteS16Le(data.AsSpan(offset), values); 45 | 46 | public static void WriteS16Le(Span data, ReadOnlySpan values) 47 | { 48 | var dst = MemoryMarshal.Cast(data); 49 | values.CopyTo(dst); 50 | if (!BitConverter.IsLittleEndian) 51 | { 52 | for (int i = 0; i < values.Length; i++) 53 | dst[i] = BinaryPrimitives.ReverseEndianness(dst[i]); 54 | } 55 | } 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public static short ReadS16Be(byte[] data, int offset) 59 | => BinaryPrimitives.ReadInt16BigEndian(data.AsSpan(offset)); 60 | 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | public static short ReadS16Be(ReadOnlySpan span) 63 | => BinaryPrimitives.ReadInt16BigEndian(span); 64 | 65 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 66 | public static ushort[] ReadU16Le(byte[] data, int offset, int count) 67 | => ReadU16Le(data.AsSpan(offset), count); 68 | 69 | public static ushort[] ReadU16Le(ReadOnlySpan data, int count) 70 | { 71 | ushort[] res = MemoryMarshal.Cast(data.Slice(0, count * 2)).ToArray(); 72 | if (!BitConverter.IsLittleEndian) 73 | { 74 | for (int i = 0; i < count; i++) 75 | res[i] = BinaryPrimitives.ReverseEndianness(res[i]); 76 | } 77 | 78 | return res; 79 | } 80 | 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public static ushort ReadU16Le(byte[] data, int offset) 83 | => BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(offset)); 84 | 85 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 86 | public static ushort ReadU16Le(ReadOnlySpan span) 87 | => BinaryPrimitives.ReadUInt16LittleEndian(span); 88 | 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | public static void WriteU16Le(byte[] data, int offset, ushort value) 91 | => BinaryPrimitives.WriteUInt16LittleEndian(data.AsSpan(offset), value); 92 | 93 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 94 | public static void WriteU16Le(Span span, ushort value) 95 | => BinaryPrimitives.WriteUInt16LittleEndian(span, value); 96 | 97 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 98 | public static void WriteU16Le(byte[] data, int offset, ReadOnlySpan values) 99 | => WriteU16Le(data.AsSpan(offset), values); 100 | 101 | public static void WriteU16Le(Span data, ReadOnlySpan values) 102 | { 103 | var dst = MemoryMarshal.Cast(data); 104 | values.CopyTo(dst); 105 | if (!BitConverter.IsLittleEndian) 106 | { 107 | for (int i = 0; i < values.Length; i++) 108 | dst[i] = BinaryPrimitives.ReverseEndianness(dst[i]); 109 | } 110 | } 111 | 112 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 113 | public static ushort ReadU16Be(byte[] data, int offset) 114 | => BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(offset)); 115 | 116 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 117 | public static ushort ReadU16Be(ReadOnlySpan span) 118 | => BinaryPrimitives.ReadUInt16BigEndian(span); 119 | 120 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 121 | public static void WriteU16Be(byte[] data, int offset, ushort value) 122 | => BinaryPrimitives.WriteUInt16BigEndian(data.AsSpan(offset), value); 123 | 124 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 125 | public static void WriteU16Be(Span span, ushort value) 126 | => BinaryPrimitives.WriteUInt16BigEndian(span, value); 127 | 128 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 129 | public static uint ReadU24Le(byte[] data, int offset) 130 | => (uint)(data[offset] | data[offset + 1] << 8 | data[offset + 2] << 16); 131 | 132 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 133 | public static uint ReadU24Le(ReadOnlySpan span) 134 | => (uint)(span[0] | span[1] << 8 | span[2] << 16); 135 | 136 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 137 | public static uint ReadU32Le(byte[] data, int offset) 138 | => BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(offset)); 139 | 140 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 141 | public static uint ReadU32Le(ReadOnlySpan span) 142 | => BinaryPrimitives.ReadUInt32LittleEndian(span); 143 | 144 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 145 | public static uint[] ReadU32Le(byte[] data, int offset, int count) 146 | => ReadU32Le(data.AsSpan(offset), count); 147 | 148 | public static uint[] ReadU32Le(ReadOnlySpan data, int count) 149 | { 150 | uint[] res = MemoryMarshal.Cast(data.Slice(0, count * 4)).ToArray(); 151 | if (!BitConverter.IsLittleEndian) 152 | { 153 | for (int i = 0; i < count; i++) 154 | res[i] = BinaryPrimitives.ReverseEndianness(res[i]); 155 | } 156 | 157 | return res; 158 | } 159 | 160 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 161 | public static uint ReadU32Be(byte[] data, int offset) 162 | => BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset)); 163 | 164 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 165 | public static uint ReadU32Be(ReadOnlySpan span) 166 | => BinaryPrimitives.ReadUInt32BigEndian(span); 167 | 168 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 169 | public static void WriteU32Le(byte[] data, int offset, uint value) 170 | => BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(offset), value); 171 | 172 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 173 | public static void WriteU32Le(Span span, uint value) 174 | => BinaryPrimitives.WriteUInt32LittleEndian(span, value); 175 | 176 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 177 | public static void WriteU32Le(byte[] data, int offset, ReadOnlySpan values) 178 | => WriteU32Le(data.AsSpan(offset), values); 179 | 180 | public static void WriteU32Le(Span data, ReadOnlySpan values) 181 | { 182 | var dst = MemoryMarshal.Cast(data); 183 | values.CopyTo(dst); 184 | if (!BitConverter.IsLittleEndian) 185 | { 186 | for (int i = 0; i < values.Length; i++) 187 | dst[i] = BinaryPrimitives.ReverseEndianness(dst[i]); 188 | } 189 | } 190 | 191 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 192 | public static ulong ReadU64Le(byte[] data, int offset) 193 | => BinaryPrimitives.ReadUInt64LittleEndian(data.AsSpan(offset)); 194 | 195 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 196 | public static ulong ReadU64Le(ReadOnlySpan span) 197 | => BinaryPrimitives.ReadUInt64LittleEndian(span); 198 | 199 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 200 | public static ulong ReadU64Be(byte[] data, int offset) 201 | => BinaryPrimitives.ReadUInt64BigEndian(data.AsSpan(offset)); 202 | 203 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 204 | public static ulong ReadU64Be(ReadOnlySpan span) 205 | => BinaryPrimitives.ReadUInt64BigEndian(span); 206 | 207 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 208 | public static void WriteU64Le(byte[] data, int offset, ulong value) 209 | => BinaryPrimitives.WriteUInt64LittleEndian(data.AsSpan(offset), value); 210 | 211 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 212 | public static void WriteU64Le(Span span, ulong value) 213 | => BinaryPrimitives.WriteUInt64LittleEndian(span, value); 214 | 215 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 216 | public static void WriteU64Be(byte[] data, int offset, ulong value) 217 | => BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan(offset), value); 218 | 219 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 220 | public static void WriteU64Be(Span span, ulong value) 221 | => BinaryPrimitives.WriteUInt64BigEndian(span, value); 222 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Pointer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace HaroohieClub.NitroPacker.IO; 5 | 6 | internal readonly struct Pointer : IEquatable> 7 | { 8 | public readonly T[] Array; 9 | public readonly int Index; 10 | 11 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 12 | public Pointer(T[] array, int index = 0) 13 | { 14 | Array = array; 15 | Index = index; 16 | } 17 | 18 | public ref T this[int offset] 19 | { 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | get => ref Array[Index + offset]; 22 | } 23 | 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public static Pointer operator ++(Pointer ptr) 26 | => new(ptr.Array, ptr.Index + 1); 27 | 28 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 29 | public static Pointer operator --(Pointer ptr) 30 | => new(ptr.Array, ptr.Index - 1); 31 | 32 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 33 | public static Pointer operator +(Pointer ptr, int offset) 34 | => new(ptr.Array, ptr.Index + offset); 35 | 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static Pointer operator -(Pointer ptr, int offset) 38 | => new(ptr.Array, ptr.Index - offset); 39 | 40 | public bool Equals(Pointer other) 41 | => Array == other.Array && Index == other.Index; 42 | 43 | public override bool Equals(object obj) 44 | => obj == null && Array == null || obj is Pointer other && Equals(other); 45 | 46 | public override int GetHashCode() 47 | => HashCode.Combine(Array, Index); 48 | 49 | public static bool operator ==(Pointer left, Pointer right) 50 | => left.Equals(right); 51 | 52 | public static bool operator !=(Pointer left, Pointer right) 53 | => !left.Equals(right); 54 | 55 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 56 | public static implicit operator Pointer(T[] array) => new(array); 57 | 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | public static implicit operator Span(Pointer ptr) => ptr.Array.AsSpan(ptr.Index); 60 | 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | public static implicit operator ReadOnlySpan(Pointer ptr) => ptr.Array.AsSpan(ptr.Index); 63 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/AlignAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// An attribute indicating the alignment of the property 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 9 | public sealed class AlignAttribute : Attribute 10 | { 11 | /// 12 | /// Byte alignment 13 | /// 14 | public int Alignment { get; } 15 | 16 | /// 17 | /// Specifies the alignment of the attribute 18 | /// 19 | /// The byte-alignment 20 | public AlignAttribute(int alignment) 21 | { 22 | Alignment = alignment; 23 | } 24 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/ArraySizeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// Attribute indicating the size of an array for binary serialization 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 9 | public sealed class ArraySizeAttribute : Attribute 10 | { 11 | /// 12 | /// The fixed size of an array 13 | /// 14 | public int FixedSize { get; } 15 | 16 | /// 17 | /// Specifies 18 | /// 19 | /// 20 | public ArraySizeAttribute(int fixedSize) 21 | { 22 | FixedSize = fixedSize; 23 | } 24 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/ConstantAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// Attribute for constants that should be serialized 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 9 | public sealed class ConstantAttribute : Attribute 10 | { 11 | /// 12 | /// The value of the constant 13 | /// 14 | public object Value { get; } 15 | 16 | /// 17 | /// Establishes a constant with a value 18 | /// 19 | /// The value of the constant 20 | public ConstantAttribute(object value) 21 | { 22 | Value = value; 23 | } 24 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/Fx16Attribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// Indicates that this property should be serialized as an fx16 fixed-point integer 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 9 | public sealed class Fx16Attribute : Attribute 10 | { 11 | /// 12 | /// Constructs the attribute 13 | /// 14 | public Fx16Attribute() { } 15 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/Fx32Attribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// Indicates that this property should be serialized as an fx32 fixed-point integer 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 9 | public sealed class Fx32Attribute : Attribute 10 | { 11 | /// 12 | /// Constructs the property 13 | /// 14 | public Fx32Attribute() { } 15 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/IgnoreAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// Indicates that this property should not be serialized 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 9 | public sealed class IgnoreAttribute : Attribute 10 | { 11 | /// 12 | /// Constructs the attribute 13 | /// 14 | public IgnoreAttribute() { } 15 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/PropertyAlignmentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// Alignment of properties 7 | /// 8 | public enum PropertyAlignment 9 | { 10 | /// 11 | /// The properties should be packed together (not aligned) 12 | /// 13 | Packed, 14 | /// 15 | /// The properties should be aligned to the field size 16 | /// 17 | FieldSize, 18 | } 19 | 20 | /// 21 | /// Indicates the alignment of this class or struct 22 | /// 23 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)] 24 | public sealed class PropertyAlignmentAttribute : Attribute 25 | { 26 | /// 27 | /// The alignment of the class or struct 28 | /// 29 | public PropertyAlignment Alignment { get; } 30 | 31 | /// 32 | /// Constructs the property 33 | /// 34 | /// The alignment of properties in this class or struct 35 | public PropertyAlignmentAttribute(PropertyAlignment alignment) 36 | { 37 | Alignment = alignment; 38 | } 39 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/ReferenceAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// The reference type of the property 7 | /// 8 | public enum ReferenceType 9 | { 10 | /// 11 | /// Absolute references 12 | /// 13 | Absolute, 14 | /// 15 | /// Relative to a given chunk 16 | /// 17 | ChunkRelative, 18 | /// 19 | /// Relative to the properties 20 | /// 21 | PropertyRelative, 22 | } 23 | 24 | /// 25 | /// Defines a reference property 26 | /// 27 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 28 | public sealed class ReferenceAttribute : Attribute 29 | { 30 | /// 31 | /// The reference type 32 | /// 33 | public ReferenceType Type { get; } 34 | /// 35 | /// The tye of property pointed to 36 | /// 37 | public PropertyType PointerPropertyType { get; } 38 | /// 39 | /// The offset 40 | /// 41 | public int Offset { get; } 42 | 43 | /// 44 | /// Constructs the attribute 45 | /// 46 | /// The type 47 | /// The type pointed to 48 | /// The offset 49 | public ReferenceAttribute(ReferenceType type, PropertyType pointerPropertyType = PropertyType.U32, int offset = 0) 50 | { 51 | Type = type; 52 | PointerPropertyType = pointerPropertyType; 53 | Offset = offset; 54 | } 55 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/SerializationUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | 8 | namespace HaroohieClub.NitroPacker.IO.Serialization; 9 | 10 | internal static class SerializationUtil 11 | { 12 | public static PropertyType TypeToPropertyType(Type type) 13 | { 14 | if (type == typeof(byte)) 15 | return PropertyType.U8; 16 | if (type == typeof(sbyte)) 17 | return PropertyType.S8; 18 | if (type == typeof(ushort)) 19 | return PropertyType.U16; 20 | if (type == typeof(short)) 21 | return PropertyType.S16; 22 | if (type == typeof(uint)) 23 | return PropertyType.U32; 24 | if (type == typeof(int)) 25 | return PropertyType.S32; 26 | if (type == typeof(ulong)) 27 | return PropertyType.U64; 28 | if (type == typeof(long)) 29 | return PropertyType.S64; 30 | 31 | throw new("Unexpected primitive field type " + type.Name); 32 | } 33 | 34 | public static Type FieldTypeToType(PropertyType type) => type switch 35 | { 36 | PropertyType.U8 => typeof(byte), 37 | PropertyType.S8 => typeof(sbyte), 38 | PropertyType.U16 => typeof(ushort), 39 | PropertyType.S16 => typeof(short), 40 | PropertyType.U32 => typeof(uint), 41 | PropertyType.S32 => typeof(int), 42 | PropertyType.U64 => typeof(ulong), 43 | PropertyType.S64 => typeof(long), 44 | PropertyType.Fx16 => typeof(double), 45 | PropertyType.Fx32 => typeof(double), 46 | PropertyType.Float => typeof(float), 47 | PropertyType.Double => typeof(double), 48 | _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) 49 | }; 50 | 51 | public static int GetTypeSize(PropertyType type) => type switch 52 | { 53 | PropertyType.U8 => 1, 54 | PropertyType.S8 => 1, 55 | PropertyType.U16 => 2, 56 | PropertyType.S16 => 2, 57 | PropertyType.U32 => 4, 58 | PropertyType.S32 => 4, 59 | PropertyType.U64 => 8, 60 | PropertyType.S64 => 8, 61 | PropertyType.Fx16 => 2, 62 | PropertyType.Fx32 => 4, 63 | PropertyType.Float => 4, 64 | PropertyType.Double => 8, 65 | _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) 66 | }; 67 | 68 | public static bool HasPrimitiveType(PropertyInfo property) 69 | { 70 | return property.PropertyType.IsPrimitive || 71 | property.PropertyType.IsEnum || 72 | property.GetCustomAttribute() != null || 73 | property.GetCustomAttribute() != null || 74 | property.GetCustomAttribute() != null; 75 | } 76 | 77 | public static bool HasPrimitiveArrayType(PropertyInfo property) 78 | { 79 | if (!property.PropertyType.IsArray) 80 | return false; 81 | 82 | Type type = property.PropertyType.GetElementType(); 83 | 84 | return type.IsPrimitive || 85 | property.GetCustomAttribute() != null || 86 | property.GetCustomAttribute() != null || 87 | property.GetCustomAttribute() != null; 88 | } 89 | 90 | public static PropertyType GetPropertyPrimitiveType(PropertyInfo property) 91 | { 92 | bool isFx32 = property.GetCustomAttribute() != null; 93 | bool isFx16 = property.GetCustomAttribute() != null; 94 | bool hasFieldType = property.GetCustomAttribute() != null; 95 | int count = (isFx32 ? 1 : 0) + (isFx16 ? 1 : 0) + (hasFieldType ? 1 : 0); 96 | if (count > 1) 97 | throw new("More than one property type specified for property " + property.Name + " in type " + 98 | property.DeclaringType?.Name); 99 | 100 | if (isFx32) 101 | return PropertyType.Fx32; 102 | if (isFx16) 103 | return PropertyType.Fx16; 104 | if (hasFieldType) 105 | return property.GetCustomAttribute().Type; 106 | if (property.PropertyType.IsArray) 107 | { 108 | Type elemType = property.PropertyType.GetElementType(); 109 | if (elemType.IsEnum) 110 | return TypeToPropertyType(elemType.GetEnumUnderlyingType()); 111 | return TypeToPropertyType(elemType); 112 | } 113 | 114 | if (property.PropertyType.IsEnum) 115 | return TypeToPropertyType(property.PropertyType.GetEnumUnderlyingType()); 116 | 117 | return TypeToPropertyType(property.PropertyType); 118 | } 119 | 120 | public static PropertyType GetVectorPrimitiveType(PropertyInfo property) 121 | { 122 | bool isFx32 = property.GetCustomAttribute() != null; 123 | bool isFx16 = property.GetCustomAttribute() != null; 124 | bool hasPropertyType = property.GetCustomAttribute() != null; 125 | int count = (isFx32 ? 1 : 0) + (isFx16 ? 1 : 0) + (hasPropertyType ? 1 : 0); 126 | if (count > 1) 127 | throw new("More than one property type specified for property " + property.Name + " in type " + 128 | property.DeclaringType?.Name); 129 | 130 | if (isFx32) 131 | return PropertyType.Fx32; 132 | if (isFx16) 133 | return PropertyType.Fx16; 134 | if (hasPropertyType) 135 | return property.GetCustomAttribute().Type; 136 | return PropertyType.Float; 137 | } 138 | 139 | public static IEnumerable GetPropertiesInOrder() 140 | => GetPropertiesInOrder(typeof(T)); 141 | 142 | public static IEnumerable GetPropertiesInOrder(Type type) 143 | { 144 | //Sorting by MetadataToken works, but may not be future-proof 145 | return type.GetProperties() 146 | .Where(f => f.GetCustomAttribute() == null) 147 | .OrderBy(f => f.MetadataToken); 148 | } 149 | 150 | public static PropertyAlignment GetPropertyAlignment() 151 | => GetPropertyAlignment(typeof(T)); 152 | 153 | public static PropertyAlignment GetPropertyAlignment(Type type) 154 | => type.GetCustomAttribute()?.Alignment ?? PropertyAlignment.Packed; 155 | 156 | private static readonly ConcurrentDictionary<(Type, Type), Delegate> CastCache = new(); 157 | 158 | public static T Cast(object data) 159 | => (T)Cast(data, typeof(T)); 160 | 161 | public static object Cast(object data, Type type) 162 | { 163 | Type inType = data.GetType(); 164 | 165 | if (inType == type) 166 | return data; 167 | 168 | if (CastCache.TryGetValue((inType, type), out Delegate func)) 169 | return func.DynamicInvoke(data); 170 | 171 | ParameterExpression dataParam = Expression.Parameter(data.GetType()); 172 | Delegate run = Expression.Lambda(Expression.Convert(dataParam, type), dataParam).Compile(); 173 | 174 | CastCache.TryAdd((inType, type), run); 175 | 176 | return run.DynamicInvoke(data); 177 | } 178 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/Serialization/TypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.IO.Serialization; 4 | 5 | /// 6 | /// The property type 7 | /// 8 | public enum PropertyType 9 | { 10 | /// 11 | /// Unsigned 8-bit integer 12 | /// 13 | U8, 14 | /// 15 | /// Signed 8-bit integer 16 | /// 17 | S8, 18 | /// 19 | /// Unsigned 16-bit integer 20 | /// 21 | U16, 22 | /// 23 | /// Signed 16-bit integer 24 | /// 25 | S16, 26 | /// 27 | /// Unsigned 32-bit integer 28 | /// 29 | U32, 30 | /// 31 | /// Signed 32-bit integer 32 | /// 33 | S32, 34 | /// 35 | /// Unsigned 64-bit integer 36 | /// 37 | U64, 38 | /// 39 | /// Signed 64-bit integer 40 | /// 41 | S64, 42 | /// 43 | /// 16-bit fixed point value 44 | /// 45 | Fx16, 46 | /// 47 | /// 32-bit fixed point value 48 | /// 49 | Fx32, 50 | /// 51 | /// Float 52 | /// 53 | Float, 54 | /// 55 | /// Double 56 | /// 57 | Double 58 | } 59 | 60 | /// 61 | /// Marks the type of this property 62 | /// 63 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 64 | public class TypeAttribute : Attribute 65 | { 66 | /// 67 | /// The type 68 | /// 69 | public PropertyType Type { get; } 70 | 71 | /// 72 | /// Constructs the attribute 73 | /// 74 | /// The type 75 | public TypeAttribute(PropertyType type) 76 | { 77 | Type = type; 78 | } 79 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/IO/SignatureNotCorrectException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace HaroohieClub.NitroPacker.IO; 5 | 6 | internal class SignatureNotCorrectException : Exception 7 | { 8 | public string Signature { get; } 9 | public string Expected { get; } 10 | public long Offset { get; } 11 | 12 | public SignatureNotCorrectException(uint signature, uint expected, long offset) 13 | : this( 14 | Encoding.ASCII.GetString( 15 | new byte[] 16 | { 17 | (byte) (signature & 0xFF), (byte) (signature >> 8 & 0xFF), 18 | (byte) (signature >> 16 & 0xFF), (byte) (signature >> 24 & 0xFF) 19 | }), 20 | Encoding.ASCII.GetString( 21 | new byte[] 22 | { 23 | (byte) (expected & 0xFF), (byte) (expected >> 8 & 0xFF), 24 | (byte) (expected >> 16 & 0xFF), (byte) (expected >> 24 & 0xFF) 25 | }), 26 | offset 27 | ) 28 | { } 29 | 30 | 31 | public SignatureNotCorrectException(string signature, string expected, long offset) 32 | : base("Signature '" + signature + "' at 0x" + offset.ToString("X8") + " does not match '" + 33 | expected + "'.") 34 | { 35 | Signature = signature; 36 | Expected = expected; 37 | Offset = offset; 38 | } 39 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/BinaryBlockHeader.cs: -------------------------------------------------------------------------------- 1 | using HaroohieClub.NitroPacker.IO; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro; 4 | 5 | internal class BinaryBlockHeader 6 | { 7 | public BinaryBlockHeader(uint signature) 8 | { 9 | Kind = signature; 10 | } 11 | 12 | public BinaryBlockHeader(EndianBinaryReader er) 13 | { 14 | Kind = er.Read(); 15 | Size = er.Read(); 16 | } 17 | 18 | public void Write(EndianBinaryWriterEx er) 19 | { 20 | er.Write(Kind); 21 | er.Write(Size); 22 | } 23 | 24 | public uint Kind; 25 | public uint Size; 26 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/BinaryFileHeader.cs: -------------------------------------------------------------------------------- 1 | using HaroohieClub.NitroPacker.IO; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro; 4 | 5 | internal class BinaryFileHeader 6 | { 7 | public BinaryFileHeader(uint signature, int nrBlocks) 8 | { 9 | Signature = signature; 10 | ByteOrder = 0xFEFF; 11 | Version = 0x100; 12 | HeaderSize = 0x10; 13 | DataBlocks = (ushort)nrBlocks; 14 | } 15 | 16 | public BinaryFileHeader(EndianBinaryReader er) 17 | { 18 | Signature = er.Read(); 19 | ByteOrder = er.Read(); 20 | Version = er.Read(); 21 | FileSize = er.Read(); 22 | HeaderSize = er.Read(); 23 | DataBlocks = er.Read(); 24 | } 25 | 26 | public void Write(EndianBinaryWriterEx er) 27 | { 28 | er.Write(Signature); 29 | er.Write(ByteOrder); 30 | er.Write(Version); 31 | er.Write((uint)0); 32 | er.Write((ushort)0x10); 33 | er.Write(DataBlocks); 34 | } 35 | 36 | public uint Signature; 37 | public ushort ByteOrder; 38 | public ushort Version; 39 | public uint FileSize; 40 | public ushort HeaderSize; 41 | public ushort DataBlocks; 42 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Banners/Banner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Text.Json.Serialization; 4 | using System.Xml.Serialization; 5 | using HaroohieClub.NitroPacker.IO; 6 | using HaroohieClub.NitroPacker.IO.Serialization; 7 | using HaroohieClub.NitroPacker.Nitro.Gx; 8 | 9 | namespace HaroohieClub.NitroPacker.Nitro.Card.Banners; 10 | 11 | /// 12 | /// Abstract class for banners used in the ROM header 13 | /// 14 | public abstract class Banner 15 | { 16 | /// 17 | /// The version of this banner (0x001, 0x002, 0x003, 0x103) 18 | /// 19 | public int Version { get; set; } 20 | 21 | /// 22 | /// Constructs an empty banner, used for serialization 23 | /// 24 | public Banner() { } 25 | 26 | /// 27 | /// Constructs a banner using an endian binary reader 28 | /// 29 | /// with initialized stream 30 | public Banner(EndianBinaryReader er) 31 | { 32 | } 33 | 34 | /// 35 | /// Writes the banner using an endian binary writer 36 | /// 37 | /// with initialized stream 38 | public virtual void Write(EndianBinaryWriter ew) 39 | { 40 | ew.Write(Image, 0, 32 * 32 / 2); 41 | ew.Write(Palette, 0, 16 * 2); 42 | foreach (string s in GameName) 43 | { 44 | ew.Write(GameName[0].PadRight(128, '\0'), Encoding.Unicode, false); 45 | } 46 | } 47 | 48 | /// 49 | /// The image contained in the banner 50 | /// 51 | [ArraySize(32 * 32 / 2)] 52 | public byte[] Image { get; set; } 53 | 54 | /// 55 | /// The palette for the banner's 56 | /// 57 | [ArraySize(16 * 2)] 58 | [XmlElement("Pltt")] 59 | public byte[] Palette { get; set; } 60 | 61 | /// 62 | /// The array of game titles that appear in the banner depending on region 63 | /// The languages are: 64 | /// 0: Japanese 65 | /// 1: English 66 | /// 2: French 67 | /// 3: German 68 | /// 4: Italian 69 | /// 5: Spanish 70 | /// 6: Chinese ( and up) 71 | /// 7: Korean (v0x003 and up) 72 | /// 73 | [JsonIgnore] 74 | [XmlIgnore] 75 | public string[] GameName { get; set; } 76 | 77 | /// 78 | /// Base 64 encoded versions of 79 | /// 80 | [XmlElement("GameName")] 81 | [JsonPropertyName("GameNames")] 82 | public virtual string[] Base64GameName 83 | { 84 | get 85 | { 86 | string[] b = new string[GameName.Length]; 87 | for (int i = 0; i < b.Length; i++) 88 | { 89 | b[i] = Convert.ToBase64String(Encoding.Unicode.GetBytes(GameName[i])); 90 | } 91 | 92 | return b; 93 | } 94 | set 95 | { 96 | GameName = new string[value.Length]; 97 | for (int i = 0; i < GameName.Length; i++) 98 | { 99 | GameName[i] = Encoding.Unicode.GetString(Convert.FromBase64String(value[i])); 100 | } 101 | } 102 | } 103 | 104 | /// 105 | /// Gets the CRC16 hashes associated with this banner 106 | /// 107 | /// An array of up to four CRC16 hashes 108 | public virtual ushort[] GetCrcs() 109 | { 110 | return new ushort[4]; 111 | } 112 | 113 | /// 114 | /// Gets an RGBA bitmap representation of the icon 115 | /// 116 | /// An RGBA8 Bitmap representing the icon 117 | public Rgba8Bitmap GetIcon() => GxUtil.DecodeChar(Image, Palette, ImageFormat.Pltt16, 32, 32, true); 118 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Banners/BannerV1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using HaroohieClub.NitroPacker.IO; 4 | using HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 5 | 6 | namespace HaroohieClub.NitroPacker.Nitro.Card.Banners; 7 | 8 | /// 9 | /// An implementation of banner version 0x001 10 | /// 11 | public class BannerV1 : Banner 12 | { 13 | /// 14 | public BannerV1() 15 | { 16 | } 17 | 18 | /// 19 | public BannerV1(EndianBinaryReader er) : base(er) 20 | { 21 | Image = er.Read(32 * 32 / 2); 22 | Palette = er.Read(16 * 2); 23 | GameName = new string[6]; 24 | for (int i = 0; i < GameName.Length; i++) 25 | { 26 | GameName[i] = er.ReadString(Encoding.Unicode, 128).Replace("\0", ""); 27 | } 28 | } 29 | 30 | /// 31 | public override ushort[] GetCrcs() 32 | { 33 | byte[] data = new byte[0x820]; 34 | Array.Copy(Image, data, 512); 35 | Array.Copy(Palette, 0, data, 512, 32); 36 | for (int i = 0; i < 6; i++) 37 | { 38 | Array.Copy(Encoding.Unicode.GetBytes(GameName[i].PadRight(128, '\0')), 0, data, 544 + 256 * i, 256); 39 | } 40 | return [Crc16.GetCrc16(data), 0, 0, 0]; 41 | } 42 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Banners/BannerV103.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using HaroohieClub.NitroPacker.IO; 4 | using HaroohieClub.NitroPacker.IO.Serialization; 5 | using HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 6 | using HaroohieClub.NitroPacker.Nitro.Gx; 7 | 8 | namespace HaroohieClub.NitroPacker.Nitro.Card.Banners; 9 | 10 | /// 11 | /// An implementation of banner version 0x0103 (used in DSi games) 12 | /// 13 | public class BannerV103 : BannerV3 14 | { 15 | /// 16 | /// The individual frames that make up the animated image in the banner 17 | /// 18 | [ArraySize(32 * 32 / 2 * 8)] 19 | public byte[] AnimationBitmaps { get; set; } 20 | 21 | /// 22 | /// The palettes for each of the banner's 23 | /// 24 | [ArraySize(16 * 2 * 8)] 25 | public byte[] AnimationPalettes { get; set; } 26 | 27 | /// 28 | /// The tokens that define the animation sequence frames 29 | /// 30 | [ArraySize(64 * 2)] 31 | public byte[] AnimationSequences { get; set; } 32 | 33 | /// 34 | public BannerV103() 35 | { 36 | } 37 | 38 | /// 39 | public BannerV103(EndianBinaryReader er) : base(er) 40 | { 41 | er.Skip(0x800); 42 | AnimationBitmaps = er.Read(0x1000); 43 | AnimationPalettes = er.Read(0x100); 44 | AnimationSequences = er.Read(0x80); 45 | } 46 | 47 | /// 48 | public override void Write(EndianBinaryWriter ew) 49 | { 50 | base.Write(ew); 51 | ew.Skip(0x800); 52 | ew.Write(AnimationBitmaps); 53 | ew.Write(AnimationPalettes); 54 | ew.Write(AnimationSequences); 55 | } 56 | 57 | /// 58 | public override ushort[] GetCrcs() 59 | { 60 | ushort[] crcs = base.GetCrcs(); 61 | 62 | byte[] data = new byte[0x1180]; 63 | Array.Copy(AnimationBitmaps, data, AnimationBitmaps.Length); 64 | Array.Copy(AnimationPalettes, data, AnimationPalettes.Length); 65 | Array.Copy(AnimationSequences, data, AnimationSequences.Length); 66 | 67 | crcs[3] = Crc16.GetCrc16(data); 68 | return crcs; 69 | } 70 | 71 | /// 72 | /// Gets a series of 8 RGBA bitmaps representing the animation frames 73 | /// 74 | /// The animation frames as 8 RGBA8 bitmaps 75 | public Rgba8Bitmap[] GetAnimatedBitmapFrames() 76 | { 77 | var animationFrames = new Rgba8Bitmap[8]; 78 | for (int i = 0; i < animationFrames.Length; i++) 79 | { 80 | GxUtil.DecodeChar(AnimationBitmaps.AsSpan()[(i * 0x200)..((i + 1) * 0x200)], AnimationPalettes.AsSpan()[(i * 0x20)..((i + 1) * 0x20)], ImageFormat.Pltt16, 32, 32, true); 81 | } 82 | 83 | return animationFrames; 84 | } 85 | 86 | /// 87 | /// Unpacks the icon animation sequence data into an array of objects 88 | /// 89 | /// An array of 64 icon animation sequence objects 90 | public IconAnimationSequence[] GetAnimationSequences() 91 | { 92 | var animationSequences = new IconAnimationSequence[64]; 93 | 94 | for (int i = 0; i < animationSequences.Length; i++) 95 | { 96 | animationSequences[i] = 97 | new(BinaryPrimitives.ReadUInt16LittleEndian(AnimationSequences.AsSpan()[(i * 2)..(i * 2 + 1)])); 98 | } 99 | 100 | return animationSequences; 101 | } 102 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Banners/BannerV2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using HaroohieClub.NitroPacker.IO; 4 | using HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 5 | 6 | namespace HaroohieClub.NitroPacker.Nitro.Card.Banners; 7 | 8 | /// 9 | /// An implementation of banner version 0x002 10 | /// 11 | public class BannerV2 : BannerV1 12 | { 13 | /// 14 | public BannerV2() 15 | { 16 | } 17 | 18 | /// 19 | public BannerV2(EndianBinaryReader er) 20 | { 21 | Image = er.Read(32 * 32 / 2); 22 | Palette = er.Read(16 * 2); 23 | GameName = new string[7]; 24 | for (int i = 0; i < GameName.Length; i++) 25 | { 26 | GameName[i] = er.ReadString(Encoding.Unicode, 128).Replace("\0", ""); 27 | } 28 | } 29 | 30 | /// 31 | public override ushort[] GetCrcs() 32 | { 33 | ushort[] crcs = base.GetCrcs(); 34 | 35 | byte[] data = new byte[0x920]; 36 | Array.Copy(Image, data, 512); 37 | Array.Copy(Palette, 0, data, 512, 32); 38 | for (int i = 0; i < 7; i++) 39 | { 40 | Array.Copy(Encoding.Unicode.GetBytes(GameName[i].PadRight(128, '\0')), 0, data, 544 + 256 * i, 256); 41 | } 42 | 43 | crcs[1] = Crc16.GetCrc16(data); 44 | return crcs; 45 | } 46 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Banners/BannerV3.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using HaroohieClub.NitroPacker.IO; 4 | using HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 5 | 6 | namespace HaroohieClub.NitroPacker.Nitro.Card.Banners; 7 | 8 | /// 9 | /// An implementation of banner version 0x003 10 | /// 11 | public class BannerV3 : BannerV2 12 | { 13 | /// 14 | public BannerV3() 15 | { 16 | } 17 | 18 | /// 19 | public BannerV3(EndianBinaryReader er) 20 | { 21 | Image = er.Read(32 * 32 / 2); 22 | Palette = er.Read(16 * 2); 23 | GameName = new string[8]; 24 | for (int i = 0; i < GameName.Length; i++) 25 | { 26 | GameName[i] = er.ReadString(Encoding.Unicode, 128).Replace("\0", ""); 27 | } 28 | } 29 | 30 | /// 31 | public override ushort[] GetCrcs() 32 | { 33 | ushort[] crcs = base.GetCrcs(); 34 | 35 | byte[] data = new byte[0xA20]; 36 | Array.Copy(Image, data, 512); 37 | Array.Copy(Palette, 0, data, 512, 32); 38 | for (int i = 0; i < 8; i++) 39 | { 40 | Array.Copy(Encoding.Unicode.GetBytes(GameName[i].PadRight(128, '\0')), 0, data, 544 + 256 * i, 256); 41 | } 42 | 43 | crcs[2] = Crc16.GetCrc16(data); 44 | return crcs; 45 | } 46 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Banners/Header.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using HaroohieClub.NitroPacker.IO; 4 | using HaroohieClub.NitroPacker.IO.Serialization; 5 | 6 | namespace HaroohieClub.NitroPacker.Nitro.Card.Banners; 7 | 8 | /// 9 | /// A banner's header 10 | /// 11 | public class Header 12 | { 13 | /// 14 | /// Blank constructor for serialization 15 | /// 16 | public Header() { } 17 | 18 | /// 19 | /// Constructs a banner header with an extended endian binary reader 20 | /// 21 | /// with an initialized stream 22 | public Header(EndianBinaryReaderEx er) 23 | { 24 | er.ReadObject(this); 25 | } 26 | 27 | /// 28 | /// Writes a banner with an extended endian binary writer 29 | /// 30 | /// with an initialized stream 31 | public void Write(EndianBinaryWriterEx er) 32 | { 33 | er.WriteObject(this); 34 | } 35 | 36 | /// 37 | /// The version of the banner (0x0001, 0x0002, 0x0003, or 0x0103) 38 | /// 39 | public ushort Version { get; set; } 40 | 41 | /// 42 | /// Deprecated. Formerly thought to be reserved but is actually part of the 43 | /// 44 | [JsonIgnore] 45 | [Ignore] 46 | [Obsolete("ReservedA is deprecated; please use the Version instead.")] 47 | public byte ReservedA 48 | { 49 | get => BitConverter.GetBytes(Version)[1]; 50 | set => Version = (ushort)((value << 8) | Version); 51 | } 52 | 53 | /// 54 | /// The set of CRC-16 hashes for this banner 55 | /// 56 | [JsonIgnore] 57 | [ArraySize(4)] 58 | public ushort[] Crc16s { get; set; } 59 | 60 | private byte[] _reservedB; 61 | /// 62 | /// Reserved 63 | /// 64 | [ArraySize(0x16)] 65 | public byte[] ReservedB 66 | { 67 | get => _reservedB; 68 | set 69 | { 70 | if (value.Length == 28) 71 | { 72 | // We don't need to set the CRCs here since they're ignored by serialization anyway 73 | _reservedB = value[6..]; 74 | } 75 | else 76 | { 77 | _reservedB = value; 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Cryptography/Blowfish.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HaroohieClub.NitroPacker.IO; 3 | 4 | namespace HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 5 | 6 | internal class Blowfish 7 | { 8 | public const int KeyTableLength = 0x1048; 9 | 10 | public const int PTableEntryCount = 18; 11 | public const int SBoxCount = 4; 12 | public const int SBoxEntryCount = 256; 13 | 14 | private readonly uint[] _pTable; 15 | private readonly uint[][] _sBoxes; 16 | 17 | public Blowfish(uint[] pTable, uint[][] sBoxes) 18 | { 19 | if (pTable.Length != PTableEntryCount) 20 | throw new ArgumentException($"Size of p table should be {PTableEntryCount}", nameof(pTable)); 21 | if (sBoxes.Length != SBoxCount) 22 | throw new ArgumentException($"Number of s boxes should be {SBoxCount}", nameof(sBoxes)); 23 | for (int i = 0; i < SBoxCount; i++) 24 | if (sBoxes[i].Length != SBoxEntryCount) 25 | throw new ArgumentException($"Size of s box {i} should be {SBoxEntryCount}", nameof(sBoxes)); 26 | 27 | _pTable = pTable; 28 | _sBoxes = sBoxes; 29 | } 30 | 31 | public Blowfish(ReadOnlySpan keyTable) 32 | { 33 | if (keyTable == null) 34 | throw new ArgumentNullException(nameof(keyTable)); 35 | if (keyTable.Length < KeyTableLength) 36 | throw new ArgumentException(nameof(keyTable)); 37 | _pTable = IOUtil.ReadU32Le(keyTable, PTableEntryCount); 38 | _sBoxes = new uint[SBoxCount][]; 39 | _sBoxes[0] = IOUtil.ReadU32Le(keyTable[0x48..], SBoxEntryCount); 40 | _sBoxes[1] = IOUtil.ReadU32Le(keyTable[0x448..], SBoxEntryCount); 41 | _sBoxes[2] = IOUtil.ReadU32Le(keyTable[0x848..], SBoxEntryCount); 42 | _sBoxes[3] = IOUtil.ReadU32Le(keyTable[0xC48..], SBoxEntryCount); 43 | } 44 | 45 | public void Encrypt(byte[] data, int offset, int length) 46 | => Encrypt(data.AsSpan(offset, length)); 47 | 48 | public void Encrypt(Span data) 49 | { 50 | if ((data.Length & 7) != 0) 51 | throw new ArgumentException(nameof(data)); 52 | for (int i = 0; i < data.Length; i += 8) 53 | { 54 | ulong val = Encrypt(IOUtil.ReadU64Le(data[i..])); 55 | IOUtil.WriteU64Le(data[i..], val); 56 | } 57 | } 58 | 59 | public ulong Encrypt(ulong val) 60 | { 61 | uint y = (uint)(val & 0xFFFFFFFF); 62 | uint x = (uint)(val >> 32); 63 | for (int i = 0; i < 16; i++) 64 | { 65 | uint z = _pTable[i] ^ x; 66 | uint a = _sBoxes[0][z >> 24 & 0xFF]; 67 | uint b = _sBoxes[1][z >> 16 & 0xFF]; 68 | uint c = _sBoxes[2][z >> 8 & 0xFF]; 69 | uint d = _sBoxes[3][z & 0xFF]; 70 | x = d + (c ^ b + a) ^ y; 71 | y = z; 72 | } 73 | 74 | return x ^ _pTable[16] | (ulong)(y ^ _pTable[17]) << 32; 75 | } 76 | 77 | public void Decrypt(byte[] src, int srcOffset, int length, byte[] dst, int dstOffset) 78 | => Decrypt(src.AsSpan(srcOffset, length), dst.AsSpan(dstOffset, length)); 79 | 80 | public void Decrypt(Span data) 81 | => Decrypt(data, data); 82 | 83 | public void Decrypt(ReadOnlySpan src, Span dst) 84 | { 85 | if ((src.Length & 7) != 0) 86 | throw new ArgumentException(nameof(src)); 87 | if (dst.Length < src.Length) 88 | throw new ArgumentException(nameof(dst)); 89 | for (int i = 0; i < src.Length; i += 8) 90 | { 91 | ulong val = Decrypt(IOUtil.ReadU64Le(src[i..])); 92 | IOUtil.WriteU64Le(dst[i..], val); 93 | } 94 | } 95 | 96 | public ulong Decrypt(ulong val) 97 | { 98 | uint y = (uint)(val & 0xFFFFFFFF); 99 | uint x = (uint)(val >> 32); 100 | for (int i = 17; i >= 2; i--) 101 | { 102 | uint z = _pTable[i] ^ x; 103 | uint a = _sBoxes[0][z >> 24 & 0xFF]; 104 | uint b = _sBoxes[1][z >> 16 & 0xFF]; 105 | uint c = _sBoxes[2][z >> 8 & 0xFF]; 106 | uint d = _sBoxes[3][z & 0xFF]; 107 | x = d + (c ^ b + a) ^ y; 108 | y = z; 109 | } 110 | 111 | return x ^ _pTable[1] | (ulong)(y ^ _pTable[0]) << 32; 112 | } 113 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Cryptography/Crc16.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 4 | 5 | internal static class Crc16 6 | { 7 | private static readonly ushort[] Crc16Table = 8 | { 9 | 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 10 | 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 11 | 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 12 | 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 13 | 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 14 | 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 15 | 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 16 | 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 17 | 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 18 | 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 19 | 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 20 | 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 21 | 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 22 | 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 23 | 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 24 | 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 25 | 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 26 | 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 27 | 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 28 | 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 29 | }; 30 | 31 | public static ushort GetCrc16(byte[] data, int offset, int length) 32 | => GetCrc16(data.AsSpan(offset, length)); 33 | 34 | public static ushort GetCrc16(ReadOnlySpan data) 35 | { 36 | ushort result = 0xFFFF; 37 | for (int i = 0; i < data.Length; i++) 38 | result = (ushort)(result >> 8 ^ Crc16Table[(result ^ data[i]) & 0xFF]); 39 | 40 | return result; 41 | } 42 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Cryptography/KeyTransform.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HaroohieClub.NitroPacker.IO; 3 | 4 | namespace HaroohieClub.NitroPacker.Nitro.Card.Cryptography; 5 | 6 | internal static class KeyTransform 7 | { 8 | private static void ApplyKeyCode(byte[] keyCode, int modulo, byte[] keyTable) 9 | { 10 | var bf = new Blowfish(keyTable); 11 | bf.Encrypt(keyCode, 4, 8); 12 | bf.Encrypt(keyCode, 0, 8); 13 | for (int i = 0; i < Blowfish.PTableEntryCount; i++) 14 | { 15 | IOUtil.WriteU32Le(keyTable, i * 4, 16 | IOUtil.ReadU32Le(keyTable, i * 4) ^ IOUtil.ReadU32Be(keyCode, i * 4 % modulo)); 17 | } 18 | 19 | var scratch = new byte[8]; 20 | for (int i = 0; i < Blowfish.KeyTableLength; i += 8) 21 | { 22 | //update table 23 | bf = new(keyTable); 24 | bf.Encrypt(scratch); 25 | Array.Copy(scratch, 4, keyTable, i, 4); 26 | Array.Copy(scratch, 0, keyTable, i + 4, 4); 27 | } 28 | } 29 | 30 | public static byte[] TransformTable(uint idCode, int level, int modulo, ReadOnlySpan keyTable) 31 | { 32 | byte[] newTable = keyTable[..Blowfish.KeyTableLength].ToArray(); 33 | 34 | var keyCode = new byte[12]; 35 | IOUtil.WriteU32Le(keyCode, 0, idCode); 36 | IOUtil.WriteU32Le(keyCode, 4, idCode >> 1); 37 | IOUtil.WriteU32Le(keyCode, 8, idCode << 1); 38 | if (level >= 1) 39 | ApplyKeyCode(keyCode, modulo, newTable); 40 | if (level >= 2) 41 | ApplyKeyCode(keyCode, modulo, newTable); 42 | IOUtil.WriteU32Le(keyCode, 4, IOUtil.ReadU32Le(keyCode, 4) << 1); 43 | IOUtil.WriteU32Le(keyCode, 8, IOUtil.ReadU32Le(keyCode, 8) >> 1); 44 | if (level >= 3) 45 | ApplyKeyCode(keyCode, modulo, newTable); 46 | return newTable; 47 | } 48 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/Headers/Cryptography.cs: -------------------------------------------------------------------------------- 1 | using HaroohieClub.NitroPacker.IO.Serialization; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro.Card.Headers; 4 | 5 | /// 6 | /// [DSi] Representation of the hashes/encryption section of the DSi header 7 | /// 8 | public class Cryptography 9 | { 10 | /// 11 | /// SHA1-HMAC hash ARM9 (with encrypted secure area) 12 | /// 13 | [ArraySize(0x14)] 14 | public byte[] Arm9WithSecureAreaSha1HmacHash { get; set; } 15 | /// 16 | /// SHA1-HMAC hash ARM7 17 | /// 18 | [ArraySize(0x14)] 19 | public byte[] Arm7Sha1HmacHash { get; set; } 20 | /// 21 | /// SHA1-HMAC hash Digest master 22 | /// 23 | [ArraySize(0x14)] 24 | public byte[] DigestMasterSha1HmacHash { get; set; } 25 | /// 26 | /// SHA1-HMAC hash Icon/Title (also in newer NDS titles) 27 | /// 28 | [ArraySize(0x14)] 29 | public byte[] IconTitleSha1HmacHash { get; set; } 30 | /// 31 | /// SHA1-HMAC hash ARM9i (decrypted) 32 | /// 33 | [ArraySize(0x14)] 34 | public byte[] Arm9iDecryptedSha1HmacHash { get; set; } 35 | /// 36 | /// SHA1-HMAC hash ARM7i (decrypted) 37 | /// 38 | [ArraySize(0x14)] 39 | public byte[] Arm7iDecryptedSha1HmacHash { get; set; } 40 | /// 41 | /// Reserved (zero-filled) (but used for non-whitelisted NDS titles) 42 | /// 43 | [ArraySize(0x14)] 44 | public byte[] ReservedA { get; set; } 45 | /// 46 | /// Reserved (zero-filled) (but used for non-whitelisted NDS titles) 47 | /// 48 | [ArraySize(0x14)] 49 | public byte[] ReservedB { get; set; } 50 | /// 51 | /// SHA1-HMAC hash ARM9 (without 16Kbyte secure area) 52 | /// 53 | [ArraySize(0x14)] 54 | public byte[] Arm9WithoutSecureAreaSha1HmacHash { get; set; } 55 | /// 56 | /// Reserved (zero-filled) 57 | /// 58 | [ArraySize(0xA4C)] 59 | public byte[] ReservedC { get; set; } 60 | /// 61 | /// Reserved and unchecked region, always zero. Used for passing arguments in debug environment. 62 | /// 63 | [ArraySize(0x180)] 64 | public byte[] ReservedD { get; set; } 65 | /// 66 | /// RSA-SHA1 signature across header entries [0x000..0xDFF] 67 | /// 68 | [ArraySize(0x80)] 69 | public byte[] RsaSha1Signature { get; set; } 70 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/NitroFooter.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | using HaroohieClub.NitroPacker.IO; 3 | 4 | namespace HaroohieClub.NitroPacker.Nitro.Card; 5 | 6 | /// 7 | /// Footer of a DS ROM file 8 | /// 9 | public class NitroFooter 10 | { 11 | /// 12 | /// Constructs an empty footer, used for serialization 13 | /// 14 | public NitroFooter() { } 15 | 16 | /// 17 | /// Constructs a DS ROM footer from an extended endian binary reader 18 | /// 19 | /// An EndianBinaryReaderEx with an initialized stream 20 | public NitroFooter(EndianBinaryReaderEx er) => er.ReadObject(this); 21 | /// 22 | /// Writes the DS ROM footer to a stream using an extended endian binary writer 23 | /// 24 | /// An EndianBinaryWriterEx with an initialized stream 25 | public void Write(EndianBinaryWriterEx er) => er.WriteObject(this); 26 | 27 | /// 28 | /// The Nitro Code at the ROM footer 29 | /// 30 | public uint NitroCode { get; set; } 31 | /// 32 | /// The start offset for module params 33 | /// 34 | [XmlElement("_start_ModuleParamsOffset")] 35 | public uint StartModuleParamsOffset { get; set; } 36 | /// 37 | /// Unknown 38 | /// 39 | public uint Unknown { get; set; } 40 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/RomBanner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text.Json.Serialization; 4 | using System.Xml.Serialization; 5 | using HaroohieClub.NitroPacker.IO; 6 | using HaroohieClub.NitroPacker.IO.Serialization; 7 | using HaroohieClub.NitroPacker.Nitro.Card.Banners; 8 | 9 | namespace HaroohieClub.NitroPacker.Nitro.Card; 10 | 11 | /// 12 | /// Representation of the ROM's icon/title/banner 13 | /// 14 | [Serializable] 15 | public class RomBanner 16 | { 17 | /// 18 | /// Empty constructor for serialization 19 | /// 20 | public RomBanner() { } 21 | 22 | /// 23 | /// Constructs a banner using an extended endian binary reader 24 | /// 25 | /// with initialized stream 26 | public RomBanner(EndianBinaryReaderEx er) 27 | { 28 | Header = new(er); 29 | Banner = Header.Version switch 30 | { 31 | 0x001 => new BannerV1(er), 32 | 0x002 => new BannerV2(er), 33 | 0x003 => new BannerV3(er), 34 | 0x103 => new BannerV103(er), 35 | _ => throw new DataException("Unsupported banner version!"), 36 | }; 37 | } 38 | 39 | /// 40 | /// Writes the banner using an extended endian binary writer 41 | /// 42 | /// with an initialized stream 43 | public void Write(EndianBinaryWriterEx ew) 44 | { 45 | Header.Crc16s = Banner is not null ? Banner.GetCrcs() : OldBanner.GetCrcs(); 46 | Header.Write(ew); 47 | if (Banner is not null) 48 | { 49 | switch (Header.Version) 50 | { 51 | case 0x001: 52 | ((BannerV1)Banner).Write(ew); 53 | break; 54 | case 0x002: 55 | ((BannerV2)Banner).Write(ew); 56 | break; 57 | case 0x003: 58 | ((BannerV3)Banner).Write(ew); 59 | break; 60 | case 0x103: 61 | ((BannerV103)Banner).Write(ew); 62 | break; 63 | } 64 | } 65 | else 66 | { 67 | OldBanner.Write(ew); 68 | } 69 | } 70 | 71 | /// 72 | /// The header of the banner 73 | /// 74 | public Header Header { get; set; } 75 | 76 | /// 77 | /// The actual ROM banner 78 | /// 79 | [JsonIgnore] 80 | [XmlIgnore] 81 | public Banner Banner { get; set; } 82 | 83 | /// 84 | /// Property used for the old NitroPacker project file format 85 | /// 86 | [Ignore] 87 | [JsonIgnore] 88 | [XmlElement("Banner")] 89 | public BannerV1 OldBanner { get; set; } 90 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/RomFileNameTable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Xml.Serialization; 3 | using HaroohieClub.NitroPacker.IO; 4 | using HaroohieClub.NitroPacker.Nitro.Fs; 5 | 6 | namespace HaroohieClub.NitroPacker.Nitro.Card; 7 | 8 | /// 9 | /// Representation of the file name table 10 | /// 11 | [XmlType("RomFNT")] 12 | public class RomFileNameTable 13 | { 14 | /// 15 | /// Constructs a blank file name table (used for serialization) 16 | /// 17 | public RomFileNameTable() 18 | { 19 | DirectoryTable = new[] { new DirectoryTableEntry { ParentId = 1 } }; 20 | NameTable = new[] { new[] { NameTableEntry.EndOfDirectory() } }; 21 | } 22 | 23 | /// 24 | /// Constructs a file name table using an extended endian binary reader 25 | /// 26 | /// with an initialized stream 27 | public RomFileNameTable(EndianBinaryReaderEx er) 28 | { 29 | er.BeginChunk(); 30 | { 31 | DirectoryTableEntry root = new(er); 32 | DirectoryTable = new DirectoryTableEntry[root.ParentId]; 33 | DirectoryTable[0] = root; 34 | for (int i = 1; i < root.ParentId; i++) 35 | DirectoryTable[i] = new(er); 36 | 37 | NameTable = new NameTableEntry[root.ParentId][]; 38 | for (int i = 0; i < root.ParentId; i++) 39 | { 40 | er.JumpRelative(DirectoryTable[i].EntryStart); 41 | var entries = new List(); 42 | 43 | NameTableEntry entry; 44 | do 45 | { 46 | entry = new(er); 47 | entries.Add(entry); 48 | } while (entry.Type != NameTableEntryType.EndOfDirectory); 49 | 50 | NameTable[i] = entries.ToArray(); 51 | } 52 | } 53 | er.EndChunk(); 54 | } 55 | 56 | /// 57 | /// Writes the file name table to a stream using an extended endian binary writer 58 | /// 59 | /// with an initialized stream 60 | public void Write(EndianBinaryWriterEx ew) 61 | { 62 | DirectoryTable[0].ParentId = (ushort)DirectoryTable.Length; 63 | ew.BeginChunk(); 64 | { 65 | long dirTabAddr = ew.BaseStream.Position; 66 | ew.BaseStream.Position += DirectoryTable.Length * 8; 67 | for (int i = 0; i < DirectoryTable.Length; i++) 68 | { 69 | DirectoryTable[i].EntryStart = (uint)ew.GetCurposRelative(); 70 | foreach (NameTableEntry entry in NameTable[i]) 71 | entry.Write(ew); 72 | } 73 | 74 | long curPos = ew.BaseStream.Position; 75 | ew.BaseStream.Position = dirTabAddr; 76 | foreach (DirectoryTableEntry entry in DirectoryTable) 77 | entry.Write(ew); 78 | ew.BaseStream.Position = curPos; 79 | } 80 | ew.EndChunk(); 81 | } 82 | 83 | /// 84 | /// The directory table portion of the FNT 85 | /// 86 | public DirectoryTableEntry[] DirectoryTable { get; set; } 87 | /// 88 | /// The name table portion of the FNT 89 | /// 90 | public NameTableEntry[][] NameTable { get; set; } 91 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Card/RomOverlayTable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using System.Xml.Serialization; 4 | using HaroohieClub.NitroPacker.IO; 5 | using HaroohieClub.NitroPacker.IO.Serialization; 6 | 7 | namespace HaroohieClub.NitroPacker.Nitro.Card; 8 | 9 | /// 10 | /// Represents the ARM9/ARM7 overlay tables in the ROM 11 | /// 12 | [XmlType("RomOVT")] 13 | public class RomOverlayTable 14 | { 15 | /// 16 | /// Flags indicating properties of the overlay table 17 | /// 18 | [Flags] 19 | public enum OverlayTableFlag : byte 20 | { 21 | /// 22 | /// If set, this overlay is compressed with the BLZ algorithm 23 | /// 24 | Compressed = 1, 25 | /// 26 | /// If set, 27 | /// 28 | AuthenticationCode = 2 29 | } 30 | 31 | /// 32 | /// Blank constructor, used for serialization 33 | /// 34 | public RomOverlayTable() { } 35 | 36 | /// 37 | /// Constructs an overlay table from a stream using an extended endian binary reader 38 | /// 39 | /// initialized with a stream 40 | public RomOverlayTable(EndianBinaryReaderEx er) 41 | { 42 | er.ReadObject(this); 43 | uint tmp = er.Read(); 44 | Compressed = tmp & 0xFFFFFF; 45 | Flag = (OverlayTableFlag)(tmp >> 24); 46 | } 47 | 48 | /// 49 | /// Writes the overlay table to a stream using an extended endian binary writer 50 | /// 51 | /// initialized with a stream 52 | public void Write(EndianBinaryWriterEx ew) 53 | { 54 | ew.WriteObject(this); 55 | ew.Write(((uint)Flag & 0xFF) << 24 | Compressed & 0xFFFFFF); 56 | } 57 | 58 | /// 59 | /// The ID of the overlay 60 | /// 61 | [XmlAttribute("Id")] 62 | public uint Id { get; set; } 63 | /// 64 | /// The address at which the game loads the overlay 65 | /// 66 | public uint RamAddress { get; set; } 67 | /// 68 | /// The amount of data from the overlay to load into RAM 69 | /// 70 | public uint RamSize { get; set; } 71 | /// 72 | /// Size of the BSS data region 73 | /// 74 | public uint BssSize { get; set; } 75 | /// 76 | /// Static initializer start address 77 | /// 78 | [XmlElement("SinitInit")] 79 | public uint StaticInitializerStartAddress { get; set; } 80 | /// 81 | /// Static initializer end address 82 | /// 83 | [XmlElement("SinitInitEnd")] 84 | public uint StaticInitializerEndAddress { get; set; } 85 | 86 | /// 87 | /// The file ID of the overlay (corresponds to its location in the file name table) 88 | /// 89 | [JsonIgnore] 90 | public uint FileId { get; set; } 91 | 92 | /// 93 | /// Indicates whether this overlay is compressed 94 | /// 95 | [Ignore] 96 | public uint Compressed { get; set; } //:24; 97 | 98 | /// 99 | /// A set of flags for this overlay 100 | /// 101 | [Ignore] 102 | [XmlAttribute("Flag")] 103 | public OverlayTableFlag Flag { get; set; } // :8; 104 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Fs/DirectoryTableEntry.cs: -------------------------------------------------------------------------------- 1 | using HaroohieClub.NitroPacker.IO; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro.Fs; 4 | 5 | /// 6 | /// Represents a directory table entry within the file name table 7 | /// 8 | public class DirectoryTableEntry 9 | { 10 | /// 11 | /// Empty constructor, used for serialization 12 | /// 13 | public DirectoryTableEntry() { } 14 | 15 | /// 16 | /// Constructs a directory table entry from a stream using an extended endian binary reader 17 | /// 18 | /// initialized with a stream 19 | public DirectoryTableEntry(EndianBinaryReaderEx er) 20 | => er.ReadObject(this); 21 | 22 | /// 23 | /// Writes the directory table entry's binary representation to a stream using an extended endian binary writer 24 | /// 25 | /// initialized with a stream 26 | public void Write(EndianBinaryWriterEx ew) 27 | => ew.WriteObject(this); 28 | 29 | /// 30 | /// Offset to associated name table 31 | /// 32 | public uint EntryStart { get; set; } 33 | /// 34 | /// The first file ID of the directory entry 35 | /// 36 | public ushort EntryFileId { get; set; } 37 | /// 38 | /// The directory ID of this directory's parent directory (or, for the root directory, the number of total entries in the directory table) 39 | /// 40 | public ushort ParentId { get; set; } 41 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Fs/FatEntry.cs: -------------------------------------------------------------------------------- 1 | using HaroohieClub.NitroPacker.IO; 2 | using HaroohieClub.NitroPacker.IO.Serialization; 3 | 4 | namespace HaroohieClub.NitroPacker.Nitro.Fs; 5 | 6 | /// 7 | /// Represents an entry in the file allocation table 8 | /// 9 | public class FatEntry 10 | { 11 | /// 12 | /// Constructs an empty FAT entry (used for serialization) 13 | /// 14 | public FatEntry() 15 | { 16 | } 17 | 18 | /// 19 | /// Constructs a FAT entry from a file offset and size 20 | /// 21 | /// The offset of the file in the ROM 22 | /// The size of the file 23 | public FatEntry(uint offset, uint size) 24 | { 25 | FileTop = offset; 26 | FileBottom = offset + size; 27 | } 28 | 29 | /// 30 | /// Reads a FAT entry from an extended endian binary reader 31 | /// 32 | /// The extended endian binary reader with an initialized stream 33 | public FatEntry(EndianBinaryReaderEx er) 34 | => er.ReadObject(this); 35 | 36 | /// 37 | /// Writes a FAT entry to a stream using an extended endian binary writer 38 | /// 39 | /// The extended endian binary writer with an initialized stream 40 | public void Write(EndianBinaryWriterEx ew) 41 | => ew.WriteObject(this); 42 | 43 | /// 44 | /// The start offset of the file in the ROM 45 | /// 46 | public uint FileTop { get; set; } 47 | /// 48 | /// The end offset of the file in the ROM 49 | /// 50 | public uint FileBottom { get; set; } 51 | 52 | /// 53 | /// The size of the file 54 | /// 55 | [Ignore] 56 | public uint FileSize => FileBottom - FileTop; 57 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Fs/NameEntryWithFatEntry.cs: -------------------------------------------------------------------------------- 1 | namespace HaroohieClub.NitroPacker.Nitro.Fs; 2 | 3 | /// 4 | /// A class used to associate a file with its file allocation table offset 5 | /// 6 | public class NameEntryWithFatEntry 7 | { 8 | /// 9 | /// The path to the file 10 | /// 11 | public string Path { get; set; } 12 | /// 13 | /// The FAT offset of the file 14 | /// 15 | public uint FatOffset { get; set; } 16 | 17 | /// 18 | /// Empty constructor for serialization 19 | /// 20 | public NameEntryWithFatEntry() 21 | { 22 | } 23 | } 24 | 25 | /// 26 | /// A class used to associated a file/FAT entry with its data 27 | /// 28 | public class NameFatWithData 29 | { 30 | /// 31 | /// The associated with this data 32 | /// 33 | public NameEntryWithFatEntry NameFat { get; set; } 34 | /// 35 | /// The binary data of this file 36 | /// 37 | public byte[] Data { get; set; } 38 | 39 | /// 40 | /// Empty constructor for serialization 41 | /// 42 | public NameFatWithData() 43 | { 44 | } 45 | 46 | /// 47 | /// Constructs an entry with just file data ( can be initialized later) 48 | /// 49 | /// 50 | public NameFatWithData(byte[] data) 51 | { 52 | Data = data; 53 | } 54 | 55 | /// 56 | /// Constructs an entry with its arguments 57 | /// 58 | /// The 59 | /// The file data for that file 60 | public NameFatWithData(NameEntryWithFatEntry nameFat, byte[] data) 61 | { 62 | NameFat = nameFat; 63 | Data = data; 64 | } 65 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Fs/NameTableEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text; 4 | using HaroohieClub.NitroPacker.IO; 5 | 6 | namespace HaroohieClub.NitroPacker.Nitro.Fs; 7 | 8 | /// 9 | /// Enum describing the types of name table entries 10 | /// 11 | public enum NameTableEntryType 12 | { 13 | /// 14 | /// Used to mark the end of a directory 15 | /// 16 | EndOfDirectory, 17 | /// 18 | /// Indicates that this entry names a file 19 | /// 20 | File, 21 | /// 22 | /// Indicates that this entry names a directory 23 | /// 24 | Directory, 25 | } 26 | 27 | /// 28 | /// Represents an entry in one of the name (or sub) tables in the file name table 29 | /// 30 | public class NameTableEntry 31 | { 32 | /// 33 | /// The type of entry (file, directory, or end) 34 | /// 35 | public NameTableEntryType Type { get; set; } 36 | /// 37 | /// The name of this entry as a Shift-JIS string 38 | /// 39 | public string Name { get; set; } 40 | /// 41 | /// If a directory, the ID of the directory; otherwise, 0 42 | /// 43 | public ushort DirectoryId { get; set; } 44 | 45 | /// 46 | /// Empty constructor, used for serialization 47 | /// 48 | public NameTableEntry() 49 | { 50 | } 51 | 52 | /// 53 | /// Constructs a name table entry from the given arguments 54 | /// 55 | /// The type of the entry 56 | /// The name of the entry 57 | /// The directory ID 58 | /// Thrown if name is null and this is not an end of directory 59 | /// Thrown if the name is too long or if the directory ID is invalid 60 | private NameTableEntry(NameTableEntryType type, string name = null, ushort directoryId = 0) 61 | { 62 | Type = type; 63 | if (type != NameTableEntryType.EndOfDirectory) 64 | { 65 | if (name == null) 66 | throw new ArgumentNullException(nameof(name)); 67 | if (name.Length == 0 || name.Length > 0x7F) 68 | throw new ArgumentException("Name length invalid", nameof(name)); 69 | Name = name; 70 | } 71 | 72 | if (type == NameTableEntryType.Directory) 73 | { 74 | if (directoryId < 0xF000) 75 | throw new ArgumentException("Directory ID invalid", nameof(directoryId)); 76 | DirectoryId = directoryId; 77 | } 78 | } 79 | 80 | /// 81 | /// Constructs a name table entry from a stream using an endian binary reader 82 | /// 83 | /// initialized with a stream 84 | public NameTableEntry(EndianBinaryReader er) 85 | { 86 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 87 | byte length = er.Read(); 88 | if (length == 0) 89 | Type = NameTableEntryType.EndOfDirectory; 90 | else if ((length & 0x80) != 0) 91 | { 92 | Type = NameTableEntryType.Directory; 93 | Name = er.ReadString(Encoding.GetEncoding("Shift-JIS"), length & ~0x80); 94 | DirectoryId = er.Read(); 95 | } 96 | else 97 | { 98 | Type = NameTableEntryType.File; 99 | Name = er.ReadString(Encoding.GetEncoding("Shift-JIS"), length); 100 | } 101 | } 102 | 103 | /// 104 | /// Writes a name table entry to a stream using an endian binary writer 105 | /// 106 | /// initialized with a stream 107 | /// Thrown if the name is too long 108 | /// Thrown if an invalid entry type is specified 109 | public void Write(EndianBinaryWriter er) 110 | { 111 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 112 | switch (Type) 113 | { 114 | case NameTableEntryType.EndOfDirectory: 115 | er.Write((byte)0); 116 | break; 117 | case NameTableEntryType.File: 118 | if (Name.Length > 0x7F) 119 | throw new DataException($"File name '{Name}' too long"); 120 | er.Write((byte)Name.Length); 121 | er.Write(Name, Encoding.GetEncoding("Shift-JIS"), false); 122 | break; 123 | case NameTableEntryType.Directory: 124 | if (Name.Length > 0x7F) 125 | throw new DataException($"Directory name '{Name}' too long"); 126 | er.Write((byte)(Name.Length | 0x80)); 127 | er.Write(Name, Encoding.GetEncoding("Shift-JIS"), false); 128 | er.Write(DirectoryId); 129 | break; 130 | default: 131 | throw new ArgumentOutOfRangeException(); 132 | } 133 | } 134 | 135 | /// 136 | /// Constructs an end of directory entry 137 | /// 138 | /// An end of directory name table entry 139 | public static NameTableEntry EndOfDirectory() => new(NameTableEntryType.EndOfDirectory); 140 | /// 141 | /// Constructs a file entry 142 | /// 143 | /// The name of the file 144 | /// A file name table entry 145 | public static NameTableEntry File(string name) => new(NameTableEntryType.File, name); 146 | /// 147 | /// Constructs a directory entry 148 | /// 149 | /// The name of the directory 150 | /// The ID of the directory 151 | /// A directory name table entry 152 | public static NameTableEntry Directory(string name, ushort directoryId) => 153 | new(NameTableEntryType.Directory, name, directoryId); 154 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Gx/ColorFormat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro.Gx; 4 | 5 | /// 6 | /// A class representing the Nitro color format 7 | /// 8 | public class ColorFormat 9 | { 10 | /// 11 | /// Shifts and sizes for the various color components 12 | /// 13 | public readonly int AShift, ASize, RShift, RSize, GShift, GSize, BShift, BSize; 14 | 15 | /// 16 | /// Constructs a color format from the various shifts and sizes for each component 17 | /// 18 | /// Alpha shit 19 | /// Alpha size 20 | /// Red shift 21 | /// Red size 22 | /// Green shift 23 | /// Green size 24 | /// Blue shift 25 | /// Blue size 26 | public ColorFormat(int aShift, int aSize, int rShift, int rSize, int gShift, int gSize, int bShift, int bSize) 27 | { 28 | AShift = aShift; 29 | ASize = aSize; 30 | RShift = rShift; 31 | RSize = rSize; 32 | GShift = gShift; 33 | GSize = gSize; 34 | BShift = bShift; 35 | BSize = bSize; 36 | } 37 | 38 | /// 39 | /// Number of bytes 40 | /// 41 | public int NrBytes => (int)Math.Ceiling((ASize + RSize + GSize + BSize) / 8f); 42 | 43 | //The naming is based on the bit order when read out in the correct endianness 44 | /// 45 | /// 32-bit ARBG color 46 | /// 47 | /// 48 | public static readonly ColorFormat ARGB8888 = new(24, 8, 16, 8, 8, 8, 0, 8); 49 | /// 50 | /// 15-bit ARBG color 51 | /// 52 | public static readonly ColorFormat ARGB3444 = new(12, 3, 8, 4, 4, 4, 0, 4); 53 | 54 | /// 55 | /// 32-bit RGBA color 56 | /// 57 | public static readonly ColorFormat RGBA8888 = new(0, 8, 24, 8, 16, 8, 8, 8); 58 | 59 | /// 60 | /// 16-bit RGBA color 61 | /// 62 | public static readonly ColorFormat RGBA4444 = new(0, 4, 12, 4, 8, 4, 4, 4); 63 | 64 | /// 65 | /// 24-bit RGB color 66 | /// 67 | public static readonly ColorFormat RGB888 = new(0, 0, 16, 8, 8, 8, 0, 8); 68 | 69 | /// 70 | /// 16-bit RGB color 71 | /// 72 | public static readonly ColorFormat RGB565 = new(0, 0, 11, 5, 5, 6, 0, 5); 73 | 74 | /// 75 | /// 16-bit ARGB color 76 | /// 77 | public static readonly ColorFormat ARGB1555 = new(15, 1, 10, 5, 5, 5, 0, 5); 78 | /// 79 | /// 16-bit XRGB color 80 | /// 81 | public static readonly ColorFormat XRGB1555 = new(0, 0, 10, 5, 5, 5, 0, 5); 82 | 83 | /// 84 | /// 16-bit ABGR color 85 | /// 86 | public static readonly ColorFormat ABGR1555 = new(15, 1, 0, 5, 5, 5, 10, 5); 87 | /// 88 | /// 16-bit XBGR color 89 | /// 90 | public static readonly ColorFormat XBGR1555 = new(0, 0, 0, 5, 5, 5, 10, 5); 91 | 92 | /// 93 | /// 16-bit RGBA color 94 | /// 95 | public static readonly ColorFormat RGBA5551 = new(0, 1, 11, 5, 6, 5, 1, 5); 96 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Gx/Enums.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro.Gx; 4 | 5 | /// 6 | /// Enum describing various image formats 7 | /// 8 | public enum ImageFormat : uint 9 | { 10 | /// 11 | /// No image format 12 | /// 13 | [Description("None")] 14 | None = 0, 15 | 16 | /// 17 | /// A3I5 (3-bit alpha, 5-bit intensity) 18 | /// 19 | [Description("A3I5")] 20 | A3I5 = 1, 21 | 22 | /// 23 | /// 4-color palette image, a.k.a. 2bpp 24 | /// 25 | [Description("Palette 4")] 26 | Pltt4 = 2, 27 | 28 | /// 29 | /// 16-color palette image, a.k.a. 4bpp 30 | /// 31 | [Description("Palette 16")] 32 | Pltt16 = 3, 33 | 34 | /// 35 | /// 256-color palette image, a.k.a. 8bpp 36 | /// 37 | [Description("Palette 256")] 38 | Pltt256 = 4, 39 | 40 | /// 41 | /// 4x4 compressed image (DXT) 42 | /// 43 | [Description("4x4")] 44 | Comp4x4 = 5, 45 | 46 | /// 47 | /// A5I3 (5-bit alpha, 3-bit intensity) 48 | /// 49 | [Description("A5I3")] 50 | A5I3 = 6, 51 | 52 | /// 53 | /// Direct, non-paletted image 54 | /// 55 | [Description("Direct")] 56 | Direct = 7 57 | } 58 | 59 | /// 60 | /// Char format 61 | /// 62 | public enum CharFormat : uint 63 | { 64 | /// 65 | /// Char 66 | /// 67 | Char, 68 | /// 69 | /// Bmp 70 | /// 71 | Bmp 72 | } 73 | 74 | /// 75 | /// Map format 76 | /// 77 | public enum MapFormat : uint 78 | { 79 | /// 80 | /// Text 81 | /// 82 | Text, 83 | /// 84 | /// Affine 85 | /// 86 | Affine 87 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Gx/IconAnimationSequence.cs: -------------------------------------------------------------------------------- 1 | namespace HaroohieClub.NitroPacker.Nitro.Gx; 2 | 3 | /// 4 | /// 16-bit animation sequence token for a v0x103 banner icon 5 | /// 6 | public class IconAnimationSequence 7 | { 8 | /// 9 | /// The duration of the current frame in 60Hz units (0-255) 10 | /// 11 | public byte FrameDuration { get; set; } 12 | /// 13 | /// The index of the animation bitmap to use (0-7) 14 | /// 15 | public byte BitmapIndex { get; set; } 16 | /// 17 | /// The index of the animation palette to use (0-7) 18 | /// 19 | public byte PaletteIndex { get; set; } 20 | /// 21 | /// If true, the animation bitmap will be flipped horizontally 22 | /// 23 | public bool FlipHorizontally { get; set; } 24 | /// 25 | /// If true, the animation bitmap will be flipped vertically 26 | /// 27 | public bool FlipVertically { get; set; } 28 | 29 | /// 30 | /// Packed animation 31 | /// 32 | /// 33 | public IconAnimationSequence(ushort packed) 34 | { 35 | FrameDuration = (byte)packed; 36 | BitmapIndex = (byte)((packed >> 8) & 0b111); 37 | PaletteIndex = (byte)((packed >> 11) & 0b111); 38 | FlipHorizontally = (packed & 0b0100_0000_0000_0000) > 0; 39 | FlipVertically = (packed & 0b1000_0000_0000_0000) > 0; 40 | } 41 | 42 | /// 43 | /// Pack the animation sequence into a ushort 44 | /// 45 | /// An unsigned short represenation of the animation sequence 46 | public ushort Pack() 47 | { 48 | return (ushort)(FrameDuration | ((BitmapIndex & 0b111) << 8) | ((PaletteIndex & 0b111) << 11) 49 | | (FlipHorizontally ? 0b0100_0000_0000_0000 : 0) 50 | | (FlipVertically ? 0b1000_0000_0000_0000 : 0)); 51 | } 52 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Nitro/Gx/Rgba8Bitmap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace HaroohieClub.NitroPacker.Nitro.Gx; 4 | 5 | /// 6 | /// An RGBA8 bitmap 7 | /// 8 | public class Rgba8Bitmap 9 | { 10 | /// 11 | /// Width of the bitmap in pixels 12 | /// 13 | public int Width { get; } 14 | /// 15 | /// Height of the bitmap in pixels 16 | /// 17 | public int Height { get; } 18 | /// 19 | /// A representation of the bitmap's pixel data 20 | /// 21 | public uint[] Pixels { get; } 22 | 23 | /// 24 | /// Constructs a blank bitmap with a given width and height 25 | /// 26 | /// Width in pixels 27 | /// Height in pixels 28 | public Rgba8Bitmap(int width, int height) 29 | { 30 | Width = width; 31 | Height = height; 32 | 33 | Pixels = new uint[width * height]; 34 | } 35 | 36 | /// 37 | /// Constructs a bitmap from data 38 | /// 39 | /// Width in pixels 40 | /// Height in pixels 41 | /// Pixel data 42 | public Rgba8Bitmap(int width, int height, uint[] data) 43 | { 44 | Width = width; 45 | Height = height; 46 | 47 | Pixels = new uint[width * height]; 48 | Array.Copy(data, Pixels, Pixels.Length); 49 | } 50 | 51 | /// 52 | /// Gets pixel data at an x-y coordinate 53 | /// 54 | /// x-coordinate 55 | /// y-coordinate 56 | public uint this[int x, int y] 57 | { 58 | get => Pixels[y * Width + x]; 59 | set => Pixels[y * Width + x] = value; 60 | } 61 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/NitroFsArchiveExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using HaroohieClub.NitroPacker.Nitro.Fs; 4 | 5 | namespace HaroohieClub.NitroPacker; 6 | 7 | internal static class NitroFsArchiveExtensions 8 | { 9 | public static void Export(this NitroFsArchive root, string outPath, bool unpackArc = false) 10 | => ExtractDirectories(root, "/", outPath, unpackArc); 11 | 12 | private static void ExtractDirectories(NitroFsArchive dir, string dirPath, string outPath, bool unpackArc = false) 13 | { 14 | string path = outPath + dirPath; 15 | 16 | Directory.CreateDirectory(path); 17 | 18 | foreach (string f in dir.EnumerateFiles(dirPath, true)) 19 | { 20 | File.WriteAllBytes(outPath + f, dir.GetFileData(f)); 21 | } 22 | 23 | Parallel.ForEach(dir.EnumerateDirectories(dirPath, true), d => 24 | { 25 | ExtractDirectories(dir, d, outPath, unpackArc); 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Patcher/Nitro/ARM9.cs: -------------------------------------------------------------------------------- 1 | // This code is heavily based on code Gericom wrote for ErmiiBuild 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace HaroohieClub.NitroPacker.Patcher.Nitro; 8 | 9 | /// 10 | /// Object representing the ARM9 binary 11 | /// 12 | public class ARM9 13 | { 14 | private readonly uint _ramAddress; 15 | private readonly List _staticData; 16 | private readonly uint _start_ModuleParamsOffset; 17 | private readonly CRT0.ModuleParams _start_ModuleParams; 18 | 19 | private readonly List _autoLoadList; 20 | 21 | /// 22 | /// Create an ARM9 object from binary data in memory (the module params offset is dynamically determined) 23 | /// 24 | /// The ARM9 binary data 25 | /// The address at which the ARM9 binary is loaded into RAM 26 | public ARM9(byte[] data, uint ramAddress) 27 | : this(data, ramAddress, FindModuleParams(data)) { } 28 | 29 | /// 30 | /// Create an ARM9 object from binary data in memory while specifying the module params offset 31 | /// 32 | /// The ARM9 binary data 33 | /// The address at which the ARM9 binary is loaded into RAM 34 | /// The offset in the data where the module params are located 35 | public ARM9(byte[] data, uint ramAddress, uint moduleParamsOffset) 36 | { 37 | //Unimportant static footer! Use it for _start_ModuleParamsOffset and remove it. 38 | if (BitConverter.ToUInt32(data.Skip(data.Length - 0x0C).Take(4).ToArray()) == 0xDEC00621) 39 | { 40 | moduleParamsOffset = BitConverter.ToUInt32(data.Skip(data.Length - 8).Take(4).ToArray()); 41 | data = data.Take(data.Length - 0x0C).ToArray(); 42 | } 43 | 44 | _ramAddress = ramAddress; 45 | _start_ModuleParamsOffset = moduleParamsOffset; 46 | _start_ModuleParams = new(data, moduleParamsOffset); 47 | if (_start_ModuleParams.CompressedStaticEnd != 0) 48 | { 49 | _start_ModuleParams = new(data, moduleParamsOffset); 50 | } 51 | 52 | _staticData = data.Take((int)(_start_ModuleParams.AutoLoadStart - ramAddress)).ToList(); 53 | 54 | _autoLoadList = new(); 55 | uint nr = (_start_ModuleParams.AutoLoadListEnd - _start_ModuleParams.AutoLoadListOffset) / 0xC; 56 | uint offset = _start_ModuleParams.AutoLoadStart - ramAddress; 57 | for (int i = 0; i < nr; i++) 58 | { 59 | var entry = new CRT0.AutoLoadEntry(data, _start_ModuleParams.AutoLoadListOffset - ramAddress + (uint)i * 0xC); 60 | entry.Data = data.Skip((int)offset).Take((int)entry.Size).ToList(); 61 | _autoLoadList.Add(entry); 62 | offset += entry.Size; 63 | } 64 | } 65 | 66 | /// 67 | /// Get the raw ARM9 binary 68 | /// 69 | /// A byte array of the ARM9 binary data 70 | public byte[] GetBytes() 71 | { 72 | List bytes = new(); 73 | bytes.AddRange(_staticData); 74 | _start_ModuleParams.AutoLoadStart = (uint)bytes.Count + _ramAddress; 75 | foreach (CRT0.AutoLoadEntry autoLoad in _autoLoadList) 76 | { 77 | bytes.AddRange(autoLoad.Data); 78 | } 79 | _start_ModuleParams.AutoLoadListOffset = (uint)bytes.Count + _ramAddress; 80 | foreach (CRT0.AutoLoadEntry autoLoad in _autoLoadList) 81 | { 82 | bytes.AddRange(autoLoad.GetEntryBytes()); 83 | } 84 | _start_ModuleParams.AutoLoadListEnd = (uint)bytes.Count + _ramAddress; 85 | List moduleParamsBytes = _start_ModuleParams.GetBytes(); 86 | bytes.RemoveRange((int)_start_ModuleParamsOffset, moduleParamsBytes.Count); 87 | bytes.InsertRange((int)_start_ModuleParamsOffset, moduleParamsBytes); 88 | return bytes.ToArray(); 89 | } 90 | 91 | internal void AddAutoLoadEntry(uint address, byte[] data) 92 | { 93 | _autoLoadList.Add(new(address, data)); 94 | } 95 | 96 | internal void WriteBytes(uint address, byte[] bytes) 97 | { 98 | _staticData.RemoveRange((int)(address - _ramAddress), bytes.Length); 99 | _staticData.InsertRange((int)(address - _ramAddress), bytes); 100 | } 101 | 102 | internal bool WriteU16LE(uint address, ushort value) 103 | { 104 | if (address > _ramAddress && address < _start_ModuleParams.AutoLoadStart) 105 | { 106 | _staticData.RemoveRange((int)(address - _ramAddress), 2); 107 | _staticData.InsertRange((int)(address - _ramAddress), BitConverter.GetBytes(value)); 108 | return true; 109 | } 110 | foreach (CRT0.AutoLoadEntry v in _autoLoadList) 111 | { 112 | if (address > v.Address && address < (v.Address + v.Size)) 113 | { 114 | v.Data.RemoveRange((int)(address - _ramAddress), 2); 115 | v.Data.InsertRange((int)(address - _ramAddress), BitConverter.GetBytes(value)); 116 | return true; 117 | } 118 | } 119 | return false; 120 | } 121 | 122 | internal uint ReadU32LE(uint address) 123 | { 124 | if (address > _ramAddress && address < _start_ModuleParams.AutoLoadStart) 125 | { 126 | return BitConverter.ToUInt32(_staticData.ToArray(), (int)(address - _ramAddress)); 127 | } 128 | foreach (CRT0.AutoLoadEntry v in _autoLoadList) 129 | { 130 | if (address > v.Address && address < (v.Address + v.Size)) 131 | { 132 | return BitConverter.ToUInt32(v.Data.ToArray(), (int)(address - v.Address)); 133 | } 134 | } 135 | return 0xFFFFFFFF; 136 | } 137 | 138 | internal bool WriteU32LE(uint address, uint value) 139 | { 140 | if (address > _ramAddress && address < _start_ModuleParams.AutoLoadStart) 141 | { 142 | _staticData.RemoveRange((int)(address - _ramAddress), 4); 143 | _staticData.InsertRange((int)(address - _ramAddress), BitConverter.GetBytes(value)); 144 | return true; 145 | } 146 | foreach (CRT0.AutoLoadEntry v in _autoLoadList) 147 | { 148 | if (address > v.Address && address < (v.Address + v.Size)) 149 | { 150 | v.Data.RemoveRange((int)(address - _ramAddress), 4); 151 | v.Data.InsertRange((int)(address - _ramAddress), BitConverter.GetBytes(value)); 152 | return true; 153 | } 154 | } 155 | return false; 156 | } 157 | 158 | private static uint FindModuleParams(byte[] data) 159 | { 160 | return (uint)(data.IndexOfSequence(new byte[] { 0x21, 0x06, 0xC0, 0xDE, 0xDE, 0xC0, 0x06, 0x21 }) - 0x1C); 161 | } 162 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Patcher/Nitro/CRT0.cs: -------------------------------------------------------------------------------- 1 | // This code is heavily based on code Gericom wrote for ErmiiBuild 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace HaroohieClub.NitroPacker.Patcher.Nitro; 8 | 9 | internal class CRT0 10 | { 11 | public class ModuleParams 12 | { 13 | public uint AutoLoadListOffset { get; set; } 14 | public uint AutoLoadListEnd { get; set; } 15 | public uint AutoLoadStart { get; set; } 16 | public uint StaticBssStart { get; set; } 17 | public uint StaticBssEnd { get; set; } 18 | public uint CompressedStaticEnd { get; set; } 19 | public uint SDKVersion { get; set; } 20 | public uint NitroCodeBE { get; set; } 21 | public uint NitroCodeLE { get; set; } 22 | 23 | public ModuleParams(byte[] data, uint offset) 24 | { 25 | AutoLoadListOffset = BitConverter.ToUInt32(data.Skip((int)offset).Take(4).ToArray()); 26 | AutoLoadListEnd = BitConverter.ToUInt32(data.Skip((int)offset + 0x04).Take(4).ToArray()); 27 | AutoLoadStart = BitConverter.ToUInt32(data.Skip((int)offset + 0x08).Take(4).ToArray()); 28 | StaticBssStart = BitConverter.ToUInt32(data.Skip((int)offset + 0x0C).Take(4).ToArray()); 29 | StaticBssEnd = BitConverter.ToUInt32(data.Skip((int)offset + 0x10).Take(4).ToArray()); 30 | CompressedStaticEnd = BitConverter.ToUInt32(data.Skip((int)offset + 0x14).Take(4).ToArray()); 31 | SDKVersion = BitConverter.ToUInt32(data.Skip((int)offset + 0x18).Take(4).ToArray()); 32 | NitroCodeBE = BitConverter.ToUInt32(data.Skip((int)offset + 0x1C).Take(4).ToArray()); 33 | NitroCodeLE = BitConverter.ToUInt32(data.Skip((int)offset + 0x20).Take(4).ToArray()); 34 | } 35 | public List GetBytes() 36 | { 37 | List bytes = new(); 38 | bytes.AddRange(BitConverter.GetBytes(AutoLoadListOffset)); 39 | bytes.AddRange(BitConverter.GetBytes(AutoLoadListEnd)); 40 | bytes.AddRange(BitConverter.GetBytes(AutoLoadStart)); 41 | bytes.AddRange(BitConverter.GetBytes(StaticBssStart)); 42 | bytes.AddRange(BitConverter.GetBytes(StaticBssEnd)); 43 | bytes.AddRange(BitConverter.GetBytes(CompressedStaticEnd)); 44 | bytes.AddRange(BitConverter.GetBytes(SDKVersion)); 45 | bytes.AddRange(BitConverter.GetBytes(NitroCodeBE)); 46 | bytes.AddRange(BitConverter.GetBytes(NitroCodeLE)); 47 | 48 | return bytes; 49 | } 50 | } 51 | 52 | public class AutoLoadEntry 53 | { 54 | public uint Address { get; set; } 55 | public uint Size { get; set; } 56 | public uint BssSize { get; set; } 57 | public List Data { get; set; } 58 | 59 | public AutoLoadEntry(uint address, byte[] data) 60 | { 61 | Address = address; 62 | Data = data.ToList(); 63 | Size = (uint)data.Length; 64 | BssSize = 0; 65 | } 66 | public AutoLoadEntry(byte[] data, uint offset) 67 | { 68 | Address = BitConverter.ToUInt32(data.Skip((int)offset).Take(4).ToArray()); 69 | Size = BitConverter.ToUInt32(data.Skip((int)offset + 0x04).Take(4).ToArray()); 70 | BssSize = BitConverter.ToUInt32(data.Skip((int)offset + 0x08).Take(4).ToArray()); 71 | } 72 | public List GetEntryBytes() 73 | { 74 | List bytes = new(); 75 | bytes.AddRange(BitConverter.GetBytes(Address)); 76 | bytes.AddRange(BitConverter.GetBytes(Size)); 77 | bytes.AddRange(BitConverter.GetBytes(BssSize)); 78 | 79 | return bytes; 80 | } 81 | } 82 | 83 | public static byte[] MIi_UncompressBackward(byte[] data) 84 | { 85 | uint leng = BitConverter.ToUInt32(data, data.Length - 4) + (uint)data.Length; 86 | byte[] Result = new byte[leng]; 87 | Array.Copy(data, Result, data.Length); 88 | int offset = (int)(data.Length - (BitConverter.ToUInt32(data, data.Length - 8) >> 24)); 89 | int dstOffs = (int)leng; 90 | while (true) 91 | { 92 | byte header = Result[--offset]; 93 | for (int i = 0; i < 8; i++) 94 | { 95 | if ((header & 0x80) == 0) Result[--dstOffs] = Result[--offset]; 96 | else 97 | { 98 | byte a = Result[--offset]; 99 | byte b = Result[--offset]; 100 | int offs = (((a & 0xF) << 8) | b) + 2;//+ 1; 101 | int length = (a >> 4) + 2; 102 | do 103 | { 104 | Result[dstOffs - 1] = Result[dstOffs + offs]; 105 | dstOffs--; 106 | length--; 107 | } 108 | while (length >= 0); 109 | } 110 | if (offset <= (data.Length - (BitConverter.ToUInt32(data, data.Length - 8) & 0xFFFFFF))) return Result; 111 | header <<= 1; 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Patcher/Overlay/Overlay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.Json; 6 | using HaroohieClub.NitroPacker.Nitro.Card; 7 | 8 | namespace HaroohieClub.NitroPacker.Patcher.Overlay; 9 | 10 | /// 11 | /// An object representing an overlay binary 12 | /// 13 | public class Overlay 14 | { 15 | /// 16 | /// The name of the overlay as designated by NitroPacker 17 | /// 18 | public string Name { get; set; } 19 | /// 20 | /// The integer ID of the overlay (e.g. main_0001's ID is 1) 21 | /// 22 | public int Id { get => int.Parse(Name[^4..], System.Globalization.NumberStyles.HexNumber); } 23 | private List _data; 24 | /// 25 | /// The address at which the overlay is loaded into memory 26 | /// 27 | public uint Address { get; } 28 | /// 29 | /// The size of the overlay in bytes 30 | /// 31 | public int Length => _data.Count; 32 | 33 | /// 34 | /// Creates an overlay 35 | /// 36 | /// The file to load the overlay from 37 | /// The path to the JSON project file produced by NitroPacker on unpack 38 | public Overlay(string file, string projectPath) 39 | { 40 | Name = Path.GetFileNameWithoutExtension(file); 41 | _data = File.ReadAllBytes(file).ToList(); 42 | // Every overlay seems to have functions that write an integer directly after the end of the overlay. 43 | // This ensures that when we begin appending, we don't have any code get overwritten. 44 | _data.AddRange(new byte[4]); 45 | 46 | NdsProjectFile project = JsonSerializer.Deserialize(File.ReadAllText(projectPath)); 47 | RomOverlayTable overlay = project.RomInfo.ARM9Ovt.First(o => o.Id == Id); 48 | Address = overlay.RamAddress; 49 | } 50 | 51 | /// 52 | /// Saves the overlay to a file 53 | /// 54 | /// The file to save the overlay to 55 | public void Save(string file) 56 | { 57 | File.WriteAllBytes(file, _data.ToArray()); 58 | } 59 | 60 | internal void Patch(uint address, byte[] patchData) 61 | { 62 | int loc = (int)(address - Address); 63 | _data.RemoveRange(loc, patchData.Length); 64 | _data.InsertRange(loc, patchData); 65 | } 66 | 67 | internal void Append(byte[] appendData, string projectPath) 68 | { 69 | _data.AddRange(appendData); 70 | NdsProjectFile project = JsonSerializer.Deserialize(File.ReadAllText(projectPath)); 71 | Console.WriteLine($"Expanding RAM size in overlay table for overlay {Id}..."); 72 | project.RomInfo.ARM9Ovt.First(o => o.Id == Id).RamSize = (uint)_data.Count; 73 | File.WriteAllText(projectPath, JsonSerializer.Serialize(project)); 74 | } 75 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/Patcher/Utility.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace HaroohieClub.NitroPacker.Patcher; 5 | 6 | internal static class Utility 7 | { 8 | public static int IndexOfSequence(this IEnumerable items, IEnumerable search) 9 | { 10 | int searchLength = search.Count(); 11 | int lastIndex = items.Count() - searchLength; 12 | for (int i = 0; i < lastIndex; i++) 13 | { 14 | if (items.Skip(i).Take(searchLength).SequenceEqual(search)) 15 | { 16 | return i; 17 | } 18 | } 19 | return -1; 20 | } 21 | } -------------------------------------------------------------------------------- /HaroohieClub.NitroPacker/README.md: -------------------------------------------------------------------------------- 1 | # NitroPacker 2 | 3 | NitroPacker is a .NET library (and utility) for interacting with Nintendo DS ROMs, especially packing and unpacking them. 4 | It is also capable of doing fairly complex ASM patching with the aid of devkitARM for assembly/compilation. 5 | 6 | This README will be expanded in future versions to more comprehensively describe what NitroPacker is capable of. 7 | For now, all the methods and classes have XML/IntelliSense documentation for ease of use. 8 | 9 | Logo designed by [Judy Cheng](https://cjudy.com). -------------------------------------------------------------------------------- /MIGRATE.md: -------------------------------------------------------------------------------- 1 | # NitroPacker 3.0 Breaking API Changes 2 | 3 | ## NuGet Structural Changes 4 | 5 | First off: NitroPacker will now be available on [nuget.org](https://nuget.org/)! If you were previously pulling from the Azure Artifacts feed, you should now be able to safely remove that from your NuGet.config. 6 | 7 | Furthermore, NitroPacker's package structure has been consolidated down to a single package: HaroohieClub.NitroPacker. You can replace the references to any other packages (including the .Core package) with that reference. 8 | 9 | ## API Changes 10 | 11 | ### Project Files 12 | The `ProjectFile` class has been removed and is now simply the `NdsProjectFile` class. Additionally, many properties were renamed, changed, and broken out to better reflect the [gbatek documentation](https://mgba-emu.github.io/gbatek/). 13 | 14 | ### `NdsProjectFile` API Changes 15 | 16 | | 2.x | 3.x | 17 | |-----|-----| 18 | | `FromByteArray` | Use `Deserialize` and pass the path of the project file rather than a byte array | 19 | | `RomHeader`, `NitroFooter`, `RomOVT`, `RomBanner`, `RomFNT` | These classes are no longer subclasses of `Rom` and now exist in the top-level namespace | 20 | | `RomOVT` | `RomOverlayTable` | 21 | | `RSASignature` | `RsaSignature` | 22 | | `RomFNT` | `RomFileNameTable` | 23 | 24 | ### `Rom` API Changes 25 | 26 | | 2.x | 3.x | 27 | |-----|-----| 28 | | `MainRom` | `Arm9Binary` | 29 | | `SubRom` | `Arm7Binary` | 30 | | `Fnt` | `FileNameTable` | 31 | | `MainOVT` | `Arm9OverlayTable` | 32 | | `SubOVT` | `Arm7OverlayTable` | 33 | | `RSASignature` | `RsaSignature` | 34 | 35 | Additionally, the following properties representing various facets of DSi/DSi-enhanced ROMs have been added: 36 | 37 | * `DigestSectorHashtableBinary` 38 | * `DigestBlockHashtableBinary` 39 | * `TwlStaticHeader` 40 | * `Arm9iBinary` 41 | * `Arm7iBinary` 42 | * `DSiWareExtraData` 43 | 44 | All of these are XML-documented and also on gbatek. 45 | 46 | ### `RomBanner` API Changes 47 | 48 | `RomBanner` has been extended to contain all possible versions of the ROM banner: `BannerV1`, `BannerV2`, `BannerV3`, and `BannerV103`. Each banner contains a `Header` and then a `Banner` object representing its actual content. You can use the `Header`'s `Version` property to appropriately cast `Banner` to the proper class. 49 | 50 | At the project level, banners are now stored as a separate binary rather than part of the project file. This allows them to be edited with third-party tools. 51 | 52 | ### `Banner` API Changes 53 | 54 | | 2.x | 3.x | 55 | |-----|-----| 56 | | `Pltt` | `Palette` | 57 | | `GetCrc` | `GetCrcs` – this method adds support for the longer CRCs of the later banner versions | 58 | 59 | ### `NitroFooter` API Changes 60 | 61 | | 2.x | 3.x | 62 | |-----|-----| 63 | | `StartModuleParamsOffset` | `_start_ModuleParamsOffset` | 64 | 65 | ### `RomOverlayTable` API Changes 66 | 67 | | 2.x | 3.x | 68 | |-----|-----| 69 | | `SinitInit` | `StaticInitializerStartAddress` | 70 | | `SinitInitEnd` | `StaticInitializerEndAddress` | 71 | 72 | ### `RomHeader` API Changes 73 | 74 | | 2.x | 3.x | Notes | 75 | |-----|-----|-------| 76 | | `byte ProductId` | `UnitCodes UnitCode` | The new `UnitCodes` enum gives the different unit code values names | 77 | | `DeviceType` | `EncryptionSeedSelect` | | 78 | | `DeviceSize` | `DeviceCapacity` | | 79 | | `byte[] ReservedA` | `DSiFlags Flags` and `RegionOrPermitJump RegionOrJump` | These bytes were previously thought to be "reserved" but are now known to contain two different values which are broken out as seen | 80 | | `GameVersion` | `RomVersion` | | 81 | | `Property` | `AutoStart` | | 82 | | `MainEntryAddress` | `Arm9EntryAddress` | | 83 | | `MainRamAddress` | `Arm9RamAddress` | | 84 | | `SubEntryAddress` | `Arm7EntryAddress` | | 85 | | `SubRamAddress` | `Arm7RamAddress` | | 86 | | `byte[] RomParamA` | `uint NormalCommandSettings` and `uint Key1CommandSettings` | | 87 | | `byte[] RomParamB` | `ushort SecureAreaDelay` | | 88 | | `MainAutoloadDone` | `Arm9AutoloadHookRamAddress` | | 89 | | `SubAutoloadDone` | `Arm7AutoloadHookRamAddress` | | 90 | | `RomParamC` | `SecureAreaDisable` | | 91 | | `RomSize` | `RomSizeExcludingDSiArea` | | 92 | | `ReservedB` | `Arm9ParametersTableOffset`, `Arm7ParametersTableOffset`, `DSiNTRRomRegionEnd`, `DSiTWLRomRegionStart`, and still some `ReservedB` | `ReservedB` is smaller than originally assumed, so it has been broken out into some more properties | 93 | | `LogoData` | `NintendoLogoData` | | 94 | 95 | Finally, many properties have been added as part of the `TwlHeader` class in property `DSiHeader` for DSi and DSi-enhanced games. 96 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | The NitroPacker logo, a red Nintendo DS Lite in a folder 3 |
4 | NitroPacker 5 |
6 | NuGet Version 7 |

8 | NitroPacker is an open source, cross-platform utility for packing and unpacking Nintendo DS ROMs. It also can apply ASM hacks to both ARM9 and overlay files. 9 | 10 | ## Authors 11 | This code was written primarily by [Ermelber](https://www.ermiisoft.net/) and [Gericom](https://github.com/Gericom). [Jonko](https://jonkode.su) has ported it to .NET 8.0 and made modifications to make it cross-platform and has added a number of features since the project was open sourced. The logo was designed by [Judy Cheng](https://cjudy.com). 12 | 13 | ## Prerequisites 14 | * [devkitARM](https://devkitpro.org/wiki/Getting_Started) and [Make](https://www.gnu.org/software/make/) are required to apply ASM hacks. 15 | - Alternatively, one can opt to install [Docker](https://www.docker.com/) (and [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) on Windows) 16 | instead. 17 | 18 | ## Migrating 2.x Projects to 3.x 19 | For versions 3.0 and 3.1 of NitroPacker, the CLI will have built-in functionality for migrating 2.x projects to 3.0. To do this, simply run: 20 | ``` 21 | NitroPacker migrate -p PATH/TO/PROJECT.xml 22 | ``` 23 | However, because of the host of new features available in NitroPacker 3.0, it is strongly recommended to simply unpack your projects again using the 3.0 version. 24 | 25 | For information on the breaking API changes in 3.0, please check [the migration documentation](MIGRATE.md). 26 | 27 | ## Packing and Unpacking 28 | The primary purpose of NitroPacker is to pack and unpack NDS ROMs. 29 | 30 | ### Unpacking 31 | To unpack a ROM with NitroPacker, run the following command: 32 | ``` 33 | NitroPacker unpack -r PATH/TO/ROM.nds -o PATH/TO/UNPACK/DIRECTORY -p PROJECT_NAME 34 | ``` 35 | Replace the capitalized arguments with: 36 | * `PATH/TO/ROM.nds` – the path to the Nintendo DS ROM you want to unpack 37 | * `PATH/TO/UNPACK/DIRECTORY` – the path to the directory you want to unpack the ROM to 38 | * `PROJECT_NAME` – the name of the project file that will be created on unpack (can be anything but usually should be the name of the game) 39 | 40 | This command will unpack all the game's files and place them in the specified directory. The ROM's file system will go in the `data` subdirectory, while 41 | any overlays present will be placed in the `overlay` subdirectory. The main ARM9 and ARM7 binaries will be placed in the root directory. Additionally, the created project file contains a bunch of metadata about the ROM, such as the ROM header, the overlay table, and the banner. 42 | 43 | In order to disassemble or patch arm9.bin, you'll need it to be decompressed. If the arm9 binary is compressed, you can append the `-d` flag to the above command to decompress it, e.g. 44 | ``` 45 | NitroPacker unpack -r PATH/TO/ROM.nds -o PATH/TO/UNPACK/DIRECTORY -p PROJECT_NAME -d 46 | ``` 47 | 48 | Finally, it is possible some games may require that their file allocation tables be in a certain order. In this case, to be able to repack successfully, you will need to pass `-f`. 49 | 50 | ### Packing 51 | NitroPacker can only pack a directory that was previously unpacked with NitroPacker (or something structured exactly like that). To pack a ROM, run the following command: 52 | ``` 53 | NitroPacker pack -p PATH/TO/PROJECT_FILE.json -r PATH/TO/OUTPUT_ROM.nds 54 | ``` 55 | 56 | The project file is expected to be in a directory with ARM9 and ARM7 binaries as well as the data and overlay subdirectories mentioned previously. If the ARM9 binary was decompressed previously, you will need to add `-c` in order to compress it at this stage. 57 | 58 | ## ASM Hacks 59 | NitroPacker can also be used as a powerful tool to apply assembly hacks, but this functionality is a bit more complicated. 60 | 61 | ### Install Prerequisites 62 | As mentioned above, NitroPacker relies on other programs to help with the assembling your hacks, namely, devkitARM and Make. DevkitARM is distributed 63 | by devkitPro and can be downloaded from their website. They have a graphical installer for Windows (for which you need only select the NDS workloads) 64 | and a package manager for Mac/Linux (for which you can run the command `dkp-pacman -S nds-dev` to install devkitARM). 65 | 66 | On Linux distros, Make can be installed from the package manager (e.g. `sudo apt install make`). On macOS, you can use [brew](https://formulae.brew.sh/formula/make). On Windows, the easiest way to install it is with Chocolatey, where the command is `choco install make`. 67 | 68 | If either of these options presents a challenge for you or doesn't work for some reason, you can instead opt to use the alternate method of assembling 69 | the code in Docker containers. After installing [Docker Desktop](https://www.docker.com/products/docker-desktop/) or the Docker engine, ensure the engine 70 | is running. Then, when calling NitroPacker, ensure you pass `-d` followed by the Docker tag you want to use. If you're not sure which one to use, 71 | we recommend choosing `latest`. See [devkitpro/devkitarm on DockerHub](https://hub.docker.com/r/devkitpro/devkitarm) for more details. 72 | 73 | ### Directory Structure 74 | You will need to move the unpacked `arm9.bin` to a different directory with a specific structure: 75 | 76 | * `src` 77 | - `overlays` 78 | - `main_HEXOVERLAYNUM` – the name here corresponds to the name of the overlay in the unpacked `overlay` directory 79 | - `replSource` – a directory containing subdirectories with instructions to be directly replaced in the specified overlay 80 | - `HEXLOCATION` – the location of an instruction to replace 81 | - `HEXLOCATION.s` – a source file containing an instruction to replace at the specified `HEXLOCATION` 82 | - `source` – a directory containing source files to append to the specified overlay 83 | - `file.s` – ARM assembly source files that will be assembled and appended to the overlay 84 | - `file.c` – C source files that will be compiled and appended to the overlay 85 | - `file.cpp` – C++ source file that will be compiled and appended to the overlay 86 | - `file.s.override` – a source file that will be assembled _only if_ `--override-suffix "override"` is passed to NitroPacker (and will replace `file.s` in that case) 87 | - `symbols.x` – an editable symbols file that allows for naming specific offsets for use in specified overlay's source files 88 | - `linker.x` – boilerplate linker file for overlays 89 | - `Makefile` – boilerplate Makefile for overlays 90 | - `replSource` – a directory containing subdirectories with instructions to be directly replaced in the ARM9 91 | - `HEXLOCATION` – the location of an instruction to replace 92 | - `HEXLOCATION.s` – a source file containing an instruction to replace at the specified `HEXLOCATION` 93 | - `source` – a directory contianing source files to append to the ARM9 94 | - `file.s` – ARM assembly source files that will be assembled and appended to the ARM9 95 | - `file.c` – C source files that will be compiled and appended to the ARM9 96 | - `file.cpp` – C++ source file that will be compiled and appended to the ARM9 97 | - `file.s.override` – a source file that will be assembled _only if_ `--override-suffix "override"` is passed to NitroPacker (and will replace `file.s` in that case) 98 | - `arm9.bin` – the main ARM9 – must be copied in from the unpacked ROM directory 99 | - `.clang-format` – boilerplate CLang format file 100 | - `linker.x` – boilerplate linker file for ARM9 101 | - `Makefile` – boilerplate Makefile for ARM9 102 | - `symbols.x` – an editable symbols file that allows for naming specific offsets for use in ARM9 source files 103 | 104 | This directory structure should be copied from the `asm_sample` directory in this repo. A fully fleshed-out example can be found in the [ChokuretsuTranslationBuild repository](https://github.com/haroohie-club/ChokuretsuTranslationBuild/tree/main/src). 105 | 106 | Additionally, prior to assembling the ARM9, you will need to find the arena lo offset. This value can be found by searching for `0x37F8000` in the ROM. 107 | IDA or Ghidra should be used to search for this value. Once you find it, you'll want to go back a few values until you see an offset that has a value 108 | between 0x20C0000 and 0x20F0000 (typically, although it could be higher or lower!) – the offset (not the value) is the arena lo offset. 109 | It will most likely be followed by two subroutines that look like: 110 | 111 | ```arm 112 | MOV R0, R0,LSL#2 113 | ADD R0, R0, #0x2700000 114 | ADD R0, R0, #0xFF000 115 | STR R1, [R0,#0xDC4] 116 | BX LR 117 | 118 | ... 119 | 120 | MOV R0, R0,LSL#2 121 | ADD R0, R0, #0x2700000 122 | ADD R0, R0, #0xFF000 123 | STR R1, [R0,#0xDA0] 124 | BX LR 125 | ``` 126 | 127 | Finally, source files may have any valid ARM assembly. To hook into a function and create a branch link to your code, the following format should be used: 128 | 129 | ```arm 130 | ahook_02061344: 131 | CODE_HERE 132 | bx lr 133 | ``` 134 | 135 | This method will replace the instruction at 0x02061344 with a BL to your code. 136 | 137 | Once this directory has been constructed, source files have been created, the arena lo offset determined, and the ARM9 has been copied in, the following commands may be used: 138 | 139 | ### Assemble ARM9 140 | To assemble the hacked ARM9, run the following command: 141 | ``` 142 | NitroPacker patch-arm9 -i PATH/TO/SRC/DIRECTORY -o PATH/TO/OUTPUT/DIRECTORY -p PROJECT_FILE -a ARENA_LO_OFFSET [-d DOCKER_TAG] 143 | ``` 144 | The project file you need to supply is the one created by unpacking the ROM with NitroPacker. The project file is only needed for determining the ARM9's RAM address; if you would rather specify this address manually, you may use `-r RAM_ADDRESS` instead. 145 | 146 | This will assemble the hacks and append them to `arm9.bin`, then copy the ARM9 to the output directory (which should be the directory you will then run 147 | the `pack` command on). 148 | 149 | Note: To be able to use the `patch-arm9` command, the `arm9.bin` *must* be decompressed. See the [`Unpacking`](#Unpacking) section for more information on how to do this. 150 | 151 | ### Assemble Overlays 152 | To patch the overlays, run the following command: 153 | ``` 154 | NitroPacker patch-overlays -i PATH/TO/ORIGINAL/OVERLAY/DIRECTORY -o PATH/TO/PATCHED/OVERLAY/DIRECTORY -s PATH/TO/OVERLAY/SOURCE -r PATH/TO/PROJECT/FILE [-d DOCKER_TAG] 155 | ``` 156 | 157 | ## Building from Source 158 | To build the project from source, ensure [.NET 8](https://dot.net) is installed. After you have installed .NET, you should be able 159 | to simply run `dotnet build` from the terminal within the root directory and it will build the NitroPacker executable. You can also 160 | open the solution in Visual Studio, Rider, or another IDE and build it from there. -------------------------------------------------------------------------------- /asm_sample/.clang-format: -------------------------------------------------------------------------------- 1 | IndentWidth: 4 2 | TabWidth: 4 3 | UseTab: Never 4 | AllowShortIfStatementsOnASingleLine: false 5 | AllowShortFunctionsOnASingleLine: None 6 | IndentCaseLabels: true 7 | AllowAllParametersOfDeclarationOnNextLine: true 8 | ColumnLimit: 80 9 | BreakBeforeBraces: Custom 10 | BraceWrapping: 11 | AfterControlStatement: true 12 | AfterEnum: true 13 | AfterFunction: true 14 | AfterStruct: true 15 | AfterUnion: true 16 | BeforeElse: false 17 | IndentBraces: false -------------------------------------------------------------------------------- /asm_sample/Makefile: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #--------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITARM)),) 6 | $(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") 7 | endif 8 | 9 | #--------------------------------------------------------------------------------- 10 | # canned command sequence for binary data 11 | #--------------------------------------------------------------------------------- 12 | define bin2o 13 | bin2s $< | $(AS) -o $(@) 14 | echo "extern const u8" `(echo $( `(echo $(> `(echo $(> `(echo $( $@ 151 | @echo written the symbol table ... $(notdir $@) 152 | 153 | #--------------------------------------------------------------------------------- 154 | %.elf: $(OFILES) 155 | @echo linking $(notdir $@) 156 | $(LD) $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ 157 | 158 | #--------------------------------------------------------------------------------- 159 | %.bin.o : %.bin 160 | #--------------------------------------------------------------------------------- 161 | @echo $(notdir $<) 162 | $(bin2o) 163 | 164 | #--------------------------------------------------------------------------------- 165 | %.o: %.cpp 166 | @echo $(notdir $<) 167 | $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) -c $< -o $@ $(ERROR_FILTER) 168 | 169 | #--------------------------------------------------------------------------------- 170 | %.o: %.c 171 | @echo $(notdir $<) 172 | $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) -c $< -o $@ $(ERROR_FILTER) 173 | 174 | #--------------------------------------------------------------------------------- 175 | %.o: %.s 176 | @echo $(notdir $<) 177 | $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@ $(ERROR_FILTER) 178 | 179 | #--------------------------------------------------------------------------------- 180 | # This rule creates assembly source files using grit 181 | # grit takes an image file and a .grit describing how the file is to be processed 182 | # add additional rules like this for each image extension 183 | # you use in the graphics folders 184 | #--------------------------------------------------------------------------------- 185 | %.s %.h : %.png %.grit 186 | #--------------------------------------------------------------------------------- 187 | grit $< -fts -o$* 188 | 189 | -include $(DEPSDIR)/*.d 190 | 191 | #--------------------------------------------------------------------------------------- 192 | endif 193 | #--------------------------------------------------------------------------------------- 194 | 195 | -------------------------------------------------------------------------------- /asm_sample/linker.x: -------------------------------------------------------------------------------- 1 | OUTPUT_ARCH(arm) 2 | 3 | SECTIONS { 4 | 5 | 6 | 7 | .text : { 8 | 9 | 10 | FILL (0x1234) 11 | 12 | __text_start = . ; 13 | *(.init) 14 | *(.text) 15 | *(.ctors) 16 | *(.dtors) 17 | *(.rodata) 18 | *(.fini) 19 | *(COMMON) 20 | *(.data) 21 | __text_end = . ; 22 | 23 | __bss_start__ = . ; 24 | *(.bss) 25 | __bss_end__ = . ; 26 | _end = __bss_end__ ; 27 | __end__ = __bss_end__ ; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /asm_sample/overlays/Makefile: -------------------------------------------------------------------------------- 1 | #--------------------------------------------------------------------------------- 2 | .SUFFIXES: 3 | #--------------------------------------------------------------------------------- 4 | 5 | ifeq ($(strip $(DEVKITARM)),) 6 | $(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") 7 | endif 8 | 9 | #--------------------------------------------------------------------------------- 10 | # canned command sequence for binary data 11 | #--------------------------------------------------------------------------------- 12 | define bin2o 13 | bin2s $< | $(AS) -o $(@) 14 | echo "extern const u8" `(echo $( `(echo $(> `(echo $(> `(echo $( $@ 151 | @echo written the symbol table ... $(notdir $@) 152 | 153 | #--------------------------------------------------------------------------------- 154 | %.elf: $(OFILES) 155 | @echo linking $(notdir $@) 156 | $(LD) $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@ 157 | 158 | #--------------------------------------------------------------------------------- 159 | %.bin.o : %.bin 160 | #--------------------------------------------------------------------------------- 161 | @echo $(notdir $<) 162 | $(bin2o) 163 | 164 | #--------------------------------------------------------------------------------- 165 | %.o: %.cpp 166 | @echo $(notdir $<) 167 | $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) -c $< -o $@ $(ERROR_FILTER) 168 | 169 | #--------------------------------------------------------------------------------- 170 | %.o: %.c 171 | @echo $(notdir $<) 172 | $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) -c $< -o $@ $(ERROR_FILTER) 173 | 174 | #--------------------------------------------------------------------------------- 175 | %.o: %.s 176 | @echo $(notdir $<) 177 | $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@ $(ERROR_FILTER) 178 | 179 | #--------------------------------------------------------------------------------- 180 | # This rule creates assembly source files using grit 181 | # grit takes an image file and a .grit describing how the file is to be processed 182 | # add additional rules like this for each image extension 183 | # you use in the graphics folders 184 | #--------------------------------------------------------------------------------- 185 | %.s %.h : %.png %.grit 186 | #--------------------------------------------------------------------------------- 187 | grit $< -fts -o$* 188 | 189 | -include $(DEPSDIR)/*.d 190 | 191 | #--------------------------------------------------------------------------------------- 192 | endif 193 | #--------------------------------------------------------------------------------------- 194 | 195 | -------------------------------------------------------------------------------- /asm_sample/overlays/linker.x: -------------------------------------------------------------------------------- 1 | OUTPUT_ARCH(arm) 2 | 3 | SECTIONS { 4 | 5 | 6 | 7 | .text : { 8 | 9 | 10 | FILL (0x1234) 11 | 12 | __text_start = . ; 13 | *(.init) 14 | *(.text) 15 | *(.ctors) 16 | *(.dtors) 17 | *(.rodata) 18 | *(.fini) 19 | *(COMMON) 20 | *(.data) 21 | __text_end = . ; 22 | 23 | __bss_start__ = . ; 24 | *(.bss) 25 | __bss_end__ = . ; 26 | _end = __bss_end__ ; 27 | __end__ = __bss_end__ ; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /asm_sample/overlays/main_000C/replSource/20CB424/20CB424.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asm_sample/overlays/main_000C/source/placeholder.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asm_sample/overlays/main_000C/symbols.x: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/asm_sample/overlays/main_000C/symbols.x -------------------------------------------------------------------------------- /asm_sample/replSource/2035A94/2035A94.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asm_sample/source/placeholder.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asm_sample/symbols.x: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haroohie-club/NitroPacker/4d9a9d15d090f5dfb571638f1e87cea5466fdad0/asm_sample/symbols.x -------------------------------------------------------------------------------- /azure-pipelines-release.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - none 3 | pr: 4 | - none 5 | 6 | parameters: 7 | - name: version 8 | displayName: Release Version 9 | type: string 10 | default: '' 11 | - name: releaseNotes 12 | displayName: Release Notes 13 | type: string 14 | default: '' 15 | 16 | variables: 17 | - name: Version 18 | value: ${{ parameters.version }} 19 | - group: NitroPackerSecrets 20 | 21 | stages: 22 | - stage: Build 23 | jobs: 24 | - job: 25 | strategy: 26 | matrix: 27 | Linux: 28 | imageName: 'ubuntu-latest' 29 | artifactName: 'NitroPacker-Linux-$(Version)' 30 | rid: 'linux-x64' 31 | artifactPath: '$(Build.SourcesDirectory)/NitroPacker-Linux-$(Version).tar.gz' 32 | macOS: 33 | imageName: 'macOS-latest' 34 | artifactName: 'NitroPacker-macOS-$(Version)' 35 | rid: 'osx-x64' 36 | artifactPath: '$(Build.SourcesDirectory)/NitroPacker-macOS-$(Version).tar.gz' 37 | Windows: 38 | imageName: 'windows-latest' 39 | artifactName: 'NitroPacker-Windows-$(Version)' 40 | rid: 'win-x64' 41 | artifactPath: '$(Build.SourcesDirectory)/NitroPacker-Windows-$(Version).zip' 42 | displayName: Build & Publish 43 | pool: 44 | vmImage: $(imageName) 45 | steps: 46 | - checkout: self 47 | clean: true 48 | 49 | - task: DotNetCoreCLI@2 50 | inputs: 51 | command: 'publish' 52 | projects: $(Build.SourcesDirectory)/HaroohieClub.NitroPacker.Cli/HaroohieClub.NitroPacker.Cli.csproj 53 | arguments: '-c Release -f net8.0 -r $(rid) --self-contained /p:PublishSingleFile=true' 54 | publishWebProjects: false 55 | displayName: Build & Publish NitroPacker 56 | 57 | - pwsh: | 58 | mkdir publish 59 | Expand-Archive -Path $(Build.SourcesDirectory)/HaroohieClub.NitroPacker.Cli/bin/Release/net8.0/$(rid)/publish.zip -DestinationPath ./publish 60 | Push-Location publish 61 | chmod +x ./NitroPacker 62 | tar -czvf $(artifactPath) ./NitroPacker 63 | Pop-Location 64 | condition: ne(variables['imageName'], 'windows-latest') 65 | displayName: Create tar 66 | 67 | - pwsh: | 68 | mkdir publish 69 | Expand-Archive -Path $(Build.SourcesDirectory)/HaroohieClub.NitroPacker.Cli/bin/Release/net8.0/$(rid)/publish.zip .\publish 70 | Push-Location publish 71 | Compress-Archive -Path .\NitroPacker.exe -DestinationPath $(artifactPath) 72 | Pop-Location 73 | condition: eq(variables['imageName'], 'windows-latest') 74 | displayName: Create zip 75 | 76 | - task: PublishBuildArtifacts@1 77 | inputs: 78 | PathtoPublish: '$(artifactPath)' 79 | ArtifactName: '$(artifactName)' 80 | publishLocation: 'Container' 81 | displayName: Publish build artifact 82 | 83 | - stage: Publish_Packages 84 | dependsOn: [] 85 | jobs: 86 | - job: 87 | pool: 88 | vmImage: ubuntu-latest 89 | displayName: Build and publish nupkgs 90 | steps: 91 | - task: NuGetAuthenticate@1 92 | displayName: NuGet Authenticate 93 | 94 | - script: | 95 | dotnet build $(Build.SourcesDirectory)/HaroohieClub.NitroPacker/HaroohieClub.NitroPacker.csproj --configuration Release 96 | dotnet pack $(Build.SourcesDirectory)/HaroohieClub.NitroPacker/HaroohieClub.NitroPacker.csproj --output $(Build.ArtifactStagingDirectory) --configuration Release 97 | displayName: Build and pack nupkgs 98 | 99 | - script: dotnet nuget push --api-key $(NuGetKey) -s nuget.org $(Build.ArtifactStagingDirectory)/HaroohieClub.NitroPacker.$(Version).nupkg 100 | displayName: NuGet push 101 | 102 | - stage: Release 103 | dependsOn: Build 104 | jobs: 105 | - job: 106 | pool: 107 | vmImage: ubuntu-latest 108 | displayName: Create release 109 | steps: 110 | - task: DownloadBuildArtifacts@0 111 | displayName: Download Linux artifacts 112 | inputs: 113 | buildType: 'current' 114 | downloadType: 'single' 115 | artifactName: 'NitroPacker-Linux-$(Version)' 116 | downloadPath: '$(Build.ArtifactStagingDirectory)' 117 | - task: DownloadBuildArtifacts@0 118 | displayName: Download macOS artifacts 119 | inputs: 120 | buildType: 'current' 121 | downloadType: 'single' 122 | artifactName: 'NitroPacker-macOS-$(Version)' 123 | downloadPath: '$(Build.ArtifactStagingDirectory)' 124 | - task: DownloadBuildArtifacts@0 125 | displayName: Download Windows artifacts 126 | inputs: 127 | buildType: 'current' 128 | downloadType: 'single' 129 | artifactName: 'NitroPacker-Windows-$(Version)' 130 | downloadPath: '$(Build.ArtifactStagingDirectory)' 131 | - pwsh: | 132 | Move-Item -Path $(Build.ArtifactStagingDirectory)/NitroPacker-Linux-$(Version)/NitroPacker-Linux-$(Version).tar.gz -Destination $(Build.ArtifactStagingDirectory)/NitroPacker-Linux-$(Version).tar.gz 133 | Move-Item -Path $(Build.ArtifactStagingDirectory)/NitroPacker-macOS-$(Version)/NitroPacker-macOS-$(Version).tar.gz -Destination $(Build.ArtifactStagingDirectory)/NitroPacker-macOS-$(Version).tar.gz 134 | Move-Item -Path $(Build.ArtifactStagingDirectory)/NitroPacker-Windows-$(Version)/NitroPacker-Windows-$(Version).zip -Destination $(Build.ArtifactStagingDirectory)/NitroPacker-Windows-$(Version).zip 135 | displayName: Move artifacts 136 | - task: GitHubRelease@1 137 | displayName: 'Create GitHub Release' 138 | inputs: 139 | gitHubConnection: 'GitHub Connection (Jonko)' 140 | tagSource: userSpecifiedTag 141 | tag: '$(Version)' 142 | title: 'NitroPacker v$(Version)' 143 | releaseNotesSource: inline 144 | releaseNotesInline: | 145 | ${{ parameters.releaseNotes }} --------------------------------------------------------------------------------