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
2021-01-21 21:46:42 -05: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
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
2021-02-18 14:32:54 -05:00
if ( ! ! actor . items ) {
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 ;
}
2020-06-24 14:23:55 -04:00
2021-02-18 14:32:54 -05:00
// 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 ;
}
2021-02-16 02:15:39 -05:00
2021-02-22 16:43:41 -05:00
// Update NPC data with new datamodel information
if ( actor . type === "npc" ) {
2021-02-23 01:17:30 -05:00
_updateNPCData ( actor ) ;
2021-02-22 16:43:41 -05:00
}
2021-02-16 02:15:39 -05:00
// migrate powers last since it relies on item classes being migrated first.
_migrateActorPowers ( actor , updateData ) ;
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
* @ param item
* /
export const migrateItemData = function ( item ) {
const updateData = { } ;
2021-02-09 02:14:10 -05:00
_migrateItemClassPowerCasting ( item , 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-02-22 16:43:41 -05:00
/* -------------------------------------------- */
/ * *
* Update an NPC Actor ' s data based on compendium
* @ param { Object } actor The data object for an Actor
* @ return { Object } The updated Actor
* /
function _updateNPCData ( actor ) {
2021-02-23 01:17:30 -05:00
let actorData = actor . data ;
2021-02-22 16:43:41 -05:00
const updateData = { } ;
// check for flag.core
const hasSource = actor ? . flags ? . core ? . sourceId !== undefined ;
if ( ! hasSource ) return actor ;
// shortcut out if dataVersion flag is set to 1.2.4
2021-02-23 01:17:30 -05:00
const sourceId = actor . flags . core . sourceId ;
const coreSource = sourceId . substr ( 0 , sourceId . length - 17 ) ;
const core _id = sourceId . substr ( sourceId . length - 16 , 16 ) ;
2021-02-22 16:43:41 -05:00
if ( coreSource === "Compendium.sw5e.monsters" ) {
2021-02-23 01:17:30 -05:00
game . packs . get ( "sw5e.monsters" ) . getEntity ( core _id ) . then ( monster => {
const monsterData = monster . data . data ;
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
updateData [ "data.attributes.movement" ] = monsterData . attributes . movement ;
updateData [ "data.attributes.senses" ] = monsterData . attributes . senses ;
updateData [ "data.attributes.powercasting" ] = monsterData . attributes . powercasting ;
updateData [ "data.attributes.force" ] = monsterData . attributes . force ;
updateData [ "data.attributes.tech" ] = monsterData . attributes . tech ;
updateData [ "data.details.powerForceLevel" ] = monsterData . details . powerForceLevel ;
updateData [ "data.details.powerTechLevel" ] = monsterData . details . powerTechLevel ;
// push missing powers onto actor
2021-02-23 07:00:43 -05:00
let newPowers = [ ] ;
2021-02-23 01:17:30 -05:00
for ( let i of monster . items ) {
const itemData = i . data ;
if ( itemData . type === "power" ) {
2021-02-23 07:00:43 -05:00
const itemCompendium _id = itemData . flags ? . core ? . sourceId . split ( "." ) . slice ( - 1 ) [ 0 ] ;
let hasPower = ! ! actor . items . find ( item => item . flags ? . core ? . sourceId . split ( "." ) . slice ( - 1 ) [ 0 ] === itemCompendium _id ) ;
2021-02-23 01:17:30 -05:00
if ( ! hasPower ) {
2021-02-23 07:00:43 -05:00
newPowers . push ( itemData ) ;
2021-02-23 01:17:30 -05:00
}
}
}
2021-02-23 07:08:21 -05:00
let updateActor = await actor . createEmbeddedEntity ( "OwnedItem" , newPowers ) ;
2021-02-23 01:17:30 -05:00
// set flag to check to see if migration has been done so we don't do it again.
2021-02-23 07:00:43 -05:00
actor . setFlag ( "sw5e" , "dataVersion" , "1.2.4" ) ;
2021-02-23 01:17:30 -05:00
} )
2021-02-22 16:43:41 -05:00
}
//merge object
actorData = mergeObject ( actorData , updateData ) ;
// Return the scrubbed data
return actor ;
}
2020-06-24 14:23:55 -04:00
/ * *
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
* /
2021-01-21 21:46:42 -05: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-04 15:23:30 -05:00
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
/* -------------------------------------------- */
2021-02-02 21:42:15 -05:00
/ * *
* Migrate the actor speed string to movement object
* @ private
* /
function _migrateActorPowers ( actorData , updateData ) {
const ad = actorData . data ;
// If new Force & Tech data is not present, create it
2021-02-11 07:04:13 -05:00
let hasNewAttrib = ad ? . attributes ? . force ? . level !== undefined ;
2021-02-02 21:42:15 -05:00
if ( ! hasNewAttrib ) {
updateData [ "data.attributes.force.known.value" ] = 0 ;
updateData [ "data.attributes.force.known.max" ] = 0 ;
updateData [ "data.attributes.force.points.value" ] = 0 ;
updateData [ "data.attributes.force.points.min" ] = 0 ;
updateData [ "data.attributes.force.points.max" ] = 0 ;
2021-02-09 02:14:10 -05:00
updateData [ "data.attributes.force.points.temp" ] = 0 ;
updateData [ "data.attributes.force.points.tempmax" ] = 0 ;
2021-02-02 21:42:15 -05:00
updateData [ "data.attributes.force.level" ] = 0 ;
updateData [ "data.attributes.tech.known.value" ] = 0 ;
updateData [ "data.attributes.tech.known.max" ] = 0 ;
updateData [ "data.attributes.tech.points.value" ] = 0 ;
updateData [ "data.attributes.tech.points.min" ] = 0 ;
updateData [ "data.attributes.tech.points.max" ] = 0 ;
2021-02-09 02:14:10 -05:00
updateData [ "data.attributes.tech.points.temp" ] = 0 ;
updateData [ "data.attributes.tech.points.tempmax" ] = 0 ;
2021-02-02 21:42:15 -05:00
updateData [ "data.attributes.tech.level" ] = 0 ;
}
2021-02-10 15:41:02 -05:00
// If new Power F/T split data is not present, create it
2021-02-16 02:15:39 -05:00
const hasNewLimit = ad ? . powers ? . power1 ? . foverride !== undefined ;
2021-02-11 09:47:46 -05:00
if ( ! hasNewLimit ) {
2021-02-10 15:41:02 -05:00
for ( let i = 1 ; i <= 9 ; i ++ ) {
2021-02-16 02:15:39 -05:00
// add new
2021-02-22 16:43:41 -05:00
updateData [ "data.powers.power" + i + ".fvalue" ] = getProperty ( ad . powers , "power" + i + ".value" ) ;
updateData [ "data.powers.power" + i + ".fmax" ] = getProperty ( ad . powers , "power" + i + ".max" ) ;
2021-02-10 15:41:02 -05:00
updateData [ "data.powers.power" + i + ".foverride" ] = null ;
2021-02-22 16:43:41 -05:00
updateData [ "data.powers.power" + i + ".tvalue" ] = getProperty ( ad . powers , "power" + i + ".value" ) ;
updateData [ "data.powers.power" + i + ".tmax" ] = getProperty ( ad . powers , "power" + i + ".max" ) ;
2021-02-10 15:41:02 -05:00
updateData [ "data.powers.power" + i + ".toverride" ] = null ;
//remove old
updateData [ "data.powers.power" + i + ".-=value" ] = null ;
updateData [ "data.powers.power" + i + ".-=override" ] = null ;
}
}
2021-02-02 21:42:15 -05:00
// If new Bonus Power DC data is not present, create it
const hasNewBonus = ad ? . bonuses ? . power ? . forceLightDC !== undefined ;
if ( ! hasNewBonus ) {
updateData [ "data.bonuses.power.forceLightDC" ] = "" ;
updateData [ "data.bonuses.power.forceDarkDC" ] = "" ;
updateData [ "data.bonuses.power.forceUnivDC" ] = "" ;
2021-02-09 23:47:32 -05:00
updateData [ "data.bonuses.power.techDC" ] = "" ;
2021-02-02 21:42:15 -05:00
}
// Remove the Power DC Bonus
updateData [ "data.bonuses.power.-=dc" ] = null ;
return updateData
}
/* -------------------------------------------- */
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"
2021-01-21 21:46:42 -05:00
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/ ;
2021-01-04 15:23:30 -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 ;
}
// 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-02-09 02:14:10 -05:00
/* -------------------------------------------- */
/ * *
* @ private
* /
function _migrateItemClassPowerCasting ( item , updateData ) {
if ( item . type === "class" ) {
switch ( item . name ) {
case "Consular" :
updateData [ "data.powercasting" ] = "consular" ;
break ;
case "Engineer" :
updateData [ "data.powercasting" ] = "engineer" ;
break ;
case "Guardian" :
updateData [ "data.powercasting" ] = "guardian" ;
break ;
case "Scout" :
updateData [ "data.powercasting" ] = "scout" ;
break ;
case "Sentinel" :
updateData [ "data.powercasting" ] = "sentinel" ;
break ;
}
}
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 ;
}