2020-06-24 14:23:55 -04:00
/ * *
* Perform a system migration for the entire World , applying migrations for Actors , Items , and Compendium packs
* @ return { Promise } A Promise which resolves once the migration is completed
* /
export const migrateWorld = async function ( ) {
2021-01-04 15:23:30 -05:00
ui . notifications . info ( ` Applying SW5e System Migration for version ${ game . system . data . version } . Please be patient and do not close your game or shut down your server. ` , { permanent : true } ) ;
2020-06-24 14:23:55 -04:00
// Migrate World Actors
for ( let a of game . actors . entities ) {
try {
const updateData = migrateActorData ( a . data ) ;
if ( ! isObjectEmpty ( updateData ) ) {
console . log ( ` Migrating Actor entity ${ a . name } ` ) ;
await a . update ( updateData , { enforceTypes : false } ) ;
}
} catch ( err ) {
2020-11-12 17:30:07 -05:00
err . message = ` Failed sw5e system migration for Actor ${ a . name } : ${ err . message } ` ;
2020-06-24 14:23:55 -04:00
console . error ( err ) ;
}
}
// Migrate World Items
for ( let i of game . items . entities ) {
try {
const updateData = migrateItemData ( i . data ) ;
if ( ! isObjectEmpty ( updateData ) ) {
console . log ( ` Migrating Item entity ${ i . name } ` ) ;
await i . update ( updateData , { enforceTypes : false } ) ;
}
} catch ( err ) {
2020-11-12 17:30:07 -05:00
err . message = ` Failed sw5e system migration for Item ${ i . name } : ${ err . message } ` ;
2020-06-24 14:23:55 -04:00
console . error ( err ) ;
}
}
// Migrate Actor Override Tokens
for ( let s of game . scenes . entities ) {
try {
const updateData = migrateSceneData ( s . data ) ;
if ( ! isObjectEmpty ( updateData ) ) {
console . log ( ` Migrating Scene entity ${ s . name } ` ) ;
await s . update ( updateData , { enforceTypes : false } ) ;
}
} catch ( err ) {
2020-11-12 17:30:07 -05:00
err . message = ` Failed sw5e system migration for Scene ${ s . name } : ${ err . message } ` ;
2020-06-24 14:23:55 -04:00
console . error ( err ) ;
}
}
// Migrate World Compendium Packs
2020-11-12 17:30:07 -05:00
for ( let p of game . packs ) {
if ( p . metadata . package !== "world" ) continue ;
if ( ! [ "Actor" , "Item" , "Scene" ] . includes ( p . metadata . entity ) ) continue ;
2020-06-24 14:23:55 -04:00
await migrateCompendium ( p ) ;
}
// Set the migration as complete
game . settings . set ( "sw5e" , "systemMigrationVersion" , game . system . data . version ) ;
2021-01-04 15:23:30 -05:00
ui . notifications . info ( ` SW5e System Migration to version ${ game . system . data . version } completed! ` , { permanent : true } ) ;
2020-06-24 14:23:55 -04:00
} ;
/* -------------------------------------------- */
/ * *
* Apply migration rules to all Entities within a single Compendium pack
* @ param pack
* @ return { Promise }
* /
export const migrateCompendium = async function ( pack ) {
const entity = pack . metadata . entity ;
if ( ! [ "Actor" , "Item" , "Scene" ] . includes ( entity ) ) return ;
2020-11-12 17:30:07 -05:00
// Unlock the pack for editing
const wasLocked = pack . locked ;
await pack . configure ( { locked : false } ) ;
2020-06-24 14:23:55 -04:00
// Begin by requesting server-side data model migration and get the migrated content
await pack . migrate ( ) ;
const content = await pack . getContent ( ) ;
// Iterate over compendium entries - applying fine-tuned migration functions
for ( let ent of content ) {
2020-11-12 17:30:07 -05:00
let updateData = { } ;
2020-06-24 14:23:55 -04:00
try {
2020-11-12 17:30:07 -05:00
switch ( entity ) {
case "Actor" :
updateData = migrateActorData ( ent . data ) ;
break ;
case "Item" :
updateData = migrateItemData ( ent . data ) ;
break ;
case "Scene" :
updateData = migrateSceneData ( ent . data ) ;
break ;
2020-06-24 14:23:55 -04:00
}
2020-11-12 17:30:07 -05:00
if ( isObjectEmpty ( updateData ) ) continue ;
// Save the entry, if data was changed
updateData [ "_id" ] = ent . _id ;
await pack . updateEntity ( updateData ) ;
console . log ( ` Migrated ${ entity } entity ${ ent . name } in Compendium ${ pack . collection } ` ) ;
}
// Handle migration failures
catch ( err ) {
err . message = ` Failed sw5e system migration for entity ${ ent . name } in pack ${ pack . collection } : ${ err . message } ` ;
2020-06-24 14:23:55 -04:00
console . error ( err ) ;
}
}
2020-11-12 17:30:07 -05:00
// Apply the original locked status for the pack
pack . configure ( { locked : wasLocked } ) ;
2020-06-24 14:23:55 -04:00
console . log ( ` Migrated all ${ entity } entities from Compendium ${ pack . collection } ` ) ;
} ;
/* -------------------------------------------- */
/* Entity Type Migration Helpers */
/* -------------------------------------------- */
/ * *
* Migrate a single Actor entity to incorporate latest data model changes
* Return an Object of updateData to be applied
* @ param { Actor } actor The actor to Update
* @ return { Object } The updateData to apply
* /
export const migrateActorData = function ( actor ) {
const updateData = { } ;
// Actor Data Updates
2020-11-12 17:30:07 -05:00
_migrateActorMovement ( actor , updateData ) ;
2021-01-04 15:23:30 -05:00
_migrateActorSenses ( actor , updateData ) ;
2020-06-24 14:23:55 -04:00
// Migrate Owned Items
if ( ! actor . items ) return updateData ;
let hasItemUpdates = false ;
const items = actor . items . map ( i => {
// Migrate the Owned Item
let itemUpdate = migrateItemData ( i ) ;
// Prepared, Equipped, and Proficient for NPC actors
if ( actor . type === "npc" ) {
if ( getProperty ( i . data , "preparation.prepared" ) === false ) itemUpdate [ "data.preparation.prepared" ] = true ;
if ( getProperty ( i . data , "equipped" ) === false ) itemUpdate [ "data.equipped" ] = true ;
if ( getProperty ( i . data , "proficient" ) === false ) itemUpdate [ "data.proficient" ] = true ;
}
// Update the Owned Item
if ( ! isObjectEmpty ( itemUpdate ) ) {
hasItemUpdates = true ;
return mergeObject ( i , itemUpdate , { enforceTypes : false , inplace : false } ) ;
} else return i ;
} ) ;
if ( hasItemUpdates ) updateData . items = items ;
return updateData ;
} ;
2020-10-06 00:45:33 -04:00
/* -------------------------------------------- */
/ * *
* Scrub an Actor ' s system data , removing all keys which are not explicitly defined in the system template
* @ param { Object } actorData The data object for an Actor
* @ return { Object } The scrubbed Actor data
* /
function cleanActorData ( actorData ) {
// Scrub system data
const model = game . system . model . Actor [ actorData . type ] ;
actorData . data = filterObject ( actorData . data , model ) ;
// Scrub system flags
const allowedFlags = CONFIG . SW5E . allowedActorFlags . reduce ( ( obj , f ) => {
obj [ f ] = null ;
return obj ;
} , { } ) ;
if ( actorData . flags . sw5e ) {
actorData . flags . sw5e = filterObject ( actorData . flags . sw5e , allowedFlags ) ;
}
// Return the scrubbed data
return actorData ;
}
2020-06-24 14:23:55 -04:00
/* -------------------------------------------- */
/ * *
* Migrate a single Item entity to incorporate latest data model changes
* @ param item
* /
export const migrateItemData = function ( item ) {
const updateData = { } ;
2021-01-04 15:23:30 -05:00
_migrateItemAttunement ( item , updateData ) ;
2020-06-24 14:23:55 -04:00
return updateData ;
} ;
/* -------------------------------------------- */
/ * *
* Migrate a single Scene entity to incorporate changes to the data model of it ' s actor data overrides
* Return an Object of updateData to be applied
* @ param { Object } scene The Scene data to Update
* @ return { Object } The updateData to apply
* /
export const migrateSceneData = function ( scene ) {
const tokens = duplicate ( scene . tokens ) ;
return {
tokens : tokens . map ( t => {
if ( ! t . actorId || t . actorLink || ! t . actorData . data ) {
t . actorData = { } ;
return t ;
}
const token = new Token ( t ) ;
if ( ! token . actor ) {
t . actorId = null ;
t . actorData = { } ;
} else if ( ! t . actorLink ) {
const updateData = migrateActorData ( token . data . actorData ) ;
t . actorData = mergeObject ( token . data . actorData , updateData ) ;
}
return t ;
} )
} ;
} ;
/* -------------------------------------------- */
/ * L o w l e v e l m i g r a t i o n u t i l i t i e s
/* -------------------------------------------- */
/ * *
2021-01-04 15:23:30 -05:00
* Migrate the actor speed string to movement object
2020-06-24 14:23:55 -04:00
* @ private
* /
2020-11-12 17:30:07 -05:00
function _migrateActorMovement ( actor , updateData ) {
2021-01-04 15:23:30 -05:00
const ad = actor . data ;
2021-01-18 23:49:04 -05:00
const old = actor . type === 'vehicle' ? ad ? . attributes ? . speed : ad ? . attributes ? . speed ? . value ;
if ( typeof old !== "string" ) return ;
2021-01-04 15:23:30 -05:00
const s = ( old || "" ) . split ( " " ) ;
2020-11-12 17:30:07 -05:00
if ( s . length > 0 ) updateData [ "data.attributes.movement.walk" ] = Number . isNumeric ( s [ 0 ] ) ? parseInt ( s [ 0 ] ) : null ;
2021-01-04 15:23:30 -05:00
updateData [ "data.attributes.-=speed" ] = null ;
return updateData
2020-11-12 17:30:07 -05:00
}
2020-10-08 02:20:12 -04:00
2021-01-04 15:23:30 -05:00
/* -------------------------------------------- */
/ * *
* Migrate the actor traits . senses string to attributes . senses object
* @ private
* /
function _migrateActorSenses ( actor , updateData ) {
const ad = actor . data ;
if ( ad ? . traits ? . senses === undefined ) return ;
const original = ad . traits . senses || "" ;
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/
let wasMatched = false ;
// Match each comma-separated term
for ( let s of original . split ( "," ) ) {
s = s . trim ( ) ;
const match = s . match ( pattern ) ;
if ( ! match ) continue ;
const type = match [ 1 ] . toLowerCase ( ) ;
if ( type in CONFIG . SW5E . senses ) {
updateData [ ` data.attributes.senses. ${ type } ` ] = Number ( match [ 2 ] ) . toNearest ( 0.5 ) ;
wasMatched = true ;
}
}
// If nothing was matched, but there was an old string - put the whole thing in "special"
if ( ! wasMatched && ! ! original ) {
updateData [ "data.attributes.senses.special" ] = original ;
}
// Remove the old traits.senses string once the migration is complete
updateData [ "data.traits.-=senses" ] = null ;
return updateData ;
}
2020-10-08 02:20:12 -04:00
/* -------------------------------------------- */
2021-01-04 15:23:30 -05:00
/ * *
* Delete the old data . attuned boolean
* @ private
* /
function _migrateItemAttunement ( item , updateData ) {
if ( item . data . attuned === undefined ) return ;
2021-01-18 23:49:04 -05:00
updateData [ "data.attunement" ] = CONFIG . SW5E . attunementTypes . NONE ;
2021-01-04 15:23:30 -05:00
updateData [ "data.-=attuned" ] = null ;
return updateData ;
}
/* -------------------------------------------- */
2020-10-08 02:20:12 -04:00
/ * *
* A general tool to purge flags from all entities in a Compendium pack .
* @ param { Compendium } pack The compendium pack to clean
* @ private
* /
export async function purgeFlags ( pack ) {
const cleanFlags = ( flags ) => {
const flags5e = flags . sw5e || null ;
return flags5e ? { sw5e : flags5e } : { } ;
} ;
await pack . configure ( { locked : false } ) ;
const content = await pack . getContent ( ) ;
for ( let entity of content ) {
const update = { _id : entity . id , flags : cleanFlags ( entity . data . flags ) } ;
if ( pack . entity === "Actor" ) {
update . items = entity . data . items . map ( i => {
i . flags = cleanFlags ( i . flags ) ;
return i ;
} )
}
await pack . updateEntity ( update , { recursive : false } ) ;
console . log ( ` Purged flags from ${ entity . name } ` ) ;
}
await pack . configure ( { locked : true } ) ;
}
2020-11-12 17:30:07 -05:00
/* -------------------------------------------- */
/ * *
* Purge the data model of any inner objects which have been flagged as _deprecated .
* @ param { object } data The data to clean
* @ private
* /
export function removeDeprecatedObjects ( data ) {
for ( let [ k , v ] of Object . entries ( data ) ) {
if ( getType ( v ) === "Object" ) {
if ( v . _deprecated === true ) {
console . log ( ` Deleting deprecated object key ${ k } ` ) ;
delete data [ k ] ;
}
else removeDeprecatedObjects ( v ) ;
}
}
return data ;
}