import unique from '../../../common/arrayunique' ; import { basename } from '../../../common/path' ; import { Animation , InterpolationType } from '../../../parsers/mdlx/animations' ; import AnimatedObject from '../../../parsers/mdlx/animatedobject' ; import GenericObject from '../../../parsers/mdlx/genericobject' ; import Extent from '../../../parsers/mdlx/extent' ; import Sequence from '../../../parsers/mdlx/sequence' ; import Texture from '../../../parsers/mdlx/texture' ; import Material from '../../../parsers/mdlx/material' ; import Layer from '../../../parsers/mdlx/layer' ; import TextureAnimation from '../../../parsers/mdlx/textureanimation' ; import Geoset from '../../../parsers/mdlx/geoset' ; import GeosetAnimation from '../../../parsers/mdlx/geosetanimation' ; import Bone from '../../../parsers/mdlx/bone' ; import Light from '../../../parsers/mdlx/light' ; import Helper from '../../../parsers/mdlx/helper' ; import Attachment from '../../../parsers/mdlx/attachment' ; import ParticleEmitter from '../../../parsers/mdlx/particleemitter' ; import ParticleEmitter2 from '../../../parsers/mdlx/particleemitter2' ; import ParticleEmitterPopcorn from '../../../parsers/mdlx/particleemitterpopcorn' ; import RibbonEmitter from '../../../parsers/mdlx/ribbonemitter' ; import EventObject from '../../../parsers/mdlx/eventobject' ; import Camera from '../../../parsers/mdlx/camera' ; import CollisionShape from '../../../parsers/mdlx/collisionshape' ; import FaceEffect from '../../../parsers/mdlx/faceeffect' ; import SanityTestData from './data' ; import testTracks from './tracks' ; import { SanityTestNode } from './data' ; export function isBetween ( x , minVal , maxVal ) { return x >= minVal && x <= maxVal ; } export const sequenceNames = new Set ( [ 'attack' , 'birth' , 'cinematic' , 'death' , 'decay' , 'dissipate' , 'morph' , 'portrait' , 'sleep' , 'spell' , 'stand' , 'walk' , 'ready' , ] ) ; export const replaceableIds = new Set ( [ 1 , 2 , 11 , 21 , 31 , 32 , 33 , 34 , 35 , 36 , 37 , ] ) ; export const animatedTypeNames = new Map ( [ [ 'KMTF' , 'Texture ID' ] , [ 'KMTA' , 'Alpha' ] , [ 'KMTE' , 'Emissive Gain' ] , [ 'KFC3' , 'Fresnel Color' ] , [ 'KFCA' , 'Fresnel Opacity' ] , [ 'KFTC' , 'Fresnel Team Color' ] , [ 'KTAT' , 'Translation' ] , [ 'KTAR' , 'Rotation' ] , [ 'KTAS' , 'Scaling' ] , [ 'KGAO' , 'Alpha' ] , [ 'KGAC' , 'Color' ] , [ 'KGTR' , 'Translation' ] , [ 'KGRT' , 'Rotation' ] , [ 'KGSC' , 'Scaling' ] , [ 'KLAS' , 'Attenuation Start' ] , [ 'KLAE' , 'Attenuation End' ] , [ 'KLAC' , 'Color' ] , [ 'KLAI' , 'Intensity' ] , [ 'KLBI' , 'Ambient Intensity' ] , [ 'KLBC' , 'Ambient Color' ] , [ 'KLAV' , 'Visibility' ] , [ 'KATV' , 'Visibility' ] , [ 'KPEE' , 'Emission Rate' ] , [ 'KPEG' , 'Gravity' ] , [ 'KPLN' , 'Longitude' ] , [ 'KPLT' , 'Latitude' ] , [ 'KPEL' , 'Lifespan' ] , [ 'KPES' , 'Speed' ] , [ 'KPEV' , 'Visibility' ] , [ 'KP2E' , 'Emission Rate' ] , [ 'KP2G' , 'Gravity' ] , [ 'KP2L' , 'Latitude' ] , [ 'KP2R' , 'Variation' ] , [ 'KP2N' , 'Length' ] , [ 'KP2W' , 'Width' ] , [ 'KP2S' , 'Speed' ] , [ 'KP2V' , 'Visibility' ] , [ 'KPPA' , 'Alpha' ] , [ 'KPPC' , 'Color' ] , [ 'KPPE' , 'EmissionRate' ] , [ 'KPPL' , 'LifeSpan' ] , [ 'KPPS' , 'Speed' ] , [ 'KPPV' , 'Visibility' ] , [ 'KRHA' , 'Height Above' ] , [ 'KRHB' , 'Height Below' ] , [ 'KRAL' , 'Alpha' ] , [ 'KRCO' , 'Color' ] , [ 'KRTX' , 'Texture Slot' ] , [ 'KRVS' , 'Visibility' ] , [ 'KCTR' , 'Translation' ] , [ 'KTTR' , 'Rotation' ] , [ 'KCRL' , 'Target Translation' ] , ] ) ; export function hasAnimation ( object , name ) { for ( const animation of object . animations ) { if ( animation . name === name ) { return true ; } } return false ; } export type MdlxType = Extent | Sequence | number | Texture | Material | Layer | TextureAnimation | Geoset | GeosetAnimation | Bone | Light | Helper | Attachment | ParticleEmitter | ParticleEmitter2 | ParticleEmitterPopcorn | RibbonEmitter | EventObject | Camera | CollisionShape | FaceEffect | Animation ; export function getObjectTypeName ( object ) { if ( object instanceof Extent ) { return 'Extent' ; } else if ( object instanceof Sequence ) { return 'Sequence' ; } else if ( typeof object === 'number' ) { return 'GlobalSequence' ; } else if ( object instanceof Texture ) { return 'Texture' ; } else if ( object instanceof Material ) { return 'Material' ; } else if ( object instanceof Layer ) { return 'Layer' ; } else if ( object instanceof TextureAnimation ) { return 'TextureAnimation' ; } else if ( object instanceof Geoset ) { return 'Geoset' ; } else if ( object instanceof GeosetAnimation ) { return 'GeosetAnimation' ; } else if ( object instanceof Bone ) { return 'Bone' ; } else if ( object instanceof Light ) { return 'Light' ; } else if ( object instanceof Helper ) { return 'Helper' ; } else if ( object instanceof Attachment ) { return 'Attachment' ; } else if ( object instanceof ParticleEmitter ) { return 'ParticleEmitter' ; } else if ( object instanceof ParticleEmitter2 ) { return 'ParticleEmitter2' ; } else if ( object instanceof ParticleEmitterPopcorn ) { return 'ParticleEmitterPopcorn' ; } else if ( object instanceof RibbonEmitter ) { return 'RibbonEmitter' ; } else if ( object instanceof EventObject ) { return 'EventObject' ; } else if ( object instanceof Camera ) { return 'Camera' ; } else if ( object instanceof CollisionShape ) { return 'CollisionShape' ; } else if ( object instanceof FaceEffect ) { return 'FaceEffect' ; } else if ( object instanceof Animation ) { return < string > animatedTypeNames . get ( object . name ) ; } else { console . warn ( 'Unknown object type' , object ) ; return 'Unknown' ; } } export function getObjectName ( object , index ) { let name = getObjectTypeName ( object ) ; if ( ! ( object instanceof Animation ) && ! ( object instanceof Extent ) ) { name += ` ${ index }` ; } if ( object instanceof Sequence || object instanceof GenericObject || object instanceof Camera ) { name += ` - "${ object . name }"` ; } if ( object instanceof Texture || object instanceof FaceEffect ) { if ( object . path . length ) { name += ` - "${ basename ( object . path ) }"` ; } if ( object instanceof Texture ) { if ( object . replaceableId === 1 ) { name += ' - Team color' ; } else if ( object . replaceableId === 2 ) { name += ' - Team glow' ; } else if ( object . replaceableId > 0 ) { name += ` - Replaceable ID ${ object . replaceableId }` ; } } } return name ; } export function testObjects < T extends MdlxType > ( data , objects , handler ? ) { const l = objects . length ; if ( l ) { const isAnimated = objects [ 0 ] instanceof AnimatedObject ; const isGeneric = objects [ 0 ] instanceof GenericObject ; for ( let i = 0 ; i < l ; i ++ ) { const object = objects [ i ] ; data . push ( object , i ) ; if ( handler ) { handler ( data , object , i ) ; } if ( isAnimated ) { const asAnimated = < AnimatedObject > object ; testObjects ( data , asAnimated . animations , testAnimation ) ; } if ( isGeneric ) { const asGeneric = < GenericObject > object ; const objectId = asGeneric . objectId ; const parentId = asGeneric . parentId ; data . assertError ( parentId === - 1 || hasGenericObject ( data , parentId ) , `Invalid parent ${ parentId }` ) ; data . assertError ( objectId !== parentId , 'Same object and parent' ) ; } data . pop ( ) ; } } } export function testAndGetReference < T extends MdlxType > ( data , objects , index , typeNameIfError ) { if ( index >= 0 && index < objects . length ) { data . addReference ( objects [ index ] ) ; return objects [ index ] ; } else { data . addError ( `Invalid ${ typeNameIfError } ${ index }` ) ; return ; } } export function testReference < T extends MdlxType > ( data , objects , index , typeNameIfError ) { return testAndGetReference ( data , objects , index , typeNameIfError ) !== undefined ; } /**
 * Get all of the texture indices referenced by a layer.
 */ export function getTextureIds ( layer ) { for ( const animation of layer . animations ) { if ( animation . name === 'KMTF' ) { return unique ( animation . values . map ( ( value ) => value [ 0 ] ) ) ; } } return [ layer . textureId ] ; } function testVertexSkinning ( data , vertex , bone , isHd ) { const object = data . objects [ bone ] ; if ( isHd ) { data . assertError ( isBetween ( bone , 0 , 255 ) , `Vertex ${ vertex }: References bone ${ bone } but there can only be 256 bones in an HD model` ) ; } if ( object ) { if ( ! ( object instanceof Bone ) ) { data . addSevere ( `Vertex ${ vertex }: Attached to "${ object . name }" which is not a bone` ) ; } else { const uses = data . boneUsageMap . get ( bone ) || 0 ; data . boneUsageMap . set ( bone , uses + 1 ) ; } } else { data . addError ( `Vertex ${ vertex }: Attached to object ${ bone } which does not exist` ) ; } } /**
 * Test geoset skinning.
 */ export function testGeosetSkinning ( data , geoset ) { if ( data . model . version > 800 && geoset . skin . length ) { data . assertWarning ( geoset . vertexGroups . length === 0 , 'This geoset has both skin/weights and vertex groups' ) ; const skin = geoset . skin ; for ( let i = 0 , l = skin . length / 8 ; i < l ; i ++ ) { const offset = i * 8 ; const bone0 = skin [ offset ] ; const bone1 = skin [ offset + 1 ] ; const bone2 = skin [ offset + 2 ] ; const bone3 = skin [ offset + 3 ] ; const weight0 = skin [ offset + 4 ] ; const weight1 = skin [ offset + 5 ] ; const weight2 = skin [ offset + 6 ] ; const weight3 = skin [ offset + 7 ] ; if ( weight0 > 0 ) { testVertexSkinning ( data , i , bone0 , true ) ; } if ( weight1 > 0 ) { testVertexSkinning ( data , i , bone1 , true ) ; } if ( weight2 > 0 ) { testVertexSkinning ( data , i , bone2 , true ) ; } if ( weight3 > 0 ) { testVertexSkinning ( data , i , bone3 , true ) ; } const weight = weight0 + weight1 + weight2 + weight3 ; if ( weight === 0 ) { data . addSevere ( `Vertex ${ i }: Not attached to anything` ) ; } else if ( weight !== 255 ) { data . addSevere ( `Vertex ${ i }: The weights are not normalized to 1` ) ; } } } else { if ( data . model . bones . length ) { const vertexGroups = geoset . vertexGroups ; const matrixGroups = geoset . matrixGroups ; const matrixIndices = geoset . matrixIndices ; const slices = [ ] ; for ( let i = 0 , l = matrixGroups . length , k = 0 ; i < l ; i ++ ) { slices . push ( matrixIndices . subarray ( k , k + matrixGroups [ i ] ) ) ; k += matrixGroups [ i ] ; } for ( let i = 0 , l = vertexGroups . length ; i < l ; i ++ ) { const slice = slices [ vertexGroups [ i ] ] ; if ( slice ) { for ( const bone of slice ) { testVertexSkinning ( data , i , bone , false ) ; } } else { const vertexGroup = vertexGroups [ i ] ; if ( vertexGroup === 255 ) { data . addSevere ( `Vertex ${ i }: Not attached to anything` ) ; } else { data . addSevere ( `Vertex ${ i }: Attached to vertex group ${ vertexGroup } which does not exist` ) ; } } } } } } /**
 * Is the given ID a valid generic object?
 */ function hasGenericObject ( data , id ) { for ( const object of data . objects ) { if ( object . objectId === id ) { return true ; } } return false ; } export function testAnimation ( data , animation ) { const name = animation . name ; const interpolationType = animation . interpolationType ; data . assertWarning ( name !== 'KP2R' , 'Using a variation animation.' ) ; data . assertWarning ( name !== 'KP2G' , 'Using a gravity animation.' ) ; data . assertWarning ( animatedTypeNames . get ( name ) !== 'Visibility' || interpolationType === InterpolationType . DontInterp , 'Interpolation type not set to None' ) ; testTracks ( data , animation ) ; } export function cleanNode ( node ) { const nodes = node . nodes ; for ( let i = nodes . length - 1 ; i >= 0 ; i -- ) { const child = nodes [ i ] ; if ( child . type === 'node' ) { if ( child . errors || child . severe || child . warnings || child . unused || ( child . uses !== undefined && ! child . uses ) ) { cleanNode ( child ) ; } else { nodes . splice ( i , 1 ) ; } } } } export function getAnimation ( object , name ) { for ( const animation of object . animations ) { if ( animation . name === name ) { return animation ; } } return ; } export function testExtent ( data , extent ) { data . push ( extent , 0 ) ; const { max , min } = extent ; if ( ( max [ 0 ] - min [ 0 ] < 0 ) || ( max [ 1 ] - min [ 1 ] < 0 ) || ( max [ 2 ] - min [ 2 ] < 0 ) ) { data . addWarning ( 'Negative extents' ) ; } data . pop ( ) ; }