Studio scene pack contract for runtime SCENE binary payload
This commit is contained in:
parent
1182d3ae1f
commit
822e892c82
@ -1,5 +1,5 @@
|
||||
{"type":"meta","next_id":{"DSC":31,"AGD":33,"DEC":29,"PLN":59,"LSN":46,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0030","status":"accepted","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-04-27","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[{"id":"AGD-0032","file":"AGD-0032-studio-scene-pack-runtime-binary-contract.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"meta","next_id":{"DSC":31,"AGD":34,"DEC":31,"PLN":62,"LSN":46,"CLSN":1}}
|
||||
{"type":"discussion","id":"DSC-0030","status":"in_progress","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-04-30","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[{"id":"AGD-0032","file":"AGD-0032-studio-scene-pack-runtime-binary-contract.md","status":"accepted","created_at":"2026-04-24","updated_at":"2026-04-29"},{"id":"AGD-0033","file":"AGD-0033-revise-scene-bank-editorial-model-to-single-tmx-per-scene.md","status":"accepted","created_at":"2026-04-30","updated_at":"2026-04-30","supersedes":["AGD-0032"],"_reason":"Explicit user request on 2026-04-30 to revise the editorial direction after concluding the current Tiled integration model is wrong."}],"decisions":[{"id":"DEC-0029","file":"DEC-0029-studio-scene-pack-runtime-binary-contract.md","status":"accepted","created_at":"2026-04-29","updated_at":"2026-04-29","ref_agenda":"AGD-0032"},{"id":"DEC-0030","file":"DEC-0030-studio-native-scene-canonical-model-with-tiled-import-export.md","status":"accepted","created_at":"2026-04-30","updated_at":"2026-04-30","ref_agenda":"AGD-0033","supersedes":["DEC-0029"],"_scope":"Supersedes only the editorial-model portion of DEC-0029 while preserving its canonical publication boundary."}],"plans":[{"id":"PLN-0059","file":"PLN-0059-scene-pack-contract-spec-propagation.md","status":"done","created_at":"2026-04-29","updated_at":"2026-04-29","ref_decisions":["DEC-0029"]},{"id":"PLN-0060","file":"PLN-0060-scene-bank-studio-request-projection-and-acceptance.md","status":"done","created_at":"2026-04-29","updated_at":"2026-04-29","ref_decisions":["DEC-0029"]},{"id":"PLN-0061","file":"PLN-0061-scene-pack-request-materialization-and-packer-build.md","status":"done","created_at":"2026-04-29","updated_at":"2026-04-29","ref_decisions":["DEC-0029"]},{"id":"PLN-0062","file":"PLN-0062-scene-canonical-model-revision-spec-propagation.md","status":"done","created_at":"2026-04-30","updated_at":"2026-04-30","ref_decisions":["DEC-0030"]},{"id":"PLN-0063","file":"PLN-0063-studio-canonical-scene-entity-analysis-and-pack-request-projection.md","status":"done","created_at":"2026-04-30","updated_at":"2026-04-30","ref_decisions":["DEC-0030"]},{"id":"PLN-0064","file":"PLN-0064-tiled-scene-import-and-tileset-export-interoperability.md","status":"done","created_at":"2026-04-30","updated_at":"2026-04-30","ref_decisions":["DEC-0030"]}],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||
{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}
|
||||
{"type":"discussion","id":"DSC-0027","status":"abandoned","ticket":"studio-scene-workspace","title":"Scene Workspace for SCENE Authoring","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["studio","workspace","scene","tilemap","asset","runtime-alignment"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted agenda and its downstream work."}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0029","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted decision and stop using it as normative guidance."}],"plans":[{"id":"PLN-0049","file":"PLN-0049-scene-workspace-spec-and-boundary-propagation.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0050","file":"PLN-0050-scene-workspace-shell-and-project-state-foundations.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0051","file":"PLN-0051-scene-artifact-and-assets-handoff-contract.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0052","file":"PLN-0052-scene-workspace-wave-1-tilemap-editor.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."}],"lessons":[]}
|
||||
|
||||
@ -175,22 +175,23 @@ Os pontos mais arriscados no boundary sao:
|
||||
Os pontos abaixo ficam cravados nesta agenda como direcao atual da discussao:
|
||||
|
||||
1. `TMX` e `TSX` permanecem surfaces editoriais do Studio e nao entram no protocolo operacional do packer.
|
||||
2. O `Asset Workspace` deve extrair, no passo de `Accept`, um request proprio para o `prometeu-packer`.
|
||||
3. O payload runtime `SCENE` carrega `glyph_asset_id`, e a resolucao `asset_id -> slot` pertence ao runtime.
|
||||
4. `glyph_asset_id` eh derivado do tileset/glyph asset selecionado no form da layer.
|
||||
5. `parallax_factor` deve existir no form da layer.
|
||||
6. `palette_id` deve existir no form da layer, ou em surface editorial equivalente no mesmo nivel.
|
||||
7. O checkbox da layer define se ela esta ativa/sera usada.
|
||||
8. Cada tile eh resolvido via `TSX`; `glyph_id` ja existe como property no `TSX`.
|
||||
9. `flip` e `palette_id` por tile podem vir como properties no `TMX`.
|
||||
10. Wave 1 continua com exatamente `1 TSX` por `TMX`/layer.
|
||||
11. O gate atual para publicar continua sendo ausencia de diagnostic error bloqueante.
|
||||
12. O request `studio -> packer` deve ser derivado backwards do payload binario e `request -> SCENE` deve ser o mais direto possivel.
|
||||
13. O shape atual assentado do request e:
|
||||
2. `TMX` e `TSX` sao surfaces editoriais suportadas em wave 1, mas nao sao a surface canonica do contrato de publicacao e permanecem substituiveis por outros editores suportados no futuro.
|
||||
3. O `Asset Workspace` deve extrair, no passo de `Accept`, um request proprio para o `prometeu-packer`.
|
||||
4. O payload runtime `SCENE` carrega `glyph_asset_id`, e a resolucao `asset_id -> slot` pertence ao runtime.
|
||||
5. `glyph_asset_id` eh derivado do tileset/glyph asset selecionado no form da layer.
|
||||
6. `parallax_factor` deve existir no form da layer.
|
||||
7. `palette_id` deve existir no form da layer, ou em surface editorial equivalente no mesmo nivel.
|
||||
8. O checkbox da layer define se ela esta ativa/sera usada.
|
||||
9. Cada tile eh resolvido via `TSX`; `glyph_id` ja existe como property no `TSX`.
|
||||
10. `flip` e `palette_id` por tile podem vir como properties no `TMX`.
|
||||
11. Wave 1 continua com exatamente `1 TSX` por `TMX`/layer.
|
||||
12. O gate atual para publicar continua sendo ausencia de diagnostic error bloqueante.
|
||||
13. O request `studio -> packer` deve ser derivado backwards do payload binario e `request -> SCENE` deve ser o mais direto possivel.
|
||||
14. O shape atual assentado do request e:
|
||||
`ScenePackRequest(version, layers[4])`;
|
||||
`ScenePackLayerRequest(active, glyph_asset_id, tile_size, parallax_factor_x, parallax_factor_y, width, height, tiles)`;
|
||||
`ScenePackTileRequest(active, glyph_id, palette_id, flip_x, flip_y)`.
|
||||
14. O manifest permanece com `SCENE/tilemap_v1` por enquanto.
|
||||
15. O manifest permanece com `SCENE/tilemap_v1` por enquanto.
|
||||
|
||||
## Recommendation
|
||||
|
||||
@ -199,17 +200,19 @@ Seguir com a `Option A`.
|
||||
Direcao recomendada:
|
||||
|
||||
1. manter `TMX`/`TSX` como surfaces editoriais wave 1 no dominio `studio`;
|
||||
2. fechar uma decision nova para o boundary `studio/packer/runtime` do `Scene Bank`;
|
||||
3. fazer o `Asset Workspace` aceitar e extrair do estado editorial um request proprio para o packer;
|
||||
4. fazer o `prometeu-packer` ser owner apenas da compilacao desse request para o payload binario `SCENE`;
|
||||
5. explicitar na decision:
|
||||
2. registrar explicitamente que `TMX`/`TSX` nao sao a surface canonica do contrato e podem coexistir no futuro com outros editores suportados, desde que materializem o mesmo request canonico;
|
||||
3. fechar uma decision nova para o boundary `studio/packer/runtime` do `Scene Bank`;
|
||||
4. fazer o `Asset Workspace` aceitar e extrair do estado editorial um request proprio para o packer;
|
||||
5. fazer o `prometeu-packer` ser owner apenas da compilacao desse request para o payload binario `SCENE`;
|
||||
6. explicitar na decision:
|
||||
modelagem da dependencia `glyph_asset_id`;
|
||||
modelagem de `parallax_factor`;
|
||||
modelagem de `palette_id`;
|
||||
semantica da layer ativa no request;
|
||||
gate baseado em diagnostics bloqueantes;
|
||||
formato do request `studio -> packer`, derivado backwards do payload binario;
|
||||
semantica oficial de `SCENE/tilemap_v1`.
|
||||
semantica oficial de `SCENE/tilemap_v1`;
|
||||
carater nao-canonico e substituivel das surfaces editoriais suportadas.
|
||||
|
||||
Essa direcao reaproveita o que ja existe, mantem o packer livre das ferramentas editoriais do Studio e concentra a publicacao runtime no subsistema que ja monta `assets.pa`.
|
||||
|
||||
@ -231,12 +234,14 @@ Agenda aceita.
|
||||
Direcao consolidada:
|
||||
|
||||
1. `TMX`/`TSX` permanecem estritamente editoriais no dominio `studio`.
|
||||
2. O `Scene Bank` aceito deve extrair um request proprio e estavel para o `prometeu-packer`.
|
||||
3. O request `studio -> packer` deve ser derivado backwards do payload binario `SCENE` e `request -> SCENE` deve ser o mais direto possivel.
|
||||
4. O contrato runtime de dependencia de glyph por layer usa `glyph_asset_id`, com resolucao `asset_id -> slot` pertencendo ao runtime.
|
||||
5. O workflow editorial do Studio deve prover `glyph_asset_id` derivado do tileset selecionado, `parallax_factor`, `palette_id`, estado ativo da layer e a traducao por tile a partir de `TSX`/`TMX`.
|
||||
6. Wave 1 permanece com exatamente `1 TSX` por `TMX`/layer.
|
||||
7. O gate para publicacao continua sendo ausencia de diagnostic error bloqueante.
|
||||
8. O manifest permanece com `SCENE/tilemap_v1` por enquanto.
|
||||
2. `TMX`/`TSX` sao surfaces editoriais suportadas em wave 1, mas nao sao a definicao canonica do asset nem do contrato de publicacao.
|
||||
3. Outros editores suportados no futuro podem substituir ou coexistir com `TMX`/`TSX`, desde que materializem o mesmo request canonico `studio -> packer` e passem pelas mesmas regras de validacao e `Accept`.
|
||||
4. O `Scene Bank` aceito deve extrair um request proprio e estavel para o `prometeu-packer`.
|
||||
5. O request `studio -> packer` deve ser derivado backwards do payload binario `SCENE` e `request -> SCENE` deve ser o mais direto possivel.
|
||||
6. O contrato runtime de dependencia de glyph por layer usa `glyph_asset_id`, com resolucao `asset_id -> slot` pertencendo ao runtime.
|
||||
7. O workflow editorial do Studio deve prover `glyph_asset_id` derivado do tileset selecionado, `parallax_factor`, `palette_id`, estado ativo da layer e a traducao por tile a partir de `TSX`/`TMX`.
|
||||
8. Wave 1 permanece com exatamente `1 TSX` por `TMX`/layer.
|
||||
9. O gate para publicacao continua sendo ausencia de diagnostic error bloqueante.
|
||||
10. O manifest permanece com `SCENE/tilemap_v1` por enquanto.
|
||||
|
||||
Proximo passo sugerido: converter esta agenda em uma `decision` normativa para o boundary `studio -> packer -> runtime`.
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
---
|
||||
id: AGD-0033
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Revise Scene Bank editorial model to Studio-native scene with TMX import-export
|
||||
status: accepted
|
||||
created: 2026-04-30
|
||||
resolved: 2026-04-30
|
||||
decision: DEC-0030
|
||||
tags: [studio, packer, runtime, scene, asset-pack, tiled, revision]
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
A direcao aceita em `DEC-0029` e implementada por `PLN-0059` a `PLN-0061` tomou um atalho editorial errado: modelamos `1 layer -> 1 TMX`, com validacao e `Accept` por layer, quando o editor suportado (`Tiled`) opera naturalmente com `1 scene -> 1 TMX` contendo multiplos layers.
|
||||
|
||||
Isso gerou uma friccao estrutural:
|
||||
|
||||
- o fluxo atual separa artificialmente layers que pertencem ao mesmo mapa;
|
||||
- o `Accept` por layer conflita com a necessidade de publicar uma scene coesa;
|
||||
- o modelo atual esconde um erro conceitual ao tratar `TMX` como surface por layer, em vez de surface por scene;
|
||||
- a integracao editorial ficou mais fraca justamente no editor que estamos suportando em wave 1.
|
||||
|
||||
Dominio owner desta agenda: `studio`.
|
||||
Boundary explicito: `studio -> packer -> runtime`.
|
||||
|
||||
## Context
|
||||
|
||||
- `DEC-0029` fechou corretamente que `TMX`/`TSX` sao surfaces editoriais suportadas, nao canonicas, e que o contrato normativo de publicacao continua sendo o request `studio -> packer -> SCENE`.
|
||||
- O erro nao esta nesse boundary canonico; o erro esta no shape editorial escolhido para a integracao com Tiled.
|
||||
- A implementacao atual assumiu:
|
||||
- `1 layer -> 1 TMX`;
|
||||
- `1 TMX -> 1 TSX`;
|
||||
- geracao, validacao e `Accept` com foco em layer.
|
||||
- O problema observado no `tiled2` reforca essa distorcao: o fluxo de workflow passou a tentar aceitar layer isoladamente, quando o valor editorial real esta na scene inteira.
|
||||
- O Tiled comporta um `TMX` com multiplos layers; cada layer pode ter seu proprio tileset referenciado no mapa, e o authoring natural da scene acontece nesse documento unico.
|
||||
- O runtime continua nao dependendo de `TMX`; ele continua dependendo apenas do request canonico aceito e do payload binario `SCENE`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [x] O modelo wave 1 deve ser formalmente revisado para `1 scene -> 1 TMX` com multiplos layers?
|
||||
Resposta: parcialmente. Essa direcao corrige o erro imediato de `TMX por layer`, mas a escolha final nao eh promover `TMX unico` a surface principal. A direcao escolhida eh um modelo nativo de scene no Studio, com `TMX` tratado como formato suportado de import/export.
|
||||
- [x] Devemos abandonar completamente `validate layer` e `accept layer`, deixando apenas `generate scene`, `validate scene` e `accept scene`?
|
||||
Resposta: sim, e mais do que isso: no modelo escolhido esses verbos deixam de ser a surface principal do workflow editorial. O Studio passa a operar com `import` e `export` via wizard para formatos suportados, com `Tiled` como primeira opcao. A validacao ocorre sobre o formato canonico do asset de scene no `Analyse` ou no `Pack`, nao sobre o formato externo.
|
||||
- [x] Como cada layer da scene referencia seu tileset no editor suportado?
|
||||
Resposta: no caminho de compatibilidade com Tiled, o `TMX` pode carregar multiplos `TSX`, e a scene pode usar ate quatro tilesets distintos. Isso descreve a compatibilidade editorial do import/export, nao o modelo canonico interno do Studio.
|
||||
- [x] Como representar layers com tamanhos independentes dentro da scene?
|
||||
Resposta: o modelo da scene deve aceitar layers com tamanho proprio. No caminho Tiled, mapas infinitos continuam suportados como concern editorial. Camera boundaries ficam para iteracao posterior.
|
||||
- [x] O `scene-bank.studio.json` deve continuar carregando `width`/`height` por layer, ou passar a carregar apenas metadata editorial complementar e bindings?
|
||||
Resposta: o asset de scene deve manter uma entidade canonica propria, distinta do request de pack. `scene-bank.studio.json` pode continuar existindo como persistencia editorial dessa entidade, com metadata como `width` e `height` por layer quando fizer sentido, mas sem se confundir com o request `studio -> packer`.
|
||||
- [x] Como o request canonico deve ser projetado sem reintroduzir dependencia canonica em formato editorial?
|
||||
Resposta: o request `studio -> packer` continua existindo, mas como projeção derivada da entidade canonica do asset de scene, nao como a propria entidade editorial. `TMX` entra apenas como formato suportado de importacao/exportacao; `import` atualiza a entidade canonica a partir do `TMX`, e `export` sobrescreve o `TMX` a partir da entidade canonica.
|
||||
- [x] O `import` de `TMX` deve aceitar qualquer alteracao editorial feita externamente no Tiled?
|
||||
Resposta: nao. O `import` deve aplicar validacoes minimas de compatibilidade antes de alterar a entidade canonica. Mudancas como adicao de novos layers, adicao de novos tilesets, ou alteracao estrutural inesperada de layers/tilesets podem invalidar o `TMX` para importacao. Quando isso ocorrer, o Studio deve recusar o `import` e emitir logs/diagnostics claros de falha.
|
||||
- [x] Como o fluxo de `TSX` deve se comportar para glyph tilesets especializados?
|
||||
Resposta: o glyph tileset pode gerar `TSX`, e isso passa a ser tratado como `export` para formato suportado `TSX (Tiled)`. O `export` sobrescreve o arquivo existente ou cria um novo, seguindo a mesma filosofia do `TMX`.
|
||||
- [x] Quais partes de `DEC-0029` permanecem validas e quais precisam ser substituidas explicitamente?
|
||||
Resposta: permanece valida a parte que fixa `TMX`/`TSX` como surfaces editoriais nao canonicas e o request `studio -> packer` como contrato normativo. Deve ser substituida explicitamente a parte editorial implementada como `TMX por layer` e o workflow `generate / validate / accept` por layer.
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Keep per-layer TMX and patch workflow
|
||||
- **Approach:** Manter `1 layer -> 1 TMX`, corrigir o bug de workflow de `Accept`, e ajustar a UX ao redor disso.
|
||||
- **Pro:** Menor custo imediato sobre a implementacao existente.
|
||||
- **Con:** Preserva o erro de modelagem editorial e continua desalinhado com o uso natural do Tiled.
|
||||
- **Maintainability:** Fraca. Corrige sintomas sem corrigir o modelo.
|
||||
|
||||
### Option B - Revise to single TMX per scene and scene-level workflow
|
||||
- **Approach:** Revisar a decision para `1 scene -> 1 TMX`, com multiplos layers editoriais no mesmo mapa, e mover `generate / validate / accept` para o nivel da scene.
|
||||
- **Pro:** Alinha o Studio ao modelo real do Tiled e fecha um boundary editorial mais coerente.
|
||||
- **Con:** Exige revisao normativa, refatoracao de specs e reimplementacao parcial do fluxo entregue em `PLN-0060` e `PLN-0061`.
|
||||
- **Maintainability:** Forte. Remove a distorcao estrutural e preserva o contrato canonico correto.
|
||||
|
||||
### Option C - Introduce a Studio-native scene model now and demote TMX to import/export
|
||||
- **Approach:** Parar de tratar `TMX` como surface principal e assumir imediatamente um modelo proprio do Studio, usando Tiled apenas como compatibilidade.
|
||||
- **Pro:** Resolve de vez o acoplamento editorial ao Tiled.
|
||||
- **Con:** Exige refatoracao mais profunda agora, incluindo suporte nativo de scene, import/export, migracao editorial e revisao dos plans ja executados.
|
||||
- **Maintainability:** Melhor opcao de longo prazo. Remove o erro estrutural na raiz e abre espaco para multiplos editores suportados.
|
||||
|
||||
Escolha fechada na agenda: `Option C`.
|
||||
|
||||
## Discussion
|
||||
|
||||
A correção necessária parece ser de direcao, nao apenas de bug.
|
||||
|
||||
O problema central nao eh que o request canonico exista ou que o packer compile `SCENE`; isso continua correto. O problema eh que o lado editorial foi fatiado na unidade errada. Ao quebrar scene em `TMX` por layer, o Studio passou a operar contra a gravidade do editor suportado e abriu espaco para workflow incoerente.
|
||||
|
||||
A agenda convergiu para um passo adicional: mesmo o modelo `TMX unico por scene` ainda seria uma otimizacao em torno do Tiled, nao uma surface editorial suficiente para o Studio no medio prazo. A escolha da `Option C` fecha uma direcao mais limpa:
|
||||
|
||||
- preservar o boundary canonico `studio -> packer -> runtime`;
|
||||
- introduzir um modelo nativo de scene no Studio como surface principal;
|
||||
- tratar `TMX`/`TSX` como formatos suportados de importacao/exportacao e compatibilidade;
|
||||
- substituir `generate / validate / accept` por um workflow de `import / export` guiado por wizard para formatos suportados;
|
||||
- aplicar validacoes minimas de compatibilidade no `import`, com recusa e logs claros quando o artefato externo divergir estruturalmente do que o Studio suporta;
|
||||
- validar a entidade canonica da scene no `Analyse` ou no `Pack`;
|
||||
- manter o request canonico de pack como projeção derivada da entidade canonica da scene, e nao como espelho bruto de qualquer formato externo.
|
||||
|
||||
Isso substitui explicitamente a parte editorial errada de `DEC-0029`, sem reabrir a parte do contrato canonico de publicacao que continuou correta.
|
||||
|
||||
## Resolution
|
||||
|
||||
Recomendacao final da agenda:
|
||||
|
||||
- revisar `DEC-0029` para abandonar o modelo `TMX por layer`;
|
||||
- nao promover `TMX unico por scene` a surface principal do produto;
|
||||
- adotar um modelo nativo de scene no Studio como owner editorial principal;
|
||||
- tratar `TMX`/`TSX` como formatos suportados de import/export e compatibilidade;
|
||||
- substituir o workflow de `generate / validate / accept` por `import / export` com wizard;
|
||||
- exigir validacoes minimas no `import` de `TMX`, com falha explicita quando houver divergencia estrutural nao suportada, como novos layers, novos tilesets, ou alteracoes estruturais equivalentes;
|
||||
- tratar a geracao de `TSX` por glyph tileset como `export` para formato suportado `TSX (Tiled)`, com sobrescrita ou criacao do arquivo de destino;
|
||||
- validar a entidade canonica do asset de scene no `Analyse` ou no `Pack`;
|
||||
- preservar a separacao entre:
|
||||
- entidade canonica do asset de scene no Studio;
|
||||
- request `studio -> packer`;
|
||||
- payload binario `SCENE` de runtime;
|
||||
- preservar como normativa a parte de `DEC-0029` que fixa `TMX`/`TSX` como surfaces editoriais nao canonicas e o request `studio -> packer` como contrato de publicacao.
|
||||
|
||||
Proximo passo sugerido:
|
||||
|
||||
- emitir uma nova decision de revisao, substituindo explicitamente a porcao editorial de `DEC-0029`;
|
||||
- derivar novos plans para `specs`, `studio` e `packer`, com migracao do fluxo atual;
|
||||
- tratar a implementacao entregue em `PLN-0060` e `PLN-0061` como baseline a ser revisada, nao como forma final do modelo editorial.
|
||||
@ -0,0 +1,123 @@
|
||||
---
|
||||
id: DEC-0029
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Studio scene pack contract for runtime SCENE binary payload
|
||||
status: accepted
|
||||
created: 2026-04-29
|
||||
accepted: 2026-04-29
|
||||
agenda: AGD-0032
|
||||
plans:
|
||||
- PLN-0059
|
||||
- PLN-0060
|
||||
- PLN-0061
|
||||
tags:
|
||||
- studio
|
||||
- packer
|
||||
- runtime
|
||||
- scene
|
||||
- asset-pack
|
||||
- binary-format
|
||||
- tiled
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
The canonical publication contract for `Scene Bank` assets SHALL be the `studio -> packer` scene-pack request and its direct compilation into runtime `SCENE` payload bytes.
|
||||
|
||||
`TMX` and `TSX` SHALL remain supported editorial surfaces for wave 1 inside the `studio` domain, but they MUST NOT define the normative publication contract, the runtime asset shape, or the packer boundary.
|
||||
|
||||
The system MAY support additional scene editors in the future. Any supported editor MUST materialize the same canonical scene-pack request and MUST pass the same validation and `Accept` gating rules before publication.
|
||||
|
||||
`prometeu-packer` SHALL compile only the canonical scene-pack request into the runtime `SCENE` payload. It MUST NOT parse `TMX`, `TSX`, or any other editor-specific surface directly.
|
||||
|
||||
The runtime `SCENE` dependency contract SHALL use `glyph_asset_id` per layer. Resolution from `glyph_asset_id` to runtime glyph slot SHALL remain a runtime concern and MUST NOT be serialized by Studio or packer as a pre-resolved slot.
|
||||
|
||||
## Rationale
|
||||
|
||||
This decision preserves the already accepted wave-1 editorial direction from `DSC-0028` while preventing Tiled from becoming the canonical asset model by accident.
|
||||
|
||||
The binary `SCENE` runtime payload already exists and is the true sink of the pipeline. The missing architectural piece is therefore not a new scene schema, but a stable domain boundary that lets Studio accept editorial state and lets packer compile a runtime-owned request without absorbing external-tool concerns.
|
||||
|
||||
Treating `TMX` and `TSX` as supported but non-canonical keeps the current workflow viable, while leaving the door open for future integrated editors or multiple editors without reopening the packer or runtime contract.
|
||||
|
||||
## Technical Specification
|
||||
|
||||
### 1. Domain Ownership
|
||||
|
||||
- `studio` SHALL own editorial scene authoring, validation, diagnostics, and `Accept`.
|
||||
- `packer` SHALL own compilation of the canonical request into binary `SCENE`.
|
||||
- `runtime` SHALL own decoding, binding, and operational slot resolution for glyph dependencies.
|
||||
|
||||
### 2. Supported Editorial Surfaces
|
||||
|
||||
- Wave 1 SHALL support `TMX` and `TSX` as editorial surfaces in `studio`.
|
||||
- These surfaces MUST be treated as replaceable editor integrations, not as canonical publication artifacts.
|
||||
- Future editors MAY replace or coexist with Tiled-backed surfaces if they emit the same canonical request and satisfy the same validation semantics.
|
||||
|
||||
### 3. Canonical `studio -> packer` Request
|
||||
|
||||
The canonical request SHALL be defined as:
|
||||
|
||||
```text
|
||||
ScenePackRequest
|
||||
- version: int
|
||||
- layers: [ScenePackLayerRequest; 4]
|
||||
|
||||
ScenePackLayerRequest
|
||||
- active: bool
|
||||
- glyph_asset_id: int
|
||||
- tile_size: int
|
||||
- parallax_factor_x: float
|
||||
- parallax_factor_y: float
|
||||
- width: int
|
||||
- height: int
|
||||
- tiles: List<ScenePackTileRequest>
|
||||
|
||||
ScenePackTileRequest
|
||||
- active: bool
|
||||
- glyph_id: int
|
||||
- palette_id: int
|
||||
- flip_x: bool
|
||||
- flip_y: bool
|
||||
```
|
||||
|
||||
Normative rules:
|
||||
|
||||
- `request -> SCENE` SHALL be as direct as possible.
|
||||
- `tile_count` SHALL be derived from `width * height`.
|
||||
- Binary reserved fields MUST NOT be mirrored into the request unless they gain explicit editorial meaning.
|
||||
- `layers` SHALL always materialize exactly `4` entries in wave 1.
|
||||
|
||||
### 4. Layer and Tile Semantics
|
||||
|
||||
- `glyph_asset_id` SHALL be derived from the glyph asset/tileset selected for the layer in Studio.
|
||||
- `parallax_factor_x` and `parallax_factor_y` SHALL be modeled as layer-owned editorial fields in Studio.
|
||||
- `palette_id` SHALL be modeled with a layer-level editorial default in Studio, with optional per-tile override when supported by the editorial surface.
|
||||
- `active` at layer scope SHALL indicate whether the layer participates in publication output.
|
||||
- `active` at tile scope SHALL indicate whether the tile contributes an active tile record to the published scene payload.
|
||||
- Wave 1 SHALL remain limited to exactly `1 TSX` per `TMX` per layer.
|
||||
|
||||
### 5. Validation and Acceptance
|
||||
|
||||
- Publication MUST be gated by absence of blocking diagnostic errors.
|
||||
- The `Accept` step in Studio SHALL be the moment where supported editorial state is validated and projected into the canonical request.
|
||||
- Studio MUST NOT silently auto-accept external editor mutations.
|
||||
|
||||
### 6. Manifest and Runtime Projection
|
||||
|
||||
- The manifest output format SHALL remain `SCENE/tilemap_v1` for this wave.
|
||||
- The runtime payload SHALL carry `glyph_asset_id` per layer.
|
||||
- Runtime slot allocation and `asset_id -> slot` resolution MUST remain runtime-internal behavior.
|
||||
|
||||
## Constraints
|
||||
|
||||
- This decision MUST NOT be interpreted as introducing a dedicated scene workspace.
|
||||
- This decision MUST NOT be interpreted as allowing packer to parse editor-specific formats directly.
|
||||
- This decision MUST NOT be interpreted as making Tiled temporary or deprecated; it is supported, but non-canonical.
|
||||
- This decision does not reopen wave-1 limits around single-tileset-per-layer authoring.
|
||||
- Any future change to the canonical request or runtime projection SHALL require explicit decision review.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-04-29: Initial draft from AGD-0032.
|
||||
- 2026-04-29: Accepted and decomposed into implementation plans PLN-0059, PLN-0060, and PLN-0061.
|
||||
@ -0,0 +1,147 @@
|
||||
---
|
||||
id: DEC-0030
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Studio-native scene canonical model with Tiled import-export compatibility
|
||||
status: accepted
|
||||
created: 2026-04-30
|
||||
accepted: 2026-04-30
|
||||
agenda: AGD-0033
|
||||
plans:
|
||||
- PLN-0062
|
||||
- PLN-0063
|
||||
- PLN-0064
|
||||
tags:
|
||||
- studio
|
||||
- packer
|
||||
- runtime
|
||||
- scene
|
||||
- asset-pack
|
||||
- tiled
|
||||
- import-export
|
||||
- revision
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
`Scene Bank` authoring in `studio` SHALL move to a Studio-native canonical scene model.
|
||||
|
||||
`TMX` and `TSX` SHALL remain supported formats, but only as import/export and compatibility surfaces. They MUST NOT be treated as the primary editorial model, the validation target, or the canonical publication state.
|
||||
|
||||
The Studio workflow for scenes SHALL NOT use `generate`, `validate`, or `accept` over `TMX`, per-layer `TMX`, or any equivalent editor-owned surface. Instead, Studio SHALL own:
|
||||
|
||||
- a canonical scene entity for the asset itself;
|
||||
- `import` from supported external formats into that canonical scene entity;
|
||||
- `export` from that canonical scene entity into supported external formats.
|
||||
|
||||
For wave 1 of this revision:
|
||||
|
||||
- `Tiled` SHALL be the first supported import/export option;
|
||||
- `TMX` scene import SHALL update the canonical scene entity from the external file;
|
||||
- `TMX` scene export SHALL overwrite the target `TMX` or create it if missing;
|
||||
- glyph-bank `TSX` generation SHALL be treated as `TSX (Tiled)` export and SHALL overwrite the target `TSX` or create it if missing.
|
||||
|
||||
Validation SHALL occur over the canonical Studio scene entity during `Analyse` and/or `Pack`. The canonical Studio scene entity SHALL remain distinct from the `studio -> packer` scene-pack request, and that request SHALL remain distinct from the runtime `SCENE` binary payload.
|
||||
|
||||
This decision SHALL supersede the editorial-model portion of `DEC-0029`. It SHALL preserve the parts of `DEC-0029` that define:
|
||||
|
||||
- the canonical `studio -> packer` boundary as normative for publication;
|
||||
- `TMX`/`TSX` as non-canonical supported surfaces;
|
||||
- `prometeu-packer` as compiler of the canonical request only;
|
||||
- runtime ownership of `glyph_asset_id -> slot` resolution and of the final `SCENE` payload contract.
|
||||
|
||||
## Rationale
|
||||
|
||||
The previous direction corrected the publication boundary but still modeled the editorial surface around the wrong unit. Treating layers as separate `TMX` files forced the Studio workflow to move against the natural structure of supported external editors and produced a brittle acceptance model.
|
||||
|
||||
Even revising that approach to a single `TMX` per scene would still make the external format too central. The selected direction avoids that trap by making the Studio's own scene entity the owner of editorial truth, while keeping Tiled available as an interoperability surface.
|
||||
|
||||
This preserves long-term flexibility:
|
||||
|
||||
- multiple external scene editors MAY be supported later;
|
||||
- the packer boundary remains editor-agnostic;
|
||||
- the runtime contract remains untouched;
|
||||
- editorial tooling can evolve independently of external file formats.
|
||||
|
||||
## Technical Specification
|
||||
|
||||
### 1. Canonical Scene Ownership
|
||||
|
||||
- `studio` SHALL persist and operate on a canonical scene entity owned by the Studio domain.
|
||||
- That canonical scene entity MUST be the source of truth for scene editorial state.
|
||||
- The canonical scene entity MUST NOT be conflated with a `TMX`, `TSX`, or any other editor-specific file.
|
||||
- The canonical scene entity MUST NOT be conflated with the `studio -> packer` request.
|
||||
|
||||
### 2. External Format Support
|
||||
|
||||
- `TMX` and `TSX` SHALL be treated as supported interoperability formats.
|
||||
- Studio SHALL expose import/export operations for supported formats through wizard-driven workflow.
|
||||
- `Tiled` SHALL be the initial supported option for both scene and glyph-bank interoperability.
|
||||
- Import/export MAY later support additional formats without changing the canonical publication boundary.
|
||||
|
||||
### 3. Import Semantics
|
||||
|
||||
- `import` MUST read a supported external file and project its compatible content into the canonical Studio entity.
|
||||
- `import` MUST perform minimum structural compatibility validation before mutating canonical state.
|
||||
- `import` MUST fail explicitly, with logs or diagnostics, when the external file diverges in unsupported structural ways.
|
||||
|
||||
Examples of structural divergence that MUST be treated as import failures unless explicitly supported by a later decision:
|
||||
|
||||
- unexpected new layers;
|
||||
- unexpected new tilesets;
|
||||
- unexpected structural modification of existing layers or tilesets;
|
||||
- equivalent external mutations that the Studio compatibility contract does not accept.
|
||||
|
||||
- Failed import MUST NOT silently partially rewrite canonical scene state.
|
||||
|
||||
### 4. Export Semantics
|
||||
|
||||
- `export` MUST serialize the canonical Studio entity into the selected supported format.
|
||||
- `TMX` export MUST overwrite the destination file if present, or create it if absent.
|
||||
- `TSX` export for glyph tilesets MUST overwrite the destination file if present, or create it if absent.
|
||||
- Exported files are compatibility artifacts. They MUST NOT become the canonical source of truth by virtue of existing on disk.
|
||||
|
||||
### 5. Validation Surface
|
||||
|
||||
- Validation of scene correctness SHALL target the canonical Studio scene entity.
|
||||
- `Analyse` and `Pack` SHALL be the normative validation surfaces for canonical scene integrity.
|
||||
- External files MAY be validated for import compatibility during import, but that validation does not replace canonical scene validation.
|
||||
- The old `validate layer`, `accept layer`, `validate scene from TMX`, and `accept scene from TMX` workflow MUST be removed or replaced so it no longer defines the editorial lifecycle.
|
||||
|
||||
### 6. Separation of Contracts
|
||||
|
||||
The system SHALL preserve three distinct layers of representation:
|
||||
|
||||
1. the canonical Studio-owned scene entity;
|
||||
2. the canonical `studio -> packer` scene-pack request derived from that entity;
|
||||
3. the runtime `SCENE` binary payload derived by `prometeu-packer`.
|
||||
|
||||
Normative implications:
|
||||
|
||||
- the canonical scene entity MAY evolve editorially without forcing the same shape onto the pack request;
|
||||
- the pack request MUST remain a publication projection, not the editorial persistence model;
|
||||
- packer MUST continue compiling only the canonical request, never `TMX`, `TSX`, or any other editor-native file directly.
|
||||
|
||||
### 7. Preserved Rules from DEC-0029
|
||||
|
||||
This decision intentionally preserves the following normative rules from `DEC-0029`:
|
||||
|
||||
- `TMX`/`TSX` remain supported but non-canonical;
|
||||
- the publication contract remains the canonical `studio -> packer` request;
|
||||
- `prometeu-packer` MUST NOT parse editor-specific formats directly;
|
||||
- runtime `SCENE` continues to depend on `glyph_asset_id` per layer;
|
||||
- runtime slot resolution remains runtime-owned;
|
||||
- any future change to the request shape or runtime payload contract still requires explicit decision review.
|
||||
|
||||
## Constraints
|
||||
|
||||
- This decision MUST NOT be interpreted as making Tiled deprecated or unsupported.
|
||||
- This decision MUST NOT be interpreted as collapsing the canonical Studio scene entity into the pack request.
|
||||
- This decision MUST NOT be interpreted as allowing import to accept arbitrary external edits without compatibility checks.
|
||||
- This decision MUST NOT be interpreted as preserving the old `TMX per layer` model.
|
||||
- This decision does not yet define the full canonical scene schema; that schema SHALL be specified by downstream plans and spec propagation derived from this decision.
|
||||
|
||||
## Revision Log
|
||||
|
||||
- 2026-04-30: Initial draft from AGD-0033.
|
||||
- 2026-04-30: Explicitly supersedes the editorial-model portion of DEC-0029 while preserving its canonical publication boundary.
|
||||
- 2026-04-30: Accepted and decomposed into implementation plans PLN-0062, PLN-0063, and PLN-0064.
|
||||
@ -0,0 +1,116 @@
|
||||
---
|
||||
id: PLN-0059
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Propagate DEC-0029 into Studio and packer normative specifications
|
||||
status: done
|
||||
created: 2026-04-29
|
||||
completed: 2026-04-29
|
||||
tags:
|
||||
- studio
|
||||
- packer
|
||||
- specs
|
||||
- scene
|
||||
- runtime-contract
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Update the normative documentation surfaces so DEC-0029 is fully reflected in Studio and packer specs before or alongside code implementation.
|
||||
|
||||
## Background
|
||||
|
||||
DEC-0029 locks the canonical publication contract for `Scene Bank` assets around a `studio -> packer` request that compiles directly into runtime `SCENE` bytes. It also locks `TMX`/`TSX` as supported but non-canonical editorial surfaces.
|
||||
|
||||
The current codebase already contains wave-1 Tiled support and Scene Bank validation flows, but the normative specs do not yet describe:
|
||||
|
||||
- the canonical scene-pack request,
|
||||
- the non-canonical status of `TMX`/`TSX`,
|
||||
- the `Accept` projection boundary,
|
||||
- or packer ownership of binary `SCENE` compilation from a request rather than from editor files.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Update Studio specs to describe Scene Bank editorial ownership, validation, `Accept`, and canonical request projection.
|
||||
- Update packer specs to describe request-owned publication, `SCENE/tilemap_v1`, and the prohibition on direct parsing of editor-specific formats by packer.
|
||||
- Cross-link Studio and packer specs to DEC-0029 and to the existing Scene Bank asset-first wave.
|
||||
|
||||
### Excluded
|
||||
- Java implementation changes in `prometeu-studio`.
|
||||
- Java implementation changes in `prometeu-packer`.
|
||||
- Lesson or housekeeping work for DSC-0030.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Update Studio asset workspace specification
|
||||
|
||||
**What:** Add normative Scene Bank language to the Studio `Assets` workspace spec.
|
||||
|
||||
**How:** Extend the asset-family-specific rules in the Studio spec so Scene Bank authoring is explicitly asset-owned, `TMX`/`TSX` are editorial integrations rather than canonical publication artifacts, and `Accept` is defined as the projection point into the canonical pack request.
|
||||
|
||||
**File(s):**
|
||||
- `docs/specs/studio/4. Assets Workspace Specification.md`
|
||||
|
||||
### Step 2 - Update packer declaration and build-boundary specifications
|
||||
|
||||
**What:** Describe the canonical request and packer-owned build behavior in packer specs.
|
||||
|
||||
**How:** Amend the common asset contract spec and the operational/build specs so `SCENE/tilemap_v1` is documented as a runtime-oriented format whose packer input is the canonical scene-pack request produced by Studio, not raw editor files. Make explicit that editor-specific files remain tooling/editorial surfaces and MUST NOT be parsed directly by packer.
|
||||
|
||||
**File(s):**
|
||||
- `docs/specs/packer/3. Asset Declaration and Virtual Asset Contract Specification.md`
|
||||
- `docs/specs/packer/4. Build Artifacts and Deterministic Packing Specification.md`
|
||||
- `docs/specs/packer/5. Diagnostics, Operations, and Studio Integration Specification.md`
|
||||
|
||||
### Step 3 - Record canonical field semantics
|
||||
|
||||
**What:** Add the canonical scene-pack request fields and rules.
|
||||
|
||||
**How:** Document the fixed four-layer request shape, layer-level `glyph_asset_id`, `tile_size`, `parallax_factor`, `width`, `height`, tile-level `active`, `glyph_id`, `palette_id`, `flip_x`, `flip_y`, and the rule that `tile_count` is derived from dimensions. Lock the default-plus-override semantics for `palette_id`.
|
||||
|
||||
**File(s):**
|
||||
- `docs/specs/studio/4. Assets Workspace Specification.md`
|
||||
- `docs/specs/packer/4. Build Artifacts and Deterministic Packing Specification.md`
|
||||
|
||||
### Step 4 - Align gating and future-editor rules
|
||||
|
||||
**What:** Ensure the specs preserve future editor substitution without weakening the current wave.
|
||||
|
||||
**How:** Add explicit normative text that any future editor may replace or coexist with Tiled-backed editing only if it emits the same canonical request and passes the same validation and `Accept` rules. Preserve the wave-1 single-tileset-per-layer restriction.
|
||||
|
||||
**File(s):**
|
||||
- `docs/specs/studio/4. Assets Workspace Specification.md`
|
||||
- `docs/specs/packer/5. Diagnostics, Operations, and Studio Integration Specification.md`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- No code unit tests are required for this editorial plan.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- No automated integration tests are required for this editorial plan.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Review the updated specs side by side with DEC-0029 and confirm that every normative rule in the decision has a corresponding spec statement.
|
||||
- Confirm there is no spec language implying that packer parses `TMX` or `TSX` directly.
|
||||
- Confirm the specs do not describe Tiled as deprecated or temporary.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] The Studio spec defines Scene Bank `Accept` as the projection boundary into the canonical scene-pack request.
|
||||
- [ ] The packer specs define `SCENE/tilemap_v1` publication in terms of the canonical request, not editor files.
|
||||
- [ ] The specs state that `TMX`/`TSX` are supported but non-canonical editorial surfaces.
|
||||
- [ ] The specs allow future additional editors only through the same canonical request and validation path.
|
||||
- [ ] Every normative requirement in DEC-0029 is represented in at least one spec file.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEC-0029 accepted.
|
||||
|
||||
## Risks
|
||||
|
||||
- Editorial drift between Studio and packer specs could reintroduce ambiguity about who owns the canonical request.
|
||||
- If the specs omit the field semantics for `palette_id` precedence or tile/layer `active`, code plans may diverge during implementation.
|
||||
@ -0,0 +1,137 @@
|
||||
---
|
||||
id: PLN-0060
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Build Scene Bank canonical request projection and acceptance in Studio
|
||||
status: done
|
||||
created: 2026-04-29
|
||||
completed: 2026-04-29
|
||||
tags:
|
||||
- studio
|
||||
- scene
|
||||
- assets
|
||||
- tiled
|
||||
- acceptance
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the Studio-owned projection from supported editorial scene state into the canonical scene-pack request, with validation and `Accept` gating aligned to DEC-0029.
|
||||
|
||||
## Background
|
||||
|
||||
Studio already owns Scene Bank support metadata, Tiled generation, diagnostics, validation, and explicit acceptance. Today, however, the acceptance flow persists only validation fingerprints and does not yet materialize the canonical `studio -> packer` request required by DEC-0029.
|
||||
|
||||
This plan turns `Accept` from a pure approval marker into the normative projection boundary for Scene Bank publication.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Extend Scene Bank support metadata and UI/editorial state to carry the fields required by the canonical request.
|
||||
- Generate and persist the canonical request during Scene Bank `Accept`.
|
||||
- Validate the editorial-to-request mapping, including layer defaults and tile overrides.
|
||||
- Keep Tiled integration as a supported editorial path without making it canonical.
|
||||
|
||||
### Excluded
|
||||
- Binary `SCENE` encoding in packer.
|
||||
- Runtime decoder or runtime scene-binding changes.
|
||||
- A new integrated scene editor.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Expand Scene Bank editorial metadata
|
||||
|
||||
**What:** Add the missing layer-owned editorial fields required by the canonical request.
|
||||
|
||||
**How:** Extend the Scene Bank metadata model and serialization so each layer can carry at minimum `parallax_factor_x`, `parallax_factor_y`, and a layer-level default `palette_id`, while preserving the existing enabled-state, dimensions, `tilemap`, and `tileset_asset_root` binding rules.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneBankMetadata.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioSceneLayerBinding.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataService.java`
|
||||
- `prometeu-studio/src/test/java/p/studio/workspaces/assets/metadata/AssetStudioMetadataServiceTest.java`
|
||||
|
||||
### Step 2 - Surface the new fields in the Scene Bank details UI
|
||||
|
||||
**What:** Make the canonical request inputs editable and reviewable in Studio.
|
||||
|
||||
**How:** Update the Scene Bank layer controls so the layer form exposes parallax values, default palette selection, and clearly ties `glyph_asset_id` derivation to the selected tileset asset. Preserve the existing enabled checkbox as the layer-level publication toggle.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/scene/AssetDetailsSceneLayersControl.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsControl.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java`
|
||||
- `prometeu-studio/src/main/resources/i18n/messages.properties`
|
||||
|
||||
### Step 3 - Build canonical request projection from accepted editorial state
|
||||
|
||||
**What:** Materialize the canonical scene-pack request during `Accept`.
|
||||
|
||||
**How:** Introduce Studio-side request DTOs or records for `ScenePackRequest`, `ScenePackLayerRequest`, and `ScenePackTileRequest`. Update `SceneBankWorkflowService` so `Accept` validates the supported editorial inputs, parses `TMX`/`TSX`, derives `glyph_asset_id`, applies layer-default plus per-tile `palette_id` overrides, resolves per-tile `glyph_id`, `flip_x`, `flip_y`, and persists the resulting canonical request as the accepted publication input for packer consumption.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/scene/SceneBankWorkflowService.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/scene/SceneBankWorkflowResult.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/tiled/TiledXmlCodec.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/tiled/TiledMapDocument.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/tiled/TiledTilesetDocument.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/tiled/TiledTilesetTile.java`
|
||||
|
||||
### Step 4 - Tighten validation around canonical semantics
|
||||
|
||||
**What:** Ensure Studio rejects editorial states that cannot be projected safely into the canonical request.
|
||||
|
||||
**How:** Extend validation so it blocks acceptance when a layer lacks a resolvable tileset asset, when per-tile data cannot be mapped to a valid `glyph_id` or `palette_id`, when unsupported Tiled features would break canonical projection, or when wave-1 constraints such as single-tileset-per-layer are violated.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/scene/SceneBankWorkflowService.java`
|
||||
- `prometeu-studio/src/test/java/p/studio/workspaces/assets/scene/SceneBankWorkflowServiceTest.java`
|
||||
- `prometeu-studio/src/test/java/p/studio/workspaces/assets/tiled/TiledXmlCodecTest.java`
|
||||
- `prometeu-studio/src/test/java/p/studio/workspaces/assets/tiled/TiledAssetGenerationServiceTest.java`
|
||||
|
||||
### Step 5 - Expose acceptance artifacts to downstream consumers
|
||||
|
||||
**What:** Make the accepted canonical request visible to the rest of the Studio-to-packer flow.
|
||||
|
||||
**How:** Persist the accepted request in a deterministic asset-local file or equivalent accepted artifact and update the details refresh flow so the accepted/publication-ready state reflects both validation success and presence of the canonical request artifact.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/scene/SceneBankWorkflowService.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceAssetDetails.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/details/AssetDetailsViewStateFactory.java`
|
||||
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/messages/AssetWorkspaceSceneBankValidation.java`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Add metadata serialization/deserialization coverage for the new layer fields.
|
||||
- Add request-projection tests for `glyph_asset_id` derivation, default `palette_id`, per-tile `palette_id` override, and tile flip mapping.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Add Scene Bank workflow tests covering validate-then-accept for a valid layer set and failure cases for unsupported or incomplete editorial state.
|
||||
- Add acceptance-artifact tests proving external file mutation invalidates the accepted state until revalidation and reacceptance.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Create or update a Scene Bank asset in the test project, validate it, accept it, and confirm the accepted artifact contains the full canonical request.
|
||||
- Change a `TMX` or `TSX` file after acceptance and confirm Studio returns the asset to a non-ready state.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scene Bank metadata and UI expose the layer-owned fields required by DEC-0029.
|
||||
- [ ] `Accept` emits a deterministic canonical scene-pack request artifact from the supported editorial state.
|
||||
- [ ] Validation blocks any Scene Bank that cannot be projected into the canonical request without guessing.
|
||||
- [ ] Layer default and per-tile override semantics for `palette_id` are implemented and tested.
|
||||
- [ ] Studio continues to support Tiled-backed editing without making `TMX`/`TSX` the canonical publication surface.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEC-0029 accepted.
|
||||
- PLN-0059 should land first or in parallel so the code aligns to updated specs.
|
||||
|
||||
## Risks
|
||||
|
||||
- The current Tiled parsing model may not yet expose every per-tile property needed for deterministic request projection.
|
||||
- If the accepted artifact shape is underspecified, packer integration may later depend on unstable serialization details.
|
||||
- UI additions for layer metadata can introduce partial-state bugs if draft/application flow is not kept consistent.
|
||||
@ -0,0 +1,130 @@
|
||||
---
|
||||
id: PLN-0061
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Compile the canonical Scene Bank request into runtime SCENE in packer
|
||||
status: done
|
||||
created: 2026-04-29
|
||||
completed: 2026-04-29
|
||||
tags:
|
||||
- packer
|
||||
- scene
|
||||
- runtime
|
||||
- binary-format
|
||||
- build
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the packer-owned path that reads the accepted canonical scene-pack request and emits runtime `SCENE` binary payloads for `Scene Bank` assets.
|
||||
|
||||
## Background
|
||||
|
||||
`prometeu-packer` already recognizes `SCENE_BANK` and the `SCENE/tilemap_v1` output format, but `SCENE_BANK` currently stops short of real materialization. DEC-0029 locks the rule that packer must compile a canonical request and must not parse `TMX` or `TSX` directly.
|
||||
|
||||
This plan delivers the missing operational half of the boundary.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Introduce packer-side models for the canonical Scene Bank request.
|
||||
- Validate the accepted request artifact as a packer input.
|
||||
- Encode runtime `SCENE` binary payloads from the canonical request.
|
||||
- Integrate Scene Bank packing into the existing runtime materialization/build flow.
|
||||
|
||||
### Excluded
|
||||
- Studio UI and metadata authoring changes.
|
||||
- Runtime decoder changes beyond what is already assumed by DEC-0029.
|
||||
- Multi-tileset-per-layer or non-wave-1 scene authoring features.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Add canonical request models to the packer API
|
||||
|
||||
**What:** Define stable packer-side Java models for the canonical Scene Bank request.
|
||||
|
||||
**How:** Add DTOs/records for `ScenePackRequest`, `ScenePackLayerRequest`, and `ScenePackTileRequest` in `prometeu-packer-api`, including validation helpers for fixed layer count, required dimensions, and field presence. These models must be editor-agnostic and must not mention `TMX` or `TSX`.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-packer/prometeu-packer-api/src/main/java/p/packer/messages/assets/`
|
||||
- `prometeu-packer/prometeu-packer-api/build.gradle.kts` if package exports or module wiring need updates
|
||||
|
||||
### Step 2 - Teach packer to read and validate the accepted request artifact
|
||||
|
||||
**What:** Load Scene Bank publication input from the accepted request artifact rather than from editor files.
|
||||
|
||||
**How:** Extend the Scene Bank walking/materialization path so packer looks for the accepted canonical request artifact in the asset root, validates the fixed four-layer structure, checks `width * height` against tile counts, rejects missing or malformed request content, and emits structural diagnostics when the request is absent or invalid.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerAssetWalker.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeAssetMaterializer.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java`
|
||||
|
||||
### Step 3 - Encode runtime `SCENE` payload bytes
|
||||
|
||||
**What:** Turn the canonical request into the runtime binary representation.
|
||||
|
||||
**How:** Implement a Scene Bank packer encoder that maps each canonical layer and tile entry directly into the versioned `SCENE` payload layout, including `glyph_asset_id`, tile size, parallax, dimensions, active state, tile activity, palette IDs, flips, and derived `tile_count`. Keep request-to-binary projection as close to isomorphic as possible and avoid injecting editor-only fields.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/models/`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/`
|
||||
|
||||
### Step 4 - Integrate Scene Bank artifacts into pack output
|
||||
|
||||
**What:** Make Scene Bank assets participate in actual pack output and runtime metadata emission.
|
||||
|
||||
**How:** Update the relevant packer services so `SCENE_BANK` assets contribute emitted runtime files and asset-table metadata in the same build lane used by other runtime asset families, while preserving `SCENE/tilemap_v1` as the declared output format.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/services/FileSystemPackerWorkspaceService.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeRegistry.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/main/java/p/packer/repositories/PackerRuntimeLoader.java`
|
||||
|
||||
### Step 5 - Prove packer/editor boundary discipline with tests
|
||||
|
||||
**What:** Prevent regression toward direct editor parsing in packer.
|
||||
|
||||
**How:** Add tests that succeed with a valid accepted canonical request even when Tiled files are ignored by packer, and tests that fail when the accepted request is missing or structurally invalid. Assert that packer diagnostics talk about the canonical request artifact rather than raw Tiled file concerns.
|
||||
|
||||
**File(s):**
|
||||
- `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/services/FileSystemPackerWorkspaceServiceTest.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/repositories/PackerRuntimeAssetMaterializerTest.java`
|
||||
- `prometeu-packer/prometeu-packer-v1/src/test/java/p/packer/testing/`
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Add model validation tests for the canonical Scene Bank request records.
|
||||
- Add encoder tests for field-level request-to-binary mapping, including `glyph_asset_id`, parallax, dimensions, activity flags, palette IDs, and flips.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Add workspace/build tests covering a valid Scene Bank asset end to end from accepted request artifact to emitted `assets.pa`.
|
||||
- Add failure tests for missing request artifact, invalid layer counts, invalid tile counts, and malformed field values.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Run a pack build on a project containing an accepted Scene Bank and inspect the emitted runtime artifact summary to confirm the scene asset is present.
|
||||
- Verify that modifying only `TMX`/`TSX` without regenerating the accepted request does not cause packer to silently read the editor files directly.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Packer can read a canonical Scene Bank request artifact without depending on `TMX` or `TSX`.
|
||||
- [ ] `SCENE_BANK` assets produce runtime `SCENE` payload bytes in pack output.
|
||||
- [ ] Structural diagnostics exist for missing or malformed accepted request artifacts.
|
||||
- [ ] Request-to-binary mapping is covered by automated tests.
|
||||
- [ ] Packer behavior preserves the editor-agnostic boundary locked by DEC-0029.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEC-0029 accepted.
|
||||
- PLN-0060 must land before final end-to-end validation because Studio owns generation of the accepted canonical request artifact.
|
||||
- PLN-0059 should land first or in parallel so packer implementation aligns to updated specs.
|
||||
|
||||
## Risks
|
||||
|
||||
- The exact runtime `SCENE` binary layout may still need careful reconciliation with existing runtime decoder assumptions.
|
||||
- If the accepted request artifact versioning is not explicit from the start, future format evolution may become harder to stage safely.
|
||||
- Scene Bank integration may touch several packer build phases, increasing regression risk in `assets.pa` generation.
|
||||
@ -0,0 +1,101 @@
|
||||
---
|
||||
id: PLN-0062
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Propagate DEC-0030 into Studio and packer specifications
|
||||
status: done
|
||||
created: 2026-04-30
|
||||
completed: 2026-04-30
|
||||
tags: [studio, packer, runtime, scene, specs, import-export]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Update the normative documentation surfaces so DEC-0030 is fully represented before or alongside code changes. The resulting specs must make the Studio-owned canonical scene entity, import/export workflow, and request separation unambiguous.
|
||||
|
||||
## Background
|
||||
|
||||
DEC-0030 supersedes the editorial-model portion of DEC-0029. The publication boundary locked by DEC-0029 remains valid, but the Studio editorial model changes materially:
|
||||
|
||||
- Studio owns a canonical scene entity;
|
||||
- `TMX`/`TSX` become import/export compatibility formats;
|
||||
- validation moves to the canonical scene entity in `Analyse` and `Pack`;
|
||||
- the scene asset model remains distinct from the `studio -> packer` request.
|
||||
|
||||
The existing specs currently describe or imply the older `TMX per layer` and `validate/accept` workflow and therefore require explicit propagation.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Update Studio specs to define the canonical scene entity as the primary editorial model.
|
||||
- Update Studio specs to define `TMX` and `TSX` as import/export formats, not canonical persistence.
|
||||
- Update packer specs to reaffirm that packer consumes only the canonical request derived from the canonical scene entity.
|
||||
- Cross-reference DEC-0029 and DEC-0030 with explicit note about what changed and what remains locked.
|
||||
- Define the validation surface shift from editor files to canonical scene analysis/pack validation.
|
||||
|
||||
### Excluded
|
||||
- Code changes in `prometeu-studio`.
|
||||
- Code changes in `prometeu-packer`.
|
||||
- Final canonical scene schema details beyond what DEC-0030 already locks.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Update Studio normative scene workflow documentation
|
||||
|
||||
**What:** Rewrite the Studio scene authoring spec so it names the Studio-owned canonical scene entity as the source of truth.
|
||||
**How:** Replace language that describes `TMX` generation, per-layer `TMX`, or `validate/accept` as the editorial workflow. Introduce import/export wizard language and preserve the distinction between canonical scene entity and external compatibility files.
|
||||
**File(s):** `docs/studio/specs/...` files that currently define Scene Bank workflow and Tiled integration.
|
||||
|
||||
### Step 2 - Update Studio validation and interoperability specification
|
||||
|
||||
**What:** Document import compatibility checks, export overwrite semantics, and validation ownership.
|
||||
**How:** Add normative statements for:
|
||||
- import compatibility failure on unsupported structural mutations;
|
||||
- diagnostic/logging requirements for failed imports;
|
||||
- `TMX` export overwrite/create behavior;
|
||||
- `TSX` export overwrite/create behavior for glyph tilesets;
|
||||
- canonical scene validation during `Analyse` and `Pack`.
|
||||
**File(s):** Studio scene and glyph tileset spec files.
|
||||
|
||||
### Step 3 - Update packer normative boundary documentation
|
||||
|
||||
**What:** Reaffirm the packer contract after DEC-0030.
|
||||
**How:** Clarify that packer still consumes only the canonical scene-pack request and remains isolated from editor-native formats, even though Studio now owns an additional canonical scene entity layer upstream.
|
||||
**File(s):** `docs/packer/specs/...` scene packing contract files.
|
||||
|
||||
### Step 4 - Record supersession and preserved rules
|
||||
|
||||
**What:** Make the DEC-0029 to DEC-0030 relationship explicit in the relevant specs.
|
||||
**How:** Note that DEC-0030 replaces only the old editorial-model assumptions, while preserving the canonical publication boundary, runtime ownership, and request compilation model.
|
||||
**File(s):** Updated Studio and packer spec references.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
No code tests. Editorial review only.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
No runtime integration tests. Validate consistency across affected spec surfaces.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Read updated Studio and packer specs side by side with DEC-0030.
|
||||
- Confirm there is no remaining normative language that presents `TMX`/`TSX` as canonical scene persistence.
|
||||
- Confirm there is no remaining normative language that treats `validate/accept` of external editor files as the scene editorial lifecycle.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Every normative requirement in DEC-0030 is represented in at least one spec surface.
|
||||
- [ ] Studio specs clearly distinguish canonical scene entity, external import/export files, and pack request.
|
||||
- [ ] Packer specs clearly preserve the editor-agnostic request compilation boundary.
|
||||
- [ ] Spec language no longer implies `TMX per layer` or `validate/accept` over `TMX` as the primary workflow.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEC-0030 accepted.
|
||||
|
||||
## Risks
|
||||
|
||||
- Leaving mixed old/new vocabulary in specs would create implementation ambiguity.
|
||||
- Over-specifying the canonical scene schema here would preempt downstream implementation design that DEC-0030 intentionally leaves for planning.
|
||||
@ -0,0 +1,107 @@
|
||||
---
|
||||
id: PLN-0063
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Introduce the Studio canonical scene entity and project it into analysis and pack requests
|
||||
status: done
|
||||
created: 2026-04-30
|
||||
completed: 2026-04-30
|
||||
tags: [studio, scene, canonical-model, analysis, pack-request]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement the Studio-owned canonical scene entity and make it the sole editorial source of truth for Scene Bank assets, including canonical validation during `Analyse` and projection into the `studio -> packer` request.
|
||||
|
||||
## Background
|
||||
|
||||
DEC-0030 removes the old `TMX per layer` workflow and replaces it with a Studio-native canonical scene entity. The Studio must now own:
|
||||
|
||||
- canonical scene persistence;
|
||||
- canonical scene analysis/diagnostics;
|
||||
- derivation of the canonical scene-pack request from canonical scene state.
|
||||
|
||||
This is the core refactor that re-centers the domain model. External `TMX`/`TSX` interoperability is handled separately.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Define and persist a canonical scene entity for Scene Bank assets.
|
||||
- Replace old scene validation state that depends on `TMX`/layer acceptance.
|
||||
- Move diagnostics and validation to the canonical scene entity in `Analyse` and asset-pack readiness surfaces.
|
||||
- Derive the canonical `studio -> packer` request from the canonical scene entity.
|
||||
- Remove or retire the old layer-level `validate/accept` workflow from Studio scene handling.
|
||||
|
||||
### Excluded
|
||||
- `TMX` scene import/export implementation details.
|
||||
- `TSX` export implementation details.
|
||||
- Changes to runtime `SCENE` binary decoding.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Define canonical scene persistence and metadata model
|
||||
|
||||
**What:** Introduce the persisted Studio-owned scene entity and associated metadata structures.
|
||||
**How:** Add or revise Scene Bank metadata classes and persistence so the canonical scene entity is stored independently of `TMX` and independently of the pack request artifact. Preserve explicit layer metadata and any required editorial fields without treating external files as the source of truth.
|
||||
**File(s):** `prometeu-studio` Scene Bank metadata models, persistence services, and asset details mappings.
|
||||
|
||||
### Step 2 - Replace old workflow state and actions
|
||||
|
||||
**What:** Remove the old `validate layer`, `accept layer`, and `accept scene from TMX` lifecycle as the authoritative workflow.
|
||||
**How:** Delete or refactor workflow services, action wiring, and UI status logic so Scene Bank readiness is derived from canonical scene analysis rather than per-layer external-file fingerprints.
|
||||
**File(s):** `SceneBankWorkflowService`, asset details controls, scene layer controls, message/status models, and related UI wiring.
|
||||
|
||||
### Step 3 - Implement canonical analysis and diagnostics
|
||||
|
||||
**What:** Make `Analyse` and pack-readiness diagnostics validate the canonical scene entity.
|
||||
**How:** Add validators that check canonical layer structure, glyph asset references, layer/tile invariants, and request derivation preconditions directly from canonical scene data. Ensure diagnostics remain explicit and actionable.
|
||||
**File(s):** Studio analysis services, scene diagnostics, asset details status surfaces, and any supporting validators.
|
||||
|
||||
### Step 4 - Project canonical scene state into the pack request
|
||||
|
||||
**What:** Derive the `studio -> packer` request exclusively from the canonical scene entity.
|
||||
**How:** Replace the old `TMX`-driven request projection with canonical entity projection, preserving the existing request/runtime boundary. Persist the request artifact only as a publication projection, not as canonical editorial state.
|
||||
**File(s):** Scene request projection services, request serialization, asset workflow persistence, and related tests.
|
||||
|
||||
### Step 5 - Migrate or adapt project fixtures and internal examples
|
||||
|
||||
**What:** Update test fixtures and example assets so they reflect the new canonical scene model.
|
||||
**How:** Replace sample assets, metadata fixtures, and scene workflow tests that currently depend on `TMX per layer` acceptance semantics.
|
||||
**File(s):** `test-projects/main`, Studio workflow tests, fixture generators, and scene sample assets.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Canonical scene persistence round-trip tests.
|
||||
- Canonical scene validation tests for blocking and non-blocking diagnostics.
|
||||
- Request projection tests proving the canonical scene entity, not `TMX`, drives the generated scene-pack request.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- End-to-end Studio asset workflow tests covering scene analysis and pack-request artifact generation from canonical scene state.
|
||||
- Regression tests proving old layer acceptance files no longer define readiness.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Open a Scene Bank asset and confirm there is no primary `validate/accept TMX` workflow.
|
||||
- Run `Analyse` and verify diagnostics reflect canonical scene state.
|
||||
- Confirm pack-request generation occurs from canonical scene state and not from external editor files.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scene Bank assets persist a Studio-owned canonical scene entity distinct from both `TMX` and the pack request.
|
||||
- [ ] Studio readiness/diagnostics for scenes are driven by canonical scene analysis.
|
||||
- [ ] The old `validate/accept` layer-centric workflow no longer defines scene publication readiness.
|
||||
- [ ] The `studio -> packer` request is derived from canonical scene state only.
|
||||
- [ ] Existing request/runtime boundary semantics from DEC-0029 remain preserved.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEC-0030 accepted.
|
||||
- PLN-0062 should land first or in parallel so code aligns with updated specs.
|
||||
|
||||
## Risks
|
||||
|
||||
- Hidden dependencies on old acceptance fingerprint files may remain in the UI or workspace services.
|
||||
- If canonical schema design is underspecified in code, the implementation may accidentally recreate a `TMX-shaped` persistence model under a different name.
|
||||
- Fixture migration may expose broad assumptions in tests and project samples.
|
||||
@ -0,0 +1,105 @@
|
||||
---
|
||||
id: PLN-0064
|
||||
ticket: studio-scene-pack-runtime-binary-contract
|
||||
title: Implement Tiled scene import and glyph tileset export as compatibility workflows
|
||||
status: done
|
||||
created: 2026-04-30
|
||||
completed: 2026-04-30
|
||||
tags: [studio, tiled, tmx, tsx, import-export, interoperability]
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Implement `Tiled` interoperability as explicit compatibility workflow: scene `TMX` import into the canonical Studio scene entity, scene `TMX` export from that entity, and glyph tileset `TSX` export for Tiled consumption.
|
||||
|
||||
## Background
|
||||
|
||||
DEC-0030 keeps `TMX` and `TSX` as supported formats, but only for import/export. This means external file handling becomes adapter logic around the canonical scene and glyph-bank models, with explicit compatibility checks and overwrite semantics.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
- Wizard-driven `TMX (Tiled)` import flow for Scene Bank assets.
|
||||
- Wizard-driven `TMX (Tiled)` export flow for Scene Bank assets.
|
||||
- Wizard-driven or equivalent `TSX (Tiled)` export flow for glyph tilesets.
|
||||
- Import compatibility validation and failure diagnostics for unsupported structural divergence.
|
||||
- Overwrite/create semantics for exported `TMX` and `TSX`.
|
||||
|
||||
### Excluded
|
||||
- Additional external scene formats beyond Tiled.
|
||||
- Packer-side parsing of `TMX` or `TSX`.
|
||||
- Broader native scene editing UX beyond what is required to support compatibility flow integration.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1 - Build Tiled import compatibility validation
|
||||
|
||||
**What:** Add import-time structural checks for supported `TMX` scene files.
|
||||
**How:** Validate the incoming `TMX` against the Studio compatibility contract before mutating canonical scene state. Reject unsupported mutations such as unexpected layers, unexpected tilesets, unsupported structural changes, or equivalent divergences. Produce explicit logs/diagnostics on failure.
|
||||
**File(s):** Tiled scene parsing/import services, diagnostics types, and import workflow orchestration.
|
||||
|
||||
### Step 2 - Implement canonical scene mutation from imported TMX
|
||||
|
||||
**What:** Update canonical scene state from accepted `TMX` imports.
|
||||
**How:** Translate compatible Tiled scene content into the canonical Studio scene entity without storing the `TMX` itself as source of truth. Ensure failed imports do not partially mutate canonical state.
|
||||
**File(s):** Scene import services, canonical scene mutation helpers, asset persistence layer, and import wizard state.
|
||||
|
||||
### Step 3 - Implement TMX export from canonical scene state
|
||||
|
||||
**What:** Serialize the canonical scene entity back into `TMX (Tiled)`.
|
||||
**How:** Generate or overwrite the destination `TMX` from canonical scene state using stable export rules. Ensure the output is explicit compatibility output and not the canonical persistence model.
|
||||
**File(s):** Tiled scene export services, wizard flows, and file-writing surfaces.
|
||||
|
||||
### Step 4 - Reframe TSX generation as export
|
||||
|
||||
**What:** Convert glyph tileset `TSX` generation into explicit `TSX (Tiled)` export behavior.
|
||||
**How:** Rename or refactor the current glyph tileset generation flow so it is clearly an export operation, with overwrite/create semantics and no implication that `TSX` is canonical storage.
|
||||
**File(s):** Glyph tileset export/generation services, UI actions, workflow naming, and related tests.
|
||||
|
||||
### Step 5 - Wire import/export into Studio UI and logs
|
||||
|
||||
**What:** Expose the interoperability workflow in the Studio asset UI.
|
||||
**How:** Add wizard entry points, action labels, progress/error reporting, and logs for successful export, successful import, and rejected imports.
|
||||
**File(s):** Asset details UI, Scene Bank controls, glyph tileset controls, event/log surfaces, and i18n messages.
|
||||
|
||||
## Test Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Import validation tests for supported and rejected `TMX` structures.
|
||||
- Scene import mutation tests proving canonical state changes only on successful import.
|
||||
- `TMX` export tests proving overwrite/create behavior.
|
||||
- `TSX` export tests proving overwrite/create behavior.
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- End-to-end import workflow test from `TMX` into canonical scene state.
|
||||
- End-to-end export workflow tests for `TMX` and `TSX`.
|
||||
- Regression tests proving failed imports do not partially rewrite canonical state.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
- Import a supported Tiled scene and confirm canonical scene state updates.
|
||||
- Import an unsupported Tiled scene mutation and confirm the import is rejected with clear logs.
|
||||
- Export a scene to `TMX` and confirm the destination file is overwritten or created.
|
||||
- Export a glyph tileset to `TSX` and confirm the destination file is overwritten or created.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Scene `TMX` import exists as explicit compatibility workflow and validates minimum structural compatibility before mutation.
|
||||
- [ ] Failed imports emit clear diagnostics/logs and do not partially rewrite canonical state.
|
||||
- [ ] Scene `TMX` export exists as explicit compatibility workflow with overwrite/create semantics.
|
||||
- [ ] Glyph tileset `TSX` export exists as explicit compatibility workflow with overwrite/create semantics.
|
||||
- [ ] Studio UI reflects `import/export` terminology rather than the old `generate/validate/accept` model for Tiled interoperability.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- DEC-0030 accepted.
|
||||
- PLN-0063 must land before final import/export integration because import/export targets the canonical scene entity owned by Studio.
|
||||
- PLN-0062 should land first or in parallel so UI and code terminology align with updated specs.
|
||||
|
||||
## Risks
|
||||
|
||||
- If import compatibility rules are too loose, external files will silently reshape canonical scene state.
|
||||
- If import compatibility rules are too strict, Tiled interoperability will be noisy and frustrating.
|
||||
- UI naming may remain half-migrated if old action labels or logs are left behind.
|
||||
@ -221,7 +221,8 @@ let status: int = Composer.emit_sprite(7, 0, 12, 18, 0, 2, false, false, 1);
|
||||
|
||||
Rules:
|
||||
|
||||
- `@sdk:composer` is the canonical sprite-composition module in this wave.
|
||||
- `@sdk:composer` is the canonical frame-composition module in this wave.
|
||||
- `Composer.bind_scene(...)`, `Composer.unbind_scene()`, and `Composer.emit_sprite(...)` belong to that module.
|
||||
- `@sdk:gfx` remains valid for primitive and overlay-oriented graphics operations.
|
||||
- `Gfx.set_sprite` is not part of the active stdlib line `1` contract.
|
||||
|
||||
|
||||
@ -141,6 +141,14 @@ For example, the PBS-facing declaration:
|
||||
|
||||
```pbs
|
||||
declare host LowComposer {
|
||||
[Host(module = "composer", name = "bind_scene", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn bind_scene(scene_bank_id: int) -> int;
|
||||
|
||||
[Host(module = "composer", name = "unbind_scene", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn unbind_scene() -> int;
|
||||
|
||||
[Host(module = "composer", name = "emit_sprite", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn emit_sprite(
|
||||
@ -160,6 +168,8 @@ declare host LowComposer {
|
||||
maps to the canonical runtime identity:
|
||||
|
||||
```text
|
||||
("composer", "bind_scene", 1)
|
||||
("composer", "unbind_scene", 1)
|
||||
("composer", "emit_sprite", 1)
|
||||
```
|
||||
|
||||
|
||||
@ -254,6 +254,8 @@ Rules:
|
||||
- stdlib line `1` MUST expose the sprite-composition interface module at `@sdk:composer`,
|
||||
- that module MUST declare `LowComposer` and public service `Composer`,
|
||||
- that module MUST use runtime module `composer` and capability `gfx`,
|
||||
- that module MUST expose `bind_scene(scene_bank_id: int) -> int`,
|
||||
- that module MUST expose `unbind_scene() -> int`,
|
||||
- that module MUST expose `emit_sprite(glyph_id: int, palette_id: int, x: int, y: int, layer: int, bank_id: int, flip_x: bool, flip_y: bool, priority: int) -> int`,
|
||||
- stdlib line `1` MUST NOT expose `Gfx.set_sprite`,
|
||||
- stdlib line `1` MUST expose the low-level asset interface module at `@sdk:asset`,
|
||||
|
||||
@ -172,11 +172,15 @@ Rules:
|
||||
|
||||
- `@sdk:composer` MUST expose a low-level host owner `LowComposer`.
|
||||
- `@sdk:composer` MUST expose a public service facade `Composer`.
|
||||
- `Composer.bind_scene(scene_bank_id: int)` MUST lower through canonical host identity `("composer", "bind_scene", 1)`.
|
||||
- `Composer.unbind_scene()` MUST lower through canonical host identity `("composer", "unbind_scene", 1)`.
|
||||
- `Composer.emit_sprite(...)` MUST lower through canonical host identity `("composer", "emit_sprite", 1)`.
|
||||
- `Composer.bind_scene(...)` MUST return a raw `int` in stdlib line `1`.
|
||||
- `Composer.unbind_scene()` MUST return a raw `int` in stdlib line `1`.
|
||||
- `Composer.emit_sprite(...)` MUST return a raw `int` in this wave.
|
||||
- `@sdk:gfx` MAY continue to expose primitive and overlay-oriented operations.
|
||||
- `@sdk:gfx` MUST NOT expose `set_sprite`.
|
||||
- `bind_scene`, `unbind_scene`, and `set_camera` are deferred and are not required members of `@sdk:composer` in this wave.
|
||||
- `set_camera` may remain deferred if not yet exposed by the active stdlib line.
|
||||
|
||||
## 13. Stdlib Contract Expectations for Builtin MVP
|
||||
|
||||
|
||||
@ -97,6 +97,18 @@ Rules:
|
||||
- `output.pipeline` carries tooling/build metadata kept separate at authoring time;
|
||||
- codec must remain explicit and must not be hidden inside format naming.
|
||||
|
||||
### Scene Bank Contract Boundary
|
||||
|
||||
For `Scene Bank` assets, `output.format = SCENE/tilemap_v1` identifies the runtime-facing scene contract.
|
||||
|
||||
Rules:
|
||||
|
||||
- `SCENE/tilemap_v1` is a runtime/publication format, not an editor-file format;
|
||||
- `TMX`, `TSX`, and other editor-specific files may exist as tooling/editorial surfaces, but they are not the canonical publication contract;
|
||||
- the canonical packer input for `Scene Bank` publication is the editor-agnostic Scene Bank request emitted by Studio from canonical scene state;
|
||||
- packer must not reinterpret the presence of editorial files as authorization to bypass the request boundary;
|
||||
- packer must not assume any external scene file is canonical persistence, even when Studio supports that file as import/export compatibility.
|
||||
|
||||
### Explicit Index Collections
|
||||
|
||||
When `output.pipeline` carries ordered collections whose members need stable semantic identity, that identity must not depend on raw list position alone.
|
||||
|
||||
@ -212,6 +212,65 @@ Rules:
|
||||
- any tile in the bank may be rendered with any palette in the bank at runtime;
|
||||
- palette selection is a runtime draw concern, not a tile-payload embedding concern.
|
||||
|
||||
## Format-Specific Baseline: `SCENE/tilemap_v1`
|
||||
|
||||
`SCENE/tilemap_v1` is the wave-1 Scene Bank publication format.
|
||||
|
||||
### Canonical Build Input
|
||||
|
||||
Rules:
|
||||
|
||||
- packer receives a canonical editor-agnostic Scene Bank request as its publication input;
|
||||
- packer must not parse `TMX`, `TSX`, or other editor-specific source files directly as part of runtime scene publication;
|
||||
- the request is produced by Studio from canonical scene state after canonical validation;
|
||||
- equivalent requests must produce equivalent emitted `SCENE` payloads.
|
||||
|
||||
Baseline request shape:
|
||||
|
||||
```text
|
||||
ScenePackRequest
|
||||
- version: int
|
||||
- layers: [ScenePackLayerRequest; 4]
|
||||
|
||||
ScenePackLayerRequest
|
||||
- active: bool
|
||||
- glyph_asset_id: int
|
||||
- tile_size: int
|
||||
- parallax_factor_x: float
|
||||
- parallax_factor_y: float
|
||||
- width: int
|
||||
- height: int
|
||||
- tiles: List<ScenePackTileRequest>
|
||||
|
||||
ScenePackTileRequest
|
||||
- active: bool
|
||||
- glyph_id: int
|
||||
- palette_id: int
|
||||
- flip_x: bool
|
||||
- flip_y: bool
|
||||
```
|
||||
|
||||
### Request Semantics
|
||||
|
||||
Rules:
|
||||
|
||||
- the request always materializes exactly `4` layer entries in wave 1;
|
||||
- `tile_count` is derived from `width * height`;
|
||||
- `glyph_asset_id` is the runtime-facing glyph dependency identity carried by the scene payload;
|
||||
- `active` at layer scope determines whether the layer participates in emitted scene output;
|
||||
- `active` at tile scope determines whether the tile contributes an active tile record to the emitted scene payload;
|
||||
- `palette_id` uses a layer-owned editorial default with optional per-tile override before the request reaches packer;
|
||||
- packer consumes only the resolved per-tile `palette_id` values present in the canonical request;
|
||||
- request-to-binary projection should remain as close to isomorphic as possible.
|
||||
|
||||
### Wave-1 Constraints
|
||||
|
||||
Rules:
|
||||
|
||||
- wave 1 remains limited to exactly one logical tileset/glyph source per layer before request emission;
|
||||
- packer does not own editor-format interoperability rules; it trusts the canonical request boundary once the request is structurally valid;
|
||||
- binary reserved fields must not require corresponding request fields unless a later decision gives them editorial meaning.
|
||||
|
||||
## Determinism
|
||||
|
||||
Equivalent build inputs must produce equivalent outputs.
|
||||
|
||||
@ -157,6 +157,25 @@ At minimum, the model should support fields equivalent to:
|
||||
- `applied_actions`
|
||||
- `blockers`
|
||||
|
||||
## Scene Bank Studio Integration Boundary
|
||||
|
||||
Scene Bank publication is a split workflow across Studio and packer.
|
||||
|
||||
Rules:
|
||||
|
||||
- Studio owns canonical scene validation and projection of the canonical Scene Bank request;
|
||||
- packer owns compilation of the canonical Scene Bank request into runtime `SCENE` output;
|
||||
- packer must report structural diagnostics against the canonical Scene Bank request artifact when that artifact is missing, malformed, or inconsistent with the locked format rules;
|
||||
- packer must not report editor-file parsing diagnostics as if `TMX` or `TSX` were direct runtime build inputs.
|
||||
|
||||
### Request Boundary Visibility
|
||||
|
||||
Rules:
|
||||
|
||||
- the Studio-to-packer handoff for Scene Bank publication must be a canonical request artifact or equivalent request surface derived from canonical Studio scene state;
|
||||
- if external compatibility files change without corresponding Studio import/update, packer must continue to treat the canonical request boundary as authoritative rather than silently reparsing editor files;
|
||||
- future supported editors may replace or coexist with Tiled-backed interoperability only if Studio continues to emit the same canonical request boundary.
|
||||
|
||||
## Asset Event Lane
|
||||
|
||||
The system includes a dedicated asset workspace event/reporting lane.
|
||||
|
||||
@ -258,6 +258,101 @@ Rules:
|
||||
- Primary actions for unregistered assets include `Register`.
|
||||
- `Register` is a direct action in Studio UX. If registration is valid, clicking it must register immediately without an extra acknowledgement step.
|
||||
- If registration is blocked, Studio may surface the blocking preview inline so the user can inspect why registration cannot proceed.
|
||||
|
||||
## Scene Bank Editorial and Publication Rules
|
||||
|
||||
`Scene Bank` is an asset-owned workflow inside `Assets`.
|
||||
|
||||
Rules:
|
||||
|
||||
- `Scene Bank` authoring, diagnostics, canonical validation, and external-format interoperability are owned by the `Assets` workspace;
|
||||
- the primary editorial surface for `Scene Bank` is a Studio-owned canonical scene entity;
|
||||
- wave-1 `TMX` and `TSX` integration is a supported import/export compatibility surface, not the canonical asset model and not the canonical publication contract;
|
||||
- supported external formats may be replaced or coexist with other editors in the future only if Studio continues to materialize the same canonical Scene Bank publication request from canonical scene state;
|
||||
- `Assets` must not treat Tiled files as the canonical asset model by accident.
|
||||
|
||||
### Scene Bank Ownership Boundary
|
||||
|
||||
The `Assets` workspace owns the editorial side of `Scene Bank` publication.
|
||||
|
||||
Rules:
|
||||
|
||||
- Studio owns the canonical Scene Bank scene entity and its editorial persistence;
|
||||
- Studio owns layer enablement, dimensions, linked glyph/tileset asset binding, layer default palette data, layer parallax data, and equivalent scene-owned editorial state;
|
||||
- Studio owns import from supported external formats into canonical scene state;
|
||||
- Studio owns export from canonical scene state into supported external formats;
|
||||
- Studio owns projection from canonical scene state into the canonical publication request;
|
||||
- packer owns only compilation of that canonical request into runtime `SCENE` bytes.
|
||||
|
||||
### Canonical Publication Request
|
||||
|
||||
The canonical publication contract from Studio to packer is the Scene Bank request.
|
||||
|
||||
Baseline shape:
|
||||
|
||||
```text
|
||||
ScenePackRequest
|
||||
- version: int
|
||||
- layers: [ScenePackLayerRequest; 4]
|
||||
|
||||
ScenePackLayerRequest
|
||||
- active: bool
|
||||
- glyph_asset_id: int
|
||||
- tile_size: int
|
||||
- parallax_factor_x: float
|
||||
- parallax_factor_y: float
|
||||
- width: int
|
||||
- height: int
|
||||
- tiles: List<ScenePackTileRequest>
|
||||
|
||||
ScenePackTileRequest
|
||||
- active: bool
|
||||
- glyph_id: int
|
||||
- palette_id: int
|
||||
- flip_x: bool
|
||||
- flip_y: bool
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- the request is derived from canonical scene state, not from editor-native files directly;
|
||||
- the request must remain editor-agnostic and must not embed `TMX`, `TSX`, or other editor-specific source structures as the publication contract;
|
||||
- wave 1 materializes exactly `4` layer entries;
|
||||
- `tile_count` is derived from `width * height`;
|
||||
- request fields reserved only for binary layout must not be surfaced unless they gain explicit editorial meaning.
|
||||
|
||||
### Scene Bank Editorial Mapping Rules
|
||||
|
||||
Rules:
|
||||
|
||||
- `glyph_asset_id` is derived from the glyph asset selected for the layer;
|
||||
- `parallax_factor_x` and `parallax_factor_y` are layer-owned editorial fields in Studio;
|
||||
- `palette_id` has a layer-owned default and may be overridden per tile when the supported editorial surface provides an explicit override;
|
||||
- layer `active` determines whether the layer participates in publication output;
|
||||
- tile `active` determines whether the tile contributes an active tile record to the published request;
|
||||
- canonical scene persistence must not depend on `TMX`, `TSX`, or any other external file format remaining present on disk.
|
||||
|
||||
### External Format Interoperability Rules
|
||||
|
||||
Rules:
|
||||
|
||||
- Studio must expose supported scene and glyph-bank interoperability through explicit import/export workflow;
|
||||
- wave-1 Tiled interoperability consists of `TMX (Tiled)` scene import/export and `TSX (Tiled)` glyph tileset export;
|
||||
- scene import must validate minimum structural compatibility before mutating canonical scene state;
|
||||
- scene import must fail explicitly, with logs or diagnostics, when an external file contains unsupported structural divergence such as unexpected layers, unexpected tilesets, or equivalent unsupported mutations;
|
||||
- failed scene import must not silently partially rewrite canonical scene state;
|
||||
- scene `TMX` export must overwrite the destination file if present, or create it if absent;
|
||||
- glyph tileset `TSX` export must overwrite the destination file if present, or create it if absent;
|
||||
- exported external files are compatibility artifacts and must not become the canonical source of truth.
|
||||
|
||||
### Canonical Validation Rules
|
||||
|
||||
Rules:
|
||||
|
||||
- publication-ready Scene Bank state requires absence of blocking diagnostics against canonical scene state;
|
||||
- Studio must validate canonical scene state during `Analyse` and/or `Pack`;
|
||||
- Studio may validate external files for import compatibility during import, but that validation does not replace canonical scene validation;
|
||||
- the old `validate layer`, `accept layer`, `validate scene from TMX`, and `accept scene from TMX` workflow must not define Scene Bank readiness.
|
||||
- Sensitive actions such as `Exclude From Build`, `Remove`, and `Relocate` must be visually separated from routine actions.
|
||||
- Sensitive actions must not look interchangeable with routine actions.
|
||||
- `Relocate` must collect an explicit user-chosen destination root before the staged preview is generated.
|
||||
|
||||
@ -1,4 +1,16 @@
|
||||
declare host LowComposer {
|
||||
[Host(module = "composer", name = "set_camera", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn set_camera(x: int, y: int) -> void;
|
||||
|
||||
[Host(module = "composer", name = "bind_scene", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn bind_scene(scene_bank_id: int) -> int;
|
||||
|
||||
[Host(module = "composer", name = "unbind_scene", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn unbind_scene() -> int;
|
||||
|
||||
[Host(module = "composer", name = "emit_sprite", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn emit_sprite(
|
||||
@ -16,6 +28,21 @@ declare host LowComposer {
|
||||
|
||||
declare service Composer
|
||||
{
|
||||
fn set_camera(x: int, y: int) -> void
|
||||
{
|
||||
LowComposer.set_camera(x, y);
|
||||
}
|
||||
|
||||
fn bind_scene(scene_bank_id: int) -> int
|
||||
{
|
||||
return LowComposer.bind_scene(scene_bank_id);
|
||||
}
|
||||
|
||||
fn unbind_scene() -> int
|
||||
{
|
||||
return LowComposer.unbind_scene();
|
||||
}
|
||||
|
||||
fn emit_sprite(
|
||||
glyph_id: int,
|
||||
palette_id: int,
|
||||
|
||||
@ -116,6 +116,16 @@ class PbsGateUSdkInterfaceConformanceTest {
|
||||
&& h.abiModule().equals("gfx")
|
||||
&& h.abiMethod().equals("clear")
|
||||
&& h.abiVersion() == 1));
|
||||
assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
|
||||
.anyMatch(h -> h.ownerName().equals("LowComposer")
|
||||
&& h.abiModule().equals("composer")
|
||||
&& h.abiMethod().equals("bind_scene")
|
||||
&& h.abiVersion() == 1));
|
||||
assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
|
||||
.anyMatch(h -> h.ownerName().equals("LowComposer")
|
||||
&& h.abiModule().equals("composer")
|
||||
&& h.abiMethod().equals("unbind_scene")
|
||||
&& h.abiVersion() == 1));
|
||||
assertTrue(positive.irBackend().getReservedMetadata().hostMethodBindings().stream()
|
||||
.anyMatch(h -> h.ownerName().equals("LowComposer")
|
||||
&& h.abiModule().equals("composer")
|
||||
|
||||
@ -103,6 +103,14 @@ class InterfaceModuleLoaderTest {
|
||||
"main.pbs",
|
||||
"""
|
||||
declare host LowComposer {
|
||||
[Host(module = "composer", name = "bind_scene", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn bind_scene(scene_bank_id: int) -> int;
|
||||
|
||||
[Host(module = "composer", name = "unbind_scene", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn unbind_scene() -> int;
|
||||
|
||||
[Host(module = "composer", name = "emit_sprite", version = 1)]
|
||||
[Capability(name = "gfx")]
|
||||
fn emit_sprite(
|
||||
@ -119,6 +127,14 @@ class InterfaceModuleLoaderTest {
|
||||
}
|
||||
|
||||
declare service Composer {
|
||||
fn bind_scene(scene_bank_id: int) -> int {
|
||||
return LowComposer.bind_scene(scene_bank_id);
|
||||
}
|
||||
|
||||
fn unbind_scene() -> int {
|
||||
return LowComposer.unbind_scene();
|
||||
}
|
||||
|
||||
fn emit_sprite(
|
||||
glyph_id: int,
|
||||
palette_id: int,
|
||||
|
||||
@ -1069,6 +1069,85 @@ class PBSFrontendPhaseServiceTest {
|
||||
&& h.abiMethod().equals("emit_sprite")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLowerSdkComposerSceneBindingFacadesUsingCanonicalComposerContract() throws IOException {
|
||||
final var projectRoot = tempDir.resolve("project-bootstrap-sdk-composer-scene-binding");
|
||||
final var sourceRoot = projectRoot.resolve("src");
|
||||
final var modulePath = sourceRoot.resolve("app");
|
||||
Files.createDirectories(modulePath);
|
||||
|
||||
final var sourceFile = modulePath.resolve("source.pbs");
|
||||
final var modBarrel = modulePath.resolve("mod.barrel");
|
||||
Files.writeString(sourceFile, """
|
||||
import { Composer } from @sdk:composer;
|
||||
|
||||
fn bind_scene_now() -> int
|
||||
{
|
||||
return Composer.bind_scene(4);
|
||||
}
|
||||
|
||||
fn unbind_scene_now() -> int
|
||||
{
|
||||
return Composer.unbind_scene();
|
||||
}
|
||||
|
||||
[Frame]
|
||||
fn frame() -> void
|
||||
{
|
||||
bind_scene_now();
|
||||
unbind_scene_now();
|
||||
return;
|
||||
}
|
||||
""");
|
||||
Files.writeString(modBarrel, """
|
||||
pub fn bind_scene_now() -> int;
|
||||
pub fn unbind_scene_now() -> int;
|
||||
pub fn frame() -> void;
|
||||
""");
|
||||
|
||||
final var projectTable = new ProjectTable();
|
||||
final var fileTable = new FileTable(1);
|
||||
final var projectId = projectTable.register(ProjectDescriptor.builder()
|
||||
.rootPath(projectRoot)
|
||||
.name("app")
|
||||
.version("1.0.0")
|
||||
.sourceRoots(ReadOnlyList.wrap(List.of(sourceRoot)))
|
||||
.build());
|
||||
|
||||
registerFile(projectId, projectRoot, sourceFile, fileTable);
|
||||
registerFile(projectId, projectRoot, modBarrel, fileTable);
|
||||
|
||||
final var ctx = new FrontendPhaseContext(
|
||||
projectTable,
|
||||
fileTable,
|
||||
new BuildStack(ReadOnlyList.wrap(List.of(projectId))),
|
||||
1);
|
||||
final var diagnostics = DiagnosticSink.empty();
|
||||
|
||||
final var irBackend = new PBSFrontendPhaseService().compile(
|
||||
ctx,
|
||||
diagnostics,
|
||||
LogAggregator.empty(),
|
||||
BuildingIssueSink.empty());
|
||||
|
||||
assertFalse(diagnostics.hasErrors(), diagnostics.stream()
|
||||
.map(d -> d.getCode() + ":" + d.getMessage())
|
||||
.toList()
|
||||
.toString());
|
||||
assertTrue(diagnostics.stream().noneMatch(d ->
|
||||
d.getCode().equals(PbsSemanticsErrors.E_SEM_EXEC_LOWERING_UNRESOLVED_CALLEE.name())));
|
||||
assertTrue(irBackend.getReservedMetadata().hostMethodBindings().stream()
|
||||
.anyMatch(h -> h.ownerName().equals("LowComposer")
|
||||
&& h.sourceMethodName().equals("bind_scene")
|
||||
&& h.abiModule().equals("composer")
|
||||
&& h.abiMethod().equals("bind_scene")));
|
||||
assertTrue(irBackend.getReservedMetadata().hostMethodBindings().stream()
|
||||
.anyMatch(h -> h.ownerName().equals("LowComposer")
|
||||
&& h.sourceMethodName().equals("unbind_scene")
|
||||
&& h.abiModule().equals("composer")
|
||||
&& h.abiMethod().equals("unbind_scene")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectLegacyGfxSetSpriteAfterComposerMigration() throws IOException {
|
||||
final var projectRoot = tempDir.resolve("project-bootstrap-sdk-gfx-set-sprite-removed");
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package p.packer.messages.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ScenePackLayerRequest(
|
||||
boolean active,
|
||||
int glyphAssetId,
|
||||
int tileSize,
|
||||
float parallaxFactorX,
|
||||
float parallaxFactorY,
|
||||
int width,
|
||||
int height,
|
||||
List<ScenePackTileRequest> tiles) {
|
||||
|
||||
public ScenePackLayerRequest {
|
||||
tiles = List.copyOf(Objects.requireNonNull(tiles, "tiles"));
|
||||
if (glyphAssetId < 0) {
|
||||
throw new IllegalArgumentException("glyphAssetId must be non-negative");
|
||||
}
|
||||
if (tileSize < 0) {
|
||||
throw new IllegalArgumentException("tileSize must be non-negative");
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new IllegalArgumentException("width and height must be positive");
|
||||
}
|
||||
if (!Float.isFinite(parallaxFactorX) || parallaxFactorX < 0.0f) {
|
||||
throw new IllegalArgumentException("parallaxFactorX must be finite and non-negative");
|
||||
}
|
||||
if (!Float.isFinite(parallaxFactorY) || parallaxFactorY < 0.0f) {
|
||||
throw new IllegalArgumentException("parallaxFactorY must be finite and non-negative");
|
||||
}
|
||||
if (tiles.size() != width * height) {
|
||||
throw new IllegalArgumentException("tiles size must match width * height");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.packer.messages.assets;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ScenePackRequest(
|
||||
int version,
|
||||
List<ScenePackLayerRequest> layers) {
|
||||
|
||||
public ScenePackRequest {
|
||||
layers = List.copyOf(Objects.requireNonNull(layers, "layers"));
|
||||
if (version <= 0) {
|
||||
throw new IllegalArgumentException("version must be positive");
|
||||
}
|
||||
if (layers.size() != 4) {
|
||||
throw new IllegalArgumentException("layers must contain exactly 4 entries");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package p.packer.messages.assets;
|
||||
|
||||
public record ScenePackTileRequest(
|
||||
boolean active,
|
||||
int glyphId,
|
||||
int paletteId,
|
||||
boolean flipX,
|
||||
boolean flipY) {
|
||||
|
||||
public ScenePackTileRequest {
|
||||
if (glyphId < 0) {
|
||||
throw new IllegalArgumentException("glyphId must be non-negative");
|
||||
}
|
||||
if (paletteId < 0) {
|
||||
throw new IllegalArgumentException("paletteId must be non-negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,15 +2,22 @@ package p.packer.repositories;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import org.apache.commons.collections4.MapUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import p.packer.messages.assets.ScenePackLayerRequest;
|
||||
import p.packer.messages.assets.ScenePackRequest;
|
||||
import p.packer.messages.assets.ScenePackTileRequest;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||
import p.packer.models.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@ -26,11 +33,14 @@ public class PackerAssetWalker {
|
||||
private static final Set<OutputFormatCatalog> SOUND_BANK_SUPPORTED_FORMATS = Set.of(
|
||||
OutputFormatCatalog.SOUND_V1
|
||||
);
|
||||
private static final String SCENE_PACK_REQUEST_FILE = "scene-bank.pack-request.json";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final PackerGlyphBankWalker glyphBankWalker;
|
||||
private final PackerSoundBankWalker soundBankWalker;
|
||||
|
||||
public PackerAssetWalker(final ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
this.glyphBankWalker = new PackerGlyphBankWalker(mapper);
|
||||
this.soundBankWalker = new PackerSoundBankWalker(mapper);
|
||||
}
|
||||
@ -101,7 +111,7 @@ public class PackerAssetWalker {
|
||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||
}
|
||||
case SCENE_BANK -> {
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
return walkSceneBank(assetRoot, declaration, diagnostics);
|
||||
}
|
||||
case UNKNOWN -> {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
@ -116,6 +126,96 @@ public class PackerAssetWalker {
|
||||
return PackerWalkResult.EMPTY;
|
||||
}
|
||||
|
||||
private PackerWalkResult walkSceneBank(
|
||||
Path assetRoot,
|
||||
PackerAssetDeclaration declaration,
|
||||
List<PackerDiagnostic> diagnostics) {
|
||||
if (declaration.outputFormat() != OutputFormatCatalog.SCENE_TILED_V1) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Unsupported output format for Scene Bank: " + declaration.outputFormat(),
|
||||
assetRoot,
|
||||
true));
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
}
|
||||
final Path requestPath = assetRoot.resolve(SCENE_PACK_REQUEST_FILE).toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(requestPath)) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Accepted Scene Bank request artifact is missing: " + SCENE_PACK_REQUEST_FILE,
|
||||
requestPath,
|
||||
true));
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
}
|
||||
try {
|
||||
final byte[] bytes = Files.readAllBytes(requestPath);
|
||||
final ScenePackRequest request = parseScenePackRequest(bytes);
|
||||
final PackerFileProbe probe = new PackerFileProbe(
|
||||
requestPath,
|
||||
"application/json",
|
||||
bytes.length,
|
||||
Math.max(0L, Files.getLastModifiedTime(requestPath).toMillis()),
|
||||
bytes);
|
||||
return new PackerWalkResult(
|
||||
List.of(new PackerProbeResult(
|
||||
probe,
|
||||
Map.of("scenePackRequest", request),
|
||||
List.of())),
|
||||
diagnostics);
|
||||
} catch (IOException | IllegalArgumentException exception) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Unable to read accepted Scene Bank request artifact: " + exception.getMessage(),
|
||||
requestPath,
|
||||
true));
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
private ScenePackRequest parseScenePackRequest(byte[] bytes) throws IOException {
|
||||
final JsonNode root = mapper.readTree(bytes);
|
||||
if (root == null || !root.isObject()) {
|
||||
throw new IllegalArgumentException("request root must be a JSON object");
|
||||
}
|
||||
final String format = root.path("format").asText("SCENE/tilemap_v1").trim();
|
||||
if (!"SCENE/tilemap_v1".equals(format)) {
|
||||
throw new IllegalArgumentException("request format must be SCENE/tilemap_v1");
|
||||
}
|
||||
if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != 4) {
|
||||
throw new IllegalArgumentException("request must contain exactly 4 layers");
|
||||
}
|
||||
final List<ScenePackLayerRequest> layers = new ArrayList<>();
|
||||
for (JsonNode layerNode : layersNode) {
|
||||
final int width = layerNode.path("width").asInt(0);
|
||||
final int height = layerNode.path("height").asInt(0);
|
||||
if (!(layerNode.path("tiles") instanceof ArrayNode tilesNode)) {
|
||||
throw new IllegalArgumentException("each layer must contain a tiles array");
|
||||
}
|
||||
final List<ScenePackTileRequest> tiles = new ArrayList<>();
|
||||
for (JsonNode tileNode : tilesNode) {
|
||||
tiles.add(new ScenePackTileRequest(
|
||||
tileNode.path("active").asBoolean(false),
|
||||
tileNode.path("glyph_id").asInt(0),
|
||||
tileNode.path("palette_id").asInt(0),
|
||||
tileNode.path("flip_x").asBoolean(false),
|
||||
tileNode.path("flip_y").asBoolean(false)));
|
||||
}
|
||||
layers.add(new ScenePackLayerRequest(
|
||||
layerNode.path("active").asBoolean(false),
|
||||
layerNode.path("glyph_asset_id").asInt(0),
|
||||
layerNode.path("tile_size").asInt(0),
|
||||
(float) layerNode.path("parallax_factor_x").asDouble(0.0d),
|
||||
(float) layerNode.path("parallax_factor_y").asDouble(0.0d),
|
||||
width,
|
||||
height,
|
||||
tiles));
|
||||
}
|
||||
return new ScenePackRequest(root.path("version").asInt(0), layers);
|
||||
}
|
||||
|
||||
private RequirementBuildResult<PackerGlyphBankRequirements> buildGlyphBankRequirements(
|
||||
final PackerAssetDeclaration declaration,
|
||||
final Map<String, String> metadata) {
|
||||
|
||||
@ -161,6 +161,9 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
if (materializationConfig.mode() != PackerWalkMode.PACKING) {
|
||||
return true;
|
||||
}
|
||||
if (declaration.assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
|
||||
return true;
|
||||
}
|
||||
if (declaration.artifacts().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ import java.util.stream.Stream;
|
||||
public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
|
||||
private static final int GLYPH_BANK_COLOR_KEY_RGB565 = 0xF81F;
|
||||
private static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
|
||||
private static final String SCENE_PACK_REQUEST_FILE = "scene-bank.pack-request.json";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||
@ -699,8 +700,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
"name", "Layer " + index,
|
||||
"width", 16,
|
||||
"height", 16,
|
||||
"tilemap", "layer-" + index + ".tmx",
|
||||
"tileset_asset_root", ""));
|
||||
"tilemap", "scene.tmx",
|
||||
"tileset_asset_root", "",
|
||||
"parallax_factor_x", 1.0,
|
||||
"parallax_factor_y", 1.0,
|
||||
"default_palette_id", 0));
|
||||
}
|
||||
supportFile.put("layers", layers);
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
@ -883,7 +887,8 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
if (declaration == null) {
|
||||
return List.of();
|
||||
}
|
||||
if (declaration.assetFamily() == AssetFamilyCatalog.GLYPH_BANK) {
|
||||
if (declaration.assetFamily() == AssetFamilyCatalog.GLYPH_BANK
|
||||
|| declaration.assetFamily() == AssetFamilyCatalog.SCENE_BANK) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(new PackerDiagnostic(
|
||||
@ -1054,10 +1059,14 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
|
||||
private PackedAsset packRuntimeAsset(PackerRuntimeAsset runtimeAsset) {
|
||||
final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
|
||||
if (declaration == null || declaration.assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
if (declaration == null) {
|
||||
throw new IllegalStateException("Unsupported pack output family for current implementation.");
|
||||
}
|
||||
return packGlyphBank(runtimeAsset, declaration);
|
||||
return switch (declaration.assetFamily()) {
|
||||
case GLYPH_BANK -> packGlyphBank(runtimeAsset, declaration);
|
||||
case SCENE_BANK -> packSceneBank(runtimeAsset, declaration);
|
||||
default -> throw new IllegalStateException("Unsupported pack output family for current implementation.");
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isEffectivelyIncludedInPackSet(PackerRuntimeAsset runtimeAsset) {
|
||||
@ -1065,7 +1074,13 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
return false;
|
||||
}
|
||||
final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
|
||||
return declaration == null || !declaration.artifacts().isEmpty();
|
||||
if (declaration == null) {
|
||||
return true;
|
||||
}
|
||||
if (declaration.assetFamily() == AssetFamilyCatalog.SCENE_BANK) {
|
||||
return true;
|
||||
}
|
||||
return !declaration.artifacts().isEmpty();
|
||||
}
|
||||
|
||||
private PackedAsset packGlyphBank(PackerRuntimeAsset runtimeAsset, PackerAssetDeclaration declaration) {
|
||||
@ -1076,6 +1091,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
final byte[] sheetPixels = new byte[width * height];
|
||||
final Map<String, PackerRuntimeWalkFile> selectedByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> selectedByPath.put(file.relativePath(), file));
|
||||
final Map<Integer, Integer> declaredPaletteIndexByRgb = declaredPaletteIndexByRgb(declaration);
|
||||
|
||||
declaration.artifacts().stream()
|
||||
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||
@ -1088,13 +1104,18 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
if (!(tileValue instanceof PackerTileIndexedV1 tile)) {
|
||||
throw new IllegalStateException("Selected tile artifact is missing normalized tile metadata: " + artifact.file());
|
||||
}
|
||||
final byte[] paletteIndices = remapTilePaletteIndices(
|
||||
tile,
|
||||
walkFile.metadata().get("palette"),
|
||||
artifact.file(),
|
||||
declaredPaletteIndexByRgb);
|
||||
final int tileX = (artifact.index() % tilesPerRow) * tileSize;
|
||||
final int tileY = (artifact.index() / tilesPerRow) * tileSize;
|
||||
for (int localY = 0; localY < tile.height(); localY += 1) {
|
||||
for (int localX = 0; localX < tile.width(); localX += 1) {
|
||||
final int sourceOffset = localY * tile.width() + localX;
|
||||
final int targetOffset = (tileY + localY) * width + (tileX + localX);
|
||||
sheetPixels[targetOffset] = tile.paletteIndices()[sourceOffset];
|
||||
sheetPixels[targetOffset] = paletteIndices[sourceOffset];
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1135,6 +1156,109 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
declaration.preloadEnabled());
|
||||
}
|
||||
|
||||
private byte[] remapTilePaletteIndices(
|
||||
PackerTileIndexedV1 tile,
|
||||
Object paletteMetadata,
|
||||
String artifactFile,
|
||||
Map<Integer, Integer> declaredPaletteIndexByRgb) {
|
||||
final int[] originalArgb8888 = originalPaletteColors(paletteMetadata);
|
||||
if (originalArgb8888.length == 0) {
|
||||
throw new IllegalStateException("Selected tile artifact is missing palette metadata required for palette remapping: " + artifactFile);
|
||||
}
|
||||
final byte[] remapped = Arrays.copyOf(tile.paletteIndices(), tile.paletteIndices().length);
|
||||
for (int index = 0; index < remapped.length; index += 1) {
|
||||
final int paletteIndex = Byte.toUnsignedInt(remapped[index]);
|
||||
if (paletteIndex == 0) {
|
||||
continue;
|
||||
}
|
||||
if (paletteIndex > originalArgb8888.length) {
|
||||
throw new IllegalStateException("Tile palette index exceeds discovered palette size for artifact: " + artifactFile);
|
||||
}
|
||||
final int originalArgb = originalArgb8888[paletteIndex - 1];
|
||||
final Integer materialIndex = declaredPaletteIndexByRgb.get(rgb24(originalArgb));
|
||||
if (materialIndex == null || materialIndex < 1 || materialIndex > 15) {
|
||||
throw new IllegalStateException("Unable to resolve canonical palette index for artifact color in " + artifactFile);
|
||||
}
|
||||
remapped[index] = (byte) (materialIndex & 0x0F);
|
||||
}
|
||||
return remapped;
|
||||
}
|
||||
|
||||
private Map<Integer, Integer> declaredPaletteIndexByRgb(PackerAssetDeclaration declaration) {
|
||||
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
|
||||
if (!(palettesNode instanceof ArrayNode palettesArray) || palettesArray.isEmpty()) {
|
||||
throw new IllegalStateException("Glyph packing requires at least one declared pipeline palette.");
|
||||
}
|
||||
final JsonNode paletteNode = palettesArray.get(0).path("palette").path("originalArgb8888");
|
||||
if (!paletteNode.isArray() || paletteNode.isEmpty()) {
|
||||
throw new IllegalStateException("Glyph packing requires originalArgb8888 entries in the declared pipeline palette.");
|
||||
}
|
||||
final LinkedHashMap<Integer, Integer> mapping = new LinkedHashMap<>();
|
||||
for (int index = 0; index < paletteNode.size(); index += 1) {
|
||||
final JsonNode colorNode = paletteNode.get(index);
|
||||
if (!colorNode.canConvertToInt()) {
|
||||
throw new IllegalStateException("Declared pipeline palette contains a non-integer originalArgb8888 entry.");
|
||||
}
|
||||
mapping.put(rgb24(colorNode.intValue()), index + 1);
|
||||
}
|
||||
return Map.copyOf(mapping);
|
||||
}
|
||||
|
||||
private int[] originalPaletteColors(Object paletteMetadata) {
|
||||
if (paletteMetadata instanceof PackerPaletteV1 palette) {
|
||||
return palette.originalArgb8888();
|
||||
}
|
||||
if (!(paletteMetadata instanceof Map<?, ?> map)) {
|
||||
return new int[0];
|
||||
}
|
||||
final Object colors = map.get("originalArgb8888");
|
||||
if (!(colors instanceof List<?> list)) {
|
||||
return new int[0];
|
||||
}
|
||||
final int[] values = new int[list.size()];
|
||||
for (int index = 0; index < list.size(); index += 1) {
|
||||
final Object color = list.get(index);
|
||||
if (!(color instanceof Number number)) {
|
||||
return new int[0];
|
||||
}
|
||||
values[index] = number.intValue();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private int rgb24(int argb) {
|
||||
return argb & 0x00FFFFFF;
|
||||
}
|
||||
|
||||
private PackedAsset packSceneBank(PackerRuntimeAsset runtimeAsset, PackerAssetDeclaration declaration) {
|
||||
final ScenePackRequest request = runtimeAsset.walkProjection().buildCandidateFiles().stream()
|
||||
.map(file -> file.metadata().get("scenePackRequest"))
|
||||
.filter(ScenePackRequest.class::isInstance)
|
||||
.map(ScenePackRequest.class::cast)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Accepted Scene Bank request artifact is missing from the packing snapshot: " + SCENE_PACK_REQUEST_FILE));
|
||||
|
||||
final byte[] payload = encodeScenePackRequest(request);
|
||||
final LinkedHashMap<String, Object> runtimeMetadata = new LinkedHashMap<>();
|
||||
runtimeMetadata.put("version", request.version());
|
||||
runtimeMetadata.put("layer_count", request.layers().size());
|
||||
runtimeMetadata.put("codec", Map.of());
|
||||
|
||||
final LinkedHashMap<String, Object> toolingMetadata = new LinkedHashMap<>(runtimeMetadata);
|
||||
final PackerRegistryEntry registryEntry = runtimeAsset.registryEntry()
|
||||
.orElseThrow(() -> new IllegalStateException("Packed runtime asset must be registered"));
|
||||
return new PackedAsset(
|
||||
registryEntry.assetId(),
|
||||
declaration.name(),
|
||||
"SCENE",
|
||||
"NONE",
|
||||
payload,
|
||||
sceneDecodedSize(request),
|
||||
runtimeMetadata,
|
||||
toolingMetadata,
|
||||
declaration.preloadEnabled());
|
||||
}
|
||||
|
||||
private int parseTileSize(String value) {
|
||||
return switch (Objects.requireNonNullElse(value, "")) {
|
||||
case "8x8" -> 8;
|
||||
@ -1192,6 +1316,67 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
return palettesNode instanceof ArrayNode palettesArray ? palettesArray.size() : 0;
|
||||
}
|
||||
|
||||
private byte[] encodeScenePackRequest(ScenePackRequest request) {
|
||||
final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
writeBytes(output, new byte[]{'S', 'C', 'N', 'E'});
|
||||
writeShortLE(output, request.version());
|
||||
writeShortLE(output, request.layers().size());
|
||||
writeIntLE(output, 0);
|
||||
for (ScenePackLayerRequest layer : request.layers()) {
|
||||
writeByte(output, layer.active() ? 0b0000_0001 : 0);
|
||||
writeIntLE(output, layer.glyphAssetId());
|
||||
writeByte(output, layer.tileSize());
|
||||
writeShortLE(output, 0);
|
||||
writeFloatLE(output, layer.parallaxFactorX());
|
||||
writeFloatLE(output, layer.parallaxFactorY());
|
||||
writeIntLE(output, layer.width());
|
||||
writeIntLE(output, layer.height());
|
||||
writeIntLE(output, layer.width() * layer.height());
|
||||
writeIntLE(output, 0);
|
||||
for (ScenePackTileRequest tile : layer.tiles()) {
|
||||
int flags = tile.active() ? 0b0000_0001 : 0;
|
||||
if (tile.flipX()) {
|
||||
flags |= 0b0000_0010;
|
||||
}
|
||||
if (tile.flipY()) {
|
||||
flags |= 0b0000_0100;
|
||||
}
|
||||
writeByte(output, flags);
|
||||
writeByte(output, tile.paletteId());
|
||||
writeShortLE(output, tile.glyphId());
|
||||
}
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
private int sceneDecodedSize(ScenePackRequest request) {
|
||||
int decodedSize = 0;
|
||||
for (ScenePackLayerRequest layer : request.layers()) {
|
||||
decodedSize += 16 + (layer.tiles().size() * 8);
|
||||
}
|
||||
return decodedSize;
|
||||
}
|
||||
|
||||
private void writeIntLE(ByteArrayOutputStream output, int value) {
|
||||
writeBytes(output, ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(value).array());
|
||||
}
|
||||
|
||||
private void writeShortLE(ByteArrayOutputStream output, int value) {
|
||||
writeBytes(output, ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) value).array());
|
||||
}
|
||||
|
||||
private void writeFloatLE(ByteArrayOutputStream output, float value) {
|
||||
writeBytes(output, ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putFloat(value).array());
|
||||
}
|
||||
|
||||
private void writeByte(ByteArrayOutputStream output, int value) {
|
||||
output.write(value & 0xFF);
|
||||
}
|
||||
|
||||
private void writeBytes(ByteArrayOutputStream output, byte[] bytes) {
|
||||
output.writeBytes(bytes);
|
||||
}
|
||||
|
||||
private Map<String, Object> normalizeToolingPipelineMetadata(PackerAssetDeclaration declaration) {
|
||||
return new LinkedHashMap<>(PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()));
|
||||
}
|
||||
|
||||
@ -125,6 +125,108 @@ final class PackerRuntimeAssetMaterializerTest {
|
||||
assertTrue(file.contentBytes().get().length > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sceneBankPackingProjectionIncludesAcceptedRequestArtifact() throws Exception {
|
||||
final var materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(MAPPER));
|
||||
final Path assetRoot = Files.createDirectories(tempDir.resolve("scene-packing"));
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
Files.writeString(manifestPath, "{}");
|
||||
Files.writeString(assetRoot.resolve("scene-bank.pack-request.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"format": "SCENE/tilemap_v1",
|
||||
"version": 1,
|
||||
"layers": [
|
||||
{
|
||||
"active": true,
|
||||
"glyph_asset_id": 7,
|
||||
"tile_size": 16,
|
||||
"parallax_factor_x": 1.0,
|
||||
"parallax_factor_y": 1.0,
|
||||
"width": 2,
|
||||
"height": 2,
|
||||
"tiles": [
|
||||
{ "active": true, "glyph_id": 1, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"glyph_asset_id": 0,
|
||||
"tile_size": 0,
|
||||
"parallax_factor_x": 1.0,
|
||||
"parallax_factor_y": 1.0,
|
||||
"width": 2,
|
||||
"height": 2,
|
||||
"tiles": [
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"glyph_asset_id": 0,
|
||||
"tile_size": 0,
|
||||
"parallax_factor_x": 1.0,
|
||||
"parallax_factor_y": 1.0,
|
||||
"width": 2,
|
||||
"height": 2,
|
||||
"tiles": [
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
|
||||
]
|
||||
},
|
||||
{
|
||||
"active": false,
|
||||
"glyph_asset_id": 0,
|
||||
"tile_size": 0,
|
||||
"parallax_factor_x": 1.0,
|
||||
"parallax_factor_y": 1.0,
|
||||
"width": 2,
|
||||
"height": 2,
|
||||
"tiles": [
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false },
|
||||
{ "active": false, "glyph_id": 0, "palette_id": 0, "flip_x": false, "flip_y": false }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
final PackerAssetDeclaration declaration = new PackerAssetDeclaration(
|
||||
1,
|
||||
"uuid",
|
||||
"scene_asset",
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
List.of(),
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
Map.of(),
|
||||
Map.of(),
|
||||
true);
|
||||
final var materialized = materializer.materialize(
|
||||
assetRoot,
|
||||
manifestPath,
|
||||
Optional.of(new PackerRegistryEntry(1, "uuid", "scene_asset", true)),
|
||||
new PackerAssetDeclarationParseResult(declaration, List.of()),
|
||||
Optional.empty(),
|
||||
PackerRuntimeMaterializationConfig.packingBuild());
|
||||
|
||||
assertEquals(1, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
|
||||
final var file = materialized.runtimeAsset().walkProjection().buildCandidateFiles().getFirst();
|
||||
assertEquals("scene-bank.pack-request.json", file.relativePath());
|
||||
assertTrue(file.contentBytes().isPresent());
|
||||
assertTrue(file.metadata().get("scenePackRequest") instanceof p.packer.messages.assets.ScenePackRequest);
|
||||
}
|
||||
|
||||
private static PackerAssetDeclaration declaration(Map<String, String> metadata) {
|
||||
return declaration(metadata, List.of(), Map.of());
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.packer.services;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@ -20,7 +21,9 @@ import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -351,6 +354,72 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertEquals(0, assetTableMetadata.get(0).path("metadata").path("pipeline").path("palettes").get(0).path("index").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void packWorkspaceRemapsGlyphTileIndicesUsingGlyphMaterialSchema() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-workspace-remap-material"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
writeTwoColorTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
|
||||
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(manifestPath.toFile());
|
||||
final ObjectNode output = (ObjectNode) manifest.path("output");
|
||||
output.putObject("metadata").put("tile_size", "16x16");
|
||||
final ObjectNode pipeline = output.putObject("pipeline");
|
||||
final var palettes = pipeline.putArray("palettes");
|
||||
final ObjectNode palette = palettes.addObject();
|
||||
palette.put("index", 0);
|
||||
palette.putObject("palette")
|
||||
.putArray("originalArgb8888").add(0xFF00FF00).add(0xFFFF0000);
|
||||
((ObjectNode) palette.path("palette"))
|
||||
.putArray("convertedRgb565").add(0x07E0).add(0xF800);
|
||||
final var artifacts = manifest.putArray("artifacts");
|
||||
artifacts.addObject()
|
||||
.put("artifact_id", "art-confirm")
|
||||
.put("file", "confirm.png")
|
||||
.put("index", 0);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
writeGlyphMaterialJson(assetRoot, """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schemas": [
|
||||
{
|
||||
"schema_id": "schema-1",
|
||||
"status": "ok",
|
||||
"version": 1,
|
||||
"material_indices": {
|
||||
"1": "#00FF00",
|
||||
"2": "#FF0000"
|
||||
},
|
||||
"artifact_bindings": [
|
||||
{
|
||||
"artifact_id": "art-confirm",
|
||||
"fingerprint": "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"palettes": [
|
||||
{
|
||||
"palette_id": "palette-1",
|
||||
"schema_id": "schema-1",
|
||||
"version": 1,
|
||||
"colors": [ "#00FF00", "#FF0000" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(fingerprint(assetRoot.resolve("confirm.png"))));
|
||||
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
final var result = service.packWorkspace(new PackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
final byte[] assetsPa = Files.readAllBytes(projectRoot.resolve("build/assets.pa"));
|
||||
final int payloadOffset = (int) readLeLong(assetsPa, 12);
|
||||
final var assetTable = MAPPER.readTree(projectRoot.resolve("build/asset_table.json").toFile());
|
||||
final int assetOffset = assetTable.get(0).path("offset").asInt();
|
||||
assertEquals(0x21, assetsPa[payloadOffset + assetOffset] & 0xFF);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateAndPackIgnoreBlockingDiagnosticsFromUnselectedTileFiles() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("pack-ignore-unselected"));
|
||||
@ -538,7 +607,84 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertEquals("Layer 1", supportFile.path("layers").get(0).path("name").asText());
|
||||
assertEquals(16, supportFile.path("layers").get(0).path("width").asInt());
|
||||
assertEquals(16, supportFile.path("layers").get(0).path("height").asInt());
|
||||
assertEquals("layer-1.tmx", supportFile.path("layers").get(0).path("tilemap").asText());
|
||||
assertEquals("scene.tmx", supportFile.path("layers").get(0).path("tilemap").asText());
|
||||
assertEquals(1.0d, supportFile.path("layers").get(0).path("parallax_factor_x").asDouble());
|
||||
assertEquals(1.0d, supportFile.path("layers").get(0).path("parallax_factor_y").asDouble());
|
||||
assertEquals(0, supportFile.path("layers").get(0).path("default_palette_id").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationBlocksSceneBankWhenAcceptedRequestArtifactIsMissing() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("scene-pack-missing-request");
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest(
|
||||
project(projectRoot),
|
||||
"scenes/overworld",
|
||||
"overworld",
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
false));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, createResult.status());
|
||||
enableSceneLayer(projectRoot.resolve("assets/scenes/overworld/scene-bank.studio.json"));
|
||||
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().stream()
|
||||
.flatMap(asset -> asset.diagnostics().stream())
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("Accepted Scene Bank request artifact is missing")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packsSceneBankFromAcceptedRequestArtifactWithoutParsingEditorFiles() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("scene-pack-success");
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
final CreateAssetResult createResult = service.createAsset(new CreateAssetRequest(
|
||||
project(projectRoot),
|
||||
"scenes/overworld",
|
||||
"overworld",
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
true));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, createResult.status());
|
||||
final Path assetRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
enableSceneLayer(assetRoot.resolve("scene-bank.studio.json"));
|
||||
Files.writeString(assetRoot.resolve("layer-1.tmx"), "editor surface should be ignored by packer");
|
||||
writeScenePackRequest(assetRoot.resolve("scene-bank.pack-request.json"));
|
||||
|
||||
final var result = service.packWorkspace(new PackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
final byte[] assetsPa = Files.readAllBytes(projectRoot.resolve("build/assets.pa"));
|
||||
final int headerLength = readLeInt(assetsPa, 8);
|
||||
final long payloadOffset = readLeLong(assetsPa, 12);
|
||||
final byte[] payload = java.util.Arrays.copyOfRange(assetsPa, (int) payloadOffset, assetsPa.length);
|
||||
assertEquals('S', payload[0]);
|
||||
assertEquals('C', payload[1]);
|
||||
assertEquals('N', payload[2]);
|
||||
assertEquals('E', payload[3]);
|
||||
final var assetTable = MAPPER.readTree(projectRoot.resolve("build/asset_table.json").toFile());
|
||||
assertEquals(1, assetTable.size());
|
||||
assertEquals("SCENE", assetTable.get(0).path("bank_type").asText());
|
||||
assertEquals("NONE", assetTable.get(0).path("codec").asText());
|
||||
assertEquals(expectedSceneDecodedSize(4, 256), assetTable.get(0).path("decoded_size").asInt());
|
||||
assertEquals(1, readLeShort(payload, 4));
|
||||
assertEquals(4, readLeShort(payload, 6));
|
||||
assertEquals(0b0000_0001, payload[12] & 0xFF);
|
||||
assertEquals(7, readLeInt(payload, 13));
|
||||
assertEquals(16, payload[17] & 0xFF);
|
||||
assertEquals(16, readLeInt(payload, 28));
|
||||
assertEquals(16, readLeInt(payload, 32));
|
||||
assertEquals(256, readLeInt(payload, 36));
|
||||
assertEquals(0b0000_0011, payload[44] & 0xFF);
|
||||
assertEquals(2, payload[45] & 0xFF);
|
||||
assertEquals(11, readLeShort(payload, 46));
|
||||
assertTrue(headerLength > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1582,6 +1728,46 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
Files.writeString(assetRoot.resolve("glyph-material.json"), content);
|
||||
}
|
||||
|
||||
private String fingerprint(Path path) throws Exception {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)));
|
||||
}
|
||||
|
||||
private void enableSceneLayer(Path supportFilePath) throws Exception {
|
||||
final ObjectNode supportFile = (ObjectNode) MAPPER.readTree(supportFilePath.toFile());
|
||||
((ObjectNode) supportFile.path("layers").get(0)).put("enabled", true);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(supportFilePath.toFile(), supportFile);
|
||||
}
|
||||
|
||||
private void writeScenePackRequest(Path path) throws Exception {
|
||||
final ObjectNode root = MAPPER.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
root.put("format", "SCENE/tilemap_v1");
|
||||
root.put("version", 1);
|
||||
final ArrayNode layers = root.putArray("layers");
|
||||
for (int layerIndex = 0; layerIndex < 4; layerIndex += 1) {
|
||||
final ObjectNode layer = layers.addObject();
|
||||
final boolean active = layerIndex == 0;
|
||||
layer.put("active", active);
|
||||
layer.put("glyph_asset_id", active ? 7 : 0);
|
||||
layer.put("tile_size", active ? 16 : 0);
|
||||
layer.put("parallax_factor_x", 1.0);
|
||||
layer.put("parallax_factor_y", 1.0);
|
||||
layer.put("width", 16);
|
||||
layer.put("height", 16);
|
||||
final ArrayNode tiles = layer.putArray("tiles");
|
||||
for (int tileIndex = 0; tileIndex < 256; tileIndex += 1) {
|
||||
final ObjectNode tile = tiles.addObject();
|
||||
final boolean tileActive = active && tileIndex == 0;
|
||||
tile.put("active", tileActive);
|
||||
tile.put("glyph_id", tileActive ? 11 : 0);
|
||||
tile.put("palette_id", tileActive ? 2 : 0);
|
||||
tile.put("flip_x", tileActive);
|
||||
tile.put("flip_y", false);
|
||||
}
|
||||
}
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), root);
|
||||
}
|
||||
|
||||
private static int readLeInt(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF)
|
||||
| ((bytes[offset + 1] & 0xFF) << 8)
|
||||
@ -1589,6 +1775,11 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
| ((bytes[offset + 3] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
private static int readLeShort(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF)
|
||||
| ((bytes[offset + 1] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
private static long readLeLong(byte[] bytes, int offset) {
|
||||
return ((long) bytes[offset] & 0xFF)
|
||||
| (((long) bytes[offset + 1] & 0xFF) << 8)
|
||||
@ -1600,6 +1791,10 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
| (((long) bytes[offset + 7] & 0xFF) << 56);
|
||||
}
|
||||
|
||||
private static int expectedSceneDecodedSize(int layerCount, int tilesPerLayer) {
|
||||
return layerCount * (16 + (tilesPerLayer * 8));
|
||||
}
|
||||
|
||||
private static int readLeUnsignedShort(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF) | ((bytes[offset + 1] & 0xFF) << 8);
|
||||
}
|
||||
|
||||
@ -157,8 +157,9 @@ public enum I18n {
|
||||
ASSETS_ACTIONS_EMPTY("assets.actions.empty"),
|
||||
ASSETS_ACTION_REGISTER("assets.action.register"),
|
||||
ASSETS_ACTION_ANALYSE("assets.action.analyse"),
|
||||
ASSETS_ACTION_GENERATE_TSX("assets.action.generateTsx"),
|
||||
ASSETS_ACTION_GENERATE_TMX("assets.action.generateTmx"),
|
||||
ASSETS_ACTION_EXPORT_TSX("assets.action.exportTsx"),
|
||||
ASSETS_ACTION_EXPORT_TMX("assets.action.exportTmx"),
|
||||
ASSETS_ACTION_IMPORT_TMX("assets.action.importTmx"),
|
||||
ASSETS_ACTION_VALIDATE_SCENE_BANK("assets.action.validateSceneBank"),
|
||||
ASSETS_ACTION_ACCEPT_SCENE_BANK("assets.action.acceptSceneBank"),
|
||||
ASSETS_ACTION_VALIDATE_LAYER("assets.action.validateLayer"),
|
||||
@ -209,6 +210,8 @@ public enum I18n {
|
||||
ASSETS_LABEL_TILEMAPS("assets.label.tilemaps"),
|
||||
ASSETS_LABEL_TILEMAP("assets.label.tilemap"),
|
||||
ASSETS_LABEL_TILESET("assets.label.tileset"),
|
||||
ASSETS_LABEL_PARALLAX("assets.label.parallax"),
|
||||
ASSETS_LABEL_DEFAULT_PALETTE("assets.label.defaultPalette"),
|
||||
ASSETS_LABEL_SUPPORT_FILE("assets.label.supportFile"),
|
||||
ASSETS_LABEL_USE_LAYER("assets.label.useLayer"),
|
||||
ASSETS_VALUE_NOT_CONFIGURED("assets.value.notConfigured"),
|
||||
|
||||
@ -170,7 +170,6 @@ public final class AssetWorkspace extends Workspace {
|
||||
}
|
||||
AddAssetWizard.showAndWait(root.getScene().getWindow(), projectReference).ifPresent(assetReference -> {
|
||||
workspaceEventBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
|
||||
workspaceEventBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(assetReference, false));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,10 @@ package p.studio.workspaces.assets.details;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@ -45,6 +48,7 @@ import p.studio.workspaces.assets.scene.SceneBankWorkflowResult;
|
||||
import p.studio.workspaces.assets.scene.SceneBankWorkflowService;
|
||||
import p.studio.workspaces.assets.tiled.TiledAssetGenerationResult;
|
||||
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
|
||||
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService.SceneTmxExportDisposition;
|
||||
import p.studio.workspaces.assets.wizards.DeleteAssetDialog;
|
||||
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
|
||||
import p.studio.workspaces.framework.StudioEventAware;
|
||||
@ -53,6 +57,9 @@ import p.studio.workspaces.framework.StudioEventBindings;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
|
||||
public final class AssetDetailsControl extends VBox implements StudioEventAware {
|
||||
private final ProjectReference projectReference;
|
||||
@ -93,9 +100,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
this.colorSchemaGroupsControl = new AssetDetailsColorSchemaGroupsControl(projectReference, workspaceBus);
|
||||
this.paletteOverhaulingControl = new AssetDetailsPaletteOverhaulingControl(projectReference, workspaceBus);
|
||||
this.sceneLayersControl = new AssetDetailsSceneLayersControl(projectReference, workspaceBus);
|
||||
this.sceneLayersControl.setGenerateLayerAction(this::generateTmx);
|
||||
this.sceneLayersControl.setValidateLayerAction(this::validateSceneBank);
|
||||
this.sceneLayersControl.setAcceptLayerAction(this::acceptSceneBank);
|
||||
this.actionsSection = createActionsSection();
|
||||
|
||||
getStyleClass().add("assets-workspace-pane");
|
||||
@ -375,10 +379,21 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
analyseButton.setOnAction(ignored -> openDiagnosticsDialog());
|
||||
nodes.add(analyseButton);
|
||||
if (canGenerateTsx()) {
|
||||
final Button generateTsxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TSX));
|
||||
generateTsxButton.setDisable(actionRunning);
|
||||
generateTsxButton.setOnAction(ignored -> generateTsx());
|
||||
nodes.add(generateTsxButton);
|
||||
final Button exportTsxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_EXPORT_TSX));
|
||||
exportTsxButton.setDisable(actionRunning);
|
||||
exportTsxButton.setOnAction(ignored -> generateTsx());
|
||||
nodes.add(exportTsxButton);
|
||||
}
|
||||
if (canSceneTmxInteroperability()) {
|
||||
final Button importTmxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_IMPORT_TMX));
|
||||
importTmxButton.setDisable(actionRunning);
|
||||
importTmxButton.setOnAction(ignored -> importTmx());
|
||||
nodes.add(importTmxButton);
|
||||
|
||||
final Button exportTmxButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_EXPORT_TMX));
|
||||
exportTmxButton.setDisable(actionRunning);
|
||||
exportTmxButton.setOnAction(ignored -> exportTmx());
|
||||
nodes.add(exportTmxButton);
|
||||
}
|
||||
final Button buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
||||
AssetDetailsUiSupport.applyActionTone(
|
||||
@ -434,7 +449,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
viewState.selectedAssetReference(),
|
||||
viewState.selectedAssetDetails().summary()).ifPresent(assetReference -> {
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(assetReference, true));
|
||||
});
|
||||
}
|
||||
|
||||
@ -519,12 +533,10 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
&& viewState.selectedAssetDetails().summary().glyphSpecialization() == AssetStudioGlyphSpecialization.TILESET;
|
||||
}
|
||||
|
||||
private boolean canGenerateTmx() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean canValidateSceneBank() {
|
||||
return false;
|
||||
private boolean canSceneTmxInteroperability() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
|
||||
&& viewState.selectedAssetDetails().sceneBankMetadata() != null;
|
||||
}
|
||||
|
||||
private void generateTsx() {
|
||||
@ -536,12 +548,73 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.generateTilesetTsx(details);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("glyph-bank-tileset", result));
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.exportTilesetTsx(details);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("glyph-bank-tileset-export", result));
|
||||
});
|
||||
}
|
||||
|
||||
private void generateTmx(int layerIndex) {
|
||||
private void exportTmx() {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
final SceneTmxExportDisposition disposition = chooseSceneTmxExportDisposition(details);
|
||||
if (disposition == null) {
|
||||
return;
|
||||
}
|
||||
actionRunning = true;
|
||||
actionFeedbackMessage = null;
|
||||
renderActions();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.exportSceneBankTmx(projectReference, details, disposition);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("scene-bank-tmx-export", result));
|
||||
});
|
||||
}
|
||||
|
||||
private SceneTmxExportDisposition chooseSceneTmxExportDisposition(AssetWorkspaceAssetDetails details) {
|
||||
final Path sceneTmxPath = resolveSceneTmxPath(details);
|
||||
if (sceneTmxPath == null || !Files.isRegularFile(sceneTmxPath)) {
|
||||
return SceneTmxExportDisposition.OVERWRITE;
|
||||
}
|
||||
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
if (getScene() != null) {
|
||||
alert.initOwner(getScene().getWindow());
|
||||
}
|
||||
final ButtonType overwriteButton = new ButtonType("Overwrite", ButtonBar.ButtonData.YES);
|
||||
final ButtonType keepBothButton = new ButtonType("Keep Both", ButtonBar.ButtonData.NO);
|
||||
final ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
alert.setTitle("Export TMX");
|
||||
alert.setHeaderText(null);
|
||||
alert.setContentText("A scene.tmx file already exists. Choose how to proceed.");
|
||||
alert.getButtonTypes().setAll(overwriteButton, keepBothButton, cancelButton);
|
||||
final ButtonType result = alert.showAndWait().orElse(cancelButton);
|
||||
if (result == overwriteButton) {
|
||||
return SceneTmxExportDisposition.OVERWRITE;
|
||||
}
|
||||
if (result == keepBothButton) {
|
||||
return SceneTmxExportDisposition.KEEP_BOTH;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Path resolveSceneTmxPath(AssetWorkspaceAssetDetails details) {
|
||||
if (details == null || details.sceneBankMetadata() == null) {
|
||||
return null;
|
||||
}
|
||||
final LinkedHashSet<String> tilemaps = new LinkedHashSet<>();
|
||||
details.sceneBankMetadata().layerBindings().forEach(binding -> {
|
||||
final String tilemap = Objects.requireNonNullElse(binding.tilemap(), "").trim();
|
||||
if (!tilemap.isBlank()) {
|
||||
tilemaps.add(tilemap);
|
||||
}
|
||||
});
|
||||
if (tilemaps.size() != 1) {
|
||||
return null;
|
||||
}
|
||||
return details.summary().assetRoot().resolve(tilemaps.iterator().next());
|
||||
}
|
||||
|
||||
private void importTmx() {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
@ -550,8 +623,8 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.generateSceneBankTilemap(projectReference, details, layerIndex);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("scene-bank-layer-" + layerIndex, result));
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.importSceneBankTmx(projectReference, details);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("scene-bank-tmx-import", result));
|
||||
});
|
||||
}
|
||||
|
||||
@ -560,8 +633,7 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
actionFeedbackMessage = result.message();
|
||||
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||
renderActions();
|
||||
if (result.success() && viewState.selectedAssetReference() != null) {
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
if (result.success() && viewState.selectedAssetReference() != null && shouldReloadDetailsAfterTiledResult(source)) {
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||
}
|
||||
}
|
||||
@ -600,17 +672,19 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||
renderActions();
|
||||
if (viewState.selectedAssetReference() != null) {
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldReloadDetailsAfterTiledResult(String source) {
|
||||
return "scene-bank-tmx-import".equals(source);
|
||||
}
|
||||
|
||||
private void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
|
||||
actionRunning = false;
|
||||
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
||||
actionFeedbackMessage = null;
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(result.assetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(result.assetReference(), false));
|
||||
return;
|
||||
}
|
||||
actionFeedbackMessage = result.summary();
|
||||
@ -641,7 +715,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
||||
actionFeedbackMessage = null;
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(result.assetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(result.assetReference(), false));
|
||||
return;
|
||||
}
|
||||
actionFeedbackMessage = Objects.requireNonNullElse(result.summary(), "Unable to register asset.");
|
||||
|
||||
@ -98,7 +98,6 @@ public final class AssetDetailsContractControl extends HBox implements StudioCon
|
||||
if (viewState == null || viewState.selectedAssetReference() == null) {
|
||||
return;
|
||||
}
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||
}
|
||||
|
||||
|
||||
@ -17,8 +17,8 @@ import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioGlyphMaterialMetadataChangedEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
@ -222,7 +222,7 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
coordinator.apply();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
renderSection();
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(assetReference, true));
|
||||
} else {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling",
|
||||
"Apply failed: " + Objects.requireNonNullElse(response.errorMessage(), "unknown error")));
|
||||
|
||||
@ -25,9 +25,9 @@ import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
@ -188,7 +188,13 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
: AssetDetailsUiSupport.projectRelativePath(projectReference, sceneAssetRoot.resolve(binding.tilemap()))),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TILESET),
|
||||
editing ? createTilesetCombo(draft, true) : createTilesetReadOnlyValue(binding, draft)));
|
||||
editing ? createTilesetCombo(draft, true) : createTilesetReadOnlyValue(binding, draft)),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_PARALLAX),
|
||||
editing ? createParallaxEditor(draft) : new Label(draft.parallaxText())),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_DEFAULT_PALETTE),
|
||||
editing ? createDefaultPaletteEditor(draft) : new Label(draft.defaultPaletteText())));
|
||||
return card;
|
||||
}
|
||||
|
||||
@ -235,27 +241,9 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
final HBox actions = new HBox(8);
|
||||
actions.setAlignment(Pos.CENTER_LEFT);
|
||||
actions.getStyleClass().add("assets-scene-layer-actions");
|
||||
final boolean enabled = draft != null && draft.enabled();
|
||||
final boolean hasTmx = hasLayerTilemap(sceneAssetRoot, layerIndex);
|
||||
|
||||
final Button generateButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_GENERATE_TMX));
|
||||
generateButton.getStyleClass().add("assets-scene-layer-action");
|
||||
generateButton.setDisable(!enabled || hasTmx);
|
||||
generateButton.setOnAction(ignored -> generateLayerAction.accept(layerIndex));
|
||||
|
||||
final Button validateButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_VALIDATE_LAYER));
|
||||
validateButton.getStyleClass().add("assets-scene-layer-action");
|
||||
validateButton.setDisable(!enabled || !hasTmx);
|
||||
validateButton.setOnAction(ignored -> validateLayerAction.accept(layerIndex));
|
||||
|
||||
final Button acceptButton = AssetDetailsUiSupport.createActionButton(Container.i18n().text(I18n.ASSETS_ACTION_ACCEPT_LAYER));
|
||||
acceptButton.getStyleClass().add("assets-scene-layer-action");
|
||||
acceptButton.setDisable(!enabled || !hasTmx);
|
||||
acceptButton.setOnAction(ignored -> acceptLayerAction.accept(layerIndex));
|
||||
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
actions.getChildren().addAll(generateButton, validateButton, acceptButton, spacer);
|
||||
actions.getChildren().add(spacer);
|
||||
return actions;
|
||||
}
|
||||
|
||||
@ -320,6 +308,24 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
return combo;
|
||||
}
|
||||
|
||||
private Node createParallaxEditor(LayerDraft draft) {
|
||||
final HBox editor = new HBox(8);
|
||||
editor.getStyleClass().add("assets-scene-layer-size-editor");
|
||||
final TextField xField = createSizeField(draft.parallaxXText(), draft::setParallaxXText);
|
||||
final Label separator = new Label(",");
|
||||
separator.getStyleClass().add("assets-scene-layer-size-separator");
|
||||
final TextField yField = createSizeField(draft.parallaxYText(), draft::setParallaxYText);
|
||||
editor.getChildren().addAll(xField, separator, yField);
|
||||
return editor;
|
||||
}
|
||||
|
||||
private Node createDefaultPaletteEditor(LayerDraft draft) {
|
||||
final TextField textField = new TextField(draft.defaultPaletteText());
|
||||
textField.getStyleClass().add("assets-scene-layer-size-field");
|
||||
textField.textProperty().addListener((ignored, oldValue, newValue) -> draft.setDefaultPaletteText(newValue));
|
||||
return textField;
|
||||
}
|
||||
|
||||
private Node createTilesetReadOnlyValue(AssetStudioSceneLayerBinding binding, LayerDraft draft) {
|
||||
final String value = binding == null || draft == null || draft.tilesetAssetRoot().isBlank()
|
||||
? Container.i18n().text(I18n.ASSETS_VALUE_NOT_CONFIGURED)
|
||||
@ -377,7 +383,6 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
metadataService.writeSceneBankMetadata(viewState.selectedAssetDetails().summary().assetRoot(), updatedMetadata);
|
||||
Platform.runLater(() -> {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("scene-bank-layer", successMessage));
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||
});
|
||||
} catch (IOException exception) {
|
||||
@ -407,10 +412,10 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
}
|
||||
|
||||
private void applyEdit(int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
if (binding == null || editingDraft == null || !editingDraft.hasValidDimensions()) {
|
||||
if (binding == null || editingDraft == null || !editingDraft.hasValidState()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"scene-bank-layer",
|
||||
"Layer size must use positive integer values before applying changes."));
|
||||
"Layer configuration must use positive dimensions, non-negative parallax, and a non-negative default palette before applying changes."));
|
||||
return;
|
||||
}
|
||||
final LayerDraft draft = editingDraft.copy();
|
||||
@ -424,10 +429,28 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
draft.widthValue(),
|
||||
draft.heightValue(),
|
||||
current.tilemap(),
|
||||
draft.tilesetAssetRoot()),
|
||||
draft.tilesetAssetRoot(),
|
||||
draft.parallaxXValue(),
|
||||
draft.parallaxYValue(),
|
||||
draft.defaultPaletteValue(),
|
||||
preservedTilesOrBlank(current, draft)),
|
||||
"Updated layer " + layerIndex + " configuration.");
|
||||
}
|
||||
|
||||
private List<AssetStudioSceneTile> preservedTilesOrBlank(
|
||||
AssetStudioSceneLayerBinding current,
|
||||
LayerDraft draft) {
|
||||
if (current.width() == draft.widthValue() && current.height() == draft.heightValue()) {
|
||||
return current.tiles();
|
||||
}
|
||||
final int tileCount = draft.widthValue() * draft.heightValue();
|
||||
final List<AssetStudioSceneTile> tiles = new ArrayList<>(tileCount);
|
||||
for (int index = 0; index < tileCount; index += 1) {
|
||||
tiles.add(AssetStudioSceneTile.inactive(draft.defaultPaletteValue()));
|
||||
}
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
|
||||
private void resetEdit(AssetStudioSceneLayerBinding binding) {
|
||||
editingDraft = LayerDraft.from(binding);
|
||||
render();
|
||||
@ -467,27 +490,43 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
private String widthText;
|
||||
private String heightText;
|
||||
private String tilesetAssetRoot;
|
||||
private String parallaxXText;
|
||||
private String parallaxYText;
|
||||
private String defaultPaletteText;
|
||||
|
||||
private LayerDraft(boolean enabled, String widthText, String heightText, String tilesetAssetRoot) {
|
||||
private LayerDraft(
|
||||
boolean enabled,
|
||||
String widthText,
|
||||
String heightText,
|
||||
String tilesetAssetRoot,
|
||||
String parallaxXText,
|
||||
String parallaxYText,
|
||||
String defaultPaletteText) {
|
||||
this.enabled = enabled;
|
||||
this.widthText = widthText;
|
||||
this.heightText = heightText;
|
||||
this.tilesetAssetRoot = Objects.requireNonNullElse(tilesetAssetRoot, "").trim();
|
||||
this.parallaxXText = Objects.requireNonNullElse(parallaxXText, "").trim();
|
||||
this.parallaxYText = Objects.requireNonNullElse(parallaxYText, "").trim();
|
||||
this.defaultPaletteText = Objects.requireNonNullElse(defaultPaletteText, "").trim();
|
||||
}
|
||||
|
||||
static LayerDraft from(AssetStudioSceneLayerBinding binding) {
|
||||
if (binding == null) {
|
||||
return new LayerDraft(false, "16", "16", "");
|
||||
return new LayerDraft(false, "16", "16", "", "1.0", "1.0", "0");
|
||||
}
|
||||
return new LayerDraft(
|
||||
binding.enabled(),
|
||||
Integer.toString(binding.width()),
|
||||
Integer.toString(binding.height()),
|
||||
binding.tilesetAssetRoot());
|
||||
binding.tilesetAssetRoot(),
|
||||
trimFloat(binding.parallaxFactorX()),
|
||||
trimFloat(binding.parallaxFactorY()),
|
||||
Integer.toString(binding.defaultPaletteId()));
|
||||
}
|
||||
|
||||
LayerDraft copy() {
|
||||
return new LayerDraft(enabled, widthText, heightText, tilesetAssetRoot);
|
||||
return new LayerDraft(enabled, widthText, heightText, tilesetAssetRoot, parallaxXText, parallaxYText, defaultPaletteText);
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
@ -522,8 +561,40 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
this.tilesetAssetRoot = Objects.requireNonNullElse(tilesetAssetRoot, "").trim();
|
||||
}
|
||||
|
||||
boolean hasValidDimensions() {
|
||||
return parsePositiveInt(widthText) > 0 && parsePositiveInt(heightText) > 0;
|
||||
String parallaxXText() {
|
||||
return parallaxXText;
|
||||
}
|
||||
|
||||
void setParallaxXText(String parallaxXText) {
|
||||
this.parallaxXText = Objects.requireNonNullElse(parallaxXText, "").trim();
|
||||
}
|
||||
|
||||
String parallaxYText() {
|
||||
return parallaxYText;
|
||||
}
|
||||
|
||||
void setParallaxYText(String parallaxYText) {
|
||||
this.parallaxYText = Objects.requireNonNullElse(parallaxYText, "").trim();
|
||||
}
|
||||
|
||||
String parallaxText() {
|
||||
return parallaxXText + ", " + parallaxYText;
|
||||
}
|
||||
|
||||
String defaultPaletteText() {
|
||||
return defaultPaletteText;
|
||||
}
|
||||
|
||||
void setDefaultPaletteText(String defaultPaletteText) {
|
||||
this.defaultPaletteText = Objects.requireNonNullElse(defaultPaletteText, "").trim();
|
||||
}
|
||||
|
||||
boolean hasValidState() {
|
||||
return parsePositiveInt(widthText) > 0
|
||||
&& parsePositiveInt(heightText) > 0
|
||||
&& parseNonNegativeFloat(parallaxXText) >= 0.0f
|
||||
&& parseNonNegativeFloat(parallaxYText) >= 0.0f
|
||||
&& parseNonNegativeInt(defaultPaletteText) >= 0;
|
||||
}
|
||||
|
||||
int widthValue() {
|
||||
@ -534,6 +605,18 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
return parsePositiveInt(heightText);
|
||||
}
|
||||
|
||||
float parallaxXValue() {
|
||||
return parseNonNegativeFloat(parallaxXText);
|
||||
}
|
||||
|
||||
float parallaxYValue() {
|
||||
return parseNonNegativeFloat(parallaxYText);
|
||||
}
|
||||
|
||||
int defaultPaletteValue() {
|
||||
return parseNonNegativeInt(defaultPaletteText);
|
||||
}
|
||||
|
||||
private static int parsePositiveInt(String value) {
|
||||
try {
|
||||
return Integer.parseInt(Objects.requireNonNullElse(value, "").trim());
|
||||
@ -541,5 +624,30 @@ public final class AssetDetailsSceneLayersControl extends VBox implements Studio
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseNonNegativeInt(String value) {
|
||||
try {
|
||||
final int parsed = Integer.parseInt(Objects.requireNonNullElse(value, "").trim());
|
||||
return parsed >= 0 ? parsed : -1;
|
||||
} catch (NumberFormatException exception) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static float parseNonNegativeFloat(String value) {
|
||||
try {
|
||||
final float parsed = Float.parseFloat(Objects.requireNonNullElse(value, "").trim());
|
||||
return Float.isFinite(parsed) && parsed >= 0.0f ? parsed : -1.0f;
|
||||
} catch (NumberFormatException exception) {
|
||||
return -1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimFloat(float value) {
|
||||
if (value == (long) value) {
|
||||
return Long.toString((long) value) + ".0";
|
||||
}
|
||||
return Float.toString(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,20 @@ public final class AssetStudioMetadataService {
|
||||
layer.put("height", binding.height());
|
||||
layer.put("tilemap", binding.tilemap());
|
||||
layer.put("tileset_asset_root", binding.tilesetAssetRoot());
|
||||
layer.put("parallax_factor_x", binding.parallaxFactorX());
|
||||
layer.put("parallax_factor_y", binding.parallaxFactorY());
|
||||
layer.put("default_palette_id", binding.defaultPaletteId());
|
||||
if (binding.tiles().stream().anyMatch(AssetStudioSceneTile::active)) {
|
||||
final ArrayNode tiles = layer.putArray("tiles");
|
||||
for (AssetStudioSceneTile tile : binding.tiles()) {
|
||||
final var tileNode = tiles.addObject();
|
||||
tileNode.put("active", tile.active());
|
||||
tileNode.put("glyph_id", tile.glyphId());
|
||||
tileNode.put("palette_id", tile.paletteId());
|
||||
tileNode.put("flip_x", tile.flipX());
|
||||
tileNode.put("flip_y", tile.flipY());
|
||||
}
|
||||
}
|
||||
}
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
normalizedAssetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
|
||||
@ -137,7 +151,7 @@ public final class AssetStudioMetadataService {
|
||||
}
|
||||
final Path supportFile = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFile)) {
|
||||
return null;
|
||||
return defaultSceneBankMetadata(supportFile);
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(supportFile.toFile());
|
||||
@ -163,10 +177,28 @@ public final class AssetStudioMetadataService {
|
||||
final int height = layerNode.path("height").asInt(mapHeight);
|
||||
final String tilemap = layerNode.path("tilemap").asText("").trim();
|
||||
final String tilesetAssetRoot = layerNode.path("tileset_asset_root").asText("").trim();
|
||||
final float parallaxFactorX = (float) layerNode.path("parallax_factor_x").asDouble(1.0d);
|
||||
final float parallaxFactorY = (float) layerNode.path("parallax_factor_y").asDouble(1.0d);
|
||||
final int defaultPaletteId = layerNode.path("default_palette_id").asInt(0);
|
||||
if (index < 1 || index > layerCount || layerName.isBlank() || width <= 0 || height <= 0 || tilemap.isBlank() || !indexes.add(index)) {
|
||||
return null;
|
||||
}
|
||||
bindings.add(new AssetStudioSceneLayerBinding(enabled, index, layerName, width, height, tilemap, tilesetAssetRoot));
|
||||
final List<AssetStudioSceneTile> tiles = readSceneTiles(layerNode.path("tiles"), width, height, defaultPaletteId);
|
||||
if (tiles == null) {
|
||||
return null;
|
||||
}
|
||||
bindings.add(new AssetStudioSceneLayerBinding(
|
||||
enabled,
|
||||
index,
|
||||
layerName,
|
||||
width,
|
||||
height,
|
||||
tilemap,
|
||||
tilesetAssetRoot,
|
||||
parallaxFactorX,
|
||||
parallaxFactorY,
|
||||
defaultPaletteId,
|
||||
tiles));
|
||||
}
|
||||
bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
|
||||
return new AssetStudioSceneBankMetadata(mapWidth, mapHeight, layerCount, bindings, supportFile);
|
||||
@ -287,4 +319,44 @@ public final class AssetStudioMetadataService {
|
||||
final String normalized = value.trim().toUpperCase();
|
||||
return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
|
||||
}
|
||||
|
||||
private AssetStudioSceneBankMetadata defaultSceneBankMetadata(Path supportFile) {
|
||||
return new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
16,
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(false, 1, "Layer 1", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 16, "scene.tmx", "", 1.0f, 1.0f, 0)),
|
||||
supportFile);
|
||||
}
|
||||
|
||||
private List<AssetStudioSceneTile> readSceneTiles(JsonNode tilesNode, int width, int height, int defaultPaletteId) {
|
||||
final int tileCount = width * height;
|
||||
if (!(tilesNode instanceof ArrayNode arrayNode)) {
|
||||
final List<AssetStudioSceneTile> defaults = new ArrayList<>(tileCount);
|
||||
for (int index = 0; index < tileCount; index += 1) {
|
||||
defaults.add(AssetStudioSceneTile.inactive(defaultPaletteId));
|
||||
}
|
||||
return List.copyOf(defaults);
|
||||
}
|
||||
if (arrayNode.size() != tileCount) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetStudioSceneTile> tiles = new ArrayList<>(tileCount);
|
||||
for (JsonNode tileNode : arrayNode) {
|
||||
final boolean active = tileNode.path("active").asBoolean(false);
|
||||
final int glyphId = tileNode.path("glyph_id").asInt(0);
|
||||
final int paletteId = tileNode.path("palette_id").asInt(defaultPaletteId);
|
||||
final boolean flipX = tileNode.path("flip_x").asBoolean(false);
|
||||
final boolean flipY = tileNode.path("flip_y").asBoolean(false);
|
||||
if (glyphId < 0 || paletteId < 0) {
|
||||
return null;
|
||||
}
|
||||
tiles.add(new AssetStudioSceneTile(active, glyphId, paletteId, flipX, flipY));
|
||||
}
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioSceneLayerBinding(
|
||||
@ -9,11 +11,16 @@ public record AssetStudioSceneLayerBinding(
|
||||
int width,
|
||||
int height,
|
||||
String tilemap,
|
||||
String tilesetAssetRoot) {
|
||||
String tilesetAssetRoot,
|
||||
float parallaxFactorX,
|
||||
float parallaxFactorY,
|
||||
int defaultPaletteId,
|
||||
List<AssetStudioSceneTile> tiles) {
|
||||
public AssetStudioSceneLayerBinding {
|
||||
layerName = Objects.requireNonNull(layerName, "layerName").trim();
|
||||
tilemap = Objects.requireNonNull(tilemap, "tilemap").trim();
|
||||
tilesetAssetRoot = Objects.requireNonNull(tilesetAssetRoot, "tilesetAssetRoot").trim();
|
||||
tiles = List.copyOf(Objects.requireNonNull(tiles, "tiles"));
|
||||
if (index <= 0) {
|
||||
throw new IllegalArgumentException("index must be positive");
|
||||
}
|
||||
@ -26,5 +33,50 @@ public record AssetStudioSceneLayerBinding(
|
||||
if (tilemap.isBlank()) {
|
||||
throw new IllegalArgumentException("tilemap must not be blank");
|
||||
}
|
||||
if (!Float.isFinite(parallaxFactorX) || parallaxFactorX < 0.0f) {
|
||||
throw new IllegalArgumentException("parallaxFactorX must be finite and non-negative");
|
||||
}
|
||||
if (!Float.isFinite(parallaxFactorY) || parallaxFactorY < 0.0f) {
|
||||
throw new IllegalArgumentException("parallaxFactorY must be finite and non-negative");
|
||||
}
|
||||
if (defaultPaletteId < 0) {
|
||||
throw new IllegalArgumentException("defaultPaletteId must be non-negative");
|
||||
}
|
||||
if (tiles.size() != width * height) {
|
||||
throw new IllegalArgumentException("tiles must match width * height");
|
||||
}
|
||||
}
|
||||
|
||||
public AssetStudioSceneLayerBinding(
|
||||
boolean enabled,
|
||||
int index,
|
||||
String layerName,
|
||||
int width,
|
||||
int height,
|
||||
String tilemap,
|
||||
String tilesetAssetRoot,
|
||||
float parallaxFactorX,
|
||||
float parallaxFactorY,
|
||||
int defaultPaletteId) {
|
||||
this(
|
||||
enabled,
|
||||
index,
|
||||
layerName,
|
||||
width,
|
||||
height,
|
||||
tilemap,
|
||||
tilesetAssetRoot,
|
||||
parallaxFactorX,
|
||||
parallaxFactorY,
|
||||
defaultPaletteId,
|
||||
blankTiles(width, height, defaultPaletteId));
|
||||
}
|
||||
|
||||
private static List<AssetStudioSceneTile> blankTiles(int width, int height, int defaultPaletteId) {
|
||||
final List<AssetStudioSceneTile> tiles = new ArrayList<>(width * height);
|
||||
for (int index = 0; index < width * height; index += 1) {
|
||||
tiles.add(AssetStudioSceneTile.inactive(defaultPaletteId));
|
||||
}
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
public record AssetStudioSceneTile(
|
||||
boolean active,
|
||||
int glyphId,
|
||||
int paletteId,
|
||||
boolean flipX,
|
||||
boolean flipY) {
|
||||
|
||||
public AssetStudioSceneTile {
|
||||
if (glyphId < 0) {
|
||||
throw new IllegalArgumentException("glyphId must be non-negative");
|
||||
}
|
||||
if (paletteId < 0) {
|
||||
throw new IllegalArgumentException("paletteId must be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
public static AssetStudioSceneTile inactive(int paletteId) {
|
||||
return new AssetStudioSceneTile(false, 0, Math.max(0, paletteId), false, false);
|
||||
}
|
||||
}
|
||||
@ -3,383 +3,333 @@ package p.studio.workspaces.assets.scene;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.packer.messages.ListAssetsRequest;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||
import p.studio.Container;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||
import p.studio.workspaces.assets.tiled.TiledMapDocument;
|
||||
import p.studio.workspaces.assets.tiled.TiledTilesetDocument;
|
||||
import p.studio.workspaces.assets.tiled.TiledUnsupportedFeatureException;
|
||||
import p.studio.workspaces.assets.tiled.TiledXmlCodec;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SceneBankWorkflowService {
|
||||
private static final String VALIDATION_FILE = "scene-bank.validation.json";
|
||||
private static final String ACCEPTANCE_FILE = "scene-bank.acceptance.json";
|
||||
public static final String ACCEPTED_REQUEST_FILE = "scene-bank.pack-request.json";
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||
private final SceneBankGlyphAssetResolver glyphAssetResolver;
|
||||
|
||||
public SceneBankWorkflowService() {
|
||||
this((projectReference, tilesetAssetRoot) -> Container.packer().workspaceService()
|
||||
.listAssets(new ListAssetsRequest(projectReference.toPackerProjectContext()))
|
||||
.assets()
|
||||
.stream()
|
||||
.filter(asset -> asset.assetFamily() == AssetFamilyCatalog.GLYPH_BANK)
|
||||
.filter(asset -> projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(tilesetAssetRoot)
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.equals(asset.identity().assetRoot().toAbsolutePath().normalize()))
|
||||
.findFirst()
|
||||
.map(asset -> asset.identity().assetId() == null ? -1 : asset.identity().assetId())
|
||||
.orElse(-1));
|
||||
}
|
||||
|
||||
SceneBankWorkflowService(SceneBankGlyphAssetResolver glyphAssetResolver) {
|
||||
this.glyphAssetResolver = Objects.requireNonNull(glyphAssetResolver, "glyphAssetResolver");
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult inspect(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
if (details == null || details.summary().assetFamily() != p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new SceneBankWorkflowResult(true, "", AssetWorkspaceSceneBankValidation.NOT_APPLICABLE, List.of());
|
||||
}
|
||||
final List<PackerDiagnosticDTO> diagnostics = validateDiagnostics(projectReference, details, null);
|
||||
final String currentFingerprint = diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)
|
||||
? ""
|
||||
: computeFingerprint(projectReference, details);
|
||||
final String validatedFingerprint = readFingerprint(details.summary().assetRoot().resolve(VALIDATION_FILE));
|
||||
final String acceptedFingerprint = readFingerprint(details.summary().assetRoot().resolve(ACCEPTANCE_FILE));
|
||||
final AssetWorkspaceSceneBankValidation validation = validationState(
|
||||
diagnostics,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
final List<PackerDiagnosticDTO> diagnostics = new ArrayList<>(validateDiagnostics(projectReference, details));
|
||||
if (diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking)) {
|
||||
try {
|
||||
writeAcceptedRequest(
|
||||
details.summary().assetRoot().resolve(ACCEPTED_REQUEST_FILE),
|
||||
buildCanonicalRequest(projectReference, details));
|
||||
} catch (IOException | IllegalArgumentException exception) {
|
||||
diagnostics.add(error(
|
||||
"Unable to persist canonical scene-pack request: " + exception.getMessage(),
|
||||
details.summary().assetRoot().resolve(ACCEPTED_REQUEST_FILE)));
|
||||
}
|
||||
}
|
||||
final AssetWorkspaceSceneBankValidation validation = validationState(diagnostics);
|
||||
return new SceneBankWorkflowResult(
|
||||
diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
|
||||
messageFor(validation),
|
||||
validation,
|
||||
diagnostics);
|
||||
List.copyOf(diagnostics));
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult validate(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
final SceneBankWorkflowResult inspection = inspect(projectReference, details);
|
||||
final Path validationFile = details.summary().assetRoot().resolve(VALIDATION_FILE);
|
||||
try {
|
||||
if (inspection.success() && !inspection.validation().currentFingerprint().isBlank()) {
|
||||
writeFingerprint(validationFile, inspection.validation().currentFingerprint());
|
||||
} else {
|
||||
Files.deleteIfExists(validationFile);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Unable to persist validation state: " + exception.getMessage(),
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
return inspect(projectReference, details);
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult accept(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
final SceneBankWorkflowResult inspection = inspect(projectReference, details);
|
||||
if (!inspection.validation().canAccept()) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Scene Bank must validate successfully before acceptance.",
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
try {
|
||||
writeFingerprint(details.summary().assetRoot().resolve(ACCEPTANCE_FILE), inspection.validation().currentFingerprint());
|
||||
writeFingerprint(details.summary().assetRoot().resolve(VALIDATION_FILE), inspection.validation().currentFingerprint());
|
||||
} catch (IOException exception) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Unable to persist acceptance state: " + exception.getMessage(),
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
return inspect(projectReference, details);
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult inspectLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
if (details == null || details.summary().assetFamily() != p.packer.messages.assets.AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new SceneBankWorkflowResult(true, "", AssetWorkspaceSceneBankValidation.NOT_APPLICABLE, List.of());
|
||||
}
|
||||
final List<PackerDiagnosticDTO> diagnostics = validateDiagnostics(projectReference, details, layerIndex);
|
||||
final String currentFingerprint = diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)
|
||||
? ""
|
||||
: computeFingerprint(projectReference, details, layerIndex);
|
||||
final String validatedFingerprint = readFingerprint(details.summary().assetRoot().resolve(layerValidationFile(layerIndex)));
|
||||
final String acceptedFingerprint = readFingerprint(details.summary().assetRoot().resolve(layerAcceptanceFile(layerIndex)));
|
||||
final AssetWorkspaceSceneBankValidation validation = validationState(
|
||||
diagnostics,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
return new SceneBankWorkflowResult(
|
||||
diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
|
||||
messageFor(validation),
|
||||
validation,
|
||||
diagnostics);
|
||||
return inspect(projectReference, details);
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult validateLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
final SceneBankWorkflowResult inspection = inspectLayer(projectReference, details, layerIndex);
|
||||
final Path validationFile = details.summary().assetRoot().resolve(layerValidationFile(layerIndex));
|
||||
try {
|
||||
if (inspection.success() && !inspection.validation().currentFingerprint().isBlank()) {
|
||||
writeFingerprint(validationFile, inspection.validation().currentFingerprint());
|
||||
} else {
|
||||
Files.deleteIfExists(validationFile);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Unable to persist validation state: " + exception.getMessage(),
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
return inspectLayer(projectReference, details, layerIndex);
|
||||
return inspect(projectReference, details);
|
||||
}
|
||||
|
||||
public SceneBankWorkflowResult acceptLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
final SceneBankWorkflowResult inspection = inspectLayer(projectReference, details, layerIndex);
|
||||
if (!inspection.validation().canAccept()) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Scene layer must validate successfully before acceptance.",
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
try {
|
||||
writeFingerprint(details.summary().assetRoot().resolve(layerAcceptanceFile(layerIndex)), inspection.validation().currentFingerprint());
|
||||
writeFingerprint(details.summary().assetRoot().resolve(layerValidationFile(layerIndex)), inspection.validation().currentFingerprint());
|
||||
} catch (IOException exception) {
|
||||
return new SceneBankWorkflowResult(
|
||||
false,
|
||||
"Unable to persist acceptance state: " + exception.getMessage(),
|
||||
inspection.validation(),
|
||||
inspection.diagnostics());
|
||||
}
|
||||
return inspectLayer(projectReference, details, layerIndex);
|
||||
return inspect(projectReference, details);
|
||||
}
|
||||
|
||||
private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details, Integer targetLayerIndex) {
|
||||
private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
final List<PackerDiagnosticDTO> diagnostics = new ArrayList<>();
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
diagnostics.add(error("Scene Bank support metadata is missing or invalid.", details.summary().assetRoot().resolve("scene-bank.studio.json")));
|
||||
diagnostics.add(error("Scene Bank support metadata is missing or invalid.",
|
||||
details.summary().assetRoot().resolve("scene-bank.studio.json")));
|
||||
return diagnostics;
|
||||
}
|
||||
final SceneLayerRuntimeDefaults sceneDefaults =
|
||||
resolveSceneLayerRuntimeDefaults(projectReference, metadata.layerBindings());
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
if (targetLayerIndex != null && binding.index() != targetLayerIndex) {
|
||||
if (resolveLayerRuntimeDefaults(projectReference, binding, sceneDefaults) == null) {
|
||||
diagnostics.add(error(
|
||||
"Unable to resolve runtime glyph/tile_size defaults for inactive layer " + binding.index() + ".",
|
||||
metadata.supportFile()));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (binding.tilesetAssetRoot().isBlank()) {
|
||||
diagnostics.add(error("Layer " + binding.index() + " is missing tileset_asset_root.", metadata.supportFile()));
|
||||
continue;
|
||||
}
|
||||
final Path tmxPath = details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize();
|
||||
final Path tsxPath = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(binding.tilesetAssetRoot())
|
||||
.resolve("tileset.tsx")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!Files.isRegularFile(tmxPath)) {
|
||||
diagnostics.add(error("TMX file is missing for layer " + binding.index() + ": " + binding.tilemap(), tmxPath));
|
||||
if (binding.tiles().size() != binding.width() * binding.height()) {
|
||||
diagnostics.add(error("Canonical scene tile count does not match width * height for layer " + binding.index() + ".",
|
||||
metadata.supportFile()));
|
||||
continue;
|
||||
}
|
||||
if (!Files.isRegularFile(tsxPath)) {
|
||||
diagnostics.add(error("Referenced TSX file is missing for layer " + binding.index() + ": " + binding.tilesetAssetRoot(), tsxPath));
|
||||
continue;
|
||||
if (resolveGlyphAssetId(projectReference, binding.tilesetAssetRoot()) < 0) {
|
||||
diagnostics.add(error("Unable to resolve glyph_asset_id for layer " + binding.index() + ": " + binding.tilesetAssetRoot(),
|
||||
metadata.supportFile()));
|
||||
}
|
||||
try {
|
||||
final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
|
||||
final TiledMapDocument map = codec.readMap(tmxPath);
|
||||
final String expectedSource = details.summary().assetRoot()
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.relativize(tsxPath)
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
if (map.tilesets().size() != 1) {
|
||||
diagnostics.add(error("TMX must reference exactly one TSX in wave 1: " + binding.tilemap(), tmxPath));
|
||||
} else if (!expectedSource.equals(map.tilesets().getFirst().source())) {
|
||||
diagnostics.add(error("TMX tileset reference does not match support metadata: " + binding.tilemap(), tmxPath));
|
||||
if (resolveTileSize(projectReference, binding.tilesetAssetRoot()) <= 0) {
|
||||
diagnostics.add(error("Unable to resolve tile_size for layer " + binding.index() + ": " + binding.tilesetAssetRoot(),
|
||||
metadata.supportFile()));
|
||||
}
|
||||
for (AssetStudioSceneTile tile : binding.tiles()) {
|
||||
if (tile.glyphId() < 0) {
|
||||
diagnostics.add(error("Canonical scene tile glyph_id must be non-negative for layer " + binding.index() + ".",
|
||||
metadata.supportFile()));
|
||||
break;
|
||||
}
|
||||
if (map.width() != binding.width() || map.height() != binding.height()) {
|
||||
diagnostics.add(error("TMX dimensions do not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||
if (tile.paletteId() < 0) {
|
||||
diagnostics.add(error("Canonical scene tile palette_id must be non-negative for layer " + binding.index() + ".",
|
||||
metadata.supportFile()));
|
||||
break;
|
||||
}
|
||||
if (map.tileWidth() != tileset.tileWidth() || map.tileHeight() != tileset.tileHeight()) {
|
||||
diagnostics.add(error("TMX tile size does not match referenced TSX: " + binding.tilemap(), tmxPath));
|
||||
}
|
||||
if (map.tileLayers().size() != 1 || !binding.layerName().equals(map.tileLayers().getFirst().name())) {
|
||||
diagnostics.add(error("TMX layer mapping does not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||
}
|
||||
} catch (TiledUnsupportedFeatureException exception) {
|
||||
diagnostics.add(error(exception.getMessage(), tmxPath));
|
||||
} catch (IOException exception) {
|
||||
diagnostics.add(error("Unable to validate Scene Bank XML: " + exception.getMessage(), tmxPath));
|
||||
}
|
||||
}
|
||||
return List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
private String computeFingerprint(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
final List<Path> files = new ArrayList<>();
|
||||
files.add(metadata.supportFile());
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
files.add(details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize());
|
||||
if (!binding.tilesetAssetRoot().isBlank()) {
|
||||
files.add(projectReference.rootPath().resolve("assets").resolve(binding.tilesetAssetRoot()).resolve("tileset.tsx").toAbsolutePath().normalize());
|
||||
}
|
||||
private ScenePackRequest buildCanonicalRequest(ProjectReference projectReference, AssetWorkspaceAssetDetails details) throws IOException {
|
||||
final AssetStudioSceneBankMetadata metadata = Objects.requireNonNull(details.sceneBankMetadata(), "sceneBankMetadata");
|
||||
final SceneLayerRuntimeDefaults sceneDefaults =
|
||||
resolveSceneLayerRuntimeDefaults(projectReference, metadata.layerBindings());
|
||||
final List<ScenePackLayerRequest> layers = new ArrayList<>();
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
layers.add(inactiveLayer(projectReference, binding, sceneDefaults));
|
||||
continue;
|
||||
}
|
||||
files.stream()
|
||||
.distinct()
|
||||
.sorted(Comparator.comparing(Path::toString))
|
||||
.forEach(path -> updateDigest(digest, path));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (Exception exception) {
|
||||
return "";
|
||||
layers.add(activeLayer(projectReference, binding));
|
||||
}
|
||||
return new ScenePackRequest(1, layers);
|
||||
}
|
||||
|
||||
private ScenePackLayerRequest inactiveLayer(ProjectReference projectReference,
|
||||
AssetStudioSceneLayerBinding binding,
|
||||
SceneLayerRuntimeDefaults sceneDefaults) {
|
||||
final SceneLayerRuntimeDefaults defaults = resolveLayerRuntimeDefaults(projectReference, binding, sceneDefaults);
|
||||
if (defaults == null) {
|
||||
throw new IllegalArgumentException("Unable to resolve runtime defaults for inactive layer " + binding.index());
|
||||
}
|
||||
final List<ScenePackTileRequest> tiles = new ArrayList<>(binding.tiles().size());
|
||||
for (AssetStudioSceneTile tile : binding.tiles()) {
|
||||
tiles.add(new ScenePackTileRequest(false, 0, tile.paletteId(), false, false));
|
||||
}
|
||||
return new ScenePackLayerRequest(
|
||||
false,
|
||||
defaults.glyphAssetId(),
|
||||
defaults.tileSize(),
|
||||
binding.parallaxFactorX(),
|
||||
binding.parallaxFactorY(),
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
tiles);
|
||||
}
|
||||
|
||||
private ScenePackLayerRequest activeLayer(ProjectReference projectReference, AssetStudioSceneLayerBinding binding) throws IOException {
|
||||
final int glyphAssetId = resolveGlyphAssetId(projectReference, binding.tilesetAssetRoot());
|
||||
if (glyphAssetId < 0) {
|
||||
throw new IllegalArgumentException("Unable to resolve glyph_asset_id for " + binding.tilesetAssetRoot());
|
||||
}
|
||||
final int tileSize = resolveTileSize(projectReference, binding.tilesetAssetRoot());
|
||||
if (tileSize <= 0) {
|
||||
throw new IllegalArgumentException("Unable to resolve tile_size for " + binding.tilesetAssetRoot());
|
||||
}
|
||||
final List<ScenePackTileRequest> tiles = new ArrayList<>(binding.tiles().size());
|
||||
for (AssetStudioSceneTile tile : binding.tiles()) {
|
||||
if (!tile.active()) {
|
||||
tiles.add(new ScenePackTileRequest(false, 0, tile.paletteId(), false, false));
|
||||
continue;
|
||||
}
|
||||
tiles.add(new ScenePackTileRequest(true, tile.glyphId(), tile.paletteId(), tile.flipX(), tile.flipY()));
|
||||
}
|
||||
return new ScenePackLayerRequest(
|
||||
true,
|
||||
glyphAssetId,
|
||||
tileSize,
|
||||
binding.parallaxFactorX(),
|
||||
binding.parallaxFactorY(),
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
tiles);
|
||||
}
|
||||
|
||||
private SceneLayerRuntimeDefaults resolveSceneLayerRuntimeDefaults(ProjectReference projectReference,
|
||||
List<AssetStudioSceneLayerBinding> bindings) {
|
||||
for (AssetStudioSceneLayerBinding binding : bindings) {
|
||||
final SceneLayerRuntimeDefaults defaults = resolveLayerOwnRuntimeDefaults(projectReference, binding);
|
||||
if (defaults != null) {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SceneLayerRuntimeDefaults resolveLayerRuntimeDefaults(ProjectReference projectReference,
|
||||
AssetStudioSceneLayerBinding binding,
|
||||
SceneLayerRuntimeDefaults sceneDefaults) {
|
||||
final SceneLayerRuntimeDefaults ownDefaults = resolveLayerOwnRuntimeDefaults(projectReference, binding);
|
||||
return ownDefaults != null ? ownDefaults : sceneDefaults;
|
||||
}
|
||||
|
||||
private SceneLayerRuntimeDefaults resolveLayerOwnRuntimeDefaults(ProjectReference projectReference,
|
||||
AssetStudioSceneLayerBinding binding) {
|
||||
if (binding.tilesetAssetRoot().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
final int glyphAssetId = resolveGlyphAssetId(projectReference, binding.tilesetAssetRoot());
|
||||
final int tileSize = resolveTileSize(projectReference, binding.tilesetAssetRoot());
|
||||
if (glyphAssetId < 0 || tileSize <= 0) {
|
||||
return null;
|
||||
}
|
||||
return new SceneLayerRuntimeDefaults(glyphAssetId, tileSize);
|
||||
}
|
||||
|
||||
private void writeAcceptedRequest(Path path, ScenePackRequest request) throws IOException {
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
root.put("format", "SCENE/tilemap_v1");
|
||||
root.put("version", request.version());
|
||||
final var layersNode = root.putArray("layers");
|
||||
for (ScenePackLayerRequest layer : request.layers()) {
|
||||
final var layerNode = layersNode.addObject();
|
||||
layerNode.put("active", layer.active());
|
||||
layerNode.put("glyph_asset_id", layer.glyphAssetId());
|
||||
layerNode.put("tile_size", layer.tileSize());
|
||||
layerNode.put("parallax_factor_x", layer.parallaxFactorX());
|
||||
layerNode.put("parallax_factor_y", layer.parallaxFactorY());
|
||||
layerNode.put("width", layer.width());
|
||||
layerNode.put("height", layer.height());
|
||||
final var tilesNode = layerNode.putArray("tiles");
|
||||
for (ScenePackTileRequest tile : layer.tiles()) {
|
||||
final var tileNode = tilesNode.addObject();
|
||||
tileNode.put("active", tile.active());
|
||||
tileNode.put("glyph_id", tile.glyphId());
|
||||
tileNode.put("palette_id", tile.paletteId());
|
||||
tileNode.put("flip_x", tile.flipX());
|
||||
tileNode.put("flip_y", tile.flipY());
|
||||
}
|
||||
}
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), root);
|
||||
}
|
||||
|
||||
private int resolveGlyphAssetId(ProjectReference projectReference, String tilesetAssetRoot) {
|
||||
try {
|
||||
return glyphAssetResolver.resolveGlyphAssetId(projectReference, tilesetAssetRoot);
|
||||
} catch (IOException exception) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private String computeFingerprint(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return "";
|
||||
private int resolveTileSize(ProjectReference projectReference, String tilesetAssetRoot) {
|
||||
final Path declarationPath = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(tilesetAssetRoot)
|
||||
.resolve("asset.json")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!Files.isRegularFile(declarationPath)) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
final List<Path> files = new ArrayList<>();
|
||||
files.add(metadata.supportFile());
|
||||
metadata.layerBindings().stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.filter(binding -> binding.index() == layerIndex)
|
||||
.findFirst()
|
||||
.ifPresent(binding -> {
|
||||
files.add(details.summary().assetRoot().resolve(binding.tilemap()).toAbsolutePath().normalize());
|
||||
if (!binding.tilesetAssetRoot().isBlank()) {
|
||||
files.add(projectReference.rootPath().resolve("assets").resolve(binding.tilesetAssetRoot()).resolve("tileset.tsx").toAbsolutePath().normalize());
|
||||
}
|
||||
});
|
||||
if (files.size() == 1) {
|
||||
return "";
|
||||
}
|
||||
files.stream()
|
||||
.distinct()
|
||||
.sorted(Comparator.comparing(Path::toString))
|
||||
.forEach(path -> updateDigest(digest, path));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (Exception exception) {
|
||||
return "";
|
||||
final JsonNode root = mapper.readTree(declarationPath.toFile());
|
||||
final String value = root.path("output").path("metadata").path("tile_size").asText("").trim().toLowerCase();
|
||||
return switch (value) {
|
||||
case "8x8" -> 8;
|
||||
case "16x16" -> 16;
|
||||
case "32x32" -> 32;
|
||||
default -> -1;
|
||||
};
|
||||
} catch (IOException exception) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDigest(MessageDigest digest, Path path) {
|
||||
try {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return;
|
||||
}
|
||||
digest.update(path.toString().getBytes(StandardCharsets.UTF_8));
|
||||
digest.update((byte) 0);
|
||||
digest.update(Files.readAllBytes(path));
|
||||
digest.update((byte) 0);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private AssetWorkspaceSceneBankValidation validationState(
|
||||
List<PackerDiagnosticDTO> diagnostics,
|
||||
String currentFingerprint,
|
||||
String validatedFingerprint,
|
||||
String acceptedFingerprint) {
|
||||
private AssetWorkspaceSceneBankValidation validationState(List<PackerDiagnosticDTO> diagnostics) {
|
||||
if (diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)) {
|
||||
return new AssetWorkspaceSceneBankValidation(
|
||||
AssetWorkspaceSceneBankStatus.VALIDATION_FAILED,
|
||||
true,
|
||||
false,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
}
|
||||
final boolean validated = !currentFingerprint.isBlank() && currentFingerprint.equals(validatedFingerprint);
|
||||
final boolean accepted = validated && currentFingerprint.equals(acceptedFingerprint);
|
||||
if (accepted) {
|
||||
return new AssetWorkspaceSceneBankValidation(
|
||||
AssetWorkspaceSceneBankStatus.READY,
|
||||
false,
|
||||
false,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
}
|
||||
if (validated) {
|
||||
return new AssetWorkspaceSceneBankValidation(
|
||||
AssetWorkspaceSceneBankStatus.VALIDATED_PENDING_ACCEPTANCE,
|
||||
true,
|
||||
true,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
"",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
return new AssetWorkspaceSceneBankValidation(
|
||||
AssetWorkspaceSceneBankStatus.PENDING_VALIDATION,
|
||||
true,
|
||||
AssetWorkspaceSceneBankStatus.READY,
|
||||
false,
|
||||
currentFingerprint,
|
||||
validatedFingerprint,
|
||||
acceptedFingerprint);
|
||||
}
|
||||
|
||||
private String layerValidationFile(int layerIndex) {
|
||||
return "scene-bank.layer-" + layerIndex + ".validation.json";
|
||||
}
|
||||
|
||||
private String layerAcceptanceFile(int layerIndex) {
|
||||
return "scene-bank.layer-" + layerIndex + ".acceptance.json";
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
|
||||
private String messageFor(AssetWorkspaceSceneBankValidation validation) {
|
||||
return switch (validation.status()) {
|
||||
case NOT_APPLICABLE -> "";
|
||||
case PENDING_VALIDATION -> "Scene Bank has pending external changes and requires validation.";
|
||||
case VALIDATED_PENDING_ACCEPTANCE -> "Scene Bank validation succeeded and is waiting for explicit acceptance.";
|
||||
case READY -> "Scene Bank is ready.";
|
||||
case PENDING_VALIDATION, VALIDATED_PENDING_ACCEPTANCE -> "Scene Bank canonical workflow no longer uses validation/acceptance staging.";
|
||||
case READY -> "Scene Bank canonical scene is ready.";
|
||||
case VALIDATION_FAILED -> "Scene Bank validation failed.";
|
||||
};
|
||||
}
|
||||
|
||||
private String readFingerprint(Path path) {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(path.toFile());
|
||||
return root.path("fingerprint").asText("").trim();
|
||||
} catch (IOException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFingerprint(Path path, String fingerprint) throws IOException {
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
root.put("fingerprint", Objects.requireNonNullElse(fingerprint, ""));
|
||||
root.put("recorded_at", Instant.now().toString());
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), root);
|
||||
}
|
||||
|
||||
private PackerDiagnosticDTO error(String message, Path path) {
|
||||
return new PackerDiagnosticDTO(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
@ -388,4 +338,12 @@ public final class SceneBankWorkflowService {
|
||||
path,
|
||||
true);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SceneBankGlyphAssetResolver {
|
||||
int resolveGlyphAssetId(ProjectReference projectReference, String tilesetAssetRoot) throws IOException;
|
||||
}
|
||||
|
||||
private record SceneLayerRuntimeDefaults(int glyphAssetId, int tileSize) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package p.studio.workspaces.assets.scene;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ScenePackLayerRequest(
|
||||
boolean active,
|
||||
int glyphAssetId,
|
||||
int tileSize,
|
||||
float parallaxFactorX,
|
||||
float parallaxFactorY,
|
||||
int width,
|
||||
int height,
|
||||
List<ScenePackTileRequest> tiles) {
|
||||
|
||||
public ScenePackLayerRequest {
|
||||
tiles = List.copyOf(Objects.requireNonNull(tiles, "tiles"));
|
||||
if (glyphAssetId < 0) {
|
||||
throw new IllegalArgumentException("glyphAssetId must be non-negative");
|
||||
}
|
||||
if (tileSize < 0) {
|
||||
throw new IllegalArgumentException("tileSize must be non-negative");
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new IllegalArgumentException("width and height must be positive");
|
||||
}
|
||||
if (!Float.isFinite(parallaxFactorX) || parallaxFactorX < 0.0f) {
|
||||
throw new IllegalArgumentException("parallaxFactorX must be finite and non-negative");
|
||||
}
|
||||
if (!Float.isFinite(parallaxFactorY) || parallaxFactorY < 0.0f) {
|
||||
throw new IllegalArgumentException("parallaxFactorY must be finite and non-negative");
|
||||
}
|
||||
if (tiles.size() != width * height) {
|
||||
throw new IllegalArgumentException("tiles size must match width * height");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.workspaces.assets.scene;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ScenePackRequest(
|
||||
int version,
|
||||
List<ScenePackLayerRequest> layers) {
|
||||
|
||||
public ScenePackRequest {
|
||||
layers = List.copyOf(Objects.requireNonNull(layers, "layers"));
|
||||
if (version <= 0) {
|
||||
throw new IllegalArgumentException("version must be positive");
|
||||
}
|
||||
if (layers.size() != 4) {
|
||||
throw new IllegalArgumentException("layers must contain exactly 4 entries");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package p.studio.workspaces.assets.scene;
|
||||
|
||||
public record ScenePackTileRequest(
|
||||
boolean active,
|
||||
int glyphId,
|
||||
int paletteId,
|
||||
boolean flipX,
|
||||
boolean flipY) {
|
||||
|
||||
public ScenePackTileRequest {
|
||||
if (glyphId < 0) {
|
||||
throw new IllegalArgumentException("glyphId must be non-negative");
|
||||
}
|
||||
if (paletteId < 0) {
|
||||
throw new IllegalArgumentException("paletteId must be non-negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,27 +4,48 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TiledAssetGenerationService {
|
||||
public static final String GENERATED_TSX_FILE = "tileset.tsx";
|
||||
|
||||
private static final long FLIP_HORIZONTAL_FLAG = 0x80000000L;
|
||||
private static final long FLIP_VERTICAL_FLAG = 0x40000000L;
|
||||
private static final long FLIP_DIAGONAL_FLAG = 0x20000000L;
|
||||
private static final long GID_MASK = ~(FLIP_HORIZONTAL_FLAG | FLIP_VERTICAL_FLAG | FLIP_DIAGONAL_FLAG);
|
||||
|
||||
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
|
||||
public enum SceneTmxExportDisposition {
|
||||
OVERWRITE,
|
||||
KEEP_BOTH
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult generateTilesetTsx(AssetWorkspaceAssetDetails details) {
|
||||
return exportTilesetTsx(details);
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult exportTilesetTsx(AssetWorkspaceAssetDetails details) {
|
||||
if (details == null
|
||||
|| details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK
|
||||
|| details.summary().glyphSpecialization() != AssetStudioGlyphSpecialization.TILESET) {
|
||||
return new TiledAssetGenerationResult(false, "TSX generation is available only for Tileset-specialized glyph banks.", List.of());
|
||||
return new TiledAssetGenerationResult(false, "TSX export is available only for Tileset-specialized glyph banks.", List.of());
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> selectedFiles = details.bankComposition().selectedFiles();
|
||||
if (selectedFiles.isEmpty()) {
|
||||
@ -55,120 +76,330 @@ public final class TiledAssetGenerationService {
|
||||
final Path outputPath = details.summary().assetRoot().resolve(GENERATED_TSX_FILE);
|
||||
try {
|
||||
codec.writeTileset(outputPath, document);
|
||||
return new TiledAssetGenerationResult(true, "Generated TSX successfully.", List.of(outputPath));
|
||||
return new TiledAssetGenerationResult(true, "Exported TSX successfully.", List.of(outputPath));
|
||||
} catch (IOException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to generate TSX: " + exception.getMessage(), List.of());
|
||||
return new TiledAssetGenerationResult(false, "Unable to export TSX: " + exception.getMessage(), List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult generateSceneBankTilemaps(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new TiledAssetGenerationResult(false, "TMX generation is available only for Scene Bank assets.", List.of());
|
||||
}
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of());
|
||||
}
|
||||
final List<Path> writtenFiles = new ArrayList<>();
|
||||
try {
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
final TiledAssetGenerationResult result = generateSceneBankTilemap(projectReference, details, binding.index());
|
||||
if (!result.success()) {
|
||||
return result;
|
||||
}
|
||||
writtenFiles.addAll(result.writtenFiles());
|
||||
}
|
||||
if (writtenFiles.isEmpty()) {
|
||||
return new TiledAssetGenerationResult(false, "Enable at least one layer before generating TMX.", List.of());
|
||||
}
|
||||
return new TiledAssetGenerationResult(true, "Generated TMX successfully.", writtenFiles);
|
||||
} catch (RuntimeException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to generate TMX: " + exception.getMessage(), List.of());
|
||||
}
|
||||
return exportSceneBankTmx(projectReference, details);
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult generateSceneBankTilemap(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
return exportSceneBankTmx(projectReference, details);
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult exportSceneBankTmx(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
return exportSceneBankTmx(projectReference, details, SceneTmxExportDisposition.OVERWRITE);
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult exportSceneBankTmx(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetDetails details,
|
||||
SceneTmxExportDisposition disposition) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new TiledAssetGenerationResult(false, "TMX generation is available only for Scene Bank assets.", List.of());
|
||||
return new TiledAssetGenerationResult(false, "TMX export is available only for Scene Bank assets.", List.of());
|
||||
}
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of());
|
||||
}
|
||||
final AssetStudioSceneLayerBinding binding = metadata.layerBindings().stream()
|
||||
.filter(candidate -> candidate.index() == layerIndex)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (binding == null) {
|
||||
return new TiledAssetGenerationResult(false, "Layer " + layerIndex + " is not defined in Scene Bank metadata.", List.of());
|
||||
final List<AssetStudioSceneLayerBinding> enabledLayers = metadata.layerBindings().stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.toList();
|
||||
if (enabledLayers.isEmpty()) {
|
||||
return new TiledAssetGenerationResult(false, "Enable at least one layer before exporting TMX.", List.of());
|
||||
}
|
||||
if (!binding.enabled()) {
|
||||
return new TiledAssetGenerationResult(false, "Layer " + layerIndex + " is disabled.", List.of());
|
||||
final String sceneTilemap = resolveSceneTilemap(metadata);
|
||||
if (sceneTilemap == null) {
|
||||
return new TiledAssetGenerationResult(false, "Scene TMX export requires a single shared tilemap binding across Scene Bank layers.", List.of());
|
||||
}
|
||||
try {
|
||||
final String tilesetAssetRoot = Objects.requireNonNullElse(binding.tilesetAssetRoot(), "").trim();
|
||||
if (tilesetAssetRoot.isBlank()) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Layer " + binding.index() + " must declare a non-blank tileset_asset_root before TMX generation.",
|
||||
List.of());
|
||||
}
|
||||
final Path tilesetPath = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(tilesetAssetRoot)
|
||||
.resolve(GENERATED_TSX_FILE)
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!java.nio.file.Files.isRegularFile(tilesetPath)) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Referenced TSX was not found for layer " + binding.index() + ": " + tilesetAssetRoot,
|
||||
List.of());
|
||||
}
|
||||
final TiledTilesetDocument tileset = codec.readTileset(tilesetPath);
|
||||
final String relativeTilesetPath = details.summary().assetRoot()
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.relativize(tilesetPath)
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
final int tileCount = binding.width() * binding.height();
|
||||
final List<Long> gids = new ArrayList<>(tileCount);
|
||||
for (int index = 0; index < tileCount; index += 1) {
|
||||
gids.add(0L);
|
||||
final ExportContext context = buildExportContext(projectReference, details, enabledLayers);
|
||||
final List<TiledTileLayer> tileLayers = new ArrayList<>();
|
||||
int nextLayerId = 1;
|
||||
for (AssetStudioSceneLayerBinding binding : enabledLayers) {
|
||||
final TilesetRef ref = context.tilesetByAssetRoot().get(binding.tilesetAssetRoot());
|
||||
final List<Long> gids = new ArrayList<>(binding.tiles().size());
|
||||
for (AssetStudioSceneTile tile : binding.tiles()) {
|
||||
if (!tile.active()) {
|
||||
gids.add(0L);
|
||||
continue;
|
||||
}
|
||||
if (tile.glyphId() >= ref.tileCount()) {
|
||||
return new TiledAssetGenerationResult(
|
||||
false,
|
||||
"Layer " + binding.index() + " references glyph_id outside the exported TSX range.",
|
||||
List.of());
|
||||
}
|
||||
long gid = ref.firstGid() + tile.glyphId();
|
||||
if (tile.flipX()) {
|
||||
gid |= FLIP_HORIZONTAL_FLAG;
|
||||
}
|
||||
if (tile.flipY()) {
|
||||
gid |= FLIP_VERTICAL_FLAG;
|
||||
}
|
||||
gids.add(gid);
|
||||
}
|
||||
tileLayers.add(new TiledTileLayer(
|
||||
nextLayerId,
|
||||
binding.layerName(),
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
gids,
|
||||
List.of()));
|
||||
nextLayerId += 1;
|
||||
}
|
||||
final TiledMapDocument document = new TiledMapDocument(
|
||||
"1.10",
|
||||
"1.12.1",
|
||||
"orthogonal",
|
||||
"right-down",
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
tileset.tileWidth(),
|
||||
tileset.tileHeight(),
|
||||
2,
|
||||
enabledLayers.stream().mapToInt(AssetStudioSceneLayerBinding::width).max().orElse(1),
|
||||
enabledLayers.stream().mapToInt(AssetStudioSceneLayerBinding::height).max().orElse(1),
|
||||
context.tileSize(),
|
||||
context.tileSize(),
|
||||
nextLayerId,
|
||||
1,
|
||||
List.of(),
|
||||
List.of(new TiledTilesetReference(1, relativeTilesetPath)),
|
||||
List.of(new TiledTileLayer(
|
||||
1,
|
||||
context.tilesetRefs(),
|
||||
tileLayers,
|
||||
List.of());
|
||||
final Path outputPath = details.summary().assetRoot().resolve(sceneTilemap);
|
||||
final ArrayList<Path> writtenFiles = new ArrayList<>();
|
||||
if (disposition == SceneTmxExportDisposition.KEEP_BOTH && Files.isRegularFile(outputPath)) {
|
||||
final Path backupPath = nextSceneTmxBackupPath(outputPath);
|
||||
Files.copy(outputPath, backupPath);
|
||||
writtenFiles.add(backupPath);
|
||||
}
|
||||
codec.writeMap(outputPath, document);
|
||||
writtenFiles.add(outputPath);
|
||||
return new TiledAssetGenerationResult(true, "Exported TMX successfully.", List.copyOf(writtenFiles));
|
||||
} catch (IOException | TiledUnsupportedFeatureException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to export TMX: " + exception.getMessage(), List.of());
|
||||
}
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult importSceneBankTmx(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import is available only for Scene Bank assets.", List.of());
|
||||
}
|
||||
final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
|
||||
if (metadata == null) {
|
||||
return new TiledAssetGenerationResult(false, "Scene Bank support metadata is missing or invalid.", List.of());
|
||||
}
|
||||
final String sceneTilemap = resolveSceneTilemap(metadata);
|
||||
if (sceneTilemap == null) {
|
||||
return new TiledAssetGenerationResult(false, "Scene TMX import requires a single shared tilemap binding across Scene Bank layers.", List.of());
|
||||
}
|
||||
final Path inputPath = details.summary().assetRoot().resolve(sceneTilemap);
|
||||
if (!Files.isRegularFile(inputPath)) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import source is missing: " + sceneTilemap, List.of());
|
||||
}
|
||||
try {
|
||||
final List<AssetStudioSceneLayerBinding> enabledLayers = metadata.layerBindings().stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.toList();
|
||||
final ExportContext context = buildExportContext(projectReference, details, enabledLayers);
|
||||
final TiledMapDocument map = codec.readMap(inputPath);
|
||||
if (map.tileLayers().isEmpty()) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: at least one tile layer is required.", List.of());
|
||||
}
|
||||
if (map.tileLayers().size() > metadata.layerCount()) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: more than four tile layers are not supported.", List.of());
|
||||
}
|
||||
if (map.tilesets().size() != context.tilesetRefs().size()) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: unexpected tileset count.", List.of());
|
||||
}
|
||||
for (int index = 0; index < context.tilesetRefs().size(); index += 1) {
|
||||
if (!context.tilesetRefs().get(index).source().equals(map.tilesets().get(index).source())) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: unexpected tileset reference.", List.of());
|
||||
}
|
||||
}
|
||||
|
||||
final List<AssetStudioSceneLayerBinding> updatedBindings = new ArrayList<>();
|
||||
for (int bindingIndex = 0; bindingIndex < metadata.layerBindings().size(); bindingIndex += 1) {
|
||||
final AssetStudioSceneLayerBinding binding = metadata.layerBindings().get(bindingIndex);
|
||||
if (bindingIndex >= map.tileLayers().size()) {
|
||||
updatedBindings.add(new AssetStudioSceneLayerBinding(
|
||||
false,
|
||||
binding.index(),
|
||||
binding.layerName(),
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
gids,
|
||||
List.of())),
|
||||
List.of());
|
||||
final Path outputPath = details.summary().assetRoot().resolve(binding.tilemap());
|
||||
codec.writeMap(outputPath, document);
|
||||
return new TiledAssetGenerationResult(true, "Generated TMX for layer " + binding.index() + ".", List.of(outputPath));
|
||||
} catch (TiledUnsupportedFeatureException | IOException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to generate TMX: " + exception.getMessage(), List.of());
|
||||
binding.tilemap(),
|
||||
binding.tilesetAssetRoot(),
|
||||
binding.parallaxFactorX(),
|
||||
binding.parallaxFactorY(),
|
||||
binding.defaultPaletteId(),
|
||||
binding.tiles()));
|
||||
continue;
|
||||
}
|
||||
final TiledTileLayer importedLayer = map.tileLayers().get(bindingIndex);
|
||||
if (importedLayer.gids().size() != importedLayer.width() * importedLayer.height()) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: layer tile data does not match layer dimensions.", List.of());
|
||||
}
|
||||
final TilesetRef ref = resolveImportedLayerTilesetRef(importedLayer, binding, context);
|
||||
if (ref == null) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: layer uses an unexpected tileset.", List.of());
|
||||
}
|
||||
final List<AssetStudioSceneTile> importedTiles = new ArrayList<>(importedLayer.width() * importedLayer.height());
|
||||
for (long rawGid : importedLayer.gids()) {
|
||||
if ((rawGid & FLIP_DIAGONAL_FLAG) != 0L) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: diagonal flips are not supported.", List.of());
|
||||
}
|
||||
final boolean flipX = (rawGid & FLIP_HORIZONTAL_FLAG) != 0L;
|
||||
final boolean flipY = (rawGid & FLIP_VERTICAL_FLAG) != 0L;
|
||||
final long gid = rawGid & GID_MASK;
|
||||
if (gid == 0L) {
|
||||
importedTiles.add(AssetStudioSceneTile.inactive(binding.defaultPaletteId()));
|
||||
continue;
|
||||
}
|
||||
if (gid < ref.firstGid() || gid >= ref.firstGid() + ref.tileCount()) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: tile uses an unexpected tileset.", List.of());
|
||||
}
|
||||
final int tilesetTileId = (int) (gid - ref.firstGid());
|
||||
final Integer glyphId = ref.glyphIdByTileId().get(tilesetTileId);
|
||||
if (glyphId == null) {
|
||||
return new TiledAssetGenerationResult(false, "TMX import rejected: tileset tile is missing glyph_id mapping.", List.of());
|
||||
}
|
||||
importedTiles.add(new AssetStudioSceneTile(
|
||||
true,
|
||||
glyphId,
|
||||
binding.defaultPaletteId(),
|
||||
flipX,
|
||||
flipY));
|
||||
}
|
||||
updatedBindings.add(new AssetStudioSceneLayerBinding(
|
||||
true,
|
||||
binding.index(),
|
||||
binding.layerName(),
|
||||
importedLayer.width(),
|
||||
importedLayer.height(),
|
||||
binding.tilemap(),
|
||||
ref.assetRoot(),
|
||||
binding.parallaxFactorX(),
|
||||
binding.parallaxFactorY(),
|
||||
binding.defaultPaletteId(),
|
||||
importedTiles));
|
||||
}
|
||||
final int mapWidth = Math.max(map.width(), updatedBindings.stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.mapToInt(AssetStudioSceneLayerBinding::width)
|
||||
.max()
|
||||
.orElse(metadata.mapWidth()));
|
||||
final int mapHeight = Math.max(map.height(), updatedBindings.stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.mapToInt(AssetStudioSceneLayerBinding::height)
|
||||
.max()
|
||||
.orElse(metadata.mapHeight()));
|
||||
metadataService.writeSceneBankMetadata(details.summary().assetRoot(), new AssetStudioSceneBankMetadata(
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
metadata.layerCount(),
|
||||
updatedBindings,
|
||||
metadata.supportFile()));
|
||||
return new TiledAssetGenerationResult(true, "Imported TMX successfully.", List.of(inputPath, metadata.supportFile()));
|
||||
} catch (IOException | TiledUnsupportedFeatureException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to import TMX: " + exception.getMessage(), List.of());
|
||||
}
|
||||
}
|
||||
|
||||
private TilesetRef resolveImportedLayerTilesetRef(
|
||||
TiledTileLayer importedLayer,
|
||||
AssetStudioSceneLayerBinding binding,
|
||||
ExportContext context) {
|
||||
TilesetRef resolved = null;
|
||||
for (long rawGid : importedLayer.gids()) {
|
||||
final long gid = rawGid & GID_MASK;
|
||||
if (gid == 0L) {
|
||||
continue;
|
||||
}
|
||||
final TilesetRef ref = context.tilesetByAssetRoot().values().stream()
|
||||
.filter(candidate -> gid >= candidate.firstGid() && gid < candidate.firstGid() + candidate.tileCount())
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
if (resolved != null && !resolved.assetRoot().equals(ref.assetRoot())) {
|
||||
return null;
|
||||
}
|
||||
resolved = ref;
|
||||
}
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
final String declaredAssetRoot = Objects.requireNonNullElse(binding.tilesetAssetRoot(), "").trim();
|
||||
if (!declaredAssetRoot.isBlank()) {
|
||||
return context.tilesetByAssetRoot().get(declaredAssetRoot);
|
||||
}
|
||||
if (context.tilesetByAssetRoot().size() == 1) {
|
||||
return context.tilesetByAssetRoot().values().iterator().next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ExportContext buildExportContext(
|
||||
ProjectReference projectReference,
|
||||
AssetWorkspaceAssetDetails details,
|
||||
List<AssetStudioSceneLayerBinding> enabledLayers) throws IOException {
|
||||
final LinkedHashMap<String, TilesetRef> refs = new LinkedHashMap<>();
|
||||
final List<TiledTilesetReference> tilesetRefs = new ArrayList<>();
|
||||
int tileSize = -1;
|
||||
int nextFirstGid = 1;
|
||||
for (AssetStudioSceneLayerBinding binding : enabledLayers) {
|
||||
if (refs.containsKey(binding.tilesetAssetRoot())) {
|
||||
continue;
|
||||
}
|
||||
final Path tilesetPath = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.resolve(binding.tilesetAssetRoot())
|
||||
.resolve(GENERATED_TSX_FILE)
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
if (!Files.isRegularFile(tilesetPath)) {
|
||||
throw new IOException("Referenced TSX was not found for layer " + binding.index() + ": " + binding.tilesetAssetRoot());
|
||||
}
|
||||
final TiledTilesetDocument tileset = codec.readTileset(tilesetPath);
|
||||
if (tileSize < 0) {
|
||||
tileSize = tileset.tileWidth();
|
||||
} else if (tileSize != tileset.tileWidth() || tileSize != tileset.tileHeight()) {
|
||||
throw new IOException("All exported Scene Bank tilesets must share the same tile size.");
|
||||
}
|
||||
final String relativeTilesetPath = details.summary().assetRoot()
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.relativize(tilesetPath)
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
refs.put(binding.tilesetAssetRoot(), new TilesetRef(
|
||||
nextFirstGid,
|
||||
binding.tilesetAssetRoot(),
|
||||
relativeTilesetPath,
|
||||
tileset.tileCount(),
|
||||
glyphIdByTileId(tileset)));
|
||||
tilesetRefs.add(new TiledTilesetReference(nextFirstGid, relativeTilesetPath));
|
||||
nextFirstGid += tileset.tileCount();
|
||||
}
|
||||
if (tileSize <= 0) {
|
||||
throw new IOException("Unable to resolve tile size for Scene Bank export.");
|
||||
}
|
||||
return new ExportContext(tileSize, List.copyOf(tilesetRefs), Map.copyOf(refs));
|
||||
}
|
||||
|
||||
private String resolveSceneTilemap(AssetStudioSceneBankMetadata metadata) {
|
||||
final LinkedHashSet<String> tilemaps = new LinkedHashSet<>();
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
final String tilemap = Objects.requireNonNullElse(binding.tilemap(), "").trim();
|
||||
if (!tilemap.isBlank()) {
|
||||
tilemaps.add(tilemap);
|
||||
}
|
||||
}
|
||||
return tilemaps.size() == 1 ? tilemaps.iterator().next() : null;
|
||||
}
|
||||
|
||||
private int parseTileSize(List<PackerCodecConfigurationFieldDTO> metadataFields) {
|
||||
final String value = metadataFields.stream()
|
||||
.filter(field -> "tile_size".equals(field.key()))
|
||||
@ -184,4 +415,56 @@ public final class TiledAssetGenerationService {
|
||||
}
|
||||
return 16;
|
||||
}
|
||||
|
||||
private Path nextSceneTmxBackupPath(Path outputPath) {
|
||||
final String filename = outputPath.getFileName().toString();
|
||||
final int extensionOffset = filename.lastIndexOf('.');
|
||||
final String stem = extensionOffset >= 0 ? filename.substring(0, extensionOffset) : filename;
|
||||
final String extension = extensionOffset >= 0 ? filename.substring(extensionOffset) : "";
|
||||
final Path parent = outputPath.getParent();
|
||||
int sequence = 1;
|
||||
while (true) {
|
||||
final Path candidate = parent.resolve(stem + ".bkp-" + sequence + extension);
|
||||
if (!Files.exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
sequence += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Integer, Integer> glyphIdByTileId(TiledTilesetDocument tileset) throws IOException {
|
||||
final LinkedHashMap<Integer, Integer> glyphs = new LinkedHashMap<>();
|
||||
for (TiledTilesetTile tile : tileset.tiles()) {
|
||||
final Integer glyphId = tile.properties().stream()
|
||||
.filter(property -> "glyph_id".equals(property.name()))
|
||||
.findFirst()
|
||||
.map(TiledProperty::value)
|
||||
.map(String::trim)
|
||||
.filter(value -> !value.isBlank())
|
||||
.map(Integer::parseInt)
|
||||
.orElse(null);
|
||||
if (glyphId == null || glyphId < 0) {
|
||||
throw new IOException("TSX tile " + tile.id() + " is missing a valid glyph_id property.");
|
||||
}
|
||||
glyphs.put(tile.id(), glyphId);
|
||||
}
|
||||
if (glyphs.size() != tileset.tileCount()) {
|
||||
throw new IOException("TSX tile count does not match declared tile_count.");
|
||||
}
|
||||
return Map.copyOf(glyphs);
|
||||
}
|
||||
|
||||
private record ExportContext(
|
||||
int tileSize,
|
||||
List<TiledTilesetReference> tilesetRefs,
|
||||
Map<String, TilesetRef> tilesetByAssetRoot) {
|
||||
}
|
||||
|
||||
private record TilesetRef(
|
||||
int firstGid,
|
||||
String assetRoot,
|
||||
String source,
|
||||
int tileCount,
|
||||
Map<Integer, Integer> glyphIdByTileId) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,8 +147,9 @@ assets.section.actions=Actions
|
||||
assets.actions.empty=No actions available for this asset.
|
||||
assets.action.register=Register
|
||||
assets.action.analyse=Analyse
|
||||
assets.action.generateTsx=Generate TSX
|
||||
assets.action.generateTmx=Generate TMX
|
||||
assets.action.exportTsx=Export TSX
|
||||
assets.action.exportTmx=Export TMX
|
||||
assets.action.importTmx=Import TMX
|
||||
assets.action.validateSceneBank=Validate Scene Bank
|
||||
assets.action.acceptSceneBank=Accept Scene Bank
|
||||
assets.action.validateLayer=Validate Layer
|
||||
@ -199,6 +200,8 @@ assets.label.layerOverview=Layer Overview
|
||||
assets.label.tilemaps=Tilemaps
|
||||
assets.label.tilemap=Tilemap
|
||||
assets.label.tileset=Tileset
|
||||
assets.label.parallax=Parallax
|
||||
assets.label.defaultPalette=Default Palette
|
||||
assets.label.supportFile=Support File
|
||||
assets.label.useLayer=Use this layer
|
||||
assets.type.glyphBank=Glyph Bank
|
||||
|
||||
@ -99,10 +99,10 @@ final class AssetStudioMetadataServiceTest {
|
||||
"map_height": 12,
|
||||
"layer_count": 4,
|
||||
"layers": [
|
||||
{ "index": 1, "enabled": true, "name": "Ground", "width": 20, "height": 12, "tilemap": "ground.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||
{ "index": 2, "enabled": true, "name": "Collision", "width": 10, "height": 8, "tilemap": "collision.tmx", "tileset_asset_root": "tilesets/ground" },
|
||||
{ "index": 3, "enabled": false, "name": "Layer 3", "width": 20, "height": 12, "tilemap": "layer-3.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 4, "enabled": false, "name": "Layer 4", "width": 20, "height": 12, "tilemap": "layer-4.tmx", "tileset_asset_root": "" }
|
||||
{ "index": 1, "enabled": true, "name": "Ground", "width": 20, "height": 12, "tilemap": "ground.tmx", "tileset_asset_root": "tilesets/ground", "parallax_factor_x": 1.0, "parallax_factor_y": 1.0, "default_palette_id": 2 },
|
||||
{ "index": 2, "enabled": true, "name": "Collision", "width": 10, "height": 8, "tilemap": "collision.tmx", "tileset_asset_root": "tilesets/ground", "parallax_factor_x": 0.5, "parallax_factor_y": 0.75, "default_palette_id": 3 },
|
||||
{ "index": 3, "enabled": false, "name": "Layer 3", "width": 20, "height": 12, "tilemap": "layer-3.tmx", "tileset_asset_root": "", "default_palette_id": 0 },
|
||||
{ "index": 4, "enabled": false, "name": "Layer 4", "width": 20, "height": 12, "tilemap": "layer-4.tmx", "tileset_asset_root": "", "default_palette_id": 0 }
|
||||
]
|
||||
}
|
||||
""");
|
||||
@ -115,6 +115,11 @@ final class AssetStudioMetadataServiceTest {
|
||||
assertEquals(4, snapshot.sceneBankMetadata().layerCount());
|
||||
assertEquals(10, snapshot.sceneBankMetadata().layerBindings().get(1).width());
|
||||
assertEquals(8, snapshot.sceneBankMetadata().layerBindings().get(1).height());
|
||||
assertEquals(0.5f, snapshot.sceneBankMetadata().layerBindings().get(1).parallaxFactorX());
|
||||
assertEquals(0.75f, snapshot.sceneBankMetadata().layerBindings().get(1).parallaxFactorY());
|
||||
assertEquals(3, snapshot.sceneBankMetadata().layerBindings().get(1).defaultPaletteId());
|
||||
assertEquals(20 * 12, snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().size());
|
||||
assertFalse(snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().active());
|
||||
assertEquals(
|
||||
java.util.List.of("ground.tmx", "collision.tmx"),
|
||||
snapshot.sceneBankMetadata().layerBindings().stream()
|
||||
@ -134,9 +139,9 @@ final class AssetStudioMetadataServiceTest {
|
||||
"map_height": 16,
|
||||
"layer_count": 3,
|
||||
"layers": [
|
||||
{ "index": 1, "enabled": true, "name": "A", "width": 16, "height": 16, "tilemap": "a.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 2, "enabled": false, "name": "B", "width": 16, "height": 16, "tilemap": "b.tmx", "tileset_asset_root": "" },
|
||||
{ "index": 3, "enabled": false, "name": "C", "width": 16, "height": 16, "tilemap": "c.tmx", "tileset_asset_root": "" }
|
||||
{ "index": 1, "enabled": true, "name": "A", "width": 16, "height": 16, "tilemap": "a.tmx", "tileset_asset_root": "", "default_palette_id": 0 },
|
||||
{ "index": 2, "enabled": false, "name": "B", "width": 16, "height": 16, "tilemap": "b.tmx", "tileset_asset_root": "", "default_palette_id": 0 },
|
||||
{ "index": 3, "enabled": false, "name": "C", "width": 16, "height": 16, "tilemap": "c.tmx", "tileset_asset_root": "", "default_palette_id": 0 }
|
||||
]
|
||||
}
|
||||
""");
|
||||
@ -145,4 +150,76 @@ final class AssetStudioMetadataServiceTest {
|
||||
|
||||
assertNull(snapshot.sceneBankMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesSceneBankSupportMetadataWithCanonicalLayerFields() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene-written");
|
||||
Files.createDirectories(assetRoot);
|
||||
final AssetStudioSceneBankMetadata metadata = new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
12,
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 16, 12, "ground.tmx", "tilesets/ground", 1.0f, 1.0f, 2),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 12, "layer-2.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 12, "layer-3.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 12, "layer-4.tmx", "", 1.0f, 1.0f, 0)),
|
||||
assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE));
|
||||
|
||||
service.writeSceneBankMetadata(assetRoot, metadata);
|
||||
|
||||
final String json = Files.readString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE));
|
||||
assertTrue(json.contains("\"parallax_factor_x\""));
|
||||
assertTrue(json.contains("\"parallax_factor_y\""));
|
||||
assertTrue(json.contains("\"default_palette_id\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesSceneBankSupportMetadataWithCanonicalTilesWhenPresent() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene-tiles");
|
||||
Files.createDirectories(assetRoot);
|
||||
final AssetStudioSceneBankMetadata metadata = new AssetStudioSceneBankMetadata(
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(
|
||||
true,
|
||||
1,
|
||||
"Ground",
|
||||
2,
|
||||
2,
|
||||
"ground.tmx",
|
||||
"tilesets/ground",
|
||||
1.0f,
|
||||
1.0f,
|
||||
2,
|
||||
List.of(
|
||||
new AssetStudioSceneTile(true, 7, 2, false, false),
|
||||
AssetStudioSceneTile.inactive(2),
|
||||
AssetStudioSceneTile.inactive(2),
|
||||
AssetStudioSceneTile.inactive(2))),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 2, 2, "layer-2.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 2, 2, "layer-3.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 2, 2, "layer-4.tmx", "", 1.0f, 1.0f, 0)),
|
||||
assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE));
|
||||
|
||||
service.writeSceneBankMetadata(assetRoot, metadata);
|
||||
|
||||
final String json = Files.readString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE));
|
||||
assertTrue(json.contains("\"tiles\""));
|
||||
assertTrue(json.contains("\"glyph_id\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void providesDefaultSceneBankMetadataWhenSupportFileIsMissing() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene-missing");
|
||||
Files.createDirectories(assetRoot);
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertEquals(16, snapshot.sceneBankMetadata().mapWidth());
|
||||
assertEquals("scene.tmx", snapshot.sceneBankMetadata().layerBindings().getFirst().tilemap());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package p.studio.workspaces.assets.scene;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
@ -12,50 +14,40 @@ import p.studio.projects.ProjectReference;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
import p.studio.workspaces.assets.tiled.TiledAssetGenerationService;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class SceneBankWorkflowServiceTest {
|
||||
private final TiledAssetGenerationService generationService = new TiledAssetGenerationService();
|
||||
private final SceneBankWorkflowService workflowService = new SceneBankWorkflowService();
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final SceneBankWorkflowService workflowService = new SceneBankWorkflowService((projectReference, tilesetAssetRoot) ->
|
||||
"tilesets/overworld".equals(tilesetAssetRoot) ? 1 : -1);
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void validateThenAcceptPromotesSceneBankToReadyUntilFilesChange() throws Exception {
|
||||
void inspectProjectsCanonicalSceneStateIntoReadyRequest() throws Exception {
|
||||
final ProjectReference project = projectReference();
|
||||
final AssetWorkspaceAssetDetails sceneDetails = createValidScene(project);
|
||||
|
||||
final SceneBankWorkflowResult initial = workflowService.inspect(project, sceneDetails);
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.PENDING_VALIDATION, initial.validation().status());
|
||||
assertFalse(initial.validation().canAccept());
|
||||
final SceneBankWorkflowResult result = workflowService.inspect(project, sceneDetails);
|
||||
|
||||
final SceneBankWorkflowResult validated = workflowService.validate(project, sceneDetails);
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.VALIDATED_PENDING_ACCEPTANCE, validated.validation().status());
|
||||
assertTrue(validated.validation().canAccept());
|
||||
|
||||
final SceneBankWorkflowResult accepted = workflowService.accept(project, sceneDetails);
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.READY, accepted.validation().status());
|
||||
assertFalse(accepted.validation().pendingExternalChanges());
|
||||
|
||||
final Path tmxPath = sceneDetails.summary().assetRoot().resolve("ground.tmx");
|
||||
Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("0,0,0", "1,0,0"));
|
||||
|
||||
final SceneBankWorkflowResult changed = workflowService.inspect(project, sceneDetails);
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.PENDING_VALIDATION, changed.validation().status());
|
||||
assertTrue(changed.validation().pendingExternalChanges());
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.READY, result.validation().status());
|
||||
assertFalse(result.validation().pendingExternalChanges());
|
||||
assertFalse(result.validation().canAccept());
|
||||
assertTrue(Files.isRegularFile(sceneDetails.summary().assetRoot().resolve(SceneBankWorkflowService.ACCEPTED_REQUEST_FILE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationFailsWhenReferencedTsxIsMissing() throws Exception {
|
||||
void validationFailsWhenReferencedGlyphAssetCannotResolveTileSize() throws Exception {
|
||||
final ProjectReference project = projectReference();
|
||||
final Path sceneRoot = project.rootPath().resolve("assets/scenes/broken");
|
||||
Files.createDirectories(sceneRoot);
|
||||
@ -86,29 +78,43 @@ final class SceneBankWorkflowServiceTest {
|
||||
8,
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 8, 8, "ground.tmx", "tilesets/missing"),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 8, 8, "layer-2.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 8, 8, "layer-3.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 8, 8, "layer-4.tmx", "")),
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 8, 8, "ground.tmx", "tilesets/missing", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 8, 8, "layer-2.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 8, 8, "layer-3.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 8, 8, "layer-4.tmx", "", 1.0f, 1.0f, 0)),
|
||||
sceneRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
|
||||
Files.writeString(sceneRoot.resolve("ground.tmx"), """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down"
|
||||
width="8" height="8" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../../tilesets/missing/tileset.tsx"/>
|
||||
<layer id="1" name="Ground" width="8" height="8">
|
||||
<data encoding="csv">0</data>
|
||||
</layer>
|
||||
</map>
|
||||
""");
|
||||
|
||||
final SceneBankWorkflowResult result = workflowService.inspect(project, brokenScene);
|
||||
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.VALIDATION_FAILED, result.validation().status());
|
||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Referenced TSX file is missing")));
|
||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("tile_size")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inspectPersistsCanonicalScenePackRequestUsingCanonicalTiles() throws Exception {
|
||||
final ProjectReference project = projectReference();
|
||||
final AssetWorkspaceAssetDetails sceneDetails = createValidScene(project);
|
||||
|
||||
final SceneBankWorkflowResult result = workflowService.inspect(project, sceneDetails);
|
||||
assertEquals(AssetWorkspaceSceneBankStatus.READY, result.validation().status());
|
||||
|
||||
final JsonNode request = mapper.readTree(sceneDetails.summary().assetRoot().resolve(SceneBankWorkflowService.ACCEPTED_REQUEST_FILE).toFile());
|
||||
assertEquals("SCENE/tilemap_v1", request.path("format").asText());
|
||||
assertEquals(1, request.path("version").asInt());
|
||||
final JsonNode firstLayer = request.path("layers").get(0);
|
||||
assertTrue(firstLayer.path("active").asBoolean());
|
||||
assertEquals(1, firstLayer.path("glyph_asset_id").asInt());
|
||||
assertEquals(2, firstLayer.path("tiles").get(0).path("palette_id").asInt());
|
||||
assertEquals(7, firstLayer.path("tiles").get(0).path("glyph_id").asInt());
|
||||
assertEquals(3, firstLayer.path("tiles").get(1).path("palette_id").asInt());
|
||||
assertEquals(9, firstLayer.path("tiles").get(1).path("glyph_id").asInt());
|
||||
assertTrue(firstLayer.path("tiles").get(1).path("flip_x").asBoolean());
|
||||
final JsonNode secondLayer = request.path("layers").get(1);
|
||||
assertFalse(secondLayer.path("active").asBoolean());
|
||||
assertEquals(1, secondLayer.path("glyph_asset_id").asInt());
|
||||
assertEquals(16, secondLayer.path("tile_size").asInt());
|
||||
}
|
||||
|
||||
private ProjectReference projectReference() {
|
||||
@ -119,50 +125,19 @@ final class SceneBankWorkflowServiceTest {
|
||||
final Path tilesetRoot = project.rootPath().resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
assertTrue(generationService.generateTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
final Path sceneRoot = project.rootPath().resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails scene = sceneDetails(sceneRoot);
|
||||
assertTrue(generationService.generateSceneBankTilemaps(project, scene).success());
|
||||
return scene;
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(1),
|
||||
"overworld_tileset",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
AssetFamilyCatalog.GLYPH_BANK,
|
||||
AssetStudioGlyphSpecialization.TILESET,
|
||||
assetRoot,
|
||||
false,
|
||||
false),
|
||||
List.of(),
|
||||
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(new PackerCodecConfigurationFieldDTO(
|
||||
"tile_size",
|
||||
"Tile Size",
|
||||
PackerCodecConfigurationFieldType.ENUM,
|
||||
"16x16",
|
||||
true,
|
||||
List.of("8x8", "16x16", "32x32"))),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(
|
||||
List.of(),
|
||||
List.of(new AssetWorkspaceBankCompositionFile("a.png", "a.png", 1L, 1L, null, Map.of())),
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
return sceneDetails(sceneRoot);
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||
@ -183,7 +158,13 @@ final class SceneBankWorkflowServiceTest {
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
List.of(new PackerCodecConfigurationFieldDTO(
|
||||
"tile_size",
|
||||
"Tile Size",
|
||||
PackerCodecConfigurationFieldType.ENUM,
|
||||
"16x16",
|
||||
true,
|
||||
List.of("8x8", "16x16", "32x32"))),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
@ -193,12 +174,33 @@ final class SceneBankWorkflowServiceTest {
|
||||
12,
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 16, 12, "ground.tmx", "tilesets/overworld"),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 12, "layer-2.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 12, "layer-3.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 12, "layer-4.tmx", "")),
|
||||
new AssetStudioSceneLayerBinding(
|
||||
true,
|
||||
1,
|
||||
"Ground",
|
||||
16,
|
||||
12,
|
||||
"ground.tmx",
|
||||
"tilesets/overworld",
|
||||
1.0f,
|
||||
1.0f,
|
||||
2,
|
||||
canonicalTiles(16, 12)),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 12, "layer-2.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 12, "layer-3.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 12, "layer-4.tmx", "", 1.0f, 1.0f, 0)),
|
||||
assetRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private List<AssetStudioSceneTile> canonicalTiles(int width, int height) {
|
||||
final ArrayList<AssetStudioSceneTile> tiles = new ArrayList<>(width * height);
|
||||
tiles.add(new AssetStudioSceneTile(true, 7, 2, false, false));
|
||||
tiles.add(new AssetStudioSceneTile(true, 9, 3, true, false));
|
||||
while (tiles.size() < width * height) {
|
||||
tiles.add(AssetStudioSceneTile.inactive(2));
|
||||
}
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,12 +10,15 @@ import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ -29,13 +32,13 @@ final class TiledAssetGenerationServiceTest {
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void generatesTsxForTilesetSpecializedGlyphBank() throws Exception {
|
||||
void exportsTsxForTilesetSpecializedGlyphBank() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(assetRoot);
|
||||
Files.writeString(assetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(assetRoot.resolve("b.png"), "fixture");
|
||||
|
||||
final TiledAssetGenerationResult result = service.generateTilesetTsx(tilesetDetails(assetRoot));
|
||||
final TiledAssetGenerationResult result = service.exportTilesetTsx(tilesetDetails(assetRoot));
|
||||
|
||||
assertTrue(result.success());
|
||||
final Path tsxPath = assetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
|
||||
@ -47,28 +50,373 @@ final class TiledAssetGenerationServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesTmxFilesForSceneBankUsingReferencedTsx() throws Exception {
|
||||
void exportsSingleSceneTmxFromCanonicalState() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
assertTrue(service.generateTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
|
||||
final TiledAssetGenerationResult result = service.generateSceneBankTilemaps(projectReference, details);
|
||||
final TiledAssetGenerationResult result = service.exportSceneBankTmx(projectReference, details);
|
||||
|
||||
assertTrue(result.success());
|
||||
final Path tmxPath = sceneRoot.resolve("ground.tmx");
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
assertTrue(Files.isRegularFile(tmxPath));
|
||||
final TiledMapDocument map = codec.readMap(tmxPath);
|
||||
assertEquals("../../tilesets/overworld/tileset.tsx", map.tilesets().getFirst().source());
|
||||
assertEquals(16, map.width());
|
||||
assertEquals(12, map.height());
|
||||
assertEquals(2, map.tileLayers().size());
|
||||
assertEquals("Ground", map.tileLayers().getFirst().name());
|
||||
assertEquals(1L, map.tileLayers().getFirst().gids().getFirst());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportsSceneTmxKeepingExistingFileAsBackup() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-export-backup");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
Files.writeString(tmxPath, "legacy");
|
||||
|
||||
final TiledAssetGenerationResult result = service.exportSceneBankTmx(
|
||||
projectReference,
|
||||
details,
|
||||
TiledAssetGenerationService.SceneTmxExportDisposition.KEEP_BOTH);
|
||||
|
||||
assertTrue(result.success());
|
||||
final Path backupPath = sceneRoot.resolve("scene.bkp-1.tmx");
|
||||
assertTrue(Files.isRegularFile(backupPath));
|
||||
assertEquals("legacy", Files.readString(backupPath));
|
||||
assertNotEquals("legacy", Files.readString(tmxPath));
|
||||
assertTrue(result.writtenFiles().contains(backupPath));
|
||||
assertTrue(result.writtenFiles().contains(tmxPath));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsSceneTmxIntoCanonicalTiles() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-import");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
assertTrue(service.exportSceneBankTmx(projectReference, details).success());
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("1,0,0,0", "2147483650,0,0,0"));
|
||||
|
||||
final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);
|
||||
|
||||
assertTrue(result.success());
|
||||
final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertTrue(snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().active());
|
||||
assertEquals(1, snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().glyphId());
|
||||
assertTrue(snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().flipX());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsSceneTmxUsingGlyphIdPropertyFromTsx() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-import-glyph-property");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
assertTrue(service.exportSceneBankTmx(projectReference, details).success());
|
||||
|
||||
final Path tsxPath = tilesetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
|
||||
Files.writeString(tsxPath, Files.readString(tsxPath)
|
||||
.replace("value=\"0\"", "value=\"7\"")
|
||||
.replace("value=\"1\"", "value=\"3\""));
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
Files.writeString(tmxPath, Files.readString(tmxPath).replaceFirst("1,0,0,0", "2,0,0,0"));
|
||||
|
||||
final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);
|
||||
|
||||
assertTrue(result.success());
|
||||
final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertEquals(3, snapshot.sceneBankMetadata().layerBindings().getFirst().tiles().getFirst().glyphId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsSceneTmxAndPropagatesLayerDimensionsFromTmx() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-import-resize");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
assertTrue(service.exportSceneBankTmx(projectReference, details).success());
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
final String resizedTmx = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="6" height="5" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
|
||||
<layer id="1" name="Ground" width="6" height="5">
|
||||
<data encoding="csv">
|
||||
1,0,0,0,0,0,
|
||||
0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,
|
||||
0,0,0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="Decor" width="3" height="2">
|
||||
<data encoding="csv">
|
||||
2,0,0,
|
||||
0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
""";
|
||||
Files.writeString(tmxPath, resizedTmx);
|
||||
|
||||
final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);
|
||||
|
||||
assertTrue(result.success());
|
||||
final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertEquals(6, snapshot.sceneBankMetadata().mapWidth());
|
||||
assertEquals(5, snapshot.sceneBankMetadata().mapHeight());
|
||||
assertEquals(6, snapshot.sceneBankMetadata().layerBindings().get(0).width());
|
||||
assertEquals(5, snapshot.sceneBankMetadata().layerBindings().get(0).height());
|
||||
assertEquals(3, snapshot.sceneBankMetadata().layerBindings().get(1).width());
|
||||
assertEquals(2, snapshot.sceneBankMetadata().layerBindings().get(1).height());
|
||||
assertEquals(30, snapshot.sceneBankMetadata().layerBindings().get(0).tiles().size());
|
||||
assertEquals(6, snapshot.sceneBankMetadata().layerBindings().get(1).tiles().size());
|
||||
assertEquals("Ground", snapshot.sceneBankMetadata().layerBindings().get(0).layerName());
|
||||
assertEquals("Decor", snapshot.sceneBankMetadata().layerBindings().get(1).layerName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsSceneTmxAndPromotesNewLayerIntoDisabledStudioSlot() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-import-new-layer");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
assertTrue(service.exportSceneBankTmx(projectReference, details).success());
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
final String expandedTmx = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
|
||||
<layer id="1" name="Ground" width="4" height="4">
|
||||
<data encoding="csv">
|
||||
1,0,0,0,
|
||||
0,0,0,0,
|
||||
0,0,0,0,
|
||||
0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="Decor" width="4" height="4">
|
||||
<data encoding="csv">
|
||||
2,0,0,0,
|
||||
0,0,0,0,
|
||||
0,0,0,0,
|
||||
0,0,0,0
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="3" name="Collision" width="2" height="2">
|
||||
<data encoding="csv">
|
||||
1,0,
|
||||
0,0
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
""";
|
||||
Files.writeString(tmxPath, expandedTmx);
|
||||
|
||||
final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);
|
||||
|
||||
assertTrue(result.success());
|
||||
final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
final var snapshot = metadataService.read(sceneRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertEquals(3, snapshot.sceneBankMetadata().enabledLayerCount());
|
||||
assertTrue(snapshot.sceneBankMetadata().layerBindings().get(2).enabled());
|
||||
assertEquals("Layer 3", snapshot.sceneBankMetadata().layerBindings().get(2).layerName());
|
||||
assertEquals("tilesets/overworld", snapshot.sceneBankMetadata().layerBindings().get(2).tilesetAssetRoot());
|
||||
assertEquals(2, snapshot.sceneBankMetadata().layerBindings().get(2).width());
|
||||
assertEquals(2, snapshot.sceneBankMetadata().layerBindings().get(2).height());
|
||||
assertEquals(0, snapshot.sceneBankMetadata().layerBindings().get(2).tiles().getFirst().glyphId());
|
||||
assertFalse(snapshot.sceneBankMetadata().layerBindings().get(3).enabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSceneTmxImportWithoutTileLayers() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-import-no-layers");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
Files.writeString(tmxPath, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="1" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
|
||||
</map>
|
||||
""");
|
||||
|
||||
final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);
|
||||
|
||||
assertFalse(result.success());
|
||||
assertEquals("TMX import rejected: at least one tile layer is required.", result.message());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSceneTmxImportWithMoreThanFourTileLayers() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("project-import-too-many-layers");
|
||||
final Path tilesetRoot = projectRoot.resolve("assets/tilesets/overworld");
|
||||
Files.createDirectories(tilesetRoot);
|
||||
Files.writeString(tilesetRoot.resolve("a.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("b.png"), "fixture");
|
||||
Files.writeString(tilesetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"output": {
|
||||
"metadata": {
|
||||
"tile_size": "16x16"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
assertTrue(service.exportTilesetTsx(tilesetDetails(tilesetRoot)).success());
|
||||
|
||||
final Path sceneRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails details = sceneDetails(sceneRoot);
|
||||
final ProjectReference projectReference = new ProjectReference("main", "1", "pbs", 1, projectRoot);
|
||||
|
||||
final Path tmxPath = sceneRoot.resolve("scene.tmx");
|
||||
Files.writeString(tmxPath, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="4" height="4" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../../tilesets/overworld/tileset.tsx"/>
|
||||
<layer id="1" name="A" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
|
||||
<layer id="2" name="B" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
|
||||
<layer id="3" name="C" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
|
||||
<layer id="4" name="D" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
|
||||
<layer id="5" name="E" width="4" height="4"><data encoding="csv">1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0</data></layer>
|
||||
</map>
|
||||
""");
|
||||
|
||||
final TiledAssetGenerationResult result = service.importSceneBankTmx(projectReference, details);
|
||||
|
||||
assertFalse(result.success());
|
||||
assertEquals("TMX import rejected: more than four tile layers are not supported.", result.message());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails tilesetDetails(Path assetRoot) {
|
||||
@ -134,16 +482,25 @@ final class TiledAssetGenerationServiceTest {
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
12,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
List.of(
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 16, 12, "ground.tmx", "tilesets/overworld"),
|
||||
new AssetStudioSceneLayerBinding(false, 2, "Layer 2", 16, 12, "layer-2.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 16, 12, "layer-3.tmx", ""),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 16, 12, "layer-4.tmx", "")),
|
||||
new AssetStudioSceneLayerBinding(true, 1, "Ground", 4, 4, "scene.tmx", "tilesets/overworld", 1.0f, 1.0f, 0, canonicalTiles(4, 4, 0)),
|
||||
new AssetStudioSceneLayerBinding(true, 2, "Decor", 4, 4, "scene.tmx", "tilesets/overworld", 1.0f, 1.0f, 0, canonicalTiles(4, 4, 1)),
|
||||
new AssetStudioSceneLayerBinding(false, 3, "Layer 3", 4, 4, "scene.tmx", "", 1.0f, 1.0f, 0),
|
||||
new AssetStudioSceneLayerBinding(false, 4, "Layer 4", 4, 4, "scene.tmx", "", 1.0f, 1.0f, 0)),
|
||||
assetRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private List<AssetStudioSceneTile> canonicalTiles(int width, int height, int glyphId) {
|
||||
final ArrayList<AssetStudioSceneTile> tiles = new ArrayList<>(width * height);
|
||||
tiles.add(new AssetStudioSceneTile(true, glyphId, 0, false, false));
|
||||
while (tiles.size() < width * height) {
|
||||
tiles.add(AssetStudioSceneTile.inactive(0));
|
||||
}
|
||||
return List.copyOf(tiles);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"next_asset_id" : 18,
|
||||
"next_asset_id" : 19,
|
||||
"assets" : [ {
|
||||
"asset_id" : 3,
|
||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||
@ -41,15 +41,15 @@
|
||||
"asset_uuid" : "87396aab-337e-479e-b1f4-ec296678389e",
|
||||
"root" : "Zelda3",
|
||||
"included_in_build" : true
|
||||
}, {
|
||||
"asset_id" : 16,
|
||||
"asset_uuid" : "6f05bee1-f974-4b65-a43f-eacd46b8ec96",
|
||||
"root" : "tiled",
|
||||
"included_in_build" : false
|
||||
}, {
|
||||
"asset_id" : 17,
|
||||
"asset_uuid" : "c12f67ad-0471-4ab1-96dd-a816cfc66462",
|
||||
"root" : "tileset",
|
||||
"included_in_build" : false
|
||||
"included_in_build" : true
|
||||
}, {
|
||||
"asset_id" : 18,
|
||||
"asset_uuid" : "5084999c-66c6-4860-85e4-0f76dd0ea193",
|
||||
"root" : "tiled2",
|
||||
"included_in_build" : true
|
||||
} ]
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"asset_uuid" : "6f05bee1-f974-4b65-a43f-eacd46b8ec96",
|
||||
"name" : "Tiled Test",
|
||||
"type" : "scene_bank",
|
||||
"output" : {
|
||||
"pipeline" : { },
|
||||
"codec" : "NONE",
|
||||
"format" : "SCENE/tilemap_v1"
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : false
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="8" tileheight="8" infinite="0" nextlayerid="2" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../tileset/tileset.tsx"/>
|
||||
<layer id="1" name="Layer 1" width="16" height="16">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,
|
||||
1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
@ -1,39 +0,0 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"map_width" : 50,
|
||||
"map_height" : 50,
|
||||
"layer_count" : 4,
|
||||
"layers" : [ {
|
||||
"index" : 1,
|
||||
"enabled" : true,
|
||||
"name" : "Layer 1",
|
||||
"width" : 50,
|
||||
"height" : 50,
|
||||
"tilemap" : "layer-1.tmx",
|
||||
"tileset_asset_root" : "tileset"
|
||||
}, {
|
||||
"index" : 2,
|
||||
"enabled" : false,
|
||||
"name" : "Layer 2",
|
||||
"width" : 16,
|
||||
"height" : 16,
|
||||
"tilemap" : "layer-2.tmx",
|
||||
"tileset_asset_root" : ""
|
||||
}, {
|
||||
"index" : 3,
|
||||
"enabled" : false,
|
||||
"name" : "Layer 3",
|
||||
"width" : 16,
|
||||
"height" : 16,
|
||||
"tilemap" : "layer-3.tmx",
|
||||
"tileset_asset_root" : ""
|
||||
}, {
|
||||
"index" : 4,
|
||||
"enabled" : false,
|
||||
"name" : "Layer 4",
|
||||
"width" : 16,
|
||||
"height" : 16,
|
||||
"tilemap" : "layer-4.tmx",
|
||||
"tileset_asset_root" : ""
|
||||
} ]
|
||||
}
|
||||
14
test-projects/main/assets/tiled2/asset.json
Normal file
14
test-projects/main/assets/tiled2/asset.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"asset_uuid" : "5084999c-66c6-4860-85e4-0f76dd0ea193",
|
||||
"name" : "Tiled2",
|
||||
"type" : "scene_bank",
|
||||
"output" : {
|
||||
"format" : "SCENE/tilemap_v1",
|
||||
"pipeline" : { },
|
||||
"codec" : "NONE"
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : true
|
||||
}
|
||||
}
|
||||
39834
test-projects/main/assets/tiled2/scene-bank.pack-request.json
Normal file
39834
test-projects/main/assets/tiled2/scene-bank.pack-request.json
Normal file
File diff suppressed because it is too large
Load Diff
36773
test-projects/main/assets/tiled2/scene-bank.studio.json
Normal file
36773
test-projects/main/assets/tiled2/scene-bank.studio.json
Normal file
File diff suppressed because it is too large
Load Diff
80
test-projects/main/assets/tiled2/scene-test.tmx
Normal file
80
test-projects/main/assets/tiled2/scene-test.tmx
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="90" height="34" tilewidth="8" tileheight="8" infinite="0" nextlayerid="3" nextobjectid="1">
|
||||
<tileset firstgid="1" source="../tileset/tileset.tsx"/>
|
||||
<layer id="1" name="Layer 1" width="90" height="34">
|
||||
<data encoding="csv">
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,1,1,1,1,1,1,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,1,1,1,1,1,8,8,8,8,8,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="Layer 2" width="90" height="34">
|
||||
<data encoding="csv">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,6,7,8,9,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,14,15,16,17,1,1,1,1,1,10,11,12,13,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
76
test-projects/main/assets/tiled2/scene.tmx
Normal file
76
test-projects/main/assets/tiled2/scene.tmx
Normal file
@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<map height="34" infinite="0" nextlayerid="3" nextobjectid="1" orientation="orthogonal" renderorder="right-down" tiledversion="1.12.1" tileheight="8" tilewidth="8" version="1.10" width="90">
|
||||
<tileset firstgid="1" source="../tileset/tileset.tsx"/>
|
||||
<layer height="34" id="1" name="Layer 1" width="90">
|
||||
<data encoding="csv">8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,1,1,1,1,1,1,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,1,1,1,1,1,8,8,8,8,8,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,1,1,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,1,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,1,1,1,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,
|
||||
8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8</data>
|
||||
</layer>
|
||||
<layer height="34" id="2" name="Layer 2" width="90">
|
||||
<data encoding="csv">0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,6,7,8,9,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,14,15,16,17,1,1,1,1,1,10,11,12,13,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,1,1,1,2,3,4,5,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,1,1,1,6,7,8,9,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,1,1,1,10,11,12,13,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,14,15,16,17,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1</data>
|
||||
</layer>
|
||||
</map>
|
||||
@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : false
|
||||
"enabled" : true
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"artifact_id" : "art-67db3cae2605",
|
||||
|
||||
@ -10,7 +10,10 @@ declare global loading_handle: int = -1;
|
||||
declare global loading_status: int = -1;
|
||||
declare global frame_counter: int = 0;
|
||||
declare global tile_id: int = 0;
|
||||
declare global camera_x: int = 0;
|
||||
declare global camera_mult: int = 1;
|
||||
declare const MAX_FRAMES: int = 4;
|
||||
declare const CAMERA_SPEED: int = 1;
|
||||
|
||||
[Init]
|
||||
fn init() -> void
|
||||
@ -19,13 +22,14 @@ fn init() -> void
|
||||
loading_status = -1;
|
||||
frame_counter = 0;
|
||||
tile_id = 0;
|
||||
camera_x = 0;
|
||||
camera_mult = 1;
|
||||
Composer.bind_scene(0);
|
||||
}
|
||||
|
||||
[Frame]
|
||||
fn frame() -> void
|
||||
{
|
||||
Gfx.clear(new Color(6577));
|
||||
|
||||
if (loading_handle == -1) {
|
||||
let t : (status: int, loading_handle: int) = Assets.load(assets.ui.atlas2, 3);
|
||||
if (t.status != 0) {
|
||||
@ -75,8 +79,15 @@ fn frame() -> void
|
||||
}
|
||||
}
|
||||
|
||||
Composer.emit_sprite(tile_id, 0, touch.x() - 16, touch.y() + 8, 0, 0, true, false, 0);
|
||||
Composer.emit_sprite(tile_id, 0, touch.x() + 16, touch.y() + 8, 0, 1, false, false, 0);
|
||||
camera_x = camera_x + (camera_mult * CAMERA_SPEED);
|
||||
if (camera_x >= 240 || camera_x <= 0)
|
||||
{
|
||||
camera_mult = -1 * camera_mult;
|
||||
}
|
||||
Log.info("camera_x:" + camera_x);
|
||||
|
||||
Composer.set_camera(camera_x, 0);
|
||||
Composer.emit_sprite(tile_id, 0, touch.x() - 16, touch.y() + 8, 2, 0, true, false, 0);
|
||||
|
||||
let a : int = 10;
|
||||
let b : int = 15;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user