mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
186 lines (159 loc) • 4.68 kB
text/typescript
import TokenStream from '../parsers/mdlx/tokenstream';
/**
* The structure of a source map node.
*/
interface SourceNode {
name: string;
ident: number;
start: number;
end: number;
data: string;
children: SourceNode[];
index?: number;
objectName?: string;
}
/**
* The need data for a single source map creation.
*/
class SourceMapData {
stream: TokenStream;
rootNode: SourceNode;
stack: SourceNode[];
counts: NumberObject = {};
ident: number = 0;
searchBitmapName: boolean = false;
lookingForAnimName: boolean = false;
constructor(stream: TokenStream) {
this.stream = stream;
this.rootNode = { name: 'root', ident: 0, start: 0, end: stream.buffer.length, data: '', children: [] };
this.stack = [this.rootNode];
}
}
// Names of chunk blocks.
let chunkNames = [
'Version',
'Model',
'Sequences',
'GlobalSequences',
'Textures',
'Materials',
'TextureAnims',
'PivotPoints',
'FaceFX',
'BindPose',
];
// Names of object blocks.
let objectNames = [
'Anim',
'Bitmap',
'Material',
'Layer',
'TVertexAnim',
'Geoset',
'GeosetAnim',
];
// Names of generic object blocks.
let genericObjectNames = [
'Bone',
'Light',
'Helper',
'Attachment',
'ParticleEmitter',
'ParticleEmitter2',
'ParticleEmitterPopcorn',
'RibbonEmitter',
'Camera',
'EventObject',
'CollisionShape',
];
/**
* Test if a token starts a block.
*/
function isTokenBlock(data: SourceMapData, name: string) {
if (name === 'Anim') {
// Geosets call their animated extents Anim too, so have to check if the parent is the sequences chunk.
return data.stack[0].name === 'Sequences';
}
return chunkNames.includes(name) || objectNames.includes(name) || genericObjectNames.includes(name);
}
/**
* Start a new block and add it to the stack.
*/
function startBlock(data: SourceMapData, name: string) {
let counts = data.counts;
let countName = name;
let stream = data.stream;
let node = <SourceNode>{ name, ident: data.ident, start: stream.index - name.length - 1, end: 0, data: '', children: [] };
// Don't index blocks, just objects.
if (!chunkNames.includes(name)) {
// Need to separate layer indices per material.
if (name === 'Layer') {
let material = data.stack[0];
countName = `Material${material.index}${name}`;
}
if (counts[countName] === undefined) {
counts[countName] = 0;
}
let index = counts[countName]++;
node.index = index;
}
// Texture names depend on the path or replaceable ID, both of which are not known.
// Therefore, start looking for them.
if (name === 'Bitmap') {
data.searchBitmapName = true;
}
// Animations and generic objects store the name in the next token.
if (name === 'Anim' || genericObjectNames.includes(name)) {
node.objectName = stream.read();
}
data.stack[0].children.push(node);
data.stack.unshift(node);
}
/**
* End the current block on the stack.
*/
function endBlock(data: SourceMapData) {
let stream = data.stream;
let node = data.stack[0];
node.end = stream.index;
node.data = stream.buffer.slice(node.start, node.end);
data.stack.shift();
}
/**
* Generate a source map from MDL source.
*/
export default function mdlSourceMap(buffer: string) {
let stream = new TokenStream(buffer);
let data = new SourceMapData(stream);
let token;
// There are empty strings, e.g. in replaceable textures.
// Empty strings evaluate to false in JS.
// Therefore compare against undefined directly.
while ((token = stream.readToken()) !== undefined) {
if (token === '{') {
data.ident += 1;
} else if (token === '}') {
data.ident -= 1;
if (data.ident === data.stack[0].ident) {
endBlock(data);
}
} else if (data.searchBitmapName && (token === 'ReplaceableId' || token === 'Image')) {
if (token === 'ReplaceableId') {
let replacebleId = stream.read();
if (replacebleId !== '0') {
data.lookingForAnimName = false;
data.stack[0].objectName = `ReplaceableId ${replacebleId}`;
}
} else if (token === 'Image') {
let image = <string>stream.read();
if (image !== '') {
data.lookingForAnimName = false;
data.stack[0].objectName = image.split('\\').slice(-1)[0]; // Discard the path leading to the file name itself.
}
}
} else if (isTokenBlock(data, token)) {
startBlock(data, token);
}
}
return data.rootNode;
}