1 module compy.nintendo; 2 3 import compy.common; 4 5 import std.algorithm; 6 import std.array; 7 import std.exception; 8 import std.range; 9 import std.traits; 10 11 enum BANKSIZE = 0x10000; 12 private static immutable (ubyte[] function(const(ubyte)[] input, const(ubyte)[] buffer, out ushort size) @safe pure nothrow)[] compFuncsV1 = [ &repeatByte, &repeatWord, &incByteFill, &bufferCopyBigEndian ]; 13 private static immutable (ubyte[] function(const(ubyte)[] input, const(ubyte)[] buffer, out ushort size) @safe pure nothrow)[] compFuncsV2 = [ &repeatByte, &repeatWord, &incByteFill, &bufferCopy ]; 14 15 private enum NintendoCommand1 : ubyte { uncompressed = 0, byteFill, shortFill, byteFillIncreasing, bufferCopy, unused1, unused2, extend } 16 17 /// Format used in early SNES games by Nintendo. Also known as LC_LZ1. 18 struct NintendoLZ1 { 19 static ubyte[] comp(const(ubyte)[] input) @safe { 20 const(ubyte)[] buffer = input; 21 ubyte[] output, tmpBuffer, tmpBuffer2, uncompBuffer; 22 ushort size, tmpSize, uncompSize; 23 short bufferPos = -1; 24 byte method; 25 float ratio; 26 float tmpRatio; 27 while (input.length > 0) { 28 ratio = 1.0; 29 method = -1; 30 tmpBuffer = []; 31 foreach (k, compFunc; compFuncsV2) { 32 tmpBuffer2 = compFunc(input, buffer[0..bufferPos+1], tmpSize); 33 tmpRatio = cast(float)tmpSize / cast(float)tmpBuffer2.length; 34 debug(verbosecomp) writefln("Method %d: %f", k, tmpRatio); 35 if (tmpRatio > ratio) { 36 debug(verbosecomp) writeln("Candidate found: ", k); 37 method = cast(byte)k; 38 tmpBuffer = tmpBuffer2; 39 ratio = tmpRatio; 40 size = tmpSize; 41 } 42 } 43 if (tmpBuffer.length == 0) { 44 uncompBuffer ~= input[0]; 45 bufferPos++; 46 size = 1; 47 } else { 48 debug(verbosecomp) writeln("Selecting method ", method); 49 bufferPos += tmpBuffer.length; 50 while (uncompBuffer.length > 0) { 51 output ~= uncompdata(uncompBuffer, uncompSize); 52 uncompBuffer = uncompBuffer[uncompSize..$]; 53 } 54 output ~= tmpBuffer; 55 } 56 input = input[size..$]; 57 } 58 while (uncompBuffer.length > 0) { 59 output ~= uncompdata(uncompBuffer, uncompSize); 60 uncompBuffer = uncompBuffer[uncompSize..$]; 61 } 62 debug(verbosecomp) writefln("Compressed size: %d/%d (%0.2f)", output.length + 1, buffer.length, (cast(double)output.length + 1.0) / cast(double)buffer.length * 100.0); 63 return output ~ 0xFF; 64 } 65 static ubyte[] decomp(const(ubyte)[] input) @safe { 66 size_t throwAway; 67 return decomp(input, throwAway); 68 } 69 static ubyte[] decomp(T)(T input, out size_t compressedSize) if (isInputRange!T) { 70 ubyte[] buffer = new ubyte[](BANKSIZE); 71 ubyte commandbyte = void; 72 NintendoCommand1 commandID = void; 73 ushort commandLength = void; 74 int decompSize = 0; 75 decompLoop: while(decompSize < BANKSIZE) { //decompressed data cannot exceed 64KB 76 commandbyte = input.readByte(); 77 compressedSize++; 78 commandID = cast(NintendoCommand1)(commandbyte >> 5); 79 if (commandID == NintendoCommand1.extend) { //Extend length of command 80 commandID = cast(NintendoCommand1)((commandbyte & 0x1C) >> 2); 81 if (commandID != NintendoCommand1.extend) { //Double extend does not have a length 82 commandLength = ((commandbyte & 3) << 8) + input.readByte() + 1; 83 compressedSize++; 84 } 85 } else { 86 commandLength = (commandbyte & 0x1F) + 1; 87 } 88 debug(verbosecomp) writeln(commandID, ", ", commandLength); 89 final switch(commandID) { 90 case NintendoCommand1.uncompressed: //Following data is uncompressed 91 buffer[decompSize..decompSize+commandLength] = array(input.takeExactly(commandLength)); 92 input.popFrontN(commandLength); 93 compressedSize += commandLength; 94 break; //copy uncompressed data directly into buffer 95 case NintendoCommand1.byteFill: //Fill range with following byte 96 buffer[decompSize..decompSize+commandLength] = input.readByte(); 97 compressedSize++; 98 break; 99 case NintendoCommand1.shortFill: //Fill range with following short 100 commandLength *= 2; 101 (cast(ushort[])buffer[decompSize..decompSize+commandLength])[] = cast(ushort)(input.readByte() + (input.readByte() << 8)); 102 compressedSize += 2; 103 break; 104 case NintendoCommand1.byteFillIncreasing: //Fill range with increasing byte, beginning with following value 105 buffer[decompSize..decompSize+commandLength] = increaseval(input.readByte(), commandLength); 106 compressedSize++; 107 break; 108 case NintendoCommand1.bufferCopy: //Copy from buffer 109 const ushort bufferpos = input.readByte() + (input.readByte() << 8); 110 compressedSize += 2; 111 enforce(bufferpos < BANKSIZE, "Buffer position exceeds bank size!"); 112 enforce(bufferpos < decompSize, "Buffer contents at position unknown!"); 113 buffer[decompSize..decompSize+commandLength] = buffer[bufferpos..bufferpos+commandLength]; 114 break; 115 case NintendoCommand1.unused1: 116 throw new Exception("Invalid compressed data - reserved command 1"); 117 case NintendoCommand1.unused2: 118 throw new Exception("Invalid compressed data - reserved command 2"); 119 case NintendoCommand1.extend: break decompLoop; 120 } 121 decompSize += commandLength; 122 } 123 buffer.length = decompSize; 124 return buffer; 125 } 126 } 127 /// Format used in early SNES games by Nintendo. Identical to NintendoLZ1, except 'bufferCopy' is big endian. Also known as LC_LZ2. 128 struct NintendoLZ2 { 129 static ubyte[] comp(const(ubyte)[] input) @safe { 130 const(ubyte)[] buffer = input; 131 ubyte[] output, tmpBuffer, tmpBuffer2, uncompBuffer; 132 ushort size, tmpSize, uncompSize; 133 short bufferPos = -1; 134 byte method; 135 float ratio; 136 float tmpRatio; 137 while (input.length > 0) { 138 ratio = 1.0; 139 method = -1; 140 tmpBuffer = []; 141 foreach (k, compFunc; compFuncsV2) { 142 tmpBuffer2 = compFunc(input, buffer[0..bufferPos+1], tmpSize); 143 tmpRatio = cast(float)tmpSize / cast(float)tmpBuffer2.length; 144 debug(verbosecomp) writefln("Method %d: %f", k, tmpRatio); 145 if (tmpRatio > ratio) { 146 debug(verbosecomp) writeln("Candidate found: ", k); 147 method = cast(byte)k; 148 tmpBuffer = tmpBuffer2; 149 ratio = tmpRatio; 150 size = tmpSize; 151 } 152 } 153 if (tmpBuffer.length == 0) { 154 uncompBuffer ~= input[0]; 155 bufferPos++; 156 size = 1; 157 } else { 158 debug(verbosecomp) writeln("Selecting method ", method); 159 bufferPos += tmpBuffer.length; 160 while (uncompBuffer.length > 0) { 161 output ~= uncompdata(uncompBuffer, uncompSize); 162 uncompBuffer = uncompBuffer[uncompSize..$]; 163 } 164 output ~= tmpBuffer; 165 } 166 input = input[size..$]; 167 } 168 while (uncompBuffer.length > 0) { 169 output ~= uncompdata(uncompBuffer, uncompSize); 170 uncompBuffer = uncompBuffer[uncompSize..$]; 171 } 172 debug(verbosecomp) writefln("Compressed size: %d/%d (%0.2f)", output.length + 1, buffer.length, (cast(double)output.length + 1.0) / cast(double)buffer.length * 100.0); 173 return output ~ 0xFF; 174 } 175 static ubyte[] decomp(ubyte[] input) @safe { 176 size_t throwAway; 177 return decomp(input, throwAway); 178 } 179 static ubyte[] decomp(T)(T input, out size_t compressedSize) if (isInputRange!T) { 180 ubyte[] buffer = new ubyte[](BANKSIZE); 181 ubyte commandbyte = void; 182 NintendoCommand1 commandID = void; 183 ushort commandLength = void; 184 int decompSize = 0; 185 decompLoop: while(decompSize < BANKSIZE) { //decompressed data cannot exceed 64KB 186 commandbyte = input.readByte(); 187 compressedSize++; 188 commandID = cast(NintendoCommand1)(commandbyte >> 5); 189 if (commandID == NintendoCommand1.extend) { //Extend length of command 190 commandID = cast(NintendoCommand1)((commandbyte & 0x1C) >> 2); 191 if (commandID != NintendoCommand1.extend) { //Double extend does not have a length 192 commandLength = ((commandbyte & 3) << 8) + input.readByte() + 1; 193 compressedSize++; 194 } 195 } else { 196 commandLength = (commandbyte & 0x1F) + 1; 197 } 198 debug(verbosecomp) writeln(commandID, ", ", commandLength); 199 final switch(commandID) { 200 case NintendoCommand1.uncompressed: //Following data is uncompressed 201 buffer[decompSize..decompSize+commandLength] = array(input.takeExactly(commandLength)); 202 input.popFrontN(commandLength); 203 compressedSize += commandLength; 204 break; //copy uncompressed data directly into buffer 205 case NintendoCommand1.byteFill: //Fill range with following byte 206 buffer[decompSize..decompSize+commandLength] = input.readByte(); 207 compressedSize++; 208 break; 209 case NintendoCommand1.shortFill: //Fill range with following short 210 commandLength *= 2; 211 (cast(ushort[])buffer[decompSize..decompSize+commandLength])[] = cast(ushort)(input.readByte() + (input.readByte() << 8)); 212 compressedSize += 2; 213 break; 214 case NintendoCommand1.byteFillIncreasing: //Fill range with increasing byte, beginning with following value 215 buffer[decompSize..decompSize+commandLength] = increaseval(input.readByte(), commandLength); 216 compressedSize++; 217 break; 218 case NintendoCommand1.bufferCopy: //Copy from buffer 219 const ushort bufferpos = (input.readByte() << 8) + input.readByte(); 220 compressedSize += 2; 221 enforce(bufferpos < BANKSIZE, "Buffer position exceeds bank size!"); 222 enforce(bufferpos < decompSize, "Buffer contents at position unknown!"); 223 buffer[decompSize..decompSize+commandLength] = buffer[bufferpos..bufferpos+commandLength]; 224 break; 225 case NintendoCommand1.unused1: 226 throw new Exception("Invalid compressed data - reserved command 1"); 227 case NintendoCommand1.unused2: 228 throw new Exception("Invalid compressed data - reserved command 2"); 229 case NintendoCommand1.extend: break decompLoop; 230 } 231 decompSize += commandLength; 232 } 233 buffer.length = decompSize; 234 return buffer; 235 } 236 }