2020-09-29 22:58:25 -03:00
const zlib = require ( 'zlib' )
2021-01-14 20:18:19 -03:00
const blocks = require ( '../misc/analysis/blocks.json' )
const colorStuff = require ( '../misc/analysis/colorProperties.json' )
const init = require ( '../misc/analysis/initialProperties.json' )
const properties = require ( '../misc/analysis/objectProperties.json' )
const ids = require ( '../misc/analysis/objects.json' )
2019-10-16 19:47:53 -03:00
module . exports = async ( app , req , res , level ) => {
2020-10-01 10:09:08 -03:00
let unencrypted = level . data . startsWith ( 'kS' ) // some gdps'es don't encrypt level data
let levelString = unencrypted ? level . data : Buffer . from ( level . data , 'base64' )
if ( unencrypted ) {
const raw _data = level . data ;
const response _data = analyze _level ( level , raw _data ) ;
return res . send ( response _data ) ;
} else {
zlib . unzip ( levelString , ( err , buffer ) => {
2021-03-10 01:14:06 -03:00
if ( err ) { return res . send ( "-2" ) ; }
2020-10-01 10:09:08 -03:00
const raw _data = buffer . toString ( ) ;
const response _data = analyze _level ( level , raw _data ) ;
return res . send ( response _data ) ;
} ) ;
}
}
function sortObj ( obj , sortBy ) {
var sorted = { }
var keys = ! sortBy ? Object . keys ( obj ) . sort ( ( a , b ) => obj [ b ] - obj [ a ] ) : Object . keys ( obj ) . sort ( ( a , b ) => obj [ b ] [ sortBy ] - obj [ a ] [ sortBy ] )
keys . forEach ( x => { sorted [ x ] = obj [ x ] } )
return sorted
}
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
function parse _obj ( obj , splitter , name _arr , valid _only ) {
const s _obj = obj . split ( splitter ) ;
let robtop _obj = { } ;
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
// semi-useless optimization depending on where at node js you're at
for ( let i = 0 , obj _l = s _obj . length ; i < obj _l ; i += 2 ) {
let k _name = s _obj [ i ] ;
if ( s _obj [ i ] in name _arr ) {
if ( ! valid _only ) {
k _name = name _arr [ s _obj [ i ] ] ;
}
robtop _obj [ k _name ] = s _obj [ i + 1 ] ;
}
}
return robtop _obj ;
}
function analyze _level ( level , rawData ) {
let response = { } ;
2020-09-30 12:25:51 -03:00
let blockCounts = { }
let miscCounts = { }
let triggerGroups = [ ]
let highDetail = 0
2021-05-11 10:22:55 -04:00
let alphaTriggers = [ ]
2020-09-30 12:25:51 -03:00
2020-10-01 10:09:08 -03:00
let misc _objects = { } ;
let block _ids = { } ;
for ( const [ name , object _ids ] of Object . entries ( ids . misc ) ) {
const copied _ids = object _ids . slice ( 1 ) ;
// funny enough, shift effects the original id list
copied _ids . forEach ( ( object _id ) => {
misc _objects [ object _id ] = name ;
} ) ;
}
2020-09-30 12:25:51 -03:00
2020-10-01 10:09:08 -03:00
for ( const [ name , object _ids ] of Object . entries ( blocks ) ) {
object _ids . forEach ( ( object _id ) => {
block _ids [ object _id ] = name ;
} ) ;
2020-09-30 12:25:51 -03:00
}
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
const data = rawData . split ( ";" ) ;
const header = data . shift ( ) ;
let level _portals = [ ] ;
2021-04-02 11:32:10 -03:00
let level _coins = [ ] ;
2020-10-01 10:09:08 -03:00
let level _text = [ ] ;
let orb _array = { } ;
let trigger _array = { } ;
let last = 0 ;
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
const obj _length = data . length ;
for ( let i = 0 ; i < obj _length ; ++ i ) {
obj = parse _obj ( data [ i ] , ',' , properties ) ;
2019-10-15 23:42:47 -03:00
2020-09-30 12:25:51 -03:00
let id = obj . id
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
if ( id in ids . portals ) {
obj . portal = ids . portals [ id ] ;
level _portals . push ( obj ) ;
2021-04-02 11:32:10 -03:00
} else if ( id in ids . coins ) {
obj . coin = ids . coins [ id ] ;
level _coins . push ( obj ) ;
2020-10-01 10:09:08 -03:00
} else if ( id in ids . orbs ) {
obj . orb = ids . orbs [ id ] ;
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
if ( obj . orb in orb _array ) {
orb _array [ obj . orb ] ++ ;
} else {
orb _array [ obj . orb ] = 1 ;
2020-09-30 12:25:51 -03:00
}
2020-10-01 10:09:08 -03:00
} else if ( id in ids . triggers ) {
obj . trigger = ids . triggers [ id ] ;
2019-11-21 10:45:56 -03:00
2020-10-01 10:09:08 -03:00
if ( obj . trigger in trigger _array ) {
trigger _array [ obj . trigger ] ++ ;
} else {
trigger _array [ obj . trigger ] = 1 ;
2020-09-30 12:25:51 -03:00
}
2020-10-01 10:09:08 -03:00
}
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
if ( obj . message ) {
level _text . push ( obj )
}
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
if ( obj . triggerGroups ) obj . triggerGroups . split ( '.' ) . forEach ( x => triggerGroups . push ( x ) )
if ( obj . highDetail == 1 ) highDetail += 1
if ( id in misc _objects ) {
const name = misc _objects [ id ] ;
if ( name in miscCounts ) {
miscCounts [ name ] [ 0 ] += 1 ;
} else {
miscCounts [ name ] = [ 1 , ids . misc [ name ] [ 0 ] ] ;
}
}
if ( id in block _ids ) {
const name = block _ids [ id ] ;
if ( name in blockCounts ) {
blockCounts [ name ] += 1 ;
} else {
blockCounts [ name ] = 1 ;
}
}
if ( obj . x ) { // sometimes the field doesn't exist
last = Math . max ( last , obj . x ) ;
}
2021-05-11 10:22:55 -04:00
if ( obj . trigger == "Alpha" ) { // invisible triggers
alphaTriggers . push ( obj )
}
2020-10-01 10:09:08 -03:00
data [ i ] = obj ;
}
2019-10-15 23:42:47 -03:00
2021-05-11 10:22:55 -04:00
let invisTriggers = [ ]
alphaTriggers . forEach ( tr => {
if ( tr . x < 500 && ! tr . touchTriggered && ! tr . spawnTriggered && tr . opacity == 0 && tr . duration == 0
&& alphaTriggers . filter ( x => x . targetGroupID == tr . targetGroupID ) . length == 1 ) invisTriggers . push ( Number ( tr . targetGroupID ) )
} )
2020-09-30 12:25:51 -03:00
response . level = {
2021-01-21 19:15:31 -03:00
name : level . name , id : level . id , author : level . author , playerID : level . playerID , accountID : level . accountID , large : level . large
2019-11-20 19:18:12 -03:00
}
2020-09-30 12:25:51 -03:00
response . objects = data . length - 2
response . highDetail = highDetail
response . settings = { }
2020-10-01 10:09:08 -03:00
response . portals = level _portals . sort ( function ( a , b ) { return parseInt ( a . x ) - parseInt ( b . x ) } ) . map ( x => x . portal + " " + Math . floor ( x . x / ( Math . max ( last , 529.0 ) + 340.0 ) * 100 ) + "%" ) . join ( ", " )
2021-04-02 21:50:22 -03:00
response . coins = level _coins . sort ( function ( a , b ) { return parseInt ( a . x ) - parseInt ( b . x ) } ) . map ( x => Math . floor ( x . x / ( Math . max ( last , 529.0 ) + 340.0 ) * 100 ) )
2021-04-02 11:32:10 -03:00
response . coinsVerified = level . verifiedCoins
2020-10-01 10:09:08 -03:00
response . orbs = orb _array
response . orbs . total = Object . values ( orb _array ) . reduce ( ( a , x ) => a + x , 0 ) ; // we already have an array of objects, use it
response . triggers = trigger _array
response . triggers . total = Object . values ( trigger _array ) . reduce ( ( a , x ) => a + x , 0 ) ;
2020-09-30 12:25:51 -03:00
response . triggerGroups = { }
response . blocks = sortObj ( blockCounts )
response . misc = sortObj ( miscCounts , '0' )
response . colors = [ ]
triggerGroups . forEach ( x => {
if ( response . triggerGroups [ 'Group ' + x ] ) response . triggerGroups [ 'Group ' + x ] += 1
else response . triggerGroups [ 'Group ' + x ] = 1
} )
2019-11-20 19:18:12 -03:00
2020-09-30 12:25:51 -03:00
response . triggerGroups = sortObj ( response . triggerGroups )
2021-05-11 10:22:55 -04:00
let triggerKeys = Object . keys ( response . triggerGroups ) . map ( x => Number ( x . slice ( 6 ) ) )
response . triggerGroups . total = triggerKeys . length
// find alpha group with the most objects
response . invisibleGroup = triggerKeys . find ( x => invisTriggers . includes ( x ) )
2020-09-30 12:25:51 -03:00
2020-10-01 10:09:08 -03:00
response . text = level _text . sort ( function ( a , b ) { return parseInt ( a . x ) - parseInt ( b . x ) } ) . map ( x => [ Buffer . from ( x . message , 'base64' ) . toString ( ) , Math . round ( x . x / last * 99 ) + "%" ] )
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
const header _response = parse _header ( header ) ;
response . settings = header _response . settings ;
response . colors = header _response . colors ;
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
response . dataLength = rawData . length
response . data = rawData
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
return response ;
}
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
function parse _header ( header ) {
let response = { } ;
response . settings = { } ;
response . colors = [ ] ;
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
const header _keyed = parse _obj ( header , ',' , init . values , true ) ;
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
Object . keys ( header _keyed ) . forEach ( x => {
let val = init . values [ x ]
let name = val [ 0 ]
let property = header _keyed [ x ]
switch ( val [ 1 ] ) {
case 'list' :
val = init [ ( val [ 0 ] + "s" ) ] [ property ] ;
break ;
case 'number' :
val = Number ( property ) ;
break ;
case 'bump' :
val = Number ( property ) + 1 ;
break ;
case 'bool' :
val = property != "0" ;
break ;
case 'extra-legacy-color' : { // scope?
// you can only imagine my fear when i discovered this was a thing
// these literally are keys set the value, and to convert this to the color list we have to do this fun messy thing that shouldn't exist
// since i wrote the 1.9 color before this, a lot of explaination will be there instead
const colorInfo = name . split ( '-' ) ;
const color = colorInfo [ 2 ] ; // r,g,b
const channel = colorInfo [ 1 ] ;
if ( color == 'r' ) {
// first we create the color object
response . colors . push ( { "channel" : channel , "opacity" : 1 } ) ;
}
// from here we touch the color object
let currentChannel = response . colors . find ( k => k . channel == channel ) ;
if ( color == 'blend' ) {
currentChannel . blending = true ; // only one color has blending though lol
} else if ( color == 'pcol' && property != 0 ) {
currentChannel . pColor = property ;
}
currentChannel [ color ] = property ;
break ;
}
case 'legacy-color' : {
// if a level has a legacy color, we can assume that it does not have a kS38 at all
const color = parse _obj ( property , "_" , colorStuff . properties ) ;
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
let colorObj = color
2019-10-15 23:42:47 -03:00
2020-10-01 10:09:08 -03:00
// so here we parse the color to something understandable by the rest
// slightly smart naming but it is also pretty gross
// in a sense - the name would be something like legacy-G -> G
const colorVal = name . split ( '-' ) . pop ( )
2020-09-30 12:25:51 -03:00
2020-10-01 10:09:08 -03:00
colorObj . channel = colorVal
// from here stuff can continue as normal, ish
if ( colorObj . pColor == "-1" || colorObj . pColor == "0" ) delete colorObj . pColor ;
colorObj . opacity = 1 ; // 1.9 colors don't have this!
if ( colorObj . blending && colorObj . blending == '1' ) colorObj . blending = true ; // 1.9 colors manage to always think they're blending - they're not
else delete colorObj . blending ;
if ( colorVal == '3DL' ) { response . colors . splice ( 4 , 0 , colorObj ) ; } // hardcode the position of 3DL, it typically goes at the end due to how RobTop make the headers
else if ( colorVal == 'Line' ) { colorObj . blending = true ; response . colors . push ( colorObj ) ; } // in line with 2.1 behavior
else { response . colors . push ( colorObj ) ; } // bruh whatever was done to make the color list originally was long
break ;
}
case 'colors' : {
let colorList = property . split ( "|" )
colorList . forEach ( ( x , y ) => {
const color = parse _obj ( x , "_" , colorStuff . properties )
let colorObj = color
if ( ! color . channel ) return colorList = colorList . filter ( ( h , i ) => y != i )
if ( colorStuff . channels [ colorObj . channel ] ) colorObj . channel = colorStuff . channels [ colorObj . channel ]
if ( colorObj . channel > 1000 ) return ;
if ( colorStuff . channels [ colorObj . copiedChannel ] ) colorObj . copiedChannel = colorStuff . channels [ colorObj . copiedChannel ]
if ( colorObj . copiedChannel > 1000 ) delete colorObj . copiedChannel ;
if ( colorObj . pColor == "-1" ) delete colorObj . pColor
if ( colorObj . blending ) colorObj . blending = true
if ( colorObj . copiedHSV ) {
let hsv = colorObj . copiedHSV . split ( "a" )
colorObj . copiedHSV = { }
hsv . forEach ( ( x , y ) => { colorObj . copiedHSV [ colorStuff . hsv [ y ] ] = x } )
colorObj . copiedHSV [ 's-checked' ] = colorObj . copiedHSV [ 's-checked' ] == 1
colorObj . copiedHSV [ 'v-checked' ] = colorObj . copiedHSV [ 'v-checked' ] == 1
if ( colorObj . copyOpacity == 1 ) colorObj . copyOpacity = true
}
colorObj . opacity = + Number ( colorObj . opacity ) . toFixed ( 2 )
colorList [ y ] = colorObj
} ) ;
// we assume this is only going to be run once so... some stuff can go here
2020-09-30 12:25:51 -03:00
colorList = colorList . filter ( x => typeof x == "object" )
if ( ! colorList . find ( x => x . channel == "Obj" ) ) colorList . push ( { "r" : "255" , "g" : "255" , "b" : "255" , "channel" : "Obj" , "opacity" : "1" } )
2020-10-01 10:09:08 -03:00
const specialSort = [ "BG" , "G" , "G2" , "Line" , "Obj" , "3DL" ]
2020-09-30 12:25:51 -03:00
let specialColors = colorList . filter ( x => isNaN ( x . channel ) ) . sort ( function ( a , b ) { return specialSort . indexOf ( a . channel ) > specialSort . indexOf ( b . channel ) } )
let regularColors = colorList . filter ( x => ! isNaN ( x . channel ) ) . sort ( function ( a , b ) { return ( + a . channel ) - ( + b . channel ) } ) ;
response . colors = specialColors . concat ( regularColors )
2020-10-01 10:09:08 -03:00
break ;
}
2020-09-30 12:25:51 -03:00
}
response . settings [ name ] = val
} )
2019-10-15 23:42:47 -03:00
2020-09-30 12:25:51 -03:00
if ( ! response . settings . ground || response . settings . ground > 17 ) response . settings . ground = 1
if ( ! response . settings . background || response . settings . background > 20 ) response . settings . background = 1
if ( ! response . settings . font ) response . settings . font = 1
2019-11-09 22:13:56 -03:00
2020-09-30 12:25:51 -03:00
if ( response . settings . alternateLine == 2 ) response . settings . alternateLine = true
else response . settings . alternateLine = false
2019-11-09 21:59:11 -03:00
2020-09-30 12:25:51 -03:00
Object . keys ( response . settings ) . filter ( k => {
// this should be parsed into color list instead
if ( k . includes ( 'legacy' ) ) delete response . settings [ k ] ;
} ) ;
2019-11-20 19:18:12 -03:00
2020-10-01 10:09:08 -03:00
delete response . settings [ 'colors' ] ;
return response ;
2020-09-30 12:25:51 -03:00
}