zischenstand

This commit is contained in:
centron\schwoerer
2025-11-14 14:52:43 +01:00
parent 30aa03c6db
commit f054a31b20
8733 changed files with 900639 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
# FoundryVTT - Forien's Copy Environment
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/League-of-Foundry-Developers/foundryvtt-forien-copy-environment) ![GitHub Releases](https://img.shields.io/github/downloads/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/latest/total) ![GitHub Releases](https://img.shields.io/github/downloads/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/total) ![Forge Installs](https://img.shields.io/badge/dynamic/json?label=Forge%20Installs&query=package.installs&suffix=%25&url=https%3A%2F%2Fforge-vtt.com%2Fapi%2Fbazaar%2Fpackage%2Fforien-copy-environment&colorB=4aa94a) ![Foundry Version](https://img.shields.io/badge/dynamic/json.svg?url=https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/releases/latest/download/module.json&label=foundry%20version&query=$.compatibleCoreVersion&colorB=blueviolet) ![Forien's Copy Environment](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FLeague-of-Foundry-Developers%2Fleague-repo-status%2Fshields-endpoint%2Fforien-copy-environment.json)
**NOTE** This is an unofficial forked version of the module maintained by the League of Foundry Developers to provide module continuity while Forien is unavailable.
**[Compatibility]**: *FoundryVTT* 0.6.0 - 12.0+
**[Systems]**: *any*
This module allows for fast copy/save environment data such as core version or list of installed modules and their versions. Supports copying as TXT or saving as JSON.
Module also allows to export (save/backup) current game settings and then import (restore) them. Non-GM users can only import client-side settings.
## Installation
1. Install Forien's Copy Environment using manifest URL: https://raw.githubusercontent.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/master/module.json
2. While loaded in World, enable **_Forien's Copy Environment_** module.
### Usage
Go to Settings tab in Sidebar and **right click** on data **below** "General Information" header
![](https://i.gyazo.com/8f41b4e7f52e8f560f9265774a9849db.gif)
## Features
* Copy Environment (core, system and module versions) to clipboard
* Save Environment (including manifest links) as a JSON file
* Export game settings (both 'world' and 'client' scopes)
* Import game settings ('client' ones, and if you are GM also 'world' ones - you will be able to choose which ones you want to import)
*Please note that importing 'world' scope settings en masse as GM might cause some issues to connected players. I advise players should logout before attempting to import World Settings*
## Info for Module Developers
### How do I opt out?
Perhaps you have a module that you don't want the settings being copied between worlds. You can add the following to your module manifest file to opt out of having the settings copied. The `active` state of your module will still be copied, just the settings won't.
1. Add `noCopyEnvironmentSettings: true` to your manifest json inside of the `flags` field of the manifest.
module.json
```md
"flags": {
"noCopyEnvironmentSettings": true
}
```
## Contact
[League of Foundry Developers](https://discord.gg/gzemMfHURH) ~~If you wish to contact me for any reason, reach me out on Discord using my tag: `Forien#2130`~~
## Translations
- Japanese by touge
- German by brockhaus
- Portuguese by vithort
- Italian by GregoryWarn
- French by rectulo
## Support
If you wish to support module development, please consider [becoming Patron](https://www.patreon.com/foundryworkshop) or donating [through Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6P2RRX7HVEMV2&source=url). Thanks!
## License
Forien's Copy Environment is a module for Foundry VTT by Forien and is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/).
This work is licensed under Foundry Virtual Tabletop [EULA - Limited License Agreement for module development from May 29, 2020](https://foundryvtt.com/article/license/).

View File

@@ -0,0 +1,157 @@
# Changelog
## v2.2.4
- Added Polish translation by Lioheart.
- Enabled Italian and French translations.
## v2.2.3
- Made it clearer when modules will not be enabled due to the selected config settings.
- This is one of the primary use cases of this module, so most of the time it is an accident to uncheck the setting.
- See [issue #49](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/49).
- Added Italian translation by GregoryWarn.
- Added French translation by rectulo.
- Added machine translations for the new setting strings:
- German
- Japanese
- Portuguese
- Italian
- French
## v2.2.2
- Ignore "core.time" and "pf2e.worldClock.worldCreatedOn" values by default.
- These values can still be selected in from the importer dialog but will be unselected by default.
- You may run `game.settings.set('forien-copy-environment', 'selected-properties', undefined);` as a script macro to reset the values to their default.
- See [issue #53](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/53).
## v2.2.1
- v12 compatibility.
## v2.2.0
- Re-added "core.compendiumConfiguration" setting to exports to export Compendium Folder structure mappings.
- Settings will try to map the folder IDs to the new world's folder IDs based on the compendium key.
- Added supporting folder data structure to the export.
- Settings will utilise the supporting folder data structure to re-create the folders where it can.
- **Important note**: any exports prior to v2.2.0 won't have this support folder data included in the exported file so might not map correctly on the new world. *Please export your world settings again.*
- See [issue #45](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/45).
## v2.1.9
- Added a module setting to set the maximum number of characters to display when displaying differences.
- This is to help with the issue where a "maximum call stack size exceeded" error may occur with very large export files (see [issue #43](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/43)).
- The default is 500 characters per difference.
- Added machine translations for the new setting strings:
- German
- Japanese
- Portuguese
## v2.1.8
- Exclude "core.compendiumConfiguration" setting from exports due to the folder ID mapping not being consistent across worlds anyway.
## v2.1.7
- Mark compatible with v11.
- Append world name to the generated settings file on export.
- Added Portuguese by vithort
## v2.1.6
- Exclude invalid settings from export [#35](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/35)
## v2.1.5
- Allow non-GM users to use the module.
- Fixes [#32](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/32)
- Users will see errors for any settings they do not have permission to update.
- Removed deprecated fields in module manifest.
- Fixes [#33](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/33)
## v2.1.4
- Added timestamp to file export.
- As requested in [#30](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/30)
- Re-added "Save as JSON" which was removed in v2.1.2 as requested in [#29](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/29)
- Have renamed this to `Copy as JSON` to help differentiate it from the `Export Settings` option.
## v2.1.3
- Fixed issue where having a player other than GM in the world would prevent the importer from correctly importing anything.
## v2.1.2
* Better handling of config options to make it less likely to break on certain module's configuration settings.
* Added more logging to assist with any future issues.
* Adjust process order to make sure that the server has time to process the updates before the client reloads.
* Removed the `Save as JSON` context menu option as it was rarely the desired action and was confusing (it was not the version that allowed the user to import the settings).
### v2.1.1
* Fixed settings stored in an object not being compared correctly.
* Importantly, this makes the "Enabled Modules" setting to be correctly saved and imported.
### v2.1.0
* Added compatibility with Foundry VTT v10
* Sorted the settings in the import dialog to make them easier to find.
* Client settings are now correctly exported and imported.
* Request: [#10](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/10)
* Grouped the settings by their module and collapsed them by default.
* Request: [#13](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/13)
* Selecting (or unselecting) a setting will now remember that state across imports, saving you time from choosing just the selections you want.
* Request: [#18](https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues/18)
### v2.0.7
* Added German translation thanks to brockhaus
* Added Foundry VTT Core v9 compatibility
### v2.0.6
* Added Japanese translation thanks to touge
### v2.0.5
* Fix client side settings not importing in v0.8.x
### v2.0.4
* Allow module developers to opt out of settings being copied.
* Re-added bug reporter compatibility since 0.8.2.
### v2.0.3
* Added 0.8.4 compatibility
### v2.0.2
* Tested 0.8.1 compatibility
### v2.0.1
* Merged world and player settings into a single json to simplify use.
* Added differential style selection for world settings import.
* Finished adding localization support.
### v2.0.0
* Migrated to [League of Foundry Developers](https://discord.gg/gzemMfHURH) stewardship
* Added Settings configuration in addition to the right-click context menu
* Added export/import of player settings including differential based selection
* Tested and bumped compatible core version to 0.8.0
### v1.1.1
* Added 0.6.6 compatibility
## v1.1.0
* Added manifest URL link to JSON environment structure
* Added options to export and import game settings. Non-GM users can only import client-side settings.
* Tested and bumped compatible core version to 0.7.1
### v1.0.1
* Initial release

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "Als Text kopieren",
"save": "Als JSON kopieren",
"export": "Einstellungen exportieren",
"import": "Einstellungen importieren"
},
"settings": {
"max-diff": "Anzahl von Charakteren",
"max-diff-hint": "Maximale Anzahl der pro Unterschied anzuzeigenden Zeichen. Auf 0 setzen, um alle anzuzeigen."
},
"title": "Einstellungen importieren",
"intro": "Bitte wähle aus, welche Welt- und Spieler-Einstellungen Du importieren möchtest.",
"message": "Die Liste wurde generiert mit Forien's Copy Environment: https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "System Daten wurden in die Zwischenablage kopiert.",
"updatedReloading": "Einstellungen der Welt erneuert. Lade die Welt in 5 Sekunden neu...",
"import": {
"title": "Welt Einstellungen",
"save": "Import Einstellungen",
"playerList": "Importiere die Einstellungen der folgenden Spieler:",
"existing": "Importiere einen existierenden Export:",
"property": "Eigenschaft",
"newValue": "Neuer Wert",
"currentValue": "Aktueller Wert",
"notFound": "Die folgenden Spieler in der Import Datei existieren nicht in dieser Welt und werden ausgelassen.",
"existingValue": "Bereits existierende Einstellungen, die unverändert sind und deshalb ausgelassen werden:",
"existingPlayerValues": "Bereits existierende Spieler, die unverändert sind und deshalb ausgelassen werden:",
"updatedPlayer": "Spieler Einstellungen angepasst für: {name}",
"noChanges": "Es gibt keinen Unterschied zwischen der aktuellen Welt und den zu importierenden Einstellungen.",
"showSettings": "{count} Einstellungen anzeigen",
"warning": "Warnung",
"warningMessage": "Sie haben sich entschieden, die ausgewählten Module nicht zu importieren. Falls dies unbeabsichtigt war, aktivieren Sie den oben stehenden Wert \"core.moduleConfiguration\""
}
}
}

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "Copy as text",
"save": "Copy as JSON",
"export": "Export Settings",
"import": "Import Settings"
},
"settings": {
"max-diff": "Number of characters",
"max-diff-hint": "Maximum number of characters to show per difference. Set to 0 to show all."
},
"title": "Import Settings",
"intro": "This form allows you to select which world and player settings you want to import.",
"message": "List generated with Forien's Copy Environment: https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "Environment data copied to clipboard!",
"updatedReloading": "Updated world settings. Reloading world in 5sec...",
"import": {
"title": "World settings",
"save": "Import settings",
"playerList": "Import settings for the following players:",
"existing": "Import an existing Export:",
"property": "Property",
"newValue": "New Value",
"currentValue": "Current Value",
"notFound": "The following users in the import file do not exist in this world and will be skipped.",
"existingValue": "Existing values that are unchanged and will be skipped:",
"existingPlayerValues": "Existing players that are unchanged and will be skipped:",
"updatedPlayer": "Updated player settings for: {name}",
"noChanges": "There are no differences between the current world and the imported settings.",
"showSettings": "Show {count} settings",
"warning": "Warning",
"warningMessage": "You have chosen not to import the selected modules. If this was unintentional, enable the \"core.moduleConfiguration\" value above."
}
}
}

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "Copier comme texte",
"save": "Copier comme JSON",
"export": "Exporter les réglages",
"import": "Importer les réglages"
},
"settings": {
"max-diff": "Nombre de caractères",
"max-diff-hint": "Nombre maximal de caractères pour montrer par différence. Régler à 0 pour tout montrer."
},
"title": "Importer les réglages",
"intro": "Ce formulaire vous permet de sélectionner les réglages du monde et des joueurs que vous souhaitez importer.",
"message": "Liste générée avec Forien's Copy Environment: https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "Données d'environnement copiées dans le presse-papier!",
"updatedReloading": "Réglages du monde mises à jour. Rechargement du monde dans 5 sec...",
"import": {
"title": "Réglages du monde",
"save": "Réglages d'importation",
"playerList": "Importer les réglages pour les joueurs suivants:",
"existing": "Importer un export existant:",
"property": "Propriété",
"newValue": "Nouvelle Valeur",
"currentValue": "Valeur actuelle",
"notFound": "Les utilisateurs suivants dans le fichier d'importation n'existent pas dans ce monde et seront ignorés.",
"existingValue": "Les valeurs existantes qui ne sont pas modifiées seront ignorées:",
"existingPlayerValues": "Les joueurs existants qui ne sont pas modifiés seront ignorés:",
"updatedPlayer": "Paramètre des joueurs mis à jour pour: {name}",
"noChanges": "Il n'y a pas de différences entre le monde actuel et les réglagess importés.",
"showSettings": "Afficher {count} paramètres",
"warning": "Avertissement",
"warningMessage": "Vous avez choisi de ne pas importer les modules sélectionnés. Si cela nétait pas intentionnel, activez la valeur \"core.moduleConfiguration\" ci-dessus."
}
}
}

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "Copia come Testo",
"save": "Copia come JSON",
"export": "Esporta Impostazioni",
"import": "Importa Impostazioni"
},
"settings": {
"max-diff": "Numero di caratteri",
"max-diff-hint": "Massimo numero di caratteri da mostrare per differenza. Imposta a 0 per mostrare tutto."
},
"title": "Importa Impostazioni",
"intro": "Questo modulo ti consente di selezionare quali impostazioni del mondo e del giocatore desideri importare.",
"message": "Elenco generato con Forien's Copy Environment: https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "Dati ambientali copiati negli appunti!",
"updatedReloading": "Impostazioni del mondo aggiornate. Ricaricamento mondo in 5sec...",
"import": {
"title": "Impostazioni Mondo",
"save": "Importa Impostazioni",
"playerList": "Importa Impostazioni per i seguenti giocatori:",
"existing": "Importa Esportazione esistente:",
"property": "Proprietà",
"newValue": "Nuovo Valore",
"currentValue": "Valore Attuale",
"notFound": "I seguenti utenti nel file di importazione non esistono in questo mondo e verranno ignorati.",
"existingValue": "Valori esistenti che non sono cambiati e verranno ignorati:",
"existingPlayerValues": "Giocatori esistenti che non sono cambiati e verranno ignorati:",
"updatedPlayer": "Impostazioni giocatore aggiornate per: {name}",
"noChanges": "Non ci sono differenze tra il mondo attuale e le impostazioni importate.",
"showSettings": "Mostra {count} impostazioni",
"warning": "Avvertenza",
"warningMessage": "Hai scelto di non importare i moduli selezionati. Se questo non era intenzionale, abilita il valore \"core.moduleConfiguration\" sopra."
}
}
}

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "クリップボードにコピー",
"save": "JSONとしてコピー",
"export": "設定のエクスポート",
"import": "設定のインポート"
},
"settings": {
"max-diff": "文字数",
"max-diff-hint": "差異ごとに表示する最大文字数。 すべてを表示するには 0 に設定します。"
},
"title": "インポート設定",
"intro": "このフォームでは、ワールドとプレイヤーのどの設定をインポートするかを選択します。",
"message": "このリストは「Forien's Copy Environment」で作成されました https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "環境データをクリップボードにコピーしました!",
"updatedReloading": "ワールド設定を更新しました。5秒後に再読み込みします……",
"import": {
"title": "ワールドの設定",
"save": "インポート設定",
"playerList": "次のプレイヤー設定をインポートします:",
"existing": "既存のエクスポートをインポートします:",
"property": "プロパティ",
"newValue": "新しい値",
"currentValue": "現在の値",
"notFound": "インポートファイルに含まれる次のユーザーは、このワールドに存在しないため、スキップされます。",
"existingValue": "次の値は変更がないためスキップされます:",
"existingPlayerValues": "次のプレイヤーは変更がないためスキップされます:",
"updatedPlayer": "次のプレイヤーの設定を更新しました:{name}",
"noChanges": "現在のワールド設定とインポートした設定に差異がありません。",
"showSettings": "{count} の設定を表示",
"warning": "警告",
"warningMessage": "選択したモジュールをインポートしないことを選択しました。これが意図しない場合は、上記の「core.moduleConfiguration」値を有効にしてください。"
}
}
}

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "Kopiuj jako tekst",
"save": "Kopiuj jako JSON",
"export": "Eksportuj ustawienia",
"import": "Importuj ustawienia"
},
"settings": {
"max-diff": "Liczba znaków",
"max-diff-hint": "Maksymalna liczba znaków do wyświetlenia różnicy. Ustaw na 0, aby wyświetlić wszystkie."
},
"title": "Importuj ustawienia",
"intro": "Ten formularz pozwala wybrać ustawienia świata i gracza, które mają zostać zaimportowane.",
"message": "Lista wygenerowana za pomocą Forien's Copy Environment: https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "Dane środowiskowe skopiowane do schowka!",
"updatedReloading": "Zaktualizowano ustawienia świata. Przeładowanie świata za 5 sekund...",
"import": {
"title": "Ustawienia świata",
"save": "Importuj ustawienia",
"playerList": "Importuj ustawienia dla następujących graczy:",
"existing": "Importowanie istniejącego eksportu:",
"property": "Własność",
"newValue": "Nowa wartość",
"currentValue": "Obecna wartość",
"notFound": "Następujący użytkownicy w pliku importu nie istnieją w tym świecie i zostaną pominięci.",
"existingValue": "Istniejące wartości, które pozostaną niezmienione i zostaną pominięte:",
"existingPlayerValues": "Istniejący gracze, którzy nie ulegli zmianie i zostaną pominięci:",
"updatedPlayer": "Zaktualizowano ustawienia gracza dla: {name}",
"noChanges": "Nie ma żadnych różnic między obecnym światem a zaimportowanymi ustawieniami.",
"showSettings": "Pokaż {count} ustawień",
"warning": "Ostrzeżenie",
"warningMessage": "Wybrane moduły nie zostały zaimportowane. Jeśli było to niezamierzone, włącz powyższą wartość „core.moduleConfiguration”."
}
}
}

View File

@@ -0,0 +1,36 @@
{
"forien-copy-environment": {
"menu": {
"copy": "Copiar como texto",
"save": "Copiar como JSON",
"export": "Exportar Configurações",
"import": "Importar Configurações"
},
"settings": {
"max-diff": "Número de caracteres",
"max-diff-hint": "Número máximo de caracteres a serem exibidos por diferença. Defina como 0 para mostrar tudo."
},
"title": "Importar Configurações",
"intro": "Este formulário permite que você selecione quais configurações do mundo e jogador você deseja importar.",
"message": "Lista gerada com Forien's Copy Environment: https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"copiedToClipboard": "Dados do ambiente copiados para a área de transferência!",
"updatedReloading": "Configurações do mundo atualizadas. Recarregando o mundo em 5 segundos...",
"import": {
"title": "Configurações do mundo",
"save": "Configurações de importação",
"playerList": "Importar configurações para os seguintes jogadores:",
"existing": "Importar uma Exportação existente:",
"property": "Propriedade",
"newValue": "Novo Valor",
"currentValue": "Valor Atual",
"notFound": "Os seguintes usuários no arquivo de importação não existem neste mundo e serão ignorados",
"existingValue": "Valores existentes que permanecem inalterados e serão ignorados:",
"existingPlayerValues": "Jogadores existentes que permanecem inalterados e serão ignorados:",
"updatedPlayer": "Configurações do jogador atualizadas para: {name}",
"noChanges": "Não há diferenças entre o mundo atual e as configurações importadas.",
"showSettings": "Mostrar {count} configurações",
"warning": "Aviso",
"warningMessage": "Você escolheu não importar os módulos selecionados. Se isso não foi intencional, ative o valor \"core.moduleConfiguration\" acima."
}
}
}

View File

@@ -0,0 +1,83 @@
{
"id": "forien-copy-environment",
"name": "forien-copy-environment",
"title": "Forien's Copy Environment",
"description": "Allows for copying list of system/modules and versions, and gives ability to export/import game and player settings",
"author": "Blair McMillan",
"authors": [
{
"name": "Blair McMillan",
"url": "https://github.com/sneat",
"discord": "blair#9056"
},
{
"name": "Forien",
"url": "https://www.patreon.com/forien",
"discord": "Forien#2130"
}
],
"url": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment",
"bugs": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/issues",
"changelog": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/blob/master/changelog.md",
"flags": {
"allowBugReporter": true
},
"minimumCoreVersion": "0.6.0",
"compatibleCoreVersion": "12",
"version": "v2.2.4",
"compatibility": {
"minimum": "0.6.0",
"verified": "12"
},
"scripts": [],
"esmodules": [
"/scripts/module.js"
],
"styles": [
"/styles/module.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "languages/en.json"
},
{
"lang": "ja",
"name": "日本語",
"path": "languages/ja.json"
},
{
"lang": "de",
"name": "Deutsch",
"path": "languages/de.json"
},
{
"lang": "pt",
"name": "Português",
"path": "languages/pt.json"
},
{
"lang": "pl",
"name": "Polski",
"path": "languages/pl.json"
},
{
"lang": "it",
"name": "Italiano",
"path": "languages/it.json"
},
{
"lang": "fr",
"name": "Français",
"path": "languages/fr.json"
}
],
"packs": [],
"socket": false,
"manifest": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/releases/latest/download/module.json",
"download": "https://github.com/League-of-Foundry-Developers/foundryvtt-forien-copy-environment/releases/download/v2.2.4/module.zip",
"protected": false,
"coreTranslation": false,
"library": true
}

View File

@@ -0,0 +1,28 @@
export const name = 'forien-copy-environment';
export const templates = {
settings: `modules/${name}/templates/settings.html`,
};
export function isV10orNewer() {
const gameVersion = game.version || game.data.version;
return gameVersion === '10.0' || foundry.utils.isNewerVersion(gameVersion, '10');
}
export function log(force, ...args) {
try {
if (typeof force !== "boolean") {
console.warn(
'Copy Environment | Invalid log usage. Expected "log(force, ...args)" as boolean but got',
force
);
}
const isDebugging = window.DEV?.getPackageDebugValue(name);
if (force || isDebugging) {
console.log("Copy Environment |", ...args);
}
} catch (e) {
}
}

View File

@@ -0,0 +1,685 @@
import { name, isV10orNewer, templates, log } from './config.js';
import Setting from './setting.js';
export default class Core extends FormApplication {
/**
* @param {Array.<Object>} settings Read from previously exported settings
*/
constructor(settings) {
super();
this.settingGroups = new Map();
this.settings = [];
this.hasWorldSettings = false;
this.playerSettings = [];
this.hasPlayerSettings = false;
this.notChangedSettings = [];
this.notChangedPlayers = [];
this.notFoundPlayers = [];
this.selectedProperties = game.settings.get(name, 'selected-properties') || {
'core.time': false,
'pf2e.worldClock.worldCreatedOn': false,
};
this.supportingData = {};
if (settings && Array.isArray(settings)) {
log(true, 'Parsing provided settings', settings);
settings.forEach((data) => {
try {
let setting = new Setting(data);
if (setting) {
switch (setting.type) {
case Setting.WorldType:
if (setting.hasChanges()) {
if (!this.settingGroups.has(setting.value.group)) {
this.settingGroups.set(setting.value.group, []);
}
this.settingGroups.get(setting.value.group).push(setting.value);
if (typeof this.selectedProperties[setting.value.key] === 'undefined') {
this.selectedProperties[setting.value.key] = true;
}
this.hasWorldSettings = true;
} else {
this.notChangedSettings.push(setting.data.key);
}
break;
case Setting.PlayerType:
if (!setting.hasChanges()) {
this.notChangedPlayers.push(setting.data.name);
break;
}
if (setting.value.playerNotFound) {
this.notFoundPlayers.push(setting.value);
break;
}
for (const [key, val] of Object.entries(setting.value.playerDifferences)) {
const combinedKey = `${setting.value.name}--${key}`;
if (typeof this.selectedProperties[combinedKey] === 'undefined') {
this.selectedProperties[combinedKey] = true;
}
}
for (const [key, val] of Object.entries(setting.value.playerFlagDifferences)) {
const combinedKey = `${setting.value.name}--flag--${key}`;
if (typeof this.selectedProperties[combinedKey] === 'undefined') {
this.selectedProperties[combinedKey] = true;
}
}
this.playerSettings.push(setting.value);
this.hasPlayerSettings = true;
break;
case Setting.SupportingDataType:
// Merge setting value with existing support data
this.supportingData = foundry.utils.mergeObject(this.supportingData, setting.value);
break;
default:
throw new Error(`Unknown setting type: ${setting.type}`);
}
}
} catch (e) {
console.error('Copy Environment |', 'Error importing setting:', data, e);
}
});
}
this.settings = Object.entries(Object.fromEntries(this.settingGroups));
this.settings.sort((a, b) => a[0].localeCompare(b[0]));
for (const playerSetting of this.playerSettings) {
playerSetting.playerDifferences = Object.entries(playerSetting.playerDifferences);
playerSetting.playerDifferences.sort((a, b) => a[0].localeCompare(b[0]));
playerSetting.playerFlagDifferences = Object.entries(playerSetting.playerFlagDifferences);
playerSetting.playerFlagDifferences.sort((a, b) => a[0].localeCompare(b[0]));
}
log(true, 'Processing world settings', this.settings);
log(true, 'Processing player settings', this.playerSettings);
log(true, 'Selected Properties', this.selectedProperties);
}
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ['copy-environment-settings'],
height: 'auto',
width: Math.ceil(window.innerWidth / 2),
id: `${name}-settings`,
title: `${name}.title`,
template: templates.settings,
});
}
// shouldShowCoreModuleWarning returns true if the core module configuration is not selected.
shouldShowCoreModuleWarning() {
return !this.selectedProperties['core.moduleConfiguration'];
}
getData() {
return {
settings: this.settings,
playerSettings: this.playerSettings,
hasWorldSettings: this.hasWorldSettings,
hasPlayerSettings: this.hasPlayerSettings,
hasChanges: this.hasWorldSettings || this.hasPlayerSettings,
notChangedSettings: this.notChangedSettings,
notChangedPlayers: this.notChangedPlayers,
notFoundPlayers: this.notFoundPlayers,
selectedProperties: this.selectedProperties,
shouldShowCoreModuleWarning: this.shouldShowCoreModuleWarning(),
};
}
activateListeners(html) {
super.activateListeners(html);
const updateCheckboxStates = (el) => {
let overall = $(el.closest('tbody')).find('input[type=checkbox].toggle-selections')[0];
if ($(el).data()?.type === 'core' || $(el).data()?.type === 'flag') {
overall = $(el.closest('fieldset')).find('input[type=checkbox].toggle-selections')[0];
}
if (!overall) {
return;
}
const options = $(el.closest('tbody')).find(
'input[type=checkbox]:not(.toggle-selections)'
);
var checkedCount = 0;
for (var i = 0; i < options.length; i++) {
if (options[i].checked) {
checkedCount++;
}
}
if (checkedCount === 0) {
overall.checked = false;
overall.indeterminate = false;
} else if (checkedCount === options.length) {
overall.checked = true;
overall.indeterminate = false;
} else {
overall.checked = false;
overall.indeterminate = true;
}
};
html.on('click', '.close', () => {
this.close();
});
html.on('change', '.toggle-all-selections', (el) => {
$(el.target.closest('fieldset'))
.find('td input')
.not(el.target)
.prop('checked', el.target.checked)
.change();
});
html.on('change', '.toggle-selections', (el) => {
$(el.target.closest('tbody'))
.find('td input')
.not(el.target)
.prop('checked', el.target.checked)
.change();
});
html.on('click', '.show-settings', (el) => {
$(el.target.closest('tbody'))
.find('tr')
.not(el.target.closest('tr'))
.toggleClass('none');
});
html.on('change', 'input[type=checkbox]', (el) => {
if (!el.target.name) {
return;
}
console.log(`Setting ${el.target.name} to ${el.target.checked}`);
this.selectedProperties[el.target.name] = el.target.checked;
game.settings.set(name, 'selected-properties', this.selectedProperties);
if (el.target.name === 'core.moduleConfiguration') {
// Update the warning visibility when the core module configuration setting changes.
$(el.target.closest('form'))
.find('.core-module-warning')
.toggleClass('hidden', !this.shouldShowCoreModuleWarning());
}
updateCheckboxStates(el.target);
});
html.on('click', '.import', async () => {
let changed = false;
for (let field of Array.from(this.form.getElementsByTagName('fieldset')).sort(f => f.dataset?.type === 'player' ? -1 : 1)) {
let targetType = field.dataset?.type;
if (!targetType) {
log(false, 'Could not find fieldset target type');
continue;
}
switch (targetType) {
case 'world':
changed = await this.importWorldSettings(field) || changed;
break;
case 'player':
changed = await this.importPlayerSettings(field) || changed;
break;
}
}
if (changed) {
ui.notifications.info(game.i18n.localize('forien-copy-environment.updatedReloading'), {permanent: true});
window.setTimeout(window.location.reload.bind(window.location), 6000);
}
this.close();
});
html.find('tbody').each((i, el) => {
updateCheckboxStates($(el).find('input[type=checkbox]:first'));
});
}
async importWorldSettings(fieldset) {
let changes = [];
for (let input of fieldset.elements) {
if (!input.checked || !input.name) {
continue;
}
const target = input.dataset?.for;
if (!target) {
continue;
}
const [group, val] = target.split('--');
if (!this.settings[group] || !this.settings[group][1] || !this.settings[group][1][val]) {
log(false, 'Import world settings: could not find target for', input);
continue;
}
log(false, 'Importing world setting', this.settings[group][1][val]);
changes.push(this.settings[group][1][val]);
}
if (!changes.length) {
return false;
}
try {
await this.processSettings(changes);
} catch (e) {
console.error('Import world settings: error', e);
return false;
}
return true;
}
async importPlayerSettings(fieldset) {
let targetUser = null;
let changes = {
flags: {},
};
for (let input of fieldset.elements) {
if (!input.checked || !input.name) {
continue;
}
let target = input.dataset?.for;
if (!this.playerSettings[target]) {
log(true, 'Import player settings: could not find target for', input);
continue;
}
let type = input.dataset?.type;
if (!type) {
log(true, 'Import player settings: missing type (core or flag)');
continue;
}
if (!targetUser) {
targetUser = game.users.getName(this.playerSettings[target].name);
}
const fieldName = input.name.split('--').pop();
if (!type) {
log(true, 'Import player settings: missing value for', input.name);
continue;
}
if (type === 'core') {
changes[fieldName] =
Object.fromEntries(this.playerSettings[target].playerDifferences)[fieldName].newVal;
}
if (type === 'flag') {
changes.flags[fieldName] =
Object.fromEntries(this.playerSettings[target].playerFlagDifferences)[fieldName].newVal;
}
}
if (!targetUser) {
log(true, 'No targetUser found.');
return false;
}
if (Object.keys(changes).length === 1 && (typeof foundry.utils.isEmpty === 'function' ? foundry.utils.isEmpty(changes.flags) : foundry.utils.isObjectEmpty(changes.flags))) {
log(true, 'No changes selected for', targetUser?.name);
return false;
}
log(true, `Updating ${targetUser.name} with`, changes);
await targetUser.update(changes);
ui.notifications.info(
game.i18n.format('forien-copy-environment.import.updatedPlayer', {
name: targetUser.name,
})
);
return true;
}
static download(data, filename) {
if (!filename) {
log(false, 'Missing filename on download request');
return;
}
let jsonStr = JSON.stringify(data, null, 2);
saveDataToFile(jsonStr, 'application/json', filename);
}
static getText() {
const system = isV10orNewer() ? game.data.system : game.data.system.data;
const core = game.version || game.data.version;
let text = `Core Version: ${core}\n\n`;
const systemAuthors = system.authors.length ? system.authors.map(a => {
if (typeof a === 'string') {
return a;
}
return a.name;
}) : [system.author];
text += `System: ${(system.id ?? system.name)} ${system.version} (${Array.from(new Set(systemAuthors)).join(', ')}) \n\n`;
text += `Modules: \n`;
Core.getModulesForExport().forEach((m) => {
const moduleAuthors = m.authors.length ? m.authors.map(a => {
if (typeof a === 'string') {
return a;
}
return a.name;
}) : [m.author];
text += `${(m.id ?? m.name)} ${m.version} (${Array.from(new Set(moduleAuthors)).join(', ')})\n`;
});
text += `\n${game.i18n.localize('forien-copy-environment.message')}`;
log(true, text);
return text;
}
static copyAsText() {
let text = this.getText();
const el = document.createElement('textarea');
el.value = text;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
ui.notifications.info(
game.i18n.localize('forien-copy-environment.copiedToClipboard'),
{},
);
}
static getModulesForExport() {
return (isV10orNewer() ? game.modules : game.data.modules).map(m => {
let mod;
if (isV10orNewer()) {
mod = m.toObject();
} else {
mod = m.data.toObject();
}
mod.active = m.active;
return mod;
}).filter((m) => m.active);
}
static saveSummaryAsJSON() {
const system = isV10orNewer() ? game.data.system : game.data.system.data;
const systemAuthors = system.authors.length ? system.authors.map(a => {
if (typeof a === 'string') {
return a;
}
return a.name;
}) : [system.author];
const data = {};
data.core = {
version: game.version || game.data.version,
};
data.system = {
id: system.id,
version: system.version,
author: Array.from(new Set(systemAuthors)).join(', '),
manifest: system.manifest,
};
data.modules = Core.getModulesForExport().map((m) => {
const moduleAuthors = m.authors.length ? m.authors.map(a => {
if (typeof a === 'string') {
return a;
}
return a.name;
}) : [m.author];
return {
id: m.id || m.name,
version: m.version,
author: Array.from(new Set(moduleAuthors)).join(', '),
manifest: m.manifest,
};
});
this.download(data, Core.getFilename('foundry-environment'));
}
static exportGameSettings() {
const excludeModules = game.data.modules.filter((m) => m.flags?.noCopyEnvironmentSettings || m.data?.flags?.noCopyEnvironmentSettings).map((m) => m.id) || [];
// Return an array with both the world settings and player settings along with their support data.
let data = Array.prototype.concat(
Array.from(game.settings.settings)
.filter(([k, v]) => {
try {
const value = game.settings.get(v.namespace, v.key);
let sameValue = value === v.default;
if (value && typeof value === 'object' && v.default && typeof v.default === 'object') {
sameValue = !Object.keys(foundry.utils.diffObject(v.default, value)).length && !Object.keys(foundry.utils.diffObject(value, v.default)).length;
}
return !sameValue && !excludeModules.some((e) => v.namespace === e);
} catch (e) {
console.error(`Copy Environment | Could not export settings for ${v.namespace}.${v.key} due to an error. Please report this as an issue on GitHub.`, e);
return false;
}
})
.map(([k, v]) => ({
key: k,
value: JSON.stringify(game.settings.get(v.namespace, v.key)),
}))
.sort((a, b) => a.key.localeCompare(b.key)),
game.users.map((u) => {
const userData = isV10orNewer() ? u : u.data;
return {
name: userData.name,
core: {
avatar: userData.avatar,
color: userData.color,
permissions: userData.permissions,
role: userData.role,
},
flags: userData.flags,
};
}),
[
{
type: Setting.SupportingDataType,
value: {
compendiumFolders: game.folders.filter((f) => f.type === 'Compendium').map(f => f.toObject()),
}
}
],
);
this.download(data, Core.getFilename('foundry-settings-export'));
}
static padNumber(number) {
return (number < 10 ? '0' : '') + number;
}
static getFilename(filename) {
const now = new Date();
const yyyy = now.getFullYear();
const MM = Core.padNumber(now.getMonth() + 1); // getMonth() is zero-based
const dd = Core.padNumber(now.getDate());
const hh = Core.padNumber(now.getHours());
const mm = Core.padNumber(now.getMinutes());
const ss = Core.padNumber(now.getSeconds());
return `${filename}-${yyyy}-${MM}-${dd}-${hh}-${mm}-${ss}-${game.world.id}.json`;
}
static importGameSettingsQuick() {
const input = $('<input type="file">');
input.on('change', this.importGameSettings);
input.trigger('click');
}
static importGameSettings() {
const file = this.files[0];
if (!file) {
log(false, 'No file provided for game settings importer.');
return;
}
readTextFromFile(file).then(async (result) => {
try {
const settings = JSON.parse(result);
let coreSettings = new Core(settings);
coreSettings.render(true);
} catch (e) {
console.error('Copy Environment | Could not parse import data.', e);
console.error('Copy Environment | If you see an error for "maximum call stack size exceeded", try reducing the "Number of Characters" setting.');
}
});
}
async processSettings(settings) {
if (foundry.utils.isNewerVersion((game.version || game.data.version), '0.7.9')) {
const updates = [];
const creates = [];
for (const data of settings) {
const config = game.settings.settings.get(data.key);
if (config?.scope === 'client') {
const storage = game.settings.storage.get(config.scope);
if (storage) {
storage.setItem(data.key, data.value);
}
} else if (game.user.isGM) {
const existing = game.data.settings.find((s) => s.key === data.key);
if (data.key === 'core.compendiumConfiguration') {
// The Compendium Configuration setting maps compendiums to folders, and the FolderIDs
// change in a new world, so migrating this value as is breaks the mapping.
// Attempt to update the IDs to match the new world, but if that fails, just use the
// existing value.
try {
const existingCompendiumMap = JSON.parse(existing.value);
const newCompendiumMap = JSON.parse(data.value);
const missingEntries = new Map();
// Replace IDs in the new map with the existing IDs if they exist.
for (const [key, value] of Object.entries(newCompendiumMap)) {
if (game.folders.get(existingCompendiumMap[key]?.folder)) {
newCompendiumMap[key].folder = existingCompendiumMap[key].folder;
} else {
missingEntries.set(key, value);
}
}
// Add any missing entries to the new map based on the supporting data.
for (const [key, value] of missingEntries) {
const folder = await this.createFolderRecursive(value?.folder);
if (folder?.id) {
newCompendiumMap[key].folder = folder.id;
}
}
data.value = JSON.stringify(newCompendiumMap);
} catch (e) {
console.warn('Copy Environment | Could not process compendium configuration, overwriting value rather than merging.', e);
}
}
if (existing?._id) {
updates.push({
_id: existing._id,
key: data.key,
value: data.value,
});
} else {
creates.push({
key: data.key,
value: data.value,
});
}
}
}
try {
if (updates.length) {
log(true, `Updating ${updates.length} world settings.`, updates);
await SocketInterface.dispatch('modifyDocument', {
type: 'Setting',
action: 'update',
updates: updates,
operation: {
pack: null,
parent: null,
updates: updates,
}
});
}
if (creates.length) {
log(true, `Creating ${creates.length} world settings.`, creates);
await SocketInterface.dispatch('modifyDocument', {
type: 'Setting',
action: 'create',
data: creates,
operation: {
pack: null,
parent: null,
data: creates,
}
});
}
return true;
} catch (e) {
log(true, `Settings update could not be dispatched to server.`);
console.error(e);
}
return false;
}
for (const setting of settings) {
const config = game.settings.settings.get(setting.key);
if (config?.scope === 'client') {
const storage = game.settings.storage.get(config.scope);
storage.setItem(setting.key, setting.value);
} else if (game.user.isGM) {
try {
await SocketInterface.dispatch('modifyDocument', {
type: 'Setting',
action: 'update',
data: setting,
});
return true;
} catch (e) {
log(true, `Setting key ${setting.key} could not be dispatched to server.`);
console.error(e);
}
return false;
}
}
}
// Recursively create folders for compendiums based on the supporting data.
async createFolderRecursive(folderID) {
if (game.folders.get(folderID)) {
return game.folders.get(folderID);
}
const folderData = this.supportingData?.compendiumFolders?.find(f => f._id === folderID);
if (!folderData) {
return undefined;
}
// Create missing folder
console.log(`Copy Environment | Creating missing folder "${folderData.name}" with ID ${folderID}`);
// Check that the parent folder exists
if (folderData.folder && !game.folders.get(folderData.folder)) {
// Create missing parent folder
const parentFolder = await this.createFolderRecursive(folderData.folder);
if (parentFolder?.id) {
folderData.folder = parentFolder.id;
}
}
return Folder.create(folderData, { keepId: true });
}
}

View File

@@ -0,0 +1,77 @@
import {name} from './config.js';
import Core from './core.js';
Hooks.once('init', function () {
game.settings.register(name, 'selected-properties', {
scope: 'client',
config: false,
type: Object,
default: {
'core.time': false,
'pf2e.worldClock.worldCreatedOn': false,
},
});
game.settings.register(name, 'diff-length', {
scope: 'world',
config: true,
type: Number,
default: 500,
name: "forien-copy-environment.settings.max-diff",
hint: "forien-copy-environment.settings.max-diff-hint",
requiresReload: false,
});
});
Hooks.once('devModeReady', ({registerPackageDebugFlag}) => {
registerPackageDebugFlag(name);
});
Hooks.on('renderSettings', function (app, html, data) {
new ContextMenu(html, 'div.game-system, ul#game-details', [
{
name: game.i18n.localize('forien-copy-environment.menu.copy'),
icon: '<i class="far fa-copy"></i>',
callback: () => {
try {
Core.copyAsText();
} catch (e) {
console.error('Copy Environment | Error copying game settings to clipboard', e);
}
},
},
{
name: game.i18n.localize('forien-copy-environment.menu.save'),
icon: '<i class="fas fa-copy"></i>',
callback: () => {
try {
Core.saveSummaryAsJSON();
} catch (e) {
console.error('Copy Environment | Error copying game settings to JSON', e);
}
},
},
{
name: game.i18n.localize('forien-copy-environment.menu.export'),
icon: '<i class="fas fa-file-export"></i>',
callback: () => {
try {
Core.exportGameSettings();
} catch (e) {
console.error('Copy Environment | Error exporting game settings', e);
}
},
},
{
name: game.i18n.localize('forien-copy-environment.menu.import'),
icon: '<i class="fas fa-file-import"></i>',
callback: () => {
try {
Core.importGameSettingsQuick();
} catch (e) {
console.error('Copy Environment | Error importing game settings', e);
}
},
},
]);
});

View File

@@ -0,0 +1,9 @@
import {templates} from './config.js';
export const preloadTemplates = async function () {
return loadTemplates(
Object.keys(templates).map(function (key) {
return templates[key];
})
);
};

View File

@@ -0,0 +1,246 @@
import {isV10orNewer, name as moduleName} from './config.js';
export default class Setting {
/**
* @param {Object} data - either World settings or Player settings
*/
constructor(data) {
this.type = Setting.UnknownType;
this.data = data;
this.value = undefined;
if (!data || typeof data !== 'object') {
console.error('Copy Environment | Unknown setting received:', data);
return this;
}
if (data.key && data.value) {
this.type = Setting.WorldType;
this.value = new WorldSetting(this.data);
} else if (data.name) {
this.type = Setting.PlayerType;
this.value = new PlayerSetting(this.data);
} else if (data.type === Setting.SupportingDataType) {
this.type = Setting.SupportingDataType;
this.value = data.value;
}
}
static UnknownType = '_unknownType';
static PlayerType = '_playerType';
static WorldType = '_worldType';
static SupportingDataType = '_supportingDataType';
isWorldSetting() {
return this.type === Setting.WorldType;
}
isPlayerSetting() {
return this.type === Setting.PlayerType;
}
isSupportingDataSetting() {
return this.type === Setting.SupportingDataType;
}
hasChanges() {
if (!this.value) {
return false;
}
return this.value.hasChanges();
}
}
/**
* WorldSetting represents a world level setting.
*/
export class WorldSetting {
/**
* Create a world setting from Foundry data.
* @param {Object} setting
*/
constructor(setting) {
if (!setting) {
throw 'Invalid data';
}
this.key = setting.key;
this.value = setting.value;
this.difference = this.calculateDifference();
this.group = this.key.split('.').shift();
}
hasChanges() {
return this.difference.hasChanges();
}
/**
* Compares the parsed JSON setting data if possible to handle object order differences.
* @returns {Difference}
*/
calculateDifference() {
const keyParts = this.key.split('.');
const namespace = keyParts.shift();
const key = keyParts.join('.');
let existingSetting;
try {
existingSetting = game.settings.get(namespace, key);
} catch (e) {
// do nothing, it just means the setting isn't registered, likely because the module isn't enabled.
}
try {
let newValue = this.value;
if (newValue) {
newValue = JSON.parse(newValue);
}
if (typeof existingSetting === 'object' && typeof newValue === 'object') {
let diff = foundry.utils.diffObject(existingSetting, newValue);
if (typeof foundry.utils.isEmpty === 'function' ? foundry.utils.isEmpty(diff) : foundry.utils.isObjectEmpty(diff)) {
// No difference in the underlying object.
return new Difference(this.key, null, null);
}
}
if (existingSetting === newValue) {
return new Difference(this.key, null, null);
}
return new Difference(this.key, existingSetting, newValue);
} catch (e) {
console.error('Copy Environment | Could not parse world setting values:', this.key, e);
}
// Return the difference of the original values, not the parsed values.
let existingSettings = game.data.settings.find((s) => s.key === this.key);
return new Difference(this.key, existingSettings?.value, this.value);
}
}
/**
* PlayerSetting represents a player level setting.
*/
export class PlayerSetting {
/**
* Create a player setting from Foundry data.
* @param {Object} setting
*/
constructor(setting) {
if (!setting) {
throw 'Invalid data';
}
this.name = setting.name;
this.playerNotFound = false;
this.playerDifferences = {};
this.playerFlagDifferences = {};
const existingUser = game.users.getName(this.name);
if (!existingUser) {
this.playerNotFound = true;
return this;
}
const userData = isV10orNewer() ? existingUser : existingUser.data;
if (setting.core.color !== userData.color) {
this.playerDifferences.color = new Difference(
'color',
userData.color,
setting.core.color
);
}
if (setting.core.role !== userData.role) {
this.playerDifferences.role = new Difference(
'role',
userData.role,
setting.core.role
);
}
if (JSON.stringify(setting.core.permissions) !== JSON.stringify(userData.permissions)) {
this.playerDifferences.permissions = new Difference(
'permissions',
userData.permissions,
this.data.core.permissions
);
}
let flagDiff = foundry.utils.diffObject(userData.flags, setting.flags);
for (const prop in flagDiff) {
if (!flagDiff.hasOwnProperty(prop)) {
continue;
}
this.playerFlagDifferences[prop] = new Difference(
prop,
userData.flags[prop],
flagDiff[prop]
);
}
this.name = setting.name;
this.value = setting.value;
}
/**
* Returns whether this player setting is identical to a player of the same name in the current world.
* @returns boolean
*/
hasChanges() {
return this.playerNotFound || this.hasDataChanges();
}
/**
* Returns whether this player setting has the same data values as a player of the same name in the current world.
* Note that if there is not a matching player, there are no data changes.
* @see hasChanges
* @returns boolean
*/
hasDataChanges() {
if (typeof foundry.utils.isEmpty === 'function') {
return !foundry.utils.isEmpty(this.playerDifferences) || !foundry.utils.isEmpty(this.playerFlagDifferences);
}
return (
!foundry.utils.isObjectEmpty(this.playerDifferences) ||
!foundry.utils.isObjectEmpty(this.playerFlagDifferences)
);
}
}
/**
* Difference represents the difference between the existing setting and the proposed setting.
*/
export class Difference {
/**
* Create a setting difference.
* @param {string} name
* @param {*} oldValue
* @param {*} newValue
*/
constructor(name, oldValue, newValue) {
this.name = name;
if (oldValue !== newValue) {
let diffSize = game.settings.get(moduleName, "diff-length");
if (diffSize < 0) {
diffSize = 0;
}
this.oldVal = oldValue;
this.oldString = JSON.stringify(oldValue);
if (diffSize && this.oldString?.length > diffSize) {
this.oldString = this.oldString.substring(0, diffSize) + '...';
}
this.newVal = newValue;
this.newString = JSON.stringify(newValue);
if (diffSize && this.newString?.length > diffSize) {
this.newString = this.newString.substring(0, diffSize) + '...';
}
}
}
hasChanges() {
return this.oldVal !== this.newVal;
}
}

View File

@@ -0,0 +1,31 @@
@charset "UTF-8";
#forien-copy-environment-settings fieldset th.property {
text-align: left;
}
#forien-copy-environment-settings fieldset td {
word-break: break-all;
vertical-align: top;
}
#forien-copy-environment-settings fieldset td:first-of-type {
width: 21%;
}
#forien-copy-environment-settings form {
max-height: 100%;
}
#forien-copy-environment-settings form tbody {
border-bottom: 1px solid #777;
}
#forien-copy-environment-settings form tbody tr.none {
display: none;
}
#forien-copy-environment-settings form tbody tr span.show-settings {
text-decoration: underline;
cursor: pointer;
}
#forien-copy-environment-settings section.import-properties {
overflow: auto;
margin-bottom: 10px;
}
#forien-copy-environment-settings .noflex {
flex: 0;
}

View File

@@ -0,0 +1,125 @@
<form class="flexcol {{classes}}" autocomplete="off">
<h2 class="noflex">{{localize 'forien-copy-environment.intro'}}</h2>
<section class="import-properties">
{{#if hasWorldSettings}}
<fieldset name="input world" data-type="world">
<legend>{{localize 'forien-copy-environment.import.title'}}</legend>
<table>
<thead>
<th class="property"><label><input type="checkbox" class="toggle-all-selections">
{{localize 'forien-copy-environment.import.property'}}</label></th>
<th>{{localize 'forien-copy-environment.import.newValue'}}</th>
<th>{{localize 'forien-copy-environment.import.currentValue'}}</th>
</thead>
{{#each settings as |setting|}}
<tbody>
<tr>
<td colspan="3"><label><input type="checkbox" class="toggle-selections"> {{setting.[0]}}</label> <span class="show-settings">({{localize 'forien-copy-environment.import.showSettings' count=setting.[1].length}})</span></td>
</tr>
{{#each setting.[1]}}
<tr class="none">
<td><label for="world-{{key}}"><input name="{{ key }}" id="world-{{key}}" type="checkbox" data-for="{{ @../index }}--{{ @index }}"
data-type="world" {{checked (lookup @root.selectedProperties key)}}>{{ key }}</label></td>
<td class="value">
<label for="world-{{key}}">{{ difference.newString }}</label>
</td>
<td class="value">
<label for="world-{{key}}">{{ difference.oldString }}</label>
</td>
</tr>
{{/each}}
</tbody>
{{/each}}
</table>
</fieldset>
{{/if}}
{{#if notChangedSettings}}
<p><strong>{{localize 'forien-copy-environment.import.existingValue'}}</strong></p>
<ul>
{{#each notChangedSettings}}
<li>{{this}}</li>
{{/each}}
</ul>
{{/if}}
{{#if hasPlayerSettings}}
<p><strong>{{localize 'forien-copy-environment.import.playerList'}}</strong></p>
{{#each playerSettings}}
<fieldset name="input {{ name }}" data-type="player">
<legend>{{ name }}</legend>
<table>
<thead>
<th class="property"><label><input type="checkbox" class="toggle-selections" checked>
{{localize 'forien-copy-environment.import.property'}}</label></th>
<th>{{localize 'forien-copy-environment.import.newValue'}}</th>
<th>{{localize 'forien-copy-environment.import.currentValue'}}</th>
</thead>
<tbody>
{{#each playerDifferences as |diff|}}
{{#with (concat ../name "--" diff.[1].name) as |fkey|}}
<tr>
<td><label for="{{fkey}}"><input name="{{fkey}}" id="{{fkey}}" type="checkbox"
data-for="{{ @../index }}" data-type="core" {{checked (lookup @root.selectedProperties fkey)}}>{{ diff.[1].name }}</label></td>
<td class="value">
<label for="{{fkey}}">{{ diff.[1].newString }}</label>
</td>
<td class="value">
<label for="{{fkey}}">{{ diff.[1].oldString }}</label>
</td>
</tr>
{{/with}}
{{/each}}
{{#each playerFlagDifferences as |diff|}}
{{#with (concat ../name "--flag--" diff.[1].name) as |fdkey|}}
<tr>
<td><label for="{{fdkey}}"><input name="{{fdkey}}" type="checkbox" id="{{fdkey}}" data-for="{{ @../index }}" data-type="flag"
{{checked (lookup @root.selectedProperties fdkey)}}>{{ diff.[1].name }}</label></td>
<td class="value">
<label for="{{fdkey}}">{{ diff.[1].newString }}</label>
</td>
<td class="value">
<label for="{{fdkey}}">{{ diff.[1].oldString }}</label>
</td>
</tr>
{{/with}}
{{/each}}
</tbody>
</table>
</fieldset>
{{/each}}
{{/if}}
{{#if notChangedPlayers}}
<p><strong>{{localize 'forien-copy-environment.import.existingPlayerValues'}}</strong></p>
<ul>
{{#each notChangedPlayers}}
<li>{{this}}</li>
{{/each}}
</ul>
{{/if}}
{{#if notFoundPlayers}}
<p><strong>{{localize 'forien-copy-environment.import.notFound'}}</strong></p>
<ul>
{{#each notFoundPlayers}}
<li>{{ name }}</li>
{{/each}}
</ul>
{{/if}}
{{#unless hasChanges}}
<p><strong>{{localize 'forien-copy-environment.import.noChanges'}}</strong></p>
{{/unless}}
</section>
<div class="noflex core-module-warning {{#unless shouldShowCoreModuleWarning}}hidden{{/unless}}">
<p><strong>{{localize 'forien-copy-environment.import.warning'}}:</strong> {{localize 'forien-copy-environment.import.warningMessage'}}</p>
</div>
<div class="noflex">
{{#if hasChanges}}<button type="button" class="import"><i class="fas fa-save"></i>
{{localize 'forien-copy-environment.import.save'}}</button>{{/if}}
<button type="button" class="close"><i class="fas fa-close"></i> {{localize 'Close'}}</button>
</div>
</form>