Files
Foundry-VTT-Docker/resources/app/dist/packages/world.mjs
2025-01-04 00:34:03 +01:00

1 line
18 KiB
JavaScript

import fs from"node:fs";import path from"node:path";import{USER_ROLES,PACKAGE_AVAILABILITY_CODES}from"../../common/constants.mjs";import{getRoute,isNewerVersion,isEmpty,randomID,deepClone,mergeObject}from"../../common/utils/helpers.mjs";import{BaseWorld}from"../../common/packages/module.mjs";import{PackageAssetField,ServerPackageMixin}from"./package.mjs";import Activity from"../components/activity.mjs";import migrations from"../migrations.mjs";import Files from"../files/files.mjs";import{HotReload}from"./_module.mjs";import DocumentCache from"../components/document-cache.mjs";import ProgressEmitter from"../components/progress-emitter.mjs";export default class World extends(ServerPackageMixin(BaseWorld)){static defineSchema(){const e=super.defineSchema();return e.background=new PackageAssetField({relativeToPackage:!1,mustExist:!1,...e.background.options}),e}#e=null;updatingPacks=Promise.resolve();_initialize(e){super._initialize(e),this.system=packages.System.get(this._source.system),this.modules=packages.Module.getPackages({system:this.system,enforceCompatibility:!0}),this.compatibility.maximum=this.compatibility.maximum||this.system?.compatibility.maximum,this.background=this.background||this.system?.background}vend(){const e=super.vend();return e.system=this._source.system,e}static loadLocalManifest(e){const{manifestData:t}=super.loadLocalManifest(e);return this.schema.get("id").validate(t.id)&&(e=World.#t(t)),{manifestPath:e,manifestData:t}}static#t(e){const t=e.id.slugify({strict:!0});let s=t,a=path.join(this.baseDir,s),i=0;for(;fs.existsSync(a);)s=`${t}-${++i}`,a=path.join(this.baseDir,s);const o=path.join(this.baseDir,e.id);fs.renameSync(o,a),e.id=s,delete e.name;const r=path.join(a,this.manifestFile);return Files.writeFileSyncSafe(r,JSON.stringify(e,null,2)),r}prepareDataModel(){const e={};for(const t of Object.values(foundry.documents)){if(!("coreTypes"in t.metadata))continue;const s=t.documentName,a=e[s]={};for(const e of t.metadata.coreTypes)a[e]={};if(!t.hasTypeData)continue;if(s in this.system.documentTypes)for(const e of Object.keys(this.system.documentTypes[s]))a[e]={};const i=this.system.template;if(i&&s in i){const e=foundry.utils.deepClone(i[s]),t=e.templates||{};for(const s of e.types||[]){const i=e[s]||{},o={};for(const e of i.templates||[])e in t&&mergeObject(o,t[e],{enforceTypes:!1});delete i.templates,mergeObject(o,i,{enforceTypes:!1}),a[s]=o}}}return e}get active(){const{game:e}=global;return e.world&&this.id===e.world.id}get canAutoLaunch(){const e=this.availability,t=globalThis.release;if(e===PACKAGE_AVAILABILITY_CODES.MISSING_SYSTEM)return!1;if(this.incompatibleWithCoreVersion)return!1;const s=this.compatibility.verified;return!!s&&(Number.isInteger(Number(s))?s>=t.generation:!isNewerVersion(t.version,s))}static create(e){if(e.id||(e.id=e.name),!e.id)throw new Error("You must provide a unique id that names this World.");if(e.id=e.id.slugify(),e.id.startsWith(".."))throw new Error("You are not allowed to install packages outside of the designated directory path");const t=path.join(this.baseDir,e.id);if(fs.existsSync(t))throw new Error(`A World already exists in the requested directory ${e.id}.`);const s=packages.System.get(e.system);if(!s)throw new Error(`The requested system ${config.system} does not seem to exist!`);e.coreVersion=global.release.version,e.compatibility={minimum:global.release.generation,verified:global.release.generation,maximum:void 0},e.systemVersion=s.version,e.lastPlayed=(new Date).toString();const a=new this(e,{strict:!0});return fs.mkdirSync(t),fs.mkdirSync(path.join(t,"data")),fs.mkdirSync(path.join(t,"scenes")),a.save(),globalThis.logger.info(`Created World "${a.id}"`),this.packages&&this.packages.set(a.id,a),a.vend()}static update(e){delete e.action;const t=this.get(e.id,{strict:!0});return t.updateSource(e),game.world&&(game.world=t),t.save(),t.vend()}static async install(e,t,s,a,i){const o=path.join(this.baseDir,e);if(fs.existsSync(o))throw new Error("You may not install a World on top of a directory that already exists.");return super.install(e,t,s,a,i)}static launch(e){const{express:t,logger:s}=global,a=CONST.SETUP_PACKAGE_PROGRESS,i=this.get(e,{strict:!0});return i.setup().catch((e=>{s.error(e),t.io.emit("progress",{action:a.ACTIONS.LAUNCH_WORLD,step:a.STEPS.ERROR,message:e.message,stack:e.stack}),i.deactivate(null,{asAdmin:!0})})),{}}static _convertRepositoryDataToPackageData(e,t){let s=super._convertRepositoryDataToPackageData(e,t);return s.system=e.requires.length>0?e.requires[0]:"unknown",s.coreVersion=e.version.required_core_version,s}_createPack(e){const t=path.join(this.path,"packs");fs.existsSync(t)||fs.mkdirSync(t),this._source.packs.push(e),this.reset(),this.save()}getActivePacks(e={}){const t=[],s=new Set,a=e=>{if(s.has(e.absPath))return globalThis.logger.error(`More than one package definition was pointing to the same file "${e.absPath}" Only the first one will be loaded.`);t.push(e)};this.packs.forEach(a),this.system.packs.forEach(a);for(const t of this.modules)if(!0===e[t.id])for(const e of t.packs)e.system&&e.system!==this.system.id||a(e);return t}async updateActivePacks(e,{onProgress:t}={}){let s=!1;const a=new Set;for(const i of this.getActivePacks(e)){let e=db.packs.get(i.id);db.packs.has(i.id)||(e=db.defineCompendium(i),await e.connect({strict:!1}),s=!0),a.add(i.id),t instanceof Function&&t(e)}for(const e of db.packs.values())a.has(e.collectionName)||(await e.disconnect(),s=!0);s&&await this.#s(e)}async migrateActivePacks({onProgress:e}={}){for(const t of db.packs.values())t?.connected&&await t.migrateDocuments(),e instanceof Function&&e(t)}async#s(e){const t=Array.from(await db.Folder.sublevel.values().all()).reduce(((e,t)=>{if("Compendium"!==t.type)return e;const s={_id:t._id,name:t.name,folder:t.folder};return e.set(t._id,s),e}),new Map);for(const e of t.values())e.hierarchyName=this.#a(t,e);const{desiredFolders:s,configUpdate:a}=await this.#i(t,e),i=this.#o(s,a,t);await db.Folder.createDocuments(i,{keepId:!0}),await db.Setting.set("core.compendiumConfiguration",a)}#a(e,t){const s=[];for(;t;)s.unshift(t.name),t=e.get(t.folder);return s.join(".")}async#i(e,t){const{db:s}=global,a=[],i=await s.Setting.getValue("core.compendiumConfiguration")??{},o=[this,this.system,...this.modules];let r=1;const n=async(t,o,c)=>{o.folder=c?._id;const d=this.#a(e,o),m=Array.from(e.values()).find((e=>e.hierarchyName===d));m?o._id=m._id:(o._id=randomID(),a.push({_id:o._id,name:o.name,type:"Compendium",sorting:o.sorting,sort:CONST.SORT_INTEGER_DENSITY*r++,color:o.color?.css,folder:c?c._id:null}),e.set(o._id,{_id:o._id,name:o.name,folder:o.folder,hierarchyName:d}));for(const e of o.packs){const a=`${t.id}.${e}`;if(!s.packs.has(a))continue;const r=i[a]??{};"folder"in r||(r.folder=o._id),i[a]=r}for(const e of o.folders)await n(t,e,o)};for(const e of o)if(("module"!==e.type||t[e.id])&&e.packs.size&&e.packFolders.size)for(const t of e.packFolders)await n(e,t);return{desiredFolders:a,configUpdate:i}}#o(e,t,s){const a=[];for(let i=e.length;i--;){const o=e[i],r=o._id,n=Object.values(t).find((e=>e.folder===r)),c=Object.values(s).find((e=>e.folder===r)),d=e.find((e=>e.folder===r));n||c||d?a.push(o):(delete s[r],e.splice(i,1))}return a}updateActiveDocumentTypes(e){for(const t of this.modules){const s=!!e[t.id];for(const[e,a]of Object.entries(t.documentTypes||{})){const i=game.model[e];if(i)for(const[e,o]of Object.entries(a)){const a=`${t.id}.${e}`;s?i[a]=o:delete i[a]}}}}async onUpdateModuleConfiguration(e){return this.updateActiveDocumentTypes(e),this.updatingPacks=this.updatingPacks.finally((async()=>{await this.updateActivePacks(e),await this.migrateActivePacks()}))}async setup(){const{game:e,db:t,release:s,logger:a,options:i}=global;this.#r(),e.release=s,e.world=this,e.system=this.system,e.modules=this.modules,e.active=!0,this.reset(),this.system.loadDataTemplate(),e.model=this.prepareDataModel();let o=0;const{ACTIONS:r,STEPS:n}=CONST.SETUP_PACKAGE_PROGRESS,c=new ProgressEmitter(r.LAUNCH_WORLD,null,0,{id:this.id},{operationName:"Launching World"});isNewerVersion(s.version,this.coreVersion)&&(o=0,c.nextStep(n.MIGRATE_CORE,this.#n().length,{message:"SETUP.WorldLaunchMigrateCore"}),await this.migrateCore({onProgress:()=>c.emit(++o,{log:"Migrating Core Data"})})),o=0,c.nextStep(n.CONNECT_WORLD,t.documents.length,{message:"SETUP.WorldLaunchConnect"}),await this.connect({onProgress:()=>c.emit(++o,{log:"Loading World Data"})}),o=0,c.nextStep(n.MIGRATE_WORLD,t.documents.length,{message:"SETUP.WorldLaunchMigrate"}),await this.migrate({onProgress:()=>c.emit(++o,{log:"Migrating World Data"})}),e.activity=new Activity(this),e.permissions=await t.Setting.getPermissions(),e.compendiumConfiguration=await t.Setting.getValue("core.compendiumConfiguration"),e.users=await t.User.getUsers(),await this.#c();const d={lastPlayed:(new Date).toString()};if(config.release.isGenerationalChange(this.compatibility.verified)||this.safeMode){a.info(`Launching World ${this.id} in Safe Mode`),await t.Setting.sublevel.findDelete({key:"core.moduleConfiguration"}),await t.Scene.sublevel.findUpdate({active:!0},{active:!1});const e=await t.Playlist.find({playing:!0});for(let t of e){const e=t.sounds.map((e=>({_id:e.id,playing:!1})));t.updateSource({playing:!1,sounds:e}),await t.save()}d.safeMode=!1}if(this.resetKeys){a.info(`Resetting all user access keys in World ${this.id}`);for(let t of e.users)""===t.password&&await t.update({password:randomID()}),await t.update({password:""});d.resetKeys=!1}this.updateSource(d),this.save();let m=await t.Setting.getValue("core.moduleConfiguration")||{};m=(await t.Setting.set("core.moduleConfiguration",m,{diff:!1,updateWorld:!1})).value,this.updateActiveDocumentTypes(m),o=0,c.nextStep(n.CONNECT_PKG,this.getActivePacks(m).length,{message:"SETUP.WorldLaunchConnectPackage"}),await this.updateActivePacks(m,{onProgress:()=>c.emit(++o,{log:"Loading Package Data"})}),o=0,c.nextStep(n.MIGRATE_PKG,t.packs.size,{message:"SETUP.WorldLaunchMigratePackage"}),await this.migrateActivePacks({onProgress:()=>c.emit(++o,{log:"Migrating Package Data"})});if(isNewerVersion(this.system.version,this.systemVersion)){o=0;const e=Array.from(t.packs.values()).filter((e=>"world"===e.metadata.package)).length;c.nextStep(n.MIGRATE_SYSTEM,e,{message:"SETUP.WorldLaunchMigrateSystem"}),await this.migrateSystem({onProgress:()=>c.emit(++o,{log:"Migrating System Data"})})}await HotReload.watchForHotReload(this,m),e.ready=!0,e.paused=!i.demoMode,c.complete({log:"Complete"})}async connect({onProgress:e}={}){for(const t of db.documents)await t.connect(),e instanceof Function&&e(t)}async migrate({onProgress:e}={}){for(const t of db.documents)await t.migrateDocuments(),e instanceof Function&&e(t)}async#c(){if(!game.users.filter((e=>e.role===USER_ROLES.GAMEMASTER)).length){let e=game.users.find((e=>"Gamemaster"===e.name));if(e)e.updateSource({role:USER_ROLES.GAMEMASTER}),await e.save(),e=await db.User.get(e.id),game.users.findSplice((t=>t.id===e.id),e);else{let e="";for(;game.users.find((t=>t.name===`Gamemaster${e}`));)e=String((parseInt(e)||0)+1);await db.User.create({name:`Gamemaster${e}`,role:USER_ROLES.GAMEMASTER})}}}async deactivate(e,{asAdmin:t=!1}={}){const{config:s,db:a,game:i,express:o}=global;let r=null;const n={};if(!t){if(!e.user)return{redirect:getRoute("join",{prefix:o.routePrefix})};r=await a.User.get(e.user);if(!(r&&r.hasRole("GAMEMASTER")||e.session&&e.session.admin))return{redirect:getRoute("join",{prefix:o.routePrefix})}}for(let e of Object.keys(i))delete i[e];if(i.ready=!1,i.paused=!0,i.activity&&clearInterval(i.activity._heartbeat),i.documentCache=new DocumentCache,await HotReload.stopWatching(),a.disconnect(),o.io.emit("shutdown",{world:this.id,userId:r?._id??null}),n.lastPlayed=(new Date).toString(),this.#e){const e=Math.round((Date.now()-this.#e)/1e3);n.playtime=this.playtime+e,this.#e=null}return this.updateSource(n),this.save(),this.#r(),{redirect:getRoute("setup",{prefix:s.options.routePrefix})}}async migrateCore({onProgress:e}={}){const{release:t,logger:s}=global;s.info(`Migrating World ${this.id} to updated core platform ${t.version}`);const a=this.#n();for(let t of a)t instanceof Function&&(await t(this).catch((e=>s.error(e))),e instanceof Function&&e());this.updateSource({coreVersion:t.version,compatibility:{minimum:t.generation,verified:t.version}}),await this.save(),s.info(`Core platform migrations for World ${this.id} to version ${t.version} completed successfully`)}async migrateSystem({onProgress:e}={}){const{db:t,logger:s}=global,a=this.system;s.info(`Migrating World ${this.id} to upgraded ${a.id} System version ${a.version}`);for(let e of t.documents)e.hasTypeData&&await e.migrateSystem();await t.Scene.migrateSystem();for(let s of Array.from(t.packs.values()).filter((e=>"world"===e.metadata.package)))await s.migrate(),e instanceof Function&&e(s);this.updateSource({systemVersion:a.version}),await this.save(),s.info(`Migration of World ${this.id} was successful to ${a.id} System version ${a.version}`)}#r(){packages.System.resetPackages(),packages.Module.resetPackages(),packages.World.resetPackages()}static socketListeners(e,t){e.on("world",(t=>this.requestWorldData(e.session,t))),e.on("manageCompendium",t.bind(e,"manageCompendium",this._onManageCompendium.bind(this))),e.on("refreshAddresses",(async e=>{await config.express.refreshAddresses(),e(config.express.getInvitationLinks())})),e.on("sizeInfo",t.bind(e,"sizeInfo",this._getSizeInfo.bind(this)));const s=game.world;s.registerCustomSocket(e),s.system.registerCustomSocket(e);for(const t of s.modules)t.registerCustomSocket(e)}static async _getSizeInfo(e,t){const s=[...CONST.WORLD_DOCUMENT_TYPES.map((e=>db[e])),...db.packs.values()],a=await Promise.all(s.map((async e=>[e.collectionName,await Files.getDirectorySize(e.filename)])));t(Object.fromEntries(a))}static async requestWorldData(e,t){const{game:s,logger:a}=global;if(!s.world)return t({});const i=e.worlds[s.world.id];if(!i)return t({});try{const e=Date.now();t(await this.#d(i));const s=Date.now()-e;a.info(`Vended World data to User [${i}] in ${Math.round(s)}ms`)}catch(e){a.error(e),t({})}}static async#d(e){const{config:t,db:s,game:a,release:i,logger:o,packages:r}=global,{model:n,paused:c}=a,d=a.world,m=a.users.find((t=>t.id===e));if(!m)throw new Error(`The requested user ID ${e} does not exist`);const l=await global.db.Setting.getValue("core.moduleConfiguration")||{},u=d.modules.map((e=>((e=e.vend()).active=l[e.id]??!1,e))),p=r.warnings.toJSON(),g={};for(const e of d.modules)e.id in p&&(g[e.id]=p[e.id]);const h={userId:e,release:i,world:d.vend(),system:d.system.vend(),modules:u,demoMode:t.options.demoMode,addresses:t.express.getInvitationLinks(),files:t.files.getClientConfig(m),options:{language:t.options.language,port:t.options.port,routePrefix:t.options.routePrefix,updateChannel:t.options.updateChannel,debug:t.options.debug},activeUsers:Array.from(Object.keys(a.activity.users)),model:n,paused:c,packageWarnings:g,template:d.system.template},f=[];f.push(s.User.dump().then((e=>h.users=e))),f.push(s.Actor.dump({sort:"name"}).then((e=>h.actors=e))),f.push(s.Cards.dump({sort:"name"}).then((e=>h.cards=e))),f.push(s.ChatMessage.dump({sort:"timestamp"}).then((e=>h.messages=e))),f.push(s.Combat.dump().then((e=>h.combats=e))),f.push(s.Folder.dump({sort:"name"}).then((e=>h.folders=e))),f.push(s.Item.dump({sort:"name"}).then((e=>h.items=e))),f.push(s.JournalEntry.dump().then((e=>h.journal=e))),f.push(s.Macro.dump().then((e=>h.macros=e))),f.push(s.Playlist.dump({sort:"name"}).then((e=>h.playlists=e))),f.push(s.RollTable.dump({sort:"name"}).then((e=>h.tables=e))),f.push(s.Scene.dump({sort:"name"}).then((e=>h.scenes=e))),f.push(s.Setting.dump().then((e=>h.settings=e))),await d.updatingPacks,h.packs=[];for(let e of d.getActivePacks(l)){const t=s.packs.get(e.id);if(t?.connected){e=deepClone(e),delete e.absPath;try{e.index=await t.getIndex(t.metadata.compendiumIndexFields),e.folders=await t.getFolders(),h.packs.push(e)}catch(e){o.error(`Unable to load pack '${t.filename}': ${e.message}`)}}}return f.push(t.updater.checkCoreUpdateAvailability().then((e=>{const s=d.system.constructor.testAvailability(d.system,{release:t.updater.target}),a=e.hasUpdate&&["testing","stable"].includes(e.channel)&&s===CONST.PACKAGE_AVAILABILITY_CODES.VERIFIED;h.coreUpdate={...e,hasUpdate:a}}))),f.push(d.system.getUpdateNotification().then((e=>h.systemUpdate=e))),await Promise.all(f),h}onUserLogin(e){null===this.#e&&(this.#e=Date.now())}onUserLogout(e){if(this.#e&&isEmpty(game.activity.users)){const e=Math.round((Date.now()-this.#e)/1e3);this.updateSource({playtime:this.playtime+e}),this.save(),this.#e=null}}static async _onManageCompendium(e,{action:t,type:s,data:a,options:i}={}){switch(t){case"create":return this._onCreateCompendium(e,a,i);case"delete":return this._onDeleteCompendium(e,a,i);case"migrate":return this._onMigrateCompendium(e,a,i);default:throw new Error(`Invalid Compendium management action ${t} requested`)}}static async _onCreateCompendium(e,t,s={}){if(!e.isGM)throw new Error(`User ${e.name} cannot create a new Compendium pack`);const a=game.world,i=BaseWorld.schema.fields.packs.element;if(t.name=t.name||t.label.slugify({strict:!0}),t.name||(t.name=`${t.type}-${randomID()}`),t.path=`packs/${t.name}`,t.system=a.system.id,(t=i.clean(t)).package="world",db.packs.has(`${t.package}.${t.name}`))throw new Error(`The Compendium pack ${t.name} already exists in the World and cannot be created`);const o=i.validate(t);if(!isEmpty(o)){const e=o.asError();throw e.message=`Invalid Compendium Pack Data: ${e.message}`,e}game.world._createPack(t);const r=a.packs.find((e=>e.name===t.name)),n=global.db.defineCompendium(r);await n.connect(),await n.migrateDocuments(),logger.info(`Created World Compendium Pack ${n.collectionName}`);const c=s.source?db.packs.get(s.source):null;if(c){const e=(await c.connect()).iterator(),t=n.db.batch();for await(const[s,a]of e)t.put(s,a);await e.close(),await t.write()}return t.id=`world.${t.name}`,t.packageType="world",t.packageName=a.id,t.index=await n.getIndex(n.metadata.compendiumIndexFields),t.folders=await n.getFolders(),t}static async _onDeleteCompendium(e,t,s={}){if(!e.isGM)throw new Error(`User ${e.name} cannot delete a Compendium pack`);const a=`world.${t}`;if(!db.packs.has(a))throw new Error(`The requested World pack name ${t} does not exist`);const i=db.packs.get(a);return await i.deleteCompendium(),game.world._source.packs.findSplice((e=>e.name===i.packData.name)),game.world.reset(),game.world.save(),a}static async _onMigrateCompendium(e,t,s={}){if(!e.isGM)throw new Error(`User ${e.name} cannot migrate a Compendium Pack`);const a=db.packs.get(t);if(!a)throw new Error(`The Compendium Pack ${t} does not exist!`);return a.connected||await a.connect(),await a.migrate({user:e,...s}),a.packData}#n(){const e=this.coreVersion;return Object.entries(migrations).reduce(((t,s)=>(isNewerVersion(s[0],e)&&(t=t.concat(s[1])),t)),[])}}