Skip to content

Instantly share code, notes, and snippets.

@HybridEidolon
Last active July 3, 2023 20:00
Show Gist options
  • Save HybridEidolon/bc35fc03579c95550d0e6df824eede42 to your computer and use it in GitHub Desktop.
Save HybridEidolon/bc35fc03579c95550d0e6df824eede42 to your computer and use it in GitHub Desktop.
Phantasy Star Online 2 Symbol Art Format

Symbol Art File Format (Phantasy Star Online 2)

SAR files are simple structures that are encrypted with Blowfish with a 32-bit key and optionally compressed with PRS and a 0x95 XOR.

Starting out

All sar files start with the magic ASCII string sar and, at position 0x03, either the byte 0x84 or 0x04.

Decrypting and Decompressing

SAR files are always encrypted. Unlike Blue Burst, PSO2 does not use special P/S tables for Blowfish, and uses the full number of rounds.

  1. Initialize Blowfish cipher with 32-bit key [0x09, 0x07, 0xc1, 0x2b] (TK is this correct?)
  2. Decrypt the maximum multiple of 8 bytes, starting from 0x04 in the SAR buffer to the end. e.g. if the size of the sar file is 36 bytes, decrypt the last 32 bytes in the file. If it is 37 bytes, decrypt everything except the first 4 bytes and the last byte. The trailing bytes will show up as plaintext in the file if you examine it with a Hex editor. Yes, it's that broken. (If it's compressed, you'll see 0x95 at the end probably)

Now, if the 0x03 byte (flag) is 0x84, that means the buffer is still compressed. If it is 0x04, it is not compressed, so skip these steps.

  1. XOR every byte in the buffer from 0x04 to the end by 0x95.
  2. PRS decompress the buffer.

The inner payload

Here is a Rust struct example representing the payload.

struct Header {
  authorId: u32, // big endian
  layers: u8,
  sizeHeight: u8, // strange encoding, 0x80 for 96, 0x40 for 32 team flag?
  sizeWidth: u8, // strange encoding, 0xC1 for 192, 0x40 for 32 team flag?
  soundEffect: u8, // the sound effect used
}

struct Payload {
  header: Header,
  layers: Vec<Layer>, // Exactly the number of layers specified in the header
  name: Vec<u16>, // UTF-16LE characters. Up to 13 characters; no null byte
}

// TK Have not verified the order of this
struct Layer {
  topLeft: Position,
  bottomLeft: Position,
  topRight: Position,
  bottomRight: Position,
  color: u16, // TK unconfirmed
  symbol: u16, // TK unconfirmed
  _unknown: u32, // ???
}

// TK This might also be y, x...
struct Position {
  x: u8, // 0x80 is the "origin" of the symbol art. So 0x70 is -16 from origin
  y: u8, // Same as above
}

Rendering

A layer is 4 vertices representing a quad of two triangles. These are drawn as a triangle strip with a square symbol texture indexed by the symbol field. It is colorized using the color field. The lower the layer number, the higher in the stack; so when drawing, draw from the last layer first and go backwards.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment