Guia para Curadoria Avançada de Conteúdo: Migrando Módulos do Foundry VTT para Cloudflare R2 para a v12
Seção 1: Estrutura Estratégica para Consolidação e Migração de Módulos
1.1. Compreendendo a Arquitetura de Dados Moderna do Foundry VTT
A transição do Foundry VTT da versão 11 para a 12 representa mais do que uma atualização incremental; é uma mudança arquitetÓnica fundamental que exige uma abordagem metódica para a migração de conteúdo. A compreensão dessas mudanças é o alicerce para o sucesso de qualquer projeto de consolidação de módulos.
1.1.1. O Salto da v11 para a v12
A versão 12 do Foundry VTT introduziu mudanças abrangentes no backend, notavelmente a formalização de um DataModel mais estruturado para todos os documentos.1 Esta alteração significa que a estrutura de dados interna de Atores, Itens, Cenas e outros documentos foi padronizada e validada por um esquema rigoroso. Consequentemente, simplesmente copiar arquivos de um módulo v11 para um ambiente v12 resultarÔ em incompatibilidades e falhas, pois os dados antigos não se alinham com o novo esquema esperado. A atualização também trouxe a
Application V2, uma nova abordagem para a renderização da interface do usuĆ”rio, e outras melhorias de backend que exigem que os módulos sejam explicitamente construĆdos e testados para garantir a compatibilidade.3
1.1.2. O Manifesto module.json como o Blueprint
O arquivo module.json serve como o projeto central para qualquer pacote, seja um sistema ou um módulo. Para a v12, sua importĆ¢ncia foi elevada. O manifesto agora contĆ©m um objeto compatibility que permite aos desenvolvedores especificar com precisĆ£o as versƵes do Foundry VTT com as quais o módulo funciona. Os campos minimum e maximum sĆ£o estritamente aplicados pelo software, impedindo a instalação ou ativação de módulos incompatĆveis.6 AlĆ©m disso, o campo
dependencies foi substituĆdo por um objeto relationships mais descritivo, que detalha as dependĆŖncias do módulo em sistemas ou outros módulos.2 A configuração correta deste arquivo Ć© o primeiro passo para que o Foundry VTT reconheƧa e carregue o módulo consolidado.
1.1.3. De NeDB para LevelDB: A Revolução dos Compêndios
Uma das mudanças mais significativas que começaram na v11 e se solidificaram é a tecnologia de banco de dados subjacente para os compêndios. O formato legado, NeDB, armazenava todos os documentos de um compêndio em um único arquivo de texto simples com a extensão .db, como observado na estrutura de arquivos do usuÔrio.8 O novo formato, LevelDB, é um banco de dados de chave-valor de alto desempenho que armazena os dados do compêndio em uma pasta contendo vÔrios arquivos, alguns dos quais são codificados em binÔrio.9
Esta mudanƧa torna a edição manual dos arquivos de compĆŖndio impossĆvel e perigosa. A Ćŗnica metodologia suportada e confiĆ”vel para interagir com esses bancos de dados fora do ambiente do Foundry VTT Ć© atravĆ©s de ferramentas especializadas. O FoundryVTT CLI (Command-Line Interface) foi desenvolvido especificamente para essa finalidade, permitindo que os desenvolvedores ādescompactemā (unpack) os bancos de dados LevelDB ou NeDB em um formato de texto legĆvel por humanos (como JSON) e, em seguida, ārecompactemā (pack) esses arquivos de volta no formato de banco de dados LevelDB.9 Tentar migrar compĆŖndios simplesmente copiando as pastas LevelDB ou editando arquivos
.db manualmente levarĆ” a dados corrompidos e erros de carregamento.11
1.2. A Lógica para Hospedagem Externa de Ativos com Cloudflare R2
A decisĆ£o de hospedar todos os ativos de mĆdia em um serviƧo de armazenamento em nuvem como o Cloudflare R2 nĆ£o Ć© apenas uma preferĆŖncia tĆ©cnica, mas uma melhor prĆ”tica estratĆ©gica para gerenciar grandes coleƧƵes de conteĆŗdo de VTT.
1.2.1. Desempenho e Escalabilidade
Servir ativos de mĆdia (mapas, tokens, arte, mĆŗsica) de uma Rede de Distribuição de ConteĆŗdo (CDN) global como a da Cloudflare melhora drasticamente o desempenho. Quando um jogador se conecta a uma sessĆ£o, seu navegador baixa esses ativos diretamente dos servidores da Cloudflare, que estĆ£o geograficamente mais próximos deles, em vez de baixĆ”-los do servidor do Mestre do Jogo. Isso reduz significativamente os tempos de carregamento do mundo, diminui a latĆŖncia e alivia a carga de largura de banda da conexĆ£o de internet do anfitriĆ£o.14 Dada a grande quantidade de dados de mapas e tokens de criadores como TomCartos e CzeAndPeku, essa abordagem transforma a experiĆŖncia do jogador.8
1.2.2. Portabilidade e Integridade dos Dados
Desacoplar os ativos de mĆdia dos dados do jogo (os documentos do compĆŖndio) torna a instalação do Foundry VTT muito mais enxuta e portĆ”til. A pasta de dados do usuĆ”rio torna-se significativamente menor, simplificando backups, migraƧƵes entre diferentes provedores de hospedagem ou a movimentação de um servidor local para a nuvem.16 Se os ativos estiverem vinculados a caminhos locais, qualquer mudanƧa na estrutura de pastas ou no ambiente de hospedagem quebraria todas as referĆŖncias de imagem e som. Com URLs externas, os dados do jogo permanecem funcionais, independentemente de onde o servidor Foundry VTT esteja sendo executado.
1.2.3. Custo-BenefĆcio do R2
O Cloudflare R2 oferece uma vantagem competitiva crucial para casos de uso de VTT: a ausĆŖncia de taxas de egresso (egress fees). Os serviƧos de armazenamento em nuvem tradicionais, como o Amazon S3, cobram pela quantidade de dados transferidos para fora de seus servidores. Em um cenĆ”rio de VTT, onde vĆ”rios jogadores estĆ£o constantemente baixando mapas e tokens, essas taxas podem se acumular rapidamente. O R2 elimina essa preocupação, tornando-se uma solução extremamente econĆ“mica para a carga de trabalho de alta leitura tĆpica de uma sessĆ£o de RPG.15
1.3. O Plano Mestre de Migração
Este projeto Ć© dividido em cinco fases distintas, formando um fluxo de trabalho lógico desde a configuração da infraestrutura atĆ© a implantação final. Esta abordagem estruturada garante que cada etapa seja concluĆda corretamente antes de prosseguir para a próxima, minimizando a complexidade e o potencial de erros.
-
Fase I: Configuração da Infraestrutura: Estabelecer a base na nuvem. Isso envolve a criação do bucket Cloudflare R2, a configuração dos registros DNS necessÔrios e a implantação do worker de compatibilidade S3, que atua como uma camada de tradução essencial.
-
Fase II: Extração de Dados: Utilizar o FoundryVTT CLI para desconstruir os compêndios v11 existentes. O comando
unpackconverterƔ os arquivos.dbem arquivos-fonte JSON editƔveis, um para cada documento. -
Fase III: Migração de Ativos e Redirecionamento de Caminhos: Realizar o upload em massa de todos os ativos de mĆdia (imagens, tokens) para o bucket R2. Em seguida, executar um script para reescrever programaticamente todos os caminhos de ativos locais nos arquivos JSON extraĆdos para apontar para as novas URLs externas.
-
Fase IV: Recompilação do Módulo: Criar um novo manifesto
module.jsoncompatĆvel com a v12. Usar o comandopackdo CLI para compilar os arquivos JSON modificados de volta para o formato de banco de dados LevelDB, criando os novos compĆŖndios. -
Fase V: Implantação e Teste: Instalar o novo módulo
yan-curated-dataem um ambiente Foundry VTT v12 limpo e realizar testes rigorosos para garantir que todos os dados e ativos sejam carregados corretamente.
Este processo depende fundamentalmente de um fluxo de trabalho de āDescompactar-Modificar-Recompactarā. Os dados do compĆŖndio sĆ£o tratados como código-fonte: extraĆdos para um formato legĆvel, modificados por ferramentas automatizadas e, em seguida, compilados de volta para o formato binĆ”rio final. Isso requer uma configuração de ambiente duplo: o ambiente v11 existente Ć© necessĆ”rio para a extração inicial dos dados, enquanto um ambiente v12 completamente novo e limpo Ć© essencial para a implantação e teste final, evitando qualquer contaminação ou conflito de configuraƧƵes legadas.4
Seção 2: Fase I - Construindo a Infraestrutura na Nuvem: Configurando o Cloudflare R2
A criação de uma base de infraestrutura robusta na Cloudflare Ć© o primeiro passo tĆ©cnico e um dos mais crĆticos. A configuração correta aqui garante que o Foundry VTT possa se comunicar de forma transparente com o armazenamento R2 como se fosse um bucket S3 nativo.
2.1. Configuração do Bucket R2 e Acesso Público
Este processo estabelece o repositório de armazenamento para todos os ativos de mĆdia.15
-
Criação do Bucket: No painel da sua conta Cloudflare, navegue até a seção R2. Crie um novo bucket. à altamente recomendÔvel nomeÔ-lo
foundrypara alinhar com as convenƧƵes dos guias da comunidade e simplificar as configuraƧƵes subsequentes. -
DomĆnios Personalizados e DNS: O Foundry VTT e o worker de compatibilidade exigem dois subdomĆnios para funcionar corretamente. Navegue atĆ© as configuraƧƵes de DNS do seu domĆnio no painel da Cloudflare e crie dois novos registros
CNAME:-
Tipo: CNAME, Nome:
s3, Destino:@(ou seu domĆnio raiz, ex:seudominio.com) -
Tipo: CNAME, Nome: foundry.s3, Destino: @ (ou seu domĆnio raiz, ex: seudominio.com)
Após a criação dos registros DNS, retorne Ć s configuraƧƵes do seu bucket R2. Na seção āAcesso PĆŗblicoā (Public Access), conecte um domĆnio personalizado e adicione o domĆnio especĆfico do bucket: foundry.s3.seudominio.com.
-
-
PolĆtica de CORS: O Cross-Origin Resource Sharing (CORS) Ć© uma medida de seguranƧa do navegador. Uma polĆtica deve ser configurada para permitir que o Foundry VTT, rodando em seu domĆnio principal, solicite e carregue ativos do seu subdomĆnio R2. Nas configuraƧƵes do bucket R2, localize a seção āPolĆtica de CORSā (CORS Policy) e insira a seguinte configuração JSON 15:
JSON
[
{
"AllowedOrigins": ["*"],
"AllowedMethods":,
"AllowedHeaders": ["*"],
"ExposeHeaders":,
"MaxAgeSeconds": 3000
}
]
2.2. O Worker de Compatibilidade S3: O Middleware CrĆtico
O Foundry VTT possui uma integração nativa com o S3 da AWS, mas suas chamadas de API não são idênticas à API de compatibilidade do R2. O Cloudflare Worker atua como uma camada de tradução inteligente, interceptando as solicitações do Foundry, reescrevendo-as para o formato que o R2 entende e assinando-as novamente com as credenciais corretas. Sem este worker, a integração falharÔ.15
-
Implantação do Worker: No painel da Cloudflare, navegue para āWorkers & Pagesā e crie uma nova aplicação, selecionando āCreate Workerā. DĆŖ um nome descritivo.
-
Configuração de Rotas: Navegue para a aba āTriggersā do seu novo worker. Na seção āRoutesā, adicione as seguintes rotas, substituindo
seudominio.compelo seu domĆnio real:-
s3.seudominio.com/* -
foundry.s3.seudominio.com/*
-
-
Código do Worker: Clique em āQuick editā e substitua o código padrĆ£o pelo script completo fornecido no ApĆŖndice A.1. Este script Ć© a implementação padrĆ£o da comunidade para compatibilidade R2-Foundry. Ć crucial que vocĆŖ edite as constantes no topo do script com suas informaƧƵes especĆficas:
my_s3_domain,my_bucket_name,my_account_id,my_access_keyemy_secret_key.
2.3. Configuração S3 do FoundryVTT
O passo final Ć© informar ao seu servidor Foundry VTT como se conectar ao novo endpoint R2.
-
Geração de Token de API: De volta Ć pĆ”gina principal do R2 no painel da Cloudflare, clique em āManage R2 API Tokensā. Crie um novo token com permissƵes de āAdmin Read & Writeā. Copie e armazene com seguranƧa o āAccess Key IDā e o āSecret Access Keyā fornecidos.
-
Criação do Arquivo
s3.json: No seu computador, dentro da pasta de dados do usuĆ”rio do Foundry VTT, navegue atĆ© o diretórioConfig. Crie um novo arquivo chamados3.json. Este arquivo conterĆ” as credenciais e o endpoint para a conexĆ£o. Popule o arquivo com o seguinte conteĆŗdo, substituindo os placeholders pelas suas credenciais e domĆnio 15:
JSON
{
"region": "auto",
"endpoint": "https://s3.seudominio.com",
"credentials": {
"accessKeyId": "SEU_ACCESS_KEY_ID_AQUI",
"secretAccessKey": "SEU_SECRET_ACCESS_KEY_AQUI"
},
"bucket": "foundry"
}
Após salvar este arquivo e reiniciar o Foundry VTT, o novo bucket R2 deve aparecer como uma fonte de armazenamento disponĆvel no Navegador de Arquivos (File Browser).
| Configuração | Valor de Exemplo | Propósito / Local de Uso |
|---|---|---|
| Nome do Bucket | foundry | Identificador Ćŗnico para o seu armazenamento R2. |
| ID da Conta | 0123456789abcdef0123456789abcdef | Usado no script do Worker para construir o endpoint da API R2. |
| Registro CNAME 1 | s3.seudominio.com | Rota para o Worker que lida com chamadas de API de listagem de buckets. |
| Registro CNAME 2 | foundry.s3.seudominio.com | Rota para o Worker que lida com chamadas de API de objetos e acesso pĆŗblico. |
| Access Key ID | a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 | Credencial para autenticação. Usado no script do Worker e no s3.json. |
| Secret Access Key | a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 | Credencial para autenticação. Usado no script do Worker e no s3.json. |
Endpoint URL s3.json | https://s3.seudominio.com | O endereƧo que o Foundry VTT usarƔ para todas as comunicaƧƵes S3. |
Seção 3: Fase II - Desconstruindo a Fonte: Descompactando Compêndios v11
Com a infraestrutura de nuvem pronta, a próxima fase foca na extração dos dados do módulo v11 existente para um formato que possa ser manipulado e modificado.
3.1. Preparando um EspaƧo de Trabalho de Desenvolvimento Limpo
à fundamental realizar todas as operações de modificação fora da pasta de dados ativa do Foundry VTT para evitar a corrupção acidental do seu mundo de jogo funcional.
-
Crie uma nova pasta de projeto em um local conveniente em seu computador (por exemplo,
C:\foundry-migration-project\). -
Navegue atƩ a sua pasta de dados do Foundry v11 (
C:\Users\Yanbd\foundry-data\v11\Data\modules\). -
Copie a pasta inteira
yan-modulopara dentro da sua nova pasta de projeto. Isso cria uma cópia de segurança e um ambiente de trabalho isolado. Todas as operações subsequentes serão realizadas nesta cópia.
3.2. Dominando a Interface de Linha de Comando (CLI) do FoundryVTT
A CLI do FoundryVTT é a ferramenta oficial e indispensÔvel para empacotar e desempacotar compêndios. O seu uso é obrigatório para este projeto.9
-
Instalação: Se ainda não o tiver, instale o Node.js (versão 16.17.0 ou superior). Em seguida, instale a CLI globalmente abrindo um terminal (Command Prompt ou PowerShell) e executando o comando:
npm install -g @foundryvtt/foundryvtt-cli. -
Configuração: A CLI precisa saber onde sua instalação do Foundry VTT v11 estÔ localizada. No terminal, execute
fvtt configure. O programa irÔ guiÔ-lo para definir o caminho para a aplicação Foundry VTT e, mais importante, o caminho para sua pasta de dados do usuÔrio da v11 (C:\Users\Yanbd\foundry-data\v11\Data). -
Trabalhando em um Pacote: Para simplificar os comandos, navegue com o terminal para a sua pasta de projeto (
cd C:\foundry-migration-project\yan-modulo) e defina-a como o pacote de trabalho atual executando:fvtt package workon "yan-modulo" --type "Module". A CLI agora saberÔ que todos os comandos de pacote se referem a este módulo.10
3.3. Executando a Operação de Descompactação
Este é o processo de conversão dos bancos de dados do compêndio em arquivos de texto editÔveis.
-
Dentro da sua pasta de projeto, crie um novo diretório para armazenar os arquivos JSON extraĆdos, por exemplo,
packs-json. -
Liste os arquivos
.dbdentro da pastayan-modulo\packs. VocĆŖ precisarĆ” executar o comandounpackpara cada um deles. -
Para cada compĆŖndio, execute o seguinte comando no terminal (a partir da raiz da pasta
yan-modulo), substituindonome-do-compendiopelo nome do arquivo sem a extensão.db:Bash
fvtt package unpack "nome-do-compendio" --outputDirectory "../packs-json/nome-do-compendio"Por exemplo, para o compĆŖndio
adventures.db, o comando seria:Bash
fvtt package unpack "adventures" --outputDirectory "../packs-json/adventures" -
Repita este processo para todos os compĆŖndios (
aventuras-dmdave-old,criaturas-yan, etc.). Ao final, a pastapacks-jsonconterĆ” uma subpasta para cada compĆŖndio, e dentro de cada uma haverĆ” centenas ou milhares de arquivos.json, cada um representando um Ćŗnico documento (Ator, Cena, etc.).10
Seção 4: Fase III - A Grande Migração: Relocando Ativos e Reescrevendo Caminhos
Esta fase Ć© o coração da migração, onde os ativos de mĆdia sĆ£o movidos para a nuvem e todas as referĆŖncias a eles nos dados do jogo sĆ£o atualizadas para apontar para suas novas localizaƧƵes.
4.1. Upload de Ativos em Massa para o R2
A transferĆŖncia fĆsica dos arquivos de mĆdia para o bucket R2 deve ser feita de forma eficiente e, crucialmente, preservando a estrutura de diretórios original.
-
Ferramenta Recomendada: A ferramenta
rcloneé o padrão da indústria para interações de linha de comando com armazenamento em nuvem. Ela é altamente eficiente para sincronizar grandes volumes de arquivos. Instale e configure orclonepara se conectar ao seu bucket Cloudflare R2, seguindo a documentação oficial dorclone. -
Preservação da Estrutura de Diretórios: à absolutamente vital que a estrutura de pastas dentro do bucket R2 espelhe exatamente a estrutura local. O caminho
modules/yan-modulo/adventures/automaton-atakebune/maps/tom-cartos-atakebune-cargo-grid.webpno R2 deve corresponder ao caminho relativo original. -
Comando de Sincronização: Use o comando
syncdorclonepara fazer o upload. Este comando irƔ copiar apenas os arquivos novos ou alterados, tornando as atualizaƧƵes futuras mais rƔpidas. Execute um comando para cada pasta de ativos principal (adventures, tokens, etc.), por exemplo:Bash
rclone sync "C:\foundry-migration-project\yan-modulo\adventures" "R2-Remote:foundry/adventures" --progress rclone sync "C:\foundry-migration-project\yan-modulo\tokens" "R2-Remote:foundry/tokens" --progressSubstitua
R2-Remotepelo nome que você deu à sua configuração R2 norcloneefoundrypelo nome do seu bucket.
4.2. Redirecionamento ProgramÔtico de Caminhos: O Coração da Migração
A atualização manual de milhares de caminhos de arquivos em arquivos JSON Ć© impraticĆ”vel e propensa a erros. Uma abordagem programĆ”tica Ć© necessĆ”ria. Embora módulos como o Asset Auditor 19 e o Mass Edit 20 sejam excelentes para gerenciar ativos dentro de um mundo ativo, eles nĆ£o sĆ£o a ferramenta ideal para um processo de construção de módulo. Operar diretamente nos arquivos JSON de origem entre as etapas de descompactação e recompactação Ć© um fluxo de trabalho mais limpo, repetĆvel e profissional. Ele garante que o módulo final seja construĆdo a partir de uma fonte de dados pura, sem artefatos de um mundo de jogo intermediĆ”rio.
-
O Script de Substituição em Python: Um script Python é ideal para esta tarefa devido às suas robustas bibliotecas de manipulação de JSON e sistema de arquivos. O script completo é fornecido no Apêndice A.2. Sua lógica de operação é a seguinte:
-
Ele aceita o caminho para a pasta
packs-jsoncomo argumento. -
Percorre recursivamente cada arquivo e subdiretório.
-
Para cada arquivo
.json, ele carrega seu conteĆŗdo. -
Uma função de busca recursiva navega por todo o objeto JSON (dicionÔrios, listas, valores aninhados).
-
Ele procura por valores de string que correspondam ao padrão de um caminho de ativo local (começando com
modules/yan-modulo/). -
Quando um caminho Ć© encontrado, ele substitui o prefixo local
modules/yan-modulo/pela nova URL base do R2 (ex:https://foundry.s3.seudominio.com/). -
O script Ć© projetado para verificar chaves comuns como
img,src,texture.src, e também para analisar o conteúdo de campos HTML (como descrições de itens ou pÔginas de diÔrio) em busca de tags<img>ou<a>e atualizar seus atributossrcouhref. -
O arquivo JSON modificado é então salvo de volta no mesmo local, sobrescrevendo o original.
-
-
Execução: Salve o script (por exemplo, como
update_paths.py) e execute-o a partir do terminal, passando o caminho para sua pastapacks-json:Bash
python update_paths.py "C:\foundry-migration-project\packs-json"
Após a execução do script, todos os caminhos de ativos nos seus arquivos JSON de origem estarão apontando para o seu bucket R2.
| Tipo de Documento | Chave JSON | Caminho Original | URL Transformada |
|---|---|---|---|
| Actor | img | modules/yan-modulo/tokens/aboleth.webp | https://foundry.s3.seudominio.com/tokens/aboleth.webp |
| Scene | background.src | modules/yan-modulo/adventures/bandit-hideout/maps/tom-cartos-bandit-hideout-grid.webp | https://foundry.s3.seudominio.com/adventures/bandit-hideout/maps/tom-cartos-bandit-hideout-grid.webp |
| Tile | texture.src | modules/yan-modulo/adventures/bloodstone-manor/artwork/beam-diagram.webp | https://foundry.s3.seudominio.com/adventures/bloodstone-manor/artwork/beam-diagram.webp |
| JournalEntryPage | src | modules/yan-modulo/adventures/banshee-tower/artwork/banshee-tower-banner.webp | https://foundry.s3.seudominio.com/adventures/banshee-tower/artwork/banshee-tower-banner.webp |
| JournalEntryPage | text.content | ...<img src="modules/yan-modulo/artwork/image.webp"/>... | ...<img src="https://foundry.s3.seudominio.com/artwork/image.webp"/>... |
Seção 5: Fase IV - O Renascimento de um Módulo: Compilando para o Foundry VTT v12
Com os dados de origem agora limpos e apontando para a nuvem, a etapa final Ć© remontar tudo em um novo módulo compatĆvel com a v12.
5.1. Criando o Manifesto module.json da v12
Crie um novo arquivo module.json na raiz da sua nova pasta de módulo (C:\foundry-migration-project\yan-curated-data\). Este arquivo definirÔ a identidade e o conteúdo do seu módulo para a v12. Um modelo completo é fornecido no Apêndice A.3.
-
Identificação e Versionamento: Defina o
idcomoyan-curated-data. O nome da pasta do módulo deve corresponder exatamente a esteid. Defina um número deversioninicial, como"1.0.0".21 -
Bloco de Compatibilidade: Este é um campo obrigatório e crucial para a v12. Adicione o objeto
compatibilitypara garantir que o módulo só possa ser ativado em versões apropriadas do Foundry VTT.6JSON
"compatibility": { "minimum": "12", "verified": "12.324" } -
Definição dos Pacotes (Packs): A matriz
packsdeve ser atualizada para refletir a nova estrutura LevelDB. O atributopathagora aponta para o diretório do compêndio (dentro da pastapacks), e a extensão.dbdeve ser removida.9JSON
"packs": [ { "name": "adventures", "label": "Curated Adventures", "path": "packs/adventures", "type": "Adventure", "system": "dnd5e" }, ... ] -
Relacionamentos (Relationships): Use o novo bloco
relationshipspara declarar dependĆŖncias, como o sistema de jogo para o qual o conteĆŗdo foi projetado.2JSON
"relationships": { "systems": [ { "id": "dnd5e", "type": "system", "compatibility": {} } ] }
5.2. Executando a Operação de Compactação
Este comando compilarĆ” os arquivos JSON de volta para o formato de banco de dados LevelDB que o Foundry VTT v12 utiliza.
-
Certifique-se de que a CLI ainda estÔ configurada para trabalhar no seu novo módulo (
fvtt package workon "yan-curated-data"). -
Para cada compĆŖndio, execute o comando
packa partir da raiz da pastayan-curated-data, apontando para a pasta de origem JSON correspondente:Bash
fvtt package pack "adventures" --inputDirectory "../packs-json/adventures" -
Repita para todos os seus compĆŖndios. A CLI irĆ” ler os arquivos
.jsone criar a estrutura de diretórios LevelDB apropriada dentro da pastayan-curated-data/packs/.10
5.3. Montagem Final do Módulo
Após a conclusão, a estrutura final da sua pasta de módulo deve ser a seguinte:
yan-curated-data/
āāā module.json
āāā packs/
āāā adventures/
ā āāā 000001.ldb
ā āāā CURRENT
ā āāā...
āāā criaturas-yan/
ā āāā 000001.ldb
ā āāā...
āāā...
A pasta yan-curated-data agora estĆ” pronta para ser instalada em um ambiente Foundry VTT v12.
Seção 6: Fase V - Verificação e Solução de Problemas
A fase final envolve a implantação do módulo recém-criado em um ambiente de teste limpo para garantir que a migração foi bem-sucedida.
6.1. Protocolo de Implantação e Teste Rigoroso
-
Ambiente Limpo: Instale uma versão limpa do Foundry VTT v12. Não migre seu mundo v11 existente. Crie um novo mundo de teste com o sistema de jogo apropriado (por exemplo, dnd5e).
-
Instalação do Módulo: Copie a pasta
yan-curated-datapara a pastamodulesdentro do seu novo caminho de dados do usuƔrio da v12. -
Verificação SistemÔtica:
-
Inicie o Foundry v12 e abra o mundo de teste.
-
VĆ” para āManage Modulesā e ative apenas o módulo
yan-curated-data. O Foundry deve recarregar sem erros. -
Abra a aba de CompĆŖndios. Todos os seus pacotes curados devem estar listados.
-
Abra cada compĆŖndio e verifique se as entradas aparecem corretamente.
-
Arraste um ator de um compĆŖndio para a tela. Verifique se a imagem do token carrega. Use as ferramentas de desenvolvedor do seu navegador (F12) para inspecionar o elemento da imagem e confirmar que o
srcĆ© uma URL do seu domĆnio R2. -
Importe uma cena de um compĆŖndio. Verifique se a imagem de fundo e quaisquer tiles carregam corretamente a partir do R2.
-
Abra uma entrada de diÔrio que continha imagens. Verifique se as imagens são exibidas corretamente.
-
Após confirmar que o módulo base funciona, comece a ativar outros módulos essenciais, um de cada vez, para verificar se hÔ conflitos.
-
6.2. Armadilhas Comuns e SoluƧƵes
-
Erros de CORS no Console do Navegador: Se o console do navegador mostrar erros relacionados a āCross-Origin Resource Sharingā, o problema estĆ” quase certamente na configuração da Cloudflare. Verifique novamente a polĆtica de CORS no seu bucket R2 e certifique-se de que o script do worker estĆ” ativo e configurado corretamente para as rotas certas.15
-
Links de Imagem Quebrados (Erro 404): Se as imagens não carregarem, indica uma discrepância entre a URL no banco de dados e o caminho real do arquivo no bucket R2. As causas mais provÔveis são:
-
O comando
rclone syncnão foi executado a partir do diretório correto, resultando em uma estrutura de pastas incorreta no bucket. -
O script de substituição de Python usou uma URL base incorreta ou a lógica de substituição falhou em algum caso especĆfico. Verifique manualmente alguns arquivos JSON para confirmar se as URLs foram geradas corretamente.
-
-
Falhas no Carregamento do Compêndio: Se um compêndio aparecer vazio ou não abrir, isso geralmente indica um problema durante a etapa
fvtt package packou uma entrada malformada nomodule.jsonpara aquele pacote especĆfico. Verifique o caminho e o nome no manifesto e tente recompactar o compĆŖndio. -
Problemas no Navegador de Arquivos S3 do Foundry: Se o bucket R2 não aparecer como uma opção no Navegador de Arquivos do Foundry, o problema estÔ na comunicação inicial. Verifique o arquivo
s3.jsonem busca de erros de digitação nas credenciais ou no endpoint. Se o arquivo estiver correto, o problema provavelmente estÔ no worker da Cloudflare que não estÔ processando corretamente as solicitações de API do Foundry.
Apêndice: Scripts e Configurações Essenciais
A.1: Script do Worker de Compatibilidade S3 da Cloudflare
JavaScript
/**
* @license MIT <https://opensource.org/licenses/MIT>
* @copyright Michael Hart 2022
* https://unpkg.com/[email protected]/dist/aws4fetch.esm.js
*/
const encoder = new TextEncoder();
const HOST_SERVICES = { appstream2: 'appstream', cloudhsmv2: 'cloudhsm', email: 'ses', marketplace: 'aws-marketplace', mobile: 'AWSMobileHubService', pinpoint: 'mobiletargeting', queue: 'sqs', 'git-codecommit': 'codecommit', 'mturk-requester-sandbox': 'mturk-requester', 'personalize-runtime': 'personalize', };
const UNSIGNABLE_HEADERS = new Set([ 'authorization', 'user-agent', 'presigned-expires', 'expect', 'x-amzn-trace-id', 'range', 'connection', ]);
class AwsClient {
constructor({ accessKeyId, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) {
if (accessKeyId == null) throw new TypeError('accessKeyId is a required option')
if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option')
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
this.service = service;
this.region = region;
this.cache = cache |
| new Map();
this.retries = retries!= null? retries : 10;
this.initRetryMs = initRetryMs |
| 50;
}
async sign(input, init) {
if (input instanceof Request) {
const { method, url, headers, body } = input;
init = Object.assign({ method, url, headers }, init);
if (init.body == null && headers.has('Content-Type')) {
init.body = body!= null && headers.has('X-Amz-Content-Sha256')? body : await input.clone().arrayBuffer();
}
input = url;
}
const signer = new AwsV4Signer(Object.assign({ url: input }, init, this, init && init.aws));
const signed = Object.assign({}, init, await signer.sign());
delete signed.aws;
try {
return new Request(signed.url.toString(), signed)
} catch (e) {
if (e instanceof TypeError) {
return new Request(signed.url.toString(), Object.assign({ duplex: 'half' }, signed))
}
throw e
}
}
async fetch(input, init) {
for (let i = 0; i <= this.retries; i++) {
const fetched = fetch(await this.sign(input, init));
if (i === this.retries) {
return fetched
}
const res = await fetched;
if (res.status < 500 && res.status!== 429) {
return res
}
await new Promise(resolve => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i)));
}
throw new Error('An unknown error occurred, ensure retries is not negative')
}
}
class AwsV4Signer {
constructor({ method, url, headers, body, accessKeyId, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) {
if (url == null) throw new TypeError('url is a required option')
if (accessKeyId == null) throw new TypeError('accessKeyId is a required option')
if (secretAccessKey == null) throw new TypeError('secretAccessKey is a required option')
this.method = method |
| (body? 'POST' : 'GET');
this.url = new URL(url);
this.headers = new Headers(headers |
| {});
this.body = body;
this.accessKeyId = accessKeyId;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
let guessedService, guessedRegion;
if (!service ||!region) {
= guessServiceRegion(this.url, this.headers);
}
this.service = service |
| guessedService |
| '';
this.region = region |
| guessedRegion |
| 'us-east-1';
this.cache = cache |
| new Map();
this.datetime = datetime |
| new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
this.signQuery = signQuery;
this.appendSessionToken = appendSessionToken |
| this.service === 'iotdevicegateway';
this.headers.delete('Host');
if (this.service === 's3' &&!this.signQuery &&!this.headers.has('X-Amz-Content-Sha256')) {
this.headers.set('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD');
}
const params = this.signQuery? this.url.searchParams : this.headers;
params.set('X-Amz-Date', this.datetime);
if (this.sessionToken &&!this.appendSessionToken) {
params.set('X-Amz-Security-Token', this.sessionToken);
}
this.signableHeaders = ['host',...this.headers.keys()]
.filter(header => allHeaders ||!UNSIGNABLE_HEADERS.has(header))
.sort();
this.signedHeaders = this.signableHeaders.join(';');
this.canonicalHeaders = this.signableHeaders
.map(header => header + ':' + (header === 'host'? this.url.host : (this.headers.get(header) |
| '').replace(/\s+/g, ' ')))
.join('\n');
this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, 'aws4_request'].join('/');
if (this.signQuery) {
if (this.service === 's3' &&!params.has('X-Amz-Expires')) {
params.set('X-Amz-Expires', '86400');
}
params.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256');
params.set('X-Amz-Credential', this.accessKeyId + '/' + this.credentialString);
params.set('X-Amz-SignedHeaders', this.signedHeaders);
}
if (this.service === 's3') {
try {
this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, ' '));
} catch (e) {
this.encodedPath = this.url.pathname;
}
} else {
this.encodedPath = this.url.pathname.replace(/\/+/g, '/');
}
if (!singleEncode) {
this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, '/');
}
this.encodedPath = encodeRfc3986(this.encodedPath);
const seenKeys = new Set();
this.encodedSearch = [...this.url.searchParams]
.filter(([k]) => {
if (!k) return false
if (this.service === 's3') {
if (seenKeys.has(k)) return false
seenKeys.add(k);
}
return true
})
.map(pair => pair.map(p => encodeRfc3986(encodeURIComponent(p))))
.sort(([k1, v1], [k2, v2]) => k1 < k2? -1 : k1 > k2? 1 : v1 < v2? -1 : v1 > v2? 1 : 0)
.map(pair => pair.join('='))
.join('&');
}
async sign() {
if (this.signQuery) {
this.url.searchParams.set('X-Amz-Signature', await this.signature());
if (this.sessionToken && this.appendSessionToken) {
this.url.searchParams.set('X-Amz-Security-Token', this.sessionToken);
}
} else {
this.headers.set('Authorization', await this.authHeader());
}
return { method: this.method, url: this.url, headers: this.headers, body: this.body, }
}
async authHeader() {
return.join(', ')
}
async signature() {
const date = this.datetime.slice(0, 8);
const cacheKey = [this.secretAccessKey, date, this.region, this.service].join();
let kCredentials = this.cache.get(cacheKey);
if (!kCredentials) {
const kDate = await hmac('AWS4' + this.secretAccessKey, date);
const kRegion = await hmac(kDate, this.region);
const kService = await hmac(kRegion, this.service);
kCredentials = await hmac(kService, 'aws4_request');
this.cache.set(cacheKey, kCredentials);
}
return buf2hex(await hmac(kCredentials, await this.stringToSign()))
}
async stringToSign() {
return.join('\n')
}
async canonicalString() {
return.join('\n')
}
async hexBodyHash() {
let hashHeader = this.headers.get('X-Amz-Content-Sha256') |
| (this.service === 's3' && this.signQuery? 'UNSIGNED-PAYLOAD' : null);
if (hashHeader == null) {
if (this.body && typeof this.body!== 'string' &&!('byteLength' in this.body)) {
throw new Error('body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header')
}
hashHeader = buf2hex(await hash(this.body |
| ''));
}
return hashHeader
}
}
async function hmac(key, string) {
const cryptoKey = await crypto.subtle.importKey(
'raw',
typeof key === 'string'? encoder.encode(key) : key,
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign'],
);
return crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(string))
}
async function hash(content) {
return crypto.subtle.digest('SHA-256', typeof content === 'string'? encoder.encode(content) : content)
}
function buf2hex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), x => ('0' + x.toString(16)).slice(-2)).join('')
}
function encodeRfc3986(urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
}
function guessServiceRegion(url, headers) {
const { hostname, pathname } = url;
if (hostname.endsWith('.r2.cloudflarestorage.com')) {
return ['s3', 'auto']
}
if (hostname.endsWith('.backblazeb2.com')) {
const match = hostname.match(/^(?:[^.]+\.)?s3\.([^.]+)\.backblazeb2\.com$/);
return match!= null? ['s3', match] : ['', '']
}
const match = hostname.replace('dualstack.', '').match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/);
let [service, region] = (match |
| ['', '']).slice(1, 3);
if (region === 'us-gov') {
region = 'us-gov-west-1';
} else if (region === 's3' |
| region === 's3-accelerate') {
region = 'us-east-1';
service = 's3';
} else if (service === 'iot') {
if (hostname.startsWith('iot.')) {
service = 'execute-api';
} else if (hostname.startsWith('data.jobs.iot.')) {
service = 'iot-jobs-data';
} else {
service = pathname === '/mqtt'? 'iotdevicegateway' : 'iotdata';
}
} else if (service === 'autoscaling') {
const targetPrefix = (headers.get('X-Amz-Target') |
| '').split('.');
if (targetPrefix === 'AnyScaleFrontendService') {
service = 'application-autoscaling';
} else if (targetPrefix === 'AnyScaleScalingPlannerFrontendService') {
service = 'autoscaling-plans';
}
} else if (region == null && service.startsWith('s3-')) {
region = service.slice(3).replace(/^fips-|^external-1/, '');
service = 's3';
} else if (service.endsWith('-fips')) {
service = service.slice(0, -5);
} else if (region && /-\d$/.test(service) &&!/-\d$/.test(region)) {
[service, region] = [region, service];
}
return |
| service, region]
}
export default {
async fetch(request, env, ctx) {
// Fill these in...
const my_s3_domain = "s3.seudominio.com"; // REPLACE WITH YOUR S3 DOMAIN
const my_bucket_name = "foundry" // REPLACE WITH YOUR BUCKET NAME
const my_account_id = "SEU_ACCOUNT_ID_AQUI"; // REPLACE WITH YOUR ACCOUNT ID
const my_access_key = "SEU_ACCESS_KEY_ID_AQUI"; // REPLACE WITH YOUR ACCESS KEY
const my_secret_key = "SEU_SECRET_ACCESS_KEY_AQUI"; // REPLACE WITH YOUR SECRET KEY
// Create the strings we need now
const my_bucket_domain = my_bucket_name + "." + my_s3_domain;
const cloudflare_s3_api = "https://" + my_account_id + ".r2.cloudflarestorage.com";
const cloudflare_s3_bucket_api = "https://" + my_bucket_name + "." + my_account_id + ".r2.cloudflarestorage.com";
// Make sure there is an Authorization header
if (request.headers.get("authorization") === null) {
// Allow public access for GET requests if there's no authorization
if (request.method === "GET") {
let new_url = new URL(request.url);
new_url.hostname = new_url.hostname.replace(my_s3_domain, "r2.dev"); // Replace with your public R2 domain if different
return fetch(new Request(new_url, request));
}
return (new Response("Forbidden", { status: 403 }));
}
// Generate a new signature and change URL
var s3_url;
const request_url = new URL(request.url);
if (request_url.hostname === my_s3_domain) {
s3_url = cloudflare_s3_api;
} else if (request_url.hostname === my_bucket_domain) {
s3_url = cloudflare_s3_bucket_api;
request_url.pathname = request_url.pathname.replace("worker/", "");
s3_url = s3_url.concat(request_url.pathname, request_url.search)
} else {
return new Response("Invalid hostname", { status: 400 });
}
const s3_request = new Request(s3_url, request);
s3_request.headers.delete("accept-encoding");
s3_request.headers.delete("cf-connecting-ip");
s3_request.headers.delete("cf-ipcountry");
s3_request.headers.delete("cf-ray");
s3_request.headers.delete("cf-visitor");
s3_request.headers.delete("x-forwarded-proto");
s3_request.headers.delete("x-real-ip");
const signer = new AwsV4Signer({
url: s3_url,
accessKeyId: my_access_key,
secretAccessKey: my_secret_key,
method: s3_request.method,
headers: s3_request.headers,
body: s3_request.body,
service: "s3",
region: "auto",
datetime: s3_request.headers.get("x-amz-date"),
});
const auth_header = await signer.authHeader();
s3_request.headers.set("Authorization", auth_header);
return fetch(s3_request);
},
};
A.2: Script Python para Substituição de Caminhos de Ativos
Python
import os
import json
import re
from pathlib import Path
# --- CONFIGURAĆĆO ---
# O diretório raiz contendo as pastas de compêndio descompactadas (ex: 'packs-json')
ROOT_DIR = "C:/foundry-migration-project/packs-json"
# O prefixo do caminho antigo que serĆ” substituĆdo
OLD_PATH_PREFIX = "modules/yan-modulo/"
# A nova URL base para os ativos no R2
NEW_URL_PREFIX = "https://foundry.s3.seudominio.com/" # Lembre-se da barra no final
# --- LĆGICA DO SCRIPT ---
def update_asset_paths(data):
"""
Percorre recursivamente uma estrutura de dados (dicionƔrio ou lista)
e substitui os prefixos de caminho de ativos.
"""
if isinstance(data, dict):
for key, value in data.items():
data[key] = update_asset_paths(value)
elif isinstance(data, list):
for i, item in enumerate(data):
data[i] = update_asset_paths(item)
elif isinstance(data, str):
# Substituição direta de caminhos
if data.strip().startswith(OLD_PATH_PREFIX):
data = data.replace(OLD_PATH_PREFIX, NEW_URL_PREFIX, 1)
# Substituição dentro de conteúdo HTML
# Procura por src="..." ou href="..." contendo o prefixo antigo
def replace_html_path(match):
tag_attr = match.group(1) # src= ou href=
quote = match.group(2) # " ou '
path = match.group(3)
if path.startswith(OLD_PATH_PREFIX):
new_path = path.replace(OLD_PATH_PREFIX, NEW_URL_PREFIX, 1)
return f'{tag_attr}{quote}{new_path}{quote}'
return match.group(0) # Retorna o original se não corresponder
# Regex para encontrar src="..." ou href="..."
html_pattern = re.compile(r'(src=|href=)(["\'])(.*?)\2', re.IGNORECASE)
data = html_pattern.sub(replace_html_path, data)
return data
def process_directory(directory_path):
"""
Processa todos os arquivos.json em um diretório e seus subdiretórios.
"""
print(f"Processando diretório: {directory_path}")
path = Path(directory_path)
for file_path in path.rglob('*.json'):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = json.load(f)
updated_content = update_asset_paths(content)
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(updated_content, f, indent=2, ensure_ascii=False)
# print(f" - Atualizado: {file_path.name}")
except json.JSONDecodeError:
print(f" - ERRO: Falha ao decodificar JSON em {file_path}")
except Exception as e:
print(f" - ERRO: Ocorreu um erro inesperado em {file_path}: {e}")
if __name__ == "__main__":
if not os.path.isdir(ROOT_DIR):
print(f"ERRO: O diretório raiz '{ROOT_DIR}' não foi encontrado.")
else:
process_directory(ROOT_DIR)
print("\nProcesso de atualização de caminhos concluĆdo.")
A.3: Modelo de module.json para a v12
JSON
{
"id": "yan-curated-data",
"title": "Yan's Curated Content",
"description": "A consolidated collection of premium content from various creators, optimized for Foundry VTT v12 with cloud-hosted assets.",
"version": "1.0.0",
"authors":,
"compatibility": {
"minimum": "12",
"verified": "12.324"
},
"relationships": {
"systems": [
{
"id": "dnd5e",
"type": "system",
"compatibility": {}
}
],
"dependencies":,
"recommends":
},
"packs":,
"packFolders":,
"scripts":,
"esmodules":,
"styles":,
"languages":,
"socket": false,
"manifest": "URL_DO_SEU_MANIFEST_SE_HOSPEDADO_ONLINE",
"download": "URL_DO_ZIP_DO_SEU_MODULO_SE_HOSPEDADO_ONLINE",
"protected": false,
"exclusive": false
}