2024-04-16 02:15:35 +00:00
const axios = require ( "axios" ) . default ;
2024-04-24 06:28:07 +00:00
const { RobloxModel } = require ( "rbxm-parser" ) ;
2024-04-16 02:15:35 +00:00
// https://create.roblox.com/docs/reference/engine/enums/AssetType
const AssetType = {
Image : 1 ,
TShirt : 2 ,
Audio : 3 ,
Mesh : 4 ,
Lua : 5 ,
Hat : 8 ,
Place : 9 ,
Model : 10 ,
Shirt : 11 ,
Pants : 12 ,
Decal : 13 ,
Head : 17 ,
Face : 18 ,
Gear : 19 ,
Badge : 21 ,
Animation : 24 ,
GamePass : 34 ,
Plugin : 38 ,
MeshPart : 40
} ;
async function getAssetInfo ( assetId ) {
const res = await axios . get ( "https://apis.roblox.com/toolbox-service/v1/items/details" , {
params : {
assetIds : assetId
} ,
validateStatus : ( status ) => status === 403 || status === 404 || ( status >= 200 && status < 300 ) // Allow 403/404 as a valid status (don't throw an error)
} ) ;
if ( res . status < 200 || res . status > 300 ) {
return {
status : res . status
2024-04-24 06:28:07 +00:00
} ;
2024-04-16 02:15:35 +00:00
}
const data = res . data . data ;
const assetInfo = data [ 0 ] ;
return {
status : res . status ,
id : assetId ,
name : assetInfo . asset . name ,
typeId : assetInfo . asset . typeId ,
creatorId : assetInfo . creator . id ,
price : assetInfo . product . price ,
productId : assetInfo . product . productId ,
forSale : assetInfo . product . isForSaleOrIsPublicDomain
2024-04-24 06:28:07 +00:00
} ;
2024-04-16 02:15:35 +00:00
}
2024-04-24 06:28:07 +00:00
const SubmissionColumnsString = "map_id,unix_timestamp,user_id,username,display_name,creator\n" ;
2024-04-17 00:58:24 +00:00
const SubmissionColumn = {
ModelID : 0 ,
UnixTimestamp : 1 ,
UserID : 2 ,
2024-04-24 06:28:07 +00:00
Username : 3 ,
DisplayName : 4 ,
Creator : 5
2024-04-17 00:58:24 +00:00
} ;
function getSubmissionLine ( line ) {
const modelId = Number ( line [ SubmissionColumn . ModelID ] ) ;
const timestamp = Number ( line [ SubmissionColumn . UnixTimestamp ] ) ;
let userId = "" ;
let username = "" ;
2024-04-24 06:28:07 +00:00
let displayName = "" ;
let creator = "" ;
2024-04-17 00:58:24 +00:00
// Backwards compatibility
if ( line . length > 2 ) {
userId = Number ( line [ SubmissionColumn . UserID ] ) ;
username = line [ SubmissionColumn . Username ] ;
}
2024-04-24 06:28:07 +00:00
if ( line . length > 4 ) {
displayName = line [ SubmissionColumn . DisplayName ] ;
creator = line [ SubmissionColumn . Creator ] ;
}
2024-04-17 00:58:24 +00:00
return {
modelId : modelId ,
timestamp : timestamp ,
userId : userId ,
2024-04-24 06:28:07 +00:00
username : username ,
displayName : displayName ,
creator : creator
} ;
}
function safeCsvFormat ( cell ) {
// https://stackoverflow.com/questions/46637955/write-a-string-containing-commas-and-double-quotes-to-csv
if ( cell . replace ( / /g , '' ) . match ( /[\s,"]/ ) ) {
return '"' + cell . replace ( /"/g , '""' ) + '"' ;
}
return cell ;
}
function createSubmissionLine ( modelId , timestamp , userId , username , displayName , creator ) {
return ` ${ modelId } , ${ timestamp } , ${ userId } , ${ safeCsvFormat ( username ) } , ${ safeCsvFormat ( displayName ) } , ${ safeCsvFormat ( creator ) } \n ` ;
}
/ * *
* @ param { RobloxModel } model
* /
function getMapStringValues ( model ) {
if ( model . Roots . length < 1 ) {
return undefined ;
}
const root = model . Roots [ 0 ] ;
const values = root . FindChildrenOfClass ( "StringValue" , ( child ) => child . Name === "DisplayName" || child . Name === "Creator" ) ;
if ( values . length !== 2 ) {
return undefined ;
}
let name , creator ;
if ( values [ 0 ] . Name === "DisplayName" ) {
name = values [ 0 ] . Value ;
creator = values [ 1 ] . Value ;
}
else {
name = values [ 1 ] . Value ;
creator = values [ 0 ] . Value ;
}
return {
displayName : name ,
creator : creator
} ;
}
function capitalize ( str ) {
if ( ! str ) return "" ;
return str [ 0 ] . toUpperCase ( ) + str . slice ( 1 ) ;
}
async function validateMapAsset ( assetId , game ) {
const model = await RobloxModel . ReadFromAssetId ( assetId ) ;
if ( ! model ) {
// For whatever reason we couldn't parse the model, so we'll just skip doing validation
return {
valid : true ,
displayName : "" ,
creator : ""
} ;
}
const errors = [ ] ;
if ( model . Roots . length > 1 ) {
errors . push ( "Your map has more than one root part, it must have a single `Model` as the root." ) ;
return {
valid : false ,
errors : errors
} ;
}
const root = model . Roots [ 0 ] ;
if ( ! root . IsA ( "Model" ) ) {
errors . push ( ` The root part of your model is a \` ${ root . ClassName } \` , it needs to be a \` Model \` instead. ` ) ;
}
else {
const prefix = ( game === "deathrun" ) ? "dr" : game ;
if ( ! root . Name . startsWith ( prefix + "_" ) ) {
errors . push ( ` Your root model's name is \` ${ root . Name } \` , its name must start with \` ${ prefix } _ \` . ` ) ;
}
if ( ! /^[a-z0-9_]*$/ . test ( root . Name ) ) {
errors . push ( ` Your root model's name is \` ${ root . Name } \` which contains invalid characters. It must only contain lowercase alphanumeric characters separated by underscores. ` ) ;
}
}
const values = getMapStringValues ( model ) ;
if ( ! values ) {
2024-04-24 16:03:25 +00:00
errors . push ( "Your map is missing a `StringValue` named `Creator` and/or a `StringValue` named `DisplayName`. You must add both to your map and they must be parented directly to the root model. These are used to set the map's creator and name in-game." ) ;
2024-04-24 06:28:07 +00:00
}
else {
if ( ! values . creator ) {
errors . push ( "Your map's `Creator` `StringValue` does not have a `Value`." ) ;
}
if ( ! values . displayName ) {
errors . push ( "Your map's `DisplayName` `StringValue` does not have a `Value`." ) ;
}
else {
const checkName = capitalize ( values . displayName ) ;
if ( values . displayName !== checkName ) {
errors . push ( ` Your map's \` DisplayName \` must be capitalized. You may change it from \` ${ values . displayName } \` to \` ${ checkName } \` . ` ) ;
}
}
}
const mapParts = root . FindDescendantsOfClass ( "BasePart" , ( part ) => part . Name === "MapStart" || part . Name === "MapFinish" ) ;
if ( mapParts . length !== 2 || mapParts [ 0 ] . Name === mapParts [ 1 ] . Name ) {
errors . push ( "Your map must have exactly one part named `MapStart` and one part named `MapFinish`." ) ;
}
else if ( mapParts [ 0 ] . CanCollide || mapParts [ 1 ] . CanCollide ) {
errors . push ( "The `MapStart` and `MapFinish` parts in your map must have the `CanCollide` property disabled." ) ;
}
const spawnOnes = root . FindDescendantsOfClass ( "BasePart" , ( part ) => part . Name === "Spawn1" ) ;
if ( spawnOnes . length !== 1 ) {
errors . push ( "Your map must have exactly one part named `Spawn1`." ) ;
}
// Why does ModuleScript not inherit from Script, and/or why does BaseScript not have a Source property?
const illegalScript = root . FindFirstDescendantOfClass ( "Script" , ( script ) => sourceHasIllegalKeywords ( script . Source ) ) ;
const illegalModuleScript = root . FindFirstDescendantOfClass ( "ModuleScript" , ( script ) => sourceHasIllegalKeywords ( script . Source ) ) ;
if ( illegalScript || illegalModuleScript ) {
errors . push ( "Your map has a `Script` that contains the keyword `getfenv` or `require`. You must remove these." ) ;
}
if ( errors . length > 0 ) {
return {
valid : false ,
errors : errors
} ;
}
return {
valid : true ,
displayName : values . displayName ,
creator : values . creator
2024-04-17 00:58:24 +00:00
} ;
}
2024-04-24 06:28:07 +00:00
function sourceHasIllegalKeywords ( source ) {
return source && ( source . includes ( "getfenv" ) || source . includes ( "require" ) ) ;
}
function getValidationMessage ( validation , game , errorOnFail ) {
if ( validation . valid ) {
return ` ✅ Your map is valid! (game: ${ game } ) ` ;
}
let msg = ` ${ errorOnFail ? "🚫" : "⚠️" } **Your map has problems.** (game: ${ game } ) ` ;
for ( const error of validation . errors ) {
msg += ` \n * ${ error } ` ;
}
return msg ;
2024-04-17 00:58:24 +00:00
}
2024-04-24 06:28:07 +00:00
module . exports = { AssetType , getAssetInfo , SubmissionColumn , SubmissionColumnsString , getSubmissionLine , createSubmissionLine , validateMapAsset , getValidationMessage , safeCsvFormat } ;