Skip to content

Instantly share code, notes, and snippets.

@trigger-segfault
Created May 17, 2021 22:16
Show Gist options
  • Select an option

  • Save trigger-segfault/1476a7a131adf7474144de89092d4d71 to your computer and use it in GitHub Desktop.

Select an option

Save trigger-segfault/1476a7a131adf7474144de89092d4d71 to your computer and use it in GitHub Desktop.
Rokucho Encoder for both RC8|RCT formats (untested)
// Refer to here for more information and listing of Constants by Image Type
// <https://github.com/AtomCrafty/MajiroTools/wiki/Format:-Rct-image>
// Encoder implantation based off GARbro's:
// <https://github.com/morkt/GARbro/blob/master/ArcFormats/Majiro/ImageRCT.cs#L470>
// Encoding type: LZ77
// <https://en.wikipedia.org/wiki/LZ77_and_LZ78#LZ77>
// Metadata:
public int Width { get; }
public int Height { get; }
// Readonly and State:
byte[] pixels;
int[] shiftTable;
int thunkSize; // Size in pixels of next data to write directly
// RC8|RCT Constants: //|RC8 |RCT
int ByteDepth { get; } // 1 3
sbyte[] BaseTable { get; } // [16] [32]
int CmdShiftBit { get; } // 3 2
int CmdCountMask { get; } // 0x7 0x3
int CmdCountBase { get; } // 3 1
const int MaxThunkSize = 0xffff + 0x7f;
const int MaxMatchSize = 0xffff;
struct ChunkPosition {
public ushort Shift; // Shift table index
public ushort Length; // Length in pixels
}
// Pack pixels and write to stream. Expects pixels[] as:
// * 8-bit indexed | BGR24 (pixel format)
// * No stride padding (4-byte alignment)
// Returns size of written data.
public uint Pack(BinaryWriter writer, byte[] pixels) {
this.shiftTable = InitShiftTable(BaseTable);
this.pixels = pixels;
this.thunkSize = 0;
long dataOffset = writer.BaseStream.Position;
// Start by writing 1 pixel
writer.Write(pixels, 0, ByteDepth);
int size = Width * Height; // total number of pixels (const)
int pos = 1; // Current position in pixels
while(pos < size) {
var chunk = FindLongest(pos, size);
if(chunk.Length >= CmdCountBase) {
Flush(writer, pos);
WriteCopyCode(writer, chunk);
pos += chunk.Length;
}
else {
if(thunkSize == MaxThunkSize)
Flush(writer, pos);
thunkSize++
pos++;
}
}
Flush(writer, pos);
long dataSize = writer.BaseStream.Position - dataOffset;
return (uint)dataSize;
}
// Flush/Write thunk (read mode pixels)
void Flush(BinaryWriter writer, int pos) {
if(thunkSize == 0)
return; // Nothing to flush
// Write data code (read mode pixels)
// CODE: 0 nnnnnnn [eeeeeeee eeeeeeee]
int count = thunkSize - 1; // Subtract base number of pixels
byte code = 0x00; // Read flag (MSB:0)
code |= (byte)Math.Min(count, 0x7f); // Count
writer.Write((byte)code);
if(count >= 0x7f) // Write extra count
writer.Write((ushort)(count - 0x7f));
// Change to byte positions
thunkSize *= ByteDepth;
pos *= ByteDepth;
writer.Write(pixels, pos - thunkSize, thunkSize);
thunkSize = 0; // Reset to 0
}
// Write copy code (copy mode pixels)
void WriteCopyCode(BinaryWriter writer, ChunkPosition chunk) {
// CODE: 1 ssss{s | n}nn [eeeeeeee eeeeeeee]
int count = chunk.Length - CmdCountBase; // Subtract base number of pixels
byte code = 0x80; // Copy flag (MSB:1)
code |= (byte)(chunk.Shift << CmdShiftBit); // Shift
code |= (byte)Math.Min(count, CmdCountMask); // Count
writer.Write((byte)code);
if(count >= CmdCountMask) // Write extra count
writer.Write((ushort)(count - CmdCountMask));
}
// Find longest match in sliding window (shift table)
ChunkPosition FindLongest(int begin, int end) {
end = Math.Min(begin + MaxMatchSize, end);
var chunk = new ChunkPosition { Shift = 0, Length = 0 };
for(int i = 0; i < shiftTable.Length; i++) {
int offset = begin + shiftTable[i];
if(offset < 0 || offset >= begin)
continue;
int last = Mismatch(begin, end, offset);
int weight = last - offset;
if(weight > chunk.Length) {
chunk.Shift = (ushort)i;
chunk.Length = (ushort)weight;
}
}
return chunk;
}
// Find position where bytes no longer match. Returns first2's last position.
int Mismatch(int first1, int last1, int first2) {
// Change to byte positions
first1 *= ByteDepth;
last1 *= ByteDepth;
first2 *= ByteDepth;
while(first1 != last1 && pixels[first1] == pixels[first2]) {
++first1;
++first2;
}
// Change back to pixel position
return first2 / ByteDepth;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment