Skip to content

Instantly share code, notes, and snippets.

@MrPowerGamerBR
Created February 22, 2025 13:37
Show Gist options
  • Save MrPowerGamerBR/aeb07dd434d371aadd49e910cd146f69 to your computer and use it in GitHub Desktop.
Save MrPowerGamerBR/aeb07dd434d371aadd49e910cd146f69 to your computer and use it in GitHub Desktop.
FAR 1a (The Sims 1) and FAR 1b (The Sims Online) reader/writer
package net.perfectdreams.simslib.utils
object BinaryUtils {
/**
* Extracts two bytes from a UShort [value]
*
* @param value the input
* @return a list containing two UBytes
*/
fun shortToUBytes(value: UShort): List<UByte> {
return listOf(
(value.toInt() shr 8 and 0xFF).toUByte(), // High byte
(value.toInt() and 0xFF).toUByte() // Low byte
)
}
// From kotlinx-io
fun Int.reverseBytes(): Int {
return (this and -0x1000000 ushr 24) or
(this and 0x00ff0000 ushr 8) or
(this and 0x0000ff00 shl 8) or
(this and 0x000000ff shl 24)
}
fun Long.reverseBytes(): Long {
return (this and -0x100000000000000L ushr 56) or
(this and 0x00ff000000000000L ushr 40) or
(this and 0x0000ff0000000000L ushr 24) or
(this and 0x000000ff00000000L ushr 8) or
(this and 0x00000000ff000000L shl 8) or
(this and 0x0000000000ff0000L shl 24) or
(this and 0x000000000000ff00L shl 40) or
(this and 0x00000000000000ffL shl 56)
}
fun Short.reverseBytes(): Short {
val i = toInt() and 0xffff
val reversed = (i and 0xff00 ushr 8) or
(i and 0x00ff shl 8)
return reversed.toShort()
}
}
package net.perfectdreams.simslib.utils
import net.perfectdreams.simslib.utils.BinaryUtils.reverseBytes
/**
* A reader that reads a ByteArray, similar to Java's ByteBuffer
*
* We don't use kotlinx.io's Buffer because it doesn't support rewinding
*/
class ByteArrayReader(val byteArray: ByteArray) {
var position = 0
val remaining
get() = byteArray.size - this.position
fun readInt(): Int {
val byte0 = this.byteArray[this.position++]
val byte1 = this.byteArray[this.position++]
val byte2 = this.byteArray[this.position++]
val byte3 = this.byteArray[this.position++]
return (byte0.toInt() and 0xFF shl 24) or (byte1.toInt() and 0xFF shl 16) or (byte2.toInt() and 0xFF shl 8) or (byte3.toInt() and 0xFF)
}
fun readIntLe(): Int {
return readInt().reverseBytes()
}
fun readUIntLe(): UInt {
return readIntLe().toUInt()
}
fun readByte(): Byte {
return this.byteArray[this.position++]
}
fun readUByte(): UByte {
return readByte().toUByte()
}
fun readShort(): Short {
val byte0 = this.byteArray[this.position++]
val byte1 = this.byteArray[this.position++]
return ((byte0.toInt() and 0xFF shl 8) or (byte1.toInt() and 0xFF)).toShort()
}
fun readShortLe(): Short {
return readShort().reverseBytes()
}
fun readUShortLe(): UShort {
return readShort().reverseBytes().toUShort()
}
fun readBytes(length: Int): ByteArray {
val target = ByteArray(length)
repeat(length) {
target[it] = byteArray[this.position]
this.position++
}
return target
}
fun readLong(): Long {
val byte0 = this.byteArray[this.position++]
val byte1 = this.byteArray[this.position++]
val byte2 = this.byteArray[this.position++]
val byte3 = this.byteArray[this.position++]
val byte4 = this.byteArray[this.position++]
val byte5 = this.byteArray[this.position++]
val byte6 = this.byteArray[this.position++]
val byte7 = this.byteArray[this.position++]
return (byte0.toLong() and 0xFF shl 56) or (byte1.toLong() and 0xFF shl 48) or (byte2.toLong() and 0xFF shl 40) or (byte3.toLong() and 0xFF shl 32) or (byte4.toLong() and 0xFF shl 24) or (byte5.toLong() and 0xFF shl 16) or (byte6.toLong() and 0xFF shl 8) or (byte7.toLong() and 0xFF)
}
fun readLongLe(): Long {
return readLong().reverseBytes()
}
fun readBytesUntilNull(): ByteArray {
val builder = mutableListOf<Byte>()
while (true) {
val b = this.readByte()
print(b.toInt().toChar().toString())
if (b == 0x00.toByte())
break
builder.add(b)
}
return builder.toByteArray()
}
fun jumpTo(position: Int) {
this.position = position
}
fun hasRemaining() = byteArray.size > this.position
}
package net.perfectdreams.simslib.utils
import net.perfectdreams.simslib.utils.BinaryUtils.reverseBytes
class ByteArrayWriter {
// TODO: Position?
val bytes = mutableListOf<Byte>()
fun writeShort(value: Short) {
val valueAsInt = value.toInt()
val byte0 = (valueAsInt shr 8).toByte()
val byte1 = valueAsInt.toByte()
bytes.add(byte0)
bytes.add(byte1)
}
fun writeShortLe(value: Short) {
writeShort(value.reverseBytes())
}
fun writeUShortLe(value: UShort) {
writeShortLe(value.toShort())
}
fun writeInt(value: Int) {
val byte0 = (value shr 24).toByte()
val byte1 = (value shr 16).toByte()
val byte2 = (value shr 8).toByte()
val byte3 = value.toByte()
bytes.add(byte0)
bytes.add(byte1)
bytes.add(byte2)
bytes.add(byte3)
}
fun writeIntLe(value: Int) {
writeInt(value.reverseBytes())
}
fun writeLong(value: Long) {
val byte0 = (value shr 56).toByte()
val byte1 = (value shr 48).toByte()
val byte2 = (value shr 40).toByte()
val byte3 = (value shr 32).toByte()
val byte4 = (value shr 24).toByte()
val byte5 = (value shr 16).toByte()
val byte6 = (value shr 8).toByte()
val byte7 = value.toByte()
bytes.add(byte0)
bytes.add(byte1)
bytes.add(byte2)
bytes.add(byte3)
bytes.add(byte4)
bytes.add(byte5)
bytes.add(byte6)
bytes.add(byte7)
}
fun writeLongLe(value: Long) {
writeLong(value.reverseBytes())
}
fun writeByte(value: Byte) {
this.bytes.add(value)
}
fun writeUByte(value: UByte) {
this.bytes.add(value.toByte())
}
fun writeBytes(bytes: ByteArray) {
for (byte in bytes) {
writeByte(byte)
}
}
fun asByteArray() = bytes.toByteArray()
}
package net.perfectdreams.simslib.far
import net.perfectdreams.simslib.utils.ByteArrayReader
import net.perfectdreams.simslib.utils.ByteArrayWriter
class FAR1a(val files: MutableList<FARFile>) {
companion object {
const val FAR_HEADER = "FAR!byAZ"
fun read(byteArray: ByteArray): FAR1a {
val reader = ByteArrayReader(byteArray)
val signature = reader.readBytes(8).map { it.toInt().toChar() }.toCharArray().concatToString()
if (signature != FAR_HEADER)
error("Invalid FAR file!")
val version = reader.readIntLe()
if (version != 1)
error("Invalid FAR version!")
// The manifest offset is the byte offset from the beginning of the file to the manifest.
val manifestOffset = reader.readIntLe()
println("Signature: $signature")
println("Version: $version")
println("Manifest Offset: $manifestOffset")
// Now we need to jump to the manifest to read the information about the files BEFORE reading information about the files themselves!
reader.position = manifestOffset
val numberOfFiles = reader.readIntLe()
println("Number of Files: $numberOfFiles")
val files = mutableListOf<FARFile>()
repeat(numberOfFiles) {
val fileLength = reader.readIntLe()
val fileLengthUnused = reader.readIntLe()
val fileOffset = reader.readIntLe()
// This is a bit tricky, FAR archives 1a and 1b are the same, with the only difference is that one uses int for the file name length, another uses shorts
// There isn't a good way of detecting which FAR version we are using... (we don't support 3 yet)
val fileNameLength = reader.readIntLe()
val fileNameBytes = reader.readBytes(fileNameLength)
val fileName = fileNameBytes.map { it.toInt().toChar() }.toCharArray().concatToString()
val lastManifestOffset = reader.position
println(fileName)
reader.position = fileOffset
val fileContent = reader.readBytes(fileLength)
files.add(FARFile(fileName, fileContent))
// Revert to where we were before
reader.position = lastManifestOffset
}
return FAR1a(files)
}
}
fun write(): ByteArray {
val buffer = ByteArrayWriter()
buffer.writeBytes(FAR_HEADER.encodeToByteArray())
buffer.writeIntLe(1)
// The +4 is due to the file count written before the manifest
val manifestOffset = buffer.bytes.size + 4 + this.files.sumOf { it.content.size }
println("Manifest Offset: ${manifestOffset}")
buffer.writeIntLe(manifestOffset)
// Write all FAR files (the contents) and annotate where in the archive they are
val indexToOffset = mutableMapOf<Int, Int>()
for ((index, file) in this.files.withIndex()) {
indexToOffset[index] = buffer.bytes.size // hacky!!
buffer.writeBytes(file.content)
}
buffer.writeIntLe(this.files.size)
// Write manifests
for ((index, file) in this.files.withIndex()) {
val fileOffset = indexToOffset[index]!!
buffer.writeIntLe(file.content.size)
buffer.writeIntLe(file.content.size)
buffer.writeIntLe(fileOffset)
buffer.writeIntLe(file.fileName.length)
buffer.writeBytes(file.fileName.encodeToByteArray())
}
return buffer.asByteArray()
}
}
package net.perfectdreams.simslib.far
import net.perfectdreams.simslib.utils.ByteArrayReader
import net.perfectdreams.simslib.utils.ByteArrayWriter
class FAR1b(val files: MutableList<FARFile>) {
companion object {
const val FAR_HEADER = "FAR!byAZ"
fun read(byteArray: ByteArray): FAR1b {
val reader = ByteArrayReader(byteArray)
val signature = reader.readBytes(8).map { it.toInt().toChar() }.toCharArray().concatToString()
if (signature != FAR_HEADER)
error("Invalid FAR file!")
val version = reader.readIntLe()
if (version != 1)
error("Invalid FAR version!")
// The manifest offset is the byte offset from the beginning of the file to the manifest.
val manifestOffset = reader.readIntLe()
println("Signature: $signature")
println("Version: $version")
println("Manifest Offset: $manifestOffset")
// Now we need to jump to the manifest to read the information about the files BEFORE reading information about the files themselves!
reader.position = manifestOffset
val numberOfFiles = reader.readIntLe()
println("Number of Files: $numberOfFiles")
val files = mutableListOf<FARFile>()
repeat(numberOfFiles) {
val fileLength = reader.readIntLe()
val fileLengthUnused = reader.readIntLe()
val fileOffset = reader.readIntLe()
val fileNameLength = reader.readShortLe().toInt()
val fileNameBytes = reader.readBytes(fileNameLength)
val fileName = fileNameBytes.map { it.toInt().toChar() }.toCharArray().concatToString()
val lastManifestOffset = reader.position
println(fileName)
reader.position = fileOffset
val fileContent = reader.readBytes(fileLength)
files.add(FARFile(fileName, fileContent))
// Revert to where we were before
reader.position = lastManifestOffset
}
return FAR1b(files)
}
}
fun write(): ByteArray {
val buffer = ByteArrayWriter()
buffer.writeBytes(FAR_HEADER.encodeToByteArray())
buffer.writeIntLe(1)
// The +4 is due to the file count written before the manifest
val manifestOffset = buffer.bytes.size + 4 + this.files.sumOf { it.content.size }
println("Manifest Offset: ${manifestOffset}")
buffer.writeIntLe(manifestOffset)
// Write all FAR files (the contents) and annotate where in the archive they are
val indexToOffset = mutableMapOf<Int, Int>()
for ((index, file) in this.files.withIndex()) {
indexToOffset[index] = buffer.bytes.size // hacky!!
buffer.writeBytes(file.content)
}
buffer.writeIntLe(this.files.size)
// Write manifests
for ((index, file) in this.files.withIndex()) {
val fileOffset = indexToOffset[index]!!
buffer.writeIntLe(file.content.size)
buffer.writeIntLe(file.content.size)
buffer.writeIntLe(fileOffset)
buffer.writeShortLe(file.fileName.length.toShort())
buffer.writeBytes(file.fileName.encodeToByteArray())
}
return buffer.asByteArray()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment