mirror of
https://github.com/AdmiralCurtiss/WfcPatcher.git
synced 2026-03-21 18:04:23 -05:00
541 lines
18 KiB
C#
541 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
|
|
namespace WfcPatcher {
|
|
class Program {
|
|
public static string ProgramName {
|
|
get {
|
|
Version version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version;
|
|
#if DEBUG
|
|
string versionString = version.ToString() + " (Debug)";
|
|
#else
|
|
string versionString = version.Major + "." + version.Minor;
|
|
#endif
|
|
return "WfcPatcher " + versionString;
|
|
}
|
|
}
|
|
|
|
static void Main( string[] args ) {
|
|
Console.WriteLine( ProgramName );
|
|
Console.WriteLine();
|
|
|
|
if ( !CommandLineArguments.ParseCommandLineArguments( args ) ) {
|
|
Console.WriteLine( "Error parsing command line options!" );
|
|
PrintUsage();
|
|
return;
|
|
}
|
|
|
|
if ( CommandLineArguments.Filenames.Length == 0 ) {
|
|
PrintUsage();
|
|
return;
|
|
}
|
|
|
|
string domainFilenamePart = "NoSSL";
|
|
if ( CommandLineArguments.Domain != null ) {
|
|
domainFilenamePart = CommandLineArguments.Domain;
|
|
}
|
|
foreach ( string filename in CommandLineArguments.Filenames ) {
|
|
string newFilename = System.IO.Path.Combine( System.IO.Path.GetDirectoryName( filename ), System.IO.Path.GetFileNameWithoutExtension( filename ) ) + " (" + domainFilenamePart + ")" + System.IO.Path.GetExtension( filename );
|
|
#if !DEBUG
|
|
try {
|
|
#endif
|
|
if ( PatchFile( filename, newFilename ) ) {
|
|
Console.WriteLine( "Patched to " + newFilename + "!" );
|
|
Console.WriteLine();
|
|
} else {
|
|
Console.WriteLine( "Found nothing to patch in " + filename + "." );
|
|
Console.WriteLine( "Are you sure this is a WFC-enabled game?" );
|
|
Console.WriteLine();
|
|
System.IO.File.Delete( newFilename );
|
|
}
|
|
#if !DEBUG
|
|
} catch ( Exception ex ) {
|
|
Console.WriteLine( "Failed patching " + filename );
|
|
Console.WriteLine( ex.ToString() );
|
|
Console.WriteLine();
|
|
System.IO.File.Delete( newFilename );
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public static void PrintUsage() {
|
|
Console.WriteLine( "Usage: WfcPatcher [options] game1.nds [game2.nds] [game3.nds] [...]" );
|
|
Console.WriteLine();
|
|
Console.WriteLine( "Command line options:" );
|
|
Console.WriteLine( " -d, --domain example.com" );
|
|
Console.WriteLine( " Point the NWFC URLs to a different URL instead." );
|
|
}
|
|
|
|
public static string GetGamecode( System.IO.FileStream nds ) {
|
|
long pos = nds.Position;
|
|
nds.Position = 0x0C;
|
|
string gamecode = nds.ReadAscii( 4 );
|
|
nds.Position = pos;
|
|
return gamecode;
|
|
}
|
|
|
|
static bool PatchFile( string filename, string newFilename ) {
|
|
Console.WriteLine( "Reading and copying " + filename + "..." );
|
|
using ( var nds = new System.IO.FileStream( newFilename, System.IO.FileMode.Create ) ) {
|
|
using ( var ndsSrc = new System.IO.FileStream( filename, System.IO.FileMode.Open ) ) {
|
|
Util.CopyStream( ndsSrc, nds, (int)ndsSrc.Length );
|
|
ndsSrc.Close();
|
|
}
|
|
|
|
// http://dsibrew.org/wiki/DSi_Cartridge_Header
|
|
|
|
// arm
|
|
Console.WriteLine( "Patching ARM Executables..." );
|
|
nds.Position = 0x20;
|
|
uint arm9offset = nds.ReadUInt32();
|
|
uint arm9entry = nds.ReadUInt32();
|
|
uint arm9load = nds.ReadUInt32();
|
|
uint arm9size = nds.ReadUInt32();
|
|
uint arm7offset = nds.ReadUInt32();
|
|
uint arm7entry = nds.ReadUInt32();
|
|
uint arm7load = nds.ReadUInt32();
|
|
uint arm7size = nds.ReadUInt32();
|
|
|
|
bool modArm9 = PatchArm9( nds, arm9offset, arm9size );
|
|
bool modArm7 = PatchArm7( nds, arm7offset, arm7size );
|
|
|
|
// overlays
|
|
Console.WriteLine( "Patching Overlays..." );
|
|
nds.Position = 0x50;
|
|
uint arm9overlayoff = nds.ReadUInt32();
|
|
uint arm9overlaylen = nds.ReadUInt32();
|
|
uint arm7overlayoff = nds.ReadUInt32();
|
|
uint arm7overlaylen = nds.ReadUInt32();
|
|
|
|
bool modOvl9 = PatchOverlay( nds, arm9overlayoff, arm9overlaylen );
|
|
bool modOvl7 = PatchOverlay( nds, arm7overlayoff, arm7overlaylen );
|
|
|
|
nds.Close();
|
|
|
|
return modArm9 || modArm7 || modOvl9 || modOvl7;
|
|
}
|
|
}
|
|
|
|
static bool PatchArm9( System.IO.FileStream nds, uint pos, uint len ) {
|
|
nds.Position = pos;
|
|
byte[] data = new byte[len];
|
|
nds.Read( data, 0, (int)len );
|
|
|
|
// decompress size info: http://www.crackerscrap.com/docs/dsromstructure.html
|
|
// TODO: Is there a better way to figure out if an ARM9 is compressed?
|
|
|
|
nds.Position = nds.Position - 8;
|
|
uint compressedSize = nds.ReadUInt24();
|
|
byte headerLength = (byte)nds.ReadByte();
|
|
uint additionalCompressedSize = nds.ReadUInt32();
|
|
uint decompressedSize = additionalCompressedSize + len;
|
|
|
|
bool compressed = false;
|
|
byte[] decData = data;
|
|
|
|
#if DEBUG
|
|
Console.WriteLine( "ARM9 old dec size: 0x" + decompressedSize.ToString( "X6" ) );
|
|
Console.WriteLine( "ARM9 old cmp size: 0x" + compressedSize.ToString( "X6" ) );
|
|
Console.WriteLine( "ARM9 old filesize: 0x" + len.ToString( "X6" ) );
|
|
Console.WriteLine( "ARM9 old diff: 0x" + additionalCompressedSize.ToString( "X6" ) );
|
|
|
|
System.IO.File.WriteAllBytes( "arm9-raw.bin", data );
|
|
#endif
|
|
|
|
blz blz = new blz();
|
|
// if one of these isn't true then it can't be blz-compressed so don't even try
|
|
bool headerLengthValid = ( headerLength >= 8 && headerLength <= 11 );
|
|
bool compressedSizeValid = ( data.Length >= compressedSize + 0x4000 && data.Length <= compressedSize + 0x400B );
|
|
if ( headerLengthValid && compressedSizeValid ) {
|
|
try {
|
|
blz.arm9 = 1;
|
|
byte[] maybeDecData = blz.BLZ_Decode( data );
|
|
|
|
if ( maybeDecData.Length == decompressedSize ) {
|
|
compressed = true;
|
|
decData = maybeDecData;
|
|
#if DEBUG
|
|
System.IO.File.WriteAllBytes( "arm9-dec.bin", decData );
|
|
#endif
|
|
}
|
|
} catch ( blzDecodingException ) {
|
|
compressed = false;
|
|
}
|
|
}
|
|
|
|
byte[] decDataUnmodified = (byte[])decData.Clone();
|
|
if ( ReplaceInData( decData, 0x00, true ) ) {
|
|
if ( compressed ) {
|
|
Console.WriteLine( "Replacing and recompressing ARM9..." );
|
|
data = blz.BLZ_Encode( decData, 0 );
|
|
|
|
uint newCompressedSize = (uint)data.Length;
|
|
if ( newCompressedSize > len ) {
|
|
// new ARM is actually bigger, redo without the additional nullterm replacement
|
|
decData = decDataUnmodified;
|
|
ReplaceInData( decData, 0x00, false );
|
|
data = blz.BLZ_Encode( decData, 0, supressWarnings: true );
|
|
newCompressedSize = (uint)data.Length;
|
|
|
|
int arm9diff = (int)len - (int)newCompressedSize;
|
|
if ( arm9diff < 0 ) {
|
|
// still too big, remove debug strings
|
|
if ( !RemoveStringsInKnownGames( GetGamecode( nds ), decData ) ) {
|
|
RemoveDebugStrings( decData );
|
|
}
|
|
#if DEBUG
|
|
System.IO.File.WriteAllBytes( "arm9-dec-without-debug.bin", decData );
|
|
#endif
|
|
data = blz.BLZ_Encode( decData, 0, supressWarnings: true );
|
|
newCompressedSize = (uint)data.Length;
|
|
|
|
arm9diff = (int)len - (int)newCompressedSize;
|
|
if ( arm9diff < 0 ) {
|
|
Console.WriteLine( "WARNING: Recompressed ARM9 is " + -arm9diff + " bytes bigger than original!" );
|
|
Console.WriteLine( " Patched game may be corrupted!" );
|
|
#if DEBUG
|
|
System.IO.File.WriteAllBytes( "arm9-too-big-recomp.bin", data );
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( newCompressedSize != len ) {
|
|
// new ARM is (still) different, attempt to find the metadata in the ARM9 secure area and replace that
|
|
bool foundSize = false;
|
|
for ( int i = 0; i < 0x4000; i += 4 ) {
|
|
uint maybeSize = BitConverter.ToUInt32( data, i );
|
|
if ( maybeSize == len + 0x02000000u || maybeSize == len + 0x02004000u ) {
|
|
foundSize = true;
|
|
|
|
byte[] newCmpSizeBytes;
|
|
if ( maybeSize == len + 0x02004000u ) {
|
|
newCmpSizeBytes = BitConverter.GetBytes( newCompressedSize + 0x02004000u );
|
|
} else {
|
|
newCmpSizeBytes = BitConverter.GetBytes( newCompressedSize + 0x02000000u );
|
|
}
|
|
|
|
data[i + 0] = newCmpSizeBytes[0];
|
|
data[i + 1] = newCmpSizeBytes[1];
|
|
data[i + 2] = newCmpSizeBytes[2];
|
|
data[i + 3] = newCmpSizeBytes[3];
|
|
break;
|
|
}
|
|
}
|
|
if ( !foundSize ) {
|
|
Console.WriteLine( "WARNING: Recompressed ARM9 is different size, and size could not be found in secure area!" );
|
|
Console.WriteLine( " Patched game will probably not boot!" );
|
|
}
|
|
}
|
|
#if DEBUG
|
|
uint newDecompressedSize = (uint)decData.Length;
|
|
uint newAdditionalCompressedSize = newDecompressedSize - newCompressedSize;
|
|
Console.WriteLine( "ARM9 new dec size: 0x" + newDecompressedSize.ToString( "X6" ) );
|
|
Console.WriteLine( "ARM9 new cmp size: 0x" + newCompressedSize.ToString( "X6" ) );
|
|
Console.WriteLine( "ARM9 new diff: 0x" + newAdditionalCompressedSize.ToString( "X6" ) );
|
|
#endif
|
|
} else {
|
|
Console.WriteLine( "Replacing ARM9..." );
|
|
data = decData;
|
|
}
|
|
#if DEBUG
|
|
System.IO.File.WriteAllBytes( "arm9-new.bin", data );
|
|
#endif
|
|
|
|
nds.Position = pos;
|
|
nds.Write( data, 0, data.Length );
|
|
|
|
int newSize = data.Length;
|
|
int diff = (int)len - newSize;
|
|
|
|
// copy back footer
|
|
if ( diff > 0 ) {
|
|
List<byte> footer = new List<byte>();
|
|
nds.Position = pos + len;
|
|
if ( nds.PeekUInt32() == 0xDEC00621 ) {
|
|
for ( int j = 0; j < 12; ++j ) {
|
|
footer.Add( (byte)nds.ReadByte() );
|
|
}
|
|
|
|
nds.Position = pos + newSize;
|
|
nds.Write( footer.ToArray(), 0, footer.Count );
|
|
}
|
|
|
|
// padding
|
|
for ( int j = 0; j < diff; ++j ) {
|
|
nds.WriteByte( 0xFF );
|
|
}
|
|
}
|
|
|
|
// write new size
|
|
byte[] newSizeBytes = BitConverter.GetBytes( newSize );
|
|
nds.Position = 0x2C;
|
|
nds.Write( newSizeBytes, 0, 4 );
|
|
|
|
// recalculate checksums
|
|
nds.Position = pos;
|
|
ushort secureChecksum = new Crc16().ComputeChecksum( nds, 0x4000, 0xFFFF );
|
|
nds.Position = 0x6C;
|
|
nds.Write( BitConverter.GetBytes( secureChecksum ), 0, 2 );
|
|
|
|
nds.Position = 0;
|
|
ushort headerChecksum = new Crc16().ComputeChecksum( nds, 0x15E, 0xFFFF );
|
|
nds.Write( BitConverter.GetBytes( headerChecksum ), 0, 2 );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool PatchArm7( System.IO.FileStream nds, uint pos, uint len ) {
|
|
nds.Position = pos;
|
|
byte[] data = new byte[len];
|
|
nds.Read( data, 0, (int)len );
|
|
|
|
if ( ReplaceInData( data ) ) {
|
|
Console.WriteLine( "Replacing ARM7..." );
|
|
nds.Position = pos;
|
|
nds.Write( data, 0, data.Length );
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool PatchOverlay( System.IO.FileStream nds, uint pos, uint len ) {
|
|
// http://sourceforge.net/p/devkitpro/ndstool/ci/master/tree/source/ndsextract.cpp
|
|
// http://sourceforge.net/p/devkitpro/ndstool/ci/master/tree/source/overlay.h
|
|
// header compression info from http://gbatemp.net/threads/recompressing-an-overlay-file.329576/
|
|
|
|
nds.Position = 0x048;
|
|
uint fatOffset = nds.ReadUInt32();
|
|
|
|
bool modified = false;
|
|
for ( uint i = 0; i < len; i += 0x20 ) {
|
|
nds.Position = pos + i;
|
|
uint id = nds.ReadUInt32();
|
|
uint ramAddr = nds.ReadUInt32();
|
|
uint ramSize = nds.ReadUInt32();
|
|
uint bssSize = nds.ReadUInt32();
|
|
uint sinitInit = nds.ReadUInt32();
|
|
uint sinitInitEnd = nds.ReadUInt32();
|
|
uint fileId = nds.ReadUInt32();
|
|
uint compressedSize = nds.ReadUInt24();
|
|
byte compressedBitmask = (byte)nds.ReadByte();
|
|
|
|
nds.Position = fatOffset + 8 * id;
|
|
uint overlayPositionStart = nds.ReadUInt32();
|
|
uint overlayPositionEnd = nds.ReadUInt32();
|
|
uint overlaySize = overlayPositionEnd - overlayPositionStart;
|
|
|
|
if ( overlaySize == 0 ) { continue; }
|
|
|
|
nds.Position = overlayPositionStart;
|
|
byte[] data = new byte[overlaySize];
|
|
nds.Read( data, 0, (int)overlaySize );
|
|
|
|
blz blz = new blz();
|
|
byte[] decData;
|
|
|
|
bool compressed = ( compressedBitmask & 0x01 ) == 0x01;
|
|
if ( compressed ) {
|
|
try {
|
|
decData = blz.BLZ_Decode( data );
|
|
} catch ( blzDecodingException ) {
|
|
Console.WriteLine( "WARNING: Decompression of Overlay " + ( i / 0x20 ) + " failed!" );
|
|
decData = data;
|
|
compressed = false;
|
|
}
|
|
} else {
|
|
decData = data;
|
|
}
|
|
|
|
#if DEBUG
|
|
System.IO.File.WriteAllBytes( "overlay" + ( i / 0x20 ) + "-dec.bin", decData );
|
|
#endif
|
|
|
|
if ( ReplaceInData( decData ) ) {
|
|
modified = true;
|
|
int newOverlaySize;
|
|
int diff;
|
|
|
|
// if something was replaced, put it back into the ROM
|
|
if ( compressed ) {
|
|
Console.WriteLine( "Replacing and recompressing overlay " + id + "..." );
|
|
|
|
uint newCompressedSize = 0;
|
|
data = blz.BLZ_Encode( decData, 0 );
|
|
newCompressedSize = (uint)data.Length;
|
|
|
|
newOverlaySize = data.Length;
|
|
diff = (int)overlaySize - newOverlaySize;
|
|
|
|
if ( diff < 0 ) {
|
|
Console.WriteLine( "Removing known debug strings and recompressing overlay " + id + "..." );
|
|
RemoveDebugStrings( decData );
|
|
data = blz.BLZ_Encode( decData, 0, supressWarnings: true );
|
|
newCompressedSize = (uint)data.Length;
|
|
|
|
newOverlaySize = data.Length;
|
|
diff = (int)overlaySize - newOverlaySize;
|
|
if ( diff < 0 ) {
|
|
Console.WriteLine( "WARNING: Recompressed overlay is " + -diff + " bytes bigger than original!" );
|
|
Console.WriteLine( " Patched game may be corrupted!" );
|
|
}
|
|
}
|
|
|
|
// replace compressed size, if it was used before
|
|
if ( compressedSize == overlaySize ) {
|
|
byte[] newCompressedSizeBytes = BitConverter.GetBytes( newCompressedSize );
|
|
nds.Position = pos + i + 0x1C;
|
|
nds.Write( newCompressedSizeBytes, 0, 3 );
|
|
}
|
|
|
|
} else {
|
|
Console.WriteLine( "Replacing overlay " + id + "..." );
|
|
|
|
data = decData;
|
|
}
|
|
|
|
newOverlaySize = data.Length;
|
|
diff = (int)overlaySize - newOverlaySize;
|
|
|
|
nds.Position = overlayPositionStart;
|
|
nds.Write( data, 0, data.Length );
|
|
|
|
overlayPositionEnd = (uint)nds.Position;
|
|
|
|
// padding
|
|
for ( int j = 0; j < diff; ++j ) {
|
|
nds.WriteByte( 0xFF );
|
|
}
|
|
|
|
// new file end offset
|
|
byte[] newPosEndData = BitConverter.GetBytes( overlayPositionEnd );
|
|
nds.Position = fatOffset + 8 * id + 4;
|
|
nds.Write( newPosEndData, 0, 4 );
|
|
}
|
|
}
|
|
|
|
return modified;
|
|
}
|
|
|
|
static void RemoveDebugStrings( byte[] data ) {
|
|
string[] debugStrings = new string[] {
|
|
"recv buffer size",
|
|
"send buffer size",
|
|
"unknown connect mode",
|
|
"Split packet parse error",
|
|
"NULL byte expected!",
|
|
"Processing adderror packet",
|
|
"Out of memory.",
|
|
" buf->buffer",
|
|
};
|
|
|
|
foreach ( string s in debugStrings ) {
|
|
byte[] searchBytes = Encoding.ASCII.GetBytes( s );
|
|
var results = data.Locate( searchBytes );
|
|
|
|
foreach ( int result in results ) {
|
|
for ( int i = 0; i < searchBytes.Length; ++i ) {
|
|
data[result + i] = 0x20;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class KnownGamedata {
|
|
public string Gamecode;
|
|
public uint Position;
|
|
public uint Length;
|
|
|
|
public KnownGamedata( string gamecode, uint position, uint length ) {
|
|
this.Gamecode = gamecode;
|
|
this.Position = position;
|
|
this.Length = length;
|
|
}
|
|
}
|
|
|
|
static bool RemoveStringsInKnownGames( string gamecode, byte[] data ) {
|
|
KnownGamedata[] knownData = new KnownGamedata[] {
|
|
new KnownGamedata( "VI2J", 0x1047C8, 0x4A4 ), // FE12 English Patch
|
|
};
|
|
|
|
bool knownGame = false;
|
|
foreach ( KnownGamedata d in knownData ) {
|
|
if ( gamecode == d.Gamecode ) {
|
|
knownGame = true;
|
|
for ( uint i = d.Position; i < d.Position + d.Length; ++i ) {
|
|
if ( data[i] != 0 ) {
|
|
data[i] = 0x20;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return knownGame;
|
|
}
|
|
|
|
static bool ReplaceInData( byte[] data, byte paddingByte = 0x00, bool writeAdditionalBytePostString = false ) {
|
|
bool replaced = ReplaceInData( data, "https://", "http://", paddingByte, writeAdditionalBytePostString );
|
|
|
|
if ( CommandLineArguments.Domain != null ) {
|
|
replaced = ReplaceInData( data, "nintendowifi.net", CommandLineArguments.Domain, paddingByte, writeAdditionalBytePostString ) || replaced;
|
|
}
|
|
|
|
return replaced;
|
|
}
|
|
|
|
static bool ReplaceInData( byte[] data, string search, string replace, byte paddingByte = 0x00, bool writeAdditionalBytePostString = false ) {
|
|
bool replacedData = false;
|
|
byte[] searchBytes = Encoding.ASCII.GetBytes( search );
|
|
byte[] replaceBytes = Encoding.ASCII.GetBytes( replace );
|
|
int requiredPadding = searchBytes.Length - replaceBytes.Length;
|
|
|
|
var results = data.Locate( searchBytes );
|
|
if ( results.Length == 0 ) {
|
|
return false;
|
|
}
|
|
|
|
foreach ( int result in results ) {
|
|
string originalString = Util.GetTextAscii( data, result );
|
|
#if DEBUG
|
|
Console.WriteLine( originalString );
|
|
#endif
|
|
if ( originalString == "https://" ) { continue; } // don't replace lone https, probably used for strcmp to figure out if an URL is SSL or not
|
|
string replacedString = originalString.Replace( search, replace );
|
|
byte[] replacedStringBytes = Encoding.ASCII.GetBytes( replacedString );
|
|
|
|
replacedData = true;
|
|
|
|
int i = 0;
|
|
for ( ; i < replacedStringBytes.Length; ++i ) {
|
|
data[result + i] = replacedStringBytes[i];
|
|
}
|
|
for ( ; i < replacedStringBytes.Length + requiredPadding; ++i ) {
|
|
data[result + i] = paddingByte;
|
|
}
|
|
|
|
// Alright, this might require some explaination.
|
|
// This is putting a byte in the location that previously held the NULL terminator at the end of the string.
|
|
// Thanks to "http" being one byte shorter than "https", the new NULL terminator was just placed in the
|
|
// padding loop above, and the byte below, at [result + i], is unused. Thus, we can just place anything in
|
|
// there without affecting the program. Now, the actual *reason* we're putting a byte in here is to reduce
|
|
// the chance of the recompressed binary becoming smaller than the original one. We want it to remain the
|
|
// exact same size. Now, of course, this is not always going to happen, but this should improve the chance
|
|
// significantly.
|
|
if ( writeAdditionalBytePostString ) {
|
|
data[result + i] = 0x7F;
|
|
}
|
|
}
|
|
|
|
return replacedData;
|
|
}
|
|
}
|
|
}
|