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-19 20:47:48 -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
2021-05-18 09:11:03 -04:00
for ( let a of game . actors . contents ) {
2020-06-24 14:23:55 -04:00
try {
const updateData = migrateActorData ( a . data ) ;
2021-05-18 09:11:03 -04:00
if ( ! foundry . utils . isObjectEmpty ( updateData ) ) {
2020-06-24 14:23:55 -04:00
console . log ( ` Migrating Actor entity ${ a . name } ` ) ;
await a . update ( updateData , { enforceTypes : false } ) ;
}
} catch ( err ) {
2021-01-19 20:47:48 -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
2021-05-18 09:11:03 -04:00
for ( let i of game . items . contents ) {
2020-06-24 14:23:55 -04:00
try {
2021-05-18 09:11:03 -04:00
const updateData = migrateItemData ( i . toObject ( ) ) ;
if ( ! foundry . utils . isObjectEmpty ( updateData ) ) {
2020-06-24 14:23:55 -04:00
console . log ( ` Migrating Item entity ${ i . name } ` ) ;
await i . update ( updateData , { enforceTypes : false } ) ;
}
} catch ( err ) {
2021-01-19 20:47:48 -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
2021-05-18 09:11:03 -04:00
for ( let s of game . scenes . contents ) {
2020-06-24 14:23:55 -04:00
try {
const updateData = migrateSceneData ( s . data ) ;
2021-05-18 09:11:03 -04:00
if ( ! foundry . utils . isObjectEmpty ( updateData ) ) {
2020-06-24 14:23:55 -04:00
console . log ( ` Migrating Scene entity ${ s . name } ` ) ;
await s . update ( updateData , { enforceTypes : false } ) ;
2021-05-18 09:11:03 -04:00
// If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData.
s . tokens . contents . forEach ( t => t . _actor = null ) ;
2020-06-24 14:23:55 -04:00
}
} catch ( err ) {
2021-01-19 20:47:48 -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
2021-01-19 20:47:48 -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-19 20:47:48 -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 ;
2021-01-19 20:47:48 -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 ( ) ;
2021-05-18 09:11:03 -04:00
const documents = await pack . getDocuments ( ) ;
2020-06-24 14:23:55 -04:00
// Iterate over compendium entries - applying fine-tuned migration functions
2021-05-18 09:11:03 -04:00
for ( let doc of documents ) {
2021-01-19 20:47:48 -05:00
let updateData = { } ;
2020-06-24 14:23:55 -04:00
try {
2021-01-19 20:47:48 -05:00
switch ( entity ) {
case "Actor" :
2021-05-18 09:11:03 -04:00
updateData = migrateActorData ( doc . data ) ;
2021-01-19 20:47:48 -05:00
break ;
case "Item" :
2021-05-18 09:11:03 -04:00
updateData = migrateItemData ( doc . toObject ( ) ) ;
2021-01-19 20:47:48 -05:00
break ;
case "Scene" :
2021-05-18 09:11:03 -04:00
updateData = migrateSceneData ( doc . data ) ;
2021-01-19 20:47:48 -05:00
break ;
2020-06-24 14:23:55 -04:00
}
2021-01-19 20:47:48 -05:00
// Save the entry, if data was changed
2021-05-18 09:11:03 -04:00
if ( foundry . utils . isObjectEmpty ( updateData ) ) continue ;
await doc . update ( updateData ) ;
console . log ( ` Migrated ${ entity } entity ${ doc . name } in Compendium ${ pack . collection } ` ) ;
2021-01-19 20:47:48 -05:00
}
// Handle migration failures
catch ( err ) {
2021-05-18 09:11:03 -04:00
err . message = ` Failed sw5e system migration for entity ${ doc . name } in pack ${ pack . collection } : ${ err . message } ` ;
2020-06-24 14:23:55 -04:00
console . error ( err ) ;
}
}
2021-01-19 20:47:48 -05:00
// Apply the original locked status for the pack
2021-05-18 09:11:03 -04:00
await 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
2021-05-18 09:11:03 -04:00
* @ param { object } actor The actor data object to update
* @ return { Object } The updateData to apply
2020-06-24 14:23:55 -04:00
* /
export const migrateActorData = function ( actor ) {
const updateData = { } ;
// Actor Data Updates
2021-05-18 09:11:03 -04:00
if ( actor . data ) {
_migrateActorMovement ( actor , updateData ) ;
_migrateActorSenses ( actor , updateData ) ;
_migrateActorType ( actor , updateData ) ;
}
2020-06-24 14:23:55 -04:00
// Migrate Owned Items
if ( ! actor . items ) return updateData ;
2021-05-18 09:11:03 -04:00
const items = actor . items . reduce ( ( arr , i ) => {
2020-06-24 14:23:55 -04:00
// Migrate the Owned Item
2021-05-18 09:11:03 -04:00
const itemData = i instanceof CONFIG . Item . documentClass ? i . toObject ( ) : i ;
let itemUpdate = migrateItemData ( itemData ) ;
2020-06-24 14:23:55 -04:00
// Prepared, Equipped, and Proficient for NPC actors
if ( actor . type === "npc" ) {
2021-05-18 09:11:03 -04:00
if ( getProperty ( itemData . data , "preparation.prepared" ) === false ) itemUpdate [ "data.preparation.prepared" ] = true ;
if ( getProperty ( itemData . data , "equipped" ) === false ) itemUpdate [ "data.equipped" ] = true ;
if ( getProperty ( itemData . data , "proficient" ) === false ) itemUpdate [ "data.proficient" ] = true ;
2020-06-24 14:23:55 -04:00
}
// Update the Owned Item
if ( ! isObjectEmpty ( itemUpdate ) ) {
2021-05-18 09:11:03 -04:00
itemUpdate . _id = itemData . _id ;
arr . push ( expandObject ( itemUpdate ) ) ;
}
return arr ;
} , [ ] ) ;
if ( items . length > 0 ) updateData . items = items ;
2020-06-24 14:23:55 -04:00
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
2021-05-18 09:11:03 -04:00
*
* @ param { object } item Item data to migrate
* @ return { object } The updateData to apply
2020-06-24 14:23:55 -04:00
* /
export const migrateItemData = function ( item ) {
const updateData = { } ;
2021-01-19 20:52:33 -05:00
_migrateItemAttunement ( item , updateData ) ;
2021-05-18 09:11:03 -04:00
_migrateItemPowercasting ( 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 ) {
2021-05-18 09:11:03 -04:00
const tokens = scene . tokens . map ( token => {
const t = token . toJSON ( ) ;
if ( ! t . actorId || t . actorLink ) {
t . actorData = { } ;
}
else if ( ! game . actors . has ( t . actorId ) ) {
t . actorId = null ;
t . actorData = { } ;
}
else if ( ! t . actorLink ) {
const actorData = duplicate ( t . actorData ) ;
actorData . type = token . actor ? . type ;
const update = migrateActorData ( actorData ) ;
[ 'items' , 'effects' ] . forEach ( embeddedName => {
if ( ! update [ embeddedName ] ? . length ) return ;
const updates = new Map ( update [ embeddedName ] . map ( u => [ u . _id , u ] ) ) ;
t . actorData [ embeddedName ] . forEach ( original => {
const update = updates . get ( original . _id ) ;
if ( update ) mergeObject ( original , update ) ;
} ) ;
delete update [ embeddedName ] ;
} ) ;
mergeObject ( t . actorData , update ) ;
}
return t ;
} ) ;
return { tokens } ;
2020-06-24 14:23:55 -04:00
} ;
/* -------------------------------------------- */
/ * 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-19 20:52:33 -05:00
* Migrate the actor speed string to movement object
2020-06-24 14:23:55 -04:00
* @ private
* /
2021-05-18 09:11:03 -04:00
function _migrateActorMovement ( actorData , updateData ) {
const ad = actorData . data ;
// Work is needed if old data is present
const old = actorData . type === 'vehicle' ? ad ? . attributes ? . speed : ad ? . attributes ? . speed ? . value ;
const hasOld = old !== undefined ;
if ( hasOld ) {
// If new data is not present, migrate the old data
const hasNew = ad ? . attributes ? . movement ? . walk !== undefined ;
if ( ! hasNew && ( typeof old === "string" ) ) {
const s = ( old || "" ) . split ( " " ) ;
if ( s . length > 0 ) updateData [ "data.attributes.movement.walk" ] = Number . isNumeric ( s [ 0 ] ) ? parseInt ( s [ 0 ] ) : null ;
}
// Remove the old attribute
updateData [ "data.attributes.-=speed" ] = null ;
}
2021-01-19 20:52:33 -05:00
return updateData
}
/* -------------------------------------------- */
/ * *
* 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 || "" ;
2021-05-18 09:11:03 -04:00
if ( typeof original !== "string" ) return ;
2021-01-19 20:52:33 -05:00
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
2021-05-18 09:11:03 -04:00
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/ ;
2021-01-19 20:52:33 -05:00
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 ;
2020-06-24 14:23:55 -04:00
}
2021-01-19 20:52:33 -05:00
// Remove the old traits.senses string once the migration is complete
updateData [ "data.traits.-=senses" ] = null ;
return updateData ;
2020-06-24 14:23:55 -04:00
}
/* -------------------------------------------- */
2021-05-18 09:11:03 -04:00
/ * *
* Migrate the actor details . type string to object
* @ private
* /
function _migrateActorType ( actor , updateData ) {
const ad = actor . data ;
const original = ad . details ? . type ;
if ( typeof original !== "string" ) return ;
// New default data structure
let data = {
"value" : "" ,
"subtype" : "" ,
"swarm" : "" ,
"custom" : ""
}
// Match the existing string
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i ;
const match = original . trim ( ) . match ( pattern ) ;
if ( match ) {
// Match a known creature type
const typeLc = match . groups . type . trim ( ) . toLowerCase ( ) ;
const typeMatch = Object . entries ( CONFIG . SW5E . creatureTypes ) . find ( ( [ k , v ] ) => {
return ( typeLc === k ) ||
( typeLc === game . i18n . localize ( v ) . toLowerCase ( ) ) ||
( typeLc === game . i18n . localize ( ` ${ v } Pl ` ) . toLowerCase ( ) ) ;
} ) ;
if ( typeMatch ) data . value = typeMatch [ 0 ] ;
else {
data . value = "custom" ;
data . custom = match . groups . type . trim ( ) . titleCase ( ) ;
}
data . subtype = match . groups . subtype ? . trim ( ) . titleCase ( ) || "" ;
// Match a swarm
const isNamedSwarm = actor . name . startsWith ( game . i18n . localize ( "SW5E.CreatureSwarm" ) ) ;
if ( match . groups . size || isNamedSwarm ) {
const sizeLc = match . groups . size ? match . groups . size . trim ( ) . toLowerCase ( ) : "tiny" ;
const sizeMatch = Object . entries ( CONFIG . SW5E . actorSizes ) . find ( ( [ k , v ] ) => {
return ( sizeLc === k ) || ( sizeLc === game . i18n . localize ( v ) . toLowerCase ( ) ) ;
} ) ;
data . swarm = sizeMatch ? sizeMatch [ 0 ] : "tiny" ;
}
else data . swarm = "" ;
}
// No match found
else {
data . value = "custom" ;
data . custom = original ;
}
// Update the actor data
updateData [ "data.details.type" ] = data ;
return updateData ;
}
/* -------------------------------------------- */
2020-06-24 14:23:55 -04:00
/ * *
2021-01-19 20:52:33 -05:00
* Delete the old data . attuned boolean
2021-05-18 09:11:03 -04:00
*
* @ param { object } item Item data to migrate
* @ param { object } updateData Existing update to expand upon
* @ return { object } The updateData to apply
2020-06-24 14:23:55 -04:00
* @ private
* /
2021-01-19 20:52:33 -05:00
function _migrateItemAttunement ( item , updateData ) {
2021-05-18 09:11:03 -04:00
if ( item . data ? . attuned === undefined ) return updateData ;
2021-01-19 20:54:45 -05:00
updateData [ "data.attunement" ] = CONFIG . SW5E . attunementTypes . NONE ;
2021-01-19 20:52:33 -05:00
updateData [ "data.-=attuned" ] = null ;
return updateData ;
2021-01-19 20:47:48 -05:00
}
2020-10-08 02:20:12 -04:00
2021-05-18 09:11:03 -04:00
/* -------------------------------------------- */
/ * *
* Replace class powercasting string to object .
*
* @ param { object } item Item data to migrate
* @ param { object } updateData Existing update to expand upon
* @ return { object } The updateData to apply
* @ private
* /
function _migrateItemPowercasting ( item , updateData ) {
if ( item . type !== "class" || ( foundry . utils . getType ( item . data . powercasting ) === "Object" ) ) return updateData ;
updateData [ "data.powercasting" ] = {
progression : item . data . powercasting ,
ability : ""
} ;
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 } ) ;
}
2021-01-19 20:47:48 -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 ;
}