Skip to content

Commit

Permalink
Merge afc18c8 into ed88f7c
Browse files Browse the repository at this point in the history
  • Loading branch information
hexjelly committed May 1, 2019
2 parents ed88f7c + afc18c8 commit 26ba65a
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 16 deletions.
Binary file added __tests__/assets/lgr/Across.lgr
Binary file not shown.
Binary file added __tests__/assets/lgr/Default.lgr
Binary file not shown.
19 changes: 19 additions & 0 deletions __tests__/lgr.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { LGR } from '../src';

describe('LGR', () => {
test('load and toBuffer matches #1', async () => {
const lgr = await LGR.load('__tests__/assets/lgr/Default.lgr');
lgr.path = '';
const lgrBuffer = await lgr.toBuffer();
const loadedBuffer = await LGR.load(lgrBuffer);
expect(lgr).toEqual(loadedBuffer);
});

test('load and toBuffer matches #2', async () => {
const lgr = await LGR.load('__tests__/assets/lgr/Across.lgr');
lgr.path = '';
const lgrBuffer = await lgr.toBuffer();
const loadedBuffer = await LGR.load(lgrBuffer);
expect(lgr).toEqual(loadedBuffer);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './lev';
export * from './rec';
export * from './shared';
export * from './util';
export * from './lgr';
8 changes: 1 addition & 7 deletions src/lev/Picture.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { Position } from '../shared';

export enum Clip {
Unclipped = 0,
Ground = 1,
Sky = 2,
}
import { Clip, Position } from '../shared';

export default class Picture {
public name: string;
Expand Down
2 changes: 1 addition & 1 deletion src/lev/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { default as ElmaObject, Gravity, ObjectType } from './ElmaObject';
export { default as Picture, Clip } from './Picture';
export { default as Picture } from './Picture';
export { default as Polygon } from './Polygon';
export { default as Level, ITimeEntry, ITop10, Version } from './Level';
192 changes: 192 additions & 0 deletions src/lgr/LGR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { readFile, writeFile } from 'fs-extra';
import {
Clip,
PictureData,
PictureDeclaration,
PictureType,
Transparency,
} from '../';
import { nullpadString, trimString } from '../util';

// Magic arbitrary number to signify start of LGR file.
const LGRStart = 0x000003ea;
// Magic arbitrary number to signify end of LGR file.
const LGREOF = 0x0b2e05e7;

export default class LGR {
/**
* Loads a level file.
* @param source Can either be a file path or a buffer
*/
public static async load(source: string | Buffer): Promise<LGR> {
if (typeof source === 'string') {
const file = await readFile(source);
return this._parseBuffer(file, source);
} else if (source instanceof Buffer) {
return this._parseBuffer(source);
}
throw new Error(
'Invalid input argument. Expected string or Buffer instance object'
);
}

private static async _parseBuffer(
buffer: Buffer,
path?: string
): Promise<LGR> {
const lgr = new LGR();
if (path) lgr.path = path;

const version = buffer.toString('ascii', 0, 5);

// there are no other LGR versions possible, so no need to store it (?)
if (version !== 'LGR12') {
throw new Error(`Invalid LGR version: ${version}`);
}

const pictureLen = buffer.readInt32LE(5);
const expectedHeader = buffer.readInt32LE(9);
if (expectedHeader !== LGRStart) {
throw new Error(`Invalid header: ${expectedHeader}`);
}

// picture.lst section
const listLen = buffer.readInt32LE(13);
lgr.pictureList = await lgr._parseListData(
buffer.slice(17, 17 + 26 * listLen),
listLen
);

// pcx data
const [pictureData, bytesRead] = await lgr._parsePictureData(
buffer.slice(17 + 26 * listLen),
pictureLen
);
lgr.pictureData = pictureData;

const expectedEof = buffer.readInt32LE(17 + 26 * listLen + bytesRead);
if (expectedEof !== LGREOF) {
throw new Error(
`EOF marker expected at byte: ${17 + 26 * listLen + bytesRead}`
);
}
return lgr;
}

public pictureList: PictureDeclaration[] = [];
public pictureData: PictureData[] = [];
public path: string = '';

/**
* Returns a buffer representation of the LGR.
*/
public async toBuffer(): Promise<Buffer> {
// calculate how many bytes to allocate:
// - 21 known static bytes, plus 26 bytes for each item in picture.lst
// - the image data is then reduced and added to that.
const pictureListLength = this.pictureList.length;
const bytesToAlloc = this.pictureData.reduce(
(bytes, picture) => bytes + picture.data.length + 24,
21 + 26 * pictureListLength
);
const buffer = Buffer.alloc(bytesToAlloc);
let offset = 0;
buffer.write('LGR12', offset, 5, 'ascii');
offset += 5;
buffer.writeUInt32LE(this.pictureData.length, offset);
offset += 4;
buffer.writeInt32LE(LGRStart, offset);
offset += 4;
buffer.writeUInt32LE(pictureListLength, offset);
offset += 4;

this.pictureList.forEach((pictureDeclaration, n) => {
buffer.write(
nullpadString(pictureDeclaration.name, 10),
offset + n * 10,
10,
'ascii'
);
buffer.writeUInt32LE(
pictureDeclaration.pictureType,
offset + 10 * pictureListLength + 4 * n
);
buffer.writeUInt32LE(
pictureDeclaration.distance,
offset + 14 * pictureListLength + 4 * n
);
buffer.writeUInt32LE(
pictureDeclaration.clipping,
offset + 18 * pictureListLength + 4 * n
);
buffer.writeUInt32LE(
pictureDeclaration.transparency,
offset + 22 * pictureListLength + 4 * n
);
});

offset += 26 * pictureListLength;

this.pictureData.forEach(pictureData => {
buffer.write(nullpadString(pictureData.name, 20), offset, 20, 'ascii');
offset += 20;
buffer.writeUInt32LE(pictureData.data.length, offset);
offset += 4;
offset += pictureData.data.copy(buffer, offset);
});
buffer.writeInt32LE(LGREOF, offset);
return buffer;
}

public async save(path?: string) {
const buffer = await this.toBuffer();
await writeFile(path || this.path, buffer);
}

private async _parseListData(
buffer: Buffer,
length: number
): Promise<PictureDeclaration[]> {
const pictureDeclarations: PictureDeclaration[] = [];
let offset = 0;
const names = buffer.slice(offset, offset + length * 10);
offset += length * 10;
const pictureTypes = buffer.slice(offset, offset + length * 4);
offset += length * 4;
const distances = buffer.slice(offset, offset + length * 4);
offset += length * 4;
const clips = buffer.slice(offset, offset + length * 4);
offset += length * 4;
const transparencies = buffer.slice(offset, offset + length * 4);
for (let i = 0; i < length; i++) {
const pictureDeclaration = new PictureDeclaration();
pictureDeclaration.name = trimString(names.slice(10 * i, 10 * i + 10));
pictureDeclaration.pictureType = pictureTypes.readInt32LE(i * 4);
pictureDeclaration.distance = distances.readInt32LE(i * 4);
pictureDeclaration.clipping = clips.readInt32LE(i * 4);
pictureDeclaration.transparency = transparencies.readInt32LE(i * 4);
pictureDeclarations.push(pictureDeclaration);
}

return pictureDeclarations;
}

private async _parsePictureData(
buffer: Buffer,
length: number
): Promise<[PictureData[], number]> {
const pictures: PictureData[] = [];
let offset = 0;
for (let i = 0; i < length; i++) {
const name = trimString(buffer.slice(offset, offset + 12));
offset += 20; // +8 garbage (?) bytes
const bytesToRead = buffer.readUInt32LE(offset);
offset += 4;
const data = buffer.slice(offset, offset + bytesToRead);
pictures.push(new PictureData(name, data));
offset += bytesToRead;
}
// we need to return amount of bytes read in order to correctly get the offset later.
return [pictures, offset];
}
}
9 changes: 9 additions & 0 deletions src/lgr/PictureData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default class PictureData {
public name: string;
public data: Buffer;

constructor(name: string, buffer: Buffer) {
this.name = name;
this.data = buffer;
}
}
35 changes: 35 additions & 0 deletions src/lgr/PictureDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Clip } from '../shared';

export default class PictureDeclaration {
/// Picture name.
public name: string = '';
/// Picture type.
public pictureType: PictureType = PictureType.Normal;
/// Default distance, 1-999.
public distance: number = 450;
/// Default clipping.
public clipping: Clip = Clip.Sky;
/// Transparency.
public transparency: Transparency = Transparency.TopLeft;
}

export enum PictureType {
Normal = 100,
Texture = 101,
Mask = 102,
}

export enum Transparency {
// No transparency. Only valid for ´Mask´ picture types.
Solid = 10,
// Palette index 0 is transparent color.
Palette = 11,
// Top left pixel is transparent color.
TopLeft = 12,
// Top right pixel is transparent color.
TopRight = 13,
// Bottom left pixel is transparent color.
BottomLeft = 14,
// Bottom right pixel is transparent color.
BottomRight = 15,
}
7 changes: 7 additions & 0 deletions src/lgr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { default as LGR } from './LGR';
export { default as PictureData } from './PictureData';
export {
default as PictureDeclaration,
PictureType,
Transparency,
} from './PictureDeclaration';
22 changes: 14 additions & 8 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@
* Simple point with x and y coordinates
*/
export class Position {
public x: number
public y: number
public x: number;
public y: number;

constructor(x: number = 0, y: number = 0) {
this.x = x
this.y = y
this.x = x;
this.y = y;
}
}

export enum Clip {
Unclipped = 0,
Ground = 1,
Sky = 2,
}

// Bike diameters and radii.
export const HEAD_DIAMETER = 0.476
export const HEAD_RADIUS = 0.238
export const OBJECT_DIAMETER = 0.8
export const OBJECT_RADIUS = 0.4
export const HEAD_DIAMETER = 0.476;
export const HEAD_RADIUS = 0.238;
export const OBJECT_DIAMETER = 0.8;
export const OBJECT_RADIUS = 0.4;

0 comments on commit 26ba65a

Please sign in to comment.