Task 354: .LGR File Format
Task 354: .LGR File Format
File Format Specifications for .LGR
The .LGR file format is used in Elasto Mania (a 2D motorcycle simulation game) to store custom graphics sets. It is a binary format that bundles multiple PCX images along with metadata for how they are used in the game (e.g., as pictures, textures, or masks). The format includes a header, a pictures.lst section for metadata about the images, the embedded PCX files, and an end marker. The specification is based on community reverse-engineering and is detailed below.
- List of all the properties of this file format intrinsic to its file system:
- Magic string: 3 bytes ASCII ('LGR')
- Version: 2 bytes ASCII (either '12' or '13')
- Number of PCX files (x): 4-byte integer
- Pictures.lst version: 4-byte integer (must be 1002 for validity)
- Number of lst elements (l): 4-byte integer
- Image names: List of l strings, each 10 bytes (null-terminated, max name length 8 characters)
- Image types: List of l 4-byte integers (100 = picture, 101 = texture, 102 = mask)
- Default distances: List of l 4-byte integers (1-999 for pictures/textures; 0 for masks; special values like 400 for certain items, though often ignored)
- Default clipping: List of l 4-byte integers (0 = unclipped, 1 = ground, 2 = sky)
- Transparent locations: List of l 4-byte integers (10 = whole image solid (masks only), 11 = palette color 0 transparent, 12 = topleft pixel transparent, 13 = topright, 14 = bottomleft, 15 = bottomright)
- PCX objects: List of x structures, each containing:
- Filename: 20 bytes (null-terminated, including '.pcx'; may have garbage after null in older files)
- Width: 2-byte short (only if version '13')
- Height: 2-byte short (only if version '13')
- Length (z): 4-byte integer (size of PCX data in bytes)
- Data: z bytes (raw PCX file content)
- End marker: 4 bytes ([0xE7, 0x05, 0x2E, 0x0B])
All multi-byte integers and shorts are little-endian.
- Two direct download links for files of format .LGR:
- https://moposite.com/downloads/lgrs/across_lgr.zip (contains across.lgr)
- https://moposite.com/downloads/lgrs/blue_lgr.zip (contains blue.lgr)
- Ghost blog embedded HTML JavaScript for drag-and-drop .LGR file dump:
Drag and drop .LGR file here
- Python class for .LGR file handling:
import struct
class LGRFile:
def __init__(self, filename=None):
self.magic = b''
self.version = b''
self.num_pcx = 0
self.lst_version = 0
self.num_lst = 0
self.image_names = []
self.image_types = []
self.distances = []
self.clippings = []
self.trans_locs = []
self.pcx_objects = [] # List of dicts: {'filename': str, 'width': int, 'height': int, 'length': int, 'data': bytes}
self.end_marker = b''
if filename:
self.open(filename)
self.read()
def open(self, filename):
self.file = open(filename, 'rb')
def read(self):
self.file.seek(0)
self.magic = self.file.read(3)
self.version = self.file.read(2)
self.num_pcx = struct.unpack('<i', self.file.read(4))[0]
self.lst_version = struct.unpack('<i', self.file.read(4))[0]
self.num_lst = struct.unpack('<i', self.file.read(4))[0]
self.image_names = []
for _ in range(self.num_lst):
name_bytes = self.file.read(10)
name = name_bytes.split(b'\x00', 1)[0].decode('ascii')
self.image_names.append(name)
self.image_types = [struct.unpack('<i', self.file.read(4))[0] for _ in range(self.num_lst)]
self.distances = [struct.unpack('<i', self.file.read(4))[0] for _ in range(self.num_lst)]
self.clippings = [struct.unpack('<i', self.file.read(4))[0] for _ in range(self.num_lst)]
self.trans_locs = [struct.unpack('<i', self.file.read(4))[0] for _ in range(self.num_lst)]
self.pcx_objects = []
for _ in range(self.num_pcx):
filename_bytes = self.file.read(20)
filename = filename_bytes.split(b'\x00', 1)[0].decode('ascii')
width = height = 0
if self.version == b'13':
width = struct.unpack('<h', self.file.read(2))[0]
height = struct.unpack('<h', self.file.read(2))[0]
length = struct.unpack('<i', self.file.read(4))[0]
data = self.file.read(length)
self.pcx_objects.append({'filename': filename, 'width': width, 'height': height, 'length': length, 'data': data})
self.end_marker = self.file.read(4)
def write(self, filename):
with open(filename, 'wb') as f:
f.write(self.magic)
f.write(self.version)
f.write(struct.pack('<i', self.num_pcx))
f.write(struct.pack('<i', self.lst_version))
f.write(struct.pack('<i', self.num_lst))
for name in self.image_names:
f.write(name.encode('ascii').ljust(10, b'\x00'))
for t in self.image_types: f.write(struct.pack('<i', t))
for d in self.distances: f.write(struct.pack('<i', d))
for c in self.clippings: f.write(struct.pack('<i', c))
for tl in self.trans_locs: f.write(struct.pack('<i', tl))
for pcx in self.pcx_objects:
f.write(pcx['filename'].encode('ascii').ljust(20, b'\x00'))
if self.version == b'13':
f.write(struct.pack('<h', pcx['width']))
f.write(struct.pack('<h', pcx['height']))
f.write(struct.pack('<i', pcx['length']))
f.write(pcx['data'])
f.write(self.end_marker)
def print_properties(self):
print(f"Magic: {self.magic.decode('ascii')}")
print(f"Version: {self.version.decode('ascii')}")
print(f"Number of PCX files: {self.num_pcx}")
print(f"Pictures.lst version: {self.lst_version}")
print(f"Number of lst elements: {self.num_lst}")
print(f"Image names: {', '.join(self.image_names)}")
print(f"Image types: {', '.join(map(str, self.image_types))}")
print(f"Default distances: {', '.join(map(str, self.distances))}")
print(f"Default clippings: {', '.join(map(str, self.clippings))}")
print(f"Transparent locations: {', '.join(map(str, self.trans_locs))}")
print("PCX objects:")
for pcx in self.pcx_objects:
print(f" - Filename: {pcx['filename']}, Width: {pcx['width']}, Height: {pcx['height']}, Length: {pcx['length']}")
print(f"End marker: [{', 0x'.join(f'{b:02x}' for b in self.end_marker)}]")
- Java class for .LGR file handling:
import java.io.*;
import java.nio.*;
import java.util.*;
public class LGRFile {
private String magic;
private String version;
private int numPcx;
private int lstVersion;
private int numLst;
private List<String> imageNames = new ArrayList<>();
private List<Integer> imageTypes = new ArrayList<>();
private List<Integer> distances = new ArrayList<>();
private List<Integer> clippings = new ArrayList<>();
private List<Integer> transLocs = new ArrayList<>();
private List<Map<String, Object>> pcxObjects = new ArrayList<>(); // Each map: "filename" String, "width" int, "height" int, "length" int, "data" byte[]
private byte[] endMarker = new byte[4];
private RandomAccessFile file;
public LGRFile(String filename) throws IOException {
open(filename);
read();
}
public void open(String filename) throws IOException {
file = new RandomAccessFile(filename, "r");
}
public void read() throws IOException {
file.seek(0);
byte[] buf = new byte[3];
file.read(buf);
magic = new String(buf);
buf = new byte[2];
file.read(buf);
version = new String(buf);
numPcx = Integer.reverseBytes(file.readInt()); // Little-endian
lstVersion = Integer.reverseBytes(file.readInt());
numLst = Integer.reverseBytes(file.readInt());
for (int i = 0; i < numLst; i++) {
buf = new byte[10];
file.read(buf);
imageNames.add(new String(buf).split("\0")[0]);
}
for (int i = 0; i < numLst; i++) imageTypes.add(Integer.reverseBytes(file.readInt()));
for (int i = 0; i < numLst; i++) distances.add(Integer.reverseBytes(file.readInt()));
for (int i = 0; i < numLst; i++) clippings.add(Integer.reverseBytes(file.readInt()));
for (int i = 0; i < numLst; i++) transLocs.add(Integer.reverseBytes(file.readInt()));
for (int i = 0; i < numPcx; i++) {
buf = new byte[20];
file.read(buf);
String filename = new String(buf).split("\0")[0];
int width = 0, height = 0;
if (version.equals("13")) {
width = Short.reverseBytes(file.readShort());
height = Short.reverseBytes(file.readShort());
}
int length = Integer.reverseBytes(file.readInt());
byte[] data = new byte[length];
file.read(data);
Map<String, Object> pcx = new HashMap<>();
pcx.put("filename", filename);
pcx.put("width", width);
pcx.put("height", height);
pcx.put("length", length);
pcx.put("data", data);
pcxObjects.add(pcx);
}
file.read(endMarker);
}
public void write(String filename) throws IOException {
try (RandomAccessFile out = new RandomAccessFile(filename, "rw")) {
out.write(magic.getBytes());
out.write(version.getBytes());
out.writeInt(Integer.reverseBytes(numPcx));
out.writeInt(Integer.reverseBytes(lstVersion));
out.writeInt(Integer.reverseBytes(numLst));
for (String name : imageNames) {
byte[] nameBytes = name.getBytes();
out.write(nameBytes);
for (int j = nameBytes.length; j < 10; j++) out.write(0);
}
for (int t : imageTypes) out.writeInt(Integer.reverseBytes(t));
for (int d : distances) out.writeInt(Integer.reverseBytes(d));
for (int c : clippings) out.writeInt(Integer.reverseBytes(c));
for (int tl : transLocs) out.writeInt(Integer.reverseBytes(tl));
for (Map<String, Object> pcx : pcxObjects) {
String fn = (String) pcx.get("filename");
byte[] fnBytes = fn.getBytes();
out.write(fnBytes);
for (int j = fnBytes.length; j < 20; j++) out.write(0);
if (version.equals("13")) {
out.writeShort(Short.reverseBytes((short) (int) pcx.get("width")));
out.writeShort(Short.reverseBytes((short) (int) pcx.get("height")));
}
out.writeInt(Integer.reverseBytes((int) pcx.get("length")));
out.write((byte[]) pcx.get("data"));
}
out.write(endMarker);
}
}
public void printProperties() {
System.out.println("Magic: " + magic);
System.out.println("Version: " + version);
System.out.println("Number of PCX files: " + numPcx);
System.out.println("Pictures.lst version: " + lstVersion);
System.out.println("Number of lst elements: " + numLst);
System.out.println("Image names: " + String.join(", ", imageNames));
System.out.println("Image types: " + imageTypes);
System.out.println("Default distances: " + distances);
System.out.println("Default clippings: " + clippings);
System.out.println("Transparent locations: " + transLocs);
System.out.println("PCX objects:");
for (Map<String, Object> pcx : pcxObjects) {
System.out.println(" - Filename: " + pcx.get("filename") + ", Width: " + pcx.get("width") + ", Height: " + pcx.get("height") + ", Length: " + pcx.get("length"));
}
System.out.printf("End marker: [0x%02x, 0x%02x, 0x%02x, 0x%02x]%n", endMarker[0], endMarker[1], endMarker[2], endMarker[3]);
}
}
- JavaScript class for .LGR file handling:
class LGRFile {
constructor(buffer = null) {
this.magic = '';
this.version = '';
this.numPcx = 0;
this.lstVersion = 0;
this.numLst = 0;
this.imageNames = [];
this.imageTypes = [];
this.distances = [];
this.clippings = [];
this.transLocs = [];
this.pcxObjects = []; // Array of {filename: str, width: num, height: num, length: num, data: Uint8Array}
this.endMarker = new Uint8Array(4);
if (buffer) {
this.read(buffer);
}
}
read(buffer) {
const view = new DataView(buffer);
let offset = 0;
this.magic = String.fromCharCode(view.getUint8(offset++), view.getUint8(offset++), view.getUint8(offset++));
this.version = String.fromCharCode(view.getUint8(offset++), view.getUint8(offset++));
this.numPcx = view.getInt32(offset, true); offset += 4;
this.lstVersion = view.getInt32(offset, true); offset += 4;
this.numLst = view.getInt32(offset, true); offset += 4;
for (let i = 0; i < this.numLst; i++) {
let name = '';
for (let j = 0; j < 10; j++) {
const char = view.getUint8(offset++);
if (char === 0) break;
name += String.fromCharCode(char);
}
this.imageNames.push(name);
}
for (let i = 0; i < this.numLst; i++) { this.imageTypes.push(view.getInt32(offset, true)); offset += 4; }
for (let i = 0; i < this.numLst; i++) { this.distances.push(view.getInt32(offset, true)); offset += 4; }
for (let i = 0; i < this.numLst; i++) { this.clippings.push(view.getInt32(offset, true)); offset += 4; }
for (let i = 0; i < this.numLst; i++) { this.transLocs.push(view.getInt32(offset, true)); offset += 4; }
for (let i = 0; i < this.numPcx; i++) {
let filename = '';
for (let j = 0; j < 20; j++) {
const char = view.getUint8(offset++);
if (char === 0) break;
filename += String.fromCharCode(char);
}
let width = 0, height = 0;
if (this.version === '13') {
width = view.getInt16(offset, true); offset += 2;
height = view.getInt16(offset, true); offset += 2;
}
const length = view.getInt32(offset, true); offset += 4;
const data = new Uint8Array(buffer.slice(offset, offset + length));
this.pcxObjects.push({filename, width, height, length, data});
offset += length;
}
for (let i = 0; i < 4; i++) { this.endMarker[i] = view.getUint8(offset++); }
}
write() {
let size = 3 + 2 + 4*4 + this.numLst*10 + this.numLst*4*4; // Header + lists
this.pcxObjects.forEach(pcx => {
size += 20 + 4 + pcx.length;
if (this.version === '13') size += 4; // width + height
});
size += 4; // end marker
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
let offset = 0;
for (let char of this.magic) view.setUint8(offset++, char.charCodeAt(0));
for (let char of this.version) view.setUint8(offset++, char.charCodeAt(0));
view.setInt32(offset, this.numPcx, true); offset += 4;
view.setInt32(offset, this.lstVersion, true); offset += 4;
view.setInt32(offset, this.numLst, true); offset += 4;
for (let name of this.imageNames) {
for (let char of name) view.setUint8(offset++, char.charCodeAt(0));
while (offset % 10 !== 0) view.setUint8(offset++, 0); // Pad to 10
}
this.imageTypes.forEach(t => { view.setInt32(offset, t, true); offset += 4; });
this.distances.forEach(d => { view.setInt32(offset, d, true); offset += 4; });
this.clippings.forEach(c => { view.setInt32(offset, c, true); offset += 4; });
this.transLocs.forEach(tl => { view.setInt32(offset, tl, true); offset += 4; });
this.pcxObjects.forEach(pcx => {
for (let char of pcx.filename) view.setUint8(offset++, char.charCodeAt(0));
while (offset % 20 !== 0) view.setUint8(offset++, 0); // Pad to 20
if (this.version === '13') {
view.setInt16(offset, pcx.width, true); offset += 2;
view.setInt16(offset, pcx.height, true); offset += 2;
}
view.setInt32(offset, pcx.length, true); offset += 4;
for (let b of pcx.data) view.setUint8(offset++, b);
});
for (let i = 0; i < 4; i++) view.setUint8(offset++, this.endMarker[i]);
return buffer;
}
printProperties() {
console.log(`Magic: ${this.magic}`);
console.log(`Version: ${this.version}`);
console.log(`Number of PCX files: ${this.numPcx}`);
console.log(`Pictures.lst version: ${this.lstVersion}`);
console.log(`Number of lst elements: ${this.numLst}`);
console.log(`Image names: ${this.imageNames.join(', ')}`);
console.log(`Image types: ${this.imageTypes.join(', ')}`);
console.log(`Default distances: ${this.distances.join(', ')}`);
console.log(`Default clippings: ${this.clippings.join(', ')}`);
console.log(`Transparent locations: ${this.transLocs.join(', ')}`);
console.log('PCX objects:');
this.pcxObjects.forEach(pcx => {
console.log(` - Filename: ${pcx.filename}, Width: ${pcx.width}, Height: ${pcx.height}, Length: ${pcx.length}`);
});
console.log(`End marker: [${Array.from(this.endMarker).map(b => `0x${b.toString(16)}`).join(', ')}]`);
}
}
// Example usage: const lgr = new LGRFile(arrayBuffer); lgr.printProperties();
- C struct and functions for .LGR file handling (using struct as "class" equivalent, with functions):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
// Assume little-endian, no byte swap needed on x86
typedef struct {
char *filename;
int width;
int height;
int length;
uint8_t *data;
} PCXObject;
typedef struct {
char magic[4]; // Null-terminated
char version[3]; // Null-terminated
int32_t num_pcx;
int32_t lst_version;
int32_t num_lst;
char **image_names;
int32_t *image_types;
int32_t *distances;
int32_t *clippings;
int32_t *trans_locs;
PCXObject *pcx_objects;
uint8_t end_marker[4];
FILE *file;
} LGR;
LGR* lgr_new() {
LGR *lgr = (LGR*)malloc(sizeof(LGR));
memset(lgr, 0, sizeof(LGR));
return lgr;
}
void lgr_open(LGR *lgr, const char *filename) {
lgr->file = fopen(filename, "rb");
}
void lgr_read(LGR *lgr) {
fseek(lgr->file, 0, SEEK_SET);
fread(lgr->magic, 1, 3, lgr->file);
lgr->magic[3] = '\0';
fread(lgr->version, 1, 2, lgr->file);
lgr->version[2] = '\0';
fread(&lgr->num_pcx, sizeof(int32_t), 1, lgr->file);
fread(&lgr->lst_version, sizeof(int32_t), 1, lgr->file);
fread(&lgr->num_lst, sizeof(int32_t), 1, lgr->file);
lgr->image_names = (char**)malloc(lgr->num_lst * sizeof(char*));
for (int i = 0; i < lgr->num_lst; i++) {
lgr->image_names[i] = (char*)malloc(11);
fread(lgr->image_names[i], 1, 10, lgr->file);
lgr->image_names[i][10] = '\0';
// Trim null
for (int j = 9; j >= 0; j--) if (lgr->image_names[i][j] == 0) lgr->image_names[i][j] = '\0';
}
lgr->image_types = (int32_t*)malloc(lgr->num_lst * sizeof(int32_t));
fread(lgr->image_types, sizeof(int32_t), lgr->num_lst, lgr->file);
lgr->distances = (int32_t*)malloc(lgr->num_lst * sizeof(int32_t));
fread(lgr->distances, sizeof(int32_t), lgr->num_lst, lgr->file);
lgr->clippings = (int32_t*)malloc(lgr->num_lst * sizeof(int32_t));
fread(lgr->clippings, sizeof(int32_t), lgr->num_lst, lgr->file);
lgr->trans_locs = (int32_t*)malloc(lgr->num_lst * sizeof(int32_t));
fread(lgr->trans_locs, sizeof(int32_t), lgr->num_lst, lgr->file);
lgr->pcx_objects = (PCXObject*)malloc(lgr->num_pcx * sizeof(PCXObject));
for (int i = 0; i < lgr->num_pcx; i++) {
lgr->pcx_objects[i].filename = (char*)malloc(21);
fread(lgr->pcx_objects[i].filename, 1, 20, lgr->file);
lgr->pcx_objects[i].filename[20] = '\0';
// Trim null
for (int j = 19; j >= 0; j--) if (lgr->pcx_objects[i].filename[j] == 0) lgr->pcx_objects[i].filename[j] = '\0';
lgr->pcx_objects[i].width = 0;
lgr->pcx_objects[i].height = 0;
if (strcmp(lgr->version, "13") == 0) {
int16_t w, h;
fread(&w, sizeof(int16_t), 1, lgr->file);
fread(&h, sizeof(int16_t), 1, lgr->file);
lgr->pcx_objects[i].width = w;
lgr->pcx_objects[i].height = h;
}
fread(&lgr->pcx_objects[i].length, sizeof(int32_t), 1, lgr->file);
lgr->pcx_objects[i].data = (uint8_t*)malloc(lgr->pcx_objects[i].length);
fread(lgr->pcx_objects[i].data, 1, lgr->pcx_objects[i].length, lgr->file);
}
fread(lgr->end_marker, 1, 4, lgr->file);
}
void lgr_write(LGR *lgr, const char *filename) {
FILE *out = fopen(filename, "wb");
fwrite(lgr->magic, 1, 3, out);
fwrite(lgr->version, 1, 2, out);
fwrite(&lgr->num_pcx, sizeof(int32_t), 1, out);
fwrite(&lgr->lst_version, sizeof(int32_t), 1, out);
fwrite(&lgr->num_lst, sizeof(int32_t), 1, out);
for (int i = 0; i < lgr->num_lst; i++) {
fwrite(lgr->image_names[i], 1, strlen(lgr->image_names[i]), out);
for (int j = strlen(lgr->image_names[i]); j < 10; j++) fputc(0, out);
}
fwrite(lgr->image_types, sizeof(int32_t), lgr->num_lst, out);
fwrite(lgr->distances, sizeof(int32_t), lgr->num_lst, out);
fwrite(lgr->clippings, sizeof(int32_t), lgr->num_lst, out);
fwrite(lgr->trans_locs, sizeof(int32_t), lgr->num_lst, out);
for (int i = 0; i < lgr->num_pcx; i++) {
fwrite(lgr->pcx_objects[i].filename, 1, strlen(lgr->pcx_objects[i].filename), out);
for (int j = strlen(lgr->pcx_objects[i].filename); j < 20; j++) fputc(0, out);
if (strcmp(lgr->version, "13") == 0) {
int16_t w = (int16_t)lgr->pcx_objects[i].width;
int16_t h = (int16_t)lgr->pcx_objects[i].height;
fwrite(&w, sizeof(int16_t), 1, out);
fwrite(&h, sizeof(int16_t), 1, out);
}
fwrite(&lgr->pcx_objects[i].length, sizeof(int32_t), 1, out);
fwrite(lgr->pcx_objects[i].data, 1, lgr->pcx_objects[i].length, out);
}
fwrite(lgr->end_marker, 1, 4, out);
fclose(out);
}
void lgr_print_properties(LGR *lgr) {
printf("Magic: %s\n", lgr->magic);
printf("Version: %s\n", lgr->version);
printf("Number of PCX files: %d\n", lgr->num_pcx);
printf("Pictures.lst version: %d\n", lgr->lst_version);
printf("Number of lst elements: %d\n", lgr->num_lst);
printf("Image names: ");
for (int i = 0; i < lgr->num_lst; i++) printf("%s%s", lgr->image_names[i], (i < lgr->num_lst - 1) ? ", " : "\n");
printf("Image types: ");
for (int i = 0; i < lgr->num_lst; i++) printf("%d%s", lgr->image_types[i], (i < lgr->num_lst - 1) ? ", " : "\n");
printf("Default distances: ");
for (int i = 0; i < lgr->num_lst; i++) printf("%d%s", lgr->distances[i], (i < lgr->num_lst - 1) ? ", " : "\n");
printf("Default clippings: ");
for (int i = 0; i < lgr->num_lst; i++) printf("%d%s", lgr->clippings[i], (i < lgr->num_lst - 1) ? ", " : "\n");
printf("Transparent locations: ");
for (int i = 0; i < lgr->num_lst; i++) printf("%d%s", lgr->trans_locs[i], (i < lgr->num_lst - 1) ? ", " : "\n");
printf("PCX objects:\n");
for (int i = 0; i < lgr->num_pcx; i++) {
printf(" - Filename: %s, Width: %d, Height: %d, Length: %d\n",
lgr->pcx_objects[i].filename, lgr->pcx_objects[i].width, lgr->pcx_objects[i].height, lgr->pcx_objects[i].length);
}
printf("End marker: [0x%02x, 0x%02x, 0x%02x, 0x%02x]\n",
lgr->end_marker[0], lgr->end_marker[1], lgr->end_marker[2], lgr->end_marker[3]);
}
void lgr_free(LGR *lgr) {
for (int i = 0; i < lgr->num_lst; i++) free(lgr->image_names[i]);
free(lgr->image_names);
free(lgr->image_types);
free(lgr->distances);
free(lgr->clippings);
free(lgr->trans_locs);
for (int i = 0; i < lgr->num_pcx; i++) {
free(lgr->pcx_objects[i].filename);
free(lgr->pcx_objects[i].data);
}
free(lgr->pcx_objects);
if (lgr->file) fclose(lgr->file);
free(lgr);
}
// Example usage: LGR *lgr = lgr_new(); lgr_open(lgr, "file.lgr"); lgr_read(lgr); lgr_print_properties(lgr); lgr_write(lgr, "new.lgr"); lgr_free(lgr);