war3-model
Version:
Warcraft 3 model parser, generator, convertor and previewer
1 lines • 983 kB
Source Map (JSON)
{"version":3,"file":"war3-model.cjs","sources":["../model.ts","../renderer/util.ts","../mdl/parse.ts","../mdx/parse.ts","../mdl/generate.ts","../mdx/generate.ts","../blp/blpimage.ts","../third_party/decoder.js","../blp/decode.ts","../node_modules/gl-matrix/esm/common.js","../node_modules/gl-matrix/esm/mat3.js","../node_modules/gl-matrix/esm/mat4.js","../node_modules/gl-matrix/esm/vec3.js","../node_modules/gl-matrix/esm/vec4.js","../node_modules/gl-matrix/esm/quat.js","../renderer/interp.ts","../renderer/modelInterp.ts","../renderer/shaders/webgl/particles.vs.glsl?raw","../renderer/shaders/webgl/particles.fs.glsl?raw","../renderer/shaders/webgpu/particles.wgsl?raw","../renderer/particles.ts","../renderer/shaders/webgl/ribbon.vs.glsl?raw","../renderer/shaders/webgl/ribbon.fs.glsl?raw","../renderer/shaders/webgpu/ribbons.wgsl?raw","../renderer/ribbons.ts","../renderer/shaders/webgl/sdHardwareSkinning.vs.glsl?raw","../renderer/shaders/webgl/sdSoftwareSkinning.vs.glsl?raw","../renderer/shaders/webgl/sd.fs.glsl?raw","../renderer/shaders/webgl/hdHardwareSkinningOld.vs.glsl?raw","../renderer/shaders/webgl/hdHardwareSkinningNew.vs.glsl?raw","../renderer/shaders/webgl/hdOld.fs.glsl?raw","../renderer/shaders/webgl/hdNew.fs.glsl?raw","../renderer/shaders/webgl/skeleton.vs.glsl?raw","../renderer/shaders/webgl/skeleton.fs.glsl?raw","../renderer/shaders/webgl/envToCubemap.vs.glsl?raw","../renderer/shaders/webgl/envToCubemap.fs.glsl?raw","../renderer/shaders/webgl/env.vs.glsl?raw","../renderer/shaders/webgl/env.fs.glsl?raw","../renderer/shaders/webgl/convoluteEnvDiffuse.vs.glsl?raw","../renderer/shaders/webgl/convoluteEnvDiffuse.fs.glsl?raw","../renderer/shaders/webgl/prefilterEnv.vs.glsl?raw","../renderer/shaders/webgl/prefilterEnv.fs.glsl?raw","../renderer/shaders/webgl/integrateBRDF.vs.glsl?raw","../renderer/shaders/webgl/integrateBRDF.fs.glsl?raw","../renderer/shaders/webgpu/sd.wgsl?raw","../renderer/shaders/webgpu/hd.wgsl?raw","../renderer/shaders/webgpu/depth.wgsl?raw","../renderer/shaders/webgpu/skeleton.wgsl?raw","../renderer/shaders/webgpu/env.wgsl?raw","../renderer/shaders/webgpu/envToCubemap.wgsl?raw","../renderer/shaders/webgpu/convoluteEnvDiffuse.wgsl?raw","../renderer/shaders/webgpu/prefilterEnv.wgsl?raw","../renderer/shaders/webgpu/integrateBRDF.wgsl?raw","../renderer/shaders/webgpu/mips.wgsl?raw","../renderer/generateMips.ts","../renderer/modelRenderer.ts"],"sourcesContent":["export interface ModelInfo {\n Name: string;\n MinimumExtent: Float32Array;\n MaximumExtent: Float32Array;\n BoundsRadius: number;\n BlendTime: number;\n NumGeosets?: number;\n NumGeosetAnims?: number;\n NumBones?: number;\n NumLights?: number;\n NumAttachments?: number;\n NumEvents?: number;\n NumParticleEmitters?: number;\n NumParticleEmitters2?: number;\n NumRibbonEmitters?: number;\n}\n\nexport interface Sequence {\n Name: string;\n Interval: Uint32Array;\n NonLooping: boolean;\n MinimumExtent: Float32Array;\n MaximumExtent: Float32Array;\n BoundsRadius: number;\n MoveSpeed: number;\n Rarity: number;\n}\n\nexport enum TextureFlags {\n WrapWidth = 1,\n WrapHeight = 2\n}\n\nexport interface Texture {\n Image: string;\n ReplaceableId?: number;\n Flags?: TextureFlags;\n}\n\nexport enum FilterMode {\n None = 0,\n Transparent = 1,\n Blend = 2,\n Additive = 3,\n AddAlpha = 4,\n Modulate = 5,\n Modulate2x = 6\n}\n\nexport enum LineType {\n DontInterp = 0,\n Linear = 1,\n Hermite = 2,\n Bezier = 3\n}\n\nexport interface AnimKeyframe {\n Frame: number;\n Vector: Float32Array|Int32Array;\n InTan?: Float32Array|Int32Array;\n OutTan?: Float32Array|Int32Array;\n}\n\nexport interface AnimVector {\n LineType: LineType;\n GlobalSeqId?: number;\n Keys: AnimKeyframe[];\n}\n\nexport enum LayerShading {\n Unshaded = 1,\n SphereEnvMap = 2,\n TwoSided = 16,\n Unfogged = 32,\n NoDepthTest = 64,\n NoDepthSet = 128\n}\n\nexport interface Layer {\n FilterMode?: FilterMode;\n Shading?: number;\n TextureID?: AnimVector|number;\n TVertexAnimId?: number;\n CoordId: number;\n Alpha?: AnimVector|number;\n /* Since Version: 900 */\n EmissiveGain?: AnimVector|number;\n /* Since Version: 1000 */\n FresnelColor?: AnimVector|Float32Array;\n /* Since Version: 1000 */\n FresnelOpacity?: AnimVector|number;\n /* Since Version: 1000 */\n FresnelTeamColor?: AnimVector|number;\n /* Since version: 1100 */\n ShaderTypeId?: number;\n NormalTextureID?: AnimVector|number;\n ORMTextureID?: AnimVector|number;\n EmissiveTextureID?: AnimVector|number;\n TeamColorTextureID?: AnimVector|number;\n ReflectionsTextureID?: AnimVector|number;\n}\n\nexport enum MaterialRenderMode {\n ConstantColor = 1,\n SortPrimsFarZ = 16,\n FullResolution = 32,\n}\n\nexport interface Material {\n PriorityPlane?: number;\n RenderMode?: number;\n Layers: Layer[];\n /* Since Version: 900 */\n Shader?: string;\n}\n\nexport interface GeosetAnimInfo {\n MinimumExtent: Float32Array;\n MaximumExtent: Float32Array;\n BoundsRadius: number;\n}\n\nexport interface Geoset {\n Vertices: Float32Array;\n Normals: Float32Array;\n TVertices: Float32Array[];\n VertexGroup: Uint8Array;\n Faces: Uint16Array;\n Groups: number[][];\n TotalGroupsCount: number;\n MinimumExtent: Float32Array;\n MaximumExtent: Float32Array;\n BoundsRadius: number;\n Anims: GeosetAnimInfo[];\n MaterialID: number;\n SelectionGroup: number;\n Unselectable: boolean;\n /* Since Version: 900 */\n LevelOfDetail?: number;\n /* Since Version: 900 */\n Name?: string;\n /* Since Version: 900 */\n Tangents?: Float32Array;\n /* Since Version: 900 */\n SkinWeights?: Uint8Array;\n}\n\nexport enum GeosetAnimFlags {\n DropShadow = 1,\n Color = 2\n}\n\nexport interface GeosetAnim {\n GeosetId: number;\n Alpha: AnimVector|number;\n Color: AnimVector|Float32Array;\n Flags: number;\n}\n\nexport enum NodeFlags {\n DontInheritTranslation = 1,\n DontInheritRotation = 2,\n DontInheritScaling = 4,\n Billboarded = 8,\n BillboardedLockX = 16,\n BillboardedLockY = 32,\n BillboardedLockZ = 64,\n CameraAnchored = 128\n}\n\nexport enum NodeType {\n Helper = 0,\n Bone = 256,\n Light = 512,\n EventObject = 1024,\n Attachment = 2048,\n ParticleEmitter = 4096, // ParticleEmitter | ParticleEmitter2 | ParticleEmitterPopcorn\n CollisionShape = 8192,\n RibbonEmitter = 16384\n}\n\nexport interface Node {\n Name: string;\n ObjectId: number;\n Parent?: number|null;\n PivotPoint: Float32Array;\n Flags: number;\n\n Translation?: AnimVector;\n Rotation?: AnimVector;\n Scaling?: AnimVector;\n}\n\nexport interface Bone extends Node {\n GeosetId?: number;\n GeosetAnimId?: number;\n}\n\nexport type Helper = Node\n\nexport interface Attachment extends Node {\n Path?: string;\n AttachmentID?: number;\n Visibility?: AnimVector;\n}\n\nexport interface EventObject extends Node {\n EventTrack: Uint32Array;\n}\n\nexport enum CollisionShapeType {\n Box = 0,\n Sphere = 2\n}\n\nexport interface CollisionShape extends Node {\n Shape: CollisionShapeType;\n Vertices: Float32Array;\n BoundsRadius?: number;\n}\n\nexport enum ParticleEmitterFlags {\n EmitterUsesMDL = 32768,\n EmitterUsesTGA = 65536\n}\n\nexport interface ParticleEmitter extends Node {\n EmissionRate: AnimVector|number;\n Gravity: AnimVector|number;\n Longitude: AnimVector|number;\n Latitude: AnimVector|number;\n Path: string;\n LifeSpan: AnimVector|number;\n InitVelocity: AnimVector|number;\n Visibility: AnimVector;\n}\n\nexport enum ParticleEmitter2Flags {\n Unshaded = 32768,\n SortPrimsFarZ = 65536,\n LineEmitter = 131072,\n Unfogged = 262144,\n ModelSpace = 524288,\n XYQuad = 1048576\n}\n\nexport enum ParticleEmitter2FilterMode {\n Blend = 0,\n Additive = 1,\n Modulate = 2,\n Modulate2x = 3,\n AlphaKey = 4\n}\n\n// Not actually mapped to mdx flags (0: Head, 1: Tail, 2: Both)\nexport enum ParticleEmitter2FramesFlags {\n Head = 1,\n Tail = 2\n}\n\nexport interface ParticleEmitter2 extends Node {\n Speed?: AnimVector|number;\n Variation?: AnimVector|number;\n Latitude?: AnimVector|number;\n Gravity?: AnimVector|number;\n Visibility?: AnimVector|number;\n Squirt?: boolean;\n LifeSpan?: number;\n EmissionRate?: AnimVector|number;\n Width?: AnimVector|number;\n Length?: AnimVector|number;\n FilterMode?: ParticleEmitter2FilterMode;\n Rows?: number;\n Columns?: number;\n FrameFlags: number;\n TailLength?: number;\n Time?: number;\n SegmentColor?: Float32Array[];\n Alpha?: Uint8Array;\n ParticleScaling?: Float32Array;\n LifeSpanUVAnim?: Uint32Array;\n DecayUVAnim?: Uint32Array;\n TailUVAnim?: Uint32Array;\n TailDecayUVAnim?: Uint32Array;\n TextureID?: number;\n ReplaceableId?: number;\n PriorityPlane?: number;\n}\n\nexport interface Camera {\n Name: string;\n Position: Float32Array;\n FieldOfView: number;\n NearClip: number;\n FarClip: number;\n TargetPosition: Float32Array;\n TargetTranslation?: AnimVector;\n Translation?: AnimVector;\n Rotation?: AnimVector;\n}\n\nexport enum LightType {\n Omnidirectional = 0,\n Directional = 1,\n Ambient = 2\n}\n\nexport interface Light extends Node {\n LightType: LightType;\n\n AttenuationStart?: AnimVector|number;\n AttenuationEnd?: AnimVector|number;\n\n Color?: AnimVector|Float32Array;\n Intensity?: AnimVector|number;\n AmbIntensity?: AnimVector|number;\n AmbColor?: AnimVector|Float32Array;\n\n Visibility?: AnimVector;\n}\n\nexport interface RibbonEmitter extends Node {\n HeightAbove?: AnimVector|number;\n HeightBelow?: AnimVector|number;\n Alpha?: AnimVector|number;\n // todo support KRCO\n Color?: Float32Array;\n LifeSpan?: number;\n TextureSlot?: AnimVector|number;\n EmissionRate?: number;\n Rows?: number;\n Columns?: number;\n MaterialID?: number;\n Gravity?: number;\n\n Visibility?: AnimVector;\n}\n\nexport interface TVertexAnim {\n Translation?: AnimVector;\n Rotation?: AnimVector;\n Scaling?: AnimVector;\n}\n\n/* Since Version: 900 */\nexport interface FaceFX {\n Name: string;\n Path: string;\n}\n\n/* Since Version: 900 */\nexport interface BindPose {\n Matrices: Float32Array[];\n}\n\n/* Since Version: 900 */\nexport enum ParticleEmitterPopcornFlags {\n Unshaded = 32768,\n SortPrimsFarZ = 65536,\n Unfogged = 262144\n}\n\n/* Since Version: 900 */\nexport interface ParticleEmitterPopcorn extends Node {\n LifeSpan?: AnimVector|number;\n EmissionRate?: AnimVector|number;\n Speed?: AnimVector|number;\n Color?: AnimVector|Float32Array;\n Alpha?: AnimVector|number;\n ReplaceableId?: number;\n Path?: string;\n AnimVisibilityGuide?: string;\n Visibility?: AnimVector;\n}\n\nexport interface Model {\n Version: number;\n Info: ModelInfo;\n Sequences: Sequence[];\n Textures: Texture[];\n Materials: Material[];\n Geosets: Geoset[];\n GeosetAnims: GeosetAnim[];\n Bones: Bone[];\n Helpers: Helper[];\n Attachments: Attachment[];\n Nodes: Node[];\n PivotPoints: Float32Array[];\n EventObjects: EventObject[];\n CollisionShapes: CollisionShape[];\n GlobalSequences: number[];\n ParticleEmitters: ParticleEmitter[];\n ParticleEmitters2: ParticleEmitter2[];\n Cameras: Camera[];\n Lights: Light[];\n RibbonEmitters: RibbonEmitter[];\n TextureAnims: TVertexAnim[];\n /* Since Version: 900 */\n FaceFX?: FaceFX[];\n /* Since Version: 900 */\n BindPoses?: BindPose[];\n /* Since Version: 900 */\n ParticleEmitterPopcorns?: ParticleEmitterPopcorn[];\n}\n","import {vec3, quat, mat4} from 'gl-matrix';\n\nexport function mat4fromRotationOrigin (out: mat4, rotation: quat, origin: vec3): mat4 {\n const x = rotation[0], y = rotation[1], z = rotation[2], w = rotation[3],\n x2 = x + x,\n y2 = y + y,\n z2 = z + z,\n\n xx = x * x2,\n xy = x * y2,\n xz = x * z2,\n yy = y * y2,\n yz = y * z2,\n zz = z * z2,\n wx = w * x2,\n wy = w * y2,\n wz = w * z2,\n\n ox = origin[0],\n oy = origin[1],\n oz = origin[2];\n\n out[0] = (1 - (yy + zz));\n out[1] = (xy + wz);\n out[2] = (xz - wy);\n out[3] = 0;\n out[4] = (xy - wz);\n out[5] = (1 - (xx + zz));\n out[6] = (yz + wx);\n out[7] = 0;\n out[8] = (xz + wy);\n out[9] = (yz - wx);\n out[10] = (1 - (xx + yy));\n out[11] = 0;\n out[12] = ox - (out[0] * ox + out[4] * oy + out[8] * oz);\n out[13] = oy - (out[1] * ox + out[5] * oy + out[9] * oz);\n out[14] = oz - (out[2] * ox + out[6] * oy + out[10] * oz);\n out[15] = 1;\n\n return out;\n}\n\n/**\n * Rotate a 3D vector around the z-axis\n * @param {vec3} out The receiving vec3\n * @param {vec3} a The vec3 point to rotate\n * @param {Number} c The angle of rotation\n * @returns {vec3} out\n */\nexport function vec3RotateZ (out: vec3, a: vec3, c: number): vec3 {\n out[0] = a[0] * Math.cos(c) - a[1] * Math.sin(c);\n out[1] = a[0] * Math.sin(c) + a[1] * Math.cos(c);\n out[2] = a[2];\n\n return out;\n}\n\nexport function rand (from: number, to: number): number {\n return from + Math.random() * (to - from);\n}\n\nexport function degToRad (angle: number): number {\n return angle * Math.PI / 180;\n}\n\nexport function getShader (gl: WebGLRenderingContext, source: string, type: number): WebGLShader {\n const shader: WebGLShader = gl.createShader(type);\n\n gl.shaderSource(shader, source);\n gl.compileShader(shader);\n\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n alert(gl.getShaderInfoLog(shader));\n return null;\n }\n\n return shader;\n}\n\nexport function isWebGL2 (gl: WebGLRenderingContext | WebGL2RenderingContext): gl is WebGL2RenderingContext {\n return gl instanceof WebGL2RenderingContext;\n}\n\nexport const LAYER_TEXTURE_NAME_MAP = {\n 'TextureID': 0,\n 'NormalTextureID': 1,\n 'ORMTextureID': 2,\n 'EmissiveTextureID': 3,\n 'TeamColorTextureID': 4,\n 'ReflectionsTextureID': 5\n};\n\nexport const LAYER_TEXTURE_ID_MAP = [\n 'TextureID',\n 'NormalTextureID',\n 'ORMTextureID',\n 'EmissiveTextureID',\n 'TeamColorTextureID',\n 'ReflectionsTextureID'\n];\n","import {\n Model, Layer, GeosetAnim, AnimVector, LineType, AnimKeyframe, Node,\n CollisionShape, ParticleEmitter2, Camera, MaterialRenderMode, FilterMode, LayerShading, TextureFlags,\n GeosetAnimFlags, NodeFlags, CollisionShapeType, ParticleEmitter2Flags, ParticleEmitter2FramesFlags, Light,\n LightType, TVertexAnim, RibbonEmitter, ParticleEmitter2FilterMode, ParticleEmitter, ParticleEmitterFlags, NodeType,\n EventObject, Sequence, ModelInfo, Geoset, GeosetAnimInfo, FaceFX, BindPose, ParticleEmitterPopcorn, ParticleEmitterPopcornFlags\n} from '../model';\nimport { LAYER_TEXTURE_NAME_MAP } from '../renderer/util';\n\nclass State {\n public readonly str: string;\n public pos: number;\n\n constructor (str: string) {\n this.str = str;\n this.pos = 0;\n }\n\n public char (): string {\n if (this.pos >= this.str.length) {\n throwError(this, 'incorrect model data');\n }\n return this.str[this.pos];\n }\n}\n\ntype IntArray = number[] | Uint8Array | Uint16Array | Uint32Array | Float32Array;\n\nfunction throwError (state: State, str = ''): void {\n throw new Error(`SyntaxError, near ${state.pos}` + (str ? ', ' + str : ''));\n}\n\nfunction parseComment (state: State): boolean {\n if (state.char() === '/' && state.str[state.pos + 1] === '/') {\n state.pos += 2;\n while (state.pos < state.str.length && state.str[++state.pos] !== '\\n');\n ++state.pos;\n return true;\n }\n return false;\n}\n\nconst spaceRE = /\\s/i;\nfunction parseSpace (state: State): void {\n while (state.pos < state.str.length && spaceRE.test(state.char())) {\n ++state.pos;\n }\n}\n\nconst keywordFirstCharRE = /[a-z]/i;\nconst keywordOtherCharRE = /[a-z0-9]/i;\nfunction parseKeyword (state: State): string {\n if (!keywordFirstCharRE.test(state.char())) {\n return null;\n }\n\n let keyword = state.char();\n ++state.pos;\n\n while (keywordOtherCharRE.test(state.char())) {\n keyword += state.str[state.pos++];\n }\n\n parseSpace(state);\n\n return keyword;\n}\n\nfunction parseSymbol (state: State, symbol: string): void {\n if (state.char() === symbol) {\n ++state.pos;\n parseSpace(state);\n }\n}\n\nfunction strictParseSymbol (state: State, symbol: string): void {\n if (state.char() !== symbol) {\n throwError(state, `extected ${symbol}`);\n }\n\n ++state.pos;\n parseSpace(state);\n}\n\nfunction parseString (state: State): string {\n if (state.char() === '\"') {\n const start = ++state.pos; // \"\n\n while (state.char() !== '\"') {\n ++state.pos;\n }\n\n ++state.pos; // \"\n\n const res = state.str.substring(start, state.pos - 1);\n\n parseSpace(state);\n\n return res;\n }\n\n return null;\n}\n\nconst numberFirstCharRE = /[-0-9]/;\nconst numberOtherCharRE = /[-+.0-9e]/i;\nfunction parseNumber (state: State): number|null {\n if (numberFirstCharRE.test(state.char())) {\n const start = state.pos;\n\n ++state.pos;\n\n while (numberOtherCharRE.test(state.char())) {\n ++state.pos;\n }\n\n const res = parseFloat(state.str.substring(start, state.pos));\n\n parseSpace(state);\n\n return res;\n }\n\n return null;\n}\n\nfunction parseArray (state: State, arr?: IntArray, pos?: number): typeof arr|null {\n if (state.char() !== '{') {\n return null;\n }\n\n if (!arr) {\n arr = [];\n pos = 0;\n }\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const num = parseNumber(state);\n\n if (num === null) {\n throwError(state, 'expected number');\n }\n\n arr[pos++] = num;\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n return arr;\n}\n\nfunction parseArrayCounted (state: State, arr: IntArray, pos: number): number {\n if (state.char() !== '{') {\n return 0;\n }\n\n const start = pos;\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const num = parseNumber(state);\n\n if (num === null) {\n throwError(state, 'expected number');\n }\n\n arr[pos++] = num;\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n return pos - start;\n}\n\nfunction parseArrayOrSingleItem<ArrType extends Uint16Array|Uint32Array|Int32Array|Float32Array>\n (state: State, arr: ArrType): ArrType {\n if (state.char() !== '{') {\n arr[0] = parseNumber(state);\n return arr;\n }\n\n let pos = 0;\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const num = parseNumber(state);\n\n if (num === null) {\n throwError(state, 'expected number');\n }\n\n arr[pos++] = num;\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n return arr;\n}\n\nfunction parseObject (state: State): [string|number|null, Record<string, number | string | boolean | IntArray>] {\n let prefix: string|number|null = null;\n const obj: Record<string, number | string | IntArray> = {};\n\n if (state.char() !== '{') {\n prefix = parseString(state);\n if (prefix === null) {\n prefix = parseNumber(state);\n }\n if (prefix === null) {\n throwError(state, 'expected string or number');\n }\n }\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const keyword: string = parseKeyword(state);\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'Interval') {\n const array = new Uint32Array(2);\n obj[keyword] = parseArray(state, array, 0);\n } else if (keyword === 'MinimumExtent' || keyword === 'MaximumExtent') {\n const array = new Float32Array(3);\n obj[keyword] = parseArray(state, array, 0);\n } else {\n obj[keyword] = parseArray(state) || parseString(state);\n if (obj[keyword] === null) {\n obj[keyword] = parseNumber(state);\n }\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n return [prefix, obj];\n}\n\nfunction parseVersion (state: State, model: Model): void {\n const [_unused, obj] = parseObject(state);\n\n if (obj.FormatVersion) {\n model.Version = obj.FormatVersion as number;\n }\n}\n\nfunction parseModelInfo (state: State, model: Model): void {\n const [name, obj] = parseObject(state);\n\n model.Info = obj as unknown as ModelInfo;\n model.Info.Name = name as string;\n}\n\nfunction parseSequences (state: State, model: Model): void {\n parseNumber(state); // count, not used\n\n strictParseSymbol(state, '{');\n\n const res: Sequence[] = [];\n\n while (state.char() !== '}') {\n parseKeyword(state); // Anim\n\n const [name, obj] = parseObject(state);\n obj.Name = name;\n obj.NonLooping = 'NonLooping' in obj;\n obj.MoveSpeed = obj.MoveSpeed || 0;\n obj.Rarity = obj.Rarity || 0;\n\n res.push(obj as unknown as Sequence);\n }\n\n strictParseSymbol(state, '}');\n\n model.Sequences = res;\n}\n\nfunction parseTextures (state: State, model: Model): void {\n const res = [];\n\n parseNumber(state); // count, not used\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n parseKeyword(state); // Bitmap\n\n const [_unused, obj] = parseObject(state);\n obj.Flags = 0;\n if ('WrapWidth' in obj) {\n obj.Flags += TextureFlags.WrapWidth;\n delete obj.WrapWidth;\n }\n if ('WrapHeight' in obj) {\n obj.Flags += TextureFlags.WrapHeight;\n delete obj.WrapHeight;\n }\n\n res.push(obj);\n }\n\n strictParseSymbol(state, '}');\n\n model.Textures = res;\n}\n\nenum AnimVectorType {\n INT1,\n FLOAT1,\n FLOAT3,\n FLOAT4\n}\n\nconst animVectorSize = {\n [AnimVectorType.INT1]: 1,\n [AnimVectorType.FLOAT1]: 1,\n [AnimVectorType.FLOAT3]: 3,\n [AnimVectorType.FLOAT4]: 4\n};\n\nfunction parseAnimKeyframe (state: State, frame: number, type: AnimVectorType, lineType: LineType): AnimKeyframe {\n const res: AnimKeyframe = {\n Frame: frame,\n Vector: null\n };\n\n const Vector = type === AnimVectorType.INT1 ? Int32Array : Float32Array;\n const itemCount = animVectorSize[type];\n\n res.Vector = parseArrayOrSingleItem(state, new Vector(itemCount));\n\n strictParseSymbol(state, ',');\n\n if (lineType === LineType.Hermite || lineType === LineType.Bezier) {\n parseKeyword(state); // InTan\n res.InTan = parseArrayOrSingleItem(state, new Vector(itemCount));\n strictParseSymbol(state, ',');\n\n parseKeyword(state); // OutTan\n res.OutTan = parseArrayOrSingleItem(state, new Vector(itemCount));\n strictParseSymbol(state, ',');\n }\n\n return res;\n}\n\nfunction parseAnimVector (state: State, type: AnimVectorType): AnimVector {\n const animVector: AnimVector = {\n LineType: LineType.DontInterp,\n GlobalSeqId: null,\n Keys: []\n };\n\n parseNumber(state); // count, not used\n\n strictParseSymbol(state, '{');\n\n const lineType: string = parseKeyword(state);\n if (lineType === 'DontInterp' || lineType === 'Linear' || lineType === 'Hermite' || lineType === 'Bezier') {\n animVector.LineType = LineType[lineType];\n }\n\n strictParseSymbol(state, ',');\n\n while (state.char() !== '}') {\n const keyword = parseKeyword(state);\n\n if (keyword === 'GlobalSeqId') {\n animVector[keyword] = parseNumber(state);\n strictParseSymbol(state, ',');\n } else {\n const frame = parseNumber(state);\n\n if (frame === null) {\n throwError(state, 'expected frame number or GlobalSeqId');\n }\n\n strictParseSymbol(state, ':');\n\n animVector.Keys.push(parseAnimKeyframe(state, frame, type, animVector.LineType));\n }\n }\n\n strictParseSymbol(state, '}');\n\n return animVector;\n}\n\nfunction parseLayer (state: State, model: Model): Layer {\n const res: Layer = {\n Alpha: null,\n TVertexAnimId: null,\n Shading: 0,\n CoordId: 0\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n let keyword = parseKeyword(state);\n let isStatic = false;\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'static') {\n isStatic = true;\n keyword = parseKeyword(state);\n }\n\n if (!isStatic && (keyword === 'TextureID' || model.Version >= 1100 && keyword in LAYER_TEXTURE_NAME_MAP)) {\n res[keyword] = parseAnimVector(state, AnimVectorType.INT1);\n } else if (!isStatic && (keyword === 'Alpha')) {\n res[keyword] = parseAnimVector(state, AnimVectorType.FLOAT1);\n } else if (\n keyword === 'Unshaded' || keyword === 'SphereEnvMap' || keyword === 'TwoSided' ||\n keyword === 'Unfogged' || keyword === 'NoDepthTest' || keyword === 'NoDepthSet'\n ) {\n res.Shading |= LayerShading[keyword];\n } else if (keyword === 'FilterMode') {\n const val = parseKeyword(state);\n\n if (\n val === 'None' || val === 'Transparent' || val === 'Blend' || val === 'Additive' ||\n val === 'AddAlpha' || val === 'Modulate' || val === 'Modulate2x'\n ) {\n res.FilterMode = FilterMode[val];\n }\n } else if (keyword === 'TVertexAnimId') {\n res.TVertexAnimId = parseNumber(state);\n } else if (model.Version >= 900 && keyword === 'EmissiveGain') {\n if (isStatic) {\n res[keyword] = parseNumber(state);\n } else {\n res[keyword] = parseAnimVector(state, AnimVectorType.FLOAT1);\n }\n } else if (model.Version >= 1000 && keyword === 'FresnelColor') {\n if (isStatic) {\n const array = new Float32Array(3);\n res[keyword] = parseArray(state, array, 0) as Float32Array;\n } else {\n res[keyword] = parseAnimVector(state, AnimVectorType.FLOAT3);\n }\n } else if (model.Version >= 1000 && (keyword === 'FresnelOpacity' || keyword === 'FresnelTeamColor')) {\n if (isStatic) {\n res[keyword] = parseNumber(state);\n } else {\n res[keyword] = parseAnimVector(state, AnimVectorType.FLOAT1);\n }\n } else {\n let val: string|number = parseNumber(state);\n\n if (val === null) {\n val = parseKeyword(state);\n }\n\n res[keyword] = val;\n }\n\n parseSymbol(state, ',');\n\n parseComment(state);\n parseSpace(state);\n }\n\n strictParseSymbol(state, '}');\n\n return res;\n}\n\nfunction parseMaterials (state: State, model: Model): void {\n const res = [];\n\n parseNumber(state); // count, not used\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const obj = {\n RenderMode: 0,\n Layers: []\n };\n\n parseKeyword(state); // Material\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const keyword = parseKeyword(state);\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'Layer') {\n obj.Layers.push(parseLayer(state, model));\n } else if (keyword === 'PriorityPlane' || keyword === 'RenderMode') {\n obj[keyword] = parseNumber(state);\n } else if (keyword === 'ConstantColor' || keyword === 'SortPrimsFarZ' || keyword === 'FullResolution') {\n obj.RenderMode |= MaterialRenderMode[keyword];\n } else if (model.Version >= 900 && model.Version <= 1100 && keyword === 'Shader') {\n obj[keyword] = parseString(state);\n } else {\n throw new Error('Unknown material property ' + keyword);\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n res.push(obj);\n }\n\n strictParseSymbol(state, '}');\n\n model.Materials = res;\n}\n\nenum GeosetPartType {\n INT,\n FLOAT\n}\n\nfunction parseGeosetPart (state: State, countPerObj: number, type: GeosetPartType) {\n const count = parseNumber(state);\n const arr = new (type === GeosetPartType.FLOAT ? Float32Array : Uint8Array)(count * countPerObj);\n\n strictParseSymbol(state, '{');\n\n for (let index = 0; index < count; ++index) {\n parseArray(state, arr, index * countPerObj);\n strictParseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n return arr;\n}\n\nfunction parseGeoset (state: State, model: Model): void {\n const res: Geoset = {\n Vertices: null,\n Normals: null,\n TVertices: [],\n VertexGroup: new Uint8Array(0),\n Faces: null,\n Groups: null,\n TotalGroupsCount: null,\n MinimumExtent: null,\n MaximumExtent: null,\n BoundsRadius: 0,\n Anims: [],\n MaterialID: null,\n SelectionGroup: null,\n Unselectable: false\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const keyword = parseKeyword(state);\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'Vertices' || keyword === 'Normals' || keyword === 'TVertices') {\n let countPerObj = 3;\n\n if (keyword === 'TVertices') {\n countPerObj = 2;\n }\n\n const arr = parseGeosetPart(state, countPerObj, GeosetPartType.FLOAT) as Float32Array;\n\n if (keyword === 'TVertices') {\n res.TVertices.push(arr);\n } else {\n res[keyword] = arr;\n }\n } else if (keyword === 'VertexGroup') {\n res[keyword] = new Uint8Array(res.Vertices.length / 3);\n\n parseArray(state, res[keyword], 0);\n } else if (keyword === 'Faces') {\n const groupCount = parseNumber(state); // group count, always 1 in the official models\n const indexCount = parseNumber(state);\n\n let pos = 0;\n res.Faces = new Uint16Array(indexCount);\n\n strictParseSymbol(state, '{');\n const keyword = parseKeyword(state);\n if (keyword !== 'Triangles') {\n throwError(state, 'unexpected faces type');\n }\n strictParseSymbol(state, '{');\n for (let g = 0; g < groupCount; ++g) {\n const count = parseArrayCounted(state, res.Faces, pos);\n if (!count) {\n throwError(state, 'expected array');\n }\n pos += count;\n\n parseSymbol(state, ',');\n }\n\n if (pos !== indexCount || indexCount % 3 !== 0) {\n throwError(state, 'mismatched faces array');\n }\n\n strictParseSymbol(state, '}');\n strictParseSymbol(state, '}');\n } else if (keyword === 'Groups') {\n const groups = [];\n parseNumber(state); // groups count, unused\n res.TotalGroupsCount = parseNumber(state); // summed in subarrays\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n parseKeyword(state); // Matrices\n\n groups.push(parseArray(state));\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n res.Groups = groups;\n } else if (keyword === 'MinimumExtent' || keyword === 'MaximumExtent') {\n const arr = new Float32Array(3);\n res[keyword] = parseArray(state, arr, 0) as Float32Array;\n strictParseSymbol(state, ',');\n } else if (keyword === 'BoundsRadius' || keyword === 'MaterialID' || keyword === 'SelectionGroup') {\n res[keyword] = parseNumber(state);\n strictParseSymbol(state, ',');\n } else if (keyword === 'Anim') {\n const [_unused, obj] = parseObject(state);\n\n if (obj.Alpha === undefined) {\n obj.Alpha = 1;\n }\n\n res.Anims.push(obj as unknown as GeosetAnimInfo);\n } else if (keyword === 'Unselectable') {\n res.Unselectable = true;\n strictParseSymbol(state, ',');\n } else if (model.Version >= 900) {\n if (keyword === 'LevelOfDetail') {\n res.LevelOfDetail = parseNumber(state);\n strictParseSymbol(state, ',');\n } else if (keyword === 'Name') {\n res.Name = parseString(state);\n strictParseSymbol(state, ',');\n } else if (keyword === 'Tangents') {\n res.Tangents = parseGeosetPart(state, 4, GeosetPartType.FLOAT) as Float32Array;\n } else if (keyword === 'SkinWeights') {\n res.SkinWeights = parseGeosetPart(state, 8, GeosetPartType.INT) as Uint8Array;\n }\n }\n }\n\n strictParseSymbol(state, '}');\n\n model.Geosets.push(res);\n}\n\nfunction parseGeosetAnim (state: State, model: Model): void {\n const res: GeosetAnim = {\n GeosetId: -1,\n Alpha: 1,\n Color: null,\n Flags: 0\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n let keyword = parseKeyword(state);\n let isStatic = false;\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'static') {\n isStatic = true;\n keyword = parseKeyword(state);\n }\n\n if (keyword === 'Alpha') {\n if (isStatic) {\n res.Alpha = parseNumber(state);\n } else {\n res.Alpha = parseAnimVector(state, AnimVectorType.FLOAT1);\n }\n } else if (keyword === 'Color') {\n if (isStatic) {\n const array = new Float32Array(3);\n res.Color = parseArray(state, array, 0) as Float32Array;\n res.Color.reverse();\n } else {\n res.Color = parseAnimVector(state, AnimVectorType.FLOAT3);\n for (const key of res.Color.Keys) {\n key.Vector.reverse();\n if (key.InTan) {\n key.InTan.reverse();\n key.OutTan.reverse();\n }\n }\n }\n } else if (keyword === 'DropShadow') {\n res.Flags |= GeosetAnimFlags[keyword];\n } else {\n res[keyword] = parseNumber(state);\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n model.GeosetAnims.push(res);\n}\n\nfunction parseNode (state: State, type: string, model: Model): Node {\n const name = parseString(state);\n\n const node: Node = {\n Name: name,\n ObjectId: null,\n Parent: null,\n PivotPoint: null,\n Flags: NodeType[type]\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const keyword = parseKeyword(state);\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'Translation' || keyword === 'Rotation' || keyword === 'Scaling' || keyword === 'Visibility') {\n let vectorType: AnimVectorType = AnimVectorType.FLOAT3;\n if (keyword === 'Rotation') {\n vectorType = AnimVectorType.FLOAT4;\n } else if (keyword === 'Visibility') {\n vectorType = AnimVectorType.FLOAT1;\n }\n node[keyword] = parseAnimVector(state, vectorType);\n } else if (keyword === 'BillboardedLockZ' || keyword === 'BillboardedLockY' || keyword === 'BillboardedLockX' ||\n keyword === 'Billboarded' || keyword === 'CameraAnchored') {\n node.Flags |= NodeFlags[keyword];\n } else if (keyword === 'DontInherit') {\n strictParseSymbol(state, '{');\n\n const val = parseKeyword(state);\n\n if (val === 'Translation') {\n node.Flags |= NodeFlags.DontInheritTranslation;\n } else if (val === 'Rotation') {\n node.Flags |= NodeFlags.DontInheritRotation;\n } else if (val === 'Scaling') {\n node.Flags |= NodeFlags.DontInheritScaling;\n }\n\n strictParseSymbol(state, '}');\n } else if (keyword === 'Path') {\n node[keyword] = parseString(state);\n } else {\n let val = parseKeyword(state) || parseNumber(state);\n\n if (keyword === 'GeosetId' && val === 'Multiple' ||\n keyword === 'GeosetAnimId' && val === 'None') {\n val = null;\n }\n\n node[keyword] = val;\n }\n\n parseSymbol(state, ',');\n parseComment(state);\n parseSpace(state);\n }\n\n strictParseSymbol(state, '}');\n\n model.Nodes[node.ObjectId] = node;\n\n return node;\n}\n\nfunction parseBone (state: State, model: Model): void {\n const node = parseNode(state, 'Bone', model);\n\n model.Bones.push(node);\n}\n\nfunction parseHelper (state: State, model: Model): void {\n const node = parseNode(state, 'Helper', model);\n\n model.Helpers.push(node);\n}\n\nfunction parseAttachment (state: State, model: Model): void {\n const node = parseNode(state, 'Attachment', model);\n\n model.Attachments.push(node);\n}\n\nfunction parsePivotPoints (state: State, model: Model): void {\n const count = parseNumber(state);\n\n const res = [];\n\n strictParseSymbol(state, '{');\n\n for (let i = 0; i < count; ++i) {\n res.push(parseArray(state, new Float32Array(3), 0));\n strictParseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n model.PivotPoints = res;\n}\n\nfunction parseEventObject (state: State, model: Model): void {\n const name = parseString(state);\n\n const res: EventObject = {\n Name: name,\n ObjectId: null,\n Parent: null,\n PivotPoint: null,\n EventTrack: null,\n Flags: NodeType.EventObject\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const keyword = parseKeyword(state);\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'EventTrack') {\n const count = parseNumber(state); // EventTrack count\n\n res.EventTrack = parseArray(state, new Uint32Array(count), 0) as Uint32Array;\n } else if (keyword === 'Translation' || keyword === 'Rotation' || keyword === 'Scaling') {\n const type: AnimVectorType = keyword === 'Rotation' ? AnimVectorType.FLOAT4 : AnimVectorType.FLOAT3;\n\n res[keyword] = parseAnimVector(state, type);\n } else {\n res[keyword] = parseNumber(state);\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n model.EventObjects.push(res);\n model.Nodes[res.ObjectId] = res;\n}\n\nfunction parseCollisionShape (state: State, model: Model): void {\n const name = parseString(state);\n\n const res: CollisionShape = {\n Name: name,\n ObjectId: null,\n Parent: null,\n PivotPoint: null,\n Shape: CollisionShapeType.Box,\n Vertices: null,\n Flags: NodeType.CollisionShape\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n const keyword = parseKeyword(state);\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'Sphere') {\n res.Shape = CollisionShapeType.Sphere;\n } else if (keyword === 'Box') {\n res.Shape = CollisionShapeType.Box;\n } else if (keyword === 'Vertices') {\n const count = parseNumber(state);\n const vertices = new Float32Array(count * 3);\n\n strictParseSymbol(state, '{');\n\n for (let i = 0; i < count; ++i) {\n parseArray(state, vertices, i * 3);\n strictParseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n res.Vertices = vertices;\n } else if (keyword === 'Translation' || keyword === 'Rotation' || keyword === 'Scaling') {\n const type: AnimVectorType = keyword === 'Rotation' ? AnimVectorType.FLOAT4 : AnimVectorType.FLOAT3;\n res[keyword] = parseAnimVector(state, type);\n } else {\n res[keyword] = parseNumber(state);\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n model.CollisionShapes.push(res);\n model.Nodes[res.ObjectId] = res;\n}\n\nfunction parseGlobalSequences (state: State, model: Model): void {\n const res = [];\n\n const count = parseNumber(state);\n\n strictParseSymbol(state, '{');\n\n for (let i = 0; i < count; ++i) {\n const keyword = parseKeyword(state);\n\n if (keyword === 'Duration') {\n res.push(parseNumber(state));\n }\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n model.GlobalSequences = res;\n}\n\nfunction parseUnknownBlock (state: State): void {\n let opened;\n while (state.char() !== undefined && state.char() !== '{') {\n ++state.pos;\n }\n opened = 1;\n ++state.pos;\n\n while (state.char() !== undefined && opened > 0) {\n if (state.char() === '{') {\n ++opened;\n } else if (state.char() === '}') {\n --opened;\n }\n ++state.pos;\n }\n parseSpace(state);\n}\n\nfunction parseParticleEmitter (state: State, model: Model): void {\n const res: ParticleEmitter = {\n ObjectId: null,\n Parent: null,\n Name: null,\n Flags: 0\n } as ParticleEmitter;\n\n res.Name = parseString(state);\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n let keyword = parseKeyword(state);\n let isStatic = false;\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'static') {\n isStatic = true;\n keyword = parseKeyword(state);\n }\n\n if (keyword === 'ObjectId' || keyword === 'Parent') {\n res[keyword] = parseNumber(state);\n } else if (keyword === 'EmitterUsesMDL' || keyword === 'EmitterUsesTGA') {\n res.Flags |= ParticleEmitterFlags[keyword];\n } else if (!isStatic && (keyword === 'Visibility' || keyword === 'Translation' || keyword === 'Rotation' ||\n keyword === 'Scaling' || keyword === 'EmissionRate' || keyword === 'Gravity' || keyword === 'Longitude' ||\n keyword === 'Latitude')) {\n let type: AnimVectorType = AnimVectorType.FLOAT3;\n if (keyword === 'Visibility' || keyword === 'EmissionRate' || keyword === 'Gravity' ||\n keyword === 'Longitude' || keyword === 'Latitude') {\n type = AnimVectorType.FLOAT1;\n } else if (keyword === 'Rotation') {\n type = AnimVectorType.FLOAT4;\n }\n res[keyword] = parseAnimVector(state, type);\n } else if (keyword === 'Particle') {\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n let keyword2 = parseKeyword(state);\n let isStatic2 = false;\n\n if (keyword2 === 'static') {\n isStatic2 = true;\n keyword2 = parseKeyword(state);\n }\n\n if (!isStatic2 && (keyword2 === 'LifeSpan' || keyword2 === 'InitVelocity')) {\n res[keyword2] = parseAnimVector(state, AnimVectorType.FLOAT1);\n } else if (keyword2 === 'LifeSpan' || keyword2 === 'InitVelocity') {\n res[keyword2] = parseNumber(state);\n } else if (keyword2 === 'Path') {\n res.Path = parseString(state);\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n } else {\n res[keyword] = parseNumber(state);\n }\n\n parseSymbol(state, ',');\n }\n\n strictParseSymbol(state, '}');\n\n model.ParticleEmitters.push(res);\n}\n\nfunction parseParticleEmitter2 (state: State, model: Model): void {\n const name = parseString(state);\n\n const res: ParticleEmitter2 = {\n Name: name,\n ObjectId: null,\n Parent: null,\n PivotPoint: null,\n Flags: NodeType.ParticleEmitter,\n FrameFlags: 0\n };\n\n strictParseSymbol(state, '{');\n\n while (state.char() !== '}') {\n let keyword = parseKeyword(state);\n let isStatic = false;\n\n if (!keyword) {\n throwError(state);\n }\n\n if (keyword === 'static') {\n isStatic = true;\n keyword = parseKeyword(state);\n }\n\n if (!isStatic && (keyword === 'Speed' || keyword === 'Latitude' || keyword === 'Visibility' ||\n keyword === 'EmissionRate' || keyword === 'Width' || keyword === 'Length' || keyword === 'Translation' ||\n keyword === 'Rotation' || keyword === 'Scaling' || keyword === 'Gravity' || keyword === 'Variation')) {\n let type: AnimVectorType = AnimVectorType.FLOAT3;\n switch (keyword) {\n case 'Rotation':\n type = AnimVectorType.FLOAT4;\n break;\n case 'Speed':\n case 'Latitude':\n case 'Visibility':\n case 'EmissionRate':\n case 'Width':\n case 'Length':\n case 'Gravity':\n case 'Variation':\n type = AnimVectorType.FLOAT1;\n break;\n }\n res[keyword] = parseAnimVector(state, type);\n } else if (keyword === 'Variation' || keyword === 'Gravity' || keyword === 'ReplaceableId' || keyword === 'PriorityPlane') {\n res[keyword] = parseNumber(state);\n } else if (keyword === 'SortPrimsFarZ' || keyword === 'Unshaded' || keyword === 'LineEmitter' ||\n keyword === 'Unfogged' || keyword === 'ModelSpace' || keyword === 'XYQuad') {\n res.Flags |= ParticleEmitter2Flags[keyword];\n } else if (keyword === 'Both') {\n res.FrameFlags |= ParticleEmitter2FramesFlags.Head | ParticleEmitter2FramesFlags.Tail;\n } else if (keyword === 'Head' || keyword === 'Tail') {\n res.FrameFlags |= ParticleEmitter2FramesFlags[keyword];\n } else if (keyword === 'Squirt') {\n res[keyword] = true;\n } else if (keyword === 'DontInherit') {\n strictParseSymbol(state, '{');\n\n const val = parseKeyword(state);\n\n if (val === 'Translation') {\n res.Flags |= NodeFlags.DontInheritTranslation;\n } else if (val === 'Rotation') {\n res.Flags |= NodeFlags.DontInheritRotation;\n } else if (val === 'Scaling') {\n res.Flags |= NodeFlags.DontInheritScaling;\n }\n\n strictParseSymbol(state, '}');\n } else if (keyword === 'SegmentColor') {\n const colors = [];\n\n strictParseSymbol(state, '{');\n while (state.char() !== '}') {\n parseKeyword(state); // Color\n\n const colorArr = new Float32Array(3);\n parseArray(state, colorArr, 0);\n\n // bgr order, inverse from mdx\n const temp = colorArr[0];\n colorArr[0] = colorArr[2];\n colorArr[2] = temp;\n colors.push(colorArr);\n\n parseSymbol(state, ',');\n }\n strictParseSymbol(state, '}');\n\n res.SegmentColor = colors;\n } else if (keyword === 'Alpha') {\n res.Alpha = new Uint8Array(3);\n parseArray(state, res.Alpha, 0);\n } else if (keyword === 'ParticleScaling') {\n res[keyword] = new Float32Array(3);\n parseArray(state, res[keyword], 0);\n } else if (keyword === 'LifeSpanUVAnim' || keyword === 'DecayUVAnim' || keyword === 'TailUVAnim' ||\n keyword === 'TailDecayUVAnim') {\n res[keyword] = new Uint32Array(3);\n parseArray(state, res[keyword], 0);\n } else if (keyword =