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 }